29 KiB
第 28 课:总复盘与小项目
这一课不引入新算法。它把前 27 课收束成一个可检查、可复述、可展示的小项目:你要能从 MiniMind 源码出发,解释一条 LLM 链路如何从数据走到模型、训练、后训练、Agent,再走到推理接口。
本节的重点不是“把所有命令都跑一遍”,而是建立一套最终验收标准:哪些源码必须能读懂,哪些张量必须能说清楚,哪些教学版实现必须能和原项目对齐,哪些内容可以写进 portfolio。
目录
- 0. 本节主线
- 1. 本节要懂的 6 个复盘原则
- 2. 完整原理:一条 LLM 链路如何闭环
- 3. 源码总地图
- 4. MiniMind 源码证据复盘
- 5. 最终小项目
- 6. 验收实验
- 7. Portfolio 记录
- 8. 本节检查
- 9. 课程结束后的下一步
0. 本节主线
MiniMind 这门课最终要你能独立讲清楚:
jsonl 样本
-> tokenizer / chat template
-> input_ids / labels / masks
-> MiniMindForCausalLM
-> logits / next-token loss
-> optimizer / lr / amp / checkpoint
-> pretrain / SFT / LoRA / distillation
-> DPO / PPO / GRPO / CISPO
-> Agent rollout / tool observation / trajectory reward
-> transformers model directory
-> CLI / OpenAI API / WebUI
一句话:
学完 MiniMind,不是记住每个脚本参数,而是能解释每个阶段把什么数据形态变成了什么数据形态,以及这个转换在哪段源码里发生。
最终小项目的产物是:
1. 一张主链路图;
2. 一组 tiny 实验输出;
3. 一份教学版实现与原项目差异说明;
4. 一份可放进 portfolio 的项目总结。
1. 本节要懂的 6 个复盘原则
| 原则 | 要理解什么 | 源码位置 |
|---|---|---|
| 数据不是文本,数据会变成训练张量 | pretrain/SFT/DPO/RL 的样本格式不同,最终都要变成 token、label、mask 或 rollout 输入 | dataset/lm_dataset.py:37-252 |
| CausalLM 的核心是不变的 next-token prediction | 不管 pretrain、SFT、DPO、RL,底层都离不开 logits 对下一个 token 的 logprob | model/model_minimind.py:234-255, trainer/rollout_engine.py:24-36 |
| 模型结构是一组可追踪的 shape 变换 | Embedding、RoPE、Attention、FFN/MoE、LM head 都能用 shape 串起来 | model/model_minimind.py:10-255 |
| 训练脚本主要组织状态变化 | optimizer、lr、amp、grad accumulation、checkpoint 是训练工程状态,不是模型结构 | trainer/train_pretrain.py:24-80, trainer/trainer_utils.py:63-139 |
| 后训练改变的是优化目标,不是 CausalLM 本质 | DPO、PPO、GRPO/CISPO 都围绕 logprob、reward、reference、mask 重组 loss | trainer/train_dpo.py:34-49, trainer/train_ppo.py:79-299, trainer/train_grpo.py:71-196 |
| 部署侧复用训练时的协议 | chat template、special tokens、tool_call、thinking、decode 都必须和训练协议一致 | eval_llm.py:71-88, scripts/serve_openai_api.py:83-102, scripts/web_demo.py:350-413 |
学完本节,你应该能用自己的话解释这些变量:
input_ids:
模型真正看到的 token id 序列。
labels:
causal LM loss 的目标 token;不训练的位置用 -100。
attention_mask:
哪些 token 是有效上下文,哪些是 padding。
loss_mask / completion_mask / response_mask:
哪些目标 token 参与某个训练目标。
logits:
每个位置对词表中下一个 token 的未归一化分数。
per_token_logps:
从 logits 里 gather 出来的目标 token logprob。
old_logps / ref_logps / policy_logps:
rollout policy、reference model、current policy 对同一批 token 的 logprob。
reward / advantage:
外部评价信号,以及它进入 policy loss 前的权重形态。
2. 完整原理:一条 LLM 链路如何闭环
2.1 数据层:所有训练目标先从样本格式开始分化
MiniMind 不是只有一种数据集:
Pretrain:
{"text": "..."}
SFT:
{"conversations": [{"role": "...", "content": "..."}]}
DPO:
{"chosen": [...], "rejected": [...]}
RLAIF / Agent:
prompt 或 messages,用于在线 rollout。
分化发生在数据集类里。不同数据集返回不同字段:
PretrainDataset:
input_ids, labels
SFTDataset:
input_ids, labels,其中只训练 assistant 区域
DPODataset:
chosen/rejected 的 x/y/mask
RLAIFDataset:
prompt,用于后续生成 response
AgentRLDataset:
messages/tools/gt,用于多轮工具 rollout
所以第一条能力标准是:
看到任意训练脚本,先问它的 DataLoader 每个 batch 到底是什么结构。
2.2 模型层:底层永远是 decoder-only CausalLM
MiniMind 的模型主线是:
input_ids
-> embedding
-> N 个 decoder block
-> RMSNorm
-> Attention + RoPE + causal mask / KV cache
-> residual
-> RMSNorm
-> FFN 或 MoE
-> residual
-> final RMSNorm
-> lm_head
-> logits
只要你能解释:
[B, T] input_ids
-> [B, T, H] hidden_states
-> [B, T, vocab] logits
后面的 pretrain、SFT、DPO、RL 才有共同地基。
2.3 训练层:loss 都在回答“哪些 token 应该提高概率”
Pretrain 和 SFT 的差异不是模型不同,而是 loss 区域不同:
Pretrain:
训练整段普通文本的下一个 token。
SFT:
仍然是 next-token prediction,但只训练 assistant 回复区域。
DPO、PPO、GRPO 也没有脱离 logprob:
DPO:
比较 chosen 和 rejected 的 sequence logprob 差异。
PPO:
用 reward / value / advantage 更新 response token 的 policy logprob。
GRPO:
用同 prompt 多条 response 的 group reward 标准化得到 advantage。
CISPO:
使用 CISPO 分支调整 ratio / policy logprob 的使用方式。
所以第三条能力标准是:
任何训练目标,都要能说清楚:
哪些 token 参与 loss?
logprob 来自哪里?
mask 怎么对齐?
reference / old policy 是否参与?
2.4 Agent 层:工具执行是环境反馈,不是模型直接学标签
Agentic RL 不是 SFT labels 训练。它的核心是环境循环:
messages + tools
-> policy 生成 <tool_call>
-> Python 执行工具
-> tool observation 拼回 messages
-> policy 继续生成
-> 整条 trajectory 得 reward
-> GRPO/CISPO 更新 policy
这里最关键的边界是:
policy token:
模型生成,参与 policy loss。
tool observation token:
环境产生,进入上下文,但不参与 policy loss。
这就是第 26 课一直强调 response_mask、response_old_logps、completion_mask 对齐的原因。
2.5 部署层:把同一套文本协议接到不同入口
部署侧没有重新定义模型能力。它只是把同一套训练协议接到不同外壳:
CLI:
终端输入输出。
OpenAI API:
HTTP request / response,支持 stream、reasoning_content、tool_calls。
WebUI:
浏览器交互,维护 session history,显示 thinking 和 tool result。
Transformers / vLLM / Ollama:
依赖 config、tokenizer、chat_template、weights 的模型目录。
所以最后一条能力标准是:
部署不是魔法。部署只是:
load model/tokenizer
-> render prompt
-> generate
-> decode
-> package response
3. 源码总地图
按学习目的看源码,而不是按目录顺序看:
| 目的 | 先读 | 再读 | 最后读 |
|---|---|---|---|
| 看懂 tokenizer / template | model/tokenizer_config.json, minimind-3/chat_template.jinja |
dataset/lm_dataset.py |
eval_llm.py |
| 看懂模型结构 | MiniMindConfig |
MiniMindModel, MiniMindBlock, Attention |
MiniMindForCausalLM.forward, generate |
| 看懂 pretrain/SFT | PretrainDataset, SFTDataset |
train_pretrain.py, train_full_sft.py |
trainer_utils.py |
| 看懂 LoRA / distillation | model_lora.py |
train_lora.py |
train_distillation.py |
| 看懂 DPO | DPODataset |
train_dpo.py:dpo_loss |
course/impl/train_dpo_impl.py |
| 看懂 PPO/GRPO | rollout_engine.py |
train_ppo.py, train_grpo.py |
course/impl/core/ppo.py, course/impl/core/grpo.py |
| 看懂 Agent | AgentRLDataset |
train_agent.py:rollout_single |
train_agent.py:rl_train_epoch |
| 看懂部署 | convert_model.py |
serve_openai_api.py |
web_demo.py, chat_api.py |
4. MiniMind 源码证据复盘
4.1 数据集决定 batch 的语义
File: dataset/lm_dataset.py:37-252
Read this to understand: 每个训练阶段从 DataLoader 里拿到的 batch 字段不同,后面的训练脚本必须围绕这些字段组织。
Code/config/template excerpt:
class PretrainDataset(Dataset):
...
def __getitem__(self, index):
...
return input_ids, labels
class SFTDataset(Dataset):
...
def __getitem__(self, index):
...
return torch.tensor(input_ids, dtype=torch.long), torch.tensor(labels, dtype=torch.long)
class DPODataset(Dataset):
...
def __getitem__(self, index):
...
return {
'x_chosen': x_chosen,
'y_chosen': y_chosen,
'mask_chosen': mask_chosen,
'x_rejected': x_rejected,
'y_rejected': y_rejected,
'mask_rejected': mask_rejected
}
class AgentRLDataset(Dataset):
...
def __getitem__(self, index):
...
return {'messages': messages, 'tools': tools, 'gt': sample['gt']}
This code shows:
- Pretrain/SFT 直接返回 supervised training 张量。
- DPO 返回 chosen/rejected 两条路径。
- Agent RL 不返回 labels,而是返回在线 rollout 需要的环境输入。
4.2 CausalLM forward 是所有训练目标的共同地基
File: model/model_minimind.py:234-255
Read this to understand: MiniMind 的语言模型头如何把 hidden states 变成 logits,并在有 labels 时计算 next-token loss。
Code/config/template excerpt:
class MiniMindForCausalLM(PreTrainedModel, GenerationMixin):
...
def forward(self, input_ids, attention_mask=None, past_key_values=None, use_cache=False, logits_to_keep=0, labels=None, **kwargs):
hidden_states, past_key_values, aux_loss = self.model(input_ids, attention_mask, past_key_values, use_cache, **kwargs)
slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
logits = self.lm_head(hidden_states[:, slice_indices, :])
loss = None
if labels is not None:
x, y = logits[..., :-1, :].contiguous(), labels[..., 1:].contiguous()
loss = F.cross_entropy(x.view(-1, x.size(-1)), y.view(-1), ignore_index=-100)
return MoeCausalLMOutputWithPast(loss=loss, aux_loss=aux_loss, logits=logits, past_key_values=past_key_values, hidden_states=hidden_states)
This code shows:
lm_head把[B, T, H]变成[B, T, vocab]。- loss 使用
logits[..., :-1, :]预测labels[..., 1:]。 ignore_index=-100是 SFT mask 生效的位置。
4.3 训练脚本组织 optimizer、lr、checkpoint
File: trainer/train_pretrain.py:24-80
Read this to understand: 一个基础训练 epoch 如何把 batch、forward、loss、backward、clip、step 串起来。
Code/config/template excerpt:
def train_epoch(epoch, loader, iters, start_step=0, wandb=None):
for step, (X, Y) in enumerate(loader, start=start_step + 1):
X = X.to(args.device)
Y = Y.to(args.device)
lr = get_lr(epoch * iters + step, args.epochs * iters, args.learning_rate)
for param_group in optimizer.param_groups:
param_group['lr'] = lr
with autocast_ctx:
res = model(X, labels=Y)
loss = res.loss
loss = loss / args.accumulation_steps
loss.backward()
if step % args.accumulation_steps == 0:
torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
optimizer.step()
optimizer.zero_grad(set_to_none=True)
This code shows:
- batch 先搬到 device。
- lr 每 step 按 schedule 更新。
- loss 会除以 gradient accumulation steps。
- 反向、裁剪、优化器 step 是训练工程的核心状态变化。
File: trainer/trainer_utils.py:63-139
Read this to understand: checkpoint 不只是保存模型参数,还保存 optimizer、epoch、step、world size 等恢复训练需要的状态。
Code/config/template excerpt:
resume_data = {
'model': state_dict,
'optimizer': optimizer.state_dict(),
'epoch': epoch,
'step': step,
'world_size': dist.get_world_size() if dist.is_initialized() else 1,
'wandb_id': wandb_id
}
...
if os.path.exists(resume_path):
ckp_data = torch.load(resume_path, map_location='cpu')
saved_ws = ckp_data.get('world_size', 1)
current_ws = dist.get_world_size() if dist.is_initialized() else 1
if saved_ws != current_ws:
ckp_data['step'] = ckp_data['step'] * saved_ws // current_ws
return ckp_data
This code shows:
- 可恢复训练必须保存 optimizer 和 step,不只是模型权重。
- world size 变化时会调整 step。
- 这是训练工程状态,不是模型结构。
4.4 DPO 把偏好学习写成 logprob 差异
File: trainer/train_dpo.py:34-49
Read this to understand: DPO 的 loss 如何比较 chosen/rejected 在 policy 相对 reference 上的提升。
Code/config/template excerpt:
def dpo_loss(ref_log_probs, policy_log_probs, mask, beta):
seq_lengths = mask.sum(dim=1, keepdim=True).clamp(min=1)
ref_log_probs = (ref_log_probs * mask).sum(dim=1) / seq_lengths.squeeze()
policy_log_probs = (policy_log_probs * mask).sum(dim=1) / seq_lengths.squeeze()
chosen_ref, rejected_ref = ref_log_probs[::2], ref_log_probs[1::2]
chosen_policy, rejected_policy = policy_log_probs[::2], policy_log_probs[1::2]
pi_logratios = chosen_policy - rejected_policy
ref_logratios = chosen_ref - rejected_ref
logits = pi_logratios - ref_logratios
loss = -F.logsigmoid(beta * logits)
return loss.mean()
This code shows:
- DPO 先用 mask 聚合 response token 的 logprob。
- chosen/rejected 必须成对排列。
- policy 比 reference 更偏向 chosen 时,loss 会变小。
4.5 PPO/GRPO 都围绕 old/current/ref logprob
File: trainer/rollout_engine.py:24-36
Read this to understand: token logprob 的核心操作是 log_softmax + gather,后面 PPO/GRPO/Agent 都复用这个思想。
Code/config/template excerpt:
def compute_per_token_logps(model, input_ids, n_keep, attention_mask=None):
logits = unwrapped(input_ids, attention_mask=attention_mask, logits_to_keep=n_keep + 1).logits[:, :-1, :]
per_token_logps = []
for logits_row, ids_row in zip(logits, input_ids[:, -n_keep:]):
per_token_logps.append(
torch.gather(logits_row.log_softmax(dim=-1), 1, ids_row.unsqueeze(1)).squeeze(1)
)
return torch.stack(per_token_logps)
This code shows:
logits[:, :-1, :]对齐 next-token targets。input_ids[:, -n_keep:]是要取 logprob 的 response/completion token。- 返回 shape 是
[B, n_keep]。
File: trainer/train_grpo.py:121-143
Read this to understand: GRPO/CISPO 如何把 group reward、ratio、reference KL 合成 policy loss。
Code/config/template excerpt:
grouped_rewards = rewards.view(-1, args.num_generations)
mean_r = grouped_rewards.mean(dim=1).repeat_interleave(args.num_generations)
std_r = grouped_rewards.std(dim=1, unbiased=False).repeat_interleave(args.num_generations)
advantages = (rewards - mean_r) / (std_r + 1e-4)
kl_div = ref_per_token_logps - per_token_logps
per_token_kl = torch.exp(kl_div) - kl_div - 1
ratio = torch.exp(per_token_logps - old_per_token_logps)
...
per_token_loss = -(torch.min(per_token_loss1, per_token_loss2) - args.beta * per_token_kl)
policy_loss = ((per_token_loss * completion_mask).sum(dim=1) / completion_mask.sum(dim=1).clamp(min=1)).mean()
This code shows:
- group reward 标准化得到 sequence-level advantage。
ratio比较 current policy 和 rollout old policy。per_token_kl比较 current policy 和 frozen reference。completion_mask决定哪些 token 参与 loss。
4.6 Agent rollout 把环境 observation 接回上下文
File: trainer/train_agent.py:98-157
Read this to understand: Agentic RL 的关键不只是 loss,而是多轮环境循环如何构造 trajectory。
Code/config/template excerpt:
for turn in range(max_turns):
context = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, tools=tools, open_thinking=open_thinking)
inputs = tokenizer(context, return_tensors="pt", add_special_tokens=False).to(device)
rollout_result = rollout_engine.rollout(...)
new_ids = rollout_result.completion_ids[0].tolist()
new_logps = rollout_result.per_token_logps[0].tolist()
response_ids.extend(new_ids)
response_mask.extend([1] * len(new_ids))
response_old_logps.extend(new_logps)
calls = parse_tool_calls(new_text)
if not calls:
break
messages.append({"role": "assistant", "content": new_text})
...
messages.append({"role": "tool", "content": result_str})
observe_context = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=not unfinished, tools=tools, open_thinking=open_thinking)
obs_delta = observe_ids[current_len:]
response_ids.extend(obs_delta)
response_mask.extend([0] * len(obs_delta))
response_old_logps.extend([0.0] * len(obs_delta))
This code shows:
- policy 生成 token 的 mask 是 1。
- tool observation delta 会进入
response_ids,因为它是后续上下文。 - observation token 的 mask 是 0,old logprob 是占位 0.0。
- Agent 的难点在 trajectory 构造,而不只是 GRPO/CISPO loss。
4.7 部署复用同一套 template 和 generate
File: eval_llm.py:71-88
Read this to understand: 最小部署闭环就是 messages、template、tokenizer、generate、decode。
Code/config/template excerpt:
conversation.append({"role": "user", "content": prompt})
inputs = tokenizer.apply_chat_template(conversation, tokenize=False, add_generation_prompt=True, open_thinking=bool(args.open_thinking))
inputs = tokenizer(inputs, return_tensors="pt", truncation=True).to(args.device)
generated_ids = model.generate(
inputs=inputs["input_ids"],
attention_mask=inputs["attention_mask"],
max_new_tokens=args.max_new_tokens,
do_sample=True,
streamer=streamer,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
)
response = tokenizer.decode(generated_ids[0][len(inputs["input_ids"][0]):], skip_special_tokens=True)
This code shows:
- 部署侧仍然依赖 chat template。
- 模型生成的是完整
[prompt + response]token 序列。 - decode 时只取 prompt 之后的新 token。
5. 最终小项目
小项目名称:
MiniMind 源码驱动 LLM 链路复盘
目标不是训练出强模型,而是产出一份能证明你真的看懂链路的报告。
5.1 必交产物
产物 A:主链路图
dataset jsonl
-> tokenizer/chat template
-> input_ids/labels/masks
-> MiniMindForCausalLM
-> logits/loss/logprob
-> optimizer/checkpoint
-> pretrain/SFT/LoRA/DPO/PPO/GRPO/Agent
-> eval/API/WebUI
每个箭头旁边写一个源码位置。例如:
SFT labels: dataset/lm_dataset.py:SFTDataset.generate_labels
next-token loss: model/model_minimind.py:MiniMindForCausalLM.forward
GRPO ratio: trainer/train_grpo.py:grpo_train_epoch
产物 B:实验记录
至少跑 6 个实验,每个实验记录:
命令:
关键输出:
这个实验验证了哪条源码原则:
我曾经容易错在哪里:
产物 C:教学版实现说明
对 course/impl/ 写一份差异说明:
我们手写实现了什么:
对齐 MiniMind 哪段源码:
省略了什么工程细节:
为什么可以省略:
如果要真实训练,必须回到原项目哪些能力:
产物 D:portfolio 总结
写进 course/portfolio/:
progress.md:
各阶段是否跑通。
experiments.md:
关键实验命令和输出。
implementation.md:
教学版实现和原项目差异。
resume_bullets.md:
一段准确不过度包装的项目描述。
5.2 推荐报告结构
# MiniMind 源码驱动 LLM 链路复盘报告
## 1. 项目目标
用 MiniMind 源码解释并验证一个小型 LLM 从数据到部署的完整链路。
## 2. 主调用链
画出 jsonl -> tokenizer -> model -> loss -> optimizer -> post-training -> deploy。
## 3. 关键源码证据
列 8-12 个源码点,每个点说明它证明了什么。
## 4. 实验记录
记录 tiny 实验命令、shape、loss、mask、logprob、reward 等输出。
## 5. 教学版实现
说明 course/impl 实现了哪些核心模块,以及和原项目差异。
## 6. 仍未覆盖的工程能力
列出分布式训练、真实大数据训练、SGLang/vLLM 服务化、线上监控等。
## 7. 下一步改进
选择 1-2 个真正要继续做的方向。
5.3 不合格的小项目长什么样
只贴 README 命令;
只说“实现了 Transformer”但没有源码位置;
只展示生成效果,不解释 input_ids / labels / masks;
只讲 PPO/GRPO 概念,不说明 old/current/ref logprob;
把 WebUI 当成模型能力本身;
没有任何 tiny 实验输出。
合格标准是:
每个结论都能回到一个源码位置;
每个实验都能验证一个变量流;
每个实现差异都能说明为什么省略。
6. 验收实验
6.1 环境与资源检查
命令:
cd /home/sun/minimind
python course/labs/check_project_readiness.py
记录:
torch =
transformers =
cuda =
pretrain_t2t_mini =
sft_t2t_mini =
minimind_3_safetensors =
这个实验验证:当前工作区能跑哪些课程实验,哪些真实训练资源仍然缺失。
6.2 tokenizer / SFT labels / loss shift
命令:
cd /home/sun/minimind
python course/labs/inspect_tokenizer.py
python course/labs/inspect_sft_dataset.py
python course/labs/trace_loss_shift.py
记录:
special token ids =
chat template 片段 =
assistant label 区域 =
logits shift =
ignore_index=-100 的位置 =
这个实验验证:文本如何变成 token,哪些 token 参与 SFT loss,以及 causal LM 为什么右移。
6.3 模型结构与 generation
命令:
cd /home/sun/minimind
python course/labs/inspect_model_config_params.py
python course/labs/trace_block_shapes.py
python course/labs/trace_attention_shapes.py
python course/labs/trace_rope_kv_cache.py
记录:
hidden_size =
num_layers =
q/k/v shape =
causal mask shape =
kv cache length growth =
这个实验验证:模型结构不是黑盒,所有核心模块都能用 shape 追踪。
6.4 pretrain / SFT / train loop
命令:
cd /home/sun/minimind
python course/labs/compare_pretrain_sft.py
python course/labs/trace_pretrain_step.py
记录:
pretrain labels =
sft labels =
loss before step =
grad norm =
参数是否更新 =
这个实验验证:训练脚本不是只调用 model.train(),而是明确组织 batch、loss、backward、optimizer step。
6.5 DPO / PPO / GRPO / Agent
命令:
cd /home/sun/minimind
python course/labs/trace_dpo_dataset_reference.py
python course/labs/trace_dpo_loss.py
python course/labs/trace_ppo_batch_flow.py
python course/labs/trace_grpo_cispo_loss.py
python course/labs/trace_reward_logprob_kl.py
python course/labs/trace_agent_tool_reward.py
记录:
chosen/rejected mask =
dpo logratio =
old/current/ref logprob shape =
ratio =
reference KL penalty =
reward =
advantages =
tool reward case 对比 =
这个实验验证:后训练算法的共同底层是 logprob、mask、reward、reference。
6.6 教学版实现测试
命令:
cd /home/sun/minimind
pytest course/impl/tests -q
记录:
通过的测试数 =
失败的测试名 =
失败原因 =
对应源码原则 =
这个实验验证:手写实现不是孤立练习,而是要通过对齐测试。
如果当前环境没有 pytest,先只记录:
pytest missing
不要把缺少测试工具误判为模型实现错误。
6.7 本地 transformers 推理
命令:
cd /home/sun/minimind
python course/labs/run_minimind3_once.py --device cpu --max_new_tokens 32 --prompt "MiniMind 是什么?"
python course/labs/trace_deploy_surfaces.py
记录:
input_ids.shape =
generated_ids.shape =
new_tokens =
contains_tools_tag =
has_open_think_prompt =
tool_calls =
这个实验验证:训练协议最终能被 transformers 模型目录和部署接口复用。
7. Portfolio 记录
7.1 course/portfolio/progress.md
建议更新成这种状态:
| 阶段 | 状态 | 产物 | 备注 |
|---|---|---|---|
| 模型结构 | 已完成 | `core/model_parts.py`, `core/causal_lm.py`, `core/generation.py` | 已通过 shape / loss / cache 相关实验 |
| Pretrain | 已完成 | `core/train_loop.py`, `train_pretrain_impl.py` | 已跑通 tiny pretrain step |
| SFT | 已完成 | `core/datasets.py`, `train_sft_impl.py` | 已验证 assistant labels mask |
| LoRA | 已完成 | `core/lora.py`, `train_lora_impl.py` | 已验证只更新 LoRA 参数 |
| DPO | 已完成 | `train_dpo_impl.py` | 已验证 chosen/rejected logratio |
| PPO/GRPO | 已完成 | `core/ppo.py`, `core/grpo.py`, `train_grpo_impl.py` | 已验证 reward/logprob/KL/advantage |
| Agent/Deploy | 已观察验证 | labs + 原项目脚本 | 复用原项目工程外围 |
只有实际跑通过的阶段才写“已完成”。没跑过就写“已实现,待验收”。
7.2 course/portfolio/experiments.md
每条实验记录建议这样写:
日期:YYYY-MM-DD
阶段:DPO
命令:python course/labs/trace_dpo_loss.py
关键输出:
- chosen_logp =
- rejected_logp =
- dpo_loss =
结论:
这个实验验证 DPO loss 比较的是 policy 相对 reference 的 chosen/rejected 偏好差。
问题:
一开始容易把 token 平均 logprob 和 sequence sum logprob 混在一起。
7.3 course/portfolio/implementation.md
建议每个模块用同一模板:
## 模块:GRPO/CISPO
我们实现了什么:
- group reward 标准化
- ratio = exp(policy_logps - old_logps)
- reference KL penalty
- GRPO/CISPO loss 分支
对齐的原源码:
- `trainer/train_grpo.py:121-143`
- `course/impl/core/grpo.py`
省略了什么:
- 真实 reward model
- rollout server
- DDP
- wandb/swanlab
为什么可以省略:
- 教学版目标是验证 loss 公式和 mask 规约,不是复刻完整训练系统。
什么时候必须回到原源码方案:
- 需要真实在线生成、多卡训练、长时间 checkpoint、真实数据集。
7.4 course/portfolio/resume_bullets.md
候选表达:
基于 MiniMind 源码完成小型 LLM 全链路学习与教学版复现,覆盖 tokenizer/chat template、Decoder-only CausalLM、RoPE/KV cache、Pretrain/SFT、LoRA、DPO、PPO/GRPO/CISPO、Agent tool-use rollout 与 OpenAI-compatible 推理接口。手写实现核心模型部件、训练循环、SFT mask、LoRA、DPO/GRPO/PPO loss,并通过 tiny 实验验证 input_ids/labels/mask/logprob/reward/KL 等关键变量流。
不要写:
训练了一个媲美大模型的 LLM。
除非你真的完成了大规模训练和评测。
8. 本节检查
- 给你一个
trainer/train_*.py,你第一步应该找哪个 dataset?为什么? - 为什么 SFT、DPO、PPO、GRPO 都离不开 next-token logprob?
labels=-100、loss_mask、completion_mask、response_mask分别解决什么问题?- 为什么 Agent tool observation 要进入上下文,但不能像 policy token 一样参与 policy loss?
- 教学版
course/impl/省略了哪些真实工程能力?这些省略什么时候会变成问题? - transformers 模型目录为什么必须包含 tokenizer / chat template,而不只是权重文件?
9. 课程结束后的下一步
课程结束后,建议只选一个方向继续,不要同时开太多分支。
方向 A:把教学版实现跑得更完整
目标:
pytest course/impl/tests -q 全部通过;
tiny pretrain -> tiny SFT -> tiny DPO/GRPO 能顺序跑通。
重点:
保持小而可解释,不追求模型效果。
方向 B:做一次真实数据小训练
目标:
下载 pretrain_t2t_mini / sft_t2t_mini;
用小 batch 跑完整 pretrain/SFT;
记录 loss 曲线、生成样例和资源消耗。
重点:
先验证训练系统稳定,不要急着调效果。
方向 C:深入一个后训练算法
目标:
只选 DPO、PPO、GRPO 或 Agent RL 之一;
写出公式、源码、实验、失败案例和改进方向。
重点:
把 logprob/mask/reward/reference 对齐讲清楚。
方向 D:做部署小闭环
目标:
用本地 minimind-3 启动 OpenAI-compatible API;
写一个客户端请求;
验证 thinking/tool_call 字段。
重点:
分清模型生成协议和 API 包装协议。
无论选哪条,继续遵守这条规则:
先说明变量流,再解释源码,再跑最小实验,最后才谈效果。