[Refactor] Unify SAC checkpointing and GLM DSA dataflow - #2039

Open
jayhenry wants to merge 6 commits into
InternLM:mainfrom
jayhenry:refactor/sac-checkpoint-dsa-dataflow
Open

[Refactor] Unify SAC checkpointing and GLM DSA dataflow#2039
jayhenry wants to merge 6 commits into
InternLM:mainfrom
jayhenry:refactor/sac-checkpoint-dsa-dataflow

Conversation

@jayhenry

Copy link
Copy Markdown
Collaborator

Summary

  • Unify activation checkpointing around one PyTree-aware reentrant boundary so structured micro-batch inputs and outputs stay on the real autograd and offload path.
  • Pass GLM DSA top-k IDs explicitly through decoder and MTP outputs, removing the mutable SequenceContext cache lifecycle.
  • Reuse frozen source-indexer results from checkpoint-local FIFO frames during replay.
  • Keep FSDP wrappers outside recompute and dense checkpoint boundaries outside compiled graphs.

Regression coverage

  • compile + top-k offload + shared-weight MTP with depth > 1; validates pinned-memory offload and restore, and that the source indexer is not recomputed
  • EP > 1 + intra-layer micro-batch 2
  • FSDP checkpointing, torch.compile, FP8, activation offload, and nested PyTree inputs and outputs

Validation

  • Post-rebase on latest upstream/main: 16 passed
    • tests/model/test_recompute.py
    • tests/model/test_glm52_mtp_checkpoint_repro.py
    • tests/model/test_fsdp_checkpoint.py
  • Qwen3.5 full 40-layer stack5 regression: step losses and grad norms matched upstream; steady sequence TGS +0.36%; peak allocated memory -0.14 GB.
  • GLM AdamW production-shaped and Muon EP2/micro2 training regressions completed.

@jayhenry
jayhenryforce-pushed the refactor/sac-checkpoint-dsa-dataflow branch from d78f607 to 81e0010CompareAugust 31, 2026 09:48
Flatten structured inputs and outputs at one checkpoint boundary so nested MTP micro-batch tensors receive gradients. Keep FSDP outside replay and remove the non-reentrant MTP switch.
Move GLM-specific decoder and MTP adapters under the model package, thread DSA IDs through keyed outputs, and remove SequenceContext cache lifecycle state. Share one saved-tensor offload window for activation and DSA ID storage.
Keep reusable no-grad outputs in checkpoint-call-local FIFO frames, freeze DSA indexers through their model config, and preserve the original int32 storage across shared layers and offload. Add real regressions for source call counts and pinned-memory DSA offload.
Only inject the detached-input grad entry when the checkpointed module still has trainable parameters. This preserves MTP parameter gradients while keeping frozen vision outputs detached, avoiding a replay with no differentiable outputs.
@jayhenry
jayhenryforce-pushed the refactor/sac-checkpoint-dsa-dataflow branch from b2047db to 5c15855CompareSeptember 2, 2026 02:35
@YanhuiDua

Copy link
Copy Markdown
Collaborator

@claude review

@github-actions

Copy link
Copy Markdown
Contributor

Claude:

Summary

本 PR 把 activation checkpointing 统一到一个 PyTree-aware 的 reentrant 边界(apply_activation_checkpointing + reuse_during_recompute),并把 GLM-5.2 的 DSA top-k 从 SequenceContext 上的可变缓存改为经 decoder / MTP 输出显式穿参,同时将 decoder 与 MTP 的返回值从位置元组改为 TypedDict,GLM-5.2 相关代码收敛到新的 xtuner/v1/model/moe/glm52/ 包。整体方向是把隐式生命周期换成显式数据流,抽象更清晰;但 offload 策略在基类与 GLM 子类各写了一份,另有一处配置项失效、一处 offload 匹配语义放宽、一处 vision compile 降级需要确认。

Main Flowchart after this PR

flowchart TD
A["MoE._forward / _micro_batch_forward"] --> B["_decoder_stack /<br/>_micro_batch_decoder_stack<br/>(新增, 11 参数 + 可变 output dict)"]
B --> C{"模型类型"}
C -->|"通用 MoE"| D["MoE._call_decoder_layer<br/>(offload 窗口 + block_idx 规则)"]
C -->|"GLM-5.2"| E["Glm52MoE._call_decoder_layer<br/>(整段 override: DSA IDs + 另一套 block_idx 规则)"]
D --> F["_saved_tensors_offload_ctx<br/>(storage-ptr 匹配, reserve_pin_memory)"]
E --> F
F --> G["decoder_layer(...) -> TypedDict"]
G --> H["GLM52AttnOutputs.dsa_topk_ids<br/>显式传给下一 consumer 层"]
H --> I["MTPBlock._call_decoder_layer<br/>GLM52MTPBlock 无条件透传 dsa_topk_ids"]
style D fill:#ffe0b2,stroke:#e65100
style E fill:#ffcdd2,stroke:#b71c1c
style F fill:#ffcdd2,stroke:#b71c1c
style I fill:#ffe0b2,stroke:#e65100
Loading

核心原理实现与单测

  • Checkpoint 统一边界apply_activation_checkpointing 固定 reentrant 并用 PyTree 桥接展平输入 / 还原输出,reuse_during_recomputeContextVar + 按 callable 的 FIFO frame 支撑 replay 复用。tests/model/test_recompute.py 全部走 public API,覆盖了 grad 模式序列 [False, True]、全 detach 输入仍产参数梯度、冻结模块不建立 replay 边、嵌套 / 关键字输入可被外层 saved_tensors_hooks 捕获(即 offload 不会静默空转)、replay 输出结构变化报错、以及跨两次 checkpoint 的 FIFO 隔离——核心行为有真实代码路径覆盖,未 mock 项目内模块。
  • GLM DSA 显式数据流:source 层算一次 IDs,consumer 层由 Glm52MoE._call_decoder_layer 显式传入;tests/module/attention/test_dsa_mla.pytests/model/test_glm52_moe.py::TestGlm52ExplicitDsaDataflowtests/model/test_glm52_mtp_checkpoint_repro.py(indexer 调用计数 1 / 2、pinned D2H 真实命中并在 backward 恢复)覆盖了 source 不重算与 top-k offload。
  • 覆盖缺口(未达 Warning,仅提示):_call_decoder_layer 的 offload block-index 计算无 CPU 可跑单测;index_share_for_mtp_iteration=False 无任何测试。

抽象与信息隐藏评估

  • Warningxtuner/v1/model/moe/moe.py_call_decoder_layerxtuner/v1/model/moe/glm52/glm52.py 的 override:Seam 切在整个 _call_decoder_layer 上,导致同一条 activation-offload 规则(窗口张量 + block_idx 编号)在基类和 GLM 子类各存一份且公式不同,基类策略变更时 GLM 会静默偏移。
  • Warningxtuner/v1/model/moe/moe.py_decoder_stack / _micro_batch_decoder_stack:抽出的私有方法带 11 个关键字参数并把 output: dict 当可变入出参写入,没有隐藏规则,只是把循环搬家。

其他 Issues

  • Warningxtuner/v1/model/moe/glm52/glm52.py:删除 index_share_for_mtp_iteration 的唯一运行时使用点后,该配置项仍保留在 config、from_hf 校验和 to_hf_config 回写中,但设为 False 既不改变行为也不报错,变成静默失效的配置。
  • Warningxtuner/v1/model/moe/moe.py_saved_tensors_offload_ctx:offload 匹配从 data_ptr() 放宽为 untyped_storage().data_ptr(),会把共享 storage 的视图一并纳入窗口,且单 micro-batch 路径新增常驻 pinned 缓冲并改变 block_idx 编号,而现有基准未覆盖 XTUNER_ACTIVATION_OFFLOAD=1 的单 micro-batch 路径。
  • Warningxtuner/v1/model/compose/qwen3_vl/modeling_vision.pyxtuner/v1/model/compose/intern_s1/modeling_vision.py:compile 目标改为 checkpoint wrapper 的 forward 且降为 fullgraph=False,但内层 vision layer 并没有注释所称的"独立 full-graph 配置",fullgraph 保证被静默移除且本 PR 无 VL 回归验证。

Verdict

REQUEST_CHANGES

Comment on lines +152 to +171
activation_offload = int(os.getenv("XTUNER_ACTIVATION_OFFLOAD", "0")) == 1
dsa_topk_offload = int(os.getenv("XTUNER_DSA_TOPK_OFFLOAD", "0")) == 1
offload_tensors: list[torch.Tensor] = []
if activation_offload and layer_idx >= self.config.first_k_dense_replace:
offload_tensors = list(hidden_states) if is_micro_batch else [cast(torch.Tensor, hidden_states)]
if dsa_topk_offload and layer_idx in self._dsa_topk_last_consumers and dsa_topk_ids is not None:
offload_tensors.extend(dsa_topk_ids if isinstance(dsa_topk_ids, list) else [dsa_topk_ids])

# The offload context expects a dense zero-based block index across every
# active activation or DSA-ID window, including dense GLM layers.
offload_block_idx = sum(
(activation_offload and previous_idx >= self.config.first_k_dense_replace)
or (
dsa_topk_offload
and previous_idx in self._dsa_topk_last_consumers
and self._dsa_topk_source_layers[previous_idx] != previous_idx
)
for previous_idx in (int(idx) for idx in self.layers)
if previous_idx < layer_idx
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [设计] activation-offload 规则出现两份实现。

Problem:基类 MoE._call_decoder_layer 已把「窗口张量选择 + block_idx 编号」写死在方法体内,GLM 为了插入 DSA IDs 只能整段 override,于是同一条规则有了两个不同公式:基类是 sum(first_k_dense_replace <= idx < layer_idx),这里则把 DSA 末位消费层也计入密集编号。做 Deletion test:删掉这个 override 会同时丢失 DSA 数据流 offload 规则,说明 Seam 切在了整个 _call_decoder_layer 上——位置偏大。后果是基类调整 offload 策略时,GLM 侧不会报错,只会静默错位到另一套编号。

Solution:基类保留唯一的 _call_decoder_layer(窗口构造 + block 计数),只把变化点做成小钩子,GLM 仅覆盖钩子:

# MoEdef_extra_offload_tensors(self, layer_idx, hidden_states) ->list[torch.Tensor]:
return []
def_layer_forward_kwargs(self, layer_idx, previous_layer_results) ->dict:
return {}

Benefits:offload 规则单点化(Locality),子类需要了解的 Interface 从「整个调用 + 窗口 + 编号」缩小到两个钩子(Leverage),block_idx 编号也能在基类上直接写 CPU 单测。

GLM52MoEDecoderLayerOutput | GLM52MoEDecoderLayerMicroBatchOutput | None,
previous_layer_results,
)
dsa_topk_ids = None if previous_results is None else previous_results["dsa_topk_ids"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [兼容性] index_share_for_mtp_iteration 的唯一运行时使用点已被删除,这里无条件透传上一 depth 的 IDs。但该字段仍保留在 config、from_hf 校验与 to_hf_config 回写中,设为 False 既不生效也不报错。建议按该配置决定是否透传,或在配置层直接拒绝 False

Comment on lines +302 to +308
"""Build one policy-neutral saved-tensor offload window.

The decoder-stack caller decides which tensors belong to the current
window and advances ``block_idx`` only when the list is non-empty.
"""
if not tensors:
return contextlib.nullcontext()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [正确性] 匹配条件从原来的 x.data_ptr() == hidden_states.data_ptr() 放宽为 storage 指针,会把共享 storage 的非零偏移视图一并纳入窗口,同一 storage 可能被重复 pack/D2H。另外单 micro-batch 路径新增了 reserve_pin_memory=True 且 block_idx 由绝对层号改为相对号,改变了 OffloadManager 的 key 与 prefetch 顺序,而本 PR 基准未覆盖 XTUNER_ACTIVATION_OFFLOAD=1 的单 micro-batch 路径。

Comment on lines 349 to +353
if self.compile_cfg:
layer.forward = torch.compile(layer.forward, fullgraph=True)
# The PyTree checkpoint adapter is an intentional graph break; model compute
# inside it keeps the independently configured full-graph compilation.
compiled_forward = torch.compile(type(layer).forward, fullgraph=False)
layer.forward = compiled_forward.__get__(layer, type(layer))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [性能] 此处 type(layer) 已是 CheckpointWrapper,而内层 Qwen3VLVisionLayer.forward 并没有注释所说的「独立 full-graph 配置」(该文件只 import 了 maybe_compile,未装饰任何 vision forward)。结果是 fullgraph 保证被静默移除,vision 侧 graph break 不再报错,且本 PR 无 VL 回归验证。建议把 fullgraph=True 保留在被包裹模块自身的 forward 上,并补一条 VL 验证。

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jayhenry@YanhuiDua
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[Refactor] Unify SAC checkpointing and GLM DSA dataflow - #2039

Open
jayhenry wants to merge 6 commits into
InternLM:mainfrom
jayhenry:refactor/sac-checkpoint-dsa-dataflow
Open

[Refactor] Unify SAC checkpointing and GLM DSA dataflow#2039
jayhenry wants to merge 6 commits into
InternLM:mainfrom
jayhenry:refactor/sac-checkpoint-dsa-dataflow

Conversation

@jayhenry

Copy link
Copy Markdown
Collaborator

Summary

  • Unify activation checkpointing around one PyTree-aware reentrant boundary so structured micro-batch inputs and outputs stay on the real autograd and offload path.
  • Pass GLM DSA top-k IDs explicitly through decoder and MTP outputs, removing the mutable SequenceContext cache lifecycle.
  • Reuse frozen source-indexer results from checkpoint-local FIFO frames during replay.
  • Keep FSDP wrappers outside recompute and dense checkpoint boundaries outside compiled graphs.

Regression coverage

  • compile + top-k offload + shared-weight MTP with depth > 1; validates pinned-memory offload and restore, and that the source indexer is not recomputed
  • EP > 1 + intra-layer micro-batch 2
  • FSDP checkpointing, torch.compile, FP8, activation offload, and nested PyTree inputs and outputs

Validation

  • Post-rebase on latest upstream/main: 16 passed
    • tests/model/test_recompute.py
    • tests/model/test_glm52_mtp_checkpoint_repro.py
    • tests/model/test_fsdp_checkpoint.py
  • Qwen3.5 full 40-layer stack5 regression: step losses and grad norms matched upstream; steady sequence TGS +0.36%; peak allocated memory -0.14 GB.
  • GLM AdamW production-shaped and Muon EP2/micro2 training regressions completed.

@jayhenry
jayhenryforce-pushed the refactor/sac-checkpoint-dsa-dataflow branch from d78f607 to 81e0010CompareAugust 31, 2026 09:48
Flatten structured inputs and outputs at one checkpoint boundary so nested MTP micro-batch tensors receive gradients. Keep FSDP outside replay and remove the non-reentrant MTP switch.
Move GLM-specific decoder and MTP adapters under the model package, thread DSA IDs through keyed outputs, and remove SequenceContext cache lifecycle state. Share one saved-tensor offload window for activation and DSA ID storage.
Keep reusable no-grad outputs in checkpoint-call-local FIFO frames, freeze DSA indexers through their model config, and preserve the original int32 storage across shared layers and offload. Add real regressions for source call counts and pinned-memory DSA offload.
Only inject the detached-input grad entry when the checkpointed module still has trainable parameters. This preserves MTP parameter gradients while keeping frozen vision outputs detached, avoiding a replay with no differentiable outputs.
@jayhenry
jayhenryforce-pushed the refactor/sac-checkpoint-dsa-dataflow branch from b2047db to 5c15855CompareSeptember 2, 2026 02:35
@YanhuiDua

Copy link
Copy Markdown
Collaborator

@claude review

@github-actions

Copy link
Copy Markdown
Contributor

Claude:

Summary

本 PR 把 activation checkpointing 统一到一个 PyTree-aware 的 reentrant 边界(apply_activation_checkpointing + reuse_during_recompute),并把 GLM-5.2 的 DSA top-k 从 SequenceContext 上的可变缓存改为经 decoder / MTP 输出显式穿参,同时将 decoder 与 MTP 的返回值从位置元组改为 TypedDict,GLM-5.2 相关代码收敛到新的 xtuner/v1/model/moe/glm52/ 包。整体方向是把隐式生命周期换成显式数据流,抽象更清晰;但 offload 策略在基类与 GLM 子类各写了一份,另有一处配置项失效、一处 offload 匹配语义放宽、一处 vision compile 降级需要确认。

Main Flowchart after this PR

flowchart TD
A["MoE._forward / _micro_batch_forward"] --> B["_decoder_stack /<br/>_micro_batch_decoder_stack<br/>(新增, 11 参数 + 可变 output dict)"]
B --> C{"模型类型"}
C -->|"通用 MoE"| D["MoE._call_decoder_layer<br/>(offload 窗口 + block_idx 规则)"]
C -->|"GLM-5.2"| E["Glm52MoE._call_decoder_layer<br/>(整段 override: DSA IDs + 另一套 block_idx 规则)"]
D --> F["_saved_tensors_offload_ctx<br/>(storage-ptr 匹配, reserve_pin_memory)"]
E --> F
F --> G["decoder_layer(...) -> TypedDict"]
G --> H["GLM52AttnOutputs.dsa_topk_ids<br/>显式传给下一 consumer 层"]
H --> I["MTPBlock._call_decoder_layer<br/>GLM52MTPBlock 无条件透传 dsa_topk_ids"]
style D fill:#ffe0b2,stroke:#e65100
style E fill:#ffcdd2,stroke:#b71c1c
style F fill:#ffcdd2,stroke:#b71c1c
style I fill:#ffe0b2,stroke:#e65100
Loading

核心原理实现与单测

  • Checkpoint 统一边界apply_activation_checkpointing 固定 reentrant 并用 PyTree 桥接展平输入 / 还原输出,reuse_during_recomputeContextVar + 按 callable 的 FIFO frame 支撑 replay 复用。tests/model/test_recompute.py 全部走 public API,覆盖了 grad 模式序列 [False, True]、全 detach 输入仍产参数梯度、冻结模块不建立 replay 边、嵌套 / 关键字输入可被外层 saved_tensors_hooks 捕获(即 offload 不会静默空转)、replay 输出结构变化报错、以及跨两次 checkpoint 的 FIFO 隔离——核心行为有真实代码路径覆盖,未 mock 项目内模块。
  • GLM DSA 显式数据流:source 层算一次 IDs,consumer 层由 Glm52MoE._call_decoder_layer 显式传入;tests/module/attention/test_dsa_mla.pytests/model/test_glm52_moe.py::TestGlm52ExplicitDsaDataflowtests/model/test_glm52_mtp_checkpoint_repro.py(indexer 调用计数 1 / 2、pinned D2H 真实命中并在 backward 恢复)覆盖了 source 不重算与 top-k offload。
  • 覆盖缺口(未达 Warning,仅提示):_call_decoder_layer 的 offload block-index 计算无 CPU 可跑单测;index_share_for_mtp_iteration=False 无任何测试。

抽象与信息隐藏评估

  • Warningxtuner/v1/model/moe/moe.py_call_decoder_layerxtuner/v1/model/moe/glm52/glm52.py 的 override:Seam 切在整个 _call_decoder_layer 上,导致同一条 activation-offload 规则(窗口张量 + block_idx 编号)在基类和 GLM 子类各存一份且公式不同,基类策略变更时 GLM 会静默偏移。
  • Warningxtuner/v1/model/moe/moe.py_decoder_stack / _micro_batch_decoder_stack:抽出的私有方法带 11 个关键字参数并把 output: dict 当可变入出参写入,没有隐藏规则,只是把循环搬家。

其他 Issues

  • Warningxtuner/v1/model/moe/glm52/glm52.py:删除 index_share_for_mtp_iteration 的唯一运行时使用点后,该配置项仍保留在 config、from_hf 校验和 to_hf_config 回写中,但设为 False 既不改变行为也不报错,变成静默失效的配置。
  • Warningxtuner/v1/model/moe/moe.py_saved_tensors_offload_ctx:offload 匹配从 data_ptr() 放宽为 untyped_storage().data_ptr(),会把共享 storage 的视图一并纳入窗口,且单 micro-batch 路径新增常驻 pinned 缓冲并改变 block_idx 编号,而现有基准未覆盖 XTUNER_ACTIVATION_OFFLOAD=1 的单 micro-batch 路径。
  • Warningxtuner/v1/model/compose/qwen3_vl/modeling_vision.pyxtuner/v1/model/compose/intern_s1/modeling_vision.py:compile 目标改为 checkpoint wrapper 的 forward 且降为 fullgraph=False,但内层 vision layer 并没有注释所称的"独立 full-graph 配置",fullgraph 保证被静默移除且本 PR 无 VL 回归验证。

Verdict

REQUEST_CHANGES

Comment on lines +152 to +171
activation_offload = int(os.getenv("XTUNER_ACTIVATION_OFFLOAD", "0")) == 1
dsa_topk_offload = int(os.getenv("XTUNER_DSA_TOPK_OFFLOAD", "0")) == 1
offload_tensors: list[torch.Tensor] = []
if activation_offload and layer_idx >= self.config.first_k_dense_replace:
offload_tensors = list(hidden_states) if is_micro_batch else [cast(torch.Tensor, hidden_states)]
if dsa_topk_offload and layer_idx in self._dsa_topk_last_consumers and dsa_topk_ids is not None:
offload_tensors.extend(dsa_topk_ids if isinstance(dsa_topk_ids, list) else [dsa_topk_ids])

# The offload context expects a dense zero-based block index across every
# active activation or DSA-ID window, including dense GLM layers.
offload_block_idx = sum(
(activation_offload and previous_idx >= self.config.first_k_dense_replace)
or (
dsa_topk_offload
and previous_idx in self._dsa_topk_last_consumers
and self._dsa_topk_source_layers[previous_idx] != previous_idx
)
for previous_idx in (int(idx) for idx in self.layers)
if previous_idx < layer_idx
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [设计] activation-offload 规则出现两份实现。

Problem:基类 MoE._call_decoder_layer 已把「窗口张量选择 + block_idx 编号」写死在方法体内,GLM 为了插入 DSA IDs 只能整段 override,于是同一条规则有了两个不同公式:基类是 sum(first_k_dense_replace <= idx < layer_idx),这里则把 DSA 末位消费层也计入密集编号。做 Deletion test:删掉这个 override 会同时丢失 DSA 数据流 offload 规则,说明 Seam 切在了整个 _call_decoder_layer 上——位置偏大。后果是基类调整 offload 策略时,GLM 侧不会报错,只会静默错位到另一套编号。

Solution:基类保留唯一的 _call_decoder_layer(窗口构造 + block 计数),只把变化点做成小钩子,GLM 仅覆盖钩子:

# MoEdef_extra_offload_tensors(self, layer_idx, hidden_states) ->list[torch.Tensor]:
return []
def_layer_forward_kwargs(self, layer_idx, previous_layer_results) ->dict:
return {}

Benefits:offload 规则单点化(Locality),子类需要了解的 Interface 从「整个调用 + 窗口 + 编号」缩小到两个钩子(Leverage),block_idx 编号也能在基类上直接写 CPU 单测。

GLM52MoEDecoderLayerOutput | GLM52MoEDecoderLayerMicroBatchOutput | None,
previous_layer_results,
)
dsa_topk_ids = None if previous_results is None else previous_results["dsa_topk_ids"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [兼容性] index_share_for_mtp_iteration 的唯一运行时使用点已被删除,这里无条件透传上一 depth 的 IDs。但该字段仍保留在 config、from_hf 校验与 to_hf_config 回写中,设为 False 既不生效也不报错。建议按该配置决定是否透传,或在配置层直接拒绝 False

Comment on lines +302 to +308
"""Build one policy-neutral saved-tensor offload window.

The decoder-stack caller decides which tensors belong to the current
window and advances ``block_idx`` only when the list is non-empty.
"""
if not tensors:
return contextlib.nullcontext()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [正确性] 匹配条件从原来的 x.data_ptr() == hidden_states.data_ptr() 放宽为 storage 指针,会把共享 storage 的非零偏移视图一并纳入窗口,同一 storage 可能被重复 pack/D2H。另外单 micro-batch 路径新增了 reserve_pin_memory=True 且 block_idx 由绝对层号改为相对号,改变了 OffloadManager 的 key 与 prefetch 顺序,而本 PR 基准未覆盖 XTUNER_ACTIVATION_OFFLOAD=1 的单 micro-batch 路径。

Comment on lines 349 to +353
if self.compile_cfg:
layer.forward = torch.compile(layer.forward, fullgraph=True)
# The PyTree checkpoint adapter is an intentional graph break; model compute
# inside it keeps the independently configured full-graph compilation.
compiled_forward = torch.compile(type(layer).forward, fullgraph=False)
layer.forward = compiled_forward.__get__(layer, type(layer))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [性能] 此处 type(layer) 已是 CheckpointWrapper,而内层 Qwen3VLVisionLayer.forward 并没有注释所说的「独立 full-graph 配置」(该文件只 import 了 maybe_compile,未装饰任何 vision forward)。结果是 fullgraph 保证被静默移除,vision 侧 graph break 不再报错,且本 PR 无 VL 回归验证。建议把 fullgraph=True 保留在被包裹模块自身的 forward 上,并补一条 VL 验证。

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jayhenry@YanhuiDua
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[Refactor] Unify SAC checkpointing and GLM DSA dataflow - #2039

Open
jayhenry wants to merge 6 commits into
InternLM:mainfrom
jayhenry:refactor/sac-checkpoint-dsa-dataflow
Open

[Refactor] Unify SAC checkpointing and GLM DSA dataflow#2039
jayhenry wants to merge 6 commits into
InternLM:mainfrom
jayhenry:refactor/sac-checkpoint-dsa-dataflow

Conversation

@jayhenry

Copy link
Copy Markdown
Collaborator

Summary

  • Unify activation checkpointing around one PyTree-aware reentrant boundary so structured micro-batch inputs and outputs stay on the real autograd and offload path.
  • Pass GLM DSA top-k IDs explicitly through decoder and MTP outputs, removing the mutable SequenceContext cache lifecycle.
  • Reuse frozen source-indexer results from checkpoint-local FIFO frames during replay.
  • Keep FSDP wrappers outside recompute and dense checkpoint boundaries outside compiled graphs.

Regression coverage

  • compile + top-k offload + shared-weight MTP with depth > 1; validates pinned-memory offload and restore, and that the source indexer is not recomputed
  • EP > 1 + intra-layer micro-batch 2
  • FSDP checkpointing, torch.compile, FP8, activation offload, and nested PyTree inputs and outputs

Validation

  • Post-rebase on latest upstream/main: 16 passed
    • tests/model/test_recompute.py
    • tests/model/test_glm52_mtp_checkpoint_repro.py
    • tests/model/test_fsdp_checkpoint.py
  • Qwen3.5 full 40-layer stack5 regression: step losses and grad norms matched upstream; steady sequence TGS +0.36%; peak allocated memory -0.14 GB.
  • GLM AdamW production-shaped and Muon EP2/micro2 training regressions completed.

@jayhenry
jayhenryforce-pushed the refactor/sac-checkpoint-dsa-dataflow branch from d78f607 to 81e0010CompareAugust 31, 2026 09:48
Flatten structured inputs and outputs at one checkpoint boundary so nested MTP micro-batch tensors receive gradients. Keep FSDP outside replay and remove the non-reentrant MTP switch.
Move GLM-specific decoder and MTP adapters under the model package, thread DSA IDs through keyed outputs, and remove SequenceContext cache lifecycle state. Share one saved-tensor offload window for activation and DSA ID storage.
Keep reusable no-grad outputs in checkpoint-call-local FIFO frames, freeze DSA indexers through their model config, and preserve the original int32 storage across shared layers and offload. Add real regressions for source call counts and pinned-memory DSA offload.
Only inject the detached-input grad entry when the checkpointed module still has trainable parameters. This preserves MTP parameter gradients while keeping frozen vision outputs detached, avoiding a replay with no differentiable outputs.
@jayhenry
jayhenryforce-pushed the refactor/sac-checkpoint-dsa-dataflow branch from b2047db to 5c15855CompareSeptember 2, 2026 02:35
@YanhuiDua

Copy link
Copy Markdown
Collaborator

@claude review

@github-actions

Copy link
Copy Markdown
Contributor

Claude:

Summary

本 PR 把 activation checkpointing 统一到一个 PyTree-aware 的 reentrant 边界(apply_activation_checkpointing + reuse_during_recompute),并把 GLM-5.2 的 DSA top-k 从 SequenceContext 上的可变缓存改为经 decoder / MTP 输出显式穿参,同时将 decoder 与 MTP 的返回值从位置元组改为 TypedDict,GLM-5.2 相关代码收敛到新的 xtuner/v1/model/moe/glm52/ 包。整体方向是把隐式生命周期换成显式数据流,抽象更清晰;但 offload 策略在基类与 GLM 子类各写了一份,另有一处配置项失效、一处 offload 匹配语义放宽、一处 vision compile 降级需要确认。

Main Flowchart after this PR

flowchart TD
A["MoE._forward / _micro_batch_forward"] --> B["_decoder_stack /<br/>_micro_batch_decoder_stack<br/>(新增, 11 参数 + 可变 output dict)"]
B --> C{"模型类型"}
C -->|"通用 MoE"| D["MoE._call_decoder_layer<br/>(offload 窗口 + block_idx 规则)"]
C -->|"GLM-5.2"| E["Glm52MoE._call_decoder_layer<br/>(整段 override: DSA IDs + 另一套 block_idx 规则)"]
D --> F["_saved_tensors_offload_ctx<br/>(storage-ptr 匹配, reserve_pin_memory)"]
E --> F
F --> G["decoder_layer(...) -> TypedDict"]
G --> H["GLM52AttnOutputs.dsa_topk_ids<br/>显式传给下一 consumer 层"]
H --> I["MTPBlock._call_decoder_layer<br/>GLM52MTPBlock 无条件透传 dsa_topk_ids"]
style D fill:#ffe0b2,stroke:#e65100
style E fill:#ffcdd2,stroke:#b71c1c
style F fill:#ffcdd2,stroke:#b71c1c
style I fill:#ffe0b2,stroke:#e65100
Loading

核心原理实现与单测

  • Checkpoint 统一边界apply_activation_checkpointing 固定 reentrant 并用 PyTree 桥接展平输入 / 还原输出,reuse_during_recomputeContextVar + 按 callable 的 FIFO frame 支撑 replay 复用。tests/model/test_recompute.py 全部走 public API,覆盖了 grad 模式序列 [False, True]、全 detach 输入仍产参数梯度、冻结模块不建立 replay 边、嵌套 / 关键字输入可被外层 saved_tensors_hooks 捕获(即 offload 不会静默空转)、replay 输出结构变化报错、以及跨两次 checkpoint 的 FIFO 隔离——核心行为有真实代码路径覆盖,未 mock 项目内模块。
  • GLM DSA 显式数据流:source 层算一次 IDs,consumer 层由 Glm52MoE._call_decoder_layer 显式传入;tests/module/attention/test_dsa_mla.pytests/model/test_glm52_moe.py::TestGlm52ExplicitDsaDataflowtests/model/test_glm52_mtp_checkpoint_repro.py(indexer 调用计数 1 / 2、pinned D2H 真实命中并在 backward 恢复)覆盖了 source 不重算与 top-k offload。
  • 覆盖缺口(未达 Warning,仅提示):_call_decoder_layer 的 offload block-index 计算无 CPU 可跑单测;index_share_for_mtp_iteration=False 无任何测试。

抽象与信息隐藏评估

  • Warningxtuner/v1/model/moe/moe.py_call_decoder_layerxtuner/v1/model/moe/glm52/glm52.py 的 override:Seam 切在整个 _call_decoder_layer 上,导致同一条 activation-offload 规则(窗口张量 + block_idx 编号)在基类和 GLM 子类各存一份且公式不同,基类策略变更时 GLM 会静默偏移。
  • Warningxtuner/v1/model/moe/moe.py_decoder_stack / _micro_batch_decoder_stack:抽出的私有方法带 11 个关键字参数并把 output: dict 当可变入出参写入,没有隐藏规则,只是把循环搬家。

其他 Issues

  • Warningxtuner/v1/model/moe/glm52/glm52.py:删除 index_share_for_mtp_iteration 的唯一运行时使用点后,该配置项仍保留在 config、from_hf 校验和 to_hf_config 回写中,但设为 False 既不改变行为也不报错,变成静默失效的配置。
  • Warningxtuner/v1/model/moe/moe.py_saved_tensors_offload_ctx:offload 匹配从 data_ptr() 放宽为 untyped_storage().data_ptr(),会把共享 storage 的视图一并纳入窗口,且单 micro-batch 路径新增常驻 pinned 缓冲并改变 block_idx 编号,而现有基准未覆盖 XTUNER_ACTIVATION_OFFLOAD=1 的单 micro-batch 路径。
  • Warningxtuner/v1/model/compose/qwen3_vl/modeling_vision.pyxtuner/v1/model/compose/intern_s1/modeling_vision.py:compile 目标改为 checkpoint wrapper 的 forward 且降为 fullgraph=False,但内层 vision layer 并没有注释所称的"独立 full-graph 配置",fullgraph 保证被静默移除且本 PR 无 VL 回归验证。

Verdict

REQUEST_CHANGES

Comment on lines +152 to +171
activation_offload = int(os.getenv("XTUNER_ACTIVATION_OFFLOAD", "0")) == 1
dsa_topk_offload = int(os.getenv("XTUNER_DSA_TOPK_OFFLOAD", "0")) == 1
offload_tensors: list[torch.Tensor] = []
if activation_offload and layer_idx >= self.config.first_k_dense_replace:
offload_tensors = list(hidden_states) if is_micro_batch else [cast(torch.Tensor, hidden_states)]
if dsa_topk_offload and layer_idx in self._dsa_topk_last_consumers and dsa_topk_ids is not None:
offload_tensors.extend(dsa_topk_ids if isinstance(dsa_topk_ids, list) else [dsa_topk_ids])

# The offload context expects a dense zero-based block index across every
# active activation or DSA-ID window, including dense GLM layers.
offload_block_idx = sum(
(activation_offload and previous_idx >= self.config.first_k_dense_replace)
or (
dsa_topk_offload
and previous_idx in self._dsa_topk_last_consumers
and self._dsa_topk_source_layers[previous_idx] != previous_idx
)
for previous_idx in (int(idx) for idx in self.layers)
if previous_idx < layer_idx
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [设计] activation-offload 规则出现两份实现。

Problem:基类 MoE._call_decoder_layer 已把「窗口张量选择 + block_idx 编号」写死在方法体内,GLM 为了插入 DSA IDs 只能整段 override,于是同一条规则有了两个不同公式:基类是 sum(first_k_dense_replace <= idx < layer_idx),这里则把 DSA 末位消费层也计入密集编号。做 Deletion test:删掉这个 override 会同时丢失 DSA 数据流 offload 规则,说明 Seam 切在了整个 _call_decoder_layer 上——位置偏大。后果是基类调整 offload 策略时,GLM 侧不会报错,只会静默错位到另一套编号。

Solution:基类保留唯一的 _call_decoder_layer(窗口构造 + block 计数),只把变化点做成小钩子,GLM 仅覆盖钩子:

# MoEdef_extra_offload_tensors(self, layer_idx, hidden_states) ->list[torch.Tensor]:
return []
def_layer_forward_kwargs(self, layer_idx, previous_layer_results) ->dict:
return {}

Benefits:offload 规则单点化(Locality),子类需要了解的 Interface 从「整个调用 + 窗口 + 编号」缩小到两个钩子(Leverage),block_idx 编号也能在基类上直接写 CPU 单测。

GLM52MoEDecoderLayerOutput | GLM52MoEDecoderLayerMicroBatchOutput | None,
previous_layer_results,
)
dsa_topk_ids = None if previous_results is None else previous_results["dsa_topk_ids"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [兼容性] index_share_for_mtp_iteration 的唯一运行时使用点已被删除,这里无条件透传上一 depth 的 IDs。但该字段仍保留在 config、from_hf 校验与 to_hf_config 回写中,设为 False 既不生效也不报错。建议按该配置决定是否透传,或在配置层直接拒绝 False

Comment on lines +302 to +308
"""Build one policy-neutral saved-tensor offload window.

The decoder-stack caller decides which tensors belong to the current
window and advances ``block_idx`` only when the list is non-empty.
"""
if not tensors:
return contextlib.nullcontext()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [正确性] 匹配条件从原来的 x.data_ptr() == hidden_states.data_ptr() 放宽为 storage 指针,会把共享 storage 的非零偏移视图一并纳入窗口,同一 storage 可能被重复 pack/D2H。另外单 micro-batch 路径新增了 reserve_pin_memory=True 且 block_idx 由绝对层号改为相对号,改变了 OffloadManager 的 key 与 prefetch 顺序,而本 PR 基准未覆盖 XTUNER_ACTIVATION_OFFLOAD=1 的单 micro-batch 路径。

Comment on lines 349 to +353
if self.compile_cfg:
layer.forward = torch.compile(layer.forward, fullgraph=True)
# The PyTree checkpoint adapter is an intentional graph break; model compute
# inside it keeps the independently configured full-graph compilation.
compiled_forward = torch.compile(type(layer).forward, fullgraph=False)
layer.forward = compiled_forward.__get__(layer, type(layer))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [性能] 此处 type(layer) 已是 CheckpointWrapper,而内层 Qwen3VLVisionLayer.forward 并没有注释所说的「独立 full-graph 配置」(该文件只 import 了 maybe_compile,未装饰任何 vision forward)。结果是 fullgraph 保证被静默移除,vision 侧 graph break 不再报错,且本 PR 无 VL 回归验证。建议把 fullgraph=True 保留在被包裹模块自身的 forward 上,并补一条 VL 验证。

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jayhenry@YanhuiDua
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[Refactor] Unify SAC checkpointing and GLM DSA dataflow - #2039

Open
jayhenry wants to merge 6 commits into
InternLM:mainfrom
jayhenry:refactor/sac-checkpoint-dsa-dataflow
Open

[Refactor] Unify SAC checkpointing and GLM DSA dataflow#2039
jayhenry wants to merge 6 commits into
InternLM:mainfrom
jayhenry:refactor/sac-checkpoint-dsa-dataflow

Conversation

@jayhenry

Copy link
Copy Markdown
Collaborator

Summary

  • Unify activation checkpointing around one PyTree-aware reentrant boundary so structured micro-batch inputs and outputs stay on the real autograd and offload path.
  • Pass GLM DSA top-k IDs explicitly through decoder and MTP outputs, removing the mutable SequenceContext cache lifecycle.
  • Reuse frozen source-indexer results from checkpoint-local FIFO frames during replay.
  • Keep FSDP wrappers outside recompute and dense checkpoint boundaries outside compiled graphs.

Regression coverage

  • compile + top-k offload + shared-weight MTP with depth > 1; validates pinned-memory offload and restore, and that the source indexer is not recomputed
  • EP > 1 + intra-layer micro-batch 2
  • FSDP checkpointing, torch.compile, FP8, activation offload, and nested PyTree inputs and outputs

Validation

  • Post-rebase on latest upstream/main: 16 passed
    • tests/model/test_recompute.py
    • tests/model/test_glm52_mtp_checkpoint_repro.py
    • tests/model/test_fsdp_checkpoint.py
  • Qwen3.5 full 40-layer stack5 regression: step losses and grad norms matched upstream; steady sequence TGS +0.36%; peak allocated memory -0.14 GB.
  • GLM AdamW production-shaped and Muon EP2/micro2 training regressions completed.

@jayhenry
jayhenryforce-pushed the refactor/sac-checkpoint-dsa-dataflow branch from d78f607 to 81e0010CompareAugust 31, 2026 09:48
Flatten structured inputs and outputs at one checkpoint boundary so nested MTP micro-batch tensors receive gradients. Keep FSDP outside replay and remove the non-reentrant MTP switch.
Move GLM-specific decoder and MTP adapters under the model package, thread DSA IDs through keyed outputs, and remove SequenceContext cache lifecycle state. Share one saved-tensor offload window for activation and DSA ID storage.
Keep reusable no-grad outputs in checkpoint-call-local FIFO frames, freeze DSA indexers through their model config, and preserve the original int32 storage across shared layers and offload. Add real regressions for source call counts and pinned-memory DSA offload.
Only inject the detached-input grad entry when the checkpointed module still has trainable parameters. This preserves MTP parameter gradients while keeping frozen vision outputs detached, avoiding a replay with no differentiable outputs.
@jayhenry
jayhenryforce-pushed the refactor/sac-checkpoint-dsa-dataflow branch from b2047db to 5c15855CompareSeptember 2, 2026 02:35
@YanhuiDua

Copy link
Copy Markdown
Collaborator

@claude review

@github-actions

Copy link
Copy Markdown
Contributor

Claude:

Summary

本 PR 把 activation checkpointing 统一到一个 PyTree-aware 的 reentrant 边界(apply_activation_checkpointing + reuse_during_recompute),并把 GLM-5.2 的 DSA top-k 从 SequenceContext 上的可变缓存改为经 decoder / MTP 输出显式穿参,同时将 decoder 与 MTP 的返回值从位置元组改为 TypedDict,GLM-5.2 相关代码收敛到新的 xtuner/v1/model/moe/glm52/ 包。整体方向是把隐式生命周期换成显式数据流,抽象更清晰;但 offload 策略在基类与 GLM 子类各写了一份,另有一处配置项失效、一处 offload 匹配语义放宽、一处 vision compile 降级需要确认。

Main Flowchart after this PR

flowchart TD
A["MoE._forward / _micro_batch_forward"] --> B["_decoder_stack /<br/>_micro_batch_decoder_stack<br/>(新增, 11 参数 + 可变 output dict)"]
B --> C{"模型类型"}
C -->|"通用 MoE"| D["MoE._call_decoder_layer<br/>(offload 窗口 + block_idx 规则)"]
C -->|"GLM-5.2"| E["Glm52MoE._call_decoder_layer<br/>(整段 override: DSA IDs + 另一套 block_idx 规则)"]
D --> F["_saved_tensors_offload_ctx<br/>(storage-ptr 匹配, reserve_pin_memory)"]
E --> F
F --> G["decoder_layer(...) -> TypedDict"]
G --> H["GLM52AttnOutputs.dsa_topk_ids<br/>显式传给下一 consumer 层"]
H --> I["MTPBlock._call_decoder_layer<br/>GLM52MTPBlock 无条件透传 dsa_topk_ids"]
style D fill:#ffe0b2,stroke:#e65100
style E fill:#ffcdd2,stroke:#b71c1c
style F fill:#ffcdd2,stroke:#b71c1c
style I fill:#ffe0b2,stroke:#e65100
Loading

核心原理实现与单测

  • Checkpoint 统一边界apply_activation_checkpointing 固定 reentrant 并用 PyTree 桥接展平输入 / 还原输出,reuse_during_recomputeContextVar + 按 callable 的 FIFO frame 支撑 replay 复用。tests/model/test_recompute.py 全部走 public API,覆盖了 grad 模式序列 [False, True]、全 detach 输入仍产参数梯度、冻结模块不建立 replay 边、嵌套 / 关键字输入可被外层 saved_tensors_hooks 捕获(即 offload 不会静默空转)、replay 输出结构变化报错、以及跨两次 checkpoint 的 FIFO 隔离——核心行为有真实代码路径覆盖,未 mock 项目内模块。
  • GLM DSA 显式数据流:source 层算一次 IDs,consumer 层由 Glm52MoE._call_decoder_layer 显式传入;tests/module/attention/test_dsa_mla.pytests/model/test_glm52_moe.py::TestGlm52ExplicitDsaDataflowtests/model/test_glm52_mtp_checkpoint_repro.py(indexer 调用计数 1 / 2、pinned D2H 真实命中并在 backward 恢复)覆盖了 source 不重算与 top-k offload。
  • 覆盖缺口(未达 Warning,仅提示):_call_decoder_layer 的 offload block-index 计算无 CPU 可跑单测;index_share_for_mtp_iteration=False 无任何测试。

抽象与信息隐藏评估

  • Warningxtuner/v1/model/moe/moe.py_call_decoder_layerxtuner/v1/model/moe/glm52/glm52.py 的 override:Seam 切在整个 _call_decoder_layer 上,导致同一条 activation-offload 规则(窗口张量 + block_idx 编号)在基类和 GLM 子类各存一份且公式不同,基类策略变更时 GLM 会静默偏移。
  • Warningxtuner/v1/model/moe/moe.py_decoder_stack / _micro_batch_decoder_stack:抽出的私有方法带 11 个关键字参数并把 output: dict 当可变入出参写入,没有隐藏规则,只是把循环搬家。

其他 Issues

  • Warningxtuner/v1/model/moe/glm52/glm52.py:删除 index_share_for_mtp_iteration 的唯一运行时使用点后,该配置项仍保留在 config、from_hf 校验和 to_hf_config 回写中,但设为 False 既不改变行为也不报错,变成静默失效的配置。
  • Warningxtuner/v1/model/moe/moe.py_saved_tensors_offload_ctx:offload 匹配从 data_ptr() 放宽为 untyped_storage().data_ptr(),会把共享 storage 的视图一并纳入窗口,且单 micro-batch 路径新增常驻 pinned 缓冲并改变 block_idx 编号,而现有基准未覆盖 XTUNER_ACTIVATION_OFFLOAD=1 的单 micro-batch 路径。
  • Warningxtuner/v1/model/compose/qwen3_vl/modeling_vision.pyxtuner/v1/model/compose/intern_s1/modeling_vision.py:compile 目标改为 checkpoint wrapper 的 forward 且降为 fullgraph=False,但内层 vision layer 并没有注释所称的"独立 full-graph 配置",fullgraph 保证被静默移除且本 PR 无 VL 回归验证。

Verdict

REQUEST_CHANGES

Comment on lines +152 to +171
activation_offload = int(os.getenv("XTUNER_ACTIVATION_OFFLOAD", "0")) == 1
dsa_topk_offload = int(os.getenv("XTUNER_DSA_TOPK_OFFLOAD", "0")) == 1
offload_tensors: list[torch.Tensor] = []
if activation_offload and layer_idx >= self.config.first_k_dense_replace:
offload_tensors = list(hidden_states) if is_micro_batch else [cast(torch.Tensor, hidden_states)]
if dsa_topk_offload and layer_idx in self._dsa_topk_last_consumers and dsa_topk_ids is not None:
offload_tensors.extend(dsa_topk_ids if isinstance(dsa_topk_ids, list) else [dsa_topk_ids])

# The offload context expects a dense zero-based block index across every
# active activation or DSA-ID window, including dense GLM layers.
offload_block_idx = sum(
(activation_offload and previous_idx >= self.config.first_k_dense_replace)
or (
dsa_topk_offload
and previous_idx in self._dsa_topk_last_consumers
and self._dsa_topk_source_layers[previous_idx] != previous_idx
)
for previous_idx in (int(idx) for idx in self.layers)
if previous_idx < layer_idx
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [设计] activation-offload 规则出现两份实现。

Problem:基类 MoE._call_decoder_layer 已把「窗口张量选择 + block_idx 编号」写死在方法体内,GLM 为了插入 DSA IDs 只能整段 override,于是同一条规则有了两个不同公式:基类是 sum(first_k_dense_replace <= idx < layer_idx),这里则把 DSA 末位消费层也计入密集编号。做 Deletion test:删掉这个 override 会同时丢失 DSA 数据流 offload 规则,说明 Seam 切在了整个 _call_decoder_layer 上——位置偏大。后果是基类调整 offload 策略时,GLM 侧不会报错,只会静默错位到另一套编号。

Solution:基类保留唯一的 _call_decoder_layer(窗口构造 + block 计数),只把变化点做成小钩子,GLM 仅覆盖钩子:

# MoEdef_extra_offload_tensors(self, layer_idx, hidden_states) ->list[torch.Tensor]:
return []
def_layer_forward_kwargs(self, layer_idx, previous_layer_results) ->dict:
return {}

Benefits:offload 规则单点化(Locality),子类需要了解的 Interface 从「整个调用 + 窗口 + 编号」缩小到两个钩子(Leverage),block_idx 编号也能在基类上直接写 CPU 单测。

GLM52MoEDecoderLayerOutput | GLM52MoEDecoderLayerMicroBatchOutput | None,
previous_layer_results,
)
dsa_topk_ids = None if previous_results is None else previous_results["dsa_topk_ids"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [兼容性] index_share_for_mtp_iteration 的唯一运行时使用点已被删除,这里无条件透传上一 depth 的 IDs。但该字段仍保留在 config、from_hf 校验与 to_hf_config 回写中,设为 False 既不生效也不报错。建议按该配置决定是否透传,或在配置层直接拒绝 False

Comment on lines +302 to +308
"""Build one policy-neutral saved-tensor offload window.

The decoder-stack caller decides which tensors belong to the current
window and advances ``block_idx`` only when the list is non-empty.
"""
if not tensors:
return contextlib.nullcontext()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [正确性] 匹配条件从原来的 x.data_ptr() == hidden_states.data_ptr() 放宽为 storage 指针,会把共享 storage 的非零偏移视图一并纳入窗口,同一 storage 可能被重复 pack/D2H。另外单 micro-batch 路径新增了 reserve_pin_memory=True 且 block_idx 由绝对层号改为相对号,改变了 OffloadManager 的 key 与 prefetch 顺序,而本 PR 基准未覆盖 XTUNER_ACTIVATION_OFFLOAD=1 的单 micro-batch 路径。

Comment on lines 349 to +353
if self.compile_cfg:
layer.forward = torch.compile(layer.forward, fullgraph=True)
# The PyTree checkpoint adapter is an intentional graph break; model compute
# inside it keeps the independently configured full-graph compilation.
compiled_forward = torch.compile(type(layer).forward, fullgraph=False)
layer.forward = compiled_forward.__get__(layer, type(layer))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [性能] 此处 type(layer) 已是 CheckpointWrapper,而内层 Qwen3VLVisionLayer.forward 并没有注释所说的「独立 full-graph 配置」(该文件只 import 了 maybe_compile,未装饰任何 vision forward)。结果是 fullgraph 保证被静默移除,vision 侧 graph break 不再报错,且本 PR 无 VL 回归验证。建议把 fullgraph=True 保留在被包裹模块自身的 forward 上,并补一条 VL 验证。

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jayhenry@YanhuiDua
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[Refactor] Unify SAC checkpointing and GLM DSA dataflow - #2039

Open
jayhenry wants to merge 6 commits into
InternLM:mainfrom
jayhenry:refactor/sac-checkpoint-dsa-dataflow
Open

[Refactor] Unify SAC checkpointing and GLM DSA dataflow#2039
jayhenry wants to merge 6 commits into
InternLM:mainfrom
jayhenry:refactor/sac-checkpoint-dsa-dataflow

Conversation

@jayhenry

Copy link
Copy Markdown
Collaborator

Summary

  • Unify activation checkpointing around one PyTree-aware reentrant boundary so structured micro-batch inputs and outputs stay on the real autograd and offload path.
  • Pass GLM DSA top-k IDs explicitly through decoder and MTP outputs, removing the mutable SequenceContext cache lifecycle.
  • Reuse frozen source-indexer results from checkpoint-local FIFO frames during replay.
  • Keep FSDP wrappers outside recompute and dense checkpoint boundaries outside compiled graphs.

Regression coverage

  • compile + top-k offload + shared-weight MTP with depth > 1; validates pinned-memory offload and restore, and that the source indexer is not recomputed
  • EP > 1 + intra-layer micro-batch 2
  • FSDP checkpointing, torch.compile, FP8, activation offload, and nested PyTree inputs and outputs

Validation

  • Post-rebase on latest upstream/main: 16 passed
    • tests/model/test_recompute.py
    • tests/model/test_glm52_mtp_checkpoint_repro.py
    • tests/model/test_fsdp_checkpoint.py
  • Qwen3.5 full 40-layer stack5 regression: step losses and grad norms matched upstream; steady sequence TGS +0.36%; peak allocated memory -0.14 GB.
  • GLM AdamW production-shaped and Muon EP2/micro2 training regressions completed.

@jayhenry
jayhenryforce-pushed the refactor/sac-checkpoint-dsa-dataflow branch from d78f607 to 81e0010CompareAugust 31, 2026 09:48
Flatten structured inputs and outputs at one checkpoint boundary so nested MTP micro-batch tensors receive gradients. Keep FSDP outside replay and remove the non-reentrant MTP switch.
Move GLM-specific decoder and MTP adapters under the model package, thread DSA IDs through keyed outputs, and remove SequenceContext cache lifecycle state. Share one saved-tensor offload window for activation and DSA ID storage.
Keep reusable no-grad outputs in checkpoint-call-local FIFO frames, freeze DSA indexers through their model config, and preserve the original int32 storage across shared layers and offload. Add real regressions for source call counts and pinned-memory DSA offload.
Only inject the detached-input grad entry when the checkpointed module still has trainable parameters. This preserves MTP parameter gradients while keeping frozen vision outputs detached, avoiding a replay with no differentiable outputs.
@jayhenry
jayhenryforce-pushed the refactor/sac-checkpoint-dsa-dataflow branch from b2047db to 5c15855CompareSeptember 2, 2026 02:35
@YanhuiDua

Copy link
Copy Markdown
Collaborator

@claude review

@github-actions

Copy link
Copy Markdown
Contributor

Claude:

Summary

本 PR 把 activation checkpointing 统一到一个 PyTree-aware 的 reentrant 边界(apply_activation_checkpointing + reuse_during_recompute),并把 GLM-5.2 的 DSA top-k 从 SequenceContext 上的可变缓存改为经 decoder / MTP 输出显式穿参,同时将 decoder 与 MTP 的返回值从位置元组改为 TypedDict,GLM-5.2 相关代码收敛到新的 xtuner/v1/model/moe/glm52/ 包。整体方向是把隐式生命周期换成显式数据流,抽象更清晰;但 offload 策略在基类与 GLM 子类各写了一份,另有一处配置项失效、一处 offload 匹配语义放宽、一处 vision compile 降级需要确认。

Main Flowchart after this PR

flowchart TD
A["MoE._forward / _micro_batch_forward"] --> B["_decoder_stack /<br/>_micro_batch_decoder_stack<br/>(新增, 11 参数 + 可变 output dict)"]
B --> C{"模型类型"}
C -->|"通用 MoE"| D["MoE._call_decoder_layer<br/>(offload 窗口 + block_idx 规则)"]
C -->|"GLM-5.2"| E["Glm52MoE._call_decoder_layer<br/>(整段 override: DSA IDs + 另一套 block_idx 规则)"]
D --> F["_saved_tensors_offload_ctx<br/>(storage-ptr 匹配, reserve_pin_memory)"]
E --> F
F --> G["decoder_layer(...) -> TypedDict"]
G --> H["GLM52AttnOutputs.dsa_topk_ids<br/>显式传给下一 consumer 层"]
H --> I["MTPBlock._call_decoder_layer<br/>GLM52MTPBlock 无条件透传 dsa_topk_ids"]
style D fill:#ffe0b2,stroke:#e65100
style E fill:#ffcdd2,stroke:#b71c1c
style F fill:#ffcdd2,stroke:#b71c1c
style I fill:#ffe0b2,stroke:#e65100
Loading

核心原理实现与单测

  • Checkpoint 统一边界apply_activation_checkpointing 固定 reentrant 并用 PyTree 桥接展平输入 / 还原输出,reuse_during_recomputeContextVar + 按 callable 的 FIFO frame 支撑 replay 复用。tests/model/test_recompute.py 全部走 public API,覆盖了 grad 模式序列 [False, True]、全 detach 输入仍产参数梯度、冻结模块不建立 replay 边、嵌套 / 关键字输入可被外层 saved_tensors_hooks 捕获(即 offload 不会静默空转)、replay 输出结构变化报错、以及跨两次 checkpoint 的 FIFO 隔离——核心行为有真实代码路径覆盖,未 mock 项目内模块。
  • GLM DSA 显式数据流:source 层算一次 IDs,consumer 层由 Glm52MoE._call_decoder_layer 显式传入;tests/module/attention/test_dsa_mla.pytests/model/test_glm52_moe.py::TestGlm52ExplicitDsaDataflowtests/model/test_glm52_mtp_checkpoint_repro.py(indexer 调用计数 1 / 2、pinned D2H 真实命中并在 backward 恢复)覆盖了 source 不重算与 top-k offload。
  • 覆盖缺口(未达 Warning,仅提示):_call_decoder_layer 的 offload block-index 计算无 CPU 可跑单测;index_share_for_mtp_iteration=False 无任何测试。

抽象与信息隐藏评估

  • Warningxtuner/v1/model/moe/moe.py_call_decoder_layerxtuner/v1/model/moe/glm52/glm52.py 的 override:Seam 切在整个 _call_decoder_layer 上,导致同一条 activation-offload 规则(窗口张量 + block_idx 编号)在基类和 GLM 子类各存一份且公式不同,基类策略变更时 GLM 会静默偏移。
  • Warningxtuner/v1/model/moe/moe.py_decoder_stack / _micro_batch_decoder_stack:抽出的私有方法带 11 个关键字参数并把 output: dict 当可变入出参写入,没有隐藏规则,只是把循环搬家。

其他 Issues

  • Warningxtuner/v1/model/moe/glm52/glm52.py:删除 index_share_for_mtp_iteration 的唯一运行时使用点后,该配置项仍保留在 config、from_hf 校验和 to_hf_config 回写中,但设为 False 既不改变行为也不报错,变成静默失效的配置。
  • Warningxtuner/v1/model/moe/moe.py_saved_tensors_offload_ctx:offload 匹配从 data_ptr() 放宽为 untyped_storage().data_ptr(),会把共享 storage 的视图一并纳入窗口,且单 micro-batch 路径新增常驻 pinned 缓冲并改变 block_idx 编号,而现有基准未覆盖 XTUNER_ACTIVATION_OFFLOAD=1 的单 micro-batch 路径。
  • Warningxtuner/v1/model/compose/qwen3_vl/modeling_vision.pyxtuner/v1/model/compose/intern_s1/modeling_vision.py:compile 目标改为 checkpoint wrapper 的 forward 且降为 fullgraph=False,但内层 vision layer 并没有注释所称的"独立 full-graph 配置",fullgraph 保证被静默移除且本 PR 无 VL 回归验证。

Verdict

REQUEST_CHANGES

Comment on lines +152 to +171
activation_offload = int(os.getenv("XTUNER_ACTIVATION_OFFLOAD", "0")) == 1
dsa_topk_offload = int(os.getenv("XTUNER_DSA_TOPK_OFFLOAD", "0")) == 1
offload_tensors: list[torch.Tensor] = []
if activation_offload and layer_idx >= self.config.first_k_dense_replace:
offload_tensors = list(hidden_states) if is_micro_batch else [cast(torch.Tensor, hidden_states)]
if dsa_topk_offload and layer_idx in self._dsa_topk_last_consumers and dsa_topk_ids is not None:
offload_tensors.extend(dsa_topk_ids if isinstance(dsa_topk_ids, list) else [dsa_topk_ids])

# The offload context expects a dense zero-based block index across every
# active activation or DSA-ID window, including dense GLM layers.
offload_block_idx = sum(
(activation_offload and previous_idx >= self.config.first_k_dense_replace)
or (
dsa_topk_offload
and previous_idx in self._dsa_topk_last_consumers
and self._dsa_topk_source_layers[previous_idx] != previous_idx
)
for previous_idx in (int(idx) for idx in self.layers)
if previous_idx < layer_idx
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [设计] activation-offload 规则出现两份实现。

Problem:基类 MoE._call_decoder_layer 已把「窗口张量选择 + block_idx 编号」写死在方法体内,GLM 为了插入 DSA IDs 只能整段 override,于是同一条规则有了两个不同公式:基类是 sum(first_k_dense_replace <= idx < layer_idx),这里则把 DSA 末位消费层也计入密集编号。做 Deletion test:删掉这个 override 会同时丢失 DSA 数据流 offload 规则,说明 Seam 切在了整个 _call_decoder_layer 上——位置偏大。后果是基类调整 offload 策略时,GLM 侧不会报错,只会静默错位到另一套编号。

Solution:基类保留唯一的 _call_decoder_layer(窗口构造 + block 计数),只把变化点做成小钩子,GLM 仅覆盖钩子:

# MoEdef_extra_offload_tensors(self, layer_idx, hidden_states) ->list[torch.Tensor]:
return []
def_layer_forward_kwargs(self, layer_idx, previous_layer_results) ->dict:
return {}

Benefits:offload 规则单点化(Locality),子类需要了解的 Interface 从「整个调用 + 窗口 + 编号」缩小到两个钩子(Leverage),block_idx 编号也能在基类上直接写 CPU 单测。

GLM52MoEDecoderLayerOutput | GLM52MoEDecoderLayerMicroBatchOutput | None,
previous_layer_results,
)
dsa_topk_ids = None if previous_results is None else previous_results["dsa_topk_ids"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [兼容性] index_share_for_mtp_iteration 的唯一运行时使用点已被删除,这里无条件透传上一 depth 的 IDs。但该字段仍保留在 config、from_hf 校验与 to_hf_config 回写中,设为 False 既不生效也不报错。建议按该配置决定是否透传,或在配置层直接拒绝 False

Comment on lines +302 to +308
"""Build one policy-neutral saved-tensor offload window.

The decoder-stack caller decides which tensors belong to the current
window and advances ``block_idx`` only when the list is non-empty.
"""
if not tensors:
return contextlib.nullcontext()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [正确性] 匹配条件从原来的 x.data_ptr() == hidden_states.data_ptr() 放宽为 storage 指针,会把共享 storage 的非零偏移视图一并纳入窗口,同一 storage 可能被重复 pack/D2H。另外单 micro-batch 路径新增了 reserve_pin_memory=True 且 block_idx 由绝对层号改为相对号,改变了 OffloadManager 的 key 与 prefetch 顺序,而本 PR 基准未覆盖 XTUNER_ACTIVATION_OFFLOAD=1 的单 micro-batch 路径。

Comment on lines 349 to +353
if self.compile_cfg:
layer.forward = torch.compile(layer.forward, fullgraph=True)
# The PyTree checkpoint adapter is an intentional graph break; model compute
# inside it keeps the independently configured full-graph compilation.
compiled_forward = torch.compile(type(layer).forward, fullgraph=False)
layer.forward = compiled_forward.__get__(layer, type(layer))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [性能] 此处 type(layer) 已是 CheckpointWrapper,而内层 Qwen3VLVisionLayer.forward 并没有注释所说的「独立 full-graph 配置」(该文件只 import 了 maybe_compile,未装饰任何 vision forward)。结果是 fullgraph 保证被静默移除,vision 侧 graph break 不再报错,且本 PR 无 VL 回归验证。建议把 fullgraph=True 保留在被包裹模块自身的 forward 上,并补一条 VL 验证。

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jayhenry@YanhuiDua
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[Refactor] Unify SAC checkpointing and GLM DSA dataflow - #2039

Open
jayhenry wants to merge 6 commits into
InternLM:mainfrom
jayhenry:refactor/sac-checkpoint-dsa-dataflow
Open

[Refactor] Unify SAC checkpointing and GLM DSA dataflow#2039
jayhenry wants to merge 6 commits into
InternLM:mainfrom
jayhenry:refactor/sac-checkpoint-dsa-dataflow

Conversation

@jayhenry

Copy link
Copy Markdown
Collaborator

Summary

  • Unify activation checkpointing around one PyTree-aware reentrant boundary so structured micro-batch inputs and outputs stay on the real autograd and offload path.
  • Pass GLM DSA top-k IDs explicitly through decoder and MTP outputs, removing the mutable SequenceContext cache lifecycle.
  • Reuse frozen source-indexer results from checkpoint-local FIFO frames during replay.
  • Keep FSDP wrappers outside recompute and dense checkpoint boundaries outside compiled graphs.

Regression coverage

  • compile + top-k offload + shared-weight MTP with depth > 1; validates pinned-memory offload and restore, and that the source indexer is not recomputed
  • EP > 1 + intra-layer micro-batch 2
  • FSDP checkpointing, torch.compile, FP8, activation offload, and nested PyTree inputs and outputs

Validation

  • Post-rebase on latest upstream/main: 16 passed
    • tests/model/test_recompute.py
    • tests/model/test_glm52_mtp_checkpoint_repro.py
    • tests/model/test_fsdp_checkpoint.py
  • Qwen3.5 full 40-layer stack5 regression: step losses and grad norms matched upstream; steady sequence TGS +0.36%; peak allocated memory -0.14 GB.
  • GLM AdamW production-shaped and Muon EP2/micro2 training regressions completed.

@jayhenry
jayhenryforce-pushed the refactor/sac-checkpoint-dsa-dataflow branch from d78f607 to 81e0010CompareAugust 31, 2026 09:48
Flatten structured inputs and outputs at one checkpoint boundary so nested MTP micro-batch tensors receive gradients. Keep FSDP outside replay and remove the non-reentrant MTP switch.
Move GLM-specific decoder and MTP adapters under the model package, thread DSA IDs through keyed outputs, and remove SequenceContext cache lifecycle state. Share one saved-tensor offload window for activation and DSA ID storage.
Keep reusable no-grad outputs in checkpoint-call-local FIFO frames, freeze DSA indexers through their model config, and preserve the original int32 storage across shared layers and offload. Add real regressions for source call counts and pinned-memory DSA offload.
Only inject the detached-input grad entry when the checkpointed module still has trainable parameters. This preserves MTP parameter gradients while keeping frozen vision outputs detached, avoiding a replay with no differentiable outputs.
@jayhenry
jayhenryforce-pushed the refactor/sac-checkpoint-dsa-dataflow branch from b2047db to 5c15855CompareSeptember 2, 2026 02:35
@YanhuiDua

Copy link
Copy Markdown
Collaborator

@claude review

@github-actions

Copy link
Copy Markdown
Contributor

Claude:

Summary

本 PR 把 activation checkpointing 统一到一个 PyTree-aware 的 reentrant 边界(apply_activation_checkpointing + reuse_during_recompute),并把 GLM-5.2 的 DSA top-k 从 SequenceContext 上的可变缓存改为经 decoder / MTP 输出显式穿参,同时将 decoder 与 MTP 的返回值从位置元组改为 TypedDict,GLM-5.2 相关代码收敛到新的 xtuner/v1/model/moe/glm52/ 包。整体方向是把隐式生命周期换成显式数据流,抽象更清晰;但 offload 策略在基类与 GLM 子类各写了一份,另有一处配置项失效、一处 offload 匹配语义放宽、一处 vision compile 降级需要确认。

Main Flowchart after this PR

flowchart TD
A["MoE._forward / _micro_batch_forward"] --> B["_decoder_stack /<br/>_micro_batch_decoder_stack<br/>(新增, 11 参数 + 可变 output dict)"]
B --> C{"模型类型"}
C -->|"通用 MoE"| D["MoE._call_decoder_layer<br/>(offload 窗口 + block_idx 规则)"]
C -->|"GLM-5.2"| E["Glm52MoE._call_decoder_layer<br/>(整段 override: DSA IDs + 另一套 block_idx 规则)"]
D --> F["_saved_tensors_offload_ctx<br/>(storage-ptr 匹配, reserve_pin_memory)"]
E --> F
F --> G["decoder_layer(...) -> TypedDict"]
G --> H["GLM52AttnOutputs.dsa_topk_ids<br/>显式传给下一 consumer 层"]
H --> I["MTPBlock._call_decoder_layer<br/>GLM52MTPBlock 无条件透传 dsa_topk_ids"]
style D fill:#ffe0b2,stroke:#e65100
style E fill:#ffcdd2,stroke:#b71c1c
style F fill:#ffcdd2,stroke:#b71c1c
style I fill:#ffe0b2,stroke:#e65100
Loading

核心原理实现与单测

  • Checkpoint 统一边界apply_activation_checkpointing 固定 reentrant 并用 PyTree 桥接展平输入 / 还原输出,reuse_during_recomputeContextVar + 按 callable 的 FIFO frame 支撑 replay 复用。tests/model/test_recompute.py 全部走 public API,覆盖了 grad 模式序列 [False, True]、全 detach 输入仍产参数梯度、冻结模块不建立 replay 边、嵌套 / 关键字输入可被外层 saved_tensors_hooks 捕获(即 offload 不会静默空转)、replay 输出结构变化报错、以及跨两次 checkpoint 的 FIFO 隔离——核心行为有真实代码路径覆盖,未 mock 项目内模块。
  • GLM DSA 显式数据流:source 层算一次 IDs,consumer 层由 Glm52MoE._call_decoder_layer 显式传入;tests/module/attention/test_dsa_mla.pytests/model/test_glm52_moe.py::TestGlm52ExplicitDsaDataflowtests/model/test_glm52_mtp_checkpoint_repro.py(indexer 调用计数 1 / 2、pinned D2H 真实命中并在 backward 恢复)覆盖了 source 不重算与 top-k offload。
  • 覆盖缺口(未达 Warning,仅提示):_call_decoder_layer 的 offload block-index 计算无 CPU 可跑单测;index_share_for_mtp_iteration=False 无任何测试。

抽象与信息隐藏评估

  • Warningxtuner/v1/model/moe/moe.py_call_decoder_layerxtuner/v1/model/moe/glm52/glm52.py 的 override:Seam 切在整个 _call_decoder_layer 上,导致同一条 activation-offload 规则(窗口张量 + block_idx 编号)在基类和 GLM 子类各存一份且公式不同,基类策略变更时 GLM 会静默偏移。
  • Warningxtuner/v1/model/moe/moe.py_decoder_stack / _micro_batch_decoder_stack:抽出的私有方法带 11 个关键字参数并把 output: dict 当可变入出参写入,没有隐藏规则,只是把循环搬家。

其他 Issues

  • Warningxtuner/v1/model/moe/glm52/glm52.py:删除 index_share_for_mtp_iteration 的唯一运行时使用点后,该配置项仍保留在 config、from_hf 校验和 to_hf_config 回写中,但设为 False 既不改变行为也不报错,变成静默失效的配置。
  • Warningxtuner/v1/model/moe/moe.py_saved_tensors_offload_ctx:offload 匹配从 data_ptr() 放宽为 untyped_storage().data_ptr(),会把共享 storage 的视图一并纳入窗口,且单 micro-batch 路径新增常驻 pinned 缓冲并改变 block_idx 编号,而现有基准未覆盖 XTUNER_ACTIVATION_OFFLOAD=1 的单 micro-batch 路径。
  • Warningxtuner/v1/model/compose/qwen3_vl/modeling_vision.pyxtuner/v1/model/compose/intern_s1/modeling_vision.py:compile 目标改为 checkpoint wrapper 的 forward 且降为 fullgraph=False,但内层 vision layer 并没有注释所称的"独立 full-graph 配置",fullgraph 保证被静默移除且本 PR 无 VL 回归验证。

Verdict

REQUEST_CHANGES

Comment on lines +152 to +171
activation_offload = int(os.getenv("XTUNER_ACTIVATION_OFFLOAD", "0")) == 1
dsa_topk_offload = int(os.getenv("XTUNER_DSA_TOPK_OFFLOAD", "0")) == 1
offload_tensors: list[torch.Tensor] = []
if activation_offload and layer_idx >= self.config.first_k_dense_replace:
offload_tensors = list(hidden_states) if is_micro_batch else [cast(torch.Tensor, hidden_states)]
if dsa_topk_offload and layer_idx in self._dsa_topk_last_consumers and dsa_topk_ids is not None:
offload_tensors.extend(dsa_topk_ids if isinstance(dsa_topk_ids, list) else [dsa_topk_ids])

# The offload context expects a dense zero-based block index across every
# active activation or DSA-ID window, including dense GLM layers.
offload_block_idx = sum(
(activation_offload and previous_idx >= self.config.first_k_dense_replace)
or (
dsa_topk_offload
and previous_idx in self._dsa_topk_last_consumers
and self._dsa_topk_source_layers[previous_idx] != previous_idx
)
for previous_idx in (int(idx) for idx in self.layers)
if previous_idx < layer_idx
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [设计] activation-offload 规则出现两份实现。

Problem:基类 MoE._call_decoder_layer 已把「窗口张量选择 + block_idx 编号」写死在方法体内,GLM 为了插入 DSA IDs 只能整段 override,于是同一条规则有了两个不同公式:基类是 sum(first_k_dense_replace <= idx < layer_idx),这里则把 DSA 末位消费层也计入密集编号。做 Deletion test:删掉这个 override 会同时丢失 DSA 数据流 offload 规则,说明 Seam 切在了整个 _call_decoder_layer 上——位置偏大。后果是基类调整 offload 策略时,GLM 侧不会报错,只会静默错位到另一套编号。

Solution:基类保留唯一的 _call_decoder_layer(窗口构造 + block 计数),只把变化点做成小钩子,GLM 仅覆盖钩子:

# MoEdef_extra_offload_tensors(self, layer_idx, hidden_states) ->list[torch.Tensor]:
return []
def_layer_forward_kwargs(self, layer_idx, previous_layer_results) ->dict:
return {}

Benefits:offload 规则单点化(Locality),子类需要了解的 Interface 从「整个调用 + 窗口 + 编号」缩小到两个钩子(Leverage),block_idx 编号也能在基类上直接写 CPU 单测。

GLM52MoEDecoderLayerOutput | GLM52MoEDecoderLayerMicroBatchOutput | None,
previous_layer_results,
)
dsa_topk_ids = None if previous_results is None else previous_results["dsa_topk_ids"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [兼容性] index_share_for_mtp_iteration 的唯一运行时使用点已被删除,这里无条件透传上一 depth 的 IDs。但该字段仍保留在 config、from_hf 校验与 to_hf_config 回写中,设为 False 既不生效也不报错。建议按该配置决定是否透传,或在配置层直接拒绝 False

Comment on lines +302 to +308
"""Build one policy-neutral saved-tensor offload window.

The decoder-stack caller decides which tensors belong to the current
window and advances ``block_idx`` only when the list is non-empty.
"""
if not tensors:
return contextlib.nullcontext()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [正确性] 匹配条件从原来的 x.data_ptr() == hidden_states.data_ptr() 放宽为 storage 指针,会把共享 storage 的非零偏移视图一并纳入窗口,同一 storage 可能被重复 pack/D2H。另外单 micro-batch 路径新增了 reserve_pin_memory=True 且 block_idx 由绝对层号改为相对号,改变了 OffloadManager 的 key 与 prefetch 顺序,而本 PR 基准未覆盖 XTUNER_ACTIVATION_OFFLOAD=1 的单 micro-batch 路径。

Comment on lines 349 to +353
if self.compile_cfg:
layer.forward = torch.compile(layer.forward, fullgraph=True)
# The PyTree checkpoint adapter is an intentional graph break; model compute
# inside it keeps the independently configured full-graph compilation.
compiled_forward = torch.compile(type(layer).forward, fullgraph=False)
layer.forward = compiled_forward.__get__(layer, type(layer))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [性能] 此处 type(layer) 已是 CheckpointWrapper,而内层 Qwen3VLVisionLayer.forward 并没有注释所说的「独立 full-graph 配置」(该文件只 import 了 maybe_compile,未装饰任何 vision forward)。结果是 fullgraph 保证被静默移除,vision 侧 graph break 不再报错,且本 PR 无 VL 回归验证。建议把 fullgraph=True 保留在被包裹模块自身的 forward 上,并补一条 VL 验证。

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jayhenry@YanhuiDua
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[Refactor] Unify SAC checkpointing and GLM DSA dataflow - #2039

Open
jayhenry wants to merge 6 commits into
InternLM:mainfrom
jayhenry:refactor/sac-checkpoint-dsa-dataflow
Open

[Refactor] Unify SAC checkpointing and GLM DSA dataflow#2039
jayhenry wants to merge 6 commits into
InternLM:mainfrom
jayhenry:refactor/sac-checkpoint-dsa-dataflow

Conversation

@jayhenry

Copy link
Copy Markdown
Collaborator

Summary

  • Unify activation checkpointing around one PyTree-aware reentrant boundary so structured micro-batch inputs and outputs stay on the real autograd and offload path.
  • Pass GLM DSA top-k IDs explicitly through decoder and MTP outputs, removing the mutable SequenceContext cache lifecycle.
  • Reuse frozen source-indexer results from checkpoint-local FIFO frames during replay.
  • Keep FSDP wrappers outside recompute and dense checkpoint boundaries outside compiled graphs.

Regression coverage

  • compile + top-k offload + shared-weight MTP with depth > 1; validates pinned-memory offload and restore, and that the source indexer is not recomputed
  • EP > 1 + intra-layer micro-batch 2
  • FSDP checkpointing, torch.compile, FP8, activation offload, and nested PyTree inputs and outputs

Validation

  • Post-rebase on latest upstream/main: 16 passed
    • tests/model/test_recompute.py
    • tests/model/test_glm52_mtp_checkpoint_repro.py
    • tests/model/test_fsdp_checkpoint.py
  • Qwen3.5 full 40-layer stack5 regression: step losses and grad norms matched upstream; steady sequence TGS +0.36%; peak allocated memory -0.14 GB.
  • GLM AdamW production-shaped and Muon EP2/micro2 training regressions completed.

@jayhenry
jayhenryforce-pushed the refactor/sac-checkpoint-dsa-dataflow branch from d78f607 to 81e0010CompareAugust 31, 2026 09:48
Flatten structured inputs and outputs at one checkpoint boundary so nested MTP micro-batch tensors receive gradients. Keep FSDP outside replay and remove the non-reentrant MTP switch.
Move GLM-specific decoder and MTP adapters under the model package, thread DSA IDs through keyed outputs, and remove SequenceContext cache lifecycle state. Share one saved-tensor offload window for activation and DSA ID storage.
Keep reusable no-grad outputs in checkpoint-call-local FIFO frames, freeze DSA indexers through their model config, and preserve the original int32 storage across shared layers and offload. Add real regressions for source call counts and pinned-memory DSA offload.
Only inject the detached-input grad entry when the checkpointed module still has trainable parameters. This preserves MTP parameter gradients while keeping frozen vision outputs detached, avoiding a replay with no differentiable outputs.
@jayhenry
jayhenryforce-pushed the refactor/sac-checkpoint-dsa-dataflow branch from b2047db to 5c15855CompareSeptember 2, 2026 02:35
@YanhuiDua

Copy link
Copy Markdown
Collaborator

@claude review

@github-actions

Copy link
Copy Markdown
Contributor

Claude:

Summary

本 PR 把 activation checkpointing 统一到一个 PyTree-aware 的 reentrant 边界(apply_activation_checkpointing + reuse_during_recompute),并把 GLM-5.2 的 DSA top-k 从 SequenceContext 上的可变缓存改为经 decoder / MTP 输出显式穿参,同时将 decoder 与 MTP 的返回值从位置元组改为 TypedDict,GLM-5.2 相关代码收敛到新的 xtuner/v1/model/moe/glm52/ 包。整体方向是把隐式生命周期换成显式数据流,抽象更清晰;但 offload 策略在基类与 GLM 子类各写了一份,另有一处配置项失效、一处 offload 匹配语义放宽、一处 vision compile 降级需要确认。

Main Flowchart after this PR

flowchart TD
A["MoE._forward / _micro_batch_forward"] --> B["_decoder_stack /<br/>_micro_batch_decoder_stack<br/>(新增, 11 参数 + 可变 output dict)"]
B --> C{"模型类型"}
C -->|"通用 MoE"| D["MoE._call_decoder_layer<br/>(offload 窗口 + block_idx 规则)"]
C -->|"GLM-5.2"| E["Glm52MoE._call_decoder_layer<br/>(整段 override: DSA IDs + 另一套 block_idx 规则)"]
D --> F["_saved_tensors_offload_ctx<br/>(storage-ptr 匹配, reserve_pin_memory)"]
E --> F
F --> G["decoder_layer(...) -> TypedDict"]
G --> H["GLM52AttnOutputs.dsa_topk_ids<br/>显式传给下一 consumer 层"]
H --> I["MTPBlock._call_decoder_layer<br/>GLM52MTPBlock 无条件透传 dsa_topk_ids"]
style D fill:#ffe0b2,stroke:#e65100
style E fill:#ffcdd2,stroke:#b71c1c
style F fill:#ffcdd2,stroke:#b71c1c
style I fill:#ffe0b2,stroke:#e65100
Loading

核心原理实现与单测

  • Checkpoint 统一边界apply_activation_checkpointing 固定 reentrant 并用 PyTree 桥接展平输入 / 还原输出,reuse_during_recomputeContextVar + 按 callable 的 FIFO frame 支撑 replay 复用。tests/model/test_recompute.py 全部走 public API,覆盖了 grad 模式序列 [False, True]、全 detach 输入仍产参数梯度、冻结模块不建立 replay 边、嵌套 / 关键字输入可被外层 saved_tensors_hooks 捕获(即 offload 不会静默空转)、replay 输出结构变化报错、以及跨两次 checkpoint 的 FIFO 隔离——核心行为有真实代码路径覆盖,未 mock 项目内模块。
  • GLM DSA 显式数据流:source 层算一次 IDs,consumer 层由 Glm52MoE._call_decoder_layer 显式传入;tests/module/attention/test_dsa_mla.pytests/model/test_glm52_moe.py::TestGlm52ExplicitDsaDataflowtests/model/test_glm52_mtp_checkpoint_repro.py(indexer 调用计数 1 / 2、pinned D2H 真实命中并在 backward 恢复)覆盖了 source 不重算与 top-k offload。
  • 覆盖缺口(未达 Warning,仅提示):_call_decoder_layer 的 offload block-index 计算无 CPU 可跑单测;index_share_for_mtp_iteration=False 无任何测试。

抽象与信息隐藏评估

  • Warningxtuner/v1/model/moe/moe.py_call_decoder_layerxtuner/v1/model/moe/glm52/glm52.py 的 override:Seam 切在整个 _call_decoder_layer 上,导致同一条 activation-offload 规则(窗口张量 + block_idx 编号)在基类和 GLM 子类各存一份且公式不同,基类策略变更时 GLM 会静默偏移。
  • Warningxtuner/v1/model/moe/moe.py_decoder_stack / _micro_batch_decoder_stack:抽出的私有方法带 11 个关键字参数并把 output: dict 当可变入出参写入,没有隐藏规则,只是把循环搬家。

其他 Issues

  • Warningxtuner/v1/model/moe/glm52/glm52.py:删除 index_share_for_mtp_iteration 的唯一运行时使用点后,该配置项仍保留在 config、from_hf 校验和 to_hf_config 回写中,但设为 False 既不改变行为也不报错,变成静默失效的配置。
  • Warningxtuner/v1/model/moe/moe.py_saved_tensors_offload_ctx:offload 匹配从 data_ptr() 放宽为 untyped_storage().data_ptr(),会把共享 storage 的视图一并纳入窗口,且单 micro-batch 路径新增常驻 pinned 缓冲并改变 block_idx 编号,而现有基准未覆盖 XTUNER_ACTIVATION_OFFLOAD=1 的单 micro-batch 路径。
  • Warningxtuner/v1/model/compose/qwen3_vl/modeling_vision.pyxtuner/v1/model/compose/intern_s1/modeling_vision.py:compile 目标改为 checkpoint wrapper 的 forward 且降为 fullgraph=False,但内层 vision layer 并没有注释所称的"独立 full-graph 配置",fullgraph 保证被静默移除且本 PR 无 VL 回归验证。

Verdict

REQUEST_CHANGES

Comment on lines +152 to +171
activation_offload = int(os.getenv("XTUNER_ACTIVATION_OFFLOAD", "0")) == 1
dsa_topk_offload = int(os.getenv("XTUNER_DSA_TOPK_OFFLOAD", "0")) == 1
offload_tensors: list[torch.Tensor] = []
if activation_offload and layer_idx >= self.config.first_k_dense_replace:
offload_tensors = list(hidden_states) if is_micro_batch else [cast(torch.Tensor, hidden_states)]
if dsa_topk_offload and layer_idx in self._dsa_topk_last_consumers and dsa_topk_ids is not None:
offload_tensors.extend(dsa_topk_ids if isinstance(dsa_topk_ids, list) else [dsa_topk_ids])

# The offload context expects a dense zero-based block index across every
# active activation or DSA-ID window, including dense GLM layers.
offload_block_idx = sum(
(activation_offload and previous_idx >= self.config.first_k_dense_replace)
or (
dsa_topk_offload
and previous_idx in self._dsa_topk_last_consumers
and self._dsa_topk_source_layers[previous_idx] != previous_idx
)
for previous_idx in (int(idx) for idx in self.layers)
if previous_idx < layer_idx
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [设计] activation-offload 规则出现两份实现。

Problem:基类 MoE._call_decoder_layer 已把「窗口张量选择 + block_idx 编号」写死在方法体内,GLM 为了插入 DSA IDs 只能整段 override,于是同一条规则有了两个不同公式:基类是 sum(first_k_dense_replace <= idx < layer_idx),这里则把 DSA 末位消费层也计入密集编号。做 Deletion test:删掉这个 override 会同时丢失 DSA 数据流 offload 规则,说明 Seam 切在了整个 _call_decoder_layer 上——位置偏大。后果是基类调整 offload 策略时,GLM 侧不会报错,只会静默错位到另一套编号。

Solution:基类保留唯一的 _call_decoder_layer(窗口构造 + block 计数),只把变化点做成小钩子,GLM 仅覆盖钩子:

# MoEdef_extra_offload_tensors(self, layer_idx, hidden_states) ->list[torch.Tensor]:
return []
def_layer_forward_kwargs(self, layer_idx, previous_layer_results) ->dict:
return {}

Benefits:offload 规则单点化(Locality),子类需要了解的 Interface 从「整个调用 + 窗口 + 编号」缩小到两个钩子(Leverage),block_idx 编号也能在基类上直接写 CPU 单测。

GLM52MoEDecoderLayerOutput | GLM52MoEDecoderLayerMicroBatchOutput | None,
previous_layer_results,
)
dsa_topk_ids = None if previous_results is None else previous_results["dsa_topk_ids"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [兼容性] index_share_for_mtp_iteration 的唯一运行时使用点已被删除,这里无条件透传上一 depth 的 IDs。但该字段仍保留在 config、from_hf 校验与 to_hf_config 回写中,设为 False 既不生效也不报错。建议按该配置决定是否透传,或在配置层直接拒绝 False

Comment on lines +302 to +308
"""Build one policy-neutral saved-tensor offload window.

The decoder-stack caller decides which tensors belong to the current
window and advances ``block_idx`` only when the list is non-empty.
"""
if not tensors:
return contextlib.nullcontext()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [正确性] 匹配条件从原来的 x.data_ptr() == hidden_states.data_ptr() 放宽为 storage 指针,会把共享 storage 的非零偏移视图一并纳入窗口,同一 storage 可能被重复 pack/D2H。另外单 micro-batch 路径新增了 reserve_pin_memory=True 且 block_idx 由绝对层号改为相对号,改变了 OffloadManager 的 key 与 prefetch 顺序,而本 PR 基准未覆盖 XTUNER_ACTIVATION_OFFLOAD=1 的单 micro-batch 路径。

Comment on lines 349 to +353
if self.compile_cfg:
layer.forward = torch.compile(layer.forward, fullgraph=True)
# The PyTree checkpoint adapter is an intentional graph break; model compute
# inside it keeps the independently configured full-graph compilation.
compiled_forward = torch.compile(type(layer).forward, fullgraph=False)
layer.forward = compiled_forward.__get__(layer, type(layer))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [性能] 此处 type(layer) 已是 CheckpointWrapper,而内层 Qwen3VLVisionLayer.forward 并没有注释所说的「独立 full-graph 配置」(该文件只 import 了 maybe_compile,未装饰任何 vision forward)。结果是 fullgraph 保证被静默移除,vision 侧 graph break 不再报错,且本 PR 无 VL 回归验证。建议把 fullgraph=True 保留在被包裹模块自身的 forward 上,并补一条 VL 验证。

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jayhenry@YanhuiDua
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[Refactor] Unify SAC checkpointing and GLM DSA dataflow - #2039

Open
jayhenry wants to merge 6 commits into
InternLM:mainfrom
jayhenry:refactor/sac-checkpoint-dsa-dataflow
Open

[Refactor] Unify SAC checkpointing and GLM DSA dataflow#2039
jayhenry wants to merge 6 commits into
InternLM:mainfrom
jayhenry:refactor/sac-checkpoint-dsa-dataflow

Conversation

@jayhenry

Copy link
Copy Markdown
Collaborator

Summary

  • Unify activation checkpointing around one PyTree-aware reentrant boundary so structured micro-batch inputs and outputs stay on the real autograd and offload path.
  • Pass GLM DSA top-k IDs explicitly through decoder and MTP outputs, removing the mutable SequenceContext cache lifecycle.
  • Reuse frozen source-indexer results from checkpoint-local FIFO frames during replay.
  • Keep FSDP wrappers outside recompute and dense checkpoint boundaries outside compiled graphs.

Regression coverage

  • compile + top-k offload + shared-weight MTP with depth > 1; validates pinned-memory offload and restore, and that the source indexer is not recomputed
  • EP > 1 + intra-layer micro-batch 2
  • FSDP checkpointing, torch.compile, FP8, activation offload, and nested PyTree inputs and outputs

Validation

  • Post-rebase on latest upstream/main: 16 passed
    • tests/model/test_recompute.py
    • tests/model/test_glm52_mtp_checkpoint_repro.py
    • tests/model/test_fsdp_checkpoint.py
  • Qwen3.5 full 40-layer stack5 regression: step losses and grad norms matched upstream; steady sequence TGS +0.36%; peak allocated memory -0.14 GB.
  • GLM AdamW production-shaped and Muon EP2/micro2 training regressions completed.

@jayhenry
jayhenryforce-pushed the refactor/sac-checkpoint-dsa-dataflow branch from d78f607 to 81e0010CompareAugust 31, 2026 09:48
Flatten structured inputs and outputs at one checkpoint boundary so nested MTP micro-batch tensors receive gradients. Keep FSDP outside replay and remove the non-reentrant MTP switch.
Move GLM-specific decoder and MTP adapters under the model package, thread DSA IDs through keyed outputs, and remove SequenceContext cache lifecycle state. Share one saved-tensor offload window for activation and DSA ID storage.
Keep reusable no-grad outputs in checkpoint-call-local FIFO frames, freeze DSA indexers through their model config, and preserve the original int32 storage across shared layers and offload. Add real regressions for source call counts and pinned-memory DSA offload.
Only inject the detached-input grad entry when the checkpointed module still has trainable parameters. This preserves MTP parameter gradients while keeping frozen vision outputs detached, avoiding a replay with no differentiable outputs.
@jayhenry
jayhenryforce-pushed the refactor/sac-checkpoint-dsa-dataflow branch from b2047db to 5c15855CompareSeptember 2, 2026 02:35
@YanhuiDua

Copy link
Copy Markdown
Collaborator

@claude review

@github-actions

Copy link
Copy Markdown
Contributor

Claude:

Summary

本 PR 把 activation checkpointing 统一到一个 PyTree-aware 的 reentrant 边界(apply_activation_checkpointing + reuse_during_recompute),并把 GLM-5.2 的 DSA top-k 从 SequenceContext 上的可变缓存改为经 decoder / MTP 输出显式穿参,同时将 decoder 与 MTP 的返回值从位置元组改为 TypedDict,GLM-5.2 相关代码收敛到新的 xtuner/v1/model/moe/glm52/ 包。整体方向是把隐式生命周期换成显式数据流,抽象更清晰;但 offload 策略在基类与 GLM 子类各写了一份,另有一处配置项失效、一处 offload 匹配语义放宽、一处 vision compile 降级需要确认。

Main Flowchart after this PR

flowchart TD
A["MoE._forward / _micro_batch_forward"] --> B["_decoder_stack /<br/>_micro_batch_decoder_stack<br/>(新增, 11 参数 + 可变 output dict)"]
B --> C{"模型类型"}
C -->|"通用 MoE"| D["MoE._call_decoder_layer<br/>(offload 窗口 + block_idx 规则)"]
C -->|"GLM-5.2"| E["Glm52MoE._call_decoder_layer<br/>(整段 override: DSA IDs + 另一套 block_idx 规则)"]
D --> F["_saved_tensors_offload_ctx<br/>(storage-ptr 匹配, reserve_pin_memory)"]
E --> F
F --> G["decoder_layer(...) -> TypedDict"]
G --> H["GLM52AttnOutputs.dsa_topk_ids<br/>显式传给下一 consumer 层"]
H --> I["MTPBlock._call_decoder_layer<br/>GLM52MTPBlock 无条件透传 dsa_topk_ids"]
style D fill:#ffe0b2,stroke:#e65100
style E fill:#ffcdd2,stroke:#b71c1c
style F fill:#ffcdd2,stroke:#b71c1c
style I fill:#ffe0b2,stroke:#e65100
Loading

核心原理实现与单测

  • Checkpoint 统一边界apply_activation_checkpointing 固定 reentrant 并用 PyTree 桥接展平输入 / 还原输出,reuse_during_recomputeContextVar + 按 callable 的 FIFO frame 支撑 replay 复用。tests/model/test_recompute.py 全部走 public API,覆盖了 grad 模式序列 [False, True]、全 detach 输入仍产参数梯度、冻结模块不建立 replay 边、嵌套 / 关键字输入可被外层 saved_tensors_hooks 捕获(即 offload 不会静默空转)、replay 输出结构变化报错、以及跨两次 checkpoint 的 FIFO 隔离——核心行为有真实代码路径覆盖,未 mock 项目内模块。
  • GLM DSA 显式数据流:source 层算一次 IDs,consumer 层由 Glm52MoE._call_decoder_layer 显式传入;tests/module/attention/test_dsa_mla.pytests/model/test_glm52_moe.py::TestGlm52ExplicitDsaDataflowtests/model/test_glm52_mtp_checkpoint_repro.py(indexer 调用计数 1 / 2、pinned D2H 真实命中并在 backward 恢复)覆盖了 source 不重算与 top-k offload。
  • 覆盖缺口(未达 Warning,仅提示):_call_decoder_layer 的 offload block-index 计算无 CPU 可跑单测;index_share_for_mtp_iteration=False 无任何测试。

抽象与信息隐藏评估

  • Warningxtuner/v1/model/moe/moe.py_call_decoder_layerxtuner/v1/model/moe/glm52/glm52.py 的 override:Seam 切在整个 _call_decoder_layer 上,导致同一条 activation-offload 规则(窗口张量 + block_idx 编号)在基类和 GLM 子类各存一份且公式不同,基类策略变更时 GLM 会静默偏移。
  • Warningxtuner/v1/model/moe/moe.py_decoder_stack / _micro_batch_decoder_stack:抽出的私有方法带 11 个关键字参数并把 output: dict 当可变入出参写入,没有隐藏规则,只是把循环搬家。

其他 Issues

  • Warningxtuner/v1/model/moe/glm52/glm52.py:删除 index_share_for_mtp_iteration 的唯一运行时使用点后,该配置项仍保留在 config、from_hf 校验和 to_hf_config 回写中,但设为 False 既不改变行为也不报错,变成静默失效的配置。
  • Warningxtuner/v1/model/moe/moe.py_saved_tensors_offload_ctx:offload 匹配从 data_ptr() 放宽为 untyped_storage().data_ptr(),会把共享 storage 的视图一并纳入窗口,且单 micro-batch 路径新增常驻 pinned 缓冲并改变 block_idx 编号,而现有基准未覆盖 XTUNER_ACTIVATION_OFFLOAD=1 的单 micro-batch 路径。
  • Warningxtuner/v1/model/compose/qwen3_vl/modeling_vision.pyxtuner/v1/model/compose/intern_s1/modeling_vision.py:compile 目标改为 checkpoint wrapper 的 forward 且降为 fullgraph=False,但内层 vision layer 并没有注释所称的"独立 full-graph 配置",fullgraph 保证被静默移除且本 PR 无 VL 回归验证。

Verdict

REQUEST_CHANGES

Comment on lines +152 to +171
activation_offload = int(os.getenv("XTUNER_ACTIVATION_OFFLOAD", "0")) == 1
dsa_topk_offload = int(os.getenv("XTUNER_DSA_TOPK_OFFLOAD", "0")) == 1
offload_tensors: list[torch.Tensor] = []
if activation_offload and layer_idx >= self.config.first_k_dense_replace:
offload_tensors = list(hidden_states) if is_micro_batch else [cast(torch.Tensor, hidden_states)]
if dsa_topk_offload and layer_idx in self._dsa_topk_last_consumers and dsa_topk_ids is not None:
offload_tensors.extend(dsa_topk_ids if isinstance(dsa_topk_ids, list) else [dsa_topk_ids])

# The offload context expects a dense zero-based block index across every
# active activation or DSA-ID window, including dense GLM layers.
offload_block_idx = sum(
(activation_offload and previous_idx >= self.config.first_k_dense_replace)
or (
dsa_topk_offload
and previous_idx in self._dsa_topk_last_consumers
and self._dsa_topk_source_layers[previous_idx] != previous_idx
)
for previous_idx in (int(idx) for idx in self.layers)
if previous_idx < layer_idx
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [设计] activation-offload 规则出现两份实现。

Problem:基类 MoE._call_decoder_layer 已把「窗口张量选择 + block_idx 编号」写死在方法体内,GLM 为了插入 DSA IDs 只能整段 override,于是同一条规则有了两个不同公式:基类是 sum(first_k_dense_replace <= idx < layer_idx),这里则把 DSA 末位消费层也计入密集编号。做 Deletion test:删掉这个 override 会同时丢失 DSA 数据流 offload 规则,说明 Seam 切在了整个 _call_decoder_layer 上——位置偏大。后果是基类调整 offload 策略时,GLM 侧不会报错,只会静默错位到另一套编号。

Solution:基类保留唯一的 _call_decoder_layer(窗口构造 + block 计数),只把变化点做成小钩子,GLM 仅覆盖钩子:

# MoEdef_extra_offload_tensors(self, layer_idx, hidden_states) ->list[torch.Tensor]:
return []
def_layer_forward_kwargs(self, layer_idx, previous_layer_results) ->dict:
return {}

Benefits:offload 规则单点化(Locality),子类需要了解的 Interface 从「整个调用 + 窗口 + 编号」缩小到两个钩子(Leverage),block_idx 编号也能在基类上直接写 CPU 单测。

GLM52MoEDecoderLayerOutput | GLM52MoEDecoderLayerMicroBatchOutput | None,
previous_layer_results,
)
dsa_topk_ids = None if previous_results is None else previous_results["dsa_topk_ids"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [兼容性] index_share_for_mtp_iteration 的唯一运行时使用点已被删除,这里无条件透传上一 depth 的 IDs。但该字段仍保留在 config、from_hf 校验与 to_hf_config 回写中,设为 False 既不生效也不报错。建议按该配置决定是否透传,或在配置层直接拒绝 False

Comment on lines +302 to +308
"""Build one policy-neutral saved-tensor offload window.

The decoder-stack caller decides which tensors belong to the current
window and advances ``block_idx`` only when the list is non-empty.
"""
if not tensors:
return contextlib.nullcontext()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [正确性] 匹配条件从原来的 x.data_ptr() == hidden_states.data_ptr() 放宽为 storage 指针,会把共享 storage 的非零偏移视图一并纳入窗口,同一 storage 可能被重复 pack/D2H。另外单 micro-batch 路径新增了 reserve_pin_memory=True 且 block_idx 由绝对层号改为相对号,改变了 OffloadManager 的 key 与 prefetch 顺序,而本 PR 基准未覆盖 XTUNER_ACTIVATION_OFFLOAD=1 的单 micro-batch 路径。

Comment on lines 349 to +353
if self.compile_cfg:
layer.forward = torch.compile(layer.forward, fullgraph=True)
# The PyTree checkpoint adapter is an intentional graph break; model compute
# inside it keeps the independently configured full-graph compilation.
compiled_forward = torch.compile(type(layer).forward, fullgraph=False)
layer.forward = compiled_forward.__get__(layer, type(layer))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: [性能] 此处 type(layer) 已是 CheckpointWrapper,而内层 Qwen3VLVisionLayer.forward 并没有注释所说的「独立 full-graph 配置」(该文件只 import 了 maybe_compile,未装饰任何 vision forward)。结果是 fullgraph 保证被静默移除,vision 侧 graph break 不再报错,且本 PR 无 VL 回归验证。建议把 fullgraph=True 保留在被包裹模块自身的 forward 上,并补一条 VL 验证。

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jayhenry@YanhuiDua