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,542 @@
1
+ """
2
+ file: data/common/chain_task.py
3
+
4
+ Generic "K facts, then M chained queries" recall+aggregation task -- shared
5
+ scaffolding for the text / audio / video dataset modules. Mirrors
6
+ data/graph_traversal/graph_traversal.py's overall shape (digit-encoded
7
+ fields, curriculum, evaluate_*, depth breakdown, OOD eval against a fixed
8
+ held-out table, memory-ablation eval, robustness eval) but factored so the
9
+ mechanical plumbing isn't re-derived three times.
10
+
11
+ What stays per-modality (the "treat each modality individually" seam, NOT
12
+ shared): the semantic meaning of a "fact"/"query" (see each of
13
+ data/text/text_dataset.py, data/audio/audio_dataset.py,
14
+ data/video/video_dataset.py), the OOD generalization axis
15
+ (_synthetic_ood_facts), and the robustness perturbation (perturb_fact).
16
+
17
+ Real-data hook (v2): `_fact_pool` / `_test_fact_pool` (None by default --
18
+ unchanged synthetic behavior). A subclass whose __init__ was given a real
19
+ dataset_link/test_dataset_link (see data/common/real_data.py) sets these to
20
+ a real (key,value) fact pool built from that source; build_episode() then
21
+ samples FROM that pool instead of drawing fresh random labels, and
22
+ build_ood_facts() uses the disjoint real test pool instead of the seeded
23
+ synthetic table -- the modality analogue of "train on synthetic graphs,
24
+ test on the held-out London Underground graph."
25
+
26
+ Task shape: present `num_facts` (key, value) pairs (digit-coded labels in
27
+ [0, label_range)), shuffled, first item phase-tagged (mirrors graph's own
28
+ convention exactly). Then `num_queries` query keys in sequence; each answer
29
+ step must predict:
30
+ - `value` : the fact value for that key (single-hop recall -- analogue of
31
+ graph's dst|src+edge lookup check)
32
+ - `cumsum` : (running sum of every value answered so far) mod label_range
33
+ (forces genuine sequential state -- analogue of graph's
34
+ src|prev_dst_ok chain check)
35
+ Depth axis = num_queries (this family's analogue of "hop count").
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import random
41
+
42
+ import torch
43
+
44
+ from initium.data.base_dataset import BaseDataset
45
+ from initium.data.common.digit_codec import DigitCodec, digit_field_diversity, digit_field_loss
46
+ from initium.memory_manipulation.dynamic_memory_resize import resize_memory
47
+
48
+ NUM_FIELDS = 2 # [value, cumsum]
49
+ NUM_PHASE_CHANNELS = 2
50
+
51
+
52
+ def score_answer_steps(output, target_digits, answer_mask, codec, verbose=False, field_log=None):
53
+ """Shared per-episode answer scoring: decode each answer step's (value,
54
+ cumsum) fields, compare to target_digits, optionally print, optionally
55
+ accumulate a per-(episode_depth, hop_position) breakdown. Used by both
56
+ KVChainDataset.evaluate_chain (text/audio/video) and
57
+ MultimodalDataset.evaluate_chain (data/multimodal/multimodal_dataset.py)
58
+ so the two families can never silently drift in what counts as
59
+ 'correct', how depth/perfect-episode bookkeeping is derived, or how the
60
+ field_log is keyed.
61
+ -> (ep_total, ep_correct, episode_perfect, correct_value, correct_cumsum, n_steps)
62
+ ep_total/ep_correct: this episode's step-count / correct-field-count
63
+ (2 fields per step), for the caller's by_depth breakdown.
64
+ correct_value/correct_cumsum/n_steps: per-field correct counts and step
65
+ count, for the caller's running totals (separate value vs cumsum
66
+ accuracy in the final summary print).
67
+ field_log: optional dict, keyed by (depth, hop_position) -> [n, value_ok,
68
+ cumsum_ok], filled in place. depth = this episode's total number of
69
+ answer steps (its 'num_queries' -- the family's hop-count analogue),
70
+ so this groups hop position WITHIN episodes of the same length,
71
+ mirroring graph_traversal.py's (path_length, hop_position) keying.
72
+ Lets a caller ask "does cumsum specifically degrade at later hop
73
+ positions within an episode (a capacity/forgetting signal), or is it
74
+ uniformly low across positions (a training-progress signal)?"
75
+ """
76
+ D = codec.label_dim
77
+ nd = codec.num_digits
78
+ answer_idx = (answer_mask == 1).nonzero(as_tuple=True)[0]
79
+ depth = int(answer_idx.shape[0])
80
+ ep_total = ep_correct = 0
81
+ episode_perfect = True
82
+ correct_value = correct_cumsum = n_steps = 0
83
+ for hop_pos, idx in enumerate(answer_idx, start=1):
84
+ v_pred = codec.decode_field(output[idx][0:D])
85
+ c_pred = codec.decode_field(output[idx][D : 2 * D])
86
+ td = target_digits[idx].tolist()
87
+ v_tgt = int("".join(map(str, td[0:nd])))
88
+ c_tgt = int("".join(map(str, td[nd : 2 * nd])))
89
+ v_ok, c_ok = v_pred == v_tgt, c_pred == c_tgt
90
+ correct_value += int(v_ok)
91
+ correct_cumsum += int(c_ok)
92
+ ep_correct += int(v_ok) + int(c_ok)
93
+ n_steps += 1
94
+ ep_total += 1
95
+ if not (v_ok and c_ok):
96
+ episode_perfect = False
97
+ if verbose:
98
+ print(
99
+ f" step {hop_pos}: value {v_pred}/{v_tgt} ok={v_ok} | cumsum {c_pred}/{c_tgt} ok={c_ok}"
100
+ )
101
+ if field_log is not None:
102
+ fl = field_log.setdefault((depth, hop_pos), [0, 0, 0])
103
+ fl[0] += 1
104
+ fl[1] += int(v_ok)
105
+ fl[2] += int(c_ok)
106
+ return ep_total, ep_correct, episode_perfect, correct_value, correct_cumsum, n_steps
107
+
108
+
109
+ def write_value_cumsum_field_log(writer, file, step, lesson, eval_type, field_log):
110
+ """Shared field_log writer for the value/cumsum family (KVChainDataset
111
+ and MultimodalDataset both fill field_log via score_answer_steps above,
112
+ so they share this same (depth, hop_position) -> [n, value_ok,
113
+ cumsum_ok] schema and can share how it's dumped/printed)."""
114
+ for (depth, hop_pos), (n, v_ok, c_ok) in sorted(field_log.items()):
115
+ writer.writerow(
116
+ [step, lesson, eval_type, depth, hop_pos, n, v_ok / n * 100, c_ok / n * 100]
117
+ )
118
+ file.flush()
119
+ agg = {}
120
+ for (_depth, hop_pos), v in field_log.items():
121
+ a = agg.setdefault(hop_pos, [0, 0, 0])
122
+ for i in range(3):
123
+ a[i] += v[i]
124
+ parts = []
125
+ for hop_pos in sorted(agg)[:4]:
126
+ n, v_ok, c_ok = agg[hop_pos]
127
+ parts.append(
128
+ f"hop{hop_pos}: value {v_ok / n * 100:.0f} cumsum {c_ok / n * 100:.0f} (n={n})"
129
+ )
130
+ print(f" [field acc {eval_type} step {step}] " + " | ".join(parts))
131
+
132
+
133
+ def value_cumsum_field_log_header():
134
+ return ["depth", "hop_position", "n_steps", "value_acc", "cumsum_acc"]
135
+
136
+
137
+ class ChainCurriculum:
138
+ def __init__(self, dataset, table, lesson_nr_cells):
139
+ assert len(table) == len(lesson_nr_cells)
140
+ self.dataset = dataset
141
+ self.table = table
142
+ self.lesson = 0
143
+ self.lesson_nr_cells = list(lesson_nr_cells)
144
+
145
+ def maybe_advance(self, model, device, step=None, optimizer=None):
146
+ nf, nq = self.table[self.lesson]
147
+ combined_acc, perfect_frac, breakdown = self.dataset.evaluate_chain(
148
+ model,
149
+ device,
150
+ num_episodes=self.dataset.eval_batch_size,
151
+ num_facts_range=nf,
152
+ num_queries_range=nq,
153
+ step_breakdown=True,
154
+ field_log=(field_log := {}),
155
+ )
156
+ if breakdown:
157
+ print(
158
+ f" [lesson {self.lesson + 1} eval by depth] "
159
+ + ", ".join(
160
+ f"{d}-step: acc {a:.1f}% perfect {p:.1f}% (n={n})"
161
+ for d, (a, p, n) in breakdown.items()
162
+ )
163
+ )
164
+ hop_writer = getattr(self, "hop_log_writer", None)
165
+ if hop_writer is not None:
166
+ for d, (a, p, n) in sorted(breakdown.items()):
167
+ hop_writer.writerow([step, self.lesson + 1, "id", d, a, p, n])
168
+ self.hop_log_file.flush()
169
+
170
+ field_writer = getattr(self, "field_log_writer", None)
171
+ if field_writer is not None and field_log:
172
+ self.dataset.write_field_log(
173
+ field_writer, self.field_log_file, step, self.lesson + 1, "id", field_log
174
+ )
175
+
176
+ if (
177
+ combined_acc / 100.0 >= self.dataset.advance_threshold
178
+ and self.lesson < len(self.table) - 1
179
+ ):
180
+ self.lesson += 1
181
+ new_n = self.lesson_nr_cells[self.lesson]
182
+ if new_n != self.lesson_nr_cells[self.lesson - 1]:
183
+ resize_memory(model, new_n, device=device, optimizer=optimizer)
184
+ print(f">>> Memory resized to nr_cells={new_n} for lesson {self.lesson + 1}")
185
+ print(f">>> Curriculum advanced to lesson {self.lesson + 1}/{len(self.table)}")
186
+ writer = getattr(self, "advance_log_writer", None)
187
+ if writer is not None:
188
+ writer.writerow([step, self.lesson + 1, len(self.table)])
189
+ self.advance_log_file.flush()
190
+ return self.lesson, combined_acc, perfect_frac
191
+
192
+
193
+ def _as_range(v):
194
+ """Accept either a fixed int or an (lo, hi) range; normalize to a range."""
195
+ return tuple(v) if isinstance(v, tuple | list) else (v, v)
196
+
197
+
198
+ class KVChainDataset(BaseDataset):
199
+ """Parameterized by a few modality-specific hooks (see below). Concrete
200
+ text/audio/video dataset classes only override `name`, the curriculum
201
+ table/lesson_nr_cells, `_synthetic_ood_facts()`, and optionally
202
+ `perturb_fact()` -- plus, when a real dataset_link is given, they set
203
+ `_fact_pool`/`_test_fact_pool` in their own __init__ (see module
204
+ docstring's "Real-data hook")."""
205
+
206
+ advance_threshold = 0.85
207
+ label_range = 1000
208
+ codec = DigitCodec(num_digits=3, digit_base=10)
209
+ input_dim = 2 * codec.label_dim + NUM_PHASE_CHANNELS # 62
210
+ output_dim = NUM_FIELDS * codec.label_dim # 60
211
+ eval_batch_size = 100
212
+ old_lesson_mix_rate = 0.10
213
+ ood_query_range = (3, 8)
214
+
215
+ # Real-data hook (v2): None -> unchanged synthetic behavior everywhere
216
+ # below. Set by a subclass's __init__ when a real dataset_link/
217
+ # test_dataset_link was given.
218
+ _fact_pool: list[tuple[int, int]] | None = None
219
+ _test_fact_pool: list[tuple[int, int]] | None = None
220
+ _table: list
221
+ _lesson_nr_cells: list[int]
222
+
223
+ # ---- modality-specific hooks -------------------------------------------
224
+ def build_ood_facts(self, n_facts: int = 20):
225
+ """Fixed held-out fact table -- the analogue of graph's
226
+ build_london_underground_eval(): unseen at training time,
227
+ reproducible across runs. Uses the real, disjoint-key test pool
228
+ when one was loaded (see module docstring); otherwise falls back to
229
+ each subclass's own seeded synthetic table via
230
+ _synthetic_ood_facts()."""
231
+ if self._test_fact_pool is not None:
232
+ rng = random.Random(999)
233
+ pool = self._test_fact_pool
234
+ return rng.sample(pool, min(n_facts, len(pool)))
235
+ return self._synthetic_ood_facts(n_facts)
236
+
237
+ def _synthetic_ood_facts(self, n_facts: int = 20):
238
+ """Default synthetic seeded held-out table. Override per modality
239
+ for a more domain-meaningful OOD story (see each of
240
+ data/text/text_dataset.py etc.)."""
241
+ rng = random.Random(1234)
242
+ keys = rng.sample(range(self.label_range), n_facts)
243
+ vals = [rng.randrange(self.label_range) for _ in range(n_facts)]
244
+ return list(zip(keys, vals))
245
+
246
+ def perturb_fact(self, key_vec: torch.Tensor, value_vec: torch.Tensor, rng, severity: float):
247
+ """Robustness hook (Layer D). Default: flip a random one-hot
248
+ digit-slot with prob=severity ("token corruption"). Override for a
249
+ more domain-meaningful perturbation."""
250
+
251
+ def _flip(vec):
252
+ vec = vec.clone()
253
+ if rng.random() < severity:
254
+ nd, db = self.codec.num_digits, self.codec.digit_base
255
+ pos = rng.randrange(nd)
256
+ vec[pos * db : (pos + 1) * db] = 0.0
257
+ vec[pos * db + rng.randrange(db)] = 1.0
258
+ return vec
259
+
260
+ return _flip(key_vec), _flip(value_vec)
261
+
262
+ # ---- curriculum ---------------------------------------------------------
263
+ def make_curriculum(self):
264
+ return ChainCurriculum(self, self._table, self._lesson_nr_cells)
265
+
266
+ # ---- episode construction ------------------------------------------------
267
+ def _encode_fact(self, key, value, phase_transition, perturb=None, rng=None):
268
+ key_vec = self.codec.encode_label(key)
269
+ val_vec = self.codec.encode_label(value)
270
+ if perturb:
271
+ key_vec, val_vec = self.perturb_fact(key_vec, val_vec, rng or random, perturb)
272
+ return torch.cat([key_vec, val_vec, torch.tensor([phase_transition, 0.0])])
273
+
274
+ def _encode_query(self, key, phase_transition):
275
+ return torch.cat(
276
+ [
277
+ self.codec.encode_label(key),
278
+ torch.zeros(self.codec.label_dim),
279
+ torch.tensor([phase_transition, 0.0]),
280
+ ]
281
+ )
282
+
283
+ def _encode_answer(self, phase_transition):
284
+ return torch.cat(
285
+ [torch.zeros(2 * self.codec.label_dim), torch.tensor([phase_transition, 1.0])]
286
+ )
287
+
288
+ def build_episode(self, num_facts_range, num_queries_range, rng=None, perturb=None):
289
+ rng = rng if rng is not None else random
290
+ num_facts_range = _as_range(num_facts_range)
291
+ num_queries_range = _as_range(num_queries_range)
292
+ num_facts = rng.randint(*num_facts_range)
293
+ if self._fact_pool is not None:
294
+ # Real-data hook: sample real (key,value) facts from the loaded
295
+ # pool instead of drawing fresh random labels every episode.
296
+ num_facts = min(num_facts, len(self._fact_pool))
297
+ facts = rng.sample(self._fact_pool, num_facts)
298
+ else:
299
+ keys = rng.sample(range(self.label_range), num_facts)
300
+ vals = [rng.randrange(self.label_range) for _ in range(num_facts)]
301
+ facts = list(zip(keys, vals))
302
+ rng.shuffle(facts)
303
+ num_queries = rng.randint(*num_queries_range)
304
+ fact_keys = [k for k, _ in facts]
305
+ query_keys = [rng.choice(fact_keys) for _ in range(num_queries)]
306
+
307
+ NF = self.codec.num_digits
308
+ inputs, target_digits, answer_mask = [], [], []
309
+ for i, (k, v) in enumerate(facts):
310
+ inputs.append(self._encode_fact(k, v, 1.0 if i == 0 else 0.0, perturb=perturb, rng=rng))
311
+ target_digits.append([0] * (2 * NF))
312
+ answer_mask.append(0)
313
+ for i, k in enumerate(query_keys):
314
+ inputs.append(self._encode_query(k, 1.0 if i == 0 else 0.0))
315
+ target_digits.append([0] * (2 * NF))
316
+ answer_mask.append(0)
317
+ value_of, cumsum = dict(facts), 0
318
+ for i, k in enumerate(query_keys):
319
+ cumsum = (cumsum + value_of[k]) % self.label_range
320
+ inputs.append(self._encode_answer(1.0 if i == 0 else 0.0))
321
+ target_digits.append(
322
+ self.codec.label_to_digit_targets(value_of[k])
323
+ + self.codec.label_to_digit_targets(cumsum)
324
+ )
325
+ answer_mask.append(1)
326
+ return (
327
+ torch.stack(inputs),
328
+ torch.tensor(target_digits, dtype=torch.long),
329
+ torch.tensor(answer_mask, dtype=torch.float32),
330
+ num_queries,
331
+ )
332
+
333
+ def collate(self, batch):
334
+ max_len = max(len(x[0]) for x in batch)
335
+ B = len(batch)
336
+ padded_input = torch.zeros(B, max_len, self.input_dim)
337
+ padded_targets = torch.zeros(
338
+ B, max_len, NUM_FIELDS * self.codec.num_digits, dtype=torch.long
339
+ )
340
+ padded_mask = torch.zeros(B, max_len)
341
+ for i, (inp, tgt, mask, _) in enumerate(batch):
342
+ T = inp.size(0)
343
+ padded_input[i, :T] = inp
344
+ padded_targets[i, :T] = tgt
345
+ padded_mask[i, :T] = mask
346
+ return padded_input, padded_targets, padded_mask
347
+
348
+ def sample_batch(self, curriculum, batch_size):
349
+ episodes = []
350
+ for _ in range(batch_size):
351
+ idx = (
352
+ random.randint(0, curriculum.lesson - 1)
353
+ if curriculum.lesson > 0 and random.random() < self.old_lesson_mix_rate
354
+ else curriculum.lesson
355
+ )
356
+ nf, nq = curriculum.table[idx]
357
+ episodes.append(self.build_episode(nf, nq))
358
+ return self.collate(episodes)
359
+
360
+ # ---- loss / diversity -----------------------------------------------------
361
+ def loss(self, output, target, mask):
362
+ return digit_field_loss(output, target, mask, NUM_FIELDS, self.codec.digit_base)
363
+
364
+ def diversity(self, output, target, mask):
365
+ return digit_field_diversity(
366
+ output, mask, NUM_FIELDS * self.codec.num_digits, self.codec.digit_base
367
+ )
368
+
369
+ def set_output_proj(self, proj):
370
+ self._output_proj = proj
371
+
372
+ # ---- evaluation -------------------------------------------------------------
373
+ def _decode_answer(self, output_step):
374
+ D = self.codec.label_dim
375
+ return self.codec.decode_field(output_step[0:D]), self.codec.decode_field(
376
+ output_step[D : 2 * D]
377
+ )
378
+
379
+ def evaluate_chain(
380
+ self,
381
+ model,
382
+ device,
383
+ num_episodes,
384
+ num_facts_range=None,
385
+ num_queries_range=None,
386
+ fixed_facts=None,
387
+ rng=None,
388
+ ablate_memory=False,
389
+ step_breakdown=False,
390
+ perturb=None,
391
+ verbose_n=0,
392
+ skip_stages=None,
393
+ field_log=None,
394
+ ):
395
+ model.eval()
396
+ total = correct_value = correct_cumsum = perfect_episodes = tested = 0
397
+ by_depth = {}
398
+ _rng = rng or random
399
+ with torch.no_grad():
400
+ while tested < num_episodes:
401
+ if fixed_facts is not None:
402
+ keys = [k for k, _ in fixed_facts]
403
+ vals_map = dict(fixed_facts)
404
+ nq = _rng.randint(*_as_range(num_queries_range or self.ood_query_range))
405
+ query_keys = [_rng.choice(keys) for _ in range(nq)]
406
+ facts = list(fixed_facts)
407
+ _rng.shuffle(facts)
408
+ NF = self.codec.num_digits
409
+ inputs, target_digits, answer_mask = [], [], []
410
+ for i, (k, v) in enumerate(facts):
411
+ inputs.append(
412
+ self._encode_fact(
413
+ k, v, 1.0 if i == 0 else 0.0, perturb=perturb, rng=_rng
414
+ )
415
+ )
416
+ target_digits.append([0] * (2 * NF))
417
+ answer_mask.append(0)
418
+ for i, k in enumerate(query_keys):
419
+ inputs.append(self._encode_query(k, 1.0 if i == 0 else 0.0))
420
+ target_digits.append([0] * (2 * NF))
421
+ answer_mask.append(0)
422
+ cumsum = 0
423
+ for i, k in enumerate(query_keys):
424
+ cumsum = (cumsum + vals_map[k]) % self.label_range
425
+ inputs.append(self._encode_answer(1.0 if i == 0 else 0.0))
426
+ target_digits.append(
427
+ self.codec.label_to_digit_targets(vals_map[k])
428
+ + self.codec.label_to_digit_targets(cumsum)
429
+ )
430
+ answer_mask.append(1)
431
+ ep = (
432
+ torch.stack(inputs),
433
+ torch.tensor(target_digits, dtype=torch.long),
434
+ torch.tensor(answer_mask, dtype=torch.float32),
435
+ nq,
436
+ )
437
+ else:
438
+ ep = self.build_episode(
439
+ num_facts_range, num_queries_range, rng=_rng, perturb=perturb
440
+ )
441
+ input_seq, target_digits, answer_mask, _ = ep
442
+ input_seq = input_seq.unsqueeze(0).to(device)
443
+ output, _ = model(
444
+ input_seq,
445
+ (None, None, None),
446
+ reset_experience=True,
447
+ pass_through_memory=not ablate_memory,
448
+ **({"combiner_skip_stages": skip_stages} if skip_stages else {}),
449
+ )
450
+ output = self._output_proj(output.transpose(0, 1).contiguous().squeeze(0))
451
+
452
+ ep_total, ep_correct, episode_perfect, ep_cv, ep_cc, ep_n = score_answer_steps(
453
+ output,
454
+ target_digits,
455
+ answer_mask,
456
+ self.codec,
457
+ verbose=(tested < verbose_n),
458
+ field_log=field_log,
459
+ )
460
+ correct_value += ep_cv
461
+ correct_cumsum += ep_cc
462
+ total += ep_n
463
+ perfect_episodes += int(episode_perfect)
464
+ tested += 1
465
+ if step_breakdown:
466
+ acc = by_depth.setdefault(ep_total, [0, 0, 0, 0])
467
+ acc[0] += 2 * ep_total
468
+ acc[1] += ep_correct
469
+ acc[2] += 1
470
+ acc[3] += int(episode_perfect)
471
+
472
+ combined_acc = 100.0 * (correct_value + correct_cumsum) / max(2 * total, 1)
473
+ perfect_frac = 100.0 * perfect_episodes / max(tested, 1)
474
+ print(
475
+ f"Eval [{self.name}]: value {100.0 * correct_value / max(total, 1):.2f}% | "
476
+ f"cumsum {100.0 * correct_cumsum / max(total, 1):.2f}% | combined {combined_acc:.2f}% | "
477
+ f"perfect {perfect_frac:.2f}% ({tested} episodes)"
478
+ )
479
+ model.train()
480
+ if step_breakdown:
481
+ breakdown = {
482
+ d: (100.0 * c / max(t, 1), 100.0 * p / max(e, 1), e)
483
+ for d, (t, c, e, p) in sorted(by_depth.items())
484
+ }
485
+ return combined_acc, perfect_frac, breakdown
486
+ return combined_acc, perfect_frac
487
+
488
+ def evaluate_ood(self, model, device, num_episodes, rng, verbose_n=0, field_log=None):
489
+ return self.evaluate_chain(
490
+ model,
491
+ device,
492
+ num_episodes,
493
+ fixed_facts=self.build_ood_facts(),
494
+ num_queries_range=self.ood_query_range,
495
+ rng=rng,
496
+ step_breakdown=True,
497
+ verbose_n=verbose_n,
498
+ field_log=field_log,
499
+ )
500
+
501
+ def evaluate_id_ablated(self, model, device, curriculum, lesson_idx):
502
+ nf, nq = curriculum.table[lesson_idx]
503
+ return self.evaluate_chain(
504
+ model,
505
+ device,
506
+ self.eval_batch_size,
507
+ num_facts_range=nf,
508
+ num_queries_range=nq,
509
+ ablate_memory=True,
510
+ )
511
+
512
+ def evaluate_id_combiner_stage_ablated(
513
+ self, model, device, curriculum, lesson_idx, skip_stages
514
+ ):
515
+ nf, nq = curriculum.table[lesson_idx]
516
+ return self.evaluate_chain(
517
+ model,
518
+ device,
519
+ self.eval_batch_size,
520
+ num_facts_range=nf,
521
+ num_queries_range=nq,
522
+ skip_stages=skip_stages,
523
+ )
524
+
525
+ def evaluate_robustness(self, model, device, curriculum, lesson_idx, perturbation, rng):
526
+ nf, nq = curriculum.table[lesson_idx]
527
+ severity = perturbation.get("severity", 0.2) if isinstance(perturbation, dict) else 0.2
528
+ return self.evaluate_chain(
529
+ model,
530
+ device,
531
+ self.eval_batch_size,
532
+ num_facts_range=nf,
533
+ num_queries_range=nq,
534
+ rng=rng,
535
+ perturb=severity,
536
+ )
537
+
538
+ def field_log_header(self):
539
+ return value_cumsum_field_log_header()
540
+
541
+ def write_field_log(self, writer, file, step, lesson, eval_type, field_log):
542
+ write_value_cumsum_field_log(writer, file, step, lesson, eval_type, field_log)
@@ -0,0 +1,85 @@
1
+ """
2
+ file: data/common/digit_codec.py
3
+
4
+ Shared digit-based label encoding/decoding + loss/diversity, factored out of
5
+ data/graph_traversal/graph_traversal.py's LABEL_DIGITS/DIGIT_BASE/encode_label/
6
+ label_to_digits/digit_loss/prediction_diversity so every new modality dataset
7
+ (text/audio/video/multimodal) can reuse the exact same "predict N digits per
8
+ field, one-hot over DIGIT_BASE" convention instead of re-deriving it.
9
+ graph_traversal.py itself is left untouched (it predates this module and is a
10
+ validated Phase-2+ dataset not worth touching) - this is purely for NEW
11
+ dataset modules to import from.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import torch
17
+
18
+
19
+ class DigitCodec:
20
+ def __init__(self, num_digits: int = 3, digit_base: int = 10):
21
+ self.num_digits = num_digits
22
+ self.digit_base = digit_base
23
+ self.label_dim = num_digits * digit_base
24
+ self.label_range = digit_base**num_digits
25
+
26
+ def encode_label(self, label) -> torch.Tensor:
27
+ vec = torch.zeros(self.label_dim)
28
+ if label is None:
29
+ return vec
30
+ for pos, d in enumerate(self._digits(label)):
31
+ vec[pos * self.digit_base + d] = 1.0
32
+ return vec
33
+
34
+ def _digits(self, label: int):
35
+ s, out = label, []
36
+ for _ in range(self.num_digits):
37
+ out.append(s % self.digit_base)
38
+ s //= self.digit_base
39
+ return list(reversed(out))
40
+
41
+ def label_to_digit_targets(self, label: int):
42
+ return self._digits(label)
43
+
44
+ def decode_field(self, logits_flat: torch.Tensor) -> int:
45
+ """logits_flat: (num_digits * digit_base,) -> int label."""
46
+ logits = logits_flat.view(self.num_digits, self.digit_base)
47
+ val = 0
48
+ for d in logits.argmax(dim=-1).tolist():
49
+ val = val * self.digit_base + d
50
+ return val
51
+
52
+
53
+ def digit_field_loss(
54
+ output: torch.Tensor,
55
+ target_digits: torch.Tensor,
56
+ answer_mask: torch.Tensor,
57
+ num_fields: int,
58
+ digit_base: int,
59
+ ) -> torch.Tensor:
60
+ """Generalizes graph_traversal.digit_loss to an arbitrary field count
61
+ (graph uses num_fields=3 [src,edge,dst]; the chain-task family uses 2
62
+ [value,cumsum])."""
63
+ B, T, _ = output.shape
64
+ num_digits_total = target_digits.shape[-1]
65
+ logits = output.view(B, T, num_digits_total, digit_base)
66
+ log_probs = torch.log_softmax(logits, dim=-1)
67
+ gathered = torch.gather(log_probs, -1, target_digits.unsqueeze(-1)).squeeze(-1)
68
+ per_step_loss = -gathered.sum(dim=-1)
69
+ mask = answer_mask.float()
70
+ total = (per_step_loss * mask).sum()
71
+ denom = mask.sum().clamp(min=1.0)
72
+ return total / denom
73
+
74
+
75
+ def digit_field_diversity(
76
+ output: torch.Tensor, answer_mask: torch.Tensor, num_digits_total: int, digit_base: int
77
+ ) -> float:
78
+ mask = answer_mask.bool()
79
+ if mask.sum() == 0:
80
+ return 0.0
81
+ logits = output.view(*output.shape[:2], num_digits_total, digit_base)
82
+ preds = logits.argmax(dim=-1)[mask]
83
+ if preds.numel() == 0:
84
+ return 0.0
85
+ return sum(preds[:, d].unique().numel() for d in range(num_digits_total)) / num_digits_total