From c9fc46e6e20b1e840e90ac943e76b26607023b23 Mon Sep 17 00:00:00 2001 From: jungfrau Date: Sun, 23 Aug 2026 11:52:37 -0400 Subject: [PATCH] Stop making the first pass write the files the second pass replaces Pass 1 exists to choose the space group and post-refine the geometry. Its merged intensities are discarded - pass 2 remakes them seconds later at the refined geometry, and that is the answer anyone reads. It was nonetheless writing the full set of merged files at the end of every pass 1: a mmCIF of every unique reflection (22 MB on an ordinary crystal, 48 MB on a crowded one), an .hkl, an .mtz and the per-image scaling table, all through one thread. Measured on an ordinary rotation set: 0.60 s of a 15 s run, and pass 2's identical block right after it takes another 0.585 s to write the files that are kept. The pass-2 quality guard is untouched, which is what disqualified an earlier attempt at this: has_merge_statistics is set at the merge, well above the write, so pass 1 still reports the completeness and CC1/2 the guard compares against. Nothing numeric moves - the same run measures 38.5 s before and 36.6 s after with a byte-identical .hkl. Also hoist the pixel-mask accessor out of the preprocessor's per-pixel loop. It called .at() on every pixel of the detector - 18 million bounds checks per engine, and an engine is built per worker per pass - for a bound the loop already respects, which stopped it vectorising. The _01.mtz/.cif/.hkl are documented output, so this is a deliberate behaviour change: the pre-pass result is no longer written. If it is wanted for comparison it should come back behind a flag rather than by default. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_011n8riB6X59oRjkrSHzNPAU --- .../image_preprocessing/ImagePreprocessorGPU.cu | 7 ++++++- rugnux/Rugnux.cpp | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/image_analysis/image_preprocessing/ImagePreprocessorGPU.cu b/image_analysis/image_preprocessing/ImagePreprocessorGPU.cu index 37c772af..1dfb2052 100644 --- a/image_analysis/image_preprocessing/ImagePreprocessorGPU.cu +++ b/image_analysis/image_preprocessing/ImagePreprocessorGPU.cu @@ -237,9 +237,14 @@ ImagePreprocessorGPU::ImagePreprocessorGPU(const DiffractionExperiment &experime cpu_stats_reg(cpu_stats) { // Setup mask. The same for every worker, so it is uploaded once per GPU and shared; keyed on the // PixelMask's own vector, which the derived table is a pure function of. + // Hoist the accessor and index without the bounds check: this runs once per worker over every + // pixel of the detector - 18 million times per engine on a 16 Mpx one, and an engine is built per + // worker per pass - and .at() on each of them stops the loop vectorising for a bound the loop + // itself already respects. + const std::vector &mask_raw = mask.GetMask(); std::vector mask_vec(npixels); for (int i = 0; i < npixels; i++) - mask_vec[i] = (mask.GetMask().at(i) != 0); + mask_vec[i] = (mask_raw[i] != 0); gpu_mask = SharedDeviceTable(mask.GetMask().data(), npixels, mask_vec.data(), *stream); // Setup GPU settings. The current device, not device 0: workers are pinned round-robin across GPUs, diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index c063e16d..6255c59a 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -2782,7 +2782,12 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b } } - if (result.consensus_cell && write_files && config_.write_merged) { + // Not on the geometry pre-pass. Pass 1 exists to choose the space group and post-refine the + // geometry; pass 2 remakes these files seconds later at the refined geometry, and that is the + // answer anyone reads. Writing them twice costs 0.6 s of a 15 s run - a fifth of the merged + // mmCIF on a large crystal - for a file that is superseded before the run ends. The pass-2 + // quality guard is unaffected: has_merge_statistics is set well above this, at the merge. + if (result.consensus_cell && write_files && config_.write_merged && !geometry_prepass) { phase("Writing reflections"); const ErrorModelReport em_report{ result.error_model_isa > 0 ? fmt::format("{:.2f}", result.error_model_isa) : "?", @@ -2799,7 +2804,7 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b ScalingResult(indexer->GetIntegrationOutcome()).SaveToFile(config_.output_prefix); } - if (result.consensus_cell && write_files && !config_.model_path.empty()) { + if (result.consensus_cell && write_files && !config_.model_path.empty() && !geometry_prepass) { phase("Validating against model"); const auto data_sg = experiment_.GetSpaceGroupNumber(); // With a reference MTZ the merohedral indexing was already resolved against it (rotation