Skip to content

[KDA] KDA MTP decode: recurrent + KVBuffer chunkwise verify + flush#96

Merged
Longxmas merged 19 commits into
inclusionAI:mainfrom
Longxmas:feat/kda-mtp-verify-4ops
Jul 9, 2026
Merged

[KDA] KDA MTP decode: recurrent + KVBuffer chunkwise verify + flush#96
Longxmas merged 19 commits into
inclusionAI:mainfrom
Longxmas:feat/kda-mtp-verify-4ops

Conversation

@Longxmas

@Longxmas Longxmas commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

📌 Description

Adds KDA (Kimi Delta Attention) multi-token-prediction (MTP) decode — the target-side gated-delta-rule recurrence for speculative decoding — in two complementary forms:

  • Recurrent (kda_decode_mtp): a single register-resident CuTe kernel threading the recurrence over the T draft tokens per (batch, head). recurrent (single-warp, lane=K / lane=V) covers small batch; recurrent_ws (4-warp warp-spec) covers the large-batch / long-T corner where the single warp turns DRAM-bandwidth bound. A unified dispatch picks per shape.
  • KVBuffer chunkwise verify (kda_decode_mtp_kvbuffer): the parallel-verification path — verify emits a compact scratch (~2·T·d) instead of the T per-token states (T·d²), and a rank-m flush rebuilds the accepted state. Two variants — shuffle (token-parallel SIMT) / tensor_core (CuTe tensor-core MMA, flat-in-T). Scores are built from bounded ordered decay-ratio products (no division), so they stay finite under unbounded softplus gates.

Both gate forms are supported: softplus and safe-gate (lower_bound, g = exp(lower_bound · σ(exp(A_log) · x))).

What changed

  • cula/ops/kda_decode_mtp.pyrecurrent / recurrent_ws ops + unified dispatch.
  • cula/ops/kda_decode_mtp_kvbuffer.pyshuffle / tensor_core chunkwise verify + rank-m flush.
  • tests/test_kda_decode_mtp.py — unit (vs fp32 oracle) + bit-exact determinism.
  • benchmarks/bench_kda_decode_mtp.py — unified verify-chain benchmark.

🔍 Related Issues

Closes #17

🧪 Tests & Accuracy

pytest tests/test_kda_decode_mtp.py131 passed. recurrent (lane=K / lane=V) / recurrent_ws / shuffle / tensor_core verify output + rank-m flush vs the fp32 single-token recurrence oracle, plus bit-exact determinism (torch.equal), for both softplus and safe-gate gates.

tests/test_kda_decode_mtp.py::test_oracle_vs_loop[N1-T1-H8-HV16-randstate] PASSED
tests/test_kda_decode_mtp.py::test_recurrent_ws_decode[N64-T8-H8-HV16-tvNone-ilpNone-smemNone] PASSED
tests/test_kda_decode_mtp.py::test_recurrent_decode[N16-T4-H16-HV32-vk-bv-1-ks1] PASSED
tests/test_kda_decode_mtp.py::test_lower_bound_safe_gate[N1-T1-H8-HV16-recurrent_vk] PASSED
tests/test_kda_decode_mtp.py::test_determinism[recurrent_vk] PASSED
tests/test_kda_decode_mtp.py::test_intermediate_vs_oracle_and_final[16-2-False] PASSED
tests/test_kda_decode_mtp.py::test_shuffle_kvbuffer_verify_and_flush[2-2-16-16] PASSED
tests/test_kda_decode_mtp.py::test_tensor_core_kvbuffer_verify_and_flush[2-3-16-16] PASSED
tests/test_kda_decode_mtp.py::test_lower_bound_kvbuffer[tensor_core-2-4-16-16] PASSED
tests/test_kda_decode_mtp.py::test_kvbuffer_dispatch_output_matches_oracle[2-16-2-shuffle] PASSED
tests/test_kda_decode_mtp.py::test_kvbuffer_flush_determinism[shuffle-4-4-16-16] PASSED
======================= 131 passed in 180.30s (0:03:00) =======================

Kernel-level accuracy sweep (HV=H ∈ {8,16,32,64} × N ∈ {1..128} × T ∈ {2,3,4,6}, K=V=128, bf16, safe-gate lower_bound=-5.0) vs Triton fused_sigmoid_gating_delta_rule_update:

  • All 4 ops × 128 shapes pass every bf16 threshold (512/512), none fail.
  • Worst case over the whole sweep: recurrent / recurrent_ws rel_rmse ≤ 3.6e-5, rel_max ≤ 3.3e-3; shuffle / tensor_core rel_rmse ≤ 3.4e-5, rel_max ≤ 3.3e-3 — within bf16 rounding noise.
  • Flush / recurrent state reconstruction vs the fp32 oracle: rel_rmse ~1e-7, |mean_diff| ~1e-11 (bit-clean, unbiased).
  • Bit-exact determinism: repeated launches (100k iterations each) of every op are identical (torch.equal) — output, state, u-buffer.
  • compute-sanitizer (memcheck / racecheck / initcheck / synccheck) reports 0 errors / 0 hazards across small + large shapes and both contiguous and dyn-stride (strided q/k/v) input paths.

⚡ Performance

H200, K=V=128, bf16, safe-gate lower_bound=-5.0 (both baseline and cuLA), commit = official sglang scatter, accept m=full, CUDA-graph kernel-chain. Speedup = triton_spec verify chain / op chain (>1 = faster than Triton). recurrent-class = max(recurrent, recurrent_ws), kvbuffer-class = max(tensor_core, shuffle); for each shape the faster class is bold.

HV = H = 16

recurrent

N T=2 T=3 T=4 T=6
1 1.86 1.93 1.93 1.89
2 1.65 1.75 1.74 1.71
4 1.46 1.53 1.59 1.68
8 1.33 1.36 1.40 1.44
16 1.17 1.14 1.13 1.11
32 1.16 1.15 1.14 1.10
64 1.13 1.12 1.10 1.07
128 1.08 1.06 1.03 1.02

kvbuffer

N T=2 T=3 T=4 T=6
1 1.26 1.43 1.58 1.57
2 1.19 1.31 1.41 1.54
4 1.17 1.28 1.41 1.56
8 1.22 1.29 1.54 1.91
16 1.36 1.58 1.73 1.91
32 1.85 1.90 2.10 2.26
64 1.52 1.52 1.68 1.87
128 1.55 1.59 1.76 1.99

HV = H = 32

recurrent

N T=2 T=3 T=4 T=6
1 1.71 1.73 1.73 1.72
2 1.44 1.55 1.57 1.69
4 1.35 1.35 1.46 1.45
8 1.19 1.15 1.14 1.12
16 1.15 1.15 1.13 1.10
32 1.13 1.13 1.10 1.07
64 1.08 1.05 1.03 1.01
128 1.07 1.04 1.02 1.01

kvbuffer

N T=2 T=3 T=4 T=6
1 1.19 1.30 1.43 1.55
2 1.17 1.26 1.40 1.56
4 1.21 1.29 1.57 1.92
8 1.46 1.58 1.76 2.05
16 1.83 2.01 2.14 2.25
32 1.52 1.55 1.69 1.87
64 1.56 1.60 1.76 1.99
128 1.58 1.66 1.84 2.10

Takeaways:

  • recurrent is fastest at small batch / short T (up to ~1.9× at N ≤ 4) and tapers toward ~1.0× as N grows (the single warp turns bandwidth-bound; recurrent_ws holds the floor at ~1.0× in the large-batch / long-T corner).
  • kvbuffer is fastest at large batch / long T (up to ~2.3× at N=32, T=6) and grows with T (flat-in-T tensor_core).
  • The two are complementary; the production dispatch routes between them per shape. Numbers are kernel-chain latency, not e2e throughput (the verify chain is a small fraction of the decode step).

Longxmas added 2 commits June 16, 2026 16:37
Single-kernel recurrent gated-delta-rule multi-token-prediction decode with register-resident state. vk (lane=K butterfly-reduce) and ws (4-warp warp-spec) CuTe ops behind a unified dispatch; single-token T=1 routes to vk regardless of batch.
Chunkwise parallel-verification KVBuffer ops for KDA MTP speculative decoding: tp (token-parallel SIMT) and cute-gemm (sm90 tensor-core) verify emit a compact u-buffer instead of T per-token states; rank-m flush rebuilds the accepted state. Adds unit + determinism tests and the unified decode-mtp benchmark.
@Longxmas Longxmas requested a review from icavan June 16, 2026 09:12
gemini-code-assist[bot]

This comment was marked as outdated.

@Longxmas Longxmas requested a review from zheyang0825 June 16, 2026 09:30
Longxmas added 2 commits June 16, 2026 21:30
…added CTAs, empty dummies

flush kvbuffer: accept_len is now per-request and read at runtime from an
[N] int32 buffer (m_buf[i_n]) instead of a compile-time constant. The kernel
statically unrolls T and masks i_i < m_n, so it compiles exactly once per
(shape, BV) regardless of accept length; b_m uses the per-request token m_n-1.
Host API accepts an int (broadcast to all N) or a per-request [N] tensor.

small-batch decode (vk + kv): wrap the compute body in `if cache_idx >= 0:` so
padded slots (cache_idx < 0) skip the whole T-loop, matching the ws kernel
(~1.3x on a half-padded batch). kv hoists its k_split constexpr decisions to
top level so they stay python constants inside the guarded block.

kvbuffer verify: torch.empty instead of torch.zeros for the write_ubuf=False
dummy buffers (only ever written, never read) — drops a per-call memset.
- cg SMEM stride K+8 -> K+4: fixes MMA fragment bank conflict (-16~19% @ N>=4, bit-identical)
- drop redundant P4 doubling-chain barrier
- 3xTF32 emulation on P3 & GEMM1 GEMMs: cgkvb max|Δ| 2.44e-4 -> ~vk level/bf16 floor (+5~6% cgkvb_v @ large N)
- bench: default H=HV=32, graph-calls=20
@Longxmas Longxmas force-pushed the feat/kda-mtp-verify-4ops branch from 4fd3268 to 9bf0493 Compare June 23, 2026 10:53
Model config kda_safe_gate=true with kda_lower_bound set uses the safe gate
g = lower_bound * sigmoid(exp(A_log) * x); the MTP decode gates previously only
implemented the softplus gate g = -exp(A_log) * softplus(x). Add the lower_bound
branch (compile-time const_expr; lower_bound=None keeps the softplus path,
bit-identical) to all five gates: vk/ws/kv in kda_decode_mtp and tp/cg in
kda_decode_mtp_kvbuffer, threaded through launcher / compile cache key / host /
dispatch. Tests: oracle lower_bound branch + test_lower_bound_safe_gate
(vk/ws/kv) + test_lower_bound_kvbuffer (tp/cg).
@Longxmas Longxmas force-pushed the feat/kda-mtp-verify-4ops branch from 50a60e6 to cf3c1e7 Compare June 24, 2026 06:42
Longxmas added 3 commits June 24, 2026 16:04
dynamic-N: mark the batch (and state-pool) axes dynamic via
mark_compact_shape_dynamic(mode=0, stride_order=...) and mark_layout_dynamic()
for the index tensors, and drop N + pool_size from the compile cache key, so one
cubin serves all batch sizes (removes the per-N JIT; no startup prewarm needed).
Applied to all five MTP decode gate kernels: vk/ws/kv in kda_decode_mtp and tp/cg
in kda_decode_mtp_kvbuffer. Validated: unit tests bit-exact/bf16-level, e2e gsm8k
unchanged at 0.8696 with cuda-graph capture succeeding without prewarm.

ruff: fix lint in the touched files (E402 noqa on intentional mid-file imports,
SIM comparison order, isort, F841 unused var, E702 multiple statements).
Run the ruff-format pre-commit hook on the files it flagged for pre-existing
format drift: cula/ops/kda_decode_mtp.py, cula/ops/kda_decode_mtp_kvbuffer.py,
tests/test_kda_decode_mtp.py, benchmarks/bench_kda_decode_mtp.py. Formatting
only, no logic change.
Add kda_flush_kvbuffer_all_layers: one launch over all L KDA layers (2D grid,
x = single-layer grid, y = layer) instead of the per-layer Python loop the
caller previously used for the spec-decode state commit. dyn-N: N (request
count) is not a compile constant -- the index tensors are marked
layout-dynamic and N is dropped from the cache key, so one cubin serves all
batch sizes (no per-N compile storm). cute.compile traces the real,
already-allocated tensors directly.

Bit-identical to the per-layer kda_flush_kvbuffer (MAXDIFF=0 vs the loop over
all layers); the single-layer entry point is left unchanged.
@Longxmas Longxmas force-pushed the feat/kda-mtp-verify-4ops branch from 68e8f3f to 0da49c7 Compare June 25, 2026 04:43
Longxmas added 7 commits July 2, 2026 19:54
… support)

Replace the kinv = k / b_run factorization (divides by the cumulative gate
product, overflows to inf/NaN once unbounded softplus gates underflow b_run)
with bounded ordered products:
- tp/cg verify: T x T scores via per-pair decay-ratio chains r(t,i) =
  prod_(i<j<=t) g_j <= 1 in fp32 SIMT (cg keeps tensor-core MMA for the S0
  projection and state-update GEMMs; state B-operand becomes the suffix-decayed
  key ksuf_t = kn_t * prod_(j>t) g_j).
- scratch now stores raw (u, k, g) per token, the same triplet as ReplaySSM's
  (d, k, g) ring.
- single-layer and all-layers flush rebuild S_m with descending suffix
  products; every factor stays <= 1, no division anywhere.

Exact identity transformation; also valid for safe-gate models.
The flush kernel is DRAM-latency bound with no per-CTA data reuse, so more,
smaller CTAs win: a back-to-back (bv, warps-per-CTA) sweep on H200 shows bv=8
beats bv=32 by 17-20% at large N*HV (e.g. N=128 T=6 HV=16: 91.2 vs 109.9 us)
and ties at small, while multi-warp CTAs only lose (MLP loss outweighs the
saved duplicate k/g reads). _select_vk_bv is tuned for the compute-heavier vk
verify kernel and picked 32 exactly where flush wants 8; use a flush-specific
selector instead. Also evaluated and rejected: row-grouped running-product
Stage-3 for tp verify (op count T^3/6 -> T^2 but the serial ratio chain kills
ILP; measured flat to -2%).
The sglang adapter feeds q/k/v as strided views of the fused qkv
projection; forcing them contiguous costs ~90 copy kernels per verify
step and, at small batch, launch-bound GPU idle. Compile a dynamic-
layout kernel variant (mark_layout_dynamic, innermost K axis static)
when the inputs are strided with a contiguous K axis, and skip the
copies. Contiguous inputs keep the byte-identical compact variant;
dyn_stride joins the compile cache key. Bit-exact vs the contiguous
path (output, final state, intermediate states) at N=4/16/32, T=4,
H=HV=16, K=V=128.
… / shuffle) + d/k/g_buffer

- Restore the warp-spec recurrent op as kda_decode_mtp_recurrent_ws; recurrent dispatch
  now routes state_layout=kv -> kv, T<=4 or N*HV<2048 -> recurrent (vk), and
  T>4 with N*HV>=2048 -> recurrent_ws (the regime where single-warp vk hits the
  DRAM-bandwidth wall).
- Rename small_batch -> recurrent (vk/kv single-warp host + kernels).
- kvbuffer: cg/tp -> tensor_core/shuffle across function/kernel/launcher/compile-cache
  names and the _kvbuffer_prefer_* helper; verify dispatch picks tensor_core vs shuffle
  by S = HV*N and T (bench-derived collapse). Scratch params u_buffer/kinv_buffer/b_buffer
  -> d_buffer/k_buffer/g_buffer (replayssm d/k/g semantics).
- recurrent_ws warp-group gate opt: distribute the per-K-channel decay gate across all
  4 warps (channel = warp_idx*threads_per_group + lane_in_group) instead of computing all
  128 channels on warp 0; bit-exact, q/k l2norm-reduce + beta stay on warp 0.
- Condense the kvbuffer flush/rebuild comments and the module docstring.
- tests and bench updated for every rename plus the recurrent_ws variant.

Validation: pytest tests/test_kda_decode_mtp.py = 131 passed; bench --check reports all
variants (vk / recurrent_ws / shuffle / tcore) within ~1e-5..6e-5 of the Triton oracle.

Note: tests/test_kda_decode.py switched from `cula.kda` to `cula.ops` imports to avoid the
box's incomplete fla install (missing fla.ops.cp); this is an environment workaround and is
independent of the naming/op changes above.
… + canonical naming

recurrent_ws: in the dispatch regime (T>4 and N*HV>=2048) the verify kernel is
write-BW bound by the T*d^2 snapshot writes; use ilp_rows=2 there (lower register
pressure, higher occupancy hides the wall) -- ~6% faster, bit-exact. Other verify
shapes keep the work_units heuristic.

kvbuffer dispatch: add _select_kvb_variant(N,HV,T) -- two threshold lines on the
work size wu=N*HV (T<=2 shuffle; T=3 wu<=64; T=4 wu<=32; T>=5 tensor_core);
_kvbuffer_prefer_tensor_core delegates to it.

naming: rename bench op-selector flags + output columns and the two kvbuffer test
functions to canonical recurrent / recurrent_ws / tensor_core / shuffle (drop
vk/tp/cg/tcore from selectors, flags, and labels; kernel variant params kept).
… q/k/v (dyn-stride)

Extend the vk dyn-stride path to recurrent_ws and both kvbuffer
(tensor_core/shuffle) verify ops. When q/k/v arrive as K-contiguous strided
views the host skips the .contiguous() copy and compiles the
mark_layout_dynamic(leading_dim=3) kernel variant (dyn_stride enters the
compile cache key); contiguous inputs keep the compact byte-identical kernel.
Kernel bodies are layout-agnostic and unchanged (a small _dlp_qkv helper picks
the descriptor).

Bit-exact vs the contiguous-copy path: 18/18 (max abs diff 0) across
recurrent_ws / tensor_core / shuffle x softplus/safe-gate x N{4,8,2} T=4
HV{16,32} K=V=128. Removes the adapter's per-layer input copies on the
ws/kvbuffer dispatch paths.
@Longxmas Longxmas force-pushed the feat/kda-mtp-verify-4ops branch 3 times, most recently from 5097eec to cbd3800 Compare July 6, 2026 04:33
…ctors; determinism 100000; drop sm90 wording; ruff

Import decode ops from the public cula.kda surface; name the single-warp
recurrent test selectors recurrent_vk / recurrent_kv; bump the determinism
iteration default to 100000 and make the kvbuffer verify/flush determinism
tests honor the same KDA_MTP_DET_ITERS knob; drop sm_90 wording from the
tensor_core kvbuffer comments/docstrings and benchmark; ruff import-sort + format.
@Longxmas Longxmas force-pushed the feat/kda-mtp-verify-4ops branch from cbd3800 to 5fec6eb Compare July 6, 2026 06:26
…acle-tolerance line

kvbuffer: write r(t,i) as the ordered product prod_{i<j<=t} g_j (drop the b_t/b_i
division form) so the formula matches the no-division numerical note. recurrent:
drop the validation-tolerance sentence from the module docstring.

@icavan icavan left a comment

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.

LGTM

Relax the recurrent dispatch guard from `T <= 4` to `T < 4` so T==4 uses the same
N*HV >= _WS_WORK_UNIT_THRESHOLD work-unit threshold as T > 4. Under the production
dyn-stride (K-contiguous strided q/k/v) path the single-warp vk kernel loses its
vectorized-load fast path at large batch (~+25% vs contiguous at N=128/HV=16,
dropping below both triton and warp-spec); recurrent_ws stages through SMEM and is
~stride-insensitive (~+4%). Contiguous vk and ws tie there, so the change is
non-regressive.
@Longxmas Longxmas force-pushed the feat/kda-mtp-verify-4ops branch from 1c81fa8 to 9e69e4e Compare July 7, 2026 02:44
Resolve conflicts against the arch-first KDA backend reorganization:
- move MTP decode ops into the new layout:
  cula/ops/kda_decode_mtp.py -> cula/ops/kda/decode/mtp.py
  cula/ops/kda_decode_mtp_kvbuffer.py -> cula/ops/kda/decode/mtp_kvbuffer.py
  and update imports in tests/benchmarks accordingly
- export kda_decode_mtp / kda_decode_mtp_recurrent / kda_decode_mtp_recurrent_ws
  via the lazy-import tables in cula/kda and cula/ops
- drop the opt_level plumbing previously added to the single-token decode op
  (cula/ops/kda/decode/cute.py stays identical to upstream); the MTP ops keep
  their own internal opt_level compile knobs
- list the MTP files in REPO_LAYOUT.md

@tongke6 tongke6 left a comment

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.

LGTM

@Longxmas Longxmas merged commit 8d572b9 into inclusionAI:main Jul 9, 2026
2 checks passed
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.

KDA MTP (Multi-Token Prediction) support

3 participants