Files
sglang/python/sglang/srt/layers/deep_gemm_wrapper/compile_utils.py
laoyao0822 cc908dd556 Reuse DeepGEMM JIT cache across restarts
New sgl-deep-gemm wheels persist cubins, but SGLang was still replaying the full warmup envelope after every restart. The wrapper also translated SGLANG_DG_* env after importing deep_gemm, so the wheel could observe stale cache settings.\n\nMove DeepGEMM JIT env preparation before the first import, bridge the current SGLANG_DG_USE_NVRTC spelling, replace dense M walks with category-specific sparse M grids, and add a SGLang-side warmup manifest under the DeepGEMM cache dir. A manifest hit skips only the SGLang replay loop; missing or stale cache entries still force warmup and refresh the manifest.\n\nConstraint: sgl-deep-gemm 0.1.2 removed the compile-mode API, so dense 1..m_max loops launch real kernels.\nConstraint: DeepGEMM cache keys include compiler flags such as the deep_gemm include path; path-stable environments are still required for cross-container cache hits.\nRejected: SGLANG_JIT_DEEPGEMM_PRECOMPILE=0 | bypasses warmup instead of making cache reuse correct.\nRejected: Enable per-process cache dirs by default | isolates corruption but defeats persistent cross-process cache reuse.\nConfidence: medium\nScope-risk: moderate\nDirective: Do not reintroduce dense DeepGEMM warmup for new wheels unless compile-mode support is verified on the target wheel.\nTested: Local py_compile for jit_cache.py configurer.py compile_utils.py model_runner.py\nTested: Local pytest test/registered/unit/layers/test_deep_gemm_jit_cache.py (6 passed)\nTested: Remote g0034 cjy-glm5-new py_compile for same files\nTested: Remote g0034 cjy-glm5-new pytest test/registered/unit/layers/test_deep_gemm_jit_cache.py (6 passed)\nTested: Remote env probe verified SGLANG_DG_CACHE_DIR and SGLANG_DG_USE_NVRTC bridge to DG_JIT_* before configurer completes\nNot-tested: Full decode cold-cache/hot-cache restart timing and manifest-hit ETE log validation
2026-06-11 02:55:04 +08:00

403 lines
15 KiB
Python

import logging
import os
from contextlib import contextmanager
from enum import IntEnum, auto
from importlib.metadata import PackageNotFoundError, version
from typing import Dict, List, Tuple
import torch
from tqdm import tqdm
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
disable_symmetric_memory_context,
restore_symmetric_memory_context,
)
from sglang.srt.environ import envs
from sglang.srt.layers.deep_gemm_wrapper.jit_cache import (
build_warmup_m_lists,
build_sparse_m_list,
cache_entries,
make_warmup_manifest_key,
mark_warmup_complete,
prepare_deep_gemm_jit_env,
should_skip_warmup,
)
from sglang.srt.layers.deep_gemm_wrapper.configurer import ENABLE_JIT_DEEPGEMM
from sglang.srt.server_args import ServerArgs
from sglang.srt.utils import ceil_div, get_available_gpu_memory
logger = logging.getLogger(__name__)
if ENABLE_JIT_DEEPGEMM:
import deep_gemm
_M_LIST_PER_RANK: List[int] = build_sparse_m_list(1024 * 16)
_M_LIST_GATHERED: List[int] = build_sparse_m_list(1024 * 16)
_M_LIST_DECODE: List[int] = build_sparse_m_list(1024)
_ENABLE_JIT_DEEPGEMM_PRECOMPILE = envs.SGLANG_JIT_DEEPGEMM_PRECOMPILE.get()
_DO_COMPILE_ALL = True
_IS_FIRST_RANK_ON_NODE = envs.SGLANG_IS_FIRST_RANK_ON_NODE.get()
_IN_PRECOMPILE_STAGE = envs.SGLANG_IN_DEEPGEMM_PRECOMPILE_STAGE.get()
# New DeepGEMM wheels dropped compile-mode, so dense 1..m_max enumeration would
# launch real kernels. Sparse warmup is now always used.
_DEEP_GEMM_CACHE_DIR = prepare_deep_gemm_jit_env(
preload_kernels=_ENABLE_JIT_DEEPGEMM_PRECOMPILE
)
def update_deep_gemm_config(gpu_id: int, server_args: ServerArgs):
global _M_LIST_PER_RANK, _M_LIST_GATHERED, _M_LIST_DECODE
global _DO_COMPILE_ALL
global _IS_FIRST_RANK_ON_NODE
m_lists = build_warmup_m_lists(
chunked_prefill_size=server_args.chunked_prefill_size,
attn_cp_size=getattr(server_args, "attn_cp_size", 1),
max_running_requests=server_args.max_running_requests,
speculative_num_draft_tokens=server_args.speculative_num_draft_tokens,
)
_M_LIST_PER_RANK = m_lists.per_rank
_M_LIST_GATHERED = m_lists.gathered
_M_LIST_DECODE = m_lists.decode
_IS_FIRST_RANK_ON_NODE = server_args.base_gpu_id == gpu_id
if _IS_FIRST_RANK_ON_NODE:
logger.info(
"[DeepGEMM precompile] chunked=%s attn_cp=%s -> "
"per_rank max_m=%s (%s entries), gathered max_m=%s (%s entries), "
"decode max_m=%s (%s entries)",
server_args.chunked_prefill_size,
getattr(server_args, "attn_cp_size", 1),
max(_M_LIST_PER_RANK) if _M_LIST_PER_RANK else 0,
len(_M_LIST_PER_RANK),
max(_M_LIST_GATHERED) if _M_LIST_GATHERED else 0,
len(_M_LIST_GATHERED),
max(_M_LIST_DECODE) if _M_LIST_DECODE else 0,
len(_M_LIST_DECODE),
)
# Check if is the first rank on node.
# Default each rank will try compile all Ms to
# load all symbols at the launch stages.
# Avoid loading symbols at the serving stages.
_DO_COMPILE_ALL = _IS_FIRST_RANK_ON_NODE
class DeepGemmKernelType(IntEnum):
GROUPED_GEMM_NT_F8F8BF16_MASKED = auto()
GROUPED_GEMM_NT_F8F8BF16_CONTIG = auto()
GEMM_NT_F8F8BF16 = auto()
GEMM_NT_BF16BF16F32 = auto()
_INITIALIZATION_DICT: Dict[Tuple[DeepGemmKernelType, int, int, int], bool] = dict()
def _m_list_for(kernel_type: DeepGemmKernelType) -> List[int]:
if kernel_type == DeepGemmKernelType.GROUPED_GEMM_NT_F8F8BF16_CONTIG:
return _M_LIST_GATHERED
if kernel_type == DeepGemmKernelType.GROUPED_GEMM_NT_F8F8BF16_MASKED:
return _M_LIST_DECODE
return _M_LIST_PER_RANK
def _deep_gemm_runtime_fingerprint() -> Dict[str, str]:
try:
deep_gemm_version = version("sgl-deep-gemm")
except PackageNotFoundError:
try:
deep_gemm_version = version("deep-gemm")
except PackageNotFoundError:
deep_gemm_version = "unknown"
try:
capability = torch.cuda.get_device_capability(0)
sm = f"{capability[0]}{capability[1]}"
except Exception:
sm = "unknown"
return {
"deep_gemm_version": deep_gemm_version,
"deep_gemm_file": str(getattr(deep_gemm, "__file__", "")),
"torch_version": str(torch.__version__),
"cuda_version": str(getattr(torch.version, "cuda", "")),
"sm": sm,
"dg_jit_use_nvrtc": os.environ.get("DG_JIT_USE_NVRTC", "0"),
}
# TODO improve code
def _maybe_compile_deep_gemm_one_type_all(
kernel_type: DeepGemmKernelType,
n: int,
k: int,
num_groups: int,
) -> None:
global _INITIALIZATION_DICT
query_key = (kernel_type, n, k, num_groups)
if (
_ENABLE_JIT_DEEPGEMM_PRECOMPILE
and _DO_COMPILE_ALL
and _INITIALIZATION_DICT.get(query_key) is None
):
_INITIALIZATION_DICT[query_key] = True
# TODO maybe improve logs
if not _IN_PRECOMPILE_STAGE and _IS_FIRST_RANK_ON_NODE:
logger.warning(
"Entering DeepGEMM JIT Pre-Compile session. "
"It may take a long time (typically 10-20 mins) "
"if you have not run `sglang.compile_deep_gemm`. "
"It is recommended to run `sglang.compile_deep_gemm` with same args as `sglang.launch_server`"
" for pre-compilation to reduce the overhead if you have not run it before. "
"For example: "
"`python3 -m sglang.compile_deep_gemm --model deepseek-ai/DeepSeek-V3 --tp 8 --trust-remote-code`"
)
m_list = _m_list_for(kernel_type)
manifest_key = make_warmup_manifest_key(
kernel_type=kernel_type.name,
n=n,
k=k,
num_groups=num_groups,
m_list=m_list,
runtime_fingerprint=_deep_gemm_runtime_fingerprint(),
)
if should_skip_warmup(_DEEP_GEMM_CACHE_DIR, manifest_key):
logger.info(
"[DeepGEMM-cache] warmup manifest hit; skip replay for "
"<%s> N=%s K=%s num_groups=%s m_count=%s max_m=%s cache_dir=%s",
kernel_type.name,
n,
k,
num_groups,
len(m_list),
max(m_list) if m_list else 0,
_DEEP_GEMM_CACHE_DIR,
)
return
logger.info(
f"Try DeepGEMM JIT Compiling for "
f"<{kernel_type.name}> N={n}, K={k}, num_groups={num_groups} "
f"with {len(m_list)} Ms (max M={max(m_list) if m_list else 0})."
f"{' It only takes a little time (typically 1 sec) if you have run `python3 -m sglang.compile_deep_gemm`. ' if not _IN_PRECOMPILE_STAGE else ''}"
)
_compile_deep_gemm_one_type_all(
kernel_type=kernel_type,
n=n,
k=k,
num_groups=num_groups,
m_list=m_list,
)
mark_warmup_complete(
_DEEP_GEMM_CACHE_DIR,
manifest_key,
cache_entries(_DEEP_GEMM_CACHE_DIR),
)
# NOTE(alcanderian): get_num_sms should be change when 2-batch-overlap is introduced
def _compile_deep_gemm_one_type_all(
kernel_type: DeepGemmKernelType,
n: int,
k: int,
num_groups: int,
m_list: List[int],
) -> None:
# Symmetric memory allocation performs a collective operation across all the GPUs.
# Temporary disable symmetric memory during compilation since it only runs on the first rank.
saved_context = disable_symmetric_memory_context()
try:
if kernel_type == DeepGemmKernelType.GROUPED_GEMM_NT_F8F8BF16_CONTIG:
m_alignment = deep_gemm.get_mk_alignment_for_contiguous_layout()
m_list = sorted(list(set(m for m in m_list if m % m_alignment == 0)))
# Here the precompilation is only run on the first rank, so gpu_id should be 0
memory_budget = get_available_gpu_memory(device="cuda", gpu_id=0)
# If the memory budget is less memory requirement, we need to reduce max_m to avoid out of memory, which might further cause hanging during warmup
max_m = max(m_list)
required_memory = _BaseWarmupExecutor.get_memory_requirement(
kernel_type, max_m=max_m, n=n, k=k, num_groups=num_groups
)
logger.info(
f"Required memory for warmup: {required_memory}GB, Available memory: {memory_budget}GB"
)
if memory_budget < required_memory:
# TODO: Maybe compute the max_m based on the memory budget
while (
_BaseWarmupExecutor.get_memory_requirement(
kernel_type, max_m=max_m, n=n, k=k, num_groups=num_groups
)
> memory_budget
and max_m > 4096
):
max_m = max_m // 2
logger.warning(
f"Available memory {memory_budget}GB is less than required memory {required_memory}GB for warmup, reducing max_m to {max_m} to avoid out of memory"
)
m_list = [m for m in m_list if m <= max_m]
# Need some methods to estimate needed memory for warmup
executor = _BaseWarmupExecutor.create(
kernel_type, max_m=max_m, n=n, k=k, num_groups=num_groups
)
# sgl-deep-gemm (DeepGEMM release-0426+) dropped the compile-mode API; guard it.
# See [[WI-2026-06-07-001]] (upstream PR #24268).
has_compile_mode_api = hasattr(deep_gemm, "get_compile_mode") and hasattr(
deep_gemm, "set_compile_mode"
)
if has_compile_mode_api:
old_compile_mode = deep_gemm.get_compile_mode()
deep_gemm.set_compile_mode(1)
# TODO can use multi thread
for m in tqdm(m_list, desc=f"DeepGEMM warmup"):
executor.execute(m=m)
if has_compile_mode_api:
deep_gemm.set_compile_mode(old_compile_mode)
# clean up input buffers
torch.cuda.current_stream().synchronize()
del executor
torch.cuda.empty_cache()
finally:
# Restore symmetric memory context
restore_symmetric_memory_context(saved_context)
class _BaseWarmupExecutor:
@staticmethod
def create(kernel_type: DeepGemmKernelType, **kwargs):
return {
DeepGemmKernelType.GEMM_NT_F8F8BF16: _NormalWarmupExecutor,
DeepGemmKernelType.GROUPED_GEMM_NT_F8F8BF16_CONTIG: _GroupedContWarmupExecutor,
DeepGemmKernelType.GROUPED_GEMM_NT_F8F8BF16_MASKED: _GroupedMaskedWarmupExecutor,
DeepGemmKernelType.GEMM_NT_BF16BF16F32: _BF16F32WarmupExecutor,
}[kernel_type](**kwargs)
@staticmethod
def get_memory_requirement(
kernel_type: DeepGemmKernelType, max_m: int, n: int, k: int, num_groups: int
) -> int:
# Return the required memory space in GB for warmup executor
_GB = 1 << 30
if kernel_type == DeepGemmKernelType.GEMM_NT_F8F8BF16:
return (max_m * k + n * k + max_m * n * 2) / _GB
elif kernel_type == DeepGemmKernelType.GROUPED_GEMM_NT_F8F8BF16_CONTIG:
return (max_m * k + num_groups * n * k + max_m * 4 + max_m * n * 2) / _GB
elif kernel_type == DeepGemmKernelType.GROUPED_GEMM_NT_F8F8BF16_MASKED:
return (
num_groups * max_m * k
+ num_groups * n * k
+ num_groups * 4
+ num_groups * max_m * n * 2
) / _GB
elif kernel_type == DeepGemmKernelType.GEMM_NT_BF16BF16F32:
# bf16 lhs + bf16 rhs + fp32 out
return (max_m * k * 2 + n * k * 2 + max_m * n * 4) / _GB
else:
raise ValueError(f"Invalid kernel type: {kernel_type}")
def execute(self, m):
raise NotImplementedError
def _empty_token_fp8(size):
*dims, k = size
return (
torch.empty(size, device="cuda", dtype=torch.float8_e4m3fn),
torch.empty(
(*dims, ceil_div(k, _BLOCK_SIZE)), device="cuda", dtype=torch.float32
),
)
def _empty_block_fp8(size):
*dims, n, k = size
return (
torch.empty(size, device="cuda", dtype=torch.float8_e4m3fn),
torch.empty(
(*dims, ceil_div(n, _BLOCK_SIZE), ceil_div(k, _BLOCK_SIZE)),
device="cuda",
dtype=torch.float32,
),
)
_BLOCK_SIZE = 128
class _NormalWarmupExecutor(_BaseWarmupExecutor):
def __init__(self, max_m: int, n: int, k: int, num_groups: int):
self.lhs_q, self.lhs_s = _empty_token_fp8((max_m, k))
self.rhs_q, self.rhs_s = _empty_block_fp8((n, k))
self.out = torch.empty((max_m, n), device="cuda", dtype=torch.bfloat16)
def execute(self, m):
deep_gemm.fp8_gemm_nt(
(self.lhs_q[:m], self.lhs_s[:m]),
(self.rhs_q, self.rhs_s),
self.out[:m],
)
class _GroupedContWarmupExecutor(_BaseWarmupExecutor):
def __init__(self, max_m: int, n: int, k: int, num_groups: int):
self.lhs_q, self.lhs_s = _empty_token_fp8((max_m, k))
self.rhs_q, self.rhs_s = _empty_block_fp8((num_groups, n, k))
self.m_indices = torch.zeros((max_m,), device="cuda", dtype=torch.int32)
self.out = torch.empty((max_m, n), device="cuda", dtype=torch.bfloat16)
def execute(self, m):
deep_gemm.m_grouped_fp8_gemm_nt_contiguous(
(self.lhs_q[:m], self.lhs_s[:m]),
(self.rhs_q, self.rhs_s),
self.out[:m],
# sgl-deep-gemm takes m_indices positionally. [[WI-2026-06-07-001]] (PR #24268)
self.m_indices[:m],
)
class _GroupedMaskedWarmupExecutor(_BaseWarmupExecutor):
def __init__(self, max_m: int, n: int, k: int, num_groups: int):
self.lhs_q, self.lhs_s = _empty_token_fp8((num_groups, max_m, k))
self.rhs_q, self.rhs_s = _empty_block_fp8((num_groups, n, k))
self.masked_m = torch.zeros((num_groups,), device="cuda", dtype=torch.int32)
self.out = torch.empty(
(num_groups, max_m, n), device="cuda", dtype=torch.bfloat16
)
def execute(self, m):
deep_gemm.fp8_m_grouped_gemm_nt_masked(
(self.lhs_q, self.lhs_s),
(self.rhs_q, self.rhs_s),
self.out,
masked_m=self.masked_m,
# DeepGEMM uses `expect_m` instead of input shape for `get_best_config`
expected_m=m,
)
class _BF16F32WarmupExecutor(_BaseWarmupExecutor):
def __init__(self, max_m: int, n: int, k: int, num_groups: int):
self.lhs = torch.empty((max_m, k), device="cuda", dtype=torch.bfloat16)
self.rhs = torch.empty((n, k), device="cuda", dtype=torch.bfloat16)
self.out = torch.empty((max_m, n), device="cuda", dtype=torch.float32)
def execute(self, m):
deep_gemm.bf16_gemm_nt(self.lhs[:m], self.rhs, self.out[:m])
@contextmanager
def deep_gemm_execution_hook(
m: int, n: int, k: int, num_groups: int, kernel_type: DeepGemmKernelType
):
if m > 0:
_maybe_compile_deep_gemm_one_type_all(kernel_type, n, k, num_groups)
yield