# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project """Warmup kernels used during model execution. This is useful specifically for JIT'ed kernels as we don't want JIT'ing to happen during model execution. """ import sys import time from typing import TYPE_CHECKING import torch import vllm.envs as envs from vllm.logger import init_logger from vllm.model_executor.warmup.b12x_warmup import b12x_warmup from vllm.model_executor.warmup.cutedsl_warmup import cutedsl_warmup from vllm.model_executor.warmup.deep_gemm_warmup import deep_gemm_warmup from vllm.model_executor.warmup.flashinfer_autotune_cache import ( resolve_flashinfer_autotune_file, write_flashinfer_autotune_cache, ) from vllm.model_executor.warmup.flashinfer_sparse_mla_warmup import ( autotune_hisparse_flashinfer_attention, deepseek_v4_sparse_mla_attention_warmup, flashinfer_sparse_mla_decode_autotune_warmup, ) from vllm.model_executor.warmup.kimi_k3_triton_warmup import ( kimi_k3_triton_warmup, ) from vllm.model_executor.warmup.mamba_triton_warmup import mamba_triton_warmup from vllm.model_executor.warmup.qwen4_exp_qsa_warmup import ( qwen4_exp_qsa_triton_warmup, ) from vllm.model_executor.warmup.qwen_triton_warmup import qwen_triton_warmup from vllm.model_executor.warmup.qwen_vl_triton_warmup import qwen_vl_triton_warmup from vllm.model_executor.warmup.replayssm_warmup import ( replayssm_autotune_warmup, ) from vllm.model_executor.warmup.watermark_sample_warmup import ( watermark_sample_warmup, ) from vllm.platforms import current_platform from vllm.utils.deep_gemm import is_deep_gemm_supported from vllm.utils.flashinfer import has_flashinfer if TYPE_CHECKING: from vllm.v1.worker.gpu_model_runner import GPUModelRunner from vllm.v1.worker.gpu_worker import Worker logger = init_logger(__name__) _LL_BF16_WARMUP_M_RANGE = range(1, 17) def _ll_bf16_router_shapes_from_model( model: torch.nn.Module, ) -> tuple[tuple[int, int], ...]: from vllm.model_executor.layers.fused_moe.router.gate_linear import GateLinear shapes: set[tuple[int, int]] = set() for module in model.modules(): if not isinstance(module, GateLinear): continue weight = getattr(module, "weight", None) if not isinstance(weight, torch.Tensor): continue if weight.dim() != 2 or weight.dtype != torch.bfloat16: continue n, k = weight.shape if k % 8 == 0: shapes.add((int(k), int(n))) return tuple(sorted(shapes)) def _warmup_ll_bf16_router_gemm(model: torch.nn.Module) -> None: from vllm.model_executor.kernels.linear.cute_dsl.ll_bf16 import ( is_available as is_ll_bf16_gemm_available, ) from vllm.model_executor.kernels.linear.cute_dsl.ll_bf16 import ( ll_bf16_gemm_kernel, ) if not is_ll_bf16_gemm_available(): return shapes = _ll_bf16_router_shapes_from_model(model) if not shapes: logger.debug_once( "Skipping ll_bf16 router GEMM warmup: no bf16 GateLinear shapes found." ) return logger.info_once("Warming up ll_bf16 router GEMM kernels for shapes: %s.", shapes) ll_bf16_gemm_kernel.warmup( shapes=shapes, m_values=_LL_BF16_WARMUP_M_RANGE, ) def _warmup_bf16x3_router_gemm( model: torch.nn.Module, max_num_tokens: int, ) -> None: from vllm.model_executor.layers.fused_moe.router.bf16x3_router_gemm_cutedsl import ( # noqa: E501 warmup_bf16x3_router_gemm, ) from vllm.model_executor.layers.fused_moe.router.gate_linear import GateLinear gate = next( ( module for module in model.modules() if isinstance(module, GateLinear) and module.allow_bf16x3_router_gemm ), None, ) if gate is None: logger.debug_once( "Skipping BF16x3 router GEMM warmup: no eligible GateLinear found." ) return min_num_tokens = gate.FP32_MAX_TOKENS + 1 if gate.allow_fp32_router_gemm else 1 logger.info_once( "Warming up BF16x3 router GEMM for K=%d, M=%d.", gate.input_size, gate.output_size, ) configs = warmup_bf16x3_router_gemm( gate.input_size, gate.output_size, min_num_tokens, max_num_tokens, ) logger.info_once("Warmed up BF16x3 router GEMM configs: %s.", configs) def _warmup_kimi_k3_gemm_rs_ar() -> None: # Kimi-K3 model construction imports this module only when GEMM-RS/AR is # enabled and initializes its singleton before kernel_warmup runs. Avoid # importing it here so other models do not compile the RS/AR variants. module = sys.modules.get("vllm.models.kimi_k3.nvidia.ops.cute_dsl.gemm_rs_ar") if module is None: return compiled = module.warmup_gemm_rs_ar() if compiled: logger.info_once("Warmed up %d Kimi-K3 GEMM-RS/AR variants.", compiled) def _autotune_kimi_k3_kda_qkvg(model: torch.nn.Module) -> None: module = sys.modules.get("vllm.models.kimi_k3.nvidia.low_latency_gemm") if module is not None: module.autotune_kda_qkvg(model) def kernel_warmup(worker: "Worker", *, process_local_only: bool = False): from vllm.model_executor.warmup.minimax_m3_msa_warmup import ( minimax_m3_msa_warmup, ) if not worker.use_v2_model_runner: # The KV-block zeroing kernel is driven by the scheduler's # `new_block_ids_to_zero`, so no dummy run ever reaches it. zeroer = getattr(worker.model_runner, "_kv_block_zeroer", None) if zeroer is not None: zeroer.warmup(worker.model_runner.kv_cache_config.num_blocks) enable_jit_warmup = worker.vllm_config.kernel_config.enable_jit_warmup if enable_jit_warmup: logger.info("JIT kernel warmup starting.") jit_warmup_start = time.perf_counter() try: registry = ( worker.model_runner.jit_warmup_registry # type: ignore[attr-defined] ) registry.warmup() except Exception: logger.exception( "JIT kernel warmup failed after %.2fs.", time.perf_counter() - jit_warmup_start, ) raise logger.info( "JIT kernel warmup finished in %.2fs.", time.perf_counter() - jit_warmup_start, ) qwen_triton_warmup(worker.model_runner, worker.vllm_config.model_config) qwen_vl_triton_warmup(worker.model_runner) mamba_triton_warmup(worker.model_runner) compilation_config = worker.vllm_config.compilation_config cudagraph_capture_sizes = list(compilation_config.cudagraph_capture_sizes or []) # Run next so input-prep kernels JIT against pristine runner state. if enable_jit_warmup: kimi_k3_triton_warmup(worker) watermark_sample_warmup(worker) qwen4_exp_qsa_triton_warmup(worker) if enable_jit_warmup and current_platform.is_device_capability_family(100): _warmup_bf16x3_router_gemm( worker.get_model(), worker.scheduler_config.max_num_batched_tokens, ) if current_platform.has_device_capability(90): _warmup_ll_bf16_router_gemm(worker.get_model()) _warmup_kimi_k3_gemm_rs_ar() if worker.vllm_config.kernel_config.enable_cutedsl_warmup: # TODO(roberto): Remove after registered CuTeDSL warmups are migrated # to the shared JIT warmup infrastructure. # https://github.com/vllm-project/vllm/pull/47451 cutedsl_warmup() if process_local_only: return flashinfer_sparse_mla_decode_autotune_warmup(worker) deepseek_v4_sparse_mla_attention_warmup(worker) # Deep GEMM warmup do_deep_gemm_warmup = ( is_deep_gemm_supported() and envs.VLLM_DEEP_GEMM_WARMUP != "skip" ) if do_deep_gemm_warmup: model = worker.get_model() max_tokens = worker.scheduler_config.max_num_batched_tokens deep_gemm_warmup(model, max_tokens) b12x_warmup(worker, cudagraph_capture_sizes) minimax_m3_msa_warmup(worker) # Allocate the exact decode-sized workspace, autotune cache misses, and # resolve every CUDA Graph bucket before capture begins. # Lazy import: flashinfer_pcie_ipc_all_reduce imports flashinfer.comm, # which initializes CUDA at import time and must not run at module scope. if envs.VLLM_ALLREDUCE_USE_FLASHINFER_PCIE_IPC: from vllm.distributed.device_communicators import ( flashinfer_pcie_ipc_all_reduce, ) flashinfer_pcie_ipc_all_reduce.warmup_flashinfer_pcie_ipc_allreduce(worker) enable_flashinfer_autotune = ( worker.vllm_config.kernel_config.enable_flashinfer_autotune ) # FlashInfer autotune for Hopper (SM 9.0) and Blackwell (SM 10.0) GPUs if enable_flashinfer_autotune is False: logger.info_once("Skipping FlashInfer autotune because it is disabled.") elif has_flashinfer() and current_platform.has_device_capability(90): flashinfer_autotune(worker.model_runner) # FlashInfer attention warmup # Only warmup if the model has FlashInfer attention groups # and is not a pooling model def _is_flashinfer_backend(backend): try: return backend.get_name() == "FLASHINFER" except NotImplementedError: return False if ( not worker.model_runner.is_pooling_model and worker.model_runner.attn_groups # NOTE: This should be `any` instead of `all` but other hybrid attention # backends don't support this dummy run. Once we remove # `build_for_cudagraph_capture`, we can change it to `any`. and all( _is_flashinfer_backend(group.backend) for groups in worker.model_runner.attn_groups for group in groups ) ): logger.info_once("Warming up FlashInfer attention.") # Warmup with mixed batch containing both prefill and decode tokens # This is to warm up both prefill and decode attention kernels worker.model_runner._dummy_run( num_tokens=16, skip_eplb=True, is_profile=True, force_attention=True, create_mixed_batch=True, ) def _flashinfer_autotune_skip_ops(runner: "GPUModelRunner") -> set[str] | None: if envs.VLLM_FLASHINFER_AUTOTUNE_SKIP_OPS is not None: return set(envs.VLLM_FLASHINFER_AUTOTUNE_SKIP_OPS) or None from vllm.model_executor.kernels.linear import ( FlashInferCuteDslNvFp4LinearKernel, ) for module in runner.get_model().modules(): for holder_name in ("quant_method", "scheme"): kernel = getattr(getattr(module, holder_name, None), "kernel", None) # CuTe-DSL mm_fp4 tuning JIT-compiles every tactic and its # fallback is already the heuristic; all mm_fp4 backends share # the "fp4_gemm" op name, so skip only when cute-dsl is selected. if isinstance(kernel, FlashInferCuteDslNvFp4LinearKernel): return {"fp4_gemm"} return None _FLASHINFER_BF16_AUTOTUNE_MAX_TOKENS = 32 def _flashinfer_deferred_moe_token_counts( runner: "GPUModelRunner", ) -> tuple[int, ...]: """Return bounded token counts that exercise deferred MoE finalization.""" from vllm.model_executor.layers.fused_moe import MoERunner max_tokens = runner.scheduler_config.max_num_batched_tokens token_counts: list[int] = [] for module in runner.get_model().modules(): if not isinstance(module, MoERunner): continue moe_config = module.moe_config max_deferred_tokens = moe_config.defer_moe_finalize_max_num_tokens if moe_config.use_deferred_moe_finalize and max_deferred_tokens > 0: token_counts.append(min(max_tokens, max_deferred_tokens)) return tuple(dict.fromkeys(token_counts)) def _flashinfer_autotune_token_counts( runner: "GPUModelRunner", *, include_bf16: bool = True ) -> tuple[int, ...]: max_tokens = runner.scheduler_config.max_num_batched_tokens # Tune the widest bucket set first so bounded passes reuse its configs. token_counts = [max_tokens] linear_backend = runner.vllm_config.kernel_config.linear_backend if ( include_bf16 and linear_backend == "flashinfer_cutedsl" and max_tokens > _FLASHINFER_BF16_AUTOTUNE_MAX_TOKENS ): token_counts.append(_FLASHINFER_BF16_AUTOTUNE_MAX_TOKENS) token_counts.extend(_flashinfer_deferred_moe_token_counts(runner)) return tuple(dict.fromkeys(token_counts)) def _run_flashinfer_autotune_dummy_runs( runner: "GPUModelRunner", *, skip_attn: bool = False ) -> None: import vllm.utils.flashinfer as fi_utils dummy_run_kwargs = {"skip_attn": True} if skip_attn else {} for num_tokens in _flashinfer_autotune_token_counts(runner, include_bf16=False): tuning_buckets = fi_utils.flashinfer_get_hybrid_num_tokens_buckets(num_tokens) logger.info( "Running FlashInfer autotune with %d tokens and token buckets %s.", num_tokens, tuning_buckets, ) with fi_utils.autotune(tuning_buckets=tuning_buckets): runner._dummy_run( num_tokens=num_tokens, skip_eplb=True, is_profile=True, randomize_inputs=True, **dummy_run_kwargs, ) def _run_flashinfer_bf16_autotune_dummy_run( runner: "GPUModelRunner", *, skip_ops: set[str] | None = None, skip_attn: bool = False, ) -> None: import vllm.utils.flashinfer as fi_utils if ( runner.vllm_config.kernel_config.linear_backend != "flashinfer_cutedsl" or runner.scheduler_config.max_num_batched_tokens <= _FLASHINFER_BF16_AUTOTUNE_MAX_TOKENS ): return num_tokens = _FLASHINFER_BF16_AUTOTUNE_MAX_TOKENS tuning_buckets = fi_utils.flashinfer_get_hybrid_num_tokens_buckets(num_tokens) logger.info("Running FlashInfer BF16-only autotune with %d tokens.", num_tokens) # Other ops execute from their existing cache or fallback, without the # BF16 bucket cap. No full-model autotune context may enclose this pass. with fi_utils.autotune_bf16_only(tuning_buckets, skip_ops=skip_ops): runner._dummy_run( num_tokens=num_tokens, skip_eplb=True, is_profile=True, randomize_inputs=True, **({"skip_attn": True} if skip_attn else {}), ) def flashinfer_autotune(runner: "GPUModelRunner") -> None: """Autotune FlashInfer operations. FlashInfer have many implementations for the same operation, autotuning runs benchmarks for each implementation and stores the results. The results are cached transparently and future calls to FlashInfer will use the best implementation. Without autotuning, FlashInfer will rely on heuristics, which may be significantly slower. Every rank profiles the same tactics. When distributed, per-tactic timings are averaged over the world CPU group so all ranks select the same tactic. """ from flashinfer.autotuner import AutoTuner, set_autotune_process_group import vllm.utils.flashinfer as fi_utils from vllm.distributed.parallel_state import get_world_group world = get_world_group() is_leader = world.rank_in_group == 0 tuner = AutoTuner.get() autotune_kwargs: dict = {} skip_ops = _flashinfer_autotune_skip_ops(runner) if skip_ops: logger.info_once( "Skipping FlashInfer autotuning for ops %s", tuple(sorted(skip_ops)), ) autotune_kwargs["skip_ops"] = skip_ops cache_path = resolve_flashinfer_autotune_file(runner) if is_leader: logger.info_once("Using FlashInfer autotune cache file: %s", cache_path) # We skip EPLB here since we don't want to record dummy metrics. # Randomize inputs to avoid every token pick the same experts, # which lead to some EP ranks receiving no tokens and skipping their # MoE kernel entirely, and cause hang due to all-reduce collective # during synchronized autotuning. # Read cached autotune results and broadcast to all ranks. cached_results: bytes | None = None if is_leader and cache_path.exists(): with open(cache_path, "rb") as f: cached_results = f.read() cached_results = world.broadcast_object(cached_results, src=0) if cached_results is not None: write_flashinfer_autotune_cache(cache_path, cached_results) world.barrier() tuner.load_configs(str(cache_path)) group = world.cpu_group if world.world_size > 1 else None set_autotune_process_group(group) try: with ( torch.inference_mode(), fi_utils.autotune(tune_mode=True, **autotune_kwargs), ): hisparse_enabled = ( runner.vllm_config.attention_config.hisparse_config is not None ) if hisparse_enabled: # HiSparse hot-buffer attention is bounded by decode batch # size, not the prefill-sized batch used for the full model. autotune_hisparse_flashinfer_attention(runner) _run_flashinfer_autotune_dummy_runs(runner, skip_attn=hisparse_enabled) replayssm_autotune_warmup(runner) _autotune_kimi_k3_kda_qkvg(runner.get_model()) with torch.inference_mode(): _run_flashinfer_bf16_autotune_dummy_run( runner, skip_ops=skip_ops, skip_attn=hisparse_enabled ) finally: set_autotune_process_group(None) if world.world_size > 1: world.barrier() if is_leader: tuner.save_configs(str(cache_path))