|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import contextlib |
| 4 | +import ctypes |
| 5 | +import fnmatch |
| 6 | +import json |
| 7 | +import multiprocessing |
3 | 8 | import os |
4 | 9 | import sys |
5 | | -import uuid |
6 | 10 | import time |
7 | | -import json |
8 | | -import ctypes |
| 11 | +import threading |
9 | 12 | import typing |
10 | | -import random |
11 | | -import fnmatch |
| 13 | +import uuid |
12 | 14 | import warnings |
13 | | -import contextlib |
14 | | -import multiprocessing |
| 15 | + |
| 16 | +import numpy as np |
| 17 | +import numpy.typing as npt |
15 | 18 |
|
16 | 19 | from typing import ( |
17 | 20 | Any, |
|
29 | 32 | from collections import deque |
30 | 33 | from pathlib import Path |
31 | 34 |
|
32 | | - |
33 | 35 | from .llama_types import * |
34 | 36 | from .llama_grammar import LlamaGrammar |
35 | 37 | from .llama_cache import ( |
|
46 | 48 |
|
47 | 49 | from llama_cpp.llama_speculative import LlamaDraftModel |
48 | 50 |
|
49 | | -import numpy as np |
50 | | -import numpy.typing as npt |
51 | | - |
52 | 51 | import llama_cpp._internals as internals |
53 | 52 | from ._internals import ( |
54 | 53 | LlamaSamplingContext, |
|
60 | 59 | from ._utils import suppress_stdout_stderr |
61 | 60 |
|
62 | 61 |
|
| 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 | + |
63 | 75 | class Llama: |
64 | 76 | """High-level Python wrapper for a llama.cpp model.""" |
65 | 77 |
|
@@ -623,6 +635,9 @@ def __init__( |
623 | 635 |
|
624 | 636 | self._sampling_ctx: Optional[LlamaSamplingContext] = None |
625 | 637 |
|
| 638 | + # Create a thread-safe interrupt event |
| 639 | + self._abort_event = threading.Event() |
| 640 | + |
626 | 641 | def close(self) -> None: |
627 | 642 | """Explicitly free the model from memory.""" |
628 | 643 | if getattr(self, "_sampling_ctx", None) is not None: |
@@ -769,6 +784,15 @@ def reset(self): |
769 | 784 | """Reset the model state.""" |
770 | 785 | self.n_tokens = 0 |
771 | 786 |
|
| 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 | + |
772 | 796 | def eval( |
773 | 797 | self, |
774 | 798 | tokens: Sequence[int], |
@@ -1442,26 +1466,18 @@ def adapter(token_data_array: llama_cpp.llama_token_data_array): |
1442 | 1466 |
|
1443 | 1467 | # Sample loop |
1444 | 1468 | while sample_idx < self.n_tokens: |
| 1469 | + if self._abort_event.is_set(): |
| 1470 | + return |
| 1471 | + |
1445 | 1472 | token = self._sampling_ctx.sample(self._ctx, idx=-1) |
1446 | 1473 | self._sampling_ctx.accept(token, False if grammar is None else True) |
1447 | 1474 |
|
1448 | 1475 | sample_idx += 1 |
1449 | 1476 |
|
1450 | | - # Halt generation if custom stopping criteria are met |
1451 | 1477 | 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( |
1463 | 1479 | 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, :] |
1465 | 1481 | ): |
1466 | 1482 | return |
1467 | 1483 |
|
@@ -1746,6 +1762,8 @@ def _create_completion( |
1746 | 1762 | Iterator[CreateCompletionResponse], Iterator[CreateCompletionStreamResponse] |
1747 | 1763 | ]: |
1748 | 1764 | 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() |
1749 | 1767 |
|
1750 | 1768 | completion_id: str = f"cmpl-{str(uuid.uuid4())}" |
1751 | 1769 | created: int = int(time.time()) |
@@ -1885,6 +1903,11 @@ def _create_completion( |
1885 | 1903 | if self.verbose: |
1886 | 1904 | print("Llama._create_completion: cache miss", file=sys.stderr) |
1887 | 1905 |
|
| 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 | + |
1888 | 1911 | finish_reason = "length" |
1889 | 1912 | multibyte_fix = 0 |
1890 | 1913 | for token in self.generate( |
@@ -1929,6 +1952,11 @@ def _create_completion( |
1929 | 1952 | finish_reason = "stop" |
1930 | 1953 | break |
1931 | 1954 |
|
| 1955 | + if self._abort_event.is_set(): |
| 1956 | + text = self.detokenize(completion_tokens, prev_tokens=prompt_tokens) |
| 1957 | + finish_reason = "abort" |
| 1958 | + break |
| 1959 | + |
1932 | 1960 | completion_tokens.append(token) |
1933 | 1961 |
|
1934 | 1962 | all_text = self.detokenize(completion_tokens, prev_tokens=prompt_tokens) |
@@ -2108,6 +2136,11 @@ def _create_completion( |
2108 | 2136 | text = self.detokenize(completion_tokens, prev_tokens=prompt_tokens) |
2109 | 2137 | finish_reason = "stop" |
2110 | 2138 |
|
| 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 | + |
2111 | 2144 | if self.verbose: |
2112 | 2145 | self._ctx.print_timings() |
2113 | 2146 |
|
|
0 commit comments