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
574 lines
23 KiB
Python
574 lines
23 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.
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
from dataclasses import dataclass
|
|
from typing import Literal
|
|
|
|
Retention = Literal["full_history", "sliding_window"]
|
|
Family = Literal["history", "state"]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PagedCacheGroupSpec:
|
|
group_id: str
|
|
retention: Retention
|
|
rows_per_page: int
|
|
entry_stride_tokens: int
|
|
sliding_window_tokens: int | None
|
|
# History groups form a chain; State groups only need the trailing window.
|
|
family: Family = "history"
|
|
|
|
|
|
_PAGED_CACHE_GROUP_DUMMY_PAGES = 1
|
|
|
|
|
|
def scheduler_ext_flat_kvcache() -> bool:
|
|
"""True iff the installed tokenspeed_scheduler ext was built with
|
|
TOKENSPEED_FLAT_KVCACHE. A missing package or an older / radix-built
|
|
ext reports False — the radix-safe default (never delivers flat tables).
|
|
"""
|
|
try:
|
|
# Local import: module must stay importable without the compiled ext.
|
|
import tokenspeed_scheduler
|
|
except ImportError:
|
|
return False
|
|
return bool(getattr(tokenspeed_scheduler, "FLAT_KVCACHE", False))
|
|
|
|
|
|
# Paged-cache label vocabulary (NOT the HF checkpoint's serialized enum:
|
|
# Qwen3.5 checkpoints spell full attention "attention").
|
|
FULL_ATTENTION = "full_attention"
|
|
LINEAR_ATTENTION = "linear_attention"
|
|
|
|
# layer_type label -> retention. GPT-OSS uses the first two, Qwen3.5 GDN
|
|
# layers use "linear_attention"; unknown labels raise.
|
|
_LAYER_TYPE_RETENTION: dict[str, Retention] = {
|
|
FULL_ATTENTION: "full_history",
|
|
"sliding_attention": "sliding_window",
|
|
# State groups ride full_history retention: the C++ side keys the
|
|
# mamba-state kind on family == State && retention != SlidingWindow.
|
|
LINEAR_ATTENTION: "full_history",
|
|
}
|
|
|
|
# Labels whose group is state-family (recurrent state rows, not KV history).
|
|
STATE_LAYER_TYPES = frozenset({LINEAR_ATTENTION})
|
|
|
|
|
|
def hybrid_slab_group_size(
|
|
layer_types: Sequence[str] | None,
|
|
*,
|
|
sliding_window_tokens: int | Sequence[int | None] | None = None,
|
|
) -> int | None:
|
|
"""Group size for the hybrid slab KV layout (one layer of EACH group
|
|
shares a K/V slab), or None to keep the legacy per-layer layout.
|
|
|
|
Single source (canonical) for both the sizing divisor (registry KV
|
|
profile) and the buffer layout (_create_buffers) -- the two must never
|
|
disagree. Safe only with the flat ext (its single BlockPool owns each
|
|
page id by at most one group, so paired layers' live rows never
|
|
overlap) and equal group sizes. Unknown labels degrade to None -- the
|
|
predicate gates an optimization, so it must not raise.
|
|
|
|
Multi-window models (a per-layer window sequence with >1 distinct
|
|
window) degrade to None: the slab pairing is per raw label, not per
|
|
(retention, window) group.
|
|
"""
|
|
if not scheduler_ext_flat_kvcache():
|
|
return None
|
|
if not layer_types:
|
|
return None
|
|
counts: dict[str, int] = {}
|
|
for label in layer_types:
|
|
# State rows are not byte-equal with KV rows, so no slab pairing.
|
|
if label not in _LAYER_TYPE_RETENTION or label in STATE_LAYER_TYPES:
|
|
return None
|
|
counts[label] = counts.get(label, 0) + 1
|
|
if len(counts) < 2:
|
|
return None
|
|
if sliding_window_tokens is not None and not isinstance(sliding_window_tokens, int):
|
|
if not isinstance(sliding_window_tokens, Sequence) or len(
|
|
sliding_window_tokens
|
|
) != len(layer_types):
|
|
return None
|
|
distinct = {
|
|
w
|
|
for label, w in zip(layer_types, sliding_window_tokens)
|
|
if _LAYER_TYPE_RETENTION[label] == "sliding_window"
|
|
and isinstance(w, int)
|
|
and not isinstance(w, bool)
|
|
and w > 0
|
|
}
|
|
if len(distinct) > 1:
|
|
return None
|
|
sizes = set(counts.values())
|
|
if len(sizes) != 1:
|
|
return None
|
|
return sizes.pop()
|
|
|
|
|
|
def _flat_kv_backend(attn_backend: object) -> object:
|
|
"""The backend whose KV-table consumption matters for flat safety: the
|
|
backend itself, or a composite's full-attention sub-backend (hybrid's
|
|
per-layer KV routing lives there and is user-selectable). The linear
|
|
side consumes only the state group's table through its own explicit
|
|
flat path and is out of scope here.
|
|
"""
|
|
sub = getattr(attn_backend, "full_attn_backend", None)
|
|
if sub is not None:
|
|
return _flat_kv_backend(sub)
|
|
return attn_backend
|
|
|
|
|
|
def validate_flat_scheduler_config(
|
|
*,
|
|
flat_kvcache_ext: bool,
|
|
paged_cache_groups: Sequence[object],
|
|
attn_backend: object,
|
|
kv_pool: object,
|
|
speculative_algorithm: str | None = None,
|
|
) -> None:
|
|
"""Fail fast, before the C++ ``Scheduler`` ctor, when a flat-built ext
|
|
cannot drive this setup: a backend (or hybrid sub-backend) that would not
|
|
consume the per-group flat tables, or zero published groups. No-op on a
|
|
radix build.
|
|
"""
|
|
if not flat_kvcache_ext:
|
|
return
|
|
pool_name = type(kv_pool).__name__
|
|
backend = _flat_kv_backend(attn_backend)
|
|
backend_name = type(backend).__name__
|
|
uses_paged = bool(getattr(backend, "uses_paged_cache_groups", False))
|
|
uses_flat = bool(getattr(backend, "uses_flat_cache_groups", False))
|
|
if uses_paged and not uses_flat:
|
|
raise RuntimeError(
|
|
"flat scheduler build (TOKENSPEED_FLAT_KVCACHE) does not support "
|
|
f"this model's cache layout yet: attention backend {backend_name} "
|
|
f"(KV pool {pool_name}) consumes paged-cache groups through the "
|
|
"radix scheduler's populate path, which the flat build compiles "
|
|
"out — CUDA graphs would silently replay against stale capture "
|
|
"placeholders. Use a radix-built tokenspeed_scheduler extension "
|
|
"for this model."
|
|
)
|
|
if speculative_algorithm is not None and not getattr(
|
|
backend, "flat_spec_capable", True
|
|
):
|
|
raise RuntimeError(
|
|
"flat scheduler build (TOKENSPEED_FLAT_KVCACHE): attention backend "
|
|
f"{backend_name} does not support flat cache groups with "
|
|
"speculative decoding yet. Use the MHA backend or a radix-built "
|
|
"tokenspeed_scheduler extension."
|
|
)
|
|
if speculative_algorithm == "DFLASH":
|
|
raise RuntimeError(
|
|
"flat scheduler build (TOKENSPEED_FLAT_KVCACHE): DFLASH block "
|
|
"decode is unsupported on the flat path. Use a radix-built "
|
|
"tokenspeed_scheduler extension."
|
|
)
|
|
if len(paged_cache_groups) > 1 and not uses_flat:
|
|
# A table-blind backend on a multi-group pool would index every
|
|
# layer through the C++ single-table fallback (a first-group
|
|
# sample) — with slab-aliased layouts that silently corrupts KV
|
|
# past the sliding window. Refuse at startup instead.
|
|
raise RuntimeError(
|
|
"flat scheduler build (TOKENSPEED_FLAT_KVCACHE): KV pool "
|
|
f"{pool_name} publishes {len(paged_cache_groups)} cache groups "
|
|
f"but attention backend {backend_name} does not consume flat "
|
|
"per-group tables (uses_flat_cache_groups=False); the single-"
|
|
"table fallback would serve one group's pages to every layer, "
|
|
"silently corrupting KV. Pick a flat-capable attention backend "
|
|
"or use a radix-built tokenspeed_scheduler extension."
|
|
)
|
|
if not paged_cache_groups:
|
|
raise RuntimeError(
|
|
"flat scheduler build (TOKENSPEED_FLAT_KVCACHE) requires at least "
|
|
f"one paged-cache group, but KV pool {pool_name} publishes none "
|
|
"(e.g. mamba/state-only pools). Use a radix-built "
|
|
"tokenspeed_scheduler extension for this model."
|
|
)
|
|
|
|
|
|
def compute_paged_cache_group_page_counts(
|
|
specs: Sequence[PagedCacheGroupSpec],
|
|
*,
|
|
max_live_requests: int,
|
|
max_scheduled_tokens: int,
|
|
max_total_tokens: int,
|
|
max_context_len: int,
|
|
decode_input_tokens: int = 1,
|
|
overlap_schedule_depth: int = 0,
|
|
safety_margin: int = 0,
|
|
) -> dict[str, int]:
|
|
# Local import: keeps this module torch-free at import time.
|
|
from tokenspeed.runtime.utils.common import ceil_div
|
|
|
|
if max_live_requests < 0:
|
|
raise ValueError(f"max_live_requests must be >= 0, got {max_live_requests}")
|
|
if max_scheduled_tokens < 0:
|
|
raise ValueError(
|
|
f"max_scheduled_tokens must be >= 0, got {max_scheduled_tokens}"
|
|
)
|
|
if max_total_tokens < 0:
|
|
raise ValueError(f"max_total_tokens must be >= 0, got {max_total_tokens}")
|
|
if max_context_len < 0:
|
|
raise ValueError(f"max_context_len must be >= 0, got {max_context_len}")
|
|
if decode_input_tokens < 0:
|
|
raise ValueError(f"decode_input_tokens must be >= 0, got {decode_input_tokens}")
|
|
if overlap_schedule_depth not in (0, 1):
|
|
raise ValueError(
|
|
f"overlap_schedule_depth must be 0 or 1, got {overlap_schedule_depth}"
|
|
)
|
|
if overlap_schedule_depth > 0 and decode_input_tokens == 0:
|
|
raise ValueError(
|
|
"overlapped paged-cache sizing requires decode_input_tokens > 0"
|
|
)
|
|
if safety_margin < 0:
|
|
raise ValueError(f"safety_margin must be >= 0, got {safety_margin}")
|
|
|
|
counts: dict[str, int] = {}
|
|
for spec in specs:
|
|
raw_per_page = spec.rows_per_page * spec.entry_stride_tokens
|
|
if raw_per_page <= 0:
|
|
raise ValueError(
|
|
f"PagedCacheGroupSpec {spec.group_id}: rows_per_page * "
|
|
"entry_stride_tokens must be > 0"
|
|
)
|
|
protected_pages = max_live_requests * ceil_div(
|
|
overlap_schedule_depth * decode_input_tokens, raw_per_page
|
|
)
|
|
# Mamba-state kind = family "state" AND retention != sliding_window
|
|
# (the C++ side keys it the same way); V4's sliding-window state tail
|
|
# buffers keep the sliding-window formula below.
|
|
if spec.family == "state" and spec.retention == "full_history":
|
|
# State group: 2 live pages/request (the W=2 write window) +
|
|
# floor(T/P) snapshot pages (snapshots are bounded by the shared
|
|
# page-id space), capped at the full-history count.
|
|
full_history_total = (
|
|
ceil_div(max_total_tokens, raw_per_page)
|
|
+ max_live_requests
|
|
+ protected_pages
|
|
+ _PAGED_CACHE_GROUP_DUMMY_PAGES
|
|
+ safety_margin
|
|
)
|
|
state_total = (
|
|
max_live_requests * 2
|
|
+ max_total_tokens // raw_per_page
|
|
+ protected_pages
|
|
+ _PAGED_CACHE_GROUP_DUMMY_PAGES
|
|
+ safety_margin
|
|
)
|
|
total = min(state_total, full_history_total)
|
|
elif spec.retention == "full_history":
|
|
full_pages = ceil_div(max_total_tokens, raw_per_page)
|
|
total = (
|
|
full_pages
|
|
+ max_live_requests
|
|
+ protected_pages
|
|
+ _PAGED_CACHE_GROUP_DUMMY_PAGES
|
|
+ safety_margin
|
|
)
|
|
elif spec.retention == "sliding_window":
|
|
window = spec.sliding_window_tokens
|
|
if window is None or window <= 0:
|
|
raise ValueError(
|
|
f"PagedCacheGroupSpec {spec.group_id}: sliding group missing "
|
|
"positive sliding_window_tokens"
|
|
)
|
|
# Capacity tracks resident history before the next token.
|
|
resident_tokens_per_req = min(max(window - 1, 0), max_context_len)
|
|
resident_pages = max_live_requests * ceil_div(
|
|
resident_tokens_per_req, raw_per_page
|
|
)
|
|
scheduled_tokens = min(max_scheduled_tokens, max_total_tokens)
|
|
scheduled_pages = ceil_div(scheduled_tokens, raw_per_page)
|
|
total = (
|
|
resident_pages
|
|
+ scheduled_pages
|
|
+ max_live_requests
|
|
+ protected_pages
|
|
+ _PAGED_CACHE_GROUP_DUMMY_PAGES
|
|
+ safety_margin
|
|
)
|
|
else:
|
|
raise ValueError(
|
|
f"PagedCacheGroupSpec {spec.group_id}: unsupported retention "
|
|
f"{spec.retention!r}"
|
|
)
|
|
counts[spec.group_id] = int(total)
|
|
return counts
|
|
|
|
|
|
def _layer_specs(
|
|
layer_types: Sequence[str],
|
|
sliding_window_tokens: int | Sequence[int | None] | None,
|
|
) -> list[tuple[str, Retention, int | None]]:
|
|
"""Per-layer (group_id, retention, window). group_id is the bare label
|
|
unless sliding layers carry more than one distinct window (then
|
|
label_<window>), so single-window models keep byte-identical ids.
|
|
A scalar window broadcasts to sliding layers; a sequence lines up 1:1."""
|
|
if isinstance(sliding_window_tokens, str):
|
|
raise ValueError(
|
|
"_layer_specs: sliding_window_tokens must be None, an int, or a "
|
|
f"sequence of int/None, got {sliding_window_tokens!r}"
|
|
)
|
|
if sliding_window_tokens is None or isinstance(sliding_window_tokens, int):
|
|
if isinstance(sliding_window_tokens, bool):
|
|
raise ValueError(
|
|
"_layer_specs: sliding_window_tokens must be None, an int, or "
|
|
f"a sequence of int/None, got {sliding_window_tokens!r}"
|
|
)
|
|
windows: list[int | None] = [sliding_window_tokens] * len(layer_types)
|
|
scalar = True
|
|
elif not isinstance(sliding_window_tokens, Sequence):
|
|
raise ValueError(
|
|
"_layer_specs: sliding_window_tokens must be None, an int, or a "
|
|
f"sequence of int/None, got {sliding_window_tokens!r}"
|
|
)
|
|
else:
|
|
windows = list(sliding_window_tokens)
|
|
scalar = False
|
|
if len(windows) != len(layer_types):
|
|
raise ValueError(
|
|
f"_layer_specs: sliding_window_tokens has {len(windows)} "
|
|
f"entries but layer_types has {len(layer_types)}"
|
|
)
|
|
rows: list[tuple[str, Retention, int | None]] = []
|
|
for i, (label, raw) in enumerate(zip(layer_types, windows)):
|
|
retention = _LAYER_TYPE_RETENTION.get(label)
|
|
if retention is None:
|
|
raise ValueError(
|
|
f"_layer_specs: unknown layer_type {label!r} at layer {i}; "
|
|
f"expected one of {sorted(_LAYER_TYPE_RETENTION)}"
|
|
)
|
|
if raw is not None and (isinstance(raw, bool) or not isinstance(raw, int)):
|
|
raise ValueError(
|
|
f"_layer_specs: layer {i} ({label!r}) window must be None or "
|
|
f"an int, got {raw!r}"
|
|
)
|
|
window = raw
|
|
if retention == "sliding_window":
|
|
if window is None or window <= 0:
|
|
raise ValueError(
|
|
f"_layer_specs: layer {i} ({label!r}) is sliding but its "
|
|
f"window is not a positive int (got {raw!r})"
|
|
)
|
|
else:
|
|
if not scalar and window is not None and window > 0:
|
|
raise ValueError(
|
|
f"_layer_specs: layer {i} ({label!r}) is full-history but "
|
|
f"carries sliding window {window}; mislabeled layer_type?"
|
|
)
|
|
window = None
|
|
rows.append((label, retention, window))
|
|
distinct = {w for _, r, w in rows if r == "sliding_window"}
|
|
multi_window = len(distinct) > 1
|
|
return [
|
|
(
|
|
(
|
|
f"{label}_{window}"
|
|
if multi_window and retention == "sliding_window"
|
|
else label
|
|
),
|
|
retention,
|
|
window,
|
|
)
|
|
for label, retention, window in rows
|
|
]
|
|
|
|
|
|
def layer_group_ids(
|
|
*,
|
|
layer_types: Sequence[str],
|
|
sliding_window_tokens: int | Sequence[int | None] | None,
|
|
) -> list[str]:
|
|
"""Per-layer paged-cache group id — the single source multi-window models
|
|
will assign ``PagedAttention(group_id=...)`` from (today gpt_oss.py
|
|
assigns group_id=layer_type, identical in the single-window case), so
|
|
``flat_block_tables`` keys line up with the published group specs."""
|
|
return [gid for gid, _, _ in _layer_specs(layer_types, sliding_window_tokens)]
|
|
|
|
|
|
def group_specs_from_layer_types(
|
|
*,
|
|
layer_types: Sequence[str],
|
|
sliding_window_tokens: int | Sequence[int | None] | None,
|
|
page_size: int,
|
|
) -> list[PagedCacheGroupSpec]:
|
|
"""Derive paged-cache group specs from per-layer attention types.
|
|
|
|
vLLM-style spec-value grouping: layers collapse into one group per
|
|
distinct (retention, window). Group order = first-appearance order.
|
|
|
|
Args:
|
|
layer_types: Per-layer labels: "full_attention" / "sliding_attention"
|
|
/ "linear_attention" (state-family, e.g. Qwen3.5 GDN).
|
|
sliding_window_tokens: One window for all sliding layers (today's HF
|
|
scalar), or a per-layer sequence (multi-window models; full-layer
|
|
positions must be None).
|
|
page_size: Tokens per page (uniform across groups).
|
|
|
|
Raises:
|
|
ValueError: unknown label; window sequence length mismatch; sliding
|
|
layer without a positive window; full layer carrying a window.
|
|
"""
|
|
specs: list[PagedCacheGroupSpec] = []
|
|
seen: set[str] = set()
|
|
for gid, retention, window in _layer_specs(layer_types, sliding_window_tokens):
|
|
if gid in seen:
|
|
continue
|
|
seen.add(gid)
|
|
specs.append(
|
|
PagedCacheGroupSpec(
|
|
group_id=gid,
|
|
retention=retention,
|
|
rows_per_page=page_size,
|
|
entry_stride_tokens=1,
|
|
sliding_window_tokens=window,
|
|
family="state" if gid in STATE_LAYER_TYPES else "history",
|
|
)
|
|
)
|
|
return specs
|
|
|
|
|
|
def publish_paged_cache_groups(
|
|
*,
|
|
layer_types: Sequence[str],
|
|
sliding_window_tokens: int | Sequence[int | None] | None,
|
|
page_size: int,
|
|
max_live_requests: int,
|
|
max_scheduled_tokens: int,
|
|
max_total_tokens: int,
|
|
max_context_len: int,
|
|
) -> tuple[list[PagedCacheGroupSpec], dict[str, int]] | None:
|
|
"""Publication rule (canonical) for a KV pool's paged-cache groups.
|
|
|
|
Publish groups iff the scheduler ext is flat-built (a radix ext never
|
|
delivers flat tables — capture would bind dead buffers). Speculative
|
|
decoding is supported: verify writes per-group [bs*N] locations and the
|
|
drafter consumes the full-attention group's table (mirrored into
|
|
req_to_page each step). Publication is THE upstream signal every flat
|
|
consumer keys off.
|
|
|
|
Args:
|
|
layer_types: Per-layer paged-cache labels (empty -> single
|
|
full-history group).
|
|
sliding_window_tokens / page_size: Forwarded to
|
|
group_specs_from_layer_types.
|
|
max_live_requests / max_scheduled_tokens / max_total_tokens /
|
|
max_context_len: Sizing inputs for
|
|
compute_paged_cache_group_page_counts.
|
|
|
|
Returns:
|
|
(specs, page_counts) when publishing, None on a radix ext.
|
|
"""
|
|
if not scheduler_ext_flat_kvcache():
|
|
return None
|
|
specs = group_specs_from_layer_types(
|
|
layer_types=tuple(layer_types) or (FULL_ATTENTION,),
|
|
sliding_window_tokens=sliding_window_tokens,
|
|
page_size=page_size,
|
|
)
|
|
counts = compute_paged_cache_group_page_counts(
|
|
specs,
|
|
max_live_requests=max_live_requests,
|
|
max_scheduled_tokens=max(0, int(max_scheduled_tokens)),
|
|
max_total_tokens=max_total_tokens,
|
|
max_context_len=max_context_len,
|
|
)
|
|
return specs, counts
|
|
|
|
|
|
def compute_max_logical_pages_for_capture(
|
|
spec: PagedCacheGroupSpec,
|
|
*,
|
|
max_context_len: int,
|
|
max_tokens_per_req: int = 1,
|
|
overlap_schedule_depth: int = 0,
|
|
) -> int:
|
|
"""Return CUDA Graph block-table width for one paged-cache group.
|
|
|
|
Decode admission reserves the current verify span plus one span for each
|
|
overlapped schedule. Include that complete reservation horizon here: a
|
|
request close to the model context limit can still expose the reserved
|
|
pages in its scheduler block-table row before the accepted tokens are
|
|
truncated by the request-length limit.
|
|
|
|
Args:
|
|
spec: Paged-cache group layout and retention policy.
|
|
max_context_len: Maximum accepted raw-token context length.
|
|
max_tokens_per_req: Runtime decode/verify width.
|
|
overlap_schedule_depth: Number of additionally in-flight decode steps.
|
|
|
|
Returns:
|
|
Required block-table columns for one request.
|
|
"""
|
|
# Local import: keeps this module torch-free at import time.
|
|
from tokenspeed.runtime.utils.common import ceil_div
|
|
|
|
if max_context_len < 0:
|
|
raise ValueError(f"max_context_len must be >= 0, got {max_context_len}")
|
|
if max_tokens_per_req <= 0:
|
|
raise ValueError(f"max_tokens_per_req must be > 0, got {max_tokens_per_req}")
|
|
if overlap_schedule_depth not in (0, 1):
|
|
raise ValueError(
|
|
f"overlap_schedule_depth must be 0 or 1, got {overlap_schedule_depth}"
|
|
)
|
|
raw_per_page = spec.rows_per_page * spec.entry_stride_tokens
|
|
if raw_per_page <= 0:
|
|
raise ValueError(
|
|
f"PagedCacheGroupSpec {spec.group_id}: rows_per_page * "
|
|
"entry_stride_tokens must be > 0"
|
|
)
|
|
reservation_horizon = (overlap_schedule_depth + 1) * max_tokens_per_req
|
|
if spec.retention == "sliding_window":
|
|
window = spec.sliding_window_tokens
|
|
if window is None or window <= 0:
|
|
raise ValueError(
|
|
f"PagedCacheGroupSpec {spec.group_id}: sliding group missing "
|
|
"positive sliding_window_tokens"
|
|
)
|
|
# Capture uses a conservative metadata bound; it does not change the
|
|
# per-token attention history counted as window - 1 above.
|
|
retention_bound = min(window, max_context_len)
|
|
live_tokens = retention_bound + reservation_horizon
|
|
return ceil_div(live_tokens, raw_per_page) + 1
|
|
if spec.retention == "full_history":
|
|
live_tokens = max_context_len + reservation_horizon
|
|
return ceil_div(live_tokens, raw_per_page)
|
|
raise ValueError(
|
|
f"PagedCacheGroupSpec {spec.group_id}: unsupported retention "
|
|
f"{spec.retention!r}"
|
|
)
|
|
|
|
|
|
__all__ = [
|
|
"FULL_ATTENTION",
|
|
"LINEAR_ATTENTION",
|
|
"PagedCacheGroupSpec",
|
|
"Retention",
|
|
"STATE_LAYER_TYPES",
|
|
"compute_max_logical_pages_for_capture",
|
|
"compute_paged_cache_group_page_counts",
|
|
"group_specs_from_layer_types",
|
|
"hybrid_slab_group_size",
|
|
"layer_group_ids",
|
|
"publish_paged_cache_groups",
|
|
"scheduler_ext_flat_kvcache",
|
|
"validate_flat_scheduler_config",
|
|
]
|