# SPDX-License-Identifier: Apache-2.0 """On-disk encoded KV format and metadata.""" # Future from __future__ import annotations # Standard from dataclasses import dataclass, field from enum import IntEnum from typing import Optional, Tuple, Union import struct import zlib # Third Party import torch # First Party from lmcache.v1.kv_codec.errors import ( CorruptEncodedKVError, UnsupportedConfigError, ) # Magic prefix used to identify a v1 EncodedKV blob. Chosen so # `head -c 8 file.bin` is human-readable. Do not change this value; # every reader on every node identifies blobs by this byte sequence. CODEC_MAGIC: bytes = b"LMCKV\x01\x00\x01" # "LMCKV", version major.minor.patch CODEC_MAGIC_LEN: int = len(CODEC_MAGIC) ASSERT_MAGIC_BYTES = 8 assert CODEC_MAGIC_LEN == ASSERT_MAGIC_BYTES, "magic must be 8 bytes" class CodecVersion(IntEnum): """Bumped when the on-disk header format changes incompatibly. The minor/patch bytes inside CODEC_MAGIC are reserved for future backwards-compatible additions (a v2 reader can read v1 blobs by interpreting unknown trailing fields as zero/None). """ V1 = 1 class ScaleScope(IntEnum): """How V scales are organized in the encoded blob. `per_tensor` one FP32 scalar for the entire V tensor. `per_layer_head` one FP32 scalar per head, applied to all pages of that layer. `per_page_head` one FP32 scalar per (page, head). Recommended default for asymmetric storage because paged KV chunks do not naturally respect "whole layer tensor" assumptions; the overhead is tiny vs V-byte volume. `external` scales not stored in this blob; expected to be attached out-of-band by the caller. """ PER_TENSOR = 0 PER_LAYER_HEAD = 1 PER_PAGE_HEAD = 2 EXTERNAL = 3 # Header packing: little-endian throughout regardless of host. # # 8 B magic ("LMCKV" + 3-byte version triplet) # 2 B header version (CodecVersion) # 2 B scale_scope (ScaleScope) # 2 B k_dtype_id (DTYPE_TO_INT) # 2 B v_dtype_id (DTYPE_TO_INT) # 2 B scale_dtype_id (DTYPE_TO_INT, typically float32 or float16) # 2 B reserved (zero) # 8 B layer_id (int64; signed sentinel value -1 means unset) # 8 B chunk_id (int64; -1 unset) # 8 B chunk_size (int64; tokens per chunk for paged caches) # 8 B page_size (int64; tokens per page) # 8 B kv_head_count (int64) # 8 B head_dim (int64) # 8 B scale_shape_n (int64; number of dimensions in scale tensor) # N×8B scale_shape values # 8 B k_payload_len (int64, bytes) # 8 B v_payload_len (int64, bytes) # 8 B scale_payload_len (int64, bytes) # 2 B hash_strs_n (number of (key, value) string pairs that follow) # For each str pair: 2-byte key_len, key, 2-byte val_len, val. # Currently used keys: model_id, model_revision_hash, # tokenizer_hash, rope_config_hash, attention_backend, kv_layout. # 4 B payload_crc32c (CRC of the K + V + scales payload) # # Followed by: K_payload (k_payload_len bytes), V_payload, scales. # # Field order is fixed; new fields added in V2+ append after CRC. _FIXED_HEADER_FMT = "<8sHHHHHH" + "qqqqqqq" # magic + 6 shorts + 7 int64 _FIXED_HEADER_LEN = struct.calcsize(_FIXED_HEADER_FMT) # 8 + 2*6 + 8*7 = 76 bytes assert _FIXED_HEADER_LEN == 76, _FIXED_HEADER_LEN def _pack_str(s: str) -> bytes: if s is None: s = "" encoded = s.encode("utf-8") if len(encoded) > 0xFFFF: raise UnsupportedConfigError( f"hash string longer than 65535 bytes: {len(encoded)}" ) return struct.pack(" Tuple[str, int]: if off + 2 > len(buf): raise CorruptEncodedKVError(f"truncated string-length field at offset {off}") (length,) = struct.unpack_from(" len(buf): raise CorruptEncodedKVError( f"string at offset {off} declares length {length} but only " f"{len(buf) - off} bytes remain" ) try: s = bytes(buf[off : off + length]).decode("utf-8") except UnicodeDecodeError as e: raise CorruptEncodedKVError( f"non-utf-8 bytes in string field at offset {off}: {e}" ) from None return s, off + length @dataclass class CodecHashes: """Identifying hashes that gate cross-config cache poisoning. Empty strings mean "not provided"; serialization treats empty string and None identically. Any non-empty mismatch on read raises `CodecMismatchError` (see codec.AsymK16V8Codec.decode). """ model_id: str = "" model_revision_hash: str = "" tokenizer_hash: str = "" rope_config_hash: str = "" attention_backend: str = "" kv_layout: str = "" # Fields are checked in this order during decode mismatch detection. _CHECK_ORDER = ( "model_id", "model_revision_hash", "tokenizer_hash", "rope_config_hash", "attention_backend", "kv_layout", ) @dataclass class EncodedKV: """A self-describing encoded KV blob. `payload` carries `K_bytes ‖ V_bytes ‖ scale_bytes` in that order. Lengths are in `k_payload_len`, `v_payload_len`, `scale_payload_len`. All numeric fields are little-endian on disk regardless of host. """ # Logical shape/dtype information (same fields as MemoryObjMetadata.dtypes/shapes) k_dtype: torch.dtype v_dtype: torch.dtype scale_dtype: torch.dtype = torch.float32 scale_scope: ScaleScope = ScaleScope.PER_PAGE_HEAD # Identity / cache-poisoning gates hashes: CodecHashes = field(default_factory=CodecHashes) # Layout / shape parameters (-1 = unset) layer_id: int = -1 chunk_id: int = -1 chunk_size: int = -1 page_size: int = -1 kv_head_count: int = -1 head_dim: int = -1 # Scale tensor shape; e.g., () for per_tensor, (kv_head_count,) # for per_layer_head, (n_pages, kv_head_count) for per_page_head. scale_shape: Tuple[int, ...] = () # Payload byte counts k_payload_len: int = 0 v_payload_len: int = 0 scale_payload_len: int = 0 # The actual encoded byte blob (header + payload). When the # EncodedKV is freshly assembled from tensors, callers populate # `payload` (the K+V+scale bytes) and `header_bytes` is computed # at serialize time. When parsed from a buffer via # ``deserialize_header``, ``payload`` is a memoryview slice over # the source buffer to avoid a full payload copy on the read # path. payload: Union[bytes, memoryview] = b"" header_bytes: Optional[bytes] = None @property def total_bytes(self) -> int: """Total encoded size: header + payload.""" if self.header_bytes is None: return -1 return len(self.header_bytes) + len(self.payload) def expected_payload_len(self) -> int: return self.k_payload_len + self.v_payload_len + self.scale_payload_len def _dtype_to_int(dtype: Optional[torch.dtype]) -> int: """Local re-export of the protocol DTYPE_TO_INT to avoid a circular import at module-load time.""" # First Party from lmcache.v1.protocol import DTYPE_TO_INT if dtype not in DTYPE_TO_INT: raise UnsupportedConfigError( f"dtype {dtype} not in protocol DTYPE_TO_INT mapping" ) return DTYPE_TO_INT[dtype] def _int_to_dtype(idx: int) -> Optional[torch.dtype]: # First Party from lmcache.v1.protocol import INT_TO_DTYPE if idx not in INT_TO_DTYPE: raise CorruptEncodedKVError(f"unknown dtype index {idx} in encoded header") return INT_TO_DTYPE[idx] def serialize_header(enc: EncodedKV) -> bytes: """Pack the EncodedKV header into bytes (excludes payload). Computes payload CRC32 from `enc.payload` and writes it as the last field. Caller is responsible for ensuring `enc.payload` matches `expected_payload_len()`. """ if len(enc.payload) != enc.expected_payload_len(): raise UnsupportedConfigError( f"payload length {len(enc.payload)} does not match " f"declared K({enc.k_payload_len}) + V({enc.v_payload_len}) " f"+ scales({enc.scale_payload_len})" ) fixed = struct.pack( _FIXED_HEADER_FMT, CODEC_MAGIC, int(CodecVersion.V1), int(enc.scale_scope), _dtype_to_int(enc.k_dtype), _dtype_to_int(enc.v_dtype), _dtype_to_int(enc.scale_dtype), 0, # reserved enc.layer_id, enc.chunk_id, enc.chunk_size, enc.page_size, enc.kv_head_count, enc.head_dim, len(enc.scale_shape), ) scale_shape_bytes = struct.pack(f"<{len(enc.scale_shape)}q", *enc.scale_shape) payload_lens = struct.pack( " EncodedKV: """Parse an EncodedKV header from a contiguous byte buffer. The buffer must contain header + payload. Returns an EncodedKV with `header_bytes` and `payload` both populated. Raises: CorruptEncodedKVError on magic/version/CRC mismatch or any truncated field. """ mv = memoryview(buf) if len(mv) < _FIXED_HEADER_LEN: raise CorruptEncodedKVError( f"buffer too short for fixed header: {len(mv)} < {_FIXED_HEADER_LEN}" ) fixed = struct.unpack_from(_FIXED_HEADER_FMT, mv, 0) ( magic, version, scale_scope, k_dtype_id, v_dtype_id, scale_dtype_id, _reserved, layer_id, chunk_id, chunk_size, page_size, kv_head_count, head_dim, scale_shape_n, ) = fixed if magic != CODEC_MAGIC: raise CorruptEncodedKVError( f"bad magic: got {magic!r}, expected {CODEC_MAGIC!r}" ) if version != int(CodecVersion.V1): raise CorruptEncodedKVError( f"unsupported codec version {version}; this build supports " f"{int(CodecVersion.V1)}" ) if scale_shape_n < 0 or scale_shape_n > 8: raise CorruptEncodedKVError(f"implausible scale_shape_n: {scale_shape_n}") off = _FIXED_HEADER_LEN if off + 8 * scale_shape_n > len(mv): raise CorruptEncodedKVError( f"buffer too short for scale_shape: need {8 * scale_shape_n} bytes" ) scale_shape = tuple(struct.unpack_from(f"<{scale_shape_n}q", mv, off)) off += 8 * scale_shape_n if off + 24 > len(mv): raise CorruptEncodedKVError("truncated payload-lengths field") k_len, v_len, s_len = struct.unpack_from(" len(mv): raise CorruptEncodedKVError("truncated hashes-count field") (n_hashes,) = struct.unpack_from(" len(mv): raise CorruptEncodedKVError("truncated CRC field") (crc_declared,) = struct.unpack_from(" len(mv): raise CorruptEncodedKVError( f"buffer truncated mid-payload: header ends at {off}, " f"declared payload = {expected_payload}, buffer total " f"= {len(mv)}" ) payload = mv[off : off + expected_payload] crc_computed = zlib.crc32(payload) & 0xFFFFFFFF if crc_computed != crc_declared: raise CorruptEncodedKVError( f"payload CRC mismatch: declared {crc_declared:#x}, " f"computed {crc_computed:#x}" ) try: scope_enum = ScaleScope(scale_scope) except ValueError: raise CorruptEncodedKVError( f"unknown scale_scope index {scale_scope} in encoded header" ) from None enc = EncodedKV( k_dtype=_int_to_dtype(k_dtype_id), v_dtype=_int_to_dtype(v_dtype_id), scale_dtype=_int_to_dtype(scale_dtype_id), scale_scope=scope_enum, hashes=hashes, layer_id=layer_id, chunk_id=chunk_id, chunk_size=chunk_size, page_size=page_size, kv_head_count=kv_head_count, head_dim=head_dim, scale_shape=scale_shape, k_payload_len=k_len, v_payload_len=v_len, scale_payload_len=s_len, payload=payload, header_bytes=bytes(mv[:off]), ) return enc