Skip to content

feat(kernels): port standalone Kimi K3 kernels - #32890

Merged
BBuf merged 45 commits into
sgl-project:mainfrom
BBuf:bbuf/kimi-k3-standalone-kernels
Aug 1, 2026
Merged

BBuf merged 45 commits into
sgl-project:mainfrom
BBuf:bbuf/kimi-k3-standalone-kernels

Conversation

@BBuf

@BBuf BBuf commented Jul 30, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

Port the standalone kernels from the Kimi K3 Day0 work to main first. Keeping
the kernels and their direct tests separate makes the reusable pieces reviewable
before the more invasive model, scheduler, and serving integration changes.

Modifications

  • Add the shared JIT/TMA support and generic prerequisite kernels used by Kimi
    K3.
  • Port the standalone KDA kernels, including ReplaySSM, packed decode, and the
    NVIDIA/PTX prefill implementations.
  • Port the Kimi K3 compute kernels for SiTU, quantization, MLA output gating,
    attention residuals, MTP, and image preprocessing.
  • Port the multi-GPU fused all-reduce, GEMM + all-gather, GEMM + all-reduce, and
    sequence-parallel collective kernels.
  • Keep four hardware-grouped registered test entrypoints for H100 prerequisite,
    B200 compute, B200 collective, and Blackwell prefill correctness.
  • Remove tuner/benchmark scaffolding and Day0 runtime dispatch/integration
    changes; keep model, scheduler, speculative decoding, multimodal, and serving
    runtime integration out of this PR.

Accuracy Tests

  • pre-commit run --all-files
  • git diff --check
  • python3 -m compileall -q python/sglang/kernels test/registered/kernels test/registered/jit
  • After consolidation, all 9 retained H100 tests passed in 12.91s. The suite
    covers MLA BF16/FP8 scatter and concat, generic prerequisite kernels, and the
    representative ReplaySSM ring parity path.
  • The 13 Blackwell-only test methods imported successfully on H100 and skipped
    through their architecture guards.
  • The SM100/SM103-specific KDA, compute, and collective tests are registered for
    the B200/GB300 CI jobs. They were not run locally because no Blackwell machine
    was reachable; H100 results are not presented as Blackwell validation.

Speed Tests and Profiling

Not applicable to this extraction. It does not enable the Kimi K3 serving path,
and no benchmark files are added. Performance validation belongs with the
follow-up runtime integration after the Blackwell kernel jobs pass.

Checklist

Review and Merge Process

  1. Ping Merge Oncalls to start the process. See the PR Merge Process.
  2. Get approvals from CODEOWNERS and other reviewers.
  3. Run the registered B200 and GB300 kernel jobs to complete the
    architecture-specific acceptance matrix.

CI States

Latest PR Test (Base): ❌ Run #30679474553
Latest PR Test (Extra): ✅ Run #30679474497

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-actions github-actions Bot added documentation Improvements or additions to documentation quant LLM Quantization Multi-modal multi-modal language model jit-kernel labels Jul 30, 2026
Comment thread python/sglang/kernels/jit/csrc/kimi_k3/situ_and_mul.cuh
Comment thread python/sglang/kernels/jit/csrc/moe/align_single_token.cuh Outdated
Comment thread python/sglang/kernels/jit/include/sgl_kernel/warp.cuh Outdated
Comment thread python/sglang/kernels/jit/csrc/kimi_k3/comm/gemm_ar.cuh Outdated
Comment thread python/sglang/kernels/jit/csrc/elementwise/set_mla_kv_concat_q.cuh Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How does this compile? Should we move this to AOT kernel first?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It builds through torch.utils.cpp_extension.load (ninja), not the sglang JIT — the only kernel under kernels/jit/ that does, cached separately in ~/.cache/torch_extensions.

Measured on B300: 109.2 s cold, 5.0 s warm, and the .so carries only sm_103a, so B200/GB200 get no kernel image although the cookbook lists B200. Agreed on AOT.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this already moved to AOT? We can defer the migration of this kernel? I remember torch.utils.cpp_extension.load is not concurrent-safe?

Comment on lines +26 to +27
def is_flash_attention_v4_available() -> bool:
return _flash_attn_varlen_func is not None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this related to this PR?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

You are right to ask. The file is byte-identical to main so it looks unrelated, but this PR is meant to export all the kimi-k3 kernel changes and is_flash_attention_v4_available() was missing. Restored.

It also prompted a file-by-file audit against kimi-k3, which found more lossy exports — dropped PDL in pack_topk_ids and concat_mla, the radix-select fast path (3.1-3.5x) and the num_warps heuristic in moe_fused_gate, v2p_ptr/PAGE_MULT in kv_indices (this one hard-crashes the K3 runtime), HEAD_DIM masking in verify_splitkv, the stride guard in kda_blackwell/kernel_h, do_not_specialize in gpu_tensor_hash, and the ROCm --use_fast_math strip in jit/utils/compile.py. All restored; the kernel tree is now byte-identical to kimi-k3 apart from the fixes in this review.

@BBuf

BBuf commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

Validation of 22cb347d47

Numbers in that commit message were taken mid-way through the restore, before the last batch of lossy exports was put back. Here is the full set, and it supersedes them.

Setup. 8× B300 (sm_103) TP8 single node, kimi-k3 runtime running against this PR's kernels, flashinfer_mxfp4 via the 0617 SiTU cubin pool, non-spec, no --enable-symm-mem, cookbook B300 low-latency recipe. ~/.cache/sglang and ~/.cache/tvm-ffi cleared before every run so nothing is served from a pre-PR artifact. bench_serving random, range-ratio 1.0, num_prompts = 5 × conc, --flush-cache per point. Baseline is the same box and recipe running kimi-k3's own kernels.

bs=1

baseline this PR
8192/1024 output 112.07 tok/s 112.16
8192/1024 TTFT 369.58 ms 369.20
8192/1024 TPOT 8.57 ms 8.56
1024/1024 output 114.61 tok/s 114.77
1024/1024 TPOT 8.49 ms 8.49

The 8k TTFT match matters on its own: that figure is the 8192-token prefill, and the review fixes in this PR touch the MLA KV write path (copy_bytes, elect.sync), so a prefill regression there would show up here. It does not.

Throughput point, 8192/1024 concurrency 24 (the SLO-optimal cell for this recipe). First reading was TTFT 5649 ms, which did not reproduce — it was the first load after a cold JIT cache. Three repeats:

baseline rep 1 rep 2 rep 3
output tok/s 843.14 858.38 858.03 857.76
total tok/s 7588.22 7725.38 7722.28 7719.85
mean TTFT 4606 ms 4748 4755 4759
median TTFT — 4761 4776 4772
P99 TTFT — 7933 7945 7956
mean TPOT 22.87 ms 23.34 23.34 23.35

Repeats agree within 0.1%. Steady state is +1.7% throughput, TTFT +3.3%, TPOT +2.1%. Worth noting the shape: theoretical output is roughly conc/TPOT, so baseline 24/22.87 = 1049 against 843 measured is 80.4% utilisation, while 24/23.35 = 1028 against 858 is 83.5%. Slightly slower per step, meaningfully less pipeline idle — the direction you would expect from PDL being back.

Accuracy. GSM8K 200q, three runs: 0.995 / 0.995 / 0.995, invalid 0.005 each. Baseline 0.990 — one question out of 200.

One process note. The checklist currently says "Speed Tests and Profiling: Not applicable to this extraction". The v2p_ptr removal argues against that for kernel-export PRs: it passes review by diff and passes a compile gate, because it is a Triton runtime kwargs check, and only surfaces when the ported kernels run one decode step under the real runtime. That single run is what turned up the other twelve regressions too.

@BBuf
BBuf force-pushed the bbuf/kimi-k3-standalone-kernels branch from 22cb347 to ca065ce Compare July 30, 2026 14:40
@BBuf
BBuf marked this pull request as ready for review July 31, 2026 03:20
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@BBuf
BBuf requested review from HydraQYH, celve and yuan-luo as code owners July 31, 2026 03:20
@BBuf

BBuf commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed b78f2aab53 (consolidation, rebased onto 7009efd7ec). Validated end to end on 8×B300; details below, including a correction to my earlier reply about align_single_token — I said we would drop it, and that was wrong.

Correcting the align_single_token reply

I removed it, then put it back. The reasoning that justified removing it is worth writing down because it applies to every file in this PR.

The argument was: its only caller is fused_marlin_moe.py, that file is not in this PR, so the kernel is unreachable here. The premise is true and the conclusion does not follow. This PR exists so kimi-k3 can drop its own kernel copies and use main's, so the question is not "who calls it inside this diff" but "who calls it on the branch this export serves". There, the caller is live:

# kimi-k3: srt/layers/moe/fused_moe_triton/fused_marlin_moe.py:244
if M == 1 and topk <= 32 and expert_map is None:
    from sglang.kernels.ops.moe.moe_align_single_token import moe_align_single_token

Three properties make removal a hard failure rather than a slow path:

  • the import is inside the branch, so a missing module raises ImportError at request time instead of falling through to the moe_align_block_size call in the else;
  • the guard is M == 1, i.e. single-token decode — the case this kernel exists to speed up (one single-warp sort instead of the align + count_and_sort pair) and the case bs=1 numbers measure;
  • Marlin is not exotic: with --moe-runner-backend at auto it is what gets picked whenever the SiTU cubin pool is absent, which is the default on a fresh box.

The file, its Python wrapper and its test case are all restored. That makes this commit three consolidations, not four.

While checking this I also swept the rest of the export. Of the files kimi-k3 changes under python/sglang/kernels/, exactly one is now not carried here: distributed/communicator.cuh. That one is deliberate — the PR supplies the same five system-scope PTX helpers from kimi_k3/comm/ptx_sys.cuh in the same device::distributed namespace, and the two helpers kimi-k3 additionally defines there (load_relaxed_sys, red_add_release_sys) are referenced by nothing in this PR. Its Semaphore refactor is cosmetic: same asm, immediate folded to a register operand.

What was measured, and on what

Running the PR branch by itself proves nothing — that tree is main plus kernels, and main has no K3 model support. So both arms are a composite: kimi-k3 @ 578edb240, with every kernel file it changes reverted to the merge-base, then this PR's kernels laid on top. Reverting matters as much as overlaying: leave kimi-k3's communicator.cuh in place and its helpers collide with ptx_sys.cuh's.

Scope is python/sglang/kernels/ only. The PR's two other source files (environ.py, custom_all_reduce_v2.py) are deliberately not overlaid — both were exported from kimi-k3, so it already holds them and additionally holds things this PR does not carry (SGLANG_ENABLE_CUSTOM_ALL_REDUCE_V2_MULTINODE, read by parallel_state.py:2001). Overlaying them replaces a superset with a subset.

Arms are one commit apart, so the delta isolates the consolidation:

017c367079 (before) f79e7b8327 (after)
bs=1 output tok/s 105.42 106.38
bs=1 TPOT 8.47 ms 8.47 ms
bs=1 TTFT p50 261.01 ms 239.31 ms
C=24 output tok/s 1000.94 1001.36
C=24 TPOT 19.66 ms 19.71 ms
GSM8K (200q) 0.995 0.995

8×B300 SXM6, TP8, --mem-fraction-static 0.85, --mamba-full-memory-ratio 7.21, MoE flashinfer_mxfp4 via the 0617 SiTU pool, attention auto-resolved to trtllm_mla. Both arms booted to the identical pool split (max_mamba_cache_size=476, max_total_num_tokens=136128), and random-range-ratio 1.0 fixes in=1024 / out=256 exactly.

TPOT is the indicative number here: it excludes TTFT, so it is the part shared activation / radix / mbarrier code could actually move, and it is flat in both regimes.

One honest note on the first pass: the after-arm initially measured 98.70 bs=1 tok/s, a 6.4% deficit. That was cold JIT — the consolidation changes source hashes, so its modules had to build while the before-arm hit an already-populated ~/.cache/tvm-ffi. The tell was that only mean/p99 TTFT moved (438/1679 ms vs 266/305) while median TTFT was already better and TPOT was unchanged; the warmup pass in the same run showed p99 5436 ms. Re-running the after-arm against a warm cache gives the 106.38 above. Reporting it because "re-ran it and it got better" deserves the mechanism attached.

Covering the path the main run cannot reach

flashinfer_mxfp4 never calls moe_align_single_token, so none of the above would have caught the removal. Separate arm, system interpreter (flashinfer 0.6.16rc5), no pool, --moe-runner-backend marlin:

  • single-request generation correct (17 × 23 → 391);
  • GSM8K 100q: accuracy 1.000, invalid 0.000;
  • and the load is verified rather than inferred — moe_align_single_token…so is mapped in every scheduler process (/proc/<pid>/maps), which only happens via that if M == 1 import.

Verification of the consolidation itself

Each of the three is a move of existing text, not a retype, and two are checked bit-exactly:

  • SiTU activation (kimi_k3/situ_activation.cuh) — bit-identical across three shapes covering both kHasLinearBeta branches.
  • Radix primitives (moe/radix_select_common.cuh) — bit-identical across 16 tensors (ids and weights × four token counts × radix fast path and fused gate). The first attempt here was a retype and it silently changed every selected expert id while leaving the weights identical; that is why these are lifted verbatim now.
  • mbarrier wrappers (sgl_kernel/mbarrier.cuh) — same global namespace ptx both consumers already open, so no call site changes.

All ten touched JIT modules cold-compile (artifacts confirmed newly built in ~/.cache/tvm-ffi, not cache hits).

One caveat on comparability

These runs are TP8 without DCP, so the absolute numbers are not comparable to the cookbook's B300 TP8+DCP8 cell. That is a branch-side version knot, not something this PR causes: cutedsl_mla_backend.py:329 passes enable_dcp=True, a kwarg flashinfer 0.6.15.post1 (the pyproject.toml pin) does not have, while 0.6.16rc5 cannot use the 0617 SiTU pool because the pool's overlay/include shadows the wheel's KernelRunner.h (TrtllmGenBatchedGemmRunnerOptions has no member perTokenSfDtype). Of the two reachable configurations I picked the one that exercises what this commit touches — the SiTU kernels are on the flashinfer_mxfp4 path, not the Marlin one. Both arms ran identically, so the A/B stands.

@BBuf
BBuf force-pushed the bbuf/kimi-k3-standalone-kernels branch from cc58c92 to b87597b Compare July 31, 2026 12:29
@BBuf BBuf mentioned this pull request Jul 31, 2026
5 tasks done
@BBuf
BBuf merged commit fb207b7 into sgl-project:main Aug 1, 2026
223 of 250 checks passed
@BBuf BBuf mentioned this pull request Aug 1, 2026
BBuf added a commit that referenced this pull request Aug 1, 2026
CI load: the branch registered ~1540s of new test time, almost all of it on GPU
runners. This removes 18 files / 1218s and leaves test_ar_fusion.py as the only
addition on 8-gpu-h200.

What made most of it safe is that #32890 already landed consolidated coverage on
main for the same ops:

  test_kimi_k3_prerequisite_ops.py  add3, tiny_gemm, route_radix,
    route_quant_fused, vision_rope, concat_q bf16+fp8, set_mla_kv_buffer,
    concat_mla, topk_sum, pack_topk_ids, per_token_group_quant, image,
    fused_sigmoid_gating, kda_replayssm ring fold, top_p_renorm
  kimi_k3/test_collectives.py       all_reduce_push, gemm_all_gather,
    gemm_all_reduce, sequence_parallel, attention_residual_direct_all_gather
  kimi_k3/test_compute.py           situ_and_mul, situ_mul_quant, moe_front,
    mla_output_gate, attn_residual_and_prefix_write
  attention/test_kda_prefill.py, attention/test_kda_fused_decode.py

Fourteen of the eighteen fall into that bucket: the op keeps a test, it just runs
once on main's file instead of twice.

Kept: test_ar_fusion.py. main's test_collectives has a single all_reduce_push
case; this file has ten, covering pull 2-shot, both tuning grids, norm and
finalize variants, mixed stress and graph capture. Dropping it would have been
the one deletion that silently thins the most failure-prone subsystem here.

Four deletions are a real loss of coverage with nothing on main behind them, and
they are called out rather than folded into the count:

  test_symm_buffers.py     symmetric-buffer invariants -- created once, offsets
                           uniform across ranks, offsets survive rank-asymmetric
                           churn, slices aligned and the two buffers disjoint.
                           These are bookkeeping assertions of the kind that
                           catch "someone extended X without updating Y".
  test_trtllm_gen_moe.py   trtllm-gen MoE against the fork kernel.
  test_attn_res_aggregate_stream.py  the aggregate-stream attn-res path.
  test_kda_mtp_cutedsl_replayssm_ring.py  nightly-only, so it was not part of the
                           per-commit cost in the first place.

The first is the one I would argue hardest to keep if anyone wants to put one
back: multi-rank buffer layout has no other test, and its failure mode is silent
corruption rather than a crash.

Deleted files have no dangling references; run_suite.py discovers tests by AST
scan, so nothing enumerates them by name.
saturn-acc pushed a commit to saturn-acc/sglang that referenced this pull request Aug 16, 2026
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: hnyls2002 <lsyincs@gmail.com>
Co-authored-by: zhangxiaolei <zhangxiaolei.666@bytedance.com>
Atituiset pushed a commit to Atituiset/sglang that referenced this pull request Sep 10, 2026
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: hnyls2002 <lsyincs@gmail.com>
Co-authored-by: zhangxiaolei <zhangxiaolei.666@bytedance.com>
yyihuang added a commit to flashinfer-ai/flashinfer that referenced this pull request Sep 26, 2026
…shortcut for the mid and large Kimi-K3 fused router batches (SM100/SM103) (#5568)

## Kimi-K3 fused MoE router: split cooperative joins and a warp-level
round-0 shortcut for the mid and large batches (SM100 / SM103)

Follow-up to #5531 / #5548 / #5564 (same source family; regenerated
programs only, no host change). Same K3 semantics (sigmoid gate, top-16
on `sigmoid(logit) + bias`, renormalized unbiased weights, lower expert
id on ties) and the same expert-aligned route plan for `block_m` 8/16;
the public entries `flashinfer.fused_moe.kimi_k3_fused_router` /
`prepare_kimi_k3_fused_router`, the route table and the arm set are
unchanged. The exporter regenerates all 14 programs per arch; for the 8
small-batch programs (`L`, `LC2`, `LC4`, `LC8` x `block_m` 8/16) the
kernel and binding sources are identical to #5564 up to the module
identifier (the module name is a content fingerprint that also covers
exporter-side build metadata), so only the 6 mid/large programs per arch
carry code changes.

### What changed
The 12 routed shapes with `num_tokens` 256 … 8192 (`block_m` 8/16) get
new kernel code (arms `M`, `Q4S`, `GW`); the 16 small-batch shapes keep
their kernel code.

- **Split cooperative join in the mid-batch arms (`M`, `Q4S`;
`num_tokens` 256 … 2048).** The single grid-wide join between expert
selection and route-plan build becomes an arrive / wait pair
(`cooperative_groups::grid_group::barrier_arrive` / `barrier_wait`):
every CTA arrives right after its last selection row, only the CTAs that
own plan-build work wait, the others retire instead of spinning in
`this_grid().sync()`.
- **Split first join in the large-batch arm (`GW`; `num_tokens` 4096 /
8192).** The join that publishes the zeroed expert counts is split into
an arrive after the grid-strided clear and one wait between a CTA's
first-row selection and its first ticket atomic, so the slowest CTA's
clear overlaps one row of selection. Every CTA arrives and waits exactly
once per round (a CTA without rows waits once after its empty loop).
- **Fence-free barrier sites in `GW`.** The three per-thread
device-scope fences in front of the GW grid barriers are dropped:
`__syncthreads()` followed by the barrier's own device-scope release
already orders the preceding stores (the same reasoning as #5548,
previously applied only to the mid-batch arms).
- **Round-0 max-bin shortcut in the `GW` radix selection.** The top byte
of the ordered key is sign + 7 exponent bits, so a row's 896 keys
concentrate in one or two round-0 histogram bins and the 28 conditional
shared-memory atomics per lane serialized on one address. Round 0 now
takes the maximum top byte and its population from two warp reductions
(`redux.sync`); when that bin holds at least 16 keys it is exactly the
bin the histogram round selects (nothing lies above it) and the state
update is identical, otherwise the unchanged histogram round runs.
Rounds 1-3 are unchanged, so the selected experts and their order are
bit-identical.

Source-side paired cold-L2 CUPTI A/B against the #5564 programs (same
session, same node, two runs per arch on different GPUs; `previous /
new` kernel time):

| row | B200 run 1 / run 2 | GB300 run 1 / run 2 |
|---|---|---|
| m256 bm8 / bm16 | 1.027 / 1.028, 1.027 / 1.036 | 1.016 / 1.021, 1.012
/ 1.034 |
| m512 bm8 / bm16 | 1.057 / 1.057, 1.075 / 1.051 | 1.075 / 1.079, 1.071
/ 1.074 |
| m1024 bm8 / bm16 | 1.067 / 1.062, 1.041 / 1.058 | 1.050 / 1.058, 1.047
/ 1.057 |
| m2048 bm8 / bm16 | 1.028 / 1.023, 1.011 / 1.026 | 1.035 / 1.056, 1.039
/ 1.058 |
| m4096 bm8 / bm16 | 1.093 / 1.091, 1.092 / 1.082 | 1.088 / 1.092, 1.087
/ 1.094 |
| m8192 bm8 / bm16 | 1.093 / 1.092, 1.094 / 1.090 | 1.083 / 1.077, 1.083
/ 1.079 |
| geomean (12 rows) | 1.060, 1.057 | 1.060, 1.061 |

Every changed row is faster than #5564 in every run on both
architectures.

### Exactness
Source-side gates on both arches: exact43 43/43, affected slice +
registered e2e, compute-sanitizer synccheck and racecheck (separate
runs, 8 frozen shapes + all 28 routed rows): `ERROR SUMMARY: 0 errors` /
`RACECHECK SUMMARY: 0 hazards` on every run. Export receipts (source <->
export bitwise parity on all 28 shapes per arch, generated grid ==
source grid): B200 (sm_100a) 28/28 rows correct, GB300 (sm_103a) 28/28
rows correct; the delivered patch produced from the two architectures is
byte-identical (SHA-256
`eb051e8e130f00c2dffa69f1095fdcc6fdb70a97c55f9d5425e49cda65fe4c22`).
`tests/experimental/test_cake_kimi_k3_fused_router.py`: 44 passed on
both arches. Pre-commit (ruff, mypy, clang-format opt-out for the
generated `csrc/`) clean.

### Benchmark
`benchmarks/bench_cake_kimi_k3_fused_router.py --cupti --rounds 5`
(interleaved A/B rounds, CUPTI kernel time with a cold L2) against
SGLang `route_radix` + `moe_align_block_size` (pinned SGLang revision
83bd2c47, kernel-only):

| row | arm (B200 / GB300) | B200 fused us | B200 SGLang us | B200
speedup | GB300 fused us | GB300 SGLang us | GB300 speedup |
|---|---|---:|---:|---:|---:|---:|---:|
| m1_bm8 | L / L | 5.79 | 9.31 | **1.608** | 5.54 | 12.64 | **2.283** |
| m2_bm8 | LC / LC | 5.28 | 9.63 | **1.825** | 5.22 | 12.64 | **2.423**
|
| m4_bm8 | LC / LC | 5.92 | 9.50 | **1.606** | 5.60 | 12.58 | **2.246**
|
| m8_bm8 | LC / LC | 5.95 | 9.73 | **1.635** | 5.76 | 12.45 | **2.161**
|
| m16_bm8 | L / L | 6.14 | 9.57 | **1.557** | 6.08 | 12.83 | **2.111** |
| m32_bm8 | L / L | 6.27 | 9.79 | **1.561** | 6.21 | 12.93 | **2.082** |
| m64_bm8 | L / L | 7.01 | 10.02 | **1.429** | 6.66 | 12.99 | **1.952**
|
| m128_bm8 | L / L | 8.06 | 10.40 | **1.290** | 7.68 | 13.18 | **1.717**
|
| m256_bm8 | M / M | 8.16 | 10.94 | **1.341** | 7.97 | 14.14 | **1.775**
|
| m512_bm8 | Q4S / Q4S | 9.89 | 12.96 | **1.311** | 9.47 | 15.33 |
**1.618** |
| m1024_bm8 | Q4S / Q4S | 13.70 | 18.34 | **1.339** | 12.80 | 18.53 |
**1.447** |
| m2048_bm8 | Q4S / Q4S | 20.74 | 28.19 | **1.360** | 19.20 | 26.78 |
**1.395** |
| m4096_bm8 | GW / GW | 28.35 | 47.52 | **1.676** | 26.75 | 45.25 |
**1.691** |
| m8192_bm8 | GW / GW | 43.33 | 87.20 | **2.013** | 40.90 | 82.08 |
**2.007** |
| m1_bm16 | L / L | 6.46 | 9.85 | **1.525** | 6.05 | 12.51 | **2.069** |
| m2_bm16 | LC / LC | 5.79 | 9.50 | **1.641** | 5.54 | 12.70 | **2.295**
|
| m4_bm16 | LC / LC | 5.60 | 9.89 | **1.766** | 5.50 | 12.74 | **2.314**
|
| m8_bm16 | LC / LC | 5.70 | 9.79 | **1.719** | 5.79 | 13.12 | **2.265**
|
| m16_bm16 | L / L | 5.86 | 9.98 | **1.705** | 6.05 | 12.93 | **2.138**
|
| m32_bm16 | L / L | 6.43 | 9.95 | **1.547** | 6.11 | 13.18 | **2.157**
|
| m64_bm16 | L / L | 7.14 | 10.05 | **1.408** | 6.91 | 13.09 | **1.894**
|
| m128_bm16 | L / L | 8.26 | 10.40 | **1.260** | 7.81 | 13.60 |
**1.742** |
| m256_bm16 | M / M | 8.22 | 11.33 | **1.377** | 7.97 | 13.89 |
**1.743** |
| m512_bm16 | Q4S / Q4S | 9.95 | 12.54 | **1.260** | 9.34 | 15.46 |
**1.654** |
| m1024_bm16 | Q4S / Q4S | 13.38 | 17.76 | **1.328** | 12.80 | 18.53 |
**1.447** |
| m2048_bm16 | Q4S / Q4S | 20.70 | 26.66 | **1.287** | 19.14 | 25.82 |
**1.349** |
| m4096_bm16 | GW / GW | 28.74 | 44.89 | **1.562** | 27.46 | 43.20 |
**1.573** |
| m8192_bm16 | GW / GW | 43.45 | 81.60 | **1.878** | 41.63 | 77.79 |
**1.869** |

Geomean speedup vs SGLang: **NVIDIA B200 1.516** (min `1.260`, `28`/28
rows > 1.0), **NVIDIA GB300 1.882** (min `1.349`, `28`/28 rows > 1.0).
#5564 on the same benchmark: B200 1.492 (min 1.220), GB300 1.873 (min
1.319).

### Baselines and their source PRs
- **Previous programs (A/B "previous" arm and the geomean reference):**
#5564 (merge 78c6e1f, 2026-09-26), on top of #5548 (c894711,
2026-09-25) and #5531 (afad701, 2026-09-25). Files:
`flashinfer/experimental/kimi_k3_fused_router/{cake_backend.py,
cake_jit.py, csrc/cake_kimi_k3_fused_router/sm_10{0,3}a/*}`,
`flashinfer/fused_moe/kimi_k3_fused_router.py`,
`benchmarks/bench_cake_kimi_k3_fused_router.py`,
`tests/experimental/test_cake_kimi_k3_fused_router.py`. This branch's
merge-base with `main` is 78c6e1f (= #5564's merge commit); `git log
78c6e1f..origin/main -- <those files>` is empty, so no upstream commit
after the merge-base touched them.
- **SGLang reference (benchmark denominator and the test oracle's
semantics):** `sglang.kernels.ops.moe.moe_route_radix.route_radix` +
`sglang.kernels.ops.moe.moe_align.moe_align_block_size` at the pinned
SGLang revision 83bd2c473f173404216f6d9851ad7a95c1e3c565, imported
kernel-only (no SGLang runtime); `moe_route_radix.py` last changed by
sgl-project/sglang#32890 (fb207b72b, 2026-08-01, "port standalone Kimi
K3 kernels") and `moe_align.py` by sgl-project/sglang#32045 (74338e94f,
2026-07-22), both before the pinned revision; the pin is unchanged since
#5531.
- **Test oracle:** the PyTorch reference in
`tests/experimental/test_cake_kimi_k3_fused_router.py` from #5531
(unchanged since).

### Notes
- No change to `cake_backend.py`; `cake_jit.py` only changes the 14
module records per arch (module names follow the generated program
fingerprint). Same-session controls on the 16 unchanged small rows:
geomean 0.994 (B200) / 1.006 (GB300) with both arms loading the same
module, so the small rows are unchanged within noise.
- Shapes outside the routed set still raise `NotImplementedError`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Performance**
* Improved fused routing efficiency across supported GPU architectures
and workloads, including faster top-k selection and more targeted
synchronization.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Yingyi Huang <averyh@nvidia.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
yyihuang added a commit to flashinfer-ai/flashinfer that referenced this pull request Sep 28, 2026
…rm on the 32-128-token shapes it wins, per-architecture route tables, regenerated programs (#5621)

## Kimi-K3 fused MoE router: register-prefetch variant of the
32-128-token kernel on the shapes where it wins (SM100 / SM103)

Follow-up to #5531 / #5548 / #5564 / #5568 / #5589 / #5611 (same source
family). This branch is stacked on the open #5611: its base commit is
#5611's head (7a46a28), so it merges after #5611 (or is rebased onto
`main` once #5611 lands). Same K3 semantics (sigmoid gate, top-16 on
`sigmoid(logit) + bias`, renormalized unbiased weights, lower expert id
on ties) and the same expert-aligned route plan for `block_m` 8/16; the
public entries `flashinfer.fused_moe.kimi_k3_fused_router` /
`prepare_kimi_k3_fused_router` are unchanged. The arm set gains one
member: `LP`, a second kernel of the `L` family (same thread count,
cooperative launch, admission guard and grid rule) that loads the bias
and the CTA's first row's logits into registers before its prologue
barrier, so their cold-L2 round trip overlaps the winner seed, the
plan-bitmap clear and the CTA barrier instead of waiting behind them.
This round carries a **host change** next to the regenerated programs:
the two route tables are no longer identical -- `LP` serves `(32, 8)`,
`(32, 16)`, `(64, 8)` and `(64, 16)` on SM100 and `(32, 8)`, `(32, 16)`,
`(64, 8)` and `(128, 8)` on SM103, the other 32-128-token cells keep arm
`L` -- and the `L` launch-grid / admission rules apply to both members
of the family (`ARM_L_FAMILY = ("L", "LP")`). The exporter emits one
module per (architecture, arm, `block_m`) that serves a routed shape: 22
records on SM100 and 21 on SM103 (no SM103 shape routes to `L` at
`block_m` 8 any more, so that record is retired); only the four `lp`
records are new kernel code, every other program is untouched.

### What changed
4 of the 28 routed shapes per architecture get new kernel code (SM100:
`num_tokens` 32 x `block_m` 8 / 16, 64 x 8 / 16; SM103: 32 x 8 / 16, 64
x 8, 128 x 8); the other 24 rows keep theirs and are the same-session
controls. Ratios below are `previous / new` kernel time from
same-session paired cold-L2 CUPTI A/Bs against the #5611 programs
(`block_m` 8 / 16 per row, two runs per arch); a change ships only when
every changed row reads > 1.0 in both runs on every architecture it
changes.

- **`LP` (the `L` body with the bias / first-row logits loads above the
prologue barrier).** The `L` prologue seeds the winner slots, zeroes the
plan bitmap (16 shared stores per thread) and passes a CTA barrier
before loading the bias float4 and the row's logits float4; neither load
depends on the barrier, so `LP` issues both above it (the next row's
logits are refilled at the same program position at which `L` issued its
load; all four prefetch registers are always defined so the consumed
row's values do not stay live through the selection). `__syncthreads`
count unchanged (11 on SM103, 10 on SM100 whose `L` family carries the
one-barrier prefix of #5611), 71 registers, no spills. Measured as a
whole-arm change on all six 32-128-token rows first: five rows per
architecture win in every run (SM100 1.005-1.037, SM103 1.005-1.022)
while one row per architecture ties the previous kernel at the 32 ns
CUPTI tick deterministically (SM100 128 tokens / `block_m` 8: 6.368 =
6.368 us in two of three runs; SM103 64 tokens / `block_m` 16: 5.920 =
5.920 us in all three runs) and the 128-token / `block_m` 16 row ties in
the routed program's own runs on both architectures (SM100 0.9998 /
0.9950, SM103 1.0000 / 1.0000). Hence the per-cell route: `LP` only
where it wins, the tie rows keep the #5611 `L` kernel byte-for-byte.

Routed program, source-side paired cold-L2 CUPTI A/B against the #5611
programs (same session, same node; `previous / new` kernel time; two
runs per arch):

| row | B200 run 1 / run 2 | GB300 run 1 / run 2 |
|---|---|---|
| m32 bm8 | 1.0317 / 1.0263 | 1.0219 / 1.0275 |
| m32 bm16 | 1.0365 / 1.0311 | 1.0053 / 1.0053 |
| m64 bm8 | 1.0211 / 1.0209 | 1.0220 / 1.0164 |
| m64 bm16 | 1.0105 / 1.0052 | same kernel code as #5611 |
| m128 bm8 | same kernel code as #5611 | 1.0208 / 1.0208 |
| geomean (changed rows) | 1.0249 / 1.0208 | 1.0175 / 1.0175 |

The 24 unchanged rows per architecture load kernel code that is
byte-identical to #5611 (GPU-free trace identity of the eleven existing
arms at `block_m` 8 / 16 on both architectures).

### Exactness
Source-side gates on both arches: exact43 43/43, affected slice +
registered e2e 56/56 per arch (28 registered cases + the 28-row
extension), compute-sanitizer synccheck and racecheck (separate runs) on
the `LP` body (eight 32-128-token labels, 13 logs per run, both arches):
`ERROR SUMMARY: 0 errors` / `RACECHECK SUMMARY: 0 hazards` on every run.
Export receipts (source <-> export bitwise parity on all 28 shapes per
arch, generated grid == source grid, including the `LP` arm and its
grid): B200 (sm_100a) 28/28 shapes pass (bitwise source/export parity,
grid parity; export-to-source kernel time 0.951-1.020), GB300 (sm_103a)
28/28 shapes pass (bitwise source/export parity, grid parity;
export-to-source kernel time 0.964-1.025); the delivered patch produced
from the two architectures is byte-identical (sha256
afba09a58ed98557c02efe199b44a8c0fa9511a2c18416d8495435fc81c6bc97,
144,130 bytes). `tests/experimental/test_cake_kimi_k3_fused_router.py`:
44 passed (B200), 44 passed (GB300). Pre-commit (ruff, mypy,
clang-format opt-out for the generated `csrc/`): all hooks pass on both
export trees.

### Benchmark
`benchmarks/bench_cake_kimi_k3_fused_router.py --cupti --rounds 5`
(interleaved A/B rounds, CUPTI kernel time with a cold L2) against
SGLang `route_radix` + `moe_align_block_size` (pinned SGLang revision
83bd2c47, kernel-only):

| row | arm (B200 / GB300) | B200 fused us | B200 SGLang us | B200
speedup | GB300 fused us | GB300 SGLang us | GB300 speedup |
|---|---|---:|---:|---:|---:|---:|---:|
| 1 tokens, block_m 8 | LC | 4.00 | 9.15 | 2.288 | 4.10 | 13.12 | 3.203
|
| 1 tokens, block_m 16 | LC | 4.38 | 9.70 | 2.212 | 4.45 | 13.09 | 2.942
|
| 2 tokens, block_m 8 | LC | 5.22 | 9.34 | 1.791 | 5.31 | 12.96 | 2.440
|
| 2 tokens, block_m 16 | LC | 5.76 | 9.25 | 1.605 | 5.60 | 13.15 | 2.349
|
| 4 tokens, block_m 8 | LC | 5.60 | 9.41 | 1.680 | 5.38 | 13.09 | 2.435
|
| 4 tokens, block_m 16 | LC | 5.28 | 9.79 | 1.855 | 5.28 | 13.12 | 2.485
|
| 8 tokens, block_m 8 | LC | 5.66 | 9.54 | 1.684 | 5.50 | 12.70 | 2.308
|
| 8 tokens, block_m 16 | LC | 5.44 | 9.66 | 1.776 | 5.57 | 13.28 | 2.385
|
| 16 tokens, block_m 8 | LC | 6.05 | 9.44 | 1.561 | 5.82 | 13.15 | 2.258
|
| 16 tokens, block_m 16 | LC | 5.95 | 9.98 | 1.677 | 5.76 | 12.93 |
2.244 |
| 32 tokens, block_m 8 | LP | 5.73 | 9.70 | 1.693 | 5.79 | 12.99 | 2.243
|
| 32 tokens, block_m 16 | LP | 5.98 | 9.57 | 1.599 | 5.76 | 13.06 |
2.267 |
| 64 tokens, block_m 8 | LP | 6.05 | 9.70 | 1.603 | 5.95 | 12.99 | 2.183
|
| 64 tokens, block_m 16 | LP / L | 6.14 | 9.76 | 1.589 | 6.11 | 13.54 |
2.215 |
| 128 tokens, block_m 8 | L / LP | 6.27 | 10.21 | 1.628 | 6.05 | 13.02 |
2.153 |
| 128 tokens, block_m 16 | L | 6.50 | 10.14 | 1.562 | 6.24 | 13.50 |
2.164 |
| 256 tokens, block_m 8 | M | 7.39 | 10.69 | 1.446 | 7.14 | 14.30 |
2.004 |
| 256 tokens, block_m 16 | M | 7.58 | 10.98 | 1.447 | 7.20 | 13.86 |
1.924 |
| 512 tokens, block_m 8 | Q4S | 9.54 | 12.83 | 1.346 | 9.22 | 15.71 |
1.705 |
| 512 tokens, block_m 16 | Q4S | 9.63 | 12.51 | 1.299 | 9.25 | 15.52 |
1.678 |
| 1024 tokens, block_m 8 | Q4S | 13.18 | 18.30 | 1.388 | 12.54 | 18.82 |
1.500 |
| 1024 tokens, block_m 16 | Q4S | 13.12 | 17.66 | 1.346 | 12.67 | 18.34
| 1.447 |
| 2048 tokens, block_m 8 | Q4SP | 19.81 | 28.06 | 1.417 | 17.98 | 27.01
| 1.502 |
| 2048 tokens, block_m 16 | Q4SP | 19.90 | 26.69 | 1.341 | 17.98 | 26.08
| 1.450 |
| 4096 tokens, block_m 8 | GW | 26.66 | 47.17 | 1.770 | 24.90 | 45.66 |
1.834 |
| 4096 tokens, block_m 16 | GW | 26.66 | 44.83 | 1.682 | 25.47 | 43.78 |
1.719 |
| 8192 tokens, block_m 8 | GW | 40.51 | 86.82 | 2.143 | 38.30 | 82.85 |
2.163 |
| 8192 tokens, block_m 16 | GW | 40.19 | 81.09 | 2.018 | 39.74 | 79.10 |
1.990 |

Geomean speedup vs SGLang: **NVIDIA B200 1.640 (28/28 > 1.0, min 1.299
at 512 tokens / block_m 16)**, **NVIDIA GB300 2.074 (28/28 > 1.0, min
1.447 at 1024 tokens / block_m 16)**. #5611 on the same benchmark: B200
1.642 (min 1.299), GB300 2.057 (min 1.450); #5589: B200 1.673 (min
1.327), GB300 2.030 (min 1.358); #5568: B200 1.516 (min 1.260), GB300
1.882 (min 1.349). The source-side sealed interleaved paired run of the
same programs against the same SGLang pin (same session, A/B/B/A order)
reads B200 geomean 1.6061, 28/28 rows > 1.0, min 1.3019 (`m512_bm16`),
max 2.187 (`m8192_bm8`); GB300 geomean 1.7627, 28/28 rows > 1.0, min
1.3573 (`m1024_bm16`), max 2.515 (`m1_bm16`) (#5611 programs under the
same protocol: B200 1.5884, GB300 1.7752).

### Baselines and their source PRs
- **Previous programs (A/B "previous" arm and the geomean reference):**
#5611 (head 7a46a28, open; the base commit of this branch), on top of
#5589 (head 1551f0c, open), #5568 (merge d67061a, 2026-09-26),
#5564 (78c6e1f, 2026-09-26), #5548 (c894711, 2026-09-25) and #5531
(afad701, 2026-09-25). Files:
`flashinfer/experimental/kimi_k3_fused_router/{cake_backend.py,
cake_jit.py, README.md, csrc/cake_kimi_k3_fused_router/sm_10{0,3}a/*}`,
`flashinfer/fused_moe/kimi_k3_fused_router.py`,
`benchmarks/bench_cake_kimi_k3_fused_router.py`,
`tests/experimental/test_cake_kimi_k3_fused_router.py`. This branch's
merge-base with `main` is d67061a (#5568's merge commit, as for
#5589 / #5611); `git log d67061a..origin/main --
flashinfer/experimental/kimi_k3_fused_router
flashinfer/fused_moe/kimi_k3_fused_router.py
benchmarks/bench_cake_kimi_k3_fused_router.py
tests/experimental/test_cake_kimi_k3_fused_router.py` is empty at `main`
b9fe0f4 (26 commits after the merge-base, none touching these paths),
so the branch rebases onto `main` without conflicts once #5589 / #5611
land.
- **SGLang reference (benchmark denominator and the test oracle's
semantics):** `sglang.kernels.ops.moe.moe_route_radix.route_radix` +
`sglang.kernels.ops.moe.moe_align.moe_align_block_size` at the pinned
SGLang revision 83bd2c473f173404216f6d9851ad7a95c1e3c565, imported
kernel-only (no SGLang runtime); `moe_route_radix.py` last changed by
sgl-project/sglang#32890 (fb207b72b, 2026-08-01, "port standalone Kimi
K3 kernels") and `moe_align.py` by sgl-project/sglang#32045 (74338e94f,
2026-07-22), both before the pinned revision; the pin is unchanged since
#5531.
- **Test oracle:** the PyTorch reference in
`tests/experimental/test_cake_kimi_k3_fused_router.py` from #5531
(unchanged since; only the route-table and launch-grid assertions of
that file change in this PR).

### Notes
- **Route tables (per architecture; they differ in exactly two cells).**
`cake_backend._SM103_SHAPE_ROUTE` is now `_SM100_SHAPE_ROUTE` with `(64,
16) -> L` and `(128, 8) -> LP`:

| num_tokens x block_m | 1-16 (any) | 32 x 8 / 16 | 64 x 8 | 64 x 16 |
128 x 8 | 128 x 16 | 256 | 512-1024 | 2048 | 4096-8192 |
  |---|---|---|---|---|---|---|---|---|---|---|
  | sm_100a (B200) | LC | LP | LP | LP | L | L | M | Q4S | Q4SP | GW |
  | sm_103a (GB300) | LC | LP | LP | L | LP | L | M | Q4S | Q4SP | GW |

- **`L` family.** `ARM_L_FAMILY = ("L", "LP")`; `launch_grid(arm, n,
...)` applies the `L` rule to both members (grid = `min(max(n, 128),
CTAs-per-SM x SM count)`, at most 512 tokens), and `cake_jit.ARMS` lists
`LP`. `test_route_tables_cover_the_routed_shapes` asserts the two
differing cells and four `LP` / two `L` cells per architecture;
`test_launch_grid_rules` asserts `launch_grid("LP", n, ...) ==
launch_grid("L", n, ...)` for 32 / 64 / 128 tokens and the same
admission error above 512 tokens; every other test is unchanged.
- **Per-architecture programs.** The SM100 and SM103 `lp` programs
differ exactly as the `l` programs do (SM100 carries the one-barrier
prefix of #5611); all other programs are generated from the same source
on both architectures. The `lp` module records carry `block: [224, 1,
1]`, no cluster and `cooperative: true`, like `l`. The `l` record at
`block_m` 8 on SM103 is removed because no routed SM103 shape uses it
(the SM103 `L` cells are 64 and 128 tokens at `block_m` 16); its SM100
counterpart stays (128 tokens x `block_m` 8).
- Shapes outside the routed set still raise `NotImplementedError`.

---

**v59 update (commit `dfc02223`): LC r16 launch bound made portable
across ptxas versions.** The internal CI run of this PR (and of #5611)
failed
`tests/experimental/test_cake_kimi_k3_fused_router.py::test_fused_router_matches_reference[16-8]`
/ `[16-16]` on B200 / GB200 / GB300 with CUDA 12.9 and 13.0 at JIT build
time: the 16-token kernel (`lc16`, one 16-CTA non-portable cluster) was
declared `__launch_bounds__(224, 1, 16) __cluster_dims__(16,1,1)`; nvcc
12.9 / 13.0 emit both `.reqnctapercluster` and `.maxclusterrank` for
that pair and their ptxas rejects the combination ("Conflicting
directives"), while nvcc / NVRTC 13.4 drop the redundant
`.maxclusterrank` (which is why the CUDA 13.4 jobs and the source-side
gates passed). The regenerated `lc16` programs (both architectures, both
`block_m`) drop the third launch-bound operand -- `__cluster_dims__`
already fixes the cluster rank -- and differ from the previous programs
in that one attribute line only; the other 18 module records are
byte-identical. Source-side evidence (Cake `cdb62e8c654`): exact43 43/43
metadata cases on both architectures; paired cold-L2 CUPTI 28 rows vs
the previous programs M16 GB300 1.0057 / 1.0273, B200 0.9947 / 0.9695 in
run 1 and 1.0000 / 1.0002 in the M16-only run 2, with the 26
identical-code control rows scattering 0.95-1.035 (`previous / new`);
NVRTC-cubin SASS of `lc16` identical (2904 SASS lines, 0 differing) and
the cubins byte-identical under CUDA 13.3, both architectures, both
`block_m`; export receipts 28/28 rows per architecture (bitwise
source/export parity, grid parity), identical 32,408-byte patch from
both architectures; FlashInfer pytest / bench / pre-commit 44 passed on
each GPU; bench `--cupti --rounds 5` vs SGLang 28/28 rows > 1 (GB300
geomean 2.041, B200 1.727); all pre-commit hooks pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Expanded Kimi K3 fused-router support across additional batch sizes,
including single-token and 2,048-token workloads.
* Added optimized routing options for different workload sizes and
supported GPU architectures.
* **Improvements**
* Improved top-expert selection and route gathering, with more efficient
handling of routing outputs.
* Distributed expert metadata generation across processing units to
improve handling of larger workloads.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Yingyi Huang <averyh@nvidia.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bypass-fastfail documentation Improvements or additions to documentation high priority jit-kernel Multi-modal multi-modal language model quant LLM Quantization run-ci CI: run the baseline test suite on this PR run-ci-extra CI: also run the extra suite (requires run-ci)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants