Prhub

#2213 fix(fsdp): apply the GDN packing patch to dense qwen3_5 and fix patch

原始 PR 作者 Rockdu 合并时间 2026-08-06 05:01 文件变更 3 提交数 2 评论 1 代码增减 +8 / -9

执行摘要

修复 dense Qwen3.5 的 GDN packing 状态泄漏与边界残留

PR body 明确指出两个动机:一是 dense Qwen3.5 的建模代码位于独立的 transformers.models.qwen3_5 模块(与 qwen3_5_moe/qwen3_next 分属不同模块),原 patch 循环只遍历后两者,dense 架构从未被打上 packing patch,但 "packing fix applied" 日志仍会被 moe/next 类触发,形成“日志显示已修复、实际未覆盖”的静默跳过;二是边界属性 _gdn_cu_seqlens/_gdn_seq_idx 是 decoder wrapper(生产者)与 GDN wrapper(消费者)之间的 mailbox,原实现只在 packed forward 时写入,当单个样本长度恰为 pad 整数倍时会出现无 pad 伪文档的单文档行,沿用上一行旧边界会导致 conv kernel 崩溃(seq_idx must have shape (batch_size, seqlen))或静默错位。

值得精读。两个设计点值得借鉴:

1) 以 decoder wrapper 为 producer、GDN wrapper 为 consumer 的边界 mailbox 模式,以及“无条件写入 + None 清理”同时解决泄漏与陈旧状态;
2) “packing fix applied 日志由其他架构触发”导致的静默跳过陷阱——日志不足以证明覆盖范围,枚举架构时应反向断言每个目标都被 patch。若团队继续维护 FSDP 后端,建议为 packing patch 增加 fast 级单元测试。

讨论亮点

唯一的 review 讨论来自 Zhichenzzz 的评论 “rename the file qwen3_5_moe to qwen?”,作者在 issue 评论中回复 “Renamed qwen3_5_moe -> qwen3_5 for all related files”。这是命名/维护性建议(dense 与 MoE 架构不应共用一个带 _moe 后缀的文件),已在第二个 commit 中通过重命名 models/qwen3_5_moe.pyadaptations/specs/qwen3_5_moe.pyqwen3_5.py 并同步更新导入解决,Zhichenzzz 最终 APPROVED。另据 PR body 自述,原计划新增的测试按 review 移除,改为以实测数据作为行为证据。

实现拆解

  1. 扩展 GDN patch 覆盖范围:在 miles/backends/experimental/fsdp_utils/models/qwen3_5.pyapply_gateddeltanet_packing_patch() 中,模块循环由 ("qwen3_5_moe", "qwen3_next") 扩展为 ("qwen3_5", "qwen3_5_moe", "qwen3_next"),使 dense Qwen3.5 的 GatedDeltaNetDecoderLayer 类也被 _patch_gdn_forward_patch_decoder_forward 包装,修复其 linear-attention 层在 THD packing 下的状态泄漏。

  2. 边界属性无条件写入_patch_decoder_forward 内的 forward 包装移除 if ctx is not None 短路,改为每次前向都遍历 self.modules(),对每个 GDN 层写入 ctx.cu_seqlens/ctx.seq_idx(packed 行)或 None(非 packed 行)。None 写入让 GDN wrapper 走到干净路径,等价于单文档前向,避免陈旧几何信息残留。

  3. 重命名对齐:按 reviewer 建议将 models/qwen3_5_moe.pyadaptations/specs/qwen3_5_moe.py 重命名为 qwen3_5.py,并更新 adaptations/specs/__init__.py 的导入(qwen3_5_moeqwen3_5),保持 “imports trigger registration” 的注册链路与 spec docstring 中的路径引用同步。

  4. 验证与配套:未新增自动化测试(PR body 注明按 review 移除,行为由 8×H200 live GRPO wandb 对比曲线和 eager 模式隔离测试覆盖);无新增 launch flags,无需更新 CLI reference 文档。

文件 模块 状态 重要度
miles/backends/experimental/fsdp_utils/models/qwen3_5.py FSDP 适配 renamed 6.85
miles/backends/experimental/fsdp_utils/adaptations/specs/qwen3_5.py 适配注册 renamed 5.07
miles/backends/experimental/fsdp_utils/adaptations/specs/__init__.py 适配注册 modified 4.09

关键符号

_patch_decoder_forward apply_gateddeltanet_packing_patch

关键源码片段

miles/backends/experimental/fsdp_utils/models/qwen3_5.py core-logic

核心修复文件:把 dense qwen3_5 模块纳入 GDN packing patch 的模块循环,并将 decoder wrapper 的边界 mailbox 写入改为无条件执行(非 packed 写 None),解决状态泄漏与陈旧边界残留。

# miles/backends/experimental/fsdp_utils/models/qwen3_5.py(由 qwen3_5_moe.py 重命名而来)def _patch_decoder_forward(dl_cls, gdn_cls):
    orig = dl_cls.forward
    if getattr(orig, "_gdn_packing", False):
        return # 幂等:同一 DecoderLayer 类只 patch 一次
​
    @functools.wraps(orig)
    def forward(self, *args, **kwargs):
        # 每次前向无条件刷新边界 mailbox:
        # 非 packed 行(如单样本长度恰为 pad 倍数)必须清为 None,
        # 否则上一行残留的 cu_seqlens/seq_idx 会导致 conv kernel 崩溃或静默错位。
        ctx = packed_seq_context(kwargs.get("position_ids"))
        for module in self.modules():
            if isinstance(module, gdn_cls):
                module._gdn_cu_seqlens = ctx.cu_seqlens if ctx is not None else None
                module._gdn_seq_idx = ctx.seq_idx if ctx is not None else None
        return orig(self, *args, **kwargs)
​
    forward._gdn_packing = True
    dl_cls.forward = forward
​
​
def apply_gateddeltanet_packing_patch():
    """Patch 每个含 GatedDeltaNet 的混合架构(幂等)。返回是否 patch 了任何类。"""
    patched = False
    # 覆盖 dense qwen3_5(独立 transformers 模块)、qwen3_5_moe 与 qwen3_next;
    # 若漏掉 dense 模块,“packing fix applied” 日志仍会被 moe/next 触发,形成静默跳过。
    for mod_name in ("qwen3_5", "qwen3_5_moe", "qwen3_next"):
        try:
            mod = __import__(f"transformers.models.{mod_name}.modeling_{mod_name}", fromlist=["x"])
        except Exception:
            continue # 当前环境未安装该架构时跳过
        gdn_cls = _find_class(mod, "GatedDeltaNet")
        dl_cls = _find_class(mod, "DecoderLayer")
        if gdn_cls is None or dl_cls is None:
            continue
        _patch_gdn_forward(gdn_cls)
        _patch_decoder_forward(dl_cls, gdn_cls)
        patched = True
​
    if patched:
        logger.info(
            "[fsdp] GatedDeltaNet packing fix applied: cu_seqlens/seq_idx reset the "
            "linear-attn recurrence and causal-conv state per packed document"
        )
    return patched

评论区精华

qwen3_5_moe 文件是否应重命名为 qwen3_5 设计

审核者 Zhichenzzz 评论建议 “rename the file qwen3_5_moe to qwen?”,作者在 issue 评论中回复 “Renamed qwen3_5_moe -> qwen3_5 for all related files”。

结论:作者按建议将 models/qwen3_5_moe.py 与 specs/qwen3_5_moe.py 统一重命名为 qwen3_5.py,并同步更新 specs/__init__.py 导入与 docstring;Zhichenzzz 最终 APPROVED。 · 已解决

风险与影响

  1. 正确性依赖:无条件写入依赖 packed_seq_contextposition_ids 的解析。若未来存在不经过 position_ids 触发的 packing 路径(如直接传入 cu_seqlens),可能漏判;不过当前对 ctx is None 走清理路径,比旧实现更安全。
  2. 性能:每次 decoder forward 都多一次 self.modules() 遍历(原 packed 行同样遍历,非 packed 行短路)。这是 O(层数) 的 Python 级扫描,对训练吞吐影响可忽略,但超长序列下需留意。
  3. 兼容性:文件重命名会影响所有引用旧模块路径的代码;仓库内 import 已同步,但外部脚本或未合并分支若直接 import miles.backends.experimental.fsdp_utils.models.qwen3_5_moe 会得到 ImportError。
  4. 缺少自动化测试:无 fast/e2e 测试断言 _patch_decoder_forward 在 packed/non-packed 两种输入下的属性写入行为,回归保障依赖人工实验;建议补充针对 apply_gateddeltanet_packing_patch 覆盖枚举的断言测试。

影响用户:使用 FSDP 后端做 Qwen3.5 dense RL 训练的任务将获得正确的 per-document 状态重置,train/rollout logprob 差异回到约 6e-3 的 kernel 噪声水平,并消除 conv kernel 偶发崩溃。影响系统:改动集中在 miles/backends/experimental/fsdp_utils(实验性 FSDP 后端)的 THD packing 路径,不影响 Megatron 主路径,性能开销可忽略。影响团队:文件命名从 qwen3_5_moe 收窄为 qwen3_5,与 transformers 建模模块名对齐;模块循环显式覆盖三个架构,避免“日志发出但实际未生效”的静默跳过陷阱再次发生。

缺少测试覆盖 文件重命名 实验性后端 训练正确性

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论