Skip to content

Commit 39afb9b

Browse files
committed
feat(hybrid): add periodic checkpointing and adaptive batch handling
- Increase default `ctx_checkpoints` from 16 to 32 - Add new parameter `checkpoint_interval` (default: 4096) for hybrid model state snapshots - Implement robust dynamic batch downgrade on KV cache exhaustion (status=1) - Introduce periodic checkpoint saves during eval in hybrid mode - Improve error handling and logging around context shifts and decoding failures Signed-off-by: JamePeng <jame_peng@sina.com>
1 parent ff3a7c8 commit 39afb9b

1 file changed

Lines changed: 87 additions & 32 deletions

File tree

llama_cpp/llama.py

Lines changed: 87 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ def __init__(
112112
swa_full: Optional[bool] = None,
113113
kv_unified: Optional[bool] = None,
114114
# HybridCheckpointCache Params
115-
ctx_checkpoints: int = 16,
115+
ctx_checkpoints: int = 32,
116+
checkpoint_interval: int = 4096,
116117
# Sampling Params
117118
last_n_tokens_size: int = 64,
118119
# LoRA Params
@@ -203,6 +204,7 @@ def __init__(
203204
swa_full: whether to use full-size SWA cache
204205
kv_unified: use single unified KV buffer for the KV cache of all sequences
205206
ctx_checkpoints: max number of context checkpoints to create per slot (default: 16)[(more info)](https://github.com/ggml-org/llama.cpp/pull/15293)
207+
checkpoint_interval: Hybrid model checkpoint token intervals, and archiving of text with interval sizes along the way.
206208
last_n_tokens_size: Maximum number of tokens to keep in the last_n_tokens deque.
207209
lora_base: Optional path to base model, useful if using a quantized base model and you want to apply LoRA to an f16 model.
208210
lora_path: Path to a LoRA file to apply to the model.
@@ -485,10 +487,11 @@ def __init__(
485487
if self.is_hybrid:
486488
if self.verbose:
487489
print(f"Llama.__init__: Hybrid/Recurrent model detected."
488-
f"(is_recurrent: {_is_recurrent}, is_hybrid: {_is_hybrid}, n_swa: {_n_swa}), swa_full: {swa_full}. "
489-
f" Enabling HybridCheckpointCache(ctx_checkpoints={ctx_checkpoints}).",
490+
f"(is_recurrent: {_is_recurrent}, is_hybrid: {_is_hybrid}, n_swa: {_n_swa}, swa_full: {swa_full}). "
491+
f" Enabling HybridCheckpointCache(ctx_checkpoints={ctx_checkpoints}, checkpoint_interval={checkpoint_interval}).",
490492
file=sys.stderr)
491493
self.ctx_checkpoints = ctx_checkpoints
494+
self.checkpoint_interval = checkpoint_interval
492495
self._hybrid_cache_mgr = HybridCheckpointCache(self._ctx.ctx, max_checkpoints=self.ctx_checkpoints, verbose=self.verbose)
493496
else:
494497
self._hybrid_cache_mgr = None
@@ -784,7 +787,7 @@ def eval(self, tokens: Sequence[int]):
784787
if n_eval == 0:
785788
return
786789

787-
# Context Shift
790+
# Context Shift: Prevent OOM by discarding older tokens when context limit is reached.
788791
if self.n_tokens + n_eval > self._n_ctx:
789792
if self.is_hybrid:
790793
raise RuntimeError(
@@ -793,7 +796,7 @@ def eval(self, tokens: Sequence[int]):
793796
)
794797
else:
795798
_n_keep = min(self.n_keep, self.n_tokens)
796-
# number of tokens after n_keep that may be discarded when shifting context
799+
# Number of tokens after n_keep that may be discarded when shifting context
797800
# defaults to half
798801
_n_discard = (self.n_tokens - _n_keep) // 2
799802

@@ -810,56 +813,108 @@ def eval(self, tokens: Sequence[int]):
810813

811814
self.n_tokens -= _n_discard
812815

813-
for i in range(0, n_eval, self.n_batch):
814-
batch_tokens = tokens[i : min(n_eval, i + self.n_batch)]
815-
n_batch_tokens = len(batch_tokens)
816+
# Adaptive batch downgrade limit initialization
817+
current_max_batch = self.n_batch
818+
last_ckpt_pos = self.n_tokens
819+
820+
# If KV slots are full, `current_batch_size` will be halved.
821+
# A `while` loop allows us to correctly resume from the exact cut-off point.
822+
i = 0
823+
while i < n_eval:
824+
# Chunk the tokens using the adaptive current_max_batch
825+
n_chunk = min(n_eval - i, current_max_batch)
826+
chunk = tokens[i : i + n_chunk]
816827
n_past = self.n_tokens
817828

818829
self._batch.reset()
819830

820-
pos_array = [self.n_tokens + j for j in range(n_batch_tokens)]
831+
pos_array = [self.n_tokens + j for j in range(n_chunk)]
821832

833+
# Configure logits extraction:
834+
# If _logits_all is True, calculate for every token.
835+
# Otherwise, only calculate for the very last token in the entire evaluation sequence.
822836
if self._logits_all:
823-
logits_array = [True] * n_batch_tokens
837+
logits_array = [True] * n_chunk
824838
else:
825-
logits_array = [False] * n_batch_tokens
826-
if i + n_batch_tokens == n_eval:
839+
logits_array = [False] * n_chunk
840+
if i + n_chunk == n_eval:
827841
logits_array[-1] = True
828842

829843
self._batch.add_sequence(
830-
token_array=batch_tokens,
844+
token_array=chunk,
831845
pos_array=pos_array,
832846
seq_ids=[0],
833847
logits_array=logits_array
834848
)
835-
current_batch_size = n_batch_tokens
836-
try:
837-
self._ctx.decode(self._batch)
838-
except Exception as e:
839-
raise RuntimeError(
840-
f"Decode Failed at "
841-
f"Batch size: {n_batch_tokens}. "
842-
f"Error: {str(e)}."
843-
) from e
844849

845-
# Save tokens
846-
self.input_ids[n_past : n_past + n_batch_tokens] = batch_tokens
850+
# Dynamic Batch Downgrade: Attempt to decode, reduce batch size if KV cache is fragmented
851+
current_batch_size = n_chunk
852+
success = False
847853

848-
# Save logits
849-
logits_ptr = self._ctx.get_logits()
854+
while current_batch_size > 0:
855+
# Tell the C++ backend to only process up to `current_batch_size` tokens
856+
self._batch.batch.n_tokens = current_batch_size
857+
858+
try:
859+
status = self._ctx.decode(self._batch)
860+
861+
# 0: Success
862+
if status == 0:
863+
success = True
864+
# If we successfully decoded after a downgrade,
865+
# update current_max_batch to prevent repeated failures in next iterations.
866+
if current_batch_size < current_max_batch:
867+
current_max_batch = current_batch_size
868+
break
869+
870+
# 1: No KV slot available (Recoverable)
871+
elif status == 1:
872+
if self.verbose:
873+
print(f"Llama.eval: KV slots full (Code 1). Halving batch size "
874+
f"from {current_batch_size} to {current_batch_size // 2}...", file=sys.stderr)
875+
current_batch_size //= 2
876+
877+
except Exception as e:
878+
# Catch fatal backend failures (e.g., Code -2, -3)
879+
raise RuntimeError(f"Llama.eval(decode): Fatal Decode Error at Pos {self.n_tokens}, "
880+
f"Batch size {current_batch_size}: {str(e)}") from e
881+
882+
if not success:
883+
raise RuntimeError("Llama.eval(decode): Failed completely even with batch size 1.")
884+
885+
# Save successfully processed tokens into the Python-side ledger
886+
self.input_ids[n_past : n_past + current_batch_size] = chunk[:current_batch_size]
887+
888+
# Extract and save all logits if requested, ensuring we only copy the successfully processed rows
850889
if self._logits_all:
851-
rows = n_batch_tokens
890+
logits_ptr = self._ctx.get_logits()
891+
rows = current_batch_size
852892
cols = self._n_vocab
853893
logits_view = np.ctypeslib.as_array(logits_ptr, shape=(rows * cols,))
854-
self.scores[n_past : n_past + n_batch_tokens, :].reshape(-1)[:] = logits_view
855-
else:
856-
logits_view = np.ctypeslib.as_array(logits_ptr, shape=(self._n_vocab,))
857-
self.scores[0, :] = logits_view
894+
self.scores[n_past : n_past + current_batch_size, :].reshape(-1)[:] = logits_view
858895

859-
# Update n_tokens
896+
# Update indices based on actual processed batch size
860897
self.n_tokens += current_batch_size
861898
i += current_batch_size
862899

900+
# Periodic Checkpoint: Save states for hybrid models to avoid massive rollbacks
901+
if self.is_hybrid and self._hybrid_cache_mgr is not None:
902+
if (self.n_tokens - last_ckpt_pos >= self.checkpoint_interval) and (i < n_eval):
903+
if self.verbose:
904+
print(f"Llama.eval: [Periodic Checkpoint] Saving hybrid state at pos {self.n_tokens}.", file=sys.stderr)
905+
self._hybrid_cache_mgr.save_checkpoint(
906+
current_pos=self.n_tokens,
907+
tokens=self.input_ids[:self.n_tokens].tolist(),
908+
seq_id=0
909+
)
910+
last_ckpt_pos = self.n_tokens
911+
912+
# Save the final logit if not in _logits_all mode
913+
if not self._logits_all:
914+
logits_ptr = self._ctx.get_logits()
915+
logits_view = np.ctypeslib.as_array(logits_ptr, shape=(self._n_vocab,))
916+
self.scores[0, :] = logits_view
917+
863918
# Helper method: Convert dict logit_bias to List[llama_logit_bias]
864919
def _convert_logit_bias(self, logit_bias: Optional[Dict[int, float]]) -> List[llama_cpp.llama_logit_bias]:
865920
if not logit_bias:

0 commit comments

Comments
 (0)