Skip to content

[kimi k3] Support sample packing in KDA - #4347

Open
JavaZeroo wants to merge 5 commits into
pytorch:mainfrom
JavaZeroo:kimi-k3-sample-packing
Open

[kimi k3] Support sample packing in KDA#4347
JavaZeroo wants to merge 5 commits into
pytorch:mainfrom
JavaZeroo:kimi-k3-sample-packing

Conversation

@JavaZeroo

@JavaZeroo JavaZeroo commented Aug 27, 2026

Copy link
Copy Markdown
Contributor
  • KimiDeltaAttention now takes document offsets as VarlenMetadata. With offsets, the Q/K/V causal convolutions run through Attention Gym's varlen causal_conv1d and the recurrence passes cu_seqlens to its chunk_kda, so both the convolution window and the recurrent state reset at document boundaries.

  • get_attention_masks returns a mask dict, mirroring Qwen3.5's hybrid layout: MLA layers read "quadratic_attention" (a BlockMask under flex, shared offsets under varlen) and KDA layers read "kda".

  • The dataloader now emits a padding_mask alongside positions: the text and multimodal packers derive it from Grain's input_ids_segment_ids, and their collators extend it with the batch tail they append.

  • VarlenMetadata also carries cu_seq_q_cpu, the host copy of the offsets that FLA's varlen kernels take, so Qwen3.5 passes one shared object per batch instead of rebuilding it in every layer.

Performance

The varlen host offsets are built once per batch and carried in VarlenMetadata instead of being rebuilt inside every layer, which cuts both the CPU ops and the d2h per training step. Qwen3.5 is the model this shows up on: Kimi K3's KDA moved from FLA to Attention Gym in #4351 and no longer rebuilds them per layer.

image

Qwen3.5

FLA memoizes its varlen index tables on argument identity, so rebuilding the host offsets inside each layer meant the first conv of every layer missed that cache and paid a full rebuild plus a d2h. qwen35_debugmodel has 12 GDN layers and calls the conv three times per layer, which is exactly what the per-call durations show:

CausalConv1dFunction calls over 600 us p50 p90
Before 12 of 36 -- one per layer 239 us 1087 us
After 1 of 36 -- the first of the step 201 us 258 us

Identical in all three runs. The median call barely moves, so the win is entirely in the calls that used to rebuild.

qwen35_debugmodel, single GPU:

Metric Before After Change
FLA CausalConv1dFunction (x36) 16.97 ms 8.36 ms -51%
FLA ChunkGatedDeltaRuleFunction (x12) 16.90 ms 13.12 ms -22%
aten::repeat_interleave (x64 -> x31) 12.42 ms 3.18 ms -74%
aten::arange (x126 -> x58) 1.87 ms 1.05 ms -44%
aten::narrow (x141 -> x54) 1.41 ms 0.30 ms -79%
All cpu_op 614.2 ms 535.3 ms -13%
Step wall time 236.8 ms 215.4 ms -9%

Ten deterministic steps of qwen35_debugmodel reproduce upstream's loss and grad_norm exactly.

Verification

Offsets isolate documents. Two documents (lengths 96 and 160) run separately, then their concatenation run once with cu_seqlens = (0, 96, 256). Packed output is bitwise equal to the per-document outputs (atol=0, rtol=0). Without the offsets the second document is contaminated, as expected.

End to end on B200 (sm_100, kimi_k3_debugmodel, 10 steps, --debug.seed 42 --debug.deterministic). The 256-token debug row holds one cc12m document, so the row was widened to 2048 to actually exercise packing:

Documents per row Padding tokens
No packer 1 1837
Packed 16 3

Loss 12.50 -> 3.26 over 10 steps (control without the packer: 12.45 -> 4.50), grad_norm 15.1 -> 2.2.

Conclusion

Qwen3.5 is unchanged. Ten deterministic steps of qwen35_debugmodel reproduce upstream's loss and grad_norm exactly.

Kimi K3 does not reproduce upstream bitwise, and shouldn't: on a padded batch upstream splits the tail into one document per pad token, which is the bug this fixes.

TODO

  • .tolist() in the mask path looked like a per-step d2h -- profiled, and the real cost was the per-layer offset rebuild, now fixed.
  • get_attention_masks in Kimi K3 and Qwen3.5 are near-identical and should be shared; needs a design.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Aug 27, 2026
@JavaZeroo
JavaZeroo marked this pull request as draft August 27, 2026 05:33
Comment thread torchtitan/models/common/attention.py Outdated
@JavaZeroo
JavaZeroo force-pushed the kimi-k3-sample-packing branch from 6316daf to bf909e7 Compare August 27, 2026 06:40
Document boundaries were inferred from positions alone, but packers pad a
row with position 0 on every padded token, so
create_varlen_metadata_for_document read that padding as a run of
one-token documents. Qwen3.5 worked around it in its own
get_attention_masks with a heuristic -- a real document start is a
position 0 followed by a position 1 -- which silently merges a genuine
one-token document into its predecessor and only ever recovered
information the packer already had.

Have the dataloader state what is padding instead of having each model
re-derive it:

- Both packers keep Grain's input_ids_segment_ids as a padding_mask (0
  marks the padding Grain appends), and both collators extend it with the
  batch tail they append.
- preprocess_inputs pops the mask out of the batch and passes it to
  get_attention_masks. It describes the batch rather than the model
  input, so it never becomes a forward kwarg.
- create_varlen_metadata_for_document takes padding_mask and collapses
  each contiguous padding run into one segment. Without a mask its
  behavior is unchanged.
- Qwen3.5 drops its heuristic, including the 'single document keeps the
  plain kernels' short-circuit: every batch is packed and padded, so the
  case it guarded does not arise.

This also fixes the text path, where TextCollator pads with position 0
too: every varlen-backend text model was reading a padded tail as one
document per pad token.

Ten deterministic steps of qwen35_debugmodel (--debug.seed=42
--debug.deterministic) reproduce loss and grad_norm exactly.
KDA already takes document offsets after pytorch#4351, but Kimi K3 still
rejected MMSamplePackingConfig and handed its KDA layers a hard-coded
None, so the offsets never reached them.

get_attention_masks now returns a mask dict, mirroring Qwen3.5's hybrid
layout: MLA layers read "quadratic_attention" (a BlockMask under flex,
shared offsets under varlen) and KDA layers read "kda". The block routes
each layer to its own entry, and the packing guard is gone.

The offsets test uses Attention Gym's eager reference implementation so
it runs on any CUDA device -- the fused kernel it ships alongside is
compiled for sm_100 only, which is why the kernel test beside it is
skipped here. What the test covers is the segmentation the offsets
encode, which is implementation independent: a packed row reproduces the
per-document runs bitwise, and the same row without offsets does not.
FLA memoizes its varlen index helpers (prepare_chunk_indices and
friends) on argument identity, with a four-entry queue. InnerGatedDeltaNet
received the offsets as a host tuple and rebuilt a CPU tensor from it in
every layer, so every call missed: the helpers were recomputed from
scratch, and each new entry evicted the ones the rest of the kernel
shares.

create_varlen_metadata_for_document already reads the offsets to the host
to materialize cu_seq_q_host, so keep that tensor as cu_seq_q_cpu and
pass it through. Every layer then hands FLA the same object and the
memoized helpers stay warm.

The two 'requires a CPU cu_seqlens tensor' guards go with it: the mask
builder always populates the field, and torch.where already rejects a
malformed one.

Profiled step 10 of qwen35_debugmodel, real training:

  CausalConv1dFunction   35.8 ms -> 16.3 ms
  ChunkGatedDeltaRule     26.4 ms ->  9.6 ms
  cudaStreamSynchronize   35 -> 25 calls
  aten::arange            124 -> 58 calls
  aten::narrow            142 -> 54 calls
  total cpu_op time      660.4 ms -> 399.0 ms

Conv call count is unchanged at 72, so nothing is skipped -- the removed
work is the index-table rebuild. Ten deterministic steps of
qwen35_debugmodel reproduce loss and grad_norm exactly.
@JavaZeroo
JavaZeroo force-pushed the kimi-k3-sample-packing branch from d12ac5f to b81093f Compare August 29, 2026 01:47
@JavaZeroo

Copy link
Copy Markdown
Contributor Author

@tianyu-l @shuhuayu All the review comments are addressed, and I verified the KDA varlen path end-to-end on a B200. Ready for another look whenever you have time.

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

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants