从几十 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. 第二阶段的目标

第二阶段我给自己定了几个明确目标:

  1. 数据规模从几十 KB 提升到几十 MB 甚至上百 MB 原始文本。
  2. 数据不再由我手写,而是真实来自开源技术文档和仓库。
  3. 重新训练一个更合理的 Tokenizer。
  4. 重新设计 Dataset,使用连续 Token Stream 进行 Packed Language Modeling。
  5. 增加验证集,不再只看 Train Loss。
  6. 增加 Checkpoint、Resume、Learning Rate Scheduler、BF16 等更接近真实预训练的机制。
  7. 最终得到一个真正意义上的 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