Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

434 lines
18 KiB
Python

from __future__ import annotations
import ctypes
import logging
import os
import tempfile
from math import prod
from typing import TYPE_CHECKING, List, Optional, Sequence
import torch
import torch.utils.cpp_extension
from torch.cuda.memory import CUDAPluggableAllocator
if TYPE_CHECKING:
from sglang.srt.mem_cache.memory_pool import KvBufferDesc
logger = logging.getLogger(__name__)
_drv = None
def _driver():
global _drv
if _drv is None:
from cuda.bindings import driver
_drv = driver
return _drv
def _check(result, label: str):
drv = _driver()
err = result[0] if isinstance(result, tuple) else result
if err != drv.CUresult.CUDA_SUCCESS:
raise RuntimeError(f"{label} failed: {err}")
return result[1] if isinstance(result, tuple) and len(result) > 1 else None
def align_up(value: int, alignment: int) -> int:
return (value + alignment - 1) // alignment * alignment
def query_granularity(device_id: int) -> int:
"""Minimum CUDA virtual-memory allocation granularity (bytes) for ``device_id``."""
drv = _driver()
prop = drv.CUmemAllocationProp()
prop.type = drv.CUmemAllocationType.CU_MEM_ALLOCATION_TYPE_PINNED
prop.location.type = drv.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE
prop.location.id = int(device_id)
return int(
_check(
drv.cuMemGetAllocationGranularity(
prop,
drv.CUmemAllocationGranularity_flags.CU_MEM_ALLOC_GRANULARITY_MINIMUM,
),
"cuMemGetAllocationGranularity",
)
)
# Bump allocator: hands back base+cursor, bounded by the RESERVED size (not the
# committed watermark) so upper-bound tensors can be allocated before physical
# commit. Allocations are granularity-aligned so each pointer can be committed at
# its own VA range (cuMemMap requires it; GB300 rejects partial-handle maps).
# Symbols are SUFFIXED per (process, arena instance) and each instance loads its
# own .so, so neither multiple arenas per process (hybrid-SWA: full + swa) nor
# co-located engine processes sharing the tempdir clobber each other.
def _stub_source(sfx: str) -> str:
return f"""
#include <cstddef>
#include <cstdint>
#include <mutex>
extern "C" {{
static uintptr_t g_base = 0;
static size_t g_cursor = 0;
static size_t g_reserved = 0;
static size_t g_align = 512;
static std::mutex g_mu;
static size_t align_up(size_t v, size_t a){{ return (v + a - 1) / a * a; }}
void kvarena_set_base_{sfx}(uintptr_t b){{ std::lock_guard<std::mutex> lk(g_mu); g_base=b; g_cursor=0; }}
void kvarena_set_reserved_{sfx}(size_t r){{ std::lock_guard<std::mutex> lk(g_mu); g_reserved=r; }}
void kvarena_set_align_{sfx}(size_t a){{ std::lock_guard<std::mutex> lk(g_mu); if (a) g_align=a; }}
size_t kvarena_cursor_{sfx}(void){{ std::lock_guard<std::mutex> lk(g_mu); return g_cursor; }}
void* kvarena_malloc_{sfx}(size_t size, int device, void* stream){{
std::lock_guard<std::mutex> lk(g_mu);
size_t need = g_cursor + align_up(size, g_align);
if (need > g_reserved) return 0; // never exceed the reserved VA range
void* p = reinterpret_cast<void*>(g_base + g_cursor);
g_cursor = need;
return p;
}}
void kvarena_free_{sfx}(void* ptr, size_t size, int device, void* stream){{}}
}}
"""
_DEFAULT_RESERVE_BYTES = 256 * (1024**3) # 256 GiB virtual; free until committed
class KvVmmArena:
"""One device's CUDA virtual-memory reservation exposed as a ``torch.cuda.MemPool``."""
# Per-instance suffix source -> isolated allocator symbols/state (see _stub_source).
_instance_count = 0
def __init__(self, device_id: int, reserve_bytes: int = _DEFAULT_RESERVE_BYTES):
self.device_id = int(device_id)
# Unique per (process, arena instance): the stub .so lives in a host-shared
# tempdir, so co-located engine processes must not build the same-named .so
# (they race and one loads a half-relinked copy -> undefined symbol crash).
self._sfx = f"{os.getpid()}_{KvVmmArena._instance_count}"
KvVmmArena._instance_count += 1
drv = _driver()
with torch.cuda.device(self.device_id):
_check(drv.cuInit(0), "cuInit")
self._prop = drv.CUmemAllocationProp()
self._prop.type = drv.CUmemAllocationType.CU_MEM_ALLOCATION_TYPE_PINNED
self._prop.location.type = drv.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE
self._prop.location.id = self.device_id
self.granularity = query_granularity(self.device_id)
self._access = drv.CUmemAccessDesc()
self._access.location.type = (
drv.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE
)
self._access.location.id = self.device_id
self._access.flags = (
drv.CUmemAccess_flags.CU_MEM_ACCESS_FLAGS_PROT_READWRITE
)
self.reserved = self._align(reserve_bytes)
# Align the base to granularity so base + (granularity-aligned cursor) is
# always a valid cuMemMap address for per-buffer commit_range().
self.base = int(
_check(
drv.cuMemAddressReserve(self.reserved, self.granularity, 0, 0),
"cuMemAddressReserve",
)
)
# commit_range bookkeeping: mapped VA -> (size, handle); committed bytes per offset.
self._ranges = {}
self._committed_by_offset = {}
self._range_backed = 0
self._closed = False
self._lib = self._build_stub()
self._fn_set_base(ctypes.c_void_p(self.base))
self._fn_set_reserved(ctypes.c_size_t(self.reserved))
self._fn_set_align(ctypes.c_size_t(self.granularity))
self._allocator = CUDAPluggableAllocator(
self._so_path, f"kvarena_malloc_{self._sfx}", f"kvarena_free_{self._sfx}"
).allocator()
# no_split so the caching allocator hands our bump pointers back verbatim.
self.pool = torch.cuda.MemPool(self._allocator, no_split=True)
logger.info(
"KvVmmArena[%s] ready: device=%d reserved=%.1f GiB granularity=%d KiB",
self._sfx,
self.device_id,
self.reserved / (1024**3),
self.granularity // 1024,
)
def _align(self, v: int) -> int:
return align_up(v, self.granularity)
def _build_stub(self) -> ctypes.CDLL:
# Per-arena build dir: load_inline writes every caller's source to the same
# main.cpp inside build_directory, so any sharing (across co-located engine
# processes under the host tempdir, or across arenas within one process)
# can compile another arena's source and link a .so missing this arena's
# symbols. One dir per stub means no shared ninja scratch or .so, ever.
out_dir = os.path.join(tempfile.gettempdir(), "sgl_kv_vmm_arena", self._sfx)
os.makedirs(out_dir, exist_ok=True)
libname = f"sgl_kv_vmm_arena_stub_{self._sfx}"
torch.utils.cpp_extension.load_inline(
name=libname,
cpp_sources=_stub_source(self._sfx),
with_cuda=False, # pure arithmetic — no nvcc, no CUDA headers
is_python_module=False,
verbose=False,
build_directory=out_dir,
)
self._so_path = f"{out_dir}/{libname}.so"
lib = ctypes.CDLL(self._so_path)
self._fn_set_base = getattr(lib, f"kvarena_set_base_{self._sfx}")
self._fn_set_base.argtypes = [ctypes.c_void_p]
self._fn_set_base.restype = None
self._fn_set_reserved = getattr(lib, f"kvarena_set_reserved_{self._sfx}")
self._fn_set_reserved.argtypes = [ctypes.c_size_t]
self._fn_set_reserved.restype = None
self._fn_set_align = getattr(lib, f"kvarena_set_align_{self._sfx}")
self._fn_set_align.argtypes = [ctypes.c_size_t]
self._fn_set_align.restype = None
self._fn_cursor = getattr(lib, f"kvarena_cursor_{self._sfx}")
self._fn_cursor.argtypes = []
self._fn_cursor.restype = ctypes.c_size_t
return lib
def commit_range(self, offset: int, want_bytes: int) -> None:
"""Back ``[base+offset, base+offset+want_bytes)`` (monotonic per offset).
``offset`` must be granularity-aligned (the bump allocator guarantees it).
Maps one full handle per extension -- GB300 rejects partial-handle maps."""
if self._closed:
raise RuntimeError("KvVmmArena.commit_range after close")
if offset % self.granularity != 0:
raise ValueError(
f"commit_range offset {offset} not granularity-aligned "
f"({self.granularity})"
)
want = self._align(int(want_bytes))
prev = self._committed_by_offset.get(offset, 0)
if want <= prev:
return
if offset + want > self.reserved:
raise RuntimeError(
f"commit_range [{offset}, {offset + want}) exceeds reservation "
f"{self.reserved}"
)
drv = _driver()
add = want - prev
addr = self.base + offset + prev
with torch.cuda.device(self.device_id):
handle = _check(drv.cuMemCreate(add, self._prop, 0), "cuMemCreate")
try:
_check(drv.cuMemMap(addr, add, 0, handle, 0), "cuMemMap")
_check(
drv.cuMemSetAccess(addr, add, [self._access], 1), "cuMemSetAccess"
)
except Exception:
# Roll back this failed extension; leave already-mapped ranges intact.
unmap = drv.cuMemUnmap(addr, add)
unmap = unmap[0] if isinstance(unmap, tuple) else unmap
rel = drv.cuMemRelease(handle)
rel = rel[0] if isinstance(rel, tuple) else rel
raise
self._ranges[addr] = (add, handle)
self._committed_by_offset[offset] = want
self._range_backed += add
@property
def backed_bytes(self) -> int:
"""Total physically-backed bytes (sum of scattered per-buffer ranges)."""
return self._range_backed
@property
def cursor_bytes(self) -> int:
return int(self._fn_cursor())
def close(self) -> None:
if self._closed:
return
self._closed = True
drv = _driver()
try:
torch.cuda.synchronize()
except Exception as e: # pragma: no cover
logger.warning("KvVmmArena.close synchronize failed: %s", e)
for addr, (size, handle) in self._ranges.items():
err = drv.cuMemUnmap(addr, size)
err = err[0] if isinstance(err, tuple) else err
if err != drv.CUresult.CUDA_SUCCESS:
logger.warning("cuMemUnmap range -> %s", err)
err = drv.cuMemRelease(handle)
err = err[0] if isinstance(err, tuple) else err
if err != drv.CUresult.CUDA_SUCCESS:
logger.warning("cuMemRelease range -> %s", err)
self._ranges.clear()
err = drv.cuMemAddressFree(self.base, self.reserved)
err = err[0] if isinstance(err, tuple) else err
if err != drv.CUresult.CUDA_SUCCESS:
logger.warning("cuMemAddressFree -> %s", err)
# torch's caching allocator hands the pluggable allocator whole large-pool segments
# (rounded up to >= ~20 MiB) per tensor, so reserve slack beyond the tight tensor sum.
# VA is free until committed, so this costs only address space, not GPU memory.
_PER_BUFFER_VA_SLACK = 32 << 20
class _BufferSpec:
"""Per-buffer placement + backing state inside the shared VA reservation."""
__slots__ = ("desc", "offset", "reserved_span", "aligned_reserved", "backed_to")
def __init__(
self,
desc: KvBufferDesc,
offset: int,
reserved_span: int,
aligned_reserved: int,
):
self.desc = desc
self.offset = offset # granularity-aligned arena offset of this buffer
self.reserved_span = reserved_span # logical (unaligned) tensor bytes
self.aligned_reserved = aligned_reserved # reserved span rounded to granularity
self.backed_to = 0 # bytes from offset currently backed
class KvVmmBufferOwner:
"""Owns one ``KvVmmArena`` plus its incrementally-backed KV buffers.
``buffer_descs`` is an ordered list of ``KvBufferDesc``; the created ``torch.empty``
tensors are exposed in the same order as ``self.tensors``.
"""
def __init__(
self,
*,
device: str,
device_id: int,
store_dtype: torch.dtype,
page_size: int,
reserved_num_tokens: int,
buffer_descs: Sequence[KvBufferDesc],
):
self.device = device
self.device_id = int(device_id)
self.store_dtype = store_dtype
self.page_size = int(page_size)
self._reserved_num_tokens = int(reserved_num_tokens)
self._final_num_tokens: Optional[int] = None
self._arena: Optional[KvVmmArena] = None
self._specs: List[_BufferSpec] = []
self.tensors: List[torch.Tensor] = []
itemsize = store_dtype.itemsize
with torch.cuda.device(self.device_id):
gran = query_granularity(self.device_id)
reserved_spans = [d.reserved_span_bytes(itemsize) for d in buffer_descs]
aligned = [align_up(s, gran) for s in reserved_spans]
reserve_bytes = sum(a + _PER_BUFFER_VA_SLACK for a in aligned) + gran
self._arena = KvVmmArena(self.device_id, reserve_bytes=reserve_bytes)
assert self._arena.granularity == gran, (self._arena.granularity, gran)
# NORMAL torch tensors through the arena MemPool; torch.empty never touches
# the unbacked tail.
with torch.cuda.use_mem_pool(self._arena.pool):
self.tensors = [
torch.empty(d.shape, dtype=store_dtype, device=self.device)
for d in buffer_descs
]
specs: List[_BufferSpec] = []
for desc, tensor, reserved_span, aligned_reserved in zip(
buffer_descs, self.tensors, reserved_spans, aligned
):
if prod(tensor.shape) * itemsize != reserved_span:
raise RuntimeError(
f"buffer {desc.name!r} tensor bytes "
f"{prod(tensor.shape) * itemsize} != reserved span {reserved_span}"
)
offset = tensor.data_ptr() - self._arena.base
if offset < 0 or offset % gran != 0:
raise RuntimeError(
f"buffer {desc.name!r} arena offset {offset} not "
f"granularity-aligned ({gran})"
)
if offset + aligned_reserved > self._arena.reserved:
raise RuntimeError(
f"buffer {desc.name!r} [{offset}, {offset + aligned_reserved}) "
f"exceeds reservation {self._arena.reserved}"
)
specs.append(_BufferSpec(desc, offset, reserved_span, aligned_reserved))
self._specs = specs
# Back one page so slot 0 is resident before capture: capture routes every
# dummy KV write to slot 0 (out_cache_loc is zeros). finalize() backs the rest.
self.ensure_prefix(self.page_size)
for t in self.tensors:
assert (
t.is_cuda and t.device.index == self.device_id
), f"post-capture KV buffer landed on {t.device}, expected cuda:{self.device_id}"
# -- backing --------------------------------------------------------------
@staticmethod
def _check_span(spec: _BufferSpec, span: int) -> int:
"""Return ``span`` if it fits ``[0, reserved_span]``; raise otherwise."""
span = int(span)
if not (0 <= span <= spec.reserved_span):
raise ValueError(
f"buffer {spec.desc.name!r}: span {span} outside "
f"[0, {spec.reserved_span}] (reserved tensor bytes)"
)
return span
def _back_spans(self, span_bytes: Sequence[int]) -> None:
"""Back each buffer to (at least) ``span_bytes[i]``. An out-of-reservation
span is a descriptor bug: raise before committing anything, never clamp."""
if self._arena is None:
raise RuntimeError("backing after close / before construction")
for spec, span in zip(self._specs, span_bytes):
self._check_span(spec, span)
gran = self._arena.granularity
for spec, span in zip(self._specs, span_bytes):
want = align_up(
int(span), gran
) # <= aligned_reserved since span <= reserved
if want > spec.backed_to:
self._arena.commit_range(spec.offset, want)
spec.backed_to = want
def ensure_prefix(self, num_tokens: int) -> None:
"""Ensure the first ``num_tokens`` slots of every buffer are physically backed."""
self._back_spans(
[s.desc.prefix_span_bytes(num_tokens, self.page_size) for s in self._specs]
)
def finalize(self, final_num_tokens: int) -> None:
"""Back each buffer's final advertised span; set the final serving capacity."""
final = int(final_num_tokens)
if not (self.page_size <= final <= self._reserved_num_tokens):
raise ValueError(
f"final_num_tokens={final} must satisfy page_size="
f"{self.page_size} <= final <= reserved={self._reserved_num_tokens}"
)
self._back_spans(
[s.desc.final_span_bytes(final, self.page_size) for s in self._specs]
)
self._final_num_tokens = final
# -- accessors / teardown -------------------------------------------------
@property
def backed_bytes(self) -> int:
return self._arena.backed_bytes if self._arena is not None else 0
def close(self) -> None:
self.tensors = []
self._specs = []
if self._arena is not None:
self._arena.close()
self._arena = None