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 @@
|
|
|
1
|
+
"""Inference tasks (one per dataset type), mirroring data/."""
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/tasks/audio_task.py
|
|
3
|
+
|
|
4
|
+
Audio-modality inference task. Delegates episode-building/decoding to
|
|
5
|
+
data.audio.audio_dataset.AudioChainDataset (same pattern graph_traversal_task.py
|
|
6
|
+
uses relative to graph_traversal.py - training and inference can never
|
|
7
|
+
silently drift apart).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import torch
|
|
13
|
+
|
|
14
|
+
from initium.data.audio.audio_dataset import AudioChainDataset
|
|
15
|
+
from initium.inference.metrics import EpisodeScore
|
|
16
|
+
from initium.inference.tasks.base_task import BaseInferenceTask, Episode
|
|
17
|
+
|
|
18
|
+
BUILTIN_LINKS = (None, "", "audio-chain")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AudioChainTask(BaseInferenceTask):
|
|
22
|
+
name = "audio-chain"
|
|
23
|
+
dataset_type = "audio"
|
|
24
|
+
|
|
25
|
+
def __init__(self, dataset_link=None, path_length_range=None, **kwargs):
|
|
26
|
+
# Inference always tests a trained model, so --dataset-link here plays the
|
|
27
|
+
# role of test_dataset_link on the training-side dataset class: a real
|
|
28
|
+
# audio file used as the held-out fact source, instead of the synthetic
|
|
29
|
+
# seeded table. Omit (or pass one of BUILTIN_LINKS) for that default.
|
|
30
|
+
link = None if dataset_link in BUILTIN_LINKS else dataset_link
|
|
31
|
+
self._ds = AudioChainDataset(test_dataset_link=link)
|
|
32
|
+
self.input_dim, self.output_dim = self._ds.input_dim, self._ds.output_dim
|
|
33
|
+
self.query_range = (
|
|
34
|
+
tuple(path_length_range) if path_length_range else self._ds.ood_query_range
|
|
35
|
+
)
|
|
36
|
+
self.facts = self._ds.build_ood_facts()
|
|
37
|
+
|
|
38
|
+
def describe(self) -> str:
|
|
39
|
+
return f"{self.name} | {len(self.facts)} facts | queries {self.query_range}"
|
|
40
|
+
|
|
41
|
+
def build_episodes(self, n: int, rng, perturbation=None) -> list[Episode]:
|
|
42
|
+
severity = (
|
|
43
|
+
(perturbation or {}).get("severity", 0.0) if isinstance(perturbation, dict) else 0.0
|
|
44
|
+
)
|
|
45
|
+
keys, vals_map = [k for k, _ in self.facts], dict(self.facts)
|
|
46
|
+
NF = self._ds.codec.num_digits
|
|
47
|
+
episodes = []
|
|
48
|
+
for _ in range(n):
|
|
49
|
+
nq = rng.randint(*self.query_range)
|
|
50
|
+
query_keys = [rng.choice(keys) for _ in range(nq)]
|
|
51
|
+
facts = list(self.facts)
|
|
52
|
+
rng.shuffle(facts)
|
|
53
|
+
inputs, targets, mask = [], [], []
|
|
54
|
+
for i, (k, v) in enumerate(facts):
|
|
55
|
+
inputs.append(
|
|
56
|
+
self._ds._encode_fact(
|
|
57
|
+
k, v, 1.0 if i == 0 else 0.0, perturb=(severity or None), rng=rng
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
targets.append([0] * (2 * NF))
|
|
61
|
+
mask.append(0)
|
|
62
|
+
for i, k in enumerate(query_keys):
|
|
63
|
+
inputs.append(self._ds._encode_query(k, 1.0 if i == 0 else 0.0))
|
|
64
|
+
targets.append([0] * (2 * NF))
|
|
65
|
+
mask.append(0)
|
|
66
|
+
cumsum = 0
|
|
67
|
+
for i, k in enumerate(query_keys):
|
|
68
|
+
cumsum = (cumsum + vals_map[k]) % self._ds.label_range
|
|
69
|
+
inputs.append(self._ds._encode_answer(1.0 if i == 0 else 0.0))
|
|
70
|
+
targets.append(
|
|
71
|
+
self._ds.codec.label_to_digit_targets(vals_map[k])
|
|
72
|
+
+ self._ds.codec.label_to_digit_targets(cumsum)
|
|
73
|
+
)
|
|
74
|
+
mask.append(1)
|
|
75
|
+
episodes.append(
|
|
76
|
+
Episode(
|
|
77
|
+
torch.stack(inputs),
|
|
78
|
+
torch.tensor(targets, dtype=torch.long),
|
|
79
|
+
torch.tensor(mask, dtype=torch.float32),
|
|
80
|
+
meta={"num_queries": nq},
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
return episodes
|
|
84
|
+
|
|
85
|
+
def score_episode(self, output, episode: Episode, verbose: bool = False) -> EpisodeScore:
|
|
86
|
+
codec, D = self._ds.codec, self._ds.codec.label_dim
|
|
87
|
+
answer_idx = (episode.mask == 1).nonzero(as_tuple=True)[0]
|
|
88
|
+
f = {"value": [0, 0], "cumsum": [0, 0]}
|
|
89
|
+
n_correct = 0
|
|
90
|
+
for idx in answer_idx:
|
|
91
|
+
v_pred, c_pred = (
|
|
92
|
+
codec.decode_field(output[idx][0:D]),
|
|
93
|
+
codec.decode_field(output[idx][D : 2 * D]),
|
|
94
|
+
)
|
|
95
|
+
td = episode.target[idx].tolist()
|
|
96
|
+
nd = codec.num_digits
|
|
97
|
+
v_tgt = int("".join(map(str, td[0:nd])))
|
|
98
|
+
c_tgt = int("".join(map(str, td[nd : 2 * nd])))
|
|
99
|
+
v_ok, c_ok = v_pred == v_tgt, c_pred == c_tgt
|
|
100
|
+
f["value"][0] += int(v_ok)
|
|
101
|
+
f["value"][1] += 1
|
|
102
|
+
f["cumsum"][0] += int(c_ok)
|
|
103
|
+
f["cumsum"][1] += 1
|
|
104
|
+
n_correct += int(v_ok) + int(c_ok)
|
|
105
|
+
if verbose:
|
|
106
|
+
print(f" value {v_pred}/{v_tgt} ok={v_ok} | cumsum {c_pred}/{c_tgt} ok={c_ok}")
|
|
107
|
+
n = 2 * len(answer_idx)
|
|
108
|
+
return EpisodeScore(
|
|
109
|
+
n_items=n,
|
|
110
|
+
n_correct=n_correct,
|
|
111
|
+
perfect=(n_correct == n),
|
|
112
|
+
group=len(answer_idx),
|
|
113
|
+
fields={k: (v[0], v[1]) for k, v in f.items()},
|
|
114
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/tasks/base_task.py
|
|
3
|
+
|
|
4
|
+
Interface the inference engine talks to (counterpart of data/base_dataset.py).
|
|
5
|
+
Any new type (text, audio, video, ...) implements BaseInferenceTask and
|
|
6
|
+
registers itself in task_registry.py; the engine never imports anything
|
|
7
|
+
task-specific.
|
|
8
|
+
|
|
9
|
+
Contract:
|
|
10
|
+
dataset_type : canonical type string matched against the checkpoint's
|
|
11
|
+
supported_dataset_types (e.g. "graph")
|
|
12
|
+
input_dim : model input width the task produces (DNC input_size)
|
|
13
|
+
output_dim : output_proj width the task's scoring expects
|
|
14
|
+
build_episodes(n, rng) -> list[Episode]
|
|
15
|
+
score_episode(output, episode, verbose) -> EpisodeScore
|
|
16
|
+
`output` is the post-output_proj (T, output_dim) CPU tensor.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
|
|
23
|
+
import torch
|
|
24
|
+
|
|
25
|
+
from initium.inference.metrics import EpisodeScore
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Episode:
|
|
30
|
+
input_seq: torch.Tensor # (T, input_dim)
|
|
31
|
+
target: torch.Tensor # task-defined, aligned with input_seq's time axis
|
|
32
|
+
mask: torch.Tensor # (T,) 1 where a step is scored
|
|
33
|
+
meta: dict = field(default_factory=dict)
|
|
34
|
+
# Step indices where the model state may be snapshotted/reused (sorted). Everything
|
|
35
|
+
# before a boundary is a static prefix identical across episodes; no scored step
|
|
36
|
+
# (mask==1) may lie before a boundary. Empty -> nothing cacheable by the prefix cache.
|
|
37
|
+
cache_boundaries: list[int] = field(default_factory=list)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BaseInferenceTask:
|
|
41
|
+
name: str = "base"
|
|
42
|
+
dataset_type: str = "base"
|
|
43
|
+
input_dim: int | None = None
|
|
44
|
+
output_dim: int | None = None
|
|
45
|
+
|
|
46
|
+
def build_episodes(self, n: int, rng, perturbation=None) -> list[Episode]:
|
|
47
|
+
"""perturbation: optional dataset-defined robustness probe (see
|
|
48
|
+
data/base_dataset.py's evaluate_robustness for the training-side
|
|
49
|
+
analogue), e.g. {'kind': 'noise', 'severity': 0.1}. None (default)
|
|
50
|
+
builds ordinary, unperturbed episodes -- purely additive, existing
|
|
51
|
+
tasks ignore it unless they opt in."""
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
def score_episode(
|
|
55
|
+
self, output: torch.Tensor, episode: Episode, verbose: bool = False
|
|
56
|
+
) -> EpisodeScore:
|
|
57
|
+
raise NotImplementedError
|
|
58
|
+
|
|
59
|
+
def describe(self) -> str:
|
|
60
|
+
return self.name
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/tasks/graph_traversal_task.py
|
|
3
|
+
|
|
4
|
+
Graph-traversal inference task. Default test graph is the built-in London
|
|
5
|
+
Underground (identical graph/label mapping to the training-time OOD eval).
|
|
6
|
+
--dataset-link may instead point to your own edge file:
|
|
7
|
+
|
|
8
|
+
.csv / .tsv / .txt : one edge per row: src,dst,line ('#' comment lines and a
|
|
9
|
+
header row starting with src/source/from are skipped)
|
|
10
|
+
.json : [["src","dst","line"], ...] or {"edges": [...]} (rows may also
|
|
11
|
+
be {"src":..,"dst":..,"line":..})
|
|
12
|
+
|
|
13
|
+
Names (stations/lines) are mapped to distinct 0-999 labels with a fixed
|
|
14
|
+
random.Random(1234) exactly like the London eval. If EVERY field in the file
|
|
15
|
+
is an integer in 0..999, the integers are used directly as labels instead.
|
|
16
|
+
Episodes are built by the dataset module's own
|
|
17
|
+
build_traversal_episode_from_graph(), so encoding is byte-identical to training.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import torch
|
|
23
|
+
|
|
24
|
+
from initium.data.common.graph_io import build_graph_from_raw_edges, load_raw_edges
|
|
25
|
+
from initium.data.graph_traversal.graph_traversal import (
|
|
26
|
+
INPUT_DIM,
|
|
27
|
+
OOD_PATH_LENGTH_RANGE,
|
|
28
|
+
TRIPLE_DIM,
|
|
29
|
+
build_london_underground_eval,
|
|
30
|
+
build_traversal_episode_from_graph,
|
|
31
|
+
decode_prediction,
|
|
32
|
+
encode_triple,
|
|
33
|
+
triple_to_digit_targets,
|
|
34
|
+
)
|
|
35
|
+
from initium.inference.metrics import EpisodeScore
|
|
36
|
+
from initium.inference.tasks.base_task import BaseInferenceTask, Episode
|
|
37
|
+
|
|
38
|
+
BUILTIN_LINKS = (None, "", "graph-traversal", "london", "london-underground")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class GraphTraversalTask(BaseInferenceTask):
|
|
42
|
+
name = "graph-traversal"
|
|
43
|
+
dataset_type = "graph"
|
|
44
|
+
input_dim = INPUT_DIM
|
|
45
|
+
output_dim = TRIPLE_DIM
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self, dataset_link=None, path_length_range=None, shared_context=False, num_contexts=1
|
|
49
|
+
):
|
|
50
|
+
if path_length_range is not None:
|
|
51
|
+
lo, hi = path_length_range
|
|
52
|
+
if lo < 1 or hi < lo:
|
|
53
|
+
raise ValueError(f"invalid path length range {path_length_range}")
|
|
54
|
+
self.path_length_range = (int(lo), int(hi))
|
|
55
|
+
else:
|
|
56
|
+
self.path_length_range = tuple(OOD_PATH_LENGTH_RANGE)
|
|
57
|
+
|
|
58
|
+
if int(num_contexts) < 1:
|
|
59
|
+
raise ValueError("num_contexts must be >= 1")
|
|
60
|
+
if int(num_contexts) > 1 and not shared_context:
|
|
61
|
+
raise ValueError("num_contexts > 1 requires shared_context=True")
|
|
62
|
+
self.shared_context, self.num_contexts = bool(shared_context), int(num_contexts)
|
|
63
|
+
|
|
64
|
+
if dataset_link in BUILTIN_LINKS:
|
|
65
|
+
self.edges, self.node_labels, self.adjacency = build_london_underground_eval()
|
|
66
|
+
self.source_desc = "London Underground (built-in)"
|
|
67
|
+
else:
|
|
68
|
+
# dataset_link may be a single edge file, a directory, a glob
|
|
69
|
+
# pattern, or a '+'-joined list of these -- load_raw_edges
|
|
70
|
+
# resolves it (data/common/real_data.py's resolve_link_paths)
|
|
71
|
+
# and raises its own clear FileNotFoundError if nothing
|
|
72
|
+
# matches, so no separate isfile guard is needed here.
|
|
73
|
+
self.edges, self.node_labels, self.adjacency = build_graph_from_raw_edges(
|
|
74
|
+
load_raw_edges(dataset_link)
|
|
75
|
+
)
|
|
76
|
+
self.source_desc = f"source: {dataset_link}"
|
|
77
|
+
|
|
78
|
+
if not any(self.adjacency[i] for i in self.adjacency):
|
|
79
|
+
raise ValueError("test graph has no outgoing edges; cannot build traversal episodes")
|
|
80
|
+
|
|
81
|
+
def describe(self) -> str:
|
|
82
|
+
shared = f" | shared context x{self.num_contexts}" if self.shared_context else ""
|
|
83
|
+
return (
|
|
84
|
+
f"{self.name} | {self.source_desc} | {len(self.node_labels)} nodes, "
|
|
85
|
+
f"{len(self.edges)} edges | walk length {self.path_length_range}{shared}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def build_episodes(self, n: int, rng, perturbation=None) -> list[Episode]:
|
|
89
|
+
# perturbation: not wired for graph yet (see base_task.py) -- accepted
|
|
90
|
+
# and ignored so the shared BaseInferenceTask interface stays uniform.
|
|
91
|
+
episodes: list[Episode] = []
|
|
92
|
+
attempts = 0
|
|
93
|
+
if self.shared_context:
|
|
94
|
+
return self._build_shared_context_episodes(n, rng)
|
|
95
|
+
while len(episodes) < n:
|
|
96
|
+
attempts += 1
|
|
97
|
+
if attempts > 100 * n + 1000:
|
|
98
|
+
raise RuntimeError("could not build enough episodes from this graph")
|
|
99
|
+
ep = build_traversal_episode_from_graph(
|
|
100
|
+
self.edges,
|
|
101
|
+
self.node_labels,
|
|
102
|
+
self.adjacency,
|
|
103
|
+
len(self.node_labels),
|
|
104
|
+
self.path_length_range,
|
|
105
|
+
rng=rng,
|
|
106
|
+
)
|
|
107
|
+
if ep is None:
|
|
108
|
+
continue
|
|
109
|
+
input_seq, target_digits, answer_mask = ep
|
|
110
|
+
episodes.append(
|
|
111
|
+
Episode(
|
|
112
|
+
input_seq,
|
|
113
|
+
target_digits,
|
|
114
|
+
answer_mask,
|
|
115
|
+
meta={"walk_length": int(answer_mask.sum().item())},
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
return episodes
|
|
119
|
+
|
|
120
|
+
# ---- shared-context episodes (static prefix -> cacheable) ----------------
|
|
121
|
+
|
|
122
|
+
def _make_context(self, rng) -> torch.Tensor:
|
|
123
|
+
"""One fixed, shuffled edge listing: the static prefix shared by many queries."""
|
|
124
|
+
shuffled = list(self.edges)
|
|
125
|
+
rng.shuffle(shuffled)
|
|
126
|
+
return torch.stack(
|
|
127
|
+
[
|
|
128
|
+
encode_triple(s, e, d, 1.0 if i == 0 else 0.0, 0.0)
|
|
129
|
+
for i, (s, e, d) in enumerate(shuffled)
|
|
130
|
+
]
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _build_query(self, rng):
|
|
134
|
+
"""Walk + answer steps only (mirrors the second half of
|
|
135
|
+
build_traversal_episode_from_graph). -> (inputs, target_digits, mask) or None."""
|
|
136
|
+
path_length = rng.randint(*self.path_length_range)
|
|
137
|
+
cur = rng.randrange(len(self.node_labels))
|
|
138
|
+
walk = []
|
|
139
|
+
for _ in range(path_length):
|
|
140
|
+
if not self.adjacency[cur]:
|
|
141
|
+
break
|
|
142
|
+
dst_idx, edge_label = rng.choice(self.adjacency[cur])
|
|
143
|
+
walk.append((cur, edge_label, dst_idx))
|
|
144
|
+
cur = dst_idx
|
|
145
|
+
if not walk:
|
|
146
|
+
return None
|
|
147
|
+
steps, targets, mask = [], [], []
|
|
148
|
+
for i, (src_idx, edge_label, _dst) in enumerate(walk):
|
|
149
|
+
src = self.node_labels[src_idx] if i == 0 else None
|
|
150
|
+
steps.append(encode_triple(src, edge_label, None, 1.0 if i == 0 else 0.0, 0.0))
|
|
151
|
+
targets.append([0] * 9)
|
|
152
|
+
mask.append(0)
|
|
153
|
+
for i, (src_idx, edge_label, dst_idx) in enumerate(walk):
|
|
154
|
+
steps.append(encode_triple(None, None, None, 1.0 if i == 0 else 0.0, 1.0))
|
|
155
|
+
targets.append(
|
|
156
|
+
triple_to_digit_targets(
|
|
157
|
+
self.node_labels[src_idx], edge_label, self.node_labels[dst_idx]
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
mask.append(1)
|
|
161
|
+
return (
|
|
162
|
+
torch.stack(steps),
|
|
163
|
+
torch.tensor(targets, dtype=torch.long),
|
|
164
|
+
torch.tensor(mask, dtype=torch.float32),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def _build_shared_context_episodes(self, n: int, rng) -> list[Episode]:
|
|
168
|
+
contexts = [self._make_context(rng) for _ in range(self.num_contexts)]
|
|
169
|
+
episodes: list[Episode] = []
|
|
170
|
+
attempts = 0
|
|
171
|
+
while len(episodes) < n:
|
|
172
|
+
attempts += 1
|
|
173
|
+
if attempts > 100 * n + 1000:
|
|
174
|
+
raise RuntimeError("could not build enough episodes from this graph")
|
|
175
|
+
q = self._build_query(rng)
|
|
176
|
+
if q is None:
|
|
177
|
+
continue
|
|
178
|
+
k = len(episodes) % self.num_contexts
|
|
179
|
+
ctx_seq, (q_in, q_tgt, q_mask) = contexts[k], q
|
|
180
|
+
P = ctx_seq.shape[0]
|
|
181
|
+
episodes.append(
|
|
182
|
+
Episode(
|
|
183
|
+
torch.cat([ctx_seq, q_in]),
|
|
184
|
+
torch.cat([torch.zeros(P, 9, dtype=torch.long), q_tgt]),
|
|
185
|
+
torch.cat([torch.zeros(P), q_mask]),
|
|
186
|
+
meta={"walk_length": int(q_mask.sum().item()), "context_id": k},
|
|
187
|
+
cache_boundaries=[P],
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
return episodes
|
|
191
|
+
|
|
192
|
+
def score_episode(self, output, episode: Episode, verbose: bool = False) -> EpisodeScore:
|
|
193
|
+
answer_idx = (episode.mask == 1).nonzero(as_tuple=True)[0]
|
|
194
|
+
f = {
|
|
195
|
+
"src": [0, 0],
|
|
196
|
+
"edge": [0, 0],
|
|
197
|
+
"dst": [0, 0],
|
|
198
|
+
"dst|src+edge": [0, 0],
|
|
199
|
+
"src|prev_dst_ok": [0, 0],
|
|
200
|
+
}
|
|
201
|
+
prev_dst_ok = None
|
|
202
|
+
n_correct = 0
|
|
203
|
+
for hop, idx in enumerate(answer_idx, start=1):
|
|
204
|
+
pred = decode_prediction(output[idx])
|
|
205
|
+
td = episode.target[idx].tolist()
|
|
206
|
+
tgt = (
|
|
207
|
+
int("".join(map(str, td[0:3]))),
|
|
208
|
+
int("".join(map(str, td[3:6]))),
|
|
209
|
+
int("".join(map(str, td[6:9]))),
|
|
210
|
+
)
|
|
211
|
+
ok = pred == tgt
|
|
212
|
+
n_correct += int(ok)
|
|
213
|
+
for name, p, t in zip(("src", "edge", "dst"), pred, tgt):
|
|
214
|
+
f[name][0] += int(p == t)
|
|
215
|
+
f[name][1] += 1
|
|
216
|
+
if pred[0] == tgt[0] and pred[1] == tgt[1]: # lookup check
|
|
217
|
+
f["dst|src+edge"][1] += 1
|
|
218
|
+
f["dst|src+edge"][0] += int(pred[2] == tgt[2])
|
|
219
|
+
if prev_dst_ok: # chain check
|
|
220
|
+
f["src|prev_dst_ok"][1] += 1
|
|
221
|
+
f["src|prev_dst_ok"][0] += int(pred[0] == tgt[0])
|
|
222
|
+
prev_dst_ok = pred[2] == tgt[2]
|
|
223
|
+
hp = f.setdefault(f"hop{hop}", [0, 0]) # triple acc per hop position
|
|
224
|
+
hp[0] += int(ok)
|
|
225
|
+
hp[1] += 1
|
|
226
|
+
if verbose:
|
|
227
|
+
print(f" Pred: {pred} | Target: {tgt} | Correct: {ok}")
|
|
228
|
+
n = len(answer_idx)
|
|
229
|
+
return EpisodeScore(
|
|
230
|
+
n_items=n,
|
|
231
|
+
n_correct=n_correct,
|
|
232
|
+
perfect=(n_correct == n),
|
|
233
|
+
group=n,
|
|
234
|
+
fields={k: (v[0], v[1]) for k, v in f.items()},
|
|
235
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/tasks/multimodal_task.py
|
|
3
|
+
|
|
4
|
+
Multimodal inference task: delegates to data.multimodal.multimodal_dataset.
|
|
5
|
+
MultimodalDataset the same way text/audio/video tasks delegate to their
|
|
6
|
+
training-side dataset classes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from initium.data.multimodal.multimodal_dataset import MultimodalDataset
|
|
12
|
+
from initium.inference.metrics import EpisodeScore
|
|
13
|
+
from initium.inference.tasks.base_task import BaseInferenceTask, Episode
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MultimodalTask(BaseInferenceTask):
|
|
17
|
+
dataset_type = "multimodal"
|
|
18
|
+
|
|
19
|
+
def __init__(self, modalities, path_length_range=None, **kwargs):
|
|
20
|
+
# Inference always tests a trained model, so each per-modality real
|
|
21
|
+
# source given via --dataset-link (e.g. "video:clip.mp4+audio:clip.mp4")
|
|
22
|
+
# is wired in as that modality's test_dataset_link, matching every
|
|
23
|
+
# other *_task.py's own convention (see e.g. text_task.py).
|
|
24
|
+
self._ds = MultimodalDataset(modalities, link_role="test_dataset_link")
|
|
25
|
+
self.name = self._ds.name
|
|
26
|
+
self.input_dim, self.output_dim = self._ds.input_dim, self._ds.output_dim
|
|
27
|
+
self.query_range = (
|
|
28
|
+
tuple(path_length_range) if path_length_range else self._ds.primary.ood_query_range
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def describe(self) -> str:
|
|
32
|
+
real = [
|
|
33
|
+
m for m, s in zip(self._ds.modalities, self._ds.subs) if s._test_fact_pool is not None
|
|
34
|
+
]
|
|
35
|
+
src = f" | real data: {', '.join(real)}" if real else ""
|
|
36
|
+
return f"{self.name} | queries {self.query_range}{src}"
|
|
37
|
+
|
|
38
|
+
def build_episodes(self, n: int, rng, perturbation=None) -> list[Episode]:
|
|
39
|
+
episodes = []
|
|
40
|
+
for _ in range(n):
|
|
41
|
+
inp, tgt, mask, nq = self._ds._build_fused_episode(
|
|
42
|
+
(15, 20), self.query_range, rng=rng, use_test_pool=True
|
|
43
|
+
)
|
|
44
|
+
episodes.append(Episode(inp, tgt, mask, meta={"num_queries": nq}))
|
|
45
|
+
return episodes
|
|
46
|
+
|
|
47
|
+
def score_episode(self, output, episode: Episode, verbose: bool = False) -> EpisodeScore:
|
|
48
|
+
codec, D = self._ds.primary.codec, self._ds.primary.codec.label_dim
|
|
49
|
+
answer_idx = (episode.mask == 1).nonzero(as_tuple=True)[0]
|
|
50
|
+
f = {"value": [0, 0], "cumsum": [0, 0]}
|
|
51
|
+
n_correct = 0
|
|
52
|
+
for idx in answer_idx:
|
|
53
|
+
v_pred, c_pred = (
|
|
54
|
+
codec.decode_field(output[idx][0:D]),
|
|
55
|
+
codec.decode_field(output[idx][D : 2 * D]),
|
|
56
|
+
)
|
|
57
|
+
td = episode.target[idx].tolist()
|
|
58
|
+
nd = codec.num_digits
|
|
59
|
+
v_tgt = int("".join(map(str, td[0:nd])))
|
|
60
|
+
c_tgt = int("".join(map(str, td[nd : 2 * nd])))
|
|
61
|
+
v_ok, c_ok = v_pred == v_tgt, c_pred == c_tgt
|
|
62
|
+
f["value"][0] += int(v_ok)
|
|
63
|
+
f["value"][1] += 1
|
|
64
|
+
f["cumsum"][0] += int(c_ok)
|
|
65
|
+
f["cumsum"][1] += 1
|
|
66
|
+
n_correct += int(v_ok) + int(c_ok)
|
|
67
|
+
n = 2 * len(answer_idx)
|
|
68
|
+
return EpisodeScore(
|
|
69
|
+
n_items=n,
|
|
70
|
+
n_correct=n_correct,
|
|
71
|
+
perfect=(n_correct == n),
|
|
72
|
+
group=len(answer_idx),
|
|
73
|
+
fields={k: (v[0], v[1]) for k, v in f.items()},
|
|
74
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/tasks/task_registry.py
|
|
3
|
+
|
|
4
|
+
Single entry point used by run_inference.py (counterpart of
|
|
5
|
+
data/dataset_registry.py):
|
|
6
|
+
task = get_task(dataset_type, dataset_link)
|
|
7
|
+
|
|
8
|
+
v2: text/audio/video/multimodal, mirroring data/dataset_registry.py's v2.
|
|
9
|
+
multimodal is excluded from IMPLEMENTED_TYPES (legacy dim-inference list in
|
|
10
|
+
capabilities.py) since its input/output dims depend on WHICH modalities are
|
|
11
|
+
combined, not a fixed per-type signature -- it's only usable as an explicit
|
|
12
|
+
--dataset-type, never inferred from a legacy checkpoint's tensor shapes.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
GRAPH_ALIASES = ("graph", "graph-traversal", "graph_traversal")
|
|
16
|
+
TEXT_ALIASES = ("text",)
|
|
17
|
+
AUDIO_ALIASES = ("audio",)
|
|
18
|
+
VIDEO_ALIASES = ("video",)
|
|
19
|
+
MULTIMODAL_ALIASES = ("multimodal", "multi-modal", "multi_modal")
|
|
20
|
+
IMPLEMENTED_TYPES = ("graph", "text", "audio", "video")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def canonical_type(dataset_type: str) -> str:
|
|
24
|
+
t = (dataset_type or "graph").lower()
|
|
25
|
+
if t in GRAPH_ALIASES:
|
|
26
|
+
return "graph"
|
|
27
|
+
if t in TEXT_ALIASES:
|
|
28
|
+
return "text"
|
|
29
|
+
if t in AUDIO_ALIASES:
|
|
30
|
+
return "audio"
|
|
31
|
+
if t in VIDEO_ALIASES:
|
|
32
|
+
return "video"
|
|
33
|
+
if t in MULTIMODAL_ALIASES:
|
|
34
|
+
return "multimodal"
|
|
35
|
+
return t
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_task_class(dataset_type: str):
|
|
39
|
+
t = canonical_type(dataset_type)
|
|
40
|
+
if t == "graph":
|
|
41
|
+
from initium.inference.tasks.graph_traversal_task import GraphTraversalTask
|
|
42
|
+
|
|
43
|
+
return GraphTraversalTask
|
|
44
|
+
if t == "text":
|
|
45
|
+
from initium.inference.tasks.text_task import TextChainTask
|
|
46
|
+
|
|
47
|
+
return TextChainTask
|
|
48
|
+
if t == "audio":
|
|
49
|
+
from initium.inference.tasks.audio_task import AudioChainTask
|
|
50
|
+
|
|
51
|
+
return AudioChainTask
|
|
52
|
+
if t == "video":
|
|
53
|
+
from initium.inference.tasks.video_task import VideoChainTask
|
|
54
|
+
|
|
55
|
+
return VideoChainTask
|
|
56
|
+
if t == "multimodal":
|
|
57
|
+
from initium.inference.tasks.multimodal_task import MultimodalTask
|
|
58
|
+
|
|
59
|
+
return MultimodalTask
|
|
60
|
+
raise ValueError(f"unknown dataset type {dataset_type!r}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_task(dataset_type: str = "graph", dataset_link: str | None = None, **kwargs):
|
|
64
|
+
t = canonical_type(dataset_type)
|
|
65
|
+
cls = get_task_class(t)
|
|
66
|
+
if t == "multimodal":
|
|
67
|
+
return cls((dataset_link or "text+audio").split("+"), **kwargs)
|
|
68
|
+
return cls(dataset_link, **kwargs)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: inference/tasks/text_task.py
|
|
3
|
+
|
|
4
|
+
Text-modality inference task. Delegates episode-building/decoding to
|
|
5
|
+
data.text.text_dataset.TextChainDataset (same pattern graph_traversal_task.py
|
|
6
|
+
uses relative to graph_traversal.py - training and inference can never
|
|
7
|
+
silently drift apart).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import torch
|
|
13
|
+
|
|
14
|
+
from initium.data.text.text_dataset import TextChainDataset
|
|
15
|
+
from initium.inference.metrics import EpisodeScore
|
|
16
|
+
from initium.inference.tasks.base_task import BaseInferenceTask, Episode
|
|
17
|
+
|
|
18
|
+
BUILTIN_LINKS = (None, "", "text-chain")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TextChainTask(BaseInferenceTask):
|
|
22
|
+
name = "text-chain"
|
|
23
|
+
dataset_type = "text"
|
|
24
|
+
|
|
25
|
+
def __init__(self, dataset_link=None, path_length_range=None, **kwargs):
|
|
26
|
+
# Inference always tests a trained model, so --dataset-link here plays the
|
|
27
|
+
# role of test_dataset_link on the training-side dataset class: a real
|
|
28
|
+
# text file used as the held-out fact source, instead of the synthetic
|
|
29
|
+
# seeded table. Omit (or pass one of BUILTIN_LINKS) for that default.
|
|
30
|
+
link = None if dataset_link in BUILTIN_LINKS else dataset_link
|
|
31
|
+
self._ds = TextChainDataset(test_dataset_link=link)
|
|
32
|
+
self.input_dim, self.output_dim = self._ds.input_dim, self._ds.output_dim
|
|
33
|
+
self.query_range = (
|
|
34
|
+
tuple(path_length_range) if path_length_range else self._ds.ood_query_range
|
|
35
|
+
)
|
|
36
|
+
self.facts = self._ds.build_ood_facts()
|
|
37
|
+
|
|
38
|
+
def describe(self) -> str:
|
|
39
|
+
return f"{self.name} | {len(self.facts)} facts | queries {self.query_range}"
|
|
40
|
+
|
|
41
|
+
def build_episodes(self, n: int, rng, perturbation=None) -> list[Episode]:
|
|
42
|
+
severity = (
|
|
43
|
+
(perturbation or {}).get("severity", 0.0) if isinstance(perturbation, dict) else 0.0
|
|
44
|
+
)
|
|
45
|
+
keys, vals_map = [k for k, _ in self.facts], dict(self.facts)
|
|
46
|
+
NF = self._ds.codec.num_digits
|
|
47
|
+
episodes = []
|
|
48
|
+
for _ in range(n):
|
|
49
|
+
nq = rng.randint(*self.query_range)
|
|
50
|
+
query_keys = [rng.choice(keys) for _ in range(nq)]
|
|
51
|
+
facts = list(self.facts)
|
|
52
|
+
rng.shuffle(facts)
|
|
53
|
+
inputs, targets, mask = [], [], []
|
|
54
|
+
for i, (k, v) in enumerate(facts):
|
|
55
|
+
inputs.append(
|
|
56
|
+
self._ds._encode_fact(
|
|
57
|
+
k, v, 1.0 if i == 0 else 0.0, perturb=(severity or None), rng=rng
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
targets.append([0] * (2 * NF))
|
|
61
|
+
mask.append(0)
|
|
62
|
+
for i, k in enumerate(query_keys):
|
|
63
|
+
inputs.append(self._ds._encode_query(k, 1.0 if i == 0 else 0.0))
|
|
64
|
+
targets.append([0] * (2 * NF))
|
|
65
|
+
mask.append(0)
|
|
66
|
+
cumsum = 0
|
|
67
|
+
for i, k in enumerate(query_keys):
|
|
68
|
+
cumsum = (cumsum + vals_map[k]) % self._ds.label_range
|
|
69
|
+
inputs.append(self._ds._encode_answer(1.0 if i == 0 else 0.0))
|
|
70
|
+
targets.append(
|
|
71
|
+
self._ds.codec.label_to_digit_targets(vals_map[k])
|
|
72
|
+
+ self._ds.codec.label_to_digit_targets(cumsum)
|
|
73
|
+
)
|
|
74
|
+
mask.append(1)
|
|
75
|
+
episodes.append(
|
|
76
|
+
Episode(
|
|
77
|
+
torch.stack(inputs),
|
|
78
|
+
torch.tensor(targets, dtype=torch.long),
|
|
79
|
+
torch.tensor(mask, dtype=torch.float32),
|
|
80
|
+
meta={"num_queries": nq},
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
return episodes
|
|
84
|
+
|
|
85
|
+
def score_episode(self, output, episode: Episode, verbose: bool = False) -> EpisodeScore:
|
|
86
|
+
codec, D = self._ds.codec, self._ds.codec.label_dim
|
|
87
|
+
answer_idx = (episode.mask == 1).nonzero(as_tuple=True)[0]
|
|
88
|
+
f = {"value": [0, 0], "cumsum": [0, 0]}
|
|
89
|
+
n_correct = 0
|
|
90
|
+
for idx in answer_idx:
|
|
91
|
+
v_pred, c_pred = (
|
|
92
|
+
codec.decode_field(output[idx][0:D]),
|
|
93
|
+
codec.decode_field(output[idx][D : 2 * D]),
|
|
94
|
+
)
|
|
95
|
+
td = episode.target[idx].tolist()
|
|
96
|
+
nd = codec.num_digits
|
|
97
|
+
v_tgt = int("".join(map(str, td[0:nd])))
|
|
98
|
+
c_tgt = int("".join(map(str, td[nd : 2 * nd])))
|
|
99
|
+
v_ok, c_ok = v_pred == v_tgt, c_pred == c_tgt
|
|
100
|
+
f["value"][0] += int(v_ok)
|
|
101
|
+
f["value"][1] += 1
|
|
102
|
+
f["cumsum"][0] += int(c_ok)
|
|
103
|
+
f["cumsum"][1] += 1
|
|
104
|
+
n_correct += int(v_ok) + int(c_ok)
|
|
105
|
+
if verbose:
|
|
106
|
+
print(f" value {v_pred}/{v_tgt} ok={v_ok} | cumsum {c_pred}/{c_tgt} ok={c_ok}")
|
|
107
|
+
n = 2 * len(answer_idx)
|
|
108
|
+
return EpisodeScore(
|
|
109
|
+
n_items=n,
|
|
110
|
+
n_correct=n_correct,
|
|
111
|
+
perfect=(n_correct == n),
|
|
112
|
+
group=len(answer_idx),
|
|
113
|
+
fields={k: (v[0], v[1]) for k, v in f.items()},
|
|
114
|
+
)
|