initium-deeplearning 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.
- initium/LNN_controller/cfc_backbone_parallel.py +265 -0
- initium/LNN_controller/cfc_controller.py +325 -0
- initium/LNN_controller/chained_controller.py +83 -0
- initium/LNN_controller/hybrid_controller.py +90 -0
- initium/MoE/README.md +1 -0
- initium/MoE/moe_layer.py +552 -0
- initium/__init__.py +1 -0
- initium/config/controller_config.py +86 -0
- initium/config/train_config.py +64 -0
- initium/core_training.py +2182 -0
- initium/data/__init__.py +0 -0
- initium/data/audio/__init__.py +0 -0
- initium/data/audio/audio_dataset.py +96 -0
- initium/data/base_dataset.py +75 -0
- initium/data/common/__init__.py +0 -0
- initium/data/common/chain_task.py +542 -0
- initium/data/common/digit_codec.py +85 -0
- initium/data/common/graph_io.py +123 -0
- initium/data/common/real_data.py +747 -0
- initium/data/dataset_registry.py +59 -0
- initium/data/graph_traversal/__init__.py +0 -0
- initium/data/graph_traversal/graph_traversal.py +746 -0
- initium/data/multimodal/__init__.py +0 -0
- initium/data/multimodal/multimodal_dataset.py +401 -0
- initium/data/text/__init__.py +0 -0
- initium/data/text/text_dataset.py +79 -0
- initium/data/video/__init__.py +0 -0
- initium/data/video/video_dataset.py +85 -0
- initium/inference/__init__.py +1 -0
- initium/inference/cache/__init__.py +1 -0
- initium/inference/cache/base_cache.py +112 -0
- initium/inference/cache/cache_config.py +21 -0
- initium/inference/cache/cache_registry.py +99 -0
- initium/inference/cache/cached_engine.py +264 -0
- initium/inference/cache/keys.py +77 -0
- initium/inference/cache/prefix_cache.py +59 -0
- initium/inference/cache/result_cache.py +57 -0
- initium/inference/cache/state_utils.py +74 -0
- initium/inference/cache/stores.py +167 -0
- initium/inference/capabilities.py +86 -0
- initium/inference/checkpoint_io.py +63 -0
- initium/inference/engine.py +88 -0
- initium/inference/inference_config.py +22 -0
- initium/inference/metrics.py +103 -0
- initium/inference/model_loader.py +270 -0
- initium/inference/run_inference.py +526 -0
- initium/inference/run_logging.py +173 -0
- initium/inference/tasks/__init__.py +1 -0
- initium/inference/tasks/audio_task.py +114 -0
- initium/inference/tasks/base_task.py +60 -0
- initium/inference/tasks/graph_traversal_task.py +235 -0
- initium/inference/tasks/multimodal_task.py +74 -0
- initium/inference/tasks/task_registry.py +68 -0
- initium/inference/tasks/text_task.py +114 -0
- initium/inference/tasks/video_task.py +114 -0
- initium/mamba_controller/mamba2_controller.py +433 -0
- initium/mamba_controller/mamba3_controller.py +242 -0
- initium/mamba_controller/mamba_backbone_parallel.py +291 -0
- initium/mamba_controller/mamba_controller.py +983 -0
- initium/mamba_controller/split_graph_dnc.py +623 -0
- initium/memory_manipulation/dynamic_memory_resize.py +157 -0
- initium/memory_manipulation/dynamic_n_controller.py +152 -0
- initium/memory_manipulation/link_matrix_ablation.py +130 -0
- initium/memory_manipulation/nvrtc_compat.py +143 -0
- initium/memory_manipulation/stochastic_write_head_v2.py +545 -0
- initium/setup_wheels.py +175 -0
- initium_deeplearning-0.1.0.dist-info/METADATA +253 -0
- initium_deeplearning-0.1.0.dist-info/RECORD +71 -0
- initium_deeplearning-0.1.0.dist-info/WHEEL +4 -0
- initium_deeplearning-0.1.0.dist-info/entry_points.txt +3 -0
- initium_deeplearning-0.1.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/cache/cache_config.py
|
|
3
|
+
|
|
4
|
+
Cache-side constants. Pure constants, imported explicitly (same convention as
|
|
5
|
+
inference_config.py).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# "off" | "prefix" | "result" | "prefix,result" | "all". Default off: the default episodes
|
|
9
|
+
# reshuffle the edge list every time (nothing to reuse), and persistent-mode experiments
|
|
10
|
+
# should not change behaviour silently. Set to "all" to make caching on-by-default.
|
|
11
|
+
DEFAULT_CACHE = "off"
|
|
12
|
+
|
|
13
|
+
DEFAULT_CACHE_RAM_MB = 512
|
|
14
|
+
DEFAULT_CACHE_DISK_MB = 2048
|
|
15
|
+
|
|
16
|
+
# Number of cache hits that are re-computed WITHOUT the cache and compared. On a
|
|
17
|
+
# mismatch the cache is disabled for the rest of the run (correctness over speed).
|
|
18
|
+
DEFAULT_VERIFY_HITS = 2
|
|
19
|
+
DEFAULT_VERIFY_TOL = 1e-3 # max abs diff on scored outputs and on carried state
|
|
20
|
+
|
|
21
|
+
ROOT_CHAIN = "root" # history-chain seed (persistent-mode result keys)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/cache/cache_registry.py
|
|
3
|
+
|
|
4
|
+
Single entry point used by run_inference.py (counterpart of data/dataset_registry.py):
|
|
5
|
+
caches, notes, fingerprint = setup_caches(spec, ...)
|
|
6
|
+
Adding a cache type = subclass BaseCache + one line in CACHE_CLASSES.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
from initium.inference.cache.base_cache import BaseCache
|
|
14
|
+
from initium.inference.cache.keys import model_fingerprint
|
|
15
|
+
from initium.inference.cache.prefix_cache import PrefixStateCache
|
|
16
|
+
from initium.inference.cache.result_cache import ResultCache
|
|
17
|
+
from initium.inference.cache.stores import DiskStore, MemoryStore, TieredStore
|
|
18
|
+
|
|
19
|
+
CACHE_CLASSES = {"prefix": PrefixStateCache, "result": ResultCache}
|
|
20
|
+
_OFF = {"", "off", "none", "false", "0", "no"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_cache_spec(spec: str) -> list[str]:
|
|
24
|
+
s = (spec or "off").strip().lower()
|
|
25
|
+
if s in _OFF:
|
|
26
|
+
return []
|
|
27
|
+
if s in ("all", "on"):
|
|
28
|
+
return list(CACHE_CLASSES)
|
|
29
|
+
names = []
|
|
30
|
+
for part in s.replace("+", ",").split(","):
|
|
31
|
+
part = part.strip()
|
|
32
|
+
if not part:
|
|
33
|
+
continue
|
|
34
|
+
if part not in CACHE_CLASSES:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"unknown cache type {part!r}; expected off, all, or a comma list of "
|
|
37
|
+
f"{sorted(CACHE_CLASSES)}"
|
|
38
|
+
)
|
|
39
|
+
if part not in names:
|
|
40
|
+
names.append(part)
|
|
41
|
+
return names
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def setup_caches(
|
|
45
|
+
spec,
|
|
46
|
+
*,
|
|
47
|
+
ckpt,
|
|
48
|
+
device,
|
|
49
|
+
sampled_writes,
|
|
50
|
+
deterministic_write,
|
|
51
|
+
ablate_memory,
|
|
52
|
+
combiner_skip_stages=None,
|
|
53
|
+
cache_dir=None,
|
|
54
|
+
ram_mb=512.0,
|
|
55
|
+
disk_mb=2048.0,
|
|
56
|
+
clear=False,
|
|
57
|
+
allow_stochastic=False,
|
|
58
|
+
):
|
|
59
|
+
"""-> (caches, notes, model_fingerprint|None). Returns no caches (with a note) when
|
|
60
|
+
the requested caching cannot be done safely."""
|
|
61
|
+
notes: list[str] = []
|
|
62
|
+
names = parse_cache_spec(spec)
|
|
63
|
+
if not names:
|
|
64
|
+
return [], notes, None
|
|
65
|
+
|
|
66
|
+
if sampled_writes and not allow_stochastic:
|
|
67
|
+
notes.append(
|
|
68
|
+
"caching disabled: this model samples its write vectors (beta>0 without "
|
|
69
|
+
"--deterministic-write), so every forward pass is random and cached "
|
|
70
|
+
"results/states would not be reproducible. Use --deterministic-write, or "
|
|
71
|
+
"--cache-allow-stochastic to override."
|
|
72
|
+
)
|
|
73
|
+
return [], notes, None
|
|
74
|
+
if sampled_writes:
|
|
75
|
+
notes.append(
|
|
76
|
+
"WARNING: caching a stochastic model -- cached entries are ONE random draw "
|
|
77
|
+
"reused for every hit (and across runs if --cache-dir is set); "
|
|
78
|
+
"verification is skipped."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
print("[cache] fingerprinting model weights (once) ...")
|
|
82
|
+
fp = model_fingerprint(
|
|
83
|
+
ckpt,
|
|
84
|
+
{
|
|
85
|
+
"deterministic_write": bool(deterministic_write),
|
|
86
|
+
"sampled_writes": bool(sampled_writes),
|
|
87
|
+
"ablate_memory": bool(ablate_memory),
|
|
88
|
+
"combiner_skip_stages": sorted(combiner_skip_stages) if combiner_skip_stages else None,
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
disk = None
|
|
92
|
+
if cache_dir:
|
|
93
|
+
disk = DiskStore(os.path.join(cache_dir, fp), int(disk_mb * 1e6))
|
|
94
|
+
if clear:
|
|
95
|
+
disk.clear()
|
|
96
|
+
notes.append(f"disk cache cleared: {disk.root}")
|
|
97
|
+
store = TieredStore(MemoryStore(int(ram_mb * 1e6)), disk)
|
|
98
|
+
caches: list[BaseCache] = [CACHE_CLASSES[n](store, fp, device) for n in names]
|
|
99
|
+
return caches, notes, fp
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/cache/cached_engine.py
|
|
3
|
+
|
|
4
|
+
InferenceEngine + caches. Only used when at least one cache is active, so with caching
|
|
5
|
+
off the plain engine (and its behaviour) is untouched.
|
|
6
|
+
|
|
7
|
+
Per episode:
|
|
8
|
+
1. episode-level lookups (ResultCache: skip the forward pass entirely)
|
|
9
|
+
2. else compute, resuming from the longest cached prefix snapshot (PrefixStateCache)
|
|
10
|
+
and snapshotting every not-yet-cached boundary on the way, in segments
|
|
11
|
+
3. store the episode result in the episode-level caches
|
|
12
|
+
|
|
13
|
+
Correctness net: for the first `verify_hits` cache hits the episode is ALSO recomputed
|
|
14
|
+
without any cache and compared (scored outputs + carried state, tolerance verify_tol).
|
|
15
|
+
A mismatch (or NaN) prints a warning, uses the recomputed values, and disables all
|
|
16
|
+
caches for the remainder of the run.
|
|
17
|
+
|
|
18
|
+
Segment execution:
|
|
19
|
+
MambaDNC / stock DNC : model(x[start:end], hidden, reset_experience=False) (state passes through)
|
|
20
|
+
SplitGraphDNC : model(x[:end], hidden, ..., start_step=start) -- the parallel
|
|
21
|
+
backbone is recomputed over the whole prefix (cheap, stateless
|
|
22
|
+
across calls, keeps outputs exact); only the sequential memory
|
|
23
|
+
loop is skipped for t < start.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import inspect
|
|
29
|
+
import time
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
import torch
|
|
33
|
+
|
|
34
|
+
from initium.inference.cache.base_cache import EpisodeCtx
|
|
35
|
+
from initium.inference.cache.cache_config import DEFAULT_VERIFY_HITS, DEFAULT_VERIFY_TOL, ROOT_CHAIN
|
|
36
|
+
from initium.inference.cache.keys import chain_next, hash_tensor
|
|
37
|
+
from initium.inference.cache.state_utils import clone_tree, tree_max_abs_diff
|
|
38
|
+
from initium.inference.engine import FRESH_HIDDEN, InferenceEngine
|
|
39
|
+
from initium.inference.metrics import EpisodeResult
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def model_supports_resume(model) -> bool:
|
|
43
|
+
if hasattr(model, "backbone"): # SplitGraphDNC
|
|
44
|
+
return "start_step" in inspect.signature(model.forward).parameters
|
|
45
|
+
return True # MambaDNC / stock dnc: hx passes through
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CachedInferenceEngine(InferenceEngine):
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
model,
|
|
52
|
+
output_proj,
|
|
53
|
+
task,
|
|
54
|
+
device,
|
|
55
|
+
caches,
|
|
56
|
+
ablate_memory: bool = False,
|
|
57
|
+
combiner_skip_stages: frozenset | None = None,
|
|
58
|
+
verify_hits: int = DEFAULT_VERIFY_HITS,
|
|
59
|
+
verify_tol: float = DEFAULT_VERIFY_TOL,
|
|
60
|
+
):
|
|
61
|
+
super().__init__(model, output_proj, task, device, ablate_memory, combiner_skip_stages)
|
|
62
|
+
self.caches = list(caches)
|
|
63
|
+
self.resumable = model_supports_resume(model)
|
|
64
|
+
self.verify_hits, self.verify_tol = verify_hits, verify_tol
|
|
65
|
+
self.verify = {
|
|
66
|
+
"checked": 0,
|
|
67
|
+
"failures": 0,
|
|
68
|
+
"max_output_diff": 0.0,
|
|
69
|
+
"max_state_diff": 0.0,
|
|
70
|
+
"disabled_after_failure": False,
|
|
71
|
+
}
|
|
72
|
+
self.notes: list[str] = []
|
|
73
|
+
self._disabled = False
|
|
74
|
+
self._warned_bounds = False
|
|
75
|
+
|
|
76
|
+
# ---- small utils -----------------------------------------------------------
|
|
77
|
+
def _note(self, msg: str) -> None:
|
|
78
|
+
if msg not in self.notes:
|
|
79
|
+
self.notes.append(msg)
|
|
80
|
+
print(f"[cache] {msg}")
|
|
81
|
+
|
|
82
|
+
def _sync(self) -> None:
|
|
83
|
+
if self.device.type == "cuda":
|
|
84
|
+
torch.cuda.synchronize(self.device)
|
|
85
|
+
|
|
86
|
+
def cache_report(self, reset: bool = False) -> dict:
|
|
87
|
+
return {c.name: c.report(reset=reset) for c in self.caches}
|
|
88
|
+
|
|
89
|
+
def _select_active(self, reset_experience: bool):
|
|
90
|
+
active = []
|
|
91
|
+
for c in self.caches:
|
|
92
|
+
ok, why = c.applicable(reset_experience=reset_experience, resumable=self.resumable)
|
|
93
|
+
if ok:
|
|
94
|
+
active.append(c)
|
|
95
|
+
else:
|
|
96
|
+
self._note(f"{c.name} cache inactive: {why}")
|
|
97
|
+
return active
|
|
98
|
+
|
|
99
|
+
def _valid_boundaries(self, ep) -> list[int]:
|
|
100
|
+
T = ep.input_seq.shape[0]
|
|
101
|
+
bs = sorted({int(b) for b in ep.cache_boundaries})
|
|
102
|
+
ok = [b for b in bs if 0 < b < T and float(ep.mask[:b].sum()) == 0.0]
|
|
103
|
+
if len(ok) != len(bs) and not self._warned_bounds:
|
|
104
|
+
self._warned_bounds = True
|
|
105
|
+
self._note(
|
|
106
|
+
"some declared cache boundaries were dropped (out of range, or a scored "
|
|
107
|
+
"step lies before them)"
|
|
108
|
+
)
|
|
109
|
+
return ok
|
|
110
|
+
|
|
111
|
+
# ---- model execution ---------------------------------------------------------
|
|
112
|
+
@torch.no_grad()
|
|
113
|
+
def _forward_segment(
|
|
114
|
+
self, x_full: torch.Tensor, start: int, end: int, hidden, reset_experience: bool
|
|
115
|
+
):
|
|
116
|
+
kw: dict[str, Any] = dict(
|
|
117
|
+
reset_experience=reset_experience, pass_through_memory=not self.ablate_memory
|
|
118
|
+
)
|
|
119
|
+
if self.combiner_skip_stages:
|
|
120
|
+
kw["combiner_skip_stages"] = self.combiner_skip_stages
|
|
121
|
+
if hasattr(self.model, "backbone"): # SplitGraphDNC
|
|
122
|
+
x = x_full[:end].unsqueeze(0).to(self.device)
|
|
123
|
+
if start > 0:
|
|
124
|
+
kw["start_step"] = start
|
|
125
|
+
else:
|
|
126
|
+
x = x_full[start:end].unsqueeze(0).to(self.device)
|
|
127
|
+
output, new_hidden = self.model(x, hidden, **kw)
|
|
128
|
+
output = output.transpose(0, 1).contiguous().squeeze(0)
|
|
129
|
+
output = self.output_proj(output)
|
|
130
|
+
return output.float().cpu(), new_hidden
|
|
131
|
+
|
|
132
|
+
def _compute_full(self, ctx: EpisodeCtx, hidden):
|
|
133
|
+
"""Uncached reference: one forward over the whole episode."""
|
|
134
|
+
x = ctx.episode.input_seq
|
|
135
|
+
t0 = time.perf_counter()
|
|
136
|
+
out, hid = self._forward_segment(x, 0, x.shape[0], hidden, ctx.reset_experience)
|
|
137
|
+
self._sync()
|
|
138
|
+
return out, hid, (time.perf_counter() - t0) * 1000.0
|
|
139
|
+
|
|
140
|
+
def _compute(self, ctx: EpisodeCtx, hidden, active):
|
|
141
|
+
x = ctx.episode.input_seq
|
|
142
|
+
T = x.shape[0]
|
|
143
|
+
use_bounds = ctx.reset_experience and self.resumable and bool(ctx.boundaries)
|
|
144
|
+
resume = None
|
|
145
|
+
if use_bounds:
|
|
146
|
+
for c in active:
|
|
147
|
+
r = c.find_resume(ctx)
|
|
148
|
+
if r is not None and (resume is None or r.position > resume.position):
|
|
149
|
+
resume = r
|
|
150
|
+
start = resume.position if resume is not None else 0
|
|
151
|
+
hid = resume.hidden if resume is not None else hidden
|
|
152
|
+
seg_reset = ctx.reset_experience and resume is None
|
|
153
|
+
cuts = [b for b in ctx.boundaries if b > start] if use_bounds else []
|
|
154
|
+
cuts.append(T)
|
|
155
|
+
|
|
156
|
+
cold_ms = resume.saved_ms if resume is not None else 0.0 # cost as if computed from scratch
|
|
157
|
+
segs, pos = [], start
|
|
158
|
+
for cut in cuts:
|
|
159
|
+
t0 = time.perf_counter()
|
|
160
|
+
seg, hid = self._forward_segment(x, pos, cut, hid, seg_reset)
|
|
161
|
+
self._sync()
|
|
162
|
+
cold_ms += (time.perf_counter() - t0) * 1000.0
|
|
163
|
+
segs.append(seg)
|
|
164
|
+
if cut < T: # snapshot BEFORE the next segment mutates state
|
|
165
|
+
for c in active:
|
|
166
|
+
c.store_boundary(ctx, cut, hid, cold_ms)
|
|
167
|
+
pos, seg_reset = cut, False
|
|
168
|
+
out = torch.cat(segs, dim=0)
|
|
169
|
+
if start > 0: # prefix rows are never scored (validated)
|
|
170
|
+
out = torch.cat([torch.zeros(start, out.shape[1]), out], dim=0)
|
|
171
|
+
return out, hid, resume, cold_ms
|
|
172
|
+
|
|
173
|
+
# ---- verification -------------------------------------------------------------
|
|
174
|
+
def _maybe_verify(self, ctx, incoming_hidden, out, new_hidden, event, state_valid):
|
|
175
|
+
v = self.verify
|
|
176
|
+
if self._disabled or v["checked"] >= self.verify_hits:
|
|
177
|
+
return out, new_hidden, event
|
|
178
|
+
ref_out, ref_hidden, _ = self._compute_full(ctx, clone_tree(incoming_hidden))
|
|
179
|
+
mask = ctx.episode.mask == 1
|
|
180
|
+
d_out = (out[mask] - ref_out[mask]).abs().max().item() if bool(mask.any()) else 0.0
|
|
181
|
+
d_state = tree_max_abs_diff(new_hidden, ref_hidden) if state_valid else 0.0
|
|
182
|
+
v["checked"] += 1
|
|
183
|
+
v["max_output_diff"] = max(v["max_output_diff"], d_out)
|
|
184
|
+
v["max_state_diff"] = max(v["max_state_diff"], d_state)
|
|
185
|
+
if d_out <= self.verify_tol and d_state <= self.verify_tol: # NaN-safe form
|
|
186
|
+
return out, new_hidden, event
|
|
187
|
+
v["failures"] += 1
|
|
188
|
+
v["disabled_after_failure"] = True
|
|
189
|
+
self._disabled = True
|
|
190
|
+
self._note(
|
|
191
|
+
f"VERIFICATION FAILED at episode {ctx.index} ({event}): max|d output|={d_out:.3e}, "
|
|
192
|
+
f"max|d state|={d_state:.3e} > tol {self.verify_tol:g}. Caches disabled for the "
|
|
193
|
+
f"rest of this run; using the recomputed result."
|
|
194
|
+
)
|
|
195
|
+
return ref_out, ref_hidden, event + ":VERIFY-FAILED"
|
|
196
|
+
|
|
197
|
+
# ---- one episode --------------------------------------------------------------
|
|
198
|
+
def _run_one(self, ctx: EpisodeCtx, hidden, active):
|
|
199
|
+
for c in active:
|
|
200
|
+
hit = c.lookup_episode(ctx)
|
|
201
|
+
if hit is not None:
|
|
202
|
+
new_hidden = FRESH_HIDDEN if ctx.reset_experience else hit.hidden
|
|
203
|
+
return self._maybe_verify(
|
|
204
|
+
ctx,
|
|
205
|
+
hidden,
|
|
206
|
+
hit.output,
|
|
207
|
+
new_hidden,
|
|
208
|
+
f"hit:{c.name}",
|
|
209
|
+
state_valid=not ctx.reset_experience,
|
|
210
|
+
)
|
|
211
|
+
out, new_hidden, resume, cold_ms = self._compute(ctx, hidden, active)
|
|
212
|
+
event = "miss"
|
|
213
|
+
if resume is not None:
|
|
214
|
+
event = f"hit:{resume.cache_name}@{resume.position}"
|
|
215
|
+
out, new_hidden, event = self._maybe_verify(
|
|
216
|
+
ctx, hidden, out, new_hidden, event, state_valid=True
|
|
217
|
+
)
|
|
218
|
+
if not self._disabled:
|
|
219
|
+
for c in active:
|
|
220
|
+
c.store_episode(ctx, out, new_hidden, cold_ms)
|
|
221
|
+
return out, new_hidden, event
|
|
222
|
+
|
|
223
|
+
def run(
|
|
224
|
+
self,
|
|
225
|
+
episodes,
|
|
226
|
+
reset_experience: bool,
|
|
227
|
+
verbose_n: int = 0,
|
|
228
|
+
progress_every: int = 0,
|
|
229
|
+
label: str = "",
|
|
230
|
+
) -> list[EpisodeResult]:
|
|
231
|
+
active = self._select_active(reset_experience)
|
|
232
|
+
want_bounds = any(c.wants_boundaries for c in active)
|
|
233
|
+
hidden, chain = FRESH_HIDDEN, ROOT_CHAIN
|
|
234
|
+
results: list[EpisodeResult] = []
|
|
235
|
+
run_items = run_correct = 0
|
|
236
|
+
for i, ep in enumerate(episodes):
|
|
237
|
+
if reset_experience:
|
|
238
|
+
hidden = FRESH_HIDDEN
|
|
239
|
+
t0 = time.perf_counter()
|
|
240
|
+
act = [] if self._disabled else active
|
|
241
|
+
ctx = EpisodeCtx(
|
|
242
|
+
index=i,
|
|
243
|
+
episode=ep,
|
|
244
|
+
reset_experience=reset_experience,
|
|
245
|
+
in_hash=hash_tensor(ep.input_seq) if act else "",
|
|
246
|
+
chain=chain,
|
|
247
|
+
boundaries=self._valid_boundaries(ep) if (want_bounds and act) else [],
|
|
248
|
+
)
|
|
249
|
+
out, hidden, event = self._run_one(ctx, hidden, act)
|
|
250
|
+
self._sync()
|
|
251
|
+
elapsed_ms = (time.perf_counter() - t0) * 1000.0
|
|
252
|
+
if not reset_experience: # fresh mode: chain stays "root" (state is always fresh)
|
|
253
|
+
chain = chain_next(chain, ctx.in_hash)
|
|
254
|
+
|
|
255
|
+
score = self.task.score_episode(out, ep, verbose=(i < verbose_n))
|
|
256
|
+
results.append(EpisodeResult(i, score, elapsed_ms, reset_experience, cache_event=event))
|
|
257
|
+
run_items += score.n_items
|
|
258
|
+
run_correct += score.n_correct
|
|
259
|
+
if progress_every and ((i + 1) % progress_every == 0 or i + 1 == len(episodes)):
|
|
260
|
+
print(
|
|
261
|
+
f"[engine{label}] episode {i + 1}/{len(episodes)} | "
|
|
262
|
+
f"running item acc {100.0 * run_correct / max(run_items, 1):.2f}%"
|
|
263
|
+
)
|
|
264
|
+
return results
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/cache/keys.py
|
|
3
|
+
|
|
4
|
+
Hashing for cache keys. blake2b (fast, stdlib). A cache key always contains the
|
|
5
|
+
MODEL FINGERPRINT (hash of every weight + config + the flags that change the
|
|
6
|
+
forward pass), so entries can never leak between different checkpoints.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import torch
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def new_hasher():
|
|
19
|
+
return hashlib.blake2b(digest_size=16)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def update_with_tensor(h, t: torch.Tensor) -> None:
|
|
23
|
+
t = t.detach().cpu().contiguous()
|
|
24
|
+
h.update(str(t.dtype).encode())
|
|
25
|
+
h.update(str(tuple(t.shape)).encode())
|
|
26
|
+
if t.numel():
|
|
27
|
+
h.update(t.reshape(-1).view(torch.uint8).numpy())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def hash_tensor(t: torch.Tensor) -> str:
|
|
31
|
+
h = new_hasher()
|
|
32
|
+
update_with_tensor(h, t)
|
|
33
|
+
return h.hexdigest()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def digest(*parts: Any) -> str:
|
|
37
|
+
h = new_hasher()
|
|
38
|
+
for p in parts:
|
|
39
|
+
h.update(str(p).encode())
|
|
40
|
+
h.update(b"\x1f")
|
|
41
|
+
return h.hexdigest()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def chain_next(chain: str, in_hash: str) -> str:
|
|
45
|
+
"""Persistent mode: the state after episode k is a function of (initial state,
|
|
46
|
+
inputs 0..k). Hashing the inputs into a chain therefore identifies the state
|
|
47
|
+
exactly, without hashing the (large) memory tensors every episode."""
|
|
48
|
+
return digest(chain, in_hash)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _update_obj(h, obj: Any) -> None:
|
|
52
|
+
if isinstance(obj, torch.Tensor):
|
|
53
|
+
update_with_tensor(h, obj)
|
|
54
|
+
elif isinstance(obj, dict):
|
|
55
|
+
for k in sorted(obj, key=str):
|
|
56
|
+
h.update(str(k).encode())
|
|
57
|
+
_update_obj(h, obj[k])
|
|
58
|
+
elif isinstance(obj, list | tuple):
|
|
59
|
+
for v in obj:
|
|
60
|
+
_update_obj(h, v)
|
|
61
|
+
else:
|
|
62
|
+
h.update(repr(obj).encode())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def model_fingerprint(ckpt: dict, extras: dict) -> str:
|
|
66
|
+
"""Hash of weights (rnn + output_proj + prior buffers) + model_config + `extras`
|
|
67
|
+
(flags that alter the forward pass: deterministic_write, ablate_memory, ...).
|
|
68
|
+
Takes a couple of seconds for a large model; only computed when caching is on.
|
|
69
|
+
Deliberately NOT included: device / torch version (tiny numeric differences are
|
|
70
|
+
what --cache-verify is for)."""
|
|
71
|
+
h = new_hasher()
|
|
72
|
+
_update_obj(h, ckpt["rnn_state_dict"])
|
|
73
|
+
_update_obj(h, ckpt["output_proj_state_dict"])
|
|
74
|
+
_update_obj(h, ckpt.get("prior_state"))
|
|
75
|
+
h.update(json.dumps(ckpt["model_config"], sort_keys=True, default=str).encode())
|
|
76
|
+
h.update(json.dumps(extras, sort_keys=True, default=str).encode())
|
|
77
|
+
return h.hexdigest()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/cache/prefix_cache.py
|
|
3
|
+
|
|
4
|
+
Prefix STATE cache (the DNC analogue of a KV / context cache). After the static
|
|
5
|
+
prefix of an episode (e.g. the whole edge listing) has been processed, the complete
|
|
6
|
+
state (controller state, Memory dict incl. link matrix / usage / weights, last read
|
|
7
|
+
vector) is snapshotted, keyed by (model fingerprint, hash of the prefix tokens).
|
|
8
|
+
Later episodes with the SAME prefix restore it and process only their suffix.
|
|
9
|
+
|
|
10
|
+
Valid because the model is causal and deterministic: state after step p depends only
|
|
11
|
+
on inputs [0, p). Requires reset_experience=True (identical start state) and a task
|
|
12
|
+
that declares cache_boundaries. If several boundaries are declared, the LONGEST cached
|
|
13
|
+
one is used (longest-prefix match) and the remaining ones are stored on the way.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from initium.inference.cache.base_cache import BaseCache, EpisodeCtx, ResumePoint
|
|
19
|
+
from initium.inference.cache.state_utils import restore, snapshot
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PrefixStateCache(BaseCache):
|
|
23
|
+
name = "prefix"
|
|
24
|
+
wants_boundaries = True
|
|
25
|
+
|
|
26
|
+
def applicable(self, *, reset_experience: bool, resumable: bool) -> tuple[bool, str]:
|
|
27
|
+
if not reset_experience:
|
|
28
|
+
return False, (
|
|
29
|
+
"needs reset_experience=True: in persistent mode every episode starts "
|
|
30
|
+
"from a different state, so a prefix snapshot is never reusable"
|
|
31
|
+
)
|
|
32
|
+
if not resumable:
|
|
33
|
+
return False, (
|
|
34
|
+
"this model cannot resume mid-sequence (SplitGraphDNC needs the "
|
|
35
|
+
"start_step edit in split_graph_dnc.py)"
|
|
36
|
+
)
|
|
37
|
+
return True, ""
|
|
38
|
+
|
|
39
|
+
def _key_for(self, ctx: EpisodeCtx, boundary: int) -> str:
|
|
40
|
+
return self._key(boundary, ctx.prefix_hash(boundary))
|
|
41
|
+
|
|
42
|
+
def find_resume(self, ctx: EpisodeCtx) -> ResumePoint | None:
|
|
43
|
+
if not ctx.boundaries:
|
|
44
|
+
return None
|
|
45
|
+
self.stats.lookups += 1
|
|
46
|
+
for b in reversed(ctx.boundaries): # longest prefix first
|
|
47
|
+
got = self._get(self._key_for(ctx, b))
|
|
48
|
+
if got is None:
|
|
49
|
+
continue
|
|
50
|
+
entry, tier = got
|
|
51
|
+
self.stats.hit(tier, entry["compute_ms"])
|
|
52
|
+
return ResumePoint(
|
|
53
|
+
b, restore(entry["payload"], self.device), entry["compute_ms"], self.name
|
|
54
|
+
)
|
|
55
|
+
self.stats.misses += 1
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
def store_boundary(self, ctx: EpisodeCtx, boundary: int, hidden, compute_ms: float) -> None:
|
|
59
|
+
self._put(self._key_for(ctx, boundary), snapshot(hidden), compute_ms)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/cache/result_cache.py
|
|
3
|
+
|
|
4
|
+
Result / lookup cache (GATI-style): memoizes the model output of a whole episode.
|
|
5
|
+
|
|
6
|
+
fresh mode : key = (model fp, "F", hash of the episode inputs). A hit skips the
|
|
7
|
+
forward pass entirely.
|
|
8
|
+
persistent mode : the output also depends on the carried-over state, so the key adds
|
|
9
|
+
a history CHAIN hash (see keys.chain_next) and the entry also stores
|
|
10
|
+
the resulting state, so the run can continue after a hit.
|
|
11
|
+
|
|
12
|
+
Exact-match only, deterministic models only (the setup gate refuses stochastic write
|
|
13
|
+
heads unless overridden).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import torch
|
|
19
|
+
|
|
20
|
+
from initium.inference.cache.base_cache import BaseCache, EpisodeCtx, EpisodeHit
|
|
21
|
+
from initium.inference.cache.state_utils import restore, snapshot
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ResultCache(BaseCache):
|
|
25
|
+
name = "result"
|
|
26
|
+
|
|
27
|
+
def applicable(self, *, reset_experience: bool, resumable: bool) -> tuple[bool, str]:
|
|
28
|
+
return True, ""
|
|
29
|
+
|
|
30
|
+
def _key_for(self, ctx: EpisodeCtx) -> str:
|
|
31
|
+
return self._key("F" if ctx.reset_experience else "P", ctx.chain, ctx.in_hash)
|
|
32
|
+
|
|
33
|
+
def lookup_episode(self, ctx: EpisodeCtx) -> EpisodeHit | None:
|
|
34
|
+
self.stats.lookups += 1
|
|
35
|
+
got = self._get(self._key_for(ctx))
|
|
36
|
+
if got is None:
|
|
37
|
+
self.stats.misses += 1
|
|
38
|
+
return None
|
|
39
|
+
entry, tier = got
|
|
40
|
+
payload = entry["payload"]
|
|
41
|
+
if not ctx.reset_experience and payload.get("hidden") is None:
|
|
42
|
+
self.stats.misses += 1
|
|
43
|
+
return None
|
|
44
|
+
self.stats.hit(tier, entry["compute_ms"])
|
|
45
|
+
hidden = (
|
|
46
|
+
restore(payload["hidden"], self.device) if payload.get("hidden") is not None else None
|
|
47
|
+
)
|
|
48
|
+
return EpisodeHit(payload["output"].clone(), hidden, entry["compute_ms"])
|
|
49
|
+
|
|
50
|
+
def store_episode(
|
|
51
|
+
self, ctx: EpisodeCtx, output: torch.Tensor, hidden_after, compute_ms: float
|
|
52
|
+
) -> None:
|
|
53
|
+
payload = {
|
|
54
|
+
"output": output.detach().cpu().clone(),
|
|
55
|
+
"hidden": None if ctx.reset_experience else snapshot(hidden_after),
|
|
56
|
+
}
|
|
57
|
+
self._put(self._key_for(ctx), payload, compute_ms)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/cache/state_utils.py
|
|
3
|
+
|
|
4
|
+
Helpers for the nested model state (chx, mhx, last_read): controller states
|
|
5
|
+
(tuples/lists), the Memory dict, tensors, None. Snapshots are stored on CPU and
|
|
6
|
+
ALWAYS deep-copied on both sides: Memory.reset(..., hidden) rebinds entries of the
|
|
7
|
+
dict it is given and the write path rebinds them again, so a cached dict passed
|
|
8
|
+
straight into forward() would silently change under us.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import torch
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def tree_map(fn: Callable[[torch.Tensor], Any], obj: Any) -> Any:
|
|
21
|
+
if isinstance(obj, torch.Tensor):
|
|
22
|
+
return fn(obj)
|
|
23
|
+
if isinstance(obj, dict):
|
|
24
|
+
return {k: tree_map(fn, v) for k, v in obj.items()}
|
|
25
|
+
if isinstance(obj, list):
|
|
26
|
+
return [tree_map(fn, v) for v in obj]
|
|
27
|
+
if isinstance(obj, tuple):
|
|
28
|
+
items = [tree_map(fn, v) for v in obj]
|
|
29
|
+
return type(obj)(*items) if hasattr(obj, "_fields") else tuple(items)
|
|
30
|
+
return obj # None, numbers, strings
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def snapshot(tree: Any) -> Any: # -> independent CPU copy
|
|
34
|
+
return tree_map(lambda t: t.detach().to("cpu", copy=True), tree)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def restore(tree: Any, device: torch.device) -> Any: # -> independent copy on `device`
|
|
38
|
+
return tree_map(lambda t: t.detach().to(device, copy=True), tree)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def clone_tree(tree: Any) -> Any:
|
|
42
|
+
return tree_map(lambda t: t.detach().clone(), tree)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def tree_nbytes(obj: Any) -> int:
|
|
46
|
+
if isinstance(obj, torch.Tensor):
|
|
47
|
+
return obj.numel() * obj.element_size()
|
|
48
|
+
if isinstance(obj, dict):
|
|
49
|
+
return sum(tree_nbytes(v) for v in obj.values())
|
|
50
|
+
if isinstance(obj, list | tuple):
|
|
51
|
+
return sum(tree_nbytes(v) for v in obj)
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def tree_max_abs_diff(a: Any, b: Any) -> float:
|
|
56
|
+
"""Max |a-b| over all tensors; inf on any structural mismatch; NaN propagates
|
|
57
|
+
(callers must test `diff <= tol`, never `diff > tol`)."""
|
|
58
|
+
if isinstance(a, torch.Tensor) and isinstance(b, torch.Tensor):
|
|
59
|
+
if a.shape != b.shape:
|
|
60
|
+
return math.inf
|
|
61
|
+
if a.numel() == 0:
|
|
62
|
+
return 0.0
|
|
63
|
+
return (a.detach().float() - b.detach().float().to(a.device)).abs().max().item()
|
|
64
|
+
if isinstance(a, dict) and isinstance(b, dict):
|
|
65
|
+
if a.keys() != b.keys():
|
|
66
|
+
return math.inf
|
|
67
|
+
return max([0.0] + [tree_max_abs_diff(a[k], b[k]) for k in a])
|
|
68
|
+
if isinstance(a, list | tuple) and isinstance(b, list | tuple):
|
|
69
|
+
if len(a) != len(b):
|
|
70
|
+
return math.inf
|
|
71
|
+
return max([0.0] + [tree_max_abs_diff(x, y) for x, y in zip(a, b)])
|
|
72
|
+
if a is None and b is None:
|
|
73
|
+
return 0.0
|
|
74
|
+
return 0.0 if a == b else math.inf
|