17 关键算法实现流程:让公式落到代码
本章给阅读和复现路线;可运行的纯NumPy参考在 mini_lab.py,无需下载模型或调用API,已验证结果见 微型实验结果。它是单层教学实验,不是完整训练框架。
17.1 BPE:两套状态必须保存
词表id→bytes负责decode;有序merge规则负责encode。训练更新语料上的pair频次并扩词表;编码只应用学好的规则,不能重训。mini_lab故意不做regex预切分、normalization或special tokens,以隔离核心合并机制。
改动实验:把训练merge数改成0/1/4/8,看token序列长度;换成“你好你好”,对比字符数、byte数、token数。验证的核心不变量是完整文本decode(encode(text))==text,不是每个token独立decode都必须得到完整汉字。
17.2 一个 GQA + RoPE 层
参考代码layer:
- X[B,T,D]通过WQ与较窄WK/WV投影。
- reshape再transpose,Q[B,Hq,T,d],KV[B,Hkv,T,d]。
- 按绝对position旋转Q/K,V不变。
- 若cache存在,拼接旧的“已旋转K”和旧V,并续接position。
- 通过Q/K实际position构造
k_pos≤q_pos的mask,支持非方形decode。 - 教学实现重复KV头到Hq,算scores/稳定softmax/AV。
- 转回[B,T,D]并过WO;保存未重复的KV。
主实验对比“7token整段因果forward”与“3token prefill后逐个decode”,输出应在float64容差内相同。差异仅是计算组织;这验证cache正确性,没有测GPU加速。修改新token position为0、对旧K再次旋转、反向mask,都会破坏这个等价性。
17.3 最小next-token训练对齐
伪代码(不是指定某个框架API):
tokens = [A, B, C, D]
logits = model(tokens[:-1], causal_mask=True) # A, B, C
targets = tokens[1:] # B, C, D
loss = cross_entropy_from_logits(logits, targets, valid_mask)
zero_grad()
loss.backward()
clip_grad_norm_if_configured()
optimizer.step()
若使用内部shift的CausalLM接口,改为传完整input_ids及同位置labels(无效labels用ignore index),不要再自己shift一次。
SFT例prompt=[P0,P1,P2]、answer=[Y0,Y1,EOS],拼接后最后一个prompt位置P2的logits预测Y0。assistant-only监督的mask是在目标token属于回答的位置打开,不是看输入token是不是assistant文字。输入和目标相差一位,画索引最可靠。
17.4 Router 的最小数值实验
mini_lab用有限差分测两种函数对router logits的导数:Top1→softmax→加权梯度0;full softmax→Top1截断→加权通常非零。只做离散选择变化处以外的局部检查;TopK边界处不光滑,普通导数可能不存在。
真实训练还要验证dispatch/scatter路径和专家梯度;做Top2时检查每token权重和1、输出形状不变、只执行选中专家、同token多路结果求和。不能仅检验输出shape就宣布MoE实现正确。
17.5 LoRA 梯度为何值得亲自验
参考脚本对L=1/2||(W0+sBA)x-y||²分别计算解析∂A/∂B,用有限差分对照。随后把B改0、再把A/B都改0,看哪条路径能启动。这能同时检查矩阵乘法顺序、scale、初始化与“冻结base”的真正含义。
有限差分[f(θ+h)-f(θ-h)]/(2h)是局部数值验证工具,h太大截断误差高、太小浮点消去误差高。它不替代高维网络高效backprop,但非常适合小模型核查推导。
17.6 蒸馏的实现契约
teacher.eval()
with no_grad():
teacher_logits = teacher(same_context)
student_logits = student(same_context)
q = softmax(teacher_logits / tau)
log_p = log_softmax(student_logits / tau)
soft_ce = -(q * log_p).sum(vocab_axis)
loss_soft = mean_over_valid_targets(soft_ce) * tau**2
loss = hard_weight * hard_ce + (1-hard_weight) * loss_soft
loss_soft这里省略teacher entropy,与teacher固定时KL对student梯度相同;若要报告真正KL数值,要加回Σq logq。先检查词表ID、上下文、偏移、mask对齐,再讨论训练效果。不同库的KL接口可能期待log-prob与target-prob,不能凭参数名猜方向。
17.7 GRPO 的对齐比写公式更容易出错
设prompt有效长度Lp,response有效长度Ly,拼接长度Lp+Ly。用logits[:,:-1]预测ids[:,1:]后,response mask的logit索引为Lp-1 ... Lp+Ly-2。左padding/packed batch还要加正确偏移,不能用所有样本同一个Lp硬套。
# 已固定的 rollout 数据
old_logp = stored_rollout_logp.detach()
adv = grouped_reward_normalization(rewards).detach()
# 本轮参数重新评分,必须保留梯度
new_logp = gather_response_logprobs(actor(prompt_plus_response))
ratio = exp(new_logp - old_logp)
unclipped = ratio * adv[:, None]
clipped = clip(ratio, 1-eps_clip, 1+eps_clip) * adv[:, None]
policy_term = minimum(unclipped, clipped)
kl_term = chosen_KL_estimator(new_logp, ref_logp, ...) # 先明确版本契约
objective = response_average_then_group_average(policy_term - beta*kl_term)
loss = -objective
loss.backward()
optimizer.step()
这里的KL估计器故意不把所有版本压成一个实现;需要说明ref是否冻结、轨迹来自哪个policy、是否importance correction、对哪个量stop-gradient。学习时先把11章经典形式复现,再读框架实现的差异。
应记录的不变量:prompt/pad不贡献response loss;old不随着当前梯度更新;ref不是old;adv不会通过reward normalization向生成token做普通反传;当前actor logp必须有梯度;loss符号必须奖励高advantage行为。
17.8 运行本包的小实验
从课程根目录执行:
python verify_math.py
python mini_lab.py
依赖为Python与NumPy。它们核查数值与机制,不下载checkpoint、不改原notebook、不进行真实模型训练,也不产生伪造性能结果。先预测输出再运行,比只看“All assertions passed”更有学习价值。