这一篇对应视频 07:“limits of RLVR,base vs. RL, pass@k, ppl 基于 vLLM 计算细节以及采样效率”(BV1pWSvBtEAk)。

我把它拆成三条主线:

  1. Base vs RL 的“能力”到底在对比什么:RLVR 更像分布削尖(distribution sharpening)还是能力外推(capability uplift)?
  2. 为什么一定要看 pass@k 而不只看 pass@1:以及怎么低方差地估算整条 pass@k 曲线。
  3. 怎么用 vLLM 可靠地算 PPL / entropy(评测细节):不踩坑地得到能解释现象的指标。

系列导航:

配套资料(你本地已有):


1. 先把“结论冲突”说清楚:Base 很强,为什么还要 RLVR?

在 math/coding 这种 verifiable reward 任务上(做对就是 1,做错就是 0,或者有一个可程序验证的 score),RLVR 的一个常见现象是:

  • RL 模型的 pass@1(只采样 1 次就做对的概率)会涨;
  • 但 Base 模型只要你肯多采样(例如 128/256/1024 次),pass@k 可能已经很高了;
  • 更糟的是,RL 训练经常会让输出分布更尖,多样性下降,一些“低概率但正确”的路径会被压没。

这就引出视频 07 的核心问题(也是你做 agentic RL / deep research 训练时必须面对的):

RLVR 到底是在“让模型更会推理”,还是只是把 Base 已经会的那一小撮正确轨迹的概率拉高,让它更容易一次采样命中?

如果你只看 pass@1,你会高估“能力提升”;如果你把 pass@k、PPL、entropy 一起看,你更容易看清它到底是在做什么。


2. pass@k:为什么它比 pass@1 更接近“能力上限”

2.1 pass@k 的定义(直觉)

pass@k 的语义很简单:

  • 你对同一道题采样 $k$ 次;
  • 只要这 $k$ 个答案里至少有一个是正确的,就算“这题被解决”。

这对 coding/math 的意义尤其大:Base 模型可能偶尔能走到正确解,但概率小。只要你愿意多采样,你会看到它的“潜力上限”。

形式化写法(论文里常见写法):

$$\text{pass}@k := \mathbb{E}\left[1 - \frac{\binom{n-c}{k}}{\binom{n}{k}}\right]$$

其中:

  • $n$ 是你为该题采样的总次数(例如 128/256/1024);
  • $c$ 是其中正确的样本数;
  • $\frac{\binom{n-c}{k}}{\binom{n}{k}}$ 是“从 $n$ 个里抽 $k$ 个,抽到的全是错的”的概率;
  • 所以 $1-\cdot$ 就是“至少有一个对”的概率。

2.2 为什么论文爱固定 $n$,再算一条完整的 pass@k 曲线

工程上你往往会固定一个比较大的 $n$(例如 1024),然后对所有 $k<n$ 估算出整条 pass@k 曲线:

  • 这样你能比较“采样次数增加”带来的收益;
  • 也能更清楚地对比 Base vs RL:RL 是否真的让曲线整体上移,还是只让小 $k$ 的点变好。

2.3 数值实现:别直接算组合数(会溢出)

rlvr.ipynb 里给了两个实现,组合数版在大 $n$ 时会数值爆炸,工程里更常用乘积版:

1
2
3
4
5
6
7
8
9
10
11
12
import numpy as np
import math

def pass_at_k(n, c, k):
if n - c < k:
return 1.0
return 1.0 - np.prod(1.0 - k / np.arange(n - c + 1, n + 1))

def pass_at_k_comb(n, c, k):
if n - c < k:
return 1.0
return 1 - math.comb(n - c, k) / math.comb(n, k)

这里的一个关键边界条件:

  • 如果错的样本数 $n-c < k$,你抽 $k$ 个一定会抽到至少一个正确样本,所以 pass@k 直接是 1。

2.4 pass@k 的“隐藏前提”:采样相关性会让你误判潜力上限

pass@k 很强,但它也非常容易被“采样细节”影响,导致你误把“采样策略变了”当成“模型能力变了”。你至少要注意两点:

  1. 样本相关性(correlation)
    • 同一 prompt 下的多次采样并不是独立同分布的“理想抽样”,尤其当你用的是低温度、强约束(top-p 很小、强 repetition penalty)或带模板的生成。
    • 相关性越强,你的“多采样潜力”会被高估或低估,曲线的形状也会变得更难解释。
  2. pass@k 对解码超参极度敏感
    • 同一个 Base 模型,temperature/top_p/top_k 一改,pass@k 曲线可能整体平移。
    • 所以对比 Base vs RL 时,除了报告 pass@k,你还应该固定并报告解码策略(最好把 Base 和 RL 放在同一组解码设置下对比)。

我建议你把 pass@k 当成“系统行为”的指标,而不是纯粹“模型静态能力”的指标:它反映的是 模型分布 + 解码策略 + 评测器 的合成效果。


3. Sampling Efficiency Gap:把“潜力上限”和“单次命中率”放到同一个数里

视频 07 里另一个很关键的指标是采样效率差距(Sampling Efficiency Gap):

$$\Delta_{\text{SE}} = \text{pass}@k_{\text{Base}} - \text{pass}@1_{\text{RL}}$$

直觉上它在衡量:

  • Base 模型“只靠多采样”能到哪(上限);
  • RL 模型“只采 1 次”离这个上限还有多远(效率)。

你可以把它理解成一种很现实的 tradeoff:

  • RL 的意义很多时候不是“发明新能力”,而是把 Base 的潜力变成可用的采样效率(从 256 次变成 1 次)。
  • 但如果 $\Delta_{\text{SE}}$ 仍然很大,说明你当前 RL 算法(PPO/GRPO/变体)仍然没把 Base 的潜力吃干榨净。

这对 agentic RL 非常重要:你做 deep research agent 时,很多能力也可能“Base 偶尔能做到”,但成本太高。你的目标往往是把它变成稳定可用的行为,而不是在 benchmark 上秀 pass@1。


4. 用 PPL 识别“分布削尖”还是“边界拓展”

4.1 Base 对 RL 输出的困惑度:RL 是否逃逸出 Base 分布?

rlvr.ipynb 里写了一个非常有用的诊断:

  • 固定 Base 模型 $P_{\text{Base}}$;
  • 让 RL 模型生成答案 $Y_{\text{RL}}$;
  • 计算 Base 模型对这些 token 的负对数似然,并转成 perplexity。

$$\text{PPL}_{\text{Base}}(Y_{\text{RL}}\mid x)=\exp\left(-\frac{1}{T}\sum_{t=1}^{T}\log P_{\text{Base}}(y_t\mid x,y_{<t})\right)$$

判读要点:

  • 如果 $\text{PPL}{\text{Base}}(Y{\text{RL}}\mid x)$ 很低,说明 RL 输出在 Base 看来“很合理”,更像是在 Base 分布内部做 reweighting(削尖)。
  • 如果它明显升高,说明 RL 输出更像“跑出了 Base 的高概率区域”,有边界拓展的迹象(但也可能是胡言乱语,需要结合 correctness)。

4.2 distillation 为什么可能更“扩边界”

视频与笔记里对比了 distillation:如果你引入 teacher(例如强推理模型)的输出再蒸馏,学生模型的推理边界可能会真的被推开。

这给你的启示是:

  • 如果你期待“深研究 agent 真的学会新东西”,只靠 RLVR 可能不够;
  • 你可能需要 distill / curriculum / tool-augmented data,让“新轨迹”进入可学习分布,然后再用 RL 提升单次命中率与稳定性。

4.3 PPL 的误读风险:风格/模板变化也会让 PPL 变大

用 $PPL_{\text{Base}}(Y_{\text{RL}})$ 做分布诊断很有用,但它也有一个常见误区:PPL 变大不一定代表“更强/更弱”,很多时候只是“写法变了”:

  1. RL 改了输出风格(更啰嗦/更简洁/更多模板化 token),Base 当然会困惑。
  2. prompt 模板、system prompt 或工具输出拼接方式不同,token 边界变化会直接影响 PPL。
  3. Base 可能对“低概率但正确”的解法本来就不自信,正确解也可能对应高 PPL。

所以我更建议把它当成 “分布漂移报警器”:PPL 明显上升时,你应该做两件事:

  1. 人工抽样看输出是否变怪(格式、重复、幻觉、答非所问)。
  2. 联合 correctness/pass@k/entropy 一起判断:是“跑偏”还是“拓展到罕见但正确的区域”。

5. vLLM 评测细节:如何可靠地算 PPL / token entropy

视频 07 专门讲了一个“很工程但决定你指标是否可信”的点:在 vLLM 里算 logprob 的方式

如果你发现自己还在 vLLM 的 OOM/吞吐低/ITL 差 里挣扎,建议先把推理引擎的参数体系搞清楚(否则评测脚本可能根本跑不起来):

5.1 核心思路:把 (prompt + response) 当成 prompt,取 prompt_logprobs

vLLM 的 LLM.generate 支持:

  • prompt_logprobs=K:返回输入文本每个位置的 Top-K token logprob;
  • max_tokens=1:只 forward,不真的生成(评测模式)。

因此你可以把完整文本拼起来:

1
2
3
full_texts = [p + r for p, r in zip(prompts, responses)]
eval_params = SamplingParams(prompt_logprobs=20, max_tokens=1, temperature=1.0)
outputs = llm.generate(full_texts, eval_params)

然后关键是做 masking:

  • 你只想算 response 的 PPL,不想把 prompt 的 logprob 算进去;
  • 所以需要找到 response 在 token 序列里的起始 index(start_idx)。

rlvr.ipynb 的做法是:重新 tokenize prompt,取其长度作为 start_idx

5.2 最容易踩的坑:Top-K 太小,actual token 不在返回字典里

vLLM 返回的 output.prompt_logprobs 是一个 list[dict]

  • 每个位置一个 dict;
  • dict 里是 Top-K 的 {token_id: Logprob}

如果你把 prompt_logprobs 设成 1,那么它只返回 top-1 token 的 logprob。

但你要算 PPL,需要的是“实际发生的那个 token”的 logprob;如果实际 token 不是 top-1,那么你在字典里根本找不到它。

因此 notebook 里强调:prompt_logprobs 应该设大一些(vLLM 目前上限通常是 20)。

下面是 notebook 里更完整的 PPL + entropy 计算逻辑(我把注释精简成“你真正要记住的”):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def calculate_metrics(llm, prompts, responses, top_k_for_entropy=20):
full_texts = [p + r for p, r in zip(prompts, responses)]
eval_params = SamplingParams(prompt_logprobs=top_k_for_entropy, max_tokens=1, temperature=1.0)
outputs = llm.generate(full_texts, eval_params)

ppl_scores, entropy_scores = [], []
tokenizer = llm.get_tokenizer()

for i, output in enumerate(outputs):
start_idx = len(tokenizer.encode(prompts[i], add_special_tokens=False))
token_logprobs = output.prompt_logprobs

if start_idx >= len(token_logprobs):
ppl_scores.append(float('nan'))
entropy_scores.append(float('nan'))
continue

response_log_probs = []
step_entropies = []

for step, logprob_dict in enumerate(token_logprobs[start_idx:], start=start_idx):
if logprob_dict is None:
continue

actual_token_id = output.prompt_token_ids[step]
if actual_token_id not in logprob_dict:
# Top-K 截断导致缺失:这时你的 PPL/entropy 都不可信,必须提升 K 或换计算方式
continue

response_log_probs.append(logprob_dict[actual_token_id].logprob)

# 近似 token entropy(只用 Top-K)
step_logps = np.array([lp.logprob for lp in logprob_dict.values()])
step_ps = np.exp(step_logps)
step_entropies.append(-np.sum(step_ps * step_logps))

ppl_scores.append(np.exp(-np.mean(response_log_probs)) if response_log_probs else float('nan'))
entropy_scores.append(np.mean(step_entropies) if step_entropies else float('nan'))

return ppl_scores, entropy_scores

你应该从这段代码里带走 3 个“评测原则”:

  1. prompt 与 response 的 token 边界必须对齐(同一个 tokenizer,同一个模板)。
  2. Top-K 截断会让 PPL/entropy 失真:K 太小就别算,或至少要在 log 里显式标记为 NaN/跳过。
  3. entropy 只是 Top-K 近似,但对“分布变尖/多样性下降”的趋势判断很有用。

5.3 prompt 模板要和训练一致,否则指标不对

rlvr.ipynb 里特意强调:不要直接用默认 chat template,而是要用和训练一致的模板拼 prompt。否则:

  • token 边界变了;
  • 你算的 PPL / pass@k / entropy 全都不可比。

它用的是一个手写的 Qwen 风格模板(system + user + assistant),这在 LLM-RL 评测里非常常见。


6. 对你做 Agentic RL / Deep Research 的启示

如果你想用 agentic RL 做 deep research,你需要一开始就想清楚“你究竟想优化什么”,以及“你愿意牺牲什么”:

  • 只要你用的是 verifiable reward(可验证正确/错误),你大概率会遇到同样的问题:RL 把分布削尖,提升单次命中率,但可能降低探索与多样性。
  • deep research 的难点往往不是“写出一个正确答案”,而是“找到正确问题、覆盖足够证据、做出可信引用、控制成本”,它更像一个需要探索的搜索问题。

因此你至少要准备一个“反削尖”的评测闭环:

  • 主指标:任务成功率(含引用/事实一致性/覆盖)。
  • 稳定性指标:KL、entropy、clip fraction(你前面几篇博客讲过)。
  • 潜力上限:Base 的 pass@k 曲线(以及 RL 的 $\Delta_{\text{SE}}$)。
  • 分布诊断:$\text{PPL}{\text{Base}}(Y{\text{RL}}\mid x)$,看是否真的出现“边界拓展”。

如果你后续想把这一套真正落到“可复现的训练 pipeline”,下一步建议从 verl 的 rollout + reward manager + objective 开始,把训练日志里的这些量都打通(我可以继续按代码结构拆给你)。