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
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:
@@ -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
@@ -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
|
||||
)
|
||||
Reference in New Issue
Block a user