RotationScaleMerge: GPU 3D-combine (CUDA port, phase 2 step 1)
Port Combine() (partials->fulls) to CUDA, mirroring process_rawrun bit-for-bit: one thread per raw-hkl run splits its usable partials into rocking events (frame gap <= 2), pools background, seeds F, runs 3 de-biased Poisson reweights and the capture-uncertainty term. Emission is deterministic - a count pass, a host exclusive prefix sum for per-run offsets, then an emit pass at those offsets - so fulls come out in raw-run-major/event order, identical to the CPU path; both pass instantiations share the same arithmetic so count == emit exactly. Dmax/Dmin/Fmax reproduce std::max/min NaN semantics (not fmax) for parity. Validated across the 18-crystal rotation battery: all 15 deterministic crystals (P1/P2/C2/H3/I23/P41212/P222/P422) match the CPU combine exactly on SG/ISa/CC1.2/ completeness and run-to-run (fulls count bit-identical); the 3 upstream-nondet crystals vary from GPU-prediction overflow, not the combine. Gated opt-in behind JFJOCH_RSM_GPU_COMBINE (default = CPU combine): combine alone is timing-neutral because the shared 1.2M SortFullsByFrame std::sort dominates and the fulls round-trip adds a copy - it only pays off once the fulls stay resident for scale-fulls + merge. Also add JFJOCH_RSM_NO_GPU master switch to force the CPU fallback (incl. phase-1 scaling) from one binary for A/B parity. SortFullsByFrame extracted from the Combine tail and shared by both paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <future>
|
||||
#include <limits>
|
||||
@@ -243,20 +244,27 @@ void RotationScaleMerge::Ingest() {
|
||||
// Bring the partial-scaling loop onto the GPU when one is present. Upload the immutable per-obs
|
||||
// fields once (corr lives on the device, refreshed each pass); the CPU keeps the sort/keying/combine.
|
||||
gpu_ = std::make_unique<RotationScaleMergeGPU>();
|
||||
gpu_active_ = gpu_->Available();
|
||||
gpu_active_ = gpu_->Available() && (std::getenv("JFJOCH_RSM_NO_GPU") == nullptr);
|
||||
if (gpu_active_) {
|
||||
const int n = static_cast<int>(partials.size());
|
||||
std::vector<float> I(n), sigma(n), rlp(n), part(n), zeta(n), corr(n);
|
||||
std::vector<float> I(n), sigma(n), rlp(n), part(n), zeta(n), corr(n), bkg(n), img(n), dd(n);
|
||||
std::vector<uint8_t> onice(n);
|
||||
std::vector<int32_t> frm(n);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const auto &o = partials[i];
|
||||
I[i] = o.I; sigma[i] = o.sigma; rlp[i] = o.rlp; part[i] = o.partiality;
|
||||
zeta[i] = o.zeta; onice[i] = o.on_ice; frm[i] = o.frame; corr[i] = o.corr;
|
||||
bkg[i] = o.bkg; img[i] = o.image_number; dd[i] = o.d;
|
||||
}
|
||||
gpu_->SetPartials(n, n_frames, I.data(), sigma.data(), rlp.data(), part.data(), zeta.data(),
|
||||
onice.data(), frm.data(), corr.data(), frame_start.data(), frame_count.data());
|
||||
logger.Info("RotationScaleMerge: GPU partial-scaling active");
|
||||
gpu_->SetCombineInputs(bkg.data(), img.data(), dd.data());
|
||||
gpu_->SetRawRuns(static_cast<int>(rawrun_start.size()), static_cast<int>(perm.size()), perm.data(),
|
||||
rawrun_start.data(), rawrun_count.data(),
|
||||
rawrun_h.data(), rawrun_k.data(), rawrun_l.data());
|
||||
gpu_combine_ = std::getenv("JFJOCH_RSM_GPU_COMBINE") != nullptr;
|
||||
logger.Info("RotationScaleMerge: GPU partial-scaling active{}",
|
||||
gpu_combine_ ? " (+ GPU combine)" : "");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -643,6 +651,11 @@ void RotationScaleMerge::Combine() {
|
||||
fulls.insert(fulls.end(), part[t].begin(), part[t].end());
|
||||
}
|
||||
|
||||
SortFullsByFrame();
|
||||
logger.Info("3D combine: {} fulls from {} partials", fulls.size(), n_used);
|
||||
}
|
||||
|
||||
void RotationScaleMerge::SortFullsByFrame() {
|
||||
// Sort the fulls by their (peak) frame and build per-frame CSR ranges, so the scale-fulls step can
|
||||
// fit a per-frame G by slicing contiguous ranges (the same layout the partials use).
|
||||
std::sort(fulls.begin(), fulls.end(),
|
||||
@@ -657,7 +670,6 @@ void RotationScaleMerge::Combine() {
|
||||
fulls_frame_count[f] = j - i;
|
||||
i = j;
|
||||
}
|
||||
logger.Info("3D combine: {} fulls from {} partials", fulls.size(), n_used);
|
||||
}
|
||||
|
||||
void RotationScaleMerge::FinalizePerFrameScale(int n_groups, const std::vector<double> &partial_group_mean,
|
||||
@@ -1085,7 +1097,37 @@ RotationScaleMerge::Result RotationScaleMerge::Run(bool for_search,
|
||||
FinalizePerFrameScale(n_groups, partial_mean, partial_scaled);
|
||||
|
||||
// --- 3. 3D combine of per-frame partials into fulls (fulls inherit their ASU group here). ---
|
||||
Combine();
|
||||
bool combined_on_gpu = false;
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
// The GPU combine mirrors Combine() exactly but keeps the fulls on the device; the diagnostic dump
|
||||
// (serial, one writer) has no GPU equivalent, so fall back to the CPU path when it is requested.
|
||||
if (gpu_active_ && gpu_combine_ && observation_dump_path.empty()) {
|
||||
std::vector<float> corr(partials.size()); // refresh the smoothed corr on the device
|
||||
for (size_t i = 0; i < partials.size(); ++i) corr[i] = partials[i].corr;
|
||||
gpu_->SetCorr(corr.data());
|
||||
const int nf = gpu_->Combine(rawrun_group.data(), min_partiality, capture_uncertainty_coeff);
|
||||
fulls.assign(nf, Obs{});
|
||||
g_full.assign(n_frames, 1.0);
|
||||
std::vector<int32_t> fh(nf), fk(nf), fl(nf), fframe(nf), fgroup(nf);
|
||||
std::vector<float> fI(nf), fsig(nf), fd(nf), fimg(nf);
|
||||
std::vector<uint8_t> fon(nf);
|
||||
gpu_->GetFulls(fh.data(), fk.data(), fl.data(), fI.data(), fsig.data(), fd.data(),
|
||||
fimg.data(), fframe.data(), fon.data(), fgroup.data());
|
||||
for (int i = 0; i < nf; ++i) {
|
||||
Obs &o = fulls[i];
|
||||
o.h = fh[i]; o.k = fk[i]; o.l = fl[i];
|
||||
o.I = fI[i]; o.sigma = fsig[i]; o.d = fd[i];
|
||||
o.rlp = 1.0f; o.partiality = 1.0f; o.corr = 1.0f;
|
||||
o.image_number = fimg[i]; o.frame = fframe[i];
|
||||
o.on_ice = fon[i]; o.group = fgroup[i];
|
||||
}
|
||||
SortFullsByFrame();
|
||||
logger.Info("3D combine (GPU): {} fulls", nf);
|
||||
combined_on_gpu = true;
|
||||
}
|
||||
#endif
|
||||
if (!combined_on_gpu)
|
||||
Combine();
|
||||
lap("combine");
|
||||
|
||||
// --- 4. Scale the fulls (XDS order, Unity model). ---
|
||||
|
||||
@@ -133,6 +133,11 @@ private:
|
||||
// inactive when no GPU; the CPU loops are the fallback. Built in Ingest.
|
||||
std::unique_ptr<RotationScaleMergeGPU> gpu_;
|
||||
bool gpu_active_ = false;
|
||||
// Phase-2 GPU combine (partials->fulls on the device). Opt-in for now (JFJOCH_RSM_GPU_COMBINE):
|
||||
// it is validated bit-for-bit against the CPU combine and deterministic, but until the fulls stay
|
||||
// resident for the scale-fulls + merge stages the fulls round-trip makes it no faster than the CPU
|
||||
// path, so it is not on by default. See RotationScaleMergeGPU::Combine.
|
||||
bool gpu_combine_ = false;
|
||||
#endif
|
||||
|
||||
// --- helpers (each a flat pass; see the .cpp) ---
|
||||
@@ -165,7 +170,11 @@ private:
|
||||
// that prediction applied. SG-independent, so done once in Ingest.
|
||||
void SmoothMosaicityAndPartiality();
|
||||
|
||||
void Combine(); // partials -> fulls
|
||||
void Combine(); // partials -> fulls (CPU)
|
||||
|
||||
// Sort `fulls` by peak frame and (re)build fulls_frame_start/count (the per-frame CSR the scale-fulls
|
||||
// step slices). Shared by the CPU Combine tail and the GPU combine path.
|
||||
void SortFullsByFrame();
|
||||
|
||||
// Per-frame CC vs the partial merge reference, then write G/CC/mosaicity back onto the partials
|
||||
// (once, at the end of partial scaling) so the offline per-image scaling table is still exported.
|
||||
|
||||
@@ -174,6 +174,168 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
// std::max / std::min return (a<b)?b:a and (b<a)?b:a - reproduce that exactly (NOT fmax/fmin, which
|
||||
// differ on NaN) so the combine matches the CPU path bit-for-bit on the same inputs.
|
||||
__device__ __forceinline__ double Dmax(double a, double b) { return (a < b) ? b : a; }
|
||||
__device__ __forceinline__ double Dmin(double a, double b) { return (b < a) ? b : a; }
|
||||
__device__ __forceinline__ float Fmax(float a, float b) { return (a < b) ? b : a; }
|
||||
|
||||
constexpr float COMBINE_MAX_FRAME_GAP = 2.0f; // == RotationScaleMerge::MAX_FRAME_GAP
|
||||
|
||||
// A partial is usable for the combine iff its corr and (I, sigma) are finite with corr>0, sigma>0.
|
||||
__device__ __forceinline__ bool CombineUsable(int i, const float *I, const float *sigma,
|
||||
const float *corr) {
|
||||
const float c = corr[i];
|
||||
if (!(c > 0.0f) || !isfinite(c)) return false;
|
||||
return isfinite(I[i]) && isfinite(sigma[i]) && sigma[i] > 0.0f;
|
||||
}
|
||||
|
||||
// All device pointers + scalars the combine kernels need, passed by value.
|
||||
struct CombineParams {
|
||||
int n_runs;
|
||||
double min_partiality, capture_uncertainty_coeff;
|
||||
const float *I, *sigma, *corr, *partiality, *bkg, *image_number, *d;
|
||||
const int32_t *frame;
|
||||
const uint8_t *on_ice;
|
||||
const int32_t *perm, *rr_start, *rr_count, *rr_h, *rr_k, *rr_l, *rr_group;
|
||||
int32_t *rr_nevents, *rr_nusable; // count pass outputs
|
||||
const int32_t *rr_offset; // emit pass: per-run base offset into the fulls arrays
|
||||
int32_t *f_h, *f_k, *f_l, *f_frame, *f_group;
|
||||
float *f_I, *f_sigma, *f_d, *f_img;
|
||||
uint8_t *f_on_ice;
|
||||
};
|
||||
|
||||
// One thread's raw-hkl run: split its usable partials (already in image_number order within the run)
|
||||
// into rocking events, and for each event pool background, seed F and run the 3-iter de-biased Poisson
|
||||
// reweight - the exact objective of RotationScaleMerge::Combine::process_rawrun. When Emit, write the
|
||||
// resulting full at rr_offset[r] + (event index); otherwise just count the emitted events. Both modes
|
||||
// run the identical accept test, so the count pass predicts the emit pass exactly.
|
||||
template <bool Emit>
|
||||
__device__ void CombineRawRun(int r, const CombineParams &p) {
|
||||
const int lo = p.rr_start[r];
|
||||
const int hi = lo + p.rr_count[r];
|
||||
const int group = p.rr_group[r];
|
||||
|
||||
int n_emit = 0;
|
||||
int cursor = lo;
|
||||
while (cursor < hi) {
|
||||
while (cursor < hi && !CombineUsable(p.perm[cursor], p.I, p.sigma, p.corr)) ++cursor;
|
||||
if (cursor >= hi) break;
|
||||
const int ev_start = cursor; // first usable position of the event
|
||||
int ev_end = cursor; // last usable position (inclusive), extended below
|
||||
float last_img = p.image_number[p.perm[cursor]];
|
||||
int probe = cursor + 1;
|
||||
while (probe < hi) {
|
||||
while (probe < hi && !CombineUsable(p.perm[probe], p.I, p.sigma, p.corr)) ++probe;
|
||||
if (probe >= hi) break;
|
||||
const float img = p.image_number[p.perm[probe]];
|
||||
if (img - last_img > COMBINE_MAX_FRAME_GAP) break;
|
||||
last_img = img;
|
||||
ev_end = probe;
|
||||
++probe;
|
||||
}
|
||||
cursor = ev_end + 1;
|
||||
|
||||
// Pass A: pooled background = mean of the event members' finite backgrounds.
|
||||
double pooled_bkg = 0.0;
|
||||
int n_pool = 0;
|
||||
for (int m = ev_start; m <= ev_end; ++m) {
|
||||
const int i = p.perm[m];
|
||||
if (!CombineUsable(i, p.I, p.sigma, p.corr)) continue;
|
||||
const float b = p.bkg[i];
|
||||
if (isfinite(b)) { pooled_bkg += b; ++n_pool; }
|
||||
}
|
||||
pooled_bkg = n_pool > 0 ? pooled_bkg / n_pool : 0.0;
|
||||
|
||||
auto pooled_I = [&](int i) -> double {
|
||||
const double n_bkg = Dmax(0.0, double(p.sigma[i]) * p.sigma[i] - p.I[i])
|
||||
/ Fmax(p.bkg[i], 1.0f);
|
||||
return double(p.I[i]) + n_bkg * (double(p.bkg[i]) - pooled_bkg);
|
||||
};
|
||||
|
||||
// Pass B: seed F (inverse-variance mean of pooled_I*corr), plus peak / d / on_ice / partiality.
|
||||
double sum_w = 0.0, sum_wI = 0.0, sum_partiality = 0.0;
|
||||
float d = NAN;
|
||||
const int first = p.perm[ev_start];
|
||||
int peak_outcome = p.frame[first];
|
||||
float peak_frame = p.image_number[first];
|
||||
float peak_partiality = -1.0f;
|
||||
const bool on_ice = p.on_ice[first];
|
||||
for (int m = ev_start; m <= ev_end; ++m) {
|
||||
const int i = p.perm[m];
|
||||
if (!CombineUsable(i, p.I, p.sigma, p.corr)) continue;
|
||||
const double sigma_corr = double(p.sigma[i]) * p.corr[i];
|
||||
const double w = 1.0 / (sigma_corr * sigma_corr);
|
||||
sum_w += w;
|
||||
sum_wI += w * pooled_I(i) * p.corr[i];
|
||||
sum_partiality += p.partiality[i];
|
||||
if (p.partiality[i] > peak_partiality) {
|
||||
peak_partiality = p.partiality[i];
|
||||
peak_outcome = p.frame[i];
|
||||
peak_frame = p.image_number[i];
|
||||
}
|
||||
if (!isfinite(d) && isfinite(p.d[i]) && p.d[i] > 0.0f) d = p.d[i];
|
||||
}
|
||||
double F = sum_wI / sum_w;
|
||||
|
||||
// Pass C: 3 de-biased Poisson reweights (variance = bkg part + corr*max(0,F)).
|
||||
for (int iter = 0; iter < 3; ++iter) {
|
||||
sum_w = 0.0; sum_wI = 0.0;
|
||||
for (int m = ev_start; m <= ev_end; ++m) {
|
||||
const int i = p.perm[m];
|
||||
if (!CombineUsable(i, p.I, p.sigma, p.corr)) continue;
|
||||
const double corr = p.corr[i];
|
||||
const double I_corr = pooled_I(i) * corr;
|
||||
const double sigma_corr = double(p.sigma[i]) * corr;
|
||||
const double bkg_var = sigma_corr * sigma_corr - corr * I_corr;
|
||||
double var = Dmax(0.0, bkg_var) + corr * Dmax(0.0, F);
|
||||
if (!(var > 0.0)) var = sigma_corr * sigma_corr;
|
||||
const double w = 1.0 / var;
|
||||
sum_w += w;
|
||||
sum_wI += w * I_corr;
|
||||
}
|
||||
F = sum_wI / sum_w;
|
||||
}
|
||||
|
||||
if (sum_w <= 0.0 || sum_partiality < p.min_partiality)
|
||||
continue;
|
||||
|
||||
double sigma_full = 1.0 / sqrt(sum_w);
|
||||
if (p.capture_uncertainty_coeff > 0.0) {
|
||||
const double frac = Dmin(1.0, sum_partiality);
|
||||
const double extra = p.capture_uncertainty_coeff * (1.0 - frac) * Dmax(0.0, F);
|
||||
sigma_full = sqrt(sigma_full * sigma_full + extra * extra);
|
||||
}
|
||||
|
||||
if (Emit) {
|
||||
const int o = p.rr_offset[r] + n_emit;
|
||||
p.f_h[o] = p.rr_h[r]; p.f_k[o] = p.rr_k[r]; p.f_l[o] = p.rr_l[r];
|
||||
p.f_I[o] = float(F);
|
||||
p.f_sigma[o] = float(sigma_full);
|
||||
p.f_d[o] = d;
|
||||
p.f_img[o] = peak_frame;
|
||||
p.f_frame[o] = peak_outcome;
|
||||
p.f_on_ice[o] = on_ice ? 1 : 0;
|
||||
p.f_group[o] = group;
|
||||
}
|
||||
++n_emit;
|
||||
}
|
||||
|
||||
if (!Emit) {
|
||||
p.rr_nevents[r] = n_emit;
|
||||
int n_usable = 0;
|
||||
for (int m = lo; m < hi; ++m)
|
||||
if (CombineUsable(p.perm[m], p.I, p.sigma, p.corr)) ++n_usable;
|
||||
p.rr_nusable[r] = n_usable;
|
||||
}
|
||||
}
|
||||
|
||||
template <bool Emit>
|
||||
__global__ void CombineKernel(CombineParams p) {
|
||||
for (int r = blockIdx.x * blockDim.x + threadIdx.x; r < p.n_runs; r += gridDim.x * blockDim.x)
|
||||
CombineRawRun<Emit>(r, p);
|
||||
}
|
||||
|
||||
void CudaCheck(cudaError_t e, const char *what) {
|
||||
if (e != cudaSuccess)
|
||||
throw JFJochException(JFJochExceptionCategory::GPUCUDAError,
|
||||
@@ -205,6 +367,17 @@ struct RotationScaleMergeGPU::Impl {
|
||||
CudaDevicePtr<uint8_t> scaled;
|
||||
CudaDevicePtr<float> sco_coeff;
|
||||
CudaDevicePtr<uint8_t> sco_ok;
|
||||
|
||||
// combine: extra per-obs inputs + the one-time raw-hkl run layout
|
||||
CudaDevicePtr<float> bkg, image_number, d_obs;
|
||||
int n_runs = 0, n_perm = 0;
|
||||
CudaDevicePtr<int32_t> perm, rr_start, rr_count, rr_h, rr_k, rr_l, rr_group;
|
||||
CudaDevicePtr<int32_t> rr_nevents, rr_nusable, rr_offset;
|
||||
// combine: resident fulls SoA (rebuilt each Combine)
|
||||
int n_fulls = 0;
|
||||
CudaDevicePtr<int32_t> f_h, f_k, f_l, f_frame, f_group;
|
||||
CudaDevicePtr<float> f_I, f_sigma, f_d, f_img;
|
||||
CudaDevicePtr<uint8_t> f_on_ice;
|
||||
};
|
||||
|
||||
RotationScaleMergeGPU::RotationScaleMergeGPU() : impl_(std::make_unique<Impl>()) {
|
||||
@@ -287,3 +460,100 @@ void RotationScaleMergeGPU::GetG(double *g_out, uint8_t *scaled_out) const {
|
||||
CudaCheck(cudaMemcpy(scaled_out, impl_->scaled.get(), size_t(impl_->n_frames) * sizeof(uint8_t),
|
||||
cudaMemcpyDeviceToHost), "download scaled");
|
||||
}
|
||||
|
||||
void RotationScaleMergeGPU::SetCombineInputs(const float *bkg, const float *image_number, const float *d) {
|
||||
auto &dd = *impl_;
|
||||
Upload(dd.bkg, bkg, dd.n_obs);
|
||||
Upload(dd.image_number, image_number, dd.n_obs);
|
||||
Upload(dd.d_obs, d, dd.n_obs);
|
||||
}
|
||||
|
||||
void RotationScaleMergeGPU::SetRawRuns(int n_runs, int n_perm, const int32_t *perm,
|
||||
const int32_t *rr_start, const int32_t *rr_count,
|
||||
const int32_t *rr_h, const int32_t *rr_k, const int32_t *rr_l) {
|
||||
auto &d = *impl_;
|
||||
d.n_runs = n_runs;
|
||||
d.n_perm = n_perm;
|
||||
Upload(d.perm, perm, n_perm);
|
||||
Upload(d.rr_start, rr_start, n_runs);
|
||||
Upload(d.rr_count, rr_count, n_runs);
|
||||
Upload(d.rr_h, rr_h, n_runs);
|
||||
Upload(d.rr_k, rr_k, n_runs);
|
||||
Upload(d.rr_l, rr_l, n_runs);
|
||||
d.rr_group = CudaDevicePtr<int32_t>(std::max(1, n_runs));
|
||||
d.rr_nevents = CudaDevicePtr<int32_t>(std::max(1, n_runs));
|
||||
d.rr_nusable = CudaDevicePtr<int32_t>(std::max(1, n_runs));
|
||||
d.rr_offset = CudaDevicePtr<int32_t>(std::max(1, n_runs));
|
||||
}
|
||||
|
||||
int RotationScaleMergeGPU::Combine(const int32_t *rawrun_group, double min_partiality,
|
||||
double capture_uncertainty_coeff) {
|
||||
auto &d = *impl_;
|
||||
CudaCheck(cudaMemcpy(d.rr_group.get(), rawrun_group, size_t(d.n_runs) * sizeof(int32_t),
|
||||
cudaMemcpyHostToDevice), "upload rr_group");
|
||||
|
||||
CombineParams p{};
|
||||
p.n_runs = d.n_runs;
|
||||
p.min_partiality = min_partiality;
|
||||
p.capture_uncertainty_coeff = capture_uncertainty_coeff;
|
||||
p.I = d.I.get(); p.sigma = d.sigma.get(); p.corr = d.corr.get(); p.partiality = d.partiality.get();
|
||||
p.bkg = d.bkg.get(); p.image_number = d.image_number.get(); p.d = d.d_obs.get();
|
||||
p.frame = d.frame.get(); p.on_ice = d.on_ice.get();
|
||||
p.perm = d.perm.get(); p.rr_start = d.rr_start.get(); p.rr_count = d.rr_count.get();
|
||||
p.rr_h = d.rr_h.get(); p.rr_k = d.rr_k.get(); p.rr_l = d.rr_l.get(); p.rr_group = d.rr_group.get();
|
||||
p.rr_nevents = d.rr_nevents.get(); p.rr_nusable = d.rr_nusable.get();
|
||||
|
||||
const int blocks = std::min(65535, (d.n_runs + BLK - 1) / BLK);
|
||||
|
||||
// Count pass: how many fulls each run emits.
|
||||
CombineKernel<false><<<blocks, BLK>>>(p);
|
||||
CudaCheck(cudaGetLastError(), "combine count launch");
|
||||
|
||||
// Exclusive prefix sum on the host (deterministic) -> per-run output offset + total fulls.
|
||||
std::vector<int32_t> nevents(d.n_runs);
|
||||
CudaCheck(cudaMemcpy(nevents.data(), d.rr_nevents.get(), size_t(d.n_runs) * sizeof(int32_t),
|
||||
cudaMemcpyDeviceToHost), "download nevents");
|
||||
std::vector<int32_t> offset(d.n_runs);
|
||||
int64_t acc = 0;
|
||||
for (int r = 0; r < d.n_runs; ++r) { offset[r] = static_cast<int32_t>(acc); acc += nevents[r]; }
|
||||
d.n_fulls = static_cast<int>(acc);
|
||||
|
||||
// Allocate the fulls SoA and emit.
|
||||
const int nf = std::max(1, d.n_fulls);
|
||||
d.f_h = CudaDevicePtr<int32_t>(nf); d.f_k = CudaDevicePtr<int32_t>(nf); d.f_l = CudaDevicePtr<int32_t>(nf);
|
||||
d.f_frame = CudaDevicePtr<int32_t>(nf); d.f_group = CudaDevicePtr<int32_t>(nf);
|
||||
d.f_I = CudaDevicePtr<float>(nf); d.f_sigma = CudaDevicePtr<float>(nf);
|
||||
d.f_d = CudaDevicePtr<float>(nf); d.f_img = CudaDevicePtr<float>(nf);
|
||||
d.f_on_ice = CudaDevicePtr<uint8_t>(nf);
|
||||
CudaCheck(cudaMemcpy(d.rr_offset.get(), offset.data(), size_t(d.n_runs) * sizeof(int32_t),
|
||||
cudaMemcpyHostToDevice), "upload offset");
|
||||
|
||||
p.rr_offset = d.rr_offset.get();
|
||||
p.f_h = d.f_h.get(); p.f_k = d.f_k.get(); p.f_l = d.f_l.get();
|
||||
p.f_frame = d.f_frame.get(); p.f_group = d.f_group.get();
|
||||
p.f_I = d.f_I.get(); p.f_sigma = d.f_sigma.get(); p.f_d = d.f_d.get(); p.f_img = d.f_img.get();
|
||||
p.f_on_ice = d.f_on_ice.get();
|
||||
if (d.n_fulls > 0) {
|
||||
CombineKernel<true><<<blocks, BLK>>>(p);
|
||||
CudaCheck(cudaGetLastError(), "combine emit launch");
|
||||
}
|
||||
CudaCheck(cudaDeviceSynchronize(), "combine sync");
|
||||
return d.n_fulls;
|
||||
}
|
||||
|
||||
void RotationScaleMergeGPU::GetFulls(int32_t *h, int32_t *k, int32_t *l, float *I, float *sigma, float *d,
|
||||
float *image_number, int32_t *frame, uint8_t *on_ice,
|
||||
int32_t *group) const {
|
||||
const auto &dd = *impl_;
|
||||
const size_t n = static_cast<size_t>(dd.n_fulls);
|
||||
if (n == 0) return;
|
||||
auto dl = [&](void *dst, const void *src, size_t bytes) {
|
||||
CudaCheck(cudaMemcpy(dst, src, bytes, cudaMemcpyDeviceToHost), "download fulls");
|
||||
};
|
||||
dl(h, dd.f_h.get(), n * sizeof(int32_t)); dl(k, dd.f_k.get(), n * sizeof(int32_t));
|
||||
dl(l, dd.f_l.get(), n * sizeof(int32_t)); dl(frame, dd.f_frame.get(), n * sizeof(int32_t));
|
||||
dl(group, dd.f_group.get(), n * sizeof(int32_t));
|
||||
dl(I, dd.f_I.get(), n * sizeof(float)); dl(sigma, dd.f_sigma.get(), n * sizeof(float));
|
||||
dl(d, dd.f_d.get(), n * sizeof(float)); dl(image_number, dd.f_img.get(), n * sizeof(float));
|
||||
dl(on_ice, dd.f_on_ice.get(), n * sizeof(uint8_t));
|
||||
}
|
||||
|
||||
@@ -54,6 +54,32 @@ public:
|
||||
void GetCorr(float *corr_out) const;
|
||||
void GetG(double *g_out, uint8_t *scaled_out) const;
|
||||
|
||||
// --- 3D combine (partials -> fulls), all on the device ---
|
||||
|
||||
// The per-obs fields the combine needs on top of the scaling inputs (image-local bkg, fractional
|
||||
// frame position for event contiguity, resolution). Uploaded once, alongside SetPartials.
|
||||
void SetCombineInputs(const float *bkg, const float *image_number, const float *d);
|
||||
|
||||
// The one-time raw-hkl run layout (space-group-independent): the (raw h,k,l, image_number)-sorted
|
||||
// permutation of the obs, split into contiguous per-raw-hkl runs. Uploaded once in Ingest.
|
||||
void SetRawRuns(int n_runs, int n_perm, const int32_t *perm,
|
||||
const int32_t *rawrun_start, const int32_t *rawrun_count,
|
||||
const int32_t *rawrun_h, const int32_t *rawrun_k, const int32_t *rawrun_l);
|
||||
|
||||
// Combine the resident partials (reading the current resident corr) into fulls on the device,
|
||||
// mirroring RotationScaleMerge::Combine: one thread per raw-hkl run splits its usable partials into
|
||||
// rocking events (frame gap <= 2), pools background, seeds F, does 3 de-biased Poisson reweights and
|
||||
// adds the capture-uncertainty term. rawrun_group (length n_runs) is the current space group's ASU
|
||||
// id per raw hkl (it becomes the full's group). Deterministic: fulls are emitted in raw-run-major,
|
||||
// event order (a count pass -> host prefix sum -> emit-at-offset), matching the CPU path. Returns the
|
||||
// number of fulls (call GetFulls with buffers of that length).
|
||||
int Combine(const int32_t *rawrun_group, double min_partiality, double capture_uncertainty_coeff);
|
||||
|
||||
// Download the combined fulls SoA (length = Combine()'s return). corr/partiality/rlp are 1 by
|
||||
// construction and not returned; the caller sets them.
|
||||
void GetFulls(int32_t *h, int32_t *k, int32_t *l, float *I, float *sigma, float *d,
|
||||
float *image_number, int32_t *frame, uint8_t *on_ice, int32_t *group) const;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
|
||||
Reference in New Issue
Block a user