????????verl 是現在非常火的 rl 框架,而且已經支持了多個 rl 算法(ppo、grpo 等等)。
????????過去對 rl 的理解很粗淺(只知道有好多個角色,有的更新權重,有的不更新),也曾硬著頭皮看了一些論文和知乎,依然有很多細節不理解,現在準備跟著 verl 的代碼梳理一遍兩個著名的 rl 算法,畢竟代碼不會隱藏任何細節!
????????雖然 GRPO 算法是基于 PPO 算法改進來的,但是畢竟更簡單,所以我先從 GRPO 的流程開始學習,然后再看 PPO。
GRPO 論文中的展示的總體流程:
論文中這張圖主要展示了 GRPO 和 PPO 的區別,隱藏了其他的細節。
圖中只能注意到以下幾個關鍵點:
-
沒有 Value Model 和輸出 v(value)
-
同一個 q 得出了一組的 o(從 1 到 G)
-
計算 A(Advantage) 的算法從 GAE 變成了 Group Computation
-
KL 散度計算不作用于 Reward Model,而是直接作用于 Policy Model
????????其他細節看不懂,結合論文也依然比較抽象,因為我完全沒有 RL 的知識基礎,下文中我們結合代碼會再一次嘗試理解。
????????下面是我根據 verl 代碼自己 DIY 的流程圖(幫助理解):
01?第一步:Rollout
????????第一步是 rollout,rollout 是一個強化學習專用詞匯,指的是從一個特定的狀態按照某個策略進行一些列動作和狀態轉移。
????????在 LLM 語境下,“某個策略”就是 actor model 的初始狀態,“進行一些列動作”指的就是推理,即輸入 prompt 輸出 response 的過程。
verl/trainer/ppo/ray_trainer.py:
gen_batch_output?= self.actor_rollout_wg.generate_sequences(gen_batch)
????????其背后的實現一般就是是 vllm 或 sglang 這些常見推理框架的離線推理功能,這部分功能相對獨立我們先不展開。
權重同步
????????一個值得注意的細節是代碼里面的?rollout_sharding_manager
?實現,它負責每一個大 step 結束后把剛剛訓練好的 actor model 參數更新到 vllm 或 sglang。
????????這樣下一個大 step 的 rollout 采用的就是最新的模型權重(最新的策略)了。
????????這是每一個大 step 里面真正要做的第一件事,在真正執行 rollout 之前。
????????verl/workers/fsdp_workers.py:
class?ActorRolloutRefWorker(Worker):? ?# ...? ??@register(dispatch_mode=Dispatch.DP_COMPUTE_PROTO)? ?? def?generate_sequences(self,?prompts:?DataProto):? ? ? ?# ...? ? ? ? with?self.rollout_sharding_manager:? ? ? ? ? ??# ...? ? ? ? ? ? prompts =?self.rollout_sharding_manager.preprocess_data(prompts)? ? ? ? ? ?output =?self.rollout.generate_sequences(prompts=prompts)? ? ? ? ? ? output =?self.rollout_sharding_manager.postprocess_data(output)
rollout_sharding_manager
?的基類是?BaseShardingManager。
verl/workers/sharding_manager/base.py:
class?BaseShardingManager:? ?def?__enter__(self):? ? ? ? pass? ??def?__exit__(self, exc_type, exc_value, traceback):? ? ? ? pass? ??def?preprocess_data(self,?data:?DataProto) ->?DataProto:? ? ? ??return?data? ??def?postprocess_data(self,?data:?DataProto) ->?DataProto:? ? ? ??return?data
??BaseShardingManager
?的派生類在各自的?__enter__
?方法中實現了把 Actor Model 的權重 Sync 到 Rollout 實例的邏輯,以保證被?with self.rollout_sharding_manager
?包裹的預處理和推理邏輯都是用的最新 Actor Model 權重。
推理 N 次
????????此外,GRPO 算法要求對每一個 prompt 都生成多個 response,后續才能根據組間對比得出相對于平均的優勢(Advantage)。
verl/trainer/config/ppo_trainer.yaml:
actor_rollout_ref:??rollout:? ??# number of responses (i.e. num sample times)? ?n:?1?# >?1?for grpo
????????在?_build_rollout
?的時候?actor_rollout_ref.rollout.n
?被傳給了?vLLMRollout
?或其他的 Rollout 實現中,從而推理出?n
?組 response。
verl/workers/fsdp_workers.py:
class?ActorRolloutRefWorker(Worker):? ??def?_build_rollout(self, trust_remote_code=False):? ? ? ??# ...? ? ? ??elif?rollout_name ==?"vllm":? ? ? ? ? ??# ...? ? ? ? ? ??if?vllm_mode ==?"customized":? ? ? ? ? ? ? ? rollout = vLLMRollout(? ? ? ? ? ? ? ? ? ?actor_module=self.actor_module_fsdp,? ? ? ? ? ? ? ? ? ? config=self.config.rollout,? ? ? ? ? ? ? ? ? ?tokenizer=self.tokenizer,? ? ? ? ? ? ? ? ? ?
model_hf_config=self.actor_model_config,? ? ? ? ? ? ? ?)
02?第二步:計算 log prob
????????log 是 logit,prob 是 probability,合起來就是對數概率,舉一個簡單的例子來說明什么是 log prob:
詞表僅有?5?個詞:? ?
<pad> (ID?0)? ?
你好 (ID?1)? ?
世界 (ID?2)? ?
! (ID?3)? ?
嗎 (ID?4)
prompt:你好
prompt?tokens: [1]
response:世界!
response?tokens: [2,3]
模型前向傳播得到完整的 logits 張量:
[? ? [-1.0,?0.5,?2.0, -0.5, -1.5], ? ?// 表示 “你好” 后接 “世界” 概率最高,數值為 2.0? ? [-2.0, -1.0,?0.1,?3.0,?0.2] ? ? ?// 表示 “你好世界” 后接 “!” 概率最高,數值為 3.0]
對每個 logit 計算 softmax 得到:
[? ? [-3.65, -2.15, -0.64, -3.15, -4.08],? ? [-4.34, -3.32, -2.20, -0.20, -2.10]]
提取實際 response 對應的數值:得到 log_probs:
[-0.64, -0.20]
總結下來:
-
首先計算 prompt + response(來自 rollout)的完整 logits,即每一個 token 的概率分布
-
截取 response 部分的 logits
-
對每一個 logits 計算 log_sofmax(先 softmax,然后取對數),取出最終預測的 token 對應的 log_sofmax
-
最終輸出 old_log_probs, size = [batchsize, seq_len]
????????此處你可能會有一個疑惑:在上一步 Rollout 的時候我們不是已經進行過完整 batch 的推理了么?
????????為什么現在還要重復進行一次 forward 來計算 log_prob,而不是在 generate 的過程中就把 log_prob 保存下來?
答:因為 generate_sequences 階段為了高效推理,不會保存每一個 token 的 log_prob,相反只關注整個序列的 log_prob。因此需要重新算一遍。
答:另外,vllm 官方 Q&A 中提到了 vllm 框架并不保證 log_probs 的穩定性。因為 pytorch 的 numerical instability 與 vllm 的并發批處理策略導致每一個 token 的 logits/log_probs 結果會略有不同,假如某一個 token 位采樣了不同 token id,那么這個誤差在后續還會被繼續累加。我們在訓練過程需要保證 log_probs 的穩定性,因此需要根據已經確定的 token id(即 response)再次 forward 一遍。
old log prob
verl/workers/fsdp_workers.py:
old_log_prob?= self.actor_rollout_wg.compute_log_prob(batch)
????????指 Actor Model 對整個 batch 的數據(prompt + response)進行 forward 得到的 log_prob
????????此處的 “old” 是相對于后續的 actor update 階段,因為現在 actor model 還沒有更新,所以依然采用的是舊策略 (ps:當前 step 的“舊策略”也是上一個大 step 的“新策略”)
ref log prob
verl/trainer/ppo/ray_trainer.py:
ref_log_prob?= self.ref_policy_wg.compute_ref_log_prob(batch)
????????指 Ref Model 對整個 batch 的數據(prompt + response)進行 forward 得到的 log_prob。
????????通常 Ref Model 就是整個強化學習開始之前 Actor Model 最初的模樣,換句話說第一個大 step 開始的時候 Actor Model == Ref Model,且 old_log_prob == ref_log_prob。
????????Ref Model 的作用是在后續計算 policy loss 之前,計算 KL 散度并作用于 policy loss,目的是讓 actor model 不要和最初的 ref model 相差太遠。
03第三步:advantage
????????advantage 是對一個策略的好壞最直接的評價,其背后就是 Reward Model,甚至也許不是一個 Model,而是一個粗暴的 function,甚至一個 sandbox 把 prompt+response 執行后得出的結果。
????????在 verl 中允許使用上述多種 Reward 方案中的一種或多種,并把得出的 score 做合。
verl/trainer/ppo/ray_trainer.py:
# compute reward model score
if?self.use_rm:? ? reward_tensor =?self.rm_wg.compute_rm_score(batch)? ? batch = batch.union(reward_tensor)
if?self.config.reward_model.launch_reward_fn_async:? ? future_reward = compute_reward_async.remote(batch,?self.config,?self.tokenizer)
else:? ?reward_tensor, reward_extra_infos_dict =?compute_reward(batch,?self.reward_fn)
然后用這個 score 計算最終的 advantage。
verl/trainer/ppo/ray_trainer.py:
# compute advantages, executed on the driver process
norm_adv_by_std_in_grpo = self.config.algorithm.get(? ??"norm_adv_by_std_in_grpo",?True) ?
# GRPO adv normalization factorbatch = compute_advantage(? ? batch,? ?
adv_estimator=self.config.algorithm.adv_estimator,? ?gamma=self.config.algorithm.gamma,? ?
lam=self.config.algorithm.lam,? ?
num_repeat=self.config.actor_rollout_ref.rollout.n,? ? norm_adv_by_std_in_grpo=norm_adv_by_std_in_grpo,)
04第四步:actor update(小循環)
????????在 PPOTrainer 中簡單地一行調用,背后可是整個 GRPO 算法中最關鍵的步驟:
actor_output?= self.actor_rollout_wg.update_actor(batch)
????????在這里,會把上面提到的整個 batch 的數據再根據?actor_rollout_ref.actor.ppo_mini_batch_size
?配置的值拆分成很多個 mini batch。
????????然后對每一個 mini batch 數據進行一輪 forward + backward + optimize step,也就是小 step。
new log prob
????????每一個小 step 中首先會對 mini batch 的數據計算(new)log_prob,第一個小 step 得到的值還是和 old_log_prob 一模一樣的。
pg_loss
????????然后通過輸入所有 Group 的 Advantage 以新舊策略的概率比例(old_log_prob 和 log_prob),得出 pg_loss(Policy Gradient),這是最終用于 backward 的 policy loss 的基礎部分。
????????再次描述一下 pg_loss 的意義,即衡量當前策略(log_prob)相比于舊策略(old_log_prob),在當前優勢函數(advantage)指導下的改進程度。
verl/workers/actor/dp_actor.py:
pg_loss, pg_clipfrac, ppo_kl, pg_clipfrac_lower = compute_policy_loss(? ? old_log_prob=old_log_prob,? ?
log_prob=log_prob,? ?
advantages=advantages,? ?
response_mask=response_mask,? ?
cliprange=clip_ratio,? ?
cliprange_low=clip_ratio_low,? ?
cliprange_high=clip_ratio_high,? ?
clip_ratio_c=clip_ratio_c,? ?
loss_agg_mode=loss_agg_mode,)
entropy loss
????????entropy
?指策略分布的熵 (Entropy):策略對選擇下一個動作(在這里是下一個 token)的不確定性程度。
????????熵越高,表示策略輸出的概率分布越均勻,選擇各個動作的概率越接近,策略的探索性越強;熵越低,表示策略越傾向于選擇少數幾個高概率的動作,確定性越強。
? entropy_loss
?指 entropy 的 平均值,是一個標量,表示探索性高低。
verl/workers/actor/dp_actor.py:
if?entropy_coeff !=?0:? ?entropy_loss?= agg_loss(loss_mat=entropy, loss_mask=response_mask, loss_agg_mode=loss_agg_mode)? ?# compute policy loss? ??policy_loss?= pg_loss - entropy_loss * entropy_coeff
else:? ?policy_loss?= pg_loss
計算 KL 散度
????????這里用到了前面 Ref Model 推出的 ref_log_prob,用這個來計算 KL 并作用于最后的 policy_loss,保證模型距離 Ref Model(初始的模型)偏差不會太大。
verl/workers/actor/dp_actor.py:
if?self.config.use_kl_loss:? ? ref_log_prob = data["ref_log_prob"]? ?# compute kl loss? ? kld = kl_penalty(logprob=log_prob, ref_logprob=ref_log_prob, kl_penalty=self.config.kl_loss_type? ? )? ? kl_loss = agg_loss(loss_mat=kld, loss_mask=response_mask, loss_agg_mode=self.config.loss_agg_mode? ? )? ? policy_loss = policy_loss + kl_loss *?self.config.kl_loss_coef? ? metrics["actor/kl_loss"] = kl_loss.detach().item()? ? metrics["actor/kl_coef"] =?self.config.kl_loss_coef
反向計算
verl/workers/actor/dp_actor.py:
loss.backward()
????????持續循環小 step,直到遍歷完所有的 mini batch,Actor Model 就完成了本輪的訓練,會在下一個大 step 前把權重 sync 到 Rollout實例當中,準備處理下一個大 batch 數據。