innards 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- innards/__init__.py +19 -0
- innards/__main__.py +7 -0
- innards/_text.py +76 -0
- innards/backends/__init__.py +39 -0
- innards/backends/base.py +156 -0
- innards/backends/hf.py +505 -0
- innards/backends/hf_cache.py +205 -0
- innards/cli.py +200 -0
- innards/collectors/__init__.py +6 -0
- innards/collectors/memory.py +52 -0
- innards/collectors/timing.py +32 -0
- innards/kv/__init__.py +26 -0
- innards/kv/calculator.py +223 -0
- innards/kv/config.py +521 -0
- innards/kv/dtypes.py +56 -0
- innards/lineage/__init__.py +180 -0
- innards/lineage/data/fp8_kv.json +16 -0
- innards/lineage/data/gqa.json +19 -0
- innards/lineage/data/hybrid.json +20 -0
- innards/lineage/data/mha.json +19 -0
- innards/lineage/data/mla.json +18 -0
- innards/lineage/data/mqa.json +19 -0
- innards/lineage/data/sliding_window.json +19 -0
- innards/lineage/data/ssm.json +18 -0
- innards/observe/__init__.py +21 -0
- innards/observe/events.py +72 -0
- innards/observe/observer.py +278 -0
- innards/observe/queue.py +62 -0
- innards/py.typed +0 -0
- innards/schema.py +208 -0
- innards/session.py +653 -0
- innards/sinks/__init__.py +85 -0
- innards/sinks/console.py +95 -0
- innards/strategies/__init__.py +44 -0
- innards-0.1.0.dist-info/METADATA +401 -0
- innards-0.1.0.dist-info/RECORD +39 -0
- innards-0.1.0.dist-info/WHEEL +4 -0
- innards-0.1.0.dist-info/entry_points.txt +3 -0
- innards-0.1.0.dist-info/licenses/LICENSE +201 -0
innards/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Innards: see inside open-weight models, one message at a time.
|
|
2
|
+
|
|
3
|
+
>>> from innards import Session, AsyncSession, compare
|
|
4
|
+
>>> from innards.kv import predict
|
|
5
|
+
>>> from innards.lineage import explain
|
|
6
|
+
>>> from innards.schema import TurnRecord
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
__version__ = version("innards")
|
|
13
|
+
except PackageNotFoundError: # pragma: no cover - running from a source tree
|
|
14
|
+
__version__ = "0.0.0"
|
|
15
|
+
|
|
16
|
+
from innards.schema import Turn, TurnRecord
|
|
17
|
+
from innards.session import AsyncSession, CompareResult, Session, compare
|
|
18
|
+
|
|
19
|
+
__all__ = ["AsyncSession", "CompareResult", "Session", "Turn", "TurnRecord", "__version__", "compare"]
|
innards/__main__.py
ADDED
innards/_text.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Incremental detokenization for streaming, with stop-string hold-back."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Sequence
|
|
6
|
+
|
|
7
|
+
__all__ = ["IncrementalDecoder", "strip_stop"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def strip_stop(text: str, stop_strings: Sequence[str]) -> tuple[str, bool]:
|
|
11
|
+
"""Cut ``text`` at the first stop string. Returns ``(text, stopped)``."""
|
|
12
|
+
cut = min((i for i in (text.find(s) for s in stop_strings if s) if i >= 0), default=-1)
|
|
13
|
+
return (text[:cut], True) if cut >= 0 else (text, False)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class IncrementalDecoder:
|
|
17
|
+
"""Turn a growing list of token ids into text deltas.
|
|
18
|
+
|
|
19
|
+
Decodes a short window (prefix/read offsets) instead of the whole sequence,
|
|
20
|
+
holds back incomplete UTF-8 sequences, and holds back any tail that could be
|
|
21
|
+
the start of a stop string.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, decode: Callable[[list[int]], str], stop_strings: Sequence[str] = ()) -> None:
|
|
25
|
+
self._decode = decode
|
|
26
|
+
self._stop = [s for s in stop_strings if s]
|
|
27
|
+
self._ids: list[int] = []
|
|
28
|
+
self._prefix = 0
|
|
29
|
+
self._read = 0
|
|
30
|
+
self._decoded = "" # all text decoded so far
|
|
31
|
+
self._released = 0 # characters of _decoded handed to the caller
|
|
32
|
+
self.stopped = False
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def text(self) -> str:
|
|
36
|
+
"""Full text so far, cut at the first stop string."""
|
|
37
|
+
return strip_stop(self._decoded, self._stop)[0]
|
|
38
|
+
|
|
39
|
+
def _hold(self, text: str) -> int:
|
|
40
|
+
"""Length of the longest suffix of ``text`` that is a proper prefix of a stop string."""
|
|
41
|
+
best = 0
|
|
42
|
+
for stop in self._stop:
|
|
43
|
+
for n in range(min(len(stop) - 1, len(text)), 0, -1):
|
|
44
|
+
if text.endswith(stop[:n]):
|
|
45
|
+
best = max(best, n)
|
|
46
|
+
break
|
|
47
|
+
return best
|
|
48
|
+
|
|
49
|
+
def push(self, ids: Sequence[int]) -> str:
|
|
50
|
+
if self.stopped:
|
|
51
|
+
return ""
|
|
52
|
+
self._ids.extend(ids)
|
|
53
|
+
prefix_text = self._decode(self._ids[self._prefix : self._read])
|
|
54
|
+
new_text = self._decode(self._ids[self._prefix :])
|
|
55
|
+
if len(new_text) > len(prefix_text) and not new_text.endswith("�"):
|
|
56
|
+
self._decoded += new_text[len(prefix_text) :]
|
|
57
|
+
self._prefix, self._read = self._read, len(self._ids)
|
|
58
|
+
return self._release(final=False)
|
|
59
|
+
|
|
60
|
+
def flush(self) -> str:
|
|
61
|
+
if not self.stopped and self._read < len(self._ids):
|
|
62
|
+
tail = self._decode(self._ids[self._prefix :])[len(self._decode(self._ids[self._prefix : self._read])) :]
|
|
63
|
+
self._decoded += tail
|
|
64
|
+
self._read = len(self._ids)
|
|
65
|
+
return self._release(final=True)
|
|
66
|
+
|
|
67
|
+
def _release(self, final: bool) -> str:
|
|
68
|
+
text, stopped = strip_stop(self._decoded, self._stop)
|
|
69
|
+
if stopped:
|
|
70
|
+
self.stopped = True
|
|
71
|
+
end = len(text)
|
|
72
|
+
else:
|
|
73
|
+
end = len(text) if final else len(text) - self._hold(text)
|
|
74
|
+
out = text[self._released : end] if end > self._released else ""
|
|
75
|
+
self._released = max(self._released, end)
|
|
76
|
+
return out
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Engine adapters. ``hf`` (in-process Hugging Face Transformers) ships in this release."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from innards.backends.base import (
|
|
8
|
+
Backend,
|
|
9
|
+
ContextFullError,
|
|
10
|
+
KVReading,
|
|
11
|
+
PreparedTurn,
|
|
12
|
+
available_backends,
|
|
13
|
+
get_backend,
|
|
14
|
+
register_backend,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Backend",
|
|
19
|
+
"ContextFullError",
|
|
20
|
+
"KVReading",
|
|
21
|
+
"PreparedTurn",
|
|
22
|
+
"available_backends",
|
|
23
|
+
"get_backend",
|
|
24
|
+
"register_backend",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _hf_factory(model: str, **kwargs: Any) -> Backend:
|
|
29
|
+
try:
|
|
30
|
+
from innards.backends.hf import HFBackend
|
|
31
|
+
except ImportError as exc: # torch / transformers missing
|
|
32
|
+
raise ImportError(
|
|
33
|
+
"The 'hf' backend needs torch and transformers. Install them with: "
|
|
34
|
+
"pip install 'innards[hf]' (or: uv add 'innards[hf]')"
|
|
35
|
+
) from exc
|
|
36
|
+
return HFBackend(model, **kwargs)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
register_backend("hf", _hf_factory)
|
innards/backends/base.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Backend interface. HF Transformers ships in this release; vLLM, Ollama and API backends plug in here."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Protocol, runtime_checkable
|
|
9
|
+
|
|
10
|
+
from innards.kv.config import ModelSpec
|
|
11
|
+
from innards.observe.events import TurnEnd, TurnKey, TurnStats
|
|
12
|
+
from innards.schema import MemoryInfo
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Backend",
|
|
16
|
+
"ContextFullError",
|
|
17
|
+
"Deliver",
|
|
18
|
+
"Emit",
|
|
19
|
+
"KVReading",
|
|
20
|
+
"PreparedTurn",
|
|
21
|
+
"available_backends",
|
|
22
|
+
"get_backend",
|
|
23
|
+
"register_backend",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
Deliver = Callable[[Any], None]
|
|
27
|
+
"""Receives each chunk of generated token ids for the caller's stream (output path)."""
|
|
28
|
+
Emit = Callable[[int, TurnKey | None, Any], bool]
|
|
29
|
+
"""Observer hot-path hook: ``emit(kind, key, data)``; must never block."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ContextFullError(RuntimeError):
|
|
33
|
+
"""The prompt plus ``max_new_tokens`` would exceed the model's context window."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(slots=True)
|
|
37
|
+
class PreparedTurn:
|
|
38
|
+
"""A rendered prompt and the cache-reuse decision for it."""
|
|
39
|
+
|
|
40
|
+
input_ids: list[int]
|
|
41
|
+
cached_tokens: int
|
|
42
|
+
"""Prompt tokens already resident in the kept cache (prefix reuse)."""
|
|
43
|
+
prefix_reused: bool
|
|
44
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def prompt_tokens(self) -> int:
|
|
48
|
+
return len(self.input_ids)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def recomputed_tokens(self) -> int:
|
|
52
|
+
return self.prompt_tokens - self.cached_tokens
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True, slots=True)
|
|
56
|
+
class KVReading:
|
|
57
|
+
"""Measured KV, produced in an observer worker from a backend snapshot."""
|
|
58
|
+
|
|
59
|
+
total_bytes: int
|
|
60
|
+
per_layer_bytes: tuple[int, ...]
|
|
61
|
+
by_layer_type: dict[str, int]
|
|
62
|
+
source: str
|
|
63
|
+
layer_tokens: tuple[int | None, ...] = ()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@runtime_checkable
|
|
67
|
+
class Backend(Protocol):
|
|
68
|
+
"""What a Session needs from an engine adapter."""
|
|
69
|
+
|
|
70
|
+
name: str
|
|
71
|
+
model_id: str
|
|
72
|
+
|
|
73
|
+
def load(self) -> None: ...
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def spec(self) -> ModelSpec: ...
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def dtype(self) -> str | None: ...
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def kv_dtype(self) -> str: ...
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def max_new_tokens(self) -> int: ...
|
|
86
|
+
|
|
87
|
+
def runtime_facts(self) -> dict[str, Any]:
|
|
88
|
+
"""``host``, ``device``, ``engine_version`` for RuntimeInfo."""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
def prepare(self, messages: list[dict[str, str]]) -> PreparedTurn:
|
|
92
|
+
"""Render the conversation and decide how much of the kept cache to reuse."""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
def generate(
|
|
96
|
+
self,
|
|
97
|
+
prepared: PreparedTurn,
|
|
98
|
+
*,
|
|
99
|
+
deliver: Deliver | None = None,
|
|
100
|
+
emit: Emit | None = None,
|
|
101
|
+
key: TurnKey | None = None,
|
|
102
|
+
cancel: threading.Event | None = None,
|
|
103
|
+
) -> tuple[TurnEnd, list[int]]:
|
|
104
|
+
"""Run one turn in the calling thread. Returns raw end-of-turn facts and generated ids."""
|
|
105
|
+
...
|
|
106
|
+
|
|
107
|
+
def decode(self, token_ids: list[int]) -> str: ...
|
|
108
|
+
|
|
109
|
+
def final_text(self, token_ids: list[int]) -> str:
|
|
110
|
+
"""Text stored in history and the record (stop strings removed, stripped)."""
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def stop_strings(self) -> tuple[str, ...]: ...
|
|
115
|
+
|
|
116
|
+
def reset(self) -> None:
|
|
117
|
+
"""Drop the kept cache (next turn prefills from scratch)."""
|
|
118
|
+
...
|
|
119
|
+
|
|
120
|
+
def measure_kv(self, snapshot: Any) -> KVReading | None:
|
|
121
|
+
"""Turn a cache snapshot into bytes. Runs in an observer worker."""
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
def memory_sampler(self) -> Callable[[], int | None] | None: ...
|
|
125
|
+
|
|
126
|
+
def memory_info(self, end: TurnEnd, stats: TurnStats, kv_bytes: int | None) -> MemoryInfo:
|
|
127
|
+
"""Memory split for a finished turn. Runs in an observer worker."""
|
|
128
|
+
...
|
|
129
|
+
|
|
130
|
+
def kv_memory_budget(self) -> int | None:
|
|
131
|
+
"""Bytes available for KV, for capacity projections."""
|
|
132
|
+
...
|
|
133
|
+
|
|
134
|
+
def composition(self, messages: list[dict[str, str]], prompt_tokens: int) -> dict[str, int | None]:
|
|
135
|
+
"""Token counts for system / history / new message. Runs in an observer worker."""
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
def close(self) -> None: ...
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
_BACKENDS: dict[str, Callable[..., Backend]] = {}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def register_backend(name: str, factory: Callable[..., Backend]) -> None:
|
|
145
|
+
_BACKENDS[name] = factory
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def available_backends() -> list[str]:
|
|
149
|
+
return sorted(_BACKENDS)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_backend(name: str, model: str, **kwargs: Any) -> Backend:
|
|
153
|
+
"""Create a backend by name (``"hf"``)."""
|
|
154
|
+
if name not in _BACKENDS:
|
|
155
|
+
raise ValueError(f"Unknown backend {name!r}. Available: {', '.join(available_backends())}")
|
|
156
|
+
return _BACKENDS[name](model, **kwargs)
|