Files
sglang/test/registered/unit/mem_cache/test_unified_tri_pool.py
T
2026-09-18 17:39:50 -07:00

1524 lines
63 KiB
Python

# Copyright 2023-2026 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tri-pool composite (`UnifiedMambaSWATokenToKVPoolAllocator`) -- full KV +
SWA KV + mamba/conv state in ONE unified byte buffer, chain
``[mamba (up END) | swa (FLOAT) | full (down END)]``.
Pure CPU; fakes stand in for the KV pools (data markers verify moves).
"""
import inspect
import unittest
from unittest.mock import MagicMock, patch
import torch
from sglang.srt.mem_cache.allocator.unified_hybrid_swa import (
UnifiedMambaSWATokenToKVPoolAllocator,
)
from sglang.srt.mem_cache.allocator.unified_sub_pool import (
FloatMultiEndedAllocator,
MultiEndedAllocator,
)
from sglang.srt.mem_cache.base_prefix_cache import EvictParams
from sglang.srt.mem_cache.unified_cache.components import ComponentType
from sglang.srt.mem_cache.unified_memory_pool import (
MambaSubPoolSpec,
MHASubPoolSpec,
UnifiedKVPool,
UnifiedMambaSlotAllocator,
init_unified_mamba_swa_pools,
)
from sglang.srt.mem_cache.unified_radix_cache import UnifiedRadixCache
from sglang.test.ci.ci_register import register_cpu_ci
# Hermetic convention of this directory's pool tests: plain unittest.TestCase,
# only ci_register imported (no heavy sglang.test.test_utils chain).
register_cpu_ci(est_time=8, suite="base-a-test-cpu")
_DEV = "cpu"
class _FakeKVCache:
"""buf[p] == virtual id stored at physical slot p (-1 free); moves copy it."""
def __init__(self, max_slots: int):
self.buf = torch.full((max_slots,), -1, dtype=torch.int64)
def move_kv_cache(self, dst_loc: torch.Tensor, src_loc: torch.Tensor):
self.buf[dst_loc] = self.buf[src_loc].clone()
class _FakeUnifiedSWAKVPool:
class _SubKV(_FakeKVCache):
def __init__(self, max_slots):
super().__init__(max_slots)
self.allocator = None
def attach_allocator(self, allocator):
self.allocator = allocator
def __init__(self, shared_pool: UnifiedKVPool):
self.full_kv_pool = self._SubKV(shared_pool.max_slots("full"))
self.swa_kv_pool = self._SubKV(shared_pool.max_slots("swa"))
self._full_allocator = None
self._swa_allocator = None
def attach_allocators(self, *, full_allocator, swa_allocator):
self._full_allocator = full_allocator
self._swa_allocator = swa_allocator
def _tri_specs(
full_layer_num=4, swa_layer_num=2, state_layer_num=2, head_num=2, head_dim=4
):
full = MHASubPoolSpec(
name="full",
layer_num=full_layer_num,
head_num=head_num,
head_dim=head_dim,
store_dtype=torch.float16,
grow_direction="down",
)
swa = MHASubPoolSpec(
name="swa",
layer_num=swa_layer_num,
head_num=head_num,
head_dim=head_dim,
store_dtype=torch.float16,
grow_direction="float",
)
mamba = MambaSubPoolSpec(
name="mamba",
layer_num=state_layer_num,
conv_state_shapes=((3, 8),),
conv_dtype=torch.bfloat16,
temporal_state_shape=(0, 0, 0), # Inkling: conv-only, no SSM state
temporal_dtype=torch.float32,
grow_direction="up",
)
return full, swa, mamba
class TestUnifiedTriPool(unittest.TestCase):
def _build(
self,
n_full=32,
n_swa=16,
n_state=8,
page_size=1,
lazy_compaction=False,
):
full, swa, mamba = _tri_specs()
total = (
n_full * full.entry_bytes()
+ n_swa * swa.entry_bytes()
+ n_state * mamba.entry_bytes()
)
pool = UnifiedKVPool(
total_bytes=total,
sub_pool_specs=[full, swa, mamba],
device=_DEV,
enable_memory_saver=False,
page_size=page_size,
)
kvcache = _FakeUnifiedSWAKVPool(pool)
mamba_kv = _FakeKVCache(pool.max_slots("mamba"))
allocator = UnifiedMambaSWATokenToKVPoolAllocator(
unified_buffer=pool,
kvcache=kvcache,
mamba_kvcache=mamba_kv,
device=_DEV,
full_max_total_num_tokens=n_full,
swa_max_total_num_tokens=n_swa,
page_size=page_size,
need_sort=False,
forward_stream=None,
lazy_compaction=lazy_compaction,
)
return pool, allocator, kvcache, mamba_kv
def _stamp(self, allocator, kvcache, v):
fa = allocator.full_attn_allocator
sa = allocator.swa_attn_allocator
kvcache.full_kv_pool.buf[fa.virtual_to_physical[v]] = v
kvcache.swa_kv_pool.buf[sa.virtual_to_physical[v]] = v
# -- construction --
def test_chain_wiring_and_float_swa(self):
pool, allocator, kvcache, _ = self._build()
fa = allocator.full_attn_allocator
sa = allocator.swa_attn_allocator
ma = allocator.mamba_allocator
self.assertIsInstance(sa, FloatMultiEndedAllocator)
self.assertIs(ma.high_peer, sa)
self.assertIs(sa.low_peer, ma)
self.assertIs(sa.high_peer, fa)
self.assertIs(fa.low_peer, sa)
# Canonical chain order in the pool.
self.assertEqual(
[s.name for s in pool.sub_pool_specs], ["mamba", "swa", "full"]
)
# KV pool got the allocators.
self.assertIs(kvcache._full_allocator, fa)
self.assertIs(kvcache._swa_allocator, sa)
def test_pd_preallocation_binds_only_swa_tail_pages(self):
_, allocator, _, _ = self._build(page_size=4)
before = allocator.available_size()
prefix = torch.tensor([0], dtype=torch.int64)
seq = torch.tensor([12], dtype=torch.int64)
# With an empty prefix, ordinary allocation supplies the same virtual
# pages without launching the GPU extend kernel. All page binding and
# capacity accounting still run through the real sub-allocators.
full = allocator.full_attn_allocator
with patch.object(
full, "alloc_extend", side_effect=lambda *a, **kw: full.alloc(12)
):
virtual = allocator.alloc_extend_swa_tail(
prefix, prefix, seq, seq, torch.tensor([-1]), 12, swa_tail_len=5
)
self.assertIsNotNone(virtual)
self.assertEqual(len(virtual), 12)
self.assertEqual(allocator.full_attn_allocator.allocated_count(), 12)
self.assertEqual(allocator.swa_attn_allocator.allocated_count(), 8)
swa_pages = allocator.swa_v2p_page_table[virtual[::4] // 4]
self.assertLessEqual(swa_pages[0].item(), 0)
self.assertTrue(torch.all(swa_pages[1:] > 0).item())
allocator.free(virtual)
self.assertEqual(allocator.full_attn_allocator.allocated_count(), 0)
self.assertEqual(allocator.swa_attn_allocator.allocated_count(), 0)
self.assertEqual(allocator.available_size(), before)
def test_pd_short_tail_fits_beyond_joint_capacity(self):
for lazy in (False, True):
for tail_len in (0, 5):
with self.subTest(lazy=lazy, tail_len=tail_len):
_, allocator, _, _ = self._build(page_size=4, lazy_compaction=lazy)
full = allocator.full_attn_allocator
length = allocator.available_size() + 4
self.assertFalse(allocator.can_reserve(length, length))
self.assertTrue(allocator.can_reserve(length, tail_len))
prefix = torch.tensor([0], dtype=torch.int64)
seq = torch.tensor([length], dtype=torch.int64)
with patch.object(
full,
"alloc_extend",
side_effect=lambda *a, **kw: full.alloc(length),
):
virtual = allocator.alloc_extend_swa_tail(
prefix,
prefix,
seq,
seq,
torch.tensor([-1]),
length,
tail_len,
)
self.assertIsNotNone(virtual)
self.assertEqual(full.allocated_count(), length)
self.assertEqual(
allocator.swa_attn_allocator.allocated_count(),
-(-tail_len // 4) * 4,
)
self.assertEqual(allocator.verify_byte_accounting(), [])
allocator.free(virtual)
self.assertEqual(full.allocated_count(), 0)
self.assertEqual(allocator.swa_attn_allocator.allocated_count(), 0)
def test_pd_tail_rejects_full_capacity_shortfall(self):
_, allocator, _, _ = self._build(page_size=4)
full = allocator.full_attn_allocator
length = full.available_size() + 4
prefix = torch.tensor([0], dtype=torch.int64)
seq = torch.tensor([length], dtype=torch.int64)
with patch.object(full, "alloc_extend") as extend:
self.assertIsNone(
allocator.alloc_extend_swa_tail(
prefix, prefix, seq, seq, torch.tensor([-1]), length, 0
)
)
extend.assert_not_called()
self.assertEqual(full.allocated_count(), 0)
self.assertEqual(allocator.swa_attn_allocator.allocated_count(), 0)
def test_empty_float_is_transparent_to_the_ends(self):
_, allocator, _, _ = self._build()
fa = allocator.full_attn_allocator
ma = allocator.mamba_allocator
self.assertTrue(allocator.swa_attn_allocator._is_frontier_transparent())
# full's chain gap reaches the mamba end's frontier straight through.
self.assertEqual(
fa._current_gap_bytes(),
fa._byte_low_frontier() - ma._byte_high_frontier(),
)
# -- the joint availability contract --
def test_available_shrinks_as_state_slots_grow(self):
_, allocator, _, _ = self._build()
before = allocator.available_size()
slots = allocator.mamba_allocator.alloc(4)
self.assertIsNotNone(slots)
after = allocator.available_size()
self.assertLess(after, before)
allocator.mamba_allocator.free(slots)
self.assertEqual(allocator.available_size(), before)
# -- steady-state SWA churn: tombstones -> holes -> in-place reuse --
def _swa_interior_block(self, allocator, blocks):
"""The block whose SWA-physical pages touch neither float boundary:
`free_swa` on it holes; a boundary block would be absorbed instead."""
sa = allocator.swa_attn_allocator
for v in blocks:
pages = set(int(x) for x in sa.virtual_to_physical[v].tolist())
if sa.low_wm_page not in pages and (sa.high_wm_page - 1) not in pages:
return v
raise AssertionError("no interior block in layout")
def test_free_swa_holes_recycled_in_place_zero_copy(self):
_, allocator, kvcache, _ = self._build()
blocks = [allocator.alloc(4) for _ in range(3)]
for v in blocks:
self.assertIsNotNone(v)
self._stamp(allocator, kvcache, v)
sa = allocator.swa_attn_allocator
fa = allocator.full_attn_allocator
span_before = (sa.low_wm_page, sa.high_wm_page)
# Window slide: an INTERIOR block ages out of the SWA window.
v_mid = self._swa_interior_block(allocator, blocks)
allocator.free_swa(v_mid)
# The full side keeps the token; the swa side tombstoned it.
self.assertTrue(bool((fa.virtual_to_physical[v_mid] >= 0).all()))
self.assertTrue(bool((sa.virtual_to_physical[v_mid] == -1).all()))
self.assertEqual(sa._hole_pages(), 4)
self.assertEqual((sa.low_wm_page, sa.high_wm_page), span_before)
# The next alloc recycles the holes IN PLACE: no span growth, no moves.
vd = allocator.alloc(4)
self.assertIsNotNone(vd)
self._stamp(allocator, kvcache, vd)
self.assertEqual(sa._hole_pages(), 0)
self.assertEqual((sa.low_wm_page, sa.high_wm_page), span_before)
self.assertEqual(len(sa._inverse_history), 0) # zero copies
def test_free_swa_boundary_block_absorbed_zero_copy(self):
# A boundary block's tombstones shrink the span instead of holing it.
# The shrink needs the hole set on the host, so it is deferred out of
# the per-step free and lands in the opportunistic flush.
_, allocator, kvcache, _ = self._build()
blocks = [allocator.alloc(4) for _ in range(2)]
for v in blocks:
self._stamp(allocator, kvcache, v)
sa = allocator.swa_attn_allocator
span_pages = sa._span_pages()
# Pick a block holding a span-boundary page.
boundary = None
for v in blocks:
pages = set(int(x) for x in sa.virtual_to_physical[v].tolist())
if sa.low_wm_page in pages or (sa.high_wm_page - 1) in pages:
boundary = v
break
self.assertIsNotNone(boundary)
allocator.free_swa(boundary)
allocator.flush_opportunistic() # the deferred reclaim point
self.assertEqual(sa._hole_pages(), 0) # absorbed, not holed
self.assertEqual(sa._span_pages(), span_pages - 4)
self.assertEqual(len(sa._inverse_history), 0) # zero copies
def test_free_releases_both_sides_and_filters_tombstones(self):
_, allocator, kvcache, _ = self._build()
va = allocator.alloc(4)
self._stamp(allocator, kvcache, va)
allocator.free_swa(va) # tombstone first (aged out of window)
allocator.free(va) # then the request finishes
fa = allocator.full_attn_allocator
sa = allocator.swa_attn_allocator
self.assertTrue(bool((fa.virtual_to_physical[va] == -1).all()))
self.assertTrue(bool((sa.virtual_to_physical[va] == -1).all()))
# Fully-freed float parks and is transparent again.
self.assertTrue(sa._is_frontier_transparent())
# -- per-request state surface --
def test_mamba_slot_allocator_surface(self):
pool, allocator, _, mamba_kv = self._build()
slot_alloc = UnifiedMambaSlotAllocator(
allocator.mamba_allocator,
max_size=pool.max_slots("mamba") - 1,
device=_DEV,
)
v = slot_alloc.alloc(3)
self.assertIsNotNone(v)
p = slot_alloc.translate(v)
self.assertTrue(bool((p >= 0).all()))
mamba_kv.buf[p] = v
self.assertEqual(
slot_alloc.available_size(),
(pool.max_slots("mamba") - 1) - 3,
)
slot_alloc.free(v)
self.assertEqual(slot_alloc.available_size(), pool.max_slots("mamba") - 1)
# Group prefetch draw-down + surplus return.
slot_alloc.alloc_group_begin(4)
s1 = slot_alloc.alloc(1)
self.assertIsNotNone(s1)
slot_alloc.alloc_group_end()
self.assertEqual(
slot_alloc.available_size(),
(pool.max_slots("mamba") - 1) - 1,
)
# -- flush semantics --
def test_urgent_flush_preserves_float_holes(self):
_, allocator, kvcache, _ = self._build(lazy_compaction=True)
blocks = [allocator.alloc(4) for _ in range(3)]
for v in blocks:
self._stamp(allocator, kvcache, v)
allocator.free_swa(self._swa_interior_block(allocator, blocks))
sa = allocator.swa_attn_allocator
holes = sa._hole_pages()
self.assertGreater(holes, 0)
from sglang.srt.mem_cache.allocator.unified_sub_pool import _relieve_for_alloc
_relieve_for_alloc(allocator, 1)
self.assertEqual(sa._hole_pages(), holes) # holes are assets, not backlog
def test_full_donor_stops_after_float_exposes_mamba_capacity(self):
for page_size, lazy_compaction in ((1, False), (4, True)):
with self.subTest(page_size=page_size, lazy_compaction=lazy_compaction):
_, allocator, _, _ = self._build(
page_size=page_size, lazy_compaction=lazy_compaction
)
mamba_slots = UnifiedMambaSlotAllocator(
allocator.mamba_allocator,
max_size=allocator.mamba_allocator.max_slots - 1,
device=_DEV,
)
full_leaves = []
while True:
indices = allocator.alloc(allocator.page_size)
if indices is None:
break
full_leaves.append(indices)
self.assertTrue(full_leaves)
residual_mamba = mamba_slots.schedulable_available_size()
if residual_mamba:
self.assertIsNotNone(mamba_slots.alloc(residual_mamba))
while mamba_slots.alloc(1) is not None:
pass
self.assertGreater(mamba_slots.available_size(), 0)
self.assertEqual(mamba_slots.schedulable_available_size(), 0)
cache = object.__new__(UnifiedRadixCache)
cache.disable = False
cache.tree_components = (
ComponentType.FULL,
ComponentType.SWA,
ComponentType.MAMBA,
)
cache.is_swa_enabled = True
cache.cache_controller = None
cache.metrics_collector = None
cache.token_to_kv_pool_allocator = allocator
cache.req_to_token_pool = MagicMock(mamba_allocator=mamba_slots)
tree_core = MagicMock()
tree_core.full_evictable_size.return_value = len(full_leaves)
tree_core.mamba_evictable_size.return_value = 0
walk = {"request_cnt": 0, "freed_leaves": 0}
def start(component_type, request_cnt):
self.assertEqual(component_type, ComponentType.FULL)
walk["request_cnt"] = request_cnt
def next_node(component_type, tracker):
self.assertEqual(component_type, ComponentType.FULL)
if tracker[ComponentType.FULL] >= walk["request_cnt"] or walk[
"freed_leaves"
] >= len(full_leaves):
return None, False
return walk["freed_leaves"] + 1, True
def evict_leaf(node_id, tracker):
self.assertEqual(node_id, walk["freed_leaves"] + 1)
indices = full_leaves[-node_id]
tracker[ComponentType.FULL] += int(indices.numel())
allocator.full_attn_allocator.free(indices)
walk["freed_leaves"] += 1
return None
tree_core.evict_device_start.side_effect = start
cache.tree_core = tree_core
cache._evict_device_next_node = MagicMock(side_effect=next_node)
cache._evict_device_leaf = MagicMock(side_effect=evict_leaf)
swa_live_before = allocator.swa_attn_allocator._live_pages()
result = cache.evict_for_alloc(EvictParams(mamba_num=1))
self.assertEqual(walk["freed_leaves"], 1)
self.assertEqual(result.num_tokens_evicted, page_size)
self.assertEqual(result.swa_num_tokens_evicted, 0)
self.assertEqual(result.mamba_num_evicted, 0)
self.assertEqual(
allocator.swa_attn_allocator._live_pages(), swa_live_before
)
self.assertGreaterEqual(mamba_slots.schedulable_available_size(), 1)
self.assertIsNotNone(mamba_slots.alloc(1))
self.assertEqual(allocator.verify_byte_accounting(), [])
class TestTriPagedFreeGroup(unittest.TestCase):
"""The tri composite at PAGE SIZE > 1, driven through the production free
path: free_group_begin -> free_segment -> free_group_end.
Regression: every other tri test runs at page_size=1, where the
page-representative machinery is dead code. At ps>1 the composite releases
reps via `swa_attn_allocator.free(..., _pages=...)`, which the float must
accept or the first decode batch dies; honouring `_pages` is also what
keeps the free path off the data-dependent `torch.unique` host sync.
"""
def _build_paged(self, page_size=4, n_full=64, n_swa=32, n_state=8):
full, swa, mamba = _tri_specs()
total = (
n_full * full.entry_bytes()
+ n_swa * swa.entry_bytes()
+ n_state * mamba.entry_bytes()
)
pool = UnifiedKVPool(
total_bytes=total,
sub_pool_specs=[full, swa, mamba],
device=_DEV,
enable_memory_saver=False,
page_size=page_size,
)
kvcache = _FakeUnifiedSWAKVPool(pool)
mamba_kv = _FakeKVCache(pool.max_slots("mamba"))
allocator = UnifiedMambaSWATokenToKVPoolAllocator(
unified_buffer=pool,
kvcache=kvcache,
mamba_kvcache=mamba_kv,
device=_DEV,
full_max_total_num_tokens=n_full,
swa_max_total_num_tokens=n_swa,
page_size=page_size,
need_sort=False,
forward_stream=None,
)
return pool, allocator
def test_free_group_segment_release_reaches_the_float(self):
"""The exact production sequence the scheduler runs per decode batch."""
pool, allocator = self._build_paged()
v = allocator.alloc(8)
self.assertIsNotNone(v)
before = allocator.available_size()
allocator.free_group_begin()
allocator.free_segment(v, start_pos=0)
allocator.free_group_end() # -> _release_page_reps -> float.free(_pages=)
self.assertEqual(allocator.verify_byte_accounting(), [])
self.assertGreaterEqual(allocator.available_size(), before)
# Capacity fully recovered: the float parked, both ends rewound.
self.assertTrue(allocator.swa_attn_allocator._is_frontier_transparent())
def test_mamba_donor_flushes_full_only_group_without_closing_it(self):
_, allocator = self._build_paged(page_size=1)
full_indices = allocator.alloc(8)
self.assertIsNotNone(full_indices)
allocator.free_swa(full_indices)
allocated_before = allocator.full_attn_allocator.allocated_count()
allocator.free_group_begin()
allocator.free_full_segment(full_indices, start_pos=0)
self.assertTrue(allocator.full_free_group)
donor = allocator.mamba_full_cache_donor()
self.assertIsNotNone(donor)
donor.flush_deferred_full_frees()
self.assertEqual(allocator.free_group, [])
self.assertEqual(allocator.free_page_reps_group, [])
self.assertEqual(allocator.full_free_group, [])
self.assertLess(
allocator.full_attn_allocator.allocated_count(), allocated_before
)
self.assertEqual(allocator.verify_byte_accounting(), [])
allocator.free_group_end()
def test_ungrouped_segment_free_also_reaches_the_float(self):
pool, allocator = self._build_paged()
v = allocator.alloc(8)
self.assertIsNotNone(v)
allocator.free_segment(v, start_pos=0)
self.assertEqual(allocator.verify_byte_accounting(), [])
class TestTriFreeSwaNoHostSync(unittest.TestCase):
"""The tri's swa side is the FLOAT, which can never run the lazy event
pipeline: unless the per-step frees carry caller-derived page ids, the tri
reintroduces the host syncs the sync-free free path removed. Fixtures run
at page_size > 1; ps==1 short-circuits the page machinery and hides this.
"""
PS = 4
def _tri(self):
inst = TestTriPagedFreeGroup(
[m for m in dir(TestTriPagedFreeGroup) if m.startswith("test_")][0]
)
return inst._build_paged(page_size=self.PS)[1]
def test_ratchet_shape_free_swa_never_syncs_on_the_float(self):
alloc = self._tri()
v = alloc.alloc(8 * self.PS)
self.assertIsNotNone(v)
from unittest import mock
with (
mock.patch.object(
torch, "unique", side_effect=AssertionError("unique = host sync")
),
mock.patch.object(
torch.Tensor, "item", side_effect=AssertionError("item = host sync")
),
):
alloc.free_swa_segment(v[: 4 * self.PS], start_pos=0)
self.assertEqual(alloc.verify_byte_accounting(), [])
def test_fallback_free_swa_still_correct_for_radix_shapes(self):
"""Radix eviction hands arbitrary node values (no start_pos): the
dedup fallback must end in the same state as the stride path."""
a1, a2 = self._tri(), self._tri()
v1, v2 = a1.alloc(6 * self.PS), a2.alloc(6 * self.PS)
self.assertTrue(torch.equal(v1, v2))
a1.free_swa_segment(v1[: 4 * self.PS], start_pos=0)
a2.free_swa(v2[: 4 * self.PS])
self.assertTrue(
torch.equal(
a1.swa_attn_allocator.virtual_to_physical,
a2.swa_attn_allocator.virtual_to_physical,
)
)
self.assertEqual(a1.available_size(), a2.available_size())
self.assertEqual(a1.verify_byte_accounting(), [])
self.assertEqual(a2.verify_byte_accounting(), [])
class TestGeneralizedRebalance(unittest.TestCase):
"""The float must yield to WHICHEVER end is short, with the direction
derived from the caller's growth direction rather than hard-coded to the
token path's side. `make_room` was always side-agnostic; these pin the
POLICY in `_ask_float_for_room`."""
PS = 4
def _tri(self):
inst = TestTriPagedFreeGroup(
[m for m in dir(TestTriPagedFreeGroup) if m.startswith("test_")][0]
)
return inst._build_paged(page_size=self.PS)[1]
def test_direction_is_derived_from_growth_on_both_ends(self):
"""Raw end+float+end chain, BOTH orientations in one fixture: the
up-growing end opens the float's LOW side, the down-growing end its
HIGH side."""
from test_multi_ended_allocator import TestFloatMultiEndedAllocator
inst = TestFloatMultiEndedAllocator(
[m for m in dir(TestFloatMultiEndedAllocator) if m.startswith("test_")][0]
)
_pool, up_end, fla, down_end, _kv = inst._build_tri()
v = fla.alloc(8) # opaque float mid-region
self.assertIsNotNone(v)
# UP end: exhaust its band below the float, then ask for more.
e_up = up_end.entry_bytes_per_page
fit = int((fla._byte_low_frontier() - up_end._byte_high_frontier()) // e_up)
if fit > 0:
self.assertIsNotNone(up_end.alloc(fit * up_end.page_size))
low_before = fla.low_wm_page
self.assertIsNotNone(up_end.alloc(up_end.page_size))
self.assertGreater(fla.low_wm_page, low_before) # opened LOW side
# DOWN end: exhaust its band above the float, then ask for more.
e_dn = down_end.entry_bytes_per_page
fit = int((down_end._byte_low_frontier() - fla._byte_high_frontier()) // e_dn)
if fit > 0:
self.assertIsNotNone(down_end.alloc(fit * down_end.page_size))
high_before = fla.high_wm_page
self.assertIsNotNone(down_end.alloc(down_end.page_size))
self.assertLess(fla.high_wm_page, high_before) # opened HIGH side
def test_two_pool_chain_rebalance_is_a_noop(self):
from test_multi_ended_allocator import (
TestPagedMultiEndedAllocator as _PagedFixture,
)
inst = _PagedFixture(
[m for m in dir(_PagedFixture) if m.startswith("test_")][0]
)
_pool, full, swa, _fkv, _skv = inst._build()
v = full.alloc(full.page_size * 2)
self.assertIsNotNone(v)
wm = full.watermark_physical
full._ask_float_for_room(full.page_size * 1000) # absurd ask
self.assertEqual(full.watermark_physical, wm) # untouched
def test_index_cap_guard_never_moves_data_uselessly(self):
"""When the caller's own INDEX space binds, no amount of float
movement helps -- make_room must not be called (poisoned)."""
from unittest import mock
alloc = self._tri()
alloc.alloc(4 * self.PS)
ma = alloc.mamba_allocator
sa = alloc.swa_attn_allocator
huge = (ma.num_pages + 10) * ma.page_size # beyond index space
with mock.patch.object(
sa, "make_room", side_effect=AssertionError("useless make_room")
):
ma._ask_float_for_room(huge)
class TestComputedShortSide(unittest.TestCase):
"""`_ask_float_for_room` must open the side that MEASURES short -- never
"the side facing full" -- including the shape with coupled ends on BOTH
sides of the float.
"""
PS = 4
def _tri(self):
inst = TestTriPagedFreeGroup(
[m for m in dir(TestTriPagedFreeGroup) if m.startswith("test_")][0]
)
return inst._build_paged(page_size=self.PS)[1]
def _sides(self, alloc):
sa = alloc.swa_attn_allocator
low = max(0, sa._byte_low_frontier() - sa._chain_high_frontier_below_bytes())
high = max(0, sa._chain_low_frontier_above_bytes() - sa._byte_high_frontier())
return low, high
def test_float_share_short_opens_the_state_side(self):
"""RED-LINE: full's demand fits its band, the float's own share fits
NEITHER band, and the state side has the larger surplus -- so the
policy must open the STATE side, i.e. slide the float toward full
during a TOKEN alloc.
"""
from unittest import mock
alloc = self._tri()
v = alloc.alloc(6 * self.PS) # places the float mid-region
self.assertIsNotNone(v)
sa = alloc.swa_attn_allocator
fa = alloc.full_attn_allocator
e_f, e_s = fa.entry_bytes_per_page, sa.entry_bytes_per_page
# Position: slide the float LOW (setup uses the mechanism directly),
# so the low band is small and the geometry below is expressible.
b_low0, b_high0 = self._sides(alloc)
# Pack the float low (leapfrog over-opens by design), then reopen LOW
# to ~2 full-pages: F must outgrow it, yet the need_n window below
# must stay non-empty.
sa.make_room(side="high", min_bytes=b_low0 + b_high0 - 2 * e_f)
sa.make_room(side="low", min_bytes=2 * e_f)
# Find a need_n where: D_high = need_n*e_f fits band_high, F =
# need_n*e_s exceeds BOTH surpluses, and low has the larger surplus.
chosen = None
for need_n in range(1, 64):
b_low, b_high = self._sides(alloc)
s_low = b_low # no coupled end on the low side
s_high = b_high - need_n * e_f
if s_high < 0:
break
F = need_n * e_s
if F > s_low and F > s_high and s_low >= s_high:
chosen = need_n
break
self.assertIsNotNone(chosen, "fixture cannot express the geometry")
calls = []
real = sa.make_room
with mock.patch.object(
sa, "make_room", side_effect=lambda **kw: calls.append(kw) or real(**kw)
):
alloc._ask_float_for_room(chosen * self.PS)
self.assertEqual(len(calls), 1, calls)
self.assertEqual(calls[0]["side"], "low") # the STATE side
def test_full_side_short_target_matches_the_closed_form(self):
"""When the full side is the short one, the ask must equal the
closed form demand + max(0, F - far_surplus) + slack."""
from unittest import mock
alloc = self._tri()
v = alloc.alloc(4 * self.PS)
self.assertIsNotNone(v)
sa, fa = alloc.swa_attn_allocator, alloc.full_attn_allocator
e_f, e_s = fa.entry_bytes_per_page, sa.entry_bytes_per_page
chosen = None
for need_n in range(1, 256):
b_low, b_high = self._sides(alloc)
if b_high - need_n * e_f < 0 and b_low >= 0:
chosen = need_n
break
self.assertIsNotNone(chosen)
b_low, b_high = self._sides(alloc)
F = max(0, chosen - sa._hole_pages()) * e_s
want = chosen * e_f + max(0, F - b_low) + max(e_f, e_s)
calls = []
with mock.patch.object(
sa, "make_room", side_effect=lambda **kw: calls.append(kw)
):
alloc._ask_float_for_room(chosen * self.PS)
self.assertEqual(len(calls), 1)
self.assertEqual(calls[0]["side"], "high")
self.assertEqual(calls[0]["min_bytes"], want)
def test_two_coupled_ends_lands_demand_on_both_sides(self):
"""A coupled set with ends on BOTH sides of the float: one-side-short
must open that side; BOTH-sides-short must not move at all
(relocation is zero-sum between the bands)."""
from unittest import mock
alloc = self._tri()
v = alloc.alloc(6 * self.PS)
self.assertIsNotNone(v)
sa, fa, ma = (
alloc.swa_attn_allocator,
alloc.full_attn_allocator,
alloc.mamba_allocator,
)
# Synthetic coupling: the state end joins the demand vector, the
# override a composite with ends on both sides would ship.
need = lambda self, t: {
fa: -(-t // self.page_size),
sa: -(-t // self.page_size),
ma: -(-t // self.page_size),
}
with mock.patch.object(type(alloc), "_alloc_demand", need):
# (a) both sides short: absurd need -> both demands exceed their
# bands -> make_room must NOT be called.
with mock.patch.object(
sa, "make_room", side_effect=AssertionError("zero-sum move")
):
alloc._ask_float_for_room(10_000 * self.PS)
# (b) one side short: find a need where the HIGH side (full) is
# short while the LOW side (mamba demand) still fits.
e_f, e_m = fa.entry_bytes_per_page, ma.entry_bytes_per_page
chosen = None
for need_n in range(1, 256):
b_low, b_high = self._sides(alloc)
if b_high - need_n * e_f < 0 and b_low - need_n * e_m >= 0:
chosen = need_n
break
if chosen is not None:
calls = []
with mock.patch.object(
sa, "make_room", side_effect=lambda **kw: calls.append(kw)
):
alloc._ask_float_for_room(chosen * self.PS)
self.assertEqual(len(calls), 1)
self.assertEqual(calls[0]["side"], "high")
def test_nothing_short_means_no_relocation(self):
"""Everything fits -> no page moves. The tri's token vector carries
{mamba: 0}: a zero entry must not move the float either."""
from unittest import mock
alloc = self._tri()
alloc.alloc(4 * self.PS)
demand = alloc._alloc_demand(2 * self.PS)
self.assertEqual(demand[alloc.mamba_allocator], 0)
sa = alloc.swa_attn_allocator
with mock.patch.object(
sa, "make_room", side_effect=AssertionError("needless move")
):
alloc._ask_float_for_room(1)
class TestFloatPolicyTotalTarget(unittest.TestCase):
"""`make_room`'s min_bytes is a TARGET for the whole band, not a delta.
Regression: an ask shaped as `deficit + one page` lands BELOW the current
gap when the band is only PARTIALLY free, so `make_room` no-ops and the
allocation fails though the float had room to slide.
"""
PS = 4
def _tri(self):
inst = TestTriPagedFreeGroup(
[m for m in dir(TestTriPagedFreeGroup) if m.startswith("test_")][0]
)
return inst._build_paged(page_size=self.PS)[1]
def test_partial_gap_state_alloc_still_succeeds(self):
alloc = self._tri()
v = alloc.alloc(6 * self.PS)
self.assertIsNotNone(v)
ma, sa = alloc.mamba_allocator, alloc.swa_attn_allocator
e_m = ma.entry_bytes_per_page
gap_slots = int((sa._byte_low_frontier() - ma._byte_high_frontier()) // e_m)
self.assertGreater(gap_slots, 2)
low_before = sa.low_wm_page
# partial-gap + 3: a delta-shaped ask lands below the current gap.
got = ma.alloc((gap_slots + 3) * ma.page_size)
self.assertIsNotNone(got, "partial-gap shortfall must relocate, not fail")
self.assertGreater(sa.low_wm_page, low_before)
self.assertEqual(alloc.verify_byte_accounting(), [])
class TestTriDeferredAbsorption(unittest.TestCase):
"""Boundary absorption is deferred out of the per-step free and paid once
at a quiescent point. These pin WHERE it is paid, and that skipping it
stays merely conservative."""
PS = 4
def _tri(self):
inst = TestTriPagedFreeGroup(
[m for m in dir(TestTriPagedFreeGroup) if m.startswith("test_")][0]
)
return inst._build_paged(page_size=self.PS)[1]
def test_per_step_flush_reclaims_the_span(self):
alloc = self._tri()
v = alloc.alloc(8 * self.PS)
sa = alloc.swa_attn_allocator
span = sa._span_pages()
alloc.free_swa_segment(v[6 * self.PS :], start_pos=6 * self.PS) # high edge
self.assertGreater(sa._hole_pages(), 0) # deferred
self.assertEqual(sa._span_pages(), span)
moved = alloc.flush_opportunistic()
self.assertGreater(moved, 0)
self.assertLess(sa._span_pages(), span)
self.assertEqual(alloc.verify_byte_accounting(), [])
def test_shortfall_ladder_absorbs_before_the_deficit_math(self):
"""The zero-copy rung must run FIRST: a stale-wide span inflates the
rebalance deficit and buys a relocation the shrink already covers."""
alloc = self._tri()
v = alloc.alloc(8 * self.PS)
sa = alloc.swa_attn_allocator
alloc.free_swa_segment(v[6 * self.PS :], start_pos=6 * self.PS)
self.assertGreater(sa._hole_pages(), 0)
moves_before = len(sa._inverse_history)
from sglang.srt.mem_cache.allocator.unified_sub_pool import _relieve_for_alloc
_relieve_for_alloc(alloc, 1) # the ladder
self.assertEqual(sa._hole_pages(), 0) # rung 0 ran
self.assertEqual(len(sa._inverse_history), moves_before) # zero copies
def test_deferral_is_conservative_never_over_reports(self):
"""Availability with a stale-wide span must never EXCEED the absorbed
value -- under-reporting is safe, over-reporting would over-admit."""
alloc = self._tri()
v = alloc.alloc(8 * self.PS)
alloc.free_swa_segment(v[6 * self.PS :], start_pos=6 * self.PS)
deferred = alloc.available_size()
alloc.swa_attn_allocator._flush(urgent=False)
absorbed = alloc.available_size()
self.assertLessEqual(deferred, absorbed)
self.assertEqual(alloc.verify_byte_accounting(), [])
def test_clean_flush_skips_the_d2h_entirely(self):
"""Only `free` can put a hole ON a boundary (alloc DRAINS holes into
live pages, extension adds live pages), so with nothing freed since
the last absorb the walk provably finds nothing and must not pay the
D2H."""
from unittest import mock
alloc = self._tri()
v = alloc.alloc(8 * self.PS)
alloc.free_swa_segment(v[2 * self.PS : 4 * self.PS], start_pos=2 * self.PS)
alloc.flush_opportunistic() # consumes the dirty flag
sa = alloc.swa_attn_allocator
self.assertGreater(sa._hole_pages(), 0) # interior holes remain
with mock.patch.object(
torch.Tensor, "tolist", side_effect=AssertionError("tolist = D2H")
):
self.assertEqual(alloc.flush_opportunistic(), 0)
self.assertEqual(sa._flush(urgent=False), 0)
def test_alloc_between_frees_cannot_hide_a_boundary_hole(self):
"""An alloc drains holes and can restore a previously-seen hole
COUNT, so the flag must be armed by `free`, not read off `numel()`."""
alloc = self._tri()
v = alloc.alloc(8 * self.PS)
sa = alloc.swa_attn_allocator
alloc.free_swa_segment(v[: 2 * self.PS], start_pos=0) # low-edge holes
n_after_free = sa._hole_pages()
alloc.alloc(2 * self.PS) # drains them back to live
alloc.free_swa_segment(v[6 * self.PS :], start_pos=6 * self.PS) # high edge
self.assertEqual(sa._hole_pages(), n_after_free) # same COUNT as before
span = sa._span_pages()
self.assertGreater(alloc.flush_opportunistic(), 0) # still absorbed
self.assertLess(sa._span_pages(), span)
self.assertEqual(alloc.verify_byte_accounting(), [])
def test_transparency_still_exact_without_absorption(self):
"""Park-on-empty stays in `free` because it is sync-free -- a float
that empties must go transparent immediately, with no flush needed."""
alloc = self._tri()
v = alloc.alloc(4 * self.PS)
sa = alloc.swa_attn_allocator
self.assertFalse(sa._is_frontier_transparent())
from unittest import mock
with mock.patch.object(
torch.Tensor, "tolist", side_effect=AssertionError("tolist = D2H")
):
alloc.free_swa_segment(v, start_pos=0)
self.assertTrue(sa._is_frontier_transparent())
self.assertEqual(sa._hole_pages(), 0)
class TestTriFactorySizing(unittest.TestCase):
"""Factory-level contracts: byte-budget sizing, the bs=1 feasibility
floor, and the boot signature the GPU harness greps for (3 sub-pools,
swa grow=float)."""
def _factory_kwargs(self, **over):
import types
cp = types.SimpleNamespace(
shape=types.SimpleNamespace(conv=[(3, 8)], temporal=(0, 0, 0)),
dtype=types.SimpleNamespace(conv=torch.bfloat16, temporal=torch.float32),
layers=[0, 1],
)
kw = dict(
device=_DEV,
kv_cache_dtype=torch.float16,
head_num=2,
head_dim=4,
v_head_dim=4,
swa_head_num=2,
swa_head_dim=4,
swa_v_head_dim=4,
page_size=1,
start_layer=0,
end_layer=2,
swa_attention_layer_ids=[1],
full_attention_layer_ids=[0],
mamba_layer_ids=[0, 1],
mamba2_cache_params=cp,
full_max_total_num_tokens=64,
swa_max_total_num_tokens=32,
max_mamba_cache_size=4,
model_context_len=16,
extra_max_context_len=4,
max_num_reqs=4,
enable_memory_saver=False,
enable_mamba_extra_buffer=False,
disable_overlap_schedule=True,
need_sort=False,
)
kw.update(over)
return kw
def test_budget_sizing_and_boot_signature(self):
from sglang.srt.mem_cache.allocator.unified_hybrid_swa import (
UnifiedSWAAllocatorBase,
UnifiedSWATokenToKVPoolAllocator,
)
budget = 1 << 20
bundle = init_unified_mamba_swa_pools(
**self._factory_kwargs(unified_total_bytes=budget)
)
pool = bundle.unified_memory_pool
allocator = bundle.token_to_kv_pool_allocator
self.assertIsInstance(allocator, UnifiedSWAAllocatorBase)
self.assertNotIsInstance(allocator, UnifiedSWATokenToKVPoolAllocator)
# Buffer = budget + the state pool's bytes (budget captured AFTER the
# state carve-out), never the token-count re-sum.
state_bytes = 4 * pool.spec("mamba").entry_bytes()
self.assertEqual(pool.total_bytes, budget + state_bytes)
# Boot signature: 3 sub-pools in chain order, swa is the float.
self.assertEqual(len(pool.sub_pool_specs), 3)
self.assertEqual(
[(sp.name, sp.grow_direction) for sp in pool.sub_pool_specs],
[("mamba", "up"), ("swa", "float"), ("full", "down")],
)
def test_fallback_is_the_token_count_resum(self):
bundle = init_unified_mamba_swa_pools(**self._factory_kwargs())
pool = bundle.unified_memory_pool
want = (
64 * pool.spec("full").entry_bytes()
+ 32 * pool.spec("swa").entry_bytes()
+ 4 * pool.spec("mamba").entry_bytes()
)
self.assertEqual(pool.total_bytes, want)
def test_bs1_floor_fails_loud_before_construction(self):
"""A budget far below one worst-case request must raise BEFORE any
pool construction -- under-sizing is a retract LIVELOCK at runtime."""
with self.assertRaisesRegex(RuntimeError, "bs=1 floor"):
init_unified_mamba_swa_pools(
**self._factory_kwargs(
unified_total_bytes=1024, # << ctx * e_f alone
model_context_len=100_000,
sliding_window_size=64,
)
)
class TestTriPoolHardening(unittest.TestCase):
"""Pressure lanes: the planned-rebalance remedy in the alloc path (a
mis-positioned float must not fail an alloc that fits in total bytes),
retract-loop convergence through `check_decode_capacity`, and bounded copy
traffic under alternating end pressure.
"""
def _build(self, **kw):
return TestUnifiedTriPool._build(self, **kw)
def test_alloc_rebalances_a_blocking_float(self):
# Fill much of the high band so the midpoint-placed float walls the
# low band's free bytes off from `full`; the next alloc must SLIDE it
# rather than fail while total bytes suffice.
_, allocator, kvcache, _ = self._build(n_full=32, n_swa=24, n_state=8)
sa = allocator.swa_attn_allocator
v0 = allocator.alloc(4) # places the float at the region midpoint
self.assertIsNotNone(v0)
TestUnifiedTriPool._stamp(self, allocator, kvcache, v0)
# Exhaust the high band directly on the full end (full-only growth,
# e.g. long decode of already-admitted requests).
fa = allocator.full_attn_allocator
b_high_pages = fa._current_gap_bytes() // fa.entry_bytes_per_page
grab = fa.alloc(max(0, (b_high_pages - 2)))
self.assertIsNotNone(grab)
# The gate under-reports (no slide credit), so asking BEYOND it fires
# the remedy; the ask still fits the free bytes the LOW band holds.
avail = allocator.available_size()
need = avail + 4
live_before = sa._live_pages()
moves_before = len(sa._inverse_history)
v1 = allocator.alloc(need)
self.assertIsNotNone(
v1, "alloc must rebalance the blocking float instead of failing"
)
self.assertEqual(int(v1.numel()), need)
moved = sum(int(s.numel()) for s, _, _ in sa._inverse_history[moves_before:])
self.assertGreater(moved, 0, "the rebalance path must have fired")
# Cost bound min(L_live, G): never more than the live pages present
# when the slide ran (the leapfrog cap).
self.assertLessEqual(moved, live_before)
self.assertEqual(allocator.verify_byte_accounting(), [])
def test_check_decode_capacity_retract_convergence(self):
# Retract loop: token blocks freed one at a time until the next step
# fits; must converge before bs=1 and never report capacity early.
_, allocator, _, _ = self._build(n_full=32, n_swa=24, n_state=8)
reqs = []
while True:
v = allocator.alloc(4)
if v is None or allocator.available_size() < 4:
if v is not None:
reqs.append(v)
break
reqs.append(v)
self.assertGreater(len(reqs), 2)
# Pool saturated: a large decode step does not fit.
need = 16
while not allocator.check_decode_capacity(num_tokens=need, tree_cache=None):
self.assertGreater(len(reqs), 1, "retract must converge before bs=1")
allocator.free(reqs.pop())
self.assertGreaterEqual(allocator.available_size(), need)
self.assertEqual(allocator.verify_byte_accounting(), [])
def test_alternating_pressure_copy_traffic_bounded(self):
# Alternating full-grow / swa-churn: hole recycling and absorption do
# the steady-state work, so total float moves stay bounded.
_, allocator, kvcache, _ = self._build(n_full=48, n_swa=32, n_state=8)
sa = allocator.swa_attn_allocator
fa = allocator.full_attn_allocator
total_alloc_pages = 0
for _ in range(6):
v = allocator.alloc(8)
self.assertIsNotNone(v)
total_alloc_pages += 8
TestUnifiedTriPool._stamp(self, allocator, kvcache, v)
allocator.free_swa(v) # window slide: tombstones -> holes/absorb
g = fa.alloc(4) # full-side decode growth
self.assertIsNotNone(g)
total_alloc_pages += 4
fa.free(g)
moved = sum(int(s.numel()) for s, _, _ in sa._inverse_history)
self.assertLessEqual(
moved,
total_alloc_pages // 2,
"steady-state churn must be predominantly zero-copy",
)
self.assertEqual(allocator.verify_byte_accounting(), [])
def test_joint_eviction_loop_stops_without_progress(self):
# tree_cache=None: the default helper no-ops; the bounded loop must
# return promptly (no infinite re-check) and the gate reports honestly.
_, allocator, _, _ = self._build()
big = allocator.available_size() + 64
self.assertFalse(
allocator.check_decode_capacity(num_tokens=big, tree_cache=None)
)
class TestJointCapacityIsHonoured(unittest.TestCase):
"""`alloc(available_size())` must never fail.
REGRESSION: the joint predicate priced the swa float's extension in RAW
BYTES, but `take_physical_pages` yields only whole pages on the float's
OWN grid, whose low edge is rounded up to a multiple of the NEIGHBOUR's
entry size -- so the budget credited a page the grid could not yield.
Swept over geometries so a symmetric mistake on the FULL side surfaces
here too.
"""
def _build(self, *, page_size, n_full, n_swa, n_state, lazy, specs):
full, swa, mamba = specs
total = (
n_full * full.entry_bytes()
+ n_swa * swa.entry_bytes()
+ n_state * mamba.entry_bytes()
)
pool = UnifiedKVPool(
total_bytes=total,
sub_pool_specs=[full, swa, mamba],
device=_DEV,
enable_memory_saver=False,
page_size=page_size,
)
kvcache = _FakeUnifiedSWAKVPool(pool)
return pool, UnifiedMambaSWATokenToKVPoolAllocator(
unified_buffer=pool,
kvcache=kvcache,
mamba_kvcache=_FakeKVCache(pool.max_slots("mamba")),
device=_DEV,
full_max_total_num_tokens=n_full,
swa_max_total_num_tokens=n_swa,
need_sort=False,
forward_stream=None,
lazy_compaction=lazy,
)
def test_fresh_boot_alloc_of_available_size_succeeds(self):
# Geometries chosen so the mamba end's frontier (a multiple of the
# STATE entry size) lands off the swa float's page grid.
for page_size in (1, 2, 4):
for fl, sl, ml in ((4, 3, 1), (4, 2, 2), (6, 3, 1), (3, 5, 2)):
for n_full, n_swa, n_state in ((24, 16, 4), (32, 16, 8), (20, 12, 6)):
for lazy in (False, True):
specs = _tri_specs(
full_layer_num=fl,
swa_layer_num=sl,
state_layer_num=ml,
head_num=1,
head_dim=8,
)
with self.subTest(
ps=page_size,
layers=(fl, sl, ml),
n=(n_full, n_swa, n_state),
lazy=lazy,
):
_pool, alloc = self._build(
page_size=page_size,
n_full=n_full * page_size,
n_swa=n_swa * page_size,
n_state=n_state,
lazy=lazy,
specs=specs,
)
n = alloc.available_size()
if n <= 0:
continue
# The whole point: the number the scheduler reads
# must be allocatable, with no backstop assert.
out = alloc.alloc(n)
self.assertIsNotNone(
out,
f"alloc(available_size()={n}) returned None",
)
self.assertEqual(out.numel(), n)
# Both sides bound for every allocated virtual id.
fa = alloc.full_attn_allocator
sa = alloc.swa_attn_allocator
self.assertTrue(
bool((fa.virtual_to_physical[out] >= 0).all())
)
self.assertTrue(
bool((sa.virtual_to_physical[out] >= 0).all())
)
def test_available_size_never_exceeds_the_float_page_grid(self):
"""Direct form: the joint answer, converted to float pages, must fit
inside what `_region_bounds_pages` actually offers."""
for page_size in (1, 4):
specs = _tri_specs(
full_layer_num=4,
swa_layer_num=3,
state_layer_num=1,
head_num=1,
head_dim=8,
)
_pool, alloc = self._build(
page_size=page_size,
n_full=24 * page_size,
n_swa=16 * page_size,
n_state=4,
lazy=False,
specs=specs,
)
sa = alloc.swa_attn_allocator
n_pages = alloc.available_size() // page_size
lo, hi = sa._region_bounds_pages()
with self.subTest(ps=page_size):
self.assertLessEqual(
n_pages - sa._hole_pages(),
max(0, hi - lo),
"joint available_size() promises more float pages than the "
"float's own page grid can yield",
)
class TestFloatRelocationIsOrderedAgainstTheForward(unittest.TestCase):
"""Float relocation must settle the in-flight forward BEFORE its first copy.
REGRESSION: `make_room` / `compact_holes` issued `move_kv_cache` and
rebound `virtual_to_physical` with no ordering against the running
forward, so a copy could carry pre-write bytes and every later reader saw
a destination that never received those writes -- silently wrong KV, no
crash. The END pools guard the same hazard via `_settle_inflight_forward`.
"""
def _tri(self, lazy=True):
full, swa, mamba = _tri_specs(head_num=1, head_dim=8)
total = (
48 * full.entry_bytes() + 32 * swa.entry_bytes() + 8 * mamba.entry_bytes()
)
pool = UnifiedKVPool(
total_bytes=total,
sub_pool_specs=[full, swa, mamba],
device=_DEV,
enable_memory_saver=False,
)
kvcache = _FakeUnifiedSWAKVPool(pool)
alloc = UnifiedMambaSWATokenToKVPoolAllocator(
unified_buffer=pool,
kvcache=kvcache,
mamba_kvcache=_FakeKVCache(pool.max_slots("mamba")),
device=_DEV,
full_max_total_num_tokens=48,
swa_max_total_num_tokens=32,
need_sort=False,
forward_stream=None,
lazy_compaction=lazy,
)
return pool, alloc, kvcache
def _trace(self, flt):
"""Record the order of (settle, move) on the float."""
order = []
real_settle = flt._settle_inflight_forward
real_move = flt._move_pages_and_rebind
def settle():
order.append("settle")
return real_settle()
def move(src, dst):
order.append("move")
return real_move(src, dst)
flt._settle_inflight_forward = settle
flt._move_pages_and_rebind = move
return order
def test_make_room_settles_before_the_first_move(self):
_pool, alloc, _kv = self._tri()
flt = alloc.swa_attn_allocator
# Occupy the float, then free an interior page so a relocation has
# something to move and somewhere to move it.
v = alloc.alloc(12)
self.assertIsNotNone(v)
alloc.free(v[:4])
order = self._trace(flt)
flt.make_room(side="low", min_bytes=flt.entry_bytes_per_page)
self.assertIn("settle", order, "make_room never settled the forward")
if "move" in order:
self.assertLess(
order.index("settle"),
order.index("move"),
f"a copy was issued before the settle: {order}",
)
def test_compact_holes_settles_before_the_first_move(self):
_pool, alloc, _kv = self._tri()
flt = alloc.swa_attn_allocator
v = alloc.alloc(12)
self.assertIsNotNone(v)
alloc.free(v[2:6]) # interior holes, so compact_holes has work
order = self._trace(flt)
flt.compact_holes(retreat_side="high")
if not order:
self.skipTest("no holes reached compact_holes in this geometry")
self.assertEqual(order[0], "settle", f"first action was not a settle: {order}")
def test_the_settle_is_a_stream_wait_not_a_host_sync(self):
"""The settle before a float move must be a stream wait: a host sync
there would land on the alloc-shortfall path every time the float moves."""
src = inspect.getsource(MultiEndedAllocator._settle_inflight_forward)
self.assertIn("wait_event", src)
self.assertNotIn(".item()", src)
self.assertNotIn("synchronize()", src)
class TestFloatHoleCreditIsPerSide(unittest.TestCase):
"""A float's schedulable credit must follow the side the holes are on.
REGRESSION: the base `_peer_drainable_hole_bytes` picks its neighbour from
`grow_direction`, which for a float is "float" -- it fell through to
`low_peer`, never saw the HIGH neighbour, and its single scalar was added
to `max(gap_low, gap_high)`, landing a LOW neighbour's holes on the HIGH
gap. Over-reporting `schedulable_available_size` admits work the shortfall
ladder cannot satisfy.
"""
def _float(self):
full, swa, mamba = _tri_specs(head_num=1, head_dim=8)
total = (
48 * full.entry_bytes() + 32 * swa.entry_bytes() + 8 * mamba.entry_bytes()
)
pool = UnifiedKVPool(
total_bytes=total,
sub_pool_specs=[full, swa, mamba],
device=_DEV,
enable_memory_saver=False,
)
alloc = UnifiedMambaSWATokenToKVPoolAllocator(
unified_buffer=pool,
kvcache=_FakeUnifiedSWAKVPool(pool),
mamba_kvcache=_FakeKVCache(pool.max_slots("mamba")),
device=_DEV,
full_max_total_num_tokens=48,
swa_max_total_num_tokens=32,
need_sort=False,
forward_stream=None,
lazy_compaction=True,
)
return alloc, alloc.swa_attn_allocator
def test_schedulable_never_exceeds_the_sum_of_the_two_sides(self):
"""Upper bound that the undirected scalar could violate: no side may be
credited with the other side's holes on top of its own gap."""
alloc, flt = self._float()
v = alloc.alloc(10)
self.assertIsNotNone(v)
alloc.free(v[:3])
epp = flt.entry_bytes_per_page
gap_low, gap_high = flt._gap_pages()
c_low = flt._side_drainable_hole_bytes("low") // epp
c_high = flt._side_drainable_hole_bytes("high") // epp
bound = (
min(
max(gap_low + c_low, gap_high + c_high),
flt.num_pages - flt.min_page_index - flt._live_pages(),
)
+ flt._hole_pages()
) * flt.page_size
self.assertLessEqual(flt.schedulable_available_size(), bound)
def test_memo_verifier_agrees_with_the_per_side_formula(self):
"""The staleness verifier recomputes through the same entry, so the
override must not make the memo look stale."""
_alloc, flt = self._float()
flt.available_size()
flt.schedulable_available_size()
self.assertEqual(flt._byte_accounting_violations(), [])
class TestPreallocIsPricedOnTheSharedGrid(unittest.TestCase):
"""REGRESSION: PD admission compared FULL and SWA against per-side token
budgets, but each side's `available_size` credits the peer's drainable
holes, so a pair that each side can host alone can be jointly infeasible.
Such a pair was admitted and then refused inside `alloc_extend_swa_tail`."""
def _build(self, **kw):
return TestUnifiedTriPool._build(self, **kw)
def test_a_pair_each_side_can_host_alone_is_still_refused(self):
# page_size 1 leaves no slack between the per-side and joint views;
# the double-count only has room to show on a paged grid.
_, allocator, _, _ = self._build(page_size=4)
full_demand = allocator.full_available_size()
swa_demand = allocator.swa_available_size()
self.assertGreater(min(full_demand, swa_demand), 0)
# Each side alone reports room for its own half ...
self.assertLessEqual(full_demand, allocator.full_available_size())
self.assertLessEqual(swa_demand, allocator.swa_available_size())
# ... yet the two draw on the same bytes, so the grid refuses the pair.
self.assertFalse(
allocator._fits_page_demand(
-(-full_demand // allocator.page_size),
-(-swa_demand // allocator.page_size),
)
)
self.assertFalse(
allocator.prealloc_fits(
MagicMock(),
full_demand,
swa_demand,
full_budget_tokens=full_demand,
swa_budget_tokens=swa_demand,
)
)
def test_the_scheduler_budget_still_binds(self):
_, allocator, _, _ = self._build()
page_size = allocator.page_size
self.assertTrue(
allocator.prealloc_fits(
MagicMock(),
page_size,
page_size,
full_budget_tokens=page_size,
swa_budget_tokens=page_size,
)
)
self.assertFalse(
allocator.prealloc_fits(
MagicMock(),
page_size,
page_size,
full_budget_tokens=page_size - 1,
swa_budget_tokens=page_size,
)
)
if __name__ == "__main__":
unittest.main()