# 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