[Perf][Kimi-K3] Cut MLA decode concat/cache epilogue latency (#54896)

Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yongye Zhu
2026-09-03 15:39:58 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent c7e6e36fa9
commit 9509fc8ae6
2 changed files with 121 additions and 36 deletions
@@ -102,6 +102,9 @@ constexpr int kQkHeadDim = kQkNopeHeadDim + kQkRopeHeadDim; // 192
constexpr int kVHeadDim = 128; // V
constexpr int kCacheEntry = kKvLoraRank + kQkRopeHeadDim; // 576
constexpr int kVecElems = 8; // 8 bf16 == one uint4 load / one uint2 fp8 store
// Max token count for the decode epilogue's 3-warps-per-row split; larger
// batches have enough rows for one warp per row.
constexpr int kDecodeRowSplitMaxTokens = 64;
#if defined(USE_ROCM) && defined(__gfx942__)
constexpr float kFp8Max = 224.0f;
@@ -299,14 +302,18 @@ __device__ __forceinline__ void writePrefillQuery(
// Concat + store a 576-wide latent: dst[e] = [a512 | b64], e in [0, 576). Used
// for the decode query mqa_q = [ql_nope | q_pe] and the plain latent cache
// entry [kv_c | k_pe]. FP8 packs to E4M3 (dst_elem_size 1); bf16 stores uint4.
// A single warp passes (laneId, 32); a row split across W warps passes
// (part * 32 + laneId, W * 32). __restrict__ lets the compiler batch loads
// ahead of stores past the byte-typed dst.
template <typename scalar_t, bool FP8, bool APPLY_ROPE = false>
__device__ __forceinline__ void writeLatent576(void* dst, const scalar_t* a512,
const scalar_t* b64, int laneId,
int dst_elem_size,
float scale_inv,
const float* cos_sin = nullptr) {
__device__ __forceinline__ void writeLatent576(
void* __restrict__ dst, const scalar_t* __restrict__ a512,
const scalar_t* __restrict__ b64, int lane, int lane_stride,
int dst_elem_size, float scale_inv,
const float* __restrict__ cos_sin = nullptr) {
auto* d = reinterpret_cast<uint8_t*>(dst);
for (int e = laneId * kVecElems; e < kCacheEntry; e += 32 * kVecElems) {
for (int e = lane * kVecElems; e < kCacheEntry;
e += lane_stride * kVecElems) {
if (e < kKvLoraRank) {
copyChunk8<scalar_t, FP8>(d + e * dst_elem_size, a512 + e, scale_inv);
} else {
@@ -428,7 +435,7 @@ __global__ void fusedKimiK3MLAKeyConcatKVCacheInsertKernel(
(slot_id % cache_block_size) * cache_token_stride;
writeLatent576<scalar_t, false, APPLY_ROPE>(
row, kv_c + tokenIdx * kv_c_tok_stride,
k_pe + tokenIdx * k_pe_tok_stride, laneId, sizeof(scalar_t), 1.0f,
k_pe + tokenIdx * k_pe_tok_stride, laneId, 32, sizeof(scalar_t), 1.0f,
rope_cache);
}
}
@@ -512,7 +519,7 @@ __global__ void fusedKimiK3MLAQKVQuantKVCacheFp8Kernel(
(slot_id % cache_block_size) * cache_token_stride;
writeLatent576<scalar_t, true, APPLY_ROPE>(
row, kv_c + tokenIdx * kv_c_tok_stride,
k_pe + tokenIdx * k_pe_tok_stride, laneId, 1, ksi, rope_cache);
k_pe + tokenIdx * k_pe_tok_stride, laneId, 32, 1, ksi, rope_cache);
}
}
@@ -660,8 +667,20 @@ __global__ void fusedKimiK3MLAKeyConcatDsMlaInsertKernel(
// Decode epilogue: concat mqa_q = [ql_nope | q_pe] (576) + latent cache insert,
// run right before forward_mqa. Q_FP8 quantizes mqa_q; KV_FP8 quantizes the
// plain per-tensor cache. (ds_mla cache uses the separate kernel below.)
//
// SPLIT warps cooperate on each 576-wide row: decode batches have too few
// rows to hide the load->store latency with one warp per row, so batches up
// to kDecodeRowSplitMaxTokens launch SPLIT=3 (one 8-element chunk per lane).
//
// Only ql_nope / q_pe come from the producing GEMM chain, so everything else
// (slot_mapping, scales, rope table, cache-row inputs) is read before the
// grid-dependency wait, and the cache-slot warps skip the wait entirely.
// The dependent-launch trigger stays at the end of the kernel: it releases
// the consuming FMHA's grid-dependency wait, so it must not fire before this
// kernel's stores to mqa_q are issued.
// ────────────────────────────────────────────────────────────────────────────
template <typename scalar_t, bool Q_FP8, bool KV_FP8, bool APPLY_ROPE>
template <typename scalar_t, bool Q_FP8, bool KV_FP8, bool APPLY_ROPE,
int SPLIT = 1>
__global__ void fusedKimiK3MLADecodeQConcatKVCacheKernel(
const scalar_t* __restrict__ ql_nope, int64_t const qn_tok_stride,
int64_t const qn_head_stride, const scalar_t* __restrict__ q_pe,
@@ -683,13 +702,13 @@ __global__ void fusedKimiK3MLADecodeQConcatKVCacheKernel(
int const laneId = threadIdx.x % 32;
int const globalWarpIdx = blockIdx.x * warpsPerBlock + threadIdx.x / 32;
int const slotsPerToken = num_heads + 1;
int const tokenIdx = globalWarpIdx / slotsPerToken;
int const slotIdx = globalWarpIdx % slotsPerToken;
int const globalSlotIdx = globalWarpIdx / SPLIT;
int const part = globalWarpIdx % SPLIT;
int const tokenIdx = globalSlotIdx / slotsPerToken;
int const slotIdx = globalSlotIdx % slotsPerToken;
if (tokenIdx >= num_tokens) return;
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900)
cudaGridDependencySynchronize();
#endif
int const lane = part * 32 + laneId;
constexpr int kLaneStride = SPLIT * 32;
const float* rope_cache = nullptr;
if constexpr (APPLY_ROPE) {
@@ -698,12 +717,18 @@ __global__ void fusedKimiK3MLADecodeQConcatKVCacheKernel(
if (slotIdx < num_heads) {
float const qsi = Q_FP8 ? __ldg(q_scale_inv) : 1.0f;
writeLatent576<scalar_t, Q_FP8, APPLY_ROPE>(
uint8_t* const dst =
reinterpret_cast<uint8_t*>(mqa_q) +
(tokenIdx * mq_tok_stride + slotIdx * mq_head_stride) * kMqElem,
ql_nope + tokenIdx * qn_tok_stride + slotIdx * qn_head_stride,
q_pe + tokenIdx * qpe_tok_stride + slotIdx * qpe_head_stride, laneId,
kMqElem, qsi, rope_cache);
(tokenIdx * mq_tok_stride + slotIdx * mq_head_stride) * kMqElem;
const scalar_t* const nope_src =
ql_nope + tokenIdx * qn_tok_stride + slotIdx * qn_head_stride;
const scalar_t* const pe_src =
q_pe + tokenIdx * qpe_tok_stride + slotIdx * qpe_head_stride;
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900)
cudaGridDependencySynchronize();
#endif
writeLatent576<scalar_t, Q_FP8, APPLY_ROPE>(
dst, nope_src, pe_src, lane, kLaneStride, kMqElem, qsi, rope_cache);
} else {
int64_t const slot_id = slot_mapping[tokenIdx];
if (slot_id >= 0) {
@@ -714,7 +739,7 @@ __global__ void fusedKimiK3MLADecodeQConcatKVCacheKernel(
slot_id % cache_block_size * cache_token_stride) *
kCacheElem,
kv_c + tokenIdx * kv_c_tok_stride, k_pe + tokenIdx * k_pe_tok_stride,
laneId, kCacheElem, ksi, rope_cache);
lane, kLaneStride, kCacheElem, ksi, rope_cache);
}
}
@@ -760,7 +785,7 @@ __global__ void fusedKimiK3MLADecodeQConcatDsMlaKernel(
mqa_q + tokenIdx * mq_tok_stride + slotIdx * mq_head_stride,
ql_nope + tokenIdx * qn_tok_stride + slotIdx * qn_head_stride,
q_pe + tokenIdx * qpe_tok_stride + slotIdx * qpe_head_stride, laneId,
sizeof(scalar_t), 1.0f, rope_cache);
32, sizeof(scalar_t), 1.0f, rope_cache);
} else {
int64_t const slot_id = slot_mapping[tokenIdx];
if (slot_id >= 0) {
@@ -1367,9 +1392,9 @@ void fused_kimi_k3_mla_decode_q_concat_kv_cache_insert(
VLLM_STABLE_DISPATCH_HALF_TYPES(
dt, "fused_kimi_k3_mla_decode_q_concat_kv_cache_insert", [&] {
auto launch = [&](auto kernel) {
kk3::launchPdl(
kernel, num_tokens, num_heads, stream,
auto launch = [&](auto kernel, int row_split) {
kk3::launchPdlSlots(
kernel, num_tokens, (num_heads + 1) * row_split, 1, 0, stream,
reinterpret_cast<scalar_t const*>(ql_nope.const_data_ptr()),
ql_nope.stride(0), ql_nope.stride(1),
reinterpret_cast<scalar_t const*>(q_pe.const_data_ptr()),
@@ -1388,12 +1413,32 @@ void fused_kimi_k3_mla_decode_q_concat_kv_cache_insert(
cos_sin_cache.value().const_data_ptr())
: nullptr);
};
// Split decode-sized rows across 3 warps (see the kernel comment).
bool const split_rows = num_tokens <= kk3::kDecodeRowSplitMaxTokens;
if (apply_rope) {
launch(kk3::fusedKimiK3MLADecodeQConcatKVCacheKernel<scalar_t, false,
false, true>);
if (split_rows) {
launch(
kk3::fusedKimiK3MLADecodeQConcatKVCacheKernel<scalar_t, false,
false, true, 3>,
3);
} else {
launch(
kk3::fusedKimiK3MLADecodeQConcatKVCacheKernel<scalar_t, false,
false, true>,
1);
}
} else {
launch(kk3::fusedKimiK3MLADecodeQConcatKVCacheKernel<scalar_t, false,
false, false>);
if (split_rows) {
launch(
kk3::fusedKimiK3MLADecodeQConcatKVCacheKernel<scalar_t, false,
false, false, 3>,
3);
} else {
launch(
kk3::fusedKimiK3MLADecodeQConcatKVCacheKernel<scalar_t, false,
false, false>,
1);
}
}
});
}
@@ -1442,9 +1487,9 @@ void fused_kimi_k3_mla_decode_q_concat_kv_cache_fp8_insert(
VLLM_STABLE_DISPATCH_HALF_TYPES(
dt, "fused_kimi_k3_mla_decode_q_concat_kv_cache_fp8_insert", [&] {
auto launch = [&](auto kernel) {
kk3::launchPdl(
kernel, num_tokens, num_heads, stream,
auto launch = [&](auto kernel, int row_split) {
kk3::launchPdlSlots(
kernel, num_tokens, (num_heads + 1) * row_split, 1, 0, stream,
reinterpret_cast<scalar_t const*>(ql_nope.const_data_ptr()),
ql_nope.stride(0), ql_nope.stride(1),
reinterpret_cast<scalar_t const*>(q_pe.const_data_ptr()),
@@ -1464,12 +1509,29 @@ void fused_kimi_k3_mla_decode_q_concat_kv_cache_fp8_insert(
cos_sin_cache.value().const_data_ptr())
: nullptr);
};
// Split decode-sized rows across 3 warps (see the kernel comment).
bool const split_rows = num_tokens <= kk3::kDecodeRowSplitMaxTokens;
if (apply_rope) {
launch(kk3::fusedKimiK3MLADecodeQConcatKVCacheKernel<scalar_t, true,
true, true>);
if (split_rows) {
launch(kk3::fusedKimiK3MLADecodeQConcatKVCacheKernel<scalar_t, true,
true, true, 3>,
3);
} else {
launch(kk3::fusedKimiK3MLADecodeQConcatKVCacheKernel<scalar_t, true,
true, true>,
1);
}
} else {
launch(kk3::fusedKimiK3MLADecodeQConcatKVCacheKernel<scalar_t, true,
true, false>);
if (split_rows) {
launch(
kk3::fusedKimiK3MLADecodeQConcatKVCacheKernel<scalar_t, true,
true, false, 3>,
3);
} else {
launch(kk3::fusedKimiK3MLADecodeQConcatKVCacheKernel<scalar_t, true,
true, false>,
1);
}
}
});
}
@@ -285,3 +285,26 @@ def test_decode_epilogue_preserves_nope_path() -> None:
torch.testing.assert_close(
_cache_rows(cache, slots), torch.cat((kv_c, k_pe), dim=-1)
)
@torch.inference_mode()
@pytest.mark.parametrize("num_tokens", [64, 65])
def test_decode_epilogue_row_split_boundary(num_tokens: int) -> None:
"""Both sides of the M-based warps-per-row dispatch produce the same rows."""
torch.manual_seed(3)
num_blocks = (num_tokens + _BLOCK_SIZE - 1) // _BLOCK_SIZE
slots = torch.randperm(num_blocks * _BLOCK_SIZE, device="cuda")[:num_tokens].long()
ql_nope = _randn(num_tokens, _NUM_HEADS, 512)
q_pe = _randn(num_tokens, _NUM_HEADS, 64)
kv_c = _randn(num_tokens, 512)
k_pe = _randn(num_tokens, 64)
cache = torch.zeros(num_blocks, _BLOCK_SIZE, 576, device="cuda", dtype=_DTYPE)
q_actual = fused_mla_decode_q_concat_kv_cache_insert(
ql_nope, q_pe, kv_c, k_pe, cache, slots
)
torch.testing.assert_close(q_actual, torch.cat((ql_nope, q_pe), dim=-1))
torch.testing.assert_close(
_cache_rows(cache, slots), torch.cat((kv_c, k_pe), dim=-1)
)