chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
@@ -0,0 +1,15 @@
"""Token-to-KV-slot allocators. One file per allocation strategy."""
from sglang.srt.mem_cache.allocator.base import BaseTokenToKVPoolAllocator
from sglang.srt.mem_cache.allocator.paged import (
PagedTokenToKVPoolAllocator,
alloc_extend_naive,
)
from sglang.srt.mem_cache.allocator.token import TokenToKVPoolAllocator
__all__ = [
"BaseTokenToKVPoolAllocator",
"PagedTokenToKVPoolAllocator",
"TokenToKVPoolAllocator",
"alloc_extend_naive",
]
@@ -0,0 +1,116 @@
"""
Copyright 2025 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.
"""
from __future__ import annotations
import abc
from typing import TYPE_CHECKING
import torch
if TYPE_CHECKING:
from sglang.srt.mem_cache.memory_pool import KVCache
class BaseTokenToKVPoolAllocator(abc.ABC):
@abc.abstractmethod
def __init__(
self,
size: int,
page_size: int,
dtype: torch.dtype,
device: str,
kvcache: KVCache,
need_sort: bool,
):
self.size = size
self.page_size = page_size
self.dtype = dtype
self.device = device
self._kvcache = kvcache
self.need_sort = need_sort
self.free_pages = None
self.release_pages = None
self.is_not_in_free_group = True
self.free_group = []
@property
def size_full(self):
return self.size
def debug_print(self) -> str:
return ""
def available_size(self):
return (len(self.free_pages) + len(self.release_pages)) * self.page_size
def get_kvcache(self):
return self._kvcache
def restore_state(self, state):
self.free_pages, self.release_pages = state
def backup_state(self):
return (self.free_pages, self.release_pages)
def free_group_begin(self):
self.is_not_in_free_group = False
self.free_group = []
def free_group_end(self):
self.is_not_in_free_group = True
if self.free_group:
self.free(torch.cat(self.free_group))
def merge_and_sort_free(self):
if len(self.release_pages) > 0:
self.free_pages = torch.cat((self.free_pages, self.release_pages))
self.free_pages, _ = torch.sort(self.free_pages)
self.release_pages = torch.empty(
(0,), dtype=self.release_pages.dtype, device=self.device
)
def get_cpu_copy(self, indices, mamba_indices=None):
# FIXME: reuse the get_cpu_copy after paged allocator is implemented
raise NotImplementedError()
def load_cpu_copy(self, kv_cache_cpu, indices, mamba_indices=None):
# FIXME: reuse the load_cpu_copy after paged allocator is implemented
raise NotImplementedError()
def alloc_extend(self, *args, **kwargs):
raise NotImplementedError("alloc_extend is only for paged allocator")
def alloc_decode(self, *args, **kwargs):
raise NotImplementedError("alloc_decode is only for paged allocator")
def resize(self, config) -> None:
self.size = config.max_total_num_tokens
if self.page_size > 1:
self.num_pages = config.max_total_num_tokens // self.page_size
self.clear()
@abc.abstractmethod
def clear(self):
raise NotImplementedError()
@abc.abstractmethod
def alloc(self, need_size: int):
raise NotImplementedError()
@abc.abstractmethod
def free(self, free_index: torch.Tensor):
raise NotImplementedError()
@@ -0,0 +1,588 @@
import weakref
import torch
from sglang.srt.mem_cache.allocator.base import BaseTokenToKVPoolAllocator
from sglang.srt.mem_cache.allocator.paged import PagedTokenToKVPoolAllocator
from sglang.srt.mem_cache.deepseek_v4_memory_pool import (
DeepSeekV4TokenToKVPool,
HiSparseC4DevicePool,
)
from sglang.srt.mem_cache.hisparse_memory_pool import HiSparseDSATokenToKVPool
from sglang.srt.utils.common import get_num_new_pages
class HiSparseTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
def __init__(
self,
size: int,
page_size: int,
dtype: torch.dtype,
device: torch.device,
kvcache: HiSparseDSATokenToKVPool,
need_sort: bool,
host_to_device_ratio: int = 2,
):
self._kvcache = kvcache
self._size_full = size * host_to_device_ratio
self._size_hisparse = size
self.compress_ratio = 1
self.dtype = dtype
self.device = device
self.page_size = page_size
self.need_sort = need_sort
self.logical_attn_allocator = PagedTokenToKVPoolAllocator(
self._size_full,
self.page_size,
self.dtype,
self.device,
kvcache,
need_sort,
)
self.hisparse_attn_allocator = PagedTokenToKVPoolAllocator(
self._size_hisparse,
self.page_size,
self.dtype,
self.device,
kvcache,
need_sort,
)
self.full_to_hisparse_device_index_mapping = torch.cat(
[
torch.zeros(
self._size_full + self.page_size,
dtype=torch.int64,
device=self.device,
),
torch.tensor([-1], dtype=torch.int64, device=self.device),
]
)
self.free_pages = None
self.release_pages = None
self.is_not_in_free_group = True
self.free_group = []
self.clear()
self._kvcache.register_mapping(
weakref.proxy(self.full_to_hisparse_device_index_mapping)
)
@property
def size_full(self) -> int:
return self._size_full
@property
def size(self) -> int:
return self._size_full
def available_size(self) -> int:
return min(
self.logical_attn_allocator.available_size(),
self.hisparse_attn_allocator.available_size(),
)
def get_kvcache(self):
return self._kvcache
def alloc(self, need_size: int):
if self.page_size != 1:
raise NotImplementedError(
"HiSparse generic allocation is only supported for page_size=1. "
"Use alloc_extend for paged allocation."
)
logical_indices = self.logical_attn_allocator.alloc(need_size)
if logical_indices is None:
return None
hisparse_indices = self.hisparse_attn_allocator.alloc(need_size)
if hisparse_indices is None:
self.logical_attn_allocator.free(logical_indices)
return None
self.full_to_hisparse_device_index_mapping[logical_indices] = hisparse_indices
return logical_indices
def alloc_logical_only(
self,
prefix_lens: torch.Tensor,
prefix_lens_cpu: torch.Tensor,
seq_lens: torch.Tensor,
seq_lens_cpu: torch.Tensor,
last_loc: torch.Tensor,
extend_num_tokens: int,
):
"""Allocate only logical indices without hisparse device indices.
Used in the direct-to-host transfer path where KV data is written
directly to host memory by the prefill node, skipping GPU staging.
"""
return self.logical_attn_allocator.alloc_extend(
prefix_lens,
prefix_lens_cpu,
seq_lens,
seq_lens_cpu,
last_loc,
extend_num_tokens,
)
def alloc_device_buffer(self, allocated_indices, need_size: int):
assert need_size % self.page_size == 0
# clear original reference and isolate the buffer from outside addressing, allocate new buffer if needed
hisparse_indices = self.full_to_hisparse_device_index_mapping[allocated_indices]
self.full_to_hisparse_device_index_mapping[allocated_indices] = 0
# Filter valid (non-zero) hisparse indices.
# In the direct-to-host path, mapping is all zeros since no hisparse
# device indices were pre-allocated.
hisparse_indices = hisparse_indices[hisparse_indices > 0]
if len(hisparse_indices) >= need_size:
buffer_indices = hisparse_indices[:need_size]
self.free_hisparse_indices(hisparse_indices[need_size:])
else:
# page alignment, claiming the residual space for an incomplete page
page_residual_length = len(hisparse_indices) % self.page_size
if page_residual_length != 0:
hisparse_indices = torch.cat(
[
hisparse_indices,
torch.arange(
hisparse_indices[-1] + 1,
hisparse_indices[-1]
+ self.page_size
- page_residual_length
+ 1,
device=self.device,
),
]
)
extra_indices = self.hisparse_attn_allocator.alloc(
need_size - len(hisparse_indices)
)
assert (
extra_indices is not None
), "Hisparse allocation failed in alloc_device_buffer"
buffer_indices = torch.cat([hisparse_indices, extra_indices])
return buffer_indices
def free_hisparse_indices(self, buffer_indices: torch.Tensor):
# disable free group mechanism for device buffer free
self.hisparse_attn_allocator.is_not_in_free_group = True
self.hisparse_attn_allocator.free(buffer_indices[buffer_indices > 0])
def get_last_loc_compressed(self, last_locs: torch.Tensor):
return last_locs
def get_last_loc_hisparse_device(self, last_locs: torch.Tensor):
return self._kvcache._translate_loc_to_hisparse_device(last_locs)
def alloc_extend(
self,
prefix_lens: torch.Tensor,
prefix_lens_cpu: torch.Tensor,
seq_lens: torch.Tensor,
seq_lens_cpu: torch.Tensor,
last_loc: torch.Tensor, # last_loc for full layers
extend_num_tokens: int,
):
num_new_pages = get_num_new_pages(
seq_lens=seq_lens_cpu, page_size=self.page_size, prefix_lens=prefix_lens_cpu
)
if (
num_new_pages
> self.logical_attn_allocator.available_size() // self.page_size
):
return None
if (
num_new_pages
> self.hisparse_attn_allocator.available_size() // self.page_size
):
return None
logical_indices = self.logical_attn_allocator.alloc_extend(
prefix_lens,
prefix_lens_cpu,
seq_lens,
seq_lens_cpu,
last_loc,
extend_num_tokens,
)
assert logical_indices is not None, "Logical allocation failed in alloc_extend"
hisparse_last_loc = self.get_last_loc_hisparse_device(last_loc)
hisparse_indices = self.hisparse_attn_allocator.alloc_extend(
prefix_lens,
prefix_lens_cpu,
seq_lens,
seq_lens_cpu,
hisparse_last_loc,
len(logical_indices),
num_new_pages=num_new_pages,
)
assert (
hisparse_indices is not None
), "Hisparse allocation failed in alloc_extend"
self.full_to_hisparse_device_index_mapping[logical_indices] = hisparse_indices
return logical_indices
def alloc_decode(
self,
seq_lens: torch.Tensor,
seq_lens_cpu: torch.Tensor,
last_loc: torch.Tensor, # last_loc for full layers
):
return self.logical_attn_allocator.alloc_decode(
seq_lens, seq_lens_cpu, last_loc
)
def free_hisparse(self, free_indices: torch.Tensor):
hisparse_indices = self._kvcache._translate_loc_to_hisparse_device(free_indices)
hisparse_indices = hisparse_indices[hisparse_indices > 0]
self.free_hisparse_indices(hisparse_indices)
self.full_to_hisparse_device_index_mapping[free_indices] = 0
def clear(self):
self.logical_attn_allocator.clear()
self.hisparse_attn_allocator.clear()
# Note: the last item is -1, we don't clear it, see the comment in __init__
self.full_to_hisparse_device_index_mapping[:-1].fill_(0)
self.is_not_in_free_group = True
self.free_group = []
def free_group_begin(self):
return
def free_group_end(self):
return
def free(self, free_index: torch.Tensor):
if free_index.numel() == 0:
return
if self.is_not_in_free_group:
self.logical_attn_allocator.free(free_index)
self.free_hisparse(free_index)
else:
self.free_group.append(free_index)
assert (
self.logical_attn_allocator.available_size()
<= self.logical_attn_allocator.size
)
assert (
self.hisparse_attn_allocator.available_size()
<= self.hisparse_attn_allocator.size
)
class DeepSeekV4HiSparseTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
def __init__(
self,
logical_attn_allocator: BaseTokenToKVPoolAllocator,
):
assert isinstance(logical_attn_allocator._kvcache, DeepSeekV4TokenToKVPool)
assert isinstance(
logical_attn_allocator._kvcache.c4_kv_pool, HiSparseC4DevicePool
)
self.compress_ratio = 4
self.hisparse_kvcache = logical_attn_allocator._kvcache.c4_kv_pool
self._size_full = logical_attn_allocator.size_full
self._size_hisparse = self.hisparse_kvcache.size
self.dtype = self.hisparse_kvcache.dtype
self.device = self.hisparse_kvcache.device
# Keep the public page_size as the logical DSV4 full/SWA page size.
# C4 HiSparse allocation/device-buffer code must use the compressed page size.
self.page_size = logical_attn_allocator.page_size
self.hisparse_page_size = self.hisparse_kvcache.page_size
self.logical_attn_allocator = logical_attn_allocator
self._kvcache = logical_attn_allocator._kvcache
self.hisparse_attn_allocator = PagedTokenToKVPoolAllocator(
self._size_hisparse,
self.hisparse_page_size,
self.dtype,
self.device,
self.hisparse_kvcache,
logical_attn_allocator.need_sort,
)
self.full_to_hisparse_device_index_mapping = torch.cat(
[
torch.zeros(
self._kvcache.c4_logical_size + self.hisparse_page_size,
dtype=torch.int64,
device=self.device,
),
torch.tensor([-1], dtype=torch.int64, device=self.device),
]
)
self.need_sort = logical_attn_allocator.need_sort
self.free_pages = None
self.release_pages = None
self.is_not_in_free_group = True
self.free_group = []
self.clear()
self.hisparse_kvcache.register_mapping(
weakref.proxy(self.full_to_hisparse_device_index_mapping)
)
@property
def size_full(self) -> int:
return self._size_full
@property
def size(self) -> int:
return self.logical_attn_allocator.size
@property
def size_swa(self) -> int:
return self.logical_attn_allocator.size_swa
@property
def full_to_swa_index_mapping(self):
return self.logical_attn_allocator.full_to_swa_index_mapping
def debug_print(self) -> str:
msg = self.logical_attn_allocator.debug_print()
msg += (
f"#hisparse-available-size: "
f"{self.hisparse_attn_allocator.available_size()}, "
)
return msg
def get_kvcache(self):
return self._kvcache
def translate_loc_from_full_to_swa(self, kv_indices: torch.Tensor):
return self.logical_attn_allocator.translate_loc_from_full_to_swa(kv_indices)
def full_available_size(self):
return min(
self.logical_attn_allocator.full_available_size(),
self.hisparse_attn_allocator.available_size() * self.compress_ratio,
)
def swa_available_size(self):
return self.logical_attn_allocator.swa_available_size()
def free_swa(self, free_indices: torch.Tensor):
self.logical_attn_allocator.free_swa(free_indices)
def available_size(self) -> int:
return min(
self.logical_attn_allocator.available_size(),
self.hisparse_attn_allocator.available_size() * self.compress_ratio,
)
def alloc(self, need_size: int):
raise NotImplementedError(
"DeepSeek V4 HiSparse allocator does not support direct token allocation; "
"use alloc_extend or alloc_decode instead."
)
def alloc_logical_only(
self,
prefix_lens: torch.Tensor,
prefix_lens_cpu: torch.Tensor,
seq_lens: torch.Tensor,
seq_lens_cpu: torch.Tensor,
last_loc: torch.Tensor,
extend_num_tokens: int,
):
"""Allocate decode logical indices without allocating C4 hisparse device pages."""
return self.logical_attn_allocator.alloc_extend(
prefix_lens,
prefix_lens_cpu,
seq_lens,
seq_lens_cpu,
last_loc,
extend_num_tokens,
)
def alloc_extend_swa_tail(
self,
prefix_lens: torch.Tensor,
prefix_lens_cpu: torch.Tensor,
seq_lens: torch.Tensor,
seq_lens_cpu: torch.Tensor,
last_loc: torch.Tensor,
extend_num_tokens: int,
swa_tail_len: int,
):
return self.logical_attn_allocator.alloc_extend_swa_tail(
prefix_lens=prefix_lens,
prefix_lens_cpu=prefix_lens_cpu,
seq_lens=seq_lens,
seq_lens_cpu=seq_lens_cpu,
last_loc=last_loc,
extend_num_tokens=extend_num_tokens,
swa_tail_len=swa_tail_len,
)
def alloc_device_buffer(self, allocated_indices, need_size: int):
assert need_size % self.hisparse_page_size == 0
hisparse_indices = self.full_to_hisparse_device_index_mapping[allocated_indices]
self.full_to_hisparse_device_index_mapping[allocated_indices] = 0
hisparse_indices = hisparse_indices[hisparse_indices > 0]
device_buffer_size = need_size - self.hisparse_page_size
P = len(hisparse_indices)
if P > device_buffer_size + 1:
newest_src = hisparse_indices[P - 1].clone()
old_at_dbs = hisparse_indices[device_buffer_size].clone()
hisparse_indices[device_buffer_size] = newest_src
hisparse_indices[P - 1] = old_at_dbs
if len(hisparse_indices) >= need_size:
buffer_indices = hisparse_indices[:need_size]
surplus = hisparse_indices[need_size:]
if surplus.numel() > 0:
buffer_pages = torch.unique(buffer_indices // self.hisparse_page_size)
surplus_pages = torch.unique(surplus // self.hisparse_page_size)
pure_surplus = surplus_pages[~torch.isin(surplus_pages, buffer_pages)]
if pure_surplus.numel() > 0:
self.hisparse_attn_allocator.is_not_in_free_group = True
self.hisparse_attn_allocator.free(
pure_surplus * self.hisparse_page_size
)
else:
page_residual_length = len(hisparse_indices) % self.hisparse_page_size
if page_residual_length != 0:
hisparse_indices = torch.cat(
[
hisparse_indices,
torch.arange(
hisparse_indices[-1] + 1,
hisparse_indices[-1]
+ self.hisparse_page_size
- page_residual_length
+ 1,
device=self.device,
),
]
)
extra_indices = self.hisparse_attn_allocator.alloc(
need_size - len(hisparse_indices)
)
assert (
extra_indices is not None
), "Hisparse allocation failed in alloc_device_buffer"
buffer_indices = torch.cat([hisparse_indices, extra_indices])
return buffer_indices
def free_hisparse_indices(self, buffer_indices: torch.Tensor):
self.hisparse_attn_allocator.is_not_in_free_group = True
self.hisparse_attn_allocator.free(buffer_indices[buffer_indices > 0])
def get_last_loc_compressed(self, last_locs: torch.Tensor):
return (last_locs - 3) // self.compress_ratio
def get_last_loc_hisparse_device(self, last_locs: torch.Tensor):
return self.hisparse_kvcache._translate_loc_to_hisparse_device(
self.get_last_loc_compressed(last_locs)
)
def alloc_extend(
self,
prefix_lens: torch.Tensor,
prefix_lens_cpu: torch.Tensor,
seq_lens: torch.Tensor,
seq_lens_cpu: torch.Tensor,
last_loc: torch.Tensor,
extend_num_tokens: int,
):
assert self.page_size > 1
num_new_pages_logical = get_num_new_pages(
seq_lens=seq_lens_cpu, page_size=self.page_size, prefix_lens=prefix_lens_cpu
)
num_new_pages_hisparse = get_num_new_pages(
seq_lens=seq_lens_cpu // self.compress_ratio,
page_size=self.hisparse_page_size,
prefix_lens=prefix_lens_cpu // self.compress_ratio,
)
if (
num_new_pages_logical
> self.logical_attn_allocator.available_size() // self.page_size
):
return None
if (
num_new_pages_hisparse
> self.hisparse_attn_allocator.available_size() // self.hisparse_page_size
):
return None
logical_indices = self.logical_attn_allocator.alloc_extend(
prefix_lens,
prefix_lens_cpu,
seq_lens,
seq_lens_cpu,
last_loc,
extend_num_tokens,
)
assert logical_indices is not None, "Logical allocation failed in alloc_extend"
compressed_logical_indices = (
self.hisparse_kvcache.translate_loc_from_full_to_compressed(logical_indices)
)
hisparse_last_loc = self.get_last_loc_hisparse_device(last_loc)
hisparse_indices = self.hisparse_attn_allocator.alloc_extend(
prefix_lens // self.compress_ratio,
prefix_lens_cpu // self.compress_ratio,
seq_lens // self.compress_ratio,
seq_lens_cpu // self.compress_ratio,
hisparse_last_loc,
len(compressed_logical_indices),
)
assert (
hisparse_indices is not None
), "Hisparse allocation failed in alloc_extend"
self.full_to_hisparse_device_index_mapping[compressed_logical_indices] = (
hisparse_indices.to(torch.int64)
)
return logical_indices
def alloc_decode(
self,
seq_lens: torch.Tensor,
seq_lens_cpu: torch.Tensor,
last_loc: torch.Tensor,
):
return self.logical_attn_allocator.alloc_decode(
seq_lens, seq_lens_cpu, last_loc
)
def free_compressed(self, compressed_indices: torch.Tensor):
hisparse_indices = self.hisparse_kvcache.translate_loc_to_hisparse_device(
compressed_indices
)
hisparse_indices = hisparse_indices[hisparse_indices > 0]
self.free_hisparse_indices(hisparse_indices)
self.full_to_hisparse_device_index_mapping[compressed_indices] = 0
def free_hisparse(self, free_indices: torch.Tensor):
compressed_indices = (
self.hisparse_kvcache.translate_loc_from_full_to_compressed(free_indices)
)
self.free_compressed(compressed_indices)
def clear(self):
self.logical_attn_allocator.clear()
self.hisparse_attn_allocator.clear()
self.full_to_hisparse_device_index_mapping[:-1].fill_(0)
self.is_not_in_free_group = True
self.free_group = []
def free(self, free_index: torch.Tensor):
if free_index.numel() == 0:
return
if self.is_not_in_free_group:
self.logical_attn_allocator.free(free_index)
else:
self.free_group.append(free_index)
@@ -0,0 +1,97 @@
"""
Copyright 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.
Slot allocator for the Mamba state pool.
Mamba caches one whole state tensor per request, so the allocator hands out
fixed-size slots (1 per request) rather than paged token KV indices. The
underlying tensor storage lives in ``MambaPool``; this class owns only the
free-slot bookkeeping.
"""
from __future__ import annotations
from typing import Iterator, Optional
import torch
class MambaSlotAllocator:
"""Manages the free-list of Mamba pool slot indices.
Unlike ``BaseTokenToKVPoolAllocator`` which is designed for per-token KV
pages, Mamba slots are request-level (typically 1 slot per request).
We keep the interface minimal and do NOT inherit the KV base class.
"""
def __init__(self, size: int, device: str):
self.size = size
self.device = device
# Active preallocated batch for `alloc_group_begin` / `alloc_group_end`.
# When non-None, `alloc(1)` consumes the next slot from this iterator
# instead of calling `_do_alloc(1)` per request. Reset to None outside
# a group window so `alloc` falls through to the per-call path.
self._alloc_iter: Optional[Iterator] = None
self.clear()
def available_size(self) -> int:
return len(self.free_slots)
def schedulable_available_size(self) -> int:
"""Planner-facing free count. Identity to ``available_size`` for the
static pool (slot-count and byte-coordinated views coincide); the shared
``UnifiedMambaSlotAllocator`` overrides it with the byte-coordinated view.
Lets ``alloc_req_slots`` call it uniformly without a getattr fallback."""
return self.available_size()
def alloc_group_begin(self, num_reqs: int):
"""Pre-allocate a batch of slots for match_prefix to amortize overhead."""
self._alloc_iter = None
if num_reqs > 0:
result = self._do_alloc(num_reqs)
if result is not None:
self._alloc_iter = iter(result.split(1))
def alloc_group_end(self):
"""Return any unused pre-allocated slots from the current group."""
if self._alloc_iter is not None:
remaining = list(self._alloc_iter)
if remaining:
self.free(torch.cat(remaining))
self._alloc_iter = None
def alloc(self, need_size: int) -> Optional[torch.Tensor]:
if self._alloc_iter is not None and need_size == 1:
slot = next(self._alloc_iter, None)
if slot is not None:
return slot
return self._do_alloc(need_size)
def _do_alloc(self, need_size: int) -> Optional[torch.Tensor]:
if need_size > len(self.free_slots):
return None
select_index = self.free_slots[:need_size]
self.free_slots = self.free_slots[need_size:]
return select_index
def free(self, free_index: torch.Tensor):
if free_index.numel() == 0:
return
self.free_slots = torch.cat((self.free_slots, free_index))
def clear(self):
# Slot 0 is reserved as a dummy write target for padded tokens.
self.free_slots = torch.arange(
1, self.size + 1, dtype=torch.int64, device=self.device
)
+292
View File
@@ -0,0 +1,292 @@
"""
Copyright 2025 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.
"""
from __future__ import annotations
"""
Page-aligned memory pool.
"""
from typing import TYPE_CHECKING
import torch
from sglang.kernels.ops.memory.allocator import (
alloc_decode_kernel,
alloc_extend_kernel,
)
from sglang.srt.mem_cache.allocator.base import BaseTokenToKVPoolAllocator
from sglang.srt.utils import (
get_bool_env_var,
get_num_new_pages,
is_hip,
next_power_of_2,
)
_is_hip = is_hip()
if TYPE_CHECKING:
from sglang.srt.mem_cache.memory_pool import KVCache
def alloc_extend_naive(
prefix_lens,
seq_lens,
last_loc,
free_pages,
out_indices,
page_size,
device,
):
extend_lens = seq_lens - prefix_lens
end_pos = torch.cumsum(extend_lens, 0)
start_pos = end_pos - extend_lens
num_new_pages = (seq_lens + page_size - 1) // page_size - (
prefix_lens + page_size - 1
) // page_size
num_full_new_pages = (seq_lens) // page_size - (
prefix_lens + page_size - 1
) // page_size
need_page = num_new_pages - num_full_new_pages
end_new_pages = torch.cumsum(num_new_pages, 0)
start_new_pages = end_new_pages - num_new_pages
pos_in_page = torch.arange(page_size, device=device, dtype=torch.int32)
for i in range(len(prefix_lens)):
num1 = (
min(
seq_lens[i],
(prefix_lens[i] + page_size - 1) // page_size * page_size,
)
- prefix_lens[i]
)
if num1:
out_indices[start_pos[i] : start_pos[i] + num1] = (
last_loc[i] + 1 + pos_in_page[:num1].view(-1)
)
if prefix_lens[i] + num1 == seq_lens[i]:
continue
num2 = (
seq_lens[i] // page_size - (prefix_lens[i] + page_size - 1) // page_size
) * page_size
if num2:
pages = (
free_pages[start_new_pages[i] : end_new_pages[i] - need_page[i]]
* page_size
)
out_indices[start_pos[i] + num1 : start_pos[i] + num1 + num2] = (
pages.view(-1, 1) + pos_in_page.view(1, -1)
).view(-1)
if prefix_lens[i] + num1 + num2 == seq_lens[i]:
continue
num3 = seq_lens[i] - seq_lens[i] // page_size * page_size
if num3:
out_indices[end_pos[i] - num3 : end_pos[i]] = (
free_pages[end_new_pages[i] - 1] * page_size + pos_in_page[:num3]
).view(-1)
class PagedTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
"""
An allocator managing the indices to kv cache data.
This class has the same interface as `TokenToKVPoolAllocator` but the output
of one request is always page-aligned.
TODO: fuse last_loc into the kernel.
"""
def __init__(
self,
size: int,
page_size: int,
dtype: torch.dtype,
device: str,
kvcache: KVCache,
need_sort: bool,
):
super().__init__(size, page_size, dtype, device, kvcache, need_sort)
self.num_pages = size // page_size
self.debug_mode = get_bool_env_var("SGLANG_DEBUG_MEMORY_POOL")
# Pre-warm the torch.unique HIP kernel used in free(). When a request
# finishes with a prompt that already exists in the radix tree (e.g.
# bench_serving sending the same warmup+measured prompt), the radix
# cache's _insert_helper frees the duplicate KV indices via
# token_to_kv_pool_allocator.free(value[start:prefix_len]). That call
# path runs `torch.unique(free_index // self.page_size)` on a
# ~prompt_len-sized int64 tensor. The first such call on AMD ROCm
# JIT-compiles rocPRIM sort/unique kernels and costs ~200ms, which
# shows up as a mysterious "second-request slow" (Run 1) for
# repeated-prompt benchmarks. Running it once at init time moves
# that JIT cost to startup. This is a ROCm-only JIT cost, so the
# warm-up is gated on _is_hip and skipped on other platforms.
if _is_hip and torch.cuda.is_available():
try:
_warmup = torch.arange(1024, dtype=torch.int64, device=device)
_ = torch.unique(_warmup // page_size)
torch.cuda.synchronize()
except Exception:
pass
self.clear()
def alloc(self, need_size: int):
# page-aligned allocation, returning contiguous indices of pages
if self.debug_mode:
assert (
need_size % self.page_size == 0
), "The allocation size should be page-aligned"
num_pages = need_size // self.page_size
if self.need_sort and num_pages > len(self.free_pages):
self.merge_and_sort_free()
if num_pages > len(self.free_pages):
return None
out_pages = self.free_pages[:num_pages]
self.free_pages = self.free_pages[num_pages:]
out_indices = (
out_pages[:, None] * self.page_size
+ torch.arange(self.page_size, device=self.device)
).reshape(-1)
return out_indices
def alloc_extend(
self,
prefix_lens: torch.Tensor,
prefix_lens_cpu: torch.Tensor,
seq_lens: torch.Tensor,
seq_lens_cpu: torch.Tensor,
last_loc: torch.Tensor,
extend_num_tokens: int,
num_new_pages: int = None,
):
if self.debug_mode:
assert torch.all(
(last_loc + 1) % self.page_size == prefix_lens % self.page_size
)
bs = len(prefix_lens)
if self.need_sort and extend_num_tokens // self.page_size + bs + 1 > len(
self.free_pages
):
self.merge_and_sort_free()
out_indices = torch.empty(
(extend_num_tokens,), dtype=torch.int64, device=self.device
)
alloc_extend_kernel[(bs,)](
prefix_lens,
seq_lens,
last_loc,
self.free_pages,
out_indices,
next_power_of_2(bs),
self.page_size,
)
if self.debug_mode:
assert len(torch.unique(out_indices)) == len(out_indices)
if num_new_pages is None:
num_new_pages = get_num_new_pages(
seq_lens=seq_lens_cpu,
page_size=self.page_size,
prefix_lens=prefix_lens_cpu,
)
if num_new_pages > len(self.free_pages):
return None
self.free_pages = self.free_pages[num_new_pages:]
return out_indices
def alloc_decode(
self,
seq_lens: torch.Tensor,
seq_lens_cpu: torch.Tensor,
last_loc: torch.Tensor,
):
if self.debug_mode:
assert torch.all(
(last_loc + 2) % self.page_size == seq_lens % self.page_size
)
bs = len(seq_lens)
if self.need_sort and bs > len(self.free_pages):
self.merge_and_sort_free()
out_indices = torch.empty((bs,), dtype=torch.int64, device=self.device)
alloc_decode_kernel[(bs,)](
seq_lens,
last_loc,
self.free_pages,
out_indices,
next_power_of_2(bs),
self.page_size,
)
if self.debug_mode:
assert len(torch.unique(out_indices)) == len(out_indices)
num_new_pages = get_num_new_pages(
seq_lens=seq_lens_cpu,
page_size=self.page_size,
decode=True,
)
if num_new_pages > len(self.free_pages):
return None
self.free_pages = self.free_pages[num_new_pages:]
return out_indices
def free(self, free_index: torch.Tensor):
if free_index.numel() == 0:
return
if self.is_not_in_free_group:
free_page_indices = torch.unique(free_index // self.page_size)
if self.need_sort:
self.release_pages = torch.cat((free_page_indices, self.release_pages))
else:
self.free_pages = torch.cat((free_page_indices, self.free_pages))
else:
self.free_group.append(free_index)
if self.debug_mode:
assert len(torch.unique(self.free_pages)) == len(self.free_pages)
def clear(self):
# The padded slot 0 is used for writing dummy outputs from padded tokens.
self.free_pages = torch.arange(
1, self.num_pages + 1, dtype=torch.int64, device=self.device
)
self.is_not_in_free_group = True
self.free_group = []
self.release_pages = torch.empty((0,), dtype=torch.int64, device=self.device)
def get_cpu_copy(self, indices, mamba_indices=None):
return self._kvcache.get_cpu_copy(indices, mamba_indices=mamba_indices)
def load_cpu_copy(self, kv_cache_cpu, indices, mamba_indices=None):
return self._kvcache.load_cpu_copy(
kv_cache_cpu, indices, mamba_indices=mamba_indices
)
@@ -0,0 +1,524 @@
import torch
from sglang.srt.mem_cache.allocator.base import BaseTokenToKVPoolAllocator
from sglang.srt.mem_cache.allocator.paged import PagedTokenToKVPoolAllocator
from sglang.srt.mem_cache.allocator.token import TokenToKVPoolAllocator
from sglang.srt.mem_cache.base_swa_memory_pool import BaseSWAKVPool
from sglang.srt.utils import is_npu
from sglang.srt.utils.common import get_num_new_pages
_is_npu = is_npu()
if _is_npu:
import torch_npu
from sglang.srt.hardware_backend.npu.allocator_npu import (
NPUPagedTokenToKVPoolAllocator,
)
class SWATokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
"""Allocator for SWA hybrid KV cache."""
def __init__(
self,
size: int,
size_swa: int,
page_size: int,
dtype: torch.dtype,
device: str,
kvcache: BaseSWAKVPool,
need_sort: bool,
):
assert isinstance(kvcache, BaseSWAKVPool)
self._size_full = size
self._size_swa = size_swa
self.dtype = dtype
self.device = device
self.page_size = page_size
full_kv_pool = getattr(kvcache, "full_kv_pool", None)
swa_kv_pool = getattr(kvcache, "swa_kv_pool", None)
if page_size == 1:
self.full_attn_allocator = TokenToKVPoolAllocator(
size,
dtype,
device,
full_kv_pool,
need_sort,
)
self.swa_attn_allocator = TokenToKVPoolAllocator(
size_swa,
dtype,
device,
swa_kv_pool,
need_sort,
)
else:
if _is_npu:
PagedTokenToKVPoolAllocatorClass = NPUPagedTokenToKVPoolAllocator
else:
PagedTokenToKVPoolAllocatorClass = PagedTokenToKVPoolAllocator
self.full_attn_allocator = PagedTokenToKVPoolAllocatorClass(
size,
page_size,
dtype,
device,
full_kv_pool,
need_sort,
)
self.swa_attn_allocator = PagedTokenToKVPoolAllocatorClass(
size_swa,
page_size,
dtype,
device,
swa_kv_pool,
need_sort,
)
# Note: append one more item of value -1 in the end so -1 maps to -1.
# It is needed for the last_loc in alloc_extend, where the first full_last_loc
# is -1, and we need to map it to swa_last_loc -1 as well.
self.full_to_swa_index_mapping = torch.cat(
[
torch.zeros(
size + self.page_size,
dtype=torch.int64,
device=device,
),
torch.tensor([-1], dtype=torch.int64, device=device),
]
)
self.need_sort = need_sort
self.free_pages = None
self.release_pages = None
self.is_not_in_free_group = True
self.free_group = []
self._kvcache = kvcache
self.clear()
self._kvcache.register_mapping(self.full_to_swa_index_mapping)
def available_size(self):
return min(
self.full_attn_allocator.available_size(),
self.swa_attn_allocator.available_size(),
)
def full_available_size(self):
return self.full_attn_allocator.available_size()
def swa_available_size(self):
return self.swa_attn_allocator.available_size()
# Slot-conservation views for the leak invariant. On the non-shared allocator
# the static budget IS physical (conserve == physical); the shared composite
# overrides these with the static-cap view.
def _conserve_full_available_size(self):
return self.full_available_size()
def _conserve_swa_available_size(self):
return self.swa_available_size()
@property
def size(self):
return min(self._size_full, self._size_swa)
@property
def size_swa(self):
return self._size_swa
@property
def size_full(self):
return self._size_full
def debug_print(self) -> str:
msg = ""
msg += f"#swa-available-size: {self.swa_attn_allocator.available_size()}, "
msg += (
f"#full-attn-available-size: {self.full_attn_allocator.available_size()}, "
)
return msg
def get_kvcache(self):
return self._kvcache
def translate_loc_from_full_to_swa(self, kv_indices: torch.Tensor):
assert self._kvcache.full_to_swa_index_mapping is not None
return self._kvcache.translate_loc_from_full_to_swa(kv_indices)
def alloc(self, need_size: int):
assert self.page_size == 1
if need_size > self.full_attn_allocator.available_size():
return None
if need_size > self.swa_attn_allocator.available_size():
return None
alloc_full_indices = self.full_attn_allocator.alloc(need_size)
alloc_swa_indices = self.swa_attn_allocator.alloc(need_size)
assert alloc_full_indices is not None
assert alloc_swa_indices is not None
self.set_full_to_swa_mapping(alloc_full_indices, alloc_swa_indices)
return alloc_full_indices
def new_pages_available(self, num_full_pages: int, num_swa_pages: int) -> bool:
return (
num_full_pages
<= self.full_attn_allocator.available_size() // self.page_size
and num_swa_pages
<= self.swa_attn_allocator.available_size() // self.page_size
)
def alloc_extend(
self,
prefix_lens: torch.Tensor,
prefix_lens_cpu: torch.Tensor,
seq_lens: torch.Tensor,
seq_lens_cpu: torch.Tensor,
last_loc: torch.Tensor, # last_loc for full layers
extend_num_tokens: int,
):
assert self.page_size > 1
num_new_pages = get_num_new_pages(
seq_lens=seq_lens_cpu, page_size=self.page_size, prefix_lens=prefix_lens_cpu
)
if not self.new_pages_available(num_new_pages, num_new_pages):
return None
swa_last_loc = self.translate_loc_from_full_to_swa(last_loc)
alloc_full_indices = self.full_attn_allocator.alloc_extend(
prefix_lens,
prefix_lens_cpu,
seq_lens,
seq_lens_cpu,
last_loc,
extend_num_tokens,
num_new_pages=num_new_pages,
)
alloc_swa_indices = self.swa_attn_allocator.alloc_extend(
prefix_lens,
prefix_lens_cpu,
seq_lens,
seq_lens_cpu,
swa_last_loc,
extend_num_tokens,
num_new_pages=num_new_pages,
)
assert alloc_full_indices is not None
assert alloc_swa_indices is not None
self.set_full_to_swa_mapping(alloc_full_indices, alloc_swa_indices)
return alloc_full_indices
def alloc_extend_swa_tail(
self,
prefix_lens: torch.Tensor,
prefix_lens_cpu: torch.Tensor,
seq_lens: torch.Tensor,
seq_lens_cpu: torch.Tensor,
last_loc: torch.Tensor, # last_loc for full layers
extend_num_tokens: int,
swa_tail_len: int,
):
"""Allocate full KV for the whole extend and SWA KV only for the tail.
This is used by disaggregated decode preallocation: decode receives full
prompt KV for full-attention layers, but only the sliding-window state is
transferred for SWA layers.
"""
assert self.page_size > 1
assert len(seq_lens_cpu) == 1, "SWA tail allocation currently supports bs=1"
assert len(prefix_lens_cpu) == 1
assert 0 <= swa_tail_len <= extend_num_tokens
num_full_pages = get_num_new_pages(
seq_lens=seq_lens_cpu, page_size=self.page_size, prefix_lens=prefix_lens_cpu
)
num_swa_pages = (swa_tail_len + self.page_size - 1) // self.page_size
if not self.new_pages_available(num_full_pages, num_swa_pages):
return None
alloc_full_indices = self.full_attn_allocator.alloc_extend(
prefix_lens,
prefix_lens_cpu,
seq_lens,
seq_lens_cpu,
last_loc,
extend_num_tokens,
num_new_pages=num_full_pages,
)
assert alloc_full_indices is not None
if swa_tail_len == 0:
return alloc_full_indices
device = self.device
swa_prefix_lens = torch.zeros((1,), dtype=torch.int64, device=device)
swa_prefix_lens_cpu = torch.zeros((1,), dtype=torch.int64)
swa_seq_lens = torch.tensor([swa_tail_len], dtype=torch.int64, device=device)
swa_seq_lens_cpu = torch.tensor([swa_tail_len], dtype=torch.int64)
swa_last_loc = torch.tensor([-1], dtype=torch.int64, device=device)
alloc_swa_indices = self.swa_attn_allocator.alloc_extend(
swa_prefix_lens,
swa_prefix_lens_cpu,
swa_seq_lens,
swa_seq_lens_cpu,
swa_last_loc,
swa_tail_len,
num_new_pages=num_swa_pages,
)
assert alloc_swa_indices is not None
self.set_full_to_swa_mapping(
alloc_full_indices[-swa_tail_len:], alloc_swa_indices
)
if swa_tail_len < extend_num_tokens:
self.full_to_swa_index_mapping[
alloc_full_indices[:-swa_tail_len].to(torch.int64)
] = 0
return alloc_full_indices
def alloc_decode(
self,
seq_lens: torch.Tensor,
seq_lens_cpu: torch.Tensor,
last_loc: torch.Tensor, # last_loc for full layers
):
assert self.page_size > 1
swa_last_loc = self.translate_loc_from_full_to_swa(last_loc)
alloc_full_indices = self.full_attn_allocator.alloc_decode(
seq_lens, seq_lens_cpu, last_loc
)
alloc_swa_indices = self.swa_attn_allocator.alloc_decode(
seq_lens, seq_lens_cpu, swa_last_loc
)
if alloc_full_indices is None or alloc_swa_indices is None:
return None
if _is_npu:
indices_2d = alloc_full_indices.to(torch.int64).unsqueeze(-1)
torch_npu.npu_scatter_nd_update_(
self.full_to_swa_index_mapping,
indices_2d,
alloc_swa_indices.to(torch.int64),
)
else:
self.full_to_swa_index_mapping[alloc_full_indices] = alloc_swa_indices
return alloc_full_indices
def free(self, free_index: torch.Tensor):
if free_index.numel() == 0:
return
# NOTE: the API is not idempotent.
if self.is_not_in_free_group:
self.full_attn_allocator.free(free_index)
self.free_swa(free_index)
else:
self.free_group.append(free_index)
assert (
self.full_attn_allocator.available_size() <= self.full_attn_allocator.size
)
assert self.swa_attn_allocator.available_size() <= self.swa_attn_allocator.size
def set_full_to_swa_mapping(
self, full_indices: torch.Tensor, swa_indices: torch.Tensor
) -> None:
"""Write full_to_swa_index_mapping[full_indices[i]] = swa_indices[i].
Used by HiCache load-back path to rebuild the mapping after FULL and SWA device alloc.
"""
if full_indices.numel() == 0:
return
assert full_indices.numel() == swa_indices.numel()
full_indices = full_indices.to(torch.int64)
swa_indices = swa_indices.to(self.full_to_swa_index_mapping.dtype)
self.full_to_swa_index_mapping[full_indices] = swa_indices
def free_swa(self, free_index: torch.Tensor):
if free_index.numel() == 0:
return
if self.page_size == 1:
mapping_indices = free_index
else:
mapping_indices = self._expand_to_full_pages(free_index)
swa_indices = self.full_to_swa_index_mapping[mapping_indices]
swa_indices = swa_indices[swa_indices > 0]
self.swa_attn_allocator.free(swa_indices)
self.full_to_swa_index_mapping[mapping_indices] = 0
def _expand_to_full_pages(self, indices: torch.Tensor) -> torch.Tensor:
pages = torch.unique(indices // self.page_size)
page_offsets = torch.arange(
self.page_size, dtype=indices.dtype, device=indices.device
)
return (pages[:, None] * self.page_size + page_offsets[None, :]).reshape(-1)
def backup_state(self):
return [
self.full_attn_allocator.backup_state(),
self.swa_attn_allocator.backup_state(),
]
def restore_state(self, state):
assert len(state) == 2
self.full_attn_allocator.restore_state(state[0])
self.swa_attn_allocator.restore_state(state[1])
def resize(self, config) -> None:
size_full = int(config.full_max_total_num_tokens)
size_swa = int(config.swa_max_total_num_tokens)
self._size_full = size_full
self._size_swa = size_swa
for alloc, sz in (
(self.full_attn_allocator, size_full),
(self.swa_attn_allocator, size_swa),
):
alloc.size = int(sz)
if self.page_size > 1:
alloc.num_pages = int(sz) // self.page_size
self.clear()
def clear(self):
self.swa_attn_allocator.clear()
self.full_attn_allocator.clear()
# Note: the last item is -1, we don't clear it, see the comment in __init__
self.full_to_swa_index_mapping[:-1].fill_(0)
self.is_not_in_free_group = True
self.free_group = []
def get_cpu_copy(self, indices, mamba_indices=None):
return self._kvcache.get_cpu_copy(indices, mamba_indices=mamba_indices)
def load_cpu_copy(self, kv_cache_cpu, indices, mamba_indices=None):
return self._kvcache.load_cpu_copy(
kv_cache_cpu, indices, mamba_indices=mamba_indices
)
class PureSWATokenToKVPoolAllocator(SWATokenToKVPoolAllocator):
"""Single-pool allocator for models whose every layer is sliding-window attention."""
def __init__(
self,
size_swa: int,
page_size: int,
dtype: torch.dtype,
device: str,
kvcache: BaseSWAKVPool,
need_sort: bool,
):
assert page_size == 1
assert isinstance(kvcache, BaseSWAKVPool)
self.page_size = page_size
self.dtype = dtype
self.device = device
self.need_sort = need_sort
self._size_full = self._size_swa = size_swa
self.swa_attn_allocator = TokenToKVPoolAllocator(
size_swa,
dtype,
device,
kvcache.swa_kv_pool,
need_sort,
)
self.full_attn_allocator = self.swa_attn_allocator
self.full_to_swa_index_mapping = torch.cat(
[
torch.arange(size_swa + page_size, dtype=torch.int64, device=device),
torch.tensor([-1], dtype=torch.int64, device=device),
]
)
self.free_pages = None
self.release_pages = None
self.is_not_in_free_group = True
self.free_group = []
self._kvcache = kvcache
self.swa_attn_allocator.clear()
self._kvcache.register_mapping(self.full_to_swa_index_mapping)
def available_size(self):
return self.swa_attn_allocator.available_size()
def full_available_size(self):
return self.swa_attn_allocator.available_size()
def swa_available_size(self):
return self.swa_attn_allocator.available_size()
def new_pages_available(self, num_full_pages: int, num_swa_pages: int) -> bool:
avail = self.swa_attn_allocator.available_size() // self.page_size
return num_full_pages <= avail and num_swa_pages <= avail
def translate_loc_from_full_to_swa(self, kv_indices: torch.Tensor):
return kv_indices
def alloc(self, need_size: int):
assert self.page_size == 1
return self.swa_attn_allocator.alloc(need_size)
def alloc_extend(self, *args, **kwargs):
raise NotImplementedError(
"PureSWATokenToKVPoolAllocator does not support page_size > 1."
)
def alloc_decode(self, *args, **kwargs):
raise NotImplementedError(
"PureSWATokenToKVPoolAllocator does not support page_size > 1."
)
def alloc_extend_swa_tail(self, *args, **kwargs):
raise NotImplementedError(
"PureSWATokenToKVPoolAllocator does not support page_size > 1."
)
def free(self, free_index: torch.Tensor):
if free_index.numel() == 0:
return
if self.is_not_in_free_group:
self.swa_attn_allocator.free(free_index[free_index > 0])
else:
self.free_group.append(free_index)
assert self.swa_attn_allocator.available_size() <= self.swa_attn_allocator.size
def free_swa(self, free_index: torch.Tensor):
if free_index.numel() == 0:
return
self.swa_attn_allocator.free(free_index[free_index > 0])
def free_group_begin(self):
self.is_not_in_free_group = False
self.free_group = []
def free_group_end(self):
self.is_not_in_free_group = True
if self.free_group:
self.free(torch.cat(self.free_group))
self.free_group = []
def backup_state(self):
return self.swa_attn_allocator.backup_state()
def restore_state(self, state):
self.swa_attn_allocator.restore_state(state)
def clear(self):
self.swa_attn_allocator.clear()
self.is_not_in_free_group = True
self.free_group = []
@@ -0,0 +1,84 @@
"""
Copyright 2025 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.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from sglang.srt.mem_cache.allocator.base import BaseTokenToKVPoolAllocator
if TYPE_CHECKING:
from sglang.srt.mem_cache.memory_pool import KVCache
class TokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
"""An allocator managing the indices to kv cache data."""
def __init__(
self,
size: int,
dtype: torch.dtype,
device: str,
kvcache: KVCache,
need_sort: bool,
):
super().__init__(size, 1, dtype, device, kvcache, need_sort)
self.clear()
def clear(self):
# The padded slot 0 is used for writing dummy outputs from padded tokens.
self.free_pages = torch.arange(
1, self.size + 1, dtype=torch.int64, device=self.device
)
self.is_not_in_free_group = True
self.free_group = []
self.release_pages = torch.empty((0,), dtype=torch.int64, device=self.device)
def available_size(self):
# To avoid minor "len(free_pages) * 1" overhead
return len(self.free_pages) + len(self.release_pages)
def alloc(self, need_size: int):
if self.need_sort and need_size > len(self.free_pages):
self.merge_and_sort_free()
if need_size > len(self.free_pages):
return None
select_index = self.free_pages[:need_size]
self.free_pages = self.free_pages[need_size:]
return select_index
def free(self, free_index: torch.Tensor):
if free_index.numel() == 0:
return
if self.is_not_in_free_group:
if self.need_sort:
self.release_pages = torch.cat((self.release_pages, free_index))
else:
self.free_pages = torch.cat((self.free_pages, free_index))
else:
self.free_group.append(free_index)
def get_cpu_copy(self, indices, mamba_indices=None):
return self._kvcache.get_cpu_copy(indices, mamba_indices=mamba_indices)
def load_cpu_copy(self, kv_cache_cpu, indices, mamba_indices=None):
return self._kvcache.load_cpu_copy(
kv_cache_cpu, indices, mamba_indices=mamba_indices
)