perf(disaggregation): reuse req pool freelists and alloc_extend tensors

This commit is contained in:
wxiwnd
2026-04-19 04:23:48 +08:00
parent a371eacd86
commit 53a04a9a97
3 changed files with 264 additions and 41 deletions
+65 -11
View File
@@ -125,7 +125,7 @@ class DecodeReqToTokenPool:
device=device, device=device,
) )
self.free_slots = list(range(size + pre_alloc_size)) self.free_slots = deque(range(size + pre_alloc_size))
def write(self, indices, values): def write(self, indices, values):
self.req_to_token[indices] = values self.req_to_token[indices] = values
@@ -147,8 +147,7 @@ class DecodeReqToTokenPool:
need_size = len(reqs) - len(reusing) need_size = len(reqs) - len(reusing)
if need_size > len(self.free_slots): if need_size > len(self.free_slots):
return None return None
select_index = self.free_slots[:need_size] select_index = [self.free_slots.popleft() for _ in range(need_size)]
self.free_slots = self.free_slots[need_size:]
offset = 0 offset = 0
for r in reqs: for r in reqs:
if r.req_pool_idx is None: if r.req_pool_idx is None:
@@ -162,7 +161,7 @@ class DecodeReqToTokenPool:
req.req_pool_idx = None req.req_pool_idx = None
def clear(self): def clear(self):
self.free_slots = list(range(self.size + self.pre_alloc_size)) self.free_slots = deque(range(self.size + self.pre_alloc_size))
class HybridMambaDecodeReqToTokenPool(HybridReqToTokenPool): class HybridMambaDecodeReqToTokenPool(HybridReqToTokenPool):
@@ -204,7 +203,7 @@ class HybridMambaDecodeReqToTokenPool(HybridReqToTokenPool):
) )
def clear(self): def clear(self):
self.free_slots = list(range(self.size + self.pre_alloc_size)) self.free_slots = deque(range(self.size + self.pre_alloc_size))
self.mamba_pool.clear() self.mamba_pool.clear()
@@ -275,6 +274,11 @@ class DecodePreallocQueue:
self._ensure_last_attempt_time: Dict[str, float] = {} self._ensure_last_attempt_time: Dict[str, float] = {}
self._ensure_retry_interval: float = 1.0 # seconds self._ensure_retry_interval: float = 1.0 # seconds
self.kv_manager = self._init_kv_manager() self.kv_manager = self._init_kv_manager()
self._alloc_extend_prefix_lens: Optional[torch.Tensor] = None
self._alloc_extend_prefix_lens_cpu: Optional[torch.Tensor] = None
self._alloc_extend_seq_lens: Optional[torch.Tensor] = None
self._alloc_extend_seq_lens_cpu: Optional[torch.Tensor] = None
self._alloc_extend_last_loc: Optional[torch.Tensor] = None
if self.scheduler.tp_worker.is_hybrid_swa: if self.scheduler.tp_worker.is_hybrid_swa:
# FIXME: current SWA allocation allocate full kv cache size in prefill # FIXME: current SWA allocation allocate full kv cache size in prefill
@@ -788,6 +792,50 @@ class DecodePreallocQueue:
) )
return allocatable_tokens return allocatable_tokens
def _get_alloc_extend_args(
self, fill_len: int
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
device = self.token_to_kv_pool_allocator.device
if self._alloc_extend_prefix_lens is None:
self._alloc_extend_prefix_lens = torch.zeros(
1, dtype=torch.int64, device=device
)
self._alloc_extend_prefix_lens_cpu = torch.zeros(1, dtype=torch.int64)
self._alloc_extend_seq_lens = torch.empty(
1, dtype=torch.int64, device=device
)
self._alloc_extend_seq_lens_cpu = torch.empty(1, dtype=torch.int64)
self._alloc_extend_last_loc = torch.empty(
1, dtype=torch.int64, device=device
)
prefix_lens = self._alloc_extend_prefix_lens
prefix_lens_cpu = self._alloc_extend_prefix_lens_cpu
seq_lens = self._alloc_extend_seq_lens
seq_lens_cpu = self._alloc_extend_seq_lens_cpu
last_loc = self._alloc_extend_last_loc
assert prefix_lens is not None
assert prefix_lens_cpu is not None
assert seq_lens is not None
assert seq_lens_cpu is not None
assert last_loc is not None
prefix_lens.zero_()
prefix_lens_cpu.zero_()
seq_lens.fill_(fill_len)
seq_lens_cpu.fill_(fill_len)
last_loc.fill_(-1)
return (
prefix_lens,
prefix_lens_cpu,
seq_lens,
seq_lens_cpu,
last_loc,
)
def _pre_alloc(self, req: Req) -> torch.Tensor: def _pre_alloc(self, req: Req) -> torch.Tensor:
"""Pre-allocate the memory for req_to_token and token_kv_pool""" """Pre-allocate the memory for req_to_token and token_kv_pool"""
req_pool_indices = self.req_to_token_pool.alloc([req]) req_pool_indices = self.req_to_token_pool.alloc([req])
@@ -803,13 +851,19 @@ class DecodePreallocQueue:
if self.token_to_kv_pool_allocator.page_size == 1: if self.token_to_kv_pool_allocator.page_size == 1:
kv_loc = self.token_to_kv_pool_allocator.alloc(fill_len) kv_loc = self.token_to_kv_pool_allocator.alloc(fill_len)
else: else:
device = self.token_to_kv_pool_allocator.device (
prefix_lens,
prefix_lens_cpu,
seq_lens,
seq_lens_cpu,
last_loc,
) = self._get_alloc_extend_args(fill_len)
kv_loc = self.token_to_kv_pool_allocator.alloc_extend( kv_loc = self.token_to_kv_pool_allocator.alloc_extend(
prefix_lens=torch.tensor([0], dtype=torch.int64, device=device), prefix_lens=prefix_lens,
prefix_lens_cpu=torch.tensor([0], dtype=torch.int64), prefix_lens_cpu=prefix_lens_cpu,
seq_lens=torch.tensor([fill_len], dtype=torch.int64, device=device), seq_lens=seq_lens,
seq_lens_cpu=torch.tensor([fill_len], dtype=torch.int64), seq_lens_cpu=seq_lens_cpu,
last_loc=torch.tensor([-1], dtype=torch.int64, device=device), last_loc=last_loc,
extend_num_tokens=fill_len, extend_num_tokens=fill_len,
) )
+32 -30
View File
@@ -27,6 +27,7 @@ KVCache actually holds the physical kv cache.
import abc import abc
import dataclasses import dataclasses
import logging import logging
from collections import deque
from contextlib import contextmanager, nullcontext from contextlib import contextmanager, nullcontext
from dataclasses import dataclass, fields from dataclasses import dataclass, fields
from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union
@@ -148,7 +149,7 @@ class ReqToTokenPool:
self.req_to_token = torch.zeros( self.req_to_token = torch.zeros(
(size, max_context_len), dtype=torch.int32, device=device (size, max_context_len), dtype=torch.int32, device=device
) )
self.free_slots = list(range(size)) self.free_slots = deque(range(size))
def write(self, indices, values): def write(self, indices, values):
self.req_to_token[indices] = values self.req_to_token[indices] = values
@@ -173,8 +174,7 @@ class ReqToTokenPool:
need_size = len(reqs) - len(reusing) need_size = len(reqs) - len(reusing)
if need_size > len(self.free_slots): if need_size > len(self.free_slots):
return None return None
select_index = self.free_slots[:need_size] select_index = [self.free_slots.popleft() for _ in range(need_size)]
self.free_slots = self.free_slots[need_size:]
offset = 0 offset = 0
for r in reqs: for r in reqs:
if r.req_pool_idx is None: if r.req_pool_idx is None:
@@ -188,7 +188,7 @@ class ReqToTokenPool:
req.req_pool_idx = None req.req_pool_idx = None
def clear(self): def clear(self):
self.free_slots = list(range(self.size)) self.free_slots = deque(range(self.size))
class MambaPool: class MambaPool:
@@ -248,10 +248,13 @@ class MambaPool:
maybe_init_custom_mem_pool(device=self.device) maybe_init_custom_mem_pool(device=self.device)
) )
with self.memory_saver_adapter.region(GPU_MEMORY_TYPE_KV_CACHE), ( with (
torch.cuda.use_mem_pool(self.custom_mem_pool) self.memory_saver_adapter.region(GPU_MEMORY_TYPE_KV_CACHE),
if self.enable_custom_mem_pool (
else nullcontext() torch.cuda.use_mem_pool(self.custom_mem_pool)
if self.enable_custom_mem_pool
else nullcontext()
),
): ):
conv_state = [ conv_state = [
torch.zeros( torch.zeros(
@@ -531,9 +534,9 @@ class HybridReqToTokenPool(ReqToTokenPool):
mid = req.mamba_pool_idx mid = req.mamba_pool_idx
else: else:
mid = self.mamba_pool.alloc(1) mid = self.mamba_pool.alloc(1)
assert ( assert mid is not None, (
mid is not None f"Not enough space for mamba cache, try to increase --mamba-full-memory-ratio or --max-mamba-cache-size. {mid=}, {self.mamba_pool.size=}, {self.mamba_pool.available_size()=}, {len(reqs)=}"
), f"Not enough space for mamba cache, try to increase --mamba-full-memory-ratio or --max-mamba-cache-size. {mid=}, {self.mamba_pool.size=}, {self.mamba_pool.available_size()=}, {len(reqs)=}" )
mid = mid[0] mid = mid[0]
req.mamba_pool_idx = mid req.mamba_pool_idx = mid
mamba_indices.append(mid) mamba_indices.append(mid)
@@ -542,18 +545,18 @@ class HybridReqToTokenPool(ReqToTokenPool):
req.mamba_ping_pong_track_buffer = self.mamba_pool.alloc( req.mamba_ping_pong_track_buffer = self.mamba_pool.alloc(
self.mamba_ping_pong_track_buffer_size self.mamba_ping_pong_track_buffer_size
) )
assert ( assert req.mamba_ping_pong_track_buffer is not None, (
req.mamba_ping_pong_track_buffer is not None "Not enough space for mamba ping pong idx, try to increase --mamba-full-memory-ratio."
), "Not enough space for mamba ping pong idx, try to increase --mamba-full-memory-ratio." )
req.mamba_next_track_idx = 0 req.mamba_next_track_idx = 0
mamba_ping_pong_track_buffers.append(req.mamba_ping_pong_track_buffer) mamba_ping_pong_track_buffers.append(req.mamba_ping_pong_track_buffer)
assert len(select_index) == len( assert len(select_index) == len(mamba_indices), (
mamba_indices f"Not enough space for mamba cache, try to increase --mamba-full-memory-ratio or --max-mamba-cache-size."
), f"Not enough space for mamba cache, try to increase --mamba-full-memory-ratio or --max-mamba-cache-size." )
if self.enable_mamba_extra_buffer: if self.enable_mamba_extra_buffer:
assert len(select_index) == len( assert len(select_index) == len(mamba_ping_pong_track_buffers), (
mamba_ping_pong_track_buffers f"Not enough space for mamba ping pong idx, try to increase --mamba-full-memory-ratio."
), f"Not enough space for mamba ping pong idx, try to increase --mamba-full-memory-ratio." )
mamba_index_tensor = torch.stack(mamba_indices).to(dtype=torch.int32) mamba_index_tensor = torch.stack(mamba_indices).to(dtype=torch.int32)
self.req_index_to_mamba_index_mapping[select_index] = mamba_index_tensor self.req_index_to_mamba_index_mapping[select_index] = mamba_index_tensor
if self.enable_mamba_extra_buffer: if self.enable_mamba_extra_buffer:
@@ -597,7 +600,9 @@ class HybridReqToTokenPool(ReqToTokenPool):
assert mamba_ping_pong_track_buffer_to_keep in [ assert mamba_ping_pong_track_buffer_to_keep in [
0, 0,
1, 1,
], f"mamba_ping_pong_track_buffer_to_keep must be 0 or 1, {mamba_ping_pong_track_buffer_to_keep=}" ], (
f"mamba_ping_pong_track_buffer_to_keep must be 0 or 1, {mamba_ping_pong_track_buffer_to_keep=}"
)
# Avoid Python-list advanced indexing on a device tensor. # Avoid Python-list advanced indexing on a device tensor.
# The ping-pong buffer size is either 2 (normal) or 1 (spec decode). # The ping-pong buffer size is either 2 (normal) or 1 (spec decode).
if self.mamba_ping_pong_track_buffer_size == 2: if self.mamba_ping_pong_track_buffer_size == 2:
@@ -728,7 +733,6 @@ class KVCache(abc.ABC):
class MHATokenToKVPool(KVCache): class MHATokenToKVPool(KVCache):
def __init__( def __init__(
self, self,
size: int, size: int,
@@ -763,7 +767,9 @@ class MHATokenToKVPool(KVCache):
self.v_head_dim = ( self.v_head_dim = (
swa_v_head_dim swa_v_head_dim
if swa_v_head_dim is not None if swa_v_head_dim is not None
else v_head_dim if v_head_dim is not None else head_dim else v_head_dim
if v_head_dim is not None
else head_dim
) )
self._create_buffers() self._create_buffers()
@@ -1029,9 +1035,9 @@ class MHATokenToKVPool(KVCache):
if N == 0: if N == 0:
return return
assert ( assert self._kv_copy_config is not None, (
self._kv_copy_config is not None "KV copy not initialized. Set enable_kv_cache_copy=True in __init__"
), "KV copy not initialized. Set enable_kv_cache_copy=True in __init__" )
cfg = self._kv_copy_config cfg = self._kv_copy_config
cap = int(cfg.get("num_locs_upper", 256)) cap = int(cfg.get("num_locs_upper", 256))
@@ -1071,7 +1077,6 @@ class MHATokenToKVPool(KVCache):
class MHATokenToKVPoolFP4(MHATokenToKVPool): class MHATokenToKVPoolFP4(MHATokenToKVPool):
def _create_buffers(self): def _create_buffers(self):
with self.memory_saver_adapter.region(GPU_MEMORY_TYPE_KV_CACHE): with self.memory_saver_adapter.region(GPU_MEMORY_TYPE_KV_CACHE):
with ( with (
@@ -1247,7 +1252,6 @@ class HybridLinearKVPool(KVCache):
assert not enable_kvcache_transpose assert not enable_kvcache_transpose
self.use_mla = use_mla self.use_mla = use_mla
if not use_mla: if not use_mla:
TokenToKVPoolClass = MHATokenToKVPool TokenToKVPoolClass = MHATokenToKVPool
if _is_npu: if _is_npu:
@@ -1268,7 +1272,6 @@ class HybridLinearKVPool(KVCache):
enable_memory_saver=enable_memory_saver, enable_memory_saver=enable_memory_saver,
) )
else: else:
TokenToKVPoolClass = MLATokenToKVPool TokenToKVPoolClass = MLATokenToKVPool
if _is_npu: if _is_npu:
@@ -1632,7 +1635,6 @@ class MLATokenToKVPool(KVCache):
class MLATokenToKVPoolFP4(MLATokenToKVPool): class MLATokenToKVPoolFP4(MLATokenToKVPool):
def _create_buffers(self): def _create_buffers(self):
with self.memory_saver_adapter.region(GPU_MEMORY_TYPE_KV_CACHE): with self.memory_saver_adapter.region(GPU_MEMORY_TYPE_KV_CACHE):
with ( with (
@@ -0,0 +1,167 @@
"""Unit tests for req-pool and decode prealloc optimizations."""
import unittest
from collections import deque
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import torch
from sglang.srt.disaggregation.decode import DecodePreallocQueue, DecodeReqToTokenPool
from sglang.srt.managers.schedule_batch import Req
from sglang.srt.mem_cache.memory_pool import ReqToTokenPool
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=6, suite="stage-a-test-cpu")
class TestReqToTokenPool(CustomTestCase):
def _make_req(self, *, req_pool_idx=None, is_chunked=0, kv_committed_len=0):
req = MagicMock(spec=Req)
req.req_pool_idx = req_pool_idx
req.is_chunked = is_chunked
req.kv_committed_len = kv_committed_len
return req
def test_req_to_token_pool_uses_deque_backed_free_slots(self):
pool = ReqToTokenPool(
size=4,
max_context_len=8,
device="cpu",
enable_memory_saver=False,
)
self.assertIsInstance(pool.free_slots, deque)
req = self._make_req()
self.assertEqual(pool.alloc([req]), [0])
pool.free(req)
pool.clear()
self.assertIsInstance(pool.free_slots, deque)
self.assertEqual(list(pool.free_slots), [0, 1, 2, 3])
def test_decode_req_to_token_pool_preserves_reuse_and_uses_deque(self):
pool = DecodeReqToTokenPool(
size=2,
max_context_len=8,
device="cpu",
enable_memory_saver=False,
pre_alloc_size=2,
)
self.assertIsInstance(pool.free_slots, deque)
self.assertEqual(pool.available_size(), 4)
reused_req = self._make_req(is_chunked=1)
self.assertEqual(pool.alloc([reused_req]), [0])
fresh_req = self._make_req()
self.assertEqual(pool.alloc([fresh_req, reused_req]), [1, 0])
pool.clear()
self.assertIsInstance(pool.free_slots, deque)
self.assertEqual(list(pool.free_slots), [0, 1, 2, 3])
class TestDecodePreallocQueue(CustomTestCase):
def _make_req(self, origin_input_ids, output_ids):
req = MagicMock(spec=Req)
req.req_pool_idx = None
req.origin_input_ids = origin_input_ids
req.output_ids = output_ids
req.kv_allocated_len = 0
req.kv_committed_len = 0
req.is_chunked = 0
req.set_extend_input_len = MagicMock()
return req
def _build_prealloc_queue(self):
req_to_token_pool = DecodeReqToTokenPool(
size=4,
max_context_len=16,
device="cpu",
enable_memory_saver=False,
pre_alloc_size=2,
)
allocator_calls = []
def alloc_extend(**kwargs):
allocator_calls.append(kwargs)
return torch.arange(kwargs["extend_num_tokens"], dtype=torch.int64)
token_to_kv_pool_allocator = MagicMock()
token_to_kv_pool_allocator.page_size = 16
token_to_kv_pool_allocator.device = "cpu"
token_to_kv_pool_allocator.alloc_extend.side_effect = alloc_extend
token_to_kv_pool_allocator.get_kvcache.return_value = MagicMock()
scheduler = SimpleNamespace(tp_worker=SimpleNamespace(is_hybrid_swa=False))
with (
patch.object(
DecodePreallocQueue, "_init_kv_manager", return_value=MagicMock()
),
patch(
"sglang.srt.disaggregation.decode.is_mla_backend", return_value=False
),
):
queue = DecodePreallocQueue(
req_to_token_pool=req_to_token_pool,
token_to_kv_pool_allocator=token_to_kv_pool_allocator,
draft_token_to_kv_pool=None,
req_to_metadata_buffer_idx_allocator=MagicMock(),
metadata_buffers=MagicMock(),
scheduler=scheduler,
transfer_queue=SimpleNamespace(queue=[]),
tree_cache=MagicMock(),
gloo_group=MagicMock(),
tp_rank=0,
tp_size=1,
dp_size=1,
gpu_id=0,
bootstrap_port=1234,
max_total_num_tokens=256,
pp_rank=0,
num_reserved_decode_tokens=1,
transfer_backend=MagicMock(),
)
return queue, allocator_calls
def test_pre_alloc_reuses_alloc_extend_tensors(self):
queue, allocator_calls = self._build_prealloc_queue()
req1 = self._make_req(origin_input_ids=[1, 2, 3], output_ids=[4, 5])
req2 = self._make_req(origin_input_ids=[1, 2, 3, 4], output_ids=[5, 6])
queue._pre_alloc(req1)
queue._pre_alloc(req2)
self.assertEqual(len(allocator_calls), 2)
self.assertIs(
allocator_calls[0]["prefix_lens"], allocator_calls[1]["prefix_lens"]
)
self.assertIs(
allocator_calls[0]["prefix_lens_cpu"],
allocator_calls[1]["prefix_lens_cpu"],
)
self.assertIs(allocator_calls[0]["seq_lens"], allocator_calls[1]["seq_lens"])
self.assertIs(
allocator_calls[0]["seq_lens_cpu"], allocator_calls[1]["seq_lens_cpu"]
)
self.assertIs(allocator_calls[0]["last_loc"], allocator_calls[1]["last_loc"])
self.assertEqual(allocator_calls[1]["prefix_lens"].item(), 0)
self.assertEqual(allocator_calls[1]["prefix_lens_cpu"].item(), 0)
self.assertEqual(allocator_calls[1]["seq_lens"].item(), 5)
self.assertEqual(allocator_calls[1]["seq_lens_cpu"].item(), 5)
self.assertEqual(allocator_calls[1]["last_loc"].item(), -1)
self.assertEqual(allocator_calls[1]["extend_num_tokens"], 5)
if __name__ == "__main__":
unittest.main()