main
py 587 lines 20.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from dataclasses import dataclass
5 from typing import Any
6
7 import cutlass
8 import cutlass.cute as cute
9 import torch
10 from cuda.bindings.driver import CUstream
11 from cutlass import Float32, Int32, Uint32, Uint64
12 from quack.compile_utils import make_fake_tensor
13
14 from vllm.cute_utils import recast_val
15 from vllm.model_executor.warmup.jit_warmup import kernel_launcher
16 from vllm.model_executor.warmup.jit_warmup_cutedsl_helper import (
17 CuTeDSLLaunchSpec,
18 VllmCuTeDSLJitKernel,
19 )
20 from vllm.model_executor.warmup.jit_warmup_triton_helper import (
21 LaunchSpec,
22 TritonWarmupTensor,
23 VllmTritonJitKernel,
24 )
25 from vllm.triton_utils import tl, triton
26
27
28 def stable_topk_from_gathered_candidates_cutedsl(
29 gathered: torch.Tensor,
30 topk: int,
31 out: torch.Tensor | None = None,
32 ) -> torch.Tensor:
33 if out is None:
34 out = torch.empty(
35 (gathered.shape[0], topk),
36 dtype=torch.int32,
37 device=gathered.device,
38 )
39 _STABLE_TOPK_FROM_GATHERED_CANDIDATES_KERNEL(gathered, out, topk=topk)
40 return out
41
42
43 def pack_dcp_topk_candidates_cutedsl(
44 logits: torch.Tensor,
45 topk_indices: torch.Tensor,
46 packed: torch.Tensor,
47 dcp_rank: int,
48 dcp_world_size: int,
49 cp_interleave: int,
50 row_starts: torch.Tensor | None,
51 ) -> None:
52 topk = topk_indices.shape[1]
53 row_starts_arg = row_starts if row_starts is not None else topk_indices
54 _PACK_DCP_TOPK_CANDIDATES_KERNEL(
55 logits,
56 topk_indices,
57 packed,
58 row_starts_arg,
59 logits_stride0=logits.stride(0),
60 logits_stride1=logits.stride(1),
61 topk_stride0=topk_indices.stride(0),
62 topk_stride1=topk_indices.stride(1),
63 packed_stride0=packed.stride(0),
64 packed_stride1=packed.stride(1),
65 packed_stride2=packed.stride(2),
66 num_cols=logits.shape[1],
67 dcp_rank=dcp_rank,
68 dcp_world_size=dcp_world_size,
69 cp_interleave=cp_interleave,
70 has_row_starts=row_starts is not None,
71 topk=topk,
72 block_size=512,
73 )
74
75
76 class PackDCPTopkCandidatesKernel(
77 VllmTritonJitKernel["PackDCPTopkCandidatesKernel.CompileKey"]
78 ):
79 @dataclass(frozen=True)
80 class CompileKey:
81 dcp_rank: int
82 dcp_world_size: int
83 cp_interleave: int
84 has_row_starts: bool
85 topk: int
86 block_size: int
87
88 # These scalars only describe runtime layouts and bounds; the constexpr
89 # fields below own the launch geometry and algorithmic specialization.
90 @staticmethod
91 @triton.jit(
92 do_not_specialize=[
93 "logits_stride0",
94 "logits_stride1",
95 "topk_stride0",
96 "topk_stride1",
97 "packed_stride0",
98 "packed_stride1",
99 "packed_stride2",
100 "num_cols",
101 ]
102 )
103 def kernel(
104 logits,
105 topk_indices,
106 packed,
107 row_starts,
108 logits_stride0,
109 logits_stride1,
110 topk_stride0,
111 topk_stride1,
112 packed_stride0,
113 packed_stride1,
114 packed_stride2,
115 num_cols,
116 DCP_RANK: tl.constexpr,
117 DCP_WORLD_SIZE: tl.constexpr,
118 CP_INTERLEAVE: tl.constexpr,
119 HAS_ROW_STARTS: tl.constexpr,
120 TOPK: tl.constexpr,
121 BLOCK_SIZE: tl.constexpr,
122 ):
123 row = tl.program_id(0)
124 tile = tl.program_id(1)
125 cols = tile * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
126 mask = cols < TOPK
127
128 local_idx = tl.load(
129 topk_indices + row * topk_stride0 + cols * topk_stride1,
130 mask=mask,
131 other=-1,
132 )
133 valid = local_idx >= 0
134 safe_local_idx = tl.maximum(local_idx, 0)
135
136 row_start = 0
137 if HAS_ROW_STARTS:
138 row_start = tl.load(row_starts + row)
139
140 score_col = safe_local_idx + row_start
141 score_col = tl.minimum(score_col, tl.maximum(num_cols - 1, 0))
142 score = tl.load(
143 logits + row * logits_stride0 + score_col * logits_stride1,
144 mask=mask & valid,
145 other=-float("inf"),
146 )
147
148 global_id = (
149 (safe_local_idx // CP_INTERLEAVE) * (DCP_WORLD_SIZE * CP_INTERLEAVE)
150 + DCP_RANK * CP_INTERLEAVE
151 + safe_local_idx % CP_INTERLEAVE
152 )
153 global_id = tl.where(valid, global_id, -1)
154
155 packed_base = packed + row * packed_stride0 + cols * packed_stride1
156 tl.store(packed_base, score, mask=mask)
157 tl.store(packed_base + packed_stride2, global_id.to(tl.float32), mask=mask)
158
159 def dispatch( # type: ignore[override]
160 self,
161 *,
162 has_row_starts: bool,
163 **compile_key_fields: int,
164 ) -> CompileKey:
165 return self.CompileKey(
166 **compile_key_fields,
167 has_row_starts=has_row_starts,
168 )
169
170 def get_warmup_keys(self, vllm_config: Any) -> list[CompileKey]:
171 dcp_world_size = vllm_config.parallel_config.decode_context_parallel_size
172 if dcp_world_size <= 1:
173 return []
174 cp_interleave = vllm_config.parallel_config.cp_kv_cache_interleave_size
175 topk = vllm_config.model_config.hf_text_config.index_topk
176 if topk <= 0:
177 return []
178
179 try:
180 from vllm.distributed.parallel_state import get_dcp_group
181
182 dcp_rank = get_dcp_group().rank_in_group
183 except Exception:
184 dcp_rank = 0
185
186 return self._trace_dispatch(self.dispatch)(
187 dcp_rank=dcp_rank,
188 dcp_world_size=dcp_world_size,
189 cp_interleave=cp_interleave,
190 has_row_starts=(False, True),
191 topk=topk,
192 block_size=512,
193 )
194
195 def warmup_inputs(self, compile_key: CompileKey) -> dict[str, Any]:
196 fp32_ptr = TritonWarmupTensor(torch.float32)
197 int32_ptr = TritonWarmupTensor(torch.int32)
198 return dict(
199 logits=fp32_ptr,
200 topk_indices=TritonWarmupTensor(torch.int32, shape=(1, compile_key.topk)),
201 packed=fp32_ptr,
202 row_starts_arg=int32_ptr,
203 logits_stride0=1,
204 logits_stride1=1,
205 topk_stride0=1,
206 topk_stride1=1,
207 packed_stride0=1,
208 packed_stride1=1,
209 packed_stride2=1,
210 num_cols=1,
211 dcp_rank=compile_key.dcp_rank,
212 dcp_world_size=compile_key.dcp_world_size,
213 cp_interleave=compile_key.cp_interleave,
214 has_row_starts=compile_key.has_row_starts,
215 topk=compile_key.topk,
216 block_size=compile_key.block_size,
217 )
218
219 @kernel_launcher
220 def __call__(
221 self,
222 logits: torch.Tensor,
223 topk_indices: torch.Tensor,
224 packed: torch.Tensor,
225 row_starts_arg: torch.Tensor,
226 *,
227 logits_stride0: int,
228 logits_stride1: int,
229 topk_stride0: int,
230 topk_stride1: int,
231 packed_stride0: int,
232 packed_stride1: int,
233 packed_stride2: int,
234 num_cols: int,
235 dcp_rank: int,
236 dcp_world_size: int,
237 cp_interleave: int,
238 has_row_starts: bool,
239 topk: int,
240 block_size: int,
241 ) -> LaunchSpec:
242 grid = (topk_indices.shape[0], triton.cdiv(topk, block_size))
243 return grid, dict(
244 row_starts=row_starts_arg,
245 DCP_RANK=dcp_rank,
246 DCP_WORLD_SIZE=dcp_world_size,
247 CP_INTERLEAVE=cp_interleave,
248 HAS_ROW_STARTS=has_row_starts,
249 TOPK=topk,
250 BLOCK_SIZE=block_size,
251 num_warps=8,
252 )
253
254
255 class StableTopKFromGatheredCandidatesKernel(
256 VllmCuTeDSLJitKernel["StableTopKFromGatheredCandidatesKernel.CompileKey"]
257 ):
258 tb_size = 512
259 hist_bins = 2048
260 radix_bits = (hist_bins - 1).bit_length()
261 key_bits = Uint64.width
262 radix_passes = (key_bits + radix_bits - 1) // radix_bits
263 final_radix_bits = key_bits - radix_bits * (radix_passes - 1)
264 hist_chunks = (hist_bins + tb_size - 1) // tb_size
265 warps_per_block = tb_size // cute.arch.WARP_SIZE
266
267 @dataclass(frozen=True)
268 class CompileKey:
269 topk: int
270 num_candidates: int
271
272 @staticmethod
273 def kernel(compile_key: CompileKey) -> Any:
274 tb_size = StableTopKFromGatheredCandidatesKernel.tb_size
275 hist_bins = StableTopKFromGatheredCandidatesKernel.hist_bins
276 radix_bits = StableTopKFromGatheredCandidatesKernel.radix_bits
277 key_bits = StableTopKFromGatheredCandidatesKernel.key_bits
278 radix_passes = StableTopKFromGatheredCandidatesKernel.radix_passes
279 final_radix_bits = StableTopKFromGatheredCandidatesKernel.final_radix_bits
280 hist_chunks = StableTopKFromGatheredCandidatesKernel.hist_chunks
281 warps_per_block = StableTopKFromGatheredCandidatesKernel.warps_per_block
282 topk = compile_key.topk
283 assert hist_bins == 1 << radix_bits
284 assert compile_key.num_candidates % tb_size == 0, (
285 "StableTopKFromGatheredCandidatesKernel requires candidate count "
286 f"to be a multiple of {tb_size}, got {compile_key.num_candidates}"
287 )
288 keys_per_thread = compile_key.num_candidates // tb_size
289
290 @cute.struct
291 class SharedStorage:
292 hist: cute.struct.MemRange[Int32, hist_bins]
293 committed_count: cute.struct.MemRange[Int32, 1]
294 running_count: cute.struct.MemRange[Int32, 1]
295 threshold_bin: cute.struct.MemRange[Int32, 1]
296 threshold_found: cute.struct.MemRange[Int32, 1]
297 include_threshold_bin: cute.struct.MemRange[Int32, 1]
298 prefix_s: cute.struct.Align[cute.struct.MemRange[Uint64, 1], 8]
299 warp_totals: cute.struct.MemRange[Int32, warps_per_block]
300
301 shared_storage = SharedStorage
302
303 @cute.jit
304 def warp_scan_inclusive_i32(val: Int32, lane: Int32) -> Int32:
305 for i in cutlass.range_constexpr(cute.arch.WARP_SIZE.bit_length() - 1):
306 offset = 1 << i
307 partial = cute.arch.shuffle_sync_up(
308 val, offset=offset, mask_and_clamp=0
309 )
310 if lane >= offset:
311 val += partial
312 return val
313
314 @cute.jit
315 def block_scan_inclusive_i32(
316 val: Int32,
317 lane: Int32,
318 warp_id: Int32,
319 warp_scratch: cute.Tensor,
320 warps_per_block: int,
321 ) -> Int32:
322 prefix = warp_scan_inclusive_i32(val, lane)
323 if lane == Int32(cute.arch.WARP_SIZE - 1):
324 warp_scratch[0, warp_id] = prefix
325 cute.arch.sync_threads()
326
327 if warp_id == Int32(0):
328 warp_total = Int32(0)
329 if lane < Int32(warps_per_block):
330 warp_total = warp_scratch[0, lane]
331 warp_prefix = warp_scan_inclusive_i32(warp_total, lane)
332 if lane < Int32(warps_per_block):
333 warp_scratch[0, lane] = warp_prefix - warp_total
334 cute.arch.sync_threads()
335
336 return prefix + warp_scratch[0, warp_id]
337
338 @cute.jit
339 def stable_key(score: Float32, token_id: Int32) -> Uint64:
340 bits = recast_val(score, Uint32)
341 mask = Uint32(0x80000000)
342 if (bits & Uint32(0x80000000)) != Uint32(0):
343 mask = Uint32(0xFFFFFFFF)
344 score_key = Uint64(bits ^ mask) << Uint64(32)
345 id_key = Uint64(~Uint32(token_id))
346 key = score_key | id_key
347 if token_id < Int32(0):
348 key = Uint64(0)
349 return key
350
351 @cute.jit
352 def prefix_matches(
353 key: Uint64,
354 prefix: Uint64,
355 prefix_bits: Int32,
356 ):
357 matches = prefix_bits == Int32(0)
358 if prefix_bits != Int32(0):
359 shift = Int32(key_bits) - prefix_bits
360 matches = (key >> Uint64(shift)) == (prefix >> Uint64(shift))
361 return matches
362
363 @cute.jit
364 def radix_pass(
365 keys: cute.Tensor,
366 output: cute.Tensor,
367 storage,
368 tid: Int32,
369 step: Int32,
370 bits: int,
371 is_final_pass: bool,
372 ):
373 hist_smem = storage.hist.get_tensor(cute.make_layout((hist_bins,)))
374 committed_count_smem = storage.committed_count.data_ptr()
375 running_count_smem = storage.running_count.data_ptr()
376 threshold_bin_smem = storage.threshold_bin.data_ptr()
377 threshold_found_smem = storage.threshold_found.data_ptr()
378 include_threshold_bin_smem = storage.include_threshold_bin.data_ptr()
379 prefix_smem = storage.prefix_s.data_ptr()
380 warp_totals_smem = storage.warp_totals.get_tensor(
381 cute.make_layout((1, warps_per_block))
382 )
383
384 prefix_bits = step * Int32(radix_bits)
385 num_bins = 1 << bits
386 block_scan_iterations = (num_bins + tb_size - 1) // tb_size
387 shift = Int32(key_bits) - prefix_bits - Int32(bits)
388 bin_mask = Uint64(num_bins - 1)
389 prefix = prefix_smem.load()
390
391 for chunk in cutlass.range_constexpr(hist_chunks):
392 hist_smem[tid + Int32(chunk * tb_size)] = Int32(0)
393 if tid == Int32(0):
394 running_count_smem.store(committed_count_smem.load())
395 include_threshold_bin_smem.store(Int32(0))
396 threshold_found_smem.store(Int32(0))
397 cute.arch.sync_threads()
398
399 for key_idx in cutlass.range_constexpr(keys_per_thread):
400 key = keys[key_idx]
401 if prefix_matches(key, prefix, prefix_bits):
402 bin_idx = Int32((key >> Uint64(shift)) & bin_mask)
403 cute.arch.atomic_add(
404 hist_smem.iterator + bin_idx,
405 Int32(1),
406 sem="relaxed",
407 scope="cta",
408 )
409 cute.arch.sync_threads()
410
411 lane = cute.arch.lane_idx()
412 warp_id = cute.arch.warp_idx()
413 # Each iteration scans one tb_size-wide slice of bins, high to low.
414 iter = Int32(0)
415 threshold_found = threshold_found_smem.load()
416 while threshold_found == Int32(0) and iter < Int32(block_scan_iterations):
417 bin_idx = Int32(num_bins - 1) - (iter * Int32(tb_size) + tid)
418 count = hist_smem[bin_idx]
419 chunk_inclusive = block_scan_inclusive_i32(
420 count,
421 lane,
422 warp_id,
423 warp_totals_smem,
424 warps_per_block,
425 )
426 running_count = running_count_smem.load()
427 prior_in_scan_slice = chunk_inclusive - count
428 remaining = Int32(topk) - running_count - prior_in_scan_slice
429 if count > Int32(0) and remaining > Int32(0) and remaining <= count:
430 threshold_bin_smem.store(bin_idx)
431 if count <= remaining or cutlass.const_expr(is_final_pass):
432 include_threshold_bin_smem.store(Int32(1))
433 threshold_found_smem.store(Int32(1))
434 # Barrier: every thread must finish reading running_count for this
435 # slice before tb_size-1 advances it, else a warp racing ahead to
436 # the store makes a lagging thread double-count the slice total
437 # (-> remaining too small -> threshold too high -> under-fill).
438 cute.arch.sync_threads()
439 if tid == Int32(tb_size - 1):
440 running_count_smem.store(running_count + chunk_inclusive)
441 cute.arch.sync_threads()
442
443 threshold_found = threshold_found_smem.load()
444 iter += Int32(1)
445
446 threshold = threshold_bin_smem.load()
447 should_include_threshold = include_threshold_bin_smem.load() != Int32(0)
448 for key_idx in cutlass.range_constexpr(keys_per_thread):
449 key = keys[key_idx]
450 if prefix_matches(key, prefix, prefix_bits):
451 bin_idx = Int32((key >> Uint64(shift)) & bin_mask)
452 selected = bin_idx > threshold
453 if should_include_threshold:
454 selected = selected or bin_idx == threshold
455 if selected:
456 dst = cute.arch.atomic_add(
457 committed_count_smem,
458 Int32(1),
459 sem="relaxed",
460 scope="cta",
461 )
462 if dst < Int32(topk):
463 output[dst] = recast_val(~Uint32(key), Int32)
464 cute.arch.sync_threads()
465
466 pass_finished = include_threshold_bin_smem.load()
467 if tid == Int32(0) and pass_finished == Int32(0):
468 prefix_smem.store(prefix | (Uint64(threshold) << Uint64(shift)))
469 cute.arch.sync_threads()
470 return pass_finished
471
472 @cute.kernel
473 def device_kernel(input: cute.Tensor, out: cute.Tensor):
474 row, _, _ = cute.arch.block_idx()
475 tid, _, _ = cute.arch.thread_idx()
476 input_row = input[row, None, None]
477 output_row = out[row, None]
478 keys = cute.make_rmem_tensor((keys_per_thread,), Uint64)
479
480 smem = cutlass.utils.SmemAllocator()
481 storage = smem.allocate(shared_storage, 8)
482 committed_count_smem = storage.committed_count.data_ptr()
483 prefix_smem = storage.prefix_s.data_ptr()
484 for i in range(tid, topk, tb_size):
485 output_row[i] = Int32(-1)
486
487 for key_idx in cutlass.range_constexpr(keys_per_thread):
488 col = tid + Int32(key_idx * tb_size)
489 score = Float32(input_row[col, 0])
490 token_id = Int32(input_row[col, 1])
491 keys[key_idx] = stable_key(score, token_id)
492
493 if tid == Int32(0):
494 committed_count_smem.store(Int32(0))
495 prefix_smem.store(Uint64(0))
496 cute.arch.sync_threads()
497
498 step = Int32(0)
499 finished = Int32(0)
500 while finished == Int32(0) and step < Int32(radix_passes - 1):
501 finished = radix_pass(
502 keys,
503 output_row,
504 storage,
505 tid,
506 step,
507 radix_bits,
508 False,
509 )
510 step += Int32(1)
511
512 if finished == Int32(0):
513 radix_pass(
514 keys,
515 output_row,
516 storage,
517 tid,
518 Int32(radix_passes - 1),
519 final_radix_bits,
520 True,
521 )
522
523 @cute.jit
524 def host_entrypoint(
525 gathered: cute.Tensor,
526 out: cute.Tensor,
527 stream: CUstream,
528 ):
529 grid = (gathered.shape[0], 1, 1)
530 device_kernel(gathered, out).launch(
531 grid=grid,
532 block=(tb_size, 1, 1),
533 stream=stream,
534 )
535
536 return host_entrypoint
537
538 def dispatch( # type: ignore[override]
539 self,
540 **compile_key_fields: int,
541 ) -> CompileKey:
542 return self.CompileKey(**compile_key_fields)
543
544 def get_warmup_keys(self, vllm_config: Any) -> list[CompileKey]:
545 dcp_world_size = vllm_config.parallel_config.decode_context_parallel_size
546 if dcp_world_size <= 1:
547 return []
548 topk = vllm_config.model_config.hf_text_config.index_topk
549 if topk <= 0:
550 return []
551 return self._trace_dispatch(self.dispatch)(
552 topk=topk,
553 num_candidates=topk * dcp_world_size,
554 )
555
556 def warmup_inputs(self, compile_key: CompileKey) -> tuple[Any, ...]:
557 num_rows = cute.sym_int()
558 gathered = cute.runtime.make_fake_tensor(
559 Float32,
560 (num_rows, compile_key.num_candidates, 2),
561 stride=(cute.sym_int64(divisibility=2), 2, 1),
562 assumed_align=8,
563 )
564 out = make_fake_tensor(
565 Int32,
566 (num_rows, compile_key.topk),
567 divisibility=1,
568 )
569 return gathered, out
570
571 @kernel_launcher
572 def __call__(
573 self,
574 gathered: torch.Tensor,
575 out: torch.Tensor,
576 *,
577 topk: int,
578 ) -> CuTeDSLLaunchSpec[CompileKey]:
579 compile_key = self.dispatch(
580 topk=topk,
581 num_candidates=gathered.shape[1],
582 )
583 return compile_key, (gathered, out)
584
585
586 _PACK_DCP_TOPK_CANDIDATES_KERNEL = PackDCPTopkCandidatesKernel()
587 _STABLE_TOPK_FROM_GATHERED_CANDIDATES_KERNEL = StableTopKFromGatheredCandidatesKernel()