From 629ea58239e5507d7d9f4d0e70845a40d57bb645 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sat, 19 Sep 2026 18:14:02 +0200 Subject: [PATCH] RotationScaleMerge: a 32-byte resident ingest record, and smaller staging slices On the resident (GPU) path the ingest's narrow host record sits on top of the source reflections for the whole smoothing and partiality recompute, and on a long axis that moment is the run's memory high-water mark. Store its Miller indices in 16 bits and move the rocking-event flag into their padding: 40 -> 32 bytes a partial. Stage the device upload in slices of two million observations rather than eight, which takes the thirteen staging arrays from ~400 MB to ~100 MB. Neither changes a value. Measured on a 3600-frame long-axis rotation set (-N 6): VmHWM 13.79 -> 12.80 GiB on top of the previous commits (15.0-15.1 GiB before any of them); merged MTZ/HKL/CIF, P1 MTZ, per-image table and the report are byte-identical. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013nW6FNRP1bBJJ8pfHiByAT --- image_analysis/scale_merge/RotationScaleMerge.cpp | 9 +++++++-- image_analysis/scale_merge/RotationScaleMerge.h | 12 ++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/image_analysis/scale_merge/RotationScaleMerge.cpp b/image_analysis/scale_merge/RotationScaleMerge.cpp index 8b3e371aa..c251c7fb7 100644 --- a/image_analysis/scale_merge/RotationScaleMerge.cpp +++ b/image_analysis/scale_merge/RotationScaleMerge.cpp @@ -712,7 +712,9 @@ void RotationScaleMerge::BuildInRangeObservations(std::vector &keys) { if (!keep[src]) { ++src; continue; } ++src; ObsIngest &obs = partials_ingest[at]; - obs.h = r.h; obs.k = r.k; obs.l = r.l; + obs.h = static_cast(r.h); + obs.k = static_cast(r.k); + obs.l = static_cast(r.l); obs.frame = o; obs.image_number = r.image_number; obs.d = r.d; @@ -734,7 +736,10 @@ void RotationScaleMerge::BuildInRangeObservations(std::vector &keys) { gpu_->SetPartialsLayout(n_keep, n_frames, frame_start.data(), frame_count.data()); using F = RotationScaleMergeGPU::ObsField; - constexpr int STAGE_SLICE = 8 << 20; + // Two million observations a slice: the thirteen staging arrays are then under a hundred + // megabytes together, where eight million put four hundred on top of the ingest's peak, and a + // slice still spans tens of frames for the threads to share. + constexpr int STAGE_SLICE = 2 << 20; constexpr int NFLOAT = 11; int max_frame_obs = 0; for (int o = 0; o < n_frames; ++o) max_frame_obs = std::max(max_frame_obs, static_cast(frame_count[o])); diff --git a/image_analysis/scale_merge/RotationScaleMerge.h b/image_analysis/scale_merge/RotationScaleMerge.h index 533242ad3..81cf8164c 100644 --- a/image_analysis/scale_merge/RotationScaleMerge.h +++ b/image_analysis/scale_merge/RotationScaleMerge.h @@ -144,16 +144,20 @@ private: // observation dump): every immutable per-obs field is uploaded straight from the source // reflections inside BuildInRangeObservations, and the host keeps only what its own remaining // stages touch - the geometry smoothing (delta_phi, then the recomputed partiality), plus the - // rocking-event walk's inputs. Forty bytes against the Obs's eighty, and on that path the fat + // rocking-event walk's inputs. Thirty-two bytes against the Obs's eighty, and on that path the fat // record is never materialised at all: on a fine-sliced long axis it alone is many gigabytes, // held beside the source reflections it was copied from. struct ObsIngest { - int32_t h, k, l, frame; - float image_number, d, zeta, delta_phi, partiality; + // Sixteen bits are plenty for a Miller index - |h| <= a / d_min, in the hundreds even on the + // longest axis at atomic resolution - and on a long axis this array sets the ingest's memory + // high-water mark, so each index costs two bytes here, not four. + int16_t h, k, l; // The rocking-event usability test (RockingEventFrames), evaluated on the ingest-time // values - which is what that walk reads on the resident path, where the host corr is - // never refreshed (see partials_released). + // never refreshed (see partials_released). Beside the indices, where it fills their padding. uint8_t rock_ok; + int32_t frame; + float image_number, d, zeta, delta_phi, partiality; }; // The narrow per-observation record the ingest sort orders: the raw hkl the runs are cut on, the