Kimi-K3 MLA decode kernel — input specification
Measured on a live vLLM-ATOM server on 8x MI355X (gfx950): every argument of
aiter.mla.mla_decode_fwd on the decode path, with shape, dtype, stride and
size. Values are from the tail of a 350-token generation (kv_len ≈ 68,43x),
not from the first decode steps.
1. Environment
| item |
value |
| model |
KimiK3ForConditionalGeneration, 93 layers |
| GPUs |
8x MI355X (288 GiB HBM3E), --tensor-parallel-size 8 |
| execution |
eager, no CUDA graphs, no torch.compile |
| online weight quant |
off |
| KV cache dtype |
fp8 → float8_e4m3fn |
| vLLM block size |
128 |
| max model len |
73728 |
| concurrency |
16 and 24 |
| workload |
63911-token shared prefix + 4089 unique input tokens, 350 output tokens, prefix caching on |
| captured layers |
83, 87, 91, 92 (last four full-attention layers), TP rank 0 |
| component |
version |
commit |
| aiter |
amd-aiter 0.1.20.dev41+g334fd72ad |
334fd72ad6e69d16785efc97cb8b6b4aec3c2a92 (main, 2026-08-03) |
| vLLM |
0.25.2.dev0+g752a3a504.rocm724 |
752a3a504485790a2e8491cacbb35c137339ad34 |
| ATOM |
0.1.6rc1.dev219 |
452a0aca281b0317fde12f98e55a803e8c88e753 (k3-rmsnorm-fusion) |
| PyTorch |
2.10.0+rocm7.2.4 |
3d3aa833 |
| Triton |
3.7.0+amd.rocm7.2.0 |
89002410 |
| HIP / ROCm |
7.2.53211 |
— |
| GPU |
gfx950:sramecc+:xnack- |
— |
2. K3 MLA geometry
K3 is a hybrid stack: 69 KDA (gated-delta) layers and 24 full-attention MLA
layers. MLA runs in absorbed (latent) form, so the kernel attends over a
single 576-dim latent entry per token rather than per-head K/V.
| item |
value |
| MLA layers, 0-based |
3, 7, 11, ... , 79, 83, 87, 91, 92 (24 total; last two adjacent) |
num_attention_heads (global) |
96 |
| heads per GPU at TP8 |
12 |
kv_lora_rank |
512 |
qk_nope_head_dim / qk_rope_head_dim |
128 / 64 |
qk_head_dim |
192 |
v_head_dim |
128 |
| KV entry width |
576 = 512 latent + 64 rope |
sm_scale |
0.07216878364870322 = 1/sqrt(192) |
All four captured layers produced byte-identical argument signatures, so one
table describes all 24 MLA layers.
3. Entry point
aiter.mla.mla_decode_fwd, aiter/mla.py:270:
mla_decode_fwd(q, kv_buffer, o, qo_indptr, kv_indptr, kv_indices,
kv_last_page_lens, max_seqlen_q, page_size=1, nhead_kv=1,
sm_scale=None, logit_cap=0.0, num_kv_splits=None,
num_kv_splits_indptr=None, work_meta_data=None, work_indptr=None,
work_info_set=None, reduce_indptr=None, reduce_final_map=None,
reduce_partial_map=None, q_scale=None, kv_scale=None,
intra_batch_mode=False, return_logits=False, return_lse=False,
g_kv_indptr=None, cp_world_size=1, cp_rank=0)
Called from AttentionForVllmMLA._forward_decode. Two defaults are not
overridden and matter: page_size stays 1 (see 4.2) and nhead_kv stays
1 (single latent KV head).
The binary dispatched on this configuration:
aiter::mla_a8w8_qh16_qseqlen1_gqaratio16_ps
aiter_meta/hsa/gfx950/mla/mla_a8w8_qh16_qseqlen1_gqaratio16_ps.co
a8w8 = fp8 Q and fp8 KV, qh16 = 16 query heads, qseqlen1 = single-token
decode, ps = persistent. The qh16 specialization is why 12 heads get padded
to 16 (see 4.1).
The persistent (split-KV) path is always taken: the concurrency gate in
_use_persistent_mla_decode (aiter/mla.py:245) only downgrades to the
non-persistent kernel for the bf16 Q + bf16 KV 16-head profile, and this
deployment is fp8/fp8.
q is produced by aiter.fused_qk_rope_concat_and_cache_mla, which also writes
the step's new KV entry into the same cache the kernel reads. The 512-dim latent
o is then mapped back to v_head_dim by a separate BMM. Those surrounding ops
are out of scope here.
4. Arguments
contiguous flags a strided view; size is the tensor's full byte footprint.
Both q and o are contiguous.
4.1 Concurrency 16
| # |
argument |
shape |
dtype |
stride |
contiguous |
size |
| 0 |
q |
(16, 16, 576) |
float8_e4m3fn |
(9216, 576, 1) |
yes |
144.0 KiB |
| 1 |
kv_buffer |
(5283840, 1, 1, 576) |
float8_e4m3fn |
(576, 576, 576, 1) |
yes |
2.8 GiB |
| 2 |
o |
(16, 16, 512) |
bfloat16 |
(8192, 512, 1) |
yes |
256.0 KiB |
| 3 |
qo_indptr |
(17) |
int32 |
(1,) |
yes |
68 B |
| 4 |
kv_indptr |
(17) |
int32 |
(1,) |
yes |
68 B |
| 5 |
kv_indices |
(4718592) |
int32 |
(1,) |
yes |
18.0 MiB |
| 6 |
kv_last_page_lens |
(16) |
int32 |
(1,) |
yes |
64 B |
| 7 |
max_seqlen_q |
non-tensor |
|
|
|
1 |
| 8 |
sm_scale |
non-tensor |
|
|
|
0.07216878364870322 |
| 9 |
q_scale |
scalar |
float32 |
() |
yes |
4 B |
| 10 |
kv_scale |
scalar |
float32 |
() |
yes |
4 B |
| 11 |
work_meta_data |
(2) |
uint64 |
(1,) |
yes |
16 B |
| 12 |
work_indptr |
(257) |
int32 |
(1,) |
yes |
1.0 KiB |
| 13 |
work_info_set |
(319, 8) |
int32 |
(8, 1) |
yes |
10.0 KiB |
| 14 |
reduce_indptr |
(65) |
int32 |
(1,) |
yes |
260 B |
| 15 |
reduce_final_map |
(64, 2) |
int32 |
(2, 1) |
yes |
512 B |
| 16 |
reduce_partial_map |
(319) |
int32 |
(1,) |
yes |
1.2 KiB |
| 17 |
return_lse |
non-tensor |
|
|
|
False |
Batch: num_decodes = 16, num_decode_tokens = 16, num_prefills = 0 (pure
decode). num_heads 12 before padding, 16 after. fold_factor = None,
use_persistent_mode = True, dcp_world_size = 1.
Per-request KV length over the run: 68,090 → 68,437 tokens
(kv_len_sum 1,089,472 → 1,094,960), q_len always 1.
Late decode steps (step counts MLA-layer forwards, so consecutive model steps
are 24 apart):
| step |
kv_len min |
kv_len max |
kv_len sum |
| 16677 |
68,430 |
68,434 |
1,094,912 |
| 16701 |
68,431 |
68,435 |
1,094,928 |
| 16725 |
68,432 |
68,436 |
1,094,944 |
| 16749 |
68,433 |
68,437 |
1,094,960 |
4.2 Concurrency 24
| # |
argument |
shape |
dtype |
stride |
contiguous |
size |
| 0 |
q |
(24, 16, 576) |
float8_e4m3fn |
(9216, 576, 1) |
yes |
216.0 KiB |
| 1 |
kv_buffer |
(5283840, 1, 1, 576) |
float8_e4m3fn |
(576, 576, 576, 1) |
yes |
2.8 GiB |
| 2 |
o |
(24, 16, 512) |
bfloat16 |
(8192, 512, 1) |
yes |
384.0 KiB |
| 3 |
qo_indptr |
(25) |
int32 |
(1,) |
yes |
100 B |
| 4 |
kv_indptr |
(25) |
int32 |
(1,) |
yes |
100 B |
| 5 |
kv_indices |
(4718592) |
int32 |
(1,) |
yes |
18.0 MiB |
| 6 |
kv_last_page_lens |
(24) |
int32 |
(1,) |
yes |
96 B |
| 7 |
max_seqlen_q |
non-tensor |
|
|
|
1 |
| 8 |
sm_scale |
non-tensor |
|
|
|
0.07216878364870322 |
| 9 |
q_scale |
scalar |
float32 |
() |
yes |
4 B |
| 10 |
kv_scale |
scalar |
float32 |
() |
yes |
4 B |
| 11 |
work_meta_data |
(2) |
uint64 |
(1,) |
yes |
16 B |
| 12 |
work_indptr |
(257) |
int32 |
(1,) |
yes |
1.0 KiB |
| 13 |
work_info_set |
(319, 8) |
int32 |
(8, 1) |
yes |
10.0 KiB |
| 14 |
reduce_indptr |
(65) |
int32 |
(1,) |
yes |
260 B |
| 15 |
reduce_final_map |
(64, 2) |
int32 |
(2, 1) |
yes |
512 B |
| 16 |
reduce_partial_map |
(319) |
int32 |
(1,) |
yes |
1.2 KiB |
| 17 |
return_lse |
non-tensor |
|
|
|
False |
Batch: num_decodes = 24, num_decode_tokens = 24, num_prefills = 0. Same
head padding, persistent mode and fold settings as c16.
Per-request KV length over the run: 68,089 → 68,438 tokens
(kv_len_sum 1,634,250 → 1,642,410), q_len always 1.
Late decode steps:
| step |
kv_len min |
kv_len max |
kv_len sum |
| 33549 |
68,426 |
68,435 |
1,642,338 |
| 33573 |
68,427 |
68,436 |
1,642,362 |
| 33597 |
68,428 |
68,437 |
1,642,386 |
| 33621 |
68,429 |
68,438 |
1,642,410 |
5. Notes for optimization
5.1 12 real heads padded to 16 — 25% of the Q tile is zeros
_MLA_MIN_HEADS = 16 in atom/model_ops/attention_mla.py:109 ("AITER MLA
kernels require at least 16 attention heads"). K3 at TP8 has 96/8 = 12
heads. 16 is not a multiple of 12, so head-repeat cannot be used and the code
zero-pads (head_pad = 4):
# atom/model_ops/attention_mla.py:342
if self.head_pad > 0:
return torch.nn.functional.pad(q, (0, 0, 0, self.head_pad))
So q arrives with 16 heads of which only the first 12 carry data, and o
comes back with 16 of which 4 are discarded. Every QK^T and PV MFMA tile wastes
25% of its issue slots. A kernel accepting 12 heads natively removes this
directly.
5.2 The kernel runs with page_size = 1
Although vLLM is configured with --block-size 128, the decode side passes a
token-major view — kv_cache is (5283840, 1, 576), one entry per token
slot, not per block:
kv_buffer = kv_c_and_k_pe_cache.unsqueeze(2)
mla_decode_fwd(q, kv_buffer.view(-1, 1, 1, q.shape[-1]), o, ...)
Consequently kv_indices is a per-token index list, kv_last_page_lens is
all ones, and kv_indptr[i+1] - kv_indptr[i] is request i's KV length in
tokens (~68,400), not its block count. At c24 the kernel gathers 1,642,410
individual token indices per layer per step (6.3 MiB of int32 index traffic
per layer, ~150 MiB per decode step across 24 layers).
Block-granular addressing (page_size 128, matching the allocator) would cut
index traffic by 128x and allow contiguous 128-token loads instead of a gather.
Worth confirming on your side: the persistent work partitioner is built by
get_mla_metadata_v1 with page_size=self.block_size and
kv_granularity=max(self.block_size, 16) from kv_cache_spec.block_size, while
the decode call itself uses page_size=1. The kernel side is unambiguously
page_size 1 (token-count indptr deltas, all-ones last-page lens); whether the
work-splitting metadata used the same granularity is unchecked.
5.3 Strongly memory-bound
Per MLA layer, per GPU, at the captured late steps:
|
c16 |
c24 |
| gathered KV token slots |
1,094,960 |
1,642,410 |
| KV bytes read (fp8, 576 B/entry) |
601 MiB |
902 MiB |
| index bytes read (int32) |
4.2 MiB |
6.3 MiB |
| Q in / O out |
144 KiB / 256 KiB |
216 KiB / 384 KiB |
| FLOPs (padded 16 heads) |
38.1 GFLOP |
57.2 GFLOP |
| FLOPs actually useful (12 heads) |
28.6 GFLOP |
42.9 GFLOP |
| arithmetic intensity |
~60 FLOP/byte |
~60 FLOP/byte |
| per decode step, all 24 MLA layers |
15.2 GB |
22.9 GB |
| HBM-bound floor @ 8 TB/s |
1.91 ms/step |
2.86 ms/step |
MI355X machine balance is roughly 625 FLOP/byte for dense FP8, so at ~60
FLOP/byte this kernel is an order of magnitude away from compute bound.
Effort should go to bytes moved and load efficiency, not MFMA utilization.
5.4 Possible cross-request KV reuse (unverified)
All requests share a 63,911-token prefix with prefix caching on, so the block
tables point at the same physical KV slots for the shared portion. Derived
from the configuration (499 shared 128-token blocks plus a ~4.4k-token unique
tail per request):
- c16: ~137k distinct slots vs 1,095k gathered → ~8.0x redundancy
- c24: ~173k distinct slots vs 1,642k gathered → ~9.5x redundancy
At 576 B/slot that is 75–95 MiB of distinct data, within MI355X's 256 MiB LLC.
If the kernel gathers per-request independently it re-reads the shared prefix
once per request; blocking the KV loop across requests (cascade / shared-prefix
attention) could cut HBM traffic substantially.
This ratio is derived from configuration, not measured — treat it as a
hypothesis to confirm.
5.5 Other fixed properties
max_seqlen_q = 1 — pure decode, no speculative decoding or MTP.
return_lse = False, dcp_world_size = 1 — no decode context parallelism, so
no LSE correction path.
- Persistent split-KV scheduler active:
work_indptr(257) = 256 work
partitions, work_info_set(319, 8), reduce metadata reduce_indptr(65) /
reduce_final_map(64, 2) / reduce_partial_map(319). The 64 is
max_num_seqs, not the live batch.
fold_factor = None — head-folding path not taken.
- KV cache allocation is 5,283,840 slots x 576 B = 3.04 GB per layer per
GPU, ~73 GB across the 24 MLA layers.
- KV lengths are near-uniform across requests (within ~10 tokens), so
per-request work distribution is naturally balanced.
- Eager mode was used for clean attribution, so no latency figures are quoted;
they would not represent production.
Kimi-K3 MLA decode kernel — input specification
Measured on a live vLLM-ATOM server on 8x MI355X (gfx950): every argument of
aiter.mla.mla_decode_fwdon the decode path, with shape, dtype, stride andsize. Values are from the tail of a 350-token generation (
kv_len ≈ 68,43x),not from the first decode steps.
1. Environment
KimiK3ForConditionalGeneration, 93 layers--tensor-parallel-size 8torch.compilefp8→float8_e4m3fnamd-aiter 0.1.20.dev41+g334fd72ad334fd72ad6e69d16785efc97cb8b6b4aec3c2a92(main, 2026-08-03)0.25.2.dev0+g752a3a504.rocm724752a3a504485790a2e8491cacbb35c137339ad340.1.6rc1.dev219452a0aca281b0317fde12f98e55a803e8c88e753(k3-rmsnorm-fusion)2.10.0+rocm7.2.43d3aa8333.7.0+amd.rocm7.2.0890024107.2.53211gfx950:sramecc+:xnack-2. K3 MLA geometry
K3 is a hybrid stack: 69 KDA (gated-delta) layers and 24 full-attention MLA
layers. MLA runs in absorbed (latent) form, so the kernel attends over a
single 576-dim latent entry per token rather than per-head K/V.
num_attention_heads(global)kv_lora_rankqk_nope_head_dim/qk_rope_head_dimqk_head_dimv_head_dimsm_scale0.07216878364870322= 1/sqrt(192)All four captured layers produced byte-identical argument signatures, so one
table describes all 24 MLA layers.
3. Entry point
aiter.mla.mla_decode_fwd,aiter/mla.py:270:Called from
AttentionForVllmMLA._forward_decode. Two defaults are notoverridden and matter:
page_sizestays 1 (see 4.2) andnhead_kvstays1 (single latent KV head).
The binary dispatched on this configuration:
a8w8= fp8 Q and fp8 KV,qh16= 16 query heads,qseqlen1= single-tokendecode,
ps= persistent. Theqh16specialization is why 12 heads get paddedto 16 (see 4.1).
The persistent (split-KV) path is always taken: the concurrency gate in
_use_persistent_mla_decode(aiter/mla.py:245) only downgrades to thenon-persistent kernel for the bf16 Q + bf16 KV 16-head profile, and this
deployment is fp8/fp8.
qis produced byaiter.fused_qk_rope_concat_and_cache_mla, which also writesthe step's new KV entry into the same cache the kernel reads. The 512-dim latent
ois then mapped back tov_head_dimby a separate BMM. Those surrounding opsare out of scope here.
4. Arguments
contiguousflags a strided view;sizeis the tensor's full byte footprint.Both
qandoare contiguous.4.1 Concurrency 16
q(16, 16, 576)float8_e4m3fn(9216, 576, 1)kv_buffer(5283840, 1, 1, 576)float8_e4m3fn(576, 576, 576, 1)o(16, 16, 512)bfloat16(8192, 512, 1)qo_indptr(17)int32(1,)kv_indptr(17)int32(1,)kv_indices(4718592)int32(1,)kv_last_page_lens(16)int32(1,)max_seqlen_q1sm_scale0.07216878364870322q_scalescalarfloat32()kv_scalescalarfloat32()work_meta_data(2)uint64(1,)work_indptr(257)int32(1,)work_info_set(319, 8)int32(8, 1)reduce_indptr(65)int32(1,)reduce_final_map(64, 2)int32(2, 1)reduce_partial_map(319)int32(1,)return_lseFalseBatch:
num_decodes = 16,num_decode_tokens = 16,num_prefills = 0(puredecode).
num_heads12 before padding, 16 after.fold_factor = None,use_persistent_mode = True,dcp_world_size = 1.Per-request KV length over the run: 68,090 → 68,437 tokens
(
kv_len_sum1,089,472 → 1,094,960),q_lenalways 1.Late decode steps (
stepcounts MLA-layer forwards, so consecutive model stepsare 24 apart):
4.2 Concurrency 24
q(24, 16, 576)float8_e4m3fn(9216, 576, 1)kv_buffer(5283840, 1, 1, 576)float8_e4m3fn(576, 576, 576, 1)o(24, 16, 512)bfloat16(8192, 512, 1)qo_indptr(25)int32(1,)kv_indptr(25)int32(1,)kv_indices(4718592)int32(1,)kv_last_page_lens(24)int32(1,)max_seqlen_q1sm_scale0.07216878364870322q_scalescalarfloat32()kv_scalescalarfloat32()work_meta_data(2)uint64(1,)work_indptr(257)int32(1,)work_info_set(319, 8)int32(8, 1)reduce_indptr(65)int32(1,)reduce_final_map(64, 2)int32(2, 1)reduce_partial_map(319)int32(1,)return_lseFalseBatch:
num_decodes = 24,num_decode_tokens = 24,num_prefills = 0. Samehead padding, persistent mode and fold settings as c16.
Per-request KV length over the run: 68,089 → 68,438 tokens
(
kv_len_sum1,634,250 → 1,642,410),q_lenalways 1.Late decode steps:
5. Notes for optimization
5.1 12 real heads padded to 16 — 25% of the Q tile is zeros
_MLA_MIN_HEADS = 16inatom/model_ops/attention_mla.py:109("AITER MLAkernels require at least 16 attention heads"). K3 at TP8 has 96/8 = 12
heads. 16 is not a multiple of 12, so head-repeat cannot be used and the code
zero-pads (
head_pad = 4):So
qarrives with 16 heads of which only the first 12 carry data, andocomes back with 16 of which 4 are discarded. Every QK^T and PV MFMA tile wastes
25% of its issue slots. A kernel accepting 12 heads natively removes this
directly.
5.2 The kernel runs with page_size = 1
Although vLLM is configured with
--block-size 128, the decode side passes atoken-major view —
kv_cacheis(5283840, 1, 576), one entry per tokenslot, not per block:
Consequently
kv_indicesis a per-token index list,kv_last_page_lensisall ones, and
kv_indptr[i+1] - kv_indptr[i]is request i's KV length intokens (~68,400), not its block count. At c24 the kernel gathers 1,642,410
individual token indices per layer per step (6.3 MiB of int32 index traffic
per layer, ~150 MiB per decode step across 24 layers).
Block-granular addressing (page_size 128, matching the allocator) would cut
index traffic by 128x and allow contiguous 128-token loads instead of a gather.
Worth confirming on your side: the persistent work partitioner is built by
get_mla_metadata_v1withpage_size=self.block_sizeandkv_granularity=max(self.block_size, 16)fromkv_cache_spec.block_size, whilethe decode call itself uses
page_size=1. The kernel side is unambiguouslypage_size 1 (token-count indptr deltas, all-ones last-page lens); whether the
work-splitting metadata used the same granularity is unchecked.
5.3 Strongly memory-bound
Per MLA layer, per GPU, at the captured late steps:
MI355X machine balance is roughly 625 FLOP/byte for dense FP8, so at ~60
FLOP/byte this kernel is an order of magnitude away from compute bound.
Effort should go to bytes moved and load efficiency, not MFMA utilization.
5.4 Possible cross-request KV reuse (unverified)
All requests share a 63,911-token prefix with prefix caching on, so the block
tables point at the same physical KV slots for the shared portion. Derived
from the configuration (499 shared 128-token blocks plus a ~4.4k-token unique
tail per request):
At 576 B/slot that is 75–95 MiB of distinct data, within MI355X's 256 MiB LLC.
If the kernel gathers per-request independently it re-reads the shared prefix
once per request; blocking the KV loop across requests (cascade / shared-prefix
attention) could cut HBM traffic substantially.
This ratio is derived from configuration, not measured — treat it as a
hypothesis to confirm.
5.5 Other fixed properties
max_seqlen_q = 1— pure decode, no speculative decoding or MTP.return_lse = False,dcp_world_size = 1— no decode context parallelism, sono LSE correction path.
work_indptr(257)= 256 workpartitions,
work_info_set(319, 8), reduce metadatareduce_indptr(65)/reduce_final_map(64, 2)/reduce_partial_map(319). The 64 ismax_num_seqs, not the live batch.fold_factor = None— head-folding path not taken.GPU, ~73 GB across the 24 MLA layers.
per-request work distribution is naturally balanced.
they would not represent production.