Source code for mstar.engine.resources.kv.bounded.config
"""What a model declares about a bounded KV cache.
A request's cache is a logical stream: an optional read-only source prefix
(a voice prompt's keys, say, shared by every request that uses it) followed by
the tokens the request writes. Retention keeps the stream's first ``sink``
entries and its last ``window`` (StreamingLLM's attention sinks plus a sliding
window), so a request's footprint is fixed and known when it starts: each slot
holds only the written part of the sink and a ring for the window, and source
entries are read where they live.
"""
from collections.abc import Callable, Mapping
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, NamedTuple
import torch
from mstar.engine.resources.spec import NodeResourceSpec
from mstar.engine.resources.step import ResourceStep
if TYPE_CHECKING:
from mstar.engine.resources.base import Resource
[docs]
class SinkWindow(NamedTuple):
"""Keep the stream's first ``sink`` and last ``window`` entries. With
``window_includes_step`` the window counts the step's own tokens, so a step
that writes ``span`` attends only the last ``window - span`` retained ones."""
sink: int
window: int
window_includes_step: bool = False
[docs]
@dataclass
class BoundedKVConfig:
num_layers: int
num_heads: int
head_dim: int
# key/value rows a request owns per layer, e.g. 2 for batched CFG
rows_per_request: int
# the longest source prefix a request may have; sizes the slots
max_source_len: int
# source length -> what that request keeps, or one policy for every length
retention: Callable[[int], SinkWindow] | SinkWindow
max_slots: int = 32
# the shortest source prefix a request may have: the slots also hold the part
# of a sink its source doesn't cover, which is largest for the shortest source
min_source_len: int = 0
# None is the engine's KV dtype (``EngineResourceInfo.kv_dtype``), settled when
# the resource is built
dtype: torch.dtype | None = torch.float32
# A step's tokens join the stream last-first. Only for attention that
# ignores key order, where it decides which of the oldest step's tokens are
# evicted first: Step-Audio2's flow decoder keeps the front of each chunk.
reverse_step_order: bool = False
# None: the window is per step, every query of a step attending the same keys
# (the retained ones and all of the step's own). Set, it is per query: each
# query attends the keys at distances [0, query_window] from it, so a step of
# several tokens is causal within itself (a streaming conformer's
# ``att_context_size=[left, 0]``). Needs no sink and the stream in order.
query_window: int | None = None
def __post_init__(self):
if self.max_slots < 1:
raise ValueError(f"max_slots must be >= 1, got {self.max_slots}")
if not 0 <= self.min_source_len <= self.max_source_len:
raise ValueError(f"source lengths {self.min_source_len}..{self.max_source_len} are not a range")
q_window = self.query_window
if q_window is not None and (q_window < 0 or self.reverse_step_order):
raise ValueError(f"query_window={q_window} needs to be >= 0, with the stream in order")
r = self.retention
# resolved once here: a plan looks one up for every row
self._policies = tuple(r if isinstance(r, SinkWindow) else r(n)
for n in range(self.min_source_len, self.max_source_len + 1))
sink = window = retained = 0
for n, policy in enumerate(self._policies, self.min_source_len):
if policy.sink < 0 or policy.window < 0:
raise ValueError(f"retention for {n} source tokens is {policy}")
if q_window is not None and (policy.sink or policy.window_includes_step or policy.window < q_window):
raise ValueError(f"retention for {n} source tokens is {policy}: a per-query window of "
f"{q_window} takes no sink and a window of at least {q_window}")
sink = max(sink, policy.sink - n)
window = max(window, policy.window)
retained = max(retained, policy.sink + policy.window)
self._sink_capacity, self._window_capacity, self._max_retained = sink, window, retained
[docs]
def retention_for(self, source_len: int) -> SinkWindow:
return self._policies[source_len - self.min_source_len]
@property
def sink_capacity(self) -> int:
"""Written tokens a slot's sink region holds (the sink past the source)."""
return self._sink_capacity
@property
def window_capacity(self) -> int:
return self._window_capacity
@property
def max_retained(self) -> int:
"""The most entries a request retains, over every allowed source length."""
return self._max_retained
@property
def slot_tokens(self) -> int:
return self.sink_capacity + self.window_capacity
[docs]
@dataclass
class BoundedKVSpec(NodeResourceSpec):
config: BoundedKVConfig
@property
def resource_class(self) -> "type[Resource]":
from mstar.engine.resources.kv.bounded.manager import BoundedKVManager
return BoundedKVManager
[docs]
def apply_yaml_overrides(self, max_slots: int | None = None):
if max_slots is not None:
if max_slots < 1:
raise ValueError(f"max_slots must be >= 1, got {max_slots}")
self.config.max_slots = max_slots
[docs]
class StreamPosition(NamedTuple):
"""Where a request's stream stands when a step starts."""
source_len: int
# tokens the request has written before this step
written: int
# which of the sources ``attend`` is given this request's starts with; -1 for its
# own entry of the ``row_source`` it is given
source: int = 0
[docs]
@dataclass(frozen=True)
class BoundedKVStep(ResourceStep):
"""Each segment's ``span`` is what its request writes this step (0 reads only).
The positions come from the model, not the resource, so a plan never waits
on the previous step's commit and planning the same step twice (a piecewise
region inside a forward that declared it too) is harmless.
"""
positions: Mapping[str, StreamPosition] = field(default_factory=dict)