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

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",
]