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
217 lines
6.7 KiB
Python
217 lines
6.7 KiB
Python
"""DeepGEMM JIT cache helpers.
|
|
|
|
This module is deliberately stdlib-only. It is imported before ``deep_gemm`` in
|
|
the wrapper startup path, so it must not import torch, SGLang utility modules, or
|
|
anything that can transitively import ``deep_gemm``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import tempfile
|
|
import time
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Iterable, Mapping
|
|
|
|
MANIFEST_FILENAME = "sglang_deep_gemm_warmup_manifest.v1.json"
|
|
MANIFEST_VERSION = 1
|
|
|
|
|
|
def _env_true(value: str | None) -> bool:
|
|
return value is not None and value.lower() in ("1", "true", "yes")
|
|
|
|
|
|
def default_deep_gemm_cache_dir() -> str:
|
|
return os.path.join(os.path.expanduser("~"), ".cache", "deep_gemm")
|
|
|
|
|
|
def prepare_deep_gemm_jit_env(*, preload_kernels: bool = True) -> str:
|
|
"""Set DeepGEMM JIT env vars before the first ``import deep_gemm``.
|
|
|
|
DeepGEMM's JIT cache key depends on its own environment variables and
|
|
compiler flags. SGLang exposes ``SGLANG_*`` knobs, but the wheel consumes
|
|
``DG_*`` knobs. This bridge is intentionally idempotent:
|
|
|
|
* explicit ``DG_JIT_CACHE_DIR`` / ``DG_JIT_USE_NVRTC`` wins;
|
|
* otherwise SGLang env aliases are translated once;
|
|
* cache directory is created eagerly so startup fails at the real cause.
|
|
"""
|
|
|
|
cache_dir = os.environ.get("DG_JIT_CACHE_DIR")
|
|
if not cache_dir:
|
|
cache_dir = os.environ.get("SGLANG_DG_CACHE_DIR", default_deep_gemm_cache_dir())
|
|
if _env_true(os.environ.get("SGLANG_DG_CACHE_DIR_PER_PROCESS")):
|
|
cache_dir = os.path.join(cache_dir, f"pid_{os.getpid()}")
|
|
os.environ["DG_JIT_CACHE_DIR"] = cache_dir
|
|
|
|
os.makedirs(cache_dir, exist_ok=True)
|
|
|
|
if "DG_JIT_USE_NVRTC" not in os.environ:
|
|
os.environ["DG_JIT_USE_NVRTC"] = os.environ.get(
|
|
"SGLANG_DG_USE_NVRTC",
|
|
os.environ.get("SGL_DG_USE_NVRTC", "0"),
|
|
)
|
|
|
|
if preload_kernels and "DG_PRELOAD_KERNELS" not in os.environ:
|
|
os.environ["DG_PRELOAD_KERNELS"] = "1"
|
|
|
|
return cache_dir
|
|
|
|
|
|
def build_sparse_m_list(m_max: int) -> list[int]:
|
|
"""Sparse M grid matching DeepGEMM v0.1+ block-size breakpoints.
|
|
|
|
The old dense 1..m_max walk was only cheap while DeepGEMM exposed
|
|
compile-mode. New wheels launch real kernels for each M, while cubin
|
|
selection only changes at coarse breakpoints. Keep 1..1024 dense for
|
|
decode/small prefill, then double both range and stride.
|
|
"""
|
|
|
|
m_max = max(1, int(m_max))
|
|
out = list(range(1, min(1024, m_max) + 1))
|
|
next_m, step = 1024, 2
|
|
while next_m < m_max:
|
|
out.extend(range(next_m, min(2 * next_m, m_max), step))
|
|
next_m *= 2
|
|
step *= 2
|
|
out.append(m_max)
|
|
return sorted(set(out))
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WarmupMLists:
|
|
per_rank: list[int]
|
|
gathered: list[int]
|
|
decode: list[int]
|
|
|
|
|
|
def build_warmup_m_lists(
|
|
*,
|
|
chunked_prefill_size: int,
|
|
attn_cp_size: int,
|
|
max_running_requests: int,
|
|
speculative_num_draft_tokens: int | None,
|
|
) -> WarmupMLists:
|
|
"""Build category-specific DeepGEMM warmup M lists.
|
|
|
|
* per_rank: dense/attention/indexer GEMMs after NSA CP split.
|
|
* gathered: MoE contiguous GEMMs after DeepEP all-to-all.
|
|
* decode: masked GEMMs, bounded by running speculative decode batch.
|
|
"""
|
|
|
|
chunked = int(chunked_prefill_size)
|
|
if chunked < 1:
|
|
chunked = 64 * 1024
|
|
chunked = min(chunked, 128 * 1024)
|
|
|
|
cp_size = max(1, int(attn_cp_size or 1))
|
|
m_max_per_rank = max(1024, chunked // cp_size)
|
|
m_max_gathered = chunked
|
|
|
|
spec_tokens = max(1, int(speculative_num_draft_tokens or 1))
|
|
m_max_decode = min(1024, max(256, int(max_running_requests) * spec_tokens))
|
|
|
|
return WarmupMLists(
|
|
per_rank=build_sparse_m_list(m_max_per_rank),
|
|
gathered=build_sparse_m_list(m_max_gathered),
|
|
decode=build_sparse_m_list(m_max_decode),
|
|
)
|
|
|
|
|
|
def cache_entries(cache_root: str | os.PathLike[str]) -> set[str]:
|
|
"""Return DeepGEMM cubin cache entry directory names under ``cache/``."""
|
|
|
|
cache_dir = Path(cache_root) / "cache"
|
|
if not cache_dir.is_dir():
|
|
return set()
|
|
return {p.name for p in cache_dir.iterdir() if p.is_dir()}
|
|
|
|
|
|
def make_warmup_manifest_key(
|
|
*,
|
|
kernel_type: str,
|
|
n: int,
|
|
k: int,
|
|
num_groups: int,
|
|
m_list: Iterable[int],
|
|
runtime_fingerprint: Mapping[str, object],
|
|
) -> str:
|
|
m_values = [int(m) for m in m_list]
|
|
payload = {
|
|
"version": MANIFEST_VERSION,
|
|
"kernel_type": kernel_type,
|
|
"n": int(n),
|
|
"k": int(k),
|
|
"num_groups": int(num_groups),
|
|
"m_list_count": len(m_values),
|
|
"m_list_max": max(m_values) if m_values else 0,
|
|
"m_list_hash": hashlib.sha256(
|
|
",".join(str(m) for m in m_values).encode("utf-8")
|
|
).hexdigest(),
|
|
"runtime": dict(sorted(runtime_fingerprint.items())),
|
|
}
|
|
return hashlib.sha256(
|
|
json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
).hexdigest()
|
|
|
|
|
|
def _manifest_path(cache_root: str | os.PathLike[str]) -> Path:
|
|
return Path(cache_root) / MANIFEST_FILENAME
|
|
|
|
|
|
def _load_manifest(cache_root: str | os.PathLike[str]) -> dict:
|
|
path = _manifest_path(cache_root)
|
|
if not path.exists():
|
|
return {"version": MANIFEST_VERSION, "entries": {}}
|
|
try:
|
|
data = json.loads(path.read_text())
|
|
except Exception:
|
|
return {"version": MANIFEST_VERSION, "entries": {}}
|
|
if data.get("version") != MANIFEST_VERSION or not isinstance(
|
|
data.get("entries"), dict
|
|
):
|
|
return {"version": MANIFEST_VERSION, "entries": {}}
|
|
return data
|
|
|
|
|
|
def should_skip_warmup(cache_root: str | os.PathLike[str], key: str) -> bool:
|
|
manifest = _load_manifest(cache_root)
|
|
entry = manifest.get("entries", {}).get(key)
|
|
if not entry or entry.get("status") != "complete":
|
|
return False
|
|
|
|
required = set(entry.get("cache_entries") or [])
|
|
if not required:
|
|
return False
|
|
return required.issubset(cache_entries(cache_root))
|
|
|
|
|
|
def mark_warmup_complete(
|
|
cache_root: str | os.PathLike[str],
|
|
key: str,
|
|
entries: Iterable[str],
|
|
) -> None:
|
|
root = Path(cache_root)
|
|
root.mkdir(parents=True, exist_ok=True)
|
|
manifest = _load_manifest(root)
|
|
manifest.setdefault("entries", {})[key] = {
|
|
"status": "complete",
|
|
"cache_entries": sorted(set(entries)),
|
|
"updated_at": time.time(),
|
|
}
|
|
|
|
fd, tmp_name = tempfile.mkstemp(
|
|
prefix=f".{MANIFEST_FILENAME}.", suffix=".tmp", dir=str(root)
|
|
)
|
|
try:
|
|
with os.fdopen(fd, "w") as f:
|
|
json.dump(manifest, f, sort_keys=True)
|
|
f.write("\n")
|
|
os.replace(tmp_name, _manifest_path(root))
|
|
finally:
|
|
if os.path.exists(tmp_name):
|
|
os.unlink(tmp_name)
|