diff --git a/image_analysis/scale_merge/RotationScaleMerge.cpp b/image_analysis/scale_merge/RotationScaleMerge.cpp index dcec6a2ba..e89f95803 100644 --- a/image_analysis/scale_merge/RotationScaleMerge.cpp +++ b/image_analysis/scale_merge/RotationScaleMerge.cpp @@ -706,6 +706,18 @@ void RotationScaleMerge::StageObservations() { } #endif +size_t RotationScaleMerge::DeviceBytes() const { + // What each costs at the peak, measured: an observation's arrays and its group CSR; a full's arrays + // with the surface terms, or with the outlier test's and the error model's samples; an hkl's raw run + // and its ASU group's sums. The events are counted on the device path's ingest; elsewhere every + // observation may be an event of its own. + constexpr size_t BYTES_PER_OBS = 80, BYTES_PER_EVENT = 260, BYTES_PER_HKL = 200; + const auto events = static_cast(resident_ingest ? rocking_events_at_ingest : n_partials_obs); + const size_t bytes = BYTES_PER_OBS * static_cast(n_partials_obs) + BYTES_PER_EVENT * events + + BYTES_PER_HKL * rawrun_start.size(); + return bytes + bytes / 5; +} + std::unique_ptr RotationScaleMerge::CloneIngest(int device, Logger &log) const { TimingMark mark(*logger, "rsm ingest copy"); std::unique_ptr c(new RotationScaleMerge(*this)); diff --git a/image_analysis/scale_merge/RotationScaleMerge.h b/image_analysis/scale_merge/RotationScaleMerge.h index ae68d4560..9d1ac125e 100644 --- a/image_analysis/scale_merge/RotationScaleMerge.h +++ b/image_analysis/scale_merge/RotationScaleMerge.h @@ -162,15 +162,14 @@ public: // The copy does not write its per-frame scales back (WriteBackPerFrameScale does). [[nodiscard]] std::unique_ptr CloneIngest(int device, Logger &log) const; - // The partial observations the ingest kept. - [[nodiscard]] int64_t ObservationCount() const { return n_partials_obs; } - - // The card memory an engine may hold during a search merge, per observation the ingest kept: what - // a caller goes by when it decides whether a second engine fits beside one already on the card. - // Between what two engines merging side by side were measured at, ~150 bytes per observation each - // (20.6M observations, 4.2M fulls), and the layout's worst case of ~280 (71 bytes per observation, - // 83 per full, ~130 per ASU group, with a full and a group for every observation). - static constexpr size_t DEVICE_BYTES_PER_OBSERVATION = 256; + // The card memory an engine may hold during a merge, the device helpers its merge calls included: + // what a caller goes by when it decides whether a second engine fits beside one already on the + // card. Call after IngestHost(). It counts what the ingest knows - the observations, the rocking + // events (the combine makes at most one full of each) and the distinct hkl (at most one ASU group + // each) - at what each costs at the engine's peak (the correction-surface terms, or the merge with + // its outlier test), plus a fifth. Measured on 11 data sets of 13-123 M observations, every + // engine's peak was at most 81% of this. + [[nodiscard]] size_t DeviceBytes() const; // Merge only the first n frames of the outcomes (by default all of them). Set before Ingest(). void SetFrameLimit(int n) { frame_limit = n; } diff --git a/rugnux/RugnuxScaleMerge.cpp b/rugnux/RugnuxScaleMerge.cpp index 3e3a63028..fb4d8f8a9 100644 --- a/rugnux/RugnuxScaleMerge.cpp +++ b/rugnux/RugnuxScaleMerge.cpp @@ -1033,15 +1033,17 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) { // and the speculative passes). Otherwise on the last card, beside the engine already there - // this one, or the guard's, which merges at most as many observations - and only where two // engines of this size fit the card's memory, judged on its total, not on what happens to be - // free: the answer must not depend on the moment. On one card that is the small and middling - // data sets; the large ones merge their arms one after the other. + // free: the answer must not depend on the moment. What else stays on the card meanwhile - the + // context, the run's indexers - was measured at up to 2.3 GB; a speculative pass running beside + // holds more, but a merge that does not fit beside it waits for it to end. On a 16 GB card that + // is the data sets up to about 50 M observations at high multiplicity; the large ones merge + // their arms one after the other. const int32_t cards = get_gpu_count(); if (!geometry_prepass && !experiment_.GetGemmiSpaceGroup().has_value() && rsm->GetSearchMinZeta() > 0.0 && cards > 0) { const int32_t device = std::min(cards - 1, 2); - const size_t engine_bytes = static_cast(rsm->ObservationCount()) - * RotationScaleMerge::DEVICE_BYTES_PER_OBSERVATION; - if (cards >= 3 || 2 * engine_bytes <= get_gpu_total_memory(device)) { + constexpr size_t CARD_BYTES_RESERVED = size_t(3) << 30; + if (cards >= 3 || 2 * rsm->DeviceBytes() + CARD_BYTES_RESERVED <= get_gpu_total_memory(device)) { rsm_all = rsm->CloneIngest(device, all_arm_log); rsm_all->SetSearchMinZeta(0.0); rsm_all->SetExportScaledFulls(false); // as scale_and_merge sets them for a search merge