【Agentic RL / 强化学习 / OPD】OpenClaw-RL 源码阅读笔记 --- (14)--- Teacher

📅 2026/7/29 21:13:50 👁️ 阅读次数 📝 编程学习
【Agentic RL / 强化学习 / OPD】OpenClaw-RL 源码阅读笔记 --- (14)--- Teacher

【Agentic RL / 强化学习 / OPD】OpenClaw-RL 源码阅读笔记 --- (14)--- Teacher

目录
  • 【Agentic RL / 强化学习 / OPD】OpenClaw-RL 源码阅读笔记 --- (14)--- Teacher
    • 0x00 概要
    • 0x01 基础背景
      • 1.1 完整数据流
      • 1.2 关键点
        • 公式
        • 代码与文件
    • 0x02 教师模型
      • 2.1 具体是什么模型?
      • 2.2 教师模型的架构特性
      • 2.3 教师模型的工作机制
      • 2.4 教师模型的具体应用场景
      • 2.5 教师模型API服务器
        • 关键代码结构
        • 核心实现
        • 双重SGLang架构
      • 2.6 流程
    • 0x03 Teacher log-probs 计算流程
      • 3.1 ① 构建带hint的输入序列
      • 3.2 ② Teacher前向计算
        • prompt
        • 如何让 Teacher 返回 log-probs(不生成新 token)
        • SGLang 返回什么
      • 3.3 ③ 解析 logprobs
      • 3.4 ④ Top-k 变体
      • 3.5 ⑤ 存入Sample
      • 3.6 ⑥ 汇总对比
      • 3.7 完整代码
        • _compute_teacher_log_probs()
        • _compute_teacher_topk_logprobs()
    • 0x04 logprobs分析
      • 4.1 学生(rollout/response)token logprobs
        • 数学符号解释
        • 关键信息
          • 数据收集阶段
          • 训练使用阶段
        • 与其他logps的区别
        • 结论
      • 4.2 教师 token logprobs(单token 路径)
      • 4.3 教师 top-k logprobs(Top-K分布路径)
      • 4.4 Reference logprobs(ref_log_probs)
      • 4.5 训练/前向产生的log_probs(trainer内部)
      • 4.6 元数据/输入输出相关的 logprobs字段(辅助字段)
      • 4.7 关键要点
        • 存储字段&代码中的映射(谁写入、谁读出)
        • 关键实现文件
        • 实践注意事项(代码层)
    • 0x05 Top-K 解读
      • 5.1 为什么需要 Top-K 蒸馏?
      • 5.2 Top-K 蒸馏在 OpenClaw 中的位置
      • 5.3 核心机制:Tail Trick + Reverse KL
        • 5.3.1 问题:15 万词表 vs 50 个 top-K
        • 5.3.2 Tail 的数值计算
        • 5.3.3 Reverse KL 散度
      • 5.4 完整数据流:从 API Server 到 Loss
      • 5.5 关键设计选择:Teacher Top-K vs Student Top-K
        • 5.5.1 为什么选 Teacher Top-K?
        • 5.5.2 Student Top-K 的优势(OpenClaw 放弃的)
      • 5.6 数值风险
      • 5.7 两种 OPD 路径对比总表
      • 5.8 常见问题
    • 0xFF 参考

0x00 概要

本系列的目的是:借着对 OpenClaw-RL 源码的学习,来梳理强化学习的一些相关概念和思想。所以,会有一些基础知识、扩展和发散,OpenClaw-RL 只是一个切入点。而且,因为整篇系列是一个整体,所以有些概念的解读/学习会在不同的文章中出现,还请大家谅解。

OpenClaw-RL 是一个用于在线强化学习(Online RL)的框架,专门针对智能体工具使用场景。它通过从环境反馈中提取过程奖励信号来训练语言模型,支持三种主要模式:

  • openclaw-rl:基于二元奖励的强化学习(Binary RL / GRPO)
  • openclaw-opd:基于后见之明提示的在线策略蒸馏(On-Policy Distillation, OPD)
  • openclaw-combine:联合方法,在同一 PPO 更新中同时利用 RL reward 和 OPD teacher signal

frameworkOPD的流程如下:

[Rollout/Agent] ──生成样本──► [Data Buffer]│                           ▲▼                           │
[Judge/Hint/PRM] ──奖励/hint───────┤│                           │▼                           │
[Teacher Model] ──log-probs────────┤│                           │▼                           │
[Trainer] ◄───读取样本/训练──────────┘│▼
[参数同步] ──更新权重──► [Rollout/Agent]

我们今天要看看其 Teacher 部分。教师模型是OpenClaw-RL项目中实现高效在线蒸馏学习的关键组件,它通过将延迟反馈转化为即时的token级别监督信号,显著提升了学习效率和模型性能。其设计充分考虑了实际部署的需求,在保证性能的同时兼顾了资源利用效率和系统稳定性。

0x01 基础背景

1.1 完整数据流

OpenClaw-OPD 完整数据流如下:

14-完整数据流

具体参见下表

流向 说明
Rollout/Agent → Judge/Hint/PRM 生成数据送评判
Judge/Hint/PRM → Teacher Model 提取 hint 后送教师模型
Judge/Hint/PRM → Data Buffer 奖励/hint 信息送入缓冲区
Teacher Model → Data Buffer teacher 信号送入缓冲区
Rollout/Agent → Trainer 学生模型参与训练
Data Buffer → Trainer 缓冲数据送入优化器
Teacher Model → Trainer 教师信号用于损失计算
Trainer → 参数同步 训练完成,更新参数
参数同步 → Rollout/Agent 同步后模型用于下一轮采样

1.2 关键点

公式

优势计算:  A_t = teacher_logp - student_logp(即教师模型与学生模型的对数概率之差)

代码与文件

  • openclaw_opd_api_server.py:消息规范、hint提取、Sample 组装的入口逻辑。
  • README.md:OPD 的算法说明、Top-K选项与公式。
  • run_qwen3_4b_openclaw_opd.sh:启动参数、HF_CKPT、并发设置OPENCLAW_OPD_TEACHER_LP_MAX_CONCURRENCY。
  • openclaw_combine_api_server.py:展示了如何把 teacher_log_probs 写到 Sample(padding/cropping)。
  • README.md:Data Buffer、rollout 与 trainer 的模块级别职责说明。
  • slime/utils/types.py(样本类型定义,可查看 Sample 字段结构)。

0x02 教师模型

教师模型的设计使得OPD能够获得比普通 RL更丰富、更细粒度的监督信号,从而实现更快的收敛速度和更好的最终性能。

openclawrl1performance

2.1 具体是什么模型?

在OpenClaw-RL框架中,教师模型是一个具有双重角色的核心组件,主要用于On-Policy Distillation(OPD)训练方法。 教师模型本质上是一个高质量的参考模型,用于从延迟的用户反馈中提取密集的监督信号,从而指导学生策略模型的学习。

OpenClaw-RL 中的“教师模型"主要指在 On-Policy Distillation (OPD)阶段用于为学生模型提供 token-level 指导信号的模型。根据训练脚本和文档,教师模型默认是 Qwen3-4B(Qwen3-4B-Thinking-2507),即通过 qwen3-4B.sh 配置和HF_CKPT 路径指定的 HuggingFace Qwen3-4B 权重。

简要总结:

  • 数师模型 = Qwen3-4B(Qwen3-4B-Thinking-2507)
  • 用于 OPD 阶段,提供 token 级别的 log-prob指导信号

2.2 教师模型的架构特性

双重角色统一端点

  • 使用同一个SGLang服务端点同时承担PRM评估(提示提取)和教师模型(对数概率计算)双重功能
  • 通过不同的请求参数来区分具体功能,实现资源的有效复用

模型配置特点

  • 默认情况下使用与学生策略相同的基座模型(如Qwen3-4B系列)
  • 运行时不加载LoRA适配器,确保提供稳定、高质量的监督信号
  • 支持通过环境变量或命令行参数单独指定不同的教师模型

2.3 教师模型的工作机制

确定性推理配置

  • 使用temperature=0.0确保推理结果的一致性

  • 设置max_new_tokens=0,只计算已有 token 的 logprobs,不生成新内容

  • 这种配置保证了在相同输入下始终产生相同的输出分布

并发控制机制

  • 通过_teacher_lp_semaphore信号量限制最大并发查询数量

  • 默认并发数由环境变量OPENCLAW_OPD_TEACHER_LP_MAX_CONCURRENCY控制

  • 防止教师模型服务过载,确保系统稳定性

增强Prompt构建流程

  • 首先从用户反馈中提取有效的“hint”(提示)

  • 将提取的提示注入到原始对话历史中,构建增强的prompt

  • 基于这个增强prompt查询教师模型的对数概率分布

2.4 教师模型的具体应用场景

OPD训练流程中的作用

原始对话 → 学生响应 → 用户反馈 → 提示提取 → 增强prompt → 教师logprobs → 蒸馏训练 

与传统RL的区别

  • 传统RL:使用标量奖励(+1/-1/0),提供稀疏的试错信号

  • OPD教师模型:提供token级别的对数概率分布,提供密集的监督信号

资源分配

  • 在多GPU部署中,教师模型通常分配专门的GPU资源(通过PRM_GPUS参数配置)
  • 与学生策略模型和rollout收集器并行运行,形成完整的异步训练流水线

2.5 教师模型API服务器

  • openclaw-opd/openclaw_opd_api_server.py:包含完整的教师模型查询逻辑
    • 教师模型端点配置(_prm_url变量)
    • 并发控制机制(_teacher_lp_semaphore信号量)
    • 确定性推理参数设置
    • 增强prompt构建和logprobs计算

关键代码结构

关键代码结构如下

OpenClaw-OPD API Server (根节点)├─ 初始化阶段 (分支)│   ├─ PRM/教师模型URL配置 (叶子)│   ├─ 并发信号量初始化 (叶子)│   └─ Tokenizer加载 (叶子)│├─ 评估阶段 (分支)│   ├─ 提示提取 (hint extraction) (叶子)│   ├─ 增强prompt构建 (叶子)│   └─ 教师logprobs查询 (叶子)│└─ 提交阶段 (分支)├─ 样本格式化 (叶子)└─ 训练队列提交 (叶子)

核心实现

OPD的核心实现在openclaw-opd/目录下,主要包含三个关键文件:

  • openclaw_opd_api_server.py:核心业务逻辑,包含提示提取、教师查询、双重评估
    • 提示提取评估 (_build_hint_judge_messages())
    • 教师模型查询(_append_hint_to_messages())
    • 双重评估机制(提示提取+PRM评估)样本构建和队列提交
  • openclaw_opd_rollout.py:与Slime训练框架的集成接口
    • rollout 函数,
    • generate_rollout_openclaw_opd() 函数
    • 与AsyncRolloutworker 的集成接口
  • topk_distillation_loss.py:
    • Top-K蒸馏损失函数实现
    • 支持Top-K logits蒸馏的损失计算
    • 教师一学生对数概率差计算

双重SGLang架构

OPD使用同一个PRM URL同时承担双重角色:

  • 资源复用:同一个 SGLang 实例承担双重角色
  • PRM评估角色:执行提示提取和PRM评分
  • 教师模型角色:执行教师对数概率计算
  • 不同payload:通过不同的请求参数区分用途

2.6 流程

OPD 实现中各阶段的函数调用链如下(2.4 节给出了概念流程,这里给出具体的代码调用链):

用户请求 → OpenClawOPDAPIServer._handle_main_turn()↓ (SGLang 推理)学生模型生成响应 + logprobs↓ (状态缓冲)缓存当前回合数据↓ (下一状态触发)新请求到达 → 触发 _fire_opd_task()↓ (异步任务)_opd_evaluate() → 提示提取评估↓ (成功提取提示)构建增强 prompt(注入提示)↓ (教师查询)_compute_teacher_log_probs() → 调用 PRM/教师 SGLang↓ (样本构建)构建包含 teacher_log_probs 的 Sample↓ (队列提交)output_queue.put() → Slime 训练器

我们以 _compute_teacher_log_probs 为基础,来看看 Teacher log-probs 计算流程。为了完整叙事,此处也会把前面完整流程的某些其它部分带入。

0x03 Teacher log-probs 计算流程

Teacher forward pass 的关键:

  • hint 注入最后一个 user 消息;
  • max_new_tokens=0 不生成,只算log-probs;
  • “logprob_start_len只取 response 部分。SGLang被当成"log-prob 计算器"。

触发条件:

  • Hint Judge 接受了 hint(select_best_hint返回非None)。

实现细节:

  • 构建完整文本:enhanced_full_text =enhanced_prompt_text+ turn_data["response_text"]

  • Token化:enhanced_ids = self.tokenizer(enhanced_full_text,add_special_tokens=False)["input_ids"]

  • 并发控制:async with self._teacher_lp_semaphore:

  • _查询教师模型:通过HTTP请求到PRM/教师端点获取log probabilities

  • Top-K蒸馏扩展:

    • 条件判断:if self._use_topk_distillation:
    • 调用:topk_lp,topk_idx = await self._compute_teacher_topk_logprobs(enhanced_ids,response_len)
    • 存储单独的top-k字段

我们接下来看看具体细节。

3.1 ① 构建带hint的输入序列

核心方法:_opd_evaluate

具体实现步骤:

  • 构建评估消息:_build_hint_judge_messages(turn_data["response_text"],next_state_text,next_state_role)
  • 应用tokenizer模板:self._prm_tokenizer.apply_chat_template()
  • 并行执行M次投票:asyncio.gather(*[self._query_judge_once(judge_prompt, i) for i in range(self._prm_m)])
  • PRM 评估提示构建:_build_prm_eval_prompt()
  • PRM 多数投票:prm_eval_majority_vote()
  • PRM 评分解析:_parse_prm_eval_score()
  • 解析提示提取结果:_parse_judge_result()
  • 选择最佳提示:selected =_select_best_hint(votes)

投票逻辑:

  • 每次投票返回评分(+1/-1)和可选的hint文本
  • 选择最长的有效正面提示
  • 如果没有有效提示,则丢弃该样本

关键函数:

  • _append_hint_to_messages:将提取的hint添加到原始对话末尾
  • normalize_messages_for_template:标准化消息格式
  • apply_chat_template:应用tokenizer的聊天模板

具体逻辑如下:

==========================================================================
【步骤一】构建带hint的输入序列
==========================================================================代码实现:hint = selected["hint"].strip()enhanced_messages = _append_hint_to_messages(turn_data["messages"], hint)norm_enhanced = _normalize_messages_for_template(enhanced_messages)enhanced_prompt_text = self.tokenizer.apply_chat_template(norm_enhanced,tools=turn_data.get("tools"),tokenize=False,add_generation_prompt=True,)原始messages (无hint):
[system:,..., user:"帮我写排序",assistant:"...",user:"能优化吗"]hint 注入  (_append_hint_to_messages):找到messages中"最后一条user消息" (从后往前遍历)在其content末尾追加:"\n\n[user's hint/ instruction]\n{hint.strip()}" ↓
enhanced_messages:
[system:,..., user:"帮我写排序\n\n[user'shint/instruction]\n使用快排,...,]apply_chat_template(enhanced_messages)→ enhanced_prompt_text 
enhanced_full_text = enhanced_prompt_text + response_text (原始response)
enhanced_ids=tokenizer(enhanced_full_text)["input_ids"]||[prompt_with_hint_tokens ... response_tokens]| ←-----start_len-------→    ←-response_len--→ |(prompt部分,含hint)▼

3.2 ② Teacher前向计算

==========================================================================
【步骤二】Teacher前向计算 (max_new_tokens=0)
==========================================================================并发控制:Semaphore (max_concurrency=3) (OPENCLAW_OPD_TEACHER_LP_MAX_CoNCURRENCY) POST→PRM Engine  (GPU 6-7)/generate
┌─────────────────────────────────────────────────────────┐ 
│ payload:                                                │ 
│     "input_ids":enhanced_ids (prompt_with_hint+ response│ 
│     "sampling_params":{"temperature":0.0,               │ 
│     "max_new_tokens":0  ←- 纯前向,不生成任何token         │ 
│     }                                                   │ 
│     "return_logprob":True                               │ 
│     "logprob_start_len":start_len                       │ 
│         = max(0,len(enhanced_ids) -response_len)        │ 
│         ←-只返回 response 位置的 logprobs,节省带宽         │ 
└─────────────────────────────────────────────────────────┘ 返回:`meta_info["input_token_logprobs"]:[ (lp_0,tok_0), (1p_1,tok_1),..., (1p_T,tok_T)](start_len之后的位置)

prompt

Teacher 用的是用户对话中原本就有的 system prompt(如果有的话)。即,Teacher 不添加任何额外 system prompt。它看到的完全是用户的原始对话+hint 注入到最后一个 user 消息。

对比如下:

对象 System Prompt
用户对话 用户自己设的(或App默认的)
Teacher forward pass 和用户对话相同(原封不动保留)
PRM/Hint Judge 独立的评分system prompt
训练sample 和用户对话相同(sample.tokens来自原始messages)

如何让 Teacher 返回 log-probs(不生成新 token)

关键参数:

payload = {"input_ids": enhanced_ids,            # ← 完整序列 (prompt + response)"sampling_params": {"temperature": 0.0,               # ← 不重要,因为不生成,不影响forward pass"max_new_tokens": 0,              # ← 🔑 关键!不生成任何新 token,只做forward pass"skip_special_tokens": False,},"return_logprob": True,               # ← 🔑 返回 log-probs"logprob_start_len": start_len,       # ← 🔑 只返回 response 部分的 log-probs"top_logprobs_num":K,                 # ← 🔑 返回每位置top-K个token的logprob
}

核心技巧:max_new_tokens=0把SGLang当成"log-prob计算器“而非"文本生成器"。max_new_tokens=0 的含义如下:

正常生成(max_new_tokens > 0):输入 [prompt] → 模型生成 [token1, token2, ...] → 返回生成的 tokensTeacher forward pass(max_new_tokens = 0):输入 [prompt + response] → 模型只做 forward pass → 不生成任何 token但计算了每个位置的 log P(token_t | context_<t) → 返回这些 log-probs

logprob_start_len 的含义:

enhanced_ids = [p1, p2, ..., pN, r1, r2, ..., rT]└──── prompt ────┘ └─ response ─┘start_len = len(enhanced_ids) - response_len = N→ 只返回位置 N 之后的 log-probs(即 response 部分)
→ 省掉了 prompt 部分的 log-probs(我们不需要)

SGLang 返回什么

{"meta_info": {"input_token_logprobs": [null, // 位置 0 (第一个 token 无条件概率)[-2.3, 15234], // 位置 1: logprob=-2.3, token_id=15234[-1.5, 8901], // 位置 2: logprob=-1.5[-0.8, 3456], // ......]}
}

3.3 ③ 解析 logprobs

==========================================================================
【步骤三】解析 logprobs - 对齐到response tokens
==========================================================================all_lp=[item[o] for item in input_token_logprobs] 注意:SGLang返回的input_token_logprobs 包含一个偏移: all_lp=all_lp[1:] ← 去掉第一个 (位置偏移修正)对齐到response_len:对齐到response_len:len(all_lp) >= response_len:teacher_log_probs=all_lp[-response_len:] ← 取末尾T个 len(all_lp)<response_len:teacher_log_probs=[0.0]*(response_len-len(all_lp))+ all_lp ← 前补零最终:teacher_log_probs shape=[T]log π_teacher(r_t| prompt_with_hint,r_1..r_{t-1}) for t in 1..T

解析 log-probs 的代码如下:

# 解析 SGLang 返回的 log-probs
all_lp = []
for item in inp:if isinstance (item, (list, tuple)) and len (item) >= 1:val = item [0] # 取第一个元素 = log-proball_lp.append (float (val))elif isinstance (item, dict) and "logprob" in item:val = item ["logprob"] # 或 dict 格式all_lp.append (float (val))all_lp = all_lp [1:] # 跳过位置 0 (BOS)
return all_lp [-response_len:] # 只取最后 response_len 个

3.4 ④ Top-k 变体

当distill_topk>0时(如K=5),会进行 top-k计算,不只记录teacher对当前token的log-prob,而是记录每个位置teacher最高K个token的分布。

==========================================================================	
【Top-k 变体】(_compute_teacher_topk_logprobs,--distill-topk K) 
==========================================================================# teacher_topk_log_probs:[T,K]←.T个位置,每个位置top-K个log-prob
# teacher_topk_indices:[T,K]对应的 token ID
# Loss: reverse KL over K+1 bins(top-K+ tail mass)    async def _compute_teacher_log_probs(self, input_ids, response_len):# 关键: max_new_tokens=0 -> 不生成, 只前向传播payload = {"input_ids": input_ids,    # 增强prompt + 原始response的token ids"sampling_params": {"temperature": 0.0,"max_new_tokens": 0,   # <- 只计算 log-probs, 不采样},"return_logprob": True,"logprob_start_len": start_len,  # 只返回 response 部分的 log-probs}# 返回: teacher 在看到 hint 后, 对原始 response 每个 token 的 log-prob此函数表达的语义: 如果 teacher 在生成时拥有这个 hint (事后诸葛亮信息), 它对这段 response 的每个 token 的置信度是多少?                      额外参数:"top_logprobs_num":K → SGLang 返回每个位置top-K候选meta_info["input_top_logprobs"]:
[[(1p,tok_id),(lp,tok_id),...xk], ← position 0
[(lp,tok_id),...xk],                ← position 1
...                                   ← position T ]同样[1:]偏移修正 + 对齐response_len: teacher_topk_log_probs  shape=[T,K]teacher_topk_indices    shape=[T,K]用途:SDFT/SDPO风格蒸馏student在每个位置学习对top-Kteacher候选的概率分布而非仅对 response 中实际 token 的单点 log-prob

3.5 ⑤ 存入Sample

此部分完成样本提交:_submit_turn_sample方法

数据处理(解析返回):

  • 对齐teacher_log_probs和response_ids长度

  • 处理长度不匹配的情况(截断或填充)

  • 构建完整的训练样本包含:

    • prompt_ids
    • response_ids
    • teacher_log_probs
    • rollout_log_probs
response.meta_info.input_token_logprobs → 解析 → teacher_log_probs
teacher_log_probs =[-0.9, -0.6,-1.8,-1.0,...]↑每个 response token 的log P(token |enhanced_prompt + 前文)

存入Sample / 队列提交:

  • 通过self.output_queue.put()将样本提交到训练队列
  • 训练系统从队列中消费样本进行蒸馏训练
sample.teacher_log_probs = tensor(teacher_log_probs) 
→ 送给Slime计算OPD advantage

3.6 ⑥ 汇总对比

【汇总对比】三种 log-prob 的来源与用途
+------------------------+--------------------+-----------------------------------+
| log-prob 类型          | 计算时机            | 含义                               |
+------------------------+--------------------+-----------------------------------+
| rollout_log_probs      | SGLang 生成时       | log π_old(r|prompt)              |
| (old policy)           | 一次性获得          | 不含 hint, 原始对话上下文            |
+------------------------+--------------------+-----------------------------------+
| teacher_log_probs      | next_state 到来后   | log π_teacher(r|prompt+hint)     |
| (teacher forward pass) | 异步计算            | 含 hint, Teacher 怎么看该response  |
+------------------------+--------------------+-----------------------------------+
| new_log_probs          | Megatron 训练前向   | log π_new(r|prompt)               |
| (current policy)       | 每个 gradient step  | 训练中当前策略                      |
+------------------------+--------------------+-----------------------------------+OPD advantage per token t:Δ_t = teacher_log_probs[t] - rollout_log_probs[t]= log π_teacher(r_t | prompt+hint) - log π_old(r_t | prompt)直觉: hint 告诉 teacher "这里应该怎么做"teacher 给 response 中每个 token 打分越高 → 该 token 越值得被学习Δ_t > 0 → 强化该 token; Δ_t < 0 → 抑制该 token     

3.7 完整代码

3.2-3.4 节给出了关键参数和解析逻辑的片段,这里给出两个核心函数的完整实现,便于对照。

_compute_teacher_log_probs()

async def _compute_teacher_log_probs(self, input_ids: list[int], response_len: int) -> list[float]:start_len = max(0, len(input_ids) - response_len)payload = {"input_ids": input_ids,"sampling_params": {"temperature": 0.0,           # 确定性推理"max_new_tokens": 0,          # 不生成新 token"skip_special_tokens": False,},"return_logprob": True,"logprob_start_len": start_len,   # 从响应开始位置计算}async with self._teacher_lp_semaphore:  # 并发控制(信号量)async with httpx.AsyncClient(timeout=None) as client:resp = await client.post(self._prm_url, json=payload)resp.raise_for_status()result = resp.json()meta = result.get("meta_info", {}) if isinstance(result, dict) else {}inp = meta.get("input_token_logprobs")if not isinstance(inp, list):return [0.0] * response_lenall_lp = []for item in inp:if isinstance(item, (list, tuple)) and len(item) >= 1:val = item[0]all_lp.append(float(val) if val is not None else 0.0)elif isinstance(item, dict) and "logprob" in item:val = item["logprob"]all_lp.append(float(val) if val is not None else 0.0)else:all_lp.append(0.0)if len(all_lp) > 1:all_lp = all_lp[1:]  # 跳过位置 0 (BOS)if len(all_lp) >= response_len:return all_lp[-response_len:]return [0.0] * (response_len - len(all_lp)) + all_lp

_compute_teacher_topk_logprobs()

async def _compute_teacher_topk_logprobs(self, input_ids: list[int], response_len: int
) -> tuple[list[list[float]], list[list[int]]]:"""Compute teacher's top-K log-probs and token indices for response tokens."""K = self.distill_topkstart_len = max(0, len(input_ids) - response_len)payload = {"input_ids": input_ids,"sampling_params": {"temperature": 0.0,"max_new_tokens": 0,"skip_special_tokens": False,},"return_logprob": True,"logprob_start_len": start_len,"top_logprobs_num": K,}async with self._teacher_lp_semaphore:async with httpx.AsyncClient(timeout=None) as client:resp = await client.post(self._prm_url, json=payload)resp.raise_for_status()result = resp.json()meta = result.get("meta_info", {}) if isinstance(result, dict) else {}inp_top = meta.get("input_top_logprobs")if not isinstance(inp_top, list):return [[0.0] * K] * response_len, [list(range(K))] * response_lenall_logprobs: list[list[float]] = []all_indices: list[list[int]] = []for pos_data in inp_top:if isinstance(pos_data, (list, tuple)):row_lp, row_idx = [], []for entry in pos_data:if isinstance(entry, (list, tuple)) and len(entry) >= 2:row_lp.append(float(entry[0]) if entry[0] is not None else 0.0)row_idx.append(int(entry[1]))elif isinstance(entry, dict):row_lp.append(float(entry.get("logprob", 0.0)))row_idx.append(int(entry.get("token_id", 0)))else:row_lp.append(0.0)row_idx.append(0)while len(row_lp) < K:row_lp.append(0.0)row_idx.append(0)all_logprobs.append(row_lp[:K])all_indices.append(row_idx[:K])else:all_logprobs.append([0.0] * K)all_indices.append(list(range(K)))if len(all_logprobs) > 1:all_logprobs = all_logprobs[1:]all_indices = all_indices[1:]if len(all_logprobs) >= response_len:return all_logprobs[-response_len:], all_indices[-response_len:]pad_len = response_len - len(all_logprobs)return ([[0.0] * K] * pad_len + all_logprobs,[list(range(K))] * pad_len + all_indices,)

0x04 logprobs分析

OpenClaw-RL 主要出现的 logprobs 类型有:学生侧(rollout)token logprobs、教师侧 token logprobs、教师侧top-k logprobs、训练/ 前向产生的model log_probs,以及著干元数据字段(input/output/top logprobs)。

四种主要logps类型:

  • Rollout Logps (学生当前策略)
  • Teacher Logps (标准 token级别)
  • Teacher Top-K Logps (分布级别)
  • Reference Logps (KL正则化)

各类型的应用场景:

  • Rollout Logps:所有RL和OPD方法都需要
  • Teacher Logps:OPD标准模式专用
  • Teacher Top-K Logps:OPD Top-K蒸馏模式专用
  • Reference Logps:KL正则化需要(可选启用)

数据流向

Rollout阶段 → 获取Rollout Logps↓ 
用户反馈 → 提示提取 → 增强Prompt↓ 
教师壹询 → 获取Teacher Logps/Top-K Logps↓ 
训练阶段 → 结合Reference Logps进行损失计算

4.1 学生(rollout/response)token logprobs

数学符号解释

  • log π_student(a_t|s)表示在状态s下,学生策略模型对动作a_t的对数概率
  • 这正是rollout阶段收集的学生模型log probabilities
  • 用于计算蒸馏优势:A_t=logπ_teacher(a_t|s+hint)-logπ_student(a_t|s)

关键信息

  • 名称与字段:常见字段名为 response_logprobs / rollout_log_probs(最终写入 Sample 为sample.rollout_log_probs)

  • 产生位置(实现):openclaw_opd_api_server.py 中的 _extract_logprobs_from_chat response(从响应 meca 中抽取)及后续量值,另有生成端实现如 generate_with_retool.py 将output_token_logprobs 聚合到 sample.rollout_log_probs.

  • 用途与消费方:

    • 在rollout阶段由学生策略模型生成的对数概率

    • 用于计算策略梯度和KL散度正则化项

    • 数据收集阶段
      • 在openclaw-opd/openclaw_opd_api_server.py的_handle_request方法中_
      • 通过SGLang服务获取当前学生策略的logprobabilities
      • 存储为turn_data["rollout_log_probs"]
      训练使用阶段
      • 在提交样本时,这些rolloutlogprobs作为logr_student传递给训练系统
      • 在损失计算中直接用作学生策略的对数概率
    • 存储在turn_data["rollout_log_probs"]字段中

    • 作为“old/rollout logprobs“,用于监控rollout vs training drift(见combine loss.py 中 rollout_log_probs 分支),也会被写入 Data Buffer 供 Trainer 计算优势或监控。

  • 数据形状:与 response_ids对齐,按token顺序的浮点 log-prob列表(长度不足时代码会pad0.0,超长则裁剪)

  • 获取方式

    • 通过SGLang服务调用时设置logprobs=True和top_logprobs=1
    • 在_handle_request方法中获取
    • 从SGLang响应的logprobs字段中提取

与其他logps的区别

log π_student 与Reference Logps的区别

  • Reference Logps来自参考模型(通常是初始策略)
  • Rollout Logps来自当前训练中的学生策略
  • 两者用途不同:前者用于KL正则化,后者用于策略梯度和蒸馏

动态更新特性

  • logπ_student随着训练进行而不断更新
  • 每次rollout都反映当前学生策略的能力
  • 这保证了蒸馏目标始终基于最新的学生表现

结论

  • log π_student就是学生模型logps(Rollout Log Probabilities)
  • 这是OPD方法中计算token级别蒸馏优势的关键组成部分,在整个训练流程中保持一致的语义和用途
  • 这种设计使得OPD能够提供精确的改进方向,支持高效的在线蒸馏学习
  • 相比传统RL的标量奖励,token级别的对比提供了更丰富的学习信号

4.2 教师 token logprobs(单token 路径)

teacher log-probs 只做forward pass(max_new_tokens=0),不生成新 token。通过logprob_start_len参数只返回 response 部分的log-probs。这比生成+评估高效得多。

  • 名称与字段:teacher_log_probs(写入 Sample为 sample.teacher_log_probs)。

  • 标准token级别logps

    • 由教师模型在增强prompt基础上计算的对数概率
    • 存储在opd_result["teacher_log_probs"]字段中
    • 用于计算token级别的蒸馏优势:A_t=logπ_teacher-logπ_student
    • logπ_student 就是 学生模型logps(Rollout Log Probabilities)
  • 产生位置(实现):在openclaw_opd_api_server.py,函数_compute_teacher_log_probs(以增强 prompt 调用教师模型并返回每) token log-prob),以及 API server 将其放入返回结果/样本。openclaw_combine_api_server.py也有相同取用/载剪逻辑。

  • 用途与消费方:用于 OPD 的 token-level advantage 计算(A_t=log π_teacher-log π_student),在 Trainer的OPD 损失路径中被消费(见README.md与训练损失实现处)

  • 数据形状:长度与响应token数对齐(代码保证pad/trim)。

  • 获取方式

    • 通过_compute_teacher_log_probs方法计算
    • 使用temperature = 0.0的确定性推理
    • 仅计算原始响应token的logprobabilities

4.3 教师 top-k logprobs(Top-K分布路径)

  • 名称与字段:teacher_topk_log_probs、teacher_topk_indices(写入 Sample为 sample.teacher_topk_log_probs / sample.teacher_topk_indices)

  • 产生位置(实现):openclaw_opd_api_server.py的 _compute_teacher_topk_logprobs,会读取 meta 字段(如 input_top_logpr obs)并返回 [T,K] 的 top-Klogprobs 和对应索引。

  • 用途与消费方:供 Top-K蒸馏路径(SDFT/SDPo 风格)使用:损失实现见 topk_distillation_loss.py,该模块读取teacher_topk_log_probs + teacher_topk_indices。

  • 数据形状:二维数组,shape=[response_len,K],短缺时pad,长余时截断(代码有相应处理)。

  • Top-K教师logps

    • 教师模型输出的top-Ktoken对数概率分布
    • 存储在opd_result["teacher_topk_log_probs"]字段中(形状[T,k])
    • 对应的token索引存储在teacher_topk_indices字段中
  • 实现细节

    • 通过_compute_teacher_topk_logprobs方法获取
    • 使用SGLang的input_top_logprobs参数请求top-Klogprobs
    • 默认K=50(可通过--distill-topk参数配置)

4.4 Reference logprobs(ref_log_probs)

  • 字段:ref_log_probs(可能存在于 batch/sample 中,trainer 可读取为 batch[“ref_log_probs"])
  • 产出/生成位置(实现):
    • 训练端按需在线计算:rl_utils.py 中有计算 ref logprobs 的逻辑,通过带 torch.no_grad() 的 ref 模型前向收集 ref_logprobs 并放入 packing context,再传入 trainer。
    • actor/rollout 端可创建 ref 模型:actor.py 中有 _create_ref _model,actor 进程可加载 args.ref _load 指定的 reference checkpoint.
    • 运行脚本/环境变量:多个启动脚本(例如openclaw-opd/run_.sh、opericlaw-combine/run_.sh)接受REF_LOAD/ref-load,默认常设为 HF_CKPT 或显式路径;arguments.py 里会校验 args.ref_load。
  • 用途与消费方:
    • KL 正则:在 combine_loss.py 中,如果 args.use_kl_loss 且 batch.get("ref_log_probs") 存在,则把 ref log_probs 用作> 参考分布,调用 KLloss 函数(new_log_probs,ref_log_probs,kl_loss_type)。
      • 由参考模型(通常是初始策路或基座模型)计算的对数概率
      • 组于KL正则化,防止策略偏离太远
    • 监控/性能统计:train_metric_utils.py会以 perf/ref_log_probs_time 等项记录与计算性能指标。
    • 对齐/包装:rl_utils.py 提供.pack_inference_logprobs、align_unpacked_inference_logprobs 等,处理 ref_logprobs 在 pa cked/unpacked序列中的对齐。
  • 数据形状与对齐:ref_log_probs 与new_log_probs/old_log_probs 需形状对齐(按序列/packed bin),代码在packing阶段> 将 ref_logprobs 拼接并放入 packing_context['ref_logprobs'](见 rl_utils)。当 batch 中存在 ref_log_probs 时,训练会 torch. cat(batch["ref_log_probs"],dim=0)。
  • 何时由样本携带vs 在线计算:项目支持两种方式:样本层携带(若 Data Buffer 提前填入)或训练端在forward 时根据args.re f_load 在线计算并注入 packing context;实际实现以训练配置为准(slime 会决定 actor/trainer 是否预先加载 ref 模型或在 trainer side 计算)。

4.5 训练/前向产生的log_probs(trainer内部)

  • 名称与字段:训练批次中有 log_probs(新 forward 计算得到)、old_log_probs/rollout_log_probs(来自样本)以及可选的 ref_log_probs(参考模型)。
  • 产生位置(实现):训练时通过get_log_probs_and_entropy(在slime/backends或Megatron 的工具里)得到new_log_probs; 打包/对齐逻辑在 rl_utils.py的 pack_inference_logprobs/ align_unpacked_inference_logprobs 中。combine_loss.py 使用这些值计算PPO/kl/combined loss。
  • 用途与消费方:用于计算PPO风格的策略损失(newvsold),与OPD的teacher信号联合计算优势/KL项。
  • 用于PPO/KL/combinedloss的主计算(见combine_loss.py)

4.6 元数据/输入输出相关的 logprobs字段(辅助字段)

  • 常见字段:input_token_logprobs、input_top_logprobs、output_token_logprobs 等,通常存在于模型返回的 meta 或推理请求体中
  • 位置与用途:这些字段在openclaw_opd_api_server.py 中被读取以加速/拼接teacher/topk计算;generate_with_retool.py产生 output_token_logprobs 用来汇总 rollout logprobs。

4.7 关键要点

存储字段&代码中的映射(谁写入、谁读出)

  • 写入 Sample(由 API server / rollout):sample.rollout_log_probs 由_extract_logprobs_from_chat_response /生成端累> 积写入。sample.teacher_log_probs/ sample.teacher_topk_log_probs ←由 _compute_teacher_log_probs / _compute_teacher_topk_l ogprobs 写入。
  • Trainer 读取:训练批次(来自 slime Data Buffer)供 combine_loss.py、topk_distillation_loss.py、Megatron 后端函数读取rollout_log_probs/ teacher_log_probs/ teacher_topk_log_probs/ log_probs 并计算损失。

关键实现文件

  • openclaw_opd_api_server.py:抽取响应 logprobs、计算teacher logprobs/top-k、组装sample(主要实现点)。
    openclaw_combine_api_server.py:样本合并、teacher log_probs 写入示例(裁剪/pad)。
  • topk_distillation_loss.py:读取并消费 teacher_topk_log_probs的损失实现。
  • combine_loss.py:训练端整合 rollout_log_probs、teacher_log_probs 与模型forward log_probs 的计算与合成损失。
  • generate_with_retool.py:生成端如何收集 output_token_logprobs 并累积到 sample.rollout_log_probs。
  • rl_utils.py:inference/pack/align logprobs的工具函数(trainer 内部对齐使用)。

实践注意事项(代码层)

  • 对齐与 tokenizer:教师与学生必须使用相同 tokenizer/ckpt,否则 response_ids 与 teacher_log_probs 无法正确对齐。
  • pad/trim策略:API server 中会对长度不一致做裁剪或pad(见openclaw-opd/openclaw-combine 的sample组装处)。
  • 并发与性能:教师查询并发受OPENCLAW_OPD_TEACHER_LP_MAX_CONCURRENCY 控制,Top-K会增加开销。
  • 监控:可对比rollout_log_probs 与训练时new_log_probs 的差异(combine_loss.py 中有监控代码段)。

0x05 Top-K 解读

5.1 为什么需要 Top-K 蒸馏?

为什么需要 Top-K 蒸馏?这是因为 单值 OPD 的局限。

标准 OPD(token-level)的工作方式是:对每个 token 位置,teacher 给一个 scalar logprob,trainer 计算 A_t = teacher_lp - student_lp,然后走 PPO。这个方案有一个根本的信息瓶颈:teacher 对每个位置只传了一个数字。teacher 知道"第 5 个 token 应该是 print",但它也知道"如果不用 printwrite 也比 echo 好得多"——这个排序信息在单值传输中完全丢失了。

问题 单 token OPD Top-K OPD
信息量 每位置 1 个 scalar 每位置 K+1 维分布
teacher 的第 2,3...K 选择 丢失 保留偏好排序
尾部 token 的处理 完全不知道 tail bin 保底
存储 [T] [T, K](K=50 时 ×50)
学生遗忘风险 只看 teacher 最强 token,可能过拟合 K+1 维 KL 更温和

Top-K 蒸馏本质上是从"teacher 告诉你哪个 token 对"升级为"teacher 给你一份参考答案册"——不仅告诉你标准答案,还告诉你每个备选答案能得多少分。这个想法来自 SDFT (arXiv:2601.19897) 和 SDPO (arXiv:2601.20802) 两篇工作,它们的共同思路是:用 teacher 的 top-K logits + tail bin 做 KL 散度蒸馏,而不是传统的全词表 KL 或单 token CE。


5.2 Top-K 蒸馏在 OpenClaw 中的位置

Top-K 蒸馏不是替代标准 OPD,而是作为 OPD 的可选增强。两者的关系如下:

                         OPD│┌──────────────────┼──────────────────┐▼                  ▼                  ▼标准 OPD            Top-K OPD           RL-onlyteacher_lp         teacher_topk_lp      无 teacherteacher_topk_idx│▼topk_distillation_loss.py(含 tail bin + reverse KL)Combine│(继承 OPD)│可以用 Top-K 路径

两种模式通过 shell 脚本区分:

标准 OPD Top-K OPD
Shell 脚本 run_qwen3_4b_openclaw_opd.sh run_qwen3_4b_openclaw_opd_topk.sh
传输字段 teacher_log_probs [T] teacher_topk_log_probs [T,K] + teacher_topk_indices [T,K]
Advantage / Loss --advantage-estimator on_policy_distillation (Slime 内置 PPO) --custom-loss-function-path topk_distillation_loss (自定义 reverse KL)
数据量 ~300MB/sample(全 vocab logprobs) ~200KB/sample(K+1 维分布)

Binary RL 完全不涉及 Top-K——它只有 reward ±1 + GRPO advantage,没有任何蒸馏。


5.3 核心机制:Tail Trick + Reverse KL

5.3.1 问题:15 万词表 vs 50 个 top-K

teacher 只传了 Top-50 token 的 logprobs,但词表有 ~150K 个 token。剩下的 149,950 个 token 怎么办?

解法:Tail Trick。把词表从 150K 维压缩为 K+1 = 51 维——前 50 个 bin 各对应一个 teacher 选的 token,第 51 个 bin("tail")是所有剩余 token 的概率之和。

原始词表 (~150K tokens):token_1: P=0.35, token_2: P=0.25, ..., token_50: P=0.001token_51: P=0.0003, ..., token_150000: P=0.000001↓ Tail Trick
压缩为 K+1 = 51 bins:bin_1: P=0.35, bin_2: P=0.25, ..., bin_50: P=0.001bin_51 (tail): P = 1 - Σ(top-50)  ← 所有剩余 token 的总概率

5.3.2 Tail 的数值计算

实际计算在 log 空间进行,代码位于 openclaw-opd/topk_distillation_loss.py:83-89

# 1. Top-K 概率之和(log 空间)
log_sum_topk = logsumexp(topk_logprobs)          # log(P1 + P2 + ... + P_K)# 2. 数值稳定:clamp 防止 log_sum_topk 接近 0 导致下溢
log_sum_topk = clamp(log_sum_topk, max=-1e-7)# 3. 用 expm1 计算 tail,避免 log(1-x) 的灾难性抵消
tail_logprob = log(-expm1(log_sum_topk))         # = log(1 - exp(log_sum_topk))# 4. 拼出 K+1 维分布
distribution = [topk_logprobs..., tail_logprob]  # shape: [K+1]

关键细节:clamp(max=-1e-7) 确保 log_sum_topk < 0,防止 exp(log_sum_topk)=1 导致 log(0) = -infexpm1 是比 1-exp(x) 更数值稳定的实现。

5.3.3 Reverse KL 散度

有了 student 和 teacher 各自的 K+1 维分布后,计算 reverse KL:

# openclaw-opd/topk_distillation_loss.py:94-99
per_token_kl = F.kl_div(input=teacher_with_tail,      # teacher 的 log-probtarget=student_with_tail,     # student 的 log-problog_target=True,reduction="none",             # 不聚合,保留逐 token 维度
).sum(dim=-1)                    # 对 K+1 个 bin 求和loss = sum_of_sample_mean(per_token_kl)

PyTorch 约定:F.kl_div(input, target, log_target=True) 计算的是 D_KL(target || input)。代码中 input=teacher, target=student,因此计算的是 D_KL(student || teacher)——反向 KL。

选反向 KL 的原因是它的 mode-seeking 特性:

Forward KL: KL(teacher||student) Reverse KL: KL(student||teacher)
公式 Σ P_teacher × log(P_teacher/P_student) Σ P_student × log(P_student/P_teacher)
行为 均值寻求(mean-seeking) 众数寻求(mode-seeking)
效果 student 试图覆盖 teacher 的所有模式 student 集中学 teacher 最强的模式
Agent 场景 分散注意力 精确模仿最优行为

在 agent 训练中,我们希望学生专注学好 teacher 最强的行为模式,而不是试图模仿所有可能性。反向 KL 恰好鼓励这种行为。

可选:Entropy 正则化

if args.entropy_coef != 0.0:entropy = -(student_probs * student_logprobs).sum(dim=-1)loss = loss - entropy_coef * entropy  # 减去 → 鼓励探索

防止学生分布坍塌到单点(太确定),保持探索能力。完整 loss:

L = KL(P_student || P_teacher)_{K+1 bins}  -  λ × H(P_student)

5.4 完整数据流:从 API Server 到 Loss

Step 1 — API Server 获取 teacher top-K(openclaw_opd_api_server.py):

_compute_teacher_topk_logprobs(input_ids, response_len)├─ payload: max_new_tokens=0, return_logprob=True, top_logprobs_num=K│           (只做 forward pass, 不生成新 token)├─ 返回: teacher_topk_log_probs [T, K]  ← 每个位置 top-K 的 log P│       teacher_topk_indices  [T, K]  ← 对应的 token ID└─ 并发控制: self._teacher_lp_semaphore (max=3)

Step 2 — 传入 Slime 训练管道(slime/ray/rollout.py):

train_data["teacher_topk_log_probs"] = [sample.teacher_topk_log_probs ...]
train_data["teacher_topk_indices"]  = [sample.teacher_topk_indices  ...]

Step 3 — 损失计算(openclaw-opd/topk_distillation_loss.py):

# A. 收集 student 在 teacher 所选 K 个 token 上的 log-prob
for k in range(K):lp_k = compute_log_probs(logits, t_indices[:, k])
student_topk = stack(student_logps_k, dim=-1)        # [T, K]# B. Tail Trick(teacher 同理)
student_log_s = logsumexp(student_topk, dim=-1)       # Top-K 概率之和
student_log_s = clamp(student_log_s, max=-1e-7)       # 数值稳定
student_tail  = log(-expm1(student_log_s))            # 1 - Σtop_K# C. 拼成 K+1 维分布
student_with_tail = cat([student_topk, student_tail]) # [T, K+1]
teacher_with_tail = cat([teacher_topk, teacher_tail]) # [T, K+1]# D. Reverse KL
per_token_kl = F.kl_div(teacher, student, log_target=True,reduction="none").sum(dim=-1)  # [T]# E. 聚合
loss = sum_of_sample_mean(per_token_kl)

5.5 关键设计选择:Teacher Top-K vs Student Top-K

OpenClaw 做了一个有别于原版 SDFT/SDPO 的修改:使用 Teacher Top-K(查 teacher 概率最高的 K 个 token,再看 student 在这些位置上的概率),而非 Student Top-K(查 student 的 top-K,再看 teacher 在这些位置上的概率)。代码注释中也提到:"we use teacher top K instead of student top K, see issue #7."

# Top-K 的原版 SDFT/SDPO: Student Top-K
Step 1: Student forward → 取 student 概率最高的 K 个 token
Step 2: 查 teacher 在这 K 个位置上的 logprob
Step 3: D_KL(student_K+1 || teacher_K+1)# OpenClaw 修改: Teacher Top-K
Step 1: Teacher forward → 取 teacher 概率最高的 K 个 token(API 端完成)
Step 2: Student forward → 查 student 在这 K 个位置上的 logprob(训练端完成)
Step 3: D_KL(student_K+1 || teacher_K+1)

代码中的体现(topk_distillation_loss.py):

t_indices = teacher_topk_indices[i]     # teacher 选的 K 个 token ID
for k in range(K):lp_k = compute_log_probs(logits, t_indices[:, k])# ↑ 查 student 在 teacher 位置上的 prob,而非用 student 自己的 argmax top-K

5.5.1 为什么选 Teacher Top-K?

原因一:架构约束(最根本)

Teacher Top-K 只需 1 次 HTTP 往返——SGLang 原生支持 top_logprobs_num=K 参数,teacher 一次返回 top-K logprobs + indices。Student Top-K 需要 2 次:先 student forward 取 top-K indices,再发给 teacher,teacher 返回对应位置的 logprob。

在 OpenClaw 的异步解耦架构中(teacher 在 GPU 6-7,trainer 在 GPU 0-3,中间只传 [T, K] 个数字),Teacher Top-K 的实现成本远低于 Student Top-K。

原因二:teacher >> student 场景更合理

OpenClaw 的实际场景是 teacher 强于 student(teacher 有 hint 辅助)。Student 的 top-K 可能指向"垃圾模式",teacher 在这些位置的分布不含有效信息。Teacher top-K 直接回答"应该学什么",而非"我哪里和 teacher 不同"。

原因三:Tail Bin 更有区分度

Teacher Top-K 的 student_tail = 1 - Σ student_log_prob(teacher 选的 K 个 token)→ student 在 teacher 重视的位置上概率可能很低→ tail 很大,有信息量:"student 的大部分概率不在 teacher 重视的地方"Student Top-K 的 student_tail = 1 - Σ student_log_prob(student 自己选的 K 个 token)→ student 自己的 top-K 已捕获大部分概率质量→ tail 很小,信息量低

当 student 和 teacher 分布差异大时,Teacher Top-K 的 tail bin 差异更大,信号更强。

维度 Teacher Top-K (OpenClaw) Student Top-K (原版 SDFT/SDPO)
实现难度 简单(1 次 API 调用) 复杂(2 次往返)
适合场景 teacher >> student teacher ≈ student
信息方向 "学 teacher 重视什么" "发现自己哪里错"
自我强化风险
Tail bin 精度 高(差异大时) 低(student 自身 top-K 已占大部分质量)
K 的合适大小 可以较大(K=50) 较小(K=10-20)即可

5.5.2 Student Top-K 的优势(OpenClaw 放弃的)

  • 梯度更直接:student 优化的就是自己概率最高的 token,梯度信号直指"把我的 top token 调整得更像 teacher"
  • 更好的 mode coverage:如果 student 有一个 teacher 没有的高概率 mode,Student Top-K 能发现并用 KL 惩罚修正
  • 自适应采样区域:随着训练推进,student 的 top-K 会变化

这些优势在 teacher ≈ student 的后期训练中更明显,但在 teacher 明显强于 student 的蒸馏场景下,Teacher Top-K 更合理。


5.6 数值风险

当 student 在 teacher 的 top-K 位置上概率之和接近 1 时(训练后期,student 已经学得很好),1 - exp(log_sum_topk) → 0,tail → -∞。代码通过 clamp(log_sum_topk, max=-1e-7) 做了上界保护,但 没有 clamp tail 的下界,极端情况下可能产生 NaN。这是已知的工程取舍——训练初期 student 分布与 teacher 差异大时 tail 的行为是正常的,而后期 student 收敛后通常不再需要继续 Top-K 蒸馏。


5.7 两种 OPD 路径对比总表

维度 标准 OPD (token-level) Top-K 蒸馏 (SDFT/SDPO 风格)
存储字段 teacher_log_probs [T] teacher_topk_log_probs [T,K] + teacher_topk_indices [T,K]
Advantage / Loss A_t = teacher_lp - rollout_lp,融入 PPO clip 独立 D_KL(student || teacher) over K+1 bins
代码路径 Slime loss.py 内置 on_policy_distillation 分支 topk_distillation_loss.py 自定义 loss
信息量 每位置 1 个值 每位置 K+1 维分布
可并存性 字段不冲突 字段不冲突
适用场景 大多数 OPD 训练场景(默认推荐) 需要更丰富 teacher 信号的研究/高级蒸馏
Shell 启用 默认 --custom-loss-function-path topk_distillation_loss

5.8 常见问题

Q1: 反向 KL 在 Top-K 蒸馏中有什么潜在问题?何时该切换?

反向 KL 是 mode-seeking 的——student 一旦放弃 teacher 的某个 mode(某 bin 的 q_k → 0),梯度 q_k·log(q_k/p_k) → 0,永远学不回来。训练后期 student ≈ teacher 时,考虑改用 JSD(对称、有界)或混合 KL:(1-α)·D_KL(Q||P) + α·D_KL(P||Q),α 从 0 渐增到 0.5。

Q2: 从全词表 KL 到 Top-K+Tail 蒸馏,信息损失有多大?

tail bin 把 |V|-K 个 token 聚合为一个标量,丢失了 tail 内部的分布结构。可忽略的条件:teacher 分布足够 peaked(top-K 已捕获 >95% 概率质量)、K 足够大(K=50 对 |V|=150K 通常足够)、目标只是蒸馏(只需排序,不需精确匹配)。不可忽略的条件:teacher 分布平坦(高 entropy)或 K 太小(如 K=5)。

Q3: Top-K 蒸馏中的 Reverse KL 是 D_KL(student || teacher) 还是 D_KL(teacher || student)?

D_KL(student || teacher)。代码中 F.kl_div(input=teacher, target=student, log_target=True),PyTorch 定义为 D_KL(target || input),即 D_KL(student || teacher)。teacher 在 input 位置,student 在 target 位置。

TransFormer-封面

0xFF 参考

神仙思路!普林斯顿提出OpenClaw-RL:Agent在线挨骂在线升级!