Repository navigation
[Perf] Warm up DeepGEMM MegaMoE kernels before serving #60468
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,11 +5,19 @@ | |
| be used during model execution beforehand. | ||
| """ | ||
|
|
||
| import sys | ||
| from typing import TYPE_CHECKING, TypeGuard | ||
|
|
||
| import torch | ||
| from tqdm import tqdm | ||
|
|
||
| import vllm.envs as envs | ||
| from vllm.distributed.parallel_state import get_dp_group, is_global_first_rank | ||
| from vllm.distributed.parallel_state import ( | ||
| get_dp_group, | ||
| get_ep_group, | ||
| is_global_first_rank, | ||
| ) | ||
| from vllm.forward_context import ForwardContext, override_forward_context | ||
| from vllm.model_executor.kernels.linear.scaled_mm.deep_gemm import ( | ||
| DeepGemmFp8BlockScaledMMKernel, | ||
| ) | ||
|
|
@@ -23,6 +31,7 @@ | |
| ) | ||
| from vllm.tracing import instrument | ||
| from vllm.utils.deep_gemm import ( | ||
| _import_deep_gemm, | ||
| fp8_gemm_nt, | ||
| get_mk_alignment_for_contiguous_layout, | ||
| m_grouped_fp8_gemm_nt_contiguous, | ||
|
|
@@ -32,6 +41,10 @@ | |
| from vllm.utils.platform_utils import num_compute_units | ||
| from vllm.v1.worker.workspace import current_workspace_manager | ||
|
|
||
| if TYPE_CHECKING: | ||
| from vllm.models.deepseek_v4.nvidia.model import DeepseekV4MoE | ||
| from vllm.models.kimi_k3.nvidia.model import KimiMoE | ||
|
|
||
|
|
||
| def _generate_optimal_warmup_m_values( | ||
| max_tokens: int, n: int, device: torch.device | ||
|
|
@@ -144,6 +157,22 @@ def _fused_moe_grouped_gemm_may_use_deep_gemm(module: torch.nn.Module) -> bool: | |
| return isinstance(fused_experts, (DeepGemmExperts, TritonOrDeepGemmExperts)) | ||
|
|
||
|
|
||
| # Model construction already imports the model modules. Avoid importing them here. | ||
| def _mega_moe_uses_mega_gate(module: torch.nn.Module) -> "TypeGuard[DeepseekV4MoE]": | ||
| # DeepSeek-V4 routes with bf16_mega_gate, Kimi K3 with its GateLinear. | ||
| dsv4 = sys.modules.get("vllm.models.deepseek_v4.nvidia.model") | ||
| return dsv4 is not None and isinstance(module, dsv4.DeepseekV4MoE) | ||
|
|
||
|
|
||
| def _mega_moe_may_use_deep_gemm( | ||
| module: torch.nn.Module, | ||
| ) -> "TypeGuard[DeepseekV4MoE | KimiMoE]": | ||
| if _mega_moe_uses_mega_gate(module): | ||
| return module.use_native_mega_moe | ||
| kimi = sys.modules.get("vllm.models.kimi_k3.nvidia.model") | ||
| return kimi is not None and isinstance(module, kimi.KimiMoE) and module.use_mega_moe | ||
|
|
||
|
|
||
| FP8_GEMM_NT_WARMUP_CACHE: set[torch.Size] = set() | ||
|
|
||
|
|
||
|
|
@@ -305,6 +334,110 @@ def _warmup(w: torch.Tensor, w_scale: torch.Tensor): | |
| GROUPED_FP8_GEMM_NT_CONTIGUOUS_WARMUP_CACHE.add(w.size()) | ||
|
|
||
|
|
||
| FP8_FP4_MEGA_MOE_WARMUP_CACHE: set[tuple] = set() | ||
|
|
||
|
|
||
| def _get_fp8_fp4_mega_moe_warmup_key(module: "DeepseekV4MoE | KimiMoE") -> tuple: | ||
| gate = module.gate | ||
| return ( | ||
| gate.output_size, | ||
| getattr(gate, "tid2eid", None) is not None, | ||
| gate.e_score_correction_bias is not None, | ||
| getattr(module, "image_sentinel_lo", 0), | ||
| module.experts.has_fused_shared_experts, | ||
| ) | ||
|
|
||
|
|
||
| def _get_fp8_fp4_mega_moe_m_values( | ||
| module: "DeepseekV4MoE | KimiMoE", max_tokens: int | ||
| ) -> list[int]: | ||
| """Get the largest M per distinct fp8_fp4_mega_moe config and, where used, | ||
| bf16_mega_gate config.""" | ||
| dg = _import_deep_gemm() | ||
| gate, experts = module.gate, module.experts | ||
| uses_mega_gate = _mega_moe_uses_mega_gate(module) | ||
| num_max_tokens_per_rank = experts.get_symm_buffer().num_max_tokens_per_rank | ||
| if module.use_sequence_parallel: | ||
| max_tokens = cdiv(max_tokens, module.tp_size) | ||
|
|
||
| m_values: dict[tuple, int] = {} | ||
| for num_tokens in range(1, max_tokens + 1): | ||
| gate_config = ( | ||
| dg.get_bf16_mega_gate_config( | ||
| num_tokens, gate.input_size, gate.output_size, experts.top_k | ||
| ) | ||
| if uses_mega_gate | ||
| else {} | ||
| ) | ||
| block_m = dg.get_block_m_for_mega_moe( | ||
| get_ep_group().world_size, | ||
| experts.num_experts, | ||
| num_max_tokens_per_rank, | ||
| num_tokens, | ||
| experts.top_k, | ||
| "fp8xfp4", | ||
| ) | ||
| # Keep the largest M: DeepSeek-V4 only calls bf16_mega_gate above a | ||
| # small token threshold, and a config's largest M is always above it. | ||
| m_values[(tuple(gate_config.items()), block_m)] = num_tokens | ||
| return sorted(m_values.values()) | ||
|
|
||
|
|
||
| def _deepgemm_fp8_fp4_mega_moe_warmup( | ||
| module: "DeepseekV4MoE | KimiMoE", | ||
| max_tokens: int, | ||
| pbar: tqdm | None = None, | ||
| ): | ||
| from vllm.models.deepseek_v4.nvidia.model import ( | ||
| prepare_mega_gate_routing_metadata, | ||
| ) | ||
|
|
||
| key = _get_fp8_fp4_mega_moe_warmup_key(module) | ||
| if key in FP8_FP4_MEGA_MOE_WARMUP_CACHE: | ||
| return | ||
|
|
||
| m_values = _get_fp8_fp4_mega_moe_m_values(module, max_tokens) | ||
| max_m = m_values[-1] # sorted | ||
|
|
||
| device = module.experts.get_symm_buffer().x.device | ||
| input_ids = torch.zeros(max_m, device=device, dtype=torch.int64) | ||
| hidden_states = torch.zeros( | ||
| (max_m, module.gate.input_size), device=device, dtype=torch.bfloat16 | ||
| ) | ||
| is_padding = torch.zeros(max_m + 1, device=device, dtype=torch.bool) | ||
| # With padding skip on, sequence parallel gives ranks > 0 a padding mask | ||
| # slice at any alignment. | ||
| sp_masked = module.use_sequence_parallel and envs.VLLM_MOE_SKIP_PADDING | ||
| offsets = (0, 1) if sp_masked else (0,) | ||
|
|
||
| # Run the whole layer so the gate sees the same optional inputs as serving. | ||
| for num_tokens in m_values: | ||
| inputs: tuple = (hidden_states[:num_tokens],) | ||
| if _mega_moe_uses_mega_gate(module): | ||
| ids = input_ids[:num_tokens] | ||
| inputs += ( | ||
| ids, | ||
| prepare_mega_gate_routing_metadata( | ||
| ids, | ||
| has_hash_routing=module.gate.tid2eid is not None, | ||
| image_sentinel_base_id=module.image_sentinel_lo or None, | ||
| ), | ||
| ) | ||
| for offset in offsets: | ||
| forward_context = ForwardContext( | ||
| no_compile_layers={}, | ||
| attn_metadata={}, | ||
| slot_mapping={}, | ||
| is_padding=is_padding[offset : offset + num_tokens], | ||
| ) | ||
| with override_forward_context(forward_context): | ||
| module(*inputs) | ||
| if pbar is not None: | ||
| pbar.update(1) | ||
|
|
||
| FP8_FP4_MEGA_MOE_WARMUP_CACHE.add(key) | ||
|
|
||
|
|
||
| def deepgemm_fp8_gemm_nt_warmup( | ||
| model: torch.nn.Module, max_tokens: int, pbar: tqdm | None = None | ||
| ): | ||
|
|
@@ -330,11 +463,21 @@ def deepgemm_grouped_fp8_gemm_nt_contiguous_warmup( | |
| ) | ||
|
|
||
|
|
||
| @torch.inference_mode() | ||
| def deepgemm_fp8_fp4_mega_moe_warmup( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. warp with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in ba3bd55. This reply was generated with Claude and reviewed by a human. |
||
| model: torch.nn.Module, max_tokens: int, pbar: tqdm | None = None | ||
| ): | ||
| for m in model.modules(): | ||
| if _mega_moe_may_use_deep_gemm(m): | ||
| _deepgemm_fp8_fp4_mega_moe_warmup(m, max_tokens, pbar=pbar) | ||
|
|
||
|
|
||
| def _count_warmup_iterations(model: torch.nn.Module, max_tokens: int) -> int: | ||
| seen_fp8_sizes: set[torch.Size] = set(FP8_GEMM_NT_WARMUP_CACHE) | ||
| seen_grouped_sizes: set[torch.Size] = set( | ||
| GROUPED_FP8_GEMM_NT_CONTIGUOUS_WARMUP_CACHE | ||
| ) | ||
| seen_mega_moe_keys: set[tuple] = set(FP8_FP4_MEGA_MOE_WARMUP_CACHE) | ||
|
|
||
| total = 0 | ||
| for m in model.modules(): | ||
|
|
@@ -355,6 +498,11 @@ def _count_warmup_iterations(model: torch.nn.Module, max_tokens: int) -> int: | |
| if w2.size() not in seen_grouped_sizes: | ||
| total += n_values | ||
| seen_grouped_sizes.add(w2.size()) | ||
| elif _mega_moe_may_use_deep_gemm(m): | ||
| key = _get_fp8_fp4_mega_moe_warmup_key(m) | ||
| if key not in seen_mega_moe_keys: | ||
| total += len(_get_fp8_fp4_mega_moe_m_values(m, max_tokens)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I think it's cheap, but it seems like the enumeration runs twice per key?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True, it runs twice (once for the progress-bar total, once for the warmup). I measured ~41 ms per key at 8192 tokens on B300 (~5 µs/token), next to seconds of JIT per kernel. This reply was generated with Claude and reviewed by a human. |
||
| seen_mega_moe_keys.add(key) | ||
| return total | ||
|
|
||
|
|
||
|
|
@@ -369,6 +517,8 @@ def deep_gemm_warmup(model: torch.nn.Module, max_tokens: int): | |
| with tqdm(total=total, desc="DeepGEMM warmup") as pbar: | ||
| deepgemm_fp8_gemm_nt_warmup(model, max_tokens, pbar) | ||
| deepgemm_grouped_fp8_gemm_nt_contiguous_warmup(model, max_tokens, pbar) | ||
| deepgemm_fp8_fp4_mega_moe_warmup(model, max_tokens, pbar) | ||
| else: | ||
| deepgemm_fp8_gemm_nt_warmup(model, max_tokens, None) | ||
| deepgemm_grouped_fp8_gemm_nt_contiguous_warmup(model, max_tokens, None) | ||
| deepgemm_fp8_fp4_mega_moe_warmup(model, max_tokens, None) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My agent flagged a potential issue. Could you double-check whether it’s actually valid? It seems plausible to me, which is exactly why I’m not a fan of these token-based warmups... :))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, verified: the mechanism is real. On current DeepGEMM the gate config changes exactly at 17 tokens, so the 256/384-expert models (threshold 16) don't miss anything, but a 128-expert gate (threshold 1, e.g. the DSpark drafter) misses the config used by sizes 2..16, because its representative is M=1 which takes the
GateLinearbranch.Rather than exposing the threshold, ba3bd55 keeps the largest M per
(gate_config, block_m)key. The forward condition isnum_tokens > threshold, so any config the forward actually passes tobf16_mega_gatehas its largest M above the threshold. That avoids duplicating the constant and adds no keys. I swept 288 configs (hidden 4096/7168, 128/256/384 experts, top_k 6/8, EP 1–8, SP) against the real heuristics on B300: largest-M misses 0bf16_mega_gate/fp8_fp4_mega_moekernels, while smallest-M missed a gate kernel in all 96 128-expert configs. Addedtest_mega_moe_m_values_reach_mega_gatefor it. Agreed that CUDA-graph capture covers most sizes otherwise; this closes the eager gap. Re-validated DSV4 with/without MTP: no MegaMoE JIT after warmup, GSM8K 0.985.This reply was generated with Claude and reviewed by a human.