Prhub

#7181 [megatron] feat: delta_sharded on Megatron-Bridge param mappings (TP+EP, hybrid-Mamba)

原始 PR 作者 ChangyiYang 合并时间 2026-07-31 10:39 文件变更 12 提交数 23 评论 5 代码增减 +1079 / -30

执行摘要

Megatron 后端接入分片 delta 权重同步

让 Megatron 后端加入分片 delta 权重同步契约。追踪 issue #7060 的路线图中,FSDP + EP 的 shard 导出已由 #7144 与 #7085 完成,mcore 是下一块拼图。PR body 开宗明义:'The mcore backend joins the sharded-delta contract (#7144)',且实现 'rides Megatron-Bridge's own per-param machinery — no legacy converters, no hand-written index math'。动机有两层:一是让 Megatron 用户获得 8–10× 的权重同步加速(235B 规模下稳态从 NCCL 全量广播的 235.0–237.6 s/step 降到 23.6 s/step,seed 同步成本低于一次 NCCL 步骤);二是不再为每个新模型手写转换器与索引数学——probe 机制让 Mamba 的 TP 解交错、GQA 融合等带真实数值语义的转换保持位级精确。

值得精读。核心看点:(1) comm-stubbed probe 设计——把进程组的 size/rank 语义与通信语义分离,用 NaN 哨兵穿过真实 megatron_to_hf 代码得到 HF 坐标,替代手写索引数学;(2) 幂等性验证扫描 _verify_dense 对多阶段 loader(如 Mamba 的 A = -exp(A_log))按最终状态而非逐次 copy_ 比对的判断;(3) 测试分层——TP>1 差分 oracle + 真 sglang 闭环 + CPU 单测构成对 probe 假设的完整回归防线。建议关注 PP/LoRA 的显式报错边界,以及将差分测试纳入 CI 的后续工作。

讨论亮点

Issue 评论区有两轮关键问答,均发生在 #7060 追踪 issue 下:

  1. ISEEKYAN 询问是否有定义 delta 格式的协议文件,倾向训练后端无关协议(类似 HF safetensor 之于全量参数);ChangyiYang 回应每个训练后端实现 get_per_tensor_param_delta_shard,其输出格式即协议——最终 HF 坐标稀疏 entry,定义在 verl/workers/engine/spec.py,线上编码在 delta_checkpoint_engine.py 头部;越过该边界后的 gather、分桶、接收端全部后端无关。

  2. wuxibin89 询问是否支持流水线并行;ChangyiYang 明确 PP 不支持且会立即抛错,由 @gxlvera 后续支持。

另有 commit 0b3e9745 记录的 review 反馈:策略旋钮还给所有者(policy knobs move to their owners)——快照 pin 从环境变量 VERL_DELTA_PIN 改为 BaseEngine 类级 delta_pin_snapshots 属性,由 MegatronEngine 覆盖为 False

实现拆解

  1. 导出索引构建(一次性、懒加载)verl/workers/engine/megatron/transformer_impl.py 新增 _mcore_export_index(),首次调用时经 delta_export.build_export_index() 基于 AutoBridge.get_conversion_tasks 枚举全部参数映射,为每个参数生成几何 spec 与 form-B 探针,并缓存 _delta_export_index / _delta_export_by_name / _delta_slot_cache。构建处对 vanilla_bridgepeft_cls 断言——旧式桥与 LoRA 直接拒绝。

  2. comm-stubbed probe(核心机制):新增 delta_export.py(307 行)。_ProbeGroup 保留真实 size()/rank(),任何真实通信通过 __getattr__ 抛错;make_probe 把 bridge 的四个通信助手(gather_from_tp_ranks / gather_from_ep_ranks[_scale] / PP 广播的 pp_size == 1 快路径)stub 为本地合成。本 rank 局部 shard 以 NaN 缓冲喂入真实 megatron_to_hf,非 NaN 幸存者即该 rank 在最终 HF 坐标的贡献。_warm_lazy_mappings 强制 AutoMapping 懒 delegate 在探针外完成实例化(否则首次探针内使用会捕获真实进程组)。_nan_block(shape, dtype, device) 池化 NaN 占位块,消解 235B 下 Opt-A 回归(+25–40%,恢复至 23.6 s)。

  3. 引擎接口与内存策略MegatronEngine 新增 get_per_tensor_param_shard()(spec 只携带线上合并组,几何全部由探针负责)、_hf_delta_entry()get_per_tensor_param_delta_shard();类属性 delta_pin_snapshots = False——整组 pin 快照在 30B/235B 下耗尽 cudaHostAlloc 池并表现为无关分配的 CUDA OOM。base.pyengine/utils.py 将 pin 决策收敛为引擎类级属性、prime_delta_snapshots 改为显式 pin 参数(review 驱动,见 commit 0b3e9745)。

  4. 发送端校验与内存回收delta_checkpoint_engine.py 新增 verify_every 引擎参数——每 K 次稳态同步在同一 receive 会话内追加完整 seed 重放(_publish_values_flush 携带 verify / is_last 元数据);_release_staging_pool() 每次发送后归还 cupy 暂存池块并打 warning 日志(种子全量同步会经池传输最高 2× bucket_size 数据,此前对 torch 分配器不可见);空 delta 走 slot 缓存零计数短回路(Opt-B,commit 4358b94a)。

  5. 接收侧验证与测试配套sglang_rollout/delta_loader.py 新增 _verify_dense()——用 snap_then_copy_ 包装 torch.Tensor.copy_,在真实加载路径执行前快照所有写目标,按最终状态逐位比对(而非逐次 copy_,规避 Mamba 多阶段 loader 的 raw-vs-transformed 误报)。测试分三层:TP>1 差分 oracle(test_mcore_probe_differential.py,真实 megatron_to_hf vs probe 组装位级一致,覆盖 qwen2 / qwen3_moe / nemotron_h / falcon_h1)、真 sglang 引擎闭环(test_sglang_delta_loop.py,扰动 → 稀疏 flush → update_weights_from_tensor → 幂等性验证)、CPU 验证扫描(test_sglang_loader.py 一致状态通过 / 分歧状态响亮失败两个用例)。

文件 模块 状态 重要度
verl/workers/engine/megatron/delta_export.py 导出器 added 9.17
verl/checkpoint_engine/delta_checkpoint_engine.py 检查点引擎 modified 8.53
verl/workers/engine/megatron/transformer_impl.py 模型引擎 modified 8.39
verl/workers/rollout/sglang_rollout/delta_loader.py 权重加载 modified 7.44
tests/special_distributed/test_mcore_probe_differential.py 差分测试 added 7.52
tests/special_distributed/test_sglang_delta_loop.py 闭环测试 added 7.43
tests/checkpoint_engine/test_sglang_loader.py 加载器测试 modified 6.26
verl/workers/engine/utils.py 引擎工具 modified 6.74
verl/workers/engine/base.py 引擎基类 modified 5.75
verl/utils/megatron_utils.py 工具函数 modified 5.17
verl/workers/engine/spec.py 协议定义 modified 4.95
tests/checkpoint_engine/test_sharded_delta.py 分片测试 modified 3.84

关键符号

build_export_index make_probe _mcore_export_index get_per_tensor_param_shard _hf_delta_entry get_per_tensor_param_delta_shard _verify_dense snap_then_copy_ _release_staging_pool _publish_values_flush prime_delta_snapshots

关键源码片段

verl/workers/engine/megatron/delta_export.py core-logic

新增 307 行的核心文件:基于 AutoBridge.get_conversion_tasks 构建导出索引,实现 _ProbeGroup 与 NaN 哨兵探针机制,是本次变更的心脏。

# verl/workers/engine/megatron/delta_export.py
# 探针核心:在 probe 副本上执行 mapping 真实的 megatron_to_hf 转换代码,
# 但把“通信”替换为本地合成,把“规模 / 秩”保留为真实值。
# 进程组在此携带两种可分离语义:size/rank(数值计算消费,例如 Mamba 的
# local_dim = global // tp_size)与 communication(真实集合通信)。
# 探针只替换后者:组变为 _ProbeGroup,bridge 的通信助手被 stub 成
# 本地合成——gather_from_tp_ranks 在本 rank 的真实下标处返回本分片,
# 其余 rank 位置用 NaN 占位(那些 rank 的贡献由它们自己的探针导出)。class _ProbeGroup:
    """size/rank 保真的进程组替身:megatron_to_hf 内的数值计算读到真实
    并行规模;任何真正尝试通过该组通信的行为都会立刻抛错——探针已 stub
    掉 bridge 的通信助手,其他路径触达进程组即代表未覆盖的通信模式。"""
​
    def __init__(self, size: int, rank: int):
        self._size = int(size)
        self._rank = int(rank)
​
    def size(self) -> int:
        return self._size
​
    def rank(self) -> int:
        return self._rank
​
    def __getattr__(self, name):
        raise RuntimeError(
            f"probe process group asked for {name!r}: this mapping communicates "
            "outside the stubbed helpers (gather_from_tp_ranks / "
            "gather_from_ep_ranks[_scale] / pp broadcasts) -- extend "
            "make_probe's comm stubs before trusting its export"
        )
​
​
_NAN_POOL: dict = {}
​
​
def _nan_block(shape, dtype, device) -> torch.Tensor:
    """按 (shape, dtype, device) 池化的只读全 NaN 占位块:gather stub
    每次探针调用都为每个参数分发这些块,而它们只被读取(转换是函数式),
    因此每个不同 shape 只需一个块服务整个模型整个运行周期,避免每次
    同步都 cudaMalloc + fill(这在 235B 规模上是可测的稳态回归)。"""
    key = (tuple(shape), dtype, str(device))
    t = _NAN_POOL.get(key)
    if t is None:
        t = torch.full(tuple(shape), float("nan"), dtype=dtype, device=device)
        _NAN_POOL[key] = t
    return t
verl/checkpoint_engine/delta_checkpoint_engine.py core-logic

发送端核心逻辑:新增 verify_every 幂等性扫描、_publish_values_flush 的 verify 元数据、_release_staging_pool 回收 cupy 暂存池,是生产级验证链路的发送半场。

# verl/checkpoint_engine/delta_checkpoint_engine.py
# 种子同步走 values-only 全量线:wire 编码标签维持 "dense" —— 它是协议,
# 与接收侧 delta_loader 的解码共享。verify 标志使该 flush 同时充当
# 幂等性扫描的载荷:接收端把 trainer 的完整当前权重重放到已同步服务器,
# 必须是无操作。def _publish_values_flush(
    self, params: list[DeltaParam], values: torch.Tensor, is_last: bool, verify: bool = False
) -> None:
    values = values.contiguous()
    empty_pos = torch.empty(0, dtype=torch.uint8, device=values.device)
    meta = {
        "is_full": True,
        "encoding": "dense",
        "is_last": is_last,
        "terminal_empty": False,
        "pos_numel": 0,
        "val_numel": int(values.numel()),
        "val_dtype": str(values.dtype).replace("torch.", ""),
        "spec": {
            "encoding": "dense",
            "verify": verify,
            "is_last": is_last,
            "params": [vars(p) for p in params],
            "checksum": int(_checksum(empty_pos, values)),
        },
    }
    self.socket.send_string(self.topic, flags=zmq.SNDMORE)
    self.socket.send_pyobj(meta)
    val_u8 = values.contiguous().view(torch.uint8)
    # 先落入 cupy 缓冲再广播:ray 的 NCCL broadcast 在独立流上入队,
    # 对零拷贝视图广播会与释放后的 allocator 复用竞争。
    val_cp = cp.empty(val_u8.numel(), dtype=cp.uint8)
    val_cp[:] = cp.asarray(val_u8)
    collective.broadcast(val_cp, src_rank=0, group_name=self.group_name)
​
​
def _release_staging_pool(self, phase: str) -> None:
    """把 cupy 暂存池的块归还 CUDA 并记录证据:种子全量同步经池传输最高
    2× bucket_size 数据,这些内存在归还前对 torch 分配器与裸 cudaMalloc
    (NCCL workspace、优化器缓冲)不可见,在紧张的 mcore 显存下会 OOM。"""
    from verl.utils.device import get_torch_device
​
    pool = cp.get_default_memory_pool()
    held = pool.total_bytes()
    free_before, _ = get_torch_device().mem_get_info()
    pool.free_all_blocks()
    free_after, _ = get_torch_device().mem_get_info()
    logger.warning(
        "cupy staging pool after %s send: held %.2fGB; device free %.2f->%.2fGB on release",
        phase, held / (1 << 30), free_before / (1 << 30), free_after / (1 << 30),
    )
verl/workers/engine/megatron/transformer_impl.py core-logic

MegatronEngine 的 delta 导出入口:_mcore_export_index 懒构建索引、get_per_tensor_param_shard / _hf_delta_entry / get_per_tensor_param_delta_shard 构成 mcore 侧协议实现,delta_pin_snapshots = False 是 30B/235B 内存策略的关键决策。

# verl/workers/engine/megatron/transformer_impl.py(MegatronEngine)
# mcore 常驻模型并行本地参数并每步搬运大型 host 缓冲区;在其上再整组
# pin 分片快照会耗尽节点 cudaHostAlloc 池,表现为无关分配的 CUDA OOM
# (30B/235B 规模实测)——故 mcore 默认 pageable 主机内存。
delta_pin_snapshots = False
​
​
def _mcore_export_index(self):
    """一次性构建逐参数 delta 导出索引:几何 spec 与 form-B 探针均由
    bridge 的转换任务推导(见 delta_export.build_export_index)。
    delta_sharded 基于 Megatron-Bridge 参数映射,vanilla mbridge 与
    LoRA 在此直接断言拒绝。"""
    index = getattr(self, "_delta_export_index", None)
    if index is None:
        from .delta_export import build_export_index
​
        assert not self.vanilla_bridge, (
            "megatron delta_sharded is built on Megatron-Bridge param "
            "mappings; the deprecated vanilla mbridge flavor is not supported"
        )
        assert self.peft_cls is None, "megatron delta_sharded does not support LoRA"
        index = build_export_index(self.bridge, self.module)
        self._delta_export_index = index
        self._delta_export_by_name = {rec.megatron_name: rec for rec in index}
        self._delta_slot_cache = {}
    return index
​
​
def get_per_tensor_param_shard(self, **kwargs):
    """逐个产出每个 rank 的本地 mcore 分片 (name, local_flat_bf16, ShardSpec):
    spec 只携带线上合并组(几何全部由通信 stub 的探针负责),
    纯导出无副作用;TP + EP 仅限(PP=1 在索引构建处断言)。"""
    load_megatron_model_to_gpu(self.module, load_grad=False)
    index = self._mcore_export_index()
​
    def _gen():
        for rec in index:
            local = rec.param.data
            if local.is_floating_point() and local.dtype != torch.bfloat16:
                local = local.to(torch.bfloat16)
            yield rec.megatron_name, local.reshape(-1), rec.spec
​
    return _gen(), None
​
​
def _hf_delta_entry(self, name, spec, place, lidx, lval):
    """mcore 的逐参数 entry 构建:用 NaN 哨兵探针调用 bridge 自己的转换器
    (通信被 stub 的副本——真实组规模、本地合成 gather),
    槽位表冻结在 slot_cache 后,空 delta 可走零计数快路径。"""
    from .delta_export import mcore_hf_delta_entry
​
    rec = self._delta_export_by_name[name]
    return mcore_hf_delta_entry(rec, place, lidx, lval, self._delta_slot_cache)

评论区精华

delta 格式协议文件与后端无关性 设计

ISEEKYAN 在 issue #7060 评论区询问是否有定义 delta 格式的协议文件,倾向训练后端无关协议(类比 HF safetensor 之于全量参数),各后端各自实现。ChangyiYang 回应:每个训练后端实现 get_per_tensor_param_delta_shard,其输出格式即协议——最终 HF 坐标稀疏 entry,定义在 verl/workers/engine/spec.py,线上编码见 delta_checkpoint_engine.py 头部;越过该边界后的 gather、分桶、接收端全部后端无关。

结论:协议边界确认:spec.py 的 ShardSpec 与 HF 坐标 entry 为后端无关协议,mcore 实现遵循同一契约。 · 已解决

流水线并行(PP)支持范围 question

wuxibin89 询问是否支持流水线并行。ChangyiYang 回应 PP 不支持且会立即抛错,由 @gxlvera 后续支持。

结论:PP 明确超出本 PR 范围,导入器显式报错兜底,后续由 @gxlvera 承接。 · 已解决

快照 pin 策略的归属(review 驱动调整) 设计

commit 0b3e9745 记录 review 意见(policy knobs move to their owners):快照 pin 从环境变量 VERL_DELTA_PIN 改为 BaseEngine 类级 delta_pin_snapshots 属性,由 MegatronEngine 覆盖为 False——整组 pin 在 30B/235B 下表现为无关分配的 CUDA OOM。

结论:内存策略归引擎所有者决策,mcore 默认 pageable 快照。 · 已解决

风险与影响

  1. 适用范围硬边界:PP/VPP>1 与 LoRA 在 _mcore_export_index 与索引构建处断言报错,使用这些配置的 Megatron 用户无法启用 delta_sharded,需要显式 fallback 到全量同步路径,文档中应明确提示。
  2. probe 假设依赖手工回归测试:两个核心假设(通信限定在 stub 助手内、变换重排而非算数混合——混合会吃掉 NaN 哨兵)的守护者是 test_mcore_probe_differential.py,它是手工驱动的 special_distributed 脚本、未接入 CI;Megatron-Bridge 升级后若未重跑可能静默失真。
  3. 测试脚本残留个人路径test_sglang_delta_loop.pytest_mcore_probe_differential.pysys.path.insert(0, os.environ.get("VERL_PATH", "/home/changyi/verl_ab_pre")) 的缺省值指向作者本机目录,他人在无 VERL_PATH 环境变量时运行可能导入错误代码。
  4. 全局 monkey-patch_verify_dense 临时替换 torch.Tensor.copy_ 是进程级操作,仅在显式开启 verify_every 时执行,但扫描期间其他线程的并发 copy_ 会被纳入快照统计。
  5. 内存权衡:cupy 暂存池在种子同步后会持有最多 2× bucket_size 内存(已通过 _release_staging_pool 回收并打日志);快照默认 pageable 以规避 cudaHostAlloc 池饥饿(30B/235B 下实测),代价是每步快照读写的性能损失。

对用户的直接收益:Megatron-Bridge 用户(TP + EP 配置)可获得 8.2–9.7× 的权重同步加速(235B 稳态 23.6 s vs 全量 NCCL 235.0–237.6 s),并继承 seed 全量导出的断点续训能力、sglang 侧无全模型镜像的内存画像。对系统而言,这是 delta 协议第三个后端(FSDP / veomni / mcore)的落地,验证了「输出格式即协议」的后端无关设计,为 #7060 路线图的 PP 支持及后续优化(closed-form 索引表 #7060、PP 稳态中继)铺路。对团队而言,23 个 commit 呈现高频自审与迭代过程,是后续后端接入的参考范本。

仅限 TP + EP(PP 立即报错) probe 假设依赖手工回归测试 快照默认 pageable 内存 全局 patch copy_ 的验证扫描 测试脚本残留个人路径

关联 Issue

#7060 [Tracking] Sharded delta weight sync (delta_sharded): roadmap & known issues

完整报告

参与讨论