[Kimi-K3][Perf] Make native CUDA AttnRes the SM100 default (#54261)
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
Yongye Zhu committed
Aug 31, 2026 at 16:18 UTC
d6d665854314f0aa90ad6ef32a3382136c71314c
7 files changed
+415
-94
benchmarks/kernels/benchmark_kimi_k3_attn_res.py
new
+235
@@ -0,0 +1,235 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Benchmark vLLM Kimi-K3 AttnRes kernels on NVIDIA Blackwell.
4
+
5
+The benchmark uses a zero delta so repeated in-place launches remain stable.
6
+
7
+Example:
8
+
9
+.. code-block:: console
10
+
11
+ .venv/bin/python benchmarks/kernels/benchmark_kimi_k3_attn_res.py \
12
+ --output /tmp/attn_res.json
13
+"""
14
+
15
+from __future__ import annotations
16
+
17
+import argparse
18
+import json
19
+from collections.abc import Callable
20
+from dataclasses import asdict, dataclass
21
+from functools import partial
22
+from pathlib import Path
23
+
24
+import torch
25
+
26
+import vllm._custom_ops # noqa: F401 - loads torch.ops._C
27
+from vllm.models.kimi_k3.nvidia.ops.attn_res import _attn_res_kernel
28
+from vllm.platforms import current_platform
29
+from vllm.triton_utils import triton
30
+
31
+HIDDEN_SIZE = 7168
32
+MAX_BLOCKS = 8
33
+EPS = 1e-5
34
+
35
+
36
+@dataclass
37
+class Inputs:
38
+ prefix: torch.Tensor
39
+ delta: torch.Tensor
40
+ blocks: torch.Tensor
41
+ norm_weight: torch.Tensor
42
+ qk_weight: torch.Tensor
43
+ output_norm_weight: torch.Tensor
44
+
45
+
46
+@dataclass
47
+class Result:
48
+ num_tokens: int
49
+ num_blocks: int
50
+ backend: str
51
+ median_us: float
52
+ p20_us: float
53
+ p80_us: float
54
+
55
+
56
+def parse_args() -> argparse.Namespace:
57
+ parser = argparse.ArgumentParser(description=__doc__)
58
+ parser.add_argument(
59
+ "--num-tokens",
60
+ type=int,
61
+ nargs="+",
62
+ default=[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192],
63
+ )
64
+ parser.add_argument("--num-blocks", type=int, nargs="+", default=[1, 4, 8])
65
+ parser.add_argument("--warmup-ms", type=int, default=50)
66
+ parser.add_argument("--repeat-ms", type=int, default=200)
67
+ parser.add_argument("--output", type=Path)
68
+ return parser.parse_args()
69
+
70
+
71
+def make_inputs(num_tokens: int) -> Inputs:
72
+ prefix = torch.randn(num_tokens, HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16)
73
+ blocks = torch.randn(
74
+ num_tokens,
75
+ MAX_BLOCKS,
76
+ HIDDEN_SIZE,
77
+ device="cuda",
78
+ dtype=torch.bfloat16,
79
+ )
80
+ norm_weight = torch.ones(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16)
81
+ qk_weight = (
82
+ torch.randn(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16) / HIDDEN_SIZE**0.5
83
+ )
84
+ output_norm_weight = 1 + 0.1 * torch.randn(
85
+ HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16
86
+ )
87
+ return Inputs(
88
+ prefix=prefix,
89
+ delta=torch.zeros_like(prefix),
90
+ blocks=blocks,
91
+ norm_weight=norm_weight,
92
+ qk_weight=qk_weight,
93
+ output_norm_weight=output_norm_weight,
94
+ )
95
+
96
+
97
+def launch_triton(inputs: Inputs, output: torch.Tensor, num_blocks: int) -> None:
98
+ num_tokens = inputs.prefix.shape[0]
99
+ if num_tokens >= 256 or num_blocks <= 1:
100
+ block_l, num_warps = 1, 4
101
+ else:
102
+ block_l, num_warps = 4, 8
103
+ _attn_res_kernel[(num_tokens,)](
104
+ inputs.prefix,
105
+ inputs.delta,
106
+ inputs.blocks,
107
+ inputs.norm_weight,
108
+ inputs.qk_weight,
109
+ inputs.output_norm_weight,
110
+ output,
111
+ inputs.prefix.stride(0),
112
+ inputs.delta.stride(0),
113
+ inputs.blocks.stride(0),
114
+ inputs.blocks.stride(1),
115
+ output.stride(0),
116
+ num_blocks,
117
+ HIDDEN_SIZE,
118
+ -1,
119
+ EPS,
120
+ EPS,
121
+ HAS_DELTA=True,
122
+ WRITE_BLOCK=False,
123
+ APPLY_OUTPUT_NORM=True,
124
+ BLOCK_L=block_l,
125
+ BLOCK_D=triton.next_power_of_2(HIDDEN_SIZE),
126
+ num_warps=num_warps,
127
+ num_stages=2,
128
+ launch_pdl=current_platform.is_arch_support_pdl(),
129
+ )
130
+
131
+
132
+def launch_cuda(inputs: Inputs, output: torch.Tensor, num_blocks: int) -> None:
133
+ torch.ops._C.kimi_k3_attn_res(
134
+ inputs.prefix,
135
+ inputs.delta,
136
+ inputs.blocks,
137
+ inputs.norm_weight,
138
+ inputs.qk_weight,
139
+ inputs.output_norm_weight,
140
+ output,
141
+ num_blocks,
142
+ -1,
143
+ EPS,
144
+ EPS,
145
+ )
146
+
147
+
148
+def benchmark(
149
+ function: Callable[[], None], warmup_ms: int, repeat_ms: int
150
+) -> tuple[float, float, float]:
151
+ median_ms, p20_ms, p80_ms = triton.testing.do_bench(
152
+ function,
153
+ warmup=warmup_ms,
154
+ rep=repeat_ms,
155
+ quantiles=[0.5, 0.2, 0.8],
156
+ )
157
+ return median_ms * 1000, p20_ms * 1000, p80_ms * 1000
158
+
159
+
160
+def validate(inputs: Inputs, num_blocks: int) -> None:
161
+ outputs = [torch.empty_like(inputs.prefix) for _ in range(2)]
162
+ launch_triton(inputs, outputs[0], num_blocks)
163
+ launch_cuda(inputs, outputs[1], num_blocks)
164
+ torch.accelerator.synchronize()
165
+ torch.testing.assert_close(outputs[1], outputs[0], atol=8e-2, rtol=3e-2)
166
+
167
+
168
+def main() -> None:
169
+ args = parse_args()
170
+ if torch.cuda.get_device_capability()[0] != 10:
171
+ raise SystemExit("The vLLM CUDA AttnRes kernel requires the SM100 family")
172
+ if not hasattr(torch.ops._C, "kimi_k3_attn_res"):
173
+ raise SystemExit("This vLLM build does not contain kimi_k3_attn_res")
174
+ if any(not 0 <= blocks <= MAX_BLOCKS for blocks in args.num_blocks):
175
+ raise SystemExit(f"--num-blocks values must be in [0, {MAX_BLOCKS}]")
176
+
177
+ torch.manual_seed(0)
178
+ check_inputs = make_inputs(5)
179
+ for num_blocks in args.num_blocks:
180
+ validate(check_inputs, num_blocks)
181
+ print("correctness: all selected block counts passed", flush=True)
182
+
183
+ results: list[Result] = []
184
+ for num_blocks in args.num_blocks:
185
+ for num_tokens in args.num_tokens:
186
+ inputs = make_inputs(num_tokens)
187
+ output = torch.empty_like(inputs.prefix)
188
+ candidates = {
189
+ "vllm_triton": partial(launch_triton, inputs, output, num_blocks),
190
+ "vllm_cuda": partial(launch_cuda, inputs, output, num_blocks),
191
+ }
192
+ row: dict[str, float] = {}
193
+ for backend, candidate in candidates.items():
194
+ median_us, p20_us, p80_us = benchmark(
195
+ candidate, args.warmup_ms, args.repeat_ms
196
+ )
197
+ results.append(
198
+ Result(
199
+ num_tokens=num_tokens,
200
+ num_blocks=num_blocks,
201
+ backend=backend,
202
+ median_us=median_us,
203
+ p20_us=p20_us,
204
+ p80_us=p80_us,
205
+ )
206
+ )
207
+ row[backend] = median_us
208
+ print(
209
+ f"blocks={num_blocks} tokens={num_tokens:5d} "
210
+ f"triton={row['vllm_triton']:9.2f} us "
211
+ f"cuda={row['vllm_cuda']:9.2f} us",
212
+ flush=True,
213
+ )
214
+
215
+ if args.output is not None:
216
+ metadata = {
217
+ "device": torch.cuda.get_device_name(),
218
+ "capability": torch.cuda.get_device_capability(),
219
+ "torch": torch.__version__,
220
+ "cuda": torch.version.cuda,
221
+ "warmup_ms": args.warmup_ms,
222
+ "repeat_ms": args.repeat_ms,
223
+ }
224
+ args.output.write_text(
225
+ json.dumps(
226
+ {"metadata": metadata, "results": [asdict(item) for item in results]},
227
+ indent=2,
228
+ )
229
+ + "\n"
230
+ )
231
+ print(f"wrote {args.output}")
232
+
233
+
234
+if __name__ == "__main__":
235
+ main()
csrc/libtorch_stable/kimi_k3/attn_res_kernel.cu
+104
-77
@@ -10,7 +10,7 @@
10
// - Q=res_weight*rms_weight remains in registers across persistent tokens.
11
// - V rows are converted once and cached as FP32 in TMEM between passes.
12
//
13
-// Integration contract: Kimi K3 H=7168, 1<=num_blocks<=8, and token-major
13
+// Integration contract: Kimi K3 H=7168, 0<=num_blocks<=8, and token-major
14
// block residual storage.
15
16
#include "../torch_utils.h"
@@ -230,14 +230,15 @@ __device__ __forceinline__ void cp_async_bulk(void* smem_dst,
230
}
231
232
template <int H, int N, int NC = N_CHUNK_DEFAULT, int B = 1,
233
- bool RELEASE_TMEM = false, bool HAS_DELTA = false,
234
- bool HAS_OUTPUT_NORM = false, bool OUTPUT_NORM_IN_SMEM = false>
233
+ bool RELEASE_TMEM = false, bool STATIC_COMMON_PATH = false,
234
+ bool HAS_OUTPUT_NORM = false>
235
__global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
236
- const bf16_t* __restrict__ block_res, bf16_t* __restrict__ layer_res,
236
+ bf16_t* __restrict__ block_res, bf16_t* __restrict__ layer_res,
237
const bf16_t* __restrict__ delta, const bf16_t* __restrict__ res_w,
238
const bf16_t* __restrict__ rms_w, bf16_t* __restrict__ output, int T,
239
int block_stride_m, int block_stride_r, float rms_eps,
240
- const bf16_t* __restrict__ output_norm_weight, float output_norm_eps) {
240
+ const bf16_t* __restrict__ output_norm_weight, float output_norm_eps,
241
+ int block_write_idx) {
242
#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 1000 && __CUDA_ARCH__ < 1100
243
constexpr float LOG2_E = 1.4426950408889634f;
244
constexpr int N_CHUNK = NC;
@@ -254,12 +255,15 @@ __global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
255
static_assert(TMEM_V_COLS_TOTAL <= TMEM_COLS_ALLOC);
256
static_assert(H >= 4096 && H <= 8192);
257
static_assert(H % K_TILE == 0);
258
+ static_assert(!STATIC_COMMON_PATH || HAS_OUTPUT_NORM);
259
260
const int tid = threadIdx.x;
261
const int wid = tid >> 5;
262
const int lane = tid & 31;
263
const int TB = T * B;
264
const int num_ctas = gridDim.x;
265
+ const bool has_delta = STATIC_COMMON_PATH || delta != nullptr;
266
+ const int write_idx = STATIC_COMMON_PATH ? -1 : block_write_idx;
267
constexpr int num_chunks = (N + N_CHUNK - 1) / N_CHUNK;
268
269
const int comp_wid = wid - 1;
@@ -270,10 +274,9 @@ __global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
274
const int k_local = ct_in_group * VEC;
275
276
constexpr size_t V_BYTES = (size_t)NUM_BUFS * H * sizeof(bf16_t);
273
- constexpr size_t DELTA_BYTES =
274
- HAS_DELTA ? (size_t)CHUNK_DEPTH * H * sizeof(bf16_t) : 0;
277
+ constexpr size_t DELTA_BYTES = (size_t)CHUNK_DEPTH * H * sizeof(bf16_t);
278
constexpr size_t OUTPUT_NORM_BYTES =
276
- OUTPUT_NORM_IN_SMEM ? (size_t)H * sizeof(bf16_t) : 0;
279
+ HAS_OUTPUT_NORM ? (size_t)H * sizeof(bf16_t) : 0;
280
extern __shared__ __align__(16) char smem_raw[];
281
bf16_t* v_bufs = reinterpret_cast<bf16_t*>(smem_raw); // [NUM_BUFS][H]
282
bf16_t* delta_bufs = reinterpret_cast<bf16_t*>(smem_raw + V_BYTES);
@@ -297,15 +300,12 @@ __global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
300
mbarrier_init(plan.bar_ready[i], 1);
301
mbarrier_init(plan.bar_consumed[i], CONSUMER_WARPS);
302
}
300
- if constexpr (OUTPUT_NORM_IN_SMEM) {
303
+ if constexpr (HAS_OUTPUT_NORM) {
304
mbarrier_init(plan.bar_output_norm_ready, 1);
305
}
306
fence_mbarrier_init();
307
}
308
306
- // gdc wait BEFORE tmem alloc
307
- cudaGridDependencySynchronize();
308
-
309
if (wid == 1) {
310
tmem_allocate(TMEM_COLS_ALLOC, &plan.tmem_base);
311
if constexpr (RELEASE_TMEM) {
@@ -314,7 +314,7 @@ __global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
314
}
315
__syncthreads();
316
317
- if constexpr (OUTPUT_NORM_IN_SMEM) {
317
+ if constexpr (HAS_OUTPUT_NORM) {
318
if (wid == 0 && elect_one_sync()) {
319
mbarrier_expect_tx(plan.bar_output_norm_ready, H * (int)sizeof(bf16_t));
320
cp_async_bulk(output_norm_buf, output_norm_weight, H * sizeof(bf16_t),
@@ -365,6 +365,7 @@ __global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
365
if (wid == 0) {
366
if (elect_one_sync()) {
367
long long gci = 0;
368
+ bool pdl_waited = false;
369
for (int tb = blockIdx.x; tb < TB; tb += num_ctas) {
370
const int t = tb / B;
371
for (int ci = 0; ci < num_chunks; ci++, gci++) {
@@ -374,7 +375,7 @@ __global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
375
int pc = phase_of(gci);
376
mbarrier_wait(plan.bar_consumed[chunk_slot], pc ^ 1);
377
int transaction_bytes = an * H * (int)sizeof(bf16_t);
377
- if constexpr (HAS_DELTA) {
378
+ if (has_delta) {
379
int prefix_n = N - 1 - ns;
380
if (prefix_n >= 0 && prefix_n < an) {
381
transaction_bytes += H * (int)sizeof(bf16_t);
@@ -388,12 +389,20 @@ __global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
389
const bf16_t* src =
390
residual_addr(block_res, layer_res, ns + n, N, t,
391
block_stride_m, block_stride_r, H);
392
+ if (!has_delta && ns + n == N - 1 && !pdl_waited) {
393
+ cudaGridDependencySynchronize();
394
+ pdl_waited = true;
395
+ }
396
cp_async_bulk(buf_ptr(slot), src, H * sizeof(bf16_t),
397
plan.bar_ready[chunk_slot]);
398
}
394
- if constexpr (HAS_DELTA) {
399
+ if (has_delta) {
400
int prefix_n = N - 1 - ns;
401
if (prefix_n >= 0 && prefix_n < an) {
402
+ if (!pdl_waited) {
403
+ cudaGridDependencySynchronize();
404
+ pdl_waited = true;
405
+ }
406
cp_async_bulk(delta_buf_ptr(chunk_slot),
407
delta + (long long)tb * H, H * sizeof(bf16_t),
408
plan.bar_ready[chunk_slot]);
@@ -401,6 +410,7 @@ __global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
410
}
411
}
412
}
413
+ cudaTriggerProgrammaticLaunchCompletion();
414
}
415
} else {
416
float acc32[ACC_PER_THREAD] = {};
@@ -442,9 +452,9 @@ __global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
452
int2 vp =
453
*reinterpret_cast<const int2*>(buf_ptr(slot) + h_base);
454
auto* v2 = reinterpret_cast<__nv_bfloat162*>(&vp);
445
- if constexpr (HAS_DELTA) {
446
- int prefix_n = N - 1 - ns;
447
- if (n == prefix_n) {
455
+ int prefix_n = N - 1 - ns;
456
+ if (n == prefix_n && (has_delta || write_idx >= 0)) {
457
+ if (has_delta) {
458
const bf16_t* delta_ptr =
459
delta_buf_ptr(chunk_slot) + h_base;
460
#pragma unroll
@@ -456,6 +466,11 @@ __global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
466
*reinterpret_cast<int2*>(layer_res + (long long)tb * H +
467
h_base) = vp;
468
}
469
+ if (write_idx >= 0) {
470
+ *reinterpret_cast<int2*>(
471
+ block_res + (long long)(tb / B) * block_stride_m +
472
+ write_idx * block_stride_r + h_base) = vp;
473
+ }
474
}
475
float2 f[2] = {__bfloat1622float2(v2[0]),
476
__bfloat1622float2(v2[1])};
@@ -480,9 +495,9 @@ __global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
495
int slot = slot_of(gci, n);
496
int4 vp = *reinterpret_cast<const int4*>(buf_ptr(slot) + h_base);
497
auto* v2 = reinterpret_cast<__nv_bfloat162*>(&vp);
483
- if constexpr (HAS_DELTA) {
484
- int prefix_n = N - 1 - ns;
485
- if (n == prefix_n) {
498
+ int prefix_n = N - 1 - ns;
499
+ if (n == prefix_n && (has_delta || write_idx >= 0)) {
500
+ if (has_delta) {
501
const bf16_t* delta_ptr = delta_buf_ptr(chunk_slot) + h_base;
502
#pragma unroll
503
for (int j = 0; j < VEC / 2; j++) {
@@ -493,6 +508,11 @@ __global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
508
*reinterpret_cast<int4*>(layer_res + (long long)tb * H +
509
h_base) = vp;
510
}
511
+ if (write_idx >= 0) {
512
+ *reinterpret_cast<int4*>(
513
+ block_res + (long long)(tb / B) * block_stride_m +
514
+ write_idx * block_stride_r + h_base) = vp;
515
+ }
516
}
517
float2 f[4] = {
518
__bfloat1622float2(v2[0]), __bfloat1622float2(v2[1]),
@@ -791,11 +811,9 @@ __global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
811
}
812
813
if constexpr (HAS_OUTPUT_NORM) {
794
- if constexpr (OUTPUT_NORM_IN_SMEM) {
795
- // The immutable weight copy is acquired once, at its first use.
796
- if (tb == blockIdx.x) {
797
- mbarrier_wait(plan.bar_output_norm_ready, 0);
798
- }
814
+ // The immutable weight copy is acquired once, at its first use.
815
+ if (tb == blockIdx.x) {
816
+ mbarrier_wait(plan.bar_output_norm_ready, 0);
817
}
818
float output_sq = output_sq_pair.x + output_sq_pair.y;
819
#pragma unroll
@@ -826,9 +844,7 @@ __global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
844
auto* values = reinterpret_cast<bf16_t*>(&packed);
845
#pragma unroll
846
for (int j = 0; j < 4; j++) {
829
- const bf16_t* weight_ptr =
830
- OUTPUT_NORM_IN_SMEM ? output_norm_buf : output_norm_weight;
831
- float weight = __bfloat162float(weight_ptr[h_base + j]);
847
+ float weight = __bfloat162float(output_norm_buf[h_base + j]);
848
values[j] = __float2bfloat16(acc32[si * VEC + j] *
849
output_rsigma * weight);
850
}
@@ -843,9 +859,7 @@ __global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
859
auto* values = reinterpret_cast<bf16_t*>(&packed);
860
#pragma unroll
861
for (int j = 0; j < VEC; j++) {
846
- const bf16_t* weight_ptr =
847
- OUTPUT_NORM_IN_SMEM ? output_norm_buf : output_norm_weight;
848
- float weight = __bfloat162float(weight_ptr[h_base + j]);
862
+ float weight = __bfloat162float(output_norm_buf[h_base + j]);
863
values[j] =
864
__float2bfloat16(acc32[si * VEC + j] * output_rsigma * weight);
865
}
@@ -855,7 +869,6 @@ __global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
869
}
870
}
871
858
- cudaTriggerProgrammaticLaunchCompletion();
872
__syncthreads();
873
if (wid == 1) {
874
tmem_free(plan.tmem_base, TMEM_COLS_ALLOC);
@@ -868,23 +881,22 @@ __global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
881
}
882
883
template <int H, int N, int NC = N_CHUNK_DEFAULT, bool RELEASE_TMEM = false,
871
- bool HAS_DELTA = false, bool HAS_OUTPUT_NORM = false,
872
- bool OUTPUT_NORM_IN_SMEM = false>
873
-static void launch_fwd(const bf16_t* block_residual, bf16_t* layer_residual,
884
+ bool STATIC_COMMON_PATH = false, bool HAS_OUTPUT_NORM = false>
885
+static void launch_fwd(bf16_t* block_residual, bf16_t* layer_residual,
886
const bf16_t* delta, const bf16_t* res_weight,
887
const bf16_t* rms_weight, bf16_t* output, int T,
888
float rms_eps, int num_sm, cudaStream_t stream,
889
const bf16_t* output_norm_weight = nullptr,
890
float output_norm_eps = 0.f, int block_stride_m = 0,
879
- int block_stride_r = 0) {
891
+ int block_stride_r = 0, int block_write_idx = -1) {
892
constexpr size_t smem_size =
881
- ((size_t)CHUNK_DEPTH * (NC + (HAS_DELTA ? 1 : 0)) * H * sizeof(bf16_t) +
882
- (OUTPUT_NORM_IN_SMEM ? (size_t)H * sizeof(bf16_t) : 0) +
893
+ ((size_t)(CHUNK_DEPTH * (NC + 1) + (HAS_OUTPUT_NORM ? 1 : 0)) * H *
894
+ sizeof(bf16_t) +
895
sizeof(FwdSmemPlan<NC>) + 15) &
896
~size_t(15);
897
auto kernel =
886
- &attn_res_fwd_online_v2_kernel<H, N, NC, 1, RELEASE_TMEM, HAS_DELTA,
887
- HAS_OUTPUT_NORM, OUTPUT_NORM_IN_SMEM>;
898
+ &attn_res_fwd_online_v2_kernel<H, N, NC, 1, RELEASE_TMEM,
899
+ STATIC_COMMON_PATH, HAS_OUTPUT_NORM>;
900
static bool attrs_set = false;
901
if (!attrs_set) {
902
if (smem_size > 48 * 1024) {
@@ -907,20 +919,21 @@ static void launch_fwd(const bf16_t* block_residual, bf16_t* layer_residual,
919
cudaLaunchKernelEx(&config, kernel, block_residual, layer_residual, delta,
920
res_weight, rms_weight, output, T, block_stride_m,
921
block_stride_r, rms_eps, output_norm_weight,
910
- output_norm_eps);
922
+ output_norm_eps, block_write_idx);
923
}
924
925
} // namespace fwd_prod_v2
926
} // namespace sm100
927
928
void kimi_k3_attn_res(torch::stable::Tensor& prefix,
917
- torch::stable::Tensor const& delta,
918
- torch::stable::Tensor const& blocks,
929
+ std::optional<torch::stable::Tensor> delta,
930
+ torch::stable::Tensor& blocks,
931
torch::stable::Tensor const& norm_weight,
932
torch::stable::Tensor const& qk_weight,
921
- torch::stable::Tensor const& output_norm_weight,
933
+ std::optional<torch::stable::Tensor> output_norm_weight,
934
torch::stable::Tensor& output, int64_t num_blocks,
923
- double eps, double output_norm_eps) {
935
+ int64_t block_write_idx, double eps,
936
+ double output_norm_eps) {
937
int const num_tokens = static_cast<int>(prefix.size(0));
938
int const device = prefix.get_device_index();
939
torch::stable::accelerator::DeviceGuard const device_guard(device);
@@ -928,49 +941,57 @@ void kimi_k3_attn_res(torch::stable::Tensor& prefix,
941
STD_TORCH_CHECK(properties->major == 10,
942
"Kimi K3 AttnRes requires the SM100 family");
943
int64_t const hidden_size = prefix.size(1);
944
+ STD_TORCH_CHECK(hidden_size == 7168,
945
+ "Kimi K3 AttnRes requires hidden_size=7168, got ",
946
+ hidden_size);
947
STD_TORCH_CHECK(prefix.stride(0) == hidden_size &&
932
- delta.stride(0) == hidden_size &&
948
+ (!delta.has_value() || delta->stride(0) == hidden_size) &&
949
output.stride(0) == hidden_size,
950
"Kimi K3 AttnRes requires densely packed rows; got strides ",
935
- prefix.stride(0), ", ", delta.stride(0), ", ",
951
+ prefix.stride(0), ", ",
952
+ delta.has_value() ? delta->stride(0) : 0, ", ",
953
output.stride(0), " for hidden_size ", hidden_size);
954
+ STD_TORCH_CHECK(
955
+ num_blocks >= 0 && num_blocks <= 8 && num_blocks <= blocks.size(1),
956
+ "Kimi K3 AttnRes: num_blocks must be 0..min(8, blocks.size(1))");
957
+ STD_TORCH_CHECK(block_write_idx >= -1 && block_write_idx < blocks.size(1),
958
+ "Kimi K3 AttnRes: block_write_idx must be -1 or a valid "
959
+ "block row, got ",
960
+ block_write_idx);
961
+
962
+ if (num_tokens == 0) {
963
+ return;
964
+ }
965
966
using namespace sm100::fwd_prod_v2;
939
- // Two-source chunks and two resident CTAs are beneficial once setup is
940
- // amortized by the long, full eight-block prefill workload.
941
- if (num_blocks == 8 && num_tokens >= 4096) {
942
- launch_fwd<7168, 9, 2, true, true, true, true>(
943
- static_cast<bf16_t const*>(blocks.data_ptr()),
944
- static_cast<bf16_t*>(prefix.data_ptr()),
945
- static_cast<bf16_t const*>(delta.data_ptr()),
946
- static_cast<bf16_t const*>(qk_weight.data_ptr()),
947
- static_cast<bf16_t const*>(norm_weight.data_ptr()),
948
- static_cast<bf16_t*>(output.data_ptr()), num_tokens,
949
- static_cast<float>(eps), properties->multiProcessorCount,
950
- get_current_cuda_stream(device),
951
- static_cast<bf16_t const*>(output_norm_weight.data_ptr()),
952
- static_cast<float>(output_norm_eps), static_cast<int>(blocks.stride(0)),
953
- static_cast<int>(blocks.stride(1)));
954
- } else {
955
- auto dispatch = [&](auto nsrc_tok) {
956
- constexpr int NSRC = decltype(nsrc_tok)::value;
957
- launch_fwd<7168, NSRC, 3, false, true, true, true>(
958
- static_cast<bf16_t const*>(blocks.data_ptr()),
967
+ cudaStream_t stream = get_current_cuda_stream(device);
968
+ auto dispatch_blocks = [&](auto static_common_path, auto has_output_norm) {
969
+ constexpr bool STATIC_COMMON_PATH = decltype(static_common_path)::value;
970
+ constexpr bool HAS_OUTPUT_NORM = decltype(has_output_norm)::value;
971
+ auto dispatch = [&](auto source_count) {
972
+ constexpr int NUM_SOURCES = decltype(source_count)::value;
973
+ launch_fwd<7168, NUM_SOURCES, 3, false, STATIC_COMMON_PATH,
974
+ HAS_OUTPUT_NORM>(
975
+ static_cast<bf16_t*>(blocks.data_ptr()),
976
static_cast<bf16_t*>(prefix.data_ptr()),
960
- static_cast<bf16_t const*>(delta.data_ptr()),
977
+ delta.has_value() ? static_cast<bf16_t const*>(delta->data_ptr())
978
+ : nullptr,
979
static_cast<bf16_t const*>(qk_weight.data_ptr()),
980
static_cast<bf16_t const*>(norm_weight.data_ptr()),
981
static_cast<bf16_t*>(output.data_ptr()), num_tokens,
964
- static_cast<float>(eps), properties->multiProcessorCount,
965
- get_current_cuda_stream(device),
966
- static_cast<bf16_t const*>(output_norm_weight.data_ptr()),
982
+ static_cast<float>(eps), properties->multiProcessorCount, stream,
983
+ output_norm_weight.has_value()
984
+ ? static_cast<bf16_t const*>(output_norm_weight->data_ptr())
985
+ : nullptr,
986
static_cast<float>(output_norm_eps),
987
static_cast<int>(blocks.stride(0)),
969
- static_cast<int>(blocks.stride(1)));
988
+ static_cast<int>(blocks.stride(1)),
989
+ static_cast<int>(block_write_idx));
990
};
971
- // The source count is num_blocks + 1; specialising on it makes the chunk
972
- // count, per-chunk source count and prefix-chunk test compile-time.
991
switch (num_blocks) {
992
+ case 0:
993
+ dispatch(std::integral_constant<int, 1>{});
994
+ break;
995
case 1:
996
dispatch(std::integral_constant<int, 2>{});
997
break;
@@ -995,9 +1016,15 @@ void kimi_k3_attn_res(torch::stable::Tensor& prefix,
1016
case 8:
1017
dispatch(std::integral_constant<int, 9>{});
1018
break;
998
- default:
999
- STD_TORCH_CHECK(false, "Kimi K3 AttnRes: num_blocks must be 1..8");
1019
}
1020
+ };
1021
+ if (delta.has_value() && output_norm_weight.has_value() &&
1022
+ block_write_idx < 0) {
1023
+ dispatch_blocks(std::true_type{}, std::true_type{});
1024
+ } else if (output_norm_weight.has_value()) {
1025
+ dispatch_blocks(std::false_type{}, std::true_type{});
1026
+ } else {
1027
+ dispatch_blocks(std::false_type{}, std::false_type{});
1028
}
1029
cudaError_t const error = cudaGetLastError();
1030
STD_TORCH_CHECK(
csrc/libtorch_stable/ops.h
+5
-4
@@ -411,13 +411,14 @@ void fused_gdn_decode_post_conv_mtp(
411
412
#ifdef VLLM_ENABLE_KIMI_K3_ATTN_RES
413
void kimi_k3_attn_res(torch::stable::Tensor& prefix,
414
- torch::stable::Tensor const& delta,
415
- torch::stable::Tensor const& blocks,
414
+ std::optional<torch::stable::Tensor> delta,
415
+ torch::stable::Tensor& blocks,
416
torch::stable::Tensor const& norm_weight,
417
torch::stable::Tensor const& qk_weight,
418
- torch::stable::Tensor const& output_norm_weight,
418
+ std::optional<torch::stable::Tensor> output_norm_weight,
419
torch::stable::Tensor& output, int64_t num_blocks,
420
- double eps, double output_norm_eps);
420
+ int64_t block_write_idx, double eps,
421
+ double output_norm_eps);
422
#endif
423
424
// Sampler kernels (shared CUDA/ROCm)
csrc/libtorch_stable/torch_bindings.cpp
+5
-3
@@ -542,9 +542,11 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C, ops) {
542
#ifdef VLLM_ENABLE_KIMI_K3_ATTN_RES
543
ops.def(
544
"kimi_k3_attn_res("
545
- "Tensor! prefix, Tensor delta, Tensor blocks, Tensor norm_weight, "
546
- "Tensor qk_weight, Tensor output_norm_weight, Tensor! output, "
547
- "int num_blocks, float eps, float output_norm_eps) -> ()");
545
+ "Tensor! prefix, Tensor? delta, Tensor! blocks, Tensor norm_weight, "
546
+ "Tensor qk_weight, Tensor? output_norm_weight, Tensor! output, "
547
+ "int num_blocks, "
548
+ "int block_write_idx, float eps, "
549
+ "float output_norm_eps) -> ()");
550
#endif
551
552
// Apply repetition penalties to logits in-place.
tests/models/kimi_k3/test_attn_res.py
+53
@@ -1,6 +1,8 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
+import importlib
5
+
6
import pytest
7
import torch
8
import torch.nn.functional as F
@@ -12,6 +14,7 @@ from vllm.platforms import current_platform
14
HIDDEN_SIZE = 7168
15
MAX_BLOCKS = 8
16
EPS = 1e-5
17
+attn_res_module = importlib.import_module("vllm.models.kimi_k3.nvidia.ops.attn_res")
18
19
20
def _randn_with_row_padding(*shape: int, padding: int = 0) -> torch.Tensor:
@@ -60,6 +63,7 @@ def _reference(
63
pytest.param(17, 5, 7, True, True, "triton", id="triton-write-add"),
64
pytest.param(3, 8, 0, False, False, "triton", id="triton-full"),
65
pytest.param(3, 8, 0, False, True, "triton", id="triton-full-add"),
66
+ pytest.param(1, 4, 0, False, True, "nvidia", id="nvidia-single-token"),
67
pytest.param(320, 1, 0, False, True, "nvidia", id="nvidia-1"),
68
pytest.param(320, 4, 0, False, True, "nvidia", id="nvidia-4"),
69
pytest.param(320, 8, 0, False, True, "nvidia", id="nvidia-8"),
@@ -194,6 +198,55 @@ def test_attn_res_without_output_norm():
198
torch.testing.assert_close(actual, expected, atol=8e-2, rtol=3e-2)
199
200
201
+@pytest.mark.parametrize(
202
+ ("num_blocks", "has_delta", "has_output_norm", "write_block"),
203
+ [
204
+ (0, False, True, True),
205
+ (4, True, True, True),
206
+ (4, False, True, False),
207
+ (8, True, False, False),
208
+ ],
209
+)
210
+def test_sm100_variants_do_not_fall_back_to_triton(
211
+ monkeypatch,
212
+ num_blocks: int,
213
+ has_delta: bool,
214
+ has_output_norm: bool,
215
+ write_block: bool,
216
+):
217
+ if not current_platform.is_device_capability_family(100) or not hasattr(
218
+ torch.ops._C, "kimi_k3_attn_res"
219
+ ):
220
+ pytest.skip("native AttnRes requires an SM100 build")
221
+
222
+ class FailingKernel:
223
+ def __getitem__(self, grid):
224
+ raise AssertionError(f"unexpected Triton launch with grid {grid}")
225
+
226
+ monkeypatch.setattr(attn_res_module, "_attn_res_kernel", FailingKernel())
227
+ prefix = torch.randn(1, HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16)
228
+ delta = torch.zeros_like(prefix) if has_delta else None
229
+ blocks = torch.randn(
230
+ 1, MAX_BLOCKS, HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16
231
+ )
232
+ norm_weight = torch.ones(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16)
233
+ qk_weight = torch.randn_like(norm_weight) / HIDDEN_SIZE**0.5
234
+ output_norm_weight = torch.ones_like(norm_weight) if has_output_norm else None
235
+
236
+ attn_res(
237
+ prefix,
238
+ delta,
239
+ blocks,
240
+ norm_weight,
241
+ qk_weight,
242
+ output_norm_weight,
243
+ num_blocks,
244
+ num_blocks if write_block else -1,
245
+ EPS,
246
+ EPS,
247
+ )
248
+
249
+
250
@pytest.mark.parametrize("num_tokens", [0, 1, 17])
251
def test_fused_mtp_input(num_tokens: int):
252
positions = torch.arange(num_tokens, device="cuda")
vllm/_custom_ops.py
+4
-2
@@ -2904,12 +2904,13 @@ def concat_and_cache_mla_grouped(
2904
2905
def kimi_k3_attn_res(
2906
prefix: torch.Tensor,
2907
- delta: torch.Tensor,
2907
+ delta: torch.Tensor | None,
2908
blocks: torch.Tensor,
2909
norm_weight: torch.Tensor,
2910
qk_weight: torch.Tensor,
2911
- output_norm_weight: torch.Tensor,
2911
+ output_norm_weight: torch.Tensor | None,
2912
num_blocks: int,
2913
+ block_write_idx: int,
2914
eps: float,
2915
output_norm_eps: float,
2916
) -> torch.Tensor:
@@ -2923,6 +2924,7 @@ def kimi_k3_attn_res(
2924
output_norm_weight,
2925
output,
2926
num_blocks,
2927
+ block_write_idx,
2928
eps,
2929
output_norm_eps,
2930
)
vllm/models/kimi_k3/nvidia/ops/attn_res.py
+9
-8
@@ -186,16 +186,16 @@ def attn_res(
186
assert norm_weight.stride(-1) == 1
187
assert qk_weight.stride(-1) == 1
188
assert output_norm_weight is None or output_norm_weight.stride(-1) == 1
189
- # The in-tree NVIDIA kernel covers the common fused-add + output-norm path;
190
- # Triton handles block boundaries and final pre-norm output. The native op
191
- # is only compiled for SM100 under CUDA >= 13, so a device check alone is
192
- # not enough to know it exists.
189
+ # The native kernel covers every Kimi-K3 AttnRes variant on dense SM100
190
+ # inputs. The op is only compiled under CUDA >= 13, so a device check alone
191
+ # is not enough to know it exists.
192
if (
193
hidden_size == 7168
195
- and delta is not None
196
- and output_norm_weight is not None
197
- and num_blocks > 0
198
- and block_write_idx < 0
194
+ and prefix.stride(0) == hidden_size
195
+ and (delta is None or delta.stride(0) == hidden_size)
196
+ and 0 <= num_blocks <= 8
197
+ and num_blocks <= blocks.shape[1]
198
+ and -1 <= block_write_idx < blocks.shape[1]
199
and current_platform.is_device_capability_family(100)
200
and hasattr(torch.ops._C, "kimi_k3_attn_res")
201
):
@@ -207,6 +207,7 @@ def attn_res(
207
qk_weight,
208
output_norm_weight,
209
num_blocks,
210
+ block_write_idx,
211
eps,
212
output_norm_eps,
213
)