Skip to content

Commit 68ac1dc

Browse files
authored
feat: lmcache-turbo-quant-serde — tqai KV-cache compression plugin for LMCache v1
Adds integrations/lmcache/ — pip-installable package bridging tqai PolarQuantizer into LMCache's v1 serde plugin system. 47/47 tests pass. 4-bit: 25.8% of fp16 size, cosine=0.9954.
1 parent 8821b08 commit 68ac1dc

13 files changed

Lines changed: 1656 additions & 0 deletions

File tree

integrations/lmcache/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
__pycache__/
2+
*.egg-info/
3+
*.pyc
4+
.pytest_cache/
5+
dist/
6+
build/
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
"""Benchmark: tqai serde vs fp16 baseline for LMCache v1 KV-cache transfer.
2+
3+
Measures:
4+
- Compression ratio (bits/variant × model size)
5+
- Reconstruction quality (cosine similarity per head)
6+
- Serialization throughput (GB/s of input KV data)
7+
- Deserialization throughput (GB/s of recovered KV data)
8+
9+
Run:
10+
python benchmarks/bench_serde.py
11+
12+
Results are printed in a Markdown table so they can be pasted directly into
13+
a blog post or PR description.
14+
15+
Requirements (in addition to tqai):
16+
pip install lmcache torch numpy tabulate
17+
"""
18+
19+
from __future__ import annotations
20+
21+
import sys
22+
import time
23+
from dataclasses import dataclass, field
24+
from typing import List
25+
26+
import numpy as np
27+
import torch
28+
import torch.nn.functional as F
29+
30+
sys.path.insert(0, "src") # ensure local src/ is on path when run from repo root
31+
32+
from lmcache_turbo_quant_serde import TqaiDeserializer, TqaiSerializer, register
33+
from lmcache_turbo_quant_serde._codec import TurboQuantDeserializer, TurboQuantSerializer
34+
35+
# ---------------------------------------------------------------------------
36+
# Configuration
37+
# ---------------------------------------------------------------------------
38+
39+
WARMUP_ITERS = 3
40+
BENCH_ITERS = 10
41+
42+
CONFIGS = [
43+
# (name, num_layers, num_tokens, num_heads, head_dim)
44+
("LLaMA-3 8B (small seq)", 32, 64, 8, 128),
45+
("LLaMA-3 8B (medium seq)", 32, 256, 8, 128),
46+
("LLaMA-3 8B (long seq)", 32, 1024, 8, 128),
47+
("Mistral 7B", 32, 256, 8, 128),
48+
]
49+
50+
BITS_LIST = [4, 3, 2]
51+
52+
53+
# ---------------------------------------------------------------------------
54+
# Helpers
55+
# ---------------------------------------------------------------------------
56+
57+
58+
class _MemObj:
59+
def __init__(self, t: torch.Tensor) -> None:
60+
self.tensor = t
61+
62+
def set_used_size(self, n: int) -> None:
63+
self.tensor = self.tensor.ravel()[:n]
64+
65+
66+
def _make_kv(num_layers, num_tokens, num_heads, head_dim, dtype=torch.bfloat16, seed=0):
67+
torch.manual_seed(seed)
68+
return torch.randn(2, num_layers, num_tokens, num_heads * head_dim, dtype=dtype)
69+
70+
71+
def _cosine_sim(original: torch.Tensor, recovered: torch.Tensor, head_dim: int) -> float:
72+
return (
73+
F.cosine_similarity(
74+
original.float().reshape(-1, head_dim),
75+
recovered.float().reshape(-1, head_dim),
76+
dim=-1,
77+
)
78+
.mean()
79+
.item()
80+
)
81+
82+
83+
# ---------------------------------------------------------------------------
84+
# tqai v1 benchmark helper
85+
# ---------------------------------------------------------------------------
86+
87+
88+
@dataclass
89+
class Result:
90+
name: str
91+
bits: int
92+
num_layers: int
93+
num_tokens: int
94+
num_heads: int
95+
head_dim: int
96+
original_bytes: int
97+
compressed_bytes: int
98+
cosine_sim: float
99+
ser_ms: float # median over BENCH_ITERS
100+
des_ms: float # median over BENCH_ITERS
101+
ser_times: List[float] = field(default_factory=list)
102+
des_times: List[float] = field(default_factory=list)
103+
104+
@property
105+
def compression_ratio(self) -> float:
106+
return self.compressed_bytes / self.original_bytes
107+
108+
@property
109+
def ser_gbps(self) -> float:
110+
"""GB/s of input KV data serialized."""
111+
return (self.original_bytes / 1e9) / (self.ser_ms / 1e3)
112+
113+
@property
114+
def des_gbps(self) -> float:
115+
"""GB/s of output KV data deserialized."""
116+
return (self.original_bytes / 1e9) / (self.des_ms / 1e3)
117+
118+
119+
def _bench_v1(name, num_layers, num_tokens, num_heads, head_dim, bits) -> Result:
120+
hidden_dim = num_heads * head_dim
121+
kv = _make_kv(num_layers, num_tokens, num_heads, head_dim)
122+
original_bytes = kv.numel() * 2 # bfloat16 = 2 bytes
123+
124+
try:
125+
from lmcache.v1.distributed.api import MemoryLayoutDesc
126+
layout = MemoryLayoutDesc(
127+
shapes=[torch.Size([2, num_layers, num_tokens, hidden_dim])],
128+
dtypes=[kv.dtype],
129+
)
130+
ser = TqaiSerializer(head_dim=head_dim, bits=bits)
131+
des = TqaiDeserializer()
132+
buf_size = ser.estimate_serialized_size(layout)
133+
except ImportError:
134+
# Fallback to standalone codec for machines without lmcache
135+
ser = TurboQuantSerializer(bits=bits) # type: ignore[assignment]
136+
des = TurboQuantDeserializer() # type: ignore[assignment]
137+
buf_size = original_bytes * 2
138+
139+
# Each serialize call needs its own write buffer; keep a shared read buffer
140+
# populated by one canonical serialize call for the deserialize benchmark.
141+
write_buf = torch.zeros(buf_size, dtype=torch.uint8)
142+
canonical_buf = torch.zeros(buf_size, dtype=torch.uint8)
143+
144+
def _do_serialize(out_buf: torch.Tensor) -> int:
145+
if hasattr(ser, "serialize"):
146+
return ser.serialize(_MemObj(kv), _MemObj(out_buf))
147+
else:
148+
bs = ser.to_bytes(kv)
149+
n = len(bs)
150+
out_buf.ravel()[:n].copy_(torch.frombuffer(bs, dtype=torch.uint8))
151+
return n
152+
153+
def _do_deserialize(src_buf: torch.Tensor, n: int) -> torch.Tensor:
154+
if hasattr(des, "deserialize"):
155+
dst = _MemObj(torch.zeros_like(kv))
156+
des.deserialize(_MemObj(src_buf[:n]), dst)
157+
return dst.tensor
158+
else:
159+
return des.from_bytes(bytes(src_buf[:n].numpy()))
160+
161+
# --- warmup + capture canonical compressed blob ---
162+
for _ in range(WARMUP_ITERS):
163+
n = _do_serialize(write_buf)
164+
canonical_buf[:n].copy_(write_buf[:n])
165+
compressed_bytes = n
166+
167+
# --- serialize benchmark ---
168+
ser_times = []
169+
for _ in range(BENCH_ITERS):
170+
t0 = time.perf_counter()
171+
_do_serialize(write_buf)
172+
ser_times.append((time.perf_counter() - t0) * 1e3)
173+
174+
# --- deserialize benchmark ---
175+
des_times = []
176+
recovered = None
177+
for _ in range(BENCH_ITERS):
178+
t0 = time.perf_counter()
179+
recovered = _do_deserialize(canonical_buf, n)
180+
des_times.append((time.perf_counter() - t0) * 1e3)
181+
182+
sim = _cosine_sim(kv, recovered, head_dim)
183+
184+
return Result(
185+
name=name,
186+
bits=bits,
187+
num_layers=num_layers,
188+
num_tokens=num_tokens,
189+
num_heads=num_heads,
190+
head_dim=head_dim,
191+
original_bytes=original_bytes,
192+
compressed_bytes=compressed_bytes,
193+
cosine_sim=sim,
194+
ser_ms=float(np.median(ser_times)),
195+
des_ms=float(np.median(des_times)),
196+
ser_times=ser_times,
197+
des_times=des_times,
198+
)
199+
200+
201+
# ---------------------------------------------------------------------------
202+
# Main
203+
# ---------------------------------------------------------------------------
204+
205+
206+
def main() -> None:
207+
register()
208+
209+
results: list[Result] = []
210+
211+
print(f"\nRunning tqai serde benchmark (warmup={WARMUP_ITERS}, bench={BENCH_ITERS})\n")
212+
213+
for cfg_name, nl, nt, nh, hd in CONFIGS:
214+
hidden_dim = nh * hd
215+
original_mb = (2 * nl * nt * hidden_dim * 2) / 1e6
216+
print(f" {cfg_name} [{2}×{nl}×{nt}×{hidden_dim}] {original_mb:.1f} MB (fp16)")
217+
for bits in BITS_LIST:
218+
r = _bench_v1(cfg_name, nl, nt, nh, hd, bits)
219+
results.append(r)
220+
print(
221+
f" bits={bits} ratio={r.compression_ratio:.3f} "
222+
f"cos={r.cosine_sim:.4f} "
223+
f"ser={r.ser_ms:.1f}ms des={r.des_ms:.1f}ms "
224+
f"({r.ser_gbps:.2f} GB/s in / {r.des_gbps:.2f} GB/s out)"
225+
)
226+
227+
# --- Markdown table ---
228+
header = [
229+
"Config", "Bits", "Tokens",
230+
"Ratio", "Cosine ↑", "Ser (ms)", "Des (ms)", "Ser GB/s", "Des GB/s",
231+
]
232+
233+
rows = []
234+
for r in results:
235+
rows.append([
236+
r.name,
237+
r.bits,
238+
r.num_tokens,
239+
f"{r.compression_ratio:.3f}",
240+
f"{r.cosine_sim:.4f}",
241+
f"{r.ser_ms:.1f}",
242+
f"{r.des_ms:.1f}",
243+
f"{r.ser_gbps:.2f}",
244+
f"{r.des_gbps:.2f}",
245+
])
246+
247+
try:
248+
from tabulate import tabulate
249+
md = tabulate(rows, headers=header, tablefmt="pipe")
250+
except ImportError:
251+
# Fallback: simple CSV
252+
md = ",".join(header) + "\n"
253+
for row in rows:
254+
md += ",".join(str(c) for c in row) + "\n"
255+
256+
print("\n\n## tqai × LMCache Serde Benchmark\n")
257+
print("> Platform: CPU (torch bfloat16) — run on GPU for production numbers")
258+
print("> Note: bits=3 uses a Python-level bitstream packer (tqai._pack_bitstream).")
259+
print("> Vectorizing it with NumPy would bring 3-bit perf in line with 4-bit.\n")
260+
print(md)
261+
print()
262+
263+
264+
if __name__ == "__main__":
265+
main()
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[build-system]
2+
requires = ["setuptools>=68", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "lmcache-turbo-quant-serde"
7+
version = "0.1.0"
8+
description = "TurboQuant KV-cache compression plugin for LMCache"
9+
requires-python = ">=3.10"
10+
license = { text = "Apache-2.0" }
11+
keywords = ["lmcache", "kv-cache", "quantization", "llm", "inference"]
12+
dependencies = [
13+
"tqai>=0.6.0",
14+
"torch>=2.1",
15+
"numpy>=1.24",
16+
]
17+
18+
[project.optional-dependencies]
19+
lmcache = ["lmcache>=0.3"]
20+
dev = ["pytest>=7", "pytest-cov"]
21+
22+
[tool.setuptools.packages.find]
23+
where = ["src"]
24+
25+
[tool.pytest.ini_options]
26+
testpaths = ["tests"]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""lmcache-turbo-quant-serde — tqai KV-cache compression for LMCache.
2+
3+
Standalone (no LMCache required)::
4+
5+
from lmcache_turbo_quant_serde import TurboQuantSerializer, TurboQuantDeserializer
6+
7+
ser = TurboQuantSerializer(bits=4)
8+
des = TurboQuantDeserializer()
9+
compressed = ser.to_bytes(kv_tensor) # → bytes
10+
recovered = des.from_bytes(compressed) # → tensor
11+
12+
LMCache v1 integration::
13+
14+
import lmcache_turbo_quant_serde
15+
lmcache_turbo_quant_serde.register() # call once at startup
16+
17+
# In your L2 adapter serde config:
18+
# {"type": "tqai", "head_dim": 128, "bits": 4}
19+
"""
20+
21+
from ._codec import TurboQuantDeserializer, TurboQuantSerializer
22+
from ._register import register
23+
from ._v1_codec import TqaiDeserializer, TqaiSerializer
24+
25+
__version__ = "0.1.0"
26+
__all__ = [
27+
# Standalone (old-style to_bytes / from_bytes)
28+
"TurboQuantSerializer",
29+
"TurboQuantDeserializer",
30+
# LMCache v1 (serialize / deserialize / estimate_serialized_size)
31+
"TqaiSerializer",
32+
"TqaiDeserializer",
33+
# Registration
34+
"register",
35+
"__version__",
36+
]

0 commit comments

Comments
 (0)