Skip to content

[Perf][GLM-5.3-Flash] Use FlashKDA for KDA chunked prefill (1.7-3.8x faster than the Triton chunk path) - #8

Closed
JaredforReal wants to merge 2 commits into
mainfrom
perf/glm53-flash-flashkda
Closed

JaredforReal wants to merge 2 commits into
mainfrom
perf/glm53-flash-flashkda

Conversation

@JaredforReal

@JaredforReal JaredforReal commented Sep 6, 2026

Copy link
Copy Markdown
Owner

One of three independent GLM-5.3-Flash perf PRs (#7: decode cleanups; this one: FlashKDA prefill; #9: masked-MHA prefill). Each is benchmarked on its own below.

Summary

Use FlashKDA (vllm._flashkda_C, already built and used by Kimi-K3) for GLM-5.3-Flash's KDA chunked prefill instead of the ~15-kernel Triton chunk_kda_with_fused_gate path.

FlashKDA implements the same bounded-gate KDA recurrence GLM-5.3-Flash uses (lower_bound * sigmoid(exp(A_log) * (g + dt_bias)), in-kernel q/k l2norm, raw beta logits), so it is a drop-in. On GB300 per KDA layer (H=16/rank, D=128, bench/flashkda_vs_chunk.py):

shape Triton chunk path FlashKDA
8 x 2048 tokens 183 us 49 us
4 x 8192 555 us 146 us
1 x 16384 1371 us 787 us

Selection: automatic when supported (SM90/SM10x/SM12x, bf16, head_dim 128, bounded gate); additional_config.kda_prefill_backend = triton keeps the old path (flashkda forces it). The output is written straight into the layer buffer (no merge copy) and the pre-sigmoid beta cast is not needed on this path. Spec-decode tokens themselves keep the Triton recurrent kernel (num_accepted_tokens rollback); in a step that carries both, FlashKDA runs the prefill segment into a workspace buffer and the outputs are scattered back by non_spec_token_indx / spec_token_indx (same scheme as the Kimi-K3 KDA layer; the merge writes straight into core_attn_out).

Performance of this PR alone

Measured on top of #7 (main + #7) vs the same + this PR, 4x GB300, TP4, --attention-backend FLASHINFER_MLA_SPARSE --max-model-len 69632 --max-num-seqs 256 --max-num-batched-tokens 16384, prefix caching disabled (hit rate 0.0% checked in the server log), vllm bench serve random dataset with warmups, back-to-back in the same session. #7 only touches decode, so this is the effect of FlashKDA on an otherwise-main prefill path. Decode tok/s is the steady-state window value; TPOT is the per-request median.

point metric +#7 +#7 +#8 delta
1k/512 c=1 decode tok/s 146 147 +0.7%
1k/512 c=1 median TPOT ms 6.46 6.47 +0.2%
1k/512 c=64 decode tok/s 3090 3132 +1.4%
1k/512 c=64 median TPOT ms 19.26 19.24 -0.1%
1k/512 c=256 decode tok/s 7367 7717 +4.7%
1k/512 c=256 median TPOT ms 32.87 31.30 -4.8%
prefill 8x2048 mean TTFT ms 449 395 -12.2%
prefill 2x8192 mean TTFT ms 380 330 -13.2%
prefill 8x32768 mean TTFT ms 5108 4693 -8.1%
prefill 2x65536 mean TTFT ms 2661 2450 -7.9%
32k/256 c=16 decode tok/s 296 316 +6.6%
32k/256 c=16 mean TTFT ms 3835 3642 -5.0%
32k/256 c=16 median TPOT ms 40.10 37.50 -6.5%

Prefill: −12-13% TTFT for 8x2048 / 2x8192 and −8% for 8x32k / 2x64k (per-KDA-layer speedup shrinks with sequence length, see the kernel table above). 32k-context serving (c=16): +6.6% decode tok/s, −6.5% TPOT, −5% TTFT, because each step still prefills the 34 KDA layers of new requests. At c=256 with 1k prompts the same effect gives +4.7% tok/s (new prompts' prefill is interleaved with decode); c=1 / c=64 decode is unchanged (±1%, within the noise floor in the last column of the ablation below).

With speculative decoding (MTP k=1)

Same build, same settings plus --speculative-config.method=mtp --speculative-config.num_speculative_tokens=1, kda_prefill_backend=triton vs flashkda, back-to-back. Random-dataset acceptance is ~1.2 tokens/step, so absolute decode numbers are low; the comparison is what matters. Whole-run throughput (the steady-state window accounting is off under spec decode).

point metric MTP k=1 + Triton KDA prefill (previous behaviour in spec steps) MTP k=1 + FlashKDA
1k/512 c=64 decode tok/s 2003 2130 (+6.3%)
1k/512 c=64 median TPOT ms 30.00 27.56 (-8.1%)
1k/512 c=256 decode tok/s 3697 4116 (+11.3%)
1k/512 c=256 median TPOT ms 67.96 60.53 (-10.9%)
prefill 8x2048 mean TTFT ms 464 423 (-8.8%)
prefill 2x8192 mean TTFT ms 390 360 (-7.7%)
32k/256 c=16 decode tok/s 260 284 (+9.5%)
32k/256 c=16 mean TTFT ms 2623 2479 (-5.5%)
32k/256 c=16 median TPOT ms 52.09 47.86 (-8.1%)

Accuracy under MTP: gsm8k 93.33 ± 0.69 (Triton) vs 93.56 ± 0.68 (FlashKDA); needle 34/36 on both, within the model's run-to-run spread. A debug hook (removed) confirmed the FlashKDA-in-spec-step path ran in mixed steps with 1, 1024 and 15349 non-spec tokens.

Ablation of the whole series

Cumulative, same session, same settings: main → +#7 → +#8 (this PR) → +#9 → main again.

point metric main +#7 +#7 +#8 +#7 +#8 +#9 main again
1k/512 c=1 decode tok/s 144 146 (+1.8%) 147 (+2.5%) 147 (+2.3%) 144 (+0.3%)
1k/512 c=1 median TPOT ms 6.57 6.46 (-1.7%) 6.47 (-1.5%) 6.49 (-1.2%) 6.57 (+0.0%)
1k/512 c=64 decode tok/s 2964 3090 (+4.3%) 3132 (+5.7%) 3133 (+5.7%) 2972 (+0.3%)
1k/512 c=64 median TPOT ms 20.02 19.26 (-3.8%) 19.24 (-3.9%) 19.09 (-4.6%) 20.02 (+0.0%)
1k/512 c=256 decode tok/s 7147 7367 (+3.1%) 7717 (+8.0%) 7755 (+8.5%) 7180 (+0.5%)
1k/512 c=256 median TPOT ms 33.87 32.87 (-3.0%) 31.30 (-7.6%) 31.29 (-7.6%) 33.77 (-0.3%)
prefill 8x2048 mean TTFT ms 461 449 (-2.5%) 395 (-14.4%) 361 (-21.7%) 453 (-1.8%)
prefill 2x8192 mean TTFT ms 389 380 (-2.4%) 330 (-15.3%) 315 (-19.2%) 374 (-3.8%)
prefill 8x32768 mean TTFT ms 5258 5108 (-2.8%) 4693 (-10.7%) 4347 (-17.3%) 5258 (+0.0%)
prefill 2x65536 mean TTFT ms 2738 2661 (-2.8%) 2450 (-10.5%) 2365 (-13.6%) 2738 (-0.0%)
32k/256 c=16 decode tok/s 287 296 (+3.1%) 316 (+9.8%) 322 (+11.9%) 288 (+0.2%)
32k/256 c=16 mean TTFT ms 3883 3835 (-1.2%) 3642 (-6.2%) 3482 (-10.3%) 3880 (-0.1%)
32k/256 c=16 median TPOT ms 41.34 40.10 (-3.0%) 37.50 (-9.3%) 36.97 (-10.6%) 41.29 (-0.1%)

Accuracy (per build, same session)

Each rung of the ablation was also evaluated on its own: gsm8k (1319 questions, 5-shot, greedy, lm_eval local-completions), prompt-logprob agreement on real 4k/12k/30k/60k prompts (mean |Δ logprob| per token vs the main build; the "main again" row is the run-to-run noise floor of this FP8 model), and a needle-in-a-haystack retrieval set (12 codes per length at 6k/16k/30k tokens, greedy, reasoning_effort=low), which exercises the 2k-36k prefill range where #9 switches kernels.

| build | gsm8k flexible-extract % | gsm8k strict-match % | prompt-logprob mean|Δ| vs main (4k/12k/30k/60k) | next token | needle hits (6k/16k/30k) | needle, 3 more passes (concurrency 6) |
|---|---|---|---|---|---|---|
| main | 93.33 ± 0.69 | 93.10 ± 0.70 | (reference) | | 12/12 / 12/12 / 12/12 | 34/36 / 36/36 / 35/36 |
| +#7 | 93.03 ± 0.70 | 92.95 ± 0.71 | 0.040 / 0.055 / 0.051 / 0.108 | all same | 12/12 / 12/12 / 11/12 | 35/36 / 36/36 / 34/36 |
| +#7 +#8 | 93.25 ± 0.69 | 93.18 ± 0.69 | 0.099 / 0.072 / 0.060 / 0.136 | all same | 12/12 / 11/12 / 11/12 | - |
| +#7 +#8 +#9 | 93.10 ± 0.70 | 93.10 ± 0.70 | 0.099 / 0.072 / 0.060 / 0.127 | all same | 12/12 / 12/12 / 11/12 | 35/36 / 34/36 / 36/36 |
| main again | 92.87 ± 0.71 | 92.87 ± 0.71 | 0.041 / 0.055 / 0.051 / 0.118 | all same | 12/12 / 12/12 / 12/12 | - |

Duplicate-work check

gh pr list --repo vllm-project/vllm --state open --search "GLM-5.3-Flash FlashKDA" / "glm5next KDA": no open PR wires FlashKDA for GLM-5.3-Flash (vllm-project#55224 is the KDA conv weight refit fix).

Tests

  • bench/kda_numerics.py: FlashKDA vs the naive recurrent reference (from tests/models/kimi_k3/test_kda.py) mean|Δ| 2e-5, max 4.8e-4 on outputs, same as the Triton chunk path vs the reference (T=48, 2 sequences, lower_bound -5). Note the Triton chunk path writes its output into v's buffer, so the comparison clones v.
  • pytest tests/models/kimi_k3/test_kda.py -k flashkda: 2 passed (extension sanity).
  • e2e: chat sanity; prompt-logprob A/B vs main within ~1-2.5x the main-vs-main noise floor with identical next tokens; gsm8k 1319 questions (5-shot, greedy): main 93.03% ± 0.70, full series 92.65% ± 0.72 (measured on the three PRs together).
  • pre-commit run ruff-check / ruff-format: passed.

Notes for review

  • Uses torch.ops._flashkda_C.fwd directly (no cross-model import from kimi_k3); the support predicate mirrors kimi_k3.nvidia.kda.is_flashkda_supported.
  • FlashKDA hardcodes dense q/k/v strides, so the three .contiguous() copies of the conv-output slices remain (~115 us/layer per 16k chunk); a follow-up could run the short conv per q/k/v into separate buffers.
  • AI assistance: developed with Claude Fable 5.1 and reviewed by the submitter; full analysis in ~/notes/glm53flash-perf/REPORT.md, ablation logs in ~/notes/glm53flash-perf/logs/e2e/ablate_*.

FlashKDA (already built as vllm._flashkda_C for Kimi-K3) implements the
same bounded-gate KDA recurrence as chunk_kda_with_fused_gate
(lower_bound * sigmoid(exp(A_log) * (g + dt_bias)), in-kernel q/k l2norm,
raw beta logits). On GB300 it replaces ~15 Triton kernels with 4 and is
1.7-3.8x faster (T=2048x8: 183->49 us, 8192x4: 555->146 us, 16384x1:
1371->787 us per layer). Select it automatically when supported
(SM90/SM10x/SM12x, bf16, head_dim 128, bounded gate);
additional_config.kda_prefill_backend = triton keeps the old path. The
output is written straight into the layer buffer and the pre-sigmoid beta
cast is no longer needed on this path.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jared Wen <jaredwen@inferact.ai>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Auto-selecting flashkda can crash on CUDA builds where the optional vllm._flashkda_C extension is not present, instead of cleanly falling back to the Triton path.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR wires the optional FlashKDA CUDA extension (vllm._flashkda_C) into the GLM-5.3-Flash KDA chunked prefill path to replace the existing Triton chunked kernels when supported, aiming to reduce prefill latency.

Changes:

  • Add backend selection logic (auto / triton / flashkda) for KDA chunked prefill, based on device capability, dtype, head_dim, and bounded-gate configuration.
  • Add a FlashKDA prefill implementation that uses the v1 workspace manager for scratch buffers and can write directly into the layer output buffer when not in spec-decode.
File summaries
File Description
vllm/models/glm5next/nvidia/kda.py Adds FlashKDA-backed chunked-prefill path and backend selection via additional_config.kda_prefill_backend.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +127 to +150
def _resolve_kda_prefill_backend(
backend: str, head_dim: int, dtype: torch.dtype, lower_bound: float | None
) -> str:
"""Pick the chunked-prefill kernel: FlashKDA (fused CUDA, ~2-4x faster on
SM90/SM10x/SM12x for bf16, head_dim 128 and a bounded gate) or the Triton
``chunk_kda_with_fused_gate`` path. ``backend`` comes from
``additional_config.kda_prefill_backend`` (auto / triton / flashkda)."""
if backend not in ("auto", "triton", "flashkda"):
raise ValueError(f"Unsupported KDA prefill backend: {backend}")
capability = current_platform.get_device_capability()
supported = (
current_platform.is_cuda()
and capability is not None
and capability.major in (9, 10, 12)
and head_dim == 128
and dtype == torch.bfloat16
and lower_bound is not None
)
if backend == "flashkda" and not supported:
raise RuntimeError(
"FlashKDA requires CUDA SM90/SM10x/SM12x, bfloat16, head_dim=128 "
"and a bounded KDA gate."
)
return "flashkda" if supported and backend != "triton" else "triton"
@JaredforReal

Copy link
Copy Markdown
Owner Author

Upstream draft: vllm-project#55737

…code steps too

The FlashKDA path was gated on `not use_spec` so it could write straight
into the layer output buffer; a step that also carried spec-decode tokens
fell back to the Triton chunk path for its whole prefill segment, which with
MTP enabled is almost every step that has a prefill. Follow the Kimi-K3 KDA
layer instead: in a spec step FlashKDA writes to a workspace buffer and the
non-spec tokens are scattered by non_spec_token_indx, alongside the spec
tokens (which keep the recurrent kernel with num_accepted_tokens rollback).
The merge now index_copy_s directly into core_attn_out instead of going
through a temporary tensor.

MTP k=1, TP4 on 4x GB300, same build, Triton -> FlashKDA: 8x2048 TTFT
464 -> 423 ms, 2x8192 390 -> 360 ms, 32k/256 c=16 260 -> 284 tok/s,
1k/512 c=64 2003 -> 2130 tok/s; gsm8k 93.33 vs 93.56 (+-0.7).

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jared Wen <jaredwen@inferact.ai>
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.

2 participants