rugnux: parallelise candidate-cell refinement, and stop repeating work in the tail
Build Packages / build:viewer-tgz:cpu (push) Successful in 20m26s
Build Packages / build:viewer-tgz:cuda (push) Successful in 21m30s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 22m36s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 24m4s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 28m10s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 28m12s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 28m23s
Build Packages / XDS test (durin plugin) (push) Successful in 11m21s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 20m56s
Build Packages / build:rpm (rocky9) (push) Successful in 21m10s
Build Packages / Generate python client (push) Successful in 40s
Build Packages / Build documentation (push) Successful in 1m34s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (rocky8) (push) Successful in 25m28s
Build Packages / DIALS test (push) Successful in 21m15s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 21m26s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 25m51s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 10m53s
Build Packages / XDS test (neggia plugin) (push) Successful in 9m41s
Build Packages / Unit tests (push) Successful in 2h21m29s
Build Packages / build:windows:nocuda (push) Successful in 1m15s
Build Packages / build:windows:cuda (push) Successful in 28m0s
Build Packages / build:viewer-tgz:cpu (push) Successful in 20m26s
Build Packages / build:viewer-tgz:cuda (push) Successful in 21m30s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 22m36s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 24m4s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 28m10s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 28m12s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 28m23s
Build Packages / XDS test (durin plugin) (push) Successful in 11m21s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 20m56s
Build Packages / build:rpm (rocky9) (push) Successful in 21m10s
Build Packages / Generate python client (push) Successful in 40s
Build Packages / Build documentation (push) Successful in 1m34s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (rocky8) (push) Successful in 25m28s
Build Packages / DIALS test (push) Successful in 21m15s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 21m26s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 25m51s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 10m53s
Build Packages / XDS test (neggia plugin) (push) Successful in 9m41s
Build Packages / Unit tests (push) Successful in 2h21m29s
Build Packages / build:windows:nocuda (push) Successful in 1m15s
Build Packages / build:windows:cuda (push) Successful in 28m0s
Three independent changes to the CPU-bound parts of an offline rotation run, none of which alters a result. Candidate-cell refinement now splits across threads. RefineCandidateCells already took a (block, nblocks) partition, but the only call site passed nblocks=1, so the whole first pass of a two-pass rotation run sat on one thread per scheme - two threads, unchanged at every -N, for a third of the run. A block touches only its own scores(j) and cells rows and holds its own scratch, so the split is exact. The budget is a new IndexingSettings::RefineThreads, left at 1 by default and set only where few indexer threads exist: raising it unconditionally would oversubscribe the paths that already run one indexer per image across all workers. The mmCIF writer built a std::ostringstream per formatted number, twelve per reflection. snprintf gives the same digits for 0.535 -> 0.220 s per file. The space-group search built the same orbit mapping twice per candidate point group - once for the merge chi^2 and once for the systematic-error b, an apply_to_hkl and Canonicalize per observation per operator each time. Build it once and hand it to both. 18 Mpx rotation set 24.6 -> 18.7 s, 2.5 Mpx 13.0 -> 10.7 s, and the 37-crystal battery 13m55s -> 10m47s with no failures, the same 34/37 space groups, and statistics unchanged on 30 of 37 (the rest drift within the run-to-run spread the binary already had, which a control build with the split disabled reproduces). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -141,6 +141,17 @@ IndexingSettings &IndexingSettings::IndexingThreads(int64_t input) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
int64_t IndexingSettings::GetRefineThreads() const {
|
||||
return refine_threads;
|
||||
}
|
||||
|
||||
IndexingSettings &IndexingSettings::RefineThreads(int64_t input) {
|
||||
check_min("Candidate-cell refinement thread count", input, 1);
|
||||
check_max("Candidate-cell refinement thread count", input, 64);
|
||||
refine_threads = input;
|
||||
return *this;
|
||||
}
|
||||
|
||||
IndexingSettings &IndexingSettings::UnitCellDistTolerance(float input) {
|
||||
check_min("Relative unit cell distance tolerance vs. reference", input, 0.0001);
|
||||
check_max("Relative unit cell distance tolerance vs. reference", input, 0.2001);
|
||||
|
||||
@@ -23,6 +23,10 @@ class IndexingSettings {
|
||||
float unit_cell_dist_tolerance_vs_reference = 0.05; // relative
|
||||
static constexpr float unit_cell_angle_tolerance_deg = 5.0; // degree
|
||||
int64_t indexing_threads = 4;
|
||||
// Threads splitting the candidate-cell refinement WITHIN one indexer call. 1 (the default) is the
|
||||
// right answer whenever indexers already run one per image across all workers; it is raised only
|
||||
// where few indexer threads exist and cores would otherwise sit idle.
|
||||
int64_t refine_threads = 1;
|
||||
int64_t viable_cell_min_spots = 9;
|
||||
|
||||
int64_t max_extra_lattices = 2;
|
||||
@@ -48,6 +52,7 @@ public:
|
||||
IndexingSettings& FFT_HighResolution_A(float input);
|
||||
IndexingSettings& Tolerance(float input);
|
||||
IndexingSettings& IndexingThreads(int64_t input);
|
||||
IndexingSettings& RefineThreads(int64_t input);
|
||||
IndexingSettings& UnitCellDistTolerance(float input);
|
||||
IndexingSettings& GeomRefinementAlgorithm(GeomRefinementAlgorithmEnum input);
|
||||
IndexingSettings& IndexIceRings(bool input);
|
||||
@@ -68,6 +73,7 @@ public:
|
||||
[[nodiscard]] float GetFFT_MinAngle_deg() const;
|
||||
[[nodiscard]] float GetFFT_MaxAngle_deg() const;
|
||||
[[nodiscard]] int64_t GetIndexingThreads() const;
|
||||
[[nodiscard]] int64_t GetRefineThreads() const;
|
||||
[[nodiscard]] float GetUnitCellDistTolerance() const;
|
||||
[[nodiscard]] float GetUnitCellAngleTolerance_deg() const;
|
||||
[[nodiscard]] bool GetIndexIceRings() const;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <tuple>
|
||||
#include <fstream>
|
||||
@@ -39,12 +40,15 @@ std::string CurrentDateISO() {
|
||||
}
|
||||
|
||||
/// Format a double with given decimal places; returns "?" for non-finite.
|
||||
/// snprintf rather than an ostringstream: the reflection loop below calls this twelve times per
|
||||
/// reflection, and building a stream (and its locale) per call dominated the time spent writing a
|
||||
/// merged file. Same digits - both go through the C locale's %.*f.
|
||||
std::string Fmt(double val, int decimals = 4) {
|
||||
if (!std::isfinite(val))
|
||||
return "?";
|
||||
std::ostringstream ss;
|
||||
ss << std::fixed << std::setprecision(decimals) << val;
|
||||
return ss.str();
|
||||
char buf[512];
|
||||
const int n = std::snprintf(buf, sizeof(buf), "%.*f", decimals, val);
|
||||
return std::string(buf, n);
|
||||
}
|
||||
|
||||
/// Quote a CIF string value; returns "?" for empty.
|
||||
|
||||
@@ -12,6 +12,7 @@ FFTIndexer::FFTIndexer(const IndexingSettings &settings)
|
||||
min_angle_deg(settings.GetFFT_MinAngle_deg()),
|
||||
max_angle_deg(settings.GetFFT_MaxAngle_deg()),
|
||||
nDirections(settings.GetFFT_NumVectors()),
|
||||
refine_threads(static_cast<unsigned>(settings.GetRefineThreads())),
|
||||
result_fft(nDirections) {
|
||||
|
||||
// Reciprocal-magnitude histogram in one_over_d = 1/d units (the internal convention -
|
||||
@@ -332,7 +333,8 @@ std::vector<CrystalLattice> FFTIndexer::ReduceAndRefine(const std::vector<Coord>
|
||||
.max_length_A = max_length_A,
|
||||
.min_angle_deg = min_angle_deg,
|
||||
.max_angle_deg = max_angle_deg,
|
||||
.indexing_tolerance = indexing_tolerance
|
||||
.indexing_tolerance = indexing_tolerance,
|
||||
.refine_threads = refine_threads
|
||||
};
|
||||
|
||||
return Refine(coord, nspots, oCell, scores, parameters);
|
||||
|
||||
@@ -21,6 +21,7 @@ protected:
|
||||
const float max_angle_deg;
|
||||
|
||||
const int nDirections;
|
||||
const unsigned refine_threads;
|
||||
float histogram_spacing;
|
||||
int64_t histogram_size;
|
||||
std::vector<Coord> direction_vectors;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "PostIndexingRefinement.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
namespace {
|
||||
struct config_ifssr final {
|
||||
@@ -145,7 +146,25 @@ std::vector<CrystalLattice> Refine(const std::vector<Coord> &in_spots,
|
||||
.min_spots = static_cast<uint32_t>(p.viable_cell_min_spots)
|
||||
};
|
||||
|
||||
RefineCandidateCells(spots.topRows(nspots), oCell, scores, cifssr);
|
||||
// Candidate cells refine independently - a block touches only its own scores(j) and cells rows, and
|
||||
// holds its own scratch - so splitting them across threads gives the same numbers as one thread.
|
||||
// Only worth it where few indexer threads run (the rotation first pass uses two, one per scheme,
|
||||
// and leaves the rest of the machine idle); refine_threads stays 1 everywhere else.
|
||||
const unsigned ncells = static_cast<unsigned>(scores.rows());
|
||||
const unsigned nblocks = std::max(1u, std::min(p.refine_threads, ncells));
|
||||
if (nblocks == 1) {
|
||||
RefineCandidateCells(spots.topRows(nspots), oCell, scores, cifssr);
|
||||
} else {
|
||||
std::vector<std::thread> workers;
|
||||
workers.reserve(nblocks - 1);
|
||||
for (unsigned b = 1; b < nblocks; b++)
|
||||
workers.emplace_back([&, b] {
|
||||
RefineCandidateCells(spots.topRows(nspots), oCell, scores, cifssr, b, nblocks);
|
||||
});
|
||||
RefineCandidateCells(spots.topRows(nspots), oCell, scores, cifssr, 0, nblocks);
|
||||
for (auto &w : workers)
|
||||
w.join();
|
||||
}
|
||||
|
||||
std::vector<RefinedCandidate> candidates;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ struct RefineParameters {
|
||||
float min_angle_deg;
|
||||
float max_angle_deg;
|
||||
float indexing_tolerance;
|
||||
unsigned refine_threads = 1;
|
||||
};
|
||||
|
||||
std::vector<CrystalLattice> Refine(const std::vector<Coord> &in_spots,
|
||||
|
||||
@@ -375,16 +375,17 @@ SearchSpaceGroupResult SearchSpaceGroup(
|
||||
holohedry = HolohedryRotationSet(opt.lattice_system.value());
|
||||
const auto point_groups = EnumeratePointGroups(holohedry);
|
||||
|
||||
// Reduced chi^2 of the intensities merged under a point group's rotations - how well its symmetry
|
||||
// equivalents agree RELATIVE TO THEIR ERRORS. A real point group gives ~1; a false operator forces
|
||||
// non-equivalent reflections together, so they disagree by many sigma and chi^2 blows up. This is
|
||||
// more sensitive than R-meas to a strong pseudo-symmetry (where the intensities still correlate well
|
||||
// - high operator CC - but not within their errors). Inverse-variance weighted mean per orbit, over
|
||||
// the present (pass_cc) reflections.
|
||||
auto chi2_under = [&](const std::vector<gemmi::Op>& rotations) -> double {
|
||||
struct Acc { double sw = 0.0, swI = 0.0; int n = 0; };
|
||||
std::unordered_map<HKLKey, Acc, HKLKeyHash> grp;
|
||||
std::vector<HKLKey> rep(n);
|
||||
// Mapping every observation onto its symmetry representative under a candidate's rotations - one
|
||||
// apply_to_hkl + Canonicalize per observation per operator - is the expensive half of BOTH
|
||||
// quantities below, and both need exactly the same mapping. Build it once per point group.
|
||||
struct Acc { double sw = 0.0, swI = 0.0; int n = 0; };
|
||||
struct Orbits {
|
||||
std::unordered_map<HKLKey, Acc, HKLKeyHash> grp; // representative -> inverse-variance accumulator
|
||||
std::vector<HKLKey> rep; // observation -> its representative
|
||||
};
|
||||
auto build_orbits = [&](const std::vector<gemmi::Op>& rotations) -> Orbits {
|
||||
Orbits orb;
|
||||
orb.rep.resize(n);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
if (!pass_cc[i] || !(Sigma[i] > 0.0))
|
||||
continue;
|
||||
@@ -395,17 +396,29 @@ SearchSpaceGroupResult SearchSpaceGroup(
|
||||
if (std::make_tuple(k2.h, k2.k, k2.l) < std::make_tuple(best.h, best.k, best.l))
|
||||
best = k2;
|
||||
}
|
||||
rep[i] = best;
|
||||
auto& g = grp[best];
|
||||
orb.rep[i] = best;
|
||||
auto& g = orb.grp[best];
|
||||
const double w = 1.0 / (Sigma[i] * Sigma[i]);
|
||||
g.sw += w; g.swI += w * I[i]; g.n += 1;
|
||||
}
|
||||
return orb;
|
||||
};
|
||||
|
||||
// Reduced chi^2 of the intensities merged under a point group's rotations - how well its symmetry
|
||||
// equivalents agree RELATIVE TO THEIR ERRORS. A real point group gives ~1; a false operator forces
|
||||
// non-equivalent reflections together, so they disagree by many sigma and chi^2 blows up. This is
|
||||
// more sensitive than R-meas to a strong pseudo-symmetry (where the intensities still correlate well
|
||||
// - high operator CC - but not within their errors). Inverse-variance weighted mean per orbit, over
|
||||
// the present (pass_cc) reflections.
|
||||
auto chi2_under = [&](const Orbits& orb) -> double {
|
||||
const auto& grp = orb.grp;
|
||||
const auto& rep = orb.rep;
|
||||
double chi2 = 0.0;
|
||||
long dof = 0;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
if (!pass_cc[i] || !(Sigma[i] > 0.0))
|
||||
continue;
|
||||
const auto& g = grp[rep[i]];
|
||||
const auto& g = grp.at(rep[i]);
|
||||
if (g.n < 2)
|
||||
continue;
|
||||
const double mean = g.swI / g.sw, dev = I[i] - mean;
|
||||
@@ -426,30 +439,14 @@ SearchSpaceGroupResult SearchSpaceGroup(
|
||||
// the systematic part of the scatter, which the fixed-sigma chi^2 ratio cannot: a genuine but
|
||||
// imperfectly-scaled high-symmetry merge and a twin can share a chi^2 ratio (~2) yet differ
|
||||
// sharply here (a genuine cubic step b x1.04 vs a merohedral twin b x1.6).
|
||||
auto merge_systematic_b = [&](const std::vector<gemmi::Op>& rotations) -> double {
|
||||
struct Acc { double sw = 0.0, swI = 0.0; int n = 0; };
|
||||
std::unordered_map<HKLKey, Acc, HKLKeyHash> grp;
|
||||
std::vector<HKLKey> rep(n);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
if (!pass_cc[i] || !(Sigma[i] > 0.0))
|
||||
continue;
|
||||
HKLKey best = key[i];
|
||||
for (const auto& op : rotations) {
|
||||
const auto m = op.apply_to_hkl(gemmi::Op::Miller{{H[i], K[i], L[i]}});
|
||||
const HKLKey k2 = Canonicalize(m[0], m[1], m[2], opt.merge_friedel);
|
||||
if (std::make_tuple(k2.h, k2.k, k2.l) < std::make_tuple(best.h, best.k, best.l))
|
||||
best = k2;
|
||||
}
|
||||
rep[i] = best;
|
||||
auto& g = grp[best];
|
||||
const double w = 1.0 / (Sigma[i] * Sigma[i]);
|
||||
g.sw += w; g.swI += w * I[i]; g.n += 1;
|
||||
}
|
||||
auto merge_systematic_b = [&](const Orbits& orb) -> double {
|
||||
const auto& grp = orb.grp;
|
||||
const auto& rep = orb.rep;
|
||||
std::vector<std::array<double, 3>> obs; // I, sigma, deviation-from-orbit-mean
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
if (!pass_cc[i] || !(Sigma[i] > 0.0))
|
||||
continue;
|
||||
const auto& g = grp[rep[i]];
|
||||
const auto& g = grp.at(rep[i]);
|
||||
if (g.n < 2)
|
||||
continue;
|
||||
obs.push_back({I[i], Sigma[i], I[i] - g.swI / g.sw});
|
||||
@@ -483,9 +480,13 @@ SearchSpaceGroupResult SearchSpaceGroup(
|
||||
const auto [present, min_class_cc] = point_group_present(pg.rotations);
|
||||
if (!present)
|
||||
continue;
|
||||
const double ch = pg.rotations.empty() ? std::numeric_limits<double>::quiet_NaN()
|
||||
: chi2_under(pg.rotations);
|
||||
const double be = pg.rotations.empty() ? 0.0 : merge_systematic_b(pg.rotations);
|
||||
double ch = std::numeric_limits<double>::quiet_NaN();
|
||||
double be = 0.0;
|
||||
if (!pg.rotations.empty()) {
|
||||
const Orbits orb = build_orbits(pg.rotations);
|
||||
ch = chi2_under(orb);
|
||||
be = merge_systematic_b(orb);
|
||||
}
|
||||
pg_cands.push_back({&pg, static_cast<int>(pg.rotations.size()) + 1, min_class_cc, ch, be});
|
||||
if (!pg.rotations.empty() && std::isfinite(ch))
|
||||
chi2_ref = std::min(chi2_ref, ch);
|
||||
|
||||
+7
-1
@@ -523,8 +523,13 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
|
||||
// parallel (the main per-image loop then uses the forced lattice, no FFT). Size the pool to
|
||||
// those two so a rotation run doesn't pay to initialise (cuFFT plan + GPU alloc) indexers it
|
||||
// never uses. Stills fire the FFT once per image across all workers, so keep the default there.
|
||||
if (config_.rotation_indexing && config_.two_pass_rotation && experiment_.IsRotationIndexing())
|
||||
if (config_.rotation_indexing && config_.two_pass_rotation && experiment_.IsRotationIndexing()) {
|
||||
indexing_settings.IndexingThreads(2);
|
||||
// Two indexer threads leave the rest of the machine idle for the whole first pass, which is
|
||||
// a third of the run. Split each scheme's candidate-cell refinement over -N/2 threads so the
|
||||
// two together use -N. The split is by candidate cell, so the result does not depend on it.
|
||||
indexing_settings.RefineThreads(std::max(1, config_.nthreads / 2));
|
||||
}
|
||||
indexer_pool = std::make_unique<IndexerThreadPool>(indexing_settings, IndexerConstruction::OnFirstUse);
|
||||
indexer = std::make_unique<IndexAndRefine>(experiment_, indexer_pool.get());
|
||||
if (!config_.reference_data.empty())
|
||||
@@ -764,6 +769,7 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
|
||||
auto coarse_settings = experiment_.GetIndexingSettings();
|
||||
coarse_settings.FFT_HighResolution_A(3.5f); // low-order reflections only -> robust long axis
|
||||
coarse_settings.IndexingThreads(2);
|
||||
coarse_settings.RefineThreads(std::max(1, config_.nthreads / 2));
|
||||
IndexerThreadPool coarse_pool(coarse_settings, IndexerConstruction::OnFirstUse);
|
||||
// Coarse first pass: keep the recovered cell with the LONGEST axis directly. Its full-
|
||||
// resolution per-frame validation would be low (the coarse cell is metrically right but
|
||||
|
||||
Reference in New Issue
Block a user