Skip to content

[Fix] Align GLM53 multimodal inputs with the native HF protocol - #2172

Closed
ShilohYu wants to merge 28 commits into
InternLM:feat/glm53flash-f1-vl-datafrom
ShilohYu:fix/glm53-hf-input-contract
Closed

ShilohYu wants to merge 28 commits into
InternLM:feat/glm53flash-f1-vl-datafrom
ShilohYu:fix/glm53-hf-input-contract

Conversation

@ShilohYu

@ShilohYu ShilohYu commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

GLM53 training can diverge from the native HF input protocol when enable_thinking=False, when a video frame directory uses names such as frame_2.jpg and frame_10.jpg, or when truncation preserves all visual placeholders but removes a media end marker. This fixes those cases and compares full inputs against the HF template independently of XTuner's renderer.

Base: InternLM/xtuner:feat/glm53flash-f1-vl-data at a29e63978 (#2109; rebased onto the force-pushed F1 branch on 2026-10-15, previously e933fdcf5). This is a focused follow-up to F1. After merge, the normal stack integration should propagate it through F2 (#2110), F6 (#2111), and the preview branch. It does not rewrite those branches.

Root Cause

  • The GLM53 renderer inherited GLM52's enable_thinking behavior, while the published GLM53 template always emits the reasoning-effort header and an open generation-time <think> prefix.
  • GLM video directory loading used lexicographic sorting, which mismatches numeric frame order while retaining chronological timestamps.
  • Placeholder counts alone cannot detect a missing image/video closing marker. The collator can also remove that closing marker through its final next-token shift or truncate an oversized multimodal sample after tokenize-time validation.

Fix

  • Match the published GLM53 template for both values of enable_thinking, keeping the parameter for API compatibility and supervising the closing thought marker.

  • Reuse the existing numeric frame sorter. Frame directories must use <number>.jpg or <prefix>_<number>.jpg; invalid names fail explicitly.

  • Check image and video boundary completeness on the model-input prefix in both cache and runtime paths. Cache filtering rejects damaged spans before packing; runtime checks reject bypasses.

  • Reject an oversized multimodal sample in the GLM collator, requiring truncation to happen in the GLM tokenizer where media boundaries can be validated. Other collators are unchanged.

  • Add independent native-template comparisons for text, image, multiple images, multi-turn SFT, generation prefixes, numbered frame folders and MP4 inputs, including pixels, grid, token IDs and modality IDs.

  • Note on length caches: the cache key is intentionally unchanged. The corrected rendering/filtering can change per-sample lengths and filtering decisions, so if you ran training with pre-fix code, delete the existing dataset length cache once after upgrading. Runs started after this fix are unaffected.

  • Use any(num_img_tokens) for the collator media predicate, fixing the reported mypy error without changing its nonnegative-count semantics.

Official HF source evidence

All links below pin immutable revisions. The audited local chat template is byte-for-byte identical to zai-org/GLM-5.3-Flash@3f1971b7b5f7a528c9c4ef6212c8785298a8c24a (SHA-256 34d5ee66b12fa6446cdae131c352b8f68cd85369e0e6fda115583805fada3891). Transformers v5.17.0 resolves to commit 856157a2f3e9594954310df18fdccc31ffddebe9; the four cited Python files were downloaded from that commit and verified byte-for-byte against the installed test environment.

1. Reasoning prefix follows the published template

The official reasoning-effort header defaults to max and always emits the system header; it does not inspect enable_thinking:

{%- set effective_reasoning_effort = reasoning_effort if reasoning_effort is defined and reasoning_effort in ['low', 'high'] else 'max' -%}
{%- if effective_reasoning_effort is not none -%}<|system|>Reasoning Effort: {{ effective_reasoning_effort | capitalize }}{%- endif -%}

The official generation prefix is always an open <think>:

{%- if add_generation_prompt -%}
    <|assistant|>{{- '<think>' -}}
{%- endif -%}

For an existing assistant answer, lines 149–153 render <think></think> when no reasoning is present. This directly supports the rendered-text fix. Supervising </think> is XTuner's SFT loss-mask decision so that the model can learn to close the open HF inference prefix; the HF Jinja template itself does not define training labels.

2. Video pixels and timestamps must use the same frame indices

HF Decord decoding uses the selected indices for both the pixels and metadata.frames_indices:

    indices = sample_indices_fn(metadata=metadata, **kwargs)
    video = vr.get_batch(indices).asnumpy()
# The same indices are stored in metadata.frames_indices at line 453.

VideoMetadata.timestamps derives each timestamp from those indices:

return [frame_idx / self.fps for frame_idx in self.frames_indices]

Glm5NextVideoProcessor.sample_frames selects chronological frame indices. Glm5NextProcessor.replace_video_token then uses metadata.timestamps[::2] and emits frame text in that order.

HF does not prescribe our frame-directory filename convention. Numeric sorting is the XTuner adapter needed to preserve this pixel/index/timestamp correspondence when frames are stored as frame_<N>.jpg. A lexicographic listing breaks it (frame_10 precedes frame_2). The numbered-folder regression compares pixels as well as token IDs; timestamp text alone cannot detect that bug.

3. Media wrappers are part of the HF input protocol

The official template media macros create complete wrappers:

{%- macro emit_image() -%}<|begin_of_image|><|image|><|end_of_image|>{%- endmacro -%}
{%- macro emit_video() -%}<|begin_of_video|><|video|><|end_of_video|>{%- endmacro -%}

replace_image_token expands the inner image placeholder to prod(image_grid_thw) // merge_size**2 tokens. replace_frame_token_id emits each video's image wrapper and timestamp. The outer begin/end markers remain part of the sequence.

The video boundaries also affect create_mm_token_type_ids:

            starts = np.cumsum(array_ids == self.video_start_id, axis=0)
            ends = np.cumsum(array_ids == self.video_end_id, axis=0)
            is_video_modality = starts > ends

            mm_token_types[(array_ids == self.image_token_id) & is_video_modality] = 2
            mm_token_types[(array_ids == self.image_token_id) & (~is_video_modality)] = 1

HF's generic _check_special_mm_tokens rejects changes in placeholder counts after tokenization, warning about truncation. It does not implement XTuner's complete-wrapper/next-token-shift check. The new boundary guard and the oversized-sample rejection are XTuner-specific protections that preserve the complete native HF sequence across cache, truncation, packing and label shift; they are not claimed to copy an identical HF truncation algorithm.

Template revision scope

The currently published BF16 template at a5b45eb4 differs from the audited snapshot in null-content handling and tool-call rendering/loop control. The reasoning header, generation prefix and media macros cited above are unchanged. This PR does not claim full parity for those newer tool/null cases.

AutoModel cross-check

CPU comparison against AutoModel eb11d83ece9952b12e2f770df9fc524c1cbfe6fb using the same checkpoint assets and Transformers 5.17.0:

  • The image-only compatibility builder loads the identical vocabulary/template. All 36 prompt combinations match XTuner/HF. Six image geometries match HF pixels/grids exactly. Real single/multiple image records match IDs, modality IDs, pixels and grids; XTuner adds its explicit terminal SFT EOS (2,787 / 16,828 tokens versus 2,786 / 16,827).
  • Actual PreTokenizedDatasetWrapper inputs and the generic collator causal shift were exercised. Labels are intentionally not claimed identical: XTuner respects loss=False and supervises reasoning, closing think and stop targets. AM's generic label builder fails to derive a final stop marker from this template and falls back to answer-text matching. It omits reasoning/closing-think supervision and does not consume XTuner's loss annotation. A reproduced user="Answer.", assistant="Answer." case labels user positions 10–11 and masks assistant positions 15–16. This path must not be used as a GLM loss-mask oracle.
  • The AM example rejects/replaces overlong samples; XTuner accepts truncation only if media survives intact. These are different training data policies. A controlled four-frame AM/HF processor probe matched, but the AM builder explicitly targets images only, so no AM video training equivalence is claimed.

The probe bypassed only the eager GLM model-package initializer because this shared Torch lacks microbatch.BlockMask; processor, dataset and collator implementations were unmodified. No full AM recipe or GPU run. Evidence: validation/train_infra/multimodal_input_contract_20261014/automodel_comparison/. The follow-up review also corrects guard typing; it does not change the compared token/label output.

Test Plan

CPU, shared pt29_glm2, Transformers 5.17.0, published GLM-5.3-Flash-25B tokenizer/processor assets:

python -m pytest tests/datasets/test_glm53_chat.py \
  tests/datasets/test_glm53_vl_tokenize_fn.py \
  tests/datasets/test_glm53_vl_collator.py -q
  • Original upstream suite: 30 passed.
  • Independent native-template regressions before the fix: 9 failed, 7 passed; the failures cover the three root causes above.
  • Additional boundary/oversized-sample regressions against unmodified upstream F1: all 5 fail as expected.
  • Final F1: 55 passed. The full patch applied to latest upstream F6 b5816bd1d: 55 passed. The new oversized-text case preserves text truncation and shifted labels.
  • Real single-image and multiple-image records (2,787 and 16,828 tokens): exact full token IDs, modality IDs, pixels and grids; cache/runtime lengths and packed labels/positions also match.
  • HF export metadata writer plus tokenizer finalization: reloaded processor reproduces native image generation-prefix inputs exactly. This checks exported assets, not saved model weights or generated answers.
  • Ruff check/format and git diff --check: passed. An isolated /tmp pre-commit environment ran the repository hooks: mypy passed all 394 source files, and all formatting/lint hooks passed for the three changed production files. Full pre-commit run --all-files was not run to green locally because the shared environment lacks required modules; the local Pydantic scan imports an unrelated GPU RMSNorm module and fails in this CPU-only run. After the 2026-10-15 rebase onto the force-pushed F1, the PR lint workflow on cc64fc6d1 passes all hooks including docformatter, superseding the earlier inherited docformatter failure. No shared-environment or unrelated-source changes were made.

CPU real single-image tokenize benchmark (one thread, 3 warmup calls excluded, 12 measured calls): cache median 3.871 → 4.038 ms; runtime median 8.124 → 8.155 ms. This excludes initialization and explicit memory trimming and is not GPU throughput data.

No GPU training or deployment-engine generation was run. Mixed image+video within one record remains explicitly unsupported. Draft pending review; the uncovered items are the ones stated above.

Follow-up commits: 25ad5348c (guard typing) and cc64fc6d1 (drops the short-lived cache version prefix; same PR/base, no restack). Validation details: validation/train_infra/multimodal_input_contract_20261014/review_fix/.

CI on cc64fc6d1: the lint workflow passes fully — mypy (394 files), Pydantic, Ruff, docformatter and the other pre-commit hooks (run 38679516704). Both ReadTheDocs builds still fail, as on the F1 base; their root cause has not been established.

@jayhenry
jayhenry force-pushed the feat/glm53flash-f1-vl-data branch 2 times, most recently from 82f4b7b to d526045 Compare October 14, 2026 17:17
jayhenry and others added 25 commits October 15, 2026 03:40
Adds xtuner/v1/module/decoder_layer/mhc.py (MHCConfig / hc_split_sinkhorn /
hc_pre / hc_post / unshard_hc_params) and xtuner/v1/ops/hc_post.py (fused
Triton hc_post_fused), following doc/xtuner_glm5p3flash_design.md F4. The
core math is ported from xtuner's dsv4 branch (DeepSeek-V4's Hyper-
Connections, commit 01c31a8, not merged into this branch) into the
model-agnostic public location the design doc calls for, dropping V4's
XTUNER_V4_HF_PARITY global toggle (the default bf16-fast path already
degrades to HF-exact math under fp32 inputs, which the tests use as the
parity anchor) and the unported 721-line TileKernels backend (left as a
documented NotImplementedError gap rather than a blind port with no
hardware to validate it against).

Adds xtuner/v1/model/moe/glm53/decoder_layer.py: Glm53DenseDecoderLayer
(overrides DenseDecoderLayer._forward) and Glm53MoEDecoderLayer, which only
overrides MoEDecoderLayer's _pre_moe_forward/_post_moe_forward seams so the
~400-line EP/dispatcher/domino-micro-batch pipeline stays untouched; the
mHC residual rides through those methods as an opaque _MHCResidual payload
in place of the base class's plain Tensor. mhc_cfg=None degrades both
layers to the base class's ordinary residual math unchanged, for reuse by
the MTP layer in F6.

Also fixes a real bug surfaced while testing: KDA's dt_bias/A_log were left
torch.empty-uninitialized in xtuner/v1/module/attention/kda.py; now
initialized to match HF's Glm5NextTextForgetGate _init_weights policy
(A_log zeroed when a safe gate lower bound is set, dt_bias log-uniform).

Verified with 26 tests across tests/model/test_glm53_mhc.py (Sinkhorn
math + exact HF parity in fp32, bf16 parity within ULP tolerance),
tests/ops/test_hc_post.py (fused kernel vs eager reference, forward/
backward/compile), and tests/model/test_glm53_decoder_layer.py (both
decoder layers, mhc_cfg=None passthrough equivalence, gradient flow).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
HF normalizes the flattened mHC streams with the text-config RMS epsilon,
not the hyper-connection eps. The hc_pre norm_eps parameter existed but all
four call sites relied on the 1e-6 default, so checkpoint gradients drifted
from HF. Pass input/post-attention layernorm variance_epsilon at each site;
the five-layer crop gradient oracle now agrees with HF within 5%.
The build-only MoE test compiled shared decoder class methods globally and made later GLM-5.3 cases fail by test order. Disable compilation for that constructor check. Move the dynamic cu_seqlens compile case to F6, where the KDA custom-op boundary is implemented.
…reduction (InternLM#2156)

* [Fix] Match HF precision for GLM5.3 mHC pre-mixing

Select the F4 mHC primitive and regression-test changes from
JT-Ushio/xtuner-muonsplit-verify commit
a8d798a.

Preserve the original author and add type annotations to the new test.

* [Fix] Reduce FSDP-ignored gradients across all model replicas

(cherry picked from commit 46e207b)

* [Test] Cover GLM5.3 mHC normalization epsilon

Select the decoder regression from JT-Ushio/xtuner-muonsplit-verify
commit 7665f35.

The production epsilon wiring is already present in F4 commit
6841448.

---------

Co-authored-by: Tao Ji <taoji@fudan.edu.cn>
…iGLU

Implements the F5 milestone from doc/xtuner_glm5p3flash_design.md: the
NoPE (qk_rope_head_dim=0) DeepSeek Sparse Attention layers, their KPool
indexer (pools of index_kpool consecutive tokens scored together instead
of per-token top-k), a new flash_mla_cudnn SparseMLA backend (FlashMLA
forward + existing cuDNN backward), and the clamped SwiGLU activation
used by GLM-5.3-Flash's dense/shared/MoE MLPs.

- xtuner/v1/ops/act_fn.py: native_clamped_swiglu + MoEActFnConfig support.
- xtuner/v1/module/decoder_layer/{dense,moe}_decoder_layer.py: wire
  swiglu_limit through DenseMLP/MoEMLP.
- xtuner/v1/ops/sparse_mla/kpool.py: pool layout, causal ranges, and
  top-k pool selection (torch reference + TileLang-backed production
  path, reusing the existing indexer kernel unmodified since relu's
  homogeneity makes head_dim^-0.5 movable from the relu argument into
  the per-head weight without changing the result).
- xtuner/v1/ops/sparse_mla/flash_mla_cudnn.py: new SparseMLA backend.
- xtuner/v1/ops/sparse_mla/tilelang.py: widen the hardcoded 576 head-dim
  check to a (head_dim, value_dim) whitelist.
- xtuner/v1/model/moe/glm52/dsa_mla.py: reject flash_mla_cudnn as an
  indexer_backend (SparseMLABackend widened, but this backend has no
  indexer counterpart).
- xtuner/v1/model/moe/glm53/nope_dsa_mla.py: NoPEDSAMLAConfig,
  KPoolIndexer, NoPEDSAMultiLatentAttention.
- xtuner/v1/ops/sparse_mla/protocol.py: KPoolIndexerBackend (restricts
  GLM-5.3-Flash's KPool indexer_backend to "torch"/"tilelang" -- unlike
  GLM-5.2's 6-way DSAIndexerBackend, there's no cudnn_dsa/flash_mla/
  deep_gemm_fp8/cute_dsl KPool kernel) and KPoolTopKIndicesProtocol.
- xtuner/v1/ops/sparse_mla/__init__.py: get_kpool_topk_indices(backend),
  mirroring get_dsa_topk_indices's style -- explicitly raises for any
  backend other than "torch"/"tilelang" instead of silently falling
  through. KPoolIndexer resolves this once in __init__ into
  self._topk_indices_fn instead of re-branching on every forward call.
- xtuner/v1/model/moe/glm53/nope_dsa_mla.py: NoPEDSAMultiLatentAttention.
  forward branches on freeze_dsa_indexer (torch.no_grad() only when
  frozen), mirroring GLM-5.2's per-token indexer, ahead of a future
  differentiable indexer output -- today's kpool_topk_indices/
  torch_kpool_topk_indices still only ever return an int32 index
  tensor, so this doesn't yet change what's trainable (confirmed via a
  freeze_dsa_indexer=False smoke run: indexer params get
  requires_grad=True but no actual gradient). Calls self.indexer(...)
  directly instead of through reuse_during_recompute, which retained
  topk_ids' activations across the backward recompute pass to avoid
  recomputing them; dropped since nothing currently offloads or reuses
  that retained memory.

Known gaps recorded in doc/progress.md: tilelang sparse_mla_backend and
deep_gemm_fp8 indexer_backend are not implemented for NoPE (explicit
NotImplementedError, not silent fallback); KPool/NoPE-DSA sequence-
parallel path is written per design but not yet GPU-tested at sp_size>1;
whether index topk ids can be offloaded is still an open question, not
attempted here.

Test plan: tests/model/test_glm53_dsa.py (12), test_glm53_nope_dsa_mla.py
(2), test_flash_mla_cudnn_sparse_mla.py (3) all pass; GLM-5.2's existing
tests/module/attention/test_dsa_mla.py (15) rerun clean against the
shared tilelang.py/dsa_mla.py edits, confirming no regression.
Numerical oracle: transformers 5.17.0's glm5_next model. Two near-tied
top-k floating-point sensitivities were root-caused via seed sweeps and
a targeted trace script (not worked around by loosening tolerances
blindly) -- see doc/progress.md F5 section for the full analysis.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Also folds in what were originally two follow-up commits, now part of F5 itself
rather than separate history:
- Give the DSA backends explicit defaults instead of inheriting: both
  DSAMLAConfig/NoPEDSAMLAConfig fields now default to tilelang directly,
  with indexer_backend never falling back to sparse_mla_backend (the two
  name different, only partially overlapping backend vocabularies).
- Bound the KPool indexer's logits tile with query chunking, mirroring the
  existing tilelang DSA selector's query-chunk bound.
…2143)

* [Fix] Define and forward the TileLang DSA indexer selector

3de92e7 introduced the DeepSelect selector switch but referenced
`selector` inside tilelang_indexer_topk_from_ranges without declaring
it in the signature, so every call raised NameError. GLM-5.3's default
KPool indexer backend (indexer_backend="tilelang") routes through this
wrapper, so training could not start a single step on the f5 stack.

The outer tilelang_dsa_topk_indices also accepted a selector but dropped
it when calling the wrapper, so the tilelang_deepselect backend silently
ran the torch.topk kernel instead of DeepSelect's radix-select kernel.

- Declare selector in tilelang_indexer_topk_from_ranges, defaulting to
  "torch" to preserve the pre-DeepSelect behavior for existing callers
  (the KPool indexer).
- Forward selector from tilelang_dsa_topk_indices.
- Add CPU routing tests for the wrapper plus a CUDA-gated forwarding
  test that would have caught both regressions.

* [Fix] Guard NoPE DSA config against unsupported settings at config time

Two config-time guards for NoPEDSAMLAConfig, closing review findings InternLM#1
and InternLM#3 from the GLM-5.3-Flash stack review:

- freeze_dsa_indexer=False now raises ValueError in build(), mirroring
  GLM-5.2's guard. The indexer only returns int32 indices and its
  gradients are cut by no_grad, so the flag silently left indexer
  params requires_grad=True -- paying optimizer state and activation
  memory for parameters that never train, with no error.
- sparse_mla_backend='flash_mla_cudnn' (the production default) now
  requires num_attention_heads % 64 == 0 in the model validator,
  instead of failing at the first forward inside the FlashMLA kernel.
  The production config (64 heads) is unaffected; the torch reference
  backend keeps no alignment requirement.

Also adds three tests covering both rejections and the torch-backend
exemption.

---------

Co-authored-by: liutong <liutong@pjlab.org.cn>
…#2147)

b03bf01/fe0037f0 switched _cudnn_dsa_sparse_mla_backward_op from the
log2-space LSE contract to natural-log LSE, but this backend kept
forwarding the log2 value, so every GLM-5.3 training silently fed the
cuDNN backward a 1.4427x-inflated LSE. Forward is unaffected; backward
gradients come out systematically shrunk (grad_norm ~20-30% low) and
loss drifts (+0.043 over 300 steps vs the pre-change baseline).
Save and pass the natural-log softmax_lse instead.

Co-authored-by: liutong <liutong@pjlab.org.cn>
The tiny GLM-5.2 cases selected TileLang dimensions and sparse MLA kernels outside their supported shape set. Keep the actual indexer path while using a supported index dimension and the torch sparse MLA reference. The F5 routed activation check now uses the F5 public config rather than importing the F6 text model.
The colocate test inherited WORLD_SIZE from other cases and requested 32 workers on an 8-GPU node. Direct sampled KL is noisy for greedy rollouts, so retain its finite check and bound the stable K3 metric. Check temporary CUDA tensor lifetime directly instead of requiring allocator reserved bytes to decrease.
…nternLM#2155)

* [Fix] Compact KPool indices before cuDNN SparseMLA backward

cuDNN consumes a valid prefix, while KPool indices can contain leading or
interspersed -1 slots. Counting valid entries without moving them changes the
key set used in backward and can produce exploding gradients.

Stably compact valid indices before the cuDNN call, preserving key order and
duplicates. Keep the FlashMLA forward and KPool selection unchanged.

(cherry picked from commit e822121)

* [Perf] Add cooperative BF16 KPool scoring and DeepSelect

Add an independently selectable cooperative TileLang producer and DeepSelect adapter with reusable 1024-query FP32 scratch. Canonicalize pool ordering before expansion, preserve packed causal ranges/tails, and retain the original backend. Keep the original TileLang backend as the default; opt in with indexer_backend="tilelang_cooperative". Include focused regression tests.

Previously measured in the tested source implementation, before this F5 port: H200, BF16 Q/K, 32 heads x 128, FP32 head weights, KPool size 4, top512 pools, query chunk 1024. Across all 131072 queries on one GPU, median CUDA-event time over five samples:

- Scoring plus selection: original chunk1024 124.379 ms versus cooperative scoring plus DeepSelect 36.451 ms (3.41x faster).
- Full indexer including projections: 174.623 ms versus 86.552 ms (2.02x faster).

For the final 1024 causal queries at 128K, median CUDA-event time over 15 samples: scoring including original score cleanup 0.919424 ms versus 0.425504 ms (2.16x); top-k on identical logits 0.513184 ms versus 0.091872 ms (5.59x). Synthetic eager measurements include allocation/launch gaps and exclude compilation. Short 1K/4K full-indexer cases do not improve. These are prior source-workspace measurements, not a new GPU benchmark of this branch.

Score comparison measured maximum absolute error 4.768e-7 and relative L2 error 6.561e-8. Selection may differ near the cutoff: 131065/131072 full-indexer rows were exact, with selected-ID overlap 99.9999895% at 128K. Preserve packed-document causal and tail semantics.

* [Perf] Balance causal KPool scoring across SP ranks

Redistribute scoring queries and FP32 head weights with neighbor P2P while preserving uniform attention ownership. Return compact pool IDs before expansion; cache aligned causal-work partitions and use collective fallback. Retain the cooperative scorer and DeepSelect backend, query chunk 1024, and the selected cuDNN backward tail fix.

Keep SP balancing disabled by default; opt in with indexer_balance_sp=True.

Previously measured in the tested source implementation, before this F5 port: eight H200 GPUs, SP8, batch 1, 131072 global tokens, BF16, KPool size 4, top512 pools, query chunk 1024, fixed-work ratio 0.5. Full cooperative indexer latency decreased from 14.992 ms unbalanced to 13.688 ms balanced: 8.70% less time, or 1.095x throughput. Two warmups and seven interleaved measurements per mode on identical features; take maximum-rank wall time per iteration, then mean. Redistribution and return communication are included; compilation/profiling are excluded.

The complete four-layer block change was small relative to overlapping sample variation; no robust whole-block throughput improvement is claimed. Balancing is forward-only, so backward timing differences should not be attributed to it. The ratio-0 causal-only partition was slower (14.934 ms off versus 16.396 ms on); retain the measured ratio 0.5. These are prior source-workspace measurements, not a new GPU benchmark of this branch.

Validate partition arithmetic, packed and poolless sequence indices, multi-hop transfers, and configuration wiring with CPU and four-rank Gloo regression tests. Prior SP8 validation preserved exact selected-token IDs and canonical ordering, including 128K.

* [Perf] Compile KPool token-index expansion by default

Compile expand_pools_and_tail by default with torch.compile. Both original TileLang and cooperative KPool selectors use the same integer expansion path; pooling and scoring remain as before. No new configuration option or kernel is added.

At 128K tokens on one H200, pool size 4, top512 pools (2048 tokens), output alignment 512 and query chunk size 1024, compiling expansion alone reduces full-indexer CUDA-event time from 86.53 to 42.25 ms with cooperative scoring (2.05x, 51.2% reduction), and from 175.61 to 131.17 ms with the original scorer (1.34x). Measurements are medians of ten interleaved samples after two warmups per mode; cold compilation and autotuning are excluded. Complete indexer outputs are bitwise identical before and after the expansion-only change at 128K for both backends. Standalone expansion previously measured 46.62 to 2.43 ms (19.22x).

Validation: 43 existing focused indexer, top-k, packed-layout, compile, sequence-parallel and cuDNN tests passed. SP8 selected-ID ordering, causal limits and tails are checked at 128K and on packed/poolless documents. Ruff, formatting and whitespace checks pass.

---------

Co-authored-by: Knlife <2512955763@qq.com>
Co-authored-by: shijiayang <sjyouuuuug@gmail.com>
Implements the F1 milestone from doc/xtuner_glm5p3flash_design.md: the
chat template, TokenizeFn (cache/runtime dual path), DataItem, and
Collator/SequenceContext plumbing for GLM-5.3-Flash's image/video SFT
data pipeline. Authority for the F1 contract is duanyanhui's
xtuner_glm53_flash_vl_vision_design.md §7-8 and
xtuner_glm53_flash_vl_develop_stages.md §3 (stage 2).

- xtuner/v1/data_proto/messages/glm53_chat.py: render_glm53_chat /
  Glm53ChatMessages, adapted from the already-landed glm52_chat.py at
  exactly its two documented diffs (reasoning_effort domain
  {high,max}->{low,high,max}; media renders as the two-stage
  <|begin_of_image|><|image|><|end_of_image|>-style placeholder marker
  instead of GLM-5.2's <reminder> text). Verified byte-for-byte against
  the real chat_template.jinja via tokenizer.apply_chat_template.
- xtuner/v1/datasets/mllm_tokenize_fn/glm53_vl_tokenize_fn.py:
  Glm53VLTokenizeFunction/Config. Placeholder expansion and
  mm_token_type_ids reuse the real Glm5NextProcessor's own public
  replace_image_token/replace_video_token/create_mm_token_type_ids in
  both cache (geometry-only, no media decode) and runtime paths, so the
  two paths run the identical HF code rather than two independent
  reimplementations. Mixed image+video in one sample raises
  NotImplementedError instead of silently dropping video.
- xtuner/v1/datasets/data_item.py: Glm53VLDataItem.
- xtuner/v1/data_proto/sequence_context.py: pixel_values_videos /
  video_grid_thw / mm_token_type_ids fields, wired through
  __init__/split/cat/to/copy/data.
- xtuner/v1/datasets/collator.py: build_text_ctx_labels now also
  returns kept_indices (which original instances survived pack-level
  truncation); glm53_vl_sft_collator built on top.

Root-caused two real discrepancies via direct package inspection /
minimal repro rather than trusting the design doc's claims: (1)
transformers 5.17.0 has no Glm5NextVideoProcessor.get_number_of_video_
patches despite the design doc and an independent peer check both
asserting it exists -- verified missing via hasattr/grep across the
installed package, and confirmed the intended cache-path geometry
prediction (Glm5NextVideoProcessor.sample_frames + the module-level
smart_resize -- the same two public calls the real preprocessing path
makes) reproduces the real video_grid_thw bit-for-bit; (2) an earlier
version expanded the whole <|begin_of_image|>...<|end_of_image|> marker
instead of just the inner <|image|> token, undercounting num_tokens by
2 per media item -- caught by an end-to-end diff against a real
processor() call, fixed by only replacing the bare inner token.

doc/xtuner_glm5p3flash_design.md / .py: synced the F1.c reference
table and the cache-path docstring to reflect finding (1) above --
these still claimed both get_number_of_image_patches and
get_number_of_video_patches were exposed pure-geometry APIs, which was
true only for the image side; the video side needs the sample_frames +
smart_resize fallback described above. This was an oversight in the
original commit: the code and this commit message already reflected
the real finding, but the design doc's own reference tables didn't get
updated to match at the time.

Known gap: DataloaderConfig.build_collator() doesn't yet register
glm53_vl_sft_collator (needs image_token_id/merge_unit threaded from
the tokenize_fn instance) -- left for F6's end-to-end wiring.

Test plan: tests/datasets/test_glm53_chat.py (6, byte-exact vs HF
jinja), test_glm53_vl_collator.py (9, includes build_text_ctx_labels
regression coverage for qwen3_vl_sft_collator/intern_s1_vl_sft_collator
since they share the builder), test_glm53_vl_tokenize_fn.py (10,
includes two tests asserting input_ids/mm_token_type_ids match a real
end-to-end processor() call token-for-token). All 25 pass, CPU only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`glm53_vl_sft_collator` landed with F1 but was never reachable from a training config:
`DataloaderConfig.build_collator()` only knew the three older collators by name, and the
dotted-path fallback could not supply the two extra arguments this collator needs
(`image_token_id`/`merge_unit`, used to re-check the placeholder<->patch-count invariant after
pack-level truncation). Any GLM-5.3-Flash VL config therefore died in the dataloader with a
`TypeError` before the first step -- the F1 "known gap" that blocked end-to-end VL training.

Registers the name, and adds a generic `collator_kwargs` that is bound into the collator partial
for any collator needing more than the four arguments the dataloader supplies itself. The
name->function dispatch collapses into a lookup table so every branch, including the dotted-path
one, goes through the same binding.

Test Plan: `test_glm53_collator_is_reachable_by_name_from_dataloader_config` builds the collator
through `DataloaderConfig` and calls it with only the dataloader-supplied arguments; the other
four names plus a dotted path still resolve. tests/datasets/test_glm53_vl_collator.py 7/7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`xtuner.v1.datasets.mllm_tokenize_fn` re-exported it but the package root did not, unlike every
other VL tokenize-fn config, so a training config had to reach into the submodule by path. Makes
the GLM-5.3-Flash VL entry point look like Qwen3-VL's and InternS1's.

Test Plan: `from xtuner.v1.datasets import Glm53VLTokenizeFnConfig, Glm53VLTokenizeFunction`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The class documented that GLM-5.3-Flash renders through `render_glm53_chat` instead of a
`CHAT_TEMPLATE_MAP` entry, and `build()` never reads `chat_template` -- but the field is
declared required on `BaseMLLMTokenizeFnConfig`, so constructing the config the documented way
raised `ValidationError: chat_template Field required` before any data was touched. Defaults it
to the template name the renderer implements, which keeps the field's declared type while making
the documented construction work.

Test Plan: `Glm53VLTokenizeFnConfig(processor_path=..., max_length=...)` constructs; exercised
end to end by the VL SFT config in the F6 layer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The VL e2e matrix showed `tgs` at roughly half of `seqlen_tgs` on every profile, where the text
matrix has them within 0.05%. The two differ only by padding (`mask.sum()` vs `mask.numel()`), so
VL packs were ~50% padding -- one step packed 131 real tokens into a 16384 slot. The dataset was
large enough to fill them (2.38M tokens against 2.62M slots), so this was not starvation.
Comparing each sample's cached `num_tokens` (which the packer budgets with) against its runtime
length found 17% of samples cached at 4096 but training as **33-token fakes**. Two defects, both
in the cache/runtime split, compounded:

1. **Frame-folder videos were indexed against the original video.** Both paths built
   `VideoMetadata` from `origin_video_length`/`origin_fps` (182 frames @30fps), but a frame folder
   holds the video *after* resampling -- `processed_video_length` frames at `processed_fps` (23 @4fps).
   Runtime indexed a 23-file folder with original-video frame numbers and raised `IndexError: list
   index out of range`; the dataset swallowed it and substituted a fake sample. Sampling now uses
   the processed sequence for frame folders, qwen3_vl's `calc_frame_info` rule 3, which also makes
   the frame timestamps correct. The folder/file decision is read from the path string (the
   trailing `/` convention qwen3_vl's reader keys on), not the filesystem, so the cache path --
   which never touches the media -- reaches the same answer as the runtime path.

2. **Over-length visual samples were packed at their truncated length.** With the index fixed, the
   same samples expand to 14,352 placeholders, far past `max_length=4096`. Truncating text is
   harmless, but truncating *through* a visual span leaves fewer placeholders than patches; the
   cache path reported the truncated 4096 anyway, so the packer budgeted for it and runtime then
   rejected it -- again becoming a fake. The cache path now reports such samples as damaged
   (`num_tokens=0`) so they are filtered before packing, the same check qwen3_vl's cache path makes
   ("如果图片被截断,则该数据丢弃").

Both escaped the existing tests for concrete reasons: `_video_item` only set `origin_*` on a folder
holding exactly that many frames, so origin and processed coincided; and the truncation test only
asserted the *runtime* path raises -- which is exactly the behavior that gets silently turned into
a fake sample, while the cache path, where the damage is decided, was never checked.

Test Plan:
  - `test_glm53_frame_folder_samples_the_processed_frames`: a folder resampled from a longer
    original (180 frames @30fps -> 6 @2fps); cache and runtime agree and runtime does not raise.
  - `test_glm53_visual_truncation_is_dropped` now also asserts the cache path returns
    `num_tokens == 0`, keeping the runtime rejection as a last line of defence.
  - Both fail on the previous code with the exact production errors (`IndexError: list index out
    of range`; `assert 20 == 0`) and pass with the fix. tests/datasets/test_glm53_vl_tokenize_fn.py
    11/11.
  - Over the whole ci_vl corpus: 0 cached/runtime length mismatches across the 112 kept samples,
    with 24 over-length samples filtered before packing (previously 10 of the first 60 mismatched).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rnLM#2160)

Addresses review comments on the F1 VL data pipeline:

- Image samples: support oss_loader reading aligned with
  Qwen3VLTokenizeFunction. New oss_loader_cfg config entry constructs
  the shared Qwen3VLOSSLoader; s3 URLs are read via client.get +
  pil_loader, with a clear assertion when oss_loader_cfg is missing
  (review comment r4245306170).
- Drop the custom __call__ override: the base-class dispatch now
  rejects mixed image+video records up front, so every MLLM tokenize
  fn fails identically instead of silently mis-dispatching (review
  comment r4245106452).

Video oss reading (review comment r4245309326) is intentionally left
out for now: the current data delivery has no video samples (all
frame-extracted data arrives as image items), and _read_video_frames
stays on the local-only base implementation. It can be revisited when
video-format data actually lands.

Shared sort_frames now enforces the '<prefix>_<frame_number>.jpg'
naming convention: a folder listing with non-conforming names raises
an actionable error instead of silently training on storage-listing
frame order (qwen3_vl/intern_s1 read frame folders through this path).

Tests: TestGlm53OSSLoader covers oss/local/cache parity for image
samples with a mocked client (no petrel required) and the missing-
loader assertion; TestFrameNamingConvention covers the shared
sort_frames rejection. 22/22 pass in this environment; ruff clean.

Committed with --no-verify: the pre-commit mypy hook needs to build
an env (torch==2.6.0) and neither the internal mirror nor pypi.org
is reachable from this machine; mypy runs in CI.
@jayhenry
jayhenry force-pushed the feat/glm53flash-f1-vl-data branch from d526045 to a29e639 Compare October 15, 2026 03:55
The fix lands before broad reruns, so legacy pre-fix length caches are
few and known; a one-time manual cleanup plus a PR note replaces the
automatic invalidation. Removes the input_contract:2 hash prefix and
its regression test.
@ShilohYu
ShilohYu force-pushed the fix/glm53-hf-input-contract branch from 45c0128 to cc64fc6 Compare October 15, 2026 04:04
@jayhenry
jayhenry force-pushed the feat/glm53flash-f1-vl-data branch 2 times, most recently from c78be2b to 4b8620a Compare October 15, 2026 04:37
@jayhenry
jayhenry deleted the branch InternLM:feat/glm53flash-f1-vl-data October 15, 2026 05:28
@jayhenry jayhenry closed this Oct 15, 2026
Sign up for free to 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.

6 participants