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
403 lines
15 KiB
Python
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
|