Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

588 lines
22 KiB
Python

"""Pre-compile deep_gemm JIT kernels used by DeepSeek V4.
deep_gemm compiles CUDA kernels on first invocation for each unique
(M, N, K, tile) combination. This module provides warmup functions that
exercise representative shapes at startup so no JIT compilation happens
on the serving hot path.
"""
from __future__ import annotations
import logging
from math import ceil
import torch
logger = logging.getLogger(__name__)
def _warmup_m_values(max_tokens: int) -> list[int]:
"""Dense set of M (token counts) covering every deep_gemm tile.
A cubin's JIT key includes ``block_m``, ``block_n`` and the ``swap_ab`` flag,
all chosen by a C++ heuristic over ``(M, N, num_sms[, num_groups])`` that
deep_gemm does NOT expose to Python (``get_best_config`` lives in ``_C.so``).
Modelling that selection from Python is unreliable -- the choice flips with M
in ways that depend on ``swap_ab`` and (for batched GEMMs) the group count.
Instead we sample M densely and let the real heuristic pick at each point,
compiling whatever it selects. Because the serving path only ever runs
``M in [1, max_tokens]``, sampling that whole interval provably covers every
reachable config. The tiling changes most often at small M (``swap_ab`` flips
and small ``block_m``), so we step by 1 there and coarsen for large M where
the selected tile is stable.
Args:
max_tokens: largest M (token count) the serving path can reach.
"""
dense = min(max_tokens, 2048)
values: set[int] = set(range(1, dense + 1))
values.update(range(dense, max_tokens + 1, 16))
values.add(max_tokens)
return sorted(values)
# ---------------------------------------------------------------------------
# mega_moe JIT warmup
# ---------------------------------------------------------------------------
def warmup_mega_moe_jit(
num_experts: int,
max_num_tokens: int,
top_k: int,
hidden_size: int,
device: torch.device,
transformed_l1_weights: tuple[torch.Tensor, torch.Tensor],
transformed_l2_weights: tuple[torch.Tensor, torch.Tensor],
symm_buffer: object,
activation_clamp: float | None = None,
) -> None:
"""Pre-compile ``fp8_fp4_mega_moe`` kernel tiles.
Moved from ``DeepseekV4MegaMoEExperts.warmup_jit_variants``.
The caller must issue a ``torch.distributed.barrier(group)`` before
invoking this so all EP ranks enter together.
All heavy objects (weights, symmetric buffer) must be passed in from
the already-initialized model to avoid duplicate GPU allocations.
"""
try:
from tokenspeed_kernel.thirdparty.deep_gemm import fp8_fp4_mega_moe
except ImportError:
logger.warning("deep_gemm mega_moe symbols unavailable, skipping warmup")
return
token_counts = _warmup_m_values(max_num_tokens)
logger.info(
"Warming up mega_moe JIT: %d token counts up to %d",
len(token_counts),
max_num_tokens,
)
for num_tokens in token_counts:
hidden_states = torch.randn(
num_tokens,
hidden_size,
dtype=torch.bfloat16,
device=device,
)
topk_ids = torch.randint(
0,
num_experts,
(num_tokens, top_k),
dtype=torch.int32,
device=device,
)
topk_weights = torch.full(
(num_tokens, top_k),
1.0 / top_k,
dtype=torch.float32,
device=device,
)
y = torch.empty_like(hidden_states)
symm_buffer.x[:num_tokens].copy_(hidden_states.to(torch.float8_e4m3fn))
symm_buffer.x_sf[:num_tokens].fill_(1.0)
symm_buffer.topk_idx[:num_tokens].copy_(topk_ids)
symm_buffer.topk_weights[:num_tokens].copy_(topk_weights)
fp8_fp4_mega_moe(
y,
transformed_l1_weights,
transformed_l2_weights,
symm_buffer,
activation_clamp=activation_clamp,
)
torch.cuda.synchronize()
# ---------------------------------------------------------------------------
# Prefill kernel JIT warmup (compressor / indexer / attention projections)
# ---------------------------------------------------------------------------
def warmup_prefill_jit(
*,
hidden_size: int,
num_attention_heads: int,
head_dim: int = 128,
hc_mult: int = 0,
kv_lora_rank: int = 0,
index_n_heads: int = 0,
index_head_dim: int = 0,
indexer_cache_block_size: int = 64,
max_decode_tokens: int = 256,
mxfp4_block_size: int = 32,
tp_size: int = 1,
max_tokens: int,
device: torch.device,
) -> None:
"""Pre-compile deep_gemm prefill/decode kernels for DeepSeek V4.
Derives kernel shapes from model config values and warms up
``tf32_hc_prenorm_gemm`` (compressor), the ragged ``fp8_fp4_mqa_logits``
(prefill indexer), and the paged ``fp8_fp4_paged_mqa_logits`` +
``get_paged_mqa_logits_metadata`` (decode indexer) kernels.
Args:
hidden_size: model hidden dimension.
num_attention_heads: total attention heads (before TP split).
head_dim: per-head dimension.
hc_mult: compressor head-coupling multiplier (0 = no compressor).
kv_lora_rank: KV LoRA rank for indexer (0 = no indexer).
index_n_heads: sparse-indexer head count (0 = no indexer). The indexer
projections are replicated (not TP-split), so this is the full
per-rank head count, not ``num_attention_heads // tp_size``.
index_head_dim: sparse-indexer per-head dim (FP4-packed in the cache).
indexer_cache_block_size: paged indexer KV-cache block size (BLOCK_KV).
max_decode_tokens: largest decode batch the server can run; the paged
decode-indexer warmup sweeps every 32-aligned bucket up to this so
no in-range decode batch JIT-compiles the metadata kernel inline.
mxfp4_block_size: MXFP4 quantization block size.
tp_size: attention tensor-parallel size.
max_tokens: maximum prefill token count to warm up to.
device: CUDA device.
"""
warmup_count = 0
if hc_mult and hc_mult > 1:
hc_hidden_size = hc_mult * hidden_size
mix_hc = (2 + hc_mult) * hc_mult
hc_dim = hc_mult * hidden_size
_warmup_tf32_hc_prenorm_gemm(
[{"hc_hidden_size": hc_hidden_size, "mix_hc": mix_hc, "hc_dim": hc_dim}],
max_tokens,
device,
)
warmup_count += 1
if index_n_heads > 0 and index_head_dim > 0:
# Sparse-indexer kernels -- distinct cubins, not covered elsewhere:
# prefill: ragged fp8_fp4_mqa_logits
# decode: paged fp8_fp4_paged_mqa_logits + get_paged_mqa_logits_metadata
# The indexer projections are replicated (config.index_n_heads heads,
# NOT TP-split), so gate on index_n_heads -- NOT kv_lora_rank, which is
# absent for V4-Flash. Without these the kernels JIT-compile inline on
# the first prefill/decode and stall the engine past the gRPC health
# probe.
_warmup_fp8_fp4_mqa_logits(
num_heads=index_n_heads,
index_head_dim=index_head_dim,
device=device,
)
_warmup_fp8_fp4_paged_mqa_logits(
num_heads=index_n_heads,
index_head_dim=index_head_dim,
cache_block_size=indexer_cache_block_size,
max_decode_tokens=max_decode_tokens,
device=device,
)
warmup_count += 1
if warmup_count > 0:
logger.info("Warmed up %d deep_gemm prefill kernel families", warmup_count)
torch.cuda.synchronize()
def _compute_num_split(block_k: int, k: int, grid_size: int) -> int:
num_sms = torch.cuda.get_device_properties(0).multi_processor_count
split_k = num_sms // grid_size
num_block_k = ceil(k / block_k)
split_k = min(split_k, num_block_k // 4)
return max(split_k, 1)
def _warmup_tf32_hc_prenorm_gemm(
shapes: list[dict],
max_tokens: int,
device: torch.device,
) -> None:
try:
from tokenspeed_kernel.thirdparty.deep_gemm import tf32_hc_prenorm_gemm
except ImportError:
logger.warning("deep_gemm tf32_hc_prenorm_gemm unavailable, skipping")
return
seen: set[tuple[int, ...]] = set()
block_k = 64
block_m = 64
for params in shapes:
hc_hidden_size = params["hc_hidden_size"]
mix_hc = params["mix_hc"]
hc_dim = params["hc_dim"]
if (hc_hidden_size, mix_hc) in seen:
continue
seen.add((hc_hidden_size, mix_hc))
fn = torch.ones(mix_hc, hc_dim, dtype=torch.float32, device=device)
token_counts = _warmup_m_values(max_tokens)
for num_tokens in token_counts:
grid_size = ceil(num_tokens / block_m)
n_splits = _compute_num_split(block_k, hc_hidden_size, grid_size)
x = torch.zeros(
num_tokens,
hc_hidden_size,
dtype=torch.bfloat16,
device=device,
)
out_mul = torch.empty(
n_splits,
num_tokens,
mix_hc,
dtype=torch.float32,
device=device,
)
out_sqrsum = torch.empty(
n_splits,
num_tokens,
dtype=torch.float32,
device=device,
)
tf32_hc_prenorm_gemm(x, fn, out_mul, out_sqrsum, n_splits)
def _warmup_fp8_fp4_mqa_logits(
*,
num_heads: int,
index_head_dim: int,
device: torch.device,
max_kv_len: int = 4096,
) -> None:
"""Pre-compile the ragged prefill sparse-indexer ``fp8_fp4_mqa_logits``.
Mirrors the prefill call (deepseek_v4.py:1392): q = (int8 values, int32
scales), kv = (gathered int8 values, int32 scales), ragged ``cu_seq_len``
of length ``num_tokens`` (not +1). FP4 packs 2 values per byte, so the
per-head value dim is ``index_head_dim // 2``. Batch and kv length are not
JIT keys, so a couple of token counts suffice.
Args:
num_heads: sparse-indexer head count (``index_n_heads``).
index_head_dim: sparse-indexer per-head dim (e.g. 128).
device: CUDA device.
max_kv_len: representative KV length to warm up to.
"""
try:
from tokenspeed_kernel.thirdparty.deep_gemm import fp8_fp4_mqa_logits
except ImportError:
logger.warning("deep_gemm fp8_fp4_mqa_logits unavailable, skipping")
return
head_dim_bytes = index_head_dim // 2
for num_tokens in (1, 256):
q_vals = torch.zeros(
num_tokens, num_heads, head_dim_bytes, dtype=torch.uint8, device=device
).view(torch.int8)
q_scales = torch.zeros(num_tokens, num_heads, dtype=torch.int32, device=device)
k_vals = torch.zeros(
max_kv_len, head_dim_bytes, dtype=torch.uint8, device=device
).view(torch.int8)
k_scales = torch.zeros(max_kv_len, dtype=torch.int32, device=device)
weights = torch.ones(num_tokens, num_heads, dtype=torch.float32, device=device)
cu_start = torch.zeros(num_tokens, dtype=torch.int32, device=device)
cu_end = torch.full((num_tokens,), max_kv_len, dtype=torch.int32, device=device)
fp8_fp4_mqa_logits(
q=(q_vals, q_scales),
kv=(k_vals, k_scales),
weights=weights,
cu_seq_len_k_start=cu_start,
cu_seq_len_k_end=cu_end,
clean_logits=False,
max_seqlen_k=max_kv_len,
logits_dtype=torch.float32,
)
def _warmup_fp8_fp4_paged_mqa_logits(
*,
num_heads: int,
index_head_dim: int,
cache_block_size: int,
max_decode_tokens: int,
device: torch.device,
) -> None:
"""Pre-compile the paged decode sparse-indexer kernels.
Mirrors the decode call ``fp8_fp4_paged_mqa_logits`` and its schedule-
metadata builder ``get_paged_mqa_logits_metadata``. These are distinct
cubins from the ragged prefill ``fp8_fp4_mqa_logits`` and are not otherwise
warmed. The paged logits kernel is keyed on (num_heads, head_dim, block_kv)
-- not the batch size -- so one call warms it, but the metadata kernel is
keyed on the 32-aligned decode batch size, so sweep every 32-aligned bucket
up to the runtime decode-batch ceiling.
Args:
num_heads: sparse-indexer head count (``index_n_heads``).
index_head_dim: sparse-indexer per-head dim (e.g. 128).
cache_block_size: paged indexer KV-cache block size (BLOCK_KV).
max_decode_tokens: largest decode batch the server can run (e.g.
``max_cudagraph_capture_size`` / ``max_num_seqs``). The metadata
kernel is swept over every 32-aligned bucket up to this value so no
in-range decode batch hits an uncompiled cubin.
device: CUDA device.
"""
try:
from tokenspeed_kernel.thirdparty.deep_gemm import (
fp8_fp4_paged_mqa_logits,
get_num_sms,
get_paged_mqa_logits_metadata,
)
except ImportError:
logger.warning("deep_gemm paged MQA logits unavailable, skipping")
return
# FP4 packs 2 values per byte; the paged KV row stores the value bytes plus
# a single int32 scale (head_dim / 2 + sizeof(int)).
head_dim_bytes = index_head_dim // 2
row_bytes = index_head_dim // 2 + 4
num_sms = get_num_sms()
# The metadata kernel is JIT-keyed on the 32-aligned decode batch size, so
# cover every bucket up to the runtime ceiling (batches < 32 map to the 32
# bucket, so they are covered too).
top_bucket = max(32, ((max_decode_tokens + 31) // 32) * 32)
decode_batch_sizes = range(32, top_bucket + 1, 32)
for num_tokens in decode_batch_sizes:
num_blocks = max(1, num_tokens)
q_values = torch.zeros(
num_tokens, num_heads, head_dim_bytes, dtype=torch.uint8, device=device
)
q_scales = torch.zeros(num_tokens, num_heads, dtype=torch.int32, device=device)
cache_2d = torch.zeros(
num_blocks, cache_block_size * row_bytes, dtype=torch.uint8, device=device
)
kv_cache = torch.as_strided(
cache_2d,
(num_blocks, cache_block_size, 1, row_bytes),
(cache_2d.stride(0), row_bytes, row_bytes, 1),
)
weights = torch.ones(num_tokens, num_heads, dtype=torch.float32, device=device)
context_lens = torch.full(
(num_tokens, 1), cache_block_size, dtype=torch.int32, device=device
)
block_table = torch.arange(num_tokens, dtype=torch.int32, device=device).view(
num_tokens, 1
)
schedule_meta = get_paged_mqa_logits_metadata(
context_lens, cache_block_size, num_sms
)
fp8_fp4_paged_mqa_logits(
q=(q_values.view(torch.int8).unsqueeze(1), q_scales.unsqueeze(1)),
kv_cache=kv_cache,
weights=weights,
context_lens=context_lens,
block_table=block_table,
schedule_meta=schedule_meta,
max_context_len=cache_block_size,
clean_logits=False,
logits_dtype=torch.float32,
)
torch.cuda.synchronize()
# ---------------------------------------------------------------------------
# FP8 GEMM warmup (attention / compressor linear projections)
# ---------------------------------------------------------------------------
def warmup_fp8_gemm_nt(
shapes: list[tuple[int, int]],
max_tokens: int,
device: torch.device,
) -> None:
"""Pre-compile ``fp8_gemm_nt`` for FP8 block-scaled linear layers.
Args:
shapes: (N, K) weight shapes for layers using deep_gemm FP8.
max_tokens: maximum prefill token count to warm up to.
device: CUDA device.
"""
try:
from tokenspeed_kernel.thirdparty.deep_gemm import fp8_gemm_nt
except ImportError:
logger.warning("deep_gemm fp8_gemm_nt unavailable, skipping warmup")
return
block_size = 128
seen: set[tuple[int, int]] = set()
for n, k in shapes:
if (n, k) in seen:
continue
seen.add((n, k))
a = torch.zeros(max_tokens, k, dtype=torch.float8_e4m3fn, device=device)
a_scales = torch.ones(
max_tokens, k // block_size, dtype=torch.float32, device=device
)
b = torch.zeros(n, k, dtype=torch.float8_e4m3fn, device=device)
b_scales = torch.ones(
n // block_size, k // block_size, dtype=torch.float32, device=device
)
out = torch.empty(max_tokens, n, dtype=torch.bfloat16, device=device)
token_counts = _warmup_m_values(max_tokens)
for num_tokens in token_counts:
fp8_gemm_nt(
(a[:num_tokens], a_scales[:num_tokens]), (b, b_scales), out[:num_tokens]
)
del a, a_scales, b, b_scales, out
logger.info("Warmed up fp8_gemm_nt for %d weight shapes", len(seen))
torch.cuda.synchronize()
def warmup_fp8_einsum(
bmm_layers: list[tuple[torch.Tensor, torch.Tensor, int, int]],
max_tokens: int,
device: torch.device,
) -> None:
"""Pre-compile ``fp8_einsum("bhr,hdr->bhd")`` for is_bmm output projections.
The attention output projection (V4 ``wo_a``) runs a per-group *batched* FP8
GEMM via ``deep_gemm.fp8_einsum`` -- a distinct ``GemmType::Batched`` path that
``warmup_fp8_gemm_nt`` does NOT cover. Its ``block_m`` grows with the prefill
token count M, so serving JITs new (N, K, block_m) tiles inline unless we
sweep M offline. The activation operand is laid out exactly as
``deepseek_v4_fused_inv_rope_fp8_quant`` returns it (per-group, transposed,
TMA-aligned INT32 UE8M0 scales); only its values are dummy -- the kernel's
JIT key is shape-only.
Args:
bmm_layers: ``(weight, weight_scale_inv, n_groups, block_n)`` tuples taken
from the loaded model (real weights/scales are reused so no scale
layout is guessed). ``weight`` is ``[n_groups * N, K]``; per group the
GEMM is ``(M, K) x (N, K) -> (M, N)``.
max_tokens: maximum prefill token count to warm up to.
device: CUDA device.
"""
try:
from tokenspeed_kernel.thirdparty.deep_gemm import fp8_einsum
except ImportError:
logger.warning("deep_gemm fp8_einsum unavailable, skipping bmm warmup")
return
for weight, weight_scale_inv, n_groups, block_n in bmm_layers:
in_dim = weight.shape[1] # K (== r == per-group quant dim)
o_lora_rank = weight.shape[0] // n_groups # N (per-group output)
w = weight.view(n_groups, o_lora_rank, in_dim)
recipe = (1, 1, block_n)
num_scale_blocks = in_dim // block_n
# N drives the batched GEMM's block_m heuristic, so sweep M against it.
for num_tokens in _warmup_m_values(max_tokens):
tma_aligned_t = ((num_tokens + 3) // 4) * 4
scale_inner = (num_scale_blocks + 3) // 4 # tma-aligned INT32 scales
o_fp8 = torch.zeros(
(n_groups, num_tokens, in_dim),
dtype=torch.float8_e4m3fn,
device=device,
).transpose(0, 1)
o_scale = (
torch.zeros(
n_groups * scale_inner * tma_aligned_t,
dtype=torch.int32,
device=device,
)
.as_strided(
(n_groups, num_tokens, scale_inner),
(scale_inner * tma_aligned_t, 1, tma_aligned_t),
)
.transpose(0, 1)
)
z = torch.empty(
(num_tokens, n_groups, o_lora_rank),
dtype=torch.bfloat16,
device=device,
)
fp8_einsum(
"bhr,hdr->bhd",
(o_fp8, o_scale),
(w, weight_scale_inv),
z,
recipe=recipe,
)
logger.info("Warmed up fp8_einsum for %d bmm shapes", len(bmm_layers))
torch.cuda.synchronize()
def warmup_fp8_gemm_nt_from_model(
model: torch.nn.Module,
max_tokens: int = 8192,
) -> None:
"""Scan a model for deep_gemm FP8 linear layers and warm their JIT tiles.
Collects (N, K) weight shapes from all modules where
``_use_deep_gemm_fp8=True`` (set by ``Fp8LinearMethod.process_weights_after_loading``):
plain projections are warmed via ``fp8_gemm_nt`` and is_bmm projections
(V4 ``wo_a``) via ``fp8_einsum`` -- both grow ``block_m`` with M, so both must
be swept offline or they JIT inline on the first long prefill.
Call after ``quant_method.process_weights_after_loading()`` has run on all
modules so the ``_use_deep_gemm_fp8`` flag is set.
"""
if torch.cuda.get_device_capability()[0] < 10:
return
shapes: set[tuple[int, int]] = set()
bmm_layers: list[tuple[torch.Tensor, torch.Tensor, int, int]] = []
bmm_seen: set[tuple] = set()
for module in model.modules():
if not getattr(module, "_use_deep_gemm_fp8", False):
continue
if getattr(module, "is_bmm", False):
n_groups = getattr(module, "bmm_batch_size", 0)
block_size = getattr(module, "_deep_gemm_block_size", None)
if not n_groups or not block_size:
continue
key = (tuple(module.weight.shape), n_groups, block_size[0])
if key in bmm_seen:
continue
bmm_seen.add(key)
bmm_layers.append(
(module.weight, module.weight_scale_inv, n_groups, block_size[0])
)
else:
n, k = module.weight.shape
shapes.add((n, k))
if not shapes and not bmm_layers:
return
device = next(model.parameters()).device
if shapes:
logger.info("Pre-compiling %d deep_gemm FP8 GEMM shapes...", len(shapes))
warmup_fp8_gemm_nt(list(shapes), max_tokens, device)
if bmm_layers:
logger.info(
"Pre-compiling %d deep_gemm FP8 einsum (bmm) shapes...", len(bmm_layers)
)
warmup_fp8_einsum(bmm_layers, max_tokens, device)