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
181 lines
6.3 KiB
Python
181 lines
6.3 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 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.
|
|
|
|
"""V4 prefix-cache metadata tests for per-group block-table /
|
|
base-offset wiring exposed by the scheduler when two same-prefix requests
|
|
land on the V4 paged-cache adjunct.
|
|
|
|
The scheduler is stood up via the nanobind bindings and the assertions read
|
|
the V4 metadata observable by Python runtime / backend code. No GPU forwards.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
import pytest
|
|
|
|
try:
|
|
from tokenspeed_scheduler import ( # type: ignore
|
|
ExecutionEvent,
|
|
ForwardEvent,
|
|
PagedCacheGroupConfig,
|
|
PagedCacheRetention,
|
|
PrefixCacheAdjunctSpec,
|
|
RequestSpec,
|
|
Scheduler,
|
|
SchedulerConfig,
|
|
)
|
|
|
|
_BINDING_AVAILABLE = True
|
|
except ImportError: # pragma: no cover - bindings unbuilt
|
|
_BINDING_AVAILABLE = False
|
|
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
not _BINDING_AVAILABLE,
|
|
reason="tokenspeed_scheduler nanobind bindings unavailable in this env",
|
|
)
|
|
|
|
|
|
def _make_two_group_config() -> "SchedulerConfig":
|
|
cfg = SchedulerConfig()
|
|
cfg.block_size = 2
|
|
cfg.num_device_pages = 64
|
|
cfg.num_host_pages = 64
|
|
cfg.max_scheduled_tokens = 16
|
|
cfg.max_batch_size = 8
|
|
cfg.paged_cache_groups = [
|
|
PagedCacheGroupConfig(
|
|
group_id="fh",
|
|
rows_per_page=4,
|
|
entry_stride_tokens=1,
|
|
total_pages=32,
|
|
retention=PagedCacheRetention.FullHistory,
|
|
),
|
|
PagedCacheGroupConfig(
|
|
group_id="swa",
|
|
rows_per_page=2,
|
|
entry_stride_tokens=1,
|
|
total_pages=32,
|
|
retention=PagedCacheRetention.SlidingWindow,
|
|
sliding_window_tokens=8,
|
|
),
|
|
]
|
|
# Opt into the prefix-cache adjunct so the scheduler actually builds the
|
|
# snapshot chain; without this the attach loop is a no-op and the
|
|
# borrowed-prefix path under test never runs. The C++ side derives
|
|
# lcm_raw_tokens and sliding_window_per_group from each required group's
|
|
# PagedCacheGroupConfig; the Python ABI only declares the group ids.
|
|
adjunct = PrefixCacheAdjunctSpec()
|
|
adjunct.required_groups = ["fh", "swa"]
|
|
cfg.prefix_cache_adjunct = adjunct
|
|
return cfg
|
|
|
|
|
|
def _post(sched: "Scheduler", payload) -> None:
|
|
ev = ExecutionEvent()
|
|
ev.add_event(payload)
|
|
sched.advance(ev)
|
|
|
|
|
|
def _prime_r1(sched: "Scheduler") -> tuple[list[int], list[int]]:
|
|
"""Prime r1 and capture page ids before finish releases its tables."""
|
|
spec = RequestSpec()
|
|
spec.request_id = "r1"
|
|
spec.tokens = list(range(1, 13))
|
|
sched.submit_requests([spec])
|
|
sched.next_execution_plan()
|
|
er = ForwardEvent.ExtendResult()
|
|
er.request_id = "r1"
|
|
er.tokens = [99]
|
|
_post(sched, er)
|
|
sched.next_execution_plan()
|
|
r1_fh = list(sched.get_request_paged_cache_page_ids("r1", "fh"))
|
|
r1_swa = list(sched.get_request_paged_cache_page_ids("r1", "swa"))
|
|
fin = ForwardEvent.Finish()
|
|
fin.request_id = "r1"
|
|
_post(sched, fin)
|
|
sched.next_execution_plan()
|
|
return r1_fh, r1_swa
|
|
|
|
|
|
def _submit_r2_same_prefix(sched: "Scheduler") -> int:
|
|
spec = RequestSpec()
|
|
spec.request_id = "r2"
|
|
spec.tokens = list(range(1, 13))
|
|
sched.submit_requests([spec])
|
|
plan = sched.next_execution_plan()
|
|
assert len(plan.forward) == 1
|
|
forward = plan.forward[0]
|
|
assert forward.request_ids == ["r2"]
|
|
assert len(forward.extend_prefix_lens) == 1
|
|
return int(forward.extend_prefix_lens[0])
|
|
|
|
|
|
class TestV4PrefixCacheMetadata(unittest.TestCase):
|
|
|
|
def setUp(self) -> None:
|
|
config = _make_two_group_config()
|
|
fh_config = next(
|
|
group for group in config.paged_cache_groups if group.group_id == "fh"
|
|
)
|
|
self.fh_raw_tokens_per_page = (
|
|
fh_config.rows_per_page * fh_config.entry_stride_tokens
|
|
)
|
|
self.sched = Scheduler(config)
|
|
self.r1_fh, self.r1_swa = _prime_r1(self.sched)
|
|
self.assertNotEqual(
|
|
self.r1_fh,
|
|
[],
|
|
"r1 fh page ids must be captured before finish releases the request table",
|
|
)
|
|
self.assertNotEqual(
|
|
self.r1_swa,
|
|
[],
|
|
"r1 swa page ids must be captured before finish releases the request table",
|
|
)
|
|
self.r2_prefix_len = _submit_r2_same_prefix(self.sched)
|
|
|
|
def test_block_table_borrowed_plus_suffix(self) -> None:
|
|
"""R2's table starts with r1's borrowed prefix page ids."""
|
|
r2_fh = list(self.sched.get_request_paged_cache_page_ids("r2", "fh"))
|
|
self.assertEqual(self.r2_prefix_len % self.fh_raw_tokens_per_page, 0)
|
|
borrowed_pages = self.r2_prefix_len // self.fh_raw_tokens_per_page
|
|
self.assertGreater(
|
|
borrowed_pages,
|
|
0,
|
|
"r2 must borrow at least one fh page from r1's prefix snapshot",
|
|
)
|
|
self.assertGreaterEqual(len(self.r1_fh), borrowed_pages)
|
|
self.assertGreater(
|
|
len(r2_fh),
|
|
borrowed_pages,
|
|
"r2 must allocate a suffix after its borrowed prefix pages",
|
|
)
|
|
self.assertEqual(r2_fh[:borrowed_pages], self.r1_fh[:borrowed_pages])
|
|
|
|
def test_base_offsets_sliding_correct(self) -> None:
|
|
"""For sliding-window groups the per-request base_logical_page
|
|
matches the snapshot's base offset; full-history is always 0."""
|
|
swa_base = self.sched.get_request_paged_cache_base_logical_page("r2", "swa")
|
|
self.assertGreaterEqual(swa_base, 0)
|
|
swa_ids = list(self.sched.get_request_paged_cache_page_ids("r2", "swa"))
|
|
self.assertLessEqual(swa_base, len(self.r1_swa) + len(swa_ids))
|
|
fh_base = self.sched.get_request_paged_cache_base_logical_page("r2", "fh")
|
|
self.assertEqual(fh_base, 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|