feat: add page-aligned index validation for CP HiCache direct transfers

Add validate_page_aligned_token_indices utility and apply it across
HiCache write/load paths, NSA indexer transfers, and CUDA direct copy
kernels to reject malformed (partial, misaligned, non-contiguous) page
groups before they reach native transfer code. Also validate supported
CP HiCache backend/layout combinations at server startup.
This commit is contained in:
2026-05-10 02:02:54 +08:00
parent f38937723e
commit 3a5928ef53
10 changed files with 472 additions and 18 deletions
@@ -10,11 +10,12 @@ from sglang.srt.mem_cache.memory_pool_host import (
)
from sglang.srt.utils import is_cuda, is_hip, is_npu, is_xpu
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=3, suite="stage-b-test-1-gpu-small")
class TestNSAHiCacheTransfer(unittest.TestCase):
class TestNSAHiCacheTransfer(CustomTestCase):
def setUp(self):
if not torch.cuda.is_available():
self.skipTest("CUDA is required for NSA host transfer tests.")
@@ -36,7 +37,7 @@ class TestNSAHiCacheTransfer(unittest.TestCase):
]
return torch.cat(parts, dim=0)
def _run_device_to_host_indexer_copy(self, io_backend: str):
def _run_device_to_host_indexer_copy(self, io_backend: str, layout: str):
page_size = 1 if is_hip() else 64
layer_num = 2
size = page_size * 4
@@ -63,7 +64,7 @@ class TestNSAHiCacheTransfer(unittest.TestCase):
host_to_device_ratio=2.0,
host_size=0,
page_size=page_size,
layout="layer_first",
layout=layout,
pin_memory=pin_memory,
device="cpu",
)
@@ -105,26 +106,88 @@ class TestNSAHiCacheTransfer(unittest.TestCase):
for host_page, device_page in zip(
host_pages.tolist(), device_pages.tolist()
):
got = host_pool.index_k_with_scale_buffer[layer_id][host_page].cpu()
if layout == "page_first_direct":
got = host_pool.index_k_with_scale_buffer[host_page, layer_id].cpu()
else:
got = host_pool.index_k_with_scale_buffer[layer_id][host_page].cpu()
expected = device_pool.index_k_with_scale_buffer[layer_id][
device_page
].cpu()
self.assertTrue(torch.equal(got, expected))
host_start = host_page * page_size
device_start = device_page * page_size
got_kv = host_pool.kv_buffer[layer_id][
host_start : host_start + page_size
].cpu()
expected_kv = device_pool.kv_buffer[layer_id][
device_start : device_start + page_size
].cpu()
if layout == "page_first_direct":
got_kv = host_pool.kv_buffer[host_page, layer_id].cpu()
else:
host_start = host_page * page_size
got_kv = host_pool.kv_buffer[layer_id][
host_start : host_start + page_size
].cpu()
self.assertTrue(torch.equal(got_kv, expected_kv))
def test_device_to_host_indexer_kernel(self):
self._run_device_to_host_indexer_copy(io_backend="kernel")
def test_device_to_host_indexer_kernel_layer_first(self):
self._run_device_to_host_indexer_copy(
io_backend="kernel", layout="layer_first"
)
def test_device_to_host_indexer_direct(self):
self._run_device_to_host_indexer_copy(io_backend="direct")
def test_device_to_host_indexer_direct_layer_first(self):
self._run_device_to_host_indexer_copy(
io_backend="direct", layout="layer_first"
)
def test_device_to_host_indexer_direct_page_first_direct(self):
self._run_device_to_host_indexer_copy(
io_backend="direct", layout="page_first_direct"
)
class TestNSAIndexerPageIndices(CustomTestCase):
def make_host_pool_stub(self, page_size: int):
host_pool = NSATokenToKVPoolHost.__new__(NSATokenToKVPoolHost)
host_pool.page_size = page_size
return host_pool
def test_indexer_page_indices_accepts_valid_page_spans(self):
host_pool = self.make_host_pool_stub(page_size=4)
host_pages, device_pages = host_pool._get_indexer_page_indices(
torch.tensor([8, 9, 10, 11], dtype=torch.int64),
torch.tensor([16, 17, 18, 19], dtype=torch.int64),
)
self.assertEqual(host_pages.tolist(), [2])
self.assertEqual(device_pages.tolist(), [4])
def test_indexer_page_indices_rejects_partial_host_page(self):
host_pool = self.make_host_pool_stub(page_size=4)
with self.assertRaisesRegex(ValueError, "host_indices.*whole pages"):
host_pool._get_indexer_page_indices(
torch.tensor([8, 9, 10], dtype=torch.int64),
torch.tensor([16, 17, 18], dtype=torch.int64),
)
def test_indexer_page_indices_rejects_misaligned_device_page(self):
host_pool = self.make_host_pool_stub(page_size=4)
with self.assertRaisesRegex(
ValueError, "device_indices.*start at page boundaries"
):
host_pool._get_indexer_page_indices(
torch.tensor([8, 9, 10, 11], dtype=torch.int64),
torch.tensor([17, 18, 19, 20], dtype=torch.int64),
)
def test_indexer_page_indices_rejects_non_contiguous_host_page(self):
host_pool = self.make_host_pool_stub(page_size=4)
with self.assertRaisesRegex(ValueError, "host_indices.*contiguous page spans"):
host_pool._get_indexer_page_indices(
torch.tensor([8, 9, 11, 10], dtype=torch.int64),
torch.tensor([16, 17, 18, 19], dtype=torch.int64),
)
if __name__ == "__main__":
@@ -0,0 +1,64 @@
import unittest
import torch
from sglang.srt.mem_cache.page_index_utils import (
validate_page_aligned_token_indices,
)
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=1, suite="stage-a-test-cpu")
class TestPageIndexUtils(CustomTestCase):
def test_valid_empty_indices(self):
validate_page_aligned_token_indices(
torch.empty((0,), dtype=torch.int64), 4, "indices"
)
def test_valid_single_page(self):
validate_page_aligned_token_indices(
torch.tensor([8, 9, 10, 11], dtype=torch.int64), 4, "indices"
)
def test_valid_multiple_pages(self):
validate_page_aligned_token_indices(
torch.tensor([4, 5, 6, 7, 16, 17, 18, 19], dtype=torch.int64),
4,
"indices",
)
def test_rejects_non_1d_tensor(self):
with self.assertRaisesRegex(ValueError, "must be a 1-D tensor"):
validate_page_aligned_token_indices(
torch.tensor([[0, 1], [2, 3]], dtype=torch.int64), 4, "indices"
)
def test_rejects_non_positive_page_size(self):
with self.assertRaisesRegex(ValueError, "page_size must be positive"):
validate_page_aligned_token_indices(
torch.tensor([0, 1, 2, 3], dtype=torch.int64), 0, "indices"
)
def test_rejects_partial_page(self):
with self.assertRaisesRegex(ValueError, "whole pages"):
validate_page_aligned_token_indices(
torch.tensor([8, 9, 10], dtype=torch.int64), 4, "indices"
)
def test_rejects_non_page_start(self):
with self.assertRaisesRegex(ValueError, "start at page boundaries"):
validate_page_aligned_token_indices(
torch.tensor([9, 10, 11, 12], dtype=torch.int64), 4, "indices"
)
def test_rejects_non_contiguous_group(self):
with self.assertRaisesRegex(ValueError, "contiguous page spans"):
validate_page_aligned_token_indices(
torch.tensor([8, 9, 11, 10], dtype=torch.int64), 4, "indices"
)
if __name__ == "__main__":
unittest.main()