Conversation
jayhenry
added this pull request to stack #2112
September 23, 2026 06:54
This was referenced Sep 23, 2026
jayhenry
force-pushed
the
feat/glm53flash-f6-text-moe
branch
from
September 24, 2026 17:27
07671ec to
b7234d9
Compare
jayhenry
force-pushed
the
feat/glm53flash-f6-text-moe
branch
from
September 24, 2026 17:41
b7234d9 to
945e7dc
Compare
jayhenry
force-pushed
the
feat/glm53flash-f6-text-moe
branch
from
September 24, 2026 17:51
29f84dd to
f84e6a0
Compare
jayhenry
force-pushed
the
feat/glm53flash-f6-text-moe
branch
2 times, most recently
from
September 24, 2026 18:01
26968ce to
27ac8ec
Compare
Implements the core of the F6 milestone from doc/xtuner_glm5p3flash_design.md:
the 45-layer KDA/NoPE-DSA text model with mHC four-stream residual, MTP,
and the compose model wiring vision_tower + multi_modal_projector +
language_model with image/video splice.
- xtuner/v1/model/moe/glm53/glm53.py: Glm53TextMoEConfig/Glm53TextMoE.
Layer schedule ([KDA,KDA,KDA,DSA]x11 + KDA) read from the real
checkpoint's text_config.layer_types via build_layers() dispatching
per-layer on config.attention (NoPEDSAMLAConfig) vs
config.linear_attention (KDAConfig). mHC's four-stream residual is
expanded/collapsed once at the _decoder_stack/_micro_batch_decoder_stack
boundary (not per layer -- each Glm53*DecoderLayer, already built in
F4, handles its own hc_pre/hc_post internally), so MoE._forward's
aux-loss/router bookkeeping is reused completely unmodified. No
cross-layer dsa_topk_ids IndexShare is needed (every indexer_types
entry in the real checkpoint is "full": 11 main-stack DSA layers + 1
MTP, confirmed by inspection) -- MTPLayer/MTPBlock are used
unmodified via build_mtp_block(mhc_cfg=None), which is also why the
design doc's listed mtp.py deliverable turned out to be unnecessary:
the base MTPLayer's enorm/hnorm/eh_proj/final_layernorm param names
already match checkpoint layers.45 exactly.
- xtuner/v1/model/compose/glm53/modeling_glm53.py +
Glm53BaseConfig (glm53_config.py): Glm53ForConditionalGeneration.
Image and video get separate vision-tower forwards; splice uses the
global mm_token_type_ids (1=image, 2=video), never
input_ids==video_token_id (F1.b: that token never appears in the
expanded sequence). A placeholder-count mismatch raises immediately
-- unlike Qwen3-VL's compose path, this never catches the mismatch
in a bare except and continues training on a corrupted splice
(design doc §16.2).
- xtuner/v1/data_proto/templates/__init__.py,
xtuner/v1/datasets/sft_tokenize_fn/openai.py: registers the
"glm5.3" chat template / Glm53ChatMessages tokenize-fn branch so a
generic OpenaiTokenizeFunctionConfig(chat_template="glm5.3") works.
- xtuner/v1/model/__init__.py: get_model_config_from_hf dispatches
glm5_next's model_type to Glm53TextMoEConfig.from_hf (text-only SFT
path; the VL compose config covers the vision half separately).
- examples/v1/config/sft_glm53.py, sft_glm53_tiny.sh: SFT training
config and launch script for the 8-GPU end-to-end smoke suite,
adapted from GLM-5.2's equivalents (glm5.3 chat template,
flash_mla_cudnn default sparse_mla_backend, no
resolve_indexer_topk_query_chunk_size -- NoPEDSAMLAConfig has no
"deep_gemm_fp8" indexer backend).
Root-caused four bugs found by end-to-end gradient-flow and 8-GPU
training smoke tests, via minimal repros rather than assuming any was
real:
(1) KDA's core params showed no gradient in a small synthetic test --
isolated to FLA's chunk_kda Triton kernel silently dropping backward
gradients when head_dim < 16 (a tl.dot minimum-K constraint), not a
production issue since real GLM-5.3-Flash uses head_dim=128.
(2) The DSA indexer showed no gradient even with
freeze_dsa_indexer=False -- traced to nope_dsa_mla.py's indexer call
being unconditionally wrapped in torch.no_grad() (existing F5 code),
the standard DSA-indexer training convention, not a bug.
(3) Glm53TextMoE had no default_compile_cfg override, so
torch.compile traced KDA's Triton-kernel-heavy forward under a strict
boundary and hit `torch.compiler.disable()`d-function errors; fixed
by adding GLM53_MOE_NON_EP_COMPILE_CFG/GLM53_MOE_EP_COMPILE_CFG
(mirroring GLM-5.2's pattern). This then surfaced a separate, real
upstream limitation: FLA's prepare_chunk_indices/prepare_lens
(fla/ops/utils/index.py) does a .tolist()-driven Python loop over
cu_seqlens, incompatible with dynamo's dynamic-shape tracing --
confirmed via isolated repro, not fixed (MODEL_COMPILE=0 used for the
8-GPU smoke suite; compile isn't in the acceptance criteria's required
coverage).
(4) Under ep_size>1, KDA's o_norm (FLA's FusedRMSNormGated) crashed
with a Triton "illegal memory access" reading address 0x0 --
CUDA_LAUNCH_BLOCKING=1 + compute-sanitizer --tool memcheck localized
it to a null data_ptr(): unlike A_log/dt_bias/the conv weight (all
explicitly unsharded via _to_local() before use), FusedRMSNormGated's
inherited forward reads self.weight directly, which stays a DTensor
under EP (ep_size=1's FSDP2 pre-forward hook happened to auto-unshard
it, masking the bug). Fixed with a minimal FusedRMSNormGated.forward
override in kda.py that unshards weight/bias first, mirroring the
existing _to_local() pattern.
Test plan:
- tests/model/test_glm53_text_moe.py, test_glm53_compose.py: unit and
real-checkpoint weight-coverage tests, GPU.
- tests/model/test_glm53_text_moe.py::TestGlm53TextMoEAccuracy::
test_fsdp_accuracy (验收 1): XTuner forward loss vs real
transformers.Glm5NextForConditionalGeneration on the F0 25B cropped
checkpoint, for (dispatcher, ep_size) in {(None, 1), ("all2all", 4),
("all2all", 8)} -- all pass. Installed transformers has no MTP
forward for this class, so both sides compare only the 5-layer main
stack (mtp_config=None on the XTuner side); sparse_mla_backend/
indexer_backend forced to "torch" (eager) since the test sentences
are shorter than flash_mla_cudnn's 512-token alignment. The
ep_size=4/8 cases double as the real-checkpoint regression test for
bug (4) above.
- sft_glm53_tiny.sh (验收 2): single-node 8-GPU end-to-end SFT, real
F0 checkpoint, PACK_MAX_LENGTH=16384, TOTAL_STEP=20, MODEL_COMPILE=0.
Default profile (EP_SIZE=4 SP_SIZE=1 XTUNER_ACTIVATION_OFFLOAD=1)
and the three required combos (SP_SIZE=2 EP_SIZE=4; EP_SIZE=8
SP_SIZE=1; XTUNER_ACTIVATION_OFFLOAD=0) all complete 20 steps with
loss monotonically decreasing (~11.3 -> ~10.3), no NaN/OOM.
Known gaps (recorded explicitly, not silently skipped): Vision SP
remains the gap F2 already recorded (not implemented, not just
untested), and the compose splice assumes sequence_parallel_mesh is
None/size 1; FSDP2/compile/FP8 paths have no multi-GPU test in the
compose layer; FP8 (FP8=1) was not included in the 8-GPU smoke combos,
only FP8=0; MODEL_COMPILE=1 end-to-end training does not work, see bug
(3) above (upstream FLA/dynamo limitation, not required by the
acceptance criteria).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…odel
The compose model asserted `sequence_parallel_mesh` was None or size 1, so VL training could
not use SP at all -- the last piece of the Vision SP gap, now that the tower shards patches
merge-aligned.
The splice is the only part that needs to know about SP. `input_ids` and `mm_token_type_ids`
arrive already sharded (they are split together, F1.b), so the local mask is what indexes the
local embeddings; what the local mask cannot say is *which* global features belong to this rank.
`_local_feature_slice` therefore gathers the mask to compute this rank's offset (the number of
same-modality placeholders on all preceding ranks), and `_gather_visual_features` reassembles
the globally-ordered features, trimming the tower's merge-alignment padding. The gather is the
autograd-aware one, so its backward reduce-scatters and hands each rank the gradient of exactly
the shard it produced.
The placeholder<->feature count check now compares *global* totals, so a corrupted sample raises
the same way with and without SP rather than turning into a per-rank count mismatch.
The pure-text dummy visual forward stays replicated: its gradient contribution is exactly zero
(`dummy_feats.sum() * 0.0`), so sharding it would only add collectives.
Test Plan:
- `TestGlm53ComposeSequenceParallel::test_image_splice_under_sp_matches_non_sp` (2 GPU): two
placeholder spans placed so one lands on each rank's shard -- forcing the cross-rank feature
slicing -- and each rank's logits match the corresponding slice of the non-SP run. This
replaces the guard test that asserted SP was refused.
- tests/model/test_glm53_compose.py 6/6.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jayhenry
force-pushed
the
feat/glm53flash-f6-text-moe
branch
from
September 24, 2026 18:10
27ac8ec to
803056c
Compare
Multimodal training had no entry point: `Glm53BaseConfig.from_hf` raised `NotImplementedError`, so the VL model could not be built from a checkpoint at all, and there was no training config or launch script to pair with the text-only `sft_glm53_tiny.sh`. `Glm53BaseConfig.from_hf` reads the checkpoint's single `vision_config` and fans it out to both XTuner modules -- the tower and the projector are one HF module that §5.2 splits in two -- and delegates the text half to `Glm53TextMoEConfig.from_hf`. `rope_parameters` is left at its default because the published config.json has no such key and HF's `AutoConfig` fills the same value. `examples/v1/config/sft_glm53_vl.py` + `sft_glm53_vl_tiny.sh` mirror the text pair knob for knob, so the two modalities run the same acceptance matrix. Media comes from the `ci_vl` corpus that zdev/sft_qwen35_mengke.sh uses (image + video samples). FP8 stays on the language tower only, matching how the published checkpoint stores the vision half in bf16. Test Plan: single-node 8-GPU run on the F0 25B cropped checkpoint completes with `PACK_MAX_LENGTH=16384` (~31k image patches per step), loss decreasing, 85.85 GB peak. Full profile matrix recorded in doc/progress.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds an H2 for the 2026-09-24 round: the contract-by-contract comparison against Automodel's GLM-5.3-Flash SFT implementation (text side matched throughout, nothing to change), the five real bugs that only surfaced by running VL training, the Vision SP design as built, the ViT activation recompute that was the actual OOM cause, and the six-profile VL acceptance matrix. Also records why `hf_config` staying None is a decision rather than a gap: with it None, `_write_hf_index_and_config` copies the source checkpoint's config, which is self-consistent for a fine-tune that does not change the architecture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack (bottom to top):
feat/glm53flash-materialize-full-f0→mainfeat/glm53flash-f3-kda→feat/glm53flash-materialize-full-f0feat/glm53flash-f4-mhc→feat/glm53flash-f3-kdafeat/glm53flash-f5-nope-dsa→feat/glm53flash-f4-mhcfeat/glm53flash-f1-vl-data→feat/glm53flash-f5-nope-dsafeat/glm53flash-f2-vision-tower→feat/glm53flash-f1-vl-datafeat/glm53flash-f6-text-moe→feat/glm53flash-f2-vision-tower← you are hereSummary
Stack layer 7/7 (top) of GLM-5.3-Flash support (base: layer 6, F2 vision tower). Covers the F6 text model + MTP + compose model, and the multimodal enablement that makes VL SFT actually runnable: sequence-parallel splice, and the VL training config/launcher.
Implements the core of the F6 milestone: the 45-layer KDA/NoPE-DSA text model with mHC four-stream residual, MTP, and the compose model wiring
vision_tower+multi_modal_projector+language_modelwith image/video splice.xtuner/v1/model/moe/glm53/glm53.py:Glm53TextMoEConfig/Glm53TextMoE. Layer schedule ([KDA,KDA,KDA,DSA]x11 + KDA) read from the real checkpoint'stext_config.layer_types. mHC's four-stream residual is expanded/collapsed once at the decoder-stack boundary, not per layer, soMoE._forward's aux-loss/router bookkeeping is reused unmodified. MTP reuses the baseMTPLayer/MTPBlockunmodified — the design doc's originally-plannedmtp.pydeliverable turned out unnecessary.xtuner/v1/model/compose/glm53/modeling_glm53.py+Glm53BaseConfig:Glm53ForConditionalGeneration. Image/video splice uses the globalmm_token_type_ids, neverinput_ids==video_token_id. A placeholder-count mismatch raises immediately (design doc §16.2) — unlike Qwen3-VL's compose path, this never silently continues training on a corrupted splice."glm5.3"chat template / tokenize-fn branch,get_model_config_from_hfdispatch, and the SFT training config/launch script (examples/v1/config/sft_glm53.py,sft_glm53_tiny.sh) for the 8-GPU end-to-end smoke suite.Root-caused four bugs found by end-to-end gradient-flow and 8-GPU training smoke tests (minimal repros, not assumed):
chunk_kdaTriton kernel silently drops backward gradients whenhead_dim < 16(atl.dotminimum-K constraint) — not a production issue since real GLM-5.3-Flash useshead_dim=128.freeze_dsa_indexer=Falsetraced to an existing, intentionaltorch.no_grad()wrap (not a bug).torch.compiletraced KDA's Triton-kernel-heavy forward under a strict boundary and hit disabled-function errors; fixed withGLM53_MOE_{NON_EP,EP}_COMPILE_CFG. Surfaced a separate real upstream limitation: FLA'sprepare_chunk_indices/prepare_lensdo a.tolist()-driven Python loop overcu_seqlens, incompatible with dynamo's dynamic-shape tracing (_mark_dynamicon the packed boundaries). Fixed by wrapping FLA's chunk/recurrent-kernel and short-conv entry points intorch._dynamo.disable()so dynamo graph-breaks at the call site instead (layer 2/7, KDA) —MODEL_COMPILE=1now verified end-to-end, see the table below.ep_size>1, KDA'so_norm(FLA'sFusedRMSNormGated) crashed with a Triton illegal-memory-access — its inherited forward readsself.weightdirectly, which stays a DTensor under EP. Fixed with a minimalforwardoverride that unshards weight/bias first.Known gaps (recorded explicitly, not silently skipped): Vision SP remains the gap the previous layer already recorded; the compose layer's FSDP2 wiring has no dedicated multi-GPU parity test beyond the smoke suite below.
Test Plan
tests/model/test_glm53_text_moe.py::TestGlm53TextMoEAccuracy::test_fsdp_accuracy: XTuner forward loss vs realtransformers.Glm5NextForConditionalGenerationon the F0 25B cropped checkpoint, for(dispatcher, ep_size)in{(None, 1), ("all2all", 4), ("all2all", 8)}— all pass. Theep_size=4/8cases double as the regression test for bug 4 above.sft_glm53_tiny.sh: single-node 8-GPU end-to-end SFT, real F0 checkpoint,PACK_MAX_LENGTH=16384,TOTAL_STEP=20. Default profile plus five combos (SP_SIZE=2 EP_SIZE=4;EP_SIZE=8 SP_SIZE=1;XTUNER_ACTIVATION_OFFLOAD=0;FP8=1;MODEL_COMPILE=1) all complete 20 steps with loss monotonically decreasing, no NaN/OOM. See the table below.Text end-to-end regression (
sft_glm53_tiny.sh, single-node 8-GPU, real F0 25B cropped checkpoint)PACK_MAX_LENGTH=16384,TOTAL_STEP=20. All six profiles complete 20 steps, loss monotonically decreasing, no NaN/OOM. Numbers are rank0;memismax_memory/reserved_memory(GB);tgs/seqlen_tgs/exp_tgsare step-20 throughput (tokens/gpu/s);timeis step1's own duration (includes one-time warmup/compile) plus the step1→step20 wall-clock gap:EP4 SP1 offload=1)SP2 EP4EP8 SP1offload=0(EP4 SP1)FP8=1(EP4 SP1 offload=1)MODEL_COMPILE=1(EP4 SP1 offload=1)SP2's per-GPU effective seqlen is halved (8192 vs 16384), sotgsis lower and wall-clock per step is also shorter than the default despite the added SP communication.offload=0is faster with highertgsthan the default (offload=1) — expected, since activation CPU-GPU transfer is skipped.EP8tracks the default (EP4) on loss/tgs; highermemis becausedp_size = world_size/ep_sizedrops from 2 to 1, so FSDP shards non-expert params over fewer dp ranks.default/EP8/offload=0step1/step20 loss match to 1e-4, as expected — these knobs change parallelism/memory strategy, not the math.FP8=1's step20 loss differs from the bf16 baseline (10.18 vs 10.31) because FP8 genuinely changes the numerics on the (unquantized-kv_b_proj) absorbed-MLA path, not a bug; itstgsis roughly half the baseline's at this 25B/20-step smoke scale, consistent with per-tensor quantize/dequantize overhead not yet amortized.MODEL_COMPILE=1's step1 duration (299s) is Dynamo/Inductor graph tracing, a one-time cost; step20 loss (10.31412) matches the eager baseline (10.31409) to 1e-4, confirming compile doesn't change the math.tgs@20is still below eager at this step count — few steps to amortize the compiled-kernel warmup, and several DSA/mHC regions stayfullgraph=False.Multimodal enablement
Sequence parallel in the compose model
The compose model asserted
sequence_parallel_meshwas None or size 1, so VL training could not use SP at all — the last piece of the Vision SP gap, now that the tower (layer 6) shards patches merge-aligned.The splice is the only part that needs to know about SP.
input_idsandmm_token_type_idsarrive already sharded (they are split together), so the local mask is what indexes the local embeddings; what the local mask cannot say is which global features belong to this rank._local_feature_slicegathers the mask to compute this rank's offset — the number of same-modality placeholders on all preceding ranks — and_gather_visual_featuresreassembles the globally-ordered features, trimming the tower's merge-alignment padding. The gather is the autograd-aware one, so its backward reduce-scatters and hands each rank the gradient of exactly the shard it produced.The placeholder↔feature count check now compares global totals, so a corrupted sample raises identically with and without SP. The pure-text dummy visual forward stays replicated: its gradient contribution is exactly zero, so sharding it would only add collectives.
VL training entry point
Glm53BaseConfig.from_hfraisedNotImplementedError, so the VL model could not be built from a checkpoint at all. It now reads the checkpoint's singlevision_configand fans it out to both XTuner modules — tower and projector are one HF module that §5.2 splits in two — and delegates the text half toGlm53TextMoEConfig.from_hf.examples/v1/config/sft_glm53_vl.py+sft_glm53_vl_tiny.shmirror the text pair knob for knob, so both modalities run the same acceptance matrix. Media comes from theci_vlcorpus (image + video samples). FP8 stays on the language tower only, matching how the published checkpoint stores the vision half in bf16.hf_configintentionally remainsNone:_write_hf_index_and_configthen copies the source checkpoint's config/tokenizer into the export, which is self-consistent for a fine-tune that does not change the architecture.VL end-to-end regression (
sft_glm53_vl_tiny.sh, single-node 8-GPU, real F0 25B cropped checkpoint)PACK_MAX_LENGTH=16384,TOTAL_STEP=20, image + video data. All six profiles complete 20 steps, loss decreasing, no NaN/OOM. Numbers are rank0;memismax_memory/reserved_memory:EP4 SP1 offload=1)SP2 EP4EP8 SP1offload=0(EP4 SP1)FP8=1MODEL_COMPILE=1EP8/offload=0step-1 loss is bit-identical (12.34349346) — these knobs change parallelism and memory strategy, not the math, the same conclusion as the text matrix.SP2's loss is not comparable row-to-row:GLOBAL_BATCH_SIZE = world/sp, so it consumes different samples per step (img_tokens6516 vs 29736). Its comparable quantity is the step-20 value, 0.004 from the default. Vision SP correctness rests on the 2-GPU bitwise parity tests in layer 6 andTestGlm53ComposeSequenceParallelhere, not on this smoke.MODEL_COMPILE=1gives the best throughput (+25% tgs) and the lowest peak memory, paying for it in step-1 graph compilation (71s total vs 53s).FP8=1's loss is visibly lower (9.99 vs 10.24), the same direction as text-side FP8: the absorbed-MLA path's numerics genuinely change; not a bug.EP8peaks highest becausedp_size = world_size/ep_sizedrops from 2 to 1, so FSDP shards non-expert params over fewer dp ranks.VL unit coverage
TestGlm53ComposeSequenceParallel::test_image_splice_under_sp_matches_non_sp(2 GPU): two placeholder spans placed so one lands on each rank's shard — forcing the cross-rank feature slicing — and each rank's logits match the corresponding slice of the non-SP run. This replaces the guard test that asserted SP was refused. tests/model/test_glm53_compose.py 6/6.