从几十 KB 到 2300 万 Token:我把 OpsLM-Toy 真正训练成了一个领域 Base Model
OpsLM-Toy 第二阶段训练记录:数据工程、Tokenizer V2、Packed Dataset、33M Transformer、10000 Step 预训练,以及一次非常重要的失败——验证集 Loss 很漂亮,但模型仍然不会真正回答 SRE 问题。
0. 前言
第一阶段,我已经从零实现并训练了一个 Decoder-only Transformer。
当时的目标非常单纯:
- 自己训练 Tokenizer
- 自己实现 Embedding
- 自己实现 Self-Attention
- 自己实现 Multi-Head Attention
- 自己实现 MLP
- 自己堆 Transformer Block
- 自己写训练循环
- 最后让模型能生成文字
第一阶段最终确实跑通了。
但那个模型有一个非常明显的问题:数据太少了。
训练集只有非常少量的 Linux、Kubernetes、网络、AI Infra 文本,最终训练 Loss 可以低到:
0.0009
看起来非常漂亮,但一旦给模型一个训练集中没有出现过的问题,它马上就会暴露本质:
它并没有真正学会语言,也没有真正学会 SRE,只是在背训练数据。
因此第二阶段,我没有急着继续改 Transformer 架构,而是决定先回答一个更重要的问题:
如果保持模型架构基本不变,只把数据工程真正做起来,模型会发生什么变化?
这就是 OpsLM 第二阶段。
1. 第二阶段的目标
第二阶段我给自己定了几个明确目标:
- 数据规模从几十 KB 提升到几十 MB 甚至上百 MB 原始文本。
- 数据不再由我手写,而是真实来自开源技术文档和仓库。
- 重新训练一个更合理的 Tokenizer。
- 重新设计 Dataset,使用连续 Token Stream 进行 Packed Language Modeling。
- 增加验证集,不再只看 Train Loss。
- 增加 Checkpoint、Resume、Learning Rate Scheduler、BF16 等更接近真实预训练的机制。
- 最终得到一个真正意义上的 SRE / AI Infra 领域 Base Model。
这一阶段,我仍然没有引入 Instruction Tuning。
也就是说,目标不是让模型直接变成“ChatGPT”,而是先让它成为一个能够建模技术文本分布的基础模型。
2. 第二阶段数据从哪里来
在开始第二阶段之前,我先在仓库根目录准备目录:
cd ~/opslm-toy
mkdir -p \
data/phase2/raw \
data/phase2/github \
data/phase2/repos \
data/phase2/processed \
data/phase2/meta \
logs
网页文档抓取使用:
python scripts/crawl_docs.py
GitHub Repository 文档下载与提取使用:
python scripts/download_repos.py
抓取完成后,我会先简单确认数据文件是否生成:
ls -lh data/phase2/raw/
ls -lh data/phase2/github/
例如本阶段主要得到:
data/phase2/raw/documents.jsonl
data/phase2/github/repositories.jsonl
这次没有继续手写几十条训练文本,而是开始抓取真实技术资料。
主要包括:
- Kubernetes 中文文档
- Kubernetes 英文文档
- Kubernetes Website GitHub Repository
- Prometheus Docs
- vLLM
- PyTorch Tutorials
- Docker Docs
- systemd
最终清洗后得到:
{
"documents": 5454,
"train": 5182,
"val": 272,
"sources": {
"kubernetes_zh": 874,
"kubernetes_en": 865,
"kubernetes_website": 1930,
"prometheus": 122,
"vllm": 210,
"pytorch_tutorials": 83,
"docker_docs": 885,
"systemd": 485
}
}
Tokenizer 训练语料预处理后大约:
97 MB
和第一阶段相比,这已经不是一个数量级了。
3. 为什么我同时抓网页和 GitHub Repository
一开始我主要抓官网文档。
但很快我发现,只依赖网页爬虫有几个问题:
- 页面结构不稳定
- 有些页面正文抓取不完整
- robots.txt 会限制抓取
- HTML 中存在大量导航、模板和页面噪音
- 很多真正有价值的技术说明其实就在 GitHub Repository 中
所以后面又增加了 Repo 数据。
例如 Kubernetes Website、Docker Docs、Prometheus Docs 这些项目,本身 Markdown 就是官方文档源。
这使数据质量提升了很多。
但是也带来了第二个问题:
网页版本和 Repository 版本可能是同一份文档。
因此第二阶段做了 Exact Hash 去重。
不过这里要特别记录一个后来才意识到的问题:
Exact Hash 只能删除完全相同的文档,无法处理 Near Duplicate。
例如:
官网 HTML 转换后的 Kubernetes 文档
和:
GitHub 中对应的 Markdown 文档
即使内容高度相似,只要格式稍微不同,SHA256 就完全不同。
所以第二阶段的数据集仍然可能存在近似重复。
这个问题后来直接影响了 Validation Loss 的可信度。
后面会讲。
4. 原始 Markdown 并不能直接拿来训练
抓取完成之后,先合并不同数据源:
cd ~/opslm-toy
python scripts/merge_phase2_data.py
合并后的中间文件:
data/phase2/processed/all_documents.jsonl
然后执行正式清洗、去重、Train/Val 切分以及 Tokenizer Corpus 构建:
python scripts/clean_phase2_data.py
清洗完成后检查结果:
ls -lh data/phase2/processed/
cat data/phase2/processed/manifest.json
本阶段最终使用的几个关键文件是:
data/phase2/processed/train.jsonl
data/phase2/processed/val.jsonl
data/phase2/processed/tokenizer_corpus.txt
data/phase2/processed/manifest.json
GitHub 文档虽然比网页干净,但也不是直接就能训练。
例如 Kubernetes 文档中大量存在 Hugo Shortcode:
{{< feature-state ... >}}
{{% heading ... %}}
还有:
- YAML Front Matter
- HTML Comment
- Markdown Link
- 页面元数据
- 模板语法
这些内容如果全部保留,模型会浪费大量容量去学习网站构建语法,而不是技术内容。
所以清洗阶段主要做了:
- 去 Front Matter
- 去 Hugo Shortcode
- 去 HTML Comment
- 适度清洗 Markdown
- 保留代码块
- 保留命令
- 保留技术符号
- 去除明显无意义页面内容
这里我没有选择“把所有 Markdown 全部变成纯文本”。
原因很简单:
对于技术模型来说,下面这些东西本身就是知识的一部分:
kubectl get pods
apiVersion: v1
kind: Pod
CrashLoopBackOff
CUDA_VISIBLE_DEVICES
过度清洗反而会损失技术信息。
5. Tokenizer V2
清洗数据完成以后,直接训练第二版 Tokenizer:
cd ~/opslm-toy
python tokenizer/train_tokenizer_v2.py
训练完成后确认产物:
ls -lh tokenizer/artifacts_v2/tokenizer.json
我这一阶段得到的输出类似:
saved: /root/opslm-toy/tokenizer/artifacts_v2/tokenizer.json
vocab: 16000
第一阶段的 Tokenizer 只是为了跑通流程。
第二阶段我重新使用 97MB 左右的真实技术语料训练 ByteLevel BPE Tokenizer。
配置:
Tokenizer: ByteLevel BPE
Vocab Size: 16000
Min Frequency: 2
特殊 Token:
<pad> = 0
<unk> = 1
<bos> = 2
<eos> = 3
最终:
vocab = 16000
之后我还统计了训练语料实际使用到的 Token:
Unique Tokens: 15571 / 16000
大约 97.3% 的词表都被使用到了。
这个结果至少说明:
16K 词表对于当前数据规模并没有明显浪费。
6. 第二阶段 Dataset:不再“一篇文档一个样本”
Dataset V2 写完以后,我没有直接开始长时间训练,而是先运行专门的测试脚本:
cd ~/opslm-toy
python scripts/test_dataset_v2.py
我重点检查三件事:
1. input_ids / target_ids 的 Shape 是否都是 512
2. <eos> Token 是否正确写入文档边界
3. input[1:] 是否严格等于 target[:-1]
测试输出中最关键的一行是:
Shift relationship correct: True
这是第二阶段我认为非常重要的改动之一。
第一阶段的数据集更像:
一条文本
→ tokenize
→ 截断
→ 作为一个训练样本
这种方式有明显缺点。
如果某个文档只有 80 Token,而 seq_len 是 512,那么大量空间都会浪费。
第二阶段改成了 Packed Language Model Dataset。
整体逻辑是:
Document A
+ <eos>
+ Document B
+ <eos>
+ Document C
+ <eos>
+ ...
形成一条连续 Token Stream。
然后按照固定长度切分:
512 Token
512 Token
512 Token
512 Token
...
训练目标仍然是标准 Next Token Prediction:
input : token[0:512]
target: token[1:513]
验证时我检查过:
input[1:] == target[:-1]
结果为:
True
说明 shift 正确。
7. 第二阶段最终数据规模
训练集 Token:
23,000,430
验证集 Token:
637,926
在:
seq_len = 512
情况下,训练集大约可以得到:
44,922 samples
验证集:
1,245 samples
和第一阶段几十 KB 的数据相比,这时候才终于有一点“预训练”的感觉。
8. 为什么增加 Token Cache
第一次执行 Dataset 测试时会自动构建 Token Cache:
python scripts/test_dataset_v2.py
可以观察到类似:
Building token cache...
Tokenizing 0
Tokenizing 500
Tokenizing 1000
...
Saved token cache
之后再次运行:
python scripts/test_dataset_v2.py
会直接看到:
Loading token cache...
检查 Cache 大小:
ls -lh data/phase2/processed/cache/
如果重新训练了 Tokenizer,则应该删除旧缓存重新生成:
rm -f data/phase2/processed/cache/train_tokens.pt
rm -f data/phase2/processed/cache/val_tokens.pt
python scripts/test_dataset_v2.py
第一次加载 23M Token 时,如果每次启动训练都重新 tokenize 一遍,速度非常慢。
所以 Dataset V2 增加了 Token Cache。
例如:
data/phase2/processed/cache/train_tokens.pt
data/phase2/processed/cache/val_tokens.pt
第一次:
Building token cache...
以后:
Loading token cache...
这样训练脚本启动速度明显改善。
需要注意:
Token Cache 和 Tokenizer 是绑定的。
如果以后重新训练 Tokenizer,旧的 Cache 必须删除重新生成。
否则 Token ID 会错位。
9. 模型架构没有大改
第二阶段我故意没有大幅修改模型。
原因是我想控制变量。
模型仍然是 Decoder-only Transformer:
vocab_size = 16000
max_seq_len = 512
d_model = 512
n_heads = 8
n_layers = 8
d_ff = 2048
dropout = 0.1
总参数量:
33.66M
也就是大约 3360 万参数。
为什么不直接升级到 100M?
因为这一阶段我想知道:
仅仅改善数据,模型能提升多少?
如果数据、模型规模、架构同时改,就很难知道提升到底来自哪里。
10. V2 训练脚本做了什么升级
第二阶段训练脚本增加了很多第一阶段没有的东西。
包括:
- BF16
- AdamW
- Gradient Accumulation
- Gradient Clipping
- Warmup
- Cosine Learning Rate Decay
- Validation
- Perplexity
- Best Checkpoint
- Latest Checkpoint
- Final Checkpoint
- Resume Training
- Token Throughput
优化器:
AdamW
lr = 3e-4
betas = (0.9, 0.95)
weight_decay = 0.1
训练使用:
BF16
4090 对 BF16 支持很好,所以没有使用 FP16 GradScaler。
11. 第一次正式训练
正式训练前先确认 GPU:
nvidia-smi
本阶段最终使用 RTX 4090 24GB。
如果从头复现最终实验,我现在更推荐直接把总训练步数设为 10000,而不是先跑 5000 再临时改变 Scheduler 总步数:
cd ~/opslm-toy
mkdir -p logs
nohup python -u scripts/train_v2.py \
--max-steps 10000 \
--batch-size 32 \
--gradient-accumulation 2 \
> logs/train_v2.log 2>&1 &
查看进程:
ps -ef | grep '[t]rain_v2.py'
实时观察训练日志:
tail -f logs/train_v2.log
实时观察 GPU:
watch -n 1 nvidia-smi
如果显存不足,可以优先降低 Micro Batch:
--batch-size 32 -> 24 -> 16
Gradient Accumulation 不会像 Micro Batch 一样直接增加单次 Forward 的显存占用。
使用 RTX 4090 24GB。
最开始 Batch Size 比较保守。
后来发现显存没有吃满,于是最终使用:
batch_size = 32
gradient_accumulation = 2
seq_len = 512
也就是说每次 Optimizer Update 实际看到的 Token 数是:
32 × 512 × 2
= 32768 tokens/update
训练吞吐长期稳定在:
≈133K tokens/s
对于这个 33M 的教学模型来说已经完全够用。
12. 训练刚开始时
一开始:
step=20 loss=4.5457
step=40 loss=4.3737
step=60 loss=4.1525
step=80 loss=3.9038
step=100 loss=3.5759
step=120 loss=3.4535
Loss 开始稳定下降。
和第一阶段最大的区别是:
这一次 Loss 不会瞬间掉到 0.00x。
因为现在模型面对的不再是几页可以直接背下来的文字,而是数千万 Token 的真实技术文本。
13. 训练到 2200 Step
当训练到 2200 Step:
step=2200 loss=1.3068
第一次比较有意义的验证结果出现:
val_loss = 2.4063
ppl = 11.09
这时已经可以明确判断:
模型不是单纯把训练集 Loss 降下来。
它在验证集上同样已经具备一定 Next Token Prediction 能力。
14. 3200 Step:第一次生成“像技术文档”的内容
训练到 3200 Step 时:
train_loss = 0.7869
val_loss = 2.1451
Prompt:
kubectl get pods
模型生成:
kubectl get pods --field-selector=template='{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}')
##### Start a pod with multiple selector
Run the command to create a Pod based on the following command:
```shell
kubectl apply -f ./pod.yaml
```
In the output, the output is similar to this:
```text
deployment.apps/web created
```
虽然内容并不完全正确,但这是第一次明显看到它已经学到了领域关联:
kubectl
→ pod
→ selector
→ yaml
→ deployment
这和第一阶段那种“看到 Kubernetes 后开始背固定句子”已经有本质区别。
15. 5000 Step 的结果
训练过程中可以随时从日志中筛选验证结果:
grep '\[eval\]' logs/train_v2.log
只看最近几次验证:
grep '\[eval\]' logs/train_v2.log | tail -n 10
如果只想盯住训练尾部:
tail -n 50 logs/train_v2.log
训练到 5000 Step:
step=5000
train_loss = 0.8128
val_loss = 2.0057
ppl = 7.43
此时 Validation Loss 仍然持续下降。
因此我决定继续训练,而不是在 5000 Step 停止。
16. 最终训练到 10000 Step
我的实际实验过程是先跑到 5000 Step,确认 Validation Loss 仍然下降后,再从 Checkpoint 继续训练。续训命令形式如下:
nohup python -u scripts/train_v2.py \
--max-steps 10000 \
--batch-size 32 \
--gradient-accumulation 2 \
--resume checkpoints_v2_latest.pt \
> logs/train_v2_continue.log 2>&1 &
继续训练时查看日志:
tail -f logs/train_v2_continue.log
不过,如果从零重新复现实验,我更建议一开始就使用:
--max-steps 10000
这样整个 Cosine Scheduler 的总步数从训练开始就是固定的,实验会更干净。
最终结果:
step = 10000
train_loss = 0.650061
val_loss = 1.725160
对应 Perplexity 大约:
ppl ≈ 5.61
Best Checkpoint 恰好出现在:
step = 10000
因此从 Validation Loss 来看:
5000 → 10000 这一阶段仍然带来了明显收益。
17. 实际上模型把训练数据看了多少遍
这里有一个很容易算错的地方。
当前配置:
batch_size = 32
seq_len = 512
gradient_accumulation = 2
所以每个 Optimizer Step:
32768 tokens
10000 Step:
32768 × 10000
= 327,680,000 token presentations
训练集总 Token:
23,000,430
因此大约相当于:
327.68M / 23.00M
≈ 14.2 次
也就是说,这 23M Token 已经被模型反复看了十四遍左右。
这是后面决定“不再继续盲目刷到 15000、20000 Step”的重要原因。
18. 一个 Checkpoint 小 Bug
训练完成后,我用下面这段命令一次性检查三个 Checkpoint:
python - <<'PY'
import torch
for path in [
"checkpoints_v2_best.pt",
"checkpoints_v2_latest.pt",
"checkpoints_v2_final.pt",
]:
ckpt = torch.load(path, map_location="cpu")
print("=" * 70)
print("checkpoint :", path)
print("step :", ckpt.get("step"))
print("train_loss :", ckpt.get("train_loss"))
print("val_loss :", ckpt.get("val_loss"))
PY
输出中可以看到:
best.pt -> val_loss = 1.725160...
latest.pt -> val_loss = 0
final.pt -> val_loss = 0
训练结束后查看:
checkpoints_v2_best.pt
checkpoints_v2_latest.pt
checkpoints_v2_final.pt
发现:
best.pt
val_loss = 1.725160
而:
latest.pt
val_loss = 0
final.pt
val_loss = 0
这里不是模型真的出现了 0 Loss。
而是训练脚本保存 latest / final 时,当时直接把 val_loss 参数写成了 0。
因此真正用于模型选择的应该是:
checkpoints_v2_best.pt
这个问题后续版本会修复。
这也是自己写训练框架非常有价值的一点:
你会真的碰到训练框架设计中的各种细节,而不是只看到 Trainer 帮你打印一个数字。
19. 到这里是不是已经成功了?
如果只看指标:
33.66M Params
23M Train Tokens
Train Loss = 0.65
Val Loss = 1.725
PPL ≈ 5.61
答案似乎是:
成功了。
但接下来这个实验,让我重新理解了 Validation Loss。
20. 真正的 SRE 问题测试
第二阶段我单独写了 scripts/generate_v2.py,使用 V2 Tokenizer 和 V2 Checkpoint 做采样。
先测试一个训练领域里非常常见的短 Prompt:
python scripts/generate_v2.py \
--checkpoint checkpoints_v2_best.pt \
--prompt "kubectl get pods" \
--max-new-tokens 150 \
--temperature 0.7 \
--top-k 50 \
--top-p 0.9
然后再测试真正的陌生 SRE 场景:
python scripts/generate_v2.py \
--checkpoint checkpoints_v2_best.pt \
--prompt "服务器 CPU 使用率不高,但是 load average 非常高,可能是什么原因?" \
--max-new-tokens 300 \
--temperature 0.6 \
--top-k 40 \
--top-p 0.9
还可以继续测试 Kubernetes / AI Infra:
python scripts/generate_v2.py \
--checkpoint checkpoints_v2_best.pt \
--prompt "一个 Kubernetes Pod 一直处于 Pending 状态,但是集群中还有空闲节点,应该如何排查?" \
--max-new-tokens 300 \
--temperature 0.6 \
--top-k 40 \
--top-p 0.9
python scripts/generate_v2.py \
--checkpoint checkpoints_v2_best.pt \
--prompt "vLLM 服务 GPU 显存占用很高,但是 GPU 利用率很低,应该从哪些方面排查?" \
--max-new-tokens 300 \
--temperature 0.6 \
--top-k 40 \
--top-p 0.9
Prompt:
服务器 CPU 使用率不高,但是 load average 非常高,可能是什么原因?
如果是一个真正能够做 SRE 分析的模型,我们期待它回答类似:
CPU 利用率不高但 Load Average 很高时,
需要重点检查不可中断睡眠进程、磁盘 IO、NFS、锁等待等。
可以先执行:
ps
vmstat
iostat
...
但 OpsLM-V2 实际生成的是:
CPU 管理策略
Kubernetes 允许在 Pod 中容器和容器共享资源。
CPU 管理策略(CPU)允许你指定 CPU 和内存使用情况。
CPU 管理策略基于 CPU 管理策略,并通过 CPU 管理策略,
为工作负载管理资源管理的 CPU 资源。
Kubernetes 支持两种类型的 Kubernetes 资源:CPU 和内存管理器。
CPU 管理策略管理了对 CPU 管理策略管理策略的管理方式。
...
它开始严重重复:
CPU 管理策略
CPU 管理策略
CPU 管理策略
管理策略管理策略
这一刻非常重要。
因为模型的:
val_loss = 1.725
明明已经非常漂亮。
但它依然不会回答这个问题。
21. 为什么会这样?
因为我训练的是:
Base Model
而不是:
Instruction Model
Base Model 的训练任务始终只有一个:
根据前面的 Token,预测下一个 Token。
训练数据主要是技术文档:
Kubernetes Documentation
Docker Documentation
systemd Documentation
vLLM Documentation
...
因此模型看到:
CPU
最容易联想到的并不是:
用户遇到了 Load Average 故障,我应该帮助他排障。
而是:
训练数据里 CPU 后面经常出现什么?
于是它开始:
CPU
→ CPU Manager
→ CPU Management Policy
→ Kubernetes Resource
从语言模型训练目标来看,它甚至没有“做错”。
它只是在执行自己接受过的训练任务。
22. Base Model 和 Instruct Model 到底差在哪
这一阶段让我真正理解了这两个概念。
Base Model 更像:
给我一段文字
→ 我接着写
Instruction Model 更像:
用户给我一个任务
→ 我理解任务
→ 我组织知识
→ 我给出答案
例如 Base Model 看到:
服务器 CPU 使用率不高,但是 load average 很高...
它可能只是把这句话当成技术文章开头。
而 Instruction Model 则应该理解:
这是用户在提问。
这种能力并不是普通 Next Token Pretraining 自然保证的。
它通常还需要:
SFT / Instruction Tuning
23. 为什么 Validation Loss 很低,实际效果仍然一般
这又涉及另一个非常重要的问题:
Validation Set 并不等于真实 Benchmark
我的 Train / Val 都来自同一批技术数据源。
例如:
Kubernetes Website
Kubernetes GitHub Repository
它们中可能存在大量近似内容。
目前第二阶段只做了:
Exact Hash Dedup
没有做:
Near Duplicate Dedup
因此完全可能出现:
Train:
某一篇 Kubernetes 文档的 Markdown 版本
Val:
同一内容的网页清洗版本
虽然两个文本 SHA256 不相同,但语言内容非常接近。
这样就会导致:
Validation Loss 很漂亮
但它无法完整代表:
真实陌生问题上的泛化能力
所以这一阶段我得到了一个非常重要的结论:
低 Validation Loss 不等于模型真的会解决问题。
24. Phase1 和 Phase2 的区别
Phase1
数据:
几十 KB
最终 Train Loss:
≈ 0.001
表现:
大量记忆
大量复读
碰到新问题不会泛化
本质:
背书模型
Phase2
数据:
23M Token
97MB Tokenizer Corpus
模型:
33.66M Params
最终:
Train Loss = 0.65
Val Loss = 1.725
表现:
开始学习领域词汇关系
可以生成像 Kubernetes 文档的内容
能够组合 kubectl / Pod / Deployment / YAML 等概念
但还不会稳定回答真实 SRE 问题
本质:
领域 Base Model
这已经是非常大的变化。
25. 为什么第二阶段不继续训练到 20000 Step
原因很简单。
当前 10000 Step 已经相当于让模型把数据看了约:
14.2 遍
继续让同一个 33M 模型反复看同样的 23M Token,收益开始越来越有限。
更重要的是,当前主要问题已经不是:
模型还没把文档学熟
而是:
模型没有接受“如何回答问题”的训练
因此下一步如果继续刷:
15000
20000
30000 Step
很可能只会让模型更擅长模仿技术文档,而不是突然变成 SRE 助手。
这时继续 Pretraining 的边际收益已经小于进入 Instruction Tuning。
26. 第二阶段最终模型命名
为了避免后续 Phase3 覆盖这一阶段的最佳权重,我先把它归档:
mkdir -p checkpoints_v2/archive
cp checkpoints_v2_best.pt \
checkpoints_v2/archive/opslm_v2_base_33m_step10000.pt
确认文件:
ls -lh checkpoints_v2/archive/
我决定把这一版正式定义为:
OpsLM-V2-Base-33M
核心信息:
Architecture : Decoder-only Transformer
Parameters : 33.66M
Vocab : 16K ByteLevel BPE
Context : 512
Train Tokens : 23.0M
Train Steps : 10000
Train Loss : 0.6501
Val Loss : 1.7252
它不是聊天模型。
也不是完整的 SRE Agent。
它是:
一个通过真实 SRE / Cloud Native / AI Infra 文档预训练得到的小型领域 Base Model。
27. 第二阶段我真正学到的东西
如果只看代码,第二阶段好像只是:
更多数据
更大的 Batch
更多 Step
但实际上真正让我理解的是下面这些问题。
27.1 数据工程比我想象中重要得多
Transformer 代码写通只是开始。
真正影响结果的还有:
数据来源
清洗
去重
Tokenizer
Packing
Train/Val Split
数据污染
27.2 Train Loss 很低不代表模型很好
第一阶段:
Loss = 0.001
结果几乎只会背书。
27.3 Validation Loss 很低也不能代表真实能力
第二阶段:
Val Loss = 1.725
但问一个真正的 SRE 问题,它仍然可能答非所问。
27.4 Base Model 并不是聊天模型
这个以前看文章也知道。
但是自己真正训练一遍以后,感受完全不一样。
Pretraining 学的是:
语言分布 + 知识模式
Instruction Tuning 才开始教模型:
如何按照人的意图组织答案
27.5 Perplexity 只是一个统计指标
PPL ≈ 5.61
很漂亮。
但 PPL 并不能告诉我:
这个模型会不会排查 Load Average?
真正的能力仍然需要 Benchmark 和人工测试。
28. 下一阶段:OpsLM-V3-Instruct
第三阶段我准备暂时保持模型规模不变。
仍然使用:
33M Parameters
从:
OpsLM-V2-Base-33M
继续训练:
SFT / Instruction Tuning
目标模型:
OpsLM-V3-Instruct
计划准备约:
20K ~ 50K
条高质量领域 Instruction 数据。
数据类型不会只做简单 QA,而会覆盖:
- 用户问题 → 回答
- 故障现象 → 排查步骤
- 日志 → 故障分析
- Metrics → 原因判断
- 命令输出 → 解释
- 架构场景 → 方案设计
- 错误操作 → 纠正
- On-call 场景 → 决策
- Kubernetes 故障
- Linux 故障
- Network
- Docker / Containerd
- Prometheus
- MySQL / Redis
- Nginx
- GPU / CUDA
- vLLM
- AI Infra
希望下一阶段能让模型从:
技术文档续写器
变成:
一个真正开始会回答问题的小型 SRE Model
29. 完整复现命令速查
如果只想从头快速复现第二阶段,核心命令可以按这个顺序执行:
cd ~/opslm-toy
# 1. 抓取网页文档
python scripts/crawl_docs.py
# 2. 下载并提取 GitHub Repository 文档
python scripts/download_repos.py
# 3. 合并所有来源
python scripts/merge_phase2_data.py
# 4. 清洗、去重、切分 Train/Val、生成 Tokenizer Corpus
python scripts/clean_phase2_data.py
# 5. 训练 Tokenizer V2
python tokenizer/train_tokenizer_v2.py
# 6. 构建 / 验证 Packed Dataset 和 Token Cache
python scripts/test_dataset_v2.py
# 7. 正式预训练
mkdir -p logs
nohup python -u scripts/train_v2.py \
--max-steps 10000 \
--batch-size 32 \
--gradient-accumulation 2 \
> logs/train_v2.log 2>&1 &
# 8. 观察训练
tail -f logs/train_v2.log
# 9. 查看 Validation 曲线
grep '\[eval\]' logs/train_v2.log
# 10. 使用最佳 Checkpoint 生成
python scripts/generate_v2.py \
--checkpoint checkpoints_v2_best.pt \
--prompt "kubectl get pods" \
--max-new-tokens 150 \
--temperature 0.7 \
--top-k 50 \
--top-p 0.9
# 11. 归档 Phase2 Base Model
mkdir -p checkpoints_v2/archive
cp checkpoints_v2_best.pt \
checkpoints_v2/archive/opslm_v2_base_33m_step10000.pt
30. Git 提交
为了避免把几十 MB 数据集、Token Cache、训练日志和模型权重直接提交到 Git,我会先检查 .gitignore。
建议至少包含:
data/phase2/raw/
data/phase2/repos/
data/phase2/github/
data/phase2/processed/cache/
checkpoints_v2*.pt
checkpoints_v2/
*.pt
logs/
查看当前改动:
git status
提交第二阶段代码和博客:
git add \
scripts/train_v2.py \
scripts/generate_v2.py \
scripts/test_dataset_v2.py \
scripts/crawl_docs.py \
scripts/download_repos.py \
scripts/merge_phase2_data.py \
scripts/clean_phase2_data.py \
tokenizer/train_tokenizer_v2.py \
tokenizer/artifacts_v2/tokenizer.json \
src/dataset.py \
docs/blog/02-opslm-phase2-from-toy-to-base-model.md
git commit -m "feat: complete OpsLM phase2 domain pretraining"
git push origin main
提交之前再确认一次没有把 Checkpoint 和大规模训练数据带进去:
git status
31. 最后的感受
做到第二阶段以后,我越来越觉得:
“手写 Transformer”其实只是大模型学习路线的入口。
Attention、QKV、Embedding 当然重要。
但真正自己训练以后,很快就会进入另外一个世界:
数据质量
Tokenizer
Packing
Optimizer
Learning Rate
Evaluation
Data Leakage
Overfitting
Instruction Tuning
Benchmark
第一阶段让我知道:
Transformer 是怎么跑起来的。
第二阶段让我开始知道:
一个模型为什么“看起来训练得很好”,但实际上还不会解决问题。
我觉得这才是自己从零训练模型最大的意义。
不是为了真的用 33M 参数干掉 GPT。
而是把以前那些只停留在文章里的概念,一个个真正跑出来。
下一篇:
OpsLM-V3:给自己的 Base Model 做 SFT,我准备让它第一次真正学会回答 SRE 问题。
附录:Phase2 最终关键数据
Model
-----
Decoder-only Transformer
33.66M Parameters
8 Layers
8 Attention Heads
DModel = 512
FFN = 2048
Context = 512
Tokenizer
---------
ByteLevel BPE
Vocab = 16000
Dataset
-------
Documents = 5454
Train Docs = 5182
Val Docs = 272
Train Tokens = 23,000,430
Val Tokens = 637,926
Training
--------
GPU = RTX 4090 24GB
BF16
Batch Size = 32
Gradient Accumulation = 2
Effective Tokens / Update = 32768
Optimizer = AdamW
Peak LR = 3e-4
Steps = 10000
Throughput ≈ 133K tokens/s
Result
------
Train Loss = 0.6501
Val Loss = 1.7252
PPL ≈ 5.61
Final Model
-----------
OpsLM-V2-Base-33M