1,GRPO
1.1,RLHF
RLHF 引入了"反馈机制",让 LLM 能够生成更符合人类期望的答案:
生成:AI 生成多个不同的回答
评分:人类(或奖励模型)对回答打分
学习:AI 学会更多地生成高分回答,更少地生成低分回答
改进:重复这个过程,AI 越来越好
这就像一个持续改进的循环:生成回答 → 获得反馈 → 调整策略 → 生成更好的回答。
监督学习 SFT 策略梯度 RLHF 学习正确答案 学习什么是好的 模仿固定的例子 根据反馈持续改进 一次性训练 交互式学习 答案相对固定 能适应新的情况
1.2,RL,PPO,GRPO
【Policy-Base】假如问题定义在离散的动作空间,智能体基于此刻的状态要输出每一个动作的概率。Policy-Base 就是你有一个 DNN,最后一层是一个 Softmax,输出了后续每一个动作的概率,概率越大未来的价值越高,如果是玩游戏就是未来得分高,存活几率大,如果是序列生成,就是生成的序列更优质。那么怎么更新这个 Policy 呢?还是对比多分类,模型输出了每个类别的概率,知道多类里有一个是正确的,那么用交叉熵损失即可,调大正确那一类的输出概率。
【强化学习】知道执行了一个动作,这个动作产生了 reward,如果 reward 的是好的,那么应该调大这一个动作的概率,如果 reward 是差的,应该调小这个动作的概率。这怎么做呢?就是交叉熵乘以 reward,来影响概率的调节方向和调节力度。
【REINFORCE with baseline】这里有一个漏洞,那就是 reward 可能都是设计成正数,reward 无论好坏都是正的,那么概率始终在被调大,只是力度不同。希望调小概率时,reward 应该是个负的。直观的想法,如果见过一批 reward,减去的均值就好了。
【Actor-Critic】这个启发式搞出来的均值 baseline 不一定是最好的,想知道的是此刻状态对应的价值,均值没有这样的物理意义。可以用一个 NN 模型来预估出这个价值呢?Actor 就是你的动作概率模型,Critic 就是用一个 NN 在算这个 baseline,或者叫 value-base 的 model。
【GRPO】PPO 就属于 Actor-Critic,所以它带着 Value-Base model,GRPO 的 motivation 就是干掉了这个 Value-Base 的 model,节省了显存。如果不用一个 model 来估计状态的价值,还有什么好办法?那你就基于每一条原始样本生成一组序列,用它们的 reward 均值作为 baseline,这种方法呢,就叫 self-critic。利用了蒙特卡洛方法代替了 TD error。
GROP 的精髓呢,就是 Self-Critic,某个状态开始生成多个结果,用这个结果 reward 的均值作为 baseline。至于其他的那个 KL 散度的动作连续性校正,是从 PPO 继承来的,但那不是精髓。这个 Self-Critic 方法是哪里来的?最早溯源到了这篇 paper《Self-critical Sequence Training for Image Captioning》,输入图片,生成一段自然语言描述,里面剔除到 Value-base model,提出了 Self-critic 的概念。《BUY 4 REINFORCE SAMPLES,GET A BASELINE FOR FREE!》就是更接近了,每个生成 4 个序列,每 4 个获得一个均值作为 baseline,而且它宣称 sample 出 4 个边际收益已经非常高了。
为什么在游戏领域,更多的使用 PPO,而在序列生成上,GRPO 才有了一席之地,并且 self-critic 最早应用在了图生文上,答案很简单。相比游戏里的轨迹,序列生成要短的多,它有明确的终止条件,而游戏未必有,因而容易通过蒙特卡洛模拟获得 baseline。
1.3,为什么 GRPO 是 On-Policy?
On-Policy:指的是“用正在学习的策略产生的数据来学习”。即,智能体(在LLM中指语言模型本身)严格使用其当前策略(Policy)与环境交互所产生的数据来更新和优化自身。这意味着,一旦策略发生更新,所有旧的交互数据都将被废弃,因为它们是由一个“过时”的策略产生的。
Off-Policy:则更为灵活,它指的是“用并非当前学习的策略所产生的数据来学习”。智能体可以利用由其他策略(甚至是历史策略或人类示范数据)产生的数据来更新当前策略。这解耦了“数据生成”和“策略学习”两个过程,从而提高了数据利用效率。
【DPO:一种典型的 Off-Policy 思路】利用一个固定的、离线的人类偏好数据集,直接优化语言模型。这个数据集通常包含一系列的提示(Prompt),以及对模型生成的两个回答的偏好标签(哪个更好
,哪个更差
)。DPO 损失函数直接将策略与人类偏好挂钩,其目标是最大化模型生成“更优”回答的概率,同时最小化生成“更差”回答的概率,并且与一个固定的参考模型(Reference Model)保持一定的距离,防止模型在优化过程中“忘掉”其预训练时学到的知识。
【为什么 DPO 是 Off-Policy?】
- 数据来源的非交互性:DPO 所用的偏好数据
,
,
是一次性收集并固定的。在整个 DPO 的训练过程中,模型不会用其正在更新的策略去与环境(或人类)交互产生新的偏好数据。
- 行为策略与目标策略的分离:将生成这个偏好数据集的策略(可能是早期的某个模型版本,甚至是多个不同模型的混合)看作是行为策略(Behavior Policy)。而正在优化的当前模型,则是目标策略(Target Policy)。DPO 的目标是利用行为策略产生的数据,来优化目标策略,使其更符合人类偏好。
【GRPO:On-Policy 算法】 对于一个给定的提示,让当前模型生成一组(Group)候选回答(例如,生成 8 个不同的回答)。然后,一个奖励函数(可以是一个训练好的奖励模型,也可以是基于规则的打分器)会为这组中的每一个回答打分。GRPO 利用这一组回答的相对好坏来计算优势(Advantage),并更新策略。它通过比较组内样本的得分均值和标准差来归一化奖励,从而指导模型向着生成更高分回答的方向优化。
- 数据由当前策略生成:在每个训练步骤中,使用当前正在优化的策略
来生成一组候选回答。
- 即时更新于抛弃:基于这组新生成的数据计算出的梯度来更新策略。更新完成后,这组数据就会被抛弃,下一步骤会由新的策略
重新生成数据。整个流程保证了数据分布与策略分布的一致性。
2,DPO 算法
DPO(Direct Preference Optimization)是一种基于人类偏好的语言模型训练方法。 它通过最小化一个特殊的损失函数来优化模型,使模型生成的文本更符合人类偏好。 相比其他基于人类偏好的训练方法(如 PPO、RLHF 等),DPO 具有以下优势:
- 计算效率高:不需要进行策略梯度估计和价值函数学习。
- 训练过程简单:可以直接在单个前向传播中计算损失。
- 计算开销小:无需额外的网络结构和训练步骤。
DPO 的核心损失函数为:
2.1,KL 散度
在 DPO 中,需要限制优化后的策略
与初始测试
之间的差异。这种限制主要是为了避免模型发生剧烈的变化,保持 pretrained model 的语言能力,避免过拟合。使用 KL 散度(Kullback-Leibler 散度)来计算两个策略之间的差异,KL 散度是衡量两个概率分布差异的度量工具。
KL 散度具有以下特点:
非负性:KL(P||Q) ≥ 0
非对称性:KL(P||Q) ≠ KL(Q||P)
当且仅当 P=Q 时,KL(P||Q)=0
在 DPO 中,KL 散度约束的具体形式为:
2.2,Bradley-Terry 模型
Bradley-Terry 模型是一个用于分析配对比较数据的概率模型。主要用于:
对事物进行排序和评级。
预测两个对象之间的胜率。
从成对比较中估计对象的潜在能力值。
模型的核心思想是:
每个对象
都有一个正实数参数
,表示其能力值
对象
胜过对象
的概率为:
能力值
越大,胜率越高。
2.3,DPO 模型推导
DPO 模型的目标是让语言模型输出更贴近人类偏好。它使用成对的偏好数据(preferred/dispreferred pairs)进行训练,使模型能直接学习人类的价值判断。
:输入的 prompt
:模型生成的回答
:状态-动作对
:衡量回答质量的 reward 函数
受 Bradley-Terry 模型启发,定义
优于
的概率为:
为确保概率值在 [0,1] 区间,对 reward 函数进行指数变换:
通常定义
为 winner,
为 loser,所以下面这个公式把
和
的公式表示为
优于
的概率。
接下来需要将这个概率公式转换为可优化的损失函数。为了在整个数据集上进行优化,引入期望值
。
训练数据集
包含大量的三元组
,其中
是输入,
是优选输出,
是次选输出。
每个三元组都对应一个具体的损失值。
目标是最小化整个数据集上的平均损失。
优选输出胜出的概率公式:
然后定义损失函数为:
带
是为了最大化
概率,符合交叉熵最小化原理,同时也有数值和优化上的好处。
因为
即:
对于单个样本,损失函数为:
2.4,基于 KL 散度约束的奖励值的推导
接下来,根据 KL 散度计算出奖励值
,这个奖励值将隐式受到 KL 散度约束。结合前面两部分就可以得到 DPO 的优化目标为下面公式,其中
是平衡 KL 散度和奖励函数之间的超参数。
越大,模型越倾向于优化 KL 散度(限制模型变化),反之则越倾向于优化奖励函数(优化模型)。
期望值的公式就是
,可用将 KL 散度的
认为
,
认为
,转换为:
合并期望:
同时除
合并 log:
除
,再乘
即:
在优化策略
时,由于
只包含参考策略
和奖励函数
,而不包含待优化的
,所以在优化过程中可用将其视为常数项,可以忽略:
带入
,分母:
可以看到,分子是某一个特定样本,而分母是所有样本的和,所以可以认为它是一个概率分布,并把它标为
所以 DPO 的优化目标转换为下面公式,它又可以转换为 KL 散度:
最终的目的是最小化上面的公式的期望,而这个期望最小的时候,
,这代表
是
的最优解,也就是说,希望:
移项:
在 Bradley-Terry 模型中定义的损失函数 Loss:
把
带入到损失函数中,可以得到:
同样,可以把不涉及到优化的
提出来,这样就可以得到最终的 DPO 损失函数:
这个损失函数可以通过一次前向传播,就可以计算出来对应的值。
3,vLLM
3.1,基础概念
vLLM 是一个 LLM(Large Lanuage Model)推理和部署服务库,它结合 iterative-level schedule(常被称为 continuous batching,该调度算法在 Orca 中首次被提出)和 PagedAttention 注意力算法以提高服务的吞吐量。
- 前者(iterative-level schedule)以单轮迭代的方式对用户的请求进行处理,即 LLM 生成一个 token 后会重新调度下一轮要处理的请求。
- 后者(PagedAttention)受操作系统虚拟内存和分页思想启发,将原本连续的 KV cache 存储在不连续的空间,以避免 KV cache 带来的显存浪费。
LLM 主要的两个接口是初始化方法和 generate 方法,前者用于实例化 LLM 对象,后者处理接收到的 prompts 和采样参数。
from vllm import LLM, SamplingParams prompts = [ "Hello, my name is", "The future of AI is", ] sampling_params = SamplingParams(temperature=0.8, top_p=0.95) llm = LLM(model="meta-llama/Llama-2-7b-chat-hf") outputs = llm.generate(prompts, sampling_params) # Print the outputs. for output in outputs: prompt = output.prompt generated_text = output.outputs[0].text print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}") ------------- Prompt: 'Hello, my name is', Generated text: ' Dustin Nelson and I’m going to be your tutor!\n' Prompt: 'The future of AI is', Generated text: ' bright, but it’s unclear how many of the blue-sky concepts we'
3.2,Scheduler & Worker
vLLM 的核心组件是 LLMEngine 类,外层接口类 LLM 和 AsyncLLMEngine 都是对 LLMEngine 的封装。LLMEngine 有两个核心组件,分别是负责请求调度的 Scheduler 和负责模型推理的 Worker,前者从等待队列中选择接下来要处理的请求,后者负责使用模型对被调度的请求进行推理。
【Scheduler】Scheduler 使用 iterative-level 策略对请求进行调度(选择要被处理的请求),被调度的请求在生成一个 token 后会被重新调度。得益于 itertive-level 策略,vLLM 能够在每一轮新的迭代时选择不固定数量的请求进行处理(即 batch size 每次都不一定相同),因此它能够尽可能多地处理请求。请求的处理通常分为两个阶段,第一个阶段对 prompt 进行处理(也被称为填充阶段,后文使用填充阶段表示这一个阶段),生成 prompt KV cache 的同时生成第一个 token,第二个阶段是生成阶段,不断预测下一个 token。
目前对 iterative-level 的实现有两种方式,一种是区分填充阶段和生成阶段,另一种是不区分这两个阶段。具体而言,同一个 batch 里被处理的请求是否均处于同一个阶段(例如填充阶段或者生成阶段。即同一批被调度的请求要么都处于填充阶段,要么都处于生成阶段,这和 huggingface 的 TGI 推理库一致。而提出 iterative-level 策略的 Orca 系统是不区分这两个阶段的。
Scheduler 中有 3 个队列,waiting(接受到的新请求会先放入 waiting 队列)、running(被调度的请求)和 swapped 队列(swapped 队列用于存放被抢占的请求。即当请求处于生成阶段时,但由于空间的不足,需暂时将 running 队列中优先级低的请求移到 swapped 队列)。在调度时,Scheduler 会按照先到先处理(first come first served)的原则从 waiting 队列中选择请求放入 running 队列(注意,实际的调度包含更多细节)。此外,Scheduler 的另一个核心组件是 BlockSpaceManager,它主要负责块表的维护。
【Worker】Worker 负责模型的执行。如果模型过大,可以将模型切分到多个 Worker 共同完成请求的处理。假设模型有 4 层,现在有 4 张卡,可以设置 Tensor Parallel=4(注意:写本文时截止 v0.4.0,vLLM 还没有支持 Pipeline Parallel),则将模型每一层切分为 4 份,每张卡存放模型的一部分。
Worker 的一个核心组件是 CacheEngine,它负责 KV cache 的初始化以及 KV cache 的相关操作。
3.3,工作流
【初始化】主要初始化 LLMEngine 中的 Scheduler 和 Worker 对象,Scheduler 的初始化主要是块表(block table)的初始化,Worker 的初始化包括模型的初始化以及 KV cache 的初始化。
【调度和推理】假设 vLLM 接收到 3 个请求(记为 s0,s1,s2)并放入 waiting 队列中,它们的 prompt 分别为 "Hello, my name is"、"The future of AI is" 和 "The life is"。
- vLLM 的第一轮处理:假设 vLLM 在这一轮只能调度两个请求进行处理,那么根据先到先处理的原则,会从 waiting 队列中选择 s0 ("Hello, my name is") 和 s1 ("The future of AI is") 放入到 running 队列。对于 s0,Worker 生成的 token 为 Dustin,对于 s1,Worker 生成的 token 为 bright。同时,Worker 会将计算过程产生的 KV 值存储在 KV cache 中。
- vLLM 的第二轮处理:由于 waiting 队列中还有一个请求 s2(The life is),因此,vLLM 在第二轮只会处理这一个请求,因为前面提到,vLLM 只会处理要么都是填充阶段的请求,要么都是生成阶段的请求。
- vLLM 的第三轮处理:waiting 队列中没有要处理的新请求,所以会从 running 队列中选择此轮要处理的请求(这些请求均处于生成阶段)。但由于没有多余的空间,vLLM 只会选择 s0 和 s1 进行处理。经过多轮调度和推理,最终完成 3 个请求的处理,以上就是 vLLM 的工作流。