CP shared-KV bs>1 cache-hit loads already merge request load ops, but the host pool still rebuilt layer-invariant mapping work from the same host/device indices. Introduce a PreparedLoadDescriptor lifecycle around begin/end load, wire MLA KV and NSA index H2D loads through tai-kernel prepared submit when available, and add timing hooks plus regression coverage for descriptor reuse and explicit fallback logging. Record the P4/P6b design and benchmark results in the advanced feature notes. Constraint: Radix residency and allocator decisions remain synchronous; only the data-transfer descriptor is prepared for per-layer async submit. Constraint: Production fast path must not silently fall back when tai prepared H2D support is missing. Rejected: Cross-batch descriptor reuse | descriptor lifetime and tensor ownership are only safe within one load operation. Rejected: Change L2->L1 scheduling to layer-ahead prefetch in this commit | that is a separate lifecycle change after descriptor reuse is stable. Confidence: medium Scope-risk: moderate Directive: Keep LayerDoneCounter per-layer readiness semantics; do not replace with all-layer waits. Tested: python -m py_compile python/sglang/srt/mem_cache/memory_pool_host.py python/sglang/srt/managers/cache_controller.py Tested: Remote g0034:cjy-glm5-new PYTHONPATH=python python -m pytest -q test/registered/unit/managers/test_hicache_controller_cp.py (88 passed) Tested: Remote tai-kernel prepared descriptor CUDA test (6 passed) and P4 benchmark full matrix (90 rows) Not-tested: ETE replay/GSM8K cache-hit correctness after this commit Not-tested: Layer-ahead L2->L1 prefetch scheduling Co-authored-by: OmX <omx@oh-my-codex.dev>
2466 lines
103 KiB
Python
2466 lines
103 KiB
Python
import sys
|
|
import types
|
|
from unittest import main
|
|
from unittest.mock import patch
|
|
|
|
import torch
|
|
|
|
try:
|
|
import sgl_kernel # noqa: F401
|
|
import sgl_kernel.kvcacheio # noqa: F401
|
|
except (ImportError, RuntimeError):
|
|
if "sgl_kernel" not in sys.modules:
|
|
sys.modules["sgl_kernel"] = types.ModuleType("sgl_kernel")
|
|
sys.modules["sgl_kernel"].__file__ = "sgl_kernel_stub.py"
|
|
sys.modules["sgl_kernel"].__path__ = []
|
|
if not hasattr(sys.modules["sgl_kernel"], "__getattr__"):
|
|
|
|
def _sgl_kernel_getattr(name):
|
|
if name.startswith("__"):
|
|
raise AttributeError(name)
|
|
fn = lambda *args, **kwargs: None
|
|
setattr(sys.modules["sgl_kernel"], name, fn)
|
|
return fn
|
|
|
|
sys.modules["sgl_kernel"].__getattr__ = _sgl_kernel_getattr
|
|
if "sgl_kernel.quantization" not in sys.modules:
|
|
quantization_stub = types.ModuleType("sgl_kernel.quantization")
|
|
quantization_stub.__file__ = "sgl_kernel_quantization_stub.py"
|
|
|
|
def _quantization_getattr(name):
|
|
if name.startswith("__"):
|
|
raise AttributeError(name)
|
|
fn = lambda *args, **kwargs: None
|
|
setattr(quantization_stub, name, fn)
|
|
return fn
|
|
|
|
quantization_stub.__getattr__ = _quantization_getattr
|
|
for name in (
|
|
"ggml_dequantize",
|
|
"ggml_moe_a8",
|
|
"ggml_moe_a8_vec",
|
|
"ggml_moe_get_block_size",
|
|
"ggml_mul_mat_a8",
|
|
"ggml_mul_mat_vec_a8",
|
|
):
|
|
setattr(quantization_stub, name, lambda *args, **kwargs: None)
|
|
sys.modules["sgl_kernel.quantization"] = quantization_stub
|
|
for name in (
|
|
"sgl_per_token_group_quant_8bit",
|
|
"sgl_per_token_group_quant_fp8",
|
|
"sgl_per_token_quant_fp8",
|
|
"fp8_blockwise_scaled_mm",
|
|
"fp8_scaled_mm",
|
|
"silu_and_mul",
|
|
):
|
|
if not hasattr(sys.modules["sgl_kernel"], name):
|
|
setattr(sys.modules["sgl_kernel"], name, lambda *args, **kwargs: None)
|
|
_sgl_kernel_lib = torch.library.Library("sgl_kernel", "FRAGMENT")
|
|
for _schema in (
|
|
"sgl_per_token_group_quant_8bit(Tensor input, Tensor(a!) output_q, Tensor(b!) output_s, int group_size, float eps, float fp8_min, float fp8_max, bool scale_ue8m0) -> ()",
|
|
"sgl_per_token_group_quant_fp8(Tensor input, Tensor(a!) output_q, Tensor(b!) output_s, int group_size, float eps, float fp8_min, float fp8_max, bool scale_ue8m0) -> ()",
|
|
"sgl_per_token_quant_fp8(Tensor input, Tensor(a!) output_q, Tensor(b!) output_s) -> ()",
|
|
"fp8_scaled_mm(Tensor mat_a, Tensor mat_b, Tensor scales_a, Tensor scales_b, ScalarType out_dtype, Tensor? bias=None) -> Tensor",
|
|
"fp8_blockwise_scaled_mm(Tensor mat_a, Tensor mat_b, Tensor scales_a, Tensor scales_b, ScalarType out_dtype) -> Tensor",
|
|
):
|
|
try:
|
|
_sgl_kernel_lib.define(_schema)
|
|
except RuntimeError as exc:
|
|
if (
|
|
"already" not in str(exc).lower()
|
|
and "duplicate" not in str(exc).lower()
|
|
):
|
|
raise
|
|
if "sgl_kernel.kvcacheio" not in sys.modules:
|
|
kvcacheio_stub = types.ModuleType("sgl_kernel.kvcacheio")
|
|
for name in (
|
|
"transfer_kv_all_layer",
|
|
"transfer_kv_all_layer_direct_lf_pf",
|
|
"transfer_kv_all_layer_lf_pf",
|
|
"transfer_kv_all_layer_lf_ph",
|
|
"transfer_kv_all_layer_mla",
|
|
"transfer_kv_all_layer_mla_lf_pf",
|
|
"transfer_kv_direct",
|
|
"transfer_kv_per_layer",
|
|
"transfer_kv_per_layer_direct_pf_lf",
|
|
"transfer_kv_per_layer_mla",
|
|
"transfer_kv_per_layer_mla_pf_lf",
|
|
"transfer_kv_per_layer_pf_lf",
|
|
"transfer_kv_per_layer_ph_lf",
|
|
):
|
|
setattr(kvcacheio_stub, name, lambda *args, **kwargs: None)
|
|
sys.modules["sgl_kernel.kvcacheio"] = kvcacheio_stub
|
|
|
|
_sgl_kernel_lib = torch.library.Library("sgl_kernel", "FRAGMENT")
|
|
for _schema in (
|
|
"sgl_per_token_group_quant_8bit(Tensor input, Tensor(a!) output_q, Tensor(b!) output_s, int group_size, float eps, float fp8_min, float fp8_max, bool scale_ue8m0) -> ()",
|
|
"sgl_per_token_group_quant_fp8(Tensor input, Tensor(a!) output_q, Tensor(b!) output_s, int group_size, float eps, float fp8_min, float fp8_max, bool scale_ue8m0) -> ()",
|
|
"sgl_per_token_quant_fp8(Tensor input, Tensor(a!) output_q, Tensor(b!) output_s) -> ()",
|
|
"fp8_scaled_mm(Tensor mat_a, Tensor mat_b, Tensor scales_a, Tensor scales_b, ScalarType out_dtype, Tensor? bias=None) -> Tensor",
|
|
"fp8_blockwise_scaled_mm(Tensor mat_a, Tensor mat_b, Tensor scales_a, Tensor scales_b, ScalarType out_dtype) -> Tensor",
|
|
):
|
|
try:
|
|
_sgl_kernel_lib.define(_schema)
|
|
except RuntimeError as exc:
|
|
if "already" not in str(exc).lower() and "duplicate" not in str(exc).lower():
|
|
raise
|
|
|
|
from sglang.srt.managers.cache_controller import CacheOperation, HiCacheController
|
|
from sglang.srt.mem_cache.cp_shared_kv_layout import CpSharedKVLayout
|
|
from sglang.srt.mem_cache.hiradix_cache import CpHiCacheNodeMetadata
|
|
from sglang.srt.mem_cache.memory_pool_host import (
|
|
HostKVCache,
|
|
MHATokenToKVPoolHost,
|
|
MLATokenToKVPoolHost,
|
|
NSATokenToKVPoolHost,
|
|
PreparedLoadDescriptor,
|
|
)
|
|
from sglang.srt.mem_cache.radix_cache import TreeNode
|
|
from sglang.test.ci.ci_register import register_cpu_ci
|
|
from sglang.test.test_utils import CustomTestCase
|
|
|
|
register_cpu_ci(est_time=2, suite="stage-a-test-cpu")
|
|
|
|
|
|
class FakeHostPool:
|
|
def __init__(self, alloc_result):
|
|
self.alloc_result = alloc_result
|
|
self.alloc_calls = []
|
|
self.backups = []
|
|
self.layer_backups = []
|
|
self.loads = []
|
|
self.frees = []
|
|
self.page_size = 4
|
|
self.layout = "page_first_direct"
|
|
|
|
def alloc(self, need_size):
|
|
self.alloc_calls.append(need_size)
|
|
if self.alloc_result is None:
|
|
return None
|
|
return self.alloc_result[:need_size].clone()
|
|
|
|
def backup_from_device_all_layer(
|
|
self, device_pool, host_indices, device_indices, io_backend
|
|
):
|
|
self.backups.append((host_indices.clone(), device_indices.clone(), device_pool))
|
|
|
|
def backup_from_device_per_layer(
|
|
self, device_pool, host_indices, device_indices, layer_id, io_backend
|
|
):
|
|
self.layer_backups.append(
|
|
(host_indices.clone(), device_indices.clone(), layer_id, device_pool)
|
|
)
|
|
|
|
def load_to_device_per_layer(
|
|
self, device_pool, host_indices, device_indices, layer_id, io_backend
|
|
):
|
|
self.loads.append(
|
|
(host_indices.clone(), device_indices.clone(), layer_id, device_pool)
|
|
)
|
|
|
|
def free(self, indices):
|
|
self.frees.append(indices.clone())
|
|
return len(indices)
|
|
|
|
|
|
class ContiguousPreferredHostPool(FakeHostPool):
|
|
def __init__(self, alloc_result):
|
|
super().__init__(alloc_result)
|
|
self.contiguous_alloc_calls = []
|
|
|
|
def alloc_contiguous_preferred(self, need_size):
|
|
self.contiguous_alloc_calls.append(need_size)
|
|
if self.alloc_result is None:
|
|
return None
|
|
return self.alloc_result[:need_size].clone()
|
|
|
|
|
|
class DummyHostKVCacheForAlloc(HostKVCache):
|
|
def get_size_per_token(self):
|
|
return 1
|
|
|
|
def init_kv_buffer(self):
|
|
return None
|
|
|
|
def load_to_device_per_layer(
|
|
self, device_pool, host_indices, device_indices, layer_id, io_backend
|
|
) -> None:
|
|
pass
|
|
|
|
def backup_from_device_per_layer(
|
|
self, device_pool, host_indices, device_indices, layer_id, io_backend
|
|
) -> None:
|
|
pass
|
|
|
|
def backup_from_device_all_layer(
|
|
self, device_pool, host_indices, device_indices, io_backend
|
|
) -> None:
|
|
pass
|
|
|
|
def get_data_page(self, index, flat: bool = True) -> torch.Tensor:
|
|
return torch.empty((0,), dtype=torch.uint8)
|
|
|
|
def get_dummy_flat_data_page(self) -> torch.Tensor:
|
|
return torch.empty((0,), dtype=torch.uint8)
|
|
|
|
def set_from_flat_data_page(self, index: int, data_page: torch.Tensor) -> None:
|
|
pass
|
|
|
|
|
|
class TestPreparedLoadDescriptor(CustomTestCase):
|
|
def test_host_begin_load_builds_page_aligned_descriptor(self):
|
|
host_pool = DummyHostKVCacheForAlloc.__new__(DummyHostKVCacheForAlloc)
|
|
host_pool.page_size = 4
|
|
host_pool.layout = "page_first_direct"
|
|
|
|
host_indices = torch.tensor([8, 9, 10, 11, 20, 21, 22, 23], dtype=torch.int64)
|
|
device_indices = torch.tensor(
|
|
[40, 41, 42, 43, 64, 65, 66, 67], dtype=torch.int64
|
|
)
|
|
|
|
host_pool.begin_load_to_device_op(
|
|
host_indices, device_indices, io_backend="direct"
|
|
)
|
|
|
|
desc = host_pool._active_load_descriptor
|
|
self.assertIsInstance(desc, PreparedLoadDescriptor)
|
|
self.assertTrue(torch.equal(desc.host_indices, host_indices))
|
|
self.assertTrue(torch.equal(desc.device_indices, device_indices))
|
|
self.assertEqual(desc.num_tokens, 8)
|
|
self.assertEqual(desc.num_pages, 2)
|
|
self.assertEqual(desc.layout, "page_first_direct")
|
|
self.assertEqual(desc.io_backend, "direct")
|
|
self.assertEqual(desc.host_page_indices.tolist(), [2, 5])
|
|
self.assertEqual(desc.device_page_indices.tolist(), [10, 16])
|
|
|
|
host_pool.end_load_to_device_op()
|
|
self.assertIsNone(host_pool._active_load_descriptor)
|
|
|
|
def test_nsa_begin_load_attaches_indexer_pages_to_descriptor(self):
|
|
host_pool = NSATokenToKVPoolHost.__new__(NSATokenToKVPoolHost)
|
|
host_pool.page_size = 4
|
|
host_pool.layout = "page_first_direct"
|
|
host_pool.index_active_layer_ids = (0, 2)
|
|
|
|
host_indices = torch.tensor([8, 9, 10, 11, 20, 21, 22, 23], dtype=torch.int64)
|
|
device_indices = torch.tensor(
|
|
[40, 41, 42, 43, 64, 65, 66, 67], dtype=torch.int64
|
|
)
|
|
|
|
host_pool.begin_load_to_device_op(
|
|
host_indices, device_indices, io_backend="direct"
|
|
)
|
|
|
|
desc = host_pool._active_load_descriptor
|
|
self.assertEqual(desc.index_active_layer_ids, (0, 2))
|
|
self.assertEqual(desc.index_host_page_indices.tolist(), [2, 5])
|
|
self.assertEqual(desc.index_device_page_indices.tolist(), [10, 16])
|
|
self.assertIs(host_pool._active_load_indexer_page_indices[0], desc.index_host_page_indices)
|
|
self.assertIs(
|
|
host_pool._active_load_indexer_page_indices[1],
|
|
desc.index_device_page_indices,
|
|
)
|
|
|
|
host_pool.end_load_to_device_op()
|
|
self.assertIsNone(host_pool._active_load_descriptor)
|
|
self.assertIsNone(host_pool._active_load_indexer_page_indices)
|
|
|
|
def test_missing_direct_load_descriptor_warns_once(self):
|
|
host_pool = DummyHostKVCacheForAlloc.__new__(DummyHostKVCacheForAlloc)
|
|
host_pool.page_size = 4
|
|
host_pool.layout = "page_first_direct"
|
|
|
|
with self.assertLogs(
|
|
"sglang.srt.mem_cache.memory_pool_host", level="WARNING"
|
|
) as logs:
|
|
self.assertIsNone(host_pool._get_active_load_descriptor("direct"))
|
|
self.assertIsNone(host_pool._get_active_load_descriptor("direct"))
|
|
|
|
warnings = [
|
|
line for line in logs.output if "missing_prepared_descriptor" in line
|
|
]
|
|
self.assertEqual(len(warnings), 1)
|
|
|
|
def test_mla_direct_load_uses_prepared_tai_descriptor_when_available(self):
|
|
calls = []
|
|
fake_desc = object()
|
|
|
|
def fake_prepare(src_indices, dst_indices, **kwargs):
|
|
calls.append(("prepare", src_indices.clone(), dst_indices.clone(), kwargs))
|
|
return fake_desc
|
|
|
|
def fake_submit(desc, src_ptrs, dst_ptrs, **kwargs):
|
|
calls.append(("submit", desc, src_ptrs, dst_ptrs, kwargs))
|
|
|
|
def fake_destroy(desc):
|
|
calls.append(("destroy", desc))
|
|
|
|
host_pool = MLATokenToKVPoolHost.__new__(MLATokenToKVPoolHost)
|
|
host_pool.layout = "page_first_direct"
|
|
host_pool.page_size = 4
|
|
host_pool.kv_buffer = torch.empty((8, 3, 4, 1, 16), dtype=torch.uint8)
|
|
device_pool = type("DevicePool", (), {})()
|
|
device_pool.kv_buffer = torch.empty((3, 32, 1, 16), dtype=torch.uint8)
|
|
host_indices = torch.tensor([4, 5, 6, 7], dtype=torch.int64)
|
|
device_indices = torch.tensor([12, 13, 14, 15], dtype=torch.int64)
|
|
|
|
with (
|
|
patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_prepare_h2d_page_descriptor",
|
|
return_value=fake_prepare,
|
|
),
|
|
patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_submit_h2d_layer",
|
|
return_value=fake_submit,
|
|
),
|
|
patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_destroy_h2d_page_descriptor",
|
|
return_value=fake_destroy,
|
|
),
|
|
):
|
|
host_pool.begin_load_to_device_op(
|
|
host_indices, device_indices, io_backend="direct"
|
|
)
|
|
host_pool.load_to_device_per_layer(
|
|
device_pool,
|
|
host_indices,
|
|
device_indices,
|
|
layer_id=2,
|
|
io_backend="direct",
|
|
)
|
|
host_pool.end_load_to_device_op()
|
|
|
|
self.assertEqual(calls[0][0], "prepare")
|
|
self.assertEqual(calls[0][1].tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(calls[0][2].tolist(), [12, 13, 14, 15])
|
|
self.assertEqual(calls[0][3], {"page_size": 4, "layout": "page_first_direct"})
|
|
self.assertEqual(calls[1][0], "submit")
|
|
self.assertIs(calls[1][1], fake_desc)
|
|
self.assertEqual(calls[1][2][0].data_ptr(), host_pool.kv_buffer.data_ptr())
|
|
self.assertEqual(
|
|
calls[1][3][0].data_ptr(), device_pool.kv_buffer[2].data_ptr()
|
|
)
|
|
self.assertEqual(calls[1][4], {"layer_id": 2})
|
|
self.assertEqual(calls[2], ("destroy", fake_desc))
|
|
|
|
def test_nsa_index_direct_load_uses_prepared_tai_index_descriptor(self):
|
|
calls = []
|
|
fake_base_desc = object()
|
|
fake_index_desc = object()
|
|
|
|
def fake_prepare(src_indices, dst_indices, **kwargs):
|
|
calls.append(("prepare", src_indices.clone(), dst_indices.clone(), kwargs))
|
|
if kwargs["page_size"] == 1:
|
|
return fake_index_desc
|
|
return fake_base_desc
|
|
|
|
def fake_submit(desc, src_ptrs, dst_ptrs, **kwargs):
|
|
calls.append(("submit", desc, src_ptrs, dst_ptrs, kwargs))
|
|
|
|
host_pool = NSATokenToKVPoolHost.__new__(NSATokenToKVPoolHost)
|
|
host_pool.layout = "page_first_direct"
|
|
host_pool.page_size = 4
|
|
host_pool.index_active_layer_ids = (0, 1, 2)
|
|
host_pool.index_k_with_scale_buffer = torch.empty(
|
|
(8, 3, 1, 32), dtype=torch.uint8
|
|
)
|
|
device_pool = type("DevicePool", (), {})()
|
|
device_pool.index_k_with_scale_buffer = torch.empty(
|
|
(3, 8, 32), dtype=torch.uint8
|
|
)
|
|
host_indices = torch.tensor([4, 5, 6, 7], dtype=torch.int64)
|
|
device_indices = torch.tensor([12, 13, 14, 15], dtype=torch.int64)
|
|
|
|
with (
|
|
patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_prepare_h2d_page_descriptor",
|
|
return_value=fake_prepare,
|
|
),
|
|
patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_submit_h2d_layer",
|
|
return_value=fake_submit,
|
|
),
|
|
):
|
|
host_pool.begin_load_to_device_op(
|
|
host_indices, device_indices, io_backend="direct"
|
|
)
|
|
host_pool._load_indexer_to_device_per_layer(
|
|
device_pool,
|
|
host_indices,
|
|
device_indices,
|
|
layer_id=1,
|
|
io_backend="direct",
|
|
)
|
|
|
|
self.assertEqual([c[0] for c in calls[:2]], ["prepare", "prepare"])
|
|
self.assertEqual(calls[1][1].tolist(), [1])
|
|
self.assertEqual(calls[1][2].tolist(), [3])
|
|
self.assertEqual(calls[1][3], {"page_size": 1, "layout": "page_first_direct"})
|
|
self.assertEqual(calls[2][0], "submit")
|
|
self.assertIs(calls[2][1], fake_index_desc)
|
|
self.assertEqual(
|
|
calls[2][2][0].data_ptr(), host_pool.index_k_with_scale_buffer.data_ptr()
|
|
)
|
|
self.assertEqual(
|
|
calls[2][3][0].data_ptr(),
|
|
device_pool.index_k_with_scale_buffer[1].data_ptr(),
|
|
)
|
|
self.assertEqual(calls[2][4], {"layer_id": 1})
|
|
|
|
|
|
class FakeDevicePool:
|
|
device = "cpu"
|
|
layer_num = 1
|
|
|
|
def __init__(self, name="target", layer_num=1):
|
|
self.name = name
|
|
self.layer_num = layer_num
|
|
self.layer_backup_notifiers = []
|
|
|
|
def register_layer_transfer_counter(self, counter):
|
|
self.counter = counter
|
|
|
|
def register_layer_backup_notifier(self, notifier):
|
|
self.layer_backup_notifiers.append(notifier)
|
|
|
|
def notify_layer_end_for_backup(self, layer_id):
|
|
for notifier in self.layer_backup_notifiers:
|
|
notifier(layer_id)
|
|
|
|
|
|
class TestPageFirstPerLayerBackupTaiKernel(CustomTestCase):
|
|
def test_mla_page_first_per_layer_backup_uses_tai_lf_pf_kernel(self):
|
|
calls = []
|
|
|
|
def fake_kernel(src, dst, src_indices, dst_indices, **kwargs):
|
|
calls.append((src, dst, src_indices.clone(), dst_indices.clone(), kwargs))
|
|
|
|
host_pool = MLATokenToKVPoolHost.__new__(MLATokenToKVPoolHost)
|
|
host_pool.layout = "page_first"
|
|
host_pool.token_stride_size = 16
|
|
host_pool.layout_dim = 64
|
|
host_pool.kv_buffer = torch.empty((32, 4, 1, 16), dtype=torch.uint8)
|
|
device_pool = type("DevicePool", (), {})()
|
|
device_pool.kv_buffer = torch.empty((4, 32, 1, 16), dtype=torch.uint8)
|
|
host_indices = torch.tensor([4, 5, 6, 7], dtype=torch.int64)
|
|
device_indices = torch.tensor([12, 13, 14, 15], dtype=torch.int64)
|
|
|
|
with patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_transfer_kv_per_layer_mla_lf_pf",
|
|
return_value=fake_kernel,
|
|
):
|
|
host_pool.backup_from_device_per_layer(
|
|
device_pool,
|
|
host_indices,
|
|
device_indices,
|
|
layer_id=2,
|
|
io_backend="kernel",
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
src, dst, src_indices, dst_indices, kwargs = calls[0]
|
|
expected_src = device_pool.kv_buffer[2]
|
|
self.assertEqual(src.data_ptr(), expected_src.data_ptr())
|
|
self.assertEqual(src.shape, expected_src.shape)
|
|
self.assertEqual(src.stride(), expected_src.stride())
|
|
self.assertIs(dst, host_pool.kv_buffer)
|
|
self.assertEqual(src_indices.tolist(), [12, 13, 14, 15])
|
|
self.assertEqual(dst_indices.tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(kwargs["layer_id"], 2)
|
|
self.assertEqual(kwargs["item_size"], 16)
|
|
self.assertEqual(kwargs["dst_layout_dim"], 64)
|
|
|
|
def test_nsa_indexer_page_first_per_layer_backup_uses_tai_lf_pf_kernel(self):
|
|
calls = []
|
|
|
|
def fake_kernel(src, dst, src_indices, dst_indices, **kwargs):
|
|
calls.append((src, dst, src_indices.clone(), dst_indices.clone(), kwargs))
|
|
|
|
host_pool = NSATokenToKVPoolHost.__new__(NSATokenToKVPoolHost)
|
|
host_pool.layout = "page_first"
|
|
host_pool.page_size = 4
|
|
host_pool.indexer_page_stride_size = 32
|
|
host_pool.indexer_layout_dim = 96
|
|
host_pool.index_k_with_scale_buffer = torch.empty(
|
|
(16, 3, 1, 32), dtype=torch.uint8
|
|
)
|
|
device_pool = type("DevicePool", (), {})()
|
|
device_pool.index_k_with_scale_buffer = torch.empty(
|
|
(3, 16, 32), dtype=torch.uint8
|
|
)
|
|
host_indices = torch.tensor([8, 9, 10, 11, 20, 21, 22, 23], dtype=torch.int64)
|
|
device_indices = torch.tensor(
|
|
[12, 13, 14, 15, 28, 29, 30, 31], dtype=torch.int64
|
|
)
|
|
|
|
with patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_transfer_kv_per_layer_mla_lf_pf",
|
|
return_value=fake_kernel,
|
|
):
|
|
host_pool._backup_indexer_from_device_per_layer(
|
|
device_pool,
|
|
host_indices,
|
|
device_indices,
|
|
layer_id=1,
|
|
io_backend="kernel",
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
src, dst, src_indices, dst_indices, kwargs = calls[0]
|
|
expected_src = device_pool.index_k_with_scale_buffer[1]
|
|
self.assertEqual(src.data_ptr(), expected_src.data_ptr())
|
|
self.assertEqual(src.shape, expected_src.shape)
|
|
self.assertEqual(src.stride(), expected_src.stride())
|
|
self.assertIs(dst, host_pool.index_k_with_scale_buffer)
|
|
self.assertEqual(src_indices.tolist(), [3, 7])
|
|
self.assertEqual(dst_indices.tolist(), [2, 5])
|
|
self.assertEqual(kwargs["layer_id"], 1)
|
|
self.assertEqual(kwargs["item_size"], 32)
|
|
self.assertEqual(kwargs["dst_layout_dim"], 96)
|
|
|
|
def test_mla_page_first_direct_per_layer_backup_uses_direct_lf_pf(self):
|
|
calls = []
|
|
|
|
def fake_direct(src_ptrs, dst_ptrs, src_indices, dst_indices, **kwargs):
|
|
calls.append(
|
|
(src_ptrs, dst_ptrs, src_indices.clone(), dst_indices.clone(), kwargs)
|
|
)
|
|
|
|
host_pool = MLATokenToKVPoolHost.__new__(MLATokenToKVPoolHost)
|
|
host_pool.layout = "page_first_direct"
|
|
host_pool.page_size = 4
|
|
host_pool.kv_buffer = torch.empty((8, 3, 4, 1, 16), dtype=torch.uint8)
|
|
device_pool = type("DevicePool", (), {})()
|
|
device_pool.kv_buffer = torch.empty((3, 32, 1, 16), dtype=torch.uint8)
|
|
host_indices = torch.tensor([4, 5, 6, 7], dtype=torch.int64)
|
|
device_indices = torch.tensor([12, 13, 14, 15], dtype=torch.int64)
|
|
|
|
with patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_transfer_kv_per_layer_direct_lf_pf",
|
|
return_value=fake_direct,
|
|
):
|
|
host_pool.backup_from_device_per_layer(
|
|
device_pool,
|
|
host_indices,
|
|
device_indices,
|
|
layer_id=2,
|
|
io_backend="direct",
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
src_ptrs, dst_ptrs, src_indices, dst_indices, kwargs = calls[0]
|
|
self.assertEqual(len(src_ptrs), 1)
|
|
self.assertEqual(src_ptrs[0].data_ptr(), device_pool.kv_buffer[2].data_ptr())
|
|
self.assertEqual(len(dst_ptrs), 1)
|
|
self.assertEqual(dst_ptrs[0].data_ptr(), host_pool.kv_buffer.data_ptr())
|
|
self.assertEqual(src_indices.tolist(), [12, 13, 14, 15])
|
|
self.assertEqual(dst_indices.tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(kwargs["layer_id"], 2)
|
|
self.assertEqual(kwargs["page_size"], 4)
|
|
|
|
def test_mha_page_first_direct_per_layer_backup_uses_direct_lf_pf(self):
|
|
calls = []
|
|
|
|
def fake_direct(src_ptrs, dst_ptrs, src_indices, dst_indices, **kwargs):
|
|
calls.append(
|
|
(src_ptrs, dst_ptrs, src_indices.clone(), dst_indices.clone(), kwargs)
|
|
)
|
|
|
|
host_pool = MHATokenToKVPoolHost.__new__(MHATokenToKVPoolHost)
|
|
host_pool.layout = "page_first_direct"
|
|
host_pool.page_size = 4
|
|
host_pool.kv_buffer = torch.empty((2, 8, 3, 4, 2, 8), dtype=torch.uint8)
|
|
device_pool = type("DevicePool", (), {})()
|
|
device_pool.k_buffer = torch.empty((3, 32, 2, 8), dtype=torch.uint8)
|
|
device_pool.v_buffer = torch.empty((3, 32, 2, 8), dtype=torch.uint8)
|
|
host_indices = torch.tensor([4, 5, 6, 7], dtype=torch.int64)
|
|
device_indices = torch.tensor([12, 13, 14, 15], dtype=torch.int64)
|
|
|
|
with patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_transfer_kv_per_layer_direct_lf_pf",
|
|
return_value=fake_direct,
|
|
):
|
|
host_pool.backup_from_device_per_layer(
|
|
device_pool,
|
|
host_indices,
|
|
device_indices,
|
|
layer_id=2,
|
|
io_backend="direct",
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
src_ptrs, dst_ptrs, src_indices, dst_indices, kwargs = calls[0]
|
|
self.assertEqual(len(src_ptrs), 2)
|
|
self.assertEqual(src_ptrs[0].data_ptr(), device_pool.k_buffer[2].data_ptr())
|
|
self.assertEqual(src_ptrs[1].data_ptr(), device_pool.v_buffer[2].data_ptr())
|
|
self.assertEqual(len(dst_ptrs), 2)
|
|
self.assertEqual(dst_ptrs[0].data_ptr(), host_pool.k_buffer.data_ptr())
|
|
self.assertEqual(dst_ptrs[1].data_ptr(), host_pool.v_buffer.data_ptr())
|
|
self.assertEqual(src_indices.tolist(), [12, 13, 14, 15])
|
|
self.assertEqual(dst_indices.tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(kwargs["layer_id"], 2)
|
|
self.assertEqual(kwargs["page_size"], 4)
|
|
|
|
def test_nsa_indexer_page_first_direct_per_layer_backup_uses_direct_lf_pf(self):
|
|
calls = []
|
|
|
|
def fake_direct(src_ptrs, dst_ptrs, src_indices, dst_indices, **kwargs):
|
|
calls.append(
|
|
(src_ptrs, dst_ptrs, src_indices.clone(), dst_indices.clone(), kwargs)
|
|
)
|
|
|
|
host_pool = NSATokenToKVPoolHost.__new__(NSATokenToKVPoolHost)
|
|
host_pool.layout = "page_first_direct"
|
|
host_pool.page_size = 4
|
|
host_pool.index_k_with_scale_buffer = torch.empty(
|
|
(8, 3, 1, 32), dtype=torch.uint8
|
|
)
|
|
device_pool = type("DevicePool", (), {})()
|
|
device_pool.index_k_with_scale_buffer = torch.empty(
|
|
(3, 8, 32), dtype=torch.uint8
|
|
)
|
|
host_indices = torch.tensor([4, 5, 6, 7], dtype=torch.int64)
|
|
device_indices = torch.tensor([12, 13, 14, 15], dtype=torch.int64)
|
|
|
|
with patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_transfer_kv_per_layer_direct_lf_pf",
|
|
return_value=fake_direct,
|
|
):
|
|
host_pool._backup_indexer_from_device_per_layer(
|
|
device_pool,
|
|
host_indices,
|
|
device_indices,
|
|
layer_id=1,
|
|
io_backend="direct",
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
src_ptrs, dst_ptrs, src_indices, dst_indices, kwargs = calls[0]
|
|
self.assertEqual(len(src_ptrs), 1)
|
|
self.assertEqual(
|
|
src_ptrs[0].data_ptr(),
|
|
device_pool.index_k_with_scale_buffer[1].data_ptr(),
|
|
)
|
|
self.assertEqual(len(dst_ptrs), 1)
|
|
self.assertEqual(
|
|
dst_ptrs[0].data_ptr(),
|
|
host_pool.index_k_with_scale_buffer.data_ptr(),
|
|
)
|
|
self.assertEqual(src_indices.tolist(), [3])
|
|
self.assertEqual(dst_indices.tolist(), [1])
|
|
self.assertEqual(kwargs["layer_id"], 1)
|
|
self.assertEqual(kwargs["page_size"], 1)
|
|
|
|
def test_mla_layer_page_first_per_layer_backup_uses_direct_lf_lpf(self):
|
|
calls = []
|
|
|
|
def fake_direct(src_ptrs, dst_ptrs, src_indices, dst_indices, **kwargs):
|
|
calls.append(
|
|
(src_ptrs, dst_ptrs, src_indices.clone(), dst_indices.clone(), kwargs)
|
|
)
|
|
|
|
host_pool = MLATokenToKVPoolHost.__new__(MLATokenToKVPoolHost)
|
|
host_pool.layout = "layer_page_first"
|
|
host_pool.page_size = 4
|
|
host_pool.kv_buffer = torch.empty((3, 8, 4, 1, 16), dtype=torch.uint8)
|
|
device_pool = type("DevicePool", (), {})()
|
|
device_pool.kv_buffer = torch.empty((3, 32, 1, 16), dtype=torch.uint8)
|
|
host_indices = torch.tensor([4, 5, 6, 7], dtype=torch.int64)
|
|
device_indices = torch.tensor([12, 13, 14, 15], dtype=torch.int64)
|
|
|
|
with patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_transfer_kv_per_layer_direct_lf_lpf",
|
|
return_value=fake_direct,
|
|
):
|
|
host_pool.backup_from_device_per_layer(
|
|
device_pool,
|
|
host_indices,
|
|
device_indices,
|
|
layer_id=2,
|
|
io_backend="direct",
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
src_ptrs, dst_ptrs, src_indices, dst_indices, kwargs = calls[0]
|
|
self.assertEqual(len(src_ptrs), 1)
|
|
self.assertEqual(src_ptrs[0].data_ptr(), device_pool.kv_buffer[2].data_ptr())
|
|
self.assertEqual(len(dst_ptrs), 1)
|
|
self.assertEqual(dst_ptrs[0].data_ptr(), host_pool.kv_buffer.data_ptr())
|
|
self.assertEqual(src_indices.tolist(), [12, 13, 14, 15])
|
|
self.assertEqual(dst_indices.tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(kwargs["layer_id"], 2)
|
|
self.assertEqual(kwargs["page_size"], 4)
|
|
|
|
def test_mha_layer_page_first_per_layer_backup_uses_direct_lf_lpf(self):
|
|
calls = []
|
|
|
|
def fake_direct(src_ptrs, dst_ptrs, src_indices, dst_indices, **kwargs):
|
|
calls.append(
|
|
(src_ptrs, dst_ptrs, src_indices.clone(), dst_indices.clone(), kwargs)
|
|
)
|
|
|
|
host_pool = MHATokenToKVPoolHost.__new__(MHATokenToKVPoolHost)
|
|
host_pool.layout = "layer_page_first"
|
|
host_pool.page_size = 4
|
|
host_pool.kv_buffer = torch.empty((2, 3, 8, 4, 2, 8), dtype=torch.uint8)
|
|
device_pool = type("DevicePool", (), {})()
|
|
device_pool.k_buffer = torch.empty((3, 32, 2, 8), dtype=torch.uint8)
|
|
device_pool.v_buffer = torch.empty((3, 32, 2, 8), dtype=torch.uint8)
|
|
host_indices = torch.tensor([4, 5, 6, 7], dtype=torch.int64)
|
|
device_indices = torch.tensor([12, 13, 14, 15], dtype=torch.int64)
|
|
|
|
with patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_transfer_kv_per_layer_direct_lf_lpf",
|
|
return_value=fake_direct,
|
|
):
|
|
host_pool.backup_from_device_per_layer(
|
|
device_pool,
|
|
host_indices,
|
|
device_indices,
|
|
layer_id=2,
|
|
io_backend="direct",
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
src_ptrs, dst_ptrs, src_indices, dst_indices, kwargs = calls[0]
|
|
self.assertEqual(len(src_ptrs), 2)
|
|
self.assertEqual(src_ptrs[0].data_ptr(), device_pool.k_buffer[2].data_ptr())
|
|
self.assertEqual(src_ptrs[1].data_ptr(), device_pool.v_buffer[2].data_ptr())
|
|
self.assertEqual(len(dst_ptrs), 2)
|
|
self.assertEqual(dst_ptrs[0].data_ptr(), host_pool.k_buffer.data_ptr())
|
|
self.assertEqual(dst_ptrs[1].data_ptr(), host_pool.v_buffer.data_ptr())
|
|
self.assertEqual(src_indices.tolist(), [12, 13, 14, 15])
|
|
self.assertEqual(dst_indices.tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(kwargs["layer_id"], 2)
|
|
self.assertEqual(kwargs["page_size"], 4)
|
|
|
|
def test_nsa_indexer_layer_page_first_per_layer_backup_uses_direct_lf_lpf(self):
|
|
calls = []
|
|
|
|
def fake_direct(src_ptrs, dst_ptrs, src_indices, dst_indices, **kwargs):
|
|
calls.append(
|
|
(src_ptrs, dst_ptrs, src_indices.clone(), dst_indices.clone(), kwargs)
|
|
)
|
|
|
|
host_pool = NSATokenToKVPoolHost.__new__(NSATokenToKVPoolHost)
|
|
host_pool.layout = "layer_page_first"
|
|
host_pool.page_size = 4
|
|
host_pool.index_k_with_scale_buffer = torch.empty(
|
|
(3, 8, 1, 32), dtype=torch.uint8
|
|
)
|
|
device_pool = type("DevicePool", (), {})()
|
|
device_pool.index_k_with_scale_buffer = torch.empty(
|
|
(3, 8, 32), dtype=torch.uint8
|
|
)
|
|
host_indices = torch.tensor([4, 5, 6, 7], dtype=torch.int64)
|
|
device_indices = torch.tensor([12, 13, 14, 15], dtype=torch.int64)
|
|
|
|
with patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_transfer_kv_per_layer_direct_lf_lpf",
|
|
return_value=fake_direct,
|
|
):
|
|
host_pool._backup_indexer_from_device_per_layer(
|
|
device_pool,
|
|
host_indices,
|
|
device_indices,
|
|
layer_id=1,
|
|
io_backend="direct",
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
src_ptrs, dst_ptrs, src_indices, dst_indices, kwargs = calls[0]
|
|
self.assertEqual(len(src_ptrs), 1)
|
|
self.assertEqual(
|
|
src_ptrs[0].data_ptr(),
|
|
device_pool.index_k_with_scale_buffer[1].data_ptr(),
|
|
)
|
|
self.assertEqual(len(dst_ptrs), 1)
|
|
self.assertEqual(
|
|
dst_ptrs[0].data_ptr(),
|
|
host_pool.index_k_with_scale_buffer.data_ptr(),
|
|
)
|
|
self.assertEqual(src_indices.tolist(), [3])
|
|
self.assertEqual(dst_indices.tolist(), [1])
|
|
self.assertEqual(kwargs["layer_id"], 1)
|
|
self.assertEqual(kwargs["page_size"], 1)
|
|
|
|
def test_mla_page_first_direct_per_layer_load_uses_tai_direct_pf_lf(self):
|
|
calls = []
|
|
|
|
def fake_direct(src_ptrs, dst_ptrs, src_indices, dst_indices, **kwargs):
|
|
calls.append(
|
|
(src_ptrs, dst_ptrs, src_indices.clone(), dst_indices.clone(), kwargs)
|
|
)
|
|
|
|
host_pool = MLATokenToKVPoolHost.__new__(MLATokenToKVPoolHost)
|
|
host_pool.layout = "page_first_direct"
|
|
host_pool.page_size = 4
|
|
host_pool.kv_buffer = torch.empty((8, 3, 4, 1, 16), dtype=torch.uint8)
|
|
device_pool = type("DevicePool", (), {})()
|
|
device_pool.kv_buffer = torch.empty((3, 32, 1, 16), dtype=torch.uint8)
|
|
host_indices = torch.tensor([4, 5, 6, 7], dtype=torch.int64)
|
|
device_indices = torch.tensor([12, 13, 14, 15], dtype=torch.int64)
|
|
|
|
with patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_transfer_kv_per_layer_direct_pf_lf",
|
|
return_value=fake_direct,
|
|
):
|
|
host_pool.load_to_device_per_layer(
|
|
device_pool,
|
|
host_indices,
|
|
device_indices,
|
|
layer_id=2,
|
|
io_backend="direct",
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
src_ptrs, dst_ptrs, src_indices, dst_indices, kwargs = calls[0]
|
|
self.assertEqual(len(src_ptrs), 1)
|
|
self.assertEqual(src_ptrs[0].data_ptr(), host_pool.kv_buffer.data_ptr())
|
|
self.assertEqual(len(dst_ptrs), 1)
|
|
self.assertEqual(dst_ptrs[0].data_ptr(), device_pool.kv_buffer[2].data_ptr())
|
|
self.assertEqual(src_indices.tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(dst_indices.tolist(), [12, 13, 14, 15])
|
|
self.assertEqual(kwargs["layer_id"], 2)
|
|
self.assertEqual(kwargs["page_size"], 4)
|
|
|
|
def test_mha_page_first_direct_per_layer_load_uses_tai_direct_pf_lf(self):
|
|
calls = []
|
|
|
|
def fake_direct(src_ptrs, dst_ptrs, src_indices, dst_indices, **kwargs):
|
|
calls.append(
|
|
(src_ptrs, dst_ptrs, src_indices.clone(), dst_indices.clone(), kwargs)
|
|
)
|
|
|
|
host_pool = MHATokenToKVPoolHost.__new__(MHATokenToKVPoolHost)
|
|
host_pool.layout = "page_first_direct"
|
|
host_pool.page_size = 4
|
|
host_pool.kv_buffer = torch.empty((2, 8, 3, 4, 2, 8), dtype=torch.uint8)
|
|
device_pool = type("DevicePool", (), {})()
|
|
device_pool.k_buffer = torch.empty((3, 32, 2, 8), dtype=torch.uint8)
|
|
device_pool.v_buffer = torch.empty((3, 32, 2, 8), dtype=torch.uint8)
|
|
host_indices = torch.tensor([4, 5, 6, 7], dtype=torch.int64)
|
|
device_indices = torch.tensor([12, 13, 14, 15], dtype=torch.int64)
|
|
|
|
with patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_transfer_kv_per_layer_direct_pf_lf",
|
|
return_value=fake_direct,
|
|
):
|
|
host_pool.load_to_device_per_layer(
|
|
device_pool,
|
|
host_indices,
|
|
device_indices,
|
|
layer_id=2,
|
|
io_backend="direct",
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
src_ptrs, dst_ptrs, src_indices, dst_indices, kwargs = calls[0]
|
|
self.assertEqual(len(src_ptrs), 2)
|
|
self.assertEqual(src_ptrs[0].data_ptr(), host_pool.k_buffer.data_ptr())
|
|
self.assertEqual(src_ptrs[1].data_ptr(), host_pool.v_buffer.data_ptr())
|
|
self.assertEqual(len(dst_ptrs), 2)
|
|
self.assertEqual(dst_ptrs[0].data_ptr(), device_pool.k_buffer[2].data_ptr())
|
|
self.assertEqual(dst_ptrs[1].data_ptr(), device_pool.v_buffer[2].data_ptr())
|
|
self.assertEqual(src_indices.tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(dst_indices.tolist(), [12, 13, 14, 15])
|
|
self.assertEqual(kwargs["layer_id"], 2)
|
|
self.assertEqual(kwargs["page_size"], 4)
|
|
|
|
def test_nsa_indexer_page_first_direct_per_layer_load_uses_tai_direct_pf_lf(self):
|
|
calls = []
|
|
|
|
def fake_direct(src_ptrs, dst_ptrs, src_indices, dst_indices, **kwargs):
|
|
calls.append(
|
|
(src_ptrs, dst_ptrs, src_indices.clone(), dst_indices.clone(), kwargs)
|
|
)
|
|
|
|
host_pool = NSATokenToKVPoolHost.__new__(NSATokenToKVPoolHost)
|
|
host_pool.layout = "page_first_direct"
|
|
host_pool.page_size = 4
|
|
host_pool.index_k_with_scale_buffer = torch.empty(
|
|
(8, 3, 1, 32), dtype=torch.uint8
|
|
)
|
|
device_pool = type("DevicePool", (), {})()
|
|
device_pool.index_k_with_scale_buffer = torch.empty(
|
|
(3, 8, 32), dtype=torch.uint8
|
|
)
|
|
host_indices = torch.tensor([4, 5, 6, 7], dtype=torch.int64)
|
|
device_indices = torch.tensor([12, 13, 14, 15], dtype=torch.int64)
|
|
|
|
with patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_transfer_kv_per_layer_direct_pf_lf",
|
|
return_value=fake_direct,
|
|
):
|
|
host_pool._load_indexer_to_device_per_layer(
|
|
device_pool,
|
|
host_indices,
|
|
device_indices,
|
|
layer_id=1,
|
|
io_backend="direct",
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
src_ptrs, dst_ptrs, src_indices, dst_indices, kwargs = calls[0]
|
|
self.assertEqual(len(src_ptrs), 1)
|
|
self.assertEqual(
|
|
src_ptrs[0].data_ptr(),
|
|
host_pool.index_k_with_scale_buffer.data_ptr(),
|
|
)
|
|
self.assertEqual(len(dst_ptrs), 1)
|
|
self.assertEqual(
|
|
dst_ptrs[0].data_ptr(),
|
|
device_pool.index_k_with_scale_buffer[1].data_ptr(),
|
|
)
|
|
self.assertEqual(src_indices.tolist(), [1])
|
|
self.assertEqual(dst_indices.tolist(), [3])
|
|
self.assertEqual(kwargs["layer_id"], 1)
|
|
self.assertEqual(kwargs["page_size"], 1)
|
|
|
|
def test_mla_layer_page_first_per_layer_load_uses_tai_direct_lpf_lf(self):
|
|
calls = []
|
|
|
|
def fake_direct(src_ptrs, dst_ptrs, src_indices, dst_indices, **kwargs):
|
|
calls.append(
|
|
(src_ptrs, dst_ptrs, src_indices.clone(), dst_indices.clone(), kwargs)
|
|
)
|
|
|
|
host_pool = MLATokenToKVPoolHost.__new__(MLATokenToKVPoolHost)
|
|
host_pool.layout = "layer_page_first"
|
|
host_pool.page_size = 4
|
|
host_pool.kv_buffer = torch.empty((3, 8, 4, 1, 16), dtype=torch.uint8)
|
|
device_pool = type("DevicePool", (), {})()
|
|
device_pool.kv_buffer = torch.empty((3, 32, 1, 16), dtype=torch.uint8)
|
|
host_indices = torch.tensor([4, 5, 6, 7], dtype=torch.int64)
|
|
device_indices = torch.tensor([12, 13, 14, 15], dtype=torch.int64)
|
|
|
|
with patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_transfer_kv_per_layer_direct_lpf_lf",
|
|
return_value=fake_direct,
|
|
):
|
|
host_pool.load_to_device_per_layer(
|
|
device_pool,
|
|
host_indices,
|
|
device_indices,
|
|
layer_id=2,
|
|
io_backend="direct",
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
src_ptrs, dst_ptrs, src_indices, dst_indices, kwargs = calls[0]
|
|
self.assertEqual(len(src_ptrs), 1)
|
|
self.assertEqual(src_ptrs[0].data_ptr(), host_pool.kv_buffer.data_ptr())
|
|
self.assertEqual(len(dst_ptrs), 1)
|
|
self.assertEqual(dst_ptrs[0].data_ptr(), device_pool.kv_buffer[2].data_ptr())
|
|
self.assertEqual(src_indices.tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(dst_indices.tolist(), [12, 13, 14, 15])
|
|
self.assertEqual(kwargs["layer_id"], 2)
|
|
self.assertEqual(kwargs["page_size"], 4)
|
|
|
|
def test_mha_layer_page_first_per_layer_load_uses_tai_direct_lpf_lf(self):
|
|
calls = []
|
|
|
|
def fake_direct(src_ptrs, dst_ptrs, src_indices, dst_indices, **kwargs):
|
|
calls.append(
|
|
(src_ptrs, dst_ptrs, src_indices.clone(), dst_indices.clone(), kwargs)
|
|
)
|
|
|
|
host_pool = MHATokenToKVPoolHost.__new__(MHATokenToKVPoolHost)
|
|
host_pool.layout = "layer_page_first"
|
|
host_pool.page_size = 4
|
|
host_pool.kv_buffer = torch.empty((2, 3, 8, 4, 2, 8), dtype=torch.uint8)
|
|
device_pool = type("DevicePool", (), {})()
|
|
device_pool.k_buffer = torch.empty((3, 32, 2, 8), dtype=torch.uint8)
|
|
device_pool.v_buffer = torch.empty((3, 32, 2, 8), dtype=torch.uint8)
|
|
host_indices = torch.tensor([4, 5, 6, 7], dtype=torch.int64)
|
|
device_indices = torch.tensor([12, 13, 14, 15], dtype=torch.int64)
|
|
|
|
with patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_transfer_kv_per_layer_direct_lpf_lf",
|
|
return_value=fake_direct,
|
|
):
|
|
host_pool.load_to_device_per_layer(
|
|
device_pool,
|
|
host_indices,
|
|
device_indices,
|
|
layer_id=2,
|
|
io_backend="direct",
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
src_ptrs, dst_ptrs, src_indices, dst_indices, kwargs = calls[0]
|
|
self.assertEqual(len(src_ptrs), 2)
|
|
self.assertEqual(src_ptrs[0].data_ptr(), host_pool.k_buffer.data_ptr())
|
|
self.assertEqual(src_ptrs[1].data_ptr(), host_pool.v_buffer.data_ptr())
|
|
self.assertEqual(len(dst_ptrs), 2)
|
|
self.assertEqual(dst_ptrs[0].data_ptr(), device_pool.k_buffer[2].data_ptr())
|
|
self.assertEqual(dst_ptrs[1].data_ptr(), device_pool.v_buffer[2].data_ptr())
|
|
self.assertEqual(src_indices.tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(dst_indices.tolist(), [12, 13, 14, 15])
|
|
self.assertEqual(kwargs["layer_id"], 2)
|
|
self.assertEqual(kwargs["page_size"], 4)
|
|
|
|
def test_nsa_indexer_layer_page_first_per_layer_load_uses_tai_direct_lpf_lf(self):
|
|
calls = []
|
|
|
|
def fake_direct(src_ptrs, dst_ptrs, src_indices, dst_indices, **kwargs):
|
|
calls.append(
|
|
(src_ptrs, dst_ptrs, src_indices.clone(), dst_indices.clone(), kwargs)
|
|
)
|
|
|
|
host_pool = NSATokenToKVPoolHost.__new__(NSATokenToKVPoolHost)
|
|
host_pool.layout = "layer_page_first"
|
|
host_pool.page_size = 4
|
|
host_pool.index_k_with_scale_buffer = torch.empty(
|
|
(3, 8, 1, 32), dtype=torch.uint8
|
|
)
|
|
device_pool = type("DevicePool", (), {})()
|
|
device_pool.index_k_with_scale_buffer = torch.empty(
|
|
(3, 8, 32), dtype=torch.uint8
|
|
)
|
|
host_indices = torch.tensor([4, 5, 6, 7], dtype=torch.int64)
|
|
device_indices = torch.tensor([12, 13, 14, 15], dtype=torch.int64)
|
|
|
|
with patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_transfer_kv_per_layer_direct_lpf_lf",
|
|
return_value=fake_direct,
|
|
):
|
|
host_pool._load_indexer_to_device_per_layer(
|
|
device_pool,
|
|
host_indices,
|
|
device_indices,
|
|
layer_id=1,
|
|
io_backend="direct",
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
src_ptrs, dst_ptrs, src_indices, dst_indices, kwargs = calls[0]
|
|
self.assertEqual(len(src_ptrs), 1)
|
|
self.assertEqual(
|
|
src_ptrs[0].data_ptr(),
|
|
host_pool.index_k_with_scale_buffer.data_ptr(),
|
|
)
|
|
self.assertEqual(len(dst_ptrs), 1)
|
|
self.assertEqual(
|
|
dst_ptrs[0].data_ptr(),
|
|
device_pool.index_k_with_scale_buffer[1].data_ptr(),
|
|
)
|
|
self.assertEqual(src_indices.tolist(), [1])
|
|
self.assertEqual(dst_indices.tolist(), [3])
|
|
self.assertEqual(kwargs["layer_id"], 1)
|
|
self.assertEqual(kwargs["page_size"], 1)
|
|
|
|
def test_nsa_indexer_load_reuses_precomputed_page_indices_across_layers(self):
|
|
calls = []
|
|
|
|
def fake_direct(src_ptrs, dst_ptrs, src_indices, dst_indices, **kwargs):
|
|
calls.append(
|
|
(src_ptrs, dst_ptrs, src_indices.clone(), dst_indices.clone(), kwargs)
|
|
)
|
|
|
|
host_pool = NSATokenToKVPoolHost.__new__(NSATokenToKVPoolHost)
|
|
host_pool.layout = "page_first_direct"
|
|
host_pool.page_size = 4
|
|
host_pool.index_k_with_scale_buffer = torch.empty(
|
|
(8, 3, 1, 32), dtype=torch.uint8
|
|
)
|
|
device_pool = type("DevicePool", (), {})()
|
|
device_pool.index_k_with_scale_buffer = torch.empty(
|
|
(3, 8, 32), dtype=torch.uint8
|
|
)
|
|
host_indices = torch.tensor([4, 5, 6, 7], dtype=torch.int64)
|
|
device_indices = torch.tensor([12, 13, 14, 15], dtype=torch.int64)
|
|
|
|
original_getter = host_pool._get_indexer_page_indices
|
|
getter_calls = []
|
|
|
|
def counting_getter(h, d):
|
|
getter_calls.append((h.clone(), d.clone()))
|
|
return original_getter(h, d)
|
|
|
|
host_pool._get_indexer_page_indices = counting_getter
|
|
|
|
with (
|
|
patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_prepare_h2d_page_descriptor",
|
|
side_effect=RuntimeError("missing prepared descriptor api"),
|
|
),
|
|
patch(
|
|
"sglang.srt.mem_cache.memory_pool_host._load_tai_transfer_kv_per_layer_direct_pf_lf",
|
|
return_value=fake_direct,
|
|
),
|
|
):
|
|
host_pool.begin_load_to_device_op(
|
|
host_indices, device_indices, io_backend="direct"
|
|
)
|
|
try:
|
|
for layer_id in range(3):
|
|
host_pool._load_indexer_to_device_per_layer(
|
|
device_pool,
|
|
host_indices,
|
|
device_indices,
|
|
layer_id=layer_id,
|
|
io_backend="direct",
|
|
)
|
|
finally:
|
|
host_pool.end_load_to_device_op()
|
|
|
|
self.assertEqual(len(getter_calls), 1)
|
|
self.assertEqual(len(calls), 3)
|
|
self.assertEqual([call[4]["layer_id"] for call in calls], [0, 1, 2])
|
|
self.assertEqual([call[2].tolist() for call in calls], [[1], [1], [1]])
|
|
self.assertEqual([call[3].tolist() for call in calls], [[3], [3], [3]])
|
|
|
|
|
|
class FakeAllocator:
|
|
def __init__(self, alloc_result=None):
|
|
self.alloc_result = alloc_result
|
|
self.alloc_calls = []
|
|
self.owner_alloc_calls = []
|
|
self.frees = []
|
|
self.cp_size = 4
|
|
self.cp_rank = 1
|
|
self.page_size = 4
|
|
self.device_pool = FakeDevicePool()
|
|
|
|
def get_kvcache(self):
|
|
return self.device_pool
|
|
|
|
def alloc(self, need_size):
|
|
self.alloc_calls.append(need_size)
|
|
if self.alloc_result is None:
|
|
return None
|
|
return self.alloc_result[:need_size].clone()
|
|
|
|
def alloc_pages_with_owners(self, page_owners):
|
|
owners = list(page_owners)
|
|
self.owner_alloc_calls.append(owners)
|
|
if self.alloc_result is None:
|
|
return None
|
|
need_size = len(owners) * self.page_size
|
|
return self.alloc_result[:need_size].clone()
|
|
|
|
def free(self, indices):
|
|
self.frees.append(indices.clone())
|
|
return len(indices)
|
|
|
|
|
|
class HostIndicesTensor(torch.Tensor):
|
|
@staticmethod
|
|
def __new__(cls, data):
|
|
return torch.Tensor._make_subclass(cls, data, require_grad=False)
|
|
|
|
def to(self, *args, **kwargs):
|
|
raise AssertionError("load_cp should not move host indices before queuing")
|
|
|
|
|
|
class DummyEvent:
|
|
def record(self):
|
|
pass
|
|
|
|
def wait(self, stream):
|
|
pass
|
|
|
|
def query(self):
|
|
return True
|
|
|
|
def synchronize(self):
|
|
pass
|
|
|
|
|
|
class DummyStream:
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
|
|
class DummyDeviceModule:
|
|
Event = DummyEvent
|
|
Stream = DummyStream
|
|
|
|
@staticmethod
|
|
def stream(stream):
|
|
return stream
|
|
|
|
|
|
class DummyLayerDoneCounter:
|
|
def __init__(self):
|
|
self.events = [
|
|
type(
|
|
"ProducerEvent",
|
|
(),
|
|
{
|
|
"start_event": DummyEvent(),
|
|
"finish_event": DummyEvent(),
|
|
"complete": lambda self, layer_id: None,
|
|
},
|
|
)()
|
|
]
|
|
|
|
def update_producer(self):
|
|
return 0
|
|
|
|
|
|
class RecordingProducerEvent:
|
|
def __init__(self, order):
|
|
self.start_event = DummyEvent()
|
|
self.finish_event = DummyEvent()
|
|
self.order = order
|
|
|
|
def complete(self, layer_id):
|
|
self.order.append(("complete", layer_id))
|
|
|
|
|
|
class RecordingLayerDoneCounter:
|
|
def __init__(self, order):
|
|
self.events = [RecordingProducerEvent(order)]
|
|
|
|
def update_producer(self):
|
|
return 0
|
|
|
|
|
|
class TestHiCacheControllerCPWrite(CustomTestCase):
|
|
def setUp(self):
|
|
self.device_module_patcher = patch(
|
|
"sglang.srt.managers.cache_controller.device_module",
|
|
DummyDeviceModule,
|
|
)
|
|
self.nsa_pool_patcher = patch(
|
|
"sglang.srt.managers.cache_controller.NSATokenToKVPool",
|
|
FakeDevicePool,
|
|
)
|
|
self.device_module_patcher.start()
|
|
self.nsa_pool_patcher.start()
|
|
self.addCleanup(self.device_module_patcher.stop)
|
|
self.addCleanup(self.nsa_pool_patcher.stop)
|
|
|
|
def make_controller(
|
|
self,
|
|
host_pool,
|
|
allocator=None,
|
|
cp_rank=1,
|
|
draft_host_pool=None,
|
|
draft_mem_pool_device=None,
|
|
):
|
|
allocator = allocator or FakeAllocator()
|
|
controller = HiCacheController(
|
|
token_to_kv_pool_allocator=allocator,
|
|
mem_pool_host=host_pool,
|
|
page_size=4,
|
|
tp_group=None,
|
|
load_cache_event=__import__("threading").Event(),
|
|
io_backend="direct",
|
|
cp_shared_kv_layout=CpSharedKVLayout(
|
|
page_size=4, cp_size=4, cp_rank=cp_rank
|
|
),
|
|
draft_mem_pool_host=draft_host_pool,
|
|
draft_mem_pool_device=draft_mem_pool_device,
|
|
)
|
|
controller.layer_done_counter = DummyLayerDoneCounter()
|
|
return controller
|
|
|
|
def test_cp_write_filters_to_owned_physical_locs(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
controller = self.make_controller(host_pool, cp_rank=1)
|
|
logical_locs = torch.arange(4, 20, dtype=torch.int64)
|
|
|
|
result = controller.write(logical_locs, node_id=7)
|
|
|
|
self.assertEqual(result.metadata.logical_len, 16)
|
|
self.assertEqual(result.metadata.owned_positions.tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(result.metadata.host_indices.tolist(), [100, 101, 102, 103])
|
|
self.assertEqual(host_pool.alloc_calls, [4])
|
|
self.assertEqual(host_pool.backups, [])
|
|
self.assertEqual(host_pool.layer_backups[0][1].tolist(), [4, 5, 6, 7])
|
|
|
|
def test_cp_write_accepts_valid_tail_and_pads_owned_physical_page(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
controller = self.make_controller(host_pool, cp_rank=1)
|
|
logical_locs = torch.tensor([8, 9, 10], dtype=torch.int64)
|
|
|
|
result = controller.write(logical_locs, node_id=21)
|
|
|
|
self.assertEqual(result.metadata.logical_len, 3)
|
|
self.assertEqual(result.metadata.valid_len, 3)
|
|
self.assertEqual(result.metadata.padded_len, 4)
|
|
self.assertEqual(result.metadata.owned_positions.tolist(), [0, 1, 2, 3])
|
|
self.assertEqual(result.metadata.host_indices.tolist(), [100, 101, 102, 103])
|
|
self.assertEqual(host_pool.alloc_calls, [4])
|
|
self.assertEqual(host_pool.layer_backups[0][1].tolist(), [4, 5, 6, 7])
|
|
|
|
def test_cp_write_rejects_non_contiguous_owned_physical_page(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
controller = self.make_controller(host_pool, cp_rank=1)
|
|
logical_locs = torch.tensor([8, 9, 11, 10], dtype=torch.int64)
|
|
|
|
with self.assertRaisesRegex(
|
|
ValueError, "physical_device_indices.*contiguous page spans"
|
|
):
|
|
controller.write(logical_locs, node_id=22)
|
|
|
|
def test_cp_write_rejects_non_contiguous_host_page(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 103, 102], dtype=torch.int64))
|
|
controller = self.make_controller(host_pool, cp_rank=1)
|
|
logical_locs = torch.arange(8, 12, dtype=torch.int64)
|
|
|
|
with self.assertRaisesRegex(ValueError, "host_indices.*contiguous page spans"):
|
|
controller.write(logical_locs, node_id=23)
|
|
|
|
def test_cp_write_zero_owned_returns_metadata_and_noop_ack(self):
|
|
host_pool = FakeHostPool(torch.tensor([], dtype=torch.int64))
|
|
controller = self.make_controller(host_pool, cp_rank=3)
|
|
logical_locs = torch.arange(4, 8, dtype=torch.int64)
|
|
|
|
result = controller.write(logical_locs, node_id=8)
|
|
|
|
self.assertEqual(result.metadata.logical_len, 4)
|
|
self.assertEqual(result.metadata.host_indices.tolist(), [])
|
|
self.assertEqual(host_pool.alloc_calls, [])
|
|
self.assertEqual(len(controller.ack_write_queue), 1)
|
|
|
|
def test_cp_write_zero_owned_with_draft_returns_empty_draft_metadata(self):
|
|
host_pool = FakeHostPool(torch.tensor([], dtype=torch.int64))
|
|
draft_host_pool = FakeHostPool(torch.tensor([], dtype=torch.int64))
|
|
controller = self.make_controller(
|
|
host_pool,
|
|
cp_rank=3,
|
|
draft_host_pool=draft_host_pool,
|
|
draft_mem_pool_device=FakeDevicePool("draft"),
|
|
)
|
|
logical_locs = torch.arange(4, 8, dtype=torch.int64)
|
|
|
|
result = controller.write(logical_locs, node_id=18)
|
|
|
|
self.assertEqual(result.metadata.logical_len, 4)
|
|
self.assertEqual(result.metadata.host_indices.tolist(), [])
|
|
self.assertEqual(result.metadata.draft_host_indices.tolist(), [])
|
|
self.assertEqual(host_pool.alloc_calls, [])
|
|
self.assertEqual(draft_host_pool.alloc_calls, [])
|
|
self.assertEqual(len(controller.ack_write_queue), 1)
|
|
|
|
def test_cp_write_allocation_failure_reports_required_host_slots(self):
|
|
host_pool = FakeHostPool(None)
|
|
controller = self.make_controller(host_pool, cp_rank=1)
|
|
logical_locs = torch.arange(4, 20, dtype=torch.int64)
|
|
|
|
result = controller.write(logical_locs, node_id=9)
|
|
|
|
self.assertEqual(result.required_host_slots, 4)
|
|
|
|
def test_cp_write_with_draft_pool_backs_target_and_draft_locs(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
draft_host_pool = FakeHostPool(
|
|
torch.tensor([200, 201, 202, 203], dtype=torch.int64)
|
|
)
|
|
draft_device_pool = FakeDevicePool("draft")
|
|
controller = self.make_controller(
|
|
host_pool,
|
|
cp_rank=1,
|
|
draft_host_pool=draft_host_pool,
|
|
draft_mem_pool_device=draft_device_pool,
|
|
)
|
|
logical_locs = torch.arange(4, 20, dtype=torch.int64)
|
|
|
|
result = controller.write(logical_locs, node_id=77)
|
|
|
|
self.assertEqual(result.metadata.host_indices.tolist(), [100, 101, 102, 103])
|
|
self.assertEqual(
|
|
result.metadata.draft_host_indices.tolist(), [200, 201, 202, 203]
|
|
)
|
|
self.assertEqual(host_pool.alloc_calls, [4])
|
|
self.assertEqual(draft_host_pool.alloc_calls, [4])
|
|
self.assertEqual(host_pool.backups, [])
|
|
self.assertEqual(draft_host_pool.backups, [])
|
|
self.assertEqual(host_pool.layer_backups[0][1].tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(draft_host_pool.layer_backups[0][1].tolist(), [4, 5, 6, 7])
|
|
self.assertIs(draft_host_pool.layer_backups[0][3], draft_device_pool)
|
|
self.assertEqual(len(controller.ack_write_queue), 1)
|
|
self.assertEqual(controller.ack_write_queue[0].node_ids, [77])
|
|
|
|
def test_cp_write_valid_tail_with_draft_mirrors_target_padded_page(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
draft_host_pool = FakeHostPool(
|
|
torch.tensor([200, 201, 202, 203], dtype=torch.int64)
|
|
)
|
|
draft_device_pool = FakeDevicePool("draft")
|
|
controller = self.make_controller(
|
|
host_pool,
|
|
cp_rank=1,
|
|
draft_host_pool=draft_host_pool,
|
|
draft_mem_pool_device=draft_device_pool,
|
|
)
|
|
logical_locs = torch.tensor([8, 9, 10], dtype=torch.int64)
|
|
|
|
result = controller.write(logical_locs, node_id=177)
|
|
|
|
self.assertEqual(result.metadata.logical_len, 3)
|
|
self.assertEqual(result.metadata.padded_len, 4)
|
|
self.assertEqual(result.metadata.owned_positions.tolist(), [0, 1, 2, 3])
|
|
self.assertEqual(result.metadata.host_indices.tolist(), [100, 101, 102, 103])
|
|
self.assertEqual(
|
|
result.metadata.draft_host_indices.tolist(), [200, 201, 202, 203]
|
|
)
|
|
self.assertEqual(host_pool.alloc_calls, [4])
|
|
self.assertEqual(draft_host_pool.alloc_calls, [4])
|
|
self.assertEqual(host_pool.layer_backups[0][1].tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(draft_host_pool.layer_backups[0][1].tolist(), [4, 5, 6, 7])
|
|
self.assertIs(draft_host_pool.layer_backups[0][3], draft_device_pool)
|
|
|
|
def test_cp_write_draft_allocation_failure_rolls_back_target_host(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
draft_host_pool = FakeHostPool(None)
|
|
controller = self.make_controller(
|
|
host_pool,
|
|
cp_rank=1,
|
|
draft_host_pool=draft_host_pool,
|
|
draft_mem_pool_device=FakeDevicePool("draft"),
|
|
)
|
|
logical_locs = torch.arange(4, 20, dtype=torch.int64)
|
|
|
|
result = controller.write(logical_locs, node_id=78)
|
|
|
|
self.assertIsNone(result.metadata)
|
|
self.assertEqual(result.required_host_slots, 4)
|
|
self.assertEqual(host_pool.frees[0].tolist(), [100, 101, 102, 103])
|
|
self.assertEqual(host_pool.backups, [])
|
|
self.assertEqual(draft_host_pool.backups, [])
|
|
|
|
def test_cp_reserve_write_queues_no_transfer_until_submit(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
controller = self.make_controller(host_pool, cp_rank=1)
|
|
logical_locs = torch.arange(4, 20, dtype=torch.int64)
|
|
|
|
reservation = controller.reserve_write_cp(logical_locs, node_id=79)
|
|
|
|
self.assertEqual(reservation.metadata.logical_len, 16)
|
|
self.assertEqual(host_pool.alloc_calls, [4])
|
|
self.assertEqual(host_pool.backups, [])
|
|
self.assertEqual(controller.write_queue, [])
|
|
self.assertEqual(controller.ack_write_queue, [])
|
|
|
|
with self.assertLogs(
|
|
"sglang.srt.managers.cache_controller", level="WARNING"
|
|
) as logs:
|
|
controller.submit_write_cp_all_layer(reservation)
|
|
|
|
self.assertEqual(host_pool.backups[0][1].tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(len(controller.ack_write_queue), 1)
|
|
self.assertEqual(controller.ack_write_queue[0].node_ids, [79])
|
|
self.assertIn("all-layer backup fallback", "\n".join(logs.output))
|
|
|
|
def test_cp_reserve_write_uses_contiguous_preferred_host_alloc(self):
|
|
host_pool = ContiguousPreferredHostPool(
|
|
torch.tensor([100, 101, 102, 103], dtype=torch.int64)
|
|
)
|
|
draft_host_pool = ContiguousPreferredHostPool(
|
|
torch.tensor([200, 201, 202, 203], dtype=torch.int64)
|
|
)
|
|
controller = self.make_controller(
|
|
host_pool,
|
|
cp_rank=1,
|
|
draft_host_pool=draft_host_pool,
|
|
draft_mem_pool_device=FakeDevicePool("draft"),
|
|
)
|
|
logical_locs = torch.arange(4, 20, dtype=torch.int64)
|
|
|
|
reservation = controller.reserve_write_cp(logical_locs, node_id=179)
|
|
|
|
self.assertEqual(reservation.metadata.host_indices.tolist(), [100, 101, 102, 103])
|
|
self.assertEqual(
|
|
reservation.metadata.draft_host_indices.tolist(), [200, 201, 202, 203]
|
|
)
|
|
self.assertEqual(host_pool.contiguous_alloc_calls, [4])
|
|
self.assertEqual(draft_host_pool.contiguous_alloc_calls, [4])
|
|
self.assertEqual(host_pool.alloc_calls, [])
|
|
self.assertEqual(draft_host_pool.alloc_calls, [])
|
|
|
|
def test_host_alloc_contiguous_preferred_skips_fragmented_fifo_prefix(self):
|
|
host_pool = DummyHostKVCacheForAlloc.__new__(DummyHostKVCacheForAlloc)
|
|
host_pool.page_size = 4
|
|
host_pool.lock = __import__("threading").RLock()
|
|
host_pool.free_slots = torch.tensor(
|
|
[100, 101, 102, 103, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
dtype=torch.int64,
|
|
)
|
|
|
|
selected = host_pool.alloc_contiguous_preferred(8)
|
|
|
|
self.assertEqual(selected.tolist(), [8, 9, 10, 11, 12, 13, 14, 15])
|
|
self.assertEqual(host_pool.free_slots.tolist(), [100, 101, 102, 103])
|
|
|
|
def test_host_alloc_contiguous_preferred_uses_lazy_extent_index(self):
|
|
host_pool = DummyHostKVCacheForAlloc.__new__(DummyHostKVCacheForAlloc)
|
|
host_pool.page_size = 4
|
|
host_pool.lock = __import__("threading").RLock()
|
|
pages = [50, 51, 52, 53, 100, 7, 8]
|
|
host_pool.free_slots = torch.tensor(
|
|
[page * 4 + offset for page in pages for offset in range(4)],
|
|
dtype=torch.int64,
|
|
)
|
|
|
|
selected = host_pool.alloc_contiguous_preferred(16)
|
|
|
|
self.assertEqual(
|
|
selected.tolist(),
|
|
[page * 4 + offset for page in [50, 51, 52, 53] for offset in range(4)],
|
|
)
|
|
self.assertEqual(host_pool.available_size(), 12)
|
|
self.assertTrue(host_pool._free_slots_dirty)
|
|
self.assertEqual(
|
|
host_pool.free_slots.tolist(),
|
|
[page * 4 + offset for page in [7, 8, 100] for offset in range(4)],
|
|
)
|
|
|
|
def test_cp_reserve_zero_owned_queues_no_ack_until_submit(self):
|
|
host_pool = FakeHostPool(torch.tensor([], dtype=torch.int64))
|
|
controller = self.make_controller(host_pool, cp_rank=3)
|
|
logical_locs = torch.arange(4, 8, dtype=torch.int64)
|
|
|
|
reservation = controller.reserve_write_cp(logical_locs, node_id=80)
|
|
|
|
self.assertEqual(reservation.metadata.host_indices.tolist(), [])
|
|
self.assertEqual(host_pool.alloc_calls, [])
|
|
self.assertEqual(controller.ack_write_queue, [])
|
|
|
|
with self.assertLogs(
|
|
"sglang.srt.managers.cache_controller", level="WARNING"
|
|
) as logs:
|
|
controller.submit_write_cp_all_layer(reservation)
|
|
|
|
self.assertEqual(len(controller.ack_write_queue), 1)
|
|
self.assertEqual(controller.ack_write_queue[0].node_ids, [80])
|
|
self.assertEqual(host_pool.backups, [])
|
|
self.assertIn("all-layer backup fallback", "\n".join(logs.output))
|
|
|
|
def test_cp_reserve_draft_allocation_failure_rolls_back_without_transfer(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
draft_host_pool = FakeHostPool(None)
|
|
controller = self.make_controller(
|
|
host_pool,
|
|
cp_rank=1,
|
|
draft_host_pool=draft_host_pool,
|
|
draft_mem_pool_device=FakeDevicePool("draft"),
|
|
)
|
|
logical_locs = torch.arange(4, 20, dtype=torch.int64)
|
|
|
|
result = controller.reserve_write_cp(logical_locs, node_id=81)
|
|
|
|
self.assertIsNone(result.metadata)
|
|
self.assertEqual(result.required_host_slots, 4)
|
|
self.assertEqual(host_pool.frees[0].tolist(), [100, 101, 102, 103])
|
|
self.assertEqual(host_pool.backups, [])
|
|
self.assertEqual(draft_host_pool.backups, [])
|
|
self.assertEqual(controller.write_queue, [])
|
|
self.assertEqual(controller.draft_write_queue, [])
|
|
|
|
def test_cp_submit_write_cp_layer_pairs_target_and_draft_with_single_final_ack(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
draft_host_pool = FakeHostPool(
|
|
torch.tensor([200, 201, 202, 203], dtype=torch.int64)
|
|
)
|
|
allocator = FakeAllocator()
|
|
allocator.device_pool = FakeDevicePool("target", layer_num=2)
|
|
draft_device_pool = FakeDevicePool("draft", layer_num=2)
|
|
controller = self.make_controller(
|
|
host_pool,
|
|
allocator=allocator,
|
|
cp_rank=1,
|
|
draft_host_pool=draft_host_pool,
|
|
draft_mem_pool_device=draft_device_pool,
|
|
)
|
|
logical_locs = torch.arange(4, 20, dtype=torch.int64)
|
|
reservation = controller.reserve_write_cp(logical_locs, node_id=82)
|
|
|
|
controller.submit_write_cp_layer(reservation, 0)
|
|
|
|
self.assertEqual(controller.ack_write_queue, [])
|
|
self.assertEqual(host_pool.layer_backups[0][1].tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(draft_host_pool.layer_backups[0][1].tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(host_pool.layer_backups[0][2], 0)
|
|
self.assertEqual(draft_host_pool.layer_backups[0][2], 0)
|
|
|
|
controller.submit_write_cp_layer(reservation, 1)
|
|
|
|
self.assertEqual(len(controller.ack_write_queue), 1)
|
|
self.assertEqual(controller.ack_write_queue[0].node_ids, [82])
|
|
self.assertEqual([x[2] for x in host_pool.layer_backups], [0, 1])
|
|
self.assertEqual([x[2] for x in draft_host_pool.layer_backups], [0, 1])
|
|
|
|
def test_cp_submit_write_cp_layer_zero_owned_final_ack_once(self):
|
|
host_pool = FakeHostPool(torch.tensor([], dtype=torch.int64))
|
|
allocator = FakeAllocator()
|
|
allocator.device_pool = FakeDevicePool("target", layer_num=2)
|
|
controller = self.make_controller(host_pool, allocator=allocator, cp_rank=3)
|
|
logical_locs = torch.arange(4, 8, dtype=torch.int64)
|
|
reservation = controller.reserve_write_cp(logical_locs, node_id=83)
|
|
|
|
controller.submit_write_cp_layer(reservation, 0)
|
|
self.assertEqual(controller.ack_write_queue, [])
|
|
|
|
controller.submit_write_cp_layer(reservation, 1)
|
|
|
|
self.assertEqual(len(controller.ack_write_queue), 1)
|
|
self.assertEqual(controller.ack_write_queue[0].node_ids, [83])
|
|
self.assertEqual(host_pool.layer_backups, [])
|
|
|
|
def test_cp_layer_hook_submits_registered_write_without_all_layer_backup(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
allocator = FakeAllocator()
|
|
allocator.device_pool = FakeDevicePool("target", layer_num=2)
|
|
controller = self.make_controller(host_pool, allocator=allocator, cp_rank=1)
|
|
logical_locs = torch.arange(4, 20, dtype=torch.int64)
|
|
reservation = controller.reserve_write_cp(logical_locs, node_id=84)
|
|
|
|
controller.submit_write_cp_per_layer(reservation, catch_up_all_layers=False)
|
|
|
|
self.assertEqual(host_pool.backups, [])
|
|
self.assertEqual(host_pool.layer_backups, [])
|
|
self.assertEqual(controller.ack_write_queue, [])
|
|
|
|
allocator.device_pool.notify_layer_end_for_backup(0)
|
|
|
|
self.assertEqual(host_pool.layer_backups[0][2], 0)
|
|
self.assertEqual(controller.ack_write_queue, [])
|
|
|
|
allocator.device_pool.notify_layer_end_for_backup(1)
|
|
|
|
self.assertEqual([x[2] for x in host_pool.layer_backups], [0, 1])
|
|
self.assertEqual(len(controller.ack_write_queue), 1)
|
|
self.assertEqual(controller.ack_write_queue[0].node_ids, [84])
|
|
self.assertEqual(host_pool.backups, [])
|
|
|
|
def test_cp_layer_hook_groups_target_backups_across_pending_reservations(self):
|
|
class SequentialHostPool(FakeHostPool):
|
|
def __init__(self, alloc_results):
|
|
super().__init__(torch.empty((0,), dtype=torch.int64))
|
|
self.alloc_results = [result.clone() for result in alloc_results]
|
|
|
|
def alloc(self, need_size):
|
|
self.alloc_calls.append(need_size)
|
|
if not self.alloc_results:
|
|
return None
|
|
return self.alloc_results.pop(0).clone()
|
|
|
|
host_pool = SequentialHostPool(
|
|
[
|
|
torch.tensor([100, 101, 102, 103], dtype=torch.int64),
|
|
torch.tensor([200, 201, 202, 203], dtype=torch.int64),
|
|
]
|
|
)
|
|
allocator = FakeAllocator()
|
|
allocator.device_pool = FakeDevicePool("target", layer_num=2)
|
|
controller = self.make_controller(host_pool, allocator=allocator, cp_rank=1)
|
|
reservation_a = controller.reserve_write_cp(
|
|
torch.arange(4, 20, dtype=torch.int64), node_id=501
|
|
)
|
|
reservation_b = controller.reserve_write_cp(
|
|
torch.arange(20, 36, dtype=torch.int64), node_id=502
|
|
)
|
|
controller.submit_write_cp_per_layer(reservation_a, catch_up_all_layers=False)
|
|
controller.submit_write_cp_per_layer(reservation_b, catch_up_all_layers=False)
|
|
|
|
allocator.device_pool.notify_layer_end_for_backup(0)
|
|
|
|
self.assertEqual(len(host_pool.layer_backups), 1)
|
|
host_indices, device_indices, layer_id, device_pool = host_pool.layer_backups[0]
|
|
self.assertEqual(
|
|
host_indices.tolist(), [100, 101, 102, 103, 200, 201, 202, 203]
|
|
)
|
|
self.assertEqual(device_indices.tolist(), [4, 5, 6, 7, 8, 9, 10, 11])
|
|
self.assertEqual(layer_id, 0)
|
|
self.assertIs(device_pool, allocator.device_pool)
|
|
self.assertEqual(controller.ack_write_queue, [])
|
|
|
|
allocator.device_pool.notify_layer_end_for_backup(1)
|
|
|
|
self.assertEqual(len(host_pool.layer_backups), 2)
|
|
self.assertEqual([backup[2] for backup in host_pool.layer_backups], [0, 1])
|
|
self.assertEqual(len(controller.ack_write_queue), 2)
|
|
self.assertEqual(
|
|
[ack.node_ids for ack in controller.ack_write_queue], [[501], [502]]
|
|
)
|
|
|
|
def test_cp_layer_hook_groups_target_and_draft_backups_by_source(self):
|
|
class SequentialHostPool(FakeHostPool):
|
|
def __init__(self, alloc_results):
|
|
super().__init__(torch.empty((0,), dtype=torch.int64))
|
|
self.alloc_results = [result.clone() for result in alloc_results]
|
|
|
|
def alloc(self, need_size):
|
|
self.alloc_calls.append(need_size)
|
|
if not self.alloc_results:
|
|
return None
|
|
return self.alloc_results.pop(0).clone()
|
|
|
|
host_pool = SequentialHostPool(
|
|
[
|
|
torch.tensor([100, 101, 102, 103], dtype=torch.int64),
|
|
torch.tensor([200, 201, 202, 203], dtype=torch.int64),
|
|
]
|
|
)
|
|
draft_host_pool = SequentialHostPool(
|
|
[
|
|
torch.tensor([300, 301, 302, 303], dtype=torch.int64),
|
|
torch.tensor([400, 401, 402, 403], dtype=torch.int64),
|
|
]
|
|
)
|
|
allocator = FakeAllocator()
|
|
allocator.device_pool = FakeDevicePool("target", layer_num=2)
|
|
draft_device_pool = FakeDevicePool("draft", layer_num=2)
|
|
controller = self.make_controller(
|
|
host_pool,
|
|
allocator=allocator,
|
|
cp_rank=1,
|
|
draft_host_pool=draft_host_pool,
|
|
draft_mem_pool_device=draft_device_pool,
|
|
)
|
|
reservation_a = controller.reserve_write_cp(
|
|
torch.arange(4, 20, dtype=torch.int64), node_id=601
|
|
)
|
|
reservation_b = controller.reserve_write_cp(
|
|
torch.arange(20, 36, dtype=torch.int64), node_id=602
|
|
)
|
|
controller.submit_write_cp_per_layer(reservation_a, catch_up_all_layers=False)
|
|
controller.submit_write_cp_per_layer(reservation_b, catch_up_all_layers=False)
|
|
|
|
allocator.device_pool.notify_layer_end_for_backup(0)
|
|
|
|
self.assertEqual(len(host_pool.layer_backups), 1)
|
|
self.assertEqual(len(draft_host_pool.layer_backups), 0)
|
|
self.assertEqual(
|
|
host_pool.layer_backups[0][0].tolist(),
|
|
[100, 101, 102, 103, 200, 201, 202, 203],
|
|
)
|
|
self.assertEqual(
|
|
host_pool.layer_backups[0][1].tolist(),
|
|
[4, 5, 6, 7, 8, 9, 10, 11],
|
|
)
|
|
|
|
draft_device_pool.notify_layer_end_for_backup(0)
|
|
|
|
self.assertEqual(len(draft_host_pool.layer_backups), 1)
|
|
self.assertEqual(
|
|
draft_host_pool.layer_backups[0][0].tolist(),
|
|
[300, 301, 302, 303, 400, 401, 402, 403],
|
|
)
|
|
self.assertEqual(
|
|
draft_host_pool.layer_backups[0][1].tolist(),
|
|
[4, 5, 6, 7, 8, 9, 10, 11],
|
|
)
|
|
self.assertEqual(controller.ack_write_queue, [])
|
|
|
|
allocator.device_pool.notify_layer_end_for_backup(1)
|
|
self.assertEqual(len(host_pool.layer_backups), 2)
|
|
self.assertEqual(controller.ack_write_queue, [])
|
|
|
|
draft_device_pool.notify_layer_end_for_backup(1)
|
|
|
|
self.assertEqual(len(draft_host_pool.layer_backups), 2)
|
|
self.assertEqual(len(controller.ack_write_queue), 2)
|
|
self.assertEqual(
|
|
[ack.node_ids for ack in controller.ack_write_queue], [[601], [602]]
|
|
)
|
|
|
|
def test_cp_layer_hook_keeps_zero_owned_ack_in_grouped_backup(self):
|
|
class SequentialHostPool(FakeHostPool):
|
|
def __init__(self, alloc_results):
|
|
super().__init__(torch.empty((0,), dtype=torch.int64))
|
|
self.alloc_results = [result.clone() for result in alloc_results]
|
|
|
|
def alloc(self, need_size):
|
|
self.alloc_calls.append(need_size)
|
|
if not self.alloc_results:
|
|
return None
|
|
return self.alloc_results.pop(0).clone()
|
|
|
|
host_pool = SequentialHostPool(
|
|
[torch.tensor([100, 101, 102, 103], dtype=torch.int64)]
|
|
)
|
|
allocator = FakeAllocator()
|
|
allocator.device_pool = FakeDevicePool("target", layer_num=2)
|
|
controller = self.make_controller(host_pool, allocator=allocator, cp_rank=1)
|
|
zero_owned = controller.reserve_write_cp(
|
|
torch.arange(4, 8, dtype=torch.int64), node_id=701
|
|
)
|
|
owned = controller.reserve_write_cp(
|
|
torch.arange(8, 24, dtype=torch.int64), node_id=702
|
|
)
|
|
controller.submit_write_cp_per_layer(zero_owned, catch_up_all_layers=False)
|
|
controller.submit_write_cp_per_layer(owned, catch_up_all_layers=False)
|
|
|
|
allocator.device_pool.notify_layer_end_for_backup(0)
|
|
allocator.device_pool.notify_layer_end_for_backup(1)
|
|
|
|
self.assertEqual(len(host_pool.layer_backups), 2)
|
|
for host_indices, device_indices, _, _ in host_pool.layer_backups:
|
|
self.assertEqual(host_indices.tolist(), [100, 101, 102, 103])
|
|
self.assertEqual(device_indices.tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(len(controller.ack_write_queue), 2)
|
|
self.assertEqual(
|
|
[ack.node_ids for ack in controller.ack_write_queue], [[701], [702]]
|
|
)
|
|
|
|
def test_cp_layer_hook_waits_for_draft_source_before_final_ack(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
draft_host_pool = FakeHostPool(
|
|
torch.tensor([200, 201, 202, 203], dtype=torch.int64)
|
|
)
|
|
allocator = FakeAllocator()
|
|
allocator.device_pool = FakeDevicePool("target", layer_num=1)
|
|
draft_device_pool = FakeDevicePool("draft", layer_num=1)
|
|
controller = self.make_controller(
|
|
host_pool,
|
|
allocator=allocator,
|
|
cp_rank=1,
|
|
draft_host_pool=draft_host_pool,
|
|
draft_mem_pool_device=draft_device_pool,
|
|
)
|
|
logical_locs = torch.arange(4, 20, dtype=torch.int64)
|
|
reservation = controller.reserve_write_cp(logical_locs, node_id=85)
|
|
|
|
controller.submit_write_cp_per_layer(reservation, catch_up_all_layers=False)
|
|
allocator.device_pool.notify_layer_end_for_backup(0)
|
|
|
|
self.assertEqual([x[2] for x in host_pool.layer_backups], [0])
|
|
self.assertEqual(draft_host_pool.layer_backups, [])
|
|
self.assertEqual(controller.ack_write_queue, [])
|
|
|
|
draft_device_pool.notify_layer_end_for_backup(0)
|
|
|
|
self.assertEqual([x[2] for x in draft_host_pool.layer_backups], [0])
|
|
self.assertEqual(len(controller.ack_write_queue), 1)
|
|
self.assertEqual(controller.ack_write_queue[0].node_ids, [85])
|
|
|
|
def test_generate_storage_config_constructs_config_at_runtime(self):
|
|
controller = HiCacheController.__new__(HiCacheController)
|
|
controller.mem_pool_device = FakeDevicePool()
|
|
controller.mem_pool_host = FakeHostPool(torch.tensor([], dtype=torch.int64))
|
|
controller.pp_rank = 1
|
|
controller.pp_size = 2
|
|
controller.enable_storage_metrics = True
|
|
|
|
with patch(
|
|
"sglang.srt.managers.cache_controller.is_dp_attention_enabled",
|
|
return_value=False,
|
|
), patch(
|
|
"sglang.srt.managers.cache_controller.get_tensor_model_parallel_rank",
|
|
return_value=3,
|
|
), patch(
|
|
"sglang.srt.managers.cache_controller.get_tensor_model_parallel_world_size",
|
|
return_value=4,
|
|
):
|
|
config = controller._generate_storage_config(
|
|
model_name="test-model",
|
|
storage_backend_extra_config={"tp_lcm_size": 8},
|
|
)
|
|
|
|
self.assertEqual(config.tp_rank, 3)
|
|
self.assertEqual(config.tp_size, 4)
|
|
self.assertEqual(config.pp_rank, 1)
|
|
self.assertEqual(config.pp_size, 2)
|
|
self.assertEqual(config.model_name, "test-model")
|
|
self.assertEqual(config.tp_lcm_size, 8)
|
|
|
|
def test_attach_storage_backend_rejects_cp_hicache(self):
|
|
host_pool = FakeHostPool(torch.tensor([], dtype=torch.int64))
|
|
controller = self.make_controller(host_pool)
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "CP shared KV.*storage backend"):
|
|
controller.attach_storage_backend("mooncake")
|
|
|
|
|
|
class TestHiCacheControllerCPLoad(TestHiCacheControllerCPWrite):
|
|
def test_cp_start_loading_batches_multiple_load_cp_requests_with_draft(self):
|
|
class SequentialOwnerAllocator(FakeAllocator):
|
|
def __init__(self, alloc_results):
|
|
super().__init__()
|
|
self.alloc_results = [result.clone() for result in alloc_results]
|
|
self.device_pool = FakeDevicePool("target", layer_num=2)
|
|
|
|
def alloc_pages_with_owners(self, page_owners):
|
|
owners = list(page_owners)
|
|
self.owner_alloc_calls.append(owners)
|
|
if not self.alloc_results:
|
|
return None
|
|
return self.alloc_results.pop(0).clone()
|
|
|
|
host_pool = FakeHostPool(torch.empty((0,), dtype=torch.int64))
|
|
draft_host_pool = FakeHostPool(torch.empty((0,), dtype=torch.int64))
|
|
allocator = SequentialOwnerAllocator(
|
|
[
|
|
torch.arange(64, 80, dtype=torch.int64),
|
|
torch.arange(80, 96, dtype=torch.int64),
|
|
]
|
|
)
|
|
draft_device_pool = FakeDevicePool("draft", layer_num=2)
|
|
controller = self.make_controller(
|
|
host_pool,
|
|
allocator=allocator,
|
|
cp_rank=1,
|
|
draft_host_pool=draft_host_pool,
|
|
draft_mem_pool_device=draft_device_pool,
|
|
)
|
|
node_a = TreeNode()
|
|
node_a.host_len = 16
|
|
node_a.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=16,
|
|
owned_positions=torch.tensor([4, 5, 6, 7], dtype=torch.int64),
|
|
host_indices=torch.tensor([100, 101, 102, 103], dtype=torch.int64),
|
|
draft_host_indices=torch.tensor([300, 301, 302, 303], dtype=torch.int64),
|
|
page_owners=torch.tensor([3, 0, 1, 2], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
node_b = TreeNode()
|
|
node_b.host_len = 16
|
|
node_b.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=16,
|
|
owned_positions=torch.tensor([4, 5, 6, 7], dtype=torch.int64),
|
|
host_indices=torch.tensor([200, 201, 202, 203], dtype=torch.int64),
|
|
draft_host_indices=torch.tensor([400, 401, 402, 403], dtype=torch.int64),
|
|
page_owners=torch.tensor([3, 0, 1, 2], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
|
|
device_indices_a = controller.load_cp([node_a], node_id=201)
|
|
device_indices_b = controller.load_cp([node_b], node_id=202)
|
|
controller.start_loading()
|
|
|
|
self.assertEqual(device_indices_a.tolist(), list(range(64, 80)))
|
|
self.assertEqual(device_indices_b.tolist(), list(range(80, 96)))
|
|
self.assertEqual(
|
|
allocator.owner_alloc_calls,
|
|
[[3, 0, 1, 2], [3, 0, 1, 2]],
|
|
)
|
|
self.assertEqual(len(host_pool.loads), 2)
|
|
self.assertEqual([load[2] for load in host_pool.loads], [0, 1])
|
|
for host_indices, device_indices, _, device_pool in host_pool.loads:
|
|
self.assertEqual(
|
|
host_indices.tolist(),
|
|
[100, 101, 102, 103, 200, 201, 202, 203],
|
|
)
|
|
self.assertEqual(device_indices.tolist(), [20, 21, 22, 23, 24, 25, 26, 27])
|
|
self.assertIs(device_pool, allocator.device_pool)
|
|
|
|
self.assertEqual(len(draft_host_pool.loads), 2)
|
|
self.assertEqual([load[2] for load in draft_host_pool.loads], [0, 1])
|
|
for host_indices, device_indices, _, device_pool in draft_host_pool.loads:
|
|
self.assertEqual(
|
|
host_indices.tolist(),
|
|
[300, 301, 302, 303, 400, 401, 402, 403],
|
|
)
|
|
self.assertEqual(device_indices.tolist(), [20, 21, 22, 23, 24, 25, 26, 27])
|
|
self.assertIs(device_pool, draft_device_pool)
|
|
|
|
self.assertEqual(len(controller.ack_load_queue), 1)
|
|
self.assertEqual(controller.ack_load_queue[0].node_ids, [201, 202])
|
|
|
|
def test_cp_start_loading_keeps_zero_owned_load_ack_in_batched_load(self):
|
|
class SequentialOwnerAllocator(FakeAllocator):
|
|
def __init__(self, alloc_results):
|
|
super().__init__()
|
|
self.alloc_results = [result.clone() for result in alloc_results]
|
|
self.device_pool = FakeDevicePool("target", layer_num=2)
|
|
|
|
def alloc_pages_with_owners(self, page_owners):
|
|
owners = list(page_owners)
|
|
self.owner_alloc_calls.append(owners)
|
|
if not self.alloc_results:
|
|
return None
|
|
return self.alloc_results.pop(0).clone()
|
|
|
|
host_pool = FakeHostPool(torch.empty((0,), dtype=torch.int64))
|
|
allocator = SequentialOwnerAllocator(
|
|
[
|
|
torch.arange(64, 68, dtype=torch.int64),
|
|
torch.arange(80, 96, dtype=torch.int64),
|
|
]
|
|
)
|
|
controller = self.make_controller(host_pool, allocator=allocator, cp_rank=1)
|
|
zero_owned_node = TreeNode()
|
|
zero_owned_node.host_len = 4
|
|
zero_owned_node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.tensor([0], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
owned_node = TreeNode()
|
|
owned_node.host_len = 16
|
|
owned_node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=16,
|
|
owned_positions=torch.tensor([4, 5, 6, 7], dtype=torch.int64),
|
|
host_indices=torch.tensor([200, 201, 202, 203], dtype=torch.int64),
|
|
page_owners=torch.tensor([3, 0, 1, 2], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
|
|
zero_visible_indices = controller.load_cp([zero_owned_node], node_id=301)
|
|
owned_visible_indices = controller.load_cp([owned_node], node_id=302)
|
|
controller.start_loading()
|
|
|
|
self.assertEqual(zero_visible_indices.tolist(), [64, 65, 66, 67])
|
|
self.assertEqual(owned_visible_indices.tolist(), list(range(80, 96)))
|
|
self.assertEqual(allocator.owner_alloc_calls, [[0], [3, 0, 1, 2]])
|
|
self.assertEqual(len(host_pool.loads), 2)
|
|
self.assertEqual([load[2] for load in host_pool.loads], [0, 1])
|
|
for host_indices, device_indices, _, _ in host_pool.loads:
|
|
self.assertEqual(host_indices.tolist(), [200, 201, 202, 203])
|
|
self.assertEqual(device_indices.tolist(), [24, 25, 26, 27])
|
|
self.assertEqual(len(controller.ack_load_queue), 1)
|
|
self.assertEqual(controller.ack_load_queue[0].node_ids, [301, 302])
|
|
|
|
def test_cp_load_allocates_full_logical_locs_and_transfers_owned_physical_locs(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
allocator = FakeAllocator(alloc_result=torch.arange(64, 80, dtype=torch.int64))
|
|
controller = self.make_controller(host_pool, allocator=allocator, cp_rank=1)
|
|
node = TreeNode()
|
|
node.host_len = 16
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=16,
|
|
owned_positions=torch.tensor([4, 5, 6, 7], dtype=torch.int64),
|
|
host_indices=torch.tensor([100, 101, 102, 103], dtype=torch.int64),
|
|
page_owners=torch.tensor([3, 0, 1, 2], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
|
|
device_indices = controller.load_cp([node], node_id=11)
|
|
controller.start_loading()
|
|
|
|
self.assertEqual(device_indices.tolist(), list(range(64, 80)))
|
|
self.assertEqual(allocator.alloc_calls, [])
|
|
self.assertEqual(allocator.owner_alloc_calls, [[3, 0, 1, 2]])
|
|
self.assertEqual(host_pool.loads[0][1].tolist(), [20, 21, 22, 23])
|
|
|
|
def test_cp_load_returns_valid_locs_while_transferring_padded_tail_page(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
allocator = FakeAllocator(alloc_result=torch.arange(64, 72, dtype=torch.int64))
|
|
controller = self.make_controller(host_pool, allocator=allocator, cp_rank=1)
|
|
node = TreeNode()
|
|
node.host_len = 6
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=6,
|
|
padded_len=8,
|
|
owned_positions=torch.tensor([4, 5, 6, 7], dtype=torch.int64),
|
|
host_indices=torch.tensor([100, 101, 102, 103], dtype=torch.int64),
|
|
page_owners=torch.tensor([3, 0], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
|
|
device_indices = controller.load_cp([node], node_id=112)
|
|
controller.start_loading()
|
|
|
|
self.assertEqual(device_indices.tolist(), list(range(64, 70)))
|
|
self.assertEqual(allocator.owner_alloc_calls, [[3, 0]])
|
|
self.assertEqual(host_pool.loads[0][0].tolist(), [100, 101, 102, 103])
|
|
self.assertEqual(host_pool.loads[0][1].tolist(), [20, 21, 22, 23])
|
|
|
|
def test_start_loading_prepares_load_op_once_for_all_layers(self):
|
|
class PreparingFakeHostPool(FakeHostPool):
|
|
def __init__(self, alloc_result):
|
|
super().__init__(alloc_result)
|
|
self.begin_calls = []
|
|
self.end_calls = 0
|
|
self.active = False
|
|
|
|
def begin_load_to_device_op(self, host_indices, device_indices, io_backend):
|
|
self.begin_calls.append(
|
|
(host_indices.clone(), device_indices.clone(), io_backend)
|
|
)
|
|
self.active = True
|
|
|
|
def end_load_to_device_op(self):
|
|
self.end_calls += 1
|
|
self.active = False
|
|
|
|
def load_to_device_per_layer(
|
|
self, device_pool, host_indices, device_indices, layer_id, io_backend
|
|
):
|
|
assert self.active
|
|
super().load_to_device_per_layer(
|
|
device_pool, host_indices, device_indices, layer_id, io_backend
|
|
)
|
|
|
|
host_pool = PreparingFakeHostPool(
|
|
torch.tensor([100, 101, 102, 103], dtype=torch.int64)
|
|
)
|
|
allocator = FakeAllocator(alloc_result=torch.arange(64, 80, dtype=torch.int64))
|
|
allocator.device_pool = FakeDevicePool(layer_num=3)
|
|
controller = self.make_controller(host_pool, allocator=allocator, cp_rank=1)
|
|
node = TreeNode()
|
|
node.host_len = 16
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=16,
|
|
owned_positions=torch.tensor([4, 5, 6, 7], dtype=torch.int64),
|
|
host_indices=torch.tensor([100, 101, 102, 103], dtype=torch.int64),
|
|
page_owners=torch.tensor([3, 0, 1, 2], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
|
|
controller.load_cp([node], node_id=113)
|
|
controller.start_loading()
|
|
|
|
self.assertEqual(len(host_pool.begin_calls), 1)
|
|
self.assertEqual(host_pool.end_calls, 1)
|
|
self.assertEqual([load[2] for load in host_pool.loads], [0, 1, 2])
|
|
|
|
def test_start_loading_emits_descriptor_timing_when_enabled(self):
|
|
host_pool = FakeHostPool(
|
|
torch.tensor([100, 101, 102, 103], dtype=torch.int64)
|
|
)
|
|
allocator = FakeAllocator(alloc_result=torch.arange(64, 80, dtype=torch.int64))
|
|
allocator.device_pool = FakeDevicePool(layer_num=2)
|
|
controller = self.make_controller(host_pool, allocator=allocator, cp_rank=1)
|
|
node = TreeNode()
|
|
node.host_len = 16
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=16,
|
|
owned_positions=torch.tensor([4, 5, 6, 7], dtype=torch.int64),
|
|
host_indices=torch.tensor([100, 101, 102, 103], dtype=torch.int64),
|
|
page_owners=torch.tensor([3, 0, 1, 2], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
controller.load_cp([node], node_id=114)
|
|
timing_keys = []
|
|
|
|
def record_timing(key, start_time, message, *args):
|
|
timing_keys.append(key)
|
|
|
|
with patch(
|
|
"sglang.srt.managers.cache_controller.envs."
|
|
"SGLANG_CP_SHARED_KV_BS_GT1_TIMING.get",
|
|
return_value=True,
|
|
), patch(
|
|
"sglang.srt.managers.cache_controller."
|
|
"_cp_shared_kv_bs_gt1_cache_timing",
|
|
side_effect=record_timing,
|
|
):
|
|
controller.start_loading()
|
|
|
|
self.assertIn("prepare_load_descriptor", timing_keys)
|
|
self.assertIn("submit_h2d_layer_loop", timing_keys)
|
|
self.assertIn("submit_h2d_layer_per_call_slow", timing_keys)
|
|
self.assertIn("end_load_descriptor", timing_keys)
|
|
|
|
def test_cp_load_frees_unexpected_owner_allocator_length(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
allocator = FakeAllocator(alloc_result=torch.arange(64, 76, dtype=torch.int64))
|
|
controller = self.make_controller(host_pool, allocator=allocator, cp_rank=1)
|
|
node = TreeNode()
|
|
node.host_len = 16
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=16,
|
|
owned_positions=torch.tensor([4, 5, 6, 7], dtype=torch.int64),
|
|
host_indices=torch.tensor([100, 101, 102, 103], dtype=torch.int64),
|
|
page_owners=torch.tensor([3, 0, 1, 2], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
|
|
with self.assertRaisesRegex(
|
|
RuntimeError, "alloc_pages_with_owners returned unexpected length"
|
|
):
|
|
controller.load_cp([node], node_id=111)
|
|
|
|
self.assertEqual(allocator.owner_alloc_calls, [[3, 0, 1, 2]])
|
|
self.assertEqual(allocator.frees[0].tolist(), list(range(64, 76)))
|
|
|
|
def test_cp_load_with_draft_pool_restores_target_and_draft_locs(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
draft_host_pool = FakeHostPool(
|
|
torch.tensor([200, 201, 202, 203], dtype=torch.int64)
|
|
)
|
|
draft_device_pool = FakeDevicePool("draft")
|
|
allocator = FakeAllocator(alloc_result=torch.arange(64, 80, dtype=torch.int64))
|
|
controller = self.make_controller(
|
|
host_pool,
|
|
allocator=allocator,
|
|
cp_rank=1,
|
|
draft_host_pool=draft_host_pool,
|
|
draft_mem_pool_device=draft_device_pool,
|
|
)
|
|
node = TreeNode()
|
|
node.host_len = 16
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=16,
|
|
owned_positions=torch.tensor([4, 5, 6, 7], dtype=torch.int64),
|
|
host_indices=torch.tensor([100, 101, 102, 103], dtype=torch.int64),
|
|
draft_host_indices=torch.tensor([200, 201, 202, 203], dtype=torch.int64),
|
|
page_owners=torch.tensor([3, 0, 1, 2], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
|
|
device_indices = controller.load_cp([node], node_id=14)
|
|
controller.start_loading()
|
|
|
|
self.assertEqual(device_indices.tolist(), list(range(64, 80)))
|
|
self.assertEqual(allocator.owner_alloc_calls, [[3, 0, 1, 2]])
|
|
self.assertEqual(host_pool.loads[0][1].tolist(), [20, 21, 22, 23])
|
|
self.assertEqual(draft_host_pool.loads[0][1].tolist(), [20, 21, 22, 23])
|
|
self.assertIs(draft_host_pool.loads[0][3], draft_device_pool)
|
|
self.assertEqual(len(controller.ack_load_queue), 1)
|
|
self.assertEqual(controller.ack_load_queue[0].node_ids, [14])
|
|
|
|
def test_cp_load_valid_tail_with_draft_returns_valid_locs_only(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
draft_host_pool = FakeHostPool(
|
|
torch.tensor([200, 201, 202, 203], dtype=torch.int64)
|
|
)
|
|
draft_device_pool = FakeDevicePool("draft")
|
|
allocator = FakeAllocator(alloc_result=torch.arange(8, 12, dtype=torch.int64))
|
|
controller = self.make_controller(
|
|
host_pool,
|
|
allocator=allocator,
|
|
cp_rank=1,
|
|
draft_host_pool=draft_host_pool,
|
|
draft_mem_pool_device=draft_device_pool,
|
|
)
|
|
node = TreeNode()
|
|
node.host_len = 3
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=3,
|
|
padded_len=4,
|
|
owned_positions=torch.tensor([0, 1, 2, 3], dtype=torch.int64),
|
|
host_indices=torch.tensor([100, 101, 102, 103], dtype=torch.int64),
|
|
draft_host_indices=torch.tensor([200, 201, 202, 203], dtype=torch.int64),
|
|
page_owners=torch.tensor([1], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
|
|
device_indices = controller.load_cp([node], node_id=178)
|
|
controller.start_loading()
|
|
|
|
self.assertEqual(device_indices.tolist(), [8, 9, 10])
|
|
self.assertEqual(allocator.owner_alloc_calls, [[1]])
|
|
self.assertEqual(host_pool.loads[0][1].tolist(), [4, 5, 6, 7])
|
|
self.assertEqual(draft_host_pool.loads[0][1].tolist(), [4, 5, 6, 7])
|
|
self.assertIs(draft_host_pool.loads[0][3], draft_device_pool)
|
|
self.assertEqual(len(controller.ack_load_queue), 1)
|
|
self.assertEqual(controller.ack_load_queue[0].node_ids, [178])
|
|
|
|
def test_cp_start_loading_loads_draft_before_target_layer_ready(self):
|
|
order = []
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
draft_host_pool = FakeHostPool(
|
|
torch.tensor([200, 201, 202, 203], dtype=torch.int64)
|
|
)
|
|
allocator = FakeAllocator(alloc_result=torch.arange(64, 80, dtype=torch.int64))
|
|
controller = self.make_controller(
|
|
host_pool,
|
|
allocator=allocator,
|
|
cp_rank=1,
|
|
draft_host_pool=draft_host_pool,
|
|
draft_mem_pool_device=FakeDevicePool("draft"),
|
|
)
|
|
controller.layer_done_counter = RecordingLayerDoneCounter(order)
|
|
|
|
def record_target_load(
|
|
device_pool, host_indices, device_indices, layer_id, io_backend
|
|
):
|
|
order.append(("target", layer_id))
|
|
|
|
def record_draft_load(
|
|
device_pool, host_indices, device_indices, layer_id, io_backend
|
|
):
|
|
order.append(("draft", layer_id))
|
|
|
|
host_pool.load_to_device_per_layer = record_target_load
|
|
draft_host_pool.load_to_device_per_layer = record_draft_load
|
|
node = TreeNode()
|
|
node.host_len = 16
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=16,
|
|
owned_positions=torch.tensor([4, 5, 6, 7], dtype=torch.int64),
|
|
host_indices=torch.tensor([100, 101, 102, 103], dtype=torch.int64),
|
|
draft_host_indices=torch.tensor([200, 201, 202, 203], dtype=torch.int64),
|
|
page_owners=torch.tensor([3, 0, 1, 2], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
|
|
controller.load_cp([node], node_id=114)
|
|
controller.start_loading()
|
|
|
|
self.assertEqual(order, [("draft", 0), ("target", 0), ("complete", 0)])
|
|
|
|
def test_cp_load_zero_owned_returns_full_logical_locs_and_noop_ack(self):
|
|
host_pool = FakeHostPool(torch.tensor([], dtype=torch.int64))
|
|
allocator = FakeAllocator(alloc_result=torch.arange(64, 68, dtype=torch.int64))
|
|
controller = self.make_controller(host_pool, allocator=allocator, cp_rank=3)
|
|
node = TreeNode()
|
|
node.host_len = 4
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.tensor([0], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
|
|
device_indices = controller.load_cp([node], node_id=12)
|
|
controller.start_loading()
|
|
|
|
self.assertEqual(device_indices.tolist(), [64, 65, 66, 67])
|
|
self.assertEqual(allocator.owner_alloc_calls, [[0]])
|
|
self.assertEqual(host_pool.loads, [])
|
|
self.assertEqual(len(controller.ack_load_queue), 1)
|
|
|
|
def test_cp_load_zero_owned_rejects_missing_draft_metadata_when_draft_attached(self):
|
|
host_pool = FakeHostPool(torch.tensor([], dtype=torch.int64))
|
|
allocator = FakeAllocator(alloc_result=torch.arange(64, 68, dtype=torch.int64))
|
|
controller = self.make_controller(
|
|
host_pool,
|
|
allocator=allocator,
|
|
cp_rank=3,
|
|
draft_host_pool=FakeHostPool(torch.tensor([], dtype=torch.int64)),
|
|
draft_mem_pool_device=FakeDevicePool("draft"),
|
|
)
|
|
node = TreeNode()
|
|
node.host_len = 4
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.tensor([0], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "draft KV restore requested"):
|
|
controller.load_cp([node], node_id=33)
|
|
|
|
self.assertEqual(allocator.owner_alloc_calls, [[0]])
|
|
self.assertEqual(allocator.frees[0].tolist(), [64, 65, 66, 67])
|
|
self.assertEqual(controller.load_queue, [])
|
|
self.assertEqual(controller.draft_load_queue, [])
|
|
|
|
def test_cp_load_queues_cpu_host_indices_before_backend_moves(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
allocator = FakeAllocator(alloc_result=torch.arange(64, 80, dtype=torch.int64))
|
|
controller = self.make_controller(host_pool, allocator=allocator, cp_rank=1)
|
|
host_indices = HostIndicesTensor(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
node = TreeNode()
|
|
node.host_len = 16
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=16,
|
|
owned_positions=torch.tensor([4, 5, 6, 7], dtype=torch.int64),
|
|
host_indices=torch.tensor([100, 101, 102, 103], dtype=torch.int64),
|
|
page_owners=torch.tensor([3, 0, 1, 2], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
metadata.host_indices = host_indices
|
|
node.cp_hicache = metadata
|
|
|
|
controller.load_cp([node], node_id=13)
|
|
|
|
queued_op = controller.load_queue[0]
|
|
self.assertEqual(queued_op.host_indices.device.type, "cpu")
|
|
self.assertEqual(queued_op.host_indices.tolist(), [100, 101, 102, 103])
|
|
self.assertEqual(queued_op.device_indices.tolist(), [20, 21, 22, 23])
|
|
|
|
def test_direct_layer_page_first_move_indices_keeps_host_order_and_cpu_device_indices(self):
|
|
host_pool = FakeHostPool(torch.empty((0,), dtype=torch.int64))
|
|
host_pool.layout = "layer_page_first"
|
|
controller = self.make_controller(host_pool, cp_rank=1)
|
|
op = CacheOperation(
|
|
host_indices=torch.tensor([12, 8, 9, 10], dtype=torch.int64),
|
|
device_indices=torch.tensor([32, 28, 29, 30], dtype=torch.int64),
|
|
node_id=7,
|
|
)
|
|
|
|
host_indices, device_indices = controller.move_indices(op, host_pool)
|
|
|
|
self.assertEqual(host_indices.tolist(), [12, 8, 9, 10])
|
|
self.assertEqual(host_indices.device.type, "cpu")
|
|
self.assertEqual(device_indices.tolist(), [32, 28, 29, 30])
|
|
self.assertEqual(device_indices.device.type, "cpu")
|
|
|
|
def test_cp_load_rejects_non_contiguous_physical_device_page(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
allocator = FakeAllocator(
|
|
alloc_result=torch.tensor(
|
|
[64, 65, 66, 67, 68, 69, 71, 70, 72, 73, 74, 75, 76, 77, 78, 79],
|
|
dtype=torch.int64,
|
|
)
|
|
)
|
|
controller = self.make_controller(host_pool, allocator=allocator, cp_rank=1)
|
|
node = TreeNode()
|
|
node.host_len = 16
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=16,
|
|
owned_positions=torch.tensor([4, 5, 6, 7], dtype=torch.int64),
|
|
host_indices=torch.tensor([100, 101, 102, 103], dtype=torch.int64),
|
|
page_owners=torch.tensor([3, 0, 1, 2], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
|
|
with self.assertRaisesRegex(
|
|
ValueError, "physical_device_indices.*contiguous page spans"
|
|
):
|
|
controller.load_cp([node], node_id=31)
|
|
|
|
def test_cp_load_rejects_non_contiguous_host_page(self):
|
|
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
|
|
allocator = FakeAllocator(alloc_result=torch.arange(64, 80, dtype=torch.int64))
|
|
controller = self.make_controller(host_pool, allocator=allocator, cp_rank=1)
|
|
node = TreeNode()
|
|
node.host_len = 16
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=16,
|
|
owned_positions=torch.tensor([4, 5, 6, 7], dtype=torch.int64),
|
|
host_indices=torch.tensor([100, 101, 103, 102], dtype=torch.int64),
|
|
page_owners=torch.tensor([3, 0, 1, 2], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
|
|
with self.assertRaisesRegex(ValueError, "host_indices.*contiguous page spans"):
|
|
controller.load_cp([node], node_id=32)
|
|
|
|
def test_cp_evict_host_frees_target_and_draft_host_indices(self):
|
|
host_pool = FakeHostPool(torch.tensor([], dtype=torch.int64))
|
|
draft_host_pool = FakeHostPool(torch.tensor([], dtype=torch.int64))
|
|
controller = self.make_controller(
|
|
host_pool,
|
|
draft_host_pool=draft_host_pool,
|
|
draft_mem_pool_device=FakeDevicePool("draft"),
|
|
)
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=16,
|
|
owned_positions=torch.tensor([0, 1, 2, 3], dtype=torch.int64),
|
|
host_indices=torch.tensor([100, 101, 102, 103], dtype=torch.int64),
|
|
draft_host_indices=torch.tensor([200, 201, 202, 203], dtype=torch.int64),
|
|
page_owners=torch.tensor([0, 1, 2, 3], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
|
|
freed = controller.evict_cp_host(metadata)
|
|
|
|
self.assertEqual(freed, 4)
|
|
self.assertEqual(host_pool.frees[0].tolist(), [100, 101, 102, 103])
|
|
self.assertEqual(draft_host_pool.frees[0].tolist(), [200, 201, 202, 203])
|
|
|
|
def test_cp_evict_host_rejects_missing_draft_metadata_before_target_free(self):
|
|
host_pool = FakeHostPool(torch.tensor([], dtype=torch.int64))
|
|
draft_host_pool = FakeHostPool(torch.tensor([], dtype=torch.int64))
|
|
controller = self.make_controller(
|
|
host_pool,
|
|
draft_host_pool=draft_host_pool,
|
|
draft_mem_pool_device=FakeDevicePool("draft"),
|
|
)
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=16,
|
|
owned_positions=torch.tensor([0, 1, 2, 3], dtype=torch.int64),
|
|
host_indices=torch.tensor([100, 101, 102, 103], dtype=torch.int64),
|
|
page_owners=torch.tensor([0, 1, 2, 3], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "draft.*evict.*draft_host_indices"):
|
|
controller.evict_cp_host(metadata)
|
|
|
|
self.assertEqual(host_pool.frees, [])
|
|
self.assertEqual(draft_host_pool.frees, [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|