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,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/cache/stores.py
|
|
3
|
+
|
|
4
|
+
Storage back-ends shared by all cache types: a byte-capped RAM LRU, an optional
|
|
5
|
+
on-disk store (survives across CLI invocations), and a tiered wrapper. Entries are
|
|
6
|
+
plain dicts {"payload", "compute_ms", "nbytes", "kind"} made only of tensors /
|
|
7
|
+
lists / tuples / dicts / numbers, so the disk tier can use torch.load(weights_only=True)
|
|
8
|
+
(no arbitrary pickle execution).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import glob
|
|
14
|
+
import os
|
|
15
|
+
from collections import OrderedDict
|
|
16
|
+
from dataclasses import asdict, dataclass
|
|
17
|
+
|
|
18
|
+
import torch
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class CacheStats:
|
|
23
|
+
lookups: int = 0
|
|
24
|
+
hits: int = 0
|
|
25
|
+
misses: int = 0
|
|
26
|
+
stores: int = 0
|
|
27
|
+
ram_hits: int = 0
|
|
28
|
+
disk_hits: int = 0
|
|
29
|
+
bytes_stored: int = 0
|
|
30
|
+
est_saved_ms: float = 0.0 # sum of recorded cold-compute time of every reused entry
|
|
31
|
+
|
|
32
|
+
def hit(self, tier: str, saved_ms: float) -> None:
|
|
33
|
+
self.hits += 1
|
|
34
|
+
self.est_saved_ms += float(saved_ms)
|
|
35
|
+
if tier == "disk":
|
|
36
|
+
self.disk_hits += 1
|
|
37
|
+
else:
|
|
38
|
+
self.ram_hits += 1
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> dict:
|
|
41
|
+
d = asdict(self)
|
|
42
|
+
d["hit_rate"] = 100.0 * self.hits / max(self.lookups, 1)
|
|
43
|
+
return d
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MemoryStore:
|
|
47
|
+
def __init__(self, max_bytes: int):
|
|
48
|
+
self.max_bytes = max_bytes
|
|
49
|
+
self._d: OrderedDict[str, dict] = OrderedDict()
|
|
50
|
+
self.bytes = 0
|
|
51
|
+
self.evictions = 0
|
|
52
|
+
|
|
53
|
+
def get(self, key: str) -> dict | None:
|
|
54
|
+
e = self._d.get(key)
|
|
55
|
+
if e is not None:
|
|
56
|
+
self._d.move_to_end(key)
|
|
57
|
+
return e
|
|
58
|
+
|
|
59
|
+
def put(self, key: str, entry: dict) -> None:
|
|
60
|
+
nb = int(entry.get("nbytes", 0))
|
|
61
|
+
if nb > self.max_bytes:
|
|
62
|
+
return
|
|
63
|
+
old = self._d.pop(key, None)
|
|
64
|
+
if old is not None:
|
|
65
|
+
self.bytes -= int(old.get("nbytes", 0))
|
|
66
|
+
self._d[key] = entry
|
|
67
|
+
self.bytes += nb
|
|
68
|
+
while self.bytes > self.max_bytes and self._d:
|
|
69
|
+
_, ev = self._d.popitem(last=False)
|
|
70
|
+
self.bytes -= int(ev.get("nbytes", 0))
|
|
71
|
+
self.evictions += 1
|
|
72
|
+
|
|
73
|
+
def __len__(self) -> int:
|
|
74
|
+
return len(self._d)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class DiskStore:
|
|
78
|
+
def __init__(self, root: str, max_bytes: int):
|
|
79
|
+
self.root, self.max_bytes = root, max_bytes
|
|
80
|
+
os.makedirs(root, exist_ok=True)
|
|
81
|
+
|
|
82
|
+
def _path(self, key: str) -> str:
|
|
83
|
+
return os.path.join(self.root, key + ".pt")
|
|
84
|
+
|
|
85
|
+
def get(self, key: str) -> dict | None:
|
|
86
|
+
p = self._path(key)
|
|
87
|
+
if not os.path.isfile(p):
|
|
88
|
+
return None
|
|
89
|
+
try:
|
|
90
|
+
entry = torch.load(p, map_location="cpu", weights_only=True)
|
|
91
|
+
os.utime(p, None) # recency for eviction
|
|
92
|
+
return entry
|
|
93
|
+
except Exception: # corrupt / incompatible -> treat as miss
|
|
94
|
+
try:
|
|
95
|
+
os.remove(p)
|
|
96
|
+
except OSError:
|
|
97
|
+
pass
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def put(self, key: str, entry: dict) -> None:
|
|
101
|
+
p = self._path(key)
|
|
102
|
+
tmp = f"{p}.tmp{os.getpid()}"
|
|
103
|
+
try:
|
|
104
|
+
torch.save(entry, tmp)
|
|
105
|
+
os.replace(tmp, p) # atomic
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f"[cache] disk write failed for {key}: {e}")
|
|
108
|
+
try:
|
|
109
|
+
os.remove(tmp)
|
|
110
|
+
except OSError:
|
|
111
|
+
pass
|
|
112
|
+
return
|
|
113
|
+
self._evict()
|
|
114
|
+
|
|
115
|
+
def _evict(self) -> None:
|
|
116
|
+
files = [
|
|
117
|
+
(os.path.getmtime(f), os.path.getsize(f), f)
|
|
118
|
+
for f in glob.glob(os.path.join(self.root, "*.pt"))
|
|
119
|
+
]
|
|
120
|
+
total = sum(s for _, s, _ in files)
|
|
121
|
+
for _, size, f in sorted(files):
|
|
122
|
+
if total <= self.max_bytes:
|
|
123
|
+
break
|
|
124
|
+
try:
|
|
125
|
+
os.remove(f)
|
|
126
|
+
total -= size
|
|
127
|
+
except OSError:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
def clear(self) -> None:
|
|
131
|
+
for f in glob.glob(os.path.join(self.root, "*.pt")):
|
|
132
|
+
try:
|
|
133
|
+
os.remove(f)
|
|
134
|
+
except OSError:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TieredStore:
|
|
139
|
+
"""RAM first, disk second (disk hits are promoted to RAM). Shared by every cache
|
|
140
|
+
of a run, so --cache-ram-mb / --cache-disk-mb are TOTAL budgets."""
|
|
141
|
+
|
|
142
|
+
def __init__(self, mem: MemoryStore, disk: DiskStore | None = None):
|
|
143
|
+
self.mem, self.disk = mem, disk
|
|
144
|
+
|
|
145
|
+
def get(self, key: str) -> tuple[dict, str] | None:
|
|
146
|
+
e = self.mem.get(key)
|
|
147
|
+
if e is not None:
|
|
148
|
+
return e, "ram"
|
|
149
|
+
if self.disk is not None:
|
|
150
|
+
e = self.disk.get(key)
|
|
151
|
+
if e is not None:
|
|
152
|
+
self.mem.put(key, e)
|
|
153
|
+
return e, "disk"
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def put(self, key: str, entry: dict) -> None:
|
|
157
|
+
self.mem.put(key, entry)
|
|
158
|
+
if self.disk is not None:
|
|
159
|
+
self.disk.put(key, entry)
|
|
160
|
+
|
|
161
|
+
def info(self) -> dict:
|
|
162
|
+
return {
|
|
163
|
+
"ram_mb": self.mem.bytes / 1e6,
|
|
164
|
+
"ram_entries": len(self.mem),
|
|
165
|
+
"ram_evictions": self.mem.evictions,
|
|
166
|
+
"disk_dir": self.disk.root if self.disk else None,
|
|
167
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/capabilities.py
|
|
3
|
+
|
|
4
|
+
Decides, FROM THE CHECKPOINT ITSELF, which dataset types a model can run, and
|
|
5
|
+
refuses to continue when the requested type/dimensions don't match.
|
|
6
|
+
|
|
7
|
+
Sources of truth (in order):
|
|
8
|
+
1. model_config["supported_dataset_types"] (written by core_training v19+).
|
|
9
|
+
May contain "*" = any type whose dims match the weights.
|
|
10
|
+
2. Legacy checkpoints (no such key): infer from the tensor dims - every
|
|
11
|
+
implemented task type whose (input_dim, output_dim) equals the model's.
|
|
12
|
+
Dims that match nothing -> supported list is empty -> run refuses.
|
|
13
|
+
Independently of (1)/(2), the task's dims must ALWAYS equal the model's dims.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
|
|
20
|
+
from initium.inference.inference_config import WILDCARD_TYPE
|
|
21
|
+
from initium.inference.tasks.task_registry import IMPLEMENTED_TYPES, canonical_type, get_task_class
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class IncompatibleModelError(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ModelCapabilities:
|
|
30
|
+
supported_types: list[str]
|
|
31
|
+
input_dim: int
|
|
32
|
+
output_dim: int
|
|
33
|
+
source: str # "recorded" | "inferred-legacy" | "unknown"
|
|
34
|
+
controller_type: str
|
|
35
|
+
|
|
36
|
+
def supports(self, canon_type: str) -> bool:
|
|
37
|
+
return WILDCARD_TYPE in self.supported_types or canon_type in self.supported_types
|
|
38
|
+
|
|
39
|
+
def describe(self) -> str:
|
|
40
|
+
return (
|
|
41
|
+
f"supported types={self.supported_types} (source: {self.source}) | "
|
|
42
|
+
f"input_dim={self.input_dim} output_dim={self.output_dim} | "
|
|
43
|
+
f"controller={self.controller_type}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def detect_capabilities(ckpt: dict) -> ModelCapabilities:
|
|
48
|
+
cfg = ckpt["model_config"]
|
|
49
|
+
out_dim, proj_in = ckpt["output_proj_state_dict"]["weight"].shape
|
|
50
|
+
input_dim = int(cfg.get("input_dim", cfg.get("input_size", proj_in)))
|
|
51
|
+
output_dim = int(out_dim)
|
|
52
|
+
|
|
53
|
+
recorded = cfg.get("supported_dataset_types")
|
|
54
|
+
if recorded:
|
|
55
|
+
types = [WILDCARD_TYPE if t == WILDCARD_TYPE else canonical_type(t) for t in recorded]
|
|
56
|
+
source = "recorded"
|
|
57
|
+
else:
|
|
58
|
+
types = []
|
|
59
|
+
for t in IMPLEMENTED_TYPES:
|
|
60
|
+
cls = get_task_class(t)
|
|
61
|
+
if (cls.input_dim, cls.output_dim) == (input_dim, output_dim):
|
|
62
|
+
types.append(t)
|
|
63
|
+
source = "inferred-legacy" if types else "unknown"
|
|
64
|
+
|
|
65
|
+
return ModelCapabilities(
|
|
66
|
+
types, input_dim, output_dim, source, cfg.get("controller_type", "lstm")
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def require_type_supported(caps: ModelCapabilities, dataset_type: str, ckpt_path: str) -> str:
|
|
71
|
+
canon = canonical_type(dataset_type)
|
|
72
|
+
if not caps.supports(canon):
|
|
73
|
+
raise IncompatibleModelError(
|
|
74
|
+
f"checkpoint '{ckpt_path}' cannot run dataset type '{canon}'. "
|
|
75
|
+
f"It supports {caps.supported_types} ({caps.source}). Not continuing."
|
|
76
|
+
)
|
|
77
|
+
return canon
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def require_dims_match(caps: ModelCapabilities, task, ckpt_path: str) -> None:
|
|
81
|
+
if (task.input_dim, task.output_dim) != (caps.input_dim, caps.output_dim):
|
|
82
|
+
raise IncompatibleModelError(
|
|
83
|
+
f"task '{task.name}' produces input_dim={task.input_dim}/output_dim={task.output_dim} "
|
|
84
|
+
f"but checkpoint '{ckpt_path}' expects input_dim={caps.input_dim}/"
|
|
85
|
+
f"output_dim={caps.output_dim}. Not continuing."
|
|
86
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/checkpoint_io.py
|
|
3
|
+
|
|
4
|
+
Load a checkpoint written by core_training.save_checkpoint() (periodic or
|
|
5
|
+
final - same format) and describe it. Loads to CPU; weights_only=False for the
|
|
6
|
+
same reason as core_training.load_checkpoint_for_resume (self-produced file
|
|
7
|
+
that also stores python/numpy RNG state).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
import torch
|
|
15
|
+
|
|
16
|
+
REQUIRED_KEYS = ("rnn_state_dict", "output_proj_state_dict", "model_config")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_checkpoint(path: str, model_config_override: dict | None = None) -> dict:
|
|
20
|
+
if not os.path.isfile(path):
|
|
21
|
+
raise FileNotFoundError(f"checkpoint not found: {path}")
|
|
22
|
+
ckpt = torch.load(path, map_location="cpu", weights_only=False)
|
|
23
|
+
if not isinstance(ckpt, dict):
|
|
24
|
+
raise ValueError(f"{path}: not a core_training checkpoint (expected a dict)")
|
|
25
|
+
missing = [k for k in REQUIRED_KEYS if k not in ckpt]
|
|
26
|
+
if missing:
|
|
27
|
+
raise ValueError(f"{path}: not a usable checkpoint, missing keys {missing}")
|
|
28
|
+
cfg = dict(ckpt["model_config"])
|
|
29
|
+
if model_config_override:
|
|
30
|
+
cfg.update(model_config_override)
|
|
31
|
+
print(f"[checkpoint_io] model_config overridden: {model_config_override}")
|
|
32
|
+
ckpt["model_config"] = cfg
|
|
33
|
+
return ckpt
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def describe_checkpoint(ckpt: dict) -> str:
|
|
37
|
+
c = ckpt["model_config"]
|
|
38
|
+
lines = [
|
|
39
|
+
f"run_id={ckpt.get('run_id')} step={ckpt.get('step')} beta_target={ckpt.get('beta_target')}",
|
|
40
|
+
f"controller={c.get('controller_type', 'lstm')} hidden={c.get('hidden_size')} "
|
|
41
|
+
f"nr_cells={c.get('nr_cells')} cell_size={c.get('cell_size')} read_heads={c.get('read_heads')}",
|
|
42
|
+
f"link_matrix={c.get('link_matrix_mode', 'dense')} moe={c.get('moe_enabled', False)} "
|
|
43
|
+
f"split_graph={c.get('split_graph_enabled', False)}"
|
|
44
|
+
+ (f"({c.get('split_graph_variant')})" if c.get("split_graph_enabled") else "")
|
|
45
|
+
+ f" dynamic_n={c.get('dynamic_n_mode', False)}",
|
|
46
|
+
]
|
|
47
|
+
combiner_variant = c.get("split_graph_combiner_variant")
|
|
48
|
+
if (
|
|
49
|
+
c.get("split_graph_enabled")
|
|
50
|
+
and c.get("split_graph_combiner_mode") == "controller"
|
|
51
|
+
and combiner_variant
|
|
52
|
+
):
|
|
53
|
+
if "+" in combiner_variant:
|
|
54
|
+
lines.append(
|
|
55
|
+
f"combiner=controller stages={list(enumerate(combiner_variant.split('+')))} "
|
|
56
|
+
f"(use --ablate-combiner-stage <name|index>)"
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
lines.append(
|
|
60
|
+
f"combiner=controller variant={combiner_variant!r} (single stage -- "
|
|
61
|
+
f"multi-source MoE routing applies only here, not to hybrid chains)"
|
|
62
|
+
)
|
|
63
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/engine.py
|
|
3
|
+
|
|
4
|
+
Runs episodes through a loaded model. Knows nothing about any specific task;
|
|
5
|
+
it only calls task.score_episode().
|
|
6
|
+
|
|
7
|
+
Two regimes (the reset_experience switch):
|
|
8
|
+
reset_experience=True : every episode starts from hidden=(None,None,None) with
|
|
9
|
+
reset_experience=True -> independent episodes, exactly
|
|
10
|
+
like the periodic OOD eval in training.
|
|
11
|
+
reset_experience=False : the (controller state, memory, last read) returned by
|
|
12
|
+
episode i is fed into episode i+1 with
|
|
13
|
+
reset_experience=False -> memory content and recurrent
|
|
14
|
+
state persist across the whole run. NO weights change;
|
|
15
|
+
any "learning" is in-context via memory + controller state.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import time
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import torch
|
|
24
|
+
|
|
25
|
+
from initium.inference.metrics import EpisodeResult
|
|
26
|
+
|
|
27
|
+
FRESH_HIDDEN = (None, None, None)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InferenceEngine:
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
model,
|
|
34
|
+
output_proj,
|
|
35
|
+
task,
|
|
36
|
+
device,
|
|
37
|
+
ablate_memory: bool = False,
|
|
38
|
+
combiner_skip_stages: frozenset | None = None,
|
|
39
|
+
):
|
|
40
|
+
self.model, self.output_proj, self.task = model, output_proj, task
|
|
41
|
+
self.device, self.ablate_memory = device, ablate_memory
|
|
42
|
+
self.combiner_skip_stages = combiner_skip_stages
|
|
43
|
+
self.model.eval()
|
|
44
|
+
self.output_proj.eval()
|
|
45
|
+
|
|
46
|
+
@torch.no_grad()
|
|
47
|
+
def _forward(self, episode, hidden, reset_experience: bool):
|
|
48
|
+
x = episode.input_seq.unsqueeze(0).to(self.device) # (1, T, input_dim)
|
|
49
|
+
kw: dict[str, Any] = dict(
|
|
50
|
+
reset_experience=reset_experience, pass_through_memory=not self.ablate_memory
|
|
51
|
+
)
|
|
52
|
+
if self.combiner_skip_stages:
|
|
53
|
+
kw["combiner_skip_stages"] = self.combiner_skip_stages
|
|
54
|
+
output, new_hidden = self.model(x, hidden, **kw)
|
|
55
|
+
output = output.transpose(0, 1).contiguous().squeeze(0) # (T, input_dim)
|
|
56
|
+
output = self.output_proj(output) # (T, output_dim)
|
|
57
|
+
return output.float().cpu(), new_hidden
|
|
58
|
+
|
|
59
|
+
def run(
|
|
60
|
+
self,
|
|
61
|
+
episodes,
|
|
62
|
+
reset_experience: bool,
|
|
63
|
+
verbose_n: int = 0,
|
|
64
|
+
progress_every: int = 0,
|
|
65
|
+
label: str = "",
|
|
66
|
+
) -> list[EpisodeResult]:
|
|
67
|
+
hidden = FRESH_HIDDEN
|
|
68
|
+
results: list[EpisodeResult] = []
|
|
69
|
+
run_items = run_correct = 0
|
|
70
|
+
for i, ep in enumerate(episodes):
|
|
71
|
+
if reset_experience:
|
|
72
|
+
hidden = FRESH_HIDDEN
|
|
73
|
+
t0 = time.perf_counter()
|
|
74
|
+
out, hidden = self._forward(ep, hidden, reset_experience)
|
|
75
|
+
if self.device.type == "cuda":
|
|
76
|
+
torch.cuda.synchronize(self.device)
|
|
77
|
+
elapsed_ms = (time.perf_counter() - t0) * 1000.0
|
|
78
|
+
|
|
79
|
+
score = self.task.score_episode(out, ep, verbose=(i < verbose_n))
|
|
80
|
+
results.append(EpisodeResult(i, score, elapsed_ms, reset_experience))
|
|
81
|
+
run_items += score.n_items
|
|
82
|
+
run_correct += score.n_correct
|
|
83
|
+
if progress_every and ((i + 1) % progress_every == 0 or i + 1 == len(episodes)):
|
|
84
|
+
print(
|
|
85
|
+
f"[engine{label}] episode {i + 1}/{len(episodes)} | "
|
|
86
|
+
f"running item acc {100.0 * run_correct / max(run_items, 1):.2f}%"
|
|
87
|
+
)
|
|
88
|
+
return results
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/inference_config.py
|
|
3
|
+
|
|
4
|
+
Inference-side constants. Pure constants, imported explicitly by the other
|
|
5
|
+
inference modules (same convention as config/train_config.py).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
INFERENCE_LOG_DIR = "./inference_logs"
|
|
9
|
+
|
|
10
|
+
# Default task: graph traversal on the London Underground graph.
|
|
11
|
+
DEFAULT_DATASET_TYPE = "graph"
|
|
12
|
+
DEFAULT_DATASET_LINK = None # None -> built-in London Underground graph
|
|
13
|
+
|
|
14
|
+
DEFAULT_NUM_EPISODES = 200 # fresh mode (and persistent mode when --loop is omitted)
|
|
15
|
+
DEFAULT_WINDOW = 10 # episodes per window in the persistent-mode adaptation table
|
|
16
|
+
DEFAULT_SEED = 0
|
|
17
|
+
DEFAULT_VERBOSE_N = 3 # print predictions of the first N episodes
|
|
18
|
+
PROGRESS_EVERY = 25 # running-accuracy console line every N episodes (0 = off)
|
|
19
|
+
|
|
20
|
+
# A checkpoint whose supported_dataset_types contains this accepts ANY task
|
|
21
|
+
# whose input/output dims match its weights.
|
|
22
|
+
WILDCARD_TYPE = "*"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/metrics.py
|
|
3
|
+
|
|
4
|
+
Task-agnostic scoring containers and aggregation. A task's score_episode()
|
|
5
|
+
returns an EpisodeScore; everything downstream (breakdowns, windows,
|
|
6
|
+
adaptation trend, logging) only ever sees EpisodeScore, so a new task type
|
|
7
|
+
needs no changes here.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class EpisodeScore:
|
|
20
|
+
n_items: int # scored items in the episode (graph: answer triples)
|
|
21
|
+
n_correct: int
|
|
22
|
+
perfect: bool # every item correct
|
|
23
|
+
group: Any = None # bucket key for breakdowns (graph: walk length / hop count)
|
|
24
|
+
fields: dict[str, tuple[int, int]] = field(default_factory=dict) # name -> (correct, total)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def item_acc(self) -> float:
|
|
28
|
+
return 100.0 * self.n_correct / max(self.n_items, 1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class EpisodeResult:
|
|
33
|
+
index: int
|
|
34
|
+
score: EpisodeScore
|
|
35
|
+
elapsed_ms: float
|
|
36
|
+
reset_experience: bool
|
|
37
|
+
cache_event: str = "" # "", "miss", "hit:result", "hit:prefix@22", ...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def aggregate(scores: list[EpisodeScore]) -> dict:
|
|
41
|
+
n_eps = len(scores)
|
|
42
|
+
n_items = sum(s.n_items for s in scores)
|
|
43
|
+
n_correct = sum(s.n_correct for s in scores)
|
|
44
|
+
n_perfect = sum(int(s.perfect) for s in scores)
|
|
45
|
+
|
|
46
|
+
by_group: dict[Any, list[int]] = {}
|
|
47
|
+
field_tot: dict[str, list[int]] = {}
|
|
48
|
+
for s in scores:
|
|
49
|
+
if s.group is not None:
|
|
50
|
+
g = by_group.setdefault(s.group, [0, 0, 0, 0])
|
|
51
|
+
g[0] += s.n_items
|
|
52
|
+
g[1] += s.n_correct
|
|
53
|
+
g[2] += 1
|
|
54
|
+
g[3] += int(s.perfect)
|
|
55
|
+
for name, (c, t) in s.fields.items():
|
|
56
|
+
f = field_tot.setdefault(name, [0, 0])
|
|
57
|
+
f[0] += c
|
|
58
|
+
f[1] += t
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
"n_episodes": n_eps,
|
|
62
|
+
"item_acc": 100.0 * n_correct / max(n_items, 1),
|
|
63
|
+
"perfect_frac": 100.0 * n_perfect / max(n_eps, 1),
|
|
64
|
+
"by_group": {
|
|
65
|
+
g: {
|
|
66
|
+
"item_acc": 100.0 * v[1] / max(v[0], 1),
|
|
67
|
+
"perfect_frac": 100.0 * v[3] / max(v[2], 1),
|
|
68
|
+
"n_episodes": v[2],
|
|
69
|
+
}
|
|
70
|
+
for g, v in sorted(by_group.items())
|
|
71
|
+
},
|
|
72
|
+
"field_acc": {n: 100.0 * c / max(t, 1) for n, (c, t) in field_tot.items()},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def windowed(scores: list[EpisodeScore], window: int) -> list[dict]:
|
|
77
|
+
"""Aggregate consecutive chunks of `window` episodes (persistent-mode adaptation view)."""
|
|
78
|
+
window = max(1, window)
|
|
79
|
+
out = []
|
|
80
|
+
for w, start in enumerate(range(0, len(scores), window)):
|
|
81
|
+
chunk = scores[start : start + window]
|
|
82
|
+
agg = aggregate(chunk)
|
|
83
|
+
agg.update(window_index=w, start_episode=start, end_episode=start + len(chunk) - 1)
|
|
84
|
+
out.append(agg)
|
|
85
|
+
return out
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def adaptation_trend(scores: list[EpisodeScore], window: int) -> dict:
|
|
89
|
+
"""First-window vs last-window accuracy and a least-squares slope of
|
|
90
|
+
per-episode accuracy -- a compact 'did it adapt mid-inference' read."""
|
|
91
|
+
if len(scores) < 2:
|
|
92
|
+
return {}
|
|
93
|
+
w = max(1, min(window, len(scores) // 2))
|
|
94
|
+
first, last = aggregate(scores[:w]), aggregate(scores[-w:])
|
|
95
|
+
accs = np.array([s.item_acc for s in scores], dtype=np.float64)
|
|
96
|
+
slope = float(np.polyfit(np.arange(len(accs)), accs, 1)[0])
|
|
97
|
+
return {
|
|
98
|
+
"window": w,
|
|
99
|
+
"first_window_item_acc": first["item_acc"],
|
|
100
|
+
"last_window_item_acc": last["item_acc"],
|
|
101
|
+
"delta_item_acc": last["item_acc"] - first["item_acc"],
|
|
102
|
+
"slope_acc_per_episode": slope,
|
|
103
|
+
}
|