Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

150 lines
4.9 KiB
Python

from __future__ import annotations
import pytest
import torch
from tokenspeed.runtime.cache.kv_cache_host import DSATokenToKVPoolHost
from tokenspeed.runtime.layers.attention.configs.dsa import dsa_index_k_row_bytes
from tokenspeed.runtime.layers.attention.kv_cache.dsa import DSATokenToKVPool
PAGE_SIZE = 64
KV_LORA_RANK = 512
QK_ROPE_HEAD_DIM = 64
INDEX_HEAD_DIM = 128
LAYER_NUM = 2
SIZE = 2 * PAGE_SIZE
cuda_only = pytest.mark.skipif(
not torch.cuda.is_available(), reason="DSA KVStore transfer requires CUDA"
)
def _make_device_pool() -> DSATokenToKVPool:
return DSATokenToKVPool(
size=SIZE,
dtype=torch.float8_e4m3fn,
model_dtype=torch.bfloat16,
quant_method="none",
kv_lora_rank=KV_LORA_RANK,
qk_rope_head_dim=QK_ROPE_HEAD_DIM,
layer_num=LAYER_NUM,
device="cuda",
enable_memory_saver=False,
max_batch_size=8,
max_context_len=256,
page_size=PAGE_SIZE,
rank=0,
index_head_dim=INDEX_HEAD_DIM,
)
def _make_host_pool(device_pool: DSATokenToKVPool) -> DSATokenToKVPoolHost:
return DSATokenToKVPoolHost(
device_pool=device_pool,
host_to_device_ratio=2.0,
host_size=0,
page_size=PAGE_SIZE,
layout="layer_first",
device="cpu",
)
@pytest.fixture(scope="module")
def dsa_pools():
# cudaHostRegister-backed host memory is not released between pools in one
# process, so register a single device/host pool pair and share it.
device_pool = _make_device_pool()
host_pool = _make_host_pool(device_pool)
return device_pool, host_pool
@cuda_only
def test_dsa_host_pool_sizing_includes_index_k(dsa_pools):
_, host_pool = dsa_pools
row_bytes = dsa_index_k_row_bytes(INDEX_HEAD_DIM)
latent_bytes = (KV_LORA_RANK + QK_ROPE_HEAD_DIM) * LAYER_NUM # uint8 store dtype
assert host_pool.index_k_row_bytes == row_bytes
assert host_pool.size_per_token == latent_bytes + row_bytes * LAYER_NUM
# Host index-K mirrors the device layout (layer_num, host_size, row_bytes).
assert host_pool.index_k_buffer.shape == (LAYER_NUM, host_pool.size, row_bytes)
assert host_pool.index_k_buffer.dtype == torch.uint8
@cuda_only
def test_dsa_host_pool_rejects_page_first_layout(dsa_pools):
device_pool, _ = dsa_pools
# The layout guard raises before any host memory is registered.
with pytest.raises(NotImplementedError):
DSATokenToKVPoolHost(
device_pool=device_pool,
host_to_device_ratio=2.0,
host_size=0,
page_size=PAGE_SIZE,
layout="page_first",
device="cpu",
)
@cuda_only
@pytest.mark.parametrize("io_backend", ["direct", "kernel"])
def test_dsa_host_pool_roundtrip_preserves_index_k(dsa_pools, io_backend):
device_pool, host_pool = dsa_pools
torch.manual_seed(0)
# Fill latent + index-K with random bytes; byte-exact round trip proves the
# block-split index-K page layout survives the page-contiguous copy.
for layer_id in range(LAYER_NUM):
device_pool.kv_buffer[layer_id].copy_(
torch.randint(
0, 256, device_pool.kv_buffer[layer_id].shape, dtype=torch.uint8
).cuda()
)
device_pool.index_k_buffer[layer_id].copy_(
torch.randint(
0, 256, device_pool.index_k_buffer[layer_id].shape, dtype=torch.uint8
).cuda()
)
# Transfer device pages [1, 2] into host pages [0, 1] (skip padded page 0).
# The kernel backend requires CUDA indices (the cache controller moves them
# to device before the transfer); direct accepts them too.
device_indices = torch.arange(
PAGE_SIZE, 3 * PAGE_SIZE, dtype=torch.int64, device="cuda"
)
host_indices = torch.arange(0, 2 * PAGE_SIZE, dtype=torch.int64, device="cuda")
orig_latent = [
device_pool.kv_buffer[i][PAGE_SIZE : 3 * PAGE_SIZE].clone()
for i in range(LAYER_NUM)
]
orig_index_k = [
device_pool.index_k_buffer[i][PAGE_SIZE : 3 * PAGE_SIZE].clone()
for i in range(LAYER_NUM)
]
host_pool.backup_from_device_all_layer(
device_pool, host_indices, device_indices, io_backend
)
torch.cuda.synchronize()
for layer_id in range(LAYER_NUM):
device_pool.kv_buffer[layer_id][PAGE_SIZE : 3 * PAGE_SIZE].zero_()
device_pool.index_k_buffer[layer_id][PAGE_SIZE : 3 * PAGE_SIZE].zero_()
for layer_id in range(LAYER_NUM):
host_pool.load_to_device_per_layer(
device_pool, host_indices, device_indices, layer_id, io_backend
)
torch.cuda.synchronize()
for layer_id in range(LAYER_NUM):
assert torch.equal(
device_pool.kv_buffer[layer_id][PAGE_SIZE : 3 * PAGE_SIZE],
orig_latent[layer_id],
)
assert torch.equal(
device_pool.index_k_buffer[layer_id][PAGE_SIZE : 3 * PAGE_SIZE],
orig_index_k[layer_id],
)