上一篇讲完 R1 的四层链路后,一个诚实的困惑是:每句话都能听懂,“激励""涌现""解剖”这个故事也能复述——但心里不踏实。这种不踏实有明确的病因:叙事层的名词在你脑子里没有对应的可执行物。“模型学会了反思”——落到代码上是哪一行?”奖励答对的回答”——奖励怎么变成参数的变化?”思考 32k token”——显存里多了几个 GB?这篇陪读只做一件事:把上一篇的每个抽象名词,兑换成代码、张量形状或一个你能手算的数字。读完的检验标准:给你纸和笔,你能把一次推理和一次训练更新用具体数字走一遍。
一句话主线
R1 的全部”神秘”只由两个循环构成:推理时的采样循环(十几行)和训练时的 GRPO 循环(几十行)。所谓”学会思考”,就是第二个循环反复微调参数,让第一个循环更常采样出能答对的长草稿。
下面两条主线各走一遍,所有数字都可以在纸上复算。
一、推理侧:“思考”就是一个 while 循环
1.1 输入的真实样子:一个拼接出来的字符串
“给 R1 一道题”在实现层是纯粹的字符串拼接。按官方 README 的使用建议(不加 system prompt、数学题注明分步作答并把答案放进 \boxed{}、强制以 <think>\n 开头),真正进入模型的字符串长这样:
<|begin▁of▁sentence|><|User|>Please reason step by step, and put your
final answer within \boxed{}. 求 1+2+...+100 的值。<|Assistant|><think>
注意最后那个 <think>:它不是模型自己”决定思考”后写下的,而是提前塞进输入里的——README 原话是”为确保模型进行充分推理,我们建议强制模型在每次输出的开头以 <think>\n 起始”。也就是说,“进入思考模式”这件事,一半是训练出来的习惯,另一半是推理框架用模板硬保证的。第一个抽象名词兑换完毕:“思考模式” = 模板里预填的一个标记。
这个字符串接着被 tokenizer 切成整数序列。<think>、</think>、“Wait” 在 V3/R1 的词表里就是 129,280 个条目中的普通几个(词表大小来自 V3 官方 config 的 vocab_size: 129280)——没有任何专门的”推理电路”入口,它们和”苹果”这个词在机制上完全平等。
1.2 采样循环:十几行,没有别的了
接下来发生的全部事情:
ids = tokenize(template(question)) # 整数列表,长度 ~50
while True:
logits = model(ids)[-1] # [129280],只取最后一个位置
probs = softmax(logits / 0.6) # 温度 0.6,官方推荐值
probs = top_p_filter(probs, 0.95) # 砍掉累计概率 95% 以外的长尾
next_id = multinomial_sample(probs) # 掷一次骰子
ids.append(next_id)
if next_id == EOS: break
这个循环跑几千到几万圈,就是一次完整的”思考 + 回答”。几个关键兑换:
“温度 0.6”改的是哪个数字?就是 logits / 0.6 这一步——除以一个小于 1 的数,大 logit 更大、小 logit 更小,softmax 之后分布变尖。手算一个三词玩具词表,logits 为 :
| 词 A | 词 B | 词 C | |
|---|---|---|---|
| 温度 1.0 | 0.665 | 0.245 | 0.090 |
| 温度 0.6 | 0.817 | 0.154 | 0.029 |
温度从 1.0 降到 0.6,首选词概率从 66% 涨到 82%,长尾词被压到 3% 以下——更收敛,但仍有随机性。
“模型决定反思”是什么?是某一圈循环里,“Wait” 这个 id 的概率恰好被骰子掷中。没有 if 语句判断”我该反思了”,只有一个概率分布在那一步恰好给了 “Wait” 较高的值。上一篇说的”重构循环启动标志”,实现层就是这一次采样的结果。
“思考结束”是什么?是 </think> 被采样出来。没有定时器、没有外部控制器——什么时候停止思考,本身也是从分布里掷出来的。这就是为什么上一篇说 R1”没有完全学会何时停止思考”:停止时机只是个学出来的概率,不是个保证。
1.3 “思考的代价”记在哪:一笔可以手算的显存账
循环每转一圈,这个 token 的注意力键值(KV)要存进显存,后续每个 token 都要回看它——这就是 KV cache。“架构让长思考付得起账”(上一篇 2.0 节)兑换成数字:
V3/R1 的 MLA 把每个 token 每层的 KV 压成一个 512 维潜向量加一个 64 维位置键(config 里的 kv_lora_rank: 512 和 qk_rope_head_dim: 64),共 61 层,BF16 每个数 2 字节:
一条拉满 32,768 token 的思考链:。
作为对照,若用传统 128 头多头注意力(每头 128 维,K、V 各存一份):,同样 32k 就是约 131 GB——一张卡都装不下,是 MLA 的 57 倍。DeepSeek 官方数字正是 70KB/token。所以”长思考在经济上可行”不是修辞:它是 2.3 GB 和 131 GB 的区别。
二、训练侧:一次 GRPO 更新的完整数据流
2.1 “奖励函数”是十几行普通 Python,不是模型
上一篇说 R1-Zero 只用”规则奖励”,兑换成代码就是这种东西:
def reward(completion: str, ground_truth: str) -> float:
# 准确性奖励:从 \boxed{} 里抠出答案,和标准答案比对
m = re.search(r"\\boxed\{(.+?)\}", completion)
acc = 1.0 if m and math_equal(m.group(1), ground_truth) else 0.0
# 格式奖励:思考过程是否包在 think 标签里
fmt = 1.0 if re.fullmatch(r"<think>.+?</think>.+", completion,
re.DOTALL) else 0.0
return acc + fmt
没有神经网络裁判,没有对推理过程的任何评价——正则表达式加字符串比对。它看不见你写了多漂亮的推导,只看得见方框里的答案对不对、格式在不在。上一篇说”奖励只认对错”,指的就是这个函数的返回值只有那几种。
2.2 一个训练 step 的张量流水账
R1 的真实配置(Nature 版披露):每步 32 道题、每题采样 16 份答卷、单份最长 32,768 token。把每个中间量的形状列出来:
| 步骤 | 产物 | 形状 | 说明 |
|---|---|---|---|
| 取一批题 | prompts | 32 条字符串 | 数学/代码题,带标准答案 |
| 采样(跑第一节的循环 512 次) | completions | [512, ≤32768] | 32 题 × 16 份答卷 |
| 逐份打分(2.1 的函数) | rewards | [512] | 每份答卷一个标量 |
| 组内标准化 | advantages | [512] | 每 16 份一组算均值/标准差 |
| 前向重算对数概率 | logprobs | [512, T] | 每个 token 一个数 |
| 加权求和 | loss | 标量 | 一个数,backward 的起点 |
一步的信息量值得体感一下:512 份答卷,若平均每份几千 token,一次参数更新背后就是百万级 token 的梯度信号——而指引这一切的,只有 512 个由正则表达式算出来的标量。
2.3 手算一次更新(缩小到 G = 4)
把组缩小到 4 份答卷方便手算。某道题的 4 份答卷,奖励算出来是 (两份答对,两份答错):
- 组均值:;组标准差:;
- 优势:。
关键的一步来了:这个优势值被广播到整份答卷的每一个 token 上(结果监督下,一份答卷内所有 token 共享同一个优势)。答卷 1 有 3,000 个 token?这 3,000 个 token 的优势全是 +1。首次更新时新旧策略概率比 ,损失对参数的作用方向就是:
现在可以兑换上一篇最”玄”的那句话——“aha moment 是涌现的”:
没有任何一行代码点名表扬 “Wait”。它只是恰好频繁出现在答对的答卷里,于是随着一次次更新被”连坐”推高概率;概率高了,下一轮采样它出现得更频繁;带 “Wait” 的答卷如果继续更常答对,它就继续被推高。所谓”模型学会了反思”,在实现层只是这个正反馈的宏观读数。回答变长同理:长答卷更常答对 → 长答卷里的 token 更常被推高 → 分布向长答卷偏移。
再手算 KL 项。GRPO 用的无偏估计器(上一篇给过公式)对每个 token 算 :
- 若某 token 的新策略概率和参考模型差 1.2 倍:,惩罚几乎为零;
- 若偏到 2 倍:,惩罚是前者的 17 倍。
这就是”KL 约束防止策略跑到语言分布的荒郊野外”的具体含义:小偏移几乎免费,大偏移代价陡增——它不阻止学习,只阻止暴走。
2.4 循环长什么样,以及”这不是玩具”
把上面串起来,训练循环的骨架:
for step in range(num_steps):
prompts = sample_questions(32)
completions = model.generate(prompts, n=16, # 512 份答卷
max_tokens=32768)
rewards = [reward(c, gt) for c, gt in zip(...)] # [512],纯规则
adv = groupwise_normalize(rewards, group_size=16) # [512]
logp = model.logprobs(completions) # [512, T]
ratio = (logp - logp_old).exp()
loss = -torch.min(ratio * adv,
ratio.clamp(1-eps, 1+eps) * adv).mean() \
+ beta * kl_penalty(logp, logp_ref)
loss.backward()
optimizer.step() # 权重位移一小步
这个骨架不是我的示意图,它就是开源实现的真实接口:Hugging Face TRL 的 GRPOTrainer 里,reward_funcs 参数接收的就是 2.1 那样的普通 Python 函数,num_generations 就是组大小 。你今天就可以在一个 0.5B 的小模型上跑通同一套循环。
2.5 更新到哪了:没有”推理模块”,只有权重的位移
optimizer.step() 改的是和推理时同一套前向权重——没有单独的”思考模块”被训练出来。几千步更新的累积效果,就是第一节那个采样循环在每一步给出的概率分布变了:该 “Wait” 的地方 “Wait” 概率高了,该收束的地方 </think> 概率高了。
这也是上一篇机制层证据在实现层的自然对应:训练自始至终只是在原有参数空间里做小位移,所以”回溯”这样的行为最终表现为激活空间里一个可加减的线性方向、思维链里几个高因果权重的锚点句——行为没有藏在新长出来的结构里,它就摊在原来那套权重的位移里。
三、两个循环咬合:飞轮的实现层视图
flowchart LR
W["权重 θ"] -->|"采样循环 ×512 份"| C["512 份带思考的答卷"]
C -->|"正则 + 比对"| R["rewards [512]"]
R -->|"组内标准化"| A["advantages [512]"]
A -->|"clip 损失 + backward"| G["梯度"]
G -->|"optimizer.step()"| W
眼熟吗?这和 MCTS 篇第五节 AlphaGo Zero 的飞轮同构:那边是”搜索产出比网络直觉更强的走法分布,回头当训练目标”;这边是”采样 + 结果打分产出比当前分布更好的答卷排序,回头当梯度方向”。区别只在探索的载体——AlphaGo 用显式的树,R1 用 16 份平行草稿。
四、自检清单:“踏实”的检验标准
合上文章,能答出这五问,说明兑换完成:
- “Wait” 在什么意义上被奖励了?(没有代码点名它;它因频繁出现在答对的答卷里被整卷连坐推高)
- 温度 0.6 具体改了哪个数组的哪一步?改完分布怎么变?(logits 除以 0.6 再 softmax;0.665 → 0.817)
- 一条 32k 思考链占多少显存?怎么算?(70KB/token × 32768 ≈ 2.3GB;公式 字节)
- 同一份答卷里不同 token 的优势一样吗?正负号由什么决定?(结果监督下全卷共享;由这份答卷的奖励高于还是低于组均值决定)
- KL 项在防什么?偏移 1.2 倍和 2 倍的惩罚差多少?(防分布暴走;0.018 对 0.307,约 17 倍)
带走的模型
这篇的方法论比内容更值得带走:
叙事层的每个名词,都应该能兑换成一个张量形状、一行代码或一个能手算的数字;兑换不出来的名词,就是你还没懂的部分。“听懂了但不踏实”,病因几乎总是脑子里还留着几个没兑换的名词——把它们找出来,逐个兑换,踏实感就是兑换完成的回执。
参考来源
一手资料:
- DeepSeek-R1 GitHub README — 官方使用建议:温度 0.6、无 system prompt、强制
<think>\n开头、\boxed{}指令 - DeepSeek-V3 config.json — vocab_size 129280、61 层、kv_lora_rank 512 等全部架构参数
- DeepSeek-V2 论文 — MLA 的出处与 KV cache 压缩公式
- DeepSeekMath 论文 — GRPO 目标函数与 KL 估计器
- DeepSeek-R1 论文 / Nature 版 — 训练配置(每步 32 题 × 16 份 × 32768 token)
可上手的实现:
- TRL GRPOTrainer 文档 —
reward_funcs、num_generations等真实接口 - Transformers 的 DeepSeek-V3 配置源码
站内系列:
- 解剖 DeepSeek-R1 的”思考” — 本篇陪读的正主(叙事层)
- 零基础读懂蒙特卡洛树搜索 — 飞轮同构的另一半
- 示范教不会的东西:为什么大模型需要强化学习 — RL 概念打底