[Kimi-K3] Add AttnRes kernels (#50090)

Signed-off-by: Thien Tran <gau.nernst@yahoo.com.sg>

Thien Tran committed Jul 28, 2026 at 15:22 UTC 61ac368021033dea54448d36ed98c7426e82cd18
14 files changed +1719
.buildkite/test_areas/models_basic.yaml
+12
@@ -61,6 +61,18 @@ steps:
61 # FA4 kernel tests require SM100; the suite skips them elsewhere.
62 - pytest -v -s models/inkling
63
64 +- label: Kimi K3 Unit Tests (B200)
65 + key: kimi-k3-unit-tests-b200
66 + timeout_in_minutes: 40
67 + device: b200-k8s
68 + source_file_dependencies:
69 + - vllm/models/kimi_k3/
70 + - csrc/libtorch_stable/kimi_k3/
71 + - tests/models/kimi_k3/
72 + commands:
73 + # The native NVIDIA AttnRes kernel requires the SM100 family.
74 + - pytest -v -s models/kimi_k3
75 +
76 - label: Basic Models Test (Other CPU) # 5min
77 key: basic-models-test-other-cpu
78 depends_on:
CMakeLists.txt
+22
@@ -1079,6 +1079,24 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
1079 set(MLA_ARCHS)
1080 endif()
1081
1082 + if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 13.0)
1083 + cuda_archs_loose_intersection(KIMI_K3_ATTN_RES_ARCHS
1084 + "10.0f" "${CUDA_ARCHS}")
1085 + endif()
1086 + if(KIMI_K3_ATTN_RES_ARCHS)
1087 + set(KIMI_K3_ATTN_RES_SRC
1088 + "csrc/libtorch_stable/kimi_k3/attn_res_kernel.cu")
1089 + set_gencode_flags_for_srcs(
1090 + SRCS "${KIMI_K3_ATTN_RES_SRC}"
1091 + CUDA_ARCHS "${KIMI_K3_ATTN_RES_ARCHS}")
1092 + set_property(SOURCE ${KIMI_K3_ATTN_RES_SRC} APPEND PROPERTY
1093 + COMPILE_OPTIONS
1094 + "$<$<COMPILE_LANGUAGE:CUDA>:--expt-relaxed-constexpr;--expt-extended-lambda;--use_fast_math>")
1095 + list(APPEND VLLM_STABLE_EXT_SRC "${KIMI_K3_ATTN_RES_SRC}")
1096 + message(STATUS
1097 + "Building Kimi K3 AttnRes for archs: ${KIMI_K3_ATTN_RES_ARCHS}")
1098 + endif()
1099 +
1100 # Hadacore kernels
1101 cuda_archs_loose_intersection(HADACORE_ARCHS "8.0+PTX;9.0+PTX" "${CUDA_ARCHS}")
1102 if(HADACORE_ARCHS)
@@ -1120,6 +1138,10 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
1138 target_compile_definitions(_C_stable_libtorch PRIVATE
1139 VLLM_ENABLE_COOPERATIVE_TOPK=1)
1140 endif()
1141 + if(KIMI_K3_ATTN_RES_ARCHS)
1142 + target_compile_definitions(_C_stable_libtorch PRIVATE
1143 + VLLM_ENABLE_KIMI_K3_ATTN_RES=1)
1144 + endif()
1145 # Needed by CUTLASS kernels
1146 target_compile_definitions(_C_stable_libtorch PRIVATE
1147 CUTLASS_ENABLE_DIRECT_CUDA_DRIVER_CALL=1)
csrc/libtorch_stable/kimi_k3/attn_res_kernel.cu new
+954
@@ -0,0 +1,954 @@
1 +/*
2 + * Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
3 + */
4 +
5 +// Production AttnRes forward for Blackwell (SM100).
6 +//
7 +// Warp-specialized online softmax + residual + RMSNorm:
8 +// - 1 producer warp issues cp.async.bulk row loads into shared memory.
9 +// - 8 consumer warps compute reductions and output.
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
14 +// block residual storage.
15 +
16 +#include "../torch_utils.h"
17 +
18 +#include <cfloat>
19 +#include <cstdint>
20 +#include <cstdio>
21 +#include <cuda_runtime.h>
22 +#include <type_traits>
23 +
24 +using bf16_t = __nv_bfloat16;
25 +
26 +namespace sm100 {
27 +namespace fwd_prod_v2 {
28 +
29 +constexpr int K_TILE = 1024;
30 +constexpr int N_CHUNK_DEFAULT = 4;
31 +constexpr int CHUNK_DEPTH = 2;
32 +constexpr int BLK = 288; // 1 producer warp + 8 consumer warps
33 +constexpr int CONSUMER_THREADS = BLK - 32; // 256
34 +constexpr int CONSUMER_WARPS = CONSUMER_THREADS / 32;
35 +constexpr int CONSUMER_GROUPS = 2; // two 128-thread consumer groups
36 +constexpr int CONSUMER_THREADS_PER_GROUP = CONSUMER_THREADS / CONSUMER_GROUPS;
37 +constexpr int FIRST_USER_NAMED_BARRIER = 8;
38 +
39 +__device__ __forceinline__ const bf16_t* residual_addr(
40 + const bf16_t* block_res, const bf16_t* layer_res, int source, int N,
41 + int token, int block_stride_m, int block_stride_r, int H) {
42 + if (source < N - 1) {
43 + return block_res + static_cast<long long>(token) * block_stride_m +
44 + source * block_stride_r;
45 + }
46 + return layer_res + static_cast<long long>(token) * H;
47 +}
48 +
49 +__device__ __forceinline__ uint32_t elect_one_sync() {
50 + uint32_t pred = 0;
51 + uint32_t laneid = 0;
52 + asm volatile(
53 + "{\n"
54 + ".reg .b32 %%rx;\n"
55 + ".reg .pred %%px;\n"
56 + " elect.sync %%rx|%%px, %2;\n"
57 + "@%%px mov.s32 %1, 1;\n"
58 + " mov.s32 %0, %%rx;\n"
59 + "}\n"
60 + : "+r"(laneid), "+r"(pred)
61 + : "r"(0xffffffff));
62 + return pred;
63 +}
64 +
65 +__device__ __forceinline__ void mbarrier_init(uint64_t& barrier,
66 + int thread_count) {
67 + uint32_t const barrier_addr =
68 + static_cast<uint32_t>(__cvta_generic_to_shared(&barrier));
69 + asm volatile("mbarrier.init.shared::cta.b64 [%0], %1;\n" ::"r"(barrier_addr),
70 + "r"(thread_count));
71 +}
72 +
73 +__device__ __forceinline__ void mbarrier_expect_tx(uint64_t& barrier,
74 + uint32_t bytes) {
75 + uint32_t const barrier_addr =
76 + static_cast<uint32_t>(__cvta_generic_to_shared(&barrier));
77 + asm volatile("mbarrier.arrive.expect_tx.shared::cta.b64 _, [%0], %1;\n" ::"r"(
78 + barrier_addr),
79 + "r"(bytes));
80 +}
81 +
82 +__device__ __forceinline__ void mbarrier_wait(uint64_t& barrier, int phase) {
83 + uint32_t const barrier_addr =
84 + static_cast<uint32_t>(__cvta_generic_to_shared(&barrier));
85 + asm volatile(
86 + "{\n"
87 + ".reg .pred p;\n"
88 + "WAIT:\n"
89 + "mbarrier.try_wait.parity.shared::cta.b64 p, [%0], %1;\n"
90 + "@p bra DONE;\n"
91 + "bra WAIT;\n"
92 + "DONE:\n"
93 + "}\n" ::"r"(barrier_addr),
94 + "r"(phase));
95 +}
96 +
97 +__device__ __forceinline__ void mbarrier_arrive(uint64_t& barrier) {
98 + uint32_t const barrier_addr =
99 + static_cast<uint32_t>(__cvta_generic_to_shared(&barrier));
100 + asm volatile(
101 + "{\n"
102 + ".reg .b64 state;\n"
103 + "mbarrier.arrive.shared::cta.b64 state, [%0];\n"
104 + "}\n" ::"r"(barrier_addr));
105 +}
106 +
107 +__device__ __forceinline__ void fence_mbarrier_init() {
108 + asm volatile("fence.mbarrier_init.release.cluster;" ::: "memory");
109 +}
110 +
111 +__device__ __forceinline__ void named_barrier_sync(uint32_t num_threads,
112 + uint32_t user_barrier_id) {
113 + asm volatile(
114 + "bar.sync %0, %1;" ::"r"(user_barrier_id + FIRST_USER_NAMED_BARRIER),
115 + "r"(num_threads)
116 + : "memory");
117 +}
118 +
119 +__device__ __forceinline__ void tmem_allocate(int num_columns, uint32_t* dst) {
120 + uint32_t const dst_addr =
121 + static_cast<uint32_t>(__cvta_generic_to_shared(dst));
122 + asm volatile(
123 + "tcgen05.alloc.cta_group::1.sync.aligned.shared::cta.b32 [%0], %1;" ::"r"(
124 + dst_addr),
125 + "r"(num_columns));
126 +}
127 +
128 +__device__ __forceinline__ void tmem_free(uint32_t tmem_ptr, int num_columns) {
129 + asm volatile(
130 + "tcgen05.dealloc.cta_group::1.sync.aligned.b32 %0, %1;" ::"r"(tmem_ptr),
131 + "r"(num_columns));
132 +}
133 +
134 +__device__ __forceinline__ void tmem_release_allocation_lock() {
135 + asm volatile("tcgen05.relinquish_alloc_permit.cta_group::1.sync.aligned;");
136 +}
137 +
138 +__device__ __forceinline__ void tmem_store_wait() {
139 + asm volatile("tcgen05.wait::st.sync.aligned;" ::: "memory");
140 +}
141 +
142 +template <int N, typename T>
143 +__device__ __forceinline__ void tmem_load(uint32_t src_addr, T* dst) {
144 + uint32_t* values = reinterpret_cast<uint32_t*>(dst);
145 + if constexpr (N == 8) {
146 + asm volatile(
147 + "tcgen05.ld.sync.aligned.32x32b.x8.b32"
148 + "{%0, %1, %2, %3, %4, %5, %6, %7}, [%8];\n"
149 + : "=r"(values[0]), "=r"(values[1]), "=r"(values[2]), "=r"(values[3]),
150 + "=r"(values[4]), "=r"(values[5]), "=r"(values[6]), "=r"(values[7])
151 + : "r"(src_addr));
152 + } else {
153 + static_assert(N == 4, "AttnRes TMEM helpers support x4 and x8");
154 + asm volatile(
155 + "tcgen05.ld.sync.aligned.32x32b.x4.b32"
156 + "{%0, %1, %2, %3}, [%4];\n"
157 + : "=r"(values[0]), "=r"(values[1]), "=r"(values[2]), "=r"(values[3])
158 + : "r"(src_addr));
159 + }
160 +}
161 +
162 +template <int N, typename T>
163 +__device__ __forceinline__ void tmem_store(uint32_t dst_addr, T* src) {
164 + uint32_t* values = reinterpret_cast<uint32_t*>(src);
165 + if constexpr (N == 8) {
166 + asm volatile(
167 + "tcgen05.st.sync.aligned.32x32b.x8.b32"
168 + "[%8], {%0, %1, %2, %3, %4, %5, %6, %7};\n" ::"r"(values[0]),
169 + "r"(values[1]), "r"(values[2]), "r"(values[3]), "r"(values[4]),
170 + "r"(values[5]), "r"(values[6]), "r"(values[7]), "r"(dst_addr));
171 + } else {
172 + static_assert(N == 4, "AttnRes TMEM helpers support x4 and x8");
173 + asm volatile(
174 + "tcgen05.st.sync.aligned.32x32b.x4.b32"
175 + "[%4], {%0, %1, %2, %3};\n" ::"r"(values[0]),
176 + "r"(values[1]), "r"(values[2]), "r"(values[3]), "r"(dst_addr));
177 + }
178 +}
179 +
180 +__device__ __forceinline__ float2 float2_add(const float2& a, const float2& b) {
181 + float2 result;
182 + asm volatile("add.rn.f32x2 %0, %1, %2;\n"
183 + : "=l"(reinterpret_cast<uint64_t&>(result))
184 + : "l"(reinterpret_cast<uint64_t const&>(a)),
185 + "l"(reinterpret_cast<uint64_t const&>(b)));
186 + return result;
187 +}
188 +
189 +__device__ __forceinline__ float2 float2_mul(const float2& a, const float2& b) {
190 + float2 result;
191 + asm volatile("mul.f32x2 %0, %1, %2;\n"
192 + : "=l"(reinterpret_cast<uint64_t&>(result))
193 + : "l"(reinterpret_cast<uint64_t const&>(a)),
194 + "l"(reinterpret_cast<uint64_t const&>(b)));
195 + return result;
196 +}
197 +
198 +__device__ __forceinline__ float2 float2_fma(const float2& a, const float2& b,
199 + const float2& c) {
200 + float2 result;
201 + asm volatile("fma.rn.f32x2 %0, %1, %2, %3;\n"
202 + : "=l"(reinterpret_cast<uint64_t&>(result))
203 + : "l"(reinterpret_cast<uint64_t const&>(a)),
204 + "l"(reinterpret_cast<uint64_t const&>(b)),
205 + "l"(reinterpret_cast<uint64_t const&>(c)));
206 + return result;
207 +}
208 +
209 +template <int NC>
210 +struct FwdSmemPlan {
211 + alignas(16) uint64_t bar_ready[CHUNK_DEPTH];
212 + alignas(16) uint64_t bar_consumed[CHUNK_DEPTH];
213 + alignas(16) uint64_t bar_output_norm_ready;
214 + alignas(16) float2 ws_stats[CONSUMER_WARPS][NC];
215 + uint32_t tmem_base;
216 +};
217 +
218 +__device__ __forceinline__ void cp_async_bulk(void* smem_dst,
219 + const void* gmem_src, int bytes,
220 + uint64_t& mbar) {
221 + uint32_t const s = static_cast<uint32_t>(__cvta_generic_to_shared(smem_dst));
222 + uint32_t const m = static_cast<uint32_t>(__cvta_generic_to_shared(&mbar));
223 + asm volatile(
224 + "cp.async.bulk.shared::cta.global.mbarrier::complete_tx::bytes [%0], "
225 + "[%1], %2, [%3];\n" ::"r"(s),
226 + "l"(gmem_src), "r"(bytes), "r"(m)
227 + : "memory");
228 +}
229 +
230 +template <int H, int NC = N_CHUNK_DEFAULT, bool RELEASE_TMEM = false,
231 + bool HAS_DELTA = false, bool HAS_OUTPUT_NORM = false,
232 + bool OUTPUT_NORM_IN_SMEM = false>
233 +__global__ void __launch_bounds__(BLK, 1) attn_res_fwd_online_v2_kernel(
234 + const bf16_t* __restrict__ block_res, bf16_t* __restrict__ layer_res,
235 + const bf16_t* __restrict__ delta, const bf16_t* __restrict__ res_w,
236 + const bf16_t* __restrict__ rms_w, bf16_t* __restrict__ output, int N, int T,
237 + int B, int block_stride_m, int block_stride_r, float rms_eps,
238 + const bf16_t* __restrict__ output_norm_weight, float output_norm_eps) {
239 +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 1000 && __CUDA_ARCH__ < 1100
240 + constexpr float LOG2_E = 1.4426950408889634f;
241 + constexpr int N_CHUNK = NC;
242 + // The two-source specialization only consumes half of the TMEM columns.
243 + constexpr int TMEM_COLS_ALLOC = NC == 2 ? 128 : 256;
244 + constexpr int NUM_BUFS = CHUNK_DEPTH * NC;
245 + constexpr int NHT = H / K_TILE;
246 + constexpr int SLICES_PER_GROUP =
247 + (NHT + CONSUMER_GROUPS - 1) / CONSUMER_GROUPS;
248 + constexpr int VEC = 8;
249 + constexpr int ACC_PER_THREAD = H == 7168 ? 28 : SLICES_PER_GROUP * VEC;
250 + constexpr int TMEM_V_COLS_PER_GROUP = SLICES_PER_GROUP * N_CHUNK * VEC;
251 + constexpr int TMEM_V_COLS_TOTAL = CONSUMER_GROUPS * TMEM_V_COLS_PER_GROUP;
252 + static_assert(TMEM_V_COLS_TOTAL <= TMEM_COLS_ALLOC);
253 + static_assert(H >= 4096 && H <= 8192);
254 + static_assert(H % K_TILE == 0);
255 +
256 + const int tid = threadIdx.x;
257 + const int wid = tid >> 5;
258 + const int lane = tid & 31;
259 + const int TB = T * B;
260 + const int num_ctas = gridDim.x;
261 + const int num_chunks = (N + N_CHUNK - 1) / N_CHUNK;
262 +
263 + const int comp_wid = wid - 1;
264 + const int comp_tid = tid - 32;
265 + const int group = (comp_wid >= 4) ? 1 : 0;
266 + const int ct_in_group =
267 + (comp_tid >= 0) ? (comp_tid & (CONSUMER_THREADS_PER_GROUP - 1)) : -1;
268 + const int k_local = ct_in_group * VEC;
269 +
270 + constexpr size_t V_BYTES = (size_t)NUM_BUFS * H * sizeof(bf16_t);
271 + constexpr size_t DELTA_BYTES =
272 + HAS_DELTA ? (size_t)CHUNK_DEPTH * H * sizeof(bf16_t) : 0;
273 + constexpr size_t OUTPUT_NORM_BYTES =
274 + OUTPUT_NORM_IN_SMEM ? (size_t)H * sizeof(bf16_t) : 0;
275 + extern __shared__ __align__(16) char smem_raw[];
276 + bf16_t* v_bufs = reinterpret_cast<bf16_t*>(smem_raw); // [NUM_BUFS][H]
277 + bf16_t* delta_bufs = reinterpret_cast<bf16_t*>(smem_raw + V_BYTES);
278 + bf16_t* output_norm_buf =
279 + reinterpret_cast<bf16_t*>(smem_raw + V_BYTES + DELTA_BYTES);
280 + FwdSmemPlan<NC>& plan = *reinterpret_cast<FwdSmemPlan<NC>*>(
281 + smem_raw + V_BYTES + DELTA_BYTES + OUTPUT_NORM_BYTES);
282 +
283 + auto slot_of = [](long long gci, int n) {
284 + return (int)(gci % CHUNK_DEPTH) * N_CHUNK + n;
285 + };
286 + auto phase_of = [](long long gci) { return (int)((gci / CHUNK_DEPTH) & 1); };
287 + auto buf_ptr = [&](int slot) -> bf16_t* { return v_bufs + slot * H; };
288 + auto delta_buf_ptr = [&](int chunk_slot) -> bf16_t* {
289 + return delta_bufs + chunk_slot * H;
290 + };
291 +
292 + if (wid == 0 && elect_one_sync()) {
293 + #pragma unroll
294 + for (int i = 0; i < CHUNK_DEPTH; i++) {
295 + mbarrier_init(plan.bar_ready[i], 1);
296 + mbarrier_init(plan.bar_consumed[i], CONSUMER_WARPS);
297 + }
298 + if constexpr (OUTPUT_NORM_IN_SMEM) {
299 + mbarrier_init(plan.bar_output_norm_ready, 1);
300 + }
301 + fence_mbarrier_init();
302 + }
303 +
304 + // gdc wait BEFORE tmem alloc
305 + cudaGridDependencySynchronize();
306 +
307 + if (wid == 1) {
308 + tmem_allocate(TMEM_COLS_ALLOC, &plan.tmem_base);
309 + if constexpr (RELEASE_TMEM) {
310 + tmem_release_allocation_lock();
311 + }
312 + }
313 + __syncthreads();
314 +
315 + if constexpr (OUTPUT_NORM_IN_SMEM) {
316 + if (wid == 0 && elect_one_sync()) {
317 + mbarrier_expect_tx(plan.bar_output_norm_ready, H * (int)sizeof(bf16_t));
318 + cp_async_bulk(output_norm_buf, output_norm_weight, H * sizeof(bf16_t),
319 + plan.bar_output_norm_ready);
320 + }
321 + }
322 +
323 + const uint32_t my_v_tmem =
324 + comp_tid >= 0 ? plan.tmem_base + group * TMEM_V_COLS_PER_GROUP : 0;
325 + float q_cache[ACC_PER_THREAD];
326 + if (comp_tid >= 0) {
327 + #pragma unroll
328 + for (int si = 0; si < SLICES_PER_GROUP; si++) {
329 + if constexpr (H == 7168) {
330 + if (si == SLICES_PER_GROUP - 1) {
331 + int h_base = 6 * K_TILE + group * (K_TILE / 2) + ct_in_group * 4;
332 + #pragma unroll
333 + for (int j = 0; j < 4; j++) {
334 + int h = h_base + j;
335 + q_cache[si * VEC + j] =
336 + __bfloat162float(rms_w[h]) * __bfloat162float(res_w[h]);
337 + }
338 + continue;
339 + }
340 + }
341 + int dt = si * CONSUMER_GROUPS + group;
342 + if (dt >= NHT) continue;
343 + int h_base = dt * K_TILE + k_local;
344 + #pragma unroll
345 + for (int j = 0; j < VEC; j++) {
346 + int h = h_base + j;
347 + q_cache[si * VEC + j] =
348 + __bfloat162float(rms_w[h]) * __bfloat162float(res_w[h]);
349 + }
350 + }
351 + }
352 +
353 + if (wid == 0) {
354 + if (elect_one_sync()) {
355 + long long gci = 0;
356 + for (int tb = blockIdx.x; tb < TB; tb += num_ctas) {
357 + const int t = tb / B;
358 + for (int ci = 0; ci < num_chunks; ci++, gci++) {
359 + int ns = ci * N_CHUNK;
360 + int an = min(N_CHUNK, N - ns);
361 + int chunk_slot = (int)(gci % CHUNK_DEPTH);
362 + int pc = phase_of(gci);
363 + mbarrier_wait(plan.bar_consumed[chunk_slot], pc ^ 1);
364 + int transaction_bytes = an * H * (int)sizeof(bf16_t);
365 + if constexpr (HAS_DELTA) {
366 + int prefix_n = N - 1 - ns;
367 + if (prefix_n >= 0 && prefix_n < an) {
368 + transaction_bytes += H * (int)sizeof(bf16_t);
369 + }
370 + }
371 + mbarrier_expect_tx(plan.bar_ready[chunk_slot], transaction_bytes);
372 + #pragma unroll
373 + for (int n = 0; n < N_CHUNK; n++) {
374 + if (n >= an) continue;
375 + int slot = slot_of(gci, n);
376 + const bf16_t* src =
377 + residual_addr(block_res, layer_res, ns + n, N, t,
378 + block_stride_m, block_stride_r, H);
379 + cp_async_bulk(buf_ptr(slot), src, H * sizeof(bf16_t),
380 + plan.bar_ready[chunk_slot]);
381 + }
382 + if constexpr (HAS_DELTA) {
383 + int prefix_n = N - 1 - ns;
384 + if (prefix_n >= 0 && prefix_n < an) {
385 + cp_async_bulk(delta_buf_ptr(chunk_slot),
386 + delta + (long long)tb * H, H * sizeof(bf16_t),
387 + plan.bar_ready[chunk_slot]);
388 + }
389 + }
390 + }
391 + }
392 + }
393 + } else {
394 + float acc32[ACC_PER_THREAD] = {};
395 + float eps_cache;
396 + asm volatile("mov.b32 %0, %1;" : "=f"(eps_cache) : "f"(rms_eps));
397 +
398 + long long gci = 0;
399 + for (int tb = blockIdx.x; tb < TB; tb += num_ctas) {
400 + float m_running = -FLT_MAX;
401 + float s_running = 0.f;
402 + #pragma unroll
403 + for (int i = 0; i < ACC_PER_THREAD; i++) {
404 + acc32[i] = 0.f;
405 + }
406 +
407 + for (int ci = 0; ci < num_chunks; ci++, gci++) {
408 + int ns = ci * N_CHUNK;
409 + int an = min(N_CHUNK, N - ns);
410 + int chunk_slot = (int)(gci % CHUNK_DEPTH);
411 + int pr = phase_of(gci);
412 + mbarrier_wait(plan.bar_ready[chunk_slot], pr);
413 +
414 + float2 sq_local[N_CHUNK] = {};
415 + float2 dot_local[N_CHUNK] = {};
416 +
417 + auto pass_A_body = [&](auto AN_TOK) {
418 + constexpr int AN = decltype(AN_TOK)::value;
419 + #pragma unroll
420 + for (int si = 0; si < SLICES_PER_GROUP; si++) {
421 + if constexpr (H == 7168) {
422 + if (si == SLICES_PER_GROUP - 1) {
423 + int h_base =
424 + 6 * K_TILE + group * (K_TILE / 2) + ct_in_group * 4;
425 + const float* qv = &q_cache[si * VEC];
426 + #pragma unroll
427 + for (int n = 0; n < AN; n++) {
428 + int slot = slot_of(gci, n);
429 + int2 vp =
430 + *reinterpret_cast<const int2*>(buf_ptr(slot) + h_base);
431 + auto* v2 = reinterpret_cast<__nv_bfloat162*>(&vp);
432 + if constexpr (HAS_DELTA) {
433 + int prefix_n = N - 1 - ns;
434 + if (n == prefix_n) {
435 + const bf16_t* delta_ptr =
436 + delta_buf_ptr(chunk_slot) + h_base;
437 + #pragma unroll
438 + for (int j = 0; j < 2; j++) {
439 + auto delta2 = *reinterpret_cast<const __nv_bfloat162*>(
440 + delta_ptr + 2 * j);
441 + v2[j] = __hadd2(v2[j], delta2);
442 + }
443 + *reinterpret_cast<int2*>(layer_res + (long long)tb * H +
444 + h_base) = vp;
445 + }
446 + }
447 + float2 f[2] = {__bfloat1622float2(v2[0]),
448 + __bfloat1622float2(v2[1])};
449 + tmem_store<4>(my_v_tmem + (si * N_CHUNK + n) * VEC, f);
450 + sq_local[n] = float2_fma(f[0], f[0], sq_local[n]);
451 + sq_local[n] = float2_fma(f[1], f[1], sq_local[n]);
452 + dot_local[n] =
453 + float2_fma(f[0], make_float2(qv[0], qv[1]), dot_local[n]);
454 + dot_local[n] =
455 + float2_fma(f[1], make_float2(qv[2], qv[3]), dot_local[n]);
456 + }
457 + continue;
458 + }
459 + }
460 + int dt = si * CONSUMER_GROUPS + group;
461 + if (dt >= NHT) continue;
462 + int h_base = dt * K_TILE + k_local;
463 + const float* qv = &q_cache[si * VEC];
464 +
465 + #pragma unroll
466 + for (int n = 0; n < AN; n++) {
467 + int slot = slot_of(gci, n);
468 + int4 vp = *reinterpret_cast<const int4*>(buf_ptr(slot) + h_base);
469 + auto* v2 = reinterpret_cast<__nv_bfloat162*>(&vp);
470 + if constexpr (HAS_DELTA) {
471 + int prefix_n = N - 1 - ns;
472 + if (n == prefix_n) {
473 + const bf16_t* delta_ptr = delta_buf_ptr(chunk_slot) + h_base;
474 + #pragma unroll
475 + for (int j = 0; j < VEC / 2; j++) {
476 + auto delta2 = *reinterpret_cast<const __nv_bfloat162*>(
477 + delta_ptr + 2 * j);
478 + v2[j] = __hadd2(v2[j], delta2);
479 + }
480 + *reinterpret_cast<int4*>(layer_res + (long long)tb * H +
481 + h_base) = vp;
482 + }
483 + }
484 + float2 f[4] = {
485 + __bfloat1622float2(v2[0]), __bfloat1622float2(v2[1]),
486 + __bfloat1622float2(v2[2]), __bfloat1622float2(v2[3])};
487 + tmem_store<VEC>(my_v_tmem + (si * N_CHUNK + n) * VEC, f);
488 + #pragma unroll
489 + for (int j = 0; j < VEC / 2; j++) {
490 + sq_local[n] = float2_fma(f[j], f[j], sq_local[n]);
491 + dot_local[n] = float2_fma(
492 + f[j], make_float2(qv[2 * j], qv[2 * j + 1]), dot_local[n]);
493 + }
494 + }
495 + }
496 + };
497 + if constexpr (NC == 4) {
498 + switch (an) {
499 + case 4:
500 + pass_A_body(std::integral_constant<int, 4>{});
501 + break;
502 + case 3:
503 + pass_A_body(std::integral_constant<int, 3>{});
504 + break;
505 + case 2:
506 + pass_A_body(std::integral_constant<int, 2>{});
507 + break;
508 + case 1:
509 + pass_A_body(std::integral_constant<int, 1>{});
510 + break;
511 + default:
512 + __builtin_unreachable();
513 + }
514 + } else if constexpr (NC == 3) {
515 + switch (an) {
516 + case 3:
517 + pass_A_body(std::integral_constant<int, 3>{});
518 + break;
519 + case 2:
520 + pass_A_body(std::integral_constant<int, 2>{});
521 + break;
522 + case 1:
523 + pass_A_body(std::integral_constant<int, 1>{});
524 + break;
525 + default:
526 + __builtin_unreachable();
527 + }
528 + } else {
529 + static_assert(NC == 2);
530 + switch (an) {
531 + case 2:
532 + pass_A_body(std::integral_constant<int, 2>{});
533 + break;
534 + case 1:
535 + pass_A_body(std::integral_constant<int, 1>{});
536 + break;
537 + default:
538 + __builtin_unreachable();
539 + }
540 + }
541 + if (lane == 0) {
542 + mbarrier_arrive(plan.bar_consumed[chunk_slot]);
543 + }
544 + tmem_store_wait();
545 +
546 + float2 reduce_pair[N_CHUNK];
547 + #pragma unroll
548 + for (int n = 0; n < N_CHUNK; n++) {
549 + reduce_pair[n] = make_float2(sq_local[n].x + sq_local[n].y,
550 + dot_local[n].x + dot_local[n].y);
551 + }
552 + #pragma unroll
553 + for (int offset = 16; offset > 0; offset >>= 1) {
554 + #pragma unroll
555 + for (int n = 0; n < N_CHUNK; n++) {
556 + uint64_t packed = reinterpret_cast<uint64_t&>(reduce_pair[n]);
557 + packed = __shfl_xor_sync(0xffffffff, packed, offset);
558 + float2 other = reinterpret_cast<float2&>(packed);
559 + reduce_pair[n] = float2_add(reduce_pair[n], other);
560 + }
561 + }
562 + if (lane == 0) {
563 + #pragma unroll
564 + for (int n = 0; n < N_CHUNK; n++) {
565 + plan.ws_stats[comp_wid][n] = reduce_pair[n];
566 + }
567 + }
568 + named_barrier_sync(CONSUMER_THREADS, 0);
569 +
570 + float local_rsig = 0.f;
571 + float local_logit = 0.f;
572 + int stat_n = lane / CONSUMER_WARPS;
573 + int stat_w = lane % CONSUMER_WARPS;
574 + float2 totals = {};
575 + if (stat_n < N_CHUNK) {
576 + totals = plan.ws_stats[stat_w][stat_n];
577 + }
578 + #pragma unroll
579 + for (int offset = CONSUMER_WARPS / 2; offset > 0; offset >>= 1) {
580 + totals.x +=
581 + __shfl_down_sync(0xffffffff, totals.x, offset, CONSUMER_WARPS);
582 + totals.y +=
583 + __shfl_down_sync(0xffffffff, totals.y, offset, CONSUMER_WARPS);
584 + }
585 + if (stat_n < N_CHUNK && stat_w == 0) {
586 + local_rsig = rsqrtf(totals.x / H + eps_cache);
587 + local_logit = totals.y * local_rsig;
588 + }
589 + float logit_n[N_CHUNK];
590 + #pragma unroll
591 + for (int n = 0; n < N_CHUNK; n++) {
592 + logit_n[n] = __shfl_sync(0xffffffff, local_logit, n * CONSUMER_WARPS);
593 + }
594 +
595 + float m_chunk = -FLT_MAX;
596 + #pragma unroll
597 + for (int n = 0; n < N_CHUNK; n++) {
598 + if (n < an) m_chunk = fmaxf(m_chunk, logit_n[n]);
599 + }
600 + float m_new = fmaxf(m_running, m_chunk);
601 + float corr = exp2f((m_running - m_new) * LOG2_E);
602 + float w_n[N_CHUNK] = {};
603 + float w_sum = 0.f;
604 + #pragma unroll
605 + for (int n = 0; n < N_CHUNK; n++) {
606 + if (n < an) {
607 + w_n[n] = exp2f((logit_n[n] - m_new) * LOG2_E);
608 + w_sum += w_n[n];
609 + }
610 + }
611 +
612 + auto pass_B_body = [&](auto AN_TOK) {
613 + constexpr int AN = decltype(AN_TOK)::value;
614 + #pragma unroll
615 + for (int si = 0; si < SLICES_PER_GROUP; si++) {
616 + if constexpr (H == 7168) {
617 + if (si == SLICES_PER_GROUP - 1) {
618 + float2 corr2 = make_float2(corr, corr);
619 + float2 a[2];
620 + #pragma unroll
621 + for (int j = 0; j < 2; j++) {
622 + float2 old = make_float2(acc32[si * VEC + 2 * j],
623 + acc32[si * VEC + 2 * j + 1]);
624 + a[j] = float2_mul(old, corr2);
625 + }
626 + float2 f_cache[AN][2];
627 + #pragma unroll
628 + for (int n = 0; n < AN; n++) {
629 + tmem_load<4>(my_v_tmem + (si * N_CHUNK + n) * VEC,
630 + f_cache[n]);
631 + }
632 + #pragma unroll
633 + for (int n = 0; n < AN; n++) {
634 + float2 wn = make_float2(w_n[n], w_n[n]);
635 + #pragma unroll
636 + for (int j = 0; j < 2; j++) {
637 + a[j] = float2_fma(wn, f_cache[n][j], a[j]);
638 + }
639 + }
640 + #pragma unroll
641 + for (int j = 0; j < 2; j++) {
642 + acc32[si * VEC + 2 * j] = a[j].x;
643 + acc32[si * VEC + 2 * j + 1] = a[j].y;
644 + }
645 + continue;
646 + }
647 + }
648 + int dt = si * CONSUMER_GROUPS + group;
649 + if (dt >= NHT) continue;
650 + float2 corr2 = make_float2(corr, corr);
651 + float2 a[VEC / 2];
652 + #pragma unroll
653 + for (int j = 0; j < VEC / 2; j++) {
654 + float2 old = make_float2(acc32[si * VEC + 2 * j],
655 + acc32[si * VEC + 2 * j + 1]);
656 + a[j] = float2_mul(old, corr2);
657 + }
658 + float2 f_cache[AN][VEC / 2];
659 + #pragma unroll
660 + for (int n = 0; n < AN; n++) {
661 + tmem_load<VEC>(my_v_tmem + (si * N_CHUNK + n) * VEC, f_cache[n]);
662 + }
663 + #pragma unroll
664 + for (int n = 0; n < AN; n++) {
665 + float2 wn = make_float2(w_n[n], w_n[n]);
666 + #pragma unroll
667 + for (int j = 0; j < VEC / 2; j++) {
668 + a[j] = float2_fma(wn, f_cache[n][j], a[j]);
669 + }
670 + }
671 + #pragma unroll
672 + for (int j = 0; j < VEC / 2; j++) {
673 + acc32[si * VEC + 2 * j] = a[j].x;
674 + acc32[si * VEC + 2 * j + 1] = a[j].y;
675 + }
676 + }
677 + };
678 + if constexpr (NC == 4) {
679 + switch (an) {
680 + case 4:
681 + pass_B_body(std::integral_constant<int, 4>{});
682 + break;
683 + case 3:
684 + pass_B_body(std::integral_constant<int, 3>{});
685 + break;
686 + case 2:
687 + pass_B_body(std::integral_constant<int, 2>{});
688 + break;
689 + case 1:
690 + pass_B_body(std::integral_constant<int, 1>{});
691 + break;
692 + default:
693 + __builtin_unreachable();
694 + }
695 + } else if constexpr (NC == 3) {
696 + switch (an) {
697 + case 3:
698 + pass_B_body(std::integral_constant<int, 3>{});
699 + break;
700 + case 2:
701 + pass_B_body(std::integral_constant<int, 2>{});
702 + break;
703 + case 1:
704 + pass_B_body(std::integral_constant<int, 1>{});
705 + break;
706 + default:
707 + __builtin_unreachable();
708 + }
709 + } else {
710 + static_assert(NC == 2);
711 + switch (an) {
712 + case 2:
713 + pass_B_body(std::integral_constant<int, 2>{});
714 + break;
715 + case 1:
716 + pass_B_body(std::integral_constant<int, 1>{});
717 + break;
718 + default:
719 + __builtin_unreachable();
720 + }
721 + }
722 +
723 + s_running = s_running * corr + w_sum;
724 + m_running = m_new;
725 + }
726 +
727 + float inv_s = 1.f / s_running;
728 + bf16_t* out_ptr = output + (long long)tb * H;
729 + float2 output_sq_pair = {};
730 + // When output RMSNorm is fused, the softmax denominator cancels:
731 + // (acc / s) * rsqrt(mean((acc / s)^2) + eps)
732 + // = acc * rsqrt(mean(acc^2) + eps * s^2).
733 + #pragma unroll
734 + for (int si = 0; si < SLICES_PER_GROUP; si++) {
735 + if constexpr (H == 7168) {
736 + if (si == SLICES_PER_GROUP - 1) {
737 + int h_base = 6 * K_TILE + group * (K_TILE / 2) + ct_in_group * 4;
738 + uint2 packed;
739 + auto* ov2 = reinterpret_cast<__nv_bfloat162*>(&packed);
740 + float2 inv2 = make_float2(inv_s, inv_s);
741 + #pragma unroll
742 + for (int j = 0; j < 2; j++) {
743 + float2 old = make_float2(acc32[si * VEC + 2 * j],
744 + acc32[si * VEC + 2 * j + 1]);
745 + if constexpr (HAS_OUTPUT_NORM) {
746 + output_sq_pair = float2_fma(old, old, output_sq_pair);
747 + } else {
748 + float2 mixed = float2_mul(old, inv2);
749 + ov2[j] = __float22bfloat162_rn(mixed);
750 + }
751 + }
752 + if constexpr (!HAS_OUTPUT_NORM) {
753 + *reinterpret_cast<uint2*>(out_ptr + h_base) = packed;
754 + }
755 + continue;
756 + }
757 + }
758 + int dt = si * CONSUMER_GROUPS + group;
759 + if (dt >= NHT) continue;
760 + int h_base = dt * K_TILE + k_local;
761 + uint4 packed;
762 + auto* ov2 = reinterpret_cast<__nv_bfloat162*>(&packed);
763 + float2 inv2 = make_float2(inv_s, inv_s);
764 + #pragma unroll
765 + for (int j = 0; j < VEC / 2; j++) {
766 + float2 old =
767 + make_float2(acc32[si * VEC + 2 * j], acc32[si * VEC + 2 * j + 1]);
768 + if constexpr (HAS_OUTPUT_NORM) {
769 + output_sq_pair = float2_fma(old, old, output_sq_pair);
770 + } else {
771 + float2 mixed = float2_mul(old, inv2);
772 + ov2[j] = __float22bfloat162_rn(mixed);
773 + }
774 + }
775 + if constexpr (!HAS_OUTPUT_NORM) {
776 + *reinterpret_cast<uint4*>(out_ptr + h_base) = packed;
777 + }
778 + }
779 +
780 + if constexpr (HAS_OUTPUT_NORM) {
781 + if constexpr (OUTPUT_NORM_IN_SMEM) {
782 + // The immutable weight copy is acquired once, at its first use.
783 + if (tb == blockIdx.x) {
784 + mbarrier_wait(plan.bar_output_norm_ready, 0);
785 + }
786 + }
787 + float output_sq = output_sq_pair.x + output_sq_pair.y;
788 + #pragma unroll
789 + for (int offset = 16; offset > 0; offset >>= 1) {
790 + output_sq += __shfl_xor_sync(0xffffffff, output_sq, offset);
791 + }
792 + if (lane == 0) {
793 + plan.ws_stats[comp_wid][0] = make_float2(output_sq, 0.f);
794 + }
795 + named_barrier_sync(CONSUMER_THREADS, 0);
796 + float total_sq = lane < CONSUMER_WARPS ? plan.ws_stats[lane][0].x : 0.f;
797 + #pragma unroll
798 + for (int offset = CONSUMER_WARPS / 2; offset > 0; offset >>= 1) {
799 + total_sq +=
800 + __shfl_down_sync(0xffffffff, total_sq, offset, CONSUMER_WARPS);
801 + }
802 + if (lane == 0) {
803 + total_sq =
804 + rsqrtf(total_sq / H + output_norm_eps * s_running * s_running);
805 + }
806 + float output_rsigma = __shfl_sync(0xffffffff, total_sq, 0);
807 + #pragma unroll
808 + for (int si = 0; si < SLICES_PER_GROUP; si++) {
809 + if constexpr (H == 7168) {
810 + if (si == SLICES_PER_GROUP - 1) {
811 + int h_base = 6 * K_TILE + group * (K_TILE / 2) + ct_in_group * 4;
812 + uint2 packed;
813 + auto* values = reinterpret_cast<bf16_t*>(&packed);
814 + #pragma unroll
815 + for (int j = 0; j < 4; j++) {
816 + const bf16_t* weight_ptr =
817 + OUTPUT_NORM_IN_SMEM ? output_norm_buf : output_norm_weight;
818 + float weight = __bfloat162float(weight_ptr[h_base + j]);
819 + values[j] = __float2bfloat16(acc32[si * VEC + j] *
820 + output_rsigma * weight);
821 + }
822 + *reinterpret_cast<uint2*>(out_ptr + h_base) = packed;
823 + continue;
824 + }
825 + }
826 + int dt = si * CONSUMER_GROUPS + group;
827 + if (dt >= NHT) continue;
828 + int h_base = dt * K_TILE + k_local;
829 + uint4 packed;
830 + auto* values = reinterpret_cast<bf16_t*>(&packed);
831 + #pragma unroll
832 + for (int j = 0; j < VEC; j++) {
833 + const bf16_t* weight_ptr =
834 + OUTPUT_NORM_IN_SMEM ? output_norm_buf : output_norm_weight;
835 + float weight = __bfloat162float(weight_ptr[h_base + j]);
836 + values[j] =
837 + __float2bfloat16(acc32[si * VEC + j] * output_rsigma * weight);
838 + }
839 + *reinterpret_cast<uint4*>(out_ptr + h_base) = packed;
840 + }
841 + }
842 + }
843 + }
844 +
845 + cudaTriggerProgrammaticLaunchCompletion();
846 + __syncthreads();
847 + if (wid == 1) {
848 + tmem_free(plan.tmem_base, TMEM_COLS_ALLOC);
849 + }
850 +#else
851 + if (threadIdx.x == 0) {
852 + printf("attn_res_fwd_online_v2_kernel requires sm_10x\n");
853 + }
854 +#endif
855 +}
856 +
857 +template <int H, int NC = N_CHUNK_DEFAULT, bool RELEASE_TMEM = false,
858 + bool HAS_DELTA = false, bool HAS_OUTPUT_NORM = false,
859 + bool OUTPUT_NORM_IN_SMEM = false>
860 +static void launch_fwd(const bf16_t* block_residual, bf16_t* layer_residual,
861 + const bf16_t* delta, const bf16_t* res_weight,
862 + const bf16_t* rms_weight, bf16_t* output, int N, int T,
863 + int B, float rms_eps, int num_sm, cudaStream_t stream,
864 + const bf16_t* output_norm_weight = nullptr,
865 + float output_norm_eps = 0.f, int block_stride_m = 0,
866 + int block_stride_r = 0) {
867 + constexpr size_t smem_size =
868 + ((size_t)CHUNK_DEPTH * (NC + (HAS_DELTA ? 1 : 0)) * H * sizeof(bf16_t) +
869 + (OUTPUT_NORM_IN_SMEM ? (size_t)H * sizeof(bf16_t) : 0) +
870 + sizeof(FwdSmemPlan<NC>) + 15) &
871 + ~size_t(15);
872 + auto kernel =
873 + &attn_res_fwd_online_v2_kernel<H, NC, RELEASE_TMEM, HAS_DELTA,
874 + HAS_OUTPUT_NORM, OUTPUT_NORM_IN_SMEM>;
875 + static bool attrs_set = false;
876 + if (!attrs_set) {
877 + if (smem_size > 48 * 1024) {
878 + cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize,
879 + smem_size);
880 + }
881 + attrs_set = true;
882 + }
883 + int grid = RELEASE_TMEM ? num_sm * 2 : num_sm;
884 + cudaLaunchConfig_t config{};
885 + config.gridDim = grid;
886 + config.blockDim = BLK;
887 + config.dynamicSmemBytes = smem_size;
888 + config.stream = stream;
889 + cudaLaunchAttribute attrs[1];
890 + attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization;
891 + attrs[0].val.programmaticStreamSerializationAllowed = 1;
892 + config.attrs = attrs;
893 + config.numAttrs = 1;
894 + cudaLaunchKernelEx(&config, kernel, block_residual, layer_residual, delta,
895 + res_weight, rms_weight, output, N, T, B, block_stride_m,
896 + block_stride_r, rms_eps, output_norm_weight,
897 + output_norm_eps);
898 +}
899 +
900 +} // namespace fwd_prod_v2
901 +} // namespace sm100
902 +
903 +void kimi_k3_attn_res(torch::stable::Tensor& prefix,
904 + torch::stable::Tensor const& delta,
905 + torch::stable::Tensor const& blocks,
906 + torch::stable::Tensor const& norm_weight,
907 + torch::stable::Tensor const& qk_weight,
908 + torch::stable::Tensor const& output_norm_weight,
909 + torch::stable::Tensor& output, int64_t num_blocks,
910 + double eps, double output_norm_eps) {
911 + int const num_tokens = static_cast<int>(prefix.size(0));
912 + int const device = prefix.get_device_index();
913 + torch::stable::accelerator::DeviceGuard const device_guard(device);
914 + cudaDeviceProp const* properties = get_device_prop();
915 + STD_TORCH_CHECK(properties->major == 10,
916 + "Kimi K3 AttnRes requires the SM100 family");
917 +
918 + using namespace sm100::fwd_prod_v2;
919 + // Two-source chunks and two resident CTAs are beneficial once setup is
920 + // amortized by the long, full eight-block prefill workload.
921 + if (num_blocks == 8 && num_tokens >= 4096) {
922 + launch_fwd<7168, 2, true, true, true, true>(
923 + static_cast<bf16_t const*>(blocks.data_ptr()),
924 + static_cast<bf16_t*>(prefix.data_ptr()),
925 + static_cast<bf16_t const*>(delta.data_ptr()),
926 + static_cast<bf16_t const*>(qk_weight.data_ptr()),
927 + static_cast<bf16_t const*>(norm_weight.data_ptr()),
928 + static_cast<bf16_t*>(output.data_ptr()),
929 + static_cast<int>(num_blocks) + 1, num_tokens, 1,
930 + static_cast<float>(eps), properties->multiProcessorCount,
931 + get_current_cuda_stream(device),
932 + static_cast<bf16_t const*>(output_norm_weight.data_ptr()),
933 + static_cast<float>(output_norm_eps), static_cast<int>(blocks.stride(0)),
934 + static_cast<int>(blocks.stride(1)));
935 + } else {
936 + launch_fwd<7168, 4, false, true, true, true>(
937 + static_cast<bf16_t const*>(blocks.data_ptr()),
938 + static_cast<bf16_t*>(prefix.data_ptr()),
939 + static_cast<bf16_t const*>(delta.data_ptr()),
940 + static_cast<bf16_t const*>(qk_weight.data_ptr()),
941 + static_cast<bf16_t const*>(norm_weight.data_ptr()),
942 + static_cast<bf16_t*>(output.data_ptr()),
943 + static_cast<int>(num_blocks) + 1, num_tokens, 1,
944 + static_cast<float>(eps), properties->multiProcessorCount,
945 + get_current_cuda_stream(device),
946 + static_cast<bf16_t const*>(output_norm_weight.data_ptr()),
947 + static_cast<float>(output_norm_eps), static_cast<int>(blocks.stride(0)),
948 + static_cast<int>(blocks.stride(1)));
949 + }
950 + cudaError_t const error = cudaGetLastError();
951 + STD_TORCH_CHECK(
952 + error == cudaSuccess,
953 + "Kimi K3 AttnRes kernel launch failed: ", cudaGetErrorString(error));
954 +}
csrc/libtorch_stable/ops.h
+11
@@ -315,6 +315,17 @@ void fused_minimax_m3_qknorm_rope_kv_insert(
315 std::optional<torch::stable::Tensor> index_q_out,
316 const std::string& kv_cache_dtype, bool skip_index_branch);
317
318 +#ifdef VLLM_ENABLE_KIMI_K3_ATTN_RES
319 +void kimi_k3_attn_res(torch::stable::Tensor& prefix,
320 + torch::stable::Tensor const& delta,
321 + torch::stable::Tensor const& blocks,
322 + torch::stable::Tensor const& norm_weight,
323 + torch::stable::Tensor const& qk_weight,
324 + torch::stable::Tensor const& output_norm_weight,
325 + torch::stable::Tensor& output, int64_t num_blocks,
326 + double eps, double output_norm_eps);
327 +#endif
328 +
329 // Sampler kernels (shared CUDA/ROCm)
330 void apply_repetition_penalties_(
331 torch::stable::Tensor& logits, const torch::stable::Tensor& prompt_mask,
csrc/libtorch_stable/torch_bindings.cpp
+11
@@ -468,6 +468,14 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C, ops) {
468 "int block_size, Tensor!? q_out, Tensor!? index_q_out, "
469 "str kv_cache_dtype, bool skip_index_branch=False) -> ()");
470
471 +#ifdef VLLM_ENABLE_KIMI_K3_ATTN_RES
472 + ops.def(
473 + "kimi_k3_attn_res("
474 + "Tensor! prefix, Tensor delta, Tensor blocks, Tensor norm_weight, "
475 + "Tensor qk_weight, Tensor output_norm_weight, Tensor! output, "
476 + "int num_blocks, float eps, float output_norm_eps) -> ()");
477 +#endif
478 +
479 // Apply repetition penalties to logits in-place.
480 ops.def(
481 "apply_repetition_penalties_(Tensor! logits, Tensor prompt_mask, "
@@ -693,6 +701,9 @@ STABLE_TORCH_LIBRARY_IMPL(_C, CUDA, ops) {
701 #endif
702 ops.impl("fused_minimax_m3_qknorm_rope_kv_insert",
703 TORCH_BOX(&fused_minimax_m3_qknorm_rope_kv_insert));
704 +#ifdef VLLM_ENABLE_KIMI_K3_ATTN_RES
705 + ops.impl("kimi_k3_attn_res", TORCH_BOX(&kimi_k3_attn_res));
706 +#endif
707
708 // Sampler kernels (shared CUDA/ROCm)
709 ops.impl("apply_repetition_penalties_",
tests/models/kimi_k3/test_amd_attn_res.py new
+102
@@ -0,0 +1,102 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import pytest
5 +import torch
6 +import torch.nn.functional as F
7 +
8 +from vllm.models.kimi_k3.amd.ops.attn_res import attn_res
9 +from vllm.platforms import current_platform
10 +
11 +pytestmark = pytest.mark.skipif(
12 + not current_platform.is_rocm(),
13 + reason="AMD AttnRes requires ROCm",
14 +)
15 +
16 +
17 +def _randn_with_row_padding(*shape: int, padding: int = 0) -> torch.Tensor:
18 + storage = torch.randn(
19 + *shape[:-1],
20 + shape[-1] + padding,
21 + device="cuda",
22 + dtype=torch.bfloat16,
23 + )
24 + return storage[..., : shape[-1]]
25 +
26 +
27 +def _reference(
28 + prefix: torch.Tensor,
29 + blocks: torch.Tensor,
30 + norm_weight: torch.Tensor,
31 + qk_weight: torch.Tensor,
32 + num_blocks: int,
33 + eps: float,
34 +) -> torch.Tensor:
35 + hidden_size = prefix.shape[-1]
36 + values = torch.cat((blocks[:, :num_blocks], prefix.unsqueeze(1)), dim=1)
37 + keys = F.rms_norm(values, (hidden_size,), norm_weight, eps)
38 + probs = (keys @ qk_weight).softmax(dim=-1)
39 + return torch.matmul(probs.unsqueeze(1), values).squeeze(1)
40 +
41 +
42 +@pytest.mark.parametrize(
43 + (
44 + "num_tokens",
45 + "num_blocks",
46 + "block_capacity",
47 + "hidden_size",
48 + "row_padding",
49 + ),
50 + [
51 + pytest.param(0, 3, 5, 128, 0, id="empty"),
52 + pytest.param(1, 1, 2, 128, 0, id="decode-single"),
53 + pytest.param(17, 4, 6, 1024, 7, id="decode-padded"),
54 + pytest.param(320, 8, 10, 7168, 0, id="prefill-full"),
55 + ],
56 +)
57 +def test_amd_attn_res_matches_reference(
58 + num_tokens: int,
59 + num_blocks: int,
60 + block_capacity: int,
61 + hidden_size: int,
62 + row_padding: int,
63 +) -> None:
64 + eps = 1e-5
65 + prefix = _randn_with_row_padding(num_tokens, hidden_size, padding=row_padding)
66 + blocks = _randn_with_row_padding(
67 + num_tokens,
68 + block_capacity,
69 + hidden_size,
70 + padding=row_padding,
71 + )
72 + norm_weight = 1 + 0.1 * torch.randn(
73 + hidden_size, device="cuda", dtype=torch.bfloat16
74 + )
75 + qk_weight = (
76 + torch.randn(hidden_size, device="cuda", dtype=torch.bfloat16) / hidden_size**0.5
77 + )
78 + expected = _reference(
79 + prefix,
80 + blocks,
81 + norm_weight,
82 + qk_weight,
83 + num_blocks,
84 + eps,
85 + )
86 + original_prefix = prefix.clone()
87 + original_blocks = blocks.clone()
88 +
89 + actual = attn_res(
90 + prefix,
91 + blocks,
92 + norm_weight,
93 + qk_weight,
94 + num_blocks,
95 + eps,
96 + )
97 +
98 + torch.testing.assert_close(actual, expected, atol=8e-2, rtol=3e-2)
99 + torch.testing.assert_close(prefix, original_prefix, atol=0, rtol=0)
100 + torch.testing.assert_close(blocks, original_blocks, atol=0, rtol=0)
101 + assert actual.shape == prefix.shape
102 + assert actual.is_contiguous()
tests/models/kimi_k3/test_attn_res.py new
+193
@@ -0,0 +1,193 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import pytest
5 +import torch
6 +import torch.nn.functional as F
7 +
8 +from vllm.models.kimi_k3.nvidia.ops import attn_res
9 +from vllm.platforms import current_platform
10 +
11 +HIDDEN_SIZE = 7168
12 +MAX_BLOCKS = 8
13 +EPS = 1e-5
14 +
15 +
16 +def _randn_with_row_padding(*shape: int, padding: int = 0) -> torch.Tensor:
17 + storage = torch.randn(
18 + *shape[:-1],
19 + shape[-1] + padding,
20 + device="cuda",
21 + dtype=torch.bfloat16,
22 + )
23 + return storage[..., : shape[-1]]
24 +
25 +
26 +def _reference(
27 + prefix: torch.Tensor,
28 + delta: torch.Tensor | None,
29 + blocks: torch.Tensor,
30 + norm_weight: torch.Tensor,
31 + qk_weight: torch.Tensor,
32 + output_norm_weight: torch.Tensor | None,
33 + num_blocks: int,
34 +) -> tuple[torch.Tensor, torch.Tensor]:
35 + if delta is not None:
36 + prefix = prefix + delta
37 + values = torch.cat((blocks[:, :num_blocks], prefix.unsqueeze(1)), dim=1)
38 + keys = F.rms_norm(values, (HIDDEN_SIZE,), norm_weight, EPS)
39 + probs = (keys @ qk_weight).softmax(dim=-1)
40 + output = torch.matmul(probs.unsqueeze(1), values).squeeze(1)
41 + if output_norm_weight is not None:
42 + output = F.rms_norm(output, (HIDDEN_SIZE,), output_norm_weight, EPS)
43 + return output, prefix
44 +
45 +
46 +@pytest.mark.parametrize(
47 + (
48 + "num_tokens",
49 + "num_blocks",
50 + "row_padding",
51 + "write_block",
52 + "has_delta",
53 + "backend",
54 + ),
55 + [
56 + pytest.param(1, 0, 0, True, False, "triton", id="triton-empty"),
57 + pytest.param(1, 0, 0, True, True, "triton", id="triton-empty-add"),
58 + pytest.param(17, 5, 7, True, False, "triton", id="triton-write"),
59 + pytest.param(17, 5, 7, True, True, "triton", id="triton-write-add"),
60 + pytest.param(3, 8, 0, False, False, "triton", id="triton-full"),
61 + pytest.param(3, 8, 0, False, True, "triton", id="triton-full-add"),
62 + pytest.param(320, 1, 0, False, True, "nvidia", id="nvidia-1"),
63 + pytest.param(320, 4, 0, False, True, "nvidia", id="nvidia-4"),
64 + pytest.param(320, 8, 0, False, True, "nvidia", id="nvidia-8"),
65 + ],
66 +)
67 +def test_attn_res(
68 + num_tokens: int,
69 + num_blocks: int,
70 + row_padding: int,
71 + write_block: bool,
72 + has_delta: bool,
73 + backend: str,
74 +):
75 + if backend == "nvidia" and not current_platform.is_device_capability_family(100):
76 + pytest.skip("NVIDIA AttnRes requires the SM100 family")
77 +
78 + prefix = _randn_with_row_padding(num_tokens, HIDDEN_SIZE, padding=row_padding)
79 + delta = (
80 + _randn_with_row_padding(num_tokens, HIDDEN_SIZE, padding=row_padding)
81 + if has_delta
82 + else None
83 + )
84 + blocks = _randn_with_row_padding(
85 + num_tokens, MAX_BLOCKS, HIDDEN_SIZE, padding=row_padding
86 + )
87 + norm_weight = 1 + 0.1 * torch.randn(
88 + HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16
89 + )
90 + qk_weight = (
91 + torch.randn(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16) / HIDDEN_SIZE**0.5
92 + )
93 + output_norm_weight = 1 + 0.1 * torch.randn(
94 + HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16
95 + )
96 + original_blocks = blocks.clone()
97 + expected, expected_prefix = _reference(
98 + prefix.clone(),
99 + delta,
100 + blocks,
101 + norm_weight,
102 + qk_weight,
103 + output_norm_weight,
104 + num_blocks,
105 + )
106 + block_write_idx = num_blocks if write_block else -1
107 +
108 + actual = attn_res(
109 + prefix,
110 + delta,
111 + blocks,
112 + norm_weight,
113 + qk_weight,
114 + output_norm_weight,
115 + num_blocks,
116 + block_write_idx,
117 + EPS,
118 + EPS,
119 + )
120 +
121 + torch.testing.assert_close(actual, expected, atol=8e-2, rtol=3e-2)
122 + torch.testing.assert_close(prefix, expected_prefix, atol=0, rtol=0)
123 + if write_block:
124 + original_blocks[:, block_write_idx].copy_(expected_prefix)
125 + torch.testing.assert_close(blocks, original_blocks, atol=0, rtol=0)
126 + assert actual.is_contiguous()
127 +
128 +
129 +@pytest.mark.parametrize("num_blocks", range(MAX_BLOCKS + 1))
130 +def test_attn_res_block_counts(num_blocks: int):
131 + prefix = torch.randn(1, HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16)
132 + blocks = torch.randn(
133 + 1, MAX_BLOCKS, HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16
134 + )
135 + norm_weight = torch.ones(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16)
136 + qk_weight = (
137 + torch.randn(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16) / HIDDEN_SIZE**0.5
138 + )
139 + output_norm_weight = torch.ones_like(norm_weight)
140 + expected, _ = _reference(
141 + prefix.clone(),
142 + None,
143 + blocks,
144 + norm_weight,
145 + qk_weight,
146 + output_norm_weight,
147 + num_blocks,
148 + )
149 +
150 + actual = attn_res(
151 + prefix,
152 + None,
153 + blocks,
154 + norm_weight,
155 + qk_weight,
156 + output_norm_weight,
157 + num_blocks,
158 + -1,
159 + EPS,
160 + EPS,
161 + )
162 +
163 + torch.testing.assert_close(actual, expected, atol=8e-2, rtol=3e-2)
164 +
165 +
166 +def test_attn_res_without_output_norm():
167 + prefix = torch.randn(7, HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16)
168 + delta = torch.randn_like(prefix)
169 + blocks = torch.randn(
170 + 7, MAX_BLOCKS, HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16
171 + )
172 + norm_weight = torch.randn(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16)
173 + qk_weight = (
174 + torch.randn(HIDDEN_SIZE, device="cuda", dtype=torch.bfloat16) / HIDDEN_SIZE**0.5
175 + )
176 + expected, _ = _reference(
177 + prefix.clone(), delta, blocks, norm_weight, qk_weight, None, MAX_BLOCKS
178 + )
179 +
180 + actual = attn_res(
181 + prefix,
182 + delta,
183 + blocks,
184 + norm_weight,
185 + qk_weight,
186 + None,
187 + MAX_BLOCKS,
188 + -1,
189 + EPS,
190 + 0.0,
191 + )
192 +
193 + torch.testing.assert_close(actual, expected, atol=8e-2, rtol=3e-2)
vllm/_custom_ops.py
+27
@@ -2746,6 +2746,33 @@ def concat_and_cache_mla(
2746 )
2747
2748
2749 +def kimi_k3_attn_res(
2750 + prefix: torch.Tensor,
2751 + delta: torch.Tensor,
2752 + blocks: torch.Tensor,
2753 + norm_weight: torch.Tensor,
2754 + qk_weight: torch.Tensor,
2755 + output_norm_weight: torch.Tensor,
2756 + num_blocks: int,
2757 + eps: float,
2758 + output_norm_eps: float,
2759 +) -> torch.Tensor:
2760 + output = torch.empty_like(prefix)
2761 + torch.ops._C.kimi_k3_attn_res(
2762 + prefix,
2763 + delta,
2764 + blocks,
2765 + norm_weight,
2766 + qk_weight,
2767 + output_norm_weight,
2768 + output,
2769 + num_blocks,
2770 + eps,
2771 + output_norm_eps,
2772 + )
2773 + return output
2774 +
2775 +
2776 def concat_and_cache_mla_rope_fused(
2777 positions: torch.Tensor,
2778 q_pe: torch.Tensor,
vllm/models/kimi_k3/amd/__init__.py new
+2
@@ -0,0 +1,2 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
vllm/models/kimi_k3/amd/ops/__init__.py
vllm/models/kimi_k3/amd/ops/attn_res.py new
+132
@@ -0,0 +1,132 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +# SPDX-FileCopyrightText: Songlin Yang, Yu Zhang, Zhiyuan Li
4 +#
5 +# This file contains code adapted from the flash-linear-attention project.
6 +# The original source code was licensed under the MIT license and included
7 +# the following copyright notice:
8 +# Copyright (c) 2023-2026, Songlin Yang, Yu Zhang, Zhiyuan Li
9 +
10 +import torch
11 +
12 +from vllm.triton_utils import tl, triton
13 +
14 +
15 +@triton.jit
16 +def _attn_res_kernel(
17 + prefix_ptr,
18 + blocks_ptr,
19 + norm_weight_ptr,
20 + qk_weight_ptr,
21 + output_ptr,
22 + stride_prefix_m: tl.constexpr,
23 + stride_block_m: tl.constexpr,
24 + stride_block_r: tl.constexpr,
25 + stride_output_m: tl.constexpr,
26 + num_blocks: tl.constexpr,
27 + hidden_size: tl.constexpr,
28 + eps: tl.constexpr,
29 + BLOCK_L: tl.constexpr,
30 + BLOCK_D: tl.constexpr,
31 +):
32 + row_idx = tl.program_id(0).to(tl.int64)
33 + d_offsets = tl.max_contiguous(tl.arange(0, BLOCK_D), BLOCK_D)
34 + d_mask = d_offsets < hidden_size
35 +
36 + prefix = tl.load(
37 + prefix_ptr + row_idx * stride_prefix_m + d_offsets,
38 + mask=d_mask,
39 + other=0.0,
40 + ).to(tl.float32)
41 + input_qk_weight = tl.load(norm_weight_ptr + d_offsets, mask=d_mask, other=0.0).to(
42 + tl.float32
43 + ) * tl.load(qk_weight_ptr + d_offsets, mask=d_mask, other=0.0).to(tl.float32)
44 +
45 + max_logit = tl.full((), -float("inf"), tl.float32)
46 + denominator = tl.zeros((), tl.float32)
47 + mixed = tl.zeros((BLOCK_D,), tl.float32)
48 + num_sources = num_blocks + 1
49 +
50 + for source_tile in range(tl.cdiv(num_sources, BLOCK_L)):
51 + source_offsets = source_tile * BLOCK_L + tl.arange(0, BLOCK_L)
52 + source_mask = source_offsets < num_sources
53 + is_prefix = source_offsets == num_blocks
54 + block_ptrs = (
55 + blocks_ptr
56 + + row_idx * stride_block_m
57 + + source_offsets[:, None] * stride_block_r
58 + + d_offsets[None, :]
59 + )
60 + block_values = tl.load(
61 + block_ptrs,
62 + mask=(source_mask[:, None] & ~is_prefix[:, None] & d_mask[None, :]),
63 + other=0.0,
64 + eviction_policy="evict_first",
65 + ).to(tl.float32)
66 + values = tl.where(is_prefix[:, None], prefix[None, :], block_values)
67 + reciprocal_std = tl.rsqrt(
68 + tl.sum(values * values, axis=1) * (1.0 / hidden_size) + eps
69 + )
70 + logits = tl.sum(values * input_qk_weight[None, :], axis=1) * reciprocal_std
71 + scores = tl.where(source_mask, logits, -float("inf"))
72 +
73 + new_max_logit = tl.maximum(max_logit, tl.max(scores, axis=0))
74 + old_scale = tl.exp(max_logit - new_max_logit)
75 + block_scales = tl.exp(scores - new_max_logit)
76 + denominator = denominator * old_scale + tl.sum(block_scales, axis=0)
77 + mixed = mixed * old_scale + tl.sum(block_scales[:, None] * values, axis=0)
78 + max_logit = new_max_logit
79 +
80 + output = mixed / denominator
81 + tl.store(
82 + output_ptr + row_idx * stride_output_m + d_offsets,
83 + output,
84 + mask=d_mask,
85 + )
86 +
87 +
88 +def attn_res(
89 + prefix: torch.Tensor,
90 + blocks: torch.Tensor,
91 + norm_weight: torch.Tensor,
92 + qk_weight: torch.Tensor,
93 + num_blocks: int,
94 + eps: float,
95 +) -> torch.Tensor:
96 + num_tokens, hidden_size = prefix.shape
97 + assert 0 < num_blocks <= blocks.shape[1]
98 + assert blocks.shape[0] == num_tokens
99 + assert norm_weight.numel() == hidden_size
100 + assert qk_weight.numel() == hidden_size
101 + assert prefix.stride(-1) == 1
102 + assert blocks.stride(-1) == 1
103 + assert norm_weight.stride(-1) == 1
104 + assert qk_weight.stride(-1) == 1
105 +
106 + output = prefix.new_empty(prefix.shape)
107 + if num_tokens == 0:
108 + return output
109 +
110 + if num_tokens >= 256 or num_blocks <= 1:
111 + block_l, num_warps = 1, 4
112 + else:
113 + block_l, num_warps = 4, 8
114 + _attn_res_kernel[(num_tokens,)](
115 + prefix,
116 + blocks,
117 + norm_weight,
118 + qk_weight,
119 + output,
120 + prefix.stride(0),
121 + blocks.stride(0),
122 + blocks.stride(1),
123 + output.stride(0),
124 + num_blocks,
125 + hidden_size,
126 + eps,
127 + BLOCK_L=block_l,
128 + BLOCK_D=triton.next_power_of_2(hidden_size),
129 + num_warps=num_warps,
130 + num_stages=2,
131 + )
132 + return output
vllm/models/kimi_k3/nvidia/__init__.py new
+2
@@ -0,0 +1,2 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
vllm/models/kimi_k3/nvidia/ops/__init__.py new
+6
@@ -0,0 +1,6 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +from .attn_res import attn_res
5 +
6 +__all__ = ["attn_res"]
vllm/models/kimi_k3/nvidia/ops/attn_res.py new
+245
@@ -0,0 +1,245 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +# SPDX-FileCopyrightText: Songlin Yang, Yu Zhang, Zhiyuan Li
4 +#
5 +# This file contains code adapted from the flash-linear-attention project.
6 +# The original source code was licensed under the MIT license and included
7 +# the following copyright notice:
8 +# Copyright (c) 2023-2026, Songlin Yang, Yu Zhang, Zhiyuan Li
9 +
10 +
11 +import torch
12 +
13 +from vllm import _custom_ops as ops
14 +from vllm.platforms import current_platform
15 +from vllm.triton_utils import tl, triton
16 +
17 +
18 +# Consumed by kimi_k3_triton_warmup.py during kernel_warmup().
19 +def get_attn_res_triton_warmup_profiles(
20 + max_blocks: int,
21 +) -> tuple[tuple[int, bool, int, bool], ...]:
22 + """Return the small-batch profiles that bypass the native kernel."""
23 + profiles = [
24 + (num_blocks, False, -1, True) for num_blocks in range(2, max_blocks + 1)
25 + ]
26 + profiles.extend(
27 + (block_write_idx, True, block_write_idx, True)
28 + for block_write_idx in range(2, max_blocks)
29 + )
30 + profiles.append((max_blocks, True, -1, False))
31 + return tuple(profiles)
32 +
33 +
34 +@triton.jit
35 +def _attn_res_kernel(
36 + prefix_ptr,
37 + delta_ptr,
38 + blocks_ptr,
39 + norm_weight_ptr,
40 + qk_weight_ptr,
41 + output_norm_weight_ptr,
42 + output_ptr,
43 + stride_prefix_m: tl.constexpr,
44 + stride_delta_m: tl.constexpr,
45 + stride_block_m: tl.constexpr,
46 + stride_block_r: tl.constexpr,
47 + stride_output_m: tl.constexpr,
48 + num_blocks: tl.constexpr,
49 + hidden_size: tl.constexpr,
50 + block_write_idx: tl.constexpr,
51 + eps: tl.constexpr,
52 + output_norm_eps: tl.constexpr,
53 + HAS_DELTA: tl.constexpr,
54 + WRITE_BLOCK: tl.constexpr,
55 + APPLY_OUTPUT_NORM: tl.constexpr,
56 + BLOCK_L: tl.constexpr,
57 + BLOCK_D: tl.constexpr,
58 + launch_pdl: tl.constexpr,
59 +):
60 + row_idx = tl.program_id(0).to(tl.int64)
61 + d_offsets = tl.max_contiguous(tl.arange(0, BLOCK_D), BLOCK_D)
62 + d_mask = d_offsets < hidden_size
63 +
64 + if launch_pdl:
65 + tl.extra.cuda.gdc_wait()
66 +
67 + updated_prefix = tl.load(
68 + prefix_ptr + row_idx * stride_prefix_m + d_offsets,
69 + mask=d_mask,
70 + other=0.0,
71 + ).to(tl.float32)
72 + if HAS_DELTA:
73 + delta = tl.load(
74 + delta_ptr + row_idx * stride_delta_m + d_offsets,
75 + mask=d_mask,
76 + other=0.0,
77 + ).to(tl.float32)
78 + updated_prefix += delta
79 + # Match the BF16 prefix-add result before using it as a residual source.
80 + updated_prefix = updated_prefix.to(prefix_ptr.dtype.element_ty).to(tl.float32)
81 + tl.store(
82 + prefix_ptr + row_idx * stride_prefix_m + d_offsets,
83 + updated_prefix,
84 + mask=d_mask,
85 + )
86 + if WRITE_BLOCK:
87 + tl.store(
88 + blocks_ptr
89 + + row_idx * stride_block_m
90 + + block_write_idx * stride_block_r
91 + + d_offsets,
92 + updated_prefix,
93 + mask=d_mask,
94 + )
95 + # With only the prefix source, the AttnRes softmax is exactly one.
96 + if num_blocks == 0:
97 + mixed = updated_prefix
98 + else:
99 + # Reloading avoids keeping the full prefix vector live across the loop.
100 + if HAS_DELTA:
101 + tl.debug_barrier()
102 + input_qk_weight = tl.load(
103 + norm_weight_ptr + d_offsets, mask=d_mask, other=0.0
104 + ).to(tl.float32) * tl.load(
105 + qk_weight_ptr + d_offsets, mask=d_mask, other=0.0
106 + ).to(tl.float32)
107 + max_logit = tl.full((), -float("inf"), tl.float32)
108 + denominator = tl.zeros((), tl.float32)
109 + mixed = tl.zeros((BLOCK_D,), tl.float32)
110 +
111 + num_sources = num_blocks + 1
112 + for source_tile in range(tl.cdiv(num_sources, BLOCK_L)):
113 + source_offsets = source_tile * BLOCK_L + tl.arange(0, BLOCK_L)
114 + source_mask = source_offsets < num_sources
115 + is_prefix = source_offsets == num_blocks
116 + block_ptrs = (
117 + blocks_ptr
118 + + row_idx * stride_block_m
119 + + source_offsets[:, None] * stride_block_r
120 + + d_offsets[None, :]
121 + )
122 + prefix_ptrs = (
123 + prefix_ptr
124 + + row_idx * stride_prefix_m
125 + + source_offsets[:, None] * 0
126 + + d_offsets[None, :]
127 + )
128 + value_ptrs = tl.where(is_prefix[:, None], prefix_ptrs, block_ptrs)
129 + values = tl.load(
130 + value_ptrs,
131 + mask=source_mask[:, None] & d_mask[None, :],
132 + other=0.0,
133 + eviction_policy="evict_first",
134 + ).to(tl.float32)
135 + reciprocal_std = tl.rsqrt(
136 + tl.sum(values * values, axis=1) * (1.0 / hidden_size) + eps
137 + )
138 + logits = tl.sum(values * input_qk_weight[None, :], axis=1) * reciprocal_std
139 + scores = tl.where(source_mask, logits, -float("inf"))
140 +
141 + new_max_logit = tl.maximum(max_logit, tl.max(scores, axis=0))
142 + old_scale = tl.exp(max_logit - new_max_logit)
143 + block_scales = tl.exp(scores - new_max_logit)
144 + denominator = denominator * old_scale + tl.sum(block_scales, axis=0)
145 + mixed = mixed * old_scale + tl.sum(block_scales[:, None] * values, axis=0)
146 + max_logit = new_max_logit
147 +
148 + mixed /= denominator
149 + output = mixed
150 +
151 + if launch_pdl:
152 + tl.extra.cuda.gdc_launch_dependents()
153 +
154 + if APPLY_OUTPUT_NORM:
155 + output_reciprocal_std = tl.rsqrt(
156 + tl.sum(tl.where(d_mask, mixed * mixed, 0.0), axis=0) * (1.0 / hidden_size)
157 + + output_norm_eps
158 + )
159 + output_norm_weight = tl.load(
160 + output_norm_weight_ptr + d_offsets, mask=d_mask, other=0.0
161 + ).to(tl.float32)
162 + output = mixed * output_reciprocal_std * output_norm_weight
163 + tl.store(
164 + output_ptr + row_idx * stride_output_m + d_offsets,
165 + output,
166 + mask=d_mask,
167 + )
168 +
169 +
170 +def attn_res(
171 + prefix: torch.Tensor,
172 + delta: torch.Tensor | None,
173 + blocks: torch.Tensor,
174 + norm_weight: torch.Tensor,
175 + qk_weight: torch.Tensor,
176 + output_norm_weight: torch.Tensor | None,
177 + num_blocks: int,
178 + block_write_idx: int,
179 + eps: float,
180 + output_norm_eps: float,
181 +) -> torch.Tensor:
182 + num_tokens, hidden_size = prefix.shape
183 + assert prefix.stride(-1) == 1
184 + assert delta is None or delta.stride(-1) == 1
185 + assert blocks.stride(-1) == 1
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.
191 + if (
192 + hidden_size == 7168
193 + and delta is not None
194 + and output_norm_weight is not None
195 + and num_blocks > 0
196 + and block_write_idx < 0
197 + and current_platform.is_device_capability_family(100)
198 + ):
199 + return ops.kimi_k3_attn_res(
200 + prefix,
201 + delta,
202 + blocks,
203 + norm_weight,
204 + qk_weight,
205 + output_norm_weight,
206 + num_blocks,
207 + eps,
208 + output_norm_eps,
209 + )
210 + output = prefix.new_empty(prefix.shape)
211 + # Tuned on GB300: source tiling helps decode, while one-source tiles scale
212 + # better for prefill.
213 + # Keep get_attn_res_triton_warmup_profiles in sync with these fallbacks.
214 + if num_tokens >= 256 or num_blocks <= 1:
215 + block_l, num_warps = 1, 4
216 + else:
217 + block_l, num_warps = 4, 8
218 + _attn_res_kernel[(num_tokens,)](
219 + prefix,
220 + delta,
221 + blocks,
222 + norm_weight,
223 + qk_weight,
224 + output_norm_weight,
225 + output,
226 + prefix.stride(0),
227 + 0 if delta is None else delta.stride(0),
228 + blocks.stride(0),
229 + blocks.stride(1),
230 + output.stride(0),
231 + num_blocks,
232 + hidden_size,
233 + block_write_idx,
234 + eps,
235 + output_norm_eps,
236 + HAS_DELTA=delta is not None,
237 + WRITE_BLOCK=block_write_idx >= 0,
238 + APPLY_OUTPUT_NORM=output_norm_weight is not None,
239 + BLOCK_L=block_l,
240 + BLOCK_D=triton.next_power_of_2(hidden_size),
241 + num_warps=num_warps,
242 + num_stages=2,
243 + launch_pdl=current_platform.is_arch_support_pdl(),
244 + )
245 + return output