Repository navigation
Conversation
jayhenry
force-pushed
the
feat/glm53flash-f1-vl-data
branch
2 times, most recently
from
October 14, 2026 17:17
82f4b7b to
d526045
Compare
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
force-pushed
the
feat/glm53flash-f1-vl-data
branch
from
October 15, 2026 03:55
d526045 to
a29e639
Compare
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
force-pushed
the
fix/glm53-hf-input-contract
branch
from
October 15, 2026 04:04
45c0128 to
cc64fc6
Compare
jayhenry
force-pushed
the
feat/glm53flash-f1-vl-data
branch
2 times, most recently
from
October 15, 2026 04:37
c78be2b to
4b8620a
Compare
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.
GLM53 training can diverge from the native HF input protocol when
enable_thinking=False, when a video frame directory uses names such asframe_2.jpgandframe_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-dataata29e63978(#2109; rebased onto the force-pushed F1 branch on 2026-10-15, previouslye933fdcf5). 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
enable_thinkingbehavior, while the published GLM53 template always emits the reasoning-effort header and an open generation-time<think>prefix.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>.jpgor<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-25634d5ee66b12fa6446cdae131c352b8f68cd85369e0e6fda115583805fada3891). Transformersv5.17.0resolves to commit856157a2f3e9594954310df18fdccc31ffddebe9; 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
maxand always emits the system header; it does not inspectenable_thinking:The official generation prefix is always an open
<think>: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:VideoMetadata.timestamps derives each timestamp from those 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_10precedesframe_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:
replace_image_token expands the inner image placeholder to
prod(image_grid_thw) // merge_size**2tokens. 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:
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
a5b45eb4differs 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
eb11d83ece9952b12e2f770df9fc524c1cbfe6fbusing the same checkpoint assets and Transformers 5.17.0:PreTokenizedDatasetWrapperinputs and the generic collator causal shift were exercised. Labels are intentionally not claimed identical: XTuner respectsloss=Falseand 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'slossannotation. A reproduceduser="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 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:b5816bd1d: 55 passed. The new oversized-text case preserves text truncation and shifted labels.git diff --check: passed. An isolated/tmppre-commit environment ran the repository hooks: mypy passed all 394 source files, and all formatting/lint hooks passed for the three changed production files. Fullpre-commit run --all-fileswas 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 oncc64fc6d1passes 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) andcc64fc6d1(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.