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,526 @@
1
+ """
2
+ file: initium/inference/run_inference.py
3
+
4
+ CLI entrypoint (counterpart of core_training.py's __main__). Flow:
5
+ load checkpoint -> detect capabilities -> REFUSE if requested dataset type /
6
+ dims are unsupported -> build task -> rebuild model + load weights ->
7
+ (optional) set up caches -> run -> summarize/log.
8
+
9
+ Examples (from the project root):
10
+ python -m initium.inference.run_inference CKPT.pt
11
+ python -m initium.inference.run_inference CKPT.pt --inspect
12
+ python -m initium.inference.run_inference CKPT.pt --no-reset-experience --loop 50 --window 10 --compare-fresh
13
+ python -m initium.inference.run_inference CKPT.pt --dataset-link my_graph.csv --path-length 3 8
14
+ # caching (see initium/inference/cache/):
15
+ python -m initium.inference.run_inference CKPT.pt --shared-context --cache prefix
16
+ python -m initium.inference.run_inference CKPT.pt --shared-context --num-contexts 3 --cache prefix,result --cache-dir ./inference_cache
17
+ python -m initium.inference.run_inference CKPT.pt --no-reset-experience --loop 50 --cache result --cache-dir ./inference_cache
18
+ """
19
+
20
+ import os
21
+ import sys
22
+
23
+ _ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
24
+ if _ROOT not in sys.path:
25
+ sys.path.insert(0, _ROOT)
26
+
27
+ import argparse
28
+ import json
29
+ import random
30
+ import time
31
+
32
+ import torch
33
+
34
+ from initium.inference.cache.cache_config import (
35
+ DEFAULT_CACHE,
36
+ DEFAULT_CACHE_DISK_MB,
37
+ DEFAULT_CACHE_RAM_MB,
38
+ DEFAULT_VERIFY_HITS,
39
+ )
40
+ from initium.inference.cache.cache_registry import parse_cache_spec, setup_caches
41
+ from initium.inference.cache.cached_engine import CachedInferenceEngine
42
+ from initium.inference.capabilities import (
43
+ IncompatibleModelError,
44
+ detect_capabilities,
45
+ require_dims_match,
46
+ require_type_supported,
47
+ )
48
+ from initium.inference.checkpoint_io import describe_checkpoint, load_checkpoint
49
+ from initium.inference.inference_config import (
50
+ DEFAULT_DATASET_LINK,
51
+ DEFAULT_DATASET_TYPE,
52
+ DEFAULT_NUM_EPISODES,
53
+ DEFAULT_SEED,
54
+ DEFAULT_VERBOSE_N,
55
+ DEFAULT_WINDOW,
56
+ INFERENCE_LOG_DIR,
57
+ PROGRESS_EVERY,
58
+ )
59
+ from initium.inference.model_loader import load_model
60
+ from initium.inference.tasks.task_registry import get_task
61
+ from initium.memory_manipulation.nvrtc_compat import patch_prod_jiterator
62
+
63
+ patch_prod_jiterator() # environment workaround - see that module's docstring
64
+ from initium.inference import run_logging as rl
65
+ from initium.inference.engine import InferenceEngine
66
+ from initium.inference.metrics import adaptation_trend, aggregate, windowed
67
+
68
+
69
+ def parse_args(argv=None):
70
+ p = argparse.ArgumentParser(
71
+ description="Checkpoint-driven inference. Refuses to run if the requested dataset "
72
+ "type is not supported by the checkpoint."
73
+ )
74
+ p.add_argument(
75
+ "checkpoint", type=str, help="path to a core_training checkpoint (.pt), periodic or final"
76
+ )
77
+ p.add_argument(
78
+ "--dataset-type",
79
+ type=str,
80
+ default=DEFAULT_DATASET_TYPE,
81
+ help="task type to run (graph | text | audio | video). Must be supported by the checkpoint.",
82
+ )
83
+ p.add_argument(
84
+ "--dataset-link",
85
+ type=str,
86
+ default=DEFAULT_DATASET_LINK,
87
+ help="test data location. Omit for the built-in London Underground graph; "
88
+ "for graph, a .csv/.tsv/.txt/.json edge file (src,dst,line).",
89
+ )
90
+ p.add_argument(
91
+ "--reset-experience",
92
+ action=argparse.BooleanOptionalAction,
93
+ default=True,
94
+ help="--reset-experience (default): every episode starts fresh (independent test). "
95
+ "--no-reset-experience: memory + controller state persist across episodes.",
96
+ )
97
+ p.add_argument(
98
+ "--loop",
99
+ type=int,
100
+ default=None,
101
+ help="persistent mode only: number of consecutive episodes with state carried over.",
102
+ )
103
+ p.add_argument(
104
+ "--num-episodes",
105
+ type=int,
106
+ default=None,
107
+ help=f"number of episodes (default {DEFAULT_NUM_EPISODES}); in persistent mode "
108
+ "use --loop instead.",
109
+ )
110
+ p.add_argument(
111
+ "--window",
112
+ type=int,
113
+ default=DEFAULT_WINDOW,
114
+ help="persistent mode: episodes per window in the adaptation table.",
115
+ )
116
+ p.add_argument(
117
+ "--compare-fresh",
118
+ action="store_true",
119
+ help="persistent mode: also replay the SAME episodes with reset each episode and report the delta.",
120
+ )
121
+ p.add_argument(
122
+ "--path-length",
123
+ type=int,
124
+ nargs=2,
125
+ metavar=("MIN", "MAX"),
126
+ default=None,
127
+ help="graph task: walk length range (default: the training-time OOD range).",
128
+ )
129
+ p.add_argument(
130
+ "--perturbation-severity",
131
+ type=float,
132
+ default=None,
133
+ help="Robustness probe (Layer D): if set, episodes are built with "
134
+ "perturbation={'severity': X} - e.g. text/audio/video token-corruption / "
135
+ "additive-noise / frame-dropping severity in [0,1]. Graph task ignores this "
136
+ "(no perturbation hook wired for it yet).",
137
+ )
138
+ p.add_argument(
139
+ "--shared-context",
140
+ action="store_true",
141
+ help="graph task: every episode = ONE fixed edge listing (static prefix) + a fresh "
142
+ "random query. Required for the prefix cache to have anything to reuse.",
143
+ )
144
+ p.add_argument(
145
+ "--num-contexts",
146
+ type=int,
147
+ default=1,
148
+ help="with --shared-context: number of distinct fixed edge listings, used round-robin.",
149
+ )
150
+ p.add_argument("--seed", type=int, default=DEFAULT_SEED)
151
+ p.add_argument(
152
+ "--device", type=str, default=None, help="default: cuda:0 if available, else cpu"
153
+ )
154
+ p.add_argument(
155
+ "--deterministic-write",
156
+ action="store_true",
157
+ help="disable write-vector sampling (use mu only), even if the model was trained with beta>0.",
158
+ )
159
+ p.add_argument(
160
+ "--ablate-memory",
161
+ action="store_true",
162
+ help="skip memory read+write (functional-usage check).",
163
+ )
164
+ p.add_argument(
165
+ "--ablate-combiner-stage",
166
+ type=str,
167
+ nargs="+",
168
+ default=None,
169
+ help="split-graph checkpoints with a hybrid controller combiner only "
170
+ "(--split-graph-combiner-mode controller, variant like 'mamba+cfc'): "
171
+ "bypass one or more stages by name (as listed by --inspect, e.g. 'cfc') "
172
+ "or 0-based index, to measure that stage's contribution on real held-out "
173
+ "episodes. Compare against a run with this flag omitted for the delta.",
174
+ )
175
+ p.add_argument(
176
+ "--report-moe-stats",
177
+ action="store_true",
178
+ help="after the run, print each installed MoE sublayer's routing "
179
+ "diagnostics (CV(load), CV(importance), max load fraction) from its "
180
+ "last forward call -- confirms MoE is actually routing/balancing on "
181
+ "this data, independent of task accuracy.",
182
+ )
183
+ p.add_argument(
184
+ "--verbose-n",
185
+ type=int,
186
+ default=DEFAULT_VERBOSE_N,
187
+ help="print per-item predictions for the first N episodes.",
188
+ )
189
+ p.add_argument("--log-dir", type=str, default=INFERENCE_LOG_DIR)
190
+ p.add_argument("--run-id-suffix", type=str, default=None)
191
+ p.add_argument("--no-log", action="store_true", help="do not write CSV/JSON files.")
192
+ p.add_argument(
193
+ "--model-config-override",
194
+ type=str,
195
+ default=None,
196
+ help="JSON dict merged into the checkpoint model_config, e.g. "
197
+ "'{\"split_graph_num_blocks\": 3}' (for legacy checkpoints).",
198
+ )
199
+ p.add_argument(
200
+ "--inspect",
201
+ action="store_true",
202
+ help="print the checkpoint's config + capabilities and exit.",
203
+ )
204
+
205
+ g = p.add_argument_group("caching (initium/inference/cache/)")
206
+ g.add_argument(
207
+ "--cache",
208
+ type=str,
209
+ default=DEFAULT_CACHE,
210
+ help="off | prefix | result | prefix,result | all. 'prefix' = snapshot the model state "
211
+ "after a static prefix and resume queries from it (needs --shared-context and "
212
+ "reset-experience). 'result' = memoize whole-episode outputs (deterministic models).",
213
+ )
214
+ g.add_argument("--no-cache", action="store_true", help="force caching off (overrides --cache).")
215
+ g.add_argument(
216
+ "--cache-dir",
217
+ type=str,
218
+ default=None,
219
+ help="enable the disk tier here, so caches survive across CLI invocations "
220
+ "(one sub-folder per model fingerprint).",
221
+ )
222
+ g.add_argument(
223
+ "--cache-ram-mb", type=float, default=DEFAULT_CACHE_RAM_MB, help="RAM tier size cap."
224
+ )
225
+ g.add_argument(
226
+ "--cache-disk-mb", type=float, default=DEFAULT_CACHE_DISK_MB, help="disk tier size cap."
227
+ )
228
+ g.add_argument(
229
+ "--cache-verify",
230
+ type=int,
231
+ default=DEFAULT_VERIFY_HITS,
232
+ help="recompute the first N cache hits without the cache and compare "
233
+ "(0 = off). On mismatch the cache is disabled for the rest of the run.",
234
+ )
235
+ g.add_argument(
236
+ "--cache-clear", action="store_true", help="wipe this model's disk cache before running."
237
+ )
238
+ g.add_argument(
239
+ "--cache-allow-stochastic",
240
+ action="store_true",
241
+ help="allow caching even if the model samples its write vectors (results are then "
242
+ "one random draw reused; verification is skipped).",
243
+ )
244
+ args = p.parse_args(argv)
245
+
246
+ if args.loop is not None and args.reset_experience:
247
+ p.error("--loop requires --no-reset-experience (state must persist across episodes).")
248
+ if args.compare_fresh and args.reset_experience:
249
+ p.error("--compare-fresh requires --no-reset-experience.")
250
+ if args.loop is not None and args.num_episodes is not None:
251
+ p.error("use either --loop (persistent) or --num-episodes, not both.")
252
+ for name in ("loop", "num_episodes"):
253
+ v = getattr(args, name)
254
+ if v is not None and v < 1:
255
+ p.error(f"--{name.replace('_', '-')} must be >= 1")
256
+ if args.num_contexts > 1 and not args.shared_context:
257
+ p.error("--num-contexts requires --shared-context")
258
+ try:
259
+ parse_cache_spec(args.cache)
260
+ except ValueError as e:
261
+ p.error(str(e))
262
+ if args.model_config_override is not None:
263
+ try:
264
+ args.model_config_override = json.loads(args.model_config_override)
265
+ assert isinstance(args.model_config_override, dict)
266
+ except (json.JSONDecodeError, AssertionError):
267
+ p.error("--model-config-override must be a JSON object")
268
+ return args
269
+
270
+
271
+ def main(argv=None) -> int:
272
+ args = parse_args(argv)
273
+ device = torch.device(args.device or ("cuda:0" if torch.cuda.is_available() else "cpu"))
274
+ print(f"[inference] device: {device}")
275
+
276
+ ckpt = load_checkpoint(args.checkpoint, args.model_config_override)
277
+ caps = detect_capabilities(ckpt)
278
+ print(f"[inference] checkpoint: {args.checkpoint}\n{describe_checkpoint(ckpt)}")
279
+ print(f"[inference] capabilities: {caps.describe()}")
280
+ if args.inspect:
281
+ return 0
282
+
283
+ # ---- compatibility gates (cheap, BEFORE building anything) --------------
284
+ try:
285
+ canon = require_type_supported(caps, args.dataset_type, args.checkpoint)
286
+ task = get_task(
287
+ canon,
288
+ args.dataset_link,
289
+ path_length_range=args.path_length,
290
+ shared_context=args.shared_context,
291
+ num_contexts=args.num_contexts,
292
+ )
293
+ require_dims_match(caps, task, args.checkpoint)
294
+ except (IncompatibleModelError, NotImplementedError, ValueError, FileNotFoundError) as e:
295
+ raise SystemExit(f"[inference] ABORT: {e}") from e
296
+ print(f"[inference] task: {task.describe()}")
297
+
298
+ torch.manual_seed(args.seed)
299
+ loaded = load_model(ckpt, device, deterministic_write=args.deterministic_write)
300
+ print(f"[inference] model loaded (step {loaded.step}, sampled_writes={loaded.sampled_writes})")
301
+ if loaded.combiner_stage_kinds:
302
+ print(f"[inference] combiner stages: {list(enumerate(loaded.combiner_stage_kinds))}")
303
+
304
+ combiner_skip_stages = None
305
+ if args.ablate_combiner_stage:
306
+ kinds = loaded.combiner_stage_kinds
307
+ if not kinds:
308
+ raise SystemExit(
309
+ "[inference] ABORT: --ablate-combiner-stage requires a split-graph "
310
+ "checkpoint with --split-graph-combiner-mode controller; this "
311
+ "checkpoint has no combiner stages to ablate."
312
+ )
313
+ idx = set()
314
+ for tok in args.ablate_combiner_stage:
315
+ if tok.isdigit() and 0 <= int(tok) < len(kinds):
316
+ idx.add(int(tok))
317
+ elif tok in kinds:
318
+ idx.add(kinds.index(tok))
319
+ else:
320
+ raise SystemExit(
321
+ f"[inference] ABORT: unknown combiner stage {tok!r}; "
322
+ f"this checkpoint's stages are {list(enumerate(kinds))}"
323
+ )
324
+ combiner_skip_stages = frozenset(idx)
325
+ print(
326
+ f"[inference] ablating combiner stage(s) "
327
+ f"{[(i, kinds[i]) for i in sorted(combiner_skip_stages)]}"
328
+ )
329
+
330
+ reset = args.reset_experience
331
+ if reset:
332
+ n = args.num_episodes if args.num_episodes is not None else DEFAULT_NUM_EPISODES
333
+ else:
334
+ n = (
335
+ args.loop
336
+ if args.loop is not None
337
+ else (args.num_episodes if args.num_episodes is not None else DEFAULT_NUM_EPISODES)
338
+ )
339
+
340
+ rng = random.Random(args.seed)
341
+ perturbation = (
342
+ {"severity": args.perturbation_severity} if args.perturbation_severity is not None else None
343
+ )
344
+ episodes = task.build_episodes(
345
+ n, rng, perturbation=perturbation
346
+ ) # materialized -> identical episodes for --compare-fresh
347
+
348
+ # ---- caches ---------------------------------------------------------------
349
+ caches, cache_notes, cache_fp = [], [], None
350
+ if not args.no_cache:
351
+ caches, cache_notes, cache_fp = setup_caches(
352
+ args.cache,
353
+ ckpt=ckpt,
354
+ device=device,
355
+ sampled_writes=loaded.sampled_writes,
356
+ deterministic_write=args.deterministic_write,
357
+ ablate_memory=args.ablate_memory,
358
+ combiner_skip_stages=combiner_skip_stages,
359
+ cache_dir=args.cache_dir,
360
+ ram_mb=args.cache_ram_mb,
361
+ disk_mb=args.cache_disk_mb,
362
+ clear=args.cache_clear,
363
+ allow_stochastic=args.cache_allow_stochastic,
364
+ )
365
+ for note in cache_notes:
366
+ print(f"[inference] cache: {note}")
367
+ if caches:
368
+ assert cache_fp is not None
369
+ print(
370
+ f"[inference] cache: {', '.join(c.name for c in caches)} | model fingerprint "
371
+ f"{cache_fp[:12]} | RAM {args.cache_ram_mb:.0f} MB"
372
+ + (f" | disk {args.cache_dir}" if args.cache_dir else "")
373
+ )
374
+ if any(c.wants_boundaries for c in caches) and not any(
375
+ ep.cache_boundaries for ep in episodes
376
+ ):
377
+ cache_notes.append(
378
+ "prefix cache is on but no episode declares a static prefix "
379
+ "(use --shared-context); it will not hit."
380
+ )
381
+ print(f"[inference] cache: {cache_notes[-1]}")
382
+
383
+ engine: InferenceEngine | CachedInferenceEngine
384
+ if caches:
385
+ engine = CachedInferenceEngine(
386
+ loaded.rnn,
387
+ loaded.output_proj,
388
+ task,
389
+ device,
390
+ caches,
391
+ ablate_memory=args.ablate_memory,
392
+ combiner_skip_stages=combiner_skip_stages,
393
+ verify_hits=(0 if loaded.sampled_writes else args.cache_verify),
394
+ )
395
+ else:
396
+ engine = InferenceEngine(
397
+ loaded.rnn,
398
+ loaded.output_proj,
399
+ task,
400
+ device,
401
+ ablate_memory=args.ablate_memory,
402
+ combiner_skip_stages=combiner_skip_stages,
403
+ )
404
+
405
+ t0 = time.time()
406
+ print(f"[inference] running {n} episodes | reset_experience={reset}")
407
+ results = engine.run(
408
+ episodes, reset_experience=reset, verbose_n=args.verbose_n, progress_every=PROGRESS_EVERY
409
+ )
410
+ scores = [r.score for r in results]
411
+
412
+ tags = [
413
+ args.run_id_suffix,
414
+ ("cache-" + "+".join(c.name for c in caches)) if caches else None,
415
+ "sharedctx" if args.shared_context else None,
416
+ ]
417
+ suffix = "_".join(t for t in tags if t) or None
418
+
419
+ summary = {
420
+ "run_id": rl.make_run_id(args.checkpoint, task.name, reset, n, suffix),
421
+ "checkpoint": args.checkpoint,
422
+ "checkpoint_step": loaded.step,
423
+ "controller": caps.controller_type,
424
+ "supported_types": caps.supported_types,
425
+ "capabilities_source": caps.source,
426
+ "dataset_type": canon,
427
+ "dataset_link": args.dataset_link,
428
+ "task": task.describe(),
429
+ "mode": "fresh" if reset else "persistent",
430
+ "reset_experience": reset,
431
+ "seed": args.seed,
432
+ "sampled_writes": loaded.sampled_writes,
433
+ "ablate_memory": args.ablate_memory,
434
+ "result": aggregate(scores),
435
+ "mean_episode_ms": sum(r.elapsed_ms for r in results) / max(len(results), 1),
436
+ }
437
+ if caches:
438
+ assert isinstance(engine, CachedInferenceEngine)
439
+ summary["cache"] = {
440
+ "requested": args.cache,
441
+ "active": [c.name for c in caches],
442
+ "model_fingerprint": cache_fp,
443
+ "disk_dir": args.cache_dir,
444
+ "report": engine.cache_report(reset=True),
445
+ "verify": dict(engine.verify),
446
+ "store": caches[0].store.info(),
447
+ "notes": cache_notes + engine.notes,
448
+ }
449
+ elif cache_notes:
450
+ summary["cache"] = {
451
+ "requested": args.cache,
452
+ "active": [],
453
+ "notes": cache_notes,
454
+ "report": {},
455
+ "disk_dir": None,
456
+ }
457
+ if not reset:
458
+ summary["windows"] = windowed(scores, args.window)
459
+ summary["adaptation"] = adaptation_trend(scores, args.window)
460
+ if args.compare_fresh:
461
+ print("[inference] replaying the same episodes with reset_experience=True (baseline)")
462
+ fresh = engine.run(episodes, reset_experience=True, progress_every=0, label=":fresh")
463
+ summary["fresh_baseline"] = aggregate([r.score for r in fresh])
464
+ summary["persistent_minus_fresh_item_acc"] = (
465
+ summary["result"]["item_acc"] - summary["fresh_baseline"]["item_acc"]
466
+ )
467
+ if caches:
468
+ assert isinstance(engine, CachedInferenceEngine)
469
+ summary["cache"]["baseline_report"] = engine.cache_report(reset=True)
470
+ summary["cache"]["notes"] = cache_notes + engine.notes
471
+ summary["elapsed_sec"] = time.time() - t0
472
+
473
+ if not args.no_log:
474
+ os.makedirs(args.log_dir, exist_ok=True)
475
+ base = os.path.join(args.log_dir, summary["run_id"])
476
+ files = [f"{base}_episodes.csv", f"{base}_summary.json"]
477
+ rl.write_episode_csv(files[0], results)
478
+ if not reset:
479
+ files.insert(1, f"{base}_windows.csv")
480
+ rl.write_window_csv(files[1], summary["windows"])
481
+ summary["log_files"] = files
482
+ rl.write_summary_json(files[-1], summary)
483
+
484
+ if args.report_moe_stats:
485
+ moe_layers = getattr(loaded.rnn, "moe_layers", None)
486
+ if not moe_layers:
487
+ print("[inference] --report-moe-stats: this checkpoint has no MoE layers installed.")
488
+ else:
489
+ print(
490
+ f"[inference] MoE routing diagnostics ({len(moe_layers)} sublayer(s), "
491
+ f"from the LAST forward call only):"
492
+ )
493
+ for i, layer in enumerate(moe_layers):
494
+ diag = layer.last_diagnostics()
495
+ if not diag:
496
+ print(f" [{i}] no diagnostics recorded")
497
+ continue
498
+ tag = (
499
+ " (multi-source combiner)" if hasattr(layer, "last_source_diagnostics") else ""
500
+ )
501
+ print(
502
+ f" [{i}]{tag} cv_load={diag['cv_load']:.4f} "
503
+ f"cv_importance={diag['cv_importance']:.4f} "
504
+ f"max_load_frac={diag['max_load_frac']:.4f}"
505
+ )
506
+ src_diag = (
507
+ layer.last_source_diagnostics()
508
+ if hasattr(layer, "last_source_diagnostics")
509
+ else None
510
+ )
511
+ if src_diag:
512
+ for s_idx, s in enumerate(src_diag):
513
+ frac_str = ", ".join(
514
+ f"e{e}:{f:.2f}" for e, f in enumerate(s["expert_frac"])
515
+ )
516
+ print(
517
+ f" source {s_idx}: top_expert={s['top_expert']} "
518
+ f"({s['top_expert_frac']:.2f}) | dist [{frac_str}]"
519
+ )
520
+
521
+ rl.print_summary(summary)
522
+ return 0
523
+
524
+
525
+ if __name__ == "__main__":
526
+ sys.exit(main())
@@ -0,0 +1,173 @@
1
+ """
2
+ file: inference/run_logging.py
3
+
4
+ Output for an inference run: per-episode CSV, window CSV (persistent mode),
5
+ summary JSON, and the console summary. (Named run_logging to avoid shadowing
6
+ the stdlib `logging` module.)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import csv
12
+ import json
13
+ import os
14
+ import re
15
+
16
+ from initium.inference.metrics import EpisodeResult
17
+
18
+
19
+ def make_run_id(
20
+ ckpt_path: str,
21
+ task_name: str,
22
+ reset_experience: bool,
23
+ n_episodes: int,
24
+ suffix: str | None = None,
25
+ ) -> str:
26
+ stem = os.path.splitext(os.path.basename(ckpt_path))[0]
27
+ mode = "fresh" if reset_experience else f"persist{n_episodes}"
28
+ rid = f"infer_{stem}_{task_name}_{mode}" + (f"_{suffix}" if suffix else "")
29
+ return re.sub(r"[^A-Za-z0-9_.-]", "_", rid)
30
+
31
+
32
+ def write_episode_csv(path: str, results: list[EpisodeResult]) -> None:
33
+ names = sorted({n for r in results for n in r.score.fields})
34
+ header = [
35
+ "episode",
36
+ "reset_experience",
37
+ "n_items",
38
+ "n_correct",
39
+ "item_acc",
40
+ "perfect",
41
+ "group",
42
+ "elapsed_ms",
43
+ "cache_event",
44
+ ]
45
+ for n in names:
46
+ header += [f"{n}_correct", f"{n}_total"]
47
+ with open(path, "w", newline="") as f:
48
+ w = csv.writer(f)
49
+ w.writerow(header)
50
+ for r in results:
51
+ s = r.score
52
+ row = [
53
+ r.index,
54
+ int(r.reset_experience),
55
+ s.n_items,
56
+ s.n_correct,
57
+ s.item_acc,
58
+ int(s.perfect),
59
+ s.group,
60
+ r.elapsed_ms,
61
+ r.cache_event,
62
+ ]
63
+ for n in names:
64
+ c, t = s.fields.get(n, ("", ""))
65
+ row += [c, t]
66
+ w.writerow(row)
67
+
68
+
69
+ def write_window_csv(path: str, windows: list[dict]) -> None:
70
+ names = sorted({n for w in windows for n in w["field_acc"]})
71
+ with open(path, "w", newline="") as f:
72
+ wr = csv.writer(f)
73
+ wr.writerow(
74
+ [
75
+ "window_index",
76
+ "start_episode",
77
+ "end_episode",
78
+ "n_episodes",
79
+ "item_acc",
80
+ "perfect_frac",
81
+ ]
82
+ + [f"{n}_acc" for n in names]
83
+ )
84
+ for w in windows:
85
+ wr.writerow(
86
+ [
87
+ w["window_index"],
88
+ w["start_episode"],
89
+ w["end_episode"],
90
+ w["n_episodes"],
91
+ w["item_acc"],
92
+ w["perfect_frac"],
93
+ ]
94
+ + [w["field_acc"].get(n, "") for n in names]
95
+ )
96
+
97
+
98
+ def write_summary_json(path: str, summary: dict) -> None:
99
+ with open(path, "w") as f:
100
+ json.dump(summary, f, indent=2, default=str)
101
+
102
+
103
+ def print_summary(s: dict) -> None:
104
+ r = s["result"]
105
+ print("\n===== Inference summary =====")
106
+ print(f"run_id : {s['run_id']}")
107
+ print(
108
+ f"checkpoint : {s['checkpoint']} (step {s['checkpoint_step']}, controller {s['controller']})"
109
+ )
110
+ print(f"task : {s['task']}")
111
+ print(
112
+ f"mode : {s['mode']} (reset_experience={s['reset_experience']}) | "
113
+ f"{r['n_episodes']} episodes | seed {s['seed']} | "
114
+ f"sampled_writes={s['sampled_writes']} | ablate_memory={s['ablate_memory']}"
115
+ )
116
+ print(f"overall : item acc {r['item_acc']:.2f}% | perfect {r['perfect_frac']:.2f}%")
117
+ if r["by_group"]:
118
+ print(
119
+ "by group : "
120
+ + " | ".join(
121
+ f"{g}: {v['item_acc']:.1f}% (n={v['n_episodes']})" for g, v in r["by_group"].items()
122
+ )
123
+ )
124
+ if r["field_acc"]:
125
+ print("fields : " + " | ".join(f"{n} {a:.1f}%" for n, a in r["field_acc"].items()))
126
+ if s.get("windows"):
127
+ print(
128
+ "windows : "
129
+ + " | ".join(
130
+ f"[{w['start_episode']}-{w['end_episode']}] {w['item_acc']:.1f}%"
131
+ for w in s["windows"]
132
+ )
133
+ )
134
+ a = s.get("adaptation")
135
+ if a:
136
+ print(
137
+ f"adaptation : first {a['window']} eps {a['first_window_item_acc']:.2f}% -> "
138
+ f"last {a['window']} eps {a['last_window_item_acc']:.2f}% "
139
+ f"(delta {a['delta_item_acc']:+.2f}) | slope {a['slope_acc_per_episode']:+.4f} acc-pts/episode"
140
+ )
141
+ fb = s.get("fresh_baseline")
142
+ if fb:
143
+ print(
144
+ f"fresh base : item acc {fb['item_acc']:.2f}% | perfect {fb['perfect_frac']:.2f}% "
145
+ f"(same episodes, reset each) -> persistent minus fresh: "
146
+ f"{s['persistent_minus_fresh_item_acc']:+.2f} acc-pts"
147
+ )
148
+ c = s.get("cache")
149
+ if c:
150
+ print(
151
+ f"cache : requested={c['requested']} | active={c['active'] or 'none'}"
152
+ + (f" | disk={c['disk_dir']}" if c.get("disk_dir") else "")
153
+ )
154
+ for label, key in (("run", "report"), ("fresh baseline", "baseline_report")):
155
+ for name, r in (c.get(key) or {}).items():
156
+ print(
157
+ f" [{label}] {name}: {r['hits']}/{r['lookups']} hits ({r['hit_rate']:.0f}%) "
158
+ f"| ram {r['ram_hits']} disk {r['disk_hits']} | stores {r['stores']} "
159
+ f"| est. compute saved ~{r['est_saved_ms']:.0f} ms"
160
+ )
161
+ v = c.get("verify")
162
+ if v and v["checked"]:
163
+ print(
164
+ f" verify: {v['checked']} hit(s) recomputed uncached | max |d output| "
165
+ f"{v['max_output_diff']:.2e} | max |d state| {v['max_state_diff']:.2e} "
166
+ f"| failures {v['failures']}"
167
+ )
168
+ for note in c.get("notes", []):
169
+ print(f" note: {note}")
170
+ print(f"speed : mean {s['mean_episode_ms']:.1f} ms/episode")
171
+ print(f"elapsed : {s['elapsed_sec']:.1f}s")
172
+ if s.get("log_files"):
173
+ print("logs : " + ", ".join(s["log_files"]))