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

238 lines
7.1 KiB
Python

"""
Benchmark the cache operations that run on the hot decode path.
During decode, for each completed page tokenspeed calls:
insert(paged_key, page_ids)
match_prefix(paged_key) # to confirm hit
dec_lock_ref(old_last_node)
inc_lock_ref(new_last_node)
We simulate a steady-state server: a large cache of pre-existing sequences
(simulating the populated prefix tree after many requests) plus N concurrent
"requests" each decoding D pages one at a time.
Run with:
python/.venv/bin/python test/runtime/benchmark/bench_decode_cache.py
"""
from __future__ import annotations
import dataclasses
import time
from typing import Optional
import torch
from tokenspeed.runtime.cache.prefix_cache import CacheInitParams, PrefixCache, TreeNode
# ---------------------------------------------------------------------------
# Minimal mock allocator — matches what real decode needs
# ---------------------------------------------------------------------------
class MockAllocator:
def __init__(self, page_size: int = 16):
self.page_size = page_size
self._next_page = 0
# Lazily allocated on first access; avoids 128MB upfront per benchmark run.
self._req_to_page: torch.Tensor | None = None
@property
def req_to_page(self) -> torch.Tensor:
if self._req_to_page is None:
self._req_to_page = torch.zeros(512, 512, dtype=torch.int32)
return self._req_to_page
@property
def req_to_page_cpu(self) -> torch.Tensor:
return self.req_to_page
def alloc_page(self) -> int:
p = self._next_page
self._next_page += 1
return p
# Called by insert/match_prefix indirectly
def free_with_diff(self, new_ids, old_ids):
diff = new_ids != old_ids
return diff
def append_to_later_free(self, t):
pass
def free_group_end(self):
pass
def _make_cache(page_size: int = 16) -> tuple[PrefixCache, MockAllocator]:
alloc = MockAllocator(page_size=page_size)
params = CacheInitParams(
disable=False,
req_to_token_pool=None,
token_to_kv_pool_allocator=alloc,
page_size=page_size,
)
return PrefixCache(params), alloc
# ---------------------------------------------------------------------------
# Scenario builders
# ---------------------------------------------------------------------------
def _populate_cache(
cache: PrefixCache, alloc: MockAllocator, n_background: int, prefix_pages: int = 0
) -> None:
"""Fill cache with n_background independent sequences (background traffic)."""
page_size = cache.page_size
for s in range(n_background):
if prefix_pages > 0:
key = [tuple([0] * page_size)] * prefix_pages + [
tuple([s + 1] + [0] * (page_size - 1))
]
else:
key = [tuple([s + 1] + [0] * (page_size - 1))]
n_pages = len(key)
value = torch.tensor(
[alloc.alloc_page() for _ in range(n_pages)], dtype=torch.int32
)
cache.insert(key, value)
# ---------------------------------------------------------------------------
# Decode simulation
# ---------------------------------------------------------------------------
@dataclasses.dataclass
class SimReq:
req_id: int
prefix_key: list # shared prefix already in cache
decode_pages: int # how many pages this req will generate
current_pages: int = 0
last_node: Optional[TreeNode] = None
def _simulate_decode_step(
cache: PrefixCache, alloc: MockAllocator, req: SimReq
) -> bool:
"""
Simulate one page completion for req.
Returns True if the request is done.
"""
page_size = cache.page_size
req.current_pages += 1
# Build key: shared prefix + unique decode pages so far
decode_part = [
tuple([req.req_id * 10000 + p + 1] + [0] * (page_size - 1))
for p in range(req.current_pages)
]
full_key = req.prefix_key + decode_part
value = torch.tensor(
[alloc.alloc_page() for _ in range(len(full_key))], dtype=torch.int32
)
# Hot path: insert → match → dec_lock_ref → inc_lock_ref
cache.insert(full_key, value)
result = cache.match_prefix(full_key)
new_last_node = result.last_device_node
cache.dec_lock_ref(req.last_node)
cache.inc_lock_ref(new_last_node)
req.last_node = new_last_node
return req.current_pages >= req.decode_pages
# ---------------------------------------------------------------------------
# Benchmark runner
# ---------------------------------------------------------------------------
def bench(
label: str,
n_background: int,
n_concurrent: int,
decode_pages: int,
prefix_pages: int = 0,
page_size: int = 16,
repeats: int = 5,
) -> dict:
"""
Measure total time for n_concurrent requests each decoding decode_pages pages,
against a background cache of n_background entries.
"""
times = []
for _ in range(repeats):
cache, alloc = _make_cache(page_size)
_populate_cache(cache, alloc, n_background, prefix_pages)
# Build shared prefix key (same for all requests)
if prefix_pages > 0:
prefix_key = [tuple([0] * page_size)] * prefix_pages
else:
prefix_key = []
requests = [
SimReq(req_id=i, prefix_key=prefix_key, decode_pages=decode_pages)
for i in range(n_concurrent)
]
# Lock each request's last_node at their prefix (simulating prefill done)
for req in requests:
if prefix_key:
result = cache.match_prefix(prefix_key)
req.last_node = result.last_device_node
cache.inc_lock_ref(req.last_node)
total_steps = n_concurrent * decode_pages
step = 0
active = list(requests)
t0 = time.perf_counter()
while active:
next_active = []
for req in active:
done = _simulate_decode_step(cache, alloc, req)
step += 1
if not done:
next_active.append(req)
active = next_active
elapsed = time.perf_counter() - t0
times.append(elapsed)
times.sort()
median = times[len(times) // 2]
total_steps = n_concurrent * decode_pages
return {
"label": label,
"n_background": n_background,
"n_concurrent": n_concurrent,
"decode_pages": decode_pages,
"prefix_pages": prefix_pages,
"total_decode_steps": total_steps,
"median_ms": round(median * 1e3, 3),
"us_per_step": round(median * 1e6 / total_steps, 2),
}
if __name__ == "__main__":
import json
scenarios = [
# (label, n_background, n_concurrent, decode_pages, prefix_pages)
("small_cache_short_seq", 1_000, 64, 32, 0),
("small_cache_long_seq", 1_000, 64, 128, 0),
("large_cache_short_seq", 20_000, 64, 32, 0),
("large_cache_long_seq", 20_000, 64, 128, 0),
("large_cache_shared_pfx", 20_000, 64, 32, 100),
("xlarge_cache_short_seq", 50_000, 64, 32, 0),
]
for args in scenarios:
r = bench(*args)
print(json.dumps(r))