lifeboatcache 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. lifeboat_cache/__init__.py +7 -0
  2. lifeboat_cache/compress/__init__.py +57 -0
  3. lifeboat_cache/compress/accuracy.py +53 -0
  4. lifeboat_cache/compress/base.py +229 -0
  5. lifeboat_cache/compress/calibration.py +81 -0
  6. lifeboat_cache/compress/demote.py +16 -0
  7. lifeboat_cache/compress/fp8.py +160 -0
  8. lifeboat_cache/compress/kvtc.py +140 -0
  9. lifeboat_cache/compress/live.py +284 -0
  10. lifeboat_cache/compress/oscar2.py +166 -0
  11. lifeboat_cache/compress/turboquant.py +131 -0
  12. lifeboat_cache/config/__init__.py +36 -0
  13. lifeboat_cache/config/cli.py +276 -0
  14. lifeboat_cache/config/loader.py +278 -0
  15. lifeboat_cache/config/paths.py +22 -0
  16. lifeboat_cache/config/schema.py +450 -0
  17. lifeboat_cache/control/__init__.py +54 -0
  18. lifeboat_cache/control/config_push.py +243 -0
  19. lifeboat_cache/control/controld.py +187 -0
  20. lifeboat_cache/control/membership.py +216 -0
  21. lifeboat_cache/control/placement.py +81 -0
  22. lifeboat_cache/control/router_export.py +87 -0
  23. lifeboat_cache/control/telemetry_agg.py +101 -0
  24. lifeboat_cache/core/__init__.py +59 -0
  25. lifeboat_cache/core/coalesce.py +100 -0
  26. lifeboat_cache/core/hashing.py +96 -0
  27. lifeboat_cache/core/header.py +158 -0
  28. lifeboat_cache/core/keys.py +57 -0
  29. lifeboat_cache/core/ring.py +52 -0
  30. lifeboat_cache/dashboard/__init__.py +334 -0
  31. lifeboat_cache/dashboard/static/index.html +582 -0
  32. lifeboat_cache/device/__init__.py +48 -0
  33. lifeboat_cache/device/base.py +75 -0
  34. lifeboat_cache/device/cpu.py +15 -0
  35. lifeboat_cache/device/cuda.py +51 -0
  36. lifeboat_cache/errors.py +31 -0
  37. lifeboat_cache/index/__init__.py +67 -0
  38. lifeboat_cache/index/backends.py +207 -0
  39. lifeboat_cache/index/entry.py +35 -0
  40. lifeboat_cache/index/hashring.py +104 -0
  41. lifeboat_cache/index/local.py +136 -0
  42. lifeboat_cache/index/sharded.py +101 -0
  43. lifeboat_cache/integrations/__init__.py +1 -0
  44. lifeboat_cache/integrations/host.py +104 -0
  45. lifeboat_cache/integrations/k8s/__init__.py +76 -0
  46. lifeboat_cache/integrations/lifeboat_store.py +386 -0
  47. lifeboat_cache/integrations/lmcache_adapter.py +395 -0
  48. lifeboat_cache/integrations/lmcache_defaults.py +54 -0
  49. lifeboat_cache/integrations/policies.py +39 -0
  50. lifeboat_cache/integrations/registration.py +60 -0
  51. lifeboat_cache/integrations/serde.py +109 -0
  52. lifeboat_cache/integrations/sglang_backend.py +187 -0
  53. lifeboat_cache/integrations/sglang_plugin.py +110 -0
  54. lifeboat_cache/integrations/vllm_connector.py +50 -0
  55. lifeboat_cache/integrations/vllm_cpu_patches.py +116 -0
  56. lifeboat_cache/integrations/vllm_plugin.py +80 -0
  57. lifeboat_cache/policy/__init__.py +98 -0
  58. lifeboat_cache/policy/accuracy_guard.py +105 -0
  59. lifeboat_cache/policy/admission.py +60 -0
  60. lifeboat_cache/policy/base.py +68 -0
  61. lifeboat_cache/policy/classic.py +205 -0
  62. lifeboat_cache/policy/episodic.py +289 -0
  63. lifeboat_cache/policy/predictive.py +89 -0
  64. lifeboat_cache/policy/reuse_score.py +100 -0
  65. lifeboat_cache/presets/base.yaml +16 -0
  66. lifeboat_cache/presets/dev_cpu.yaml +22 -0
  67. lifeboat_cache/presets/fleet_50k.yaml +53 -0
  68. lifeboat_cache/presets/max_accuracy.yaml +15 -0
  69. lifeboat_cache/presets/max_concurrency.yaml +15 -0
  70. lifeboat_cache/presets/single_node_gpu.yaml +18 -0
  71. lifeboat_cache/presets/storage_efficient.yaml +13 -0
  72. lifeboat_cache/registry.py +114 -0
  73. lifeboat_cache/session/__init__.py +142 -0
  74. lifeboat_cache/sim/__init__.py +110 -0
  75. lifeboat_cache/sim/fleet.py +148 -0
  76. lifeboat_cache/sim/trace.py +78 -0
  77. lifeboat_cache/storage/__init__.py +198 -0
  78. lifeboat_cache/storage/backpressure.py +89 -0
  79. lifeboat_cache/storage/circuit_breaker.py +89 -0
  80. lifeboat_cache/storage/coalesced.py +63 -0
  81. lifeboat_cache/storage/doca_memos.py +82 -0
  82. lifeboat_cache/storage/filesystem.py +177 -0
  83. lifeboat_cache/storage/manager.py +238 -0
  84. lifeboat_cache/storage/memory.py +42 -0
  85. lifeboat_cache/storage/ontap.py +385 -0
  86. lifeboat_cache/storage/rdma_nixl.py +100 -0
  87. lifeboat_cache/storage/s3.py +181 -0
  88. lifeboat_cache/storage/tier.py +75 -0
  89. lifeboat_cache/telemetry/__init__.py +89 -0
  90. lifeboat_cache/telemetry/metrics.py +86 -0
  91. lifeboat_cache/tenancy/__init__.py +98 -0
  92. lifeboat_cache/tenancy/qos.py +76 -0
  93. lifeboat_cache/tenancy/quota.py +73 -0
  94. lifeboatcache-0.1.0.dist-info/METADATA +121 -0
  95. lifeboatcache-0.1.0.dist-info/RECORD +97 -0
  96. lifeboatcache-0.1.0.dist-info/WHEEL +4 -0
  97. lifeboatcache-0.1.0.dist-info/entry_points.txt +9 -0
@@ -0,0 +1,7 @@
1
+ """Lifeboat Cache — commercial LMCache plugin (L2 storage, serde, policies).
2
+
3
+ See the project brief / docs/ for architecture. This package is import-safe on a
4
+ CPU-only Mac (the "fleet of size one") and on GPU fleets alike.
5
+ """
6
+
7
+ __version__ = "0.0.0"
@@ -0,0 +1,57 @@
1
+ """CPU reference compression layer for Lifeboat Cache.
2
+
3
+ A set of KV-cache codecs behind one :class:`Codec` interface, driven by the
4
+ ``compression`` config section. Every codec is pure-numpy (torch optional) and produces a
5
+ **self-describing blob** so ``decode`` needs no external arguments.
6
+
7
+ Codecs (each approximates a published method; see the module docstrings for exactly what is
8
+ reproduced vs. approximated):
9
+
10
+ * ``turboquant`` — group-wise affine int3/int4 with float16-kept layers.
11
+ * ``fp8`` — e4m3 / e5m2 emulation via an exact FP8 value grid.
12
+ * ``oscar`` — 2-bit middle + full-precision sink/recent tokens + optional rotation.
13
+ * ``kvtc`` — PCA + quantized coefficients/residual + zlib (ANS stand-in).
14
+
15
+ Importing this package registers all four codec factories, so
16
+ :func:`build_codecs` can instantiate any ladder from config.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from .accuracy import cosine, logit_kl, mse, tensor_relative_error
21
+ from .base import (
22
+ Codec,
23
+ build_codecs,
24
+ create_codec,
25
+ pack_arrays,
26
+ pack_blob,
27
+ register_codec,
28
+ unpack_arrays,
29
+ unpack_blob,
30
+ )
31
+ from .calibration import CalibrationProfile
32
+ from .demote import recompress
33
+ from .fp8 import FP8Codec
34
+ from .kvtc import KVTCCodec
35
+ from .oscar2 import Oscar2Codec
36
+ from .turboquant import TurboQuantCodec
37
+
38
+ __all__ = [
39
+ "Codec",
40
+ "register_codec",
41
+ "create_codec",
42
+ "build_codecs",
43
+ "pack_blob",
44
+ "unpack_blob",
45
+ "pack_arrays",
46
+ "unpack_arrays",
47
+ "TurboQuantCodec",
48
+ "FP8Codec",
49
+ "Oscar2Codec",
50
+ "KVTCCodec",
51
+ "CalibrationProfile",
52
+ "recompress",
53
+ "logit_kl",
54
+ "cosine",
55
+ "mse",
56
+ "tensor_relative_error",
57
+ ]
@@ -0,0 +1,53 @@
1
+ """Accuracy metrics for the compression accuracy-guard.
2
+
3
+ The guard samples a fraction of writes, decodes them, and compares against the original to
4
+ decide whether a codec is degrading quality (config ``compression.accuracy_guard.metric``).
5
+ The headline metric is softmax KL on logits — the distributional distance that actually
6
+ matters for generation quality — plus cheaper tensor-space helpers.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import numpy as np
11
+
12
+ _EPS = 1e-12
13
+
14
+
15
+ def _log_softmax(z: np.ndarray) -> np.ndarray:
16
+ z = z - z.max(axis=-1, keepdims=True)
17
+ return z - np.log(np.exp(z).sum(axis=-1, keepdims=True) + _EPS)
18
+
19
+
20
+ def logit_kl(p_logits: np.ndarray, q_logits: np.ndarray) -> float:
21
+ """Mean KL(softmax(p) || softmax(q)) over the last axis.
22
+
23
+ Zero iff the two logit tensors induce the same distribution (identical logits, or logits
24
+ differing only by a per-row additive constant), strictly positive otherwise. Computed in
25
+ log-space for numerical stability.
26
+ """
27
+ lp = _log_softmax(np.asarray(p_logits, dtype=np.float64))
28
+ lq = _log_softmax(np.asarray(q_logits, dtype=np.float64))
29
+ p = np.exp(lp)
30
+ kl = (p * (lp - lq)).sum(axis=-1)
31
+ return float(np.maximum(kl, 0.0).mean())
32
+
33
+
34
+ def cosine(a: np.ndarray, b: np.ndarray) -> float:
35
+ """Mean cosine similarity in [-1, 1] over flattened vectors (1.0 == identical)."""
36
+ a = np.asarray(a, dtype=np.float64).reshape(-1)
37
+ b = np.asarray(b, dtype=np.float64).reshape(-1)
38
+ denom = (np.linalg.norm(a) * np.linalg.norm(b)) + _EPS
39
+ return float(np.dot(a, b) / denom)
40
+
41
+
42
+ def mse(a: np.ndarray, b: np.ndarray) -> float:
43
+ """Mean squared error between two arrays."""
44
+ a = np.asarray(a, dtype=np.float64)
45
+ b = np.asarray(b, dtype=np.float64)
46
+ return float(np.mean((a - b) ** 2))
47
+
48
+
49
+ def tensor_relative_error(a: np.ndarray, b: np.ndarray) -> float:
50
+ """Relative reconstruction error ``||a - b|| / (||a|| + eps)`` (Frobenius norm)."""
51
+ a = np.asarray(a, dtype=np.float64)
52
+ b = np.asarray(b, dtype=np.float64)
53
+ return float(np.linalg.norm(a - b) / (np.linalg.norm(a) + _EPS))
@@ -0,0 +1,229 @@
1
+ """Codec base class, self-describing blob format, and the codec registry.
2
+
3
+ Every codec in this package is a *CPU reference* implementation: it uses only numpy so
4
+ the whole compression layer runs on a laptop with no CUDA. The on-GPU production codecs
5
+ (real TurboQuant / OSCAR / KVTC kernels) would plug in behind the same :class:`Codec`
6
+ interface — these reference versions exist to validate the format, the round-trip math,
7
+ and the ladder/demote plumbing.
8
+
9
+ Blob self-description
10
+ ---------------------
11
+ ``decode`` takes *only* the blob — no shape, dtype, or param arguments. We achieve that
12
+ with a tiny length-prefixed JSON header followed by a raw binary payload::
13
+
14
+ u32 little-endian json_len
15
+ json_len bytes UTF-8 JSON header (scalar params + an ``_arrays`` manifest)
16
+ rest concatenated raw array bytes (order = manifest order)
17
+
18
+ The ``_arrays`` manifest records each stored numpy array's ``dtype`` and ``shape`` so the
19
+ payload can be sliced back apart deterministically (dicts preserve insertion order, and
20
+ JSON preserves it too). This keeps each codec's serialization declarative: build a dict of
21
+ numpy arrays + a dict of scalar params, and the helpers do the rest.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import struct
27
+ from abc import ABC, abstractmethod
28
+ from typing import TYPE_CHECKING, Any, Callable
29
+
30
+ import numpy as np
31
+
32
+ if TYPE_CHECKING: # avoid a hard import cycle; only needed for type hints
33
+ from .calibration import CalibrationProfile
34
+
35
+
36
+ # --------------------------------------------------------------------------- #
37
+ # blob (de)serialization
38
+ # --------------------------------------------------------------------------- #
39
+ def pack_blob(header: dict[str, Any], payload: bytes) -> bytes:
40
+ """Serialize a JSON ``header`` + raw ``payload`` into one self-describing blob."""
41
+ hb = json.dumps(header, separators=(",", ":")).encode("utf-8")
42
+ return struct.pack("<I", len(hb)) + hb + payload
43
+
44
+
45
+ def unpack_blob(blob: bytes) -> tuple[dict[str, Any], bytes]:
46
+ """Inverse of :func:`pack_blob` — return (header dict, payload bytes)."""
47
+ (n,) = struct.unpack_from("<I", blob, 0)
48
+ header = json.loads(blob[4 : 4 + n].decode("utf-8"))
49
+ return header, blob[4 + n :]
50
+
51
+
52
+ def pack_arrays(arrays: dict[str, np.ndarray]) -> tuple[dict[str, Any], bytes]:
53
+ """Concatenate ``arrays`` to bytes and return a manifest describing each one.
54
+
55
+ The manifest (dtype + shape per key) goes in the JSON header; the bytes become the
56
+ payload. Order is preserved so :func:`unpack_arrays` can slice them back out.
57
+ """
58
+ meta: dict[str, Any] = {}
59
+ parts: list[bytes] = []
60
+ for key, arr in arrays.items():
61
+ a = np.ascontiguousarray(arr)
62
+ meta[key] = {"dtype": str(a.dtype), "shape": list(a.shape)}
63
+ parts.append(a.tobytes())
64
+ return meta, b"".join(parts)
65
+
66
+
67
+ def unpack_arrays(meta: dict[str, Any], payload: bytes) -> dict[str, np.ndarray]:
68
+ """Inverse of :func:`pack_arrays` using the manifest produced there."""
69
+ out: dict[str, np.ndarray] = {}
70
+ off = 0
71
+ for key, m in meta.items():
72
+ dt = np.dtype(m["dtype"])
73
+ shape = tuple(int(s) for s in m["shape"])
74
+ nbytes = int(np.prod(shape)) * dt.itemsize if shape else dt.itemsize
75
+ buf = payload[off : off + nbytes]
76
+ out[key] = np.frombuffer(buf, dtype=dt).reshape(shape).copy()
77
+ off += nbytes
78
+ return out
79
+
80
+
81
+ # --------------------------------------------------------------------------- #
82
+ # bit packing (for sub-byte integer codes)
83
+ # --------------------------------------------------------------------------- #
84
+ def pack_bits(codes: np.ndarray, bits: int) -> np.ndarray:
85
+ """Pack a 1-D array of small unsigned ints (0..2**bits-1) into a dense bit stream.
86
+
87
+ Each code contributes ``bits`` bits (LSB-first within the code); the stream is then
88
+ byte-packed with :func:`numpy.packbits`. Returned as a uint8 array so it can be stored
89
+ directly via :func:`pack_arrays`.
90
+ """
91
+ codes = codes.astype(np.uint8).reshape(-1)
92
+ if bits == 8:
93
+ return codes.copy()
94
+ # (N, bits) matrix: column j is bit j of each code.
95
+ shifts = np.arange(bits, dtype=np.uint8)
96
+ bit_mat = ((codes[:, None] >> shifts) & 1).astype(np.uint8)
97
+ return np.packbits(bit_mat.reshape(-1))
98
+
99
+
100
+ def unpack_bits(packed: np.ndarray, n: int, bits: int) -> np.ndarray:
101
+ """Inverse of :func:`pack_bits`: recover ``n`` codes each of width ``bits``."""
102
+ packed = np.asarray(packed, dtype=np.uint8)
103
+ if bits == 8:
104
+ return packed[:n].astype(np.uint8).copy()
105
+ flat = np.unpackbits(packed)[: n * bits].reshape(n, bits)
106
+ weights = (1 << np.arange(bits)).astype(np.uint16)
107
+ return (flat.astype(np.uint16) * weights).sum(axis=1).astype(np.uint8)
108
+
109
+
110
+ # --------------------------------------------------------------------------- #
111
+ # group-wise affine integer quantization (shared by turboquant / oscar / kvtc)
112
+ # --------------------------------------------------------------------------- #
113
+ def quantize_rows(x: np.ndarray, bits: int, group_size: int) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
114
+ """Per-group affine quantize a 2-D array along its last axis.
115
+
116
+ For each row and each contiguous group of ``group_size`` columns we store a scale and
117
+ a zero-point (the group minimum): ``q = round((x - min) / scale)`` clipped to
118
+ ``[0, 2**bits - 1]`` with ``scale = (max - min) / (2**bits - 1)``.
119
+
120
+ Because every value lies within its group's ``[min, max]``, no clipping is ever needed
121
+ and round-to-nearest bounds the reconstruction error by ``scale / 2`` per group — the
122
+ property the tests assert. A constant group gets ``scale = 1`` and quantizes exactly.
123
+
124
+ Returns ``(codes, scales, zeros)`` with ``codes`` shape ``(rows, D)`` uint8 and
125
+ ``scales``/``zeros`` shape ``(rows, n_groups)`` float32.
126
+ """
127
+ rows, D = x.shape
128
+ qmax = (1 << bits) - 1
129
+ n_groups = (D + group_size - 1) // group_size
130
+ codes = np.empty((rows, D), dtype=np.uint8)
131
+ scales = np.empty((rows, n_groups), dtype=np.float32)
132
+ zeros = np.empty((rows, n_groups), dtype=np.float32)
133
+ for g in range(n_groups):
134
+ s, e = g * group_size, min((g + 1) * group_size, D)
135
+ blk = x[:, s:e]
136
+ mn = blk.min(axis=1)
137
+ mx = blk.max(axis=1)
138
+ scale = (mx - mn) / qmax
139
+ # Constant group -> scale 0; set to 1 so (x-min)/scale == 0 and dequant is exact.
140
+ scale = np.where(scale > 0, scale, 1.0).astype(np.float32)
141
+ q = np.rint((blk - mn[:, None]) / scale[:, None])
142
+ codes[:, s:e] = np.clip(q, 0, qmax).astype(np.uint8)
143
+ scales[:, g] = scale
144
+ zeros[:, g] = mn.astype(np.float32)
145
+ return codes, scales, zeros
146
+
147
+
148
+ def dequantize_rows(codes: np.ndarray, scales: np.ndarray, zeros: np.ndarray, group_size: int) -> np.ndarray:
149
+ """Inverse of :func:`quantize_rows`."""
150
+ rows, D = codes.shape
151
+ n_groups = scales.shape[1]
152
+ out = np.empty((rows, D), dtype=np.float32)
153
+ for g in range(n_groups):
154
+ s, e = g * group_size, min((g + 1) * group_size, D)
155
+ out[:, s:e] = codes[:, s:e].astype(np.float32) * scales[:, g : g + 1] + zeros[:, g : g + 1]
156
+ return out
157
+
158
+
159
+ def orthogonal_matrix(dim: int, seed: int) -> np.ndarray:
160
+ """Deterministic random orthogonal matrix via QR (used for OSCAR rotations).
161
+
162
+ The sign of ``Q`` is pinned by the diagonal of ``R`` so the factorization is unique
163
+ and reproducible from ``seed`` alone — encode and decode regenerate the same matrix
164
+ with no need to store it.
165
+ """
166
+ rng = np.random.default_rng(seed)
167
+ a = rng.standard_normal((dim, dim))
168
+ q, r = np.linalg.qr(a)
169
+ q *= np.sign(np.diag(r))
170
+ return q.astype(np.float32)
171
+
172
+
173
+ # --------------------------------------------------------------------------- #
174
+ # Codec interface
175
+ # --------------------------------------------------------------------------- #
176
+ class Codec(ABC):
177
+ """A compression codec. Encode a numpy array to a self-describing blob and back.
178
+
179
+ ``decode`` must not need any external arguments — everything required to reconstruct
180
+ (shape, dtype, params, scales, bases, rotation seeds, ...) lives in the blob header.
181
+ """
182
+
183
+ #: Instance name (the key under which the codec appears in config, e.g. ``turboquant4``).
184
+ name: str = "codec"
185
+
186
+ @abstractmethod
187
+ def encode(self, arr: np.ndarray, *, layer_id: int = 0, profile: "CalibrationProfile | None" = None) -> bytes:
188
+ """Compress ``arr`` to a blob. ``layer_id``/``profile`` tune per-layer behavior."""
189
+
190
+ @abstractmethod
191
+ def decode(self, blob: bytes) -> np.ndarray:
192
+ """Reconstruct the array from a blob produced by :meth:`encode`."""
193
+
194
+
195
+ # --------------------------------------------------------------------------- #
196
+ # registry
197
+ # --------------------------------------------------------------------------- #
198
+ # Factories are keyed by codec *type* (``turboquant``/``oscar``/``kvtc``/``fp8``) — the
199
+ # ``Codec.type`` field in config. A factory takes a config object (anything exposing the
200
+ # codec params as attributes) and returns a ready Codec instance.
201
+ _REGISTRY: dict[str, Callable[[Any], Codec]] = {}
202
+
203
+
204
+ def register_codec(name: str, factory: Callable[[Any], Codec]) -> None:
205
+ """Register a codec factory under its type ``name`` (idempotent / overwriting)."""
206
+ _REGISTRY[name] = factory
207
+
208
+
209
+ def create_codec(name: str, codec_cfg: Any) -> Codec:
210
+ """Instantiate the codec registered under type ``name`` from ``codec_cfg``."""
211
+ try:
212
+ factory = _REGISTRY[name]
213
+ except KeyError as exc: # pragma: no cover - defensive
214
+ raise KeyError(f"no codec registered for type '{name}' (have {sorted(_REGISTRY)})") from exc
215
+ return factory(codec_cfg)
216
+
217
+
218
+ def build_codecs(compression_cfg: Any) -> dict[str, Codec]:
219
+ """Instantiate every codec named in ``compression_cfg.codecs``.
220
+
221
+ Returns a mapping of config key (e.g. ``turboquant4``) -> Codec, with each codec's
222
+ ``.name`` set to that key so blobs/telemetry can report which ladder entry produced them.
223
+ """
224
+ out: dict[str, Codec] = {}
225
+ for key, cfg in compression_cfg.codecs.items():
226
+ codec = create_codec(cfg.type, cfg)
227
+ codec.name = key
228
+ out[key] = codec
229
+ return out
@@ -0,0 +1,81 @@
1
+ """Per-model calibration profile: cached state shared across encode calls.
2
+
3
+ A :class:`CalibrationProfile` is fit once per model (from a handful of representative KV
4
+ tensors) and then handed to ``Codec.encode`` to tune per-layer behavior. The reference
5
+ codecs are mostly self-calibrating per blob, so this profile stays deliberately small; it
6
+ carries:
7
+
8
+ * ``num_layers`` — so TurboQuant can resolve negative ``keep_bf16_layers`` indices.
9
+ * ``state`` — a dict of numpy arrays (e.g. a cached KVTC PCA basis + mean, per-codec scales).
10
+ * ``meta`` — JSON-serializable scalars (e.g. a global scale).
11
+
12
+ Persistence uses ``.npz`` (arrays) with the JSON metadata embedded as a byte array in the
13
+ same file — no pickle, so a profile file is safe to load from untrusted locations.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from dataclasses import dataclass, field
19
+ from typing import Any, Sequence
20
+
21
+ import numpy as np
22
+
23
+ _META_KEY = "__meta__"
24
+ _STATE_PREFIX = "state::"
25
+
26
+
27
+ @dataclass
28
+ class CalibrationProfile:
29
+ num_layers: int = 0
30
+ model_hash: str = ""
31
+ state: dict[str, np.ndarray] = field(default_factory=dict)
32
+ meta: dict[str, Any] = field(default_factory=dict)
33
+
34
+ def fit(self, samples: "np.ndarray | Sequence[np.ndarray]") -> "CalibrationProfile":
35
+ """Fit the profile from one or more representative tensors.
36
+
37
+ Computes a global magnitude scale and a cached PCA basis (mean + right singular
38
+ vectors over the last-axis feature vectors) that a KVTC codec could reuse instead
39
+ of recomputing SVD per blob. ``num_layers`` is inferred from the first sample's
40
+ leading axis when not already set.
41
+ """
42
+ arrs = list(samples) if isinstance(samples, (list, tuple)) else [samples]
43
+ arrs = [np.asarray(a, dtype=np.float32) for a in arrs]
44
+ if not arrs:
45
+ return self
46
+ if not self.num_layers:
47
+ self.num_layers = int(arrs[0].shape[0])
48
+
49
+ D = arrs[0].shape[-1]
50
+ X = np.concatenate([a.reshape(-1, D) for a in arrs], axis=0)
51
+ self.meta["global_scale"] = float(np.abs(X).max())
52
+ mean = X.mean(axis=0)
53
+ Xc = X - mean
54
+ _, _, vt = np.linalg.svd(Xc, full_matrices=False)
55
+ self.state["kvtc_mean"] = mean.astype(np.float32)
56
+ self.state["kvtc_basis"] = vt.astype(np.float32)
57
+ return self
58
+
59
+ # -- persistence --
60
+ def save(self, path: str) -> None:
61
+ meta = {"num_layers": self.num_layers, "model_hash": self.model_hash, "meta": self.meta}
62
+ meta_bytes = np.frombuffer(json.dumps(meta).encode("utf-8"), dtype=np.uint8)
63
+ payload = {_META_KEY: meta_bytes}
64
+ payload.update({f"{_STATE_PREFIX}{k}": v for k, v in self.state.items()})
65
+ np.savez(path, **payload)
66
+
67
+ @classmethod
68
+ def load(cls, path: str) -> "CalibrationProfile":
69
+ with np.load(path, allow_pickle=False) as data:
70
+ meta = json.loads(bytes(data[_META_KEY]).decode("utf-8"))
71
+ state = {
72
+ k[len(_STATE_PREFIX) :]: np.array(data[k])
73
+ for k in data.files
74
+ if k.startswith(_STATE_PREFIX)
75
+ }
76
+ return cls(
77
+ num_layers=meta["num_layers"],
78
+ model_hash=meta["model_hash"],
79
+ state=state,
80
+ meta=meta["meta"],
81
+ )
@@ -0,0 +1,16 @@
1
+ """Recompress-on-demote: move a chunk from one codec to another.
2
+
3
+ When a chunk is demoted down the storage ladder (e.g. GPU ``turboquant4`` -> archive
4
+ ``kvtc``), it is re-encoded into the destination tier's codec. The reference path is simply
5
+ decode-then-encode through numpy; a production implementation might transcode directly in
6
+ the quantized domain, but decode/encode is always correct and is what the ladder relies on.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from .base import Codec
11
+
12
+
13
+ def recompress(blob: bytes, from_codec: Codec, to_codec: Codec, *, layer_id: int = 0) -> bytes:
14
+ """Decode ``blob`` with ``from_codec`` and re-encode it with ``to_codec``."""
15
+ arr = from_codec.decode(blob)
16
+ return to_codec.encode(arr, layer_id=layer_id)
@@ -0,0 +1,160 @@
1
+ """FP8 emulation (e4m3 / e5m2) on CPU via an exact 256-entry value table.
2
+
3
+ Approximation note
4
+ ------------------
5
+ Real hardware stores one byte per element in the OCP FP8 formats. We emulate that exactly
6
+ by enumerating all 256 bit patterns of the chosen format, decoding each to its float value
7
+ (this *is* the FP8 numeric grid, including subnormals), and encoding an input by snapping
8
+ each element to the nearest representable value (``searchsorted`` on the sorted grid).
9
+ Specials (e4m3 ``S.1111.111`` NaN; e5m2 ``exp==all-ones`` inf/NaN) are excluded from the
10
+ candidate set, so finite inputs never map to them. This reproduces true round-to-nearest
11
+ FP8 behavior with no hand-rolled mantissa bit-twiddling.
12
+
13
+ A per-tensor ``scale`` maps the tensor's peak magnitude to ~half the format's max before
14
+ quantization (and divides back out on decode). FP8's error is relative (the grid is roughly
15
+ log-uniform), so scaling chiefly prevents overflow and lifts tiny tensors out of the
16
+ subnormal region; it does not change the ~relative tolerance.
17
+
18
+ Tolerance: e4m3 has 3 mantissa bits -> max relative step ~2**-4 = 6.25% per element; e5m2
19
+ has 2 mantissa bits -> ~12.5%. Tensor-level relative error is well under those.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import functools
24
+ from typing import TYPE_CHECKING, Any
25
+
26
+ import numpy as np
27
+
28
+ from .base import Codec, pack_arrays, pack_blob, register_codec, unpack_arrays, unpack_blob
29
+
30
+ if TYPE_CHECKING:
31
+ from .calibration import CalibrationProfile
32
+
33
+ _FORMATS = {"e4m3": (4, 3, 7), "e5m2": (5, 2, 15)}
34
+
35
+
36
+ @functools.lru_cache(maxsize=None)
37
+ def _fp8_grid(fmt: str) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
38
+ """Return (all_values[256], finite_mask[256], sorted_finite_values) for ``fmt``."""
39
+ ebits, mbits, bias = _FORMATS[fmt]
40
+ vals = np.empty(256, dtype=np.float32)
41
+ finite = np.ones(256, dtype=bool)
42
+ exp_all_ones = (1 << ebits) - 1
43
+ for b in range(256):
44
+ s = (b >> 7) & 1
45
+ exp = (b >> mbits) & ((1 << ebits) - 1)
46
+ man = b & ((1 << mbits) - 1)
47
+ is_special = (fmt == "e4m3" and exp == exp_all_ones and man == (1 << mbits) - 1) or (
48
+ fmt == "e5m2" and exp == exp_all_ones
49
+ )
50
+ if is_special:
51
+ finite[b] = False
52
+ vals[b] = np.nan
53
+ continue
54
+ if exp == 0: # subnormal (or zero)
55
+ v = (man / (1 << mbits)) * 2.0 ** (1 - bias)
56
+ else: # normal
57
+ v = (1.0 + man / (1 << mbits)) * 2.0 ** (exp - bias)
58
+ vals[b] = (-1.0) ** s * v
59
+ return vals, finite, np.sort(vals[finite]).astype(np.float32)
60
+
61
+
62
+ @functools.lru_cache(maxsize=None)
63
+ def _torch_fp8(fmt: str):
64
+ """(torch, fp8 dtype) when torch has native float8, else None (numpy grid path)."""
65
+ try:
66
+ import torch
67
+ except ImportError:
68
+ return None
69
+ dt = {"e4m3": "float8_e4m3fn", "e5m2": "float8_e5m2"}[fmt]
70
+ return (torch, getattr(torch, dt)) if hasattr(torch, dt) else None
71
+
72
+
73
+ class FP8Codec(Codec):
74
+ def __init__(self, fmt: str = "e4m3", name: str = "fp8") -> None:
75
+ if fmt not in _FORMATS:
76
+ raise ValueError(f"fp8 format must be one of {sorted(_FORMATS)}, got {fmt}")
77
+ self.fmt = fmt
78
+ self.name = name
79
+
80
+ @classmethod
81
+ def from_cfg(cls, cfg: Any) -> "FP8Codec":
82
+ # Accept either `format` or `fmt`; default e4m3 (the common KV-cache choice).
83
+ return cls(fmt=getattr(cfg, "format", getattr(cfg, "fmt", "e4m3")))
84
+
85
+ def encode(self, arr: np.ndarray, *, layer_id: int = 0, profile: "CalibrationProfile | None" = None) -> bytes:
86
+ arr = np.asarray(arr, dtype=np.float32)
87
+ fast = _torch_fp8(self.fmt)
88
+ if fast is not None:
89
+ return self._encode_torch(arr, *fast)
90
+ vals, finite, sorted_vals = _fp8_grid(self.fmt)
91
+ max_val = float(np.nanmax(np.abs(vals)))
92
+
93
+ amax = float(np.abs(arr).max()) if arr.size else 0.0
94
+ target = max_val * 0.5
95
+ scale = amax / target if amax > 0 else 1.0
96
+
97
+ x = (arr.reshape(-1) / scale).astype(np.float32)
98
+ # Nearest representable value: searchsorted gives the right neighbor; compare both.
99
+ idx = np.searchsorted(sorted_vals, x)
100
+ idx = np.clip(idx, 1, len(sorted_vals) - 1)
101
+ left = sorted_vals[idx - 1]
102
+ right = sorted_vals[idx]
103
+ choose_left = np.abs(x - left) <= np.abs(x - right)
104
+
105
+ # Map chosen values back to their byte codes (value -> byte lookup).
106
+ finite_bytes = np.nonzero(finite)[0].astype(np.uint8)
107
+ order = np.argsort(vals[finite])
108
+ sorted_bytes = finite_bytes[order]
109
+ chosen_sorted_idx = np.where(choose_left, idx - 1, idx)
110
+ codes = sorted_bytes[chosen_sorted_idx].astype(np.uint8)
111
+
112
+ arr_meta, payload = pack_arrays({"codes": codes})
113
+ header = {
114
+ "codec": self.name,
115
+ "type": "fp8",
116
+ "fmt": self.fmt,
117
+ "scale": scale,
118
+ "shape": list(arr.shape),
119
+ "dtype": str(arr.dtype),
120
+ "_arrays": arr_meta,
121
+ }
122
+ return pack_blob(header, payload)
123
+
124
+ def _encode_torch(self, arr: np.ndarray, torch, fp8_dtype) -> bytes:
125
+ """Same blob as the grid path (the stored byte *is* the e4m3/e5m2 bit pattern), via
126
+ torch's native round-to-nearest-even cast — orders of magnitude faster on big chunks.
127
+ The scale keeps |x| <= max/2, so the cast never saturates."""
128
+ max_val = float(torch.finfo(fp8_dtype).max)
129
+ amax = float(np.abs(arr).max()) if arr.size else 0.0
130
+ scale = amax / (max_val * 0.5) if amax > 0 else 1.0
131
+ x = torch.from_numpy(arr.reshape(-1)) / scale
132
+ codes = x.to(fp8_dtype).view(torch.uint8).numpy()
133
+ arr_meta, payload = pack_arrays({"codes": codes})
134
+ header = {
135
+ "codec": self.name,
136
+ "type": "fp8",
137
+ "fmt": self.fmt,
138
+ "scale": scale,
139
+ "shape": list(arr.shape),
140
+ "dtype": str(arr.dtype),
141
+ "_arrays": arr_meta,
142
+ }
143
+ return pack_blob(header, payload)
144
+
145
+ def decode(self, blob: bytes) -> np.ndarray:
146
+ header, payload = unpack_blob(blob)
147
+ arrays = unpack_arrays(header["_arrays"], payload)
148
+ fast = _torch_fp8(header["fmt"])
149
+ if fast is not None:
150
+ torch, fp8_dtype = fast
151
+ codes = torch.from_numpy(arrays["codes"])
152
+ recon = (codes.view(fp8_dtype).to(torch.float32) * float(header["scale"])).numpy()
153
+ return recon.reshape(tuple(header["shape"])).astype(np.dtype(header["dtype"]))
154
+ vals, _, _ = _fp8_grid(header["fmt"])
155
+ codes = arrays["codes"]
156
+ recon = vals[codes.astype(np.intp)] * np.float32(header["scale"])
157
+ return recon.reshape(tuple(header["shape"])).astype(np.dtype(header["dtype"]))
158
+
159
+
160
+ register_codec("fp8", FP8Codec.from_cfg)