[Task32] feat: fla mcore chunkwise cp stage 2 - #254
Open
jambow0320 wants to merge 4 commits into
Open
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>
Contributor
|
有点反直觉。 |
Author
Contributor
|
你尝试下把 #5664 的核心优化接入下;另外调整下 recompute 和 sequence length,看看有没有正向收益呢? |
Author
okk我试试;不过活动好像明天就截止了,可能一时半会搞不完;我先试试看 |
Author
|
@xiaoliang0601 Hi~ 我接入了5664的缓存优化,现在chunkwise的性能已经是实验结果最优的了;#273 |
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 第二阶段:Relax 侧 GDN CP 路由接入
对应 RFC:redai-infra/Relax#213。以及一些实验结果参考from redai-infra/Relax#206
对应第一阶段 PR:redai-infra/Relax#251。(本PR基于第一阶段PR的修改上新增改动,即commit 22b1206)
根据 RFC issue 中的讨论结果,本PR保留最简单的启动级静态模式:
auto。Relax 直接暴露 MCore 同名的--linear-cp-mode={headwise,chunkwise,all_gather},默认headwise;不满足 Headwise 几何时构造期 fail fast,由用户显式选择chunkwise或all_gather。5139086e),暂不接受 #5664(THD route 预构建)。1. Summary
本 PR 完成 Relax 接入层:
TransformerConfig已暴露的--linear-cp-mode={headwise,chunkwise,all_gather},默认headwise;Relax 不重复注册同名参数;relax/backends/megatron/arguments.py新增_validate_linear_cp_mode,对 Chunkwise 新路径的非法组合提前报错;core_transformer_config_from_args()自动读取args.linear_cp_mode;Bridge 通过统一 config override 列表在provider.finalize()前透传同一字段;CP 启用后由 rank 0 打印最终实际 mode;relax/backends/megatron/model.py中构造期的_relax_gdn_cp_config_asserthead 数 monkey patch;_dcp_gdn_forward和 All-gather 主体的位置,只把顶部路由改成读取静态linear_cp_mode:CP=1、headwise、chunkwise调用 patched MCore 原始forward,all_gather且 CP>1 继续执行原 fallback;cp_size_headwise=1,不再临时改写self.cp_size;本 PR 不再修改 Stage 1 的 MCore config 语义:MCore 仍只接受三个具体 mode;不实现
auto,不实现#5664route 预构建。正确性与性能验证结果分别见 §11 和 §12。2. Background
Stage 1 PR #251 只做了两件事:把 FLA 升级到 0.4.2;把上游
linear_cp_mode(headwise/chunkwise)能力和 Relax-only 的all_gather声明 backport 到 pinned MCore。Stage 1 没有改relax/目录,训练时实际走的路由仍是 Stage 0 的运行时判定:_relax_gdn_cp_config_assert在 CP 生效时临时把linear_num_{key,value}_heads乘以 CP,骗过 MCore 的heads % (tp*cp)校验,构造完再还原;_patch_gdn_for_dynamic_cp装的_dcp_gdn_forward每次 forward 现算native_ok = num_key_heads % (tp_size * cp_size) == 0,native_ok时临时把self.cp_size/self.pg_collection.cp换成当前 micro-batch 的动态值再调用原始 forward,用完还原;native_ok为假且是 packed THD 时才用 Relax 自己实现的 all-gather fallback。即原本relax实现中"构造时声明的算法"和"运行时执行的算法"是分离的、且靠运行时反复读写共享的
self.cp_size/self.pg_collection.cp来"借用"MCore 的原生 forward——第二阶段的任务就是把这套隐式判定换成第 1 节列的显式静态路由。3. Scope
本 PR 修改:
relax/backends/megatron/arguments.py(新增_validate_linear_cp_mode)relax/backends/megatron/model_provider.py(Bridge override 列表透传linear_cp_mode)relax/backends/megatron/model.py(删除构造期 monkey patch;最小改动收敛 GDN wrapper;打印实际启用 mode)tests/backends/megatron/test_gdn_chunkwise_cp_layout.py(更新具体 mode/CLI 命名相关断言)tests/backends/megatron/test_gdn_cp_mode_stage2.py(新增,CPU)4.
--linear-cp-modeCLI 参数Stage 1 的 MCore patch 已把
linear_cp_mode加入TransformerConfig;Megatron 参数系统会自动把该 dataclass 字段暴露为--linear-cp-mode,默认值就是headwise。Relax 如果再次parser.add_argument("--linear-cp-mode", ...)会产生 argparse option conflict,因此 Stage 2 只消费和校验现有字段,不重复定义。默认
headwise。如果 key/value heads 不能被TP*max_CP整除,MCore 构造期直接报错;用户必须显式改用chunkwise或all_gather。5. 参数校验
文件:
relax/backends/megatron/arguments.py,新增_validate_linear_cp_mode(args),在validate_args末尾调用。chunkwise + --allgather-cplinear_cp_mode == "chunkwise" and allgather_cp--allgather-cp把数据路径换成单一连续分片,两者结构冲突chunkwise + --deterministic-mode(当 CP 可能 > 1)linear_cp_mode == "chunkwise" and (dynamic_context_parallel or context_parallel_size > 1) and deterministic_mode这里不新增
all_gather + CP>1的 recompute 启动参数限制。旧版_assert_gdn_full_recompute()仍只在训练实际进入 all-gather fallback 时检查,并保留其no_grad跳过语义;把同一策略提前到模型无关的参数校验阶段会扩大旧行为边界,不是 Chunkwise 接入所必需的改动。不在参数校验阶段实现:
TransformerConfig.__post_init__的既有 gate 校验;Relax 不复制 head 数判断。context_parallel_size在_validate_linear_cp_mode执行时已经是_validate_dynamic_context_parallel算出的最终值。6. 三条 provider 路径直接透传
参数名称与 MCore dataclass 字段完全相同,因此不需要 resolver:
core_transformer_config_from_args(args)会自动复制args.linear_cp_mode;"linear_cp_mode"加入现有bridge_keysoverride 列表,在provider.finalize()前直接写入;core_transformer_config_from_args(),同样自动获得该字段;完全手写TransformerConfig(...)的 custom provider 必须主动传linear_cp_mode=get_args().linear_cp_mode。模型构造完成且确认静态/dynamic CP 已启用后,
setup_model_and_optimizer()在_patch_gdn_for_dynamic_cp()安装完成的位置读取三条路径最终生成的 MCore config,仅对 GDN 模型由 rank 0 打印一次实际启用配置:日志读取的是构造完成后的实际 config,而不是仅回显 CLI requested value;默认
headwise(Bridge override 前后值相同、不会触发通用 Override 日志)也会明确打印。CP=1 且未启用 dynamic CP 时不会打印,避免把“配置字段存在”误报成“CP 实际启用”。7. 删除构造期 monkey patch
文件:
relax/backends/megatron/model.py。删除了:setup_model_and_optimizer里的调用点(原第 313-314 行附近,if ...: _relax_gdn_cp_config_assert());_relax_gdn_cp_config_assert函数定义本身(原第 413-468 行,约 56 行)。替代机制:Stage 1 已让
linear_cp_mode="all_gather"使用 TP-only 规则构造不可整除几何;Stage 2 的 §6 直接把具体 mode 透传给 MCore,不再需要临时改写 head 数骗过校验。8. GDN wrapper 重构
8.1 patched
GatedDeltaNet.forward已经自己解析动态 CP group 和 modeStage 1 backport 的
forward(megatron/core/ssm/gated_delta_net.py)在函数最开头就做了:也就是说:headwise/chunkwise 不需要 Relax wrapper 实现算法或修改状态——wrapper 只解析当前 micro-batch 的 CP size 并做分支判断,随后把这两种模式直接转发给原始 MCore forward;原始 forward 自己从
packed_seq_params.cp_group解析动态 group/size,并读取构造期固定的self.config.linear_cp_mode。当配置为all_gather且当前 micro-batch 的cp_size > 1时,wrapper 改为调用 Relax 实现的 All-gather fallback;CP=1 时不需要任何跨 rank gather,因此仍直接走普通 MCore forward。8.2 新的 dispatcher
和 原版Relax 的
_dcp_gdn_forward/_call_orig_with_dynamic_cp/native_ok相比:native_ok = num_key_heads % (tp_size*cp_size)==0,动态决定走 headwise 还是 all-gatherself.config.linear_cp_mode(构造时定死),不再看 runtime head 整除情况self.cp_size = cp_size; self.pg_collection.cp = cp_group,finally还原_orig_forward(...),不碰self.cp_size/self.pg_collection.cp——因为原始 forward 自己会从packed_seq_params解析动态组(见 8.1)_dcp_gdn_forward9. all-gather fallback:显式
cp_size_headwise=1Stage 1 PR #251 已把 MCore
GatedDeltaNet._prepare_qkv_for_gated_delta_rule()的cp_size_headwise参数改为 optional;调用方不传时仍回退读取self.cp_size,用于兼容 Stage 0。旧 All-gather fallback 因为已经恢复完整序列、没有按 CP 切 heads,只能临时把self.cp_size改成 1 再调用该函数。Stage 2 改为直接使用这个接口:
因此可以删除对
self.cp_size的临时写入,计算语义不变。另外在 All-gather 分支入口增加assert is_thd:Relax fallback 依赖 THD 的cu_seqlens和 zig-zag shard 重组,非 THD 输入直接给出明确错误。10. 完成标准
auto/resolverPackedSeqParamsgroup/sizedata.py/loss.py11. 测试
11.1 CPU
Stage 1 回归文件
test_gdn_chunkwise_cp_layout.py共 51 项,其中两项直接覆盖 Chunkwise 长度 fail-fast:test_rank_indices_reject_lengths_not_divisible_by_two_cp:每条 packed sequence 长度不能被2×CP整除时,在布局 route 构造阶段报错。test_gdn_rejects_packed_lengths_not_divisible_by_cp:cu_seqlens中任一序列长度不能被 CP 整除时,在GatedDeltaNet._resolve_cu_seqlens()报错。Stage 2 新增
test_gdn_cp_mode_stage2.py共 19 项headwise,三个具体值可解析;auto/非法值、chunkwise + --allgather-cp和chunkwise + deterministic CP>1明确报错。headwise/chunkwise在 CP>1 时也直接转发;all_gather且 CP>1 进入原有 Relax fallback;cp_size=8的模块跑一个动态cp_size=1的 micro-batch,必须走 CP=1 分支而不是被静态组的 8 带偏);本地运行:
12. 端到端正确性与性能实验
12.1 统一配置与正确性口径
统一使用 8×H200、Qwen3.5-9B、OpenMathReasoning-mini SFT、TP2、SP on、full recompute 和
--megatron-to-hf-mode bridge;训练数据为 text-only,不包含视觉输入。四方配置为老镜像(旧代码)默认路径,以及候选镜像(本次PR以及升级后的FLA+MCore)强制headwise、all_gather、chunkwise。每次运行 220 个训练 step,前 20 step warmup,后 200 step 计量;基础 recipe 使用仓库已有脚本(该脚本默认full recompute,可能是因为all gather必须和recompute一起开;所以看不出cp的显存收益,因此本次测试只注重于实际端到端正确性和速度性能):
CP环境变量,使用脚本默认的--dynamic-context-parallel(max CP=4)。CP=4,脚本切换为静态--context-parallel-size 4。--linear-cp-mode {headwise,all_gather,chunkwise},其余 recipe 参数保持一致。正确性口径:同一 seed 下各模式的有效 token 数必须精确相同,并逐 step 对比训练 loss。E1b seed 1234 的 220 step(step 0–219)中,任意两个配置间的单步 loss 绝对差值最大为 0.000486(headwise vs all_gather),均值为 0.00006–0.00008;差异处于不同 CP 通信/规约顺序引入的 BF16 数值噪声范围。
12.2 E1a:Dynamic CP 训练
CP 在
{1,2,4}中动态选择。表中吞吐为 3 个 seed 的几何平均,loss 为 3 个 seed 的平均值;每个 seed 均统计 warmup 后的 200 step。Correctness:四组均完成 220 step,覆盖 CP1/2/4,无整除、packed length 或 collective 错误;同 seed 有效 token 数精确一致,
mean train/loss差值 <0.002。结论:
all_gather最快;chunkwise比headwise慢 11.6%。12.3 E1b:静态 CP4 训练
单 seed 1234,所有 microbatch 均使用 CP4;统计 warmup 后的 200 step。
Correctness:四组有效 token 数均为 57,820,637,
mean train/loss均为 0.3384,220 step 全部完成。结论:完整排序为
all_gather > 老镜像默认 > headwise > chunkwise。12.4 Profiler 根因
all_to_allvall_gather+reduce_scatterall_to_allv+ 状态all_gatherchunkwise的同步热点:get_thd_context_parallel_rank_indices中的cu[0].item():1,113 ms / 30,720 次。_resolve_cu_seqlens中的[-1].item():921 ms / 1,920 次。.item()/sync:3.1 s / 34,148 次,GPU 约 43% 时间空闲。all_gather虽重复 CP 倍计算,但使用少量大块 collective;headwise被大量小all_to_allv拖慢;chunkwise被布局/状态通信,以及每层重复 route 引入的 CPU-GPU 同步拖慢。#5664 可优化该瓶颈,但不在我们这次pr范围内。12.5 结论
mean train/loss差值 <0.002、dynamic CP1/2/4 无报错;chunkwise性能比headwise差,不建议设为默认或推荐;默认headwise合理。all_gather在当前实验下最快,是因为通信实现更高效,而不是省计算或显存;它重复 CP 倍 scan,且 GDN 内部几乎不享受 CP 激活显存节省,长上下文或大 CP 下可能 OOM 或变慢。chunkwise的瓶颈是 #3282 基线每层重复 route 和大量.item()同步;#5664 的后续优化可能改变结果。