# Lesson 23 笔记:PPO 中 Response 采样与梯度问题 ## 目录 - [核心问题:为什么 y 不能直接求导](#核心问题为什么-y-不能直接求导) - [普通 CE Loss 的梯度怎么传](#普通-ce-loss-的梯度怎么传) - [PPO 的问题:y 是"采样"出来的](#ppo-的问题y-是采样出来的) - [DPO 的解决方案](#dpo-的解决方案) - [PPO 的解决方案:Reinforce 估计](#ppo-的解决方案reinforce-估计) - [一句话总结](#一句话总结) --- ## 核心问题:为什么 y 不能直接求导 > response y 是模型采样出来的离散 token 序列,不能像普通 CE loss 那样直接对 token 选择过程求导。 --- ## 普通 CE Loss 的梯度怎么传 ```python # 普通 SFT logits = model(input_ids) # 模型输出每个 token 的 logits loss = F.cross_entropy(logits, labels) # 直接拿 ground truth labels 算 loss loss.backward() # 梯度直接流回 logits → embedding ``` **关键:** labels 是**固定的 ground truth**,不是从模型里采样出来的。 | 步骤 | 说明 | |------|------| | 1 | logits = model(input_ids) | | 2 | loss = cross_entropy(logits, y_fixed) | | 3 | backward → 梯度流回 logits | --- ## PPO 的问题:y 是"采样"出来的 ```python # PPO 里 y_w = torch.multinomial(probs, num_samples) # 从模型分布中采样 ``` | 问题 | 说明 | |------|------| | 采样是随机的 | `torch.multinomial` 每次结果可能不同 | | 采样不可导 | autograd 无法跟踪随机采样过程 | **梯度流断在这里:** ``` logits → softmax → sample → y (随机!) ↑ 这个随机采样过程没有梯度 ``` --- ## DPO 的解决方案 DPO **绕过了这个问题**,不采样,而是直接用 model 输出的 log_probs: ```python # DPO 直接用模型输出的概率分布 policy_log_probs = model(x_chosen).log_probs # 可导 ref_log_probs = ref_model(x_chosen).log_probs # 可导 # 计算 ratio 时,不需要对采样求导 log_ratio = policy_log_probs - ref_log_probs ``` --- ## PPO 的解决方案:Reinforce 估计 PPO 需要对采样过程求导,用的是 **Reinforce 梯度估计**: ``` ∇θ E_{y~πθ}[r(y)] = E_{y~πθ}[r(y) * ∇θ log πθ(y)] ``` | 步骤 | 说明 | |------|------| | 1 | 从 π_θ(y|x) 采样 y | | 2 | 计算 reward r(y) | | 3 | 用 ∇ log πθ(y) 把 reward 的信号传回去 | **关键技巧:** 用 log probability 的梯度来"引导"随机采样的方向。 --- ## PPO + KL 约束公式:k = r - log r - 1 ### 公式拆解 ```python k = π_ref(a|s) / π_θ(a|s) - log(π_ref(a|s) / π_θ(a|s)) - 1 ``` 设 `r = π_ref / π_θ`,则: ``` k(r) = r - log r - 1 ``` ### 核心性质 | 性质 | 说明 | |------|------| | **最小值在 r = 1** | k(1) = 1 - 0 - 1 = 0,policy = reference 时无惩罚 | | **永远 ≥ 0** | 经典不等式 r - log r - 1 ≥ 0 | | **偏离越大,惩罚越大** | r >> 1 或 r << 1 时,k 增大 | ### 为什么这样设计 > 限制 policy 不要离 reference 太远,但又不能太死 | 公式项 | 含义 | |--------|------| | `r = π_ref / π_θ` | policy 相对于 reference 的概率比值 | | `r - log r - 1` | KL divergence 的单 token surrogate | **作用:** - 保证非负 - 在 r=1 处最小 - 对大偏离进行更强惩罚 - 实现稳定的策略更新约束 --- ## Thinking 机制:open_thinking 的作用 ### RLAIFDataset 中的 thinking 控制 ```python def create_chat_prompt(self, conversations): use_thinking = random.random() < self.thinking_ratio return self.tokenizer.apply_chat_template( conversations[:-1], tokenize=False, open_thinking=use_thinking, add_generation_prompt=True ) ``` | 模式 | 效果 | |------|------| | `open_thinking=True` | 模型学会"先想再答",输出包含 `` 推理过程 | | `open_thinking=False` | 模型学会"直接回答",不输出推理过程 | ### chat template 中的实现 ```jinja {%- if open_thinking is defined and open_thinking is true %} {{- '\n' }} {# 只打开,不关闭,让模型自己生成 #} {%- else %} {{- '\n\n\n\n' }} {# 完整空 thinking 块 #} {%- endif %} ``` | open_thinking | 模板输出 | |----------------|----------| | True | `<|im_start|>assistant\n`(打开但没关闭) | | False | `<|im_start|>assistant\n\n\n\n\n`(完整空块) | ### 之前阶段有没有训练过 thinking | 阶段 | 有 thinking 训练数据吗 | |------|----------------------| | Pretrain | 没有 | | SFT | 有字段但**实际数据没有**(`reasoning_content` 是空的) | | RLAIFDataset | **有** — 通过 `open_thinking` 参数控制 | ### SFT 中也有 thinking 相关机制 SFTDataset 的数据 features 定义了 `reasoning_content` 字段,chat template 对 assistant 消息会做: ```jinja <|im_start|>assistant {reasoning_content} {content}<|im_end|> ``` 另外 `post_processing_chat()` 会随机移除空 thinking 块: ```python if '\n\n\n\n' in prompt_content and random.random() > empty_think_ratio: prompt_content = prompt_content.replace('\n\n\n\n', '') ``` ### 总结:thinking 训练流程 | 阶段 | 作用 | |------|------| | **SFT** | 让模型见过 ``...`` 格式,也见过空 thinking 或被移除的情况 | | **RLAIF/PPO** | 用 `thinking_ratio` 随机决定 rollout 时"打开 thinking"还是"空 thinking 直答" | | **推理** | 用户用 `open_thinking` 开关控制模型偏向"显式思考"还是"直接回答" | --- ## PPO 中四个模型的角色 | 模型 | 来源 | 训练方式 | |------|------|----------| | **actor_model** | 从 full_sft 来 | PPO 训练它 | | **ref_model** | 从 full_sft 来 | 冻结,当参考模型 | | **critic_model** | 从 full_sft backbone 来,加 value_head | PPO 训练它 | | **reward_model** | 外部奖励模型 | 只负责给 response 打分 | --- ## SGLang 详解 ### 一句话定性 > HTTP API 的作用:把"模型生成能力"变成一个"可调用的远程服务接口"。本质不是为了"网络",而是为了:解耦 + 调度 + 并发 + 工程化部署。 --- ### model.generate vs SGLang | 方式 | model.generate | SGLang | |------|---------------|--------| | 形式 | 函数调用 | HTTP / RPC | | 执行 | 当前进程 | 独立推理服务 | | batch | 固定 batch | Continuous batching | | cache | 本地函数里 | server 统一管理 | --- ### SGLang 额外做了什么 **没有发生的事:** - ❌ 改变模型结构 - ❌ 改变 forward 计算 - ❌ 替换 model.generate 的算法本质 **发生的事:** - 把"generate 循环"从 Python 进程搬到一个高性能推理系统里执行 --- ### Continuous Batching(关键) | 方式 | 行为 | |------|------| | HF generate | 等所有请求到齐,固定 batch 一起生成 | | SGLang | 动态拼 batch,完成就退出,新请求随时加入 | ``` t=0: prompt A, B t=1: prompt C 加进来 t=2: prompt D 加进来 → GPU 利用率更高 ``` --- ### HTTP API 的核心作用 | 作用 | 说明 | |------|------| | **解耦** | 训练和推理完全分开,可独立升级/扩展 | | **服务化** | 变成标准接口,不关心内部怎么生成 | | **并发调度入口** | 统一入口供 scheduler 做 continuous batching | | **分布式扩展** | 多训练 worker 共用一个推理服务 | --- ### RLHF 场景为什么特别需要 | 需求 | 为什么需要 HTTP API | |------|---------------------| | rollout 量极大 | 一个 step = 100~1000 prompts,必须高吞吐入口 | | 训练/推理资源分离 | Trainer 用一批 GPU,Rollout 用另一批,必须跨进程通信 | | 动态调度 | prompt 随时进来,generation 长度不一致,必须统一调度 | --- ### RLHF pipeline 中的位置 ``` Prompt dataset ↓ HTTP API(SGLang) ↓ Generated trajectories ↓ Reward model ↓ PPO / GRPO update ``` --- ### 一句话总结(考试级) > SGLang 并没有替代 model.generate,而是把"生成过程(forward + sampling + KV cache loop)"从本地函数调用,升级为可并发调度的推理服务;模型参数 θ 完全不变,只是执行环境和调度方式变了。 --- ## approx_kl 详解 ### 公式 ```python approx_kl = ( 0.5 * (log_ratio ** 2) * resp_policy_mask[inds] ).sum() / resp_policy_mask[inds].sum().clamp(min=1) ``` 展开: ``` approx_kl = Σ_{b,t} m_{b,t} * 0.5 * (log π_θ(a_{b,t}|s_{b,t}) - log π_old(a_{b,t}|s_{b,t}))² / Σ_{b,t} m_{b,t} ``` 其中 `m_{b,t} = resp_policy_mask[b,t]` ### resp_policy_mask 的作用 | 排除的内容 | 说明 | |-----------|------| | prompt tokens | 只统计 response | | padding | 只统计有效 token | | EOS 之后 | 只统计有效 response 区域 | ### 为什么叫 approx_kl PPO 需要监控:当前 actor `π_θ` 离 rollout 时的 old actor `π_old` 有多远。 KL 本来是衡量两个分布距离的量。当新旧策略很接近时,可以用二阶近似: ``` KL(π_old || π_θ) ≈ 0.5 * (log π_θ - log π_old)² ``` 所以源码用 `0.5 * (log_ratio ** 2)` 来近似新旧策略的 KL。 ### 直观判断 | log_ratio | 含义 | approx_kl | |-----------|------|-----------| | 接近 0 | new actor 和 old actor 很接近 | 小 | | 绝对值变大 | new actor 和 old actor 差得更远 | 大 | --- ## 一句话总结 | 概念 | 说明 | |------|------| | **普通 CE** | y 是 ground truth 常数 → 梯度直接流回 | | **DPO** | y 是分布比较,不需要采样 → 直接用 log_probs | | **PPO** | y 必须采样 → 用 Reinforce 估计器把 reward 信号传回去 | | **KL 约束** | k = r - log r - 1,保证 policy 不偏离 reference 太远 | | **approx_kl** | 0.5 * (log_ratio)²,新旧策略 KL 散度的近似 | | **Thinking** | 通过 `open_thinking` 控制模型是否显式输出推理过程 | | **SGLang** | 生成服务化,支持并发调度和连续 batching |