[Task32] feat: fla mcore chunkwise cp stage 1 - #251
Open
jambow0320 wants to merge 3 commits into
Open
jambow0320 wants to merge 3 commits into
jambow0320 wants to merge 3 commits into
Conversation
Compatibility layer only: FLA 0.4.1 -> 0.4.2 and a selective backport of Megatron-LM `5139086e` (NVIDIA/Megatron-LM#3282) onto the pinned MCore `85bced0a`. No Relax routing changes -- `relax/` is untouched, `auto` never selects chunkwise, and every existing recipe runs the same path as before. RFC: redai-studio#213, reworked per the 2026-08-05 review decisions: * the GDN CP mode is **static** for the whole process. There is no per-call override; `linear_cp_mode` is read by both the construction-time head check and by `GatedDeltaNet.forward`, and nothing in a forward writes to `self` or the shared config. Dynamic CP varies only `cp_group` / `local_cp_size`. * **v1 depends on #3282 only.** Nothing from the still-open #5664 is included. # ⭐ Feature ## MCore backport (docker/patch/megatron/20260805-85bced0ae.patch) - New `megatron/core/context_parallel_layout.py`, **byte-identical to `5139086e` below the module docstring**: zigzag <-> contiguous THD/SBHD partitions and a single-all-to-all swap between them. The THD swap rebuilds its routing from `cu_seqlens` per call, which is upstream behaviour. - `packed_seq_params.py`: `resolve_cp_group()` only. The dataclass field list is untouched. - `transformer_config.py`: `linear_cp_mode` with headwise `% (tp*cp)` vs chunkwise `% tp` head divisibility. Default is `headwise`, NOT upstream's `chunkwise`, so upgrading the image cannot silently reroute a recipe. `all_gather` is accepted as a third declared value using the TP-only rule, so the declared config equals the resolved `--gdn-cp-mode` rather than declaring one mode while running another. Unknown values, including an unresolved `auto`, assert at construction. - `gated_delta_net.py`: `_resolve_cp_routing()` gives the whole CP group to exactly one of headwise / chunkwise and `None` to the other; validates `local_cp_size == cp_group.size()`; never creates a process group; short circuits on `cp_size == 1` before the mode is read so a CP=1 micro-batch is legal under any declared mode; raises if `all_gather` reaches MCore's forward with cp>1 (the Relax wrapper was not installed). Plus zigzag<->contiguous conversion around conv + scan and `cp_context` for both FLA kernels. - Backwards compatible by construction: `cp_context=` is only passed when chunkwise is active, so with it off the FLA call is byte-identical to before and still works against FLA 0.4.1; `_prepare_qkv_for_gated_delta_rule` gains an optional argument so Relax's all-gather fallback keeps calling it unchanged; both existing Relax GDN fixes are preserved verbatim. ## Dependency - `docker/Dockerfile`: `flash-linear-attention==0.4.2` (first release carrying `fla.ops.cp`), plus build-time capability assertions after the FLA install and after the patch apply. One of them asserts `linear_cp_mode` is **absent** from `GatedDeltaNet.forward`, so re-introducing a per-call override fails the build. --- # ✅ Tests ## tests/backends/megatron/test_gdn_chunkwise_cp_layout.py (45 CPU tests) - Both partitions cover every token exactly once for CP in {1,2,4,8} and are permutations of each other. - MCore's zigzag partition is token-for-token identical to Relax's `slice_with_cp` and `gdn_cp_slice`. - Construction gate: default is `headwise`; chunkwise and `all_gather` use the TP-only head rule; `auto` and other unresolved values are rejected. ## tests/backends/megatron/test_gdn_chunkwise_cp_gpu.py (7 NCCL tests) - FLA `causal_conv1d` / `chunk_gated_delta_rule` under `cp_context` vs no CP, in fp32 and bf16, including `dweight`/`dbias`. - Full `GatedDeltaNet` CP=2 vs CP=1 in fp32 and bf16, with headwise run side by side as the control: in fp32 the two CP algorithms' deviation from CP=1 agrees to 1.00x-1.03x per tensor. - zigzag -> contiguous -> zigzag over a real CP group is token-exact, for packed THD with unequal-length samples and for SBHD. - Illegal combinations fail fast: `all_gather` reaching MCore's forward, mismatched `local_cp_size`, chunkwise + deterministic, chunkwise + inference. - `state_dict` / `sharded_state_dict` keys and shard dims identical across CP=1 / headwise / chunkwise. ## tests/backends/megatron/gdn_cp_numeric_probe.py - Cross-image probe for RFC 3.4-2. Old image vs new image on CP=1 / headwise / all-gather: 41 of 45 tensors bitwise identical and **0 outside tolerance**. The four that differ are at relative RMS 1e-9..6e-7 with cosine 1.0000000000 -- FLA 0.4.2 reorders a few backward reductions. All-gather is 18 of 18 bitwise identical. --- # 📝 Documentation - `docker/patch/megatron/TASK32-BACKPORT.md`: file-level and hunk-level record of what came from `5139086e`, which Relax adaptations were made, which existing Relax GDN fixes are preserved, and what was excluded. Includes two mechanical commands a reviewer can run to confirm the new module matches upstream byte-for-byte and that no #5664 content is present. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| - seq_len, | ||
| + seq_len_global, | ||
| "cu_seqlens_q", | ||
| + cp_size=cp_size_chunkwise, |
Contributor
There was a problem hiding this comment.
这里是如果是 headwise 会有问题吗?是否需要改成 cp_size=self.cp_size?
Author
There was a problem hiding this comment.
确实,headwise 后续也会执行 cu_seqlens_q // cp_size_headwise,原先传 cp_size_chunkwise 会在 headwise 下变成 1,导致跳过 per-sequence divisibility check (虽然其实在relax的逻辑中,headwise的cu_seqlens_q数据会在data.py中就提前check,所以其实不会出问题,但是为了统一和上游pr的异同,我们还是在这里把这块check加上)。已提交最新commit,改为传当前 resolved cp_size = cp_group.size()。
没有使用 self.cp_size,因为 dynamic CP 下它指的是静态最大 CP,可能大于当前 micro-batch 的 runtime CP,使用cp_size才是真正的runtime CP。
This was referenced Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Task 32 第一阶段:FLA 0.4.2 与 MCore GDN Chunkwise CP 兼容层
对应 RFC:redai-infra/Relax#213。
上游参考:NVIDIA/Megatron-LM#3282,merge commit
5139086e。1. Summary
这个 PR 只完成第一阶段的依赖和 MCore 能力准备:
flash-linear-attention从0.4.1精确升级到0.4.2;85bced0a;TransformerConfig.linear_cp_mode能正式声明三种构造模式:headwisechunkwiseall_gather这个 PR 不修改
relax/目录中的运行时路由,也不新增 CLI 参数。--gdn-cp-mode、auto解析和 all-gather dispatcher 收敛留给第二阶段。为什么 MCore 需要声明
all_gatherMCore 自己只实现 headwise 和 chunkwise;all-gather 算法仍由 Relax 现有 fallback 实现。
但 all-gather 和 chunkwise 都只要求:
而 headwise 要求:
如果 MCore 只有
headwise/chunkwise两个声明值,Relax 为了构造 all-gather 模型只能:chunkwise,实际运行 all-gather;或TransformerConfig.__post_init__,临时修改 head 数骗过校验。两种方式都会让“构造时声明的算法”和“运行时执行的算法”不一致。
因此本 PR 增加 Relax-only
all_gather声明:auto在模型构造前显式解析为headwise或all_gather,不再 monkey-patch head 数。2. Background
Relax 当前 GDN CP 有两条路径:
Headwise
通过 all-to-all 将“每卡一段序列”转换成“每卡一部分 head、完整序列”。
优点:
限制:
Relax all-gather fallback
当 head 无法被
TP×CP整除时,Relax:in_proj;out_proj。优点:
代价:
Chunkwise
每个 rank 保留一段连续时间片和全部 TP-local heads,只跨 rank 交换:
它同样只要求:
但不会 all-gather 完整序列,也不会重复完整 scan。
3. Scope
本 PR 修改:
docker/Dockerfiledocker/patch/megatron/20260805-85bced0ae.patchdocker/patch/latest/megatron.patchtests/backends/megatron/test_gdn_chunkwise_cp_layout.pytests/backends/megatron/test_gdn_chunkwise_cp_gpu.py4. Dependency change
FLA 0.4.1 → 0.4.2
FLA 0.4.2 是第一个提供以下接口的发布版本:
并让:
接受:
Relax 不通过 MCore 的
pyproject.toml/uv.lock安装依赖,因此没有照搬上游这两个文件,而是在 Dockerfile 精确固定:FLA/MCore 能力由专项测试直接验证,不在 Dockerfile 做 import 检测:
build_cp_context;causal_conv1d和chunk_gated_delta_rule会带真实cp_context执行 forward/backward;5. MCore backport
5.1
context_parallel_layout.py这个文件从
5139086e完整迁入,内容与上游一致,提供:5.2
packed_seq_params.py上游 #3282 所在 dev 基线已经有
resolve_cp_group(),因此该文件不在官方 PR diff 中。Relax 固定的
85bced0a已经有:但缺少统一解析 helper,所以本 PR额外 backport:
行为:
packed_seq_params.cp_group存在时使用当前 micro-batch 动态组;这里的“动态组”不是在 forward 中临时创建。Relax 启动分布式环境时通过:
一次性创建最大 CP 组及允许的 size 1/2/4/... 子组。
data.py每个 micro-batch 只根据长度选择已经存在的子组:并把这个现成的
ProcessGroup对象写进最终PackedSeqParams。GDN forward 只读取并使用它,不调用new_group,因此不会在训练过程中反复建组。MCore 直接从最终 group 派生 size/rank,同时按 maintainer 要求,每次 GDN forward 都校验对象的:
5.3
TransformerConfig.linear_cp_mode从上游迁入:
linear_cp_mode字段;TP×CPhead 校验;cp_comm_type分离。Relax 修改一:默认值保持
headwise上游默认:
本 PR默认:
原因:只升级镜像不能让现有 recipe 静默切换算法。
Relax 修改二:增加
all_gather合法值变成:
all_gather构造时使用 TP-only head 校验,但 MCore 不做该all_gather fallback实现,而是会在relax端劫持调用已经写好的实现。因此 CP>1 时如果 MCore 原始 forward 收到
all_gather,会 fail-fast,防止漏装 Relax wrapper 后静默执行错误算法。Relax 修改三:拒绝未解析的
autoauto只属于未来 Relax CLI,必须在构造模型前解析。MCore 构造时只接受三个具体值。5.4
gated_delta_net.py从上游迁入的核心能力:
build_cp_context;seq_len_post_headwise/seq_len_global;cp_context传给 conv 和 GDN rule;Relax 要求:最终 dynamic CP 元数据只校验一次
GDN 收到最终
PackedSeqParams后检查:cp_group与local_cp_size必须同时存在或同时为空;local_cp_size必须等于cp_group.size()。每次 GDN forward 都直接检查,不缓存“已验证”状态。这样逻辑更直观,也能覆盖 Bridge repack 后替换的新对象。
以下差异需要重点说明。
旧 MCore 兼容:保留单体 forward
官方 PR基于更新的 dev,GDN 已经拆成:
Relax 固定的
85bced0a仍是单体 forward。本 PR没有回移约 597 个提交间的整套 GDN 重构,而是在旧 forward 内嵌入最小 chunkwise 能力:
cp_size_headwise替换旧self.cp_size;这样避免同时改变旧 headwise 路径的整体函数结构。
Relax 兼容:
_prepare_qkv_for_gated_delta_rule参数可选上游把
cp_size_headwise设为必填参数。现有 Relax all-gather fallback 直接调用这个 MCore 私有 helper,由于我们第一个pr不修改Relax中的逻辑,为了fallback调用时不出错:
因此本 PR暂时允许:
未传时回退到
self.cp_size。现有 fallback 调用前会临时将self.cp_size=1,含义是 all-gather 后不再沿 CP 切 head。第二阶段修改 Relax wrapper 显式传
cp_size_headwise=1后,可以再将该参数恢复成上游的必填形式。旧 MCore 兼容:保留 helper 名称
上游将:
本 PR保留旧名称,只让
cp_group=None表示 size 1,减少旧基线调用面变化。与上游对齐:统一传
cp_context本 PR给 FLA rule 和 torch reference rule(主要是deterministic模式使用) 都统一传:
非 chunkwise 时值为
None。同时给
torch_chunk_gated_delta_rule增加上游相同的cp_context=None参数,并断言它只能为None。真正的 chunkwise CP 仍然只支持 FLA。保留既有 Relax 修复
当前 Docker 每次只应用一份累计 MCore patch。新 patch 必须继续包含旧 patch 的有效修复:
_prepare_qkv_for_gated_delta_rule周围禁用 Dynamo compile;param[tuple(slices)]的多维基本切片修复。否则
latest切到新 patch 后,旧修复会从镜像中消失。6. Explicit exclusions from upstream PR
gdn_conv_pad_alignment没有迁入。
官方 PR所在的新 dev 已有固定 conv alignment padding,并将其改为可配置;Relax pinned 基线没有这套优化。它不是 chunkwise 必需能力,而且 local chunk padding 与 chunkwise recurrence 不兼容。
transformer_engine.pypadded boundary fix没有迁入。
该修复解决的是通用问题:物理 THD tensor 比普通
cu_seqlens描述得更长时,TE 可能创建过短输出。当前 Relax 标准路径会把尾部 padding 直接追加到普通边界;unsplit handoff 也将普通和 padded 边界设为同一值,因此现有表示不触发该Bug。
第二阶段PR会在实际场景进一步测试该Bug。
新版 GDN 重构和融合
没有迁入:
_forward_compute整体拆分;recompute_gdnwrapper;-新版 headwise A2A wrapper;
原因是这些不是 chunkwise 最小依赖,且会显著扩大旧路径回归面。
#5664 route prebuild
没有迁入未合入的 NVIDIA/Megatron-LM#5664:
prebuild_thd_cp_partition_routes;cu_seqlens重建。这是性能项,不影响数值语义。是否需要 route cache 留给实际 profiler 数据决定。
7. Tests
7.1
test_gdn_chunkwise_cp_layout.py50 个 CPU item,来源和目的:
slice_with_cp/gdn_cp_slice逐 token 一致;TP×CP、chunkwise TP-only;GatedDeltaNet.forward不允许 per-call mode override;resolve_cp_group优先动态组。结果:
7.2
test_gdn_chunkwise_cp_gpu.py10 个 pytest item:
cp_context=None并与 CP1 对齐。运行结果:
1e-5过严失败;2.813e-5、cosine1.0;1e-3后,该项单独复跑通过。即 10 个 item 均分别通过。
7.3 与上游测试的差异
比上游增加:
dweight/dbias/dg/dbeta;从上游补回:
仍留到第二阶段pr的测试——真实 recipe:
{1,2,4}混合;8. Controlled old/new numerical evidence
测试升级FLA版本是否会带来精度问题,相关代码没有放进本PR,在本地环境实验后仅将结论表述如下:
实验环境:
relaxrl/relax:latest,digest 前缀sha256:2d8fce08a40e;20260506-85bced0ae.patch;结果:
5.7e-7;1.0。非 bitwise 的差异只出现在少量 backward reduction,量级符合 FLA 版本改变归约顺序。
9. How Relax will integrate in phase 2
9.1 新增启动参数
9.2 启动时一次性解析
auto使用静态最大 CP:
auto暂不自动选择 chunkwise;chunkwise 只允许显式启用。解析结果必须在模型构造前写入:
并打印:
9.3 删除构造期 monkey patch
删除
_relax_gdn_cp_config_assert()临时放大 head 数的方式。linear_cp_mode="all_gather"已能用正式 TP-only 规则完成构造。9.4 收敛 runtime wrapper
现有
_patch_gdn_for_dynamic_cp收敛为薄 dispatcher:wrapper 不再修改:
动态 group 只通过最终
PackedSeqParams.cp_group传入。9.5 all-gather fallback 显式适配新 helper
将现有调用改成:
随后可将 MCore 中该参数恢复成与上游一致的必填参数。
9.6 数据与 Bridge
2×runtime_CP整除;PackedSeqParams构造cp_context;batch["dynamic_cp_size"]/dynamic_cp_rank,而是从同一个最终PackedSeqParams派生 group/size/rank;9.7 第二阶段正确性 gate
在性能实验前完成:
{1,2,4}同一训练任务混合;--allgather-cp等冲突参数 fail-fast。10. Maintainer feedback requested
希望 maintainer 明确确认以下设计点:
linear_cp_mode="all_gather"作为 Relax-only 构造声明:headwise,而不是采用上游默认chunkwise。cp_size_headwise暂时保留为可选参数;是否接受第二阶段显式传 1 后再恢复上游必填签名。transformer_engine.py的 padded-boundary fix,还是等 Bridge 最终 repack 测试证明存在长度不一致后再迁。gdn_conv_pad_alignment、新版 GDN 重构和 #5664 route cache。11. Test plan
如下命令已在本地8xH200运行并全部通过;