[Perf][Qwen4Exp] Fuse HC down projection and SiLU on NVIDIA (#58957)

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

Thien Tran committed Sep 28, 2026 at 18:51 UTC d6cce94fd4221e6f5547e1c00ca5e0c92ab52d6c
7 files changed +1175 -23
tests/models/qwen4_exp/test_hc_ops.py
+30
@@ -4,11 +4,13 @@
4 import pytest
5 import torch
6
7 +from vllm.models.qwen4_exp.nvidia.ops.cute_dsl.hc_down_silu import hc_down_silu
8 from vllm.models.qwen4_exp.nvidia.ops.hc import (
9 grouped_gemma_rmsnorm,
10 hc_combine,
11 hc_combine_norm,
12 hc_gate_mix,
13 + hc_silu,
14 )
15 from vllm.platforms import current_platform
16 from vllm.triton_utils import HAS_TRITON
@@ -22,6 +24,13 @@ HC = 4
24 HIDDEN_SIZE = 2560
25 HYPER_HIDDEN_SIZE = HC * HIDDEN_SIZE
26 EPS = 1e-6
27 +LORA_RANK = 320
28 +DOWN_N = LORA_RANK + HC + 12 # merged down+inject weight, 16-row padded
29 +
30 +requires_sm90 = pytest.mark.skipif(
31 + not current_platform.has_device_capability(90),
32 + reason="fused HC down+SiLU requires SM90+",
33 +)
34
35
36 def test_grouped_gemma_rmsnorm() -> None:
@@ -125,3 +134,24 @@ def test_hc_combine_norm_unit_injection(num_tokens: int) -> None:
134 expected_norm = grouped_gemma_rmsnorm(expected, weight, EPS, HC)
135 assert torch.equal(actual, expected)
136 torch.testing.assert_close(actual_norm, expected_norm)
137 +
138 +
139 +@requires_sm90
140 +@pytest.mark.parametrize("num_tokens", [1, 3, 5, 17, 48])
141 +def test_hc_down_silu_fused(num_tokens: int) -> None:
142 + # Compare computed columns with the unfused ll_bf16 + hc_silu reference.
143 + from vllm.model_executor.kernels.linear.cute_dsl.ll_bf16 import ll_bf16_gemm
144 +
145 + torch.manual_seed(0)
146 + x = torch.randn(num_tokens, HYPER_HIDDEN_SIZE, dtype=torch.bfloat16, device="cuda")
147 + weight = torch.randn(DOWN_N, HYPER_HIDDEN_SIZE, dtype=torch.bfloat16, device="cuda")
148 +
149 + lora, injection = hc_down_silu(x, weight, LORA_RANK, HC)
150 +
151 + down = ll_bf16_gemm(x, weight).to(torch.bfloat16)
152 + torch.testing.assert_close(
153 + lora, hc_silu(down[:, :LORA_RANK], HC), rtol=0.01, atol=0.01
154 + )
155 + torch.testing.assert_close(
156 + injection, down[:, LORA_RANK : LORA_RANK + HC], rtol=0.01, atol=0.01
157 + )
vllm/models/qwen4_exp/nvidia/hyperconnection.py
+38 -23
@@ -25,16 +25,19 @@ Typical usage inside a transformer decoder layer::
25 import torch
26 from torch import nn
27
28 +import vllm.envs as envs
29 from vllm.model_executor.layers.linear import (
30 MergedColumnParallelLinear,
31 ReplicatedLinear,
32 )
33 from vllm.model_executor.models.utils import maybe_prefix
34 +from vllm.platforms import current_platform
35
36 from ..common.hyperconnection import (
37 GroupedGemmaRMSNorm,
38 HyperConnectionConfig,
39 )
40 +from .ops.cute_dsl.hc_down_silu import MAX_FUSED_M, hc_down_silu
41 from .ops.hc import (
42 grouped_gemma_rmsnorm,
43 hc_combine,
@@ -58,9 +61,9 @@ class GatedResidual(nn.Module):
61 injection.
62
63 Weights: the norm owns the grouped GemmaRMSNorm affine; the projections
61 - are vLLM Linear modules (merged replicated linear for down+inject), so
62 - GEMM dispatch (e.g. the low-latency skinny GEMM) applies through the
63 - standard quant_method mechanism.
64 + are vLLM Linear modules (merged replicated linear for down+inject).
65 + Eligible down+inject projections use the fused SiLU GEMM; other projections
66 + use their Linear module's GEMM dispatch.
67 """
68
69 def __init__(
@@ -105,6 +108,12 @@ class GatedResidual(nn.Module):
108 return_bias=False,
109 disable_tp=True,
110 )
111 + weight = self.input_mix_weight_down_block_inject.weight
112 + self._use_hc_down_silu = (
113 + weight.shape[1] % 8 == 0
114 + and weight.dtype == torch.bfloat16
115 + and current_platform.has_device_capability(90)
116 + )
117 else:
118 self.input_mix_weight_down = ReplicatedLinear(
119 self.hyper_hidden_size,
@@ -125,6 +134,30 @@ class GatedResidual(nn.Module):
134 return_bias=False,
135 )
136
137 + def _down_and_inject(
138 + self, xn: torch.Tensor
139 + ) -> tuple[torch.Tensor, torch.Tensor | None]:
140 + """Down projection + SiLU; also returns injection logits if combined."""
141 + if not self.use_combine:
142 + return hc_silu(self.input_mix_weight_down(xn), self.hc_count), None
143 +
144 + use_fused = (
145 + self._use_hc_down_silu
146 + and not envs.VLLM_BATCH_INVARIANT
147 + and 1 <= xn.shape[0] <= MAX_FUSED_M
148 + )
149 + if use_fused:
150 + return hc_down_silu(
151 + xn,
152 + self.input_mix_weight_down_block_inject.weight,
153 + self.lora_rank,
154 + self.hc_count,
155 + )
156 + down_and_injection = self.input_mix_weight_down_block_inject(xn)
157 + split_sizes = [self.lora_rank, self.hc_count, self.pad_size]
158 + lora, injection, _ = down_and_injection.split(split_sizes, dim=-1)
159 + return hc_silu(lora, self.hc_count), injection
160 +
161 def mix(
162 self, hidden_states: torch.Tensor
163 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]:
@@ -135,16 +168,7 @@ class GatedResidual(nn.Module):
168 self.hc_count,
169 )
170
138 - if self.use_combine:
139 - # produce injection logits for combine
140 - split_sizes = [self.lora_rank, self.hc_count, self.pad_size]
141 - down_and_injection = self.input_mix_weight_down_block_inject(xn)
142 - lora, injection, _ = down_and_injection.split(split_sizes, dim=-1)
143 - else:
144 - lora = self.input_mix_weight_down(xn)
145 - injection = None
146 -
147 - lora = hc_silu(lora, self.hc_count)
171 + lora, injection = self._down_and_inject(xn)
172 gate = self.input_mix_weight_up(lora) # [M, D]
173 block_input = hc_gate_mix(xn, gate, self.hc_count)
174
@@ -172,16 +196,7 @@ class GatedResidual(nn.Module):
196 self.hc_count,
197 )
198
175 - if self.use_combine:
176 - # produce injection logits for combine
177 - split_sizes = [self.lora_rank, self.hc_count, self.pad_size]
178 - down_and_injection = self.input_mix_weight_down_block_inject(xn)
179 - lora, injection, _ = down_and_injection.split(split_sizes, dim=-1)
180 - else:
181 - lora = self.input_mix_weight_down(xn)
182 - injection = None
183 -
184 - lora = hc_silu(lora, self.hc_count)
199 + lora, injection = self._down_and_inject(xn)
200 gate = self.input_mix_weight_up(lora) # [M, D]
201 block_input = hc_gate_mix(xn, gate, self.hc_count)
202
vllm/models/qwen4_exp/nvidia/model.py
+11
@@ -78,6 +78,7 @@ from vllm.v1.kv_cache_interface import MambaSpec
78 from ..config import ATTENTION_LAYER_TYPES, QSA_LAYER_TYPE, Qwen4ExpConfig
79 from .hyperconnection import GatedResidual, HyperConnectionConfig
80 from .low_latency_gemm import enable_qwen4_exp_low_latency_gemm
81 +from .ops.cute_dsl.hc_down_silu import request_hc_down_silu_warmup
82 from .ple_layer import Qwen4ExpPLELayer
83 from .qsa import Qwen4ExpQSAAttention
84
@@ -686,6 +687,16 @@ class Qwen4ExpForCausalLM(
687 )
688 self.set_moe_parameters(self.model.layers)
689 enable_qwen4_exp_low_latency_gemm(self, self.model_config.dtype)
690 + if self.model_config.dtype == torch.bfloat16:
691 + # Precompile the fused HC down+SiLU kernels for every CUDA-graph
692 + # capture size in the fused dispatch range, so no CuTe-DSL JIT
693 + # happens during graph capture.
694 + request_hc_down_silu_warmup(
695 + vllm_config.compilation_config.cudagraph_capture_sizes or (),
696 + self.config.hc_lowrank,
697 + self.config.hc_count,
698 + self.config.hidden_size * self.config.hc_count,
699 + )
700
701 @staticmethod
702 def get_model_state_cls():
vllm/models/qwen4_exp/nvidia/ops/cute_dsl/__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/qwen4_exp/nvidia/ops/cute_dsl/_hc_down_silu_fma.py new
+334
@@ -0,0 +1,334 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Adapted from vllm/model_executor/kernels/linear/cute_dsl/_ll_bf16_dotprod.py.
4 +
5 +Fuses the Qwen4Exp mHC SiLU epilogue (SiLU(x/HC) on columns < rank, passthrough
6 +elsewhere, with the production bf16 rounding boundary) into the FMA kernel.
7 +Output is bf16 instead of fp32.
8 +"""
9 +
10 +import cutlass
11 +import cutlass.cute as cute
12 +from cuda.bindings.driver import CUstream
13 +from cutlass import const_expr
14 +
15 +
16 +def _sigmoid_f32(x):
17 + return cute.math.rcp(1.0 + cute.math.exp(-x), approx=True)
18 +
19 +
20 +class HcDownSiluFma:
21 + """BF16 GEMM kernel based on CTA-local FMA dot products, with the mHC SiLU
22 + epilogue fused into the final store.
23 +
24 + Computes C[M, N] = A[M, K] @ B[N, K]^T for bf16 inputs, rounds the fp32
25 + accumulator to bf16 (production rounding boundary), then applies
26 + SiLU(x/HC) to columns < ``rank`` and passes columns >= ``rank`` through.
27 + Output is bf16 [M, N].
28 +
29 + See the base class docstring in
30 + vllm/model_executor/kernels/linear/cute_dsl/_ll_bf16_dotprod.py for the
31 + GEMM structure; only the epilogue differs. The compile key is
32 + ``(M, K, threadblock_size, rank, hc)``.
33 + """
34 +
35 + def __init__(
36 + self,
37 + k: int,
38 + threadblock_size: int = 128,
39 + main_vec_width: int = 8,
40 + tail_vec_width: int = 4,
41 + prefetch_pdl_weights: bool = False,
42 + rank: int = 320,
43 + hc: int = 4,
44 + ):
45 + self.threadblock_size = threadblock_size
46 + self.main_vec_width = main_vec_width
47 + self.tail_vec_width = tail_vec_width
48 + self.prefetch_pdl_weights = prefetch_pdl_weights
49 + self.rank = rank
50 + self.hc = hc
51 + self.num_warps = threadblock_size // cute.arch.WARP_SIZE
52 + self._init_k_tiles(k)
53 + self.main_prefetch_tiles = min(self.main_tiles, 8)
54 +
55 + def _vectorized_elems(self, k_extent: int, vec_width: int) -> int:
56 + vector_tile = vec_width * self.threadblock_size
57 + return (k_extent // vector_tile) * vector_tile
58 +
59 + def _init_k_tiles(self, k: int) -> None:
60 + """Split K into vector loops, scalar rounds, and ragged tail."""
61 + self.k_main_elems = self._vectorized_elems(k, self.main_vec_width)
62 + self.k_after_main = k - self.k_main_elems
63 + self.k_tail_elems = self._vectorized_elems(
64 + self.k_after_main, self.tail_vec_width
65 + )
66 + self.k_done_all = self.k_main_elems + self.k_tail_elems
67 + self.scalar_rem = k - self.k_done_all
68 + self.ks_full = self.scalar_rem // self.threadblock_size
69 + self.ks_part = self.scalar_rem % self.threadblock_size
70 + self.k_scalar_full = self.ks_full * self.threadblock_size
71 + self.k_part_offset = self.k_done_all + self.k_scalar_full
72 + self.main_tiles = self.k_main_elems // (
73 + self.main_vec_width * self.threadblock_size
74 + )
75 + self.tail_tiles = self.k_tail_elems // (
76 + self.tail_vec_width * self.threadblock_size
77 + )
78 +
79 + @cute.jit
80 + def _vector_fma(
81 + self,
82 + acc: cute.Tensor,
83 + tA: cute.Tensor,
84 + tB: cute.Tensor,
85 + M: cutlass.Constexpr,
86 + num_tiles: cutlass.Constexpr,
87 + align_bytes: cutlass.Constexpr,
88 + ):
89 + for tile in cutlass.range_constexpr(num_tiles):
90 + bt = tB[None, tile]
91 + br = cute.make_rmem_tensor_like(bt)
92 + cute.autovec_copy(bt, br)
93 + br_f32 = br.load().to(cutlass.Float32)
94 +
95 + for m in cutlass.range_constexpr(M):
96 + at = tA[m, None, tile]
97 + ar = cute.make_rmem_tensor_like(at)
98 + cute.autovec_copy(at, ar)
99 + vec_width: cutlass.Constexpr = cute.size(ar)
100 + for v in cutlass.range_constexpr(vec_width):
101 + acc[m] = acc[m] + ar[v].to(cutlass.Float32) * br_f32[v]
102 +
103 + @cute.jit
104 + def _vector_fma_prefetched(
105 + self,
106 + acc: cute.Tensor,
107 + tA: cute.Tensor,
108 + tB: cute.Tensor,
109 + rB: cute.Tensor,
110 + M: cutlass.Constexpr,
111 + num_tiles: cutlass.Constexpr,
112 + prefetch_tiles: cutlass.Constexpr,
113 + ):
114 + for tile in cutlass.range_constexpr(num_tiles):
115 + if const_expr(tile < prefetch_tiles):
116 + br_f32 = rB[tile, None].load().to(cutlass.Float32)
117 + else:
118 + bt = tB[None, tile]
119 + br = cute.make_rmem_tensor_like(bt)
120 + cute.autovec_copy(bt, br)
121 + br_f32 = br.load().to(cutlass.Float32)
122 + for m in cutlass.range_constexpr(M):
123 + at = tA[m, None, tile]
124 + ar = cute.make_rmem_tensor_like(at)
125 + cute.autovec_copy(at, ar)
126 + vec_width: cutlass.Constexpr = cute.size(ar)
127 + for v in cutlass.range_constexpr(vec_width):
128 + acc[m] = acc[m] + ar[v].to(cutlass.Float32) * br_f32[v]
129 +
130 + def _make_thread_vector_slice(
131 + self,
132 + gA_vec: cute.Tensor,
133 + gB_vec: cute.Tensor,
134 + tidx: cutlass.Int32,
135 + n_idx: cutlass.Int32,
136 + threadblock_size: cutlass.Constexpr,
137 + ):
138 + # (M/N, K_TILE, K_LANE, K_VEC); tidx selects K_LANE.
139 + tA = cute.logical_divide(gA_vec, (None, (None, threadblock_size)))
140 + tB = cute.logical_divide(gB_vec, (None, (None, threadblock_size)))
141 + return tA[None, (None, (tidx, None))], tB[n_idx, (None, (tidx, None))]
142 +
143 + def _make_k_slice(
144 + self,
145 + gX: cute.Tensor,
146 + k_offset: cutlass.Constexpr,
147 + k_extent: cutlass.Constexpr,
148 + ):
149 + k_layout_extent: cutlass.Constexpr = (
150 + 1 if const_expr(k_extent == 0) else k_extent
151 + )
152 +
153 + if const_expr(k_offset == 0):
154 + return cute.local_tile(
155 + gX, (cute.size(gX, mode=[0]), k_layout_extent), (0, 0)
156 + )
157 + return cute.local_tile(
158 + cute.domain_offset((0, k_offset), gX),
159 + (cute.size(gX, mode=[0]), k_layout_extent),
160 + (0, 0),
161 + )
162 +
163 + @cute.jit
164 + def __call__(
165 + self,
166 + gA: cute.Tensor,
167 + gB: cute.Tensor,
168 + gC: cute.Tensor,
169 + M: cutlass.Constexpr,
170 + K_dim: cutlass.Constexpr,
171 + N_dim: cutlass.Int32,
172 + stream: CUstream,
173 + ):
174 + self.kernel(
175 + gA,
176 + gB,
177 + gC,
178 + M,
179 + self.main_vec_width,
180 + self.tail_vec_width,
181 + self.threadblock_size,
182 + self.num_warps,
183 + self.k_main_elems,
184 + self.k_tail_elems,
185 + self.k_done_all,
186 + self.ks_full,
187 + self.ks_part,
188 + self.k_scalar_full,
189 + self.k_part_offset,
190 + self.main_tiles,
191 + self.tail_tiles,
192 + self.rank,
193 + self.hc,
194 + ).launch(
195 + grid=[N_dim, 1, 1],
196 + block=[self.threadblock_size, 1, 1],
197 + smem=M * 4 * self.num_warps,
198 + stream=stream,
199 + use_pdl=True,
200 + min_blocks_per_mp=1,
201 + )
202 +
203 + @cute.kernel
204 + def kernel(
205 + self,
206 + gA: cute.Tensor,
207 + gB: cute.Tensor,
208 + gC: cute.Tensor,
209 + M: cutlass.Constexpr,
210 + main_vec_width: cutlass.Constexpr,
211 + tail_vec_width: cutlass.Constexpr,
212 + threadblock_size: cutlass.Constexpr,
213 + num_warps: cutlass.Constexpr,
214 + k_main_elems: cutlass.Constexpr,
215 + k_tail_elems: cutlass.Constexpr,
216 + k_done_all: cutlass.Constexpr,
217 + ks_full: cutlass.Constexpr,
218 + ks_part: cutlass.Constexpr,
219 + k_scalar_full: cutlass.Constexpr,
220 + k_part_offset: cutlass.Constexpr,
221 + main_tiles: cutlass.Constexpr,
222 + tail_tiles: cutlass.Constexpr,
223 + rank: cutlass.Constexpr,
224 + hc: cutlass.Constexpr,
225 + ):
226 + tidx, _, _ = cute.arch.thread_idx()
227 + n_idx, _, _ = cute.arch.block_idx()
228 + wid = cute.arch.warp_idx()
229 +
230 + # One FP32 accumulator per token.
231 + acc = cute.make_rmem_tensor((M,), cutlass.Float32)
232 + acc.fill(0.0)
233 +
234 + if const_expr(k_main_elems > 0):
235 + gA_main = self._make_k_slice(gA, 0, k_main_elems)
236 + gB_main = self._make_k_slice(gB, 0, k_main_elems)
237 + gA_vec = cute.logical_divide(gA_main, (None, main_vec_width))
238 + gB_vec = cute.logical_divide(gB_main, (None, main_vec_width))
239 + tA, tB = self._make_thread_vector_slice(
240 + gA_vec, gB_vec, tidx, n_idx, threadblock_size
241 + )
242 + if const_expr(self.prefetch_pdl_weights):
243 + prefetch_tiles: cutlass.Constexpr = self.main_prefetch_tiles
244 + prefetched_b = cute.make_rmem_tensor(
245 + cute.make_layout(
246 + (prefetch_tiles, main_vec_width),
247 + stride=(main_vec_width, 1),
248 + ),
249 + cutlass.BFloat16,
250 + )
251 + for tile in cutlass.range_constexpr(prefetch_tiles):
252 + cute.autovec_copy(tB[None, tile], prefetched_b[tile, None])
253 +
254 + cute.arch.griddepcontrol_wait()
255 +
256 + # 128-bit vectorized main loop
257 + if const_expr(k_main_elems > 0):
258 + if const_expr(self.prefetch_pdl_weights):
259 + self._vector_fma_prefetched(
260 + acc,
261 + tA,
262 + tB,
263 + prefetched_b,
264 + M,
265 + main_tiles,
266 + prefetch_tiles,
267 + )
268 + else:
269 + self._vector_fma(acc, tA, tB, M, main_tiles, 16)
270 +
271 + # 64-bit vectorized tail (K remainder after main loop)
272 + if const_expr(k_tail_elems > 0):
273 + gA_tail = self._make_k_slice(gA, k_main_elems, k_tail_elems)
274 + gB_tail = self._make_k_slice(gB, k_main_elems, k_tail_elems)
275 + gA_tail_vec = cute.logical_divide(gA_tail, (None, tail_vec_width))
276 + gB_tail_vec = cute.logical_divide(gB_tail, (None, tail_vec_width))
277 + tA_t, tB_t = self._make_thread_vector_slice(
278 + gA_tail_vec, gB_tail_vec, tidx, n_idx, threadblock_size
279 + )
280 + self._vector_fma(acc, tA_t, tB_t, M, tail_tiles, 8)
281 +
282 + # Full scalar rounds use CuTe width-1 tiles; KS_PART is the ragged tail.
283 + if const_expr(ks_full > 0):
284 + gA_scalar = self._make_k_slice(gA, k_done_all, k_scalar_full)
285 + gB_scalar = self._make_k_slice(gB, k_done_all, k_scalar_full)
286 + gA_scalar_vec = cute.logical_divide(gA_scalar, (None, 1))
287 + gB_scalar_vec = cute.logical_divide(gB_scalar, (None, 1))
288 + tA_s, tB_s = self._make_thread_vector_slice(
289 + gA_scalar_vec, gB_scalar_vec, tidx, n_idx, threadblock_size
290 + )
291 + self._vector_fma(acc, tA_s, tB_s, M, ks_full, 2)
292 +
293 + # Only threads below KS_PART load the ragged tail.
294 + if const_expr(ks_part > 0):
295 + gA_part = self._make_k_slice(gA, k_part_offset, ks_part)
296 + gB_part = self._make_k_slice(gB, k_part_offset, ks_part)
297 + if tidx < ks_part:
298 + bv2 = gB_part[n_idx, tidx].to(cutlass.Float32)
299 + for m in cutlass.range_constexpr(M):
300 + acc[m] = acc[m] + gA_part[m, tidx].to(cutlass.Float32) * bv2
301 +
302 + # Intra-warp shuffle reduction
303 + for m in cutlass.range_constexpr(M):
304 + acc[m] = cute.arch.warp_reduction_sum(acc[m])
305 +
306 + # Cross-warp reduction via shared memory
307 + smem_red_layout = cute.make_layout((M, num_warps), stride=(num_warps, 1))
308 + smem = cutlass.utils.SmemAllocator()
309 + sm = smem.allocate_tensor(cutlass.Float32, smem_red_layout, byte_alignment=16)
310 + with cute.arch.elect_one():
311 + for m in cutlass.range_constexpr(M):
312 + sm[m, wid] = acc[m]
313 +
314 + # Final reduction, fused mHC SiLU epilogue, and output.
315 + cute.arch.sync_threads()
316 + if tidx == 0:
317 + for m in cutlass.range_constexpr(M):
318 + partials = sm[m, None].load()
319 + total = cutlass.Float32(
320 + partials.reduce(
321 + cute.ReductionOp.ADD,
322 + init_val=cutlass.Float32(0.0),
323 + reduction_profile=0,
324 + )
325 + )
326 + # Production rounding boundary: GEMM output is materialized as
327 + # bf16 before _hc_silu_kernel reads it.
328 + x_bf16 = total.to(cutlass.BFloat16)
329 + if n_idx < rank:
330 + z = x_bf16.to(cutlass.Float32) * (1.0 / hc)
331 + gC[m, n_idx] = (z * _sigmoid_f32(z)).to(cutlass.BFloat16)
332 + else:
333 + gC[m, n_idx] = x_bf16
334 + cute.arch.griddepcontrol_launch_dependents()
vllm/models/qwen4_exp/nvidia/ops/cute_dsl/_hc_down_silu_mma.py new
+536
@@ -0,0 +1,536 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Adapted from vllm/model_executor/kernels/linear/cute_dsl/_ll_bf16_splitk.py.
4 +
5 +Fuses the Qwen4Exp mHC SiLU epilogue (SiLU(x/HC) on columns < rank, passthrough
6 +elsewhere, with the production bf16 rounding boundary) into the split-K kernel,
7 +applied after the cluster reduction where the final fp32 result materializes.
8 +Output is bf16 instead of fp32.
9 +"""
10 +
11 +import math
12 +
13 +import cutlass
14 +import cutlass.cute as cute
15 +from cuda.bindings.driver import CUstream
16 +from cutlass._mlir import ir as _ir
17 +from cutlass._mlir.dialects import llvm as _llvm
18 +from cutlass.cutlass_dsl import dsl_user_op
19 +from cutlass.pipeline import sm90 as pipeline
20 +
21 +
22 +# Map a local smem address to the same peer CTA DSMEM offset.
23 +@dsl_user_op
24 +def set_block_rank(smem_ptr, peer_rank, *, loc=None, ip=None):
25 + dsmem_ptr = cute.arch.map_dsmem_ptr(smem_ptr, peer_rank, loc=loc, ip=ip)
26 + return cutlass.Int32(dsmem_ptr.toint(loc=loc, ip=ip))
27 +
28 +
29 +# Plain DSMEM store; mbarrier helpers do not model this reduction.
30 +@dsl_user_op
31 +def st_shared_remote_f32(remote_addr, val, *, loc=None, ip=None):
32 + i32 = _ir.IntegerType.get_signless(32)
33 + addr_ir = remote_addr.ir_value(loc=loc, ip=ip)
34 + val_ir = val.ir_value(loc=loc, ip=ip)
35 + _llvm.inline_asm(
36 + i32,
37 + [addr_ir, val_ir],
38 + "st.shared::cluster.f32 [$0], $1; mov.u32 $2, 0;",
39 + "r,f,=r",
40 + has_side_effects=True, # keep the inline store ordered
41 + loc=loc,
42 + ip=ip,
43 + )
44 +
45 +
46 +def _sigmoid_f32(x):
47 + return cute.math.rcp(1.0 + cute.math.exp(-x), approx=True)
48 +
49 +
50 +class HcDownSiluMma:
51 + """BF16 GEMM kernel based on clustered split-K MMA, with the mHC SiLU
52 + epilogue fused into the final store.
53 +
54 + Computes C[M, N] = A[M, K] @ B[N, K]^T for bf16 inputs, rounds the reduced
55 + fp32 accumulator to bf16 (production rounding boundary), then applies
56 + SiLU(x/HC) to columns < ``rank`` and passes columns >= ``rank`` through.
57 + Output is bf16 [M, N].
58 +
59 + See the base class docstring in
60 + vllm/model_executor/kernels/linear/cute_dsl/_ll_bf16_splitk.py for the
61 + GEMM structure; only the epilogue differs. The compile key is
62 + ``(split_k, num_stages, tile_n, rank, hc)``.
63 + """
64 +
65 + def __init__(
66 + self,
67 + ab_dtype=cutlass.BFloat16,
68 + acc_dtype=cutlass.Float32,
69 + tile_n: int = 16,
70 + tile_k: int = 256,
71 + num_stages: int = 2,
72 + num_dma_warps: int = 4,
73 + split_k: int = 8,
74 + rank: int = 320,
75 + hc: int = 4,
76 + ):
77 + self.ab_dtype = ab_dtype
78 + self.acc_dtype = acc_dtype
79 + self.rank = rank
80 + self.hc = hc
81 + self.tile_m = 16
82 + self.tile_n = tile_n
83 + self.tile_k = tile_k
84 + self.copy_bits = 128
85 + self.num_stages = num_stages
86 + self.split_k = split_k
87 + self.mma_shape = (16, 8, 16) # mma.sync.aligned.m16n8k16
88 + self.atom_layout = (1, 1, 1) # one MMA atom per warp
89 + self.num_dma_warps = num_dma_warps
90 + self.num_mma_warps = 4
91 + self.num_dma_threads = self.num_dma_warps * cute.arch.WARP_SIZE
92 + self.num_mma_threads = self.num_mma_warps * cute.arch.WARP_SIZE
93 + self.num_threads = self.num_dma_threads + self.num_mma_threads
94 + self.num_epilogue_elems = self.tile_m * self.tile_n
95 + self.epilogue_elems_per_thread = self.num_epilogue_elems // self.num_mma_threads
96 +
97 + def _make_smem_layout_AB(self, dtype, copy_bits, smem_tiler):
98 + """Build the staged swizzled SMEM layout for A or B tiles."""
99 + major_size = min(smem_tiler[1], 64)
100 + # Match swizzle span to contiguous K bytes, capped by CuTe 3-bit swizzle.
101 + swizzle_bits = int(math.log2(major_size * dtype.width // copy_bits))
102 + swizzle_bits = min(swizzle_bits, 3)
103 + # Tile the swizzled atom across (M_or_N, K, stages).
104 + layout_atom_outer = cute.make_layout((8, major_size), stride=(major_size, 1))
105 + layout_atom = cute.make_composed_layout(
106 + cute.make_swizzle(swizzle_bits, 3, 3), 0, layout_atom_outer
107 + )
108 + return cute.tile_to_shape(layout_atom, smem_tiler, (0, 1, 2))
109 +
110 + def _make_gmem_tiled_copy(self, atom_copy, dtype, copy_bits, num_threads):
111 + """Build the per-thread cp.async vector-copy layout."""
112 + # Lay threads across K so each lane issues one vector copy.
113 + copy_elems = copy_bits // dtype.width
114 + k_threads = cute.size(self.tile_k) // copy_elems # threads along K
115 + thread_layout = cute.make_layout(
116 + (num_threads // k_threads, k_threads), stride=(k_threads, 1)
117 + )
118 + value_layout = cute.make_layout((1, copy_elems))
119 + return cute.make_tiled_copy_tv(atom_copy, thread_layout, value_layout)
120 +
121 + @cute.jit
122 + def _fill_pred(self, pred_flat, coord_tensor, k_tile, dim_limit, K_total):
123 + # Predicate one K tile and keep the stage-broadcast view in sync.
124 + coord_ktile = coord_tensor[None, None, 0, k_tile]
125 + num_vec = pred_flat.shape[0]
126 + num_mn = pred_flat.shape[1]
127 + for v in cutlass.range_constexpr(num_vec):
128 + # pred_flat is (K_VEC, M/N) for one K_TILE.
129 + for j in cutlass.range_constexpr(num_mn):
130 + pred_flat[v, j] = cute.elem_less(
131 + coord_ktile[(0, v), j], (dim_limit, K_total)
132 + )
133 +
134 + def _make_pred(self, tXcX, k_tile, dim_limit, K_total):
135 + # pred_flat is (K_VEC, M/N); pred is (K_VEC, M/N, STAGE).
136 + num_vec = tXcX.shape[0][1]
137 + num_mn = cute.size(tXcX, mode=[1])
138 + pred_flat = cute.make_rmem_tensor(
139 + cute.make_layout((num_vec, num_mn), stride=(num_mn, 1)),
140 + cutlass.Boolean,
141 + )
142 + pred = cute.make_tensor(
143 + pred_flat.iterator,
144 + cute.make_layout(
145 + (num_vec, num_mn, cute.size(tXcX, mode=[2])),
146 + stride=(num_mn, 1, 0),
147 + ),
148 + )
149 + return pred_flat, pred
150 +
151 + @cute.jit
152 + def __call__(
153 + self,
154 + mA: cute.Tensor,
155 + mB: cute.Tensor,
156 + mC: cute.Tensor,
157 + stream: CUstream,
158 + ):
159 + bM, bN, bK = self.tile_m, self.tile_n, self.tile_k
160 + copy_bits: cutlass.Constexpr = self.copy_bits
161 + sA_layout = self._make_smem_layout_AB(
162 + mA.element_type, copy_bits, (bM, bK, self.num_stages)
163 + )
164 + sB_layout = self._make_smem_layout_AB(
165 + mB.element_type, copy_bits, (bN, bK, self.num_stages)
166 + )
167 +
168 + @cute.struct
169 + class SharedStorage:
170 + a: cute.struct.Align[
171 + cute.struct.MemRange[mA.element_type, cute.cosize(sA_layout)], 16
172 + ]
173 + b: cute.struct.Align[
174 + cute.struct.MemRange[mB.element_type, cute.cosize(sB_layout)], 16
175 + ]
176 + mbar: cute.struct.Align[
177 + cute.struct.MemRange[cutlass.Int64, self.num_stages * 2], 8
178 + ]
179 +
180 + atom_g2s = cute.make_copy_atom(
181 + cute.nvgpu.cpasync.CopyG2SOp(
182 + cache_mode=cute.nvgpu.cpasync.LoadCacheMode.GLOBAL
183 + ),
184 + mA.element_type,
185 + num_bits_per_copy=copy_bits,
186 + ) # cp.async GMEM -> SMEM, bypassing L1
187 + tiled_copy_A = self._make_gmem_tiled_copy(
188 + atom_g2s, mA.element_type, copy_bits, self.num_dma_threads
189 + )
190 + tiled_copy_B = self._make_gmem_tiled_copy(
191 + atom_g2s, mB.element_type, copy_bits, self.num_dma_threads
192 + )
193 + op = cute.nvgpu.warp.MmaF16BF16Op(self.ab_dtype, self.acc_dtype, self.mma_shape)
194 + # Repeat the m16n8k16 atom along N to cover the CTA output tile.
195 + perm_mnk = (
196 + self.atom_layout[0] * self.mma_shape[0],
197 + self.atom_layout[1] * self.mma_shape[1] * (self.tile_n // 8),
198 + self.atom_layout[2] * self.mma_shape[2],
199 + )
200 + tiled_mma = cute.make_tiled_mma(
201 + op, cute.make_layout(self.atom_layout), permutation_mnk=perm_mnk
202 + )
203 + tiler_mn = (bM, bN)
204 + grid_m, grid_n = cute.ceil_div(mC.shape, tiler_mn)
205 + self.kernel(
206 + mA,
207 + mB,
208 + mC,
209 + sA_layout,
210 + sB_layout,
211 + tiled_copy_A,
212 + tiled_copy_B,
213 + tiled_mma,
214 + SharedStorage,
215 + self.rank,
216 + self.hc,
217 + ).launch(
218 + grid=[
219 + cute.size(grid_m),
220 + cute.size(grid_n),
221 + self.split_k,
222 + ],
223 + block=[self.num_threads, 1, 1],
224 + cluster=[
225 + 1,
226 + 1,
227 + self.split_k,
228 + ], # split-K CTAs form one cluster
229 + stream=stream,
230 + use_pdl=True,
231 + )
232 +
233 + @cute.kernel
234 + def kernel(
235 + self,
236 + mA,
237 + mB,
238 + mC,
239 + sA_layout: cute.ComposedLayout,
240 + sB_layout: cute.ComposedLayout,
241 + tiled_copy_A: cute.TiledCopy,
242 + tiled_copy_B: cute.TiledCopy,
243 + tiled_mma: cute.TiledMma,
244 + shared_storage: cutlass.Constexpr,
245 + rank: cutlass.Constexpr,
246 + hc: cutlass.Constexpr,
247 + ):
248 + bM, bN, bK = self.tile_m, self.tile_n, self.tile_k
249 + num_stages = self.num_stages
250 + tidx, _, _ = cute.arch.thread_idx()
251 + bid_m, bid_n, bid_z = cute.arch.block_idx()
252 + warp_idx = cute.arch.warp_idx()
253 + lane_id = cute.arch.lane_idx()
254 + num_dma_warps: cutlass.Constexpr = self.num_dma_warps
255 + is_dma_warp = warp_idx < num_dma_warps
256 + dma_tidx = tidx
257 + mma_tidx = tidx - self.num_dma_threads
258 + mma_warp_idx = warp_idx - num_dma_warps
259 +
260 + cta_tiler = (bM, bN, bK)
261 + coord = (bid_m, bid_n, None) # all K tiles
262 + # CTA-local tiles.
263 + gA = cute.local_tile(
264 + mA, tiler=cta_tiler, coord=coord, proj=(1, None, 1)
265 + ) # skip N
266 + gB = cute.local_tile(
267 + mB, tiler=cta_tiler, coord=coord, proj=(None, 1, 1)
268 + ) # skip M
269 + gC = cute.local_tile(
270 + mC, tiler=cta_tiler, coord=coord, proj=(1, 1, None)
271 + ) # skip K
272 +
273 + mcA = cute.make_identity_tensor(mA.layout.shape)
274 + mcB = cute.make_identity_tensor(mB.layout.shape)
275 + mcC = cute.make_identity_tensor(mC.layout.shape)
276 + # Coordinate modes: cA=(M,K,k_tile), cB=(N,K,k_tile), cC=(M,N).
277 + cA = cute.local_tile(mcA, tiler=cta_tiler, coord=coord, proj=(1, None, 1))
278 + cB = cute.local_tile(mcB, tiler=cta_tiler, coord=coord, proj=(None, 1, 1))
279 + cC = cute.local_tile(mcC, tiler=cta_tiler, coord=coord, proj=(1, 1, None))
280 +
281 + # 128-bit cp.async copies require 16-byte aligned GMEM views.
282 + gA = cute.make_tensor(gA.iterator.align(16), gA.layout)
283 + gB = cute.make_tensor(gB.iterator.align(16), gB.layout)
284 +
285 + smem = cutlass.utils.SmemAllocator()
286 + storage_ptr = smem.allocate(shared_storage.size_in_bytes(), byte_alignment=16) # type: ignore[attr-defined]
287 + storage = shared_storage(storage_ptr) # type: ignore[call-arg]
288 + sA = storage.a.get_tensor(sA_layout)
289 + sB = storage.b.get_tensor(sB_layout)
290 +
291 + # Pipeline cp.async producers into MMA consumers.
292 + producer_group = pipeline.CooperativeGroup(
293 + pipeline.Agent.Thread, self.num_dma_threads
294 + )
295 + consumer_group = pipeline.CooperativeGroup(
296 + pipeline.Agent.Thread, self.num_mma_threads
297 + )
298 + mainloop_pipeline = pipeline.PipelineCpAsync.create(
299 + barrier_storage=storage.mbar.data_ptr(),
300 + num_stages=num_stages,
301 + producer_group=producer_group,
302 + consumer_group=consumer_group,
303 + )
304 +
305 + # Round-robin split-K: split z handles tiles z, z+split_k, ...
306 + K_total = cute.size(mA, mode=[1])
307 + k_tile_count = cute.size(gA, mode=[2])
308 + k_start = bid_z
309 + num_k_tiles = cute.ceil_div(k_tile_count - k_start, self.split_k)
310 +
311 + if is_dma_warp:
312 + # DMA warps trade registers for copy throughput.
313 + cute.arch.setmaxregister_decrease(40)
314 + thr_A = tiled_copy_A.get_slice(dma_tidx)
315 + thr_B = tiled_copy_B.get_slice(dma_tidx)
316 + tAgA = thr_A.partition_S(gA)
317 + tAsA = thr_A.partition_D(sA)
318 + tBgB = thr_B.partition_S(gB)
319 + tBsB = thr_B.partition_D(sB)
320 + tAcA = thr_A.partition_S(cA)
321 + tBcB = thr_B.partition_S(cB)
322 +
323 + # Build M/K and N/K predicates, broadcast across K-tile copies.
324 + tApA_flat, tApA = self._make_pred(tAcA, k_start, mA.shape[0], K_total)
325 + tBpB_flat, tBpB = self._make_pred(tBcB, k_start, mB.shape[0], K_total)
326 + self._fill_pred(tApA_flat, tAcA, k_start, mA.shape[0], K_total)
327 + self._fill_pred(tBpB_flat, tBcB, k_start, mB.shape[0], K_total)
328 +
329 + producer_state = pipeline.make_pipeline_state(
330 + pipeline.PipelineUserType.Producer, num_stages
331 + )
332 +
333 + # Prime the first pipeline stage.
334 + mainloop_pipeline.producer_acquire(producer_state)
335 + cute.copy(
336 + tiled_copy_B,
337 + tBgB[None, None, None, k_start],
338 + tBsB[None, None, None, producer_state.index],
339 + pred=tBpB,
340 + )
341 + cute.arch.griddepcontrol_wait()
342 + cute.copy(
343 + tiled_copy_A,
344 + tAgA[None, None, None, k_start],
345 + tAsA[None, None, None, producer_state.index],
346 + pred=tApA,
347 + )
348 + mainloop_pipeline.producer_commit(producer_state)
349 + producer_state.advance()
350 +
351 + for k_tile in cutlass.range(
352 + k_start + self.split_k, k_tile_count, self.split_k, unroll=1
353 + ):
354 + self._fill_pred(tApA_flat, tAcA, k_tile, mA.shape[0], K_total)
355 + self._fill_pred(tBpB_flat, tBcB, k_tile, mB.shape[0], K_total)
356 + mainloop_pipeline.producer_acquire(producer_state)
357 + cute.copy(
358 + tiled_copy_A,
359 + tAgA[None, None, None, k_tile],
360 + tAsA[None, None, None, producer_state.index],
361 + pred=tApA,
362 + )
363 + cute.copy(
364 + tiled_copy_B,
365 + tBgB[None, None, None, k_tile],
366 + tBsB[None, None, None, producer_state.index],
367 + pred=tBpB,
368 + )
369 + mainloop_pipeline.producer_commit(producer_state)
370 + producer_state.advance()
371 +
372 + mainloop_pipeline.producer_tail(producer_state)
373 +
374 + else:
375 + # MMA warps with k-phase interleaving
376 + cute.arch.setmaxregister_increase(232) # large MMA fragments
377 +
378 + num_mma_warps: cutlass.Constexpr = self.num_mma_warps
379 +
380 + thr_mma = tiled_mma.get_slice(lane_id)
381 + tCsA = thr_mma.partition_A(sA)
382 + tCsB = thr_mma.partition_B(sB)
383 + tCgC = thr_mma.partition_C(gC)
384 +
385 + tCrA = tiled_mma.make_fragment_A(tCsA[None, None, None, 0])
386 + tCrB = tiled_mma.make_fragment_B(tCsB[None, None, None, 0])
387 + tCrC = tiled_mma.make_fragment_C(tCgC)
388 + tCrC.fill(0.0)
389 +
390 + # ldmatrix moves SMEM fragments into MMA registers.
391 + atom_s2r_A = cute.make_copy_atom(
392 + cute.nvgpu.warp.LdMatrix8x8x16bOp(False, 4), mA.element_type
393 + ) # non-transposed, x4
394 + atom_s2r_B = cute.make_copy_atom(
395 + cute.nvgpu.warp.LdMatrix8x8x16bOp(False, 4), mB.element_type
396 + )
397 +
398 + # SMEM -> register copy path.
399 + tiled_s2r_A = cute.make_tiled_copy_A(atom_s2r_A, tiled_mma)
400 + tiled_s2r_B = cute.make_tiled_copy_B(atom_s2r_B, tiled_mma)
401 + thr_s2r_A = tiled_s2r_A.get_slice(lane_id)
402 + thr_s2r_B = tiled_s2r_B.get_slice(lane_id)
403 + tCsA_v = thr_s2r_A.partition_S(sA) # views, not copies
404 + tCrA_v = thr_s2r_A.retile(tCrA)
405 + tCsB_v = thr_s2r_B.partition_S(sB)
406 + tCrB_v = thr_s2r_B.retile(tCrB)
407 + # Split the MMA K-fragments across the MMA warps.
408 + tCsA_warp_v = cute.logical_divide(tCsA_v, (None, None, num_mma_warps, None))
409 + tCsB_warp_v = cute.logical_divide(tCsB_v, (None, None, num_mma_warps, None))
410 +
411 + num_k_blocks = cute.size(tCrA, mode=[2])
412 + k_blocks_per_warp: cutlass.Constexpr = num_k_blocks // num_mma_warps
413 +
414 + consumer_state = pipeline.make_pipeline_state(
415 + pipeline.PipelineUserType.Consumer, num_stages
416 + )
417 +
418 + # Shape-dynamic split-K count, so this stays a runtime range.
419 + for _ in cutlass.range(num_k_tiles, unroll_full=True):
420 + mainloop_pipeline.consumer_wait(consumer_state)
421 + for ki in cutlass.range_constexpr(k_blocks_per_warp):
422 + cute.copy(
423 + tiled_s2r_A,
424 + tCsA_warp_v[
425 + None, None, (mma_warp_idx, ki), consumer_state.index
426 + ],
427 + tCrA_v[None, None, 0],
428 + ) # ldmatrix
429 + cute.copy(
430 + tiled_s2r_B,
431 + tCsB_warp_v[
432 + None, None, (mma_warp_idx, ki), consumer_state.index
433 + ],
434 + tCrB_v[None, None, 0],
435 + )
436 + cute.gemm(
437 + tiled_mma, tCrC, tCrA[None, None, 0], tCrB[None, None, 0], tCrC
438 + ) # mma.sync
439 + mainloop_pipeline.consumer_release(consumer_state)
440 + consumer_state.advance()
441 +
442 + # Cluster reduction epilogue.
443 + # Reduce per-warp accumulators within this CTA.
444 + num_elems: cutlass.Constexpr = self.num_epilogue_elems
445 + elems_per_thread: cutlass.Constexpr = self.epilogue_elems_per_thread
446 + # Map MMA threads to linear CTA output elements.
447 + epilogue_thread_layout = cute.make_layout(
448 + (elems_per_thread, self.num_mma_threads),
449 + stride=(self.num_mma_threads, 1),
450 + )
451 + epilogue_slots = cute.make_tensor(0, epilogue_thread_layout)
452 + epilogue_slot_coords = cute.make_identity_tensor((bN, bM))
453 + # Layout: (mma_warp, linear MN element).
454 + smem_red = cute.make_tensor(
455 + cute.arch.alloc_smem(
456 + cutlass.Float32, num_elems * num_mma_warps, alignment=16
457 + ),
458 + cute.make_layout((num_mma_warps, num_elems), stride=(num_elems, 1)),
459 + )
460 + smem_warp = cute.make_tensor(
461 + cute.domain_offset((mma_warp_idx, 0), smem_red).iterator,
462 + cute.make_layout((bM, bN), stride=(bN, 1)),
463 + )
464 + tCsC_partial = thr_mma.partition_C(smem_warp)
465 + cute.autovec_copy(tCrC, tCsC_partial)
466 + cute.arch.sync_threads()
467 +
468 + # Layout: (split-K rank, linear MN element).
469 + partials = cute.make_tensor(
470 + cute.arch.alloc_smem(
471 + cutlass.Float32, num_elems * self.split_k, alignment=16
472 + ),
473 + cute.make_layout((self.split_k, num_elems), stride=(num_elems, 1)),
474 + )
475 + cta_rank = cute.arch.block_idx_in_cluster()
476 +
477 + for ei in cutlass.range_constexpr(elems_per_thread):
478 + elem_idx = epilogue_slots[ei, mma_tidx]
479 + local_coord = cute.select(epilogue_slot_coords[elem_idx], mode=[1, 0])
480 + total = cutlass.Float32(0.0)
481 + if cute.elem_less(local_coord, gC.shape):
482 + total = (
483 + smem_red[None, elem_idx]
484 + .load()
485 + .reduce(
486 + cute.ReductionOp.ADD,
487 + init_val=cutlass.Float32(0.0),
488 + reduction_profile=0,
489 + )
490 + )
491 + partials[cta_rank, elem_idx] = total
492 +
493 + cute.arch.sync_threads()
494 +
495 + # Broadcast this CTA's partials to peer DSMEM.
496 + for ei in cutlass.range_constexpr(elems_per_thread):
497 + elem_idx = epilogue_slots[ei, mma_tidx]
498 + my_slot = cute.domain_offset((cta_rank, elem_idx), partials).iterator
499 + my_val = partials[cta_rank, elem_idx]
500 + for peer in cutlass.range_constexpr(self.split_k):
501 + remote = set_block_rank(my_slot, cutlass.Int32(peer))
502 + st_shared_remote_f32(remote, my_val)
503 +
504 + cute.arch.cluster_arrive()
505 + cute.arch.cluster_wait() # peer DSMEM stores are now visible
506 +
507 + if mma_tidx == 0:
508 + cute.arch.griddepcontrol_launch_dependents()
509 + cute.arch.sync_threads()
510 +
511 + # Reduce split-K partials, apply the fused mHC SiLU epilogue, and
512 + # write the bf16 global output.
513 + for ei in cutlass.range_constexpr(elems_per_thread):
514 + elem_idx = epilogue_slots[ei, mma_tidx]
515 + local_coord = cute.select(epilogue_slot_coords[elem_idx], mode=[1, 0])
516 + global_coord = cC[local_coord]
517 + if cute.elem_less(global_coord, mC.shape):
518 + acc = cutlass.Float32(
519 + partials[None, elem_idx]
520 + .load()
521 + .reduce(
522 + cute.ReductionOp.ADD,
523 + init_val=cutlass.Float32(0.0),
524 + reduction_profile=0,
525 + )
526 + )
527 + # Production rounding boundary: the GEMM result is
528 + # materialized as bf16 before _hc_silu_kernel reads it.
529 + x_bf16 = acc.to(cutlass.BFloat16)
530 + if global_coord[1] < rank:
531 + z = x_bf16.to(cutlass.Float32) * (1.0 / hc)
532 + gC[local_coord] = (z * _sigmoid_f32(z)).to(cutlass.BFloat16)
533 + else:
534 + gC[local_coord] = x_bf16
535 +
536 + cute.arch.sync_threads()
vllm/models/qwen4_exp/nvidia/ops/cute_dsl/hc_down_silu.py new
+224
@@ -0,0 +1,224 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Adapted from vllm/model_executor/kernels/linear/cute_dsl/ll_bf16.py.
4 +
5 +Fused Qwen4Exp mHC down projection + SiLU epilogue, where
6 +columns < lora_rank get ``silu(bf16(acc) / hc_count)``, the hc_count
7 +injection-logit columns pass through as ``bf16(acc)``, and the pad columns
8 +are never computed. Output is bf16 (ll_bf16's fp32-output bonus is given up
9 +to preserve the production rounding boundary).
10 +
11 +The GEMM uses an FMA backend for M <= 4 and a split-K MMA backend beyond,
12 +both with PDL. The model uses its Linear module above M=48.
13 +"""
14 +
15 +import logging
16 +from collections.abc import Iterable
17 +from functools import lru_cache, partial
18 +from typing import Any, Literal
19 +
20 +import torch
21 +
22 +from vllm.platforms import current_platform
23 +
24 +logger = logging.getLogger(__name__)
25 +
26 +# The fused kernel stops winning past M ~ 64 on Qwen3.8-Next-Flash.
27 +MAX_FUSED_M = 48
28 +
29 +# Split-K configs tuned on SM100f for Qwen3.8-Flash-Next (K=10240, N=324).
30 +_SM100F_TUNED_SPLITK: dict[int, tuple[int, int, int]] = {
31 + **{m: (6, 5, 8) for m in range(5, 17)},
32 + **{m: (6, 5, 16) for m in range(17, 33)},
33 + **{m: (6, 4, 16) for m in range(33, MAX_FUSED_M + 1)},
34 +}
35 +
36 +# FMA: (backend, M, K, threadblock_size); MMA: (backend, split_k, stages, tile_n).
37 +CompileKey = tuple[Literal["fma", "mma"], int, int, int]
38 +
39 +
40 +class HcDownSiluGemm:
41 + """Dispatch/compile cache for the fused mHC down+SiLU GEMM."""
42 +
43 + def __init__(
44 + self, rank: int, hc: int, k: int, *, prefetch_pdl_weights: bool = False
45 + ) -> None:
46 + self.rank = rank
47 + self.hc = hc
48 + self.k = k
49 + self._prefetch_pdl_weights = prefetch_pdl_weights
50 + self._compiled: dict[CompileKey, Any] = {}
51 + self._warmup_m: set[int] = set()
52 + self._warmup_registered = False
53 +
54 + def dispatch(self, m: int) -> CompileKey:
55 + if m <= 4 or self.k < 2048:
56 + return ("fma", m, self.k, 128)
57 + return ("mma", *_SM100F_TUNED_SPLITK[m])
58 +
59 + def compile(self, compile_key: CompileKey) -> None:
60 + if compile_key in self._compiled:
61 + return
62 +
63 + import cutlass.cute as cute
64 + from cutlass import BFloat16
65 + from quack.compile_utils import make_fake_tensor
66 +
67 + N = cute.sym_int()
68 + gemm: Any
69 + extra_args: tuple[int, ...]
70 + if compile_key[0] == "mma":
71 + from ._hc_down_silu_mma import HcDownSiluMma
72 +
73 + _, split_k, num_stages, tile_n = compile_key
74 + M, K = cute.sym_int(), cute.sym_int()
75 + gemm = HcDownSiluMma(
76 + tile_n=tile_n,
77 + num_stages=num_stages,
78 + split_k=split_k,
79 + rank=self.rank,
80 + hc=self.hc,
81 + )
82 + extra_args = ()
83 + options = "--enable-tvm-ffi"
84 + else:
85 + from ._hc_down_silu_fma import HcDownSiluFma
86 +
87 + _, M, K, threadblock_size = compile_key
88 + gemm = HcDownSiluFma(
89 + k=K,
90 + threadblock_size=threadblock_size,
91 + prefetch_pdl_weights=self._prefetch_pdl_weights,
92 + rank=self.rank,
93 + hc=self.hc,
94 + )
95 + extra_args = (M, K, 1) # runtime N placeholder
96 + options = "--enable-tvm-ffi --ptxas-options -maxrregcount=64"
97 +
98 + hidden_states = make_fake_tensor(BFloat16, (M, K), divisibility=8)
99 + router_weight = make_fake_tensor(BFloat16, (N, K), divisibility=8)
100 + output = make_fake_tensor(BFloat16, (M, N), divisibility=1)
101 + self._compiled[compile_key] = cute.compile(
102 + gemm,
103 + hidden_states,
104 + router_weight,
105 + output,
106 + *extra_args,
107 + cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True),
108 + options=options,
109 + )
110 + logger.debug("Compiled hc_down_silu: %s", compile_key)
111 +
112 + def request_warmup(self, m_values: Iterable[int]) -> None:
113 + m_values = set(m_values)
114 + if not m_values:
115 + return
116 + self._warmup_m.update(m_values)
117 + if self._warmup_registered:
118 + return
119 + from vllm.model_executor.warmup.cutedsl_warmup import (
120 + register_cutedsl_warmup_provider,
121 + )
122 +
123 + register_cutedsl_warmup_provider(self)
124 + self._warmup_registered = True
125 +
126 + def get_cutedsl_warmup_compile_units(self):
127 + from vllm.model_executor.warmup.cutedsl_warmup import CuTeDSLCompileUnit
128 +
129 + return tuple(
130 + CuTeDSLCompileUnit(
131 + name="Qwen4Exp HC down+SiLU GEMM",
132 + key=(
133 + "qwen4-exp-hc-down-silu",
134 + self.rank,
135 + self.hc,
136 + self.k,
137 + self._prefetch_pdl_weights,
138 + compile_key,
139 + ),
140 + compile=partial(self.compile, compile_key),
141 + )
142 + for compile_key in dict.fromkeys(
143 + self.dispatch(m) for m in sorted(self._warmup_m)
144 + )
145 + )
146 +
147 + def __call__(
148 + self,
149 + hidden_states: torch.Tensor, # [M, K] bf16
150 + router_weight: torch.Tensor, # [N, K] bf16
151 + ) -> torch.Tensor: # [M, N] bf16
152 + M = hidden_states.shape[0]
153 + N = router_weight.shape[0]
154 + n_compute = self.rank + self.hc
155 + w_gemm = router_weight[:n_compute]
156 + compile_key = self.dispatch(M)
157 + self.compile(compile_key)
158 + kernel = self._compiled[compile_key]
159 +
160 + output = torch.empty(M, N, dtype=torch.bfloat16, device=hidden_states.device)
161 + out_gemm = output[:, :n_compute]
162 + if compile_key[0] == "mma":
163 + kernel(hidden_states, w_gemm, out_gemm)
164 + else:
165 + kernel(hidden_states, w_gemm, out_gemm, n_compute)
166 + return output
167 +
168 +
169 +@lru_cache
170 +def _get_kernel(
171 + rank: int, hc: int, k: int, prefetch_pdl_weights: bool = False
172 +) -> HcDownSiluGemm:
173 + return HcDownSiluGemm(rank, hc, k, prefetch_pdl_weights=prefetch_pdl_weights)
174 +
175 +
176 +def hc_down_silu(
177 + x: torch.Tensor,
178 + weight: torch.Tensor,
179 + rank: int,
180 + hc: int,
181 +) -> tuple[torch.Tensor, torch.Tensor]:
182 + """Fused Qwen4Exp HC down projection + SiLU.
183 +
184 + Args:
185 + x: Normalized hyper-hidden input, [M, K].
186 + weight: Merged down+inject weight, [N, K].
187 + rank: Number of low-rank output columns.
188 + hc: Number of injection-logit output columns.
189 +
190 + Returns:
191 + LoRA activations [M, rank] and injection logits [M, hc].
192 +
193 + """
194 + kernel = _get_kernel(rank, hc, weight.shape[1], x.shape[0] == 1)
195 + output = kernel(x, weight)
196 + return output[:, :rank], output[:, rank : rank + hc]
197 +
198 +
199 +def request_hc_down_silu_warmup(
200 + m_values: Iterable[int], rank: int, hc: int, k: int
201 +) -> None:
202 + """Precompile the fused kernels for CUDA-graph capture token counts.
203 +
204 + Args:
205 + m_values: Token counts the model may capture CUDA graphs for; values
206 + outside the fused dispatch range are ignored.
207 + rank: Number of low-rank output columns.
208 + hc: Number of injection-logit output columns.
209 + k: Input feature size.
210 +
211 + """
212 + if not current_platform.has_device_capability(90) or k % 8 != 0:
213 + return
214 + m_set = {int(m) for m in m_values if 1 <= m <= MAX_FUSED_M}
215 + # M=1 dispatches to the weight-prefetching PDL variant.
216 + _get_kernel(rank, hc, k, True).request_warmup(m_set & {1})
217 + _get_kernel(rank, hc, k).request_warmup(m_set - {1})
218 +
219 +
220 +__all__ = [
221 + "MAX_FUSED_M",
222 + "hc_down_silu",
223 + "request_hc_down_silu_warmup",
224 +]