Signed-off-by: jiahanc <173873397+jiahanc@users.noreply.github.com> Co-authored-by: OpenAI Codex <codex@openai.com>
482 lines
18 KiB
Python
482 lines
18 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 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))
|