Skip to content

Commit 2cccd2e

Browse files
committed
feat(core): implement thread-safe generation abort mechanism
- Add `AbortCriteria` class and a thread-safe `Llama.abort()` method to allow graceful interruption of ongoing text generation from external threads (e.g., UI or async environments). - Automatically inject `AbortCriteria` into the stopping criteria sequence at the start of `_create_completion`. - Ensure that when an abort is triggered, the partially generated `completion_tokens` are correctly detokenized and preserved. - Set `finish_reason` to `"abort"` when generation is interrupted, allowing downstream streaming clients to correctly identify manual cancellations. - Simplify and optimize the stopping criteria evaluation logic within the core `generate` loop. - Reorganize and sort module imports for better readability. Signed-off-by: JamePeng <jame_peng@sina.com>
1 parent 3ec636d commit 2cccd2e

1 file changed

Lines changed: 57 additions & 24 deletions

File tree

llama_cpp/llama.py

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
from __future__ import annotations
22

3+
import contextlib
4+
import ctypes
5+
import fnmatch
6+
import json
7+
import multiprocessing
38
import os
49
import sys
5-
import uuid
610
import time
7-
import json
8-
import ctypes
11+
import threading
912
import typing
10-
import random
11-
import fnmatch
13+
import uuid
1214
import warnings
13-
import contextlib
14-
import multiprocessing
15+
16+
import numpy as np
17+
import numpy.typing as npt
1518

1619
from typing import (
1720
Any,
@@ -29,7 +32,6 @@
2932
from collections import deque
3033
from pathlib import Path
3134

32-
3335
from .llama_types import *
3436
from .llama_grammar import LlamaGrammar
3537
from .llama_cache import (
@@ -46,9 +48,6 @@
4648

4749
from llama_cpp.llama_speculative import LlamaDraftModel
4850

49-
import numpy as np
50-
import numpy.typing as npt
51-
5251
import llama_cpp._internals as internals
5352
from ._internals import (
5453
LlamaSamplingContext,
@@ -60,6 +59,19 @@
6059
from ._utils import suppress_stdout_stderr
6160

6261

62+
class AbortCriteria:
63+
"""
64+
Listen for external interruption signals to trigger a stop condition.
65+
When an external thread calls `llama.abort()`, a loop interrupt is generated.
66+
"""
67+
def __init__(self, abort_event: threading.Event):
68+
self.abort_event = abort_event
69+
70+
def __call__(self, _input_ids: npt.NDArray[np.intc], _logits: npt.NDArray[np.single]) -> bool:
71+
# Note: _input_ids and _logits are required by the signature but unused here.
72+
return self.abort_event.is_set()
73+
74+
6375
class Llama:
6476
"""High-level Python wrapper for a llama.cpp model."""
6577

@@ -623,6 +635,9 @@ def __init__(
623635

624636
self._sampling_ctx: Optional[LlamaSamplingContext] = None
625637

638+
# Create a thread-safe interrupt event
639+
self._abort_event = threading.Event()
640+
626641
def close(self) -> None:
627642
"""Explicitly free the model from memory."""
628643
if getattr(self, "_sampling_ctx", None) is not None:
@@ -769,6 +784,15 @@ def reset(self):
769784
"""Reset the model state."""
770785
self.n_tokens = 0
771786

787+
def abort(self) -> None:
788+
"""
789+
Safely aborts any ongoing text generation.
790+
Useful for async API environments or UI interruption buttons.
791+
"""
792+
if self.verbose:
793+
print(f"Llama.abort: Abort signal received. Terminating generation...", file=sys.stderr)
794+
self._abort_event.set()
795+
772796
def eval(
773797
self,
774798
tokens: Sequence[int],
@@ -1442,26 +1466,18 @@ def adapter(token_data_array: llama_cpp.llama_token_data_array):
14421466

14431467
# Sample loop
14441468
while sample_idx < self.n_tokens:
1469+
if self._abort_event.is_set():
1470+
return
1471+
14451472
token = self._sampling_ctx.sample(self._ctx, idx=-1)
14461473
self._sampling_ctx.accept(token, False if grammar is None else True)
14471474

14481475
sample_idx += 1
14491476

1450-
# Halt generation if custom stopping criteria are met
14511477
if stopping_criteria is not None:
1452-
if self._logits_all:
1453-
logits_idx = sample_idx - self.n_tokens
1454-
check_stopping = True
1455-
else:
1456-
if sample_idx == self.n_tokens:
1457-
logits_idx = 0
1458-
check_stopping = True
1459-
else:
1460-
check_stopping = False
1461-
1462-
if check_stopping and stopping_criteria(
1478+
if stopping_criteria(
14631479
self._input_ids[: sample_idx],
1464-
self._scores[logits_idx, :]
1480+
self._scores[0 if not self._logits_all else sample_idx - self.n_tokens, :]
14651481
):
14661482
return
14671483

@@ -1746,6 +1762,8 @@ def _create_completion(
17461762
Iterator[CreateCompletionResponse], Iterator[CreateCompletionStreamResponse]
17471763
]:
17481764
assert suffix is None or suffix.__class__ is str
1765+
# Each time a new request is initiated, the previous abort state must be cleared.
1766+
self._abort_event.clear()
17491767

17501768
completion_id: str = f"cmpl-{str(uuid.uuid4())}"
17511769
created: int = int(time.time())
@@ -1885,6 +1903,11 @@ def _create_completion(
18851903
if self.verbose:
18861904
print("Llama._create_completion: cache miss", file=sys.stderr)
18871905

1906+
if stopping_criteria is None:
1907+
stopping_criteria = StoppingCriteriaList([AbortCriteria(self._abort_event)])
1908+
else:
1909+
stopping_criteria.append(AbortCriteria(self._abort_event))
1910+
18881911
finish_reason = "length"
18891912
multibyte_fix = 0
18901913
for token in self.generate(
@@ -1929,6 +1952,11 @@ def _create_completion(
19291952
finish_reason = "stop"
19301953
break
19311954

1955+
if self._abort_event.is_set():
1956+
text = self.detokenize(completion_tokens, prev_tokens=prompt_tokens)
1957+
finish_reason = "abort"
1958+
break
1959+
19321960
completion_tokens.append(token)
19331961

19341962
all_text = self.detokenize(completion_tokens, prev_tokens=prompt_tokens)
@@ -2108,6 +2136,11 @@ def _create_completion(
21082136
text = self.detokenize(completion_tokens, prev_tokens=prompt_tokens)
21092137
finish_reason = "stop"
21102138

2139+
# If the abort is triggered externally, force the `finish_reason` to be changed to "abort".
2140+
if self._abort_event.is_set():
2141+
text = self.detokenize(completion_tokens, prev_tokens=prompt_tokens)
2142+
finish_reason = "abort"
2143+
21112144
if self.verbose:
21122145
self._ctx.print_timings()
21132146

0 commit comments

Comments
 (0)