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.
Files changed (71) hide show
  1. initium/LNN_controller/cfc_backbone_parallel.py +265 -0
  2. initium/LNN_controller/cfc_controller.py +325 -0
  3. initium/LNN_controller/chained_controller.py +83 -0
  4. initium/LNN_controller/hybrid_controller.py +90 -0
  5. initium/MoE/README.md +1 -0
  6. initium/MoE/moe_layer.py +552 -0
  7. initium/__init__.py +1 -0
  8. initium/config/controller_config.py +86 -0
  9. initium/config/train_config.py +64 -0
  10. initium/core_training.py +2182 -0
  11. initium/data/__init__.py +0 -0
  12. initium/data/audio/__init__.py +0 -0
  13. initium/data/audio/audio_dataset.py +96 -0
  14. initium/data/base_dataset.py +75 -0
  15. initium/data/common/__init__.py +0 -0
  16. initium/data/common/chain_task.py +542 -0
  17. initium/data/common/digit_codec.py +85 -0
  18. initium/data/common/graph_io.py +123 -0
  19. initium/data/common/real_data.py +747 -0
  20. initium/data/dataset_registry.py +59 -0
  21. initium/data/graph_traversal/__init__.py +0 -0
  22. initium/data/graph_traversal/graph_traversal.py +746 -0
  23. initium/data/multimodal/__init__.py +0 -0
  24. initium/data/multimodal/multimodal_dataset.py +401 -0
  25. initium/data/text/__init__.py +0 -0
  26. initium/data/text/text_dataset.py +79 -0
  27. initium/data/video/__init__.py +0 -0
  28. initium/data/video/video_dataset.py +85 -0
  29. initium/inference/__init__.py +1 -0
  30. initium/inference/cache/__init__.py +1 -0
  31. initium/inference/cache/base_cache.py +112 -0
  32. initium/inference/cache/cache_config.py +21 -0
  33. initium/inference/cache/cache_registry.py +99 -0
  34. initium/inference/cache/cached_engine.py +264 -0
  35. initium/inference/cache/keys.py +77 -0
  36. initium/inference/cache/prefix_cache.py +59 -0
  37. initium/inference/cache/result_cache.py +57 -0
  38. initium/inference/cache/state_utils.py +74 -0
  39. initium/inference/cache/stores.py +167 -0
  40. initium/inference/capabilities.py +86 -0
  41. initium/inference/checkpoint_io.py +63 -0
  42. initium/inference/engine.py +88 -0
  43. initium/inference/inference_config.py +22 -0
  44. initium/inference/metrics.py +103 -0
  45. initium/inference/model_loader.py +270 -0
  46. initium/inference/run_inference.py +526 -0
  47. initium/inference/run_logging.py +173 -0
  48. initium/inference/tasks/__init__.py +1 -0
  49. initium/inference/tasks/audio_task.py +114 -0
  50. initium/inference/tasks/base_task.py +60 -0
  51. initium/inference/tasks/graph_traversal_task.py +235 -0
  52. initium/inference/tasks/multimodal_task.py +74 -0
  53. initium/inference/tasks/task_registry.py +68 -0
  54. initium/inference/tasks/text_task.py +114 -0
  55. initium/inference/tasks/video_task.py +114 -0
  56. initium/mamba_controller/mamba2_controller.py +433 -0
  57. initium/mamba_controller/mamba3_controller.py +242 -0
  58. initium/mamba_controller/mamba_backbone_parallel.py +291 -0
  59. initium/mamba_controller/mamba_controller.py +983 -0
  60. initium/mamba_controller/split_graph_dnc.py +623 -0
  61. initium/memory_manipulation/dynamic_memory_resize.py +157 -0
  62. initium/memory_manipulation/dynamic_n_controller.py +152 -0
  63. initium/memory_manipulation/link_matrix_ablation.py +130 -0
  64. initium/memory_manipulation/nvrtc_compat.py +143 -0
  65. initium/memory_manipulation/stochastic_write_head_v2.py +545 -0
  66. initium/setup_wheels.py +175 -0
  67. initium_deeplearning-0.1.0.dist-info/METADATA +253 -0
  68. initium_deeplearning-0.1.0.dist-info/RECORD +71 -0
  69. initium_deeplearning-0.1.0.dist-info/WHEEL +4 -0
  70. initium_deeplearning-0.1.0.dist-info/entry_points.txt +3 -0
  71. 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
+ )