1- import sys
21from abc import ABC , abstractmethod
2+ from collections import OrderedDict
3+ import ctypes
4+ from dataclasses import dataclass
5+ import diskcache
6+ import hashlib
7+ import sys
38from typing import (
9+ Any ,
10+ List ,
411 Optional ,
512 Sequence ,
613 Tuple ,
714)
8- from collections import OrderedDict
9-
10- import diskcache
1115
1216import llama_cpp .llama
17+ import llama_cpp ._internals as _internals
18+ import llama_cpp .llama_cpp as llama_cpp
1319
1420from .llama_types import *
1521
@@ -46,6 +52,60 @@ def __setitem__(
4652 raise NotImplementedError
4753
4854
55+ class LlamaDiskCache (BaseLlamaCache ):
56+ """Cache for a llama.cpp model using disk."""
57+
58+ def __init__ (
59+ self , cache_dir : str = ".cache/llama_cache" , capacity_bytes : int = (2 << 30 )
60+ ):
61+ super ().__init__ (capacity_bytes )
62+ self .cache = diskcache .Cache (cache_dir )
63+
64+ @property
65+ def cache_size (self ):
66+ return int (self .cache .volume ()) # type: ignore
67+
68+ def _find_longest_prefix_key (
69+ self ,
70+ key : Tuple [int , ...],
71+ ) -> Optional [Tuple [int , ...]]:
72+ min_len = 0
73+ min_key : Optional [Tuple [int , ...]] = None
74+ for k in self .cache .iterkeys (): # type: ignore
75+ prefix_len = llama_cpp .llama .Llama .longest_token_prefix (k , key )
76+ if prefix_len > min_len :
77+ min_len = prefix_len
78+ min_key = k # type: ignore
79+ return min_key
80+
81+ def __getitem__ (self , key : Sequence [int ]) -> "llama_cpp.llama.LlamaState" :
82+ key = tuple (key )
83+ _key = self ._find_longest_prefix_key (key )
84+ if _key is None :
85+ raise KeyError ("Key not found" )
86+ value : "llama_cpp.llama.LlamaState" = self .cache .pop (_key ) # type: ignore
87+ # NOTE: This puts an integer as key in cache, which breaks,
88+ # Llama.longest_token_prefix(k, key) above since k is not a tuple of ints/tokens
89+ # self.cache.push(_key, side="front") # type: ignore
90+ return value
91+
92+ def __contains__ (self , key : Sequence [int ]) -> bool :
93+ return self ._find_longest_prefix_key (tuple (key )) is not None
94+
95+ def __setitem__ (self , key : Sequence [int ], value : "llama_cpp.llama.LlamaState" ):
96+ print ("LlamaDiskCache.__setitem__: called" , file = sys .stderr )
97+ key = tuple (key )
98+ if key in self .cache :
99+ print ("LlamaDiskCache.__setitem__: delete" , file = sys .stderr )
100+ del self .cache [key ]
101+ self .cache [key ] = value
102+ print ("LlamaDiskCache.__setitem__: set" , file = sys .stderr )
103+ while self .cache_size > self .capacity_bytes and len (self .cache ) > 0 :
104+ key_to_remove = next (iter (self .cache ))
105+ del self .cache [key_to_remove ]
106+ print ("LlamaDiskCache.__setitem__: trim" , file = sys .stderr )
107+
108+
49109class LlamaRAMCache (BaseLlamaCache ):
50110 """Cache for a llama.cpp model using RAM."""
51111
@@ -259,55 +319,166 @@ def __setitem__(self, key: Sequence[int], value: "llama_cpp.llama.LlamaState"):
259319LlamaCache = LlamaRAMCache
260320
261321
262- class LlamaDiskCache (BaseLlamaCache ):
263- """Cache for a llama.cpp model using disk."""
322+ @dataclass
323+ class HybridCheckpoint :
324+ """Represents a single snapshot of the RNN/Hybrid model's hidden state."""
325+ pos : int # The token position (cursor) where this snapshot was taken
326+ data : bytes # The raw binary RNN state data
327+ hash_val : str # SHA-256 hash of the token prefix to ensure exact sequence matching
328+ size : int # Size of the state data in bytes
329+ seq_id : int # Sequence ID this checkpoint belongs to
264330
265- def __init__ (
266- self , cache_dir : str = ".cache/llama_cache" , capacity_bytes : int = (2 << 30 )
267- ):
268- super ().__init__ (capacity_bytes )
269- self .cache = diskcache .Cache (cache_dir )
331+ class HybridCheckpointCache (BaseLlamaCache ):
332+ """
333+ Manager for RNN state snapshots (Checkpoints) tailored for Hybrid/Recurrent models.
334+ Provides rollback capabilities for models that cannot physically truncate KV cache.
335+ """
336+ def __init__ (self , ctx : llama_cpp .llama_context_p , seq_id : int = 0 , max_checkpoints : int = 16 , verbose : bool = False ):
337+ if ctx is None :
338+ raise ValueError ("HybridCheckpointCache: Failed to create HybridCheckpointCache with model context" )
339+ self ._ctx = ctx
340+ self .seq_id = seq_id
341+ self .max_checkpoints = max_checkpoints
342+ self .checkpoints : list [HybridCheckpoint ] = []
343+ self ._current_size = 0
344+
345+ # Cache C-type API function pointers for performance
346+ self ._get_size_ext = llama_cpp .llama_state_seq_get_size_ext
347+ self ._get_data_ext = llama_cpp .llama_state_seq_get_data_ext
348+ self ._set_data_ext = llama_cpp .llama_state_seq_set_data_ext
349+ self ._flag_partial = llama_cpp .LLAMA_STATE_SEQ_FLAGS_PARTIAL_ONLY
350+
351+ self .verbose = verbose
270352
271353 @property
272- def cache_size (self ):
273- return int (self .cache .volume ()) # type: ignore
354+ def cache_size (self ) -> int :
355+ """Returns the total memory used by all stored checkpoints in bytes."""
356+ return self ._current_size
274357
275- def _find_longest_prefix_key (
358+ def clear (self ):
359+ """Clears all stored checkpoints and resets memory tracking."""
360+ self .checkpoints .clear ()
361+ self ._current_size = 0
362+ if self .verbose :
363+ print ("HybridCheckpointCache: cleared" )
364+
365+ # Helper tools
366+
367+ def _hash_prefix (self , tokens : List [int ], length : int ) -> str :
368+ """
369+ Computes a SHA-256 hash for a sequence of tokens up to the specified length.
370+ This ensures that checkpoints are only restored for the EXACT same conversation history.
371+ """
372+ if length <= 0 :
373+ return "empty"
374+ tokens_size = len (tokens )
375+ if length > tokens_size :
376+ length = tokens_size
377+ data = bytes (tokens [:length ])
378+ return hashlib .sha256 (data ).hexdigest ()[:32 ]
379+
380+ def find_best_checkpoint (self , tokens : List [int ], seq_id : int = 0 ) -> Optional [HybridCheckpoint ]:
381+ """
382+ Finds the longest valid checkpoint that perfectly matches the provided token prefix.
383+ Returns None if no matching checkpoint is found.
384+ """
385+ best_cp = None
386+ best_pos = - 1
387+ for cp in self .checkpoints :
388+ if cp .seq_id != seq_id or cp .pos > len (tokens ):
389+ # Skip if sequence ID mismatches or checkpoint is longer than the current prompt
390+ continue
391+
392+ # Verify cryptographic integrity of the prompt history
393+ if self ._hash_prefix (tokens , cp .pos ) == cp .hash_val :
394+ if cp .pos > best_pos :
395+ # Keep the checkpoint with the longest matching prefix (highest pos)
396+ best_pos = cp .pos
397+ best_cp = cp
398+ return best_cp
399+
400+ def save_checkpoint (
276401 self ,
277- key : Tuple [int , ...],
278- ) -> Optional [Tuple [int , ...]]:
279- min_len = 0
280- min_key : Optional [Tuple [int , ...]] = None
281- for k in self .cache .iterkeys (): # type: ignore
282- prefix_len = llama_cpp .llama .Llama .longest_token_prefix (k , key )
283- if prefix_len > min_len :
284- min_len = prefix_len
285- min_key = k # type: ignore
286- return min_key
402+ current_pos : int ,
403+ tokens : List [int ],
404+ seq_id : int = 0
405+ ) -> bool :
406+ """
407+ Extracts the RNN hidden state from the C++ backend and saves it as a checkpoint.
408+ Manages eviction (FIFO) if the maximum number of checkpoints is exceeded.
409+ """
410+ flags = self ._flag_partial
411+
412+ # 1. Query the required buffer size
413+ size = llama_cpp .llama_state_seq_get_size_ext (self ._ctx , seq_id , flags )
414+ if size == 0 :
415+ if self .verbose :
416+ print ("HybridCheckpointCache: size=0, skip" )
417+ return False
418+
419+ # 2. Allocate buffer and extract data
420+ buffer = (ctypes .c_uint8 * size )()
421+ n_written = llama_cpp .llama_state_seq_get_data_ext (self ._ctx , buffer , size , seq_id , flags )
422+ if n_written != size :
423+ if self .verbose :
424+ print (f"HybridCheckpointCache: get failed { n_written } /{ size } " )
425+ return False
426+
427+ data_bytes = bytes (buffer [:n_written ])
428+ hash_val = self ._hash_prefix (tokens , current_pos )
429+
430+ # 3. Store the checkpoint
431+ self .checkpoints .append (HybridCheckpoint (
432+ pos = current_pos ,
433+ data = data_bytes ,
434+ hash_val = hash_val ,
435+ size = n_written ,
436+ seq_id = seq_id )
437+ )
438+ self ._current_size += n_written
287439
288- def __getitem__ (self , key : Sequence [int ]) -> "llama_cpp.llama.LlamaState" :
289- key = tuple (key )
290- _key = self ._find_longest_prefix_key (key )
291- if _key is None :
292- raise KeyError ("Key not found" )
293- value : "llama_cpp.llama.LlamaState" = self .cache .pop (_key ) # type: ignore
294- # NOTE: This puts an integer as key in cache, which breaks,
295- # Llama.longest_token_prefix(k, key) above since k is not a tuple of ints/tokens
296- # self.cache.push(_key, side="front") # type: ignore
297- return value
440+ # 4. Enforce capacity limits (FIFO eviction)
441+ while len (self .checkpoints ) > self .max_checkpoints :
442+ if not self .checkpoints :
443+ break
444+ old_cp = self .checkpoints .pop (0 )
445+ self ._current_size -= old_cp .size
446+ if self .verbose :
447+ print (f"HybridCheckpointCache: evicted pos={ old_cp .pos } " )
298448
299- def __contains__ (self , key : Sequence [int ]) -> bool :
300- return self ._find_longest_prefix_key (tuple (key )) is not None
449+ if self .verbose :
450+ print (f"HybridCheckpointCache: Saved checkpoint at pos { current_pos } ({ size / 1024 / 1024 :.2f} MiB) "
451+ f"total={ len (self .checkpoints )} used={ self ._current_size / 1024 / 1024 :.2f} MiB" ,
452+ file = sys .stderr )
301453
302- def __setitem__ (self , key : Sequence [int ], value : "llama_cpp.llama.LlamaState" ):
303- print ("LlamaDiskCache.__setitem__: called" , file = sys .stderr )
304- key = tuple (key )
305- if key in self .cache :
306- print ("LlamaDiskCache.__setitem__: delete" , file = sys .stderr )
307- del self .cache [key ]
308- self .cache [key ] = value
309- print ("LlamaDiskCache.__setitem__: set" , file = sys .stderr )
310- while self .cache_size > self .capacity_bytes and len (self .cache ) > 0 :
311- key_to_remove = next (iter (self .cache ))
312- del self .cache [key_to_remove ]
313- print ("LlamaDiskCache.__setitem__: trim" , file = sys .stderr )
454+ return True
455+
456+ def restore_checkpoint (self , cp : HybridCheckpoint , seq_id : int = 0 ) -> bool :
457+ """
458+ Injects a previously saved RNN state checkpoint back into the C++ backend memory.
459+ """
460+ if cp .seq_id != seq_id :
461+ return False
462+ flags = self ._flag_partial
463+
464+ # Copy data back to a ctypes buffer and push to backend
465+ buffer = (ctypes .c_uint8 * cp .size ).from_buffer_copy (cp .data )
466+ ret = llama_cpp .llama_state_seq_set_data_ext (
467+ self ._ctx , buffer , cp .size , seq_id , flags
468+ )
469+ success = (ret == cp .size )
470+
471+ if self .verbose :
472+ print (f"HybridCheckpointCache: restore { 'OK' if success else 'FAIL' } pos={ cp .pos } " )
473+ return success
474+
475+ # Disable BaseLlamaCache Dictionary Interfaces
476+
477+ def __getitem__ (self , key ):
478+ raise NotImplementedError ("HybridCheckpointCache: pls use save_checkpoint or restore_checkpoint method" )
479+
480+ def __setitem__ (self , key , value ):
481+ raise NotImplementedError ("HybridCheckpointCache: pls use save_checkpoint or restore_checkpoint method" )
482+
483+ def __contains__ (self , key ):
484+ raise NotImplementedError ("HybridCheckpointCache: pls use save_checkpoint or restore_checkpoint method" )
0 commit comments