correlation_refresh: O(N²) peak memory exceeds host RAM at current universe size (~24k symbols) #75
Labels
No labels
bug
documentation
duplicate
enhancement
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
gertjan/bodega#75
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
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
correlation_refreshpeak memory scales O(N²) with the post-filter universe sizeN, andNhas grown past the point where the nightly snapshot fits in host RAM. The task now either OOM-crashes (pre-swap) or swap-thrashes to a multi-hour runtime (post-swap). The mitigations already shipped (#73, #74) keep the box alive but do not address the footprint — they are stopgaps. This issue tracks the durable fix: reduce correlation's peak memory so it fits comfortably in RAM at the current and projected universe size.Impact / symptoms
universe_size = 23,529). Since late June the job has been failing most nights.task_runshistory forcorrelation_refresh:interrupted(container killed mid-run) on Jun 23/24/27/28, Jul 3/4.successon Jun 25 (4288s / 71 min — the run just fit in RAM).timeouton Jul 7 at 8865s (~2h28m) — the first swap-backed run: no longer crashes, but thrashes and exceeds the old 7200s limit.dmesg) of the analytics python process on Jun 22, 24, 25, 28, Jul 4, 5,anon-rss ≈ 39 GBeach. Notedocker inspect … OOMKilled=false: the cgroup 48 GB limit was never reached — the host ran out of RAM and the kernel killed the biggest process. This means a bad night can take down an unrelated host process (e.g.bodega-timescaledb), not just analytics.correlation's synchronous compute blocking the event loop also causedmomentum_refreshto misfire and skip (addressed in #74 viaasyncio.to_thread).Root cause
The universe is far larger than the docstring's illustrative "5 000-symbol" figure:
min_history_days=252in the last good snapshot (and climbing — active grew ~16% since mid-June).N = 23,529, a singleN×Nfloat32 matrix is23529² × 4 B ≈ **2.2 GB**. At today's ~27k it is ~2.9 GB (+31%).The pipeline holds ~10–15 simultaneous
N×Nmatrices, peaking during the lead-lag pass:src/analytics/correlation_discovery.py_corr_two_windows(L55) /compute_corr_matrix(L101):cosum,sqsum_x,sqsum_y,counts,corr— ~5 × N×N.find_lead_lag_pairs(L280): persistentbest_improvement,best_lag,best_corr,best_overlap,abs_syncplus per-lag temporaries fromcompute_lagged_corr(L247) —corr_k,counts_k,improvement_k,valid— allocated for each ofmax_lag=5iterations.hierarchical_clusters(L173): fullN×Ndistmatrix + scipylinkage, which is itself O(N²) memory (condensed distance ≈N(N-1)/2floats ≈ 1.1 GB at 23.5k) and O(N² log N) time.5 × 2.2 GB(base corr)+ ~8 × 2.2 GB(lead-lag f32/int variants, including transientcompute_lagged_corrtemporaries)≈ 30–40 GBpeak — matching the observed 39 GB. Because the cost is quadratic in N, the ~16% universe growth since mid-June inflated peak memory ~35% and pushed it over the host ceiling.Current mitigations (stopgaps — do not fix the footprint)
scores/rangingtimeouts. Removed the acute concurrent pile-up.asyncio.to_thread) so it no longer blocks the scheduler event loop; raisedcorrelation_refreshtimeout 7200 → 14400 s (4h);erp_refresh1800 → 2400 s.These keep the host stable and let the snapshot eventually complete, but correlation still runs swap-bound for hours and the footprint keeps growing with the universe.
Proposed fixes (in rough priority)
N×Ncorr matrix never materialises.top_k_neighborsonly needs, per symbol, its top-k by correlation. Compute correlations in row-blocks (e.g. 1–2k rows × N), extract top-k for those rows, discard the block. Peak drops from O(N²) → O(block × N). Results unchanged. Biggest win.N²is wasteful. Limitfind_lead_lag_pairsto each symbol's lag-0 top-k neighbours (≈25·Npairs vsN²). Massive memory + time reduction; negligible quality loss.hierarchical_clusters/ avoid the denseN×Ndist matrix — cluster within the correlation candidate graph, or use a memory-bounded clustering that doesn't require the full condensed distance array at 23.5k+.N. Simplest lever; atN = 12k,N×Nf32 ≈ 0.6 GB → ~8 GB peak. Changes coverage, so a product decision.out=args), and considerdel corr_kbetween lag iterations.Recommended approach
Do (1) + (2) — tiling the neighbour search and restricting lead-lag to candidate pairs together take peak memory from O(N²) to roughly O(block × N) while preserving full-universe coverage and results. Revisit (3)/(4) only if clustering remains the ceiling afterwards. Once peak is safely under RAM, revert the 4h
correlation_refreshtimeout to something tight (~90 min) and consider movingmomentumback to a slot adjacent to correlation.Acceptance criteria
correlation_refreshpeak RSS stays under a safe fraction of host RAM (target: < 20 GB) atN ≥ 30k, verified viadocker stats//procsampling on a full run.correlation_refreshtimeout reverted from 14400 s to the new headroom;momentumoverlap risk retired.References
src/scheduler/tasks/correlation_refresh.py:52src/analytics/correlation_discovery.py(_corr_two_windowsL55,compute_corr_matrixL101,top_k_neighborsL124,hierarchical_clustersL173,compute_lagged_corrL247,find_lead_lag_pairsL280)universe_size=23,529(2026-06-17); OOM kills indmesgJun 22–Jul 5 (~39 GB anon-rss);correlation_refreshtimeout 8865 s on 2026-07-07.The durable fix is merged and deployed in #76 (squash of two commits: tiled corr build + load hygiene, screened lead-lag scan + stopgap reverts).
Status against acceptance criteria:
mem_limitalso lowered 48g → 24g so a regression OOM-kills the container, not a host-picked victim.docker stats//procconfirmation on tonight's 01:00 UTC run.task_runs.momentum_refreshfrom 03:00 back adjacent — after a few clean runs.Leaving this open until tonight's run confirms the RSS/duration criteria; then it can close.
🤖 Posted by Claude Code — https://claude.ai/code/session_01JRnthdVhGGt5FeSQ8m3aUA
Validated on production, 2026-07-09 01:00 UTC nightly run — closing.
Acceptance criteria:
docker statssample on a future run can pin the exact figure; projections say ~10–11 GB).Also fixed en route (#78, discovered because this issue's fix made the nightly fast enough to reach persistence): correlation snapshot persistence had been failing silently since 2026-06-25 — destructive delete-then-insert save, event-loop starvation cancelling the Mongo handshake, task_runs recording success for failed runs, and a dependency-gate freshness window that deadlocked the 03:00/04:00 tasks. All landed in #78; peers/clusters/lead-lag are restored and stamped (55,239 docs, snapshot_ts 2026-07-09).
Remaining follow-ups tracked separately: momentum_refresh now surfaces a real failure (previously masked as green) and needs its own investigation; mongo healthcheck timeout bump still open as a suggestion.
🤖 Posted by Claude Code — https://claude.ai/code/session_01JRnthdVhGGt5FeSQ8m3aUA