aither-kvcache 2.1.0__tar.gz → 2.2.0__tar.gz

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 (54) hide show
  1. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/PKG-INFO +1 -1
  2. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/vllm/plugin.py +20 -2
  3. aither_kvcache-2.2.0/aither_kvcache/vllm/tenant_isolation.py +364 -0
  4. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache.egg-info/PKG-INFO +1 -1
  5. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache.egg-info/SOURCES.txt +1 -0
  6. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/pyproject.toml +1 -1
  7. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/turboquant/__init__.py +3 -0
  8. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/turboquant/codebook.py +60 -0
  9. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/turboquant/fused_attention.py +27 -18
  10. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/turboquant/quantizer.py +24 -1
  11. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/LICENSE +0 -0
  12. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/README.md +0 -0
  13. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/__init__.py +0 -0
  14. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/bench.py +0 -0
  15. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/block_selector.py +0 -0
  16. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/codebook.py +0 -0
  17. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/eviction_advisor.py +0 -0
  18. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/fused_attention.py +0 -0
  19. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/fused_kv_update.py +0 -0
  20. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/hybrid_quantizer.py +0 -0
  21. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/kvcache_graph.py +0 -0
  22. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/packing.py +0 -0
  23. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/quantizer.py +0 -0
  24. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/rotation.py +0 -0
  25. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/triattention/__init__.py +0 -0
  26. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/triattention/attention.py +0 -0
  27. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/triattention/cache.py +0 -0
  28. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/triattention/calibration.py +0 -0
  29. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/triattention/config.py +0 -0
  30. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/triattention/encoder.py +0 -0
  31. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/triattention/scorer.py +0 -0
  32. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/triattention/spectral.py +0 -0
  33. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/triton_ops.py +0 -0
  34. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/vllm/__init__.py +0 -0
  35. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/vllm/backend.py +0 -0
  36. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/vllm/cache.py +0 -0
  37. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/vllm/engine.py +0 -0
  38. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/vllm/eviction_plugin.py +0 -0
  39. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/vllm/hooks.py +0 -0
  40. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache/vllm/sitecustomize.py +0 -0
  41. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache.egg-info/dependency_links.txt +0 -0
  42. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache.egg-info/entry_points.txt +0 -0
  43. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache.egg-info/requires.txt +0 -0
  44. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/aither_kvcache.egg-info/top_level.txt +0 -0
  45. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/setup.cfg +0 -0
  46. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/tests/test_core.py +0 -0
  47. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/tests/test_fused.py +0 -0
  48. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/tests/test_hybrid_hooks.py +0 -0
  49. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/tests/test_kvcache_graph.py +0 -0
  50. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/tests/test_triattention.py +0 -0
  51. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/turboquant/bench.py +0 -0
  52. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/turboquant/packing.py +0 -0
  53. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/turboquant/rotation.py +0 -0
  54. {aither_kvcache-2.1.0 → aither_kvcache-2.2.0}/turboquant/triton_ops.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aither-kvcache
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Near-optimal KV cache compression for LLM inference — TurboQuant (vector quantization) + TriAttention (spectral compression)
5
5
  Author: Aitherium
6
6
  License-Expression: CC-BY-4.0
@@ -1,5 +1,5 @@
1
1
  """
2
- vLLM plugin entrypoint.
2
+ vLLM plugin entrypoint for the aither-kvcache wheel.
3
3
 
4
4
  Registered via pyproject.toml entry_points under vllm.general_plugins.
5
5
  vLLM loads this at startup in every process (API server + engine workers).
@@ -7,6 +7,11 @@ vLLM loads this at startup in every process (API server + engine workers).
7
7
  Registers:
8
8
  1. TurboQuant CUSTOM attention backend (--attention-backend CUSTOM)
9
9
  2. Graph-aware eviction (replaces LRU with semantic scoring)
10
+ 3. Per-request tenant isolation (default OFF; AITHER_REQUEST_TENANT_ISOLATION=1)
11
+
12
+ CANONICAL SOURCE. Synced by .github/workflows/sync-kvcache.yml into the public
13
+ Aitherium/aitherkvcache repo as aither_kvcache/vllm/plugin.py, alongside
14
+ tenant_isolation.py. Edit here, not in the public repo.
10
15
  """
11
16
 
12
17
  import logging
@@ -24,8 +29,8 @@ def register():
24
29
  # 1. Register TurboQuant attention backend
25
30
  try:
26
31
  from vllm.v1.attention.backends.registry import (
27
- register_backend,
28
32
  AttentionBackendEnum,
33
+ register_backend,
29
34
  )
30
35
  register_backend(
31
36
  AttentionBackendEnum.CUSTOM,
@@ -51,3 +56,16 @@ def register():
51
56
  except Exception as e:
52
57
  print(f"[aither-kvcache] Eviction install failed: {e}",
53
58
  file=sys.stderr, flush=True)
59
+
60
+ # 3. Per-request tenant isolation (default OFF; AITHER_REQUEST_TENANT_ISOLATION=1).
61
+ # Rides this plugin so it loads in every process (API server + EngineCore
62
+ # workers). Non-fatal: a failure leaves the process on the instance tenant.
63
+ if os.environ.get("AITHER_REQUEST_TENANT_ISOLATION") == "1":
64
+ try:
65
+ from .tenant_isolation import install_tenant_isolation
66
+ install_tenant_isolation()
67
+ except ImportError:
68
+ pass
69
+ except Exception as e:
70
+ print(f"[aither-kvcache] Tenant isolation install failed: {e}",
71
+ file=sys.stderr, flush=True)
@@ -0,0 +1,364 @@
1
+ """
2
+ Per-request tenant isolation for the aither-kvcache vLLM plugin.
3
+
4
+ This is the SELF-CONTAINED, in-wheel port of AitherOS's per-request KV-cache
5
+ tenant isolation. It rides the ACTIVE path on the orchestrator: the aither-kvcache
6
+ `vllm.general_plugins` entrypoint (`plugin.register()`) loads in every vLLM
7
+ process (API server + EngineCore workers), so a hook installed here actually runs
8
+ — unlike the AitherOS `lib.gpu.turboquant` import hooks, which are not mounted
9
+ into the served image.
10
+
11
+ Three seams, all fail-closed and non-fatal (a wrong symbol degrades to the
12
+ instance tenant; it never crashes vLLM startup):
13
+
14
+ 1. Serving layer (API-server process): read the gateway-signed OpenAI ``user``
15
+ field, verify its HMAC, and publish the tenant into a ContextVar.
16
+ 2. Engine ``generate`` (API-server process): embed the verified tenant into the
17
+ real vLLM ``request_id`` (positional index 2 in vLLM 0.19.x), so it crosses
18
+ the frontend -> EngineCore process boundary verbatim.
19
+ 3. Block manager (EngineCore worker): on ``KVCacheManager.allocate_slots`` the
20
+ ``request.request_id`` carries the embedded tenant — tag the request's blocks
21
+ with it; untag on ``free``.
22
+
23
+ Self-contained by necessity: the wheel cannot import ``lib.gpu.turboquant``. The
24
+ crypto/transport helpers mirror ``request_tenant_registry.py`` byte-for-byte so
25
+ tokens the AitherOS gateway signs verify here.
26
+
27
+ Gate: AITHER_REQUEST_TENANT_ISOLATION=1 (default OFF). Key: AITHER_TENANT_SIGNING_KEY
28
+ (injected from AitherSecrets into the image env; empty => fail-closed).
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import contextvars
34
+ import hashlib
35
+ import hmac
36
+ import logging
37
+ import os
38
+ import sys
39
+ import threading
40
+ from typing import Any
41
+
42
+ logger = logging.getLogger("aither.kvcache.tenant")
43
+
44
+ _ENABLED = os.environ.get("AITHER_REQUEST_TENANT_ISOLATION", "0") == "1"
45
+
46
+ # Must match request_tenant_registry.py exactly (cross-repo token compatibility).
47
+ _TOKEN_SEP = "|"
48
+ _RID_PREFIX = "tqt1:"
49
+ _RID_SEP = "~"
50
+
51
+ _pending_tenant: contextvars.ContextVar[str | None] = contextvars.ContextVar(
52
+ "tq_pending_tenant", default=None
53
+ )
54
+
55
+ # block_id -> tenant_slug. Populated at allocate_slots, cleared at free. Guarded.
56
+ _block_tenant: dict[int, str] = {}
57
+ _block_lock = threading.Lock()
58
+
59
+ _isolation_status: dict[str, bool] = {
60
+ "enabled": _ENABLED,
61
+ "serving_patched": False,
62
+ "engine_patched": False,
63
+ "block_manager_patched": False,
64
+ "key_present": False,
65
+ "verified": False,
66
+ }
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Token crypto + request_id transport (mirror of request_tenant_registry.py)
71
+ # ---------------------------------------------------------------------------
72
+ def _signing_key() -> bytes:
73
+ return os.environ.get("AITHER_TENANT_SIGNING_KEY", "").encode("utf-8")
74
+
75
+
76
+ def verify_tenant_token(
77
+ token: str | None, *, now: float | None = None
78
+ ) -> str | None:
79
+ """Return the tenant slug iff ``token`` has a valid, unexpired gateway signature."""
80
+ import time
81
+ key = _signing_key()
82
+ if not token or not key:
83
+ return None
84
+ parts = token.split(_TOKEN_SEP)
85
+ if len(parts) != 3:
86
+ return None
87
+ tenant, exp_str, sig = parts
88
+ if not tenant or not exp_str or not sig:
89
+ return None
90
+ try:
91
+ exp = int(exp_str)
92
+ except (TypeError, ValueError):
93
+ return None
94
+ expected = hmac.new(
95
+ key, f"{tenant}{_TOKEN_SEP}{exp}".encode(), hashlib.sha256
96
+ ).hexdigest()
97
+ if not hmac.compare_digest(sig, expected):
98
+ logger.warning("Rejected tenant token with invalid signature")
99
+ return None
100
+ if exp < (now if now is not None else time.time()):
101
+ logger.warning("Rejected expired tenant token")
102
+ return None
103
+ return tenant
104
+
105
+
106
+ def embed_tenant_in_request_id(request_id: str, tenant_slug: str) -> str:
107
+ """Prefix a verified tenant onto a request_id (idempotent, boundary-safe)."""
108
+ if not request_id or not tenant_slug:
109
+ return request_id
110
+ rid = str(request_id)
111
+ if _RID_SEP in tenant_slug or rid.startswith(_RID_PREFIX):
112
+ return request_id
113
+ return f"{_RID_PREFIX}{tenant_slug}{_RID_SEP}{rid}"
114
+
115
+
116
+ def extract_tenant_from_request_id(
117
+ request_id: str | None,
118
+ ) -> tuple[str | None, str | None]:
119
+ """Inverse of embed_tenant_in_request_id -> (tenant_or_None, original_id)."""
120
+ if not request_id:
121
+ return None, request_id
122
+ rid = str(request_id)
123
+ if not rid.startswith(_RID_PREFIX):
124
+ return None, request_id
125
+ tenant, sep, original = rid[len(_RID_PREFIX):].partition(_RID_SEP)
126
+ if not sep or not tenant:
127
+ return None, request_id
128
+ return tenant, original
129
+
130
+
131
+ # ---------------------------------------------------------------------------
132
+ # Block -> tenant map
133
+ # ---------------------------------------------------------------------------
134
+ def tag_blocks(tenant_slug: str, block_ids) -> None:
135
+ if not tenant_slug or not block_ids:
136
+ return
137
+ with _block_lock:
138
+ for bid in block_ids:
139
+ _block_tenant[int(bid)] = tenant_slug
140
+
141
+
142
+ def untag_blocks(block_ids) -> None:
143
+ if not block_ids:
144
+ return
145
+ with _block_lock:
146
+ for bid in block_ids:
147
+ _block_tenant.pop(int(bid), None)
148
+
149
+
150
+ def block_tenant(block_id: int) -> str | None:
151
+ with _block_lock:
152
+ return _block_tenant.get(int(block_id))
153
+
154
+
155
+ def block_tenant_count() -> int:
156
+ with _block_lock:
157
+ return len(_block_tenant)
158
+
159
+
160
+ # ---------------------------------------------------------------------------
161
+ # Status
162
+ # ---------------------------------------------------------------------------
163
+ def get_isolation_status() -> dict[str, bool]:
164
+ _isolation_status["enabled"] = _ENABLED
165
+ _isolation_status["key_present"] = bool(_signing_key())
166
+ _isolation_status["verified"] = (
167
+ _isolation_status["enabled"]
168
+ and _isolation_status["serving_patched"]
169
+ and _isolation_status["engine_patched"]
170
+ and _isolation_status["block_manager_patched"]
171
+ and _isolation_status["key_present"]
172
+ )
173
+ return dict(_isolation_status)
174
+
175
+
176
+ # ---------------------------------------------------------------------------
177
+ # Seam 1: serving-layer wrap (verified tenant -> ContextVar)
178
+ # ---------------------------------------------------------------------------
179
+ def _wrap_serving_method(cls: Any, method_name: str) -> bool:
180
+ orig = getattr(cls, method_name, None)
181
+ if orig is None:
182
+ return False
183
+ if getattr(orig, "_tq_tenant_wrapped", False):
184
+ return True
185
+
186
+ async def _tenant_serving_wrapper(self, request, *args, **kwargs):
187
+ token = None
188
+ try:
189
+ user = getattr(request, "user", None)
190
+ tenant = verify_tenant_token(user) if user else None
191
+ if tenant:
192
+ token = _pending_tenant.set(tenant)
193
+ except (AttributeError, TypeError, ValueError) as e:
194
+ logger.debug("tenant capture skipped: %s", e)
195
+ token = None
196
+ try:
197
+ return await orig(self, request, *args, **kwargs)
198
+ finally:
199
+ if token is not None:
200
+ _pending_tenant.reset(token)
201
+
202
+ _tenant_serving_wrapper._tq_tenant_wrapped = True
203
+ setattr(cls, method_name, _tenant_serving_wrapper)
204
+ return True
205
+
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Seam 2: engine generate wrap (embed tenant into positional request_id)
209
+ # ---------------------------------------------------------------------------
210
+ # vLLM 0.19.x: AsyncLLM.generate(self, prompt, sampling_params, request_id, *, ...)
211
+ # request_id is the 3rd positional arg -> index 2 in the wrapper's *args.
212
+ _GENERATE_RID_POS = 2
213
+
214
+
215
+ def _wrap_engine_generate(cls: Any, rid_pos: int = _GENERATE_RID_POS) -> bool:
216
+ orig = getattr(cls, "generate", None)
217
+ if orig is None:
218
+ return False
219
+ if getattr(orig, "_tq_tenant_wrapped", False):
220
+ return True
221
+
222
+ async def _tenant_generate_wrapper(self, *args, **kwargs):
223
+ tenant = _pending_tenant.get(None)
224
+ if tenant:
225
+ rid = None
226
+ pos = None
227
+ if isinstance(kwargs.get("request_id"), str) and kwargs["request_id"]:
228
+ rid = kwargs["request_id"]
229
+ elif (len(args) > rid_pos and isinstance(args[rid_pos], str)
230
+ and args[rid_pos]):
231
+ pos = rid_pos
232
+ rid = args[pos]
233
+ if rid:
234
+ embedded = embed_tenant_in_request_id(rid, tenant)
235
+ if pos is not None:
236
+ args = args[:pos] + (embedded,) + args[pos + 1:]
237
+ else:
238
+ kwargs = dict(kwargs)
239
+ kwargs["request_id"] = embedded
240
+ async for out in orig(self, *args, **kwargs):
241
+ yield out
242
+
243
+ _tenant_generate_wrapper._tq_tenant_wrapped = True
244
+ cls.generate = _tenant_generate_wrapper
245
+ return True
246
+
247
+
248
+ # ---------------------------------------------------------------------------
249
+ # Seam 3: block manager wrap (tag blocks by tenant from request.request_id)
250
+ # ---------------------------------------------------------------------------
251
+ def _wrap_block_manager(cls: Any) -> bool:
252
+ alloc = getattr(cls, "allocate_slots", None)
253
+ free = getattr(cls, "free", None)
254
+ if alloc is None:
255
+ return False
256
+ if getattr(alloc, "_tq_tenant_wrapped", False):
257
+ return True
258
+
259
+ def _flatten_block_ids(mgr, request_id) -> list:
260
+ try:
261
+ groups = mgr.get_block_ids(request_id)
262
+ except Exception:
263
+ return []
264
+ ids: list = []
265
+ for g in groups or ():
266
+ try:
267
+ ids.extend(int(b) for b in g)
268
+ except TypeError:
269
+ ids.append(int(g))
270
+ return ids
271
+
272
+ def _tenant_allocate_slots(self, request, *args, **kwargs):
273
+ result = alloc(self, request, *args, **kwargs)
274
+ if result is not None:
275
+ try:
276
+ rid = getattr(request, "request_id", None)
277
+ tenant, _orig = extract_tenant_from_request_id(rid)
278
+ if tenant:
279
+ tag_blocks(tenant, _flatten_block_ids(self, rid))
280
+ except Exception as e: # never break allocation for tagging
281
+ logger.debug("tenant block-tag skipped: %s", e)
282
+ return result
283
+
284
+ _tenant_allocate_slots._tq_tenant_wrapped = True
285
+ cls.allocate_slots = _tenant_allocate_slots
286
+
287
+ if free is not None and not getattr(free, "_tq_tenant_wrapped", False):
288
+ def _tenant_free(self, request, *args, **kwargs):
289
+ try:
290
+ rid = getattr(request, "request_id", None)
291
+ if rid:
292
+ untag_blocks(_flatten_block_ids(self, rid))
293
+ except Exception as e:
294
+ logger.debug("tenant block-untag skipped: %s", e)
295
+ return free(self, request, *args, **kwargs)
296
+
297
+ _tenant_free._tq_tenant_wrapped = True
298
+ cls.free = _tenant_free
299
+
300
+ return True
301
+
302
+
303
+ # ---------------------------------------------------------------------------
304
+ # Install
305
+ # ---------------------------------------------------------------------------
306
+ def install_tenant_isolation() -> bool:
307
+ """Install all three seams. Called from plugin.register() when enabled.
308
+
309
+ Returns True iff verified (serving + engine + block manager + key). Every
310
+ failure is non-fatal and leaves the process serving with the instance tenant.
311
+ """
312
+ if not _ENABLED:
313
+ logger.debug("tenant isolation disabled (AITHER_REQUEST_TENANT_ISOLATION!=1)")
314
+ return False
315
+
316
+ serving_targets = [
317
+ ("vllm.entrypoints.openai.chat_completion.serving", "OpenAIServingChat",
318
+ "create_chat_completion"),
319
+ ("vllm.entrypoints.openai.serving_chat", "OpenAIServingChat",
320
+ "create_chat_completion"),
321
+ ("vllm.entrypoints.openai.completion.serving", "OpenAIServingCompletion",
322
+ "create_completion"),
323
+ ("vllm.entrypoints.openai.serving_completion", "OpenAIServingCompletion",
324
+ "create_completion"),
325
+ ]
326
+ seen = set()
327
+ for modpath, clsname, method in serving_targets:
328
+ try:
329
+ mod = __import__(modpath, fromlist=[clsname])
330
+ cls = getattr(mod, clsname)
331
+ if cls in seen:
332
+ continue
333
+ if _wrap_serving_method(cls, method):
334
+ seen.add(cls)
335
+ _isolation_status["serving_patched"] = True
336
+ except Exception as e:
337
+ logger.debug("serving wrap skipped %s.%s: %s", clsname, method, e)
338
+
339
+ for modpath, clsname in [
340
+ ("vllm.v1.engine.async_llm", "AsyncLLM"),
341
+ ("vllm.engine.async_llm_engine", "AsyncLLMEngine"),
342
+ ]:
343
+ try:
344
+ mod = __import__(modpath, fromlist=[clsname])
345
+ if _wrap_engine_generate(getattr(mod, clsname)):
346
+ _isolation_status["engine_patched"] = True
347
+ except Exception as e:
348
+ logger.debug("engine wrap skipped %s: %s", clsname, e)
349
+
350
+ try:
351
+ from vllm.v1.core.kv_cache_manager import KVCacheManager
352
+ if _wrap_block_manager(KVCacheManager):
353
+ _isolation_status["block_manager_patched"] = True
354
+ except Exception as e:
355
+ logger.debug("block-manager wrap skipped: %s", e)
356
+
357
+ st = get_isolation_status()
358
+ print(
359
+ f"[aither-kvcache] tenant isolation: serving={st['serving_patched']} "
360
+ f"engine={st['engine_patched']} block_mgr={st['block_manager_patched']} "
361
+ f"key={st['key_present']} VERIFIED={st['verified']}",
362
+ file=sys.stderr, flush=True,
363
+ )
364
+ return st["verified"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aither-kvcache
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Near-optimal KV cache compression for LLM inference — TurboQuant (vector quantization) + TriAttention (spectral compression)
5
5
  Author: Aitherium
6
6
  License-Expression: CC-BY-4.0
@@ -36,6 +36,7 @@ aither_kvcache/vllm/eviction_plugin.py
36
36
  aither_kvcache/vllm/hooks.py
37
37
  aither_kvcache/vllm/plugin.py
38
38
  aither_kvcache/vllm/sitecustomize.py
39
+ aither_kvcache/vllm/tenant_isolation.py
39
40
  tests/test_core.py
40
41
  tests/test_fused.py
41
42
  tests/test_hybrid_hooks.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aither-kvcache"
7
- version = "2.1.0"
7
+ version = "2.2.0"
8
8
  description = "Near-optimal KV cache compression for LLM inference — TurboQuant (vector quantization) + TriAttention (spectral compression)"
9
9
  readme = "README.md"
10
10
  license = "CC-BY-4.0"
@@ -22,6 +22,7 @@ Usage:
22
22
  """
23
23
 
24
24
  from .quantizer import TurboQuant, TurboQuantConfig
25
+ from .gb10_fp8_rescue import get_gb10_fp8_status, install_gb10_fp8_rescue
25
26
 
26
27
  __all__ = [
27
28
  "TurboQuant",
@@ -34,6 +35,8 @@ __all__ = [
34
35
  "get_tier_cache_bridge",
35
36
  "get_strata_cache_shadow",
36
37
  "get_graph_eviction_advisor",
38
+ "install_gb10_fp8_rescue",
39
+ "get_gb10_fp8_status",
37
40
  ]
38
41
  __version__ = "0.9.0"
39
42
 
@@ -82,6 +82,66 @@ def get_codebook(d: int, bits: int) -> Tuple[np.ndarray, np.ndarray, float]:
82
82
  return centroids, boundaries_inner, mse_total
83
83
 
84
84
 
85
+ def get_correction_factors(d: int, bits: int) -> np.ndarray:
86
+ """
87
+ Compute per-centroid inner-product bias correction factors.
88
+
89
+ For Lloyd-Max quantization on N(0, 1/d), the reconstructed inner product
90
+ <q, x_hat> is a biased estimator of <q, x>. The correction factor for
91
+ centroid i is: alpha_i = E[x | x in bin_i] / c_i.
92
+
93
+ For a perfect Lloyd-Max codebook this is ~1.0, but finite-precision
94
+ centroids and boundary discretization introduce small biases. Computing
95
+ these analytically and applying them removes the systematic IP bias.
96
+
97
+ Returns:
98
+ correction: array of shape [2^bits], values close to 1.0.
99
+ """
100
+ if bits not in _STANDARD_CODEBOOKS:
101
+ raise ValueError(f"Supported bit-widths: {list(_STANDARD_CODEBOOKS.keys())}. Got {bits}")
102
+
103
+ entry = _STANDARD_CODEBOOKS[bits]
104
+ std_centroids = entry["centroids"]
105
+ num_levels = 1 << bits
106
+ sigma = 1.0 / math.sqrt(d)
107
+
108
+ # Standard N(0,1) boundaries
109
+ boundaries = np.empty(num_levels + 1)
110
+ boundaries[0] = -10.0 # effectively -inf
111
+ boundaries[-1] = 10.0 # effectively +inf
112
+ for i in range(num_levels - 1):
113
+ boundaries[i + 1] = (std_centroids[i] + std_centroids[i + 1]) / 2
114
+
115
+ # Compute E[x | x in bin_i] for N(0,1)
116
+ # E[x | b_lo <= x < b_hi] = (phi(b_lo) - phi(b_hi)) / (Phi(b_hi) - Phi(b_lo))
117
+ # where phi = pdf, Phi = cdf of N(0,1)
118
+ from math import exp as _exp, pi as _pi
119
+
120
+ def _phi(x):
121
+ return (1.0 / math.sqrt(2 * _pi)) * _exp(-0.5 * x * x)
122
+
123
+ def _Phi(x):
124
+ # Abramowitz & Stegun approximation (accurate to 1e-7)
125
+ t = 1.0 / (1.0 + 0.2316419 * abs(x))
126
+ poly = t * (0.319381530 + t * (-0.356563782 + t * (
127
+ 1.781477937 + t * (-1.821255978 + t * 1.330274429))))
128
+ val = 1.0 - _phi(x) * poly
129
+ return val if x >= 0 else 1.0 - val
130
+
131
+ corrections = np.ones(num_levels, dtype=np.float64)
132
+ for i in range(num_levels):
133
+ b_lo = boundaries[i]
134
+ b_hi = boundaries[i + 1]
135
+ prob = _Phi(b_hi) - _Phi(b_lo)
136
+ if prob < 1e-15 or abs(std_centroids[i]) < 1e-15:
137
+ corrections[i] = 1.0
138
+ continue
139
+ cond_mean = (_phi(b_lo) - _phi(b_hi)) / prob
140
+ corrections[i] = cond_mean / std_centroids[i]
141
+
142
+ return corrections.astype(np.float32)
143
+
144
+
85
145
  def get_theory_bounds(bits: int) -> Tuple[float, float]:
86
146
  """Return (lower_bound, upper_bound) on MSE for b-bit quantization."""
87
147
  lower = 1.0 / (4 ** bits)
@@ -66,9 +66,11 @@ class TQPagedAttentionRef:
66
66
  head_dim: int,
67
67
  block_size: int,
68
68
  bits: int = 4,
69
+ k_centroids: Optional[torch.Tensor] = None,
69
70
  ):
70
71
  self.rotation = rotation # [head_dim, head_dim]
71
- self.centroids = centroids # [num_levels]
72
+ self.centroids = centroids # [num_levels] — used for V accumulation
73
+ self.k_centroids = k_centroids if k_centroids is not None else centroids # for K scoring
72
74
  self.scale = scale
73
75
  self.num_kv_heads = num_kv_heads
74
76
  self.num_query_heads = num_query_heads
@@ -81,12 +83,14 @@ class TQPagedAttentionRef:
81
83
  from .packing import unpack_4bit, unpack_3bit, unpack_2bit
82
84
  self._unpack_fns = {4: unpack_4bit, 3: unpack_3bit, 2: unpack_2bit}
83
85
 
84
- def _unpack_to_full(self, packed_vec: torch.Tensor) -> torch.Tensor:
86
+ def _unpack_to_full(self, packed_vec: torch.Tensor,
87
+ codebook: Optional[torch.Tensor] = None) -> torch.Tensor:
85
88
  """Unpack a single packed vector to full head_dim via codebook lookup."""
86
89
  # packed_vec: [packed_dim] uint8
90
+ cb = codebook if codebook is not None else self.centroids
87
91
  unpack_fn = self._unpack_fns.get(self.bits, self._unpack_fns[4])
88
92
  indices = unpack_fn(packed_vec.unsqueeze(0), self.head_dim).squeeze(0)
89
- return self.centroids[indices.long()]
93
+ return cb[indices.long()]
90
94
 
91
95
  def forward(
92
96
  self,
@@ -136,9 +140,9 @@ class TQPagedAttentionRef:
136
140
  n_valid = end - start
137
141
 
138
142
  for pos in range(n_valid):
139
- # Unpack K → full rotated vector via codebook
143
+ # Unpack K → full rotated vector via corrected codebook
140
144
  packed_k = k_packed[phys_block, pos, kv_head]
141
- k_full = self._unpack_to_full(packed_k)
145
+ k_full = self._unpack_to_full(packed_k, self.k_centroids)
142
146
  k_even = k_full[0::2]
143
147
  k_odd = k_full[1::2]
144
148
 
@@ -214,8 +218,9 @@ if HAS_TRITON:
214
218
  # Paged block indirection
215
219
  block_tables_ptr, # [num_seqs, max_blocks_per_seq] int32/int64
216
220
  context_lens_ptr, # [num_seqs] int32/int64
217
- # Codebook
218
- centroids_ptr, # [16] float32
221
+ # Codebooks (separate for K scoring and V accumulation)
222
+ k_centroids_ptr, # [16] float32 — corrected centroids for K scoring
223
+ v_centroids_ptr, # [16] float32 — raw centroids for V accumulation
219
224
  # Output accumulators (rotated domain, even/odd split)
220
225
  out_even_ptr, # [num_seqs, num_q_heads, HALF_D] float32
221
226
  out_odd_ptr, # [num_seqs, num_q_heads, HALF_D] float32
@@ -300,8 +305,8 @@ if HAS_TRITON:
300
305
  k_packed_ptr + kp_base + half_offs
301
306
  ).to(tl.int32)
302
307
 
303
- k_even = tl.load(centroids_ptr + ((kp >> 4) & 0xF))
304
- k_odd = tl.load(centroids_ptr + (kp & 0xF))
308
+ k_even = tl.load(k_centroids_ptr + ((kp >> 4) & 0xF))
309
+ k_odd = tl.load(k_centroids_ptr + (kp & 0xF))
305
310
 
306
311
  # ── Score ──
307
312
  k_norm = tl.load(
@@ -332,8 +337,8 @@ if HAS_TRITON:
332
337
  v_packed_ptr + vp_base + half_offs
333
338
  ).to(tl.int32)
334
339
 
335
- v_even = tl.load(centroids_ptr + ((vp >> 4) & 0xF))
336
- v_odd = tl.load(centroids_ptr + (vp & 0xF))
340
+ v_even = tl.load(v_centroids_ptr + ((vp >> 4) & 0xF))
341
+ v_odd = tl.load(v_centroids_ptr + (vp & 0xF))
337
342
  v_norm = tl.load(
338
343
  v_norms_ptr
339
344
  + phys_block * stride_kn_block
@@ -371,7 +376,7 @@ if HAS_TRITON:
371
376
  k_packed_ptr, k_norms_ptr,
372
377
  v_packed_ptr, v_norms_ptr,
373
378
  block_tables_ptr, context_lens_ptr,
374
- centroids_ptr,
379
+ k_centroids_ptr, v_centroids_ptr,
375
380
  # Partial outputs: [num_q_heads, num_seqs, NUM_SPLITS, HALF_D]
376
381
  part_even_ptr, part_odd_ptr,
377
382
  # Partial softmax state: [num_q_heads, num_seqs, NUM_SPLITS]
@@ -431,8 +436,8 @@ if HAS_TRITON:
431
436
  k_packed_ptr + kp_base + half_offs
432
437
  ).to(tl.int32)
433
438
 
434
- k_even = tl.load(centroids_ptr + ((kp >> 4) & 0xF))
435
- k_odd = tl.load(centroids_ptr + (kp & 0xF))
439
+ k_even = tl.load(k_centroids_ptr + ((kp >> 4) & 0xF))
440
+ k_odd = tl.load(k_centroids_ptr + (kp & 0xF))
436
441
 
437
442
  k_norm = tl.load(
438
443
  k_norms_ptr + phys_block * stride_kn_block
@@ -457,8 +462,8 @@ if HAS_TRITON:
457
462
  v_packed_ptr + vp_base + half_offs
458
463
  ).to(tl.int32)
459
464
 
460
- v_even = tl.load(centroids_ptr + ((vp >> 4) & 0xF))
461
- v_odd = tl.load(centroids_ptr + (vp & 0xF))
465
+ v_even = tl.load(v_centroids_ptr + ((vp >> 4) & 0xF))
466
+ v_odd = tl.load(v_centroids_ptr + (vp & 0xF))
462
467
  v_norm = tl.load(
463
468
  v_norms_ptr + phys_block * stride_kn_block
464
469
  + pos * stride_kn_pos + kv_head)
@@ -560,6 +565,9 @@ class TQPagedAttention:
560
565
  self.rotation = tq.rotation
561
566
  self.rotation_T = tq.rotation.T.contiguous() # pre-transposed, contiguous
562
567
  self.centroids = tq.centroids
568
+ # Corrected centroids for unbiased inner product scoring (K path only).
569
+ # V accumulation uses raw centroids (correction only affects score bias).
570
+ self.k_centroids = tq.centroids * tq.correction_factors
563
571
  self.scale = 1.0 / math.sqrt(tq.head_dim)
564
572
  self.head_dim = tq.head_dim
565
573
  self.num_levels = tq.num_levels
@@ -577,6 +585,7 @@ class TQPagedAttention:
577
585
  head_dim=tq.head_dim,
578
586
  block_size=16,
579
587
  bits=tq.bits,
588
+ k_centroids=self.k_centroids,
580
589
  )
581
590
 
582
591
  # Block selector for sparse attention (optional)
@@ -697,7 +706,7 @@ class TQPagedAttention:
697
706
  q_even, q_odd,
698
707
  k_packed, k_norms, v_packed, v_norms,
699
708
  block_tables, context_lens,
700
- self.centroids,
709
+ self.k_centroids, self.centroids,
701
710
  part_even, part_odd, part_m, part_l,
702
711
  q_even.stride(0), q_even.stride(1),
703
712
  k_packed.stride(0), k_packed.stride(1), k_packed.stride(2),
@@ -724,7 +733,7 @@ class TQPagedAttention:
724
733
  q_even, q_odd,
725
734
  k_packed, k_norms, v_packed, v_norms,
726
735
  block_tables, context_lens,
727
- self.centroids,
736
+ self.k_centroids, self.centroids,
728
737
  out_even, out_odd,
729
738
  q_even.stride(0), q_even.stride(1),
730
739
  k_packed.stride(0), k_packed.stride(1), k_packed.stride(2),
@@ -23,7 +23,7 @@ import torch
23
23
  from dataclasses import dataclass
24
24
  from typing import Optional, Tuple
25
25
 
26
- from .codebook import get_codebook, get_theory_bounds
26
+ from .codebook import get_codebook, get_theory_bounds, get_correction_factors
27
27
  from .rotation import random_orthogonal, randomized_hadamard_matrix
28
28
  from .packing import (
29
29
  pack_4bit, unpack_4bit,
@@ -102,6 +102,12 @@ class TurboQuant:
102
102
  # Theory bounds
103
103
  self.theory_lower, self.theory_upper = get_theory_bounds(config.bits)
104
104
 
105
+ # Per-centroid correction factors for unbiased inner product estimation
106
+ correction_np = get_correction_factors(config.head_dim, config.bits)
107
+ self.correction_factors = torch.tensor(
108
+ correction_np, dtype=torch.float32, device=config.device,
109
+ )
110
+
105
111
  # Pack/unpack dispatch
106
112
  self._pack_fn = {2: pack_2bit, 3: pack_3bit, 4: pack_4bit}[config.bits]
107
113
  self._unpack_fn = {2: unpack_2bit, 3: unpack_3bit, 4: unpack_4bit}[config.bits]
@@ -211,6 +217,12 @@ class TurboQuant:
211
217
  indices = self._unpack_fn(packed, self.head_dim)
212
218
  return self.centroids[indices.long()]
213
219
 
220
+ def _pytorch_decode_corrected(self, packed: torch.Tensor) -> torch.Tensor:
221
+ """Unpack + codebook lookup with per-centroid IP bias correction."""
222
+ indices = self._unpack_fn(packed, self.head_dim)
223
+ idx_long = indices.long()
224
+ return self.centroids[idx_long] * self.correction_factors[idx_long]
225
+
214
226
  # ================================================================
215
227
  # INTERNAL: Triton path (GPU only)
216
228
  # ================================================================
@@ -295,6 +307,16 @@ class TurboQuant:
295
307
  ip_mse = (true_ip - est_ip).pow(2).mean().item()
296
308
  ip_bias = (est_ip - true_ip).mean().item()
297
309
 
310
+ # Corrected decode: apply per-centroid bias correction factors
311
+ packed_flat = packed.reshape(-1, packed.shape[-1])
312
+ norms_flat = norms.reshape(-1).float()
313
+ y_hat_corr = self._pytorch_decode_corrected(packed_flat)
314
+ x_hat_corr = torch.matmul(y_hat_corr, self.rotation)
315
+ x_hat_corr = x_hat_corr * norms_flat.unsqueeze(-1)
316
+ x_hat_corr = x_hat_corr.reshape(x.shape)
317
+ est_ip_corr = (x_hat_corr[idx_a] * x[idx_b]).sum(dim=-1)
318
+ ip_bias_corrected = (est_ip_corr - true_ip).mean().item()
319
+
298
320
  # Max absolute error
299
321
  max_err = (x - x_hat).abs().max().item()
300
322
 
@@ -308,6 +330,7 @@ class TurboQuant:
308
330
  "mse_ratio_to_lower": mse / self.theory_lower if self.theory_lower > 0 else float("inf"),
309
331
  "ip_mse": ip_mse,
310
332
  "ip_bias": ip_bias,
333
+ "ip_bias_corrected": ip_bias_corrected,
311
334
  "max_abs_error": max_err,
312
335
  "compression_vs_fp16": self.compression_ratio(),
313
336
  "compression_vs_fp8": self.compression_ratio_vs_fp8(),
File without changes
File without changes
File without changes