OpsLM Phase3:一次夭折的 Instruct 实验——为什么 33M Base 没被 SFT 救回来

前两阶段,我已经从零实现了一个 Decoder-only Transformer,并完成了一轮面向 Linux、Kubernetes、Docker、Prometheus、vLLM 等技术文档的领域预训练。

到 Phase2 结束时,我得到了一版大约 33.66M 参数的模型:

OpsLM-V2-Base-33M

它可以续写 Kubernetes、Linux、AI Infra 等领域文本,也确实学到了一些技术词之间的关联。

但它有一个非常明显的问题:

它更像一个“技术文档续写模型”,而不是一个能回答 SRE 问题的助手。

所以 Phase3 的目标很自然:

在 OpsLM-V2-Base-33M 上进行 SFT,让它从 Base Model 变成 Instruct Model。

结果却并不理想。

这篇文章记录的不是一次成功训练,而是一次最终决定放弃的 V3 实验,以及我如何一步步排除数据集、Loss、Label Mask、Optimizer 等问题,最后发现真正的瓶颈很可能已经进入了 Base Model 本身。


1. Phase3 想解决什么问题?

Phase2 有一个很典型的例子。

输入:

服务器 CPU 使用率不高,但是 load average 非常高,可能是什么原因?

模型并不会真正分析 Linux Load Average,而是倾向继续生成 Kubernetes、CPU Policy、Requests/Limits 等相关技术文本。

也就是说,它已经知道:

CPU
Kubernetes
Pod
Resource
Policy

这些 Token 经常一起出现。

但它还没有学会:

用户提出问题
        ↓
理解问题
        ↓
检索相关知识
        ↓
组织成回答

所以 Phase3 引入 SFT。

训练格式非常简单:

用户:<instruction>

助手:<answer>

并且只对 Assistant 的回答部分计算 Loss。

User Prompt 对应 Label 全部设置为:

-100

核心 Loss:

shift_logits = logits[:, :-1, :].contiguous()
shift_labels = labels[:, 1:].contiguous()

loss = F.cross_entropy(
    shift_logits.view(-1, vocab_size),
    shift_labels.view(-1),
    ignore_index=-100,
)

也就是说:

模型看到完整的“用户问题”,但训练目标只有“助手应该怎么回答”。

这是标准的 Causal LM SFT 思路。


2. 第一次 Seed SFT

为了先验证链路,我人工准备了大约 20 条 SRE 问答。

包括:

  • Linux Load Average
  • OOMKilled
  • Pod Pending
  • CrashLoopBackOff
  • inode
  • TCP
  • Redis
  • MySQL
  • Prometheus
  • GPU
  • vLLM
  • systemd
  • Docker

Seed 实验很快出现了一个有意思的变化。

原来的 Base Model 更像:

Kubernetes documentation continuation...

经过几十步 SFT 后,模型开始输出:

建议:
1.
2.
3.

这说明:

SFT 确实改变了模型的行为模式。

但内容依旧经常是错的。

例如 Linux Load Average 问题,它仍然会把答案引向 Kubernetes CPU Requests/Limits。

结论也很明显:

20 条数据只能教模型“回答的样子”,不可能真正建立大规模 SRE 问答能力。

于是开始正式构建 Phase3 数据。


3. 引入 Hugging Face DevOps 数据

这一步我没有继续完全依靠 Teacher Model 合成,而是优先寻找现成公开数据。

最终主要使用了:

Skilln/devops-qa-dataset
pavanmantha/devops-v1

经过:

质量过滤
Exact Dedup
SimHash Near Dedup
长度过滤
Train / Val Split

最后正式训练集得到:

Train Samples: 19,071

序列长度:

seq_len = 512

随机检查 SFT Dataset:

input:  torch.Size([512])
labels: torch.Size([512])

train tokens: 233

也就是说一个 512 Token 的 Sample 中,真正计算 Loss 的只有 Assistant Answer 部分。

至此数据管线看起来一切正常。


4. 第一轮正式 SFT:LR = 3e-5

第一轮参数:

Base:
OpsLM-V2-Base-33M

Train:
19,071 samples

Batch:
32

LR:
3e-5

约:
3 epochs

到 Step 1800:

Train loss: 3.9621
Val loss:   3.7762

Loss 在下降。

Validation Loss 也没有出现明显过拟合。

但真实生成非常差。

例如:

How to: How do I debug a Pending pod?

模型输出:

kubectl run -it --image=busybox:1.28 --restart=Never

问题在于:

这甚至就是训练集中的原题。

而训练集的真正答案明确包括:

kubectl describe pod

检查 CPU / Memory Requests

检查 Taints

检查节点是否被 Cordon

检查 Node Ready 状态

也就是说:

模型训练了三轮之后,连训练集里见过的问题都没有正确学会。

这时候第一个怀疑自然是:

SFT 实现是不是有 Bug?


5. 最关键的实验:单样本过拟合

这是整个 Phase3 最有价值的排错实验之一。

我把:

How to: How do I debug a Pending pod?

和它的标准答案单独提取出来。

整个训练集:

1 sample

Batch:

1

然后故意反复训练这一条数据。

结果:

模型可以完整背下来。

输入同一个 Prompt 后,可以正确生成训练答案。

这一刻基本可以排除:

Tokenizer ❌
Label Mask ❌
Causal Shift ❌
CrossEntropy ❌
Backward ❌
Optimizer ❌
Checkpoint ❌
Generation ❌

整个 SFT Pipeline 是工作的。

这也说明:

OpsLM-33M 并不是“完全学不会”。

它可以记忆。

真正的问题是:

当 19K 个不同任务同时进入训练以后,它没有能力把这些知识稳定组织起来。


6. 第二轮:把学习率提升到 1e-4

于是我重新从:

OpsLM-V2-Base-33M

开始训练,而不是继续第一轮 checkpoint。

参数调整为:

batch_size = 16
lr         = 1e-4
max_steps  = 3600

19,071 条数据,Batch 16:

19071 / 16
≈ 1192 steps / epoch

3600 Step 大约:

≈ 3 epochs

第二轮 Loss 明显比第一轮下降得快。

例如:

step=1900

Train loss:
3.3702

Val loss:
3.6015

随后:

step=2300

Val loss:
3.5568

到 Step 2900:

Train loss:
3.5573

Val loss:
3.5208

从曲线来看:

第二轮显然比 3e-5 的训练有效。

但真正的问题仍然存在。


7. Step 2900,再次测试训练集原题

输入:

How to: How do I debug a Pending pod?

一次输出:

kubectl describe pod mypod -o yaml | less

另一次:

kubectl run -it \
  --image=busybox:1.28 \
  --restart=Never

相比最开始已经有进步。

至少模型已经知道:

Pending Pod
→ kubectl
→ describe

但是它依然没有形成完整的排障回答。

更加严重的一次生成甚至出现:

--command=Never
--command=Never
--command=Never
--command=Never
...

即典型的 Repetition Collapse。

调整:

temperature
top-k
top-p

只能改变复读形式,并不能解决回答能力本身的问题。


8. 为什么 Loss 在下降,模型却还是不会回答?

这是我在 Phase3 里真正理解的一件事:

Validation Loss 下降,不代表模型一定拥有了我想要的任务能力。

Loss 衡量的是:

给定前面的 Token
↓
预测下一个 Token 的概率有没有变好

但我真正想要的是:

理解 Pending 的语义
↓
识别这是 Scheduler 问题
↓
知道“节点有资源”不代表一定可调度
↓
想到 Requests
↓
想到 Taints / Tolerations
↓
想到 Affinity
↓
想到 PVC
↓
想到 Node Ready / Cordon
↓
按照合理优先级组织回答

两者并不是同一件事情。

一个模型完全可能:

Loss ↓

同时:

真实 QA 能力仍然很弱

Phase3 就出现了这个现象。


9. V3 为什么最终决定停止?

目前我认为主要有三个原因。

原因一:33M 参数的容量太小

当前模型只有:

33.66M Parameters

却希望同时学习:

Linux
Kubernetes
Docker
Network
Prometheus
Database
systemd
GPU
CUDA
vLLM
AI Infra
中文
英文
故障排查
命令分析
概念解释

单独背一道题没有问题。

但让这些知识同时存在,并根据自然语言问题正确选择知识,对 33M 模型已经非常困难。

这种现象在全量 SFT 时尤其明显:

Pending
↓
模型知道和 Kubernetes 有关
↓
模型知道 kubectl 有关
↓
却无法稳定选择正确排障路径

也就是:

相关性学到了,组合能力没建立起来。


10. 更严重的问题:Base 预训练不足

其实参数量还不是唯一问题。

OpsLM-V2 的原始预训练数据只有:

≈ 23M Tokens

虽然训练跑了大约十几遍,累计 Token Presentation 达到数亿:

23M
×
约 14 passes

但:

反复看 23M Token

和:

真正看过数亿个丰富的新 Token

完全不是一回事。

当前 Base Model 本质上更像:

一个看过大量 Kubernetes/Linux 技术文档的小型文本续写器。

而不是:

一个先掌握语言理解、知识表示和基本组合能力,然后再学习 SRE 的 Base Model。

这也是为什么 Phase2 的模型非常容易:

看到 Pod
↓
进入 Kubernetes Documentation 模式

而不是首先理解:

用户到底想问什么?

11. SFT 无法凭空创造 Base Model 能力

这是 Phase3 最大的收获。

最开始我潜意识里有一种期待:

Base 不太会回答
+
大量 QA SFT
=
会回答

但实验告诉我不是这么简单。

更加准确的关系应该是:

Pretraining
负责形成:

语言
知识
概念
关联
表示能力
一定的泛化能力


SFT
负责形成:

如何响应用户
如何组织回答
遵循什么格式
如何调用已有能力

SFT 更像:

教一个已经掌握知识的人怎么参加考试。

而不是:

靠几万道考试题,从零补完他的基础教育。

当前 OpsLM-V2 的 Base 能力不够成熟,所以 V3 的 SFT 实际承担了太多任务。

最终效果自然有限。


12. 数据质量也存在问题

这轮使用的 19K 数据虽然数量很大,但并不是全部都是理想 SRE Instruction。

随机检查可以发现很多:

How to: Lab 5.2. Managing Pods with YAML file

或者:

kubectl run ...

这类:

教程
实验
命令示例
文档片段

而不是:

真实故障
→ 分析
→ 定位
→ 解决

对于大型成熟 Base Model,这种噪声可能不是致命问题。

但对于 33M 模型:

每一条训练数据都会明显影响它的行为分布。

所以才可能出现:

Pending Pod
↓
kubectl run busybox

这种“技术上有关,但回答完全不对”的现象。


13. 为什么我不继续把 V3 训练到 10000 Step?

因为问题已经不再是:

是不是少训练了几轮?

我们已经确认:

  1. 单样本可以完全拟合;
  2. 全量训练 Loss 持续下降;
  3. 更大学习率确实改善 Loss;
  4. 训练集原题依然不能完整回答;
  5. 真实问题泛化更差;
  6. 模型开始出现命令模式竞争和重复退化。

继续:

5000
10000
20000

更多是在反复强化当前数据分布。

它不会自动把一个 33M、只看过 23M 独立预训练 Token 的 Base Model 变成成熟的 SRE 推理模型。

所以 V3 到这里正式停止。


14. Phase3 最终结论

这轮实验虽然“夭折”,但得到的结论非常明确。

已验证

Transformer 实现             ✅

Pretraining Pipeline         ✅

SFT Dataset                  ✅

Assistant-only Loss          ✅

Causal Shift                 ✅

Optimizer                    ✅

Checkpoint                   ✅

Generation                   ✅

单样本 Memorization          ✅

未达到

大规模 Instruction Learning  ❌

SRE QA 泛化                  ❌

稳定多步排障                 ❌

中文 SRE 问答                ❌

复杂知识组合                 ❌

最终判断:

Base Model 能力不足
        +
模型容量有限
        +
预训练数据量不足
        +
SFT 数据存在噪声
        ↓
OpsLM-V3 未达到预期

15. OpsLM-V3 正式归档

这一版我不会删除。

它会作为:

OpsLM-V3-Instruct-Failed-Experiment

保留下来。

因为它证明了一件非常重要的事情:

“能训练”与“有能力”是两回事。

以及:

SFT 不是魔法。

这是从零训练语言模型时非常值得亲自踩一次的坑。


16. 下一站:OpsLM-V4

V3 之后,我决定暂时不再折腾 33M 模型的学习率。

下一阶段重新训练 Base Model。

目标大约:

100M ~ 150M Parameters

并将独立预训练数据从:

23M Tokens

提高到:

约 1B Tokens

数据也不再只有技术文档,而是:

通用英文
+
通用中文
+
高质量教育文本
+
Linux / Kubernetes / SRE
+
GPU / CUDA / AI Infra
+
少量代码与配置

下一版真正想验证的是:

33M + 23M Tokens
        VS
100M+ + 1B Tokens

在完全相同的问题上:

How do I debug a Pending pod?

究竟会出现怎样的差距。

如果下一版 Base 在 SFT 后能够稳定回答:

先 kubectl describe pod 查看 Events

检查 Requests 是否满足

检查 Taints / Tolerations

检查 NodeSelector / Affinity

检查 PVC

检查节点 Ready / Cordon 状态

那么这整个 OpsLM 项目最重要的一课可能就是:

真正决定模型上限的,首先是 Base Model。

而不是 SFT 技巧。


Phase3 到此结束

V3 没有成为一个好用的 SRE Assistant。

但它成功暴露了 V2 的能力边界。

所以它不是没有价值。

相反,它告诉了我:

下一步到底应该把算力和时间花在哪里。

下一篇:

《OpsLM-V4:重新训练 Base Model——从 33M / 23M Token 走向亿级参数与 10 亿 Token》