Files
2026-07-13 12:24:33 +08:00

340 lines
13 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Fold / unfold logic for multi-object-group prefix-cache hit computation.
With hybrid models, an LMCache request is split across several *object groups*
(full attention, sliding window, mamba, ...), each stored as its own
``MemoryObj`` and each with a different token-dependency rule:
- **full attention** needs every chunk of the prefix present to serve it;
- a **sliding window of ``w`` chunks** only needs the last ``w`` chunks of the
prefix present (mamba is the ``w == 1`` case).
A model-wide prefix-cache hit of length ``L`` requires *every* object group to
be able to serve a prefix of length ``L`` under its own rule. This module turns
the per-group presence bitmaps into that single answer in three steps:
1. **fold** — combine the per-group presence into a servable bitmap: bit ``j``
is set iff every group can serve a length-``j + 1`` prefix;
2. **highest-set-bit** — the model-wide hit length is the highest set bit plus
one (``-1`` -> 0, no hit);
3. **unfold** — expand the hit length into the concrete chunks each group needs
(``unfold_range``), producing the retain mask used to load / lock / transfer.
When every group is full attention the servable set is a downward-closed prefix,
so the hit length equals the leading-ones count of the AND of the per-group
presences -- i.e. the plain ``TrimPolicy.PREFIX`` / require-all intersection.
Fold/unfold is a strict generalization of that behavior.
Bitmaps here are laid out **group-major**: bit ``g * num_chunks + j`` is set iff
chunk ``j`` is available for object group ``g``.
"""
# Standard
from collections.abc import Iterable, Sequence
# First Party
from lmcache.native_storage_ops import Bitmap
from lmcache.native_storage_ops import fold as _native_fold
from lmcache.native_storage_ops import unfold as _native_unfold
from lmcache.v1.distributed.api import TrimPolicy
FULL_ATTENTION_WINDOW = -1
"""Sentinel ``group_windows`` value marking a full-attention object group
(needs the whole prefix). Any value ``<= 0`` is treated as full attention."""
def unfold_range(prefix_len: int, window: int) -> tuple[int, int]:
"""Chunk range ``[lo, hi)`` one object group needs to serve a prefix.
Full attention needs ``[0, prefix_len)``; a window of ``w`` chunks needs
``[max(0, prefix_len - w), prefix_len)``.
Args:
prefix_len: model-wide hit length in chunks.
window: cross-chunk sliding-window size in chunks; ``<= 0`` means full
attention.
Returns:
The half-open chunk range this group must keep. ``(0, 0)`` when
``prefix_len <= 0``.
"""
if prefix_len <= 0:
return (0, 0)
lo = 0 if window <= 0 else max(0, prefix_len - window)
return (lo, prefix_len)
def fold(
found: Bitmap,
num_chunks: int,
num_ranks: int,
group_windows: Sequence[int],
) -> Bitmap:
"""Fold per-(group, chunk, rank) presence into servable prefix lengths.
For each object group, computes which prefix lengths it can serve under its
rule (a length-``L`` prefix needs the last ``min(window, L)`` chunks
present), and intersects across groups. A chunk is present for a group only
when **every** kv_rank shard is present.
Args:
found: presence bitmap of length
``len(group_windows) * num_chunks * num_ranks``; bit
``g * (num_chunks * num_ranks) + j * num_ranks + r`` set iff chunk
``j`` of object group ``g`` is present for kv_rank ``r``.
num_chunks: number of LMCache chunks in the request.
num_ranks: number of kv_rank shards per chunk (``world_size`` at lookup).
group_windows: per-object-group cross-chunk sliding-window size in
chunks, in object-group order; ``<= 0`` means full attention.
Returns:
A bitmap of size ``num_chunks``; bit ``j`` set iff every group can
serve a length-``j + 1`` prefix. Feed it to :func:`highest_set_bit`;
the model-wide hit length is that index plus one (``-1`` -> 0).
Raises:
ValueError: If ``group_windows`` is empty, ``num_chunks`` is negative,
or ``num_ranks`` is not positive.
"""
if num_ranks < 1:
raise ValueError(f"num_ranks must be >= 1 (got {num_ranks})")
if not group_windows:
raise ValueError("group_windows must be non-empty")
if num_chunks < 0:
raise ValueError(f"num_chunks must be >= 0 (got {num_chunks})")
return _native_fold(found, num_chunks, num_ranks, list(group_windows))
def highest_set_bit(servable: Bitmap) -> int:
"""Model-wide prefix hit length: the highest set bit of a fold output.
Args:
servable: A servable-lengths bitmap from :func:`fold`.
Returns:
The highest set bit index. On :func:`fold`'s output this is the
model-wide hit length, always ``>= 0`` because :func:`fold` always sets
bit 0 (the empty prefix is servable by every group).
Raises:
ValueError: If ``servable`` has no set bit. This cannot occur on
:func:`fold`'s output, which always sets bit 0.
"""
return servable.highest_set_bit()
def unfold(
hit_length: int,
num_chunks: int,
num_ranks: int,
group_windows: Sequence[int],
) -> Bitmap:
"""Expand a model-wide hit length into the per-group retain mask.
Each group retains the chunks it needs to serve ``hit_length``:
``[0, hit_length)`` for full attention, ``[hit_length - window, hit_length)``
for a sliding window, with **all** kv_ranks of each retained
``(group, chunk)`` set.
Args:
hit_length: model-wide prefix hit length in chunks (clamped to
``num_chunks``).
num_chunks: number of LMCache chunks in the request.
num_ranks: number of kv_rank shards per chunk.
group_windows: per-object-group cross-chunk sliding-window size in
chunks, in object-group order; ``<= 0`` means full attention.
Returns:
A retain mask over the same ranked layout as :func:`fold`'s input,
length ``len(group_windows) * num_chunks * num_ranks``.
Raises:
ValueError: If ``group_windows`` is empty, ``num_chunks`` is negative,
or ``num_ranks`` is not positive.
"""
if num_ranks < 1:
raise ValueError(f"num_ranks must be >= 1 (got {num_ranks})")
if not group_windows:
raise ValueError("group_windows must be non-empty")
if num_chunks < 0:
raise ValueError(f"num_chunks must be >= 0 (got {num_chunks})")
return _native_unfold(hit_length, num_chunks, num_ranks, list(group_windows))
def fold_unfold_ranked(
found: Bitmap,
num_chunks: int,
num_ranks: int,
group_windows: Sequence[int],
) -> tuple[int, Bitmap]:
"""Compose :func:`fold` -> :func:`highest_set_bit` -> :func:`unfold`.
Convenience for the full pipeline over the ``group x chunk x kv_rank``
lookup key layout: the model-wide hit length and the keys each group must
retain to serve it.
Args:
found: presence bitmap (see :func:`fold`).
num_chunks: number of LMCache chunks in the request.
num_ranks: number of kv_rank shards per chunk.
group_windows: per-object-group cross-chunk window sizes.
Returns:
``(hit_length, retain_mask)`` over the same ranked layout as ``found``.
"""
servable = fold(found, num_chunks, num_ranks, group_windows)
# fold's bits are chunk-indexed (bit j == prefix length j + 1), so the hit
# length is the highest set bit plus one; -1 (no servable prefix) -> 0.
hit_length = highest_set_bit(servable) + 1
return hit_length, unfold(hit_length, num_chunks, num_ranks, group_windows)
def fold_unfold(
found: Bitmap,
num_chunks: int,
group_windows: Sequence[int],
) -> tuple[int, Bitmap]:
""":func:`fold_unfold_ranked` for the single-rank (group-major) layout.
Args:
found: group-major presence bitmap of length
``len(group_windows) * num_chunks``; bit ``g * num_chunks + j`` set
iff chunk ``j`` is available for object group ``g``.
num_chunks: number of LMCache chunks in the request.
group_windows: per-object-group cross-chunk window sizes.
Returns:
``(hit_length, retain_mask)`` over the group-major layout.
"""
return fold_unfold_ranked(found, num_chunks, 1, group_windows)
def _fold_python(
found: Bitmap,
num_chunks: int,
num_ranks: int,
group_windows: Sequence[int],
) -> Bitmap:
"""Reference (pure-Python) implementation of :func:`fold`, for testing only.
Kept as the oracle the native :func:`fold` is validated against in the test
suite; it is not used at runtime. Behavior matches :func:`fold`.
Args:
found: Group-major / chunk-major / rank-minor presence bitmap.
num_chunks: Number of LMCache chunks in the request.
num_ranks: Number of kv_rank shards per chunk.
group_windows: Per-object-group cross-chunk window size in chunks;
``<= 0`` means full attention.
Returns:
A bitmap of size ``num_chunks``; bit ``j`` set iff every group can
serve a length-``j + 1`` prefix.
"""
group_stride = num_chunks * num_ranks
# For each group, ``run`` is the count of consecutive present chunks ending
# at the current chunk, so a length-L prefix is servable iff the last
# ``min(window, L)`` chunks are present. ``servable[j]`` (bit ``j``, prefix
# length ``j + 1``) stays True only if every group can serve that length.
servable = [True] * num_chunks
for group_idx, window in enumerate(group_windows):
gbase = group_idx * group_stride
effective_window = num_chunks if window <= 0 else window
run = 0
for prefix_len in range(1, num_chunks + 1):
cbase = gbase + (prefix_len - 1) * num_ranks
present = all(found.test(cbase + r) for r in range(num_ranks))
run = run + 1 if present else 0
if servable[prefix_len - 1] and run < min(effective_window, prefix_len):
servable[prefix_len - 1] = False
servable_lengths = Bitmap(num_chunks)
for j in range(num_chunks):
if servable[j]:
servable_lengths.set(j)
return servable_lengths
def _unfold_python(
hit_length: int,
num_chunks: int,
num_ranks: int,
group_windows: Sequence[int],
) -> Bitmap:
"""Reference (pure-Python) implementation of :func:`unfold`, for testing only.
Kept as the oracle the native :func:`unfold` is validated against in the
test suite; it is not used at runtime. Behavior matches :func:`unfold`.
Args:
hit_length: Model-wide prefix hit length in chunks (clamped to
``num_chunks``).
num_chunks: Number of LMCache chunks in the request.
num_ranks: Number of kv_rank shards per chunk.
group_windows: Per-object-group cross-chunk window size in chunks;
``<= 0`` means full attention.
Returns:
Retain mask of length ``len(group_windows) * num_chunks * num_ranks``.
"""
hit_length = min(hit_length, num_chunks)
num_groups = len(group_windows)
group_stride = num_chunks * num_ranks
retain_mask = Bitmap(num_groups * group_stride)
if hit_length <= 0:
return retain_mask
for group_idx, window in enumerate(group_windows):
lo, hi = unfold_range(hit_length, window)
gbase = group_idx * group_stride
for j in range(lo, hi):
cbase = gbase + j * num_ranks
for r in range(num_ranks):
retain_mask.set(cbase + r)
return retain_mask
def merge_bitmaps(bitmaps: Iterable[Bitmap], num_keys: int) -> Bitmap:
"""Merge bitmaps with a bitwise OR into a ``num_keys``-sized bitmap.
Always returns a ``num_keys``-sized bitmap (empty input -> all zeros), so
downstream ``&`` operations never hit a size mismatch.
Args:
bitmaps: Per-source presence bitmaps to union.
num_keys: Size of the merged bitmap.
Returns:
The bitwise-OR of all inputs as a ``num_keys``-sized bitmap.
"""
merged = Bitmap(num_keys)
for bm in bitmaps:
merged = merged | bm
return merged
def select_retained(
found: Bitmap,
num_keys: int,
policy: TrimPolicy = TrimPolicy.PREFIX,
) -> Bitmap:
"""Select the retained subset of ``found`` for the non-windowed selections.
``PREFIX`` (LONGEST) keeps the leading contiguous run and drops everything
from the first gap on; any other policy keeps every set bit, gaps included.
The windowed hybrid fold is handled by :func:`fold_unfold_ranked`, not here.
Args:
found: Bitmap of found keys, over key indices ``0..num_keys-1``.
num_keys: Total number of requested keys.
policy: Selection to apply (see :class:`TrimPolicy`).
Returns:
Bitmap of the retained key indices.
"""
if policy is TrimPolicy.PREFIX:
return Bitmap(num_keys, found.count_leading_ones())
return found