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

374 lines
14 KiB
Python

"""
Copyright 2023-2026 SGLang Team
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
MambaCheckpointPool — the radix prefix cache's int8-compressed store for cached
linear-attention (KDA / GDN / Mamba2 gated-delta-rule) recurrent states.
It decouples the *cached* states (radix-owned, idle, compressed) from the *active*
``MambaPool`` (running requests, full precision, kernel-facing). The radix stores
one cached state per node HERE; on a prefix-cache hit it is dequantized back into
a fresh active slot (copy-on-write).
Per cached slot it holds:
* the SSM temporal state in **int8** (per-(head,k-channel) symmetric), via the
embedded ``Int8CheckpointStore`` — ~2x more cached states than bf16,
quality-safe (quantized once on store, dequantized once on a hit; never
re-enters the recurrence as a quant->dequant loop).
* the conv1d window state at its native dtype (tiny, W-1 tokens; not worth
quantizing).
Why int8 (not fp8): a cached checkpoint is loaded ONCE on a cache hit, then
decoding continues at full precision, so the only error is a single rounding of
S. The temporal state is roughly uniformly distributed, so int8-per-(head,
k-channel) beats fp8-e4m3 at the same 1 byte (fp8 wastes bits on the exponent).
The scale axis (reduces over d_v) matches the per-k-channel decay diag(alpha), so
the large state entries keep ~bf16 precision and the error concentrates on small
entries that barely affect the readout. Storing cached states int8 gives ~2x the
cached-prefix capacity at fixed memory, and composes with host-offload
(HiMambaRadixCache) which it also halves.
This is strategy-agnostic: whether the active slot to be cached was produced by
the ``no_buffer`` donate (copy_from) or the ``extra_buffer`` ping-pong track
buffer (spec path), both converge on "an active slot becomes the cached
``mamba_value``" — which is exactly the (store_from_active) hook here. Slot
lifecycle is owned by the caller via the embedded ``MambaSlotAllocator``.
"""
from __future__ import annotations
import logging
from typing import List, Optional
import torch
from sglang.srt.mem_cache.allocator.mamba import MambaSlotAllocator
logger = logging.getLogger(__name__)
class Int8CheckpointStore:
"""int8 store for cached multi-layer linear-attn states.
Tensors (slot index handed out by the caller's allocator):
qdata : [L, num_slots, H, d_v, d_k] int8 (the quantized state)
scale : [L, num_slots, H, 1, d_k] scale_dtype (per layer,slot,head,k-chan)
A "state" spans all L mamba layers for one cached point (matching how the
radix caches one full state per node). The reduction axis for the scale is
d_v (dim=-2), so each (head, k-channel) gets its own scale — aligned with the
per-k-channel decay diag(alpha).
``scale_dtype`` should match the source state's dtype (bf16 / fp16 / fp32) so
that quantize and dequantize use the identical scale — it is NOT required to
be bf16.
"""
QMAX = 127
def __init__(
self,
*,
num_layers: int,
num_slots: int,
num_heads: int,
head_v_dim: int,
head_k_dim: int,
device: str,
scale_dtype: torch.dtype = torch.bfloat16,
):
self.num_layers = num_layers
self.num_slots = num_slots
self.H = num_heads
self.d_v = head_v_dim
self.d_k = head_k_dim
self.device = device
self.qdata = torch.empty(
num_layers,
num_slots,
num_heads,
head_v_dim,
head_k_dim,
dtype=torch.int8,
device=device,
)
self.scale = torch.empty(
num_layers,
num_slots,
num_heads,
1,
head_k_dim,
dtype=scale_dtype,
device=device,
)
# ---- (de)quant math (also usable standalone for probes/tests) ----
@classmethod
def quantize(cls, state: torch.Tensor):
"""state [..., H, d_v, d_k] -> (qint8, scale[..., H, 1, d_k]).
amax / scale / round are computed in float32 so quantizing a low-precision
state doesn't lose precision in the intermediate (symmetric with
``dequantize``, which is already float32). The scale is rounded to the
state dtype (its storage precision) BEFORE the division, so quantize and
dequantize use the identical scale."""
state_fp32 = state.to(torch.float32)
amax = state_fp32.abs().amax(dim=-2, keepdim=True).clamp(min=1e-8)
scale = (amax / cls.QMAX).to(state.dtype)
q = (
torch.round(state_fp32 / scale.to(torch.float32))
.clamp(-cls.QMAX, cls.QMAX)
.to(torch.int8)
)
return q, scale
@staticmethod
def dequantize(q: torch.Tensor, scale: torch.Tensor, out_dtype: torch.dtype):
return (q.to(torch.float32) * scale.to(torch.float32)).to(out_dtype)
# ---- store / load (caller supplies slot indices) ----
def store(self, slots: torch.Tensor, state: torch.Tensor) -> None:
"""Quantize and write states. state: [L, N, H, d_v, d_k] for the N slots
(or [L, H, d_v, d_k] when slots is a scalar/len-1)."""
if state.dim() == 4:
state = state.unsqueeze(1)
q, scale = self.quantize(state)
self.qdata[:, slots] = q
self.scale[:, slots] = scale.to(self.scale.dtype)
def load(self, slots: torch.Tensor, out_dtype: torch.dtype) -> torch.Tensor:
"""Dequantize states at slots -> [L, N, H, d_v, d_k] in out_dtype."""
return self.dequantize(self.qdata[:, slots], self.scale[:, slots], out_dtype)
def copy_to_pool(
self,
dst_temporal: torch.Tensor,
src_slots: torch.Tensor,
dst_slots: torch.Tensor,
) -> None:
"""Dequantize checkpoints at ``src_slots`` directly into the active pool
tensor ``dst_temporal`` [L, pool_slots, H, d_v, d_k] at ``dst_slots`` (the
copy-on-write on a cache hit). Output dtype follows ``dst_temporal``."""
dst_temporal[:, dst_slots] = self.load(src_slots, dst_temporal.dtype)
def store_from_pool(
self,
src_temporal: torch.Tensor,
src_slots: torch.Tensor,
dst_slots: torch.Tensor,
) -> None:
"""Quantize states from an active pool tensor into checkpoint slots (cache
store / donate)."""
self.store(dst_slots, src_temporal[:, src_slots])
def mem_usage_bytes(self) -> int:
return (
self.qdata.numel() * self.qdata.element_size()
+ self.scale.numel() * self.scale.element_size()
)
def bytes_per_slot(self) -> int:
return self.mem_usage_bytes() // max(1, self.num_slots)
class MambaCheckpointPool:
def __init__(
self,
*,
num_layers: int,
num_slots: int,
num_heads: int,
head_v_dim: int,
head_k_dim: int,
conv_shapes: List[tuple],
conv_dtype: torch.dtype,
device: str,
temporal_dtype: Optional[torch.dtype] = None,
):
self.num_slots = num_slots
self.device = device
self.temporal = Int8CheckpointStore(
num_layers=num_layers,
num_slots=num_slots + 1, # slot 0 reserved (matches MambaSlotAllocator)
num_heads=num_heads,
head_v_dim=head_v_dim,
head_k_dim=head_k_dim,
device=device,
# store the scale in the temporal state's own dtype so quantize and
# dequantize use the identical scale (not hard-coded to bf16)
scale_dtype=(
temporal_dtype if temporal_dtype is not None else torch.bfloat16
),
)
# conv windows stay at their native dtype (small); one buffer per conv
# tensor in the State
self.conv = [
torch.empty(
(num_layers, num_slots + 1) + tuple(shape),
dtype=conv_dtype,
device=device,
)
for shape in conv_shapes
]
self.allocator = MambaSlotAllocator(size=num_slots, device=device)
# ---- lifecycle (delegates to the embedded allocator) ----
def alloc(self, n: int = 1):
return self.allocator.alloc(n)
def free(self, slots: torch.Tensor):
self.allocator.free(slots)
def available_size(self) -> int:
return self.allocator.available_size()
def clear(self) -> None:
"""Release every checkpoint slot (radix flush/reset). The int8 qdata is
left as-is; slots are reused/overwritten on the next store."""
self.allocator.clear()
# ---- state transfer between the active MambaPool and this store ----
def store_from_active(self, active_mamba_pool, active_slots, ckpt_slots) -> None:
"""Quantize temporal + copy conv from the active pool into checkpoint slots
(the radix donate / cache-store)."""
cache = active_mamba_pool.mamba_cache
self.temporal.store_from_pool(cache.temporal, active_slots, ckpt_slots)
for i, c in enumerate(self.conv):
c[:, ckpt_slots] = cache.conv[i][:, active_slots]
def load_to_active(self, active_mamba_pool, ckpt_slots, active_slots) -> None:
"""Dequantize temporal + copy conv from checkpoint slots into the active pool
(the cache-hit copy-on-write)."""
cache = active_mamba_pool.mamba_cache
self.temporal.copy_to_pool(cache.temporal, ckpt_slots, active_slots)
for i, c in enumerate(self.conv):
cache.conv[i][:, active_slots] = c[:, ckpt_slots].to(cache.conv[i].dtype)
@staticmethod
def estimate_mem_usage_bytes(
*,
num_layers: int,
num_slots: int,
num_heads: int,
head_v_dim: int,
head_k_dim: int,
conv_shapes: List[tuple],
conv_dtype: torch.dtype,
temporal_dtype: torch.dtype,
) -> dict:
"""Estimate the pool's HBM footprint (bytes) WITHOUT allocating, so a
caller can check it against free memory before construction. Mirrors the
real layout: int8 qdata + per-(head,k) scale + bf16 conv windows, including
the reserved slot 0."""
slots = num_slots + 1 # slot 0 reserved (matches MambaSlotAllocator)
scale_isz = torch.empty((), dtype=temporal_dtype).element_size()
conv_isz = torch.empty((), dtype=conv_dtype).element_size()
qdata = num_layers * slots * num_heads * head_v_dim * head_k_dim # int8 = 1B
scale = num_layers * slots * num_heads * head_k_dim * scale_isz
conv = 0
for shape in conv_shapes:
n = 1
for s in shape:
n *= int(s)
conv += num_layers * slots * n * conv_isz
return {
"qdata": qdata,
"scale": scale,
"conv": conv,
"total": qdata + scale + conv,
}
def mem_usage_bytes(self) -> int:
conv_bytes = sum(c.numel() * c.element_size() for c in self.conv)
return self.temporal.mem_usage_bytes() + conv_bytes
def maybe_init_int8_mamba_checkpoint_pool(
*,
mamba_size: int,
cache_params,
mamba_layer_ids: List[int],
device: str,
) -> Optional[MambaCheckpointPool]:
"""Build the optional int8 ``MambaCheckpointPool`` when
``--enable-int8-mamba-checkpoint`` is set (and a global server-args context
exists), else return ``None``. The radix caches states here (int8) instead of
in the active bf16 pool -> ~2x cached-prefix capacity at fixed memory.
Estimates the pool's HBM footprint and checks it against free memory BEFORE
allocating, so an oversized ``--int8-mamba-ckpt-size`` fails with an actionable
message instead of a cryptic mid-allocation CUDA OOM.
"""
from sglang.srt.runtime_context import get_server_args
try:
_sa = get_server_args()
except ValueError:
# Some unit-test / mock runners construct HybridReqToTokenPool directly
# without a global server-args context. The int8 checkpoint pool is opt-in
# via a CLI flag, so an unset context unambiguously means it is off.
_sa = None
if not getattr(_sa, "enable_int8_mamba_checkpoint", False):
return None
GB = 1 << 30
H, d_v, d_k = cache_params.shape.temporal
ckpt_size = _sa.int8_mamba_ckpt_size or (2 * mamba_size)
kwargs = dict(
num_layers=len(mamba_layer_ids),
num_slots=ckpt_size,
num_heads=H,
head_v_dim=d_v,
head_k_dim=d_k,
conv_shapes=list(cache_params.shape.conv),
conv_dtype=cache_params.dtype.conv,
temporal_dtype=cache_params.dtype.temporal,
)
est = MambaCheckpointPool.estimate_mem_usage_bytes(**kwargs)
free_bytes = None
if isinstance(device, str) and device.startswith("cuda"):
try:
free_bytes, _ = torch.cuda.mem_get_info(device)
except Exception:
free_bytes = None
logger.info(
f"int8 mamba checkpoint pool: {ckpt_size} slots, "
f"{est['total'] / GB:.2f}GB (qdata {est['qdata'] / GB:.2f} + scale "
f"{est['scale'] / GB:.2f} + conv {est['conv'] / GB:.2f}); active mamba "
f"pool {mamba_size} slots"
+ (f"; free HBM {free_bytes / GB:.2f}GB" if free_bytes is not None else "")
)
if free_bytes is not None and est["total"] >= free_bytes:
raise RuntimeError(
f"int8 mamba checkpoint pool needs ~{est['total'] / GB:.2f}GB but only "
f"{free_bytes / GB:.2f}GB HBM is free. Lower --int8-mamba-ckpt-size "
f"(currently {ckpt_size}) or --mem-fraction-static."
)
pool = MambaCheckpointPool(device=device, **kwargs)
# NOTE: this pool's HBM is NOT subtracted from the KV-cache budget
# (max_total_num_tokens); it is allocated from --mem-fraction-static headroom.
# The estimate check above guards against an oversized pool; accounting it in
# the KV budget is a follow-up.
logger.warning(
f"int8 mamba checkpoint pool ({est['total'] / GB:.2f}GB) is allocated from "
f"--mem-fraction-static headroom and is not reflected in "
f"max_total_num_tokens; ensure headroom covers it."
)
return pool