Skip to content

Memoize live results in RAM to avoid duplicate computation#17

Open
vincenzoml wants to merge 3 commits into
mainfrom
feat/inrun-result-memo
Open

Memoize live results in RAM to avoid duplicate computation#17
vincenzoml wants to merge 3 commits into
mainfrom
feat/inrun-result-memo

Conversation

@vincenzoml

@vincenzoml vincenzoml commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Tracking issue: #14 — fixes defects §2/§3.1 (no in-RAM image memo → recompute under --no-cache) and §3.2 (repeated disk deserialize under --store-db). Does not address §3.3 (double serialize/write) or §4 (node-level parallelism), which stay open in #14.


Summary

First of two stacked PRs addressing the lazy-interpreter caching defects in #14. This one is the minimal correctness fix; the follow-up (#TBD) layers a bounded two-level cache on top.

Scope: avoid duplicate computation only. No parallelization.

The defect

MaterializationStore.put() replaced image-like values (image/bytes/ndarray/overlay) with a node-id placeholder in the in-memory record:

if vox_type in ("bytes", "overlay", "ndarray", "image"):
    stored_value = node_id      # <- real value dropped
else:
    stored_value = value

get() then had to round-trip to the backend to recover the value. With no backend (--no-cache) that round-trip returns None, so _evaluate_node_lazy's top-level cache_lookup misses and the node is recomputed on every demand. A node shared by N consumers (a diamond DAG over images — exactly the BraTS sweeps) recomputes its whole subtree N times. Scalars were unaffected because their real value was kept.

The fix

Keep the real value in the in-memory record for all types; return it directly from get(). Each node is now computed at most once per run regardless of backend. With a backend present, reuse also stops re-reading and re-deserializing the payload from disk on every demand.

Two small edits in storage.py (put, get), plus removal of the now-dead placeholder branch.

Trade-off

Live intermediates are now held in RAM for the duration of the run (previously image-like values were dropped). The stacked follow-up PR bounds this with an LRU memory tier backed by the disk tier — the "two-level cache" of #14.

Tests

  • New tests/unit/test_inrun_memo.py: an ndarray result is returned from get() with no backend (the exact case that used to recompute), plus a scalar round-trip.
  • test_async_persistence and the storage/execution unit tests still pass.
  • End-to-end CLI run under --no-cache verified.

(Pre-existing unrelated failures in test_default_primitives.pySequenceValue/.compute() API drift — fail identically on main.)

Refs #14

🤖 Generated with Claude Code

MaterializationStore.put() replaced image-like values (image/bytes/
ndarray/overlay) with a node-id placeholder in the in-memory record, and
get() then had to round-trip to the backend to recover them. With no
backend (--no-cache) that round-trip returned None, so a node shared by
multiple consumers was recomputed once per consumer -- a diamond DAG over
images recomputed the shared subtree every time it was demanded.

Keep the real value in the in-memory record for all types and return it
directly from get(). Each node is now computed at most once per run
regardless of the backend; with a backend, reuse no longer re-reads and
re-deserializes the payload from disk on every demand.

This trades memory for compute (live intermediates are now held for the
run); the follow-up two-level cache bounds that memory with an LRU tier.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant