main
py 494 lines 18 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Warmup kernels used during model execution.
4 This is useful specifically for JIT'ed kernels as we don't want JIT'ing to
5 happen during model execution.
6 """
7
8 import sys
9 import time
10 from typing import TYPE_CHECKING
11
12 import torch
13
14 import vllm.envs as envs
15 from vllm.logger import init_logger
16 from vllm.model_executor.warmup.b12x_warmup import b12x_warmup
17 from vllm.model_executor.warmup.cutedsl_warmup import cutedsl_warmup
18 from vllm.model_executor.warmup.deep_gemm_warmup import deep_gemm_warmup
19 from vllm.model_executor.warmup.flashinfer_autotune_cache import (
20 resolve_flashinfer_autotune_file,
21 write_flashinfer_autotune_cache,
22 )
23 from vllm.model_executor.warmup.flashinfer_sparse_mla_warmup import (
24 autotune_hisparse_flashinfer_attention,
25 deepseek_v4_sparse_mla_attention_warmup,
26 flashinfer_sparse_mla_decode_autotune_warmup,
27 )
28 from vllm.model_executor.warmup.kimi_k3_triton_warmup import (
29 kimi_k3_triton_warmup,
30 )
31 from vllm.model_executor.warmup.mamba_triton_warmup import mamba_triton_warmup
32 from vllm.model_executor.warmup.qwen4_exp_qsa_warmup import (
33 qwen4_exp_qsa_triton_warmup,
34 )
35 from vllm.model_executor.warmup.qwen_triton_warmup import qwen_triton_warmup
36 from vllm.model_executor.warmup.qwen_vl_triton_warmup import qwen_vl_triton_warmup
37 from vllm.model_executor.warmup.replayssm_warmup import (
38 replayssm_autotune_warmup,
39 )
40 from vllm.model_executor.warmup.watermark_sample_warmup import (
41 watermark_sample_warmup,
42 )
43 from vllm.platforms import current_platform
44 from vllm.utils.deep_gemm import is_deep_gemm_supported
45 from vllm.utils.flashinfer import has_flashinfer
46
47 if TYPE_CHECKING:
48 from vllm.v1.worker.gpu_model_runner import GPUModelRunner
49 from vllm.v1.worker.gpu_worker import Worker
50
51 logger = init_logger(__name__)
52
53 _LL_BF16_WARMUP_M_RANGE = range(1, 17)
54
55
56 def _ll_bf16_router_shapes_from_model(
57 model: torch.nn.Module,
58 ) -> tuple[tuple[int, int], ...]:
59 from vllm.model_executor.layers.fused_moe.router.gate_linear import GateLinear
60
61 shapes: set[tuple[int, int]] = set()
62 for module in model.modules():
63 if not isinstance(module, GateLinear):
64 continue
65 weight = getattr(module, "weight", None)
66 if not isinstance(weight, torch.Tensor):
67 continue
68 if weight.dim() != 2 or weight.dtype != torch.bfloat16:
69 continue
70 n, k = weight.shape
71 if k % 8 == 0:
72 shapes.add((int(k), int(n)))
73 return tuple(sorted(shapes))
74
75
76 def _warmup_ll_bf16_router_gemm(model: torch.nn.Module) -> None:
77 from vllm.model_executor.kernels.linear.cute_dsl.ll_bf16 import (
78 is_available as is_ll_bf16_gemm_available,
79 )
80 from vllm.model_executor.kernels.linear.cute_dsl.ll_bf16 import (
81 ll_bf16_gemm_kernel,
82 )
83
84 if not is_ll_bf16_gemm_available():
85 return
86
87 shapes = _ll_bf16_router_shapes_from_model(model)
88 if not shapes:
89 logger.debug_once(
90 "Skipping ll_bf16 router GEMM warmup: no bf16 GateLinear shapes found."
91 )
92 return
93
94 logger.info_once("Warming up ll_bf16 router GEMM kernels for shapes: %s.", shapes)
95 ll_bf16_gemm_kernel.warmup(
96 shapes=shapes,
97 m_values=_LL_BF16_WARMUP_M_RANGE,
98 )
99
100
101 def _warmup_bf16x3_router_gemm(
102 model: torch.nn.Module,
103 max_num_tokens: int,
104 ) -> None:
105 from vllm.model_executor.layers.fused_moe.router.bf16x3_router_gemm_cutedsl import ( # noqa: E501
106 warmup_bf16x3_router_gemm,
107 )
108 from vllm.model_executor.layers.fused_moe.router.gate_linear import GateLinear
109
110 gate = next(
111 (
112 module
113 for module in model.modules()
114 if isinstance(module, GateLinear) and module.allow_bf16x3_router_gemm
115 ),
116 None,
117 )
118 if gate is None:
119 logger.debug_once(
120 "Skipping BF16x3 router GEMM warmup: no eligible GateLinear found."
121 )
122 return
123
124 min_num_tokens = gate.FP32_MAX_TOKENS + 1 if gate.allow_fp32_router_gemm else 1
125 logger.info_once(
126 "Warming up BF16x3 router GEMM for K=%d, M=%d.",
127 gate.input_size,
128 gate.output_size,
129 )
130 configs = warmup_bf16x3_router_gemm(
131 gate.input_size,
132 gate.output_size,
133 min_num_tokens,
134 max_num_tokens,
135 )
136 logger.info_once("Warmed up BF16x3 router GEMM configs: %s.", configs)
137
138
139 def _warmup_gemm_rs_ar() -> None:
140 # Model construction (Kimi-K3, DeepSeek-V4.1) imports this module only
141 # when GEMM-RS/AR is enabled and initializes its singleton before
142 # kernel_warmup runs. Avoid importing it here so other models do not
143 # compile the RS/AR variants.
144 module = sys.modules.get("vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar")
145 if module is None:
146 return
147 compiled = module.warmup_gemm_rs_ar()
148 if compiled:
149 logger.info_once("Warmed up %d GEMM-RS/AR variants.", compiled)
150
151
152 def _autotune_kimi_k3_kda_qkvg(model: torch.nn.Module) -> None:
153 module = sys.modules.get("vllm.models.kimi_k3.nvidia.low_latency_gemm")
154 if module is not None:
155 module.autotune_kda_qkvg(model)
156
157
158 def kernel_warmup(worker: "Worker", *, process_local_only: bool = False):
159 from vllm.model_executor.warmup.minimax_m3_msa_warmup import (
160 minimax_m3_msa_warmup,
161 )
162
163 if not worker.use_v2_model_runner:
164 # The KV-block zeroing kernel is driven by the scheduler's
165 # `new_block_ids_to_zero`, so no dummy run ever reaches it.
166 zeroer = getattr(worker.model_runner, "_kv_block_zeroer", None)
167 if zeroer is not None:
168 zeroer.warmup(worker.model_runner.kv_cache_config.num_blocks)
169
170 enable_jit_warmup = worker.vllm_config.kernel_config.enable_jit_warmup
171 if enable_jit_warmup:
172 logger.info("JIT kernel warmup starting.")
173 jit_warmup_start = time.perf_counter()
174 try:
175 registry = (
176 worker.model_runner.jit_warmup_registry # type: ignore[attr-defined]
177 )
178 registry.warmup()
179 except Exception:
180 logger.exception(
181 "JIT kernel warmup failed after %.2fs.",
182 time.perf_counter() - jit_warmup_start,
183 )
184 raise
185 logger.info(
186 "JIT kernel warmup finished in %.2fs.",
187 time.perf_counter() - jit_warmup_start,
188 )
189
190 qwen_triton_warmup(worker.model_runner, worker.vllm_config.model_config)
191 qwen_vl_triton_warmup(worker.model_runner)
192 mamba_triton_warmup(worker.model_runner)
193
194 compilation_config = worker.vllm_config.compilation_config
195 cudagraph_capture_sizes = list(compilation_config.cudagraph_capture_sizes or [])
196
197 # Run next so input-prep kernels JIT against pristine runner state.
198 if enable_jit_warmup:
199 kimi_k3_triton_warmup(worker)
200 watermark_sample_warmup(worker)
201 qwen4_exp_qsa_triton_warmup(worker)
202
203 if enable_jit_warmup and current_platform.is_device_capability_family(100):
204 _warmup_bf16x3_router_gemm(
205 worker.get_model(),
206 worker.scheduler_config.max_num_batched_tokens,
207 )
208
209 if current_platform.has_device_capability(90):
210 _warmup_ll_bf16_router_gemm(worker.get_model())
211
212 _warmup_gemm_rs_ar()
213
214 if worker.vllm_config.kernel_config.enable_cutedsl_warmup:
215 # TODO(roberto): Remove after registered CuTeDSL warmups are migrated
216 # to the shared JIT warmup infrastructure.
217 # https://github.com/vllm-project/vllm/pull/47451
218 cutedsl_warmup()
219
220 if process_local_only:
221 return
222
223 flashinfer_sparse_mla_decode_autotune_warmup(worker)
224 deepseek_v4_sparse_mla_attention_warmup(worker)
225
226 # Deep GEMM warmup
227 do_deep_gemm_warmup = (
228 is_deep_gemm_supported() and envs.VLLM_DEEP_GEMM_WARMUP != "skip"
229 )
230 if do_deep_gemm_warmup:
231 model = worker.get_model()
232 max_tokens = worker.scheduler_config.max_num_batched_tokens
233 deep_gemm_warmup(model, max_tokens)
234
235 b12x_warmup(worker, cudagraph_capture_sizes)
236
237 minimax_m3_msa_warmup(worker)
238
239 # Allocate the exact decode-sized workspace, autotune cache misses, and
240 # resolve every CUDA Graph bucket before capture begins.
241 # Lazy import: flashinfer_pcie_ipc_all_reduce imports flashinfer.comm,
242 # which initializes CUDA at import time and must not run at module scope.
243 if envs.VLLM_ALLREDUCE_USE_FLASHINFER_PCIE_IPC:
244 from vllm.distributed.device_communicators import (
245 flashinfer_pcie_ipc_all_reduce,
246 )
247
248 flashinfer_pcie_ipc_all_reduce.warmup_flashinfer_pcie_ipc_allreduce(worker)
249
250 enable_flashinfer_autotune = (
251 worker.vllm_config.kernel_config.enable_flashinfer_autotune
252 )
253 # FlashInfer autotune for Hopper (SM 9.0) and Blackwell (SM 10.0) GPUs
254 if enable_flashinfer_autotune is False:
255 logger.info_once("Skipping FlashInfer autotune because it is disabled.")
256 elif has_flashinfer() and current_platform.has_device_capability(90):
257 flashinfer_autotune(worker.model_runner)
258
259 # FlashInfer attention warmup
260 # Only warmup if the model has FlashInfer attention groups
261 # and is not a pooling model
262 def _is_flashinfer_backend(backend):
263 try:
264 return backend.get_name() == "FLASHINFER"
265 except NotImplementedError:
266 return False
267
268 if (
269 not worker.model_runner.is_pooling_model
270 and worker.model_runner.attn_groups
271 # NOTE: This should be `any` instead of `all` but other hybrid attention
272 # backends don't support this dummy run. Once we remove
273 # `build_for_cudagraph_capture`, we can change it to `any`.
274 and all(
275 _is_flashinfer_backend(group.backend)
276 for groups in worker.model_runner.attn_groups
277 for group in groups
278 )
279 ):
280 logger.info_once("Warming up FlashInfer attention.")
281 # Warmup with mixed batch containing both prefill and decode tokens
282 # This is to warm up both prefill and decode attention kernels
283 worker.model_runner._dummy_run(
284 num_tokens=16,
285 skip_eplb=True,
286 is_profile=True,
287 force_attention=True,
288 create_mixed_batch=True,
289 )
290
291
292 def _flashinfer_autotune_skip_ops(runner: "GPUModelRunner") -> set[str] | None:
293 if envs.VLLM_FLASHINFER_AUTOTUNE_SKIP_OPS is not None:
294 return set(envs.VLLM_FLASHINFER_AUTOTUNE_SKIP_OPS) or None
295
296 from vllm.model_executor.kernels.linear import (
297 FlashInferCuteDslNvFp4LinearKernel,
298 )
299
300 for module in runner.get_model().modules():
301 for holder_name in ("quant_method", "scheme"):
302 kernel = getattr(getattr(module, holder_name, None), "kernel", None)
303 # CuTe-DSL mm_fp4 tuning JIT-compiles every tactic and its
304 # fallback is already the heuristic; all mm_fp4 backends share
305 # the "fp4_gemm" op name, so skip only when cute-dsl is selected.
306 if isinstance(kernel, FlashInferCuteDslNvFp4LinearKernel):
307 return {"fp4_gemm"}
308 return None
309
310
311 _FLASHINFER_BF16_AUTOTUNE_MAX_TOKENS = 32
312
313
314 def _flashinfer_deferred_moe_token_counts(
315 runner: "GPUModelRunner",
316 ) -> tuple[int, ...]:
317 """Return bounded token counts that exercise deferred MoE finalization."""
318 from vllm.model_executor.layers.fused_moe import MoERunner
319
320 max_tokens = runner.scheduler_config.max_num_batched_tokens
321 token_counts: list[int] = []
322 for module in runner.get_model().modules():
323 if not isinstance(module, MoERunner):
324 continue
325
326 moe_config = module.moe_config
327 max_deferred_tokens = moe_config.defer_moe_finalize_max_num_tokens
328 if moe_config.use_deferred_moe_finalize and max_deferred_tokens > 0:
329 token_counts.append(min(max_tokens, max_deferred_tokens))
330
331 return tuple(dict.fromkeys(token_counts))
332
333
334 def _flashinfer_autotune_token_counts(
335 runner: "GPUModelRunner", *, include_bf16: bool = True
336 ) -> tuple[int, ...]:
337 max_tokens = runner.scheduler_config.max_num_batched_tokens
338 # Tune the widest bucket set first so bounded passes reuse its configs.
339 token_counts = [max_tokens]
340 linear_backend = runner.vllm_config.kernel_config.linear_backend
341 if (
342 include_bf16
343 and linear_backend == "flashinfer_cutedsl"
344 and max_tokens > _FLASHINFER_BF16_AUTOTUNE_MAX_TOKENS
345 ):
346 token_counts.append(_FLASHINFER_BF16_AUTOTUNE_MAX_TOKENS)
347 token_counts.extend(_flashinfer_deferred_moe_token_counts(runner))
348 return tuple(dict.fromkeys(token_counts))
349
350
351 def _run_flashinfer_autotune_dummy_runs(
352 runner: "GPUModelRunner", *, skip_attn: bool = False
353 ) -> None:
354 import vllm.utils.flashinfer as fi_utils
355
356 dummy_run_kwargs = {"skip_attn": True} if skip_attn else {}
357 for num_tokens in _flashinfer_autotune_token_counts(runner, include_bf16=False):
358 tuning_buckets = fi_utils.flashinfer_get_hybrid_num_tokens_buckets(num_tokens)
359 logger.info(
360 "Running FlashInfer autotune with %d tokens and token buckets %s.",
361 num_tokens,
362 tuning_buckets,
363 )
364 with fi_utils.autotune(tuning_buckets=tuning_buckets):
365 runner._dummy_run(
366 num_tokens=num_tokens,
367 skip_eplb=True,
368 is_profile=True,
369 randomize_inputs=True,
370 **dummy_run_kwargs,
371 )
372
373
374 def _run_flashinfer_bf16_autotune_dummy_run(
375 runner: "GPUModelRunner",
376 *,
377 skip_ops: set[str] | None = None,
378 skip_attn: bool = False,
379 ) -> None:
380 import vllm.utils.flashinfer as fi_utils
381
382 if (
383 runner.vllm_config.kernel_config.linear_backend != "flashinfer_cutedsl"
384 or runner.scheduler_config.max_num_batched_tokens
385 <= _FLASHINFER_BF16_AUTOTUNE_MAX_TOKENS
386 ):
387 return
388
389 num_tokens = _FLASHINFER_BF16_AUTOTUNE_MAX_TOKENS
390 tuning_buckets = fi_utils.flashinfer_get_hybrid_num_tokens_buckets(num_tokens)
391 logger.info("Running FlashInfer BF16-only autotune with %d tokens.", num_tokens)
392 # Other ops execute from their existing cache or fallback, without the
393 # BF16 bucket cap. No full-model autotune context may enclose this pass.
394 with fi_utils.autotune_bf16_only(tuning_buckets, skip_ops=skip_ops):
395 runner._dummy_run(
396 num_tokens=num_tokens,
397 skip_eplb=True,
398 is_profile=True,
399 randomize_inputs=True,
400 **({"skip_attn": True} if skip_attn else {}),
401 )
402
403
404 def flashinfer_autotune(runner: "GPUModelRunner") -> None:
405 """Autotune FlashInfer operations.
406 FlashInfer have many implementations for the same operation,
407 autotuning runs benchmarks for each implementation and stores
408 the results. The results are cached transparently and
409 future calls to FlashInfer will use the best implementation.
410 Without autotuning, FlashInfer will rely on heuristics, which may
411 be significantly slower.
412
413 With PP > 1, stages run different layers and may profile different ops,
414 so each stage's TP group tunes separately with its own cache file;
415 otherwise the world group tunes together. Per-tactic timings are
416 averaged over the tuning group so all its ranks select the same tactic.
417 """
418 from flashinfer.autotuner import AutoTuner, set_autotune_process_group
419
420 import vllm.utils.flashinfer as fi_utils
421 from vllm.distributed.parallel_state import (
422 get_pp_group,
423 get_tp_group,
424 get_world_group,
425 )
426
427 world = get_world_group()
428 pp_size = get_pp_group().world_size
429 tune_group = get_tp_group() if pp_size > 1 else world
430 is_leader = tune_group.rank_in_group == 0
431 tuner = AutoTuner.get()
432
433 autotune_kwargs: dict = {}
434 skip_ops = _flashinfer_autotune_skip_ops(runner)
435 if skip_ops:
436 logger.info_once(
437 "Skipping FlashInfer autotuning for ops %s",
438 tuple(sorted(skip_ops)),
439 )
440 autotune_kwargs["skip_ops"] = skip_ops
441
442 cache_path = resolve_flashinfer_autotune_file(runner)
443 if pp_size > 1:
444 ranks = "-".join(str(rank) for rank in tune_group.ranks)
445 cache_path = cache_path.with_name(
446 f"{cache_path.stem}_tp_{ranks}{cache_path.suffix}"
447 )
448 if is_leader:
449 logger.info_once("Using FlashInfer autotune cache file: %s", cache_path)
450
451 # We skip EPLB here since we don't want to record dummy metrics.
452 # Randomize inputs to avoid every token pick the same experts,
453 # which lead to some EP ranks receiving no tokens and skipping their
454 # MoE kernel entirely, and cause hang due to all-reduce collective
455 # during synchronized autotuning.
456 # Read cached autotune results and broadcast within the tuning group.
457 cached_results: bytes | None = None
458 if is_leader and cache_path.exists():
459 with open(cache_path, "rb") as f:
460 cached_results = f.read()
461 cached_results = tune_group.broadcast_object(cached_results, src=0)
462 if cached_results is not None:
463 write_flashinfer_autotune_cache(cache_path, cached_results)
464 tune_group.barrier()
465 tuner.load_configs(str(cache_path))
466
467 group = tune_group.cpu_group if tune_group.world_size > 1 else None
468 set_autotune_process_group(group)
469 try:
470 with (
471 torch.inference_mode(),
472 fi_utils.autotune(tune_mode=True, **autotune_kwargs),
473 ):
474 hisparse_enabled = (
475 runner.vllm_config.attention_config.hisparse_config is not None
476 )
477 if hisparse_enabled:
478 # HiSparse hot-buffer attention is bounded by decode batch
479 # size, not the prefill-sized batch used for the full model.
480 autotune_hisparse_flashinfer_attention(runner)
481 _run_flashinfer_autotune_dummy_runs(runner, skip_attn=hisparse_enabled)
482 replayssm_autotune_warmup(runner)
483 _autotune_kimi_k3_kda_qkvg(runner.get_model())
484 with torch.inference_mode():
485 _run_flashinfer_bf16_autotune_dummy_run(
486 runner, skip_ops=skip_ops, skip_attn=hisparse_enabled
487 )
488 finally:
489 set_autotune_process_group(None)
490
491 if world.world_size > 1:
492 world.barrier()
493 if is_leader:
494 tuner.save_configs(str(cache_path))