Prhub

#7242 [veomni] feat: add DeepSeek V4 support

原始 PR 作者 wuxibin89 合并时间 2026-08-04 21:45 文件变更 14 提交数 11 评论 1 代码增减 +336 / -58

执行摘要

VeOmni 引擎新增 DeepSeek V4 支持与 MXFP4 多后端回灌

verl 此前已在 Megatron 引擎上完成 DeepSeek V4 支持(PR 7221、PR 7224),本 PR 的目标是在 VeOmni 引擎上补齐同一条能力线,让 DSv4-Flash 的大规模 GRPO/GSPO 训练可以跑在 VeOmni 上并复用其 FSDP offload、ulysses/expert 并行等能力(VeOmni 侧对应 PR 为 ByteDance-Seed/VeOmni#962)。PR body 中贴出的 rollout_corr 指标(rollout_actor_probs_pearson_corr 0.9925、rollout_corr/kl 0.0053、ppl_ratio 1.0045)用于证明训练端与 rollout 端概率分布一致;同时预告了后续两步:R3 hash router replay 与 F8/FP4 QAT 训练。

值得精读。三个设计点有复用价值:一是 _build_ops_implementation_config 的「按已安装依赖字段动态过滤 + 显式设置即报错」版本解耦范式;二是 _mxfp4_staging_data 通过字节数等价的 view/dtype 重解释复用 live storage,把回灌的额外内存从「整份 MoE」降到「1/16 的 scale 缓冲」;三是回灌流程结束后用 leftover 检查兜底,任何不经过 replace_parameter 的路径都会显式失败。需要跟进的事项:为 Marlin 回灌与 converter 路径补专项测试、评估 max_num_seqs 全局默认调整、补充与 Megatron 的性能对比数据。

讨论亮点

PR 全程没有 review 评论,唯一的互动是 Issue 评论区 vermouth1992 的提问「How does the speed compare with Megatron?」,作者未回复。该问题指向 VeOmni 与 Megatron 双引擎的取舍:本 PR 只提供了训练/rollout 概率一致性指标(actor_probs_pearson_corr 0.9925、kl 0.0053、ppl_ratio 1.0045)作为正确性证据,性能对比仍缺失。决策结论:功能按原设计合入(作者自行合入),性能问题留待后续验证。

实现拆解

实现按 5 步拆解:

  1. 引擎装配与模型构建verl/workers/engine/veomni/transformer_impl.py
    - 新增 _build_ops_implementation_config:以已安装 VeOmni 的 OpsImplementationConfig 字段集合为准,动态过滤 verl 侧全部 *_implementation 配置;未知字段若处于 verl 默认值则跳过并记日志,若被用户显式设置则抛 ValueError。这是为了处理 verl 构建版本新于已安装 VeOmni 时构造器报未知关键字的问题,同时保证用户点名的 kernel 不会被静默降级。
    - _build_model_optimizerbuild_parallelize_model 调用新增 broadcast_model_weights_from_rank0=Truefqn_to_index_mapping=load_safetensors_index(...),配合 veomni/utils.py 新增的 load_safetensors_index 读取 model.safetensors.index.json,把每个 FQN 映射到分片编号,支撑 rank0 广播加载,避免大规模模型每 rank 全量读权重文件。
    - get_per_tensor_param 新增 DSv4 快捷路径:若 VeOmni 的 get_checkpoint_tensor_converter 返回的 converter 具备 export_weights,直接用 converter 导出权重并返回 (weights, None);代码注释自述该路径当前仅限 DeepseekV4,其余模型仍走 state_dict + param_generator 旧路径。旧路径同时修复了 CPU offload 场景:DTensor 需先 .to(device, non_blocking=True)full_tensor(),避免整模半迁移导致 state_dict() 崩溃。

  2. 配置扩展verl/workers/config/engine.py、各 generated YAML)
    - VeOmniEngineConfig 新增 dsa_indexer_implementationdsa_attention_implementationmhc_implementation 三个 kernel 选择器,默认 eagerverl/trainer/config/engine/veomni.yaml_generated_ppo_veomni_trainer.yaml 三处(actor/ref/critic)同步补齐。
    - 所有 generated 配置(ppo/veomni/megatron/torchtitan 及 rollout.yaml)将 vLLM rollout 的 max_num_seqs 默认值从 1024 调为 256,这是一个超出 DSv4 范围的全局默认变更。

  3. vLLM MXFP4 MoE 回灌泛化verl/utils/vllm/vllm_fp4_utils.py
    - 用 _refittable_mxfp4_backends 取代原先的 _is_deepgemm_mxfp4_moe_module 判断,支持后端从仅 DeepGEMM 扩到 DeepGEMM/MARLIN/BATCHED_MARLIN;Triton 明确排除(权重是非 Parameter 包装,无法被 _replace_parameter_in_place 拦截)。
    - _mxfp4_checkpoint_layout 追加 w13_bias/w2_bias 条目:无条件列出、缺失时跳过,防止 Marlin 的 bias 置换落入真实 replace_parameter 而悄悄搬走缓冲区。
    - 新增 _mxfp4_staging_data:字节数等价的场景(Marlin int32 tile 与 uint8 checkpoint 权重同字节数)用 view + dtype 重解释复用 live storage,回灌额外内存从整份 MoE 降到 1/16 的 scale 缓冲;否则用 zeros 兜底。
    - stage_mxfp4_moe_params_for_loading 对不支持的后端改为显式 NotImplementedError,错误信息列出支持列表。

  4. 权重同步与显存处理verl/workers/engine_workers.py
    - naive 权重同步路径在 set_expandable_segments(False) 之后追加 aggressive_empty_cache(force_sync=True),在 rollout resume 前同步清空显存缓存,为超大模型权重唤醒腾出确定的内存。

  5. 示例与测试配套
    - 新增 examples/grpo_trainer/run_deepseek_v4_veomni.sh:8 卡 FSDP offload + ulysses/expert 并行,MoE 用 fused_triton,DSA indexer/attention 与 MHC 用 tilelang,rollout 侧 vLLM(TP=1、EP=8、kv_cache_dtype=fp8),算法侧为 GSPO。
    - run_deepseek_v4_flash_megatron.sh 删除 1 行,结合最后一个 commit「remove deep_gemm config」推测为移除 deep_gemm 相关配置行(diff 未展示具体内容,属推断)。
    - tests/workers/test_router_replay_engine_helpers_on_cpu.py 仅为新增 import 补充 sys.modules mock;Marlin 回灌与 converter 路径都没有新增专项测试。

文件 模块 状态 重要度
verl/utils/vllm/vllm_fp4_utils.py 量化回灌 modified 8.28
verl/workers/engine/veomni/transformer_impl.py 引擎实现 modified 7.59
verl/workers/engine/veomni/utils.py 权重加载 modified 6.72
verl/workers/config/engine.py 引擎配置 modified 5.2
examples/grpo_trainer/run_deepseek_v4_veomni.sh 示例脚本 added 5.14
verl/workers/engine_workers.py 权重同步 modified 4.39
verl/trainer/config/_generated_ppo_veomni_trainer.yaml 训练配置 modified 3.95
tests/workers/test_router_replay_engine_helpers_on_cpu.py 单元测试 modified 3.66
verl/trainer/config/engine/veomni.yaml 引擎配置 modified 3.58
verl/trainer/config/_generated_ppo_megatron_trainer.yaml 训练配置 modified 3.07
verl/trainer/config/_generated_ppo_torchtitan_trainer.yaml 训练配置 modified 3.07
verl/trainer/config/_generated_ppo_trainer.yaml 训练配置 modified 3.07
verl/trainer/config/rollout/rollout.yaml 训练配置 modified 3.07
examples/grpo_trainer/run_deepseek_v4_flash_megatron.sh 示例脚本 modified 2.2

关键符号

_build_ops_implementation_config load_safetensors_index _refittable_mxfp4_backends _mxfp4_staging_data _mxfp4_checkpoint_layout stage_mxfp4_moe_params_for_loading _stage_mxfp4_moe_params _process_mxfp4_moe_params get_per_tensor_param update_weights

关键源码片段

verl/utils/vllm/vllm_fp4_utils.py core-logic

MXFP4 MoE 回灌的核心泛化:从仅 DeepGEMM 扩展到 DeepGEMM/Marlin/BatchedMarlin,用字节级存储重解释避免回灌时 MoE 内存翻倍,并新增 bias 条目防止 Marlin 置换悄悄搬走缓冲区。

# verl/utils/vllm/vllm_fp4_utils.py
# MXFP4 MoE 回灌(refit)的后端白名单:转换必须是以 checkpoint layout 为输入的纯函数,
# 并且结果必须经由 replace_parameter 发布,_replace_parameter_in_place 才能拦截并
# 把新权重写回 CUDA graph 已捕获的 live storage。
# Triton 是明确排除项:它把权重写成非 Parameter 包装、scale 挂在 quant_method 上,
# kernel 读取的缓冲区根本不在 Parameter 体系里,无法被拦截。def _refittable_mxfp4_backends():
    from vllm.model_executor.layers.fused_moe.oracle.mxfp4 import Mxfp4MoeBackend
​
    return (
        Mxfp4MoeBackend.DEEPGEMM_MXFP4,
        Mxfp4MoeBackend.MARLIN,
        Mxfp4MoeBackend.BATCHED_MARLIN,
    )
​
​
def _mxfp4_staging_data(param, shape, dtype):
    """为 load_weights 构造 checkpoint layout 的暂存张量。    只要字节数相同就复用 live storage:Marlin 的 repack 本质是 4bit 值的置换,
    其 int32 tile 与 uint8 checkpoint 权重字节数完全一致,view + dtype 重解释后
    reload 直接落在 kernel 读取的地址上,回灌的额外内存从整份 MoE(近乎翻倍)
    降到只有 block scale 的 1/16 缓冲。
    """
    data = param.data
    shape = torch.Size(shape)
    if data.dtype == dtype and data.shape == shape:
        # 形状 dtype 完全一致,直接原地吸收
        return data
    if data.is_contiguous() and data.numel() * data.element_size() == shape.numel() * dtype.itemsize:
        # 字节数等价:flatten 后按目标 dtype 重新解释并 reshape,零拷贝
        return data.flatten().view(dtype).reshape(shape)
    # zeros 而不是 empty:refit 流若跳过某参数,零填充会让该层输出坍缩为 0,
    # 而不是让 kernel 读取释放后内存里的随机数据。
    return torch.zeros(shape, dtype=dtype, device=param.device)
​
​
# _mxfp4_checkpoint_layout 中新增的 bias 条目是无条件列出的:
# 无 MoE bias 的模型从未注册这些参数,stage 时遇到缺失会自动跳过;
# 但若不列出,Marlin 的 bias 置换会落进真实的 replace_parameter,
# 悄悄搬走缓冲区且不经过 leftover 检查。def stage_mxfp4_moe_params_for_loading(model):
    """给 load_weights 发 checkpoint layout 的 expert 缓冲,返回待处理模块列表。"""
    supported = _refittable_mxfp4_backends()
    staged_modules = []
    backends = set()
    for module in model.modules():
        if not _is_mxfp4_fused_moe_module(module):
            continue
        backend = getattr(module.quant_method, "mxfp4_backend", None)
        if backend not in supported:
            # 显式拒绝胜过静默跳过:不支持的 backend 会把 checkpoint layout 数据
            # 写进已被重排的参数里,rollout 概率漂移且没有任何报错指向原因。
            raise NotImplementedError(
                f"mxfp4 MoE refit does not support the {backend} backend selected by "
                f"{type(module).__name__}. Supported backends: "
                f"{', '.join(str(b) for b in supported)}."
            )
        _stage_mxfp4_moe_params(module)
        staged_modules.append(module)
        backends.add(backend)
​
    logger.info(
        "Staged %d mxfp4 MoE modules for in-place refit (backends: %s)",
        len(staged_modules),
        ", ".join(sorted(str(b) for b in backends)) or "none",
    )
    return staged_modules
verl/workers/engine/veomni/transformer_impl.py dependency-wiring

VeOmni 引擎装配的核心改动:新增版本感知的 ops config 构造器、rank0 广播加载与 DSv4 converter 权重导出路径,并修复 CPU offload 下的 DTensor 处理。

# verl/workers/engine/veomni/transformer_impl.py
# verl 的 VeOmniEngineConfig 与 VeOmni 的 OpsImplementationConfig 逐字段镜像。
# 当 verl 构建版本新于已安装的 VeOmni 时,直接构造会因未知关键字报错;
# 这里按“已安装 VeOmni 实际接受的字段”动态过滤,避免 kernel 选择被静默降级。def _build_ops_implementation_config(engine_config: VeOmniEngineConfig) -> OpsImplementationConfig:
    # 以已安装 VeOmni 的字段集合为准,而不是以 verl 侧配置为准
    accepted = {f.name for f in fields(OpsImplementationConfig)}
​
    kwargs, unsupported, skipped = {}, {}, []
    for f in fields(engine_config):
        if not f.name.endswith("_implementation"):
            continue
        value = getattr(engine_config, f.name)
        if f.name in accepted:
            kwargs[f.name] = value
        elif value != f.default:
            # 用户显式点名的 kernel 在旧版 VeOmni 上不存在,直接报错而不是降级
            unsupported[f.name] = value
        else:
            skipped.append(f.name)
​
    if unsupported:
        raise ValueError(
            f"The installed VeOmni's OpsImplementationConfig has no {sorted(unsupported)}, but "
            f"they were explicitly set to {unsupported}. Upgrade VeOmni or unset these options."
        )
    if skipped:
        logger.info(
            f"Skipping {sorted(skipped)}: unknown to the installed VeOmni, left at the verl default."
        )
​
    return OpsImplementationConfig(**kwargs)
​
​
# _build_model_optimizer 中:并行化时传入 safetensors 分片索引并允许 rank0
# 广播权重复本,避免每个 rank 都全量加载大模型权重文件(DSv4 类千亿模型关键)。
module = build_parallelize_model(
    module,
    init_device=self.engine_config.init_device,
    weights_path=self.model_config.local_path,
    enable_full_shard=self.engine_config.enable_full_shard,
    mixed_precision=veomni_mixed_precision_config,
    enable_gradient_checkpointing=self.model_config.enable_gradient_checkpointing,
    enable_fsdp_offload=self.engine_config.enable_fsdp_offload,
    basic_modules=list(
        set(getattr(module, "_no_split_modules", None) or []) | set(self.engine_config.basic_modules)
    ),
    enable_reentrant=self.engine_config.enable_reentrant,
    enable_forward_prefetch=self.engine_config.forward_prefetch,
    broadcast_model_weights_from_rank0=True,
    fqn_to_index_mapping=load_safetensors_index(self.model_config.local_path),
)
​
​
# get_per_tensor_param 中:DSv4 走 VeOmni 的 checkpoint converter 直接导出权重,
# 返回 (weights, None) 表示无 peft 配置;其余模型仍走 state_dict 旧路径。
# 注意:新建路径当前仅限 DeepseekV4,后续计划统一所有模型(代码内已留 TODO)。
converter = get_checkpoint_tensor_converter(self.module)
if converter is not None and hasattr(converter, "export_weights"):
    return converter.export_weights(self.module), None
verl/workers/engine/veomni/utils.py core-logic

新增 load_safetensors_index,为 rank0 广播加载提供 FQN 到分片编号的映射,是超大模型加载路径的地基。

# verl/workers/engine/veomni/utils.py
# 读取 HF safetensors 分片索引,把每个 FQN 映射到其所在分片文件的编号,
# 供 build_parallelize_model 的 fqn_to_index_mapping 参数使用,
# 配合 broadcast_model_weights_from_rank0 实现 rank0 广播按需加载。def load_safetensors_index(model_path: str) -> dict[str, int]:
    path = os.path.join(model_path, "model.safetensors.index.json")
    if not os.path.exists(path):
        # 非分片权重(单文件或无索引)时返回空映射,调用方按旧逻辑处理
        return {}
    with open(path) as f:
        weight_map = json.load(f)["weight_map"]
    # 文件名形如 model-00001-of-00010.safetensors,取中间的分片编号
    index = {fqn: int(filename.split("-")[1]) for fqn, filename in weight_map.items()}
    return index

评论区精华

VeOmni 与 Megatron 引擎的性能对比 question

vermouth1992 在 Issue 评论区提问:How does the speed compare with Megatron? 这是 PR 唯一的互动内容,指向双引擎路径的性能取舍。

结论:作者未回复;PR 内只提供了训练 /rollout 概率一致性指标(pearson 0.9925、kl 0.0053、ppl_ratio 1.0045)作为正确性证据,没有与 Megatron 的吞吐对比基准。 · unresolved

风险与影响

  1. 测试缺口:MXFP4 Marlin/BatchedMarlin 回灌和 VeOmni DSv4 converter 加载路径没有任何专项测试,tests/workers/test_router_replay_engine_helpers_on_cpu.py 只是为新增 import 补 sys.modules mock;这些逻辑深度依赖 vLLM 内部 replace_parameterweight_loader 与 VeOmni checkpoint converter 的行为契约,上游升级可能悄悄破坏。
  2. 双路径并存:get_per_tensor_param 中 converter 导出与 state_dict/param_generator 旧路径并存,代码注释自认「currently only for DeepseekV4」,后续需统一,期间两条路径都要维护。
  3. 全局默认变更:所有 generated 配置(veomni/megatron/torchtitan/ppo 与 rollout.yaml)的 max_num_seqs 从 1024 改为 256,影响所有引擎的默认 rollout 并发,可能降低小模型默认吞吐,需要确认是否有意为之。
  4. 性能未验证:aggressive_empty_cache(force_sync=True) 追加在 naive 权重同步关键路径上,引入同步清缓存开销;与 Megatron 的对比问题未答复,性能风险未量化。
  5. 版本耦合:dsa_*/mhc_implementation 三个新配置依赖 VeOmni 0.1.12+,老版本显式设置会抛 ValueError,用户需先升级 VeOmni 才能显式使用这些 kernel。

用户侧:DeepSeek V4-Flash 用户新增一条 VeOmni 训练路径(FSDP offload + ulysses/expert 并行 + tilelang DSA/MHC,配合 vLLM rollout 与 kv_cache_dtype=fp8);同时 vLLM 量化 MoE 回灌从 DeepGEMM 扩展到 Marlin/BatchedMarlin,Hopper 用户直接受益。系统侧:DSv4 支持从 Megatron 单引擎扩展到 VeOmni 双引擎,为后续 R3 hash router replay 与 FP8/FP4 QAT 训练铺路。流程侧:作者自合且无 review,11 次提交中可见多轮返工(fqn_to_index_mapping 提交两次、DSA 配置、CPU offload 修复、CI 修复等),说明与 VeOmni 上游 PR 962 的联调过程较曲折。

缺少专项测试 全局默认配置变更 双导出路径并存 性能对比待验证 自合并且无 review

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论