main
py 504 lines 13.9 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """NVIDIA HyperConnection kernels for Qwen4Exp."""
4
5 import torch
6
7 from vllm.platforms import current_platform
8 from vllm.triton_utils import tl, triton
9 from vllm.utils.torch_utils import direct_register_custom_op
10
11
12 @triton.jit
13 def _grouped_gemma_rmsnorm_kernel(
14 x_ptr,
15 w_ptr,
16 y_ptr,
17 stride_x,
18 stride_y,
19 DIM: tl.constexpr,
20 NUM_GROUPS: tl.constexpr,
21 W_SHARED: tl.constexpr,
22 EPS: tl.constexpr,
23 launch_pdl: tl.constexpr,
24 ) -> None:
25 GROUP_DIM: tl.constexpr = DIM // NUM_GROUPS
26 BLOCK_SIZE: tl.constexpr = triton.next_power_of_2(GROUP_DIM)
27
28 pid = tl.program_id(0)
29 group_id = pid % NUM_GROUPS
30 row = pid // NUM_GROUPS
31
32 offs_g = tl.arange(0, BLOCK_SIZE)
33 offsets = group_id * GROUP_DIM + offs_g
34 mask = offs_g < GROUP_DIM
35 # A [GROUP_DIM] affine is shared; a [DIM] affine follows the grouped
36 # checkpoint layout.
37 w_offs = offs_g if W_SHARED else offsets
38
39 if launch_pdl:
40 tl.extra.cuda.gdc_wait()
41
42 x = tl.load(x_ptr + row * stride_x + offsets, mask, other=0.0).to(tl.float32)
43 w = tl.load(w_ptr + w_offs, mask, other=0.0)
44
45 rrms = tl.rsqrt(tl.sum(x * x) / GROUP_DIM + EPS)
46 # Gemma's (1 + w) affine is written this way to lower to an FMA.
47 y = x * rrms
48 y += y * w.to(tl.float32)
49
50 if launch_pdl:
51 tl.extra.cuda.gdc_launch_dependents()
52 tl.store(y_ptr + row * stride_y + offsets, y, mask)
53
54
55 def _grouped_gemma_rmsnorm(
56 x: torch.Tensor, weight: torch.Tensor, eps: float, num_groups: int
57 ) -> torch.Tensor:
58 N, DIM = x.shape
59 assert x.stride(1) == 1, "grouped Gemma RMSNorm requires unit inner stride"
60 assert weight.is_contiguous(), "grouped Gemma RMSNorm weight must be contiguous"
61 assert DIM % num_groups == 0
62 group_dim = DIM // num_groups
63 assert weight.numel() in (group_dim, DIM)
64
65 y = x.new_empty(x.shape)
66 _grouped_gemma_rmsnorm_kernel[(N * num_groups,)](
67 x,
68 weight,
69 y,
70 x.stride(0),
71 y.stride(0),
72 DIM,
73 num_groups,
74 W_SHARED=weight.numel() == group_dim,
75 EPS=eps,
76 launch_pdl=current_platform.is_arch_support_pdl(),
77 )
78 return y
79
80
81 @triton.jit
82 def _hc_silu_kernel(
83 x_ptr,
84 y_ptr,
85 stride_x,
86 stride_y,
87 DIM: tl.constexpr,
88 HC: tl.constexpr,
89 launch_pdl: tl.constexpr,
90 ) -> None:
91 BLOCK_SIZE: tl.constexpr = triton.next_power_of_2(DIM)
92
93 row = tl.program_id(0)
94 offs = tl.arange(0, BLOCK_SIZE)
95 mask = offs < DIM
96
97 if launch_pdl:
98 tl.extra.cuda.gdc_wait()
99
100 x = tl.load(x_ptr + row * stride_x + offs, mask).to(tl.float32) / HC
101 y = x * tl.sigmoid(x)
102
103 if launch_pdl:
104 tl.extra.cuda.gdc_launch_dependents()
105 tl.store(y_ptr + row * stride_y + offs, y, mask)
106
107
108 def _hc_silu(x: torch.Tensor, hc_count: int) -> torch.Tensor:
109 num_tokens, DIM = x.shape
110 assert x.stride(1) == 1
111
112 output = x.new_empty(x.shape)
113 _hc_silu_kernel[(num_tokens,)](
114 x,
115 output,
116 x.stride(0),
117 output.stride(0),
118 DIM=DIM,
119 HC=hc_count,
120 launch_pdl=current_platform.is_arch_support_pdl(),
121 )
122 return output
123
124
125 @triton.jit
126 def _hc_gate_mix_kernel(
127 x_ptr,
128 g_ptr,
129 y_ptr,
130 stride_x,
131 stride_g,
132 stride_y,
133 DIM: tl.constexpr,
134 HC: tl.constexpr,
135 BLOCK_SIZE: tl.constexpr,
136 launch_pdl: tl.constexpr,
137 ) -> None:
138 HC_DIM: tl.constexpr = DIM // HC
139
140 row = tl.program_id(0)
141 tile_id = tl.program_id(1)
142 offs_inner = tile_id * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
143 mask = offs_inner < HC_DIM
144
145 if launch_pdl:
146 tl.extra.cuda.gdc_wait()
147
148 # The constexpr loop is unrolled and keeps one stream live at a time.
149 # Materializing [HC, BLOCK_SIZE] more than doubles latency at large M.
150 acc = tl.zeros([BLOCK_SIZE], dtype=tl.float32)
151 for stream in tl.static_range(HC):
152 offsets = stream * HC_DIM + offs_inner
153 g = tl.load(g_ptr + row * stride_g + offsets, mask, other=0.0)
154 x = tl.load(x_ptr + row * stride_x + offsets, mask, other=0.0)
155 acc += tl.sigmoid(g.to(tl.float32)) * x.to(tl.float32)
156 acc /= HC
157
158 if launch_pdl:
159 tl.extra.cuda.gdc_launch_dependents()
160 tl.store(y_ptr + row * stride_y + offs_inner, acc, mask)
161
162
163 def _hc_gate_mix(x: torch.Tensor, gate: torch.Tensor, hc_count: int) -> torch.Tensor:
164 N, DIM = gate.shape
165 assert x.shape == gate.shape
166 assert DIM % hc_count == 0
167 assert x.stride(1) == 1
168 assert gate.stride(1) == 1
169
170 HC_DIM = DIM // hc_count
171 out = x.new_empty(N, HC_DIM)
172 BLOCK_SIZE = 512
173 _hc_gate_mix_kernel[(N, triton.cdiv(HC_DIM, BLOCK_SIZE))](
174 x,
175 gate,
176 out,
177 x.stride(0),
178 gate.stride(0),
179 out.stride(0),
180 DIM,
181 hc_count,
182 BLOCK_SIZE,
183 launch_pdl=current_platform.is_arch_support_pdl(),
184 )
185 return out
186
187
188 @triton.jit
189 def _hc_combine_kernel(
190 block_ptr,
191 res_ptr,
192 inj_ptr,
193 out_ptr,
194 stride_block,
195 stride_res,
196 stride_inj,
197 stride_out,
198 HC_DIM: tl.constexpr,
199 HC: tl.constexpr,
200 BLOCK_SIZE: tl.constexpr,
201 launch_pdl: tl.constexpr,
202 ) -> None:
203 HC_PAD: tl.constexpr = triton.next_power_of_2(HC)
204
205 row = tl.program_id(0)
206 tile_id = tl.program_id(1)
207
208 offs_inner = tile_id * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
209 mask_inner = offs_inner < HC_DIM
210 offs_hc = tl.arange(0, HC_PAD)
211 mask_hc = offs_hc < HC
212 offs = offs_hc[:, None] * HC_DIM + offs_inner[None, :]
213 mask = mask_hc[:, None] & mask_inner[None, :]
214
215 if launch_pdl:
216 tl.extra.cuda.gdc_wait()
217
218 if inj_ptr is not None:
219 inj = tl.load(inj_ptr + row * stride_inj + offs_hc, mask_hc, other=0.0)
220 block = tl.load(block_ptr + row * stride_block + offs_inner, mask_inner, other=0.0)
221 res = tl.load(res_ptr + row * stride_res + offs, mask, other=0.0)
222
223 # Keeping HC as a broadcast dimension is faster here than four separate
224 # residual load/store sequences.
225 if inj_ptr is not None:
226 inj = 2.0 * tl.sigmoid(inj.to(tl.float32) / HC)
227 block = block.to(tl.float32)[None, :] * inj[:, None]
228 out = res.to(tl.float32) + block.to(tl.float32)
229
230 if launch_pdl:
231 tl.extra.cuda.gdc_launch_dependents()
232 tl.store(out_ptr + row * stride_out + offs, out, mask=mask)
233
234
235 def _hc_combine(
236 residual: torch.Tensor,
237 block_output: torch.Tensor,
238 injection_logits: torch.Tensor | None,
239 hc_count: int,
240 ) -> torch.Tensor:
241 N, DIM = residual.shape
242 assert DIM % hc_count == 0
243 hc_dim = DIM // hc_count
244 assert block_output.shape == (N, hc_dim)
245 assert residual.stride(1) == 1
246 assert block_output.stride(1) == 1
247 if injection_logits is not None:
248 assert injection_logits.shape == (N, hc_count)
249 assert injection_logits.stride(1) == 1
250
251 stride_injection = injection_logits.stride(0) if injection_logits is not None else 0
252
253 out = residual.new_empty(residual.shape)
254 BLOCK_SIZE = 512
255 _hc_combine_kernel[(N, triton.cdiv(hc_dim, BLOCK_SIZE))](
256 block_output,
257 residual,
258 injection_logits,
259 out,
260 block_output.stride(0),
261 residual.stride(0),
262 stride_injection,
263 out.stride(0),
264 hc_dim,
265 hc_count,
266 BLOCK_SIZE,
267 launch_pdl=current_platform.is_arch_support_pdl(),
268 )
269 return out
270
271
272 @triton.jit
273 def _hc_combine_norm_kernel(
274 block_ptr,
275 res_ptr,
276 inj_ptr,
277 w_ptr,
278 out_ptr,
279 y_ptr,
280 stride_block,
281 stride_res,
282 stride_inj,
283 stride_out,
284 stride_y,
285 HC_DIM: tl.constexpr,
286 HC: tl.constexpr,
287 W_SHARED: tl.constexpr,
288 EPS: tl.constexpr,
289 BLOCK_SIZE: tl.constexpr,
290 launch_pdl: tl.constexpr,
291 ) -> None:
292 HC_PAD: tl.constexpr = triton.next_power_of_2(HC)
293 NUM_TILES: tl.constexpr = triton.cdiv(HC_DIM, BLOCK_SIZE)
294 NUM_TILES_PAD: tl.constexpr = triton.next_power_of_2(NUM_TILES)
295
296 pid = tl.program_id(0)
297 row = pid // HC
298 stream = pid % HC
299 offs_hc = tl.arange(0, HC_PAD)
300 mask_hc = offs_hc < HC
301 tile_ids = tl.arange(0, NUM_TILES_PAD)
302 offs_inner = tile_ids[:, None] * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)[None, :]
303 mask_inner = offs_inner < HC_DIM
304 offs = stream * HC_DIM + offs_inner
305 # Shared norm weights repeat across streams; per-branch weights use the
306 # same flattened HC layout as the residual.
307 w_offs = offs_inner if W_SHARED else offs
308
309 if launch_pdl:
310 tl.extra.cuda.gdc_wait()
311
312 # Start the uncached residual load first, then issue the other combine
313 # loads before consuming any of them.
314 res = tl.load(res_ptr + row * stride_res + offs, mask_inner, other=0.0)
315 if inj_ptr is not None:
316 inj = tl.load(inj_ptr + row * stride_inj + offs_hc, mask_hc, other=0.0)
317 block = tl.load(
318 block_ptr + row * stride_block + offs_inner,
319 mask_inner,
320 other=0.0,
321 )
322 if inj_ptr is not None:
323 inj = 2.0 * tl.sigmoid(inj.to(tl.float32) / HC)
324 block = block.to(tl.float32) * tl.sum(tl.where(offs_hc == stream, inj, 0.0))
325 # Round the materialized combine result before normalization. This matches
326 # the unfused combine -> RMSNorm boundary.
327 out = (res.to(tl.float32) + block.to(tl.float32)).to(out_ptr.dtype.element_ty)
328 if inj_ptr is None:
329 w = tl.load(w_ptr + w_offs, mask_inner, other=0.0)
330 tl.store(out_ptr + row * stride_out + offs, out, mask=mask_inner)
331
332 out = out.to(tl.float32)
333 sum_sq = tl.sum(tl.sum(out * out, axis=1), axis=0)
334 rrms = tl.rsqrt(sum_sq / HC_DIM + EPS)
335
336 if launch_pdl:
337 tl.extra.cuda.gdc_launch_dependents()
338
339 if inj_ptr is not None:
340 # Loading the weight earlier helps decode but keeps the tile live across
341 # the reduction and regresses larger batches, so defer it to the norm.
342 w = tl.load(w_ptr + w_offs, mask_inner, other=0.0)
343 y = out * rrms
344 y += y * w.to(tl.float32)
345 tl.store(y_ptr + row * stride_y + offs, y, mask_inner)
346
347
348 def _hc_combine_norm(
349 residual: torch.Tensor,
350 block_output: torch.Tensor,
351 injection_logits: torch.Tensor | None,
352 norm_weight: torch.Tensor,
353 eps: float,
354 hc_count: int,
355 ) -> tuple[torch.Tensor, torch.Tensor]:
356 N, DIM = residual.shape
357 assert DIM % hc_count == 0
358 hc_dim = DIM // hc_count
359 assert block_output.shape == (N, hc_dim)
360 assert residual.stride(1) == 1
361 assert block_output.stride(1) == 1
362 if injection_logits is not None:
363 assert injection_logits.shape == (N, hc_count)
364 assert injection_logits.stride(1) == 1
365 assert norm_weight.is_contiguous()
366 assert norm_weight.numel() in (hc_dim, DIM)
367
368 stride_injection = injection_logits.stride(0) if injection_logits is not None else 0
369
370 out = residual.new_empty(residual.shape)
371 y = residual.new_empty(residual.shape)
372 BLOCK_SIZE = 512
373 _hc_combine_norm_kernel[(N * hc_count,)](
374 block_output,
375 residual,
376 injection_logits,
377 norm_weight,
378 out,
379 y,
380 block_output.stride(0),
381 residual.stride(0),
382 stride_injection,
383 out.stride(0),
384 y.stride(0),
385 hc_dim,
386 hc_count,
387 W_SHARED=norm_weight.numel() == hc_dim,
388 EPS=eps,
389 BLOCK_SIZE=BLOCK_SIZE,
390 launch_pdl=current_platform.is_arch_support_pdl(),
391 )
392 return out, y
393
394
395 def _same_shape_fake(x: torch.Tensor, *args) -> torch.Tensor:
396 return x.new_empty(x.shape)
397
398
399 def _hc_gate_mix_fake(
400 x: torch.Tensor, gate: torch.Tensor, hc_count: int
401 ) -> torch.Tensor:
402 del gate
403 return x.new_empty((x.shape[0], x.shape[1] // hc_count))
404
405
406 def _hc_combine_fake(
407 residual: torch.Tensor,
408 block_output: torch.Tensor,
409 injection_logits: torch.Tensor | None,
410 hc_count: int,
411 ) -> torch.Tensor:
412 del block_output, injection_logits, hc_count
413 return residual.new_empty(residual.shape)
414
415
416 def _hc_combine_norm_fake(
417 residual: torch.Tensor,
418 block_output: torch.Tensor,
419 injection_logits: torch.Tensor | None,
420 norm_weight: torch.Tensor,
421 eps: float,
422 hc_count: int,
423 ) -> tuple[torch.Tensor, torch.Tensor]:
424 del block_output, injection_logits, norm_weight, eps, hc_count
425 return residual.new_empty(residual.shape), residual.new_empty(residual.shape)
426
427
428 direct_register_custom_op(
429 op_name="qwen4_exp_grouped_gemma_rmsnorm",
430 op_func=_grouped_gemma_rmsnorm,
431 fake_impl=_same_shape_fake,
432 )
433 direct_register_custom_op(
434 op_name="qwen4_exp_hc_silu",
435 op_func=_hc_silu,
436 fake_impl=_same_shape_fake,
437 )
438 direct_register_custom_op(
439 op_name="qwen4_exp_hc_gate_mix",
440 op_func=_hc_gate_mix,
441 fake_impl=_hc_gate_mix_fake,
442 )
443 direct_register_custom_op(
444 op_name="qwen4_exp_hc_combine",
445 op_func=_hc_combine,
446 fake_impl=_hc_combine_fake,
447 )
448 direct_register_custom_op(
449 op_name="qwen4_exp_hc_combine_norm",
450 op_func=_hc_combine_norm,
451 fake_impl=_hc_combine_norm_fake,
452 )
453
454
455 def grouped_gemma_rmsnorm(
456 x: torch.Tensor, weight: torch.Tensor, eps: float, num_groups: int
457 ) -> torch.Tensor:
458 return torch.ops.vllm.qwen4_exp_grouped_gemma_rmsnorm(x, weight, eps, num_groups)
459
460
461 def hc_silu(x: torch.Tensor, hc_count: int) -> torch.Tensor:
462 return torch.ops.vllm.qwen4_exp_hc_silu(x, hc_count)
463
464
465 def hc_gate_mix(x: torch.Tensor, gate: torch.Tensor, hc_count: int) -> torch.Tensor:
466 return torch.ops.vllm.qwen4_exp_hc_gate_mix(x, gate, hc_count)
467
468
469 def hc_combine(
470 residual: torch.Tensor,
471 block_output: torch.Tensor,
472 injection_logits: torch.Tensor | None,
473 hc_count: int,
474 ) -> torch.Tensor:
475 return torch.ops.vllm.qwen4_exp_hc_combine(
476 residual, block_output, injection_logits, hc_count
477 )
478
479
480 def hc_combine_norm(
481 residual: torch.Tensor,
482 block_output: torch.Tensor,
483 injection_logits: torch.Tensor | None,
484 norm_weight: torch.Tensor,
485 eps: float,
486 hc_count: int,
487 ) -> tuple[torch.Tensor, torch.Tensor]:
488 return torch.ops.vllm.qwen4_exp_hc_combine_norm(
489 residual,
490 block_output,
491 injection_logits,
492 norm_weight,
493 eps,
494 hc_count,
495 )
496
497
498 __all__ = [
499 "grouped_gemma_rmsnorm",
500 "hc_combine",
501 "hc_combine_norm",
502 "hc_gate_mix",
503 "hc_silu",
504 ]