972 lines
31 KiB
Python
972 lines
31 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# Standard
|
|
from typing import List
|
|
import random
|
|
|
|
# Third Party
|
|
import pytest
|
|
import torch
|
|
|
|
# First Party
|
|
from lmcache.v1.memory_allocators.pin_memory_allocator import PinMemoryAllocator
|
|
|
|
if not torch.cuda.is_available():
|
|
pytest.skip(
|
|
"CUDA is not available, skipping the test",
|
|
allow_module_level=True,
|
|
)
|
|
|
|
# First Party
|
|
if torch.cuda.is_available():
|
|
try:
|
|
# First Party
|
|
import lmcache.c_ops as lmc_ops
|
|
except ImportError:
|
|
lmc_ops = None
|
|
else:
|
|
lmc_ops = None
|
|
|
|
# Mock c_ops when not available
|
|
if lmc_ops is None:
|
|
|
|
class MockEngineKVFormat:
|
|
NL_X_TWO_NB_BS_NH_HS = 0
|
|
NL_X_NB_TWO_BS_NH_HS = 1
|
|
NL_X_NB_BS_HS = 2
|
|
NL_X_TWO_NB_NH_BS_HS = 3
|
|
NL_X_NB_TWO_NH_BS_HS = 4
|
|
|
|
class MockTransferDirection:
|
|
H2D = 0
|
|
D2H = 1
|
|
|
|
class MockCOps:
|
|
EngineKVFormat = MockEngineKVFormat
|
|
GPUKVFormat = MockEngineKVFormat
|
|
TransferDirection = MockTransferDirection
|
|
|
|
lmc_ops = MockCOps()
|
|
|
|
# Local
|
|
from .utils import (
|
|
check_mem_obj_equal,
|
|
check_paged_kv_cache_equal,
|
|
generate_kv_cache_paged,
|
|
generate_kv_cache_paged_list_tensors,
|
|
)
|
|
|
|
|
|
def _tuple_kv_to_blob(
|
|
kv_tensors,
|
|
) -> torch.Tensor:
|
|
k_temp = []
|
|
v_temp = []
|
|
for kv_layer in kv_tensors:
|
|
k_temp.append(kv_layer[0])
|
|
v_temp.append(kv_layer[1])
|
|
k_tensor_blob = torch.stack(k_temp)
|
|
v_tensor_blob = torch.stack(v_temp)
|
|
|
|
# kv_tensors: [num_layer, 2, num_tok, num_kv_head, head_size]
|
|
kv_tensors_flatten = torch.stack((k_tensor_blob, v_tensor_blob))
|
|
kv_tensors_flatten = kv_tensors_flatten.permute([1, 0, 2, 3, 4])
|
|
|
|
return kv_tensors_flatten
|
|
|
|
|
|
def _slice_kv_at(
|
|
start_idx: int,
|
|
kv_tensors: torch.Tensor,
|
|
chunk_size: int,
|
|
) -> List[torch.Tensor]:
|
|
return [
|
|
x.contiguous()
|
|
for x in list(
|
|
torch.split(
|
|
kv_tensors[:, :, start_idx:, ...],
|
|
chunk_size,
|
|
dim=2,
|
|
)
|
|
)
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("num_tokens", [256, 500, 1024, 8000])
|
|
def test_extract_and_load_back(num_tokens):
|
|
device = "cuda"
|
|
|
|
num_blocks = 1000
|
|
block_size = 16
|
|
num_heads = 8
|
|
head_size = 128
|
|
num_layers = 32
|
|
dtype = torch.bfloat16
|
|
kv_cache = generate_kv_cache_paged(num_blocks, device, block_size, dtype)
|
|
|
|
slot_mapping = random.sample(range(0, num_blocks * block_size), num_tokens)
|
|
slot_mapping = torch.tensor(slot_mapping, device=device)
|
|
|
|
pinned_cpu_size = 4 * 1024 * 1024 * 1024 # 4GB
|
|
mem_allocator = PinMemoryAllocator(pinned_cpu_size)
|
|
|
|
# Old extract
|
|
kv_tuple_list = []
|
|
memory_obj_old_list = []
|
|
chunk_size = 256
|
|
start_event = torch.cuda.Event(enable_timing=True)
|
|
end_event = torch.cuda.Event(enable_timing=True)
|
|
start_event.record()
|
|
for layer_id in range(num_layers):
|
|
key_cache = kv_cache[layer_id][0].reshape(-1, num_heads, head_size)
|
|
value_cache = kv_cache[layer_id][1].reshape(-1, num_heads, head_size)
|
|
kv_tuple_list.append((key_cache[slot_mapping], value_cache[slot_mapping]))
|
|
kv_blob = _tuple_kv_to_blob(kv_tuple_list)
|
|
kv_chunked = _slice_kv_at(0, kv_blob, chunk_size)
|
|
for chunk_id, chunk in enumerate(kv_chunked):
|
|
mem_obj_shape = torch.Size(
|
|
[2, num_layers, chunk.shape[2], num_heads * head_size]
|
|
)
|
|
|
|
memory_obj_old = mem_allocator.allocate(mem_obj_shape, dtype)
|
|
chunk = chunk.contiguous()
|
|
for layer_id in range(num_layers):
|
|
memory_obj_old.tensor[0, layer_id].copy_(
|
|
chunk[layer_id, 0].reshape(-1, 1024)
|
|
)
|
|
memory_obj_old.tensor[1, layer_id].copy_(
|
|
chunk[layer_id, 1].reshape(-1, 1024)
|
|
)
|
|
memory_obj_old_list.append(memory_obj_old)
|
|
end_event.record()
|
|
torch.cuda.synchronize()
|
|
elapsed_time_ms = start_event.elapsed_time(end_event)
|
|
print("Old extract time: ", elapsed_time_ms / 1000)
|
|
|
|
# New extract (zero-copy kernels)
|
|
memory_obj_new_list = []
|
|
start_event = torch.cuda.Event(enable_timing=True)
|
|
end_event = torch.cuda.Event(enable_timing=True)
|
|
start_event.record()
|
|
slot_mapping_chunked = torch.split(slot_mapping, chunk_size)
|
|
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
|
|
mem_obj_shape = torch.Size(
|
|
[2, num_layers, len(slot_mapping_temp), num_heads * head_size]
|
|
)
|
|
|
|
memory_obj_new = mem_allocator.allocate(mem_obj_shape, dtype)
|
|
for layer_id in range(num_layers):
|
|
lmc_ops.load_and_reshape_flash(
|
|
memory_obj_new.tensor,
|
|
kv_cache[layer_id][0],
|
|
kv_cache[layer_id][1],
|
|
slot_mapping_temp,
|
|
layer_id,
|
|
)
|
|
memory_obj_new_list.append(memory_obj_new)
|
|
end_event.record()
|
|
# wait for all the operations to finish
|
|
torch.cuda.synchronize()
|
|
elapsed_time_ms = start_event.elapsed_time(end_event)
|
|
print("New extract time: ", elapsed_time_ms / 1000)
|
|
check_mem_obj_equal(
|
|
memory_obj_old_list,
|
|
memory_obj_new_list,
|
|
)
|
|
|
|
# Generate new paged kv_cache
|
|
kv_cache_new = generate_kv_cache_paged(num_blocks, device, block_size, dtype)
|
|
|
|
# New load back (zero-copy kernels)
|
|
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
|
|
memory_obj_new = memory_obj_new_list[chunk_id]
|
|
for layer_id in range(num_layers):
|
|
lmc_ops.reshape_and_cache_back_flash(
|
|
memory_obj_new.tensor,
|
|
kv_cache_new[layer_id][0],
|
|
kv_cache_new[layer_id][1],
|
|
slot_mapping_temp,
|
|
layer_id,
|
|
)
|
|
check_paged_kv_cache_equal(
|
|
kv_cache,
|
|
kv_cache_new,
|
|
slot_mapping,
|
|
)
|
|
|
|
mem_allocator.close()
|
|
|
|
|
|
@pytest.mark.parametrize("num_tokens", [256, 500, 1024, 8000])
|
|
@pytest.mark.parametrize(
|
|
"engine_kv_format",
|
|
[
|
|
lmc_ops.EngineKVFormat.NL_X_TWO_NB_BS_NH_HS, # vllm non-MLA flash attention
|
|
lmc_ops.EngineKVFormat.NL_X_NB_TWO_BS_NH_HS, # vllm non-MLA flash infer
|
|
],
|
|
)
|
|
def test_multi_layer_kernel(num_tokens, engine_kv_format):
|
|
device = "cuda"
|
|
|
|
num_blocks = 1000
|
|
block_size = 16
|
|
num_heads = 8
|
|
head_size = 128
|
|
chunk_size = 256
|
|
num_layers = 32
|
|
dtype = torch.bfloat16
|
|
kv_cache = generate_kv_cache_paged_list_tensors(
|
|
num_blocks, device, block_size, dtype, engine_kv_format=engine_kv_format
|
|
)
|
|
# old deprecated kernels only handle vllm non-MLA flash attention format
|
|
if engine_kv_format == lmc_ops.EngineKVFormat.NL_X_NB_TWO_BS_NH_HS:
|
|
kv_cache_force_old = [
|
|
kv_layer.clone().permute(1, 0, 2, 3, 4).contiguous()
|
|
for kv_layer in kv_cache
|
|
]
|
|
else:
|
|
kv_cache_force_old = kv_cache
|
|
page_buffer_size = num_blocks * block_size
|
|
|
|
slot_mapping = random.sample(range(0, page_buffer_size), num_tokens)
|
|
slot_mapping = torch.tensor(slot_mapping, device=device)
|
|
|
|
pinned_cpu_size = 4 * 1024 * 1024 * 1024 # 4GB
|
|
mem_allocator = PinMemoryAllocator(pinned_cpu_size)
|
|
|
|
# layer by layer extract
|
|
memory_obj_old_list = []
|
|
start_event = torch.cuda.Event(enable_timing=True)
|
|
end_event = torch.cuda.Event(enable_timing=True)
|
|
start_event.record()
|
|
slot_mapping_chunked = torch.split(slot_mapping, chunk_size)
|
|
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
|
|
mem_obj_shape = torch.Size(
|
|
[2, num_layers, len(slot_mapping_temp), num_heads * head_size]
|
|
)
|
|
|
|
memory_obj_old = mem_allocator.allocate(mem_obj_shape, dtype)
|
|
for layer_id in range(num_layers):
|
|
lmc_ops.load_and_reshape_flash(
|
|
memory_obj_old.tensor,
|
|
kv_cache_force_old[layer_id][0],
|
|
kv_cache_force_old[layer_id][1],
|
|
slot_mapping_temp,
|
|
layer_id,
|
|
)
|
|
memory_obj_old_list.append(memory_obj_old)
|
|
end_event.record()
|
|
# wait for all the operations to finish
|
|
torch.cuda.synchronize()
|
|
elapsed_time_ms = start_event.elapsed_time(end_event)
|
|
print("Old extract time: ", elapsed_time_ms / 1000)
|
|
|
|
# New extract with multi layer kernel
|
|
kv_cache_pointers = torch.empty(
|
|
num_layers, dtype=torch.int64, device="cpu", pin_memory=True
|
|
)
|
|
for i in range(num_layers):
|
|
kv_cache_pointers[i] = kv_cache[i].data_ptr()
|
|
|
|
memory_obj_new_list = []
|
|
start_event = torch.cuda.Event(enable_timing=True)
|
|
end_event = torch.cuda.Event(enable_timing=True)
|
|
start_event.record()
|
|
slot_mapping_chunked = torch.split(slot_mapping, chunk_size)
|
|
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
|
|
mem_obj_shape = torch.Size(
|
|
[2, num_layers, len(slot_mapping_temp), num_heads * head_size]
|
|
)
|
|
|
|
memory_obj_new = mem_allocator.allocate(mem_obj_shape, dtype)
|
|
lmc_ops.multi_layer_kv_transfer(
|
|
memory_obj_new.tensor,
|
|
kv_cache_pointers,
|
|
slot_mapping_temp,
|
|
kv_cache[0].device,
|
|
page_buffer_size,
|
|
lmc_ops.TransferDirection.D2H,
|
|
engine_kv_format,
|
|
block_size,
|
|
)
|
|
memory_obj_new_list.append(memory_obj_new)
|
|
|
|
end_event.record()
|
|
# wait for all the operations to finish
|
|
torch.cuda.synchronize()
|
|
elapsed_time_ms = start_event.elapsed_time(end_event)
|
|
print("New extract time: ", elapsed_time_ms / 1000)
|
|
|
|
check_mem_obj_equal(
|
|
memory_obj_old_list,
|
|
memory_obj_new_list,
|
|
)
|
|
|
|
# Generate new paged kv_cache
|
|
kv_cache_new = generate_kv_cache_paged_list_tensors(
|
|
num_blocks, device, block_size, dtype, engine_kv_format=engine_kv_format
|
|
)
|
|
|
|
kv_cache_pointers_new = torch.empty(
|
|
num_layers, dtype=torch.int64, device="cpu", pin_memory=True
|
|
)
|
|
for i in range(num_layers):
|
|
kv_cache_pointers_new[i] = kv_cache_new[i].data_ptr()
|
|
|
|
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
|
|
memory_obj_new = memory_obj_new_list[chunk_id]
|
|
lmc_ops.multi_layer_kv_transfer(
|
|
memory_obj_new.tensor,
|
|
kv_cache_pointers_new,
|
|
slot_mapping_temp,
|
|
kv_cache_new[0].device,
|
|
page_buffer_size,
|
|
lmc_ops.TransferDirection.H2D,
|
|
engine_kv_format,
|
|
block_size,
|
|
)
|
|
|
|
check_paged_kv_cache_equal(
|
|
kv_cache,
|
|
kv_cache_new,
|
|
slot_mapping,
|
|
engine_kv_format=engine_kv_format,
|
|
)
|
|
|
|
mem_allocator.close()
|
|
|
|
|
|
@pytest.mark.parametrize("num_tokens", [256, 500, 1024, 8000])
|
|
@pytest.mark.parametrize("head_size", [576, 66]) # Use 68 for dsv32 (132x int8)
|
|
@pytest.mark.parametrize(
|
|
"engine_kv_format",
|
|
[lmc_ops.EngineKVFormat.NL_X_NB_BS_HS], # vllm MLA
|
|
)
|
|
def test_multi_layer_kernel_use_mla(num_tokens, head_size, engine_kv_format):
|
|
device = "cuda"
|
|
|
|
num_blocks = 1000
|
|
block_size = 64
|
|
chunk_size = 256
|
|
dtype = torch.bfloat16
|
|
num_layers = 32
|
|
kv_cache = generate_kv_cache_paged_list_tensors(
|
|
num_blocks,
|
|
device,
|
|
block_size,
|
|
dtype,
|
|
num_layers,
|
|
head_size,
|
|
engine_kv_format=engine_kv_format,
|
|
)
|
|
|
|
slot_mapping = random.sample(range(0, num_blocks * block_size), num_tokens)
|
|
slot_mapping = torch.tensor(slot_mapping, device=device)
|
|
|
|
pinned_cpu_size = 4 * 1024 * 1024 * 1024 # 4GB
|
|
mem_allocator = PinMemoryAllocator(pinned_cpu_size)
|
|
|
|
# layer by layer extract
|
|
memory_obj_old_list = []
|
|
start_event = torch.cuda.Event(enable_timing=True)
|
|
end_event = torch.cuda.Event(enable_timing=True)
|
|
start_event.record()
|
|
slot_mapping_chunked = torch.split(slot_mapping, chunk_size)
|
|
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
|
|
mem_obj_shape = torch.Size([1, num_layers, len(slot_mapping_temp), head_size])
|
|
memory_obj_old = mem_allocator.allocate(mem_obj_shape, dtype)
|
|
|
|
for layer_id in range(num_layers):
|
|
for token_idx, slot_idx in enumerate(slot_mapping_temp):
|
|
slot_idx = slot_idx.item()
|
|
|
|
block_idx = slot_idx // block_size
|
|
block_offset = slot_idx % block_size
|
|
|
|
memory_obj_old.tensor[0][layer_id][token_idx] = kv_cache[layer_id][
|
|
block_idx
|
|
][block_offset]
|
|
|
|
memory_obj_old_list.append(memory_obj_old)
|
|
end_event.record()
|
|
# wait for all the operations to finish
|
|
torch.cuda.synchronize()
|
|
elapsed_time_ms = start_event.elapsed_time(end_event)
|
|
print("Old extract time: ", elapsed_time_ms / 1000)
|
|
|
|
# New extract with multi layer kernel
|
|
kv_cache_pointers = torch.empty(
|
|
num_layers, dtype=torch.int64, device="cpu", pin_memory=True
|
|
)
|
|
for i in range(num_layers):
|
|
kv_cache_pointers[i] = kv_cache[i].data_ptr()
|
|
|
|
memory_obj_new_list = []
|
|
start_event = torch.cuda.Event(enable_timing=True)
|
|
end_event = torch.cuda.Event(enable_timing=True)
|
|
start_event.record()
|
|
slot_mapping_chunked = torch.split(slot_mapping, chunk_size)
|
|
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
|
|
mem_obj_shape = torch.Size([1, num_layers, len(slot_mapping_temp), head_size])
|
|
|
|
memory_obj_new = mem_allocator.allocate(mem_obj_shape, dtype)
|
|
lmc_ops.multi_layer_kv_transfer(
|
|
memory_obj_new.tensor,
|
|
kv_cache_pointers,
|
|
slot_mapping_temp,
|
|
kv_cache[0].device,
|
|
0,
|
|
lmc_ops.TransferDirection.D2H,
|
|
engine_kv_format,
|
|
block_size,
|
|
)
|
|
memory_obj_new_list.append(memory_obj_new)
|
|
|
|
end_event.record()
|
|
# wait for all the operations to finish
|
|
torch.cuda.synchronize()
|
|
elapsed_time_ms = start_event.elapsed_time(end_event)
|
|
print("New extract time: ", elapsed_time_ms / 1000)
|
|
|
|
for left_mem_obj, right_mem_obj in zip(
|
|
memory_obj_old_list, memory_obj_new_list, strict=False
|
|
):
|
|
left_kv, right_kv = left_mem_obj.tensor[0], right_mem_obj.tensor[0]
|
|
right_kv = right_kv.to(left_kv.device)
|
|
|
|
assert len(left_kv.shape) == 3
|
|
assert len(right_kv.shape) == 3
|
|
|
|
assert (left_kv[:, :, :] == right_kv[:, :, :]).all()
|
|
|
|
# Generate new paged kv_cache
|
|
kv_cache_new = generate_kv_cache_paged_list_tensors(
|
|
num_blocks,
|
|
device,
|
|
block_size,
|
|
dtype,
|
|
num_layers,
|
|
head_size,
|
|
engine_kv_format=engine_kv_format,
|
|
)
|
|
|
|
kv_cache_pointers_new = torch.empty(
|
|
num_layers, dtype=torch.int64, device="cpu", pin_memory=True
|
|
)
|
|
for i in range(num_layers):
|
|
kv_cache_pointers_new[i] = kv_cache_new[i].data_ptr()
|
|
|
|
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
|
|
memory_obj_new = memory_obj_new_list[chunk_id]
|
|
lmc_ops.multi_layer_kv_transfer(
|
|
memory_obj_new.tensor,
|
|
kv_cache_pointers_new,
|
|
slot_mapping_temp,
|
|
kv_cache_new[0].device,
|
|
0,
|
|
lmc_ops.TransferDirection.H2D,
|
|
engine_kv_format,
|
|
block_size,
|
|
)
|
|
|
|
for left_kv, right_kv in zip(kv_cache, kv_cache_new, strict=False):
|
|
assert len(left_kv.shape) == 3
|
|
assert len(right_kv.shape) == 3
|
|
|
|
left_reshaped = left_kv.reshape(
|
|
left_kv.shape[0] * left_kv.shape[1], left_kv.shape[2]
|
|
)
|
|
right_reshaped = right_kv.reshape(
|
|
right_kv.shape[0] * right_kv.shape[1], right_kv.shape[2]
|
|
)
|
|
|
|
assert (left_reshaped[slot_mapping, :] == right_reshaped[slot_mapping, :]).all()
|
|
|
|
mem_allocator.close()
|
|
|
|
|
|
@pytest.mark.parametrize("num_tokens", [256, 500, 1024, 8000])
|
|
@pytest.mark.parametrize("token_major", [True, False])
|
|
@pytest.mark.parametrize(
|
|
"engine_kv_format",
|
|
[
|
|
lmc_ops.EngineKVFormat.NL_X_TWO_NB_BS_NH_HS, # vllm non-MLA flash attention
|
|
lmc_ops.EngineKVFormat.NL_X_NB_TWO_BS_NH_HS, # vllm non-MLA flash infer
|
|
],
|
|
)
|
|
def test_single_layer_kernel(num_tokens, token_major, engine_kv_format):
|
|
device = "cuda"
|
|
|
|
num_layers = 32
|
|
num_blocks = 1000
|
|
block_size = 16
|
|
num_heads = 8
|
|
head_size = 128
|
|
hidden_dim_size = num_heads * head_size
|
|
dtype = torch.bfloat16
|
|
kv_cache = generate_kv_cache_paged_list_tensors(
|
|
num_blocks, device, block_size, dtype, engine_kv_format=engine_kv_format
|
|
)
|
|
kv_cache_new = generate_kv_cache_paged_list_tensors(
|
|
num_blocks, device, block_size, dtype, engine_kv_format=engine_kv_format
|
|
)
|
|
slot_mapping = random.sample(range(0, num_blocks * block_size), num_tokens)
|
|
slot_mapping = torch.tensor(slot_mapping, device=device)
|
|
|
|
if token_major:
|
|
tmp_gpu_buffer = torch.empty(
|
|
(num_tokens, 2, hidden_dim_size), dtype=dtype, device=device
|
|
)
|
|
else:
|
|
tmp_gpu_buffer = torch.empty(
|
|
(2, num_tokens, hidden_dim_size), dtype=dtype, device=device
|
|
)
|
|
|
|
for layer_id in range(num_layers):
|
|
lmc_ops.single_layer_kv_transfer(
|
|
tmp_gpu_buffer,
|
|
kv_cache[layer_id],
|
|
slot_mapping,
|
|
lmc_ops.TransferDirection.D2H,
|
|
engine_kv_format,
|
|
token_major,
|
|
)
|
|
lmc_ops.single_layer_kv_transfer(
|
|
tmp_gpu_buffer,
|
|
kv_cache_new[layer_id],
|
|
slot_mapping,
|
|
lmc_ops.TransferDirection.H2D,
|
|
engine_kv_format,
|
|
token_major,
|
|
)
|
|
|
|
check_paged_kv_cache_equal(
|
|
kv_cache,
|
|
kv_cache_new,
|
|
slot_mapping,
|
|
engine_kv_format=engine_kv_format,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("num_tokens", [256, 500, 1024])
|
|
@pytest.mark.parametrize(
|
|
"engine_kv_format",
|
|
[
|
|
lmc_ops.EngineKVFormat.NL_X_TWO_NB_NH_BS_HS, # vllm HND flash attention
|
|
lmc_ops.EngineKVFormat.NL_X_NB_TWO_NH_BS_HS, # vllm HND flash infer
|
|
],
|
|
)
|
|
def test_multi_layer_kernel_hnd(num_tokens, engine_kv_format):
|
|
"""Round-trip test for HND multi-layer kernel: D2H then H2D."""
|
|
device = "cuda"
|
|
|
|
num_blocks = 200
|
|
block_size = 16
|
|
num_heads = 8
|
|
head_size = 128
|
|
chunk_size = 256
|
|
num_layers = 4
|
|
dtype = torch.bfloat16
|
|
|
|
kv_cache = generate_kv_cache_paged_list_tensors(
|
|
num_blocks,
|
|
device,
|
|
block_size,
|
|
dtype,
|
|
num_layers=num_layers,
|
|
head_size=head_size,
|
|
engine_kv_format=engine_kv_format,
|
|
)
|
|
page_buffer_size = num_blocks * block_size
|
|
|
|
slot_mapping = random.sample(range(0, page_buffer_size), num_tokens)
|
|
slot_mapping = torch.tensor(slot_mapping, device=device)
|
|
|
|
pinned_cpu_size = 512 * 1024 * 1024 # 512MB
|
|
mem_allocator = PinMemoryAllocator(pinned_cpu_size)
|
|
|
|
# D2H: extract from paged cache into flat memory
|
|
kv_cache_pointers = torch.empty(
|
|
num_layers, dtype=torch.int64, device="cpu", pin_memory=True
|
|
)
|
|
for i in range(num_layers):
|
|
kv_cache_pointers[i] = kv_cache[i].data_ptr()
|
|
|
|
memory_obj_list = []
|
|
slot_mapping_chunked = torch.split(slot_mapping, chunk_size)
|
|
for slot_mapping_temp in slot_mapping_chunked:
|
|
mem_obj_shape = torch.Size(
|
|
[2, num_layers, len(slot_mapping_temp), num_heads * head_size]
|
|
)
|
|
memory_obj = mem_allocator.allocate(mem_obj_shape, dtype)
|
|
lmc_ops.multi_layer_kv_transfer(
|
|
memory_obj.tensor,
|
|
kv_cache_pointers,
|
|
slot_mapping_temp,
|
|
kv_cache[0].device,
|
|
page_buffer_size,
|
|
lmc_ops.TransferDirection.D2H,
|
|
engine_kv_format,
|
|
block_size,
|
|
head_size=head_size,
|
|
)
|
|
memory_obj_list.append(memory_obj)
|
|
torch.cuda.synchronize()
|
|
|
|
# H2D: write back into a fresh paged cache
|
|
kv_cache_new = generate_kv_cache_paged_list_tensors(
|
|
num_blocks,
|
|
device,
|
|
block_size,
|
|
dtype,
|
|
num_layers=num_layers,
|
|
head_size=head_size,
|
|
engine_kv_format=engine_kv_format,
|
|
)
|
|
kv_cache_pointers_new = torch.empty(
|
|
num_layers, dtype=torch.int64, device="cpu", pin_memory=True
|
|
)
|
|
for i in range(num_layers):
|
|
kv_cache_pointers_new[i] = kv_cache_new[i].data_ptr()
|
|
|
|
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
|
|
lmc_ops.multi_layer_kv_transfer(
|
|
memory_obj_list[chunk_id].tensor,
|
|
kv_cache_pointers_new,
|
|
slot_mapping_temp,
|
|
kv_cache_new[0].device,
|
|
page_buffer_size,
|
|
lmc_ops.TransferDirection.H2D,
|
|
engine_kv_format,
|
|
block_size,
|
|
head_size=head_size,
|
|
)
|
|
torch.cuda.synchronize()
|
|
|
|
check_paged_kv_cache_equal(
|
|
kv_cache,
|
|
kv_cache_new,
|
|
slot_mapping,
|
|
engine_kv_format=engine_kv_format,
|
|
)
|
|
mem_allocator.close()
|
|
|
|
|
|
@pytest.mark.parametrize("num_tokens", [256, 500, 1024])
|
|
@pytest.mark.parametrize("token_major", [True, False])
|
|
@pytest.mark.parametrize(
|
|
"engine_kv_format",
|
|
[
|
|
lmc_ops.EngineKVFormat.NL_X_TWO_NB_NH_BS_HS, # vllm HND flash attention
|
|
lmc_ops.EngineKVFormat.NL_X_NB_TWO_NH_BS_HS, # vllm HND flash infer
|
|
],
|
|
)
|
|
def test_single_layer_kernel_hnd(num_tokens, token_major, engine_kv_format):
|
|
"""Round-trip test for HND single-layer kernel: D2H then H2D per layer."""
|
|
device = "cuda"
|
|
|
|
num_layers = 4
|
|
num_blocks = 200
|
|
block_size = 16
|
|
num_heads = 8
|
|
head_size = 128
|
|
hidden_dim_size = num_heads * head_size
|
|
dtype = torch.bfloat16
|
|
|
|
kv_cache = generate_kv_cache_paged_list_tensors(
|
|
num_blocks,
|
|
device,
|
|
block_size,
|
|
dtype,
|
|
num_layers=num_layers,
|
|
head_size=head_size,
|
|
engine_kv_format=engine_kv_format,
|
|
)
|
|
kv_cache_new = generate_kv_cache_paged_list_tensors(
|
|
num_blocks,
|
|
device,
|
|
block_size,
|
|
dtype,
|
|
num_layers=num_layers,
|
|
head_size=head_size,
|
|
engine_kv_format=engine_kv_format,
|
|
)
|
|
slot_mapping = random.sample(range(0, num_blocks * block_size), num_tokens)
|
|
slot_mapping = torch.tensor(slot_mapping, device=device)
|
|
|
|
if token_major:
|
|
tmp_gpu_buffer = torch.empty(
|
|
(num_tokens, 2, hidden_dim_size), dtype=dtype, device=device
|
|
)
|
|
else:
|
|
tmp_gpu_buffer = torch.empty(
|
|
(2, num_tokens, hidden_dim_size), dtype=dtype, device=device
|
|
)
|
|
|
|
for layer_id in range(num_layers):
|
|
lmc_ops.single_layer_kv_transfer(
|
|
tmp_gpu_buffer,
|
|
kv_cache[layer_id],
|
|
slot_mapping,
|
|
lmc_ops.TransferDirection.D2H,
|
|
engine_kv_format,
|
|
token_major,
|
|
)
|
|
lmc_ops.single_layer_kv_transfer(
|
|
tmp_gpu_buffer,
|
|
kv_cache_new[layer_id],
|
|
slot_mapping,
|
|
lmc_ops.TransferDirection.H2D,
|
|
engine_kv_format,
|
|
token_major,
|
|
)
|
|
|
|
check_paged_kv_cache_equal(
|
|
kv_cache,
|
|
kv_cache_new,
|
|
slot_mapping,
|
|
engine_kv_format=engine_kv_format,
|
|
)
|
|
|
|
|
|
def test_lmcache_memcpy_async():
|
|
# Register 4 memory regions and try to launch copy
|
|
chunk_size = 1 << 26
|
|
num_chunks = 4
|
|
dtype = torch.bfloat16
|
|
elements_per_chunk = chunk_size // dtype.itemsize
|
|
|
|
big_cpu_tensor = torch.rand(
|
|
elements_per_chunk * num_chunks, dtype=dtype, device="cpu"
|
|
)
|
|
big_gpu_tensor = torch.rand(
|
|
elements_per_chunk * num_chunks, dtype=dtype, device="cuda"
|
|
)
|
|
|
|
def check_gpu_and_cpu_equal(
|
|
gpu_tensor: torch.Tensor,
|
|
cpu_tensor: torch.Tensor,
|
|
):
|
|
cpu_tensor_from_gpu = gpu_tensor.to("cpu")
|
|
assert torch.allclose(cpu_tensor_from_gpu, cpu_tensor, atol=1e-5)
|
|
|
|
rt = torch.cuda.cudart()
|
|
|
|
# Register the cpu memory
|
|
ptr = big_cpu_tensor.data_ptr()
|
|
for i in range(num_chunks):
|
|
rt.cudaHostRegister(ptr + i * chunk_size, chunk_size, 0)
|
|
|
|
# Launch default cuda copy
|
|
with pytest.raises(RuntimeError):
|
|
big_gpu_tensor.copy_(big_cpu_tensor, non_blocking=True)
|
|
torch.cuda.synchronize()
|
|
|
|
# Launc default cuda copy for a small page
|
|
big_gpu_tensor[: elements_per_chunk // 2].copy_(
|
|
big_cpu_tensor[: elements_per_chunk // 2], non_blocking=True
|
|
)
|
|
torch.cuda.synchronize()
|
|
|
|
check_gpu_and_cpu_equal(
|
|
big_gpu_tensor[: elements_per_chunk // 2],
|
|
big_cpu_tensor[: elements_per_chunk // 2],
|
|
)
|
|
|
|
# Positions to copy
|
|
# - 0.25 chunk -- 0.75 chunk
|
|
# - 0.75 chunk -- 1.25 chunk
|
|
# - 1.75 chunk -- 3.25 chunk
|
|
# - whole 4 chunks
|
|
starts = [chunk_size // 4, (3 * chunk_size) // 4, (7 * chunk_size) // 4, 0]
|
|
ends = [
|
|
(3 * chunk_size) // 4,
|
|
(5 * chunk_size) // 4,
|
|
(13 * chunk_size) // 4,
|
|
chunk_size * num_chunks,
|
|
]
|
|
|
|
# H2D copy
|
|
for start, end in zip(starts, ends, strict=False):
|
|
# should not be equal before copy
|
|
with pytest.raises(AssertionError):
|
|
check_gpu_and_cpu_equal(
|
|
big_gpu_tensor[start // dtype.itemsize : end // dtype.itemsize],
|
|
big_cpu_tensor[start // dtype.itemsize : end // dtype.itemsize],
|
|
)
|
|
|
|
lmc_ops.lmcache_memcpy_async(
|
|
big_gpu_tensor.data_ptr() + start,
|
|
big_cpu_tensor.data_ptr() + start,
|
|
end - start,
|
|
lmc_ops.TransferDirection.H2D,
|
|
start,
|
|
chunk_size,
|
|
)
|
|
torch.cuda.synchronize()
|
|
|
|
check_gpu_and_cpu_equal(
|
|
big_gpu_tensor[start // dtype.itemsize : end // dtype.itemsize],
|
|
big_cpu_tensor[start // dtype.itemsize : end // dtype.itemsize],
|
|
)
|
|
|
|
# Reset the data in gpu
|
|
big_gpu_tensor = torch.rand(
|
|
elements_per_chunk * num_chunks, dtype=dtype, device="cuda"
|
|
)
|
|
# D2H copy
|
|
for start, end in zip(starts, ends, strict=False):
|
|
# copy from gpu to cpu
|
|
with pytest.raises(AssertionError):
|
|
check_gpu_and_cpu_equal(
|
|
big_gpu_tensor[start // dtype.itemsize : end // dtype.itemsize],
|
|
big_cpu_tensor[start // dtype.itemsize : end // dtype.itemsize],
|
|
)
|
|
|
|
lmc_ops.lmcache_memcpy_async(
|
|
big_cpu_tensor.data_ptr() + start,
|
|
big_gpu_tensor.data_ptr() + start,
|
|
end - start,
|
|
lmc_ops.TransferDirection.D2H,
|
|
start,
|
|
chunk_size,
|
|
)
|
|
torch.cuda.synchronize()
|
|
|
|
check_gpu_and_cpu_equal(
|
|
big_gpu_tensor[start // dtype.itemsize : end // dtype.itemsize],
|
|
big_cpu_tensor[start // dtype.itemsize : end // dtype.itemsize],
|
|
)
|
|
|
|
# Unregister the cpu memory
|
|
ptr = big_cpu_tensor.data_ptr()
|
|
for i in range(num_chunks):
|
|
rt.cudaHostUnregister(ptr + i * chunk_size)
|
|
|
|
|
|
def test_lmcache_memcpy_async_int8_hidden132():
|
|
"""Test lmcache_memcpy_async with int8 dtype and hidden_size=132.
|
|
|
|
hidden_size=132 means each token occupies 132 bytes, which is NOT a
|
|
multiple of 4 (uint32 size). This tests that the kernel correctly handles
|
|
non-uint32-aligned data sizes when splitting copies across registered
|
|
memory chunk boundaries.
|
|
|
|
The KV buffer shape is (2, num_layers, num_tokens, hidden_size), so the
|
|
total bytes = 2 * num_layers * num_tokens * 132. We pick values such that
|
|
the copy spans across at least one PIN_CHUNK_SIZE (64 MB) boundary.
|
|
"""
|
|
# Use PIN_CHUNK_SIZE = 64 MB as the registration granularity
|
|
chunk_size = 1 << 26 # 64 MB
|
|
num_chunks = 2
|
|
dtype = torch.int8
|
|
hidden_size = 132 # NOT a multiple of 4 — tests uint32 alignment handling
|
|
|
|
# Total bytes must span at least one chunk boundary.
|
|
# We allocate num_chunks * chunk_size bytes worth of int8 elements.
|
|
total_bytes = chunk_size * num_chunks
|
|
total_elements = total_bytes # int8: 1 byte per element
|
|
|
|
cpu_tensor = torch.randint(-128, 127, (total_elements,), dtype=dtype, device="cpu")
|
|
gpu_tensor = torch.zeros(total_elements, dtype=dtype, device="cuda")
|
|
|
|
rt = torch.cuda.cudart()
|
|
|
|
# Register the cpu memory in PIN_CHUNK_SIZE chunks
|
|
ptr = cpu_tensor.data_ptr()
|
|
for i in range(num_chunks):
|
|
rt.cudaHostRegister(ptr + i * chunk_size, chunk_size, 0)
|
|
|
|
def check_equal(gpu_t: torch.Tensor, cpu_t: torch.Tensor):
|
|
assert torch.equal(gpu_t.cpu(), cpu_t), "GPU and CPU tensors are not equal"
|
|
|
|
# Test cases: copy ranges that cross chunk boundaries with non-4-byte-aligned
|
|
# sizes, simulating real KV cache transfers where hidden_size=132 (int8).
|
|
#
|
|
# Each "KV row" is hidden_size=132 bytes. We pick copy sizes that are
|
|
# multiples of 132 but NOT multiples of 4, so the copy boundary falls on
|
|
# a non-uint32-aligned offset within a registered chunk.
|
|
#
|
|
# num_tokens=3: 3 * 132 = 396 bytes (396 % 4 == 0, but 132 % 4 != 0)
|
|
# num_tokens=1: 1 * 132 = 132 bytes (132 % 4 == 0)
|
|
# num_tokens=5: 5 * 132 = 660 bytes (660 % 4 == 0)
|
|
# Use an offset that puts the copy right across the 64MB boundary.
|
|
boundary = chunk_size # 64 MB boundary in bytes
|
|
|
|
# Place the copy so it straddles the chunk boundary:
|
|
# start = boundary - 2 * hidden_size, length = 4 * hidden_size
|
|
# This means 2 * hidden_size bytes before the boundary and 2 * hidden_size
|
|
# bytes after — each segment is 264 bytes, not a multiple of 4*hidden_size.
|
|
test_cases = [
|
|
# (start_bytes, num_hidden_rows)
|
|
(boundary - 2 * hidden_size, 4), # straddles boundary, 528 bytes
|
|
(boundary - hidden_size, 3), # straddles boundary, 396 bytes
|
|
(boundary - 3 * hidden_size, 6), # straddles boundary, 792 bytes
|
|
(0, 1), # single row at start, 132 bytes
|
|
(total_bytes - hidden_size, 1), # single row at end, 132 bytes
|
|
]
|
|
|
|
for start_bytes, num_rows in test_cases:
|
|
nbytes = num_rows * hidden_size
|
|
end_bytes = start_bytes + nbytes
|
|
assert end_bytes <= total_bytes, (
|
|
f"Test case out of bounds: start={start_bytes}, nbytes={nbytes}"
|
|
)
|
|
|
|
# Reset gpu_tensor slice to zeros so we can detect the copy
|
|
gpu_tensor[start_bytes:end_bytes].zero_()
|
|
|
|
# H2D copy
|
|
lmc_ops.lmcache_memcpy_async(
|
|
gpu_tensor.data_ptr() + start_bytes,
|
|
cpu_tensor.data_ptr() + start_bytes,
|
|
nbytes,
|
|
lmc_ops.TransferDirection.H2D,
|
|
start_bytes,
|
|
chunk_size,
|
|
)
|
|
torch.cuda.synchronize()
|
|
|
|
check_equal(
|
|
gpu_tensor[start_bytes:end_bytes],
|
|
cpu_tensor[start_bytes:end_bytes],
|
|
)
|
|
|
|
# D2H copy: write known values to GPU, copy back to CPU, verify
|
|
gpu_src = torch.randint(-128, 127, (total_elements,), dtype=dtype, device="cuda")
|
|
cpu_dst = torch.zeros(total_elements, dtype=dtype, device="cpu")
|
|
|
|
# Register cpu_dst for D2H
|
|
ptr_dst = cpu_dst.data_ptr()
|
|
for i in range(num_chunks):
|
|
rt.cudaHostRegister(ptr_dst + i * chunk_size, chunk_size, 0)
|
|
|
|
for start_bytes, num_rows in test_cases:
|
|
nbytes = num_rows * hidden_size
|
|
end_bytes = start_bytes + nbytes
|
|
|
|
cpu_dst[start_bytes:end_bytes].zero_()
|
|
|
|
lmc_ops.lmcache_memcpy_async(
|
|
cpu_dst.data_ptr() + start_bytes,
|
|
gpu_src.data_ptr() + start_bytes,
|
|
nbytes,
|
|
lmc_ops.TransferDirection.D2H,
|
|
start_bytes,
|
|
chunk_size,
|
|
)
|
|
torch.cuda.synchronize()
|
|
|
|
check_equal(
|
|
gpu_src[start_bytes:end_bytes],
|
|
cpu_dst[start_bytes:end_bytes],
|
|
)
|
|
|
|
# Unregister memory
|
|
ptr = cpu_tensor.data_ptr()
|
|
for i in range(num_chunks):
|
|
rt.cudaHostUnregister(ptr + i * chunk_size)
|
|
|
|
ptr_dst = cpu_dst.data_ptr()
|
|
for i in range(num_chunks):
|
|
rt.cudaHostUnregister(ptr_dst + i * chunk_size)
|