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 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
+ ]
@@ -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)