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/cache.py ADDED
@@ -0,0 +1,654 @@
1
+ """Unified codec KV cache manager.
2
+
3
+ Design:
4
+ - Each KV stream (key/value x layer) keeps internally a ``raw_buffer``
5
+ (tail tokens that do not fill a full block, held at full fidelity) and a
6
+ ``compressed_storage`` (compressed archive of completed blocks: jpeg keeps
7
+ low-frequency coefficients / png keeps anchors + deltas);
8
+ - Once block_size tokens have accumulated, batched archive compression is
9
+ triggered (a single GEMM, no loops);
10
+ - Causal safety: only "complete blocks" are archived, the current partial
11
+ block always stays raw — future perturbations at any point of incremental
12
+ decoding never rewrite already-archived tensors;
13
+ - Dual interface: ``forward(key_seq, value_seq)`` tiles the full sequence
14
+ block by block; ``decode_step(new_k, new_v)`` appends a single step and
15
+ archives automatically across block boundaries;
16
+ - Duck-typed compatible with HuggingFace ``Cache`` (update / get_seq_length /
17
+ key_cache / value_cache), so it can be injected directly into generate.
18
+
19
+ Tensor contract: KV layout (batch, num_heads, seq_len, head_dim), compression
20
+ axis dim=-2.
21
+ """
22
+
23
+ import torch
24
+
25
+ from .codecs import JPEGCodec, PNGCodec, dct_decompress, png_reconstruct
26
+
27
+ try: # native transformers 5.x Cache ecosystem (layer container mode)
28
+ from functools import partial
29
+
30
+ from transformers.cache_utils import Cache as _HFCache
31
+ from transformers.cache_utils import DynamicLayer as _HFDynamicLayer
32
+ HAS_HF_CACHE = True
33
+ except Exception: # fallback when transformers is absent
34
+ _HFCache = object
35
+ _HFDynamicLayer = object
36
+ HAS_HF_CACHE = False
37
+
38
+
39
+ def make_codec(mode="jpeg", block_size=8, keep_freqs=2, keep_ratio=1.0,
40
+ quant_bits=0):
41
+ """Build a codec instance from a mode name.
42
+
43
+ ``quant_bits=8`` switches the jpeg archive to int8 coefficients with a
44
+ per-(block, frequency) scale (real byte-level saving); png ignores it (its
45
+ lossless tier stores fp32 deltas on purpose)."""
46
+ if mode == "jpeg":
47
+ return JPEGCodec(block_size=block_size, keep_freqs=keep_freqs,
48
+ quant_bits=quant_bits)
49
+ if mode == "png":
50
+ if quant_bits:
51
+ raise ValueError("quant_bits is only supported for mode='jpeg' "
52
+ "(the png lossless tier must keep fp32 deltas)")
53
+ return PNGCodec(block_size=block_size, keep_ratio=keep_ratio)
54
+ raise ValueError(f"unknown mode: {mode!r} (expected 'jpeg' | 'png')")
55
+
56
+
57
+ class _CodecStream:
58
+ """Single (B,H,S,D) stream: raw tail + capacity-doubling preallocated archive.
59
+
60
+ Archive storage uses std::vector-style geometric capacity doubling
61
+ (starting at 16 blocks, doubling when full): each full block is written
62
+ once as an in-place slice (no reallocation), and read() runs a single
63
+ large GEMM / cumsum over the valid region — eliminating per-archive
64
+ Python loops and fragmented kernel launches entirely.
65
+ jpeg: ``_coeffs (B,H,capacity,k,D)`` (input dtype), or int8 +
66
+ ``_scales (B,H,capacity,k,1)`` fp32 when the codec is quantized;
67
+ png: ``_deltas (B,H,capacity,r-1,D)`` (fp32) + ``_anchors (B,H,capacity,D)``.
68
+ """
69
+
70
+ _INITIAL_BLOCKS = 16
71
+
72
+ def __init__(self, codec):
73
+ self.codec = codec
74
+ self.raw = None # (B,H,t,D), t < block_size
75
+ self.n_archived = 0 # total number of archived tokens
76
+ self.n_blocks = 0 # blocks used in the archive region
77
+ self.meta = None # (B, H, D, elem_size)
78
+ self.dtype = None # stream input dtype (restored; SDPA dtype contract)
79
+ self._capacity = 0 # blocks allocated in the archive (geometric growth)
80
+ self._coeffs = None # jpeg: (B,H,capacity,k,D)
81
+ self._scales = None # jpeg+int8: (B,H,capacity,k,1) fp32
82
+ self._deltas = None # png: (B,H,capacity,r-1,D) fp32
83
+ self._anchors = None # png: (B,H,capacity,D)
84
+ # Incrementally decoded archive (append-only, one decode per new
85
+ # block instead of re-decoding the whole history on every read).
86
+ self._rec = None # (B,H,_rec_capacity,D) input dtype
87
+ self._rec_blocks = 0 # blocks already materialized into _rec
88
+ self._rec_capacity = 0 # tokens allocated in _rec
89
+
90
+ @property
91
+ def block_size(self):
92
+ return self.codec.block_size
93
+
94
+ def _archive_spec(self, cap, B, H, D):
95
+ """(attr, shape, dtype) for every archive tensor at capacity `cap`."""
96
+ if self.codec.name == "jpeg":
97
+ k = self.codec.keep_freqs
98
+ if self.codec.quantized:
99
+ return [("_coeffs", (B, H, cap, k, D), torch.int8),
100
+ ("_scales", (B, H, cap, k, 1), torch.float32)]
101
+ return [("_coeffs", (B, H, cap, k, D), self.dtype)]
102
+ r = self.block_size
103
+ return [("_deltas", (B, H, cap, r - 1, D), torch.float32),
104
+ ("_anchors", (B, H, cap, D), self.dtype)]
105
+
106
+ def _grow(self, need_blocks, like):
107
+ """Geometric growth: when capacity is insufficient, double until
108
+ >= need_blocks and copy the old data over verbatim."""
109
+ if need_blocks <= self._capacity:
110
+ return
111
+ cap = max(self._INITIAL_BLOCKS, 1)
112
+ while cap < need_blocks:
113
+ cap *= 2
114
+ B, H, D = like.shape[0], like.shape[1], like.shape[-1]
115
+ for attr, shape, dtype in self._archive_spec(cap, B, H, D):
116
+ new = torch.empty(shape, dtype=dtype, device=like.device)
117
+ if self.n_blocks:
118
+ new[:, :, :self.n_blocks].copy_(
119
+ getattr(self, attr)[:, :, :self.n_blocks])
120
+ setattr(self, attr, new)
121
+ self._capacity = cap
122
+
123
+ def _archive(self, head):
124
+ """Compress full blocks head (..., n_full*r, D) into the archive
125
+ region (in-place slice)."""
126
+ n_full = head.shape[-2] // self.block_size
127
+ self._grow(self.n_blocks + n_full, head)
128
+ sl = slice(self.n_blocks, self.n_blocks + n_full)
129
+ if self.codec.name == "jpeg":
130
+ coeffs = self.codec.compress(head)
131
+ q, s = self.codec.quantize(coeffs)
132
+ if self.codec.quantized:
133
+ self._coeffs[:, :, sl].copy_(q)
134
+ self._scales[:, :, sl].copy_(s)
135
+ else:
136
+ self._coeffs[:, :, sl].copy_(coeffs)
137
+ else:
138
+ delta, anchor = self.codec.filter(head)
139
+ self._deltas[:, :, sl].copy_(delta)
140
+ self._anchors[:, :, sl].copy_(anchor)
141
+ self.n_blocks += n_full
142
+
143
+ def append(self, tokens):
144
+ """Append (B,H,m,D); full blocks are batch-compressed into the archive
145
+ immediately (a single batched GEMM)."""
146
+ r = self.block_size
147
+ if self.meta is None:
148
+ self.meta = (tokens.shape[0], tokens.shape[1], tokens.shape[-1],
149
+ tokens.element_size())
150
+ self.dtype = tokens.dtype # archive/read restore the input dtype
151
+ buf = tokens if self.raw is None else torch.cat(
152
+ [self.raw, tokens], dim=-2)
153
+ total = buf.shape[-2]
154
+ n_full = total // r
155
+ if n_full > 0:
156
+ self._archive(buf[..., :n_full * r, :])
157
+ self.n_archived += n_full * r
158
+ self.raw = (buf[..., n_full * r:, :].contiguous()
159
+ if total > n_full * r else None)
160
+
161
+ def _decode_pending(self):
162
+ """Materialize archive blocks that were added since the last read.
163
+
164
+ The archive is append-only (causal-safety invariant), so the decoded
165
+ prefix never changes: decoding only the new blocks makes `read()` O(new
166
+ blocks) amortized instead of O(history) on every decode step."""
167
+ if self._rec_blocks >= self.n_blocks:
168
+ return
169
+ r = self.block_size
170
+ a, b = self._rec_blocks, self.n_blocks
171
+ like = self._coeffs if self.codec.name == "jpeg" else self._deltas
172
+ if self._rec is None or self._rec.shape[-2] < b * r:
173
+ cap = max(b * r, self._rec_capacity * 2,
174
+ self._INITIAL_BLOCKS * r)
175
+ new = torch.empty((like.shape[0], like.shape[1], cap,
176
+ like.shape[-1]),
177
+ dtype=self.dtype, device=like.device)
178
+ if self._rec is not None and self._rec_blocks:
179
+ done = self._rec_blocks * r
180
+ new[:, :, :done].copy_(self._rec[:, :, :done])
181
+ self._rec, self._rec_capacity = new, cap
182
+ if self.codec.name == "jpeg":
183
+ rec = self.codec.decompress(
184
+ self._coeffs[:, :, a:b],
185
+ scales=(self._scales[:, :, a:b]
186
+ if self.codec.quantized else None))
187
+ if rec.dtype != self.dtype:
188
+ rec = rec.to(self.dtype)
189
+ else:
190
+ rec = png_reconstruct(self._deltas[:, :, a:b],
191
+ self._anchors[:, :, a:b])
192
+ self._rec[:, :, a * r:b * r].copy_(rec)
193
+ self._rec_blocks = b
194
+
195
+ def discard_view(self):
196
+ """Drop the memoized reconstruction buffer (it is rebuilt on demand by
197
+ `read()`). Used by the materialization-free decode path: once prefill is
198
+ over the view is dead weight, and decode attends on the stored rows."""
199
+ self._rec = None
200
+ self._rec_blocks = 0
201
+ self._rec_capacity = 0
202
+
203
+ def read(self):
204
+ """Reconstructed view (B,H,S,D): one decode of the newly archived
205
+ blocks (memoized, append-only) + concatenation of the raw tail.
206
+ Everything is restored to the stream input dtype (attention q/k/v dtype
207
+ contract).
208
+
209
+ When the raw tail is empty the result is a view into the internal
210
+ reconstruction buffer; appends only ever write past `seq_len`, and
211
+ structural ops (crop / beam gather) never write into the live region
212
+ in place, so a tensor handed out earlier stays valid (until an
213
+ explicit `reset()`)."""
214
+ if self.n_blocks == 0:
215
+ return self.raw
216
+ self._decode_pending()
217
+ rec = self._rec[:, :, :self.n_archived]
218
+ if self.raw is not None:
219
+ rec = torch.cat([rec, self.raw], dim=-2)
220
+ return rec
221
+
222
+ def reset(self):
223
+ """Zero the contents but keep the capacity: crop / refill reuse the
224
+ allocated VRAM instead of re-allocating it over and over."""
225
+ self.raw = None
226
+ self.n_archived = 0
227
+ self.n_blocks = 0
228
+ self._rec_blocks = 0
229
+
230
+ def crop(self, tokens_to_remove):
231
+ """Remove trailing tokens exactly (negative count, HF semantics).
232
+
233
+ Already-archived blocks are kept verbatim; only the partial block at
234
+ the new boundary is re-exposed as a raw tail, taking its values from
235
+ the current reconstruction. So a crop never re-compresses the history
236
+ and never adds loss — unlike a rebuild-from-view crop, repeated crops
237
+ are idempotent."""
238
+ if tokens_to_remove > 0:
239
+ raise ValueError("crop only supports negative values "
240
+ "(remove trailing tokens)")
241
+ if tokens_to_remove == 0:
242
+ return
243
+ r = self.block_size
244
+ keep = max(0, self.seq_len - abs(tokens_to_remove))
245
+ keep_blocks = keep // r
246
+ tail = keep - keep_blocks * r
247
+ if keep_blocks < self.n_blocks:
248
+ self._decode_pending() # boundary tokens come from the view
249
+ self.n_blocks = keep_blocks
250
+ self.n_archived = keep_blocks * r
251
+ self._rec_blocks = min(self._rec_blocks, keep_blocks)
252
+ self.raw = (self._rec[:, :, keep_blocks * r:keep, :].clone()
253
+ if tail else None)
254
+ elif tail == 0:
255
+ self.raw = None
256
+ else:
257
+ self.raw = self.raw[..., :tail, :].contiguous()
258
+
259
+ def select_batch(self, idx):
260
+ """Gather rows of the batch dim (beam-search reorder / batch ops)."""
261
+ if self.raw is None and self.n_blocks == 0 and self._rec is None:
262
+ return
263
+ like = (self.raw if self.raw is not None else
264
+ (self._coeffs if self.codec.name == "jpeg" else self._deltas))
265
+ idx = idx.to(like.device)
266
+ r = self.block_size
267
+ if idx.numel() == like.shape[0]: # same batch size: in-place
268
+ if self.raw is not None:
269
+ self.raw = self.raw.index_select(0, idx)
270
+ if self.n_blocks:
271
+ for t in self._archive_tensors():
272
+ t[:, :, :self.n_blocks] = t[:, :, :self.n_blocks].index_select(
273
+ 0, idx)
274
+ if self._rec is not None and self._rec_blocks:
275
+ done = self._rec_blocks * r
276
+ # allocate instead of writing in place: read() may have handed
277
+ # out views into the current reconstruction buffer
278
+ new_r = torch.empty_like(self._rec)
279
+ new_r[:, :, :done] = self._rec[:, :, :done].index_select(
280
+ 0, idx)
281
+ self._rec = new_r
282
+ return
283
+ B, H, D = idx.numel(), like.shape[1], like.shape[-1]
284
+ for attr, shape, dtype in self._archive_spec(self._capacity, B, H, D):
285
+ old = getattr(self, attr)
286
+ new = torch.empty(shape, dtype=dtype, device=like.device)
287
+ if self.n_blocks:
288
+ new[:, :, :self.n_blocks] = old[
289
+ :, :, :self.n_blocks].index_select(0, idx)
290
+ setattr(self, attr, new)
291
+ if self._rec is not None:
292
+ new_r = torch.empty((B, H, self._rec_capacity, D),
293
+ dtype=self._rec.dtype, device=like.device)
294
+ if self._rec_blocks:
295
+ done = self._rec_blocks * r
296
+ new_r[:, :, :done] = self._rec[:, :, :done].index_select(0, idx)
297
+ self._rec = new_r
298
+ if self.raw is not None:
299
+ self.raw = self.raw.index_select(0, idx)
300
+ if self.meta is not None:
301
+ self.meta = (B,) + self.meta[1:]
302
+
303
+ def _archive_tensors(self):
304
+ """Live archive tensors (shared by batch ops / storage accounting)."""
305
+ if self.codec.name == "jpeg":
306
+ return ((self._coeffs, self._scales) if self.codec.quantized
307
+ else (self._coeffs,))
308
+ return (self._deltas, self._anchors)
309
+
310
+ @property
311
+ def seq_len(self):
312
+ return self.n_archived + (0 if self.raw is None else self.raw.shape[-2])
313
+
314
+ def storage_bytes(self):
315
+ """Logical storage (valid region + raw tail), excluding the idle
316
+ capacity reserved by the doubling preallocation and the persistent
317
+ reconstruction view (`_rec`)."""
318
+ total = 0
319
+ if self.n_blocks:
320
+ for t in self._archive_tensors():
321
+ total += (t[:, :, :self.n_blocks].numel()
322
+ * t.element_size())
323
+ if self.raw is not None:
324
+ total += self.raw.numel() * self.raw.element_size()
325
+ return total
326
+
327
+
328
+ class CodecKVCache:
329
+ """Codec KV cache (multi-layer, duck-typed as an HF Cache).
330
+
331
+ Usage::
332
+
333
+ cache = CodecKVCache(mode="jpeg", block_size=8, keep_freqs=2)
334
+ k, v = cache.update(k_states, v_states, layer_idx=0) # incremental
335
+ k, v = cache.forward(k_seq, v_seq, layer_idx=0) # full sequence
336
+ """
337
+
338
+ def __init__(self, mode="jpeg", block_size=8, keep_freqs=2,
339
+ keep_ratio=1.0, quant_bits=0):
340
+ self.mode = mode
341
+ self.codec = make_codec(mode, block_size, keep_freqs, keep_ratio,
342
+ quant_bits=quant_bits)
343
+ self._keys = {}
344
+ self._values = {}
345
+
346
+ # ---------- internals ----------
347
+ def _stream(self, store, layer_idx):
348
+ st = store.get(layer_idx)
349
+ if st is None:
350
+ st = _CodecStream(self.codec)
351
+ store[layer_idx] = st
352
+ return st
353
+
354
+ # ---------- dual interface ----------
355
+ def update(self, key_states, value_states, layer_idx=0):
356
+ """Incremental autoregressive interface: append new (B,H,m,D) and
357
+ archive automatically across block boundaries; returns the layer's
358
+ reconstructed (key_view, value_view)."""
359
+ self._stream(self._keys, layer_idx).append(key_states)
360
+ self._stream(self._values, layer_idx).append(value_states)
361
+ return (self._keys[layer_idx].read(),
362
+ self._values[layer_idx].read())
363
+
364
+ decode_step = update
365
+
366
+ def forward(self, key_seq, value_seq, layer_idx=0):
367
+ """Full-sequence forward interface: tile the sequence dimension block
368
+ by block directly (reset the layer, then fill it in one shot); returns
369
+ the reconstructed (key_view, value_view)."""
370
+ self.reset(layer_idx)
371
+ return self.update(key_seq, value_seq, layer_idx)
372
+
373
+ def reset(self, layer_idx=None):
374
+ if layer_idx is None:
375
+ self._keys.clear()
376
+ self._values.clear()
377
+ else:
378
+ self._keys.pop(layer_idx, None)
379
+ self._values.pop(layer_idx, None)
380
+
381
+ def crop(self, tokens_to_remove, layer_idx=None):
382
+ """Exact trailing-token removal (negative count) for one or all layers."""
383
+ for store in (self._keys, self._values):
384
+ for i, st in store.items():
385
+ if layer_idx is None or i == layer_idx:
386
+ st.crop(tokens_to_remove)
387
+
388
+ def reorder_cache(self, beam_idx, layer_idx=None):
389
+ """Beam search: gather the batch dim of one or all layers."""
390
+ for store in (self._keys, self._values):
391
+ for i, st in store.items():
392
+ if layer_idx is None or i == layer_idx:
393
+ st.select_batch(beam_idx)
394
+
395
+ # ---------- HF Cache duck typing ----------
396
+ @property
397
+ def key_cache(self):
398
+ mx = max(self._keys, default=-1)
399
+ return [self._keys[i].read() if i in self._keys else None
400
+ for i in range(mx + 1)]
401
+
402
+ @property
403
+ def value_cache(self):
404
+ mx = max(self._values, default=-1)
405
+ return [self._values[i].read() if i in self._values else None
406
+ for i in range(mx + 1)]
407
+
408
+ def get_seq_length(self, layer_idx=0):
409
+ st = self._keys.get(layer_idx)
410
+ return 0 if st is None else st.seq_len
411
+
412
+ def get_max_length(self):
413
+ return None # no upper bound
414
+
415
+ def get_usable_length(self, new_seq_length, layer_idx=0):
416
+ return self.get_seq_length(layer_idx)
417
+
418
+ # ---------- diagnostics ----------
419
+ def storage_bytes(self, layer_idx=None):
420
+ layers = ([layer_idx] if layer_idx is not None
421
+ else sorted(set(self._keys) | set(self._values)))
422
+ return sum(self._keys[i].storage_bytes() for i in layers if i in self._keys
423
+ ) + sum(self._values[i].storage_bytes()
424
+ for i in layers if i in self._values)
425
+
426
+ def stats(self, layer_idx=0):
427
+ ks = self._keys.get(layer_idx)
428
+ if ks is None or ks.meta is None:
429
+ return {"tokens": 0, "archived": 0, "raw": 0,
430
+ "storage_bytes": 0, "raw_bytes": 0}
431
+ B, H, D, elem = ks.meta
432
+ tokens = ks.seq_len
433
+ return {"tokens": tokens, "archived": ks.n_archived,
434
+ "raw": tokens - ks.n_archived,
435
+ "storage_bytes": self.storage_bytes(layer_idx),
436
+ "raw_bytes": tokens * B * H * D * elem * 2}
437
+
438
+ def __repr__(self):
439
+ return (f"CodecKVCache(mode={self.mode!r}, codec={self.codec}, "
440
+ f"layers={sorted(set(self._keys) | set(self._values))})")
441
+
442
+
443
+ # ============================================================
444
+ # transformers 5.x native Cache backend (layer container mode)
445
+ # ============================================================
446
+ class FlashCodecLayer(_HFDynamicLayer):
447
+ """transformers 5.x CacheLayer: replaces DynamicLayer's cat-style growth
448
+ with a codec stream (raw tail + archived full blocks). update returns the
449
+ reconstructed, causally visible KV for the attention layer to consume
450
+ directly — the write and read replacements are both contained in this
451
+ layer, requiring zero model surgery."""
452
+
453
+ def __init__(self, mode="jpeg", block_size=8, keep_freqs=2,
454
+ keep_ratio=1.0, quant_bits=0):
455
+ super().__init__()
456
+ codec = make_codec(mode, block_size, keep_freqs, keep_ratio,
457
+ quant_bits=quant_bits)
458
+ self.mode = mode
459
+ self.codec = codec
460
+ self._key_stream = _CodecStream(codec)
461
+ self._value_stream = _CodecStream(codec)
462
+
463
+ def lazy_initialization(self, key_states, value_states):
464
+ self.dtype, self.device = key_states.dtype, key_states.device
465
+ self.keys = torch.tensor([], dtype=self.dtype, device=self.device)
466
+ self.values = torch.tensor([], dtype=self.dtype, device=self.device)
467
+ self.is_initialized = True
468
+
469
+ def update(self, key_states, value_states, *args, **kwargs):
470
+ if not self.is_initialized:
471
+ self.lazy_initialization(key_states, value_states)
472
+ self._key_stream.append(key_states)
473
+ self._value_stream.append(value_states)
474
+ self.keys = self._key_stream.read()
475
+ self.values = self._value_stream.read()
476
+ return self.keys, self.values
477
+
478
+ def get_seq_length(self):
479
+ return self._key_stream.seq_len
480
+
481
+ def get_max_length(self):
482
+ return -1 # dynamic cache, no upper bound
483
+
484
+ def crop(self, tokens_to_remove):
485
+ """Exact trailing-token removal (speculative decoding). Archived blocks
486
+ are kept verbatim; only the boundary partial block falls back into the
487
+ raw tail, so cropping never re-compresses and never adds loss."""
488
+ self._key_stream.crop(tokens_to_remove)
489
+ self._value_stream.crop(tokens_to_remove)
490
+ self._rebuild_views()
491
+
492
+ def reorder_cache(self, beam_idx):
493
+ """Beam search: gather the batch dim of both streams. The archive and
494
+ the raw tail have to be reordered too — reordering only the
495
+ materialized view would silently corrupt the next read."""
496
+ self._key_stream.select_batch(beam_idx)
497
+ self._value_stream.select_batch(beam_idx)
498
+ self._rebuild_views()
499
+
500
+ def batch_repeat_interleave(self, repeats):
501
+ """Contrastive search: replicate batch rows (archives included)."""
502
+ if not self.is_initialized:
503
+ return
504
+ idx = torch.arange(self._key_stream.meta[0],
505
+ device=self.keys.device).repeat_interleave(repeats)
506
+ self.select_batch(idx)
507
+
508
+ def batch_select_indices(self, indices):
509
+ """Contrastive search: keep only the given batch rows."""
510
+ if not self.is_initialized:
511
+ return
512
+ self.select_batch(indices)
513
+
514
+ def select_batch(self, idx):
515
+ self._key_stream.select_batch(idx)
516
+ self._value_stream.select_batch(idx)
517
+ self._rebuild_views()
518
+
519
+ def _rebuild_views(self):
520
+ """Re-materialize the attention-facing views after a structural
521
+ change (crop / batch gather)."""
522
+ if not self.is_initialized:
523
+ return
524
+ if self._key_stream.seq_len > 0:
525
+ self.keys = self._key_stream.read()
526
+ self.values = self._value_stream.read()
527
+ else:
528
+ self.keys = torch.empty(0, dtype=self.dtype, device=self.device)
529
+ self.values = torch.empty(0, dtype=self.dtype, device=self.device)
530
+
531
+ def reset(self):
532
+ self._key_stream.reset()
533
+ self._value_stream.reset()
534
+ super().reset()
535
+
536
+ def offload(self):
537
+ pass # prototype has no layer offload (archives are off-CPU)
538
+
539
+ def prefetch(self):
540
+ pass
541
+
542
+ def storage_bytes(self):
543
+ return (self._key_stream.storage_bytes()
544
+ + self._value_stream.storage_bytes())
545
+
546
+
547
+ class FlashCodecCache(_HFCache):
548
+ """transformers 5.x native codec KV cache (first-class backend).
549
+
550
+ Usage 1 (explicit)::
551
+
552
+ cache = FlashCodecCache(mode="jpeg", block_size=8, keep_freqs=2)
553
+ model.generate(input_ids, past_key_values=cache, use_cache=True)
554
+
555
+ Usage 2 (automatic injection via the apply_flash_codec hot patch).
556
+ The Cache base class lazily creates layers through
557
+ layer_class_to_replicate and forwards update to FlashCodecLayer;
558
+ get_seq_length / get_max_length / crop and friends come for free by
559
+ inheritance.
560
+ """
561
+
562
+ def __init__(self, mode="jpeg", block_size=8, keep_freqs=2,
563
+ keep_ratio=1.0, quant_bits=0):
564
+ if not HAS_HF_CACHE:
565
+ raise ImportError(
566
+ "FlashCodecCache requires transformers 5.x (this project only "
567
+ "supports the installed 5.20 Cache ecosystem); use "
568
+ "CodecKVCache when transformers is unavailable.")
569
+ super().__init__(
570
+ layers=None,
571
+ layer_class_to_replicate=partial(
572
+ FlashCodecLayer, mode=mode, block_size=block_size,
573
+ keep_freqs=keep_freqs, keep_ratio=keep_ratio,
574
+ quant_bits=quant_bits))
575
+ self.mode = mode
576
+ self.codec_cfg = dict(block_size=block_size, keep_freqs=keep_freqs,
577
+ keep_ratio=keep_ratio, quant_bits=quant_bits)
578
+
579
+ @property
580
+ def _seen_tokens(self):
581
+ """Counter attribute familiar from older transformers versions (unused
582
+ internally by 5.x, kept for compatibility)."""
583
+ return self.get_seq_length()
584
+
585
+ @classmethod
586
+ def from_config(cls, config, mode="jpeg", block_size=8, keep_freqs=2,
587
+ keep_ratio=1.0, quant_bits=0, per_layer=None):
588
+ """Build the mixed-layer list from the model config (Qwen3.5-style
589
+ hybrid architectures): only full_attention layers get a codec layer,
590
+ while the remaining layer types (linear_attention / sliding, etc.)
591
+ keep their native dynamic layer — consistent with how
592
+ DynamicCache(config=...) dispatches, so native interfaces such as
593
+ linear-attention recurrent state stay unaffected.
594
+
595
+ ``per_layer`` overrides the codec config for individual layer indices,
596
+ e.g. ``{3: {"keep_freqs": 4}, 7: {"quant_bits": 8}}`` — this is how a
597
+ calibrated (non-uniform) budget is applied."""
598
+ from transformers.cache_utils import (DYNAMIC_LAYER_TYPE_MAPPING,
599
+ get_layer_types_and_kwargs)
600
+ decoder = (config.get_text_config(decoder=True)
601
+ if hasattr(config, "get_text_config") else config)
602
+ try:
603
+ layer_types, per_layer_kwargs = get_layer_types_and_kwargs(decoder)
604
+ except Exception: # legacy config, no per_layer_config
605
+ layer_types = list(getattr(decoder, "layer_types",
606
+ ["full_attention"] *
607
+ getattr(decoder, "num_hidden_layers", 1)))
608
+ per_layer_kwargs = [{} for _ in layer_types]
609
+ per_layer = per_layer or {}
610
+ layers = []
611
+ for idx, (ltype, lkw) in enumerate(zip(layer_types, per_layer_kwargs)):
612
+ if ltype == "full_attention":
613
+ cfg = dict(mode=mode, block_size=block_size,
614
+ keep_freqs=keep_freqs, keep_ratio=keep_ratio,
615
+ quant_bits=quant_bits)
616
+ cfg.update(per_layer.get(idx, {}))
617
+ layers.append(FlashCodecLayer(**cfg))
618
+ else:
619
+ layers.append(DYNAMIC_LAYER_TYPE_MAPPING[ltype](**lkw))
620
+ obj = cls.__new__(cls)
621
+ _HFCache.__init__(obj, layers=layers, layer_class_to_replicate=None)
622
+ obj.mode = mode
623
+ obj.codec_cfg = dict(block_size=block_size, keep_freqs=keep_freqs,
624
+ keep_ratio=keep_ratio, quant_bits=quant_bits,
625
+ per_layer=per_layer)
626
+ return obj
627
+
628
+ def codec_layer_indices(self):
629
+ """Indices of the layers that actually carry a codec (i.e. the
630
+ full_attention layers in hybrid architectures)."""
631
+ return [i for i, layer in enumerate(self.layers)
632
+ if isinstance(layer, FlashCodecLayer)]
633
+
634
+ def to_legacy_cache(self):
635
+ """Export to the legacy {layer_idx: (keys, values)} format
636
+ (reconstructed full views)."""
637
+ return {i: (layer.keys, layer.values)
638
+ for i, layer in enumerate(self.layers)}
639
+
640
+ @classmethod
641
+ def from_legacy_cache(cls, legacy, **cfg):
642
+ """Rebuild from the legacy {(layer_idx): (k, v)} format (the whole
643
+ span goes through the codec once more)."""
644
+ cache = cls(**cfg)
645
+ for layer_idx in sorted(legacy):
646
+ k, v = legacy[layer_idx]
647
+ cache.update(k, v, layer_idx)
648
+ return cache
649
+
650
+ def storage_bytes(self, layer_idx=None):
651
+ layers = (self.layers if layer_idx is None
652
+ else [self.layers[layer_idx]])
653
+ return sum(layer.storage_bytes() for layer in layers
654
+ if isinstance(layer, FlashCodecLayer))
@@ -0,0 +1,13 @@
1
+ """flashcodec codecs: jpeg (lossy frequency domain) and
2
+ png (lossless/near-lossless time domain)."""
3
+
4
+ from .jpeg import (JPEGCodec, dct_basis, dct_compress, dct_decompress,
5
+ dequantize_coeffs, quantize_coeffs)
6
+ from .png import PNGCodec, png_filter, png_reconstruct
7
+
8
+ __all__ = [
9
+ "JPEGCodec", "PNGCodec",
10
+ "dct_basis", "dct_compress", "dct_decompress",
11
+ "quantize_coeffs", "dequantize_coeffs",
12
+ "png_filter", "png_reconstruct",
13
+ ]