Rigid body: backend seam, GPU engine for density/composition/Jacobian, engine pool
RigidBodyTarget becomes the CPU backend behind RigidBodyTargetBase, numerics unchanged. RigidBodyTargetGPU evaluates the same target on a GPU engine: atoms placed on the device, density by a deterministic brick gather (atoms in model order per point), cuFFT R2C, the SymmetryComposition gather for Fcalc and dF/dt in double, the numeric rotation columns as a batch-3 transform, the Jacobian rows and the Kaufman sums by fixed-order reductions. The mask and the scale fit are still computed on the host (stubs until ModelMaskGPU and ModelScaleGPU land). A pool of 1-4 engines is reserved once per validation, capped at min(25% of the card, free - 1 GB); GPU or CPU is decided once for the whole validation, and a CUDA failure restarts it on the CPU. JFJOCH_RIGID_BODY_CPU forces the CPU. PutMaskOnGrid skips gemmi's shrink where its stencil is empty (every rigid-body zone): exact, it cannot change a point. GPU vs CPU on the five test groups: residuals within 7e-6 of <F>, Jacobian columns within 4e-5 relative, fits 4e-7 A apart. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D1G8gJVAy6gp1K5Dz3NE5C
This commit is contained in:
@@ -21,6 +21,10 @@
|
||||
#include "../rugnux/ModelGrid.h"
|
||||
#include "../rugnux/ModelValidation.h"
|
||||
#include "../rugnux/RigidBodyRefine.h"
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
#include "../rugnux/RigidBodyGPU.h"
|
||||
#include "../common/CUDAWrapper.h"
|
||||
#endif
|
||||
#include "../rugnux/SigmaA.h"
|
||||
#include "../rugnux/WriteModel.h"
|
||||
#include "../image_analysis/scale_merge/ReindexAmbiguity.h"
|
||||
@@ -1027,8 +1031,34 @@ namespace {
|
||||
// it. Per column (those in `columns`): the cosine between the two and the ratio of their norms.
|
||||
// Also logged, not checked: how far the difference moves once the mask is let move with the body,
|
||||
// which is what holding it costs.
|
||||
// The model's own amplitudes to `d_min`: "observed" data whose minimum is where the model is.
|
||||
gemmi::AsuData<gemmi::ValueSigma<float>> OwnAmplitudes(const char *cryst, const gemmi::Structure &st,
|
||||
double d_min, Logger &logger) {
|
||||
const auto path = WriteTemp("rigid_body_own_amplitudes.pdb", ClusterPdb(cryst).c_str());
|
||||
const auto ref = ModelReferenceIntensities(path, {}, {}, 3.0, logger);
|
||||
std::filesystem::remove(path);
|
||||
gemmi::AsuData<gemmi::ValueSigma<float>> fobs;
|
||||
fobs.unit_cell_ = st.cell;
|
||||
fobs.spacegroup_ = st.find_spacegroup();
|
||||
for (const auto &r : ref)
|
||||
if (st.cell.calculate_d({{r.h, r.k, r.l}}) >= d_min)
|
||||
fobs.v.push_back({{{r.h, r.k, r.l}}, {std::sqrt(r.I), 1.0f}});
|
||||
fobs.ensure_sorted();
|
||||
return fobs;
|
||||
}
|
||||
|
||||
// The rigid body's target on the CPU, or on an engine of `gpu` where it is given.
|
||||
std::unique_ptr<RigidBodyTargetBase> MakeTarget(gemmi::Model &model, const gemmi::Structure &st,
|
||||
RigidBodyGPUPool *gpu) {
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
if (gpu != nullptr)
|
||||
return std::make_unique<RigidBodyTargetGPU>(*gpu, model, st.cell, *st.find_spacegroup(), 4);
|
||||
#endif
|
||||
return std::make_unique<RigidBodyTarget>(model, st.cell, *st.find_spacegroup(), 4);
|
||||
}
|
||||
|
||||
void CheckRigidBodyJacobian(const char *cryst, const double q0[6], const std::vector<int> &columns,
|
||||
double min_cosine, double max_norm_error) {
|
||||
double min_cosine, double max_norm_error, bool gpu = false) {
|
||||
Logger logger("CheckRigidBodyJacobian");
|
||||
const gemmi::Structure st = AnisoCluster(cryst);
|
||||
const gemmi::SpaceGroup *sg = st.find_spacegroup();
|
||||
@@ -1048,7 +1078,15 @@ namespace {
|
||||
fobs.ensure_sorted();
|
||||
|
||||
gemmi::Model model = st.models[0];
|
||||
RigidBodyTarget target(model, st.cell, *sg, 4);
|
||||
std::unique_ptr<RigidBodyGPUPool> pool;
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
if (gpu) {
|
||||
pool = RigidBodyGPUPool::Create(model, st.cell, *sg, zone, fobs.v.size(), 1, logger);
|
||||
REQUIRE(pool);
|
||||
}
|
||||
#endif
|
||||
const std::unique_ptr<RigidBodyTargetBase> target_backend = MakeTarget(model, st, pool.get());
|
||||
RigidBodyTargetBase &target = *target_backend;
|
||||
target.SetZone(fobs, zone);
|
||||
const size_t n = target.NumObservations();
|
||||
std::vector<double> r(n), jacobian(n * 6);
|
||||
@@ -1190,3 +1228,150 @@ TEST_CASE("ModelValidation_RigidBodyReportsTheRotationItApplied", "[ModelValidat
|
||||
CHECK(std::acos(std::clamp((trace - 1) / 2, -1.0, 1.0)) * 180 / PI ==
|
||||
Catch::Approx(result.angle_deg).margin(1e-6));
|
||||
}
|
||||
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
|
||||
// The GPU target is the CPU target's function, computed on the device: at the same placement the two
|
||||
// give the same residuals and the same Jacobian to rounding (float distances on the device, cuFFT for
|
||||
// FFTW), on every group of the composition tests, both zones, anisotropic atoms included.
|
||||
TEST_CASE("RigidBodyGPU_MatchesCPU", "[ModelValidation][gpu]") {
|
||||
if (get_gpu_count() == 0) {
|
||||
SUCCEED("no GPU");
|
||||
return;
|
||||
}
|
||||
Logger logger("RigidBodyGPU_MatchesCPU");
|
||||
const double q0[6] = {0.12, -0.09, 0.07, 0.15, -0.10, 0.05};
|
||||
for (const char *cryst : kRigidBodyCrysts) {
|
||||
const gemmi::Structure st = AnisoCluster(cryst);
|
||||
const gemmi::SpaceGroup *sg = st.find_spacegroup();
|
||||
for (double zone : {6.0, 3.5}) {
|
||||
const auto fobs = OwnAmplitudes(cryst, st, zone, logger);
|
||||
gemmi::Model cpu_model = st.models[0], gpu_model = st.models[0];
|
||||
auto pool = RigidBodyGPUPool::Create(gpu_model, st.cell, *sg, zone, fobs.v.size(), 1, logger);
|
||||
REQUIRE(pool);
|
||||
RigidBodyTarget cpu(cpu_model, st.cell, *sg, 4);
|
||||
RigidBodyTargetGPU gpu(*pool, gpu_model, st.cell, *sg, 4);
|
||||
cpu.SetZone(fobs, zone);
|
||||
gpu.SetZone(fobs, zone);
|
||||
const size_t n = cpu.NumObservations();
|
||||
REQUIRE(gpu.NumObservations() == n);
|
||||
std::vector<double> rc(n), rg(n), jc(6 * n), jg(6 * n);
|
||||
REQUIRE(cpu.Residuals(q0, rc.data()));
|
||||
REQUIRE(gpu.Residuals(q0, rg.data()));
|
||||
REQUIRE(cpu.Jacobian(q0, jc.data()));
|
||||
REQUIRE(gpu.Jacobian(q0, jg.data()));
|
||||
CHECK(gpu.unmatched == cpu.unmatched);
|
||||
CHECK(gpu.k_sol == cpu.k_sol);
|
||||
CHECK(gpu.b_sol == cpu.b_sol);
|
||||
|
||||
double worst = 0, rms = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
worst = std::max(worst, std::fabs(rc[i] - rg[i]));
|
||||
rms += rc[i] * rc[i] / static_cast<double>(n);
|
||||
}
|
||||
INFO(cryst << " at " << zone << " A: residuals differ by at most " << worst << ", rms residual "
|
||||
<< std::sqrt(rms));
|
||||
CHECK(worst <= 1e-4);
|
||||
for (int j = 0; j < 6; j++) {
|
||||
double diff = 0, norm = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
diff += gemmi::sq(jc[6 * i + j] - jg[6 * i + j]);
|
||||
norm += gemmi::sq(jc[6 * i + j]);
|
||||
}
|
||||
INFO(cryst << " at " << zone << " A, column " << j << ": relative difference "
|
||||
<< std::sqrt(diff / norm));
|
||||
CHECK(std::sqrt(diff) <= 2e-3 * std::sqrt(norm) + 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The whole Jacobian on the GPU against the central difference of the GPU's own residuals, with the
|
||||
// bounds of ModelValidation_RigidBodyJacobianMatchesNumericScaleRefit.
|
||||
TEST_CASE("RigidBodyGPU_JacobianMatchesNumericScaleRefit", "[ModelValidation][gpu]") {
|
||||
if (get_gpu_count() == 0) {
|
||||
SUCCEED("no GPU");
|
||||
return;
|
||||
}
|
||||
const double close[6] = {0.0, 0.0, 0.0, 0.05, -0.04, 0.03};
|
||||
const double rotated[6] = {0.12, -0.09, 0.07, 0.15, -0.10, 0.05};
|
||||
for (const char *cryst : kRigidBodyCrysts) {
|
||||
CheckRigidBodyJacobian(cryst, close, {0, 1, 2, 3, 4, 5}, 0.99, 0.02, true);
|
||||
CheckRigidBodyJacobian(cryst, rotated, {0, 1, 2}, 0.99, 0.03, true);
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// A whole placement on the GPU from the model displaced by 0.54 A and turned 2 deg, against the
|
||||
// model's own amplitudes; the model is left where the fit put it.
|
||||
RigidBodyRefineResult DisplacedFit(const char *cryst, gemmi::Structure &st, RigidBodyGPUPool *pool,
|
||||
Logger &logger) {
|
||||
const gemmi::SpaceGroup *sg = st.find_spacegroup();
|
||||
const auto fobs = OwnAmplitudes(cryst, st, 3.0, logger);
|
||||
std::vector<gemmi::Position> moved = ModelPositions(st.models[0]);
|
||||
gemmi::Vec3 centre;
|
||||
for (const gemmi::Position &p : moved)
|
||||
centre += p;
|
||||
centre *= 1.0 / static_cast<double>(moved.size());
|
||||
const double a = 2.0 * PI / 180.0;
|
||||
const gemmi::Mat33 rz(std::cos(a), -std::sin(a), 0, std::sin(a), std::cos(a), 0, 0, 0, 1);
|
||||
for (gemmi::Position &p : moved)
|
||||
p = gemmi::Position(rz.multiply(gemmi::Vec3(p) - centre) + centre + gemmi::Vec3(0.40, -0.30, 0.20));
|
||||
SetModelPositions(st.models[0], moved);
|
||||
return RefineRigidBody(st.models[0], st.cell, *sg, fobs, 3.0, logger, 4, pool);
|
||||
}
|
||||
}
|
||||
|
||||
// The GPU fit walks where the CPU fit walks: from the same displaced start, to the same placement
|
||||
// within a few thousandths of an angstrom.
|
||||
TEST_CASE("RigidBodyGPU_FitAgreesWithCPU", "[ModelValidation][gpu]") {
|
||||
if (get_gpu_count() == 0) {
|
||||
SUCCEED("no GPU");
|
||||
return;
|
||||
}
|
||||
Logger logger("RigidBodyGPU_FitAgreesWithCPU");
|
||||
for (const char *cryst : {kCryst, kPolarCryst, kRigidBodyCrysts[3]}) {
|
||||
gemmi::Structure cpu_st = AnisoCluster(cryst), gpu_st = AnisoCluster(cryst);
|
||||
auto pool = RigidBodyGPUPool::Create(gpu_st.models[0], gpu_st.cell, *gpu_st.find_spacegroup(), 3.0, 100000, 1,
|
||||
logger);
|
||||
REQUIRE(pool);
|
||||
const RigidBodyRefineResult cpu = DisplacedFit(cryst, cpu_st, nullptr, logger);
|
||||
const RigidBodyRefineResult gpu = DisplacedFit(cryst, gpu_st, pool.get(), logger);
|
||||
CHECK(gpu.converged == cpu.converged);
|
||||
const std::vector<gemmi::Position> pc = ModelPositions(cpu_st.models[0]), pg = ModelPositions(gpu_st.models[0]);
|
||||
double rmsd = 0;
|
||||
for (size_t i = 0; i < pc.size(); i++)
|
||||
rmsd += pc[i].dist_sq(pg[i]) / static_cast<double>(pc.size());
|
||||
INFO(cryst << ": CPU " << cpu.angle_deg << " deg " << cpu.shift_A << " A, GPU " << gpu.angle_deg << " deg "
|
||||
<< gpu.shift_A << " A, " << std::sqrt(rmsd) << " A apart");
|
||||
CHECK(std::sqrt(rmsd) < 2e-3);
|
||||
}
|
||||
}
|
||||
|
||||
// Deterministic: the same fit twice, and on a pool of one engine and of four, gives the same placement
|
||||
// bit for bit.
|
||||
TEST_CASE("RigidBodyGPU_Deterministic", "[ModelValidation][gpu]") {
|
||||
if (get_gpu_count() == 0) {
|
||||
SUCCEED("no GPU");
|
||||
return;
|
||||
}
|
||||
Logger logger("RigidBodyGPU_Deterministic");
|
||||
const char *cryst = kRigidBodyCrysts[4];
|
||||
std::vector<std::vector<gemmi::Position>> placed;
|
||||
for (size_t engines : {1, 1, 4}) {
|
||||
gemmi::Structure st = AnisoCluster(cryst);
|
||||
auto pool = RigidBodyGPUPool::Create(st.models[0], st.cell, *st.find_spacegroup(), 3.0, 100000, engines, logger);
|
||||
REQUIRE(pool);
|
||||
const RigidBodyRefineResult r = DisplacedFit(cryst, st, pool.get(), logger);
|
||||
CHECK(r.evaluations > 0);
|
||||
placed.push_back(ModelPositions(st.models[0]));
|
||||
}
|
||||
for (size_t k = 1; k < placed.size(); k++)
|
||||
for (size_t i = 0; i < placed[0].size(); i++) {
|
||||
CHECK(placed[k][i].x == placed[0][i].x);
|
||||
CHECK(placed[k][i].y == placed[0][i].y);
|
||||
CHECK(placed[k][i].z == placed[0][i].z);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user