Ctrl K
ComunidadGuías…

llm/prefix_activity.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# SPDX-License-Identifier: BUSL-1.1
# Copyright (c) 2024-2026 Ali Sadeghi Aghili
"""When did a request last use each static prompt prefix?

The model server's prefix cache is refreshed by every request that starts
with a cached prefix: serving it counts as a use, and what was used last is
evicted last. So a prefix that a real question used a minute ago does not
need a keep-warm request (:class:`llm.warmup.KeepWarm`) -- that question
already did the keep-warm's job.

:class:`PrefixActivity` is the record of that, kept per prefix and not per
data source: :meth:`llm.router.LLMRouter._call_chain` knows the prompt it
sent, not which source it was routed to, and the keep-warm can build each
source's prefix (the same string, from the same cache) and ask about it.

Only the time of the last use is kept, as a hash of the prefix and never its
text. The record is bounded (the least recently used entry is dropped), so a
deployment with many data sources, or a test that builds many prefixes,
cannot grow it without limit.

Examples
--------
>>> clock = iter([100.0, 130.0, 500.0]).__next__
>>> activity = PrefixActivity(clock=clock)
>>> activity.seconds_since_use("prefix")
>>> activity.note_use("prefix")
>>> activity.seconds_since_use("prefix")
30.0
>>> activity.note_use("")  # no prefix, nothing recorded
>>> len(activity)
1
"""

from __future__ import annotations

import threading
import time
from collections import OrderedDict
from collections.abc import Callable

__all__ = ["PrefixActivity", "activity"]


class PrefixActivity:
    """Per-prefix time of the last request that used it. Thread-safe.

    Parameters
    ----------
    clock:
        Returns the current time in seconds on a monotonic scale; tests
        pass a fake one.
    max_entries:
        Prefixes remembered at once, far above the number of data sources a
        deployment has; only a bound on memory.
    """

    def __init__(
        self, clock: Callable[[], float] = time.monotonic, max_entries: int = 256,
    ) -> None:
        self._clock = clock
        self._max_entries = max_entries
        self._lock = threading.Lock()
        self._last: OrderedDict[tuple[int, int], float] = OrderedDict()

    @staticmethod
    def _key(prefix: str) -> tuple[int, int]:
        # hash() of a str is computed once per string object and cached on
        # it, and the static prefix is the one cached string object
        # ``build_static_prefix`` returns, so this is O(1) per request after
        # the first. The length makes an accidental collision of two
        # different prefixes vanishingly unlikely.
        return (len(prefix), hash(prefix))

    def note_use(self, prefix: str) -> None:
        """Record that a request using *prefix* has just been served.

        An empty *prefix* (a prompt with no static prefix: the retrieval
        path, interpretation, assumption extraction) is ignored.
        """
        if not prefix:
            return
        key = self._key(prefix)
        now = self._clock()
        with self._lock:
            self._last[key] = now
            self._last.move_to_end(key)
            while len(self._last) > self._max_entries:
                self._last.popitem(last=False)

    def seconds_since_use(self, prefix: str) -> float | None:
        """Seconds since *prefix* was last used, or ``None`` if never (this process)."""
        if not prefix:
            return None
        with self._lock:
            last = self._last.get(self._key(prefix))
        return None if last is None else max(self._clock() - last, 0.0)

    def clear(self) -> None:
        """Forget everything. Test-only."""
        with self._lock:
            self._last.clear()

    def __len__(self) -> int:
        with self._lock:
            return len(self._last)


#: The process-wide record the router writes to and the keep-warm reads.
activity = PrefixActivity()