flashcodec-kv 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flashcodec/__init__.py +37 -0
- flashcodec/attention.py +140 -0
- flashcodec/cache.py +654 -0
- flashcodec/codecs/__init__.py +13 -0
- flashcodec/codecs/jpeg.py +159 -0
- flashcodec/codecs/png.py +107 -0
- flashcodec/patch.py +136 -0
- flashcodec/patch_attention.py +151 -0
- flashcodec/triton_attention.py +222 -0
- flashcodec_kv-1.0.0.dist-info/METADATA +430 -0
- flashcodec_kv-1.0.0.dist-info/RECORD +14 -0
- flashcodec_kv-1.0.0.dist-info/WHEEL +5 -0
- flashcodec_kv-1.0.0.dist-info/licenses/LICENSE +21 -0
- flashcodec_kv-1.0.0.dist-info/top_level.txt +1 -0
flashcodec/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""flashcodec — compress time-series KV cache tensors as "high-dimensional
|
|
2
|
+
multimedia image signals".
|
|
3
|
+
|
|
4
|
+
Two classic compression theories ported to Transformer KV cache management:
|
|
5
|
+
- mode="jpeg": blockwise 1D DCT-II over an orthonormal basis, lossy
|
|
6
|
+
frequency-domain compression (low-frequency skeleton);
|
|
7
|
+
- mode="png": causal blockwise prediction deltas, lossless/near-lossless
|
|
8
|
+
time-domain compression (anchor + Δ).
|
|
9
|
+
|
|
10
|
+
Tensor contract: KV layout (B, H, S, D), compression axis dim=-2 (seq axis).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .attention import block_basis, codec_decode_attention
|
|
14
|
+
from .cache import HAS_HF_CACHE, CodecKVCache, FlashCodecCache, \
|
|
15
|
+
FlashCodecLayer, make_codec
|
|
16
|
+
from .codecs import JPEGCodec, PNGCodec, dct_basis, dct_compress, \
|
|
17
|
+
dct_decompress, dequantize_coeffs, png_filter, png_reconstruct, \
|
|
18
|
+
quantize_coeffs
|
|
19
|
+
from .patch import apply_flash_codec, find_attention_modules, \
|
|
20
|
+
remove_flash_codec, undo_flash_codec
|
|
21
|
+
from .patch_attention import apply_codec_attention, codec_attention_stats, \
|
|
22
|
+
remove_codec_attention
|
|
23
|
+
|
|
24
|
+
__version__ = "1.0.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"apply_flash_codec", "remove_flash_codec", "undo_flash_codec",
|
|
28
|
+
"find_attention_modules",
|
|
29
|
+
"apply_codec_attention", "remove_codec_attention", "codec_attention_stats",
|
|
30
|
+
"CodecKVCache", "FlashCodecCache", "FlashCodecLayer", "make_codec",
|
|
31
|
+
"HAS_HF_CACHE", "JPEGCodec", "PNGCodec",
|
|
32
|
+
"codec_decode_attention", "block_basis",
|
|
33
|
+
"dct_basis", "dct_compress", "dct_decompress",
|
|
34
|
+
"quantize_coeffs", "dequantize_coeffs",
|
|
35
|
+
"png_filter", "png_reconstruct",
|
|
36
|
+
"__version__",
|
|
37
|
+
]
|
flashcodec/attention.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Materialization-free decode attention: attend directly on the compressed
|
|
2
|
+
archive instead of reconstructing the full KV view.
|
|
3
|
+
|
|
4
|
+
The key observation is that both codecs are **linear maps along the seq axis**:
|
|
5
|
+
every token inside an archived block is a fixed linear combination of the stored
|
|
6
|
+
rows. For a block of r tokens with stored rows ``S`` (s rows of dimension D) and
|
|
7
|
+
a basis matrix ``A`` (r x s),
|
|
8
|
+
|
|
9
|
+
K_block = A @ S (r x D)
|
|
10
|
+
|
|
11
|
+
so the attention score of a query q against token p of that block is
|
|
12
|
+
|
|
13
|
+
q . K[p] = (q . S^T) @ A[p] (k dot products instead of r),
|
|
14
|
+
|
|
15
|
+
and the block's output contribution is
|
|
16
|
+
|
|
17
|
+
sum_p prob[p] * V[p] = (prob @ A) @ S.
|
|
18
|
+
|
|
19
|
+
Both are tiny matmuls: for a single decode query the intermediates are O(S)
|
|
20
|
+
scores and O(s) coefficient weights — the (S, D) reconstructed KV view never
|
|
21
|
+
exists. jpeg uses ``A = M^T`` (r x k, the DCT-II synthesis basis) and png uses
|
|
22
|
+
``A = tril(ones(r, r))`` (anchor + cumulative deltas).
|
|
23
|
+
|
|
24
|
+
Scope: decoding (query length 1) against jpeg/png archives with the raw tail
|
|
25
|
+
handled by the standard path. Prefill still goes through the materialized view —
|
|
26
|
+
the coefficient-domain form would blow up the score tensor there.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
import torch
|
|
31
|
+
|
|
32
|
+
from .codecs import dequantize_coeffs, dct_basis
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def block_basis(codec, block_size, device=None, dtype=torch.float32):
|
|
36
|
+
"""Synthesis matrix A (r, s) mapping the stored rows of an archived block to
|
|
37
|
+
its r tokens."""
|
|
38
|
+
r = block_size
|
|
39
|
+
if codec.name == "jpeg":
|
|
40
|
+
return dct_basis(r, codec.keep_freqs, device=device, dtype=dtype).t()
|
|
41
|
+
tri = torch.tril(torch.ones(r, r, dtype=dtype, device=device))
|
|
42
|
+
return tri
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _stored_rows(stream, dtype=torch.float32):
|
|
46
|
+
"""Stored rows of the archived blocks: (B, H, n, s, D). jpeg-int8 is
|
|
47
|
+
dequantized straight into ``dtype`` (bf16 on half-precision models: halves
|
|
48
|
+
the traffic and uses the tensor cores); png stays fp32 because its lossless
|
|
49
|
+
promise forbids down-casting the deltas."""
|
|
50
|
+
n = stream.n_blocks
|
|
51
|
+
if n == 0:
|
|
52
|
+
return None
|
|
53
|
+
codec = stream.codec
|
|
54
|
+
if codec.name == "jpeg":
|
|
55
|
+
rows = stream._coeffs[:, :, :n]
|
|
56
|
+
if codec.quantized:
|
|
57
|
+
return dequantize_coeffs(rows, stream._scales[:, :, :n], dtype)
|
|
58
|
+
return rows
|
|
59
|
+
# png: anchor + deltas are already the stored rows; stack them on the row axis
|
|
60
|
+
return torch.cat([stream._anchors[:, :, :n].unsqueeze(-2).float(),
|
|
61
|
+
stream._deltas[:, :, :n].float()], dim=-2)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def codec_decode_attention(query, key_stream, value_stream, scale=None,
|
|
65
|
+
impl="auto"):
|
|
66
|
+
"""Attention for one new query token against a codec archive, **without**
|
|
67
|
+
materializing the reconstructed KV.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
query: (B, Hq, 1, D) — the query for the current step.
|
|
71
|
+
key_stream, value_stream: ``_CodecStream`` instances of one layer.
|
|
72
|
+
scale: attention scale (default ``D ** -0.5``).
|
|
73
|
+
impl: ``"auto"`` (Triton when available and supported, else torch),
|
|
74
|
+
``"torch"`` or ``"triton"``.
|
|
75
|
+
Returns:
|
|
76
|
+
(B, Hq, 1, D) attention output, in the query dtype.
|
|
77
|
+
|
|
78
|
+
GQA is handled by grouping the query heads (no per-head copy of the stored
|
|
79
|
+
rows). Memory scales with scores + stored rows, never with ``S * D``
|
|
80
|
+
reconstructed KV — for jpeg with k < r that is r/k times smaller than raw.
|
|
81
|
+
png stores r rows per block (anchor + deltas), so it is numerically correct
|
|
82
|
+
here but gains nothing in memory; the coefficient path is a jpeg win.
|
|
83
|
+
"""
|
|
84
|
+
B, Hq, Sq, D = query.shape
|
|
85
|
+
if Sq != 1:
|
|
86
|
+
raise ValueError("codec_decode_attention only supports a single query "
|
|
87
|
+
f"token (got Sq={Sq}); prefill should use the "
|
|
88
|
+
"materialized view")
|
|
89
|
+
scale = D ** -0.5 if scale is None else scale
|
|
90
|
+
|
|
91
|
+
if impl in ("auto", "triton") and key_stream.codec.name == "jpeg":
|
|
92
|
+
from .triton_attention import codec_decode_attention_triton
|
|
93
|
+
out = codec_decode_attention_triton(query, key_stream, value_stream,
|
|
94
|
+
scale=scale)
|
|
95
|
+
if out is not None:
|
|
96
|
+
return out
|
|
97
|
+
if impl == "triton":
|
|
98
|
+
raise RuntimeError("triton implementation does not support this "
|
|
99
|
+
"case (see codec_decode_attention_triton)")
|
|
100
|
+
|
|
101
|
+
krows = _stored_rows(key_stream, query.dtype)
|
|
102
|
+
vrows = _stored_rows(value_stream, query.dtype)
|
|
103
|
+
if krows is None and key_stream.raw is None: # empty cache
|
|
104
|
+
return torch.zeros_like(query)
|
|
105
|
+
Hk = krows.shape[1] if krows is not None else key_stream.raw.shape[1]
|
|
106
|
+
assert Hq % Hk == 0, \
|
|
107
|
+
f"query heads ({Hq}) must be a multiple of kv heads ({Hk})"
|
|
108
|
+
G = Hq // Hk
|
|
109
|
+
wd = krows.dtype if krows is not None else torch.float32
|
|
110
|
+
qg = query.to(wd).reshape(B, Hk, G, 1, D)
|
|
111
|
+
|
|
112
|
+
logits, out = [], torch.zeros(B, Hk, G, D, dtype=wd, device=qg.device)
|
|
113
|
+
|
|
114
|
+
if krows is not None:
|
|
115
|
+
A = block_basis(key_stream.codec, key_stream.block_size,
|
|
116
|
+
device=qg.device, dtype=wd)
|
|
117
|
+
s = torch.einsum("bhgqd,bhnsd->bhgns", qg, krows) # (B,Hk,G,n,s)
|
|
118
|
+
logits.append(torch.einsum("bhgns,rs->bhgnr", s, A).flatten(-2))
|
|
119
|
+
|
|
120
|
+
if key_stream.raw is not None: # raw tail (full fidelity)
|
|
121
|
+
logits.append(torch.einsum("bhgqd,bhtd->bhgt", qg,
|
|
122
|
+
key_stream.raw.to(wd)))
|
|
123
|
+
|
|
124
|
+
if not logits: # empty cache
|
|
125
|
+
return torch.zeros_like(query)
|
|
126
|
+
|
|
127
|
+
probs = torch.softmax(torch.cat(logits, dim=-1).float() * scale,
|
|
128
|
+
dim=-1).to(wd)
|
|
129
|
+
pos = 0
|
|
130
|
+
if krows is not None:
|
|
131
|
+
n, r = krows.shape[2], key_stream.block_size
|
|
132
|
+
p_blk = probs[..., :n * r].reshape(B, Hk, G, n, r)
|
|
133
|
+
w = torch.einsum("bhgnr,rs->bhgns", p_blk, A) # (B,Hk,G,n,s)
|
|
134
|
+
out = out + torch.einsum("bhgns,bhnsd->bhgd", w, vrows)
|
|
135
|
+
pos = n * r
|
|
136
|
+
if key_stream.raw is not None:
|
|
137
|
+
p_raw = probs[..., pos:]
|
|
138
|
+
out = out + torch.einsum("bhgt,bhtd->bhgd", p_raw,
|
|
139
|
+
value_stream.raw.to(wd))
|
|
140
|
+
return out.reshape(B, Hq, 1, D).to(query.dtype)
|