Prhub

#6409 [ckpt, model] fix: save LoRA train metadata for PPO actor checkpoint

原始 PR 作者 Yatogaii 合并时间 2026-05-22 22:03 文件变更 1 提交数 3 评论 9 代码增减 +37 / -0

执行摘要

修复 PPO actor LoRA 检查点丢失训练元数据

Issue #6380 报告了 PPO 使用 LoRA 训练时,合并检查点得到的适配器 lora_alpha 变为 0,导致合并后模型无有效 adapter 效果。问题根源在于 PPO actor 检查点未像 SFT 检查点(PR #5326)一样保存 lora_train_meta.json。本 PR 修复此遗漏。

值得阅读的 PR,展示了如何通过扩展 CheckpointManager 而非修改 trainer 来支持新功能,以及基于分布式后端设计(FSDP vs Megatron)决定是否复用已有机制的权衡决策。review 中关于 Megatron Bridge 的讨论尤其值得关注。

讨论亮点

wuxibin89 建议将 LoRA 元数据保存逻辑从 trainer 移入 CheckpointManager:"Better move into CheckpointManager(FSDP/Megatron) instead of exposing to trainer"。Yatogaii 接受建议,将代码迁移至 FSDPCheckpointManagerMegatronCheckpointManager(后移除 Megatron)。
HollowMan6 指出 Megatron Bridge 已通过 save_hf_adapter 正确导出 PEFT 适配器,质疑 model_merger + lora_train_meta 路径的必要性。Yatogaii 认同并移除 Megatron 部分,仅保留 FSDP 路径。
gemini-code-assist[bot] 提醒原逻辑可能导致 lora_alpha 为 0,应默认使用 lora_rank:"Following standard PEFT conventions, lora_alpha should default to lora_rank if not explicitly specified." 该建议被采纳到最终代码中。

实现拆解

  1. FSDPCheckpointManager 中添加私有方法 _get_lora_train_meta,从 unwrap_modelpeft_config 中提取 rlora_alphatask_type,并返回字典(如果 peft_config 不存在或 r <= 0 则返回 None)。
  2. 添加 _save_lora_train_meta 方法,调用 _get_lora_train_meta,若非 None 则在 local_path 下写入 lora_train_meta.json
  3. save_checkpoint 方法中,写入 fsdp_config 后立即调用 _save_lora_train_meta(local_path, unwrap_model),确保 LoRA 元数据随检查点一起持久化。
  4. 调整默认逻辑:从 peft_config 中获取 lora_alpha 时若未设置则默认为 lora_rank,遵循 PEFT 惯例;且仅当 lora_rank > 0 时才写入。
    (原实现还包含 MegatronCheckpointManager 的修改,但在 review 中被删除,因为 Megatron Bridge 已通过 save_hf_adapter 处理 PEFT 导出,无需 model_merger。)
文件 模块 状态 重要度
verl/utils/checkpoint/fsdp_checkpoint_manager.py 检查点管理 modified 6.97

关键符号

_get_lora_train_meta _save_lora_train_meta

关键源码片段

verl/utils/checkpoint/fsdp_checkpoint_manager.py core-logic

所有变更集中于此文件,新增 LoRA 训练元数据持久化逻辑。

# pylint: skip-file
# 从 unwrapped model 的 peft_config 中提取 LoRA 训练超参数
def _get_lora_train_meta(self, unwrap_model):
    peft_config = getattr(unwrap_model, "peft_config", None)
    if not peft_config:
        return None
    # peft_config 可能是 dict,取默认适配器
    if isinstance(peft_config, dict):
        peft_config = peft_config.get("default") or next(iter(peft_config.values()), None)
    if peft_config is None:
        return None
​
    lora_rank = int(getattr(peft_config, "r", 0) or 0)
    if lora_rank <= 0:
        return None
​
    # 遵循 PEFT 约定:lora_alpha 未设置时默认等于 lora_rank
    lora_alpha = int(getattr(peft_config, "lora_alpha", lora_rank) or 0)
    task_type = getattr(peft_config, "task_type", None) or "CAUSAL_LM"
    if hasattr(task_type, "value"):
        task_type = task_type.value
​
    return {"r": lora_rank, "lora_alpha": lora_alpha, "task_type": str(task_type)}def _save_lora_train_meta(self, local_path: str, unwrap_model):
    lora_train_meta = self._get_lora_train_meta(unwrap_model)
    if lora_train_meta is None:
        return None
​
    lora_meta_path = os.path.join(local_path, "lora_train_meta.json")
    with open(lora_meta_path, "w", encoding="utf-8") as f:
        json.dump(lora_train_meta, f, ensure_ascii=False, indent=4)
    # 仅在 rank 0 打印日志,但实际写入在所有 rank 上都执行
    log_with_rank(
        f"Saved LoRA rank/alpha metadata to {os.path.abspath(lora_meta_path)}",
        rank=self.rank,
        logger=logger,
        log_only_rank_0=True,
    )
    return lora_meta_path# 在 save_checkpoint 方法中,保存 fsdp_config 后立即调用
# (位于 torch.distributed.barrier() 之前,与 fsdp_config 保存对齐)
...
with open(fsdp_config_path, "w") as f:
    json.dump(asdict(fsdp_config), f, indent=4)
self._save_lora_train_meta(local_path, unwrap_model) # 新增:持久化 LoRA 元数据
# wait for everyone to dump to local
torch.distributed.barrier()
...

评论区精华

将 LoRA 元数据保存逻辑从 trainer 移至 CheckpointManager 设计

wuxibin89 建议:"Better move into CheckpointManager(FSDP/Megatron) instead of exposing to trainer"

结论:Yatogaii 接受建议,将代码从 trainer 移至 FSDPCheckpointManager 和 MegatronCheckpointManager(随后移除了 Megatron 部分) · 已解决

移除 Megatron 端的 lora_train_meta 保存 设计

HollowMan6 指出 Megatron Bridge 已通过 save_hf_adapter 正确导出 PEFT 适配器,可能不需要 model_merger + lora_train_meta 路径。评论:"Will model merger really correctly handle the PEFT adapters export since its format is defined in Megatron Bridge? This PEFT adapters export should have been handled by Megatron Bridge"

结论:Yatogaii 认同,移除 MegatronCheckpointManager 的相关修改,仅保留 FSDP 路径。 · 已解决

确保 lora_alpha 默认不为 0 正确性

gemini-code-assist 评论指出原逻辑允许 lora_alpha 为 0,应默认使用 lora_rank。评论:"Following standard PEFT conventions, lora_alpha should default to lora_rank if not explicitly specified."

结论:代码中已采用 `lora_alpha = int(getattr(peft_config, 'lora_alpha', lora_rank) or 0)` 实现默认值 lora_rank。 · 已解决

缺少自动化测试 测试

PR body 中说明无法添加 UT,因为 PPO 完整检查点生成成本高。review 中未进一步讨论。

结论:仅通过手动合成检查点验证合并流程。 · acknowledged

风险与影响

  1. 多 rank 写入冲突风险_save_lora_train_meta 在所有 rank 上执行(无 rank 0 限制),若 local_path 共享,多个进程同时写同一文件可能导致内容损坏。不过 PPO actor 检查点通常只在 rank 0 实际执行写入,实际风险较低。
  2. 缺少测试覆盖:未添加自动化测试,仅通过手动合成检查点验证。合并流程变更依赖人工确认。
  3. 仅 FSDP 路径覆盖:Megatron 后端已移除,如果未来 Megatron PPO LoRA 用户需要使用 model_merger 合并,该路径仍会缺失元数据。但 Megatron Bridge 已有自己的 PEFT 导出路径,故风险可控。

用户:修复了 PPO LoRA 用户检查点合并后 lora_alpha 为 0 的 bug,使合并的适配器能正确生效。系统:每次 actor 检查点保存时增加一次小文件写入(约 100 字节),性能影响可忽略。团队:统一了 SFT 和 PPO 的 LoRA 元数据保存方式,降低后续维护复杂度。

多 rank 写入竞争风险 缺少测试覆盖

关联 Issue

#5326 [ckpt, model] fix: preserve lora_alpha in model_merger via training meta
#6380 train while use lora and bugs

完整报告

参与讨论