Merge branch 'gpu-modelval' into rc175: --model validation's scale fits on the GPU, change-of-basis scoring gridded once in parallel
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -344,49 +344,63 @@ constexpr double FRAME_PROBE_D_MAX = 8.0;
|
||||
constexpr size_t FRAME_PROBE_MIN_REFLECTIONS = 200;
|
||||
|
||||
// R of the model against the observed amplitudes after an overall scale and an isotropic B, over
|
||||
// that coarse shell. Only the ranking is ever used, never the value.
|
||||
// that coarse shell - the lowest over `labellings`, the labellings of the data it is scored against,
|
||||
// each on the same F_calc. Only the ranking is ever used, never the value.
|
||||
double frame_probe_r(const gemmi::Structure &st, const gemmi::SpaceGroup *sg,
|
||||
const std::vector<MergedReflection> &obs, double d_min) {
|
||||
const std::vector<const std::vector<MergedReflection> *> &labellings, double d_min,
|
||||
size_t nthreads) {
|
||||
const double probe_d_min = std::max(d_min, FRAME_PROBE_D_MIN);
|
||||
gemmi::DensityCalculator<Table, float> dc;
|
||||
dc.d_min = probe_d_min;
|
||||
dc.rate = 1.5;
|
||||
dc.set_grid_cell_and_spacegroup(st);
|
||||
dc.set_refmac_compatible_blur(st.models[0]);
|
||||
dc.put_model_density_on_grid(st.models[0]);
|
||||
// put_model_density_on_grid() spread over the threads, the same grid bit for bit (ModelGrid.h); the
|
||||
// orbits are those of the grid initialize_grid() sizes.
|
||||
gemmi::Grid<float> sized;
|
||||
sized.unit_cell = dc.grid.unit_cell;
|
||||
sized.spacegroup = dc.grid.spacegroup;
|
||||
sized.set_size_from_spacing(dc.requested_grid_spacing(), gemmi::GridSizeRounding::Up);
|
||||
PutModelDensityOnGrid(dc, st.models[0], OrbitLeaders(sized, nthreads), nthreads);
|
||||
gemmi::AsuData<std::complex<float>> fcalc =
|
||||
MapToFPhi(dc.grid)
|
||||
.prepare_asu_data(probe_d_min, dc.blur, false, false, false);
|
||||
|
||||
gemmi::GroupOps gops = sg->operations();
|
||||
gemmi::ReciprocalAsu asu(sg);
|
||||
gemmi::AsuData<gemmi::ValueSigma<float>> fobs;
|
||||
fobs.unit_cell_ = st.cell;
|
||||
fobs.spacegroup_ = sg;
|
||||
// The low-resolution cap is dropped, rather than the probe abandoned, when a small cell does not
|
||||
// put enough reflections in the shell.
|
||||
for (double d_max : {FRAME_PROBE_D_MAX, 1e9}) {
|
||||
fobs.v.clear();
|
||||
for (const MergedReflection &r : obs) {
|
||||
if (std::isnan(r.F) || r.d < probe_d_min || r.d > d_max)
|
||||
continue;
|
||||
gemmi::Miller h{{r.h, r.k, r.l}};
|
||||
if (!asu.is_in(h))
|
||||
h = asu.to_asu(h, gops).first;
|
||||
fobs.v.push_back({h, {r.F, 1.0f}});
|
||||
double best = 0;
|
||||
for (size_t i = 0; i < labellings.size(); i++) {
|
||||
gemmi::AsuData<gemmi::ValueSigma<float>> fobs;
|
||||
fobs.unit_cell_ = st.cell;
|
||||
fobs.spacegroup_ = sg;
|
||||
// The low-resolution cap is dropped, rather than the probe abandoned, when a small cell does not
|
||||
// put enough reflections in the shell.
|
||||
for (double d_max : {FRAME_PROBE_D_MAX, 1e9}) {
|
||||
fobs.v.clear();
|
||||
for (const MergedReflection &r : *labellings[i]) {
|
||||
if (std::isnan(r.F) || r.d < probe_d_min || r.d > d_max)
|
||||
continue;
|
||||
gemmi::Miller h{{r.h, r.k, r.l}};
|
||||
if (!asu.is_in(h))
|
||||
h = asu.to_asu(h, gops).first;
|
||||
fobs.v.push_back({h, {r.F, 1.0f}});
|
||||
}
|
||||
if (fobs.v.size() >= FRAME_PROBE_MIN_REFLECTIONS)
|
||||
break;
|
||||
}
|
||||
if (fobs.v.size() >= FRAME_PROBE_MIN_REFLECTIONS)
|
||||
break;
|
||||
double r = 1.0;
|
||||
if (!fobs.v.empty()) {
|
||||
fobs.ensure_asu();
|
||||
fobs.ensure_sorted();
|
||||
gemmi::Scaling<float> scaling(st.cell, sg);
|
||||
scaling.use_solvent = false;
|
||||
scaling.prepare_points(fcalc, fobs, nullptr);
|
||||
scaling.fit_isotropic_b_approximately();
|
||||
r = scaling.calculate_r_factor();
|
||||
}
|
||||
best = i == 0 ? r : std::min(best, r);
|
||||
}
|
||||
if (fobs.v.empty())
|
||||
return 1.0;
|
||||
fobs.ensure_asu();
|
||||
fobs.ensure_sorted();
|
||||
gemmi::Scaling<float> scaling(st.cell, sg);
|
||||
scaling.use_solvent = false;
|
||||
scaling.prepare_points(fcalc, fobs, nullptr);
|
||||
scaling.fit_isotropic_b_approximately();
|
||||
return scaling.calculate_r_factor();
|
||||
return best;
|
||||
}
|
||||
|
||||
// The anomalous difference map's coefficients on the reflections of `fmodel`: F(+) - F(-) carried on the
|
||||
@@ -516,6 +530,33 @@ std::unique_ptr<ModelStructureFactorsGPU> StructureFactorsGPU(int device, const
|
||||
"card's {:.2f} GB", d_min, n[0], n[1], n[2], sf->DeviceBytes() / 1e9, total / 1e9);
|
||||
return sf;
|
||||
}
|
||||
|
||||
// The scale fits on the GPU (ModelScaleGPUPool) - or not, decided here, once, on the card's total memory as
|
||||
// the structure factors are: SCALE_GPU_ENGINES engines, fewer where they would take over an eighth of the
|
||||
// card, none where not even one fits.
|
||||
constexpr size_t SCALE_GPU_ENGINES = 4;
|
||||
|
||||
std::unique_ptr<ModelScaleGPUPool> ScaleFitsGPU(int device, size_t max_points, Logger &logger) {
|
||||
const size_t total = get_gpu_total_memory(device);
|
||||
size_t engines = SCALE_GPU_ENGINES;
|
||||
while (engines > 0 && ModelScaleGPUPool::DeviceBytes(max_points, engines) > total / 8)
|
||||
engines--;
|
||||
if (engines == 0) {
|
||||
logger.Info("Model validation: scale fits on the CPU - one fit of {} reflections needs {:.2f} GB of GPU "
|
||||
"memory, over an eighth of the card's {:.2f} GB", max_points,
|
||||
ModelScaleGPUPool::DeviceBytes(max_points, 1) / 1e9, total / 1e9);
|
||||
return nullptr;
|
||||
}
|
||||
std::unique_ptr<ModelScaleGPUPool> pool;
|
||||
try {
|
||||
pool = std::make_unique<ModelScaleGPUPool>(device, max_points, engines);
|
||||
} catch (const JFJochException &e) {
|
||||
throw RigidBodyGPUFailure(e.what());
|
||||
}
|
||||
logger.Info("Model validation: scale fits on the GPU, {} engine(s), {:.2f} GB of the card's {:.2f} GB",
|
||||
engines, pool->Bytes() / 1e9, total / 1e9);
|
||||
return pool;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
@@ -671,9 +712,20 @@ ModelValidationResult Validate(const std::vector<MergedReflection> &merged,
|
||||
sg->xhm(), frames.size() - 1);
|
||||
// The candidates are scored at the same time, each on its own copy of the model, and read
|
||||
// in their own order below, so the ranking and its tie-break are those of one after another.
|
||||
// On threads of their own, not on ParallelFor's pool, so that each one's gridding spreads over
|
||||
// the pool (a parallel pass reached from a pool worker runs inline, ParallelFor.h).
|
||||
std::vector<double> frame_r(frames.size(), 0.0);
|
||||
std::vector<const gemmi::SpaceGroup *> frame_sg(frames.size(), nullptr);
|
||||
ParallelFor(static_cast<int>(frames.size()), nthreads, [&](int i) {
|
||||
// The data as merged and, where the alternative indexings are left to the probe below, in each
|
||||
// of them: the lowest R over these is the frame's.
|
||||
std::vector<std::vector<MergedReflection>> relabelled;
|
||||
if (same_point_group)
|
||||
for (const gemmi::Op &law : reindex_ops)
|
||||
relabelled.push_back(ReindexReflections(obs, law));
|
||||
std::vector<const std::vector<MergedReflection> *> labellings{&obs};
|
||||
for (const auto &v : relabelled)
|
||||
labellings.push_back(&v);
|
||||
auto score_frame = [&](size_t i) {
|
||||
gemmi::Structure trial = st;
|
||||
const gemmi::SpaceGroup *trial_sg = sg;
|
||||
if (i > 0 && !change_model_basis(trial, trial_sg, frames[i]))
|
||||
@@ -685,13 +737,14 @@ ModelValidationResult Validate(const std::vector<MergedReflection> &merged,
|
||||
// as every other and the ranking is noise. Measured on a pseudo-orthorhombic P2
|
||||
// crystal: every frame read R 0.77-0.81 in the merged indexing, and the frame chosen
|
||||
// on that noise swapped two axes the model shares with the data.
|
||||
double r = frame_probe_r(trial, trial_sg, obs, d_min);
|
||||
if (same_point_group)
|
||||
for (const gemmi::Op &law : reindex_ops)
|
||||
r = std::min(r, frame_probe_r(trial, trial_sg, ReindexReflections(obs, law), d_min));
|
||||
frame_r[i] = r;
|
||||
frame_r[i] = frame_probe_r(trial, trial_sg, labellings, d_min, nthreads);
|
||||
frame_sg[i] = trial_sg;
|
||||
});
|
||||
};
|
||||
std::vector<std::future<void>> scored;
|
||||
for (size_t i = 0; i < frames.size(); i++)
|
||||
scored.push_back(std::async(std::launch::async, score_frame, i));
|
||||
for (std::future<void> &f : scored)
|
||||
f.get();
|
||||
size_t best = 0;
|
||||
double best_r = 0;
|
||||
for (size_t i = 0; i < frames.size(); i++) {
|
||||
@@ -739,6 +792,10 @@ ModelValidationResult Validate(const std::vector<MergedReflection> &merged,
|
||||
sf_engine = StructureFactorsGPU(RigidBodyGPUEngine::CurrentDevice(), st.models[0], ucell, *sg, d_min, 0,
|
||||
logger);
|
||||
sf_gpu = sf_engine.get();
|
||||
// The scale fits beside them, on the same card, for as many reflections as the data have.
|
||||
std::unique_ptr<ModelScaleGPUPool> scale_gpu;
|
||||
if (rigid_body_gpu && get_gpu_count() > 0)
|
||||
scale_gpu = ScaleFitsGPU(RigidBodyGPUEngine::CurrentDevice(), obs.size(), logger);
|
||||
#endif
|
||||
|
||||
// --- Fcalc (atomic) via electron density on a grid + FFT, plus a flat bulk-solvent mask -> Fmask.
|
||||
@@ -876,6 +933,15 @@ ModelValidationResult Validate(const std::vector<MergedReflection> &merged,
|
||||
gemmi::Scaling<float> scaling(ucell, sg);
|
||||
scaling.use_solvent = true;
|
||||
scaling.prepare_points(out.fmodel, fobs_work, &ms.fmask);
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
if (scale_gpu) {
|
||||
try {
|
||||
scale_gpu->Fit(scaling, SolventBMax(ms.st.models[0]), nthreads);
|
||||
} catch (const JFJochException &e) {
|
||||
throw RigidBodyGPUFailure(e.what());
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
FitModelScale(scaling, {.b_hi = SolventBMax(ms.st.models[0])}, nthreads);
|
||||
scaling.scale_data(out.fmodel, &ms.fmask); // out.fmodel now holds the scaled, solvent-corrected Fmodel
|
||||
out.k_sol = scaling.k_sol;
|
||||
@@ -1042,6 +1108,10 @@ ModelValidationResult Validate(const std::vector<MergedReflection> &merged,
|
||||
planned += (decision_pending ? 2 : 1) * sf_gpu->DeviceBytes();
|
||||
card = sf_gpu->DeviceTotalMemory();
|
||||
}
|
||||
if (scale_gpu) {
|
||||
planned += scale_gpu->Bytes();
|
||||
card = get_gpu_total_memory(RigidBodyGPUEngine::CurrentDevice());
|
||||
}
|
||||
if (planned > 0)
|
||||
may_run_beside = 2 * planned <= static_cast<size_t>(get_gpu_count()) * card / 2;
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user