Signed-off-by: liusy58 <mg21330037@smail.nju.edu.cn> Signed-off-by: Isotr0py <Isotr0py@outlook.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Isotr0py <Isotr0py@outlook.com>
510 lines
19 KiB
Python
510 lines
19 KiB
Python
# 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 json
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
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,
|
|
)
|
|
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_gemm_rs_ar() -> None:
|
|
# Model construction (Kimi-K3, DeepSeek-V4.1) 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.model_executor.kernels.linear.cute_dsl.gemm_rs_ar")
|
|
if module is None:
|
|
return
|
|
compiled = module.warmup_gemm_rs_ar()
|
|
if compiled:
|
|
logger.info_once("Warmed up %d 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_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
|
|
|
|
if current_platform.is_rocm():
|
|
from vllm.model_executor.warmup.rocm_segmented_attn_autotune_warmup import (
|
|
rocm_segmented_attn_autotune_warmup,
|
|
)
|
|
|
|
rocm_segmented_attn_autotune_warmup(worker)
|
|
|
|
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
|
|
|
|
|
|
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") -> 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]
|
|
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):
|
|
# The drafter may use more tokens than this pass in the same dummy run.
|
|
# Include its max M when building the autotune buckets.
|
|
max_tuning_tokens = num_tokens
|
|
speculator = getattr(runner, "speculator", None)
|
|
if speculator is not None:
|
|
num_query_per_req = speculator.num_query_per_req
|
|
num_draft_reqs = min(
|
|
num_tokens,
|
|
runner.max_num_reqs,
|
|
runner.max_num_tokens // num_query_per_req,
|
|
)
|
|
max_tuning_tokens = max(
|
|
max_tuning_tokens, num_draft_reqs * num_query_per_req
|
|
)
|
|
|
|
tuning_buckets = fi_utils.flashinfer_get_hybrid_num_tokens_buckets(
|
|
max_tuning_tokens
|
|
)
|
|
logger.info(
|
|
"Running FlashInfer autotune with %d tokens and token buckets %s.",
|
|
num_tokens,
|
|
tuning_buckets,
|
|
)
|
|
# Round M up to match serving-time bucket selection for non-bucket M.
|
|
# See https://github.com/flashinfer-ai/flashinfer/issues/5450
|
|
with fi_utils.autotune(tuning_buckets=tuning_buckets, round_up=True):
|
|
runner._dummy_run(
|
|
num_tokens=num_tokens,
|
|
skip_eplb=True,
|
|
is_profile=True,
|
|
randomize_inputs=True,
|
|
**dummy_run_kwargs,
|
|
)
|
|
|
|
|
|
def _autotune_cache_fingerprint(path: Path) -> tuple[str, int] | None:
|
|
"""Identify a saved autotune file by its FlashInfer metadata and size."""
|
|
try:
|
|
configs = json.loads(path.read_text())
|
|
metadata = configs.pop("_metadata", None)
|
|
except (OSError, ValueError, AttributeError):
|
|
return None
|
|
return json.dumps(metadata, sort_keys=True), len(configs)
|
|
|
|
|
|
def _all_ranks_have_matching_cache(path: Path, group) -> bool:
|
|
"""True iff every rank in ``group`` has its own, mutually consistent file."""
|
|
fingerprint = _autotune_cache_fingerprint(path)
|
|
if group.world_size == 1:
|
|
return fingerprint is not None
|
|
gathered: list[tuple[str, int] | None] = [None] * group.world_size
|
|
torch.distributed.all_gather_object(gathered, fingerprint, group=group.cpu_group)
|
|
return fingerprint is not None and all(f == fingerprint for f in gathered)
|
|
|
|
|
|
def flashinfer_autotune(runner: "GPUModelRunner", *, skip_attn: bool = False) -> 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.
|
|
|
|
Args:
|
|
runner: The model runner to tune.
|
|
skip_attn: Additionally skip attention in the tuning dummy runs.
|
|
|
|
With PP > 1, stages run different layers and may profile different ops,
|
|
so each stage's TP group tunes separately with its own cache file;
|
|
otherwise the world group tunes together. Per-tactic timings are
|
|
averaged over the tuning group so all its ranks select the same tactic.
|
|
|
|
Results are persisted per rank: FlashInfer keys MoE entries by tp/ep rank
|
|
(``MoERunner.get_cache_key_extras``), so one rank's file only hits on that
|
|
rank. A rank with a cache hit skips the per-tactic reduce the others block
|
|
in, so ranks keep loaded configs only if every rank in the tuning group
|
|
has a matching file and successfully loads it.
|
|
|
|
"""
|
|
from flashinfer.autotuner import AutoTuner, set_autotune_process_group
|
|
|
|
import vllm.utils.flashinfer as fi_utils
|
|
from vllm.distributed.parallel_state import (
|
|
get_pp_group,
|
|
get_tp_group,
|
|
get_world_group,
|
|
)
|
|
|
|
world = get_world_group()
|
|
pp_size = get_pp_group().world_size
|
|
tune_group = get_tp_group() if pp_size > 1 else world
|
|
is_leader = tune_group.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)
|
|
# The world group only spans DP ranks when vLLM folds DP into it.
|
|
dp_rank = runner.vllm_config.parallel_config.data_parallel_rank
|
|
cache_path = cache_path.with_name(
|
|
f"{cache_path.stem}_dp{dp_rank}_rank{world.rank_in_group}{cache_path.suffix}"
|
|
)
|
|
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.
|
|
if _all_ranks_have_matching_cache(cache_path, tune_group):
|
|
loaded = tuner.load_configs(str(cache_path))
|
|
if tune_group.world_size > 1:
|
|
loaded_by_rank: list[bool | None] = [None] * tune_group.world_size
|
|
torch.distributed.all_gather_object(
|
|
loaded_by_rank, loaded, group=tune_group.cpu_group
|
|
)
|
|
loaded = all(loaded_by_rank)
|
|
if not loaded:
|
|
tuner.clear_cache()
|
|
|
|
group = tune_group.cpu_group if tune_group.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
|
|
)
|
|
skip_attn = skip_attn or hisparse_enabled
|
|
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=skip_attn)
|
|
replayssm_autotune_warmup(runner)
|
|
_autotune_kimi_k3_kda_qkvg(runner.get_model())
|
|
finally:
|
|
set_autotune_process_group(None)
|
|
|
|
if world.world_size > 1:
|
|
world.barrier()
|
|
# Skip the rewrite when nothing was tuned this start (every entry came from
|
|
# the file). FlashInfer gates its own autotune(cache=...) save the same way.
|
|
if tuner._dirty:
|
|
tuner.save_configs(str(cache_path))
|