aither-kvcache 2.0.1__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.
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/PKG-INFO +1 -1
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/vllm/backend.py +1 -1
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/vllm/engine.py +26 -2
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/vllm/hooks.py +67 -49
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/vllm/plugin.py +20 -2
- aither_kvcache-2.2.0/aither_kvcache/vllm/tenant_isolation.py +364 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache.egg-info/PKG-INFO +1 -1
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache.egg-info/SOURCES.txt +1 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/pyproject.toml +1 -1
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/turboquant/__init__.py +3 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/turboquant/codebook.py +60 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/turboquant/fused_attention.py +27 -18
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/turboquant/quantizer.py +24 -1
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/LICENSE +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/README.md +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/__init__.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/bench.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/block_selector.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/codebook.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/eviction_advisor.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/fused_attention.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/fused_kv_update.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/hybrid_quantizer.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/kvcache_graph.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/packing.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/quantizer.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/rotation.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/triattention/__init__.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/triattention/attention.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/triattention/cache.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/triattention/calibration.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/triattention/config.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/triattention/encoder.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/triattention/scorer.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/triattention/spectral.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/triton_ops.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/vllm/__init__.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/vllm/cache.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/vllm/eviction_plugin.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache/vllm/sitecustomize.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache.egg-info/dependency_links.txt +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache.egg-info/entry_points.txt +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache.egg-info/requires.txt +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/aither_kvcache.egg-info/top_level.txt +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/setup.cfg +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/tests/test_core.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/tests/test_fused.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/tests/test_hybrid_hooks.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/tests/test_kvcache_graph.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/tests/test_triattention.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/turboquant/bench.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/turboquant/packing.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/turboquant/rotation.py +0 -0
- {aither_kvcache-2.0.1 → aither_kvcache-2.2.0}/turboquant/triton_ops.py +0 -0
|
@@ -133,6 +133,27 @@ def _tq_dim_for_head(head_size: int) -> int:
|
|
|
133
133
|
return packed_size(head_size, _TQ_BITS) + 4
|
|
134
134
|
|
|
135
135
|
|
|
136
|
+
def _align_page_size(raw_bytes: int) -> int:
|
|
137
|
+
"""Round page size up to the next power of 2 for cross-layer divisibility.
|
|
138
|
+
|
|
139
|
+
vLLM's unify_kv_cache_spec_page_size() requires all layer page sizes to be
|
|
140
|
+
divisible by the max page size. With heterogeneous head dims (e.g. Gemma 4:
|
|
141
|
+
256 + 512), the smaller layers produce page sizes that don't divide the
|
|
142
|
+
larger ones. Padding to next power-of-2 ensures any layer's padded page
|
|
143
|
+
size divides any other's (both are powers of 2 -> larger divides by smaller).
|
|
144
|
+
|
|
145
|
+
For uniform models, this is a no-op if page size is already a power of 2,
|
|
146
|
+
and at worst wastes a few bytes per block (acceptable for correctness).
|
|
147
|
+
"""
|
|
148
|
+
if raw_bytes <= 0:
|
|
149
|
+
return raw_bytes
|
|
150
|
+
# Next power of 2 >= raw_bytes
|
|
151
|
+
p = 1
|
|
152
|
+
while p < raw_bytes:
|
|
153
|
+
p <<= 1
|
|
154
|
+
return p
|
|
155
|
+
|
|
156
|
+
|
|
136
157
|
def _patch_page_size() -> bool:
|
|
137
158
|
"""
|
|
138
159
|
Patch real_page_size_bytes on both AttentionSpec and FullAttentionSpec.
|
|
@@ -148,8 +169,11 @@ def _patch_page_size() -> bool:
|
|
|
148
169
|
|
|
149
170
|
@property
|
|
150
171
|
def _tq_real_page_size_bytes(self):
|
|
151
|
-
"""Return TQ-compressed page size (handles both uniform and hybrid).
|
|
152
|
-
|
|
172
|
+
"""Return TQ-compressed page size (handles both uniform and hybrid).
|
|
173
|
+
Pads to ensure cross-layer divisibility for heterogeneous head dims
|
|
174
|
+
(e.g. Gemma 4: head_dim=256 local + head_dim=512 global)."""
|
|
175
|
+
raw = _tq_page_size_bytes(self.block_size, self.num_kv_heads, self.head_size)
|
|
176
|
+
return _align_page_size(raw)
|
|
153
177
|
|
|
154
178
|
# Patch BOTH classes -- FullAttentionSpec shadows parent's property
|
|
155
179
|
AttentionSpec.real_page_size_bytes = _tq_real_page_size_bytes
|
|
@@ -29,7 +29,7 @@ Set AITHER_TQ_DEBUG_STEPS=N to log the first N forward calls per layer-0
|
|
|
29
29
|
import logging
|
|
30
30
|
import os
|
|
31
31
|
import sys
|
|
32
|
-
from typing import Optional
|
|
32
|
+
from typing import Dict, Optional, Tuple
|
|
33
33
|
|
|
34
34
|
import torch
|
|
35
35
|
|
|
@@ -83,8 +83,9 @@ _hooks_applied = False
|
|
|
83
83
|
_original_forward = None # Saved reference to TritonAttentionImpl.forward
|
|
84
84
|
|
|
85
85
|
# TQ state shared across all layers (class-level in the patched impl)
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
# Per-head-dim quantizers for heterogeneous models (e.g. Gemma 4: 256 + 512)
|
|
87
|
+
_tq_quantizers: Dict[int, object] = {} # head_dim -> TurboQuant instance
|
|
88
|
+
_tq_packed_dims: Dict[int, int] = {} # head_dim -> packed bytes per head
|
|
88
89
|
_tq_fused_attn: dict = {} # layer_idx -> TQPagedAttention instance
|
|
89
90
|
_primary_k_norms: Optional[torch.Tensor] = None # None for hybrid (norms embedded)
|
|
90
91
|
_primary_v_norms: Optional[torch.Tensor] = None
|
|
@@ -96,6 +97,18 @@ _prefill_k_buf: Optional[torch.Tensor] = None
|
|
|
96
97
|
_prefill_v_buf: Optional[torch.Tensor] = None
|
|
97
98
|
_PREFILL_BUF_BLOCKS = 512
|
|
98
99
|
|
|
100
|
+
def _get_tq(head_size: int) -> Tuple[object, int]:
|
|
101
|
+
"""Get quantizer and packed_dim for a given head_size."""
|
|
102
|
+
return _tq_quantizers[head_size], _tq_packed_dims[head_size]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _any_tq() -> Optional[object]:
|
|
106
|
+
"""Get any initialized quantizer (for operations that don't care about head_dim)."""
|
|
107
|
+
if _tq_quantizers:
|
|
108
|
+
return next(iter(_tq_quantizers.values()))
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
99
112
|
# Custom op flag -- set by _register_custom_ops()
|
|
100
113
|
_USE_CUSTOM_OP = False
|
|
101
114
|
|
|
@@ -135,10 +148,9 @@ def _register_custom_ops():
|
|
|
135
148
|
num_actual: int,
|
|
136
149
|
) -> torch.Tensor:
|
|
137
150
|
"""Encode new token + fused TQ attention. No graph break.
|
|
138
|
-
Accesses module globals (
|
|
151
|
+
Accesses module globals (_tq_quantizers, _tq_fused_attn) in body --
|
|
139
152
|
these are initialized during vLLM warmup before CUDA graph capture."""
|
|
140
|
-
tq =
|
|
141
|
-
packed_dim = head_size // 2
|
|
153
|
+
tq, packed_dim = _get_tq(head_size)
|
|
142
154
|
key_cache = kv_cache[:, 0]
|
|
143
155
|
value_cache = kv_cache[:, 1]
|
|
144
156
|
block_size = key_cache.shape[1]
|
|
@@ -260,8 +272,7 @@ def _register_hybrid_custom_ops():
|
|
|
260
272
|
"""
|
|
261
273
|
global _hybrid_dk_buf, _hybrid_dv_buf
|
|
262
274
|
|
|
263
|
-
tq =
|
|
264
|
-
packed_dim = _tq_packed_dim
|
|
275
|
+
tq, packed_dim = _get_tq(head_size)
|
|
265
276
|
key_cache = kv_cache[:, 0]
|
|
266
277
|
value_cache = kv_cache[:, 1]
|
|
267
278
|
block_size = key_cache.shape[1]
|
|
@@ -369,32 +380,38 @@ def _tq_init_layer(self_impl):
|
|
|
369
380
|
|
|
370
381
|
@torch.compiler.disable
|
|
371
382
|
def _ensure_quantizer(device, head_size, num_heads, num_kv_heads):
|
|
372
|
-
"""Lazy-init TQ quantizer
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
383
|
+
"""Lazy-init TQ quantizer for a given head_size. Creates/caches per unique head_dim.
|
|
384
|
+
Supports heterogeneous head dimensions (e.g. Gemma 4: 256 local + 512 global)."""
|
|
385
|
+
global _num_layers
|
|
386
|
+
if head_size in _tq_quantizers:
|
|
387
|
+
return # Already initialized for this head_dim
|
|
376
388
|
|
|
377
|
-
|
|
389
|
+
if not _tq_quantizers: # First quantizer -- snapshot layer count
|
|
390
|
+
_num_layers = _layer_counter
|
|
378
391
|
|
|
379
392
|
if _TQ_IS_HYBRID:
|
|
380
393
|
from ..hybrid_quantizer import HybridTurboQuant
|
|
381
|
-
|
|
394
|
+
tq = HybridTurboQuant(
|
|
382
395
|
head_dim=head_size, mode=_TQ_MODE, device=str(device),
|
|
383
396
|
num_kv_heads=num_kv_heads)
|
|
384
|
-
|
|
385
|
-
|
|
397
|
+
tq.calibrate_uniform()
|
|
398
|
+
pd = tq.layout.packed_dim
|
|
399
|
+
_tq_quantizers[head_size] = tq
|
|
400
|
+
_tq_packed_dims[head_size] = pd
|
|
386
401
|
logger.info("TQ hybrid quantizer initialized: head_dim=%d, mode=%s, "
|
|
387
|
-
"packed_dim=%d, %d layers",
|
|
388
|
-
head_size, _TQ_MODE,
|
|
402
|
+
"packed_dim=%d, %d layers (total quantizers: %d)",
|
|
403
|
+
head_size, _TQ_MODE, pd, _num_layers, len(_tq_quantizers))
|
|
389
404
|
else:
|
|
390
405
|
from ..quantizer import TurboQuant
|
|
391
406
|
from ..packing import packed_size
|
|
392
|
-
|
|
407
|
+
tq = TurboQuant(
|
|
393
408
|
head_dim=head_size, bits=_TQ_BITS, device=str(device))
|
|
394
|
-
|
|
409
|
+
pd = packed_size(head_size, _TQ_BITS)
|
|
410
|
+
_tq_quantizers[head_size] = tq
|
|
411
|
+
_tq_packed_dims[head_size] = pd
|
|
395
412
|
logger.info("TQ quantizer initialized: head_dim=%d, bits=%d, "
|
|
396
|
-
"packed_dim=%d, %d layers",
|
|
397
|
-
head_size, _TQ_BITS,
|
|
413
|
+
"packed_dim=%d, %d layers (total quantizers: %d)",
|
|
414
|
+
head_size, _TQ_BITS, pd, _num_layers, len(_tq_quantizers))
|
|
398
415
|
|
|
399
416
|
|
|
400
417
|
@torch.compiler.disable
|
|
@@ -420,12 +437,11 @@ def _ensure_norms(num_layers, num_blocks, block_size, num_kv_heads, device):
|
|
|
420
437
|
def _tq_encode_phase(layer_idx, key, value, kv_cache, slot_mapping):
|
|
421
438
|
"""Encode new K,V tokens into TQ primary cache. Runs eagerly.
|
|
422
439
|
Supports both uniform (separate norms) and hybrid (norms embedded) modes."""
|
|
423
|
-
global
|
|
424
|
-
|
|
425
|
-
if
|
|
440
|
+
global _primary_k_norms, _primary_v_norms
|
|
441
|
+
head_size = key.shape[-1]
|
|
442
|
+
if head_size not in _tq_quantizers:
|
|
426
443
|
return
|
|
427
|
-
|
|
428
|
-
packed_dim = _tq_packed_dim
|
|
444
|
+
tq, packed_dim = _get_tq(head_size)
|
|
429
445
|
|
|
430
446
|
key_cache = kv_cache[:, 0] # [blocks, block_size, kv_heads, tq_dim]
|
|
431
447
|
value_cache = kv_cache[:, 1]
|
|
@@ -508,18 +524,17 @@ def _tq_decompress_active(layer_idx, kv_cache, block_table, num_kv_heads,
|
|
|
508
524
|
"""Decompress active TQ blocks into a temporary fp8/bf16 buffer.
|
|
509
525
|
Returns a standard-shaped kv_cache tensor for TritonAttention.
|
|
510
526
|
Runs eagerly."""
|
|
511
|
-
global
|
|
527
|
+
global _primary_k_norms, _primary_v_norms
|
|
512
528
|
global _prefill_k_buf, _prefill_v_buf
|
|
513
529
|
|
|
514
|
-
|
|
515
|
-
if tq is None:
|
|
530
|
+
if head_size not in _tq_quantizers:
|
|
516
531
|
return kv_cache # Fallback: return original (will fail but shouldn't happen)
|
|
532
|
+
tq, packed_dim = _get_tq(head_size)
|
|
517
533
|
|
|
518
534
|
key_cache = kv_cache[:, 0]
|
|
519
535
|
value_cache = kv_cache[:, 1]
|
|
520
536
|
num_blocks = key_cache.shape[0]
|
|
521
537
|
block_size = key_cache.shape[1]
|
|
522
|
-
packed_dim = head_size // 2
|
|
523
538
|
|
|
524
539
|
# Allocate or reuse decompress buffers
|
|
525
540
|
need_realloc = (
|
|
@@ -576,15 +591,16 @@ def _tq_fused_decode(layer_idx, query, kv_cache, attn_metadata, output,
|
|
|
576
591
|
num_heads, num_kv_heads, head_size, scale):
|
|
577
592
|
"""Fused TQ decode using TQPagedAttention kernel. No decompress needed.
|
|
578
593
|
Runs eagerly (graph break)."""
|
|
579
|
-
global _tq_fused_attn,
|
|
594
|
+
global _tq_fused_attn, _primary_k_norms, _primary_v_norms
|
|
595
|
+
|
|
596
|
+
tq, packed_dim = _get_tq(head_size)
|
|
580
597
|
|
|
581
598
|
# Lazy-init fused attention for this layer
|
|
582
599
|
if layer_idx not in _tq_fused_attn:
|
|
583
600
|
from ..fused_attention import TQPagedAttention
|
|
584
|
-
_tq_fused_attn[layer_idx] = TQPagedAttention(
|
|
601
|
+
_tq_fused_attn[layer_idx] = TQPagedAttention(tq, num_heads)
|
|
585
602
|
|
|
586
603
|
fused = _tq_fused_attn[layer_idx]
|
|
587
|
-
packed_dim = head_size // 2
|
|
588
604
|
|
|
589
605
|
key_cache = kv_cache[:, 0]
|
|
590
606
|
value_cache = kv_cache[:, 1]
|
|
@@ -609,8 +625,7 @@ def _encode_inline(layer_idx, key, value, kv_cache, slot_mapping):
|
|
|
609
625
|
"""Encode K/V into TQ cache. No decorator -- called from _tq_decode_step.
|
|
610
626
|
Branchless for decode: no .any() CPU-GPU syncs. Works for any N tokens.
|
|
611
627
|
Supports both uniform and hybrid modes."""
|
|
612
|
-
tq =
|
|
613
|
-
packed_dim = _tq_packed_dim
|
|
628
|
+
tq, packed_dim = _get_tq(key.shape[-1])
|
|
614
629
|
key_cache = kv_cache[:, 0]
|
|
615
630
|
value_cache = kv_cache[:, 1]
|
|
616
631
|
block_size = key_cache.shape[1]
|
|
@@ -658,14 +673,14 @@ def _tq_decode_step(layer_idx, query, key, value, kv_cache, attn_metadata,
|
|
|
658
673
|
_encode_inline(layer_idx, key, value, kv_cache,
|
|
659
674
|
attn_metadata.slot_mapping)
|
|
660
675
|
|
|
676
|
+
tq, packed_dim = _get_tq(head_size)
|
|
677
|
+
|
|
661
678
|
# Fused attention -- dict lookup for cached TQPagedAttention
|
|
662
679
|
fused = _tq_fused_attn.get(layer_idx)
|
|
663
680
|
if fused is None:
|
|
664
681
|
from ..fused_attention import TQPagedAttention
|
|
665
|
-
fused = TQPagedAttention(
|
|
682
|
+
fused = TQPagedAttention(tq, num_heads)
|
|
666
683
|
_tq_fused_attn[layer_idx] = fused
|
|
667
|
-
|
|
668
|
-
packed_dim = _tq_packed_dim
|
|
669
684
|
key_cache = kv_cache[:, 0]
|
|
670
685
|
value_cache = kv_cache[:, 1]
|
|
671
686
|
|
|
@@ -715,7 +730,12 @@ def _tq_prefill_sdpa(layer_idx, query, key, value, kv_cache, attn_metadata,
|
|
|
715
730
|
Manual GQA expansion. is_causal=True for prefill, is_causal=False for decode
|
|
716
731
|
(single query attends to all prior positions -- no mask needed)."""
|
|
717
732
|
import torch.nn.functional as F
|
|
718
|
-
global
|
|
733
|
+
global _primary_k_norms, _primary_v_norms
|
|
734
|
+
|
|
735
|
+
if head_size not in _tq_quantizers:
|
|
736
|
+
output.fill_(0)
|
|
737
|
+
return output
|
|
738
|
+
tq_inst, packed_dim = _get_tq(head_size)
|
|
719
739
|
|
|
720
740
|
num_actual = attn_metadata.num_actual_tokens
|
|
721
741
|
cu_seqlens = attn_metadata.query_start_loc
|
|
@@ -729,7 +749,6 @@ def _tq_prefill_sdpa(layer_idx, query, key, value, kv_cache, attn_metadata,
|
|
|
729
749
|
key_cache = kv_cache[:, 0]
|
|
730
750
|
value_cache = kv_cache[:, 1]
|
|
731
751
|
block_size = key_cache.shape[1]
|
|
732
|
-
packed_dim = _tq_packed_dim
|
|
733
752
|
block_table = attn_metadata.block_table
|
|
734
753
|
|
|
735
754
|
for i in range(num_seqs):
|
|
@@ -746,7 +765,7 @@ def _tq_prefill_sdpa(layer_idx, query, key, value, kv_cache, attn_metadata,
|
|
|
746
765
|
# First prefill: all context is in current K/V
|
|
747
766
|
ki = k_raw[q_start:q_end]
|
|
748
767
|
vi = v_raw[q_start:q_end]
|
|
749
|
-
elif k_raw is not None and
|
|
768
|
+
elif k_raw is not None and tq_inst is not None:
|
|
750
769
|
# Continuation: decompress prior blocks + append current
|
|
751
770
|
prior_len = ctx_len - q_len
|
|
752
771
|
prior_blocks = (prior_len + block_size - 1) // block_size
|
|
@@ -754,7 +773,7 @@ def _tq_prefill_sdpa(layer_idx, query, key, value, kv_cache, attn_metadata,
|
|
|
754
773
|
bt = bt[bt >= 0]
|
|
755
774
|
|
|
756
775
|
if bt.numel() > 0:
|
|
757
|
-
tq =
|
|
776
|
+
tq = tq_inst
|
|
758
777
|
H = num_kv_heads
|
|
759
778
|
|
|
760
779
|
kp = key_cache[bt, :, :, :packed_dim]
|
|
@@ -843,7 +862,7 @@ def _make_tq_forward(original_fwd):
|
|
|
843
862
|
# Runs during BOTH normal execution AND CUDA graph capture.
|
|
844
863
|
# Falls through to slow path for: init, prefill, missing key/value.
|
|
845
864
|
if (hasattr(self, '_tq_layer_idx')
|
|
846
|
-
and
|
|
865
|
+
and self.head_size in _tq_quantizers
|
|
847
866
|
and attn_metadata is not None
|
|
848
867
|
and output is not None
|
|
849
868
|
and key is not None and value is not None
|
|
@@ -892,7 +911,7 @@ def _make_tq_forward(original_fwd):
|
|
|
892
911
|
output_block_scale=output_block_scale)
|
|
893
912
|
|
|
894
913
|
# --- Init quantizer + norms (guarded: no graph break after first call) ---
|
|
895
|
-
if
|
|
914
|
+
if self.head_size not in _tq_quantizers:
|
|
896
915
|
_ensure_quantizer(query.device, self.head_size,
|
|
897
916
|
self.num_heads, self.num_kv_heads)
|
|
898
917
|
if not _TQ_IS_HYBRID and _primary_k_norms is None:
|
|
@@ -946,7 +965,7 @@ def _make_tq_forward(original_fwd):
|
|
|
946
965
|
# --- Phase 2a: Decode (fused TQ attention for uniform, custom op for hybrid) ---
|
|
947
966
|
# Hybrid mode uses tq::hybrid_decode_step custom op (zero graph breaks),
|
|
948
967
|
# or falls through to encode+SDPA path if custom op not available.
|
|
949
|
-
if is_decode and
|
|
968
|
+
if is_decode and self.head_size in _tq_quantizers and _TQ_IS_HYBRID:
|
|
950
969
|
if _USE_HYBRID_CUSTOM_OP and key is not None and value is not None:
|
|
951
970
|
num_actual = attn_metadata.num_actual_tokens
|
|
952
971
|
try:
|
|
@@ -965,7 +984,7 @@ def _make_tq_forward(original_fwd):
|
|
|
965
984
|
self._tq_layer_idx, e)
|
|
966
985
|
# Fall through to graph-break SDPA
|
|
967
986
|
|
|
968
|
-
if is_decode and
|
|
987
|
+
if is_decode and self.head_size in _tq_quantizers and not _TQ_IS_HYBRID:
|
|
969
988
|
num_actual = attn_metadata.num_actual_tokens
|
|
970
989
|
try:
|
|
971
990
|
if _TQ_DEBUG and self._tq_layer_idx == 0:
|
|
@@ -1119,8 +1138,7 @@ def _debug_prefill_sdpa(layer_idx, query, key, value, kv_cache, attn_metadata,
|
|
|
1119
1138
|
file=sys.stderr, flush=True)
|
|
1120
1139
|
|
|
1121
1140
|
# Decode first few vectors and check
|
|
1122
|
-
tq =
|
|
1123
|
-
packed_dim = _tq_packed_dim
|
|
1141
|
+
tq, packed_dim = _get_tq(head_size)
|
|
1124
1142
|
kc = kv_cache[:, 0]
|
|
1125
1143
|
kp0 = kc[block_ids[0], 0, 0, :packed_dim] # first slot, first head
|
|
1126
1144
|
kn0 = _primary_k_norms[0, block_ids[0], 0, 0] # first slot, first head
|
|
@@ -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"]
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "aither-kvcache"
|
|
7
|
-
version = "2.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
|
|
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
|
|
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
|
-
#
|
|
218
|
-
|
|
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(
|
|
304
|
-
k_odd = tl.load(
|
|
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(
|
|
336
|
-
v_odd = tl.load(
|
|
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
|
-
|
|
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(
|
|
435
|
-
k_odd = tl.load(
|
|
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(
|
|
461
|
-
v_odd = tl.load(
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|