BLOCKER: stage1-r sweep uses ~2 GiB/member — full-history sweeps exhaust RAM+swap #138
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
A single multi-member stage1-r sweep over full M1 history consumes
~2 GiB per swept member, so a full-history sweep nearly exhausts RAM +
swap and gets SIGKILL-137'd. This blocks the edge-research milestone: the
walk-forward OOS phase runs a sweep per window across many windows, which
multiplies this footprint.
Evidence (measured, reproduced)
The original cross-symbol screen died with exit 137 mid-run. Reproduced the
exact failing command (EURUSD, 9-member grid, default rayon = 24 threads,
no throttle) under per-second memory sampling:
auraprocess.timeout) and had marginally more headroom; the original died at the edge.
RAYON_NUM_THREADS=2(fewer concurrent members) survives — whichlocalises the cost to per-member, materialised concurrently.
18.5 GiB / 9 members ≈ 2 GiB/member over ~5.5 M one-minute cycles
(EURUSD, ~15 y).
Likely cause (to confirm RED-first when tackled)
Hypothesis: the per-cycle data is retained in full rather than folded
incrementally —
summarize_rconsumes the whole dense PositionManagementrecord (14 columns × ~5.5 M cycles), and/or the recorders hold the full
equity / exposure / r_equity series per member, all resident at once across
the concurrently-running sweep members. ~2 GiB/member is far more than the
16 bytes/cycle that equity+exposure alone would need, so something is holding
the dense record (or all taps) for every cycle. This violates the C7 /
streaming-source spirit (the M1 source is lazy; the reduction is not).
Direction (not a prescription)
summarize_rand the metricrecorders incrementally so per-cycle rows are not retained
(O(1)/O(trades) memory, not O(cycles)).
the real fix and keeps full-history multi-member sweeps (and walk-forward)
viable.
Process
RED-first (
debug): pin the footprint with a test that fails on the currentretain-everything path (e.g. a bounded-memory assertion or a record-length
invariant), before the GREEN fix. Behaviour-preserving for the metric values
(goldens unchanged).
refs #137 (the research run this blocks)
Diagnosis update (RED-first) — the obvious fix is insufficient; the real fix is an engine-contract change
The RED-first diagnosis confirmed the root cause but also surfaced that the
dominant O(cycles) retainer is the unbounded
mpscchannel buffering duringthe run, not (only)
summarize_r's slice API.Confirmed
run_one(aura-cli/src/main.rs~1207-1240) wires each recorder to anunbounded
mpsc::Sender;Harness::run(aura-engine/src/harness.rs:371)is one synchronous call with no end-of-stream / finalize hook, so during the
run the channels accumulate all ~5.5M rows (dominant: the 14-column R-record
≈ 1.4 GB/member) before anything drains them. The post-run
rx.try_iter().collect()is a second O(cycles) copy, but the channel-full peakat end-of-run is the real ceiling.
Why "just stream the consumer" does NOT fix it
Folding
rx.try_iter()lazily instead of.collect()removes the second copy,but the channel already holds every row when
h.run()returns — so the peak(channel-full ≈ 1.4 GB/member) is essentially unchanged. A streaming
summarize_ralone makes the attached RED test pass while leaving the BLOCKERunfixed (green-but-wrong).
The fork: every O(1) fix touches the engine contract or invariant C1
Rows must be consumed during production; production is inside the synchronous
h.run(), so:but adds within-sim concurrency, in tension with invariant C1
("parallelism across sims, never within one"). Whether a sink-draining consumer
counts as within-sim parallelism is an invariant-interpretation call
(ledger-level), not a silent refactor.
finalizelifecycle on sinks + online-foldingreduction sink nodes — the reducer folds per
evalinto ownedO(trades)+O(1) state and emits a single summary row on
finalize;Harness::runcallsfinalizeafter the source loop;run_onethen drains onesummary, not 5.5M raw rows. Idiomatic streaming-dataflow flush, reuses the
existing channel, aligns with C8/C22, no within-sim threads. Touches the
Nodecontract (additive
finalize, default no-op) +Harness::run.Node: Any+ a harness node accessor; no threads; a less idiomatic back-channel.window-end force-close (a golden) needs the final row, which the recorder cannot
identify without a finalize hook.
Decision needed (engine direction)
This is no longer a localized bug-fix — it is a small engine execution-model
change (streaming sink reductions). Recommend M3 as a bounded design cycle
(specify → plan → implement, all metric goldens byte-identical). It also unblocks
#139 (walk-forward multiplies the per-member footprint). Alternatives: accept M2
and amend C1's interpretation in the ledger, or ship the
RAYON_NUM_THREADScapas a documented interim and defer the proper fix.
The RED test from this diagnosis
(
crates/aura-engine/tests/r_reduction_streaming.rs, unstaged) pins thesummarize_rslice-API sub-condition but not the channel-buffer site; thereal RED must exercise the recorder → channel → reduction path end-to-end.
Direction decided (user): M3
Minuted from chat — the user reviewed the fork and chose M3: an end-of-stream
finalizelifecycle on sinks plus online-folding reduction sink nodes, to beimplemented autonomously. Proceeding spec → plan → implement, all metric goldens
byte-identical.
Mechanism (settled):
Node::finalize(&mut self) {}— additive default no-op on the trait(
aura-core/src/node.rs); only a folding sink overrides it. Existing nodescompile untouched.
Harness::run(aura-engine/src/harness.rs) gains a post-loop epilogue: afterthe sources drain, call
finalize()on every node once, in topo order.aura-std/src/recorder.rs):fold per
evalinto O(trades)+O(1) owned state (noRc/RefCell— C7preserved, like today's
Recorder), emit a compacted summary onfinalize.The R path filters to CLOSED rows + the genuine final row, so
summarize_rstays unchanged and its goldens byte-identical; the pip equity/exposure path
is a true online fold (running peak/shortfall, Welford) that must replicate
summarize's arithmetic exactly.run_one(sweep) andrun_stage1_r(single) drain O(trades)/one summaryinstead of ~5.5M raw rows.
Determinism:
finalizeruns once after the deterministic source loop; nowithin-sim concurrency (C1 intact). This also unblocks #139 (walk-forward
multiplies the per-member footprint).
The debug-run RED test (
r_reduction_streaming.rs) under-pinned the bug(
summarize_rslice API only, not the channel buffer); it has been lifted outof the tree. The spec defines the stronger end-to-end RED — the
recorder → channel → reduction peak is O(trades), not O(cycles) — plus the
golden-preservation tests.
Fixed — M3 streaming sink reductions landed (cycle 0070)
The no-trace stage1-r sweep now folds its reductions online via a new end-of-stream
finalizelifecycle:SeriesReducerfolds the equity/exposure series to onesummary row each,
GatedRecorderretains only the gated (closed) trade rows + thegenuine final row. Per-member memory drops from O(cycles) (~2 GiB over ~5.5M
one-minute bars) to O(trades). The
--tracepath and the single run keep the rawrecorders as the byte-for-byte reference.
Verified:
cargo test --workspacegreen (42 suites);cargo clippy --workspace --all-targets -- -D warningsclean; stage1-r goldens byte-identical (a newCLI-seam test pins folded-no-trace == raw-
--tracemetrics); a counting-allocatorcapstone holds peak flat across a 770x cycle-count growth.
Landed in
cfe7bad(machinery: finalize hook + SeriesFold + folding sinks +integration tests) and
eb86534(CLI wiring). Cycle closed inffd18b9— thefinalizelifecycle is recorded in ledger C8. This unblocks #139 (walk-forward,which multiplies the per-member footprint). Related: #77 (cross-referenced there).