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
393 lines
12 KiB
Python
393 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
from contextlib import nullcontext
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
from tokenspeed.runtime.cache.transfer.types import CacheKind, Location, TransferUnit
|
|
|
|
|
|
class FakeEvent:
|
|
def __init__(self):
|
|
self.recorded = False
|
|
|
|
def record(self):
|
|
self.recorded = True
|
|
|
|
def wait(self, stream):
|
|
return None
|
|
|
|
def query(self):
|
|
return True
|
|
|
|
def synchronize(self):
|
|
self.recorded = True
|
|
|
|
|
|
class FakeStream:
|
|
def synchronize(self):
|
|
return None
|
|
|
|
|
|
class FakeDeviceModule:
|
|
Event = FakeEvent
|
|
Stream = FakeStream
|
|
|
|
@staticmethod
|
|
def stream(stream):
|
|
return nullcontext()
|
|
|
|
@staticmethod
|
|
def current_stream():
|
|
return FakeStream()
|
|
|
|
|
|
class FakeLayerEvent:
|
|
def __init__(self, num_layers: int):
|
|
self.start_event = FakeEvent()
|
|
self.load_events = [FakeEvent() for _ in range(num_layers)]
|
|
|
|
def complete(self, layer_idx: int):
|
|
self.load_events[layer_idx].record()
|
|
|
|
@property
|
|
def finish_event(self):
|
|
return self.load_events[-1]
|
|
|
|
|
|
class FakeCounter:
|
|
def __init__(self, num_layers: int):
|
|
self.events = [FakeLayerEvent(num_layers) for _ in range(3)]
|
|
self.producer = -1
|
|
self.consumer = None
|
|
|
|
def update_producer(self):
|
|
self.producer = (self.producer + 1) % len(self.events)
|
|
return self.producer
|
|
|
|
def set_consumer(self, producer_index):
|
|
self.consumer = producer_index
|
|
|
|
def reset(self):
|
|
self.producer = -1
|
|
self.consumer = None
|
|
|
|
|
|
class FakePool:
|
|
def __init__(self, kind: CacheKind, page_size: int, num_layers: int):
|
|
self.kind = kind
|
|
self._page_size = page_size
|
|
self._num_layers = num_layers
|
|
self.device = torch.device("cpu")
|
|
self.host_layout = "layer_first"
|
|
self.writebacks: list[tuple[list[int], list[int]]] = []
|
|
self.loadbacks: list[tuple[int, list[int], list[int]]] = []
|
|
self.counter = FakeCounter(num_layers)
|
|
|
|
def page_size(self):
|
|
return self._page_size
|
|
|
|
def num_layers(self):
|
|
return self._num_layers
|
|
|
|
def supports_layerwise_loadback(self):
|
|
return True
|
|
|
|
def writeback(self, src_indices, dst_indices):
|
|
self.writebacks.append((src_indices.tolist(), dst_indices.tolist()))
|
|
|
|
def loadback(self, src_indices, dst_indices, layer_idx: int):
|
|
self.loadbacks.append((layer_idx, src_indices.tolist(), dst_indices.tolist()))
|
|
|
|
def get_layer_done_counter(self):
|
|
return self.counter
|
|
|
|
def reset(self):
|
|
self.writebacks.clear()
|
|
self.loadbacks.clear()
|
|
self.counter.reset()
|
|
|
|
|
|
def _patch_host_executor_device(monkeypatch):
|
|
import tokenspeed.runtime.cache.executor.host_executor as host_executor
|
|
|
|
monkeypatch.setattr(host_executor, "device_module", FakeDeviceModule)
|
|
return host_executor.HostExecutor
|
|
|
|
|
|
def test_transfer_unit_exposes_direction():
|
|
unit = TransferUnit(
|
|
kind=CacheKind.MAMBA,
|
|
src_loc=Location.DEVICE,
|
|
dst_loc=Location.HOST,
|
|
src_indices=torch.tensor([1, 2], dtype=torch.int64),
|
|
dst_indices=torch.tensor([3, 4], dtype=torch.int64),
|
|
op_id=99,
|
|
)
|
|
|
|
assert unit.direction == (Location.DEVICE, Location.HOST)
|
|
|
|
|
|
def test_host_executor_keeps_page_indices_on_cpu_until_flush(monkeypatch):
|
|
host_executor = __import__(
|
|
"tokenspeed.runtime.cache.executor.host_executor",
|
|
fromlist=["HostExecutor"],
|
|
)
|
|
monkeypatch.setattr(host_executor, "device_module", FakeDeviceModule)
|
|
|
|
seen_devices = []
|
|
real_converter = host_executor.page_ids_to_token_indices
|
|
|
|
def spy_page_ids_to_token_indices(page_ids, page_size, device="cpu"):
|
|
seen_devices.append(device)
|
|
return real_converter(page_ids, page_size, device)
|
|
|
|
monkeypatch.setattr(
|
|
host_executor, "page_ids_to_token_indices", spy_page_ids_to_token_indices
|
|
)
|
|
executor = host_executor.HostExecutor(
|
|
pools=[FakePool(CacheKind.KV, page_size=4, num_layers=2)], io_backend="kernel"
|
|
)
|
|
|
|
executor.enqueue_writeback(1, src_pages=[2], dst_pages=[5], kind=CacheKind.KV)
|
|
executor.enqueue_loadback(2, src_pages=[7], dst_pages=[11], kind=CacheKind.KV)
|
|
|
|
assert seen_devices == ["cpu", "cpu", "cpu", "cpu"]
|
|
assert executor.write_queues[CacheKind.KV][0].src_indices.device.type == "cpu"
|
|
assert executor.write_queues[CacheKind.KV][0].dst_indices.device.type == "cpu"
|
|
assert executor.load_queues[CacheKind.KV][0].src_indices.device.type == "cpu"
|
|
assert executor.load_queues[CacheKind.KV][0].dst_indices.device.type == "cpu"
|
|
|
|
|
|
def test_host_executor_batches_writeback_by_cache_kind_and_acks_once(monkeypatch):
|
|
HostExecutor = _patch_host_executor_device(monkeypatch)
|
|
kv_pool = FakePool(CacheKind.KV, page_size=4, num_layers=2)
|
|
mamba_pool = FakePool(CacheKind.MAMBA, page_size=1, num_layers=3)
|
|
executor = HostExecutor(pools=[kv_pool, mamba_pool], io_backend="kernel")
|
|
|
|
executor.enqueue_writeback(
|
|
7, src_pages=[2], dst_pages=[5], kind=CacheKind.KV, is_retract=True
|
|
)
|
|
executor.enqueue_writeback(
|
|
7, src_pages=[11], dst_pages=[13], kind=CacheKind.MAMBA, is_retract=True
|
|
)
|
|
|
|
executor.flush()
|
|
|
|
assert kv_pool.writebacks == [([8, 9, 10, 11], [20, 21, 22, 23])]
|
|
assert mamba_pool.writebacks == [([11], [13])]
|
|
|
|
results = executor.drain()
|
|
assert [event.op_id for event in results] == [7]
|
|
assert all(event.success for event in results)
|
|
|
|
|
|
def test_host_executor_rejects_loadback_during_cuda_graph_capture(monkeypatch):
|
|
import tokenspeed.runtime.cache.executor.host_executor as host_executor
|
|
|
|
monkeypatch.setattr(host_executor, "device_module", FakeDeviceModule)
|
|
monkeypatch.setattr(host_executor, "get_is_capture_mode", lambda: True)
|
|
executor = host_executor.HostExecutor(
|
|
pools=[FakePool(CacheKind.MAMBA, page_size=1, num_layers=1)],
|
|
io_backend="kernel",
|
|
)
|
|
|
|
executor.enqueue_loadback(1, src_pages=[2], dst_pages=[3], kind=CacheKind.MAMBA)
|
|
|
|
with pytest.raises(AssertionError, match="eager admission iter"):
|
|
executor.flush()
|
|
|
|
|
|
def test_host_executor_loadback_uses_independent_layer_counters(monkeypatch):
|
|
HostExecutor = _patch_host_executor_device(monkeypatch)
|
|
kv_pool = FakePool(CacheKind.KV, page_size=2, num_layers=2)
|
|
mamba_pool = FakePool(CacheKind.MAMBA, page_size=1, num_layers=3)
|
|
executor = HostExecutor(pools=[kv_pool, mamba_pool], io_backend="kernel")
|
|
|
|
executor.enqueue_loadback(10, src_pages=[4], dst_pages=[8], kind=CacheKind.KV)
|
|
executor.enqueue_loadback(20, src_pages=[6], dst_pages=[9], kind=CacheKind.MAMBA)
|
|
|
|
executor.flush()
|
|
|
|
assert kv_pool.loadbacks == [
|
|
(0, [8, 9], [16, 17]),
|
|
(1, [8, 9], [16, 17]),
|
|
]
|
|
assert mamba_pool.loadbacks == [
|
|
(0, [6], [9]),
|
|
(1, [6], [9]),
|
|
(2, [6], [9]),
|
|
]
|
|
|
|
assert executor.get_producer_index(CacheKind.KV, 10) == 0
|
|
assert executor.get_producer_index(CacheKind.MAMBA, 20) == 0
|
|
executor.set_consumer(CacheKind.KV, [0])
|
|
executor.set_consumer(CacheKind.MAMBA, [0])
|
|
assert kv_pool.counter.consumer == [0]
|
|
|
|
|
|
def test_memory_executor_submit_dispatches_flat_op_by_cache_kind(monkeypatch):
|
|
import tokenspeed.runtime.cache.executor.memory_executor as memory_executor
|
|
|
|
class FakeCache:
|
|
class WriteBackOp:
|
|
pass
|
|
|
|
class LoadBackOp:
|
|
pass
|
|
|
|
class PrefetchOp:
|
|
pass
|
|
|
|
class BackUpOp:
|
|
pass
|
|
|
|
class FakeHostExec:
|
|
def __init__(self):
|
|
self.pools = {CacheKind.KV: object(), CacheKind.MAMBA: object()}
|
|
self.writebacks = []
|
|
self.loadbacks = []
|
|
self.completed_writebacks = []
|
|
self.order = []
|
|
|
|
def enqueue_writeback(
|
|
self, op_id, src_pages, dst_pages, is_retract=False, kind=CacheKind.KV
|
|
):
|
|
self.order.append(("writeback", kind, op_id))
|
|
self.writebacks.append((kind, op_id, src_pages, dst_pages, is_retract))
|
|
|
|
def enqueue_loadback(self, op_id, src_pages, dst_pages, kind=CacheKind.KV):
|
|
self.order.append(("loadback", kind, op_id))
|
|
self.loadbacks.append((kind, op_id, src_pages, dst_pages))
|
|
|
|
def flush(self):
|
|
self.order.append(("flush",))
|
|
|
|
monkeypatch.setattr(memory_executor, "Cache", FakeCache)
|
|
executor = object.__new__(memory_executor.MemoryExecutor)
|
|
executor.host_exec = FakeHostExec()
|
|
executor.storage_exec = None
|
|
|
|
wb = FakeCache.WriteBackOp()
|
|
wb.op_ids = [7]
|
|
wb.src_pages = [[1]]
|
|
wb.dst_pages = [[11]]
|
|
wb.src_pages_by_kind = {"kv": [[1]], "mamba": [[2, 3]]}
|
|
wb.dst_pages_by_kind = {"kv": [[11]], "mamba": [[22, 23]]}
|
|
wb.is_retract = [True]
|
|
executor.submit(wb)
|
|
|
|
assert executor.host_exec.writebacks == [
|
|
(CacheKind.KV, 7, [1], [11], True),
|
|
(CacheKind.MAMBA, 7, [2, 3], [22, 23], True),
|
|
]
|
|
assert executor.host_exec.completed_writebacks == []
|
|
|
|
lb = FakeCache.LoadBackOp()
|
|
lb.op_ids = [9]
|
|
lb.src_pages = [[10]]
|
|
lb.dst_pages = [[20]]
|
|
lb.src_pages_by_kind = {"kv": [[10]], "mamba": [[30]]}
|
|
lb.dst_pages_by_kind = {"kv": [[20]], "mamba": [[40]]}
|
|
executor.submit(lb)
|
|
|
|
assert executor.host_exec.loadbacks == [
|
|
(CacheKind.KV, 9, [10], [20]),
|
|
(CacheKind.MAMBA, 9, [30], [40]),
|
|
]
|
|
|
|
|
|
def test_memory_executor_submit_plan_keeps_generic_submit_signature(monkeypatch):
|
|
import tokenspeed.runtime.cache.executor.memory_executor as memory_executor
|
|
|
|
class FakeCache:
|
|
class WriteBackOp:
|
|
pass
|
|
|
|
class LoadBackOp:
|
|
pass
|
|
|
|
class PrefetchOp:
|
|
pass
|
|
|
|
class BackUpOp:
|
|
pass
|
|
|
|
monkeypatch.setattr(memory_executor, "Cache", FakeCache)
|
|
executor = object.__new__(memory_executor.MemoryExecutor)
|
|
executor.seen = []
|
|
|
|
wb = FakeCache.WriteBackOp()
|
|
plan = type("Plan", (), {"cache": [wb]})()
|
|
|
|
def submit(self, op):
|
|
self.seen.append(op)
|
|
|
|
monkeypatch.setattr(memory_executor.MemoryExecutor, "submit", submit)
|
|
executor.host_exec = type("HostExec", (), {"flush": lambda self: None})()
|
|
|
|
executor.submit_plan(plan)
|
|
|
|
assert executor.seen == [wb]
|
|
|
|
|
|
def test_memory_executor_mamba_layerwise_cow_uses_dedicated_context(monkeypatch):
|
|
import tokenspeed.runtime.cache.executor.memory_executor as memory_executor
|
|
|
|
class FakeCache:
|
|
class WriteBackOp:
|
|
pass
|
|
|
|
class LoadBackOp:
|
|
pass
|
|
|
|
class PrefetchOp:
|
|
pass
|
|
|
|
class BackUpOp:
|
|
pass
|
|
|
|
class FakeHostExec:
|
|
def __init__(self):
|
|
self.pools = {CacheKind.KV: object(), CacheKind.MAMBA: object()}
|
|
self.completed_writebacks = []
|
|
self.loadbacks = []
|
|
|
|
def enqueue_loadback(
|
|
self,
|
|
op_id,
|
|
src_pages,
|
|
dst_pages,
|
|
kind=CacheKind.KV,
|
|
layerwise_cow_dst_pages_by_src=None,
|
|
):
|
|
self.loadbacks.append(
|
|
(kind, op_id, src_pages, dst_pages, layerwise_cow_dst_pages_by_src)
|
|
)
|
|
|
|
def flush(self):
|
|
pass
|
|
|
|
monkeypatch.setattr(memory_executor, "Cache", FakeCache)
|
|
executor = object.__new__(memory_executor.MemoryExecutor)
|
|
executor.host_exec = FakeHostExec()
|
|
executor.storage_exec = None
|
|
executor.set_mamba_layerwise_cow({40: [400]})
|
|
|
|
lb = FakeCache.LoadBackOp()
|
|
lb.op_ids = [9]
|
|
lb.src_pages = [[10]]
|
|
lb.dst_pages = [[20]]
|
|
lb.src_pages_by_kind = {"kv": [[10]], "mamba": [[30]]}
|
|
lb.dst_pages_by_kind = {"kv": [[20]], "mamba": [[40]]}
|
|
plan = type("Plan", (), {"cache": [lb]})()
|
|
|
|
executor.submit_plan(plan)
|
|
|
|
assert executor.host_exec.loadbacks == [
|
|
(CacheKind.KV, 9, [10], [20], None),
|
|
(CacheKind.MAMBA, 9, [30], [40], {40: [400]}),
|
|
]
|
|
assert executor._pending_mamba_layerwise_cow is None
|