项目文件夹

文件
2026-07-13 12:24:33 +08:00

404 行
13 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# 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("<H", len(encoded)) + encoded
def _unpack_str(buf: memoryview, off: int) -> Tuple[str, int]:
if off + 2 > len(buf):
raise CorruptEncodedKVError(f"truncated string-length field at offset {off}")
(length,) = struct.unpack_from("<H", buf, off)
off += 2
if off + length > 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(
"<qqq", enc.k_payload_len, enc.v_payload_len, enc.scale_payload_len
)
# Hash strings
hash_keys = list(CodecHashes._CHECK_ORDER)
hash_blob = struct.pack("<H", len(hash_keys))
for k in hash_keys:
hash_blob += _pack_str(k) + _pack_str(getattr(enc.hashes, k))
# CRC over the actual payload bytes (the K+V+scales blob), not
# over the header itself. zlib.crc32 is CRC32/IEEE; if a future
# reader needs CRC32C specifically we bump CodecVersion.
crc = zlib.crc32(enc.payload) & 0xFFFFFFFF
crc_bytes = struct.pack("<I", crc)
return fixed + scale_shape_bytes + payload_lens + hash_blob + crc_bytes
def deserialize_header(buf: bytes) -> 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("<qqq", mv, off)
off += 24
if off + 2 > len(mv):
raise CorruptEncodedKVError("truncated hashes-count field")
(n_hashes,) = struct.unpack_from("<H", mv, off)
off += 2
hashes = CodecHashes()
expected_keys = set(CodecHashes._CHECK_ORDER)
for _ in range(n_hashes):
key, off = _unpack_str(mv, off)
val, off = _unpack_str(mv, off)
if key in expected_keys:
setattr(hashes, key, val)
if off + 4 > len(mv):
raise CorruptEncodedKVError("truncated CRC field")
(crc_declared,) = struct.unpack_from("<I", mv, off)
off += 4
expected_payload = k_len + v_len + s_len
if off + expected_payload > 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