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
231 lines
7.8 KiB
Python
231 lines
7.8 KiB
Python
# Copyright (c) 2026 LightSeek Foundation
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
from __future__ import annotations
|
|
|
|
import torch
|
|
from tokenspeed_kernel.ops.kvcache.triton import (
|
|
transfer_kv_all_layer,
|
|
transfer_kv_all_layer_mla,
|
|
transfer_kv_per_layer,
|
|
transfer_kv_per_layer_mla,
|
|
)
|
|
|
|
|
|
def test_transfer_kv_per_layer(device: str) -> None:
|
|
num_slots = 6
|
|
num_heads = 8
|
|
head_dim = 128
|
|
element_dim = num_heads * head_dim
|
|
|
|
k_cache_dst = torch.zeros(
|
|
num_slots, num_heads, head_dim, device=device, dtype=torch.float16
|
|
)
|
|
v_cache_dst = torch.zeros_like(k_cache_dst)
|
|
|
|
k_cache_src = torch.arange(
|
|
num_slots * num_heads * head_dim,
|
|
device=device,
|
|
dtype=torch.float16,
|
|
).reshape(num_slots, num_heads, head_dim)
|
|
v_cache_src = torch.arange(
|
|
10_000,
|
|
10_000 + num_slots * num_heads * head_dim,
|
|
device=device,
|
|
dtype=torch.float16,
|
|
).reshape(num_slots, num_heads, head_dim)
|
|
|
|
indices_dst = torch.tensor([1, 4], device=device, dtype=torch.int32)
|
|
indices_src = torch.tensor([0, 5], device=device, dtype=torch.int32)
|
|
|
|
expected_k = k_cache_dst.clone()
|
|
expected_v = v_cache_dst.clone()
|
|
expected_k[indices_dst.to(torch.int64)] = k_cache_src[indices_src.to(torch.int64)]
|
|
expected_v[indices_dst.to(torch.int64)] = v_cache_src[indices_src.to(torch.int64)]
|
|
|
|
transfer_kv_per_layer(
|
|
src_k=k_cache_src,
|
|
dst_k=k_cache_dst,
|
|
src_v=v_cache_src,
|
|
dst_v=v_cache_dst,
|
|
src_indices=indices_src,
|
|
dst_indices=indices_dst,
|
|
item_size=element_dim * k_cache_src.element_size(),
|
|
)
|
|
|
|
torch.cuda.synchronize()
|
|
|
|
assert torch.equal(k_cache_dst, expected_k)
|
|
assert torch.equal(v_cache_dst, expected_v)
|
|
|
|
|
|
def test_transfer_kv_all_layer(device: str) -> None:
|
|
num_layers = 3
|
|
num_slots = 6
|
|
num_heads = 8
|
|
head_dim = 128
|
|
|
|
k_layers_dst = [
|
|
torch.zeros(num_slots, num_heads, head_dim, device=device, dtype=torch.float16)
|
|
for _ in range(num_layers)
|
|
]
|
|
v_layers_dst = [torch.zeros_like(k_layers_dst[0]) for _ in range(num_layers)]
|
|
k_layers_src = [
|
|
torch.arange(
|
|
layer_idx * num_slots * num_heads * head_dim,
|
|
(layer_idx + 1) * num_slots * num_heads * head_dim,
|
|
device=device,
|
|
dtype=torch.float16,
|
|
).reshape(num_slots, num_heads, head_dim)
|
|
for layer_idx in range(num_layers)
|
|
]
|
|
v_layers_src = [
|
|
torch.arange(
|
|
20_000 + layer_idx * num_slots * num_heads * head_dim,
|
|
20_000 + (layer_idx + 1) * num_slots * num_heads * head_dim,
|
|
device=device,
|
|
dtype=torch.float16,
|
|
).reshape(num_slots, num_heads, head_dim)
|
|
for layer_idx in range(num_layers)
|
|
]
|
|
|
|
k_ptr_dst = torch.tensor(
|
|
[layer.data_ptr() for layer in k_layers_dst], device=device, dtype=torch.uint64
|
|
)
|
|
v_ptr_dst = torch.tensor(
|
|
[layer.data_ptr() for layer in v_layers_dst], device=device, dtype=torch.uint64
|
|
)
|
|
k_ptr_src = torch.tensor(
|
|
[layer.data_ptr() for layer in k_layers_src], device=device, dtype=torch.uint64
|
|
)
|
|
v_ptr_src = torch.tensor(
|
|
[layer.data_ptr() for layer in v_layers_src], device=device, dtype=torch.uint64
|
|
)
|
|
indices_dst = torch.tensor([1, 4], device=device, dtype=torch.int32)
|
|
indices_src = torch.tensor([0, 5], device=device, dtype=torch.int32)
|
|
slot_stride_bytes = k_layers_dst[0].stride(0) * k_layers_dst[0].element_size()
|
|
|
|
expected_k = [layer.clone() for layer in k_layers_dst]
|
|
expected_v = [layer.clone() for layer in v_layers_dst]
|
|
for layer_idx in range(num_layers):
|
|
expected_k[layer_idx][indices_dst.to(torch.int64)] = k_layers_src[layer_idx][
|
|
indices_src.to(torch.int64)
|
|
]
|
|
expected_v[layer_idx][indices_dst.to(torch.int64)] = v_layers_src[layer_idx][
|
|
indices_src.to(torch.int64)
|
|
]
|
|
|
|
transfer_kv_all_layer(
|
|
src_k_layers=k_ptr_src,
|
|
dst_k_layers=k_ptr_dst,
|
|
src_v_layers=v_ptr_src,
|
|
dst_v_layers=v_ptr_dst,
|
|
src_indices=indices_src,
|
|
dst_indices=indices_dst,
|
|
item_size=slot_stride_bytes,
|
|
num_layers=num_layers,
|
|
)
|
|
|
|
torch.cuda.synchronize()
|
|
|
|
for layer_idx in range(num_layers):
|
|
assert torch.equal(k_layers_dst[layer_idx], expected_k[layer_idx])
|
|
assert torch.equal(v_layers_dst[layer_idx], expected_v[layer_idx])
|
|
|
|
|
|
def test_transfer_kv_per_layer_mla(device: str) -> None:
|
|
num_slots = 6
|
|
kv_cache_dim = 576
|
|
|
|
cache_dst = torch.zeros(
|
|
num_slots, 1, kv_cache_dim, device=device, dtype=torch.float16
|
|
)
|
|
cache_src = torch.arange(
|
|
num_slots * kv_cache_dim,
|
|
device=device,
|
|
dtype=torch.float16,
|
|
).reshape(num_slots, 1, kv_cache_dim)
|
|
indices_dst = torch.tensor([1, 4], device=device, dtype=torch.int32)
|
|
indices_src = torch.tensor([0, 5], device=device, dtype=torch.int32)
|
|
|
|
expected = cache_dst.clone()
|
|
expected[indices_dst.to(torch.int64)] = cache_src[indices_src.to(torch.int64)]
|
|
|
|
transfer_kv_per_layer_mla(
|
|
src=cache_src,
|
|
dst=cache_dst,
|
|
src_indices=indices_src,
|
|
dst_indices=indices_dst,
|
|
item_size=kv_cache_dim * cache_src.element_size(),
|
|
)
|
|
|
|
torch.cuda.synchronize()
|
|
|
|
assert torch.equal(cache_dst, expected)
|
|
|
|
|
|
def test_transfer_kv_all_layer_mla(device: str) -> None:
|
|
num_layers = 3
|
|
num_slots = 6
|
|
kv_cache_dim = 576
|
|
|
|
layers_dst = [
|
|
torch.zeros(num_slots, 1, kv_cache_dim, device=device, dtype=torch.float16)
|
|
for _ in range(num_layers)
|
|
]
|
|
layers_src = [
|
|
torch.arange(
|
|
layer_idx * num_slots * kv_cache_dim,
|
|
(layer_idx + 1) * num_slots * kv_cache_dim,
|
|
device=device,
|
|
dtype=torch.float16,
|
|
).reshape(num_slots, 1, kv_cache_dim)
|
|
for layer_idx in range(num_layers)
|
|
]
|
|
ptr_dst = torch.tensor(
|
|
[layer.data_ptr() for layer in layers_dst], device=device, dtype=torch.uint64
|
|
)
|
|
ptr_src = torch.tensor(
|
|
[layer.data_ptr() for layer in layers_src], device=device, dtype=torch.uint64
|
|
)
|
|
indices_dst = torch.tensor([1, 4], device=device, dtype=torch.int32)
|
|
indices_src = torch.tensor([0, 5], device=device, dtype=torch.int32)
|
|
slot_stride_bytes = layers_dst[0].stride(0) * layers_dst[0].element_size()
|
|
|
|
expected = [layer.clone() for layer in layers_dst]
|
|
for layer_idx in range(num_layers):
|
|
expected[layer_idx][indices_dst.to(torch.int64)] = layers_src[layer_idx][
|
|
indices_src.to(torch.int64)
|
|
]
|
|
|
|
transfer_kv_all_layer_mla(
|
|
src_layers=ptr_src,
|
|
dst_layers=ptr_dst,
|
|
src_indices=indices_src,
|
|
dst_indices=indices_dst,
|
|
item_size=slot_stride_bytes,
|
|
num_layers=num_layers,
|
|
)
|
|
|
|
torch.cuda.synchronize()
|
|
|
|
for layer_idx in range(num_layers):
|
|
assert torch.equal(layers_dst[layer_idx], expected[layer_idx])
|