161 lines
6.7 KiB
Python
161 lines
6.7 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""The ``StateObject`` contract for session state (RFC #4480).
|
|
|
|
A session is a named collection of ``StateObject`` instances. The interface
|
|
deliberately speaks only of bytes, growth, read views, commitment, and
|
|
evictability -- never tokens, layers, or attention. Writes are two-phase:
|
|
``stage()`` holds data for an in-progress window, ``commit()`` promotes it to
|
|
persistent context, and ``discard()`` drops it.
|
|
|
|
A concrete class may back its storage with plain (non-paged) buffers; the
|
|
contract itself does not require that. Several methods are
|
|
intentionally inert and documented as such; they exist so that paged-KV
|
|
backing and speculative forking can be added without changing any call site.
|
|
See RFC #4480 for the full design and roadmap.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from typing import Any, Generic, TypeVar
|
|
|
|
from vllm_omni.experimental.world_models.session_state.accounting import SeenStorages
|
|
|
|
# What writers pass into ``stage()`` / ``commit()`` / ``append()``.
|
|
PayloadT = TypeVar("PayloadT")
|
|
# What ``view()`` hands to the consumer (not necessarily the payload type:
|
|
# an append buffer views as a list of payloads).
|
|
ViewT = TypeVar("ViewT")
|
|
|
|
|
|
class StateObject(ABC, Generic[PayloadT, ViewT]):
|
|
"""One typed unit of session state with a uniform lifecycle.
|
|
|
|
A session's state is a named collection of these objects; the kinds of
|
|
memory they represent include attention K/V (growing self-attention
|
|
caches, or conditioning K/V written once and reread), constant-size
|
|
recurrent state (e.g. a linear-attention carry), frame or latent buffers,
|
|
and retrieval pools. A concrete class binds ``PayloadT`` and ``ViewT`` to
|
|
its real payload and view types and implements the abstract methods; the
|
|
non-abstract methods provide the default behaviour described in their
|
|
docstrings.
|
|
|
|
The contract is storage-agnostic. A concrete class may hold its bytes in
|
|
a buffer it owns, or it may be a handle to storage owned by another
|
|
component, such as a paged KV pool. In the handle case it delegates its
|
|
lifecycle operations to that owner instead of copying the data.
|
|
"""
|
|
|
|
# The dependency edge: what this object rebuilds from. ``None`` means it is
|
|
# never recomputable (it must be preempted or rejected, not silently
|
|
# dropped).
|
|
recompute_source: StateObject | None = None
|
|
# Estimated work to rebuild, given ``recompute_source`` is resident.
|
|
recompute_cost: int = 0
|
|
# Payload held by ``stage()`` until ``discard()`` clears it.
|
|
_staged: PayloadT | None = None
|
|
|
|
@abstractmethod
|
|
def allocate(self) -> None:
|
|
"""Size and initialise the backing storage to its empty state.
|
|
|
|
Concrete classes declare their sizing parameters as typed keyword-only
|
|
arguments (e.g. ``LatentBuffer.allocate(maxlen=...)``); a mistyped
|
|
keyword raises ``TypeError`` instead of being silently ignored.
|
|
"""
|
|
|
|
def stage(self, payload: PayloadT) -> None:
|
|
"""Hold data for an in-progress window.
|
|
|
|
Single-window writers go straight through ``commit()``, so the default
|
|
implementation only records the payload and is otherwise inert.
|
|
Speculative forking is the intended consumer of staged state.
|
|
"""
|
|
self._staged = payload
|
|
|
|
@abstractmethod
|
|
def commit(self, payload: PayloadT | None = None) -> None:
|
|
"""Promote data to persistent context.
|
|
|
|
For a plain-buffer object this is the synchronous write into its
|
|
committed storage (e.g. ``cache[i] = updated_kv.clone()``).
|
|
"""
|
|
|
|
def discard(self) -> None:
|
|
"""Drop staged data (speculative rejection).
|
|
|
|
Clears any recorded staged payload; committed bytes are never touched.
|
|
"""
|
|
self._staged = None
|
|
|
|
def append(self, payload: PayloadT) -> None:
|
|
"""``stage()`` + ``commit()``; single-shot writers use only this."""
|
|
self.commit(payload)
|
|
|
|
@abstractmethod
|
|
def view(self, *, include_staged: bool = True) -> ViewT:
|
|
"""Return what the consumer (attention metadata, pipeline) reads."""
|
|
|
|
# ``policy`` deliberately stays ``Any``: the policy protocol belongs to
|
|
# the byte-budget eviction planner (RFC #4480), and defining it here
|
|
# would invent an interface that design owns.
|
|
def evict(self, policy: Any = None) -> int:
|
|
"""Release this object's backing storage so it can be reused; return the
|
|
bytes freed.
|
|
|
|
This is the backend-agnostic release hook. For a plain buffer it drops
|
|
the buffer and lets the garbage collector reclaim it; for a paged backend
|
|
it returns the blocks to the pool so they can be recycled. Never reclaims
|
|
staged bytes.
|
|
|
|
Precondition: only call this when the object is no longer in use.
|
|
Releasing storage that is still referenced is a correctness bug (another
|
|
consumer could reuse it). Count-based session eviction therefore does NOT
|
|
call this — it only drops the lookup-table entry and lets an unreferenced
|
|
session be garbage-collected. The intended driver of this hook is a
|
|
byte-budget eviction planner with the recompute-DAG safety described in
|
|
RFC #4480.
|
|
"""
|
|
freed = self.nbytes
|
|
self.reset()
|
|
return freed
|
|
|
|
@abstractmethod
|
|
def reset(self) -> None:
|
|
"""Clear back to the unallocated state."""
|
|
|
|
@abstractmethod
|
|
def nbytes_by_device(self, seen: SeenStorages) -> dict[str, int]:
|
|
"""Bytes held in this object's committed storage, keyed by device.
|
|
|
|
Staged payloads are not counted. ``seen`` deduplicates storages across
|
|
the whole session -- pass it through to ``device_bytes`` rather than
|
|
starting a fresh set, or two views on one buffer are counted twice.
|
|
"""
|
|
|
|
@property
|
|
def nbytes(self) -> int:
|
|
"""Total bytes held, summed over every device.
|
|
|
|
Only meaningful when the caller genuinely does not care where the bytes
|
|
live (``evict()`` reporting what it freed, for instance). Anything
|
|
deciding against a budget wants ``nbytes_by_device`` instead: host and
|
|
device memory are different resources and their sum is not a quantity.
|
|
"""
|
|
return sum(self.nbytes_by_device(set()).values())
|
|
|
|
@property
|
|
@abstractmethod
|
|
def resident(self) -> bool:
|
|
"""Whether data is currently in memory (not yet evicted)."""
|
|
|
|
@property
|
|
def recomputable(self) -> bool:
|
|
"""Droppable-and-rebuildable *right now*.
|
|
|
|
Derived, not declared: true only while ``recompute_source`` is still
|
|
resident, so the answer changes as other objects are evicted.
|
|
"""
|
|
return self.recompute_source is not None and self.recompute_source.resident
|