sfm: speed up mapper / BA solver

This commit is contained in:
Harry Chen
2026-09-25 18:03:34 -04:00
parent caee88fa71
commit 4e9849161c
16 changed files with 1342 additions and 154 deletions
+1
View File
@@ -259,6 +259,7 @@ int cmdBa(int argc, char** argv) {
if (cmp_step) {
opt.solver = SolverSel::CG;
opt.cg_fallback = CgFallback::On;
opt.cg_model_tol = 0; // --cg-tol alone sets how exact the step is
}
// A directory is a COLMAP sparse model: build the problem the mapper's
+38
View File
@@ -699,6 +699,11 @@ What the run does with it, in the order it happens:
through the calibration exactly as if they shared those images, so a 360
capture that reconstructs as one component per direction is merged rather
than written as pieces (`map/Merge.h` `poseCorrespondences`).
- **Splitting.** The consistency split (`splitInconsistent`) groups images by
the verified pairs a model still agrees with, and counts a calibrated rig
frame as joining its images too. Back-to-back fisheyes share no matches, so
without that a 4000-image dual-fisheye model with every pair agreeing split
into its two lenses and spent five minutes merging them back.
`--final-free-rig` (off) runs one last bundle adjustment with the rig set
aside, for a mount that flexed or lenses that did not fire together. With no
@@ -804,6 +809,39 @@ adjacent pairs' own two-view geometry, or the video's IMU. `SS_SFM_SEQ_DUMP=1`
prints one line per registration attempt (near and whole-pool inliers, and
the rival's) to read such a spot from the log.
### Retriangulation
Every global refinement round after the first starts by completing tracks and
retriangulating the whole model (`completeAndRetriangulate`, COLMAP's
CompleteTracks + Retriangulate): each point's correspondences are tested
against it, and each registered image's free features against their
registered partners. Both were serial whole-model passes, so on a long video
they were the stretch between two bundle adjustments where the GPU idles and
one core works -- 34 s of a 3000-frame capture's 332 s of mapping, as much as
the seed search and registration together.
Each half now collects its per-item result on every core against the state
the pass starts from, and commits in the old serial order. A claim only ever
takes a free feature, so an item's collected result can be wrong only if an
earlier commit took one of the features it claims; that item is redone
serially at its turn. The output is the serial pass's exactly -- checked by
running both on the same state, 0 differences over 373 passes and every
growth-time triangulation of a 638-image capture. Growth-time triangulation
after each registration goes the same way, feature by feature.
Most of what the pass then does is futile: on a 4000-image dual-fisheye video
it tried 166M candidate pairs a pass for 13M free features and made no point
from any of them in steady state -- neighbouring frames 1/30 s apart are all
far under the 1.5 degree triangulation angle. Two rays can only reach that
angle if the angle between them is within both reprojection tolerances of it
(each bounded as twice the tolerance over the focal), so a candidate further
off is dropped before triangulation, on world-frame rays cached per pass in
float (`worldRays`; the fisheye bearing behind each is an iterative inversion,
and computing it per candidate was the actual cost). That is 94% of the
candidates there; results are identical with the filter on and off on all
three stress captures. A pass over the 4000-image model went from 27.3 s to
4.9 s, over a 3000-frame DJI walk from 8.1 s to 0.6 s.
### The finishing passes
Reconstruction ends with up to two more global bundle adjustments, on models
+14 -2
View File
@@ -60,8 +60,13 @@ public:
factor(pool, nthreads);
solveInPlace(g, pool, nthreads);
}
void solve(double* g, Pool& pool, int nthreads) { solveInPlace(g, pool, nthreads); }
void factor(Pool& pool, int nthreads) {
// With `diag`, a pivot under `rel` of its row's diag entry is replaced by
// that entry (cholesky.slang's pivot(), for the coarse matrix).
void factor(Pool& pool, int nthreads, const double* diag = nullptr, double rel = 0) {
diag_in_ = diag;
rel_ = rel;
const uint32_t nb = (n_ + kBlock - 1) / kBlock;
for (uint32_t k = 0; k < nb; k++) {
const uint32_t base = k * kBlock;
@@ -120,7 +125,12 @@ private:
void factorDiag(uint32_t base, uint32_t m) {
for (uint32_t j = 0; j < m; j++) {
double* Rj = row(base + j) + base;
double d = std::sqrt(std::max(Rj[j], 1e-30));
double d;
if (!diag_in_)
d = std::sqrt(Rj[j] > 1e-30 ? Rj[j] : 1e-30);
else
d = std::sqrt(Rj[j] > rel_ * diag_in_[base + j] ? Rj[j]
: std::max(diag_in_[base + j], 1e-30));
Rj[j] = d;
const double inv = 1.0 / d;
for (uint32_t r = j + 1; r < m; r++) {
@@ -254,6 +264,8 @@ private:
}
std::vector<double> a_, panel_, panelT_, diag_;
const double* diag_in_ = nullptr;
double rel_ = 0;
uint32_t n_ = 0;
};
+4
View File
@@ -97,6 +97,10 @@ struct SolverOptions {
bool over_budget_throws = false;
int cg_max_iters = 100; // CG iteration cap per LM step
double cg_tol = 0.1; // relative residual tolerance eta
// ... and CG also stops once a step improves the quadratic model by under
// this fraction of the total so far (Nash-Sofer; 0 = off). It settles for a
// residual near sqrt of it, so a caller that wants the exact step turns it off.
double cg_model_tol = 0.1;
CgFallback cg_fallback = CgFallback::Auto;
// The kernels are compiled per (real, loss); `loss` selects the embedded
// blob "ba_<real>_<loss>". spv_path overrides it with a module from disk
+65
View File
@@ -16,6 +16,7 @@
#include <string>
#include <vector>
#include "core/Env.h"
#include "sfm/core/Log.h"
// Camera model registry; must match the entry points in sfm/shaders/ba/ba.slang.
@@ -211,6 +212,70 @@ inline uint64_t pairEntryCount(const BAProblem& P) {
static const uint64_t kMaxPairEntries = 400ull << 20; // 3.2 GB of entry data
// Coarse-correction tables (cg.slang). A run is a track's observations in one
// cluster of k frames (contiguous: tracks are sorted by image); each pair of a
// point's runs u >= v is one entry, (first obs of u, first obs of v).
template <class F>
inline void forCoarseRunPairs(const BAProblem& P, uint32_t k, F&& fn) {
auto cl = [&](uint32_t o) { return P.image_frame[P.obs_image[o]] / k; };
std::vector<uint32_t> runs;
for (uint32_t p = 0; p < P.num_points; p++) {
runs.clear();
for (uint32_t o = P.obs_ranges[p]; o < P.obs_ranges[p + 1]; o++)
if (o == P.obs_ranges[p] || cl(o) != cl(o - 1)) runs.push_back(o);
for (size_t u = 0; u < runs.size(); u++)
for (size_t v = 0; v <= u; v++) {
const uint64_t cu = cl(runs[u]), cv = cl(runs[v]);
fn(cu * (cu + 1) / 2 + cv, runs[u], runs[v]);
}
}
}
inline uint64_t coarseEntryCount(const BAProblem& P, uint32_t k) {
uint64_t n = 0;
forCoarseRunPairs(P, k, [&](uint64_t, uint32_t, uint32_t) { n++; });
return n;
}
// Entries grouped by cluster pair: `key` holds each pair's range, indexed
// cu (cu + 1) / 2 + cv.
inline void buildCoarseEntries(const BAProblem& P, uint32_t k, std::vector<uint32_t>& ent,
std::vector<uint32_t>& key) {
const uint64_t nc = (P.num_frames + k - 1) / k, nkeys = nc * (nc + 1) / 2;
key.assign(nkeys + 1, 0);
forCoarseRunPairs(P, k, [&](uint64_t kk, uint32_t, uint32_t) { key[kk + 1]++; });
for (uint64_t i = 0; i < nkeys; i++) key[i + 1] += key[i];
std::vector<uint32_t> fill(key.begin(), key.end() - 1);
ent.resize(2 * (size_t)key[nkeys]);
forCoarseRunPairs(P, k, [&](uint64_t kk, uint32_t a, uint32_t b) {
const uint32_t e = fill[kk]++;
ent[2 * (size_t)e] = a;
ent[2 * (size_t)e + 1] = b;
});
}
// Clusters as small as a coarse matrix of `max_dim` allows (7 dofs each), and
// as large as keeps the entries under two an observation (long tracks span many
// small clusters). False: fewer than 4 clusters, or SS_SFM_BA_COARSE=0.
inline bool planCoarse(const BAProblem& P, uint32_t max_dim, uint32_t& k, uint32_t& dim,
uint64_t& entries) {
k = dim = 0;
entries = 0;
const char* e = spirula::env("SFM_BA_COARSE");
if (e && std::atoi(e) == 0) return false;
const uint32_t nf = P.num_frames;
for (uint32_t kk = std::max<uint32_t>(2, (7 * nf + max_dim - 1) / max_dim);
(nf + kk - 1) / kk >= 4; kk *= 2) {
const uint64_t n = coarseEntryCount(P, kk);
if (n > 2 * (uint64_t)P.num_obs) continue;
k = kk;
dim = 7 * ((nf + kk - 1) / kk);
entries = n;
return true;
}
return false;
}
// Group observations by image (CSR) for the CG path's per-camera kernels.
inline void buildCamTables(BAProblem& P) {
P.cam_obs_ranges.assign(P.num_images + 1, 0);
+106 -13
View File
@@ -288,16 +288,103 @@ state (rho, alpha, beta, tolerance) lives in a small device buffer, dot
products are two-stage reductions, and every kernel no-ops once the
convergence flag is set, so a fixed iteration cap is recorded (adapted each
LM iteration from the previous count) and the LM loop keeps its single
cost readback. Stopping rule: relative residual `--cg-tol` (default 0.1,
inexact-Newton style -- LM's accept/reject guards the step quality) with a
`--cg-iters` cap (default 100).
cost readback.
Stopping rule: relative residual `--cg-tol` (default 0.1, inexact-Newton
style -- LM's accept/reject guards the step quality), or, whichever comes
first, the last step lowering the quadratic model `Q = x^T S x / 2 - g^T x` by
less than `SolverOptions::cg_model_tol` (default 0.1) of the average step so
far -- Nash and Sofer's truncated-Newton test, `i (Q_i - Q_i-1) / Q_i`, which
is how Ceres stops its CG under LM. `Q` costs nothing to track: a CG step
lowers it by `alpha rho / 2`. The residual test alone keeps iterating after
the steps have stopped mattering: on the 4102-image capture below the model
test takes 40% fewer iterations (2376 against 3954 over a 50-iteration solve,
34 s against 55 s) to the same final cost, 1.879209721e6 to all ten digits
printed. It settles for a residual near the square root of its tolerance,
though, so a caller that wants the exact step (the tests, `SS_SFM_CMP_STEP`)
sets it to 0. `--cg-iters` caps both (default 100).
### Coarse correction (two-level preconditioner)
Block Jacobi sees one camera at a time, and the slow modes of a long capture
are not local: a stretch of the trajectory bending, twisting or drifting in
scale *as a whole*, which every camera block finds nearly free. On a
4102-image dataset, CG hit its 100-iteration cap from the
fourth LM iteration on, and the truncated steps fell behind the dense
solver's: 1.879283e6 after 10 LM iterations against its 1.879271e6. Holding
the intrinsics fixed changed nothing, so it is not the dropped pose/intrinsics
coupling.
So the preconditioner has a second level: `M^-1 = M_J^-1 + P A_c^-1 P^T` with
`A_c = P^T S P`. `P` gives every cluster of `k` consecutive frames 7 dofs, a
similarity motion of the world (rotation `w`, translation `tau`, scale `s`),
mapped onto each frame's pose parameters exactly: `d(angle-axis) = -Jr^-1 w`,
`dt = s t - R tau` (`tc_basis`, in fp32 -- the basis only has to span the slow
modes). A constant pose delta per cluster instead was measured and is clearly
worse (66 and 100 CG iterations where the similarity basis takes 35 and 50):
it cannot move a cluster rigidly unless every frame in it faces the same way.
`A_c` comes from the same per-observation Jacobians as everything else: the
`B` part per image from `cg_cam_diag`'s Gram blocks (`tc_bpart`), the Schur
part per pair of *runs* -- a track's observations inside one cluster, which
are contiguous since tracks are sorted by image -- grouped by cluster pair on
the host so that one warp sums a coarse block and writes it once
(`tc_schur`; issuing its sums as atomics per point was 145 ms a build against
80). It is factored by the dense kernels in the packed-`S` buffer the CG path
otherwise leaves empty, and the factor is then inverted in place (`tc_dinv`,
`tc_inv_row`), so applying it is two triangular matrix-vector products
(`tc_lmul`, `tc_ltmul`, ~0.17 ms at 3591 dofs) rather than 2 n / 32 dependent
solve dispatches (~4 ms, more than half a CG iteration).
Size and cost: `k` is the smallest cluster that keeps `A_c` under 4096 dofs
and the run-pair table under two entries per observation (long tracks span
many small clusters) -- 8 frames on the 4102-image capture, 38 on a
22042-image one. The extra VRAM is the table and the frame basis; `A_c`
itself (67 MB at most) reuses packed `S`. A build is the size of a dense
factor, so it happens only when the last CG solve took more than 12
iterations, and is reused for up to three solves unless the damping has moved
tenfold -- a stale `A_c` only costs iterations.
One global BA each (`spirula sfm ba`, defaults, RTX 5070), before and after
this correction and the stopping rule above:
| capture | solver | CG its/solve | solve | final cost |
|---|---|---|---|---|
| 4102-image (well-conditioned) | cg | 89.8 -> 50.4 | 29.1 s -> 31.6 s (30 -> 49 LM its) | 1.879241e6 -> 1.879210e6 |
| 6946-image (dual-fisheye rig) | cg | 81.0 -> 41.8 | 97.6 s -> 51.2 s | 4.659347e6 -> 4.659219e6 |
| 22042-image ([campus](https://repo-sam.inria.fr/fungraph/hierarchical-3d-gaussians/datasets/)) | cg | 47.5 -> 73.7 | 120.2 s -> 193.4 s (47 -> 50 LM its) | 1.046347e7 -> 1.044832e7 |
| 1068-image (linear trajectory) | dense -> cg | -- -> 8.1 | 56.7 s -> 3.6 s | 1.822009e6 both |
Where a run got longer it is because it no longer stalls: the old 4102-image
solve stopped on tie-zone patience, and the campus one sat at 1.0483e7 from
iteration 12 to 20 where the new one passes its final cost at iteration 14
(about 55 s) and goes on improving.
On the 4102-image dataset the coarse-corrected CG tracks the dense solver's cost to
seven digits iteration for iteration (1.879220953e6 against 1.879220943e6 at
iteration 29), which the plain one never does; it then keeps improving where
the plain one stops on its tie-zone patience. `SS_SFM_BA_COARSE=0` turns it
off; the host solver has the same correction (`coarseBuild`).
The global similarity is a gauge freedom, so `A_c` is singular up to the
damping. Its diagonal is scaled by `1 + 1e-8`, and a pivot under 1e-6 of its
row's original diagonal is replaced by that diagonal (`pivot()` in
`cholesky.slang`), which keeps the factor bounded however far rounding has
made the matrix indefinite. If a CG solve still stops before its first step,
the iteration is redone without `A_c`, and it stays off for the solve when
that was the difference.
The CG path allocates no packed S, no Y, and no pair-entry lists, so VRAM
stays linear in observations. Preprocessing also skips the pair-table sort (3.6 s -> 1.2 s
there). Solver selection and the dense fallback are VRAM-aware: the budget
is `--vram-budget` (default 90% of the device-local heap); `auto` picks
dense for small problems, CG when the dense estimate exceeds the budget or
`n_dim > 8192`. With `--cg-fallback on` (or `auto`, which enables it only
dense for small problems, CG when the dense estimate exceeds the budget,
`n_dim > 8192`, or CG's estimated iteration costs under half the dense one's.
That last test is about track length: the dense assembly is quadratic in it
(`schur_obs` walks the track per observation), CG linear. A 1068-image
capture of a robot drifting slowly through one module, 70 observations per
track by `sum t^2 / sum t`, spent 3.7 s an iteration in `schur_obs` alone and
59 s a solve; on CG it is 6.3 s, to the same final cost. With `--cg-fallback on` (or `auto`, which enables it only
when dense+CG together fit in half the budget) the dense machinery is kept
allocated; a truncated-CG step is kept if it still lowered the cost, and
only a cost-raising capped solve is re-solved densely from the reused
@@ -310,6 +397,14 @@ inexact steps on its own, at zero extra memory.
step difference: with `--cg-tol 1e-8` the CG step matches the dense step to
~1e-8 at fp64.
Two guards keep a badly scaled problem from breaking CG. A 22042-image model
with a point 2e-9 from a camera centre has camera Gram entries of 1e23, and
below damping ~1e-9 rounding makes `S` indefinite there: the LM damping is
floored at 1e-8, which is still Gauss-Newton to eight digits, and a CG solve
that stops before its first step counts as a failed step (damping up) rather
than a zero one. The block-Jacobi factor floors a pivot under 1e-10 of its
block's largest diagonal and decouples it, where it used to cascade to NaN.
### Host fallback (`--real cpu`)
`pickRealForDevice` steps down `double` -> `cpu`, and `cpu` is the same solver
@@ -484,14 +579,12 @@ peak (the largest single BA being 6372 images / 12.7 M observations).
400M-entry cap it falls back to the atomic per-observation Schur kernel),
and packed indexing is 32-bit: `n(n+1)/2 < 2^31` (n ≲ 65k camera DOF).
Neither limit applies to the CG path.
- CG iteration counts are conditioning-dependent: on well-behaved covis
graphs (1936) ~9 iters/solve; on the outlier-heavy 871 ~56, which is why
auto keeps problems below `n_dim = 8192` on the dense path. A stronger
preconditioner (visibility clustering / power series) is the known next
step for ill-conditioned sets -- and the more so now that sharing is
allowed, since a shared group costs the preconditioner its pose/intrinsics
coupling: the 4194-image capture above converges in 10 iterations/solve, but
the 6281-image one sits at the 100 cap for its final refinement pass.
- CG iteration counts are conditioning-dependent. The coarse correction
takes care of the long-wavelength modes of a long capture, but its clusters
are consecutive frames, so it assumes image order follows the trajectory
(true of video and of most photo walks); a shuffled collection gets a
valid but weaker coarse space. At damping ~1e-7 the 4102-image dataset still
needs 80-100 iterations per solve.
- fp32 CG inherits the documented fp32 normal-equation stall (block-Jacobi
is nearly exact at high damping, so it still descends, but final cost is
looser than fp64/df -- same as fp32 dense, slightly amplified).
+227 -28
View File
@@ -11,6 +11,7 @@
#include <chrono>
#include <cmath>
#include <cstdint>
#include <limits>
#include <map>
#include <mutex>
#include <set>
@@ -189,7 +190,8 @@ public:
bModelObs_ = mkUint(P_.model_obs.size());
bJcOff_ = mkUint(P_.num_obs);
bJp_ = mkReal(6 * (uint64_t)P_.num_obs);
bS_ = mkReal(needS ? packed : 1);
const uint64_t packedTc = (uint64_t)tcN_ * (tcN_ + 1) / 2;
bS_ = mkReal(std::max<uint64_t>(needS ? packed : 1, packedTc + 42 * (uint64_t)P_.num_frames));
bG_ = mkReal(P_.n_dim);
bApp_ = mkReal(9 * (uint64_t)P_.num_points);
bBp_ = mkReal(3 * (uint64_t)P_.num_points);
@@ -198,8 +200,8 @@ public:
bExtsBak_ = mkReal(P_.exts.size());
bIntrBak_ = mkReal(P_.total_intr);
bPointsBak_ = mkReal(3 * (uint64_t)P_.num_points);
bPairEntries_ = mkUint(std::max<size_t>(P_.pair_entries.size(), 2));
bPairChunks_ = mkUint(std::max<size_t>(P_.pair_chunks.size(), 2));
bPairEntries_ = mkUint(std::max<size_t>(P_.pair_entries.size() + tcEnt_.size(), 2));
bPairChunks_ = mkUint(std::max<size_t>(P_.pair_chunks.size() + tcChk_.size(), 2));
// S/g are rebuilt from the per-observation Jacobians on every path, so
// a rejected step needs no snapshot of them -- only Bp, which the
// point back-substitution overwrites in place.
@@ -208,7 +210,7 @@ public:
bRes_ = mkReal(2 * (uint64_t)P_.num_obs);
bW_ = mkReal(9 * (uint64_t)P_.num_points);
bYp_ = mkReal(needS && P_.use_pair_schur ? 6 * (uint64_t)P_.num_obs : 1);
bY_ = mkReal(P_.n_dim);
bY_ = mkReal(std::max<uint64_t>(P_.n_dim, 34 * (uint64_t)tcN_));
// CG path buffers (1-element dummies when unused)
bCamRanges_ = mkUint(cgAllocated_ ? P_.num_images + 1 : 1);
bCamObs_ = mkUint(cgAllocated_ ? P_.num_obs : 1);
@@ -220,7 +222,7 @@ public:
bCgV_ = mkReal(cgAllocated_ ? 3 * (uint64_t)P_.num_points : 1);
bCgB_ = mkReal(cgAllocated_ ? (uint64_t)bBlk_ * P_.num_images : 1);
bCgM_ = mkReal(cgAllocated_ ? (uint64_t)kCamBlk * P_.num_prec_blocks : 1);
bCgScal_ = mkReal(8);
bCgScal_ = mkReal(16);
bCgPart_ = mkReal(cgAllocated_ ? 2 * (uint64_t)npart : 1);
bPrecBlocks_ = mkUint(cgAllocated_ ? P_.prec_blocks.size() : 4);
@@ -278,6 +280,13 @@ public:
for (const char* k : {"cg_cam_diag", "cg_gather", "cg_bmul", "cg_scatter"})
entries.push_back(std::string(k) + cgSuffix_);
}
if (tcN_) {
const char* tc[] = {"tc_basis", "tc_schur", "tc_reg", "tc_dinv", "tc_inv_row",
"tc_inv_copy", "tc_restrict", "tc_lmul", "tc_ltmul",
"tc_prolong", "chol_diag", "chol_panel", "chol_update"};
entries.insert(entries.end(), std::begin(tc), std::end(tc));
entries.push_back(std::string("tc_bpart") + cgSuffix_);
}
if (!useCG_ || haveFallback_) {
const char* ch[] = {"chol_diag", "chol_panel", "chol_update", "tri_fwd", "tri_bwd"};
entries.insert(entries.end(), std::begin(ch), std::end(ch));
@@ -368,10 +377,17 @@ public:
up.push_back({&bMemberInfo_, mi.data(), mi.size() * 4});
up.push_back({&bExts_, exts.data(), exts.size()});
}
if (P_.use_pair_schur) {
up.push_back({&bPairEntries_, P_.pair_entries.data(), P_.pair_entries.size() * 4});
up.push_back({&bPairChunks_, P_.pair_chunks.data(), P_.pair_chunks.size() * 4});
if (P_.use_pair_schur || tcN_) {
tcEnt_.insert(tcEnt_.begin(), P_.pair_entries.begin(), P_.pair_entries.end());
tcChk_.insert(tcChk_.begin(), P_.pair_chunks.begin(), P_.pair_chunks.end());
up.push_back({&bPairEntries_, tcEnt_.data(), tcEnt_.size() * 4});
up.push_back({&bPairChunks_, tcChk_.data(), tcChk_.size() * 4});
}
struct Release {
std::vector<uint32_t>& a;
std::vector<uint32_t>& b;
~Release() { a = {}; b = {}; }
} release{tcEnt_, tcChk_};
if (cgAllocated_) {
up.push_back({&bCamRanges_, P_.cam_obs_ranges.data(), P_.cam_obs_ranges.size() * 4});
up.push_back({&bCamObs_, P_.cam_obs.data(), P_.cam_obs.size() * 4});
@@ -457,6 +473,20 @@ public:
reuse ? " (reuse)" : "");
LinSolve path = useCG_ ? LinSolve::CG : densePath_;
// A few CG iterations do not repay building A_c, and a stale one
// only costs iterations: it is rebuilt every third solve, or once
// the damping has moved tenfold.
tcUse_ = tcN_ && !tcOff_ && lastCg_ > kTcMinIters;
const bool tcBuild = tcUse_ && (!tcHave_ || tcAge_ >= 2 ||
std::fabs(std::log(damping / tcLambda_)) > std::log(10.0));
tcBuild_ = tcBuild;
if (tcBuild) {
tcHave_ = true;
tcAge_ = 0;
tcLambda_ = damping;
} else if (tcUse_) {
tcAge_++;
}
beginSeg();
recordIteration((float)damping, reuse, path);
endSeg();
@@ -467,8 +497,27 @@ public:
bool conv;
double cg_iters;
readCgStatus(conv, cg_iters);
// CG stopped before its first step (r.z or p.Sp not positive):
// retried without A_c, then a failed step that raises the damping
// -- S went indefinite by rounding below ~1e-9 on Gram blocks of 1e23.
if (cg_iters == 0 && !conv && tcUse_) {
tcUse_ = tcBuild_ = false;
restore_pending_ = true;
beginSeg();
recordIteration((float)damping, true, path);
endSeg();
newCost = readCost();
readCgStatus(conv, cg_iters);
if (cg_iters > 0 || conv) {
tcOff_ = true;
sfm::slog::diag(sfm::slog::Tag::Map,
"[vk] coarse correction broke down; continuing without it");
}
}
if (cg_iters == 0 && !conv) newCost = std::numeric_limits<double>::infinity();
stats_.cg_solves++;
stats_.cg_iters_total += cg_iters;
lastCg_ = conv ? cg_iters : 1e9;
if (conv) {
consec_fallbacks = 0;
// adapt the recorded iteration cap to the observed count
@@ -495,6 +544,7 @@ public:
recordIteration((float)damping, true, densePath_);
endSeg();
newCost = readCost();
tcHave_ = false; // the dense solve reused u_S
stats_.cg_fallbacks++;
if (++consec_fallbacks >= 3) {
useCG_ = false; // CG is not paying off; stay dense
@@ -516,7 +566,7 @@ public:
if (++noimprov >= opt_.patience) { cost = newCost; break; }
} else {
noimprov = 0;
damping *= 1.0 / 3.0;
damping = std::max(damping / 3.0, kMinDamping);
}
cost = newCost;
stats_.accepted++;
@@ -638,6 +688,13 @@ private:
const uint32_t tier = maxDof <= 18 ? 18 : 24;
bBlk_ = tier * (tier + 1) / 2;
wide_ = std::max(maxDof, 6u) / 24.0;
double t1 = 0, t2 = 0;
for (uint32_t p = 0; p < P_.num_points; p++) {
const double t = P_.obs_ranges[p + 1] - P_.obs_ranges[p];
t1 += t;
t2 += t * t;
}
if (t1 > 0) meanTrackT_ = t2 / t1;
}
static std::string costEntry(const BAProblem::ModelRange& mr) {
@@ -668,8 +725,8 @@ private:
const bool pairOk = exclusive && P_.num_obs > 0 && pairEntries <= kMaxPairEntries &&
densePairMB <= budget;
const double denseMB = pairOk ? densePairMB : denseObsMB;
const double cgMB = estimateMB(false, true, false, 0);
const double bothMB = estimateMB(true, true, pairOk, pairEntries);
double cgMB = estimateMB(false, true, false, 0);
double bothMB = estimateMB(true, true, pairOk, pairEntries);
const uint32_t kDenseMaxDim = 8192;
@@ -690,12 +747,18 @@ private:
}
break;
case SolverSel::Auto:
useCG_ = cgOk && (P_.n_dim > kDenseMaxDim || !denseOk || denseMB > budget);
useCG_ = cgOk && (P_.n_dim > kDenseMaxDim || !denseOk || denseMB > budget ||
cgWork() < 0.5 * denseWork(pairOk, pairEntries));
if (!useCG_ && !denseOk)
throw std::runtime_error("reduced system too large for the dense solver");
break;
}
if (useCG_ && ::planCoarse(P_, kTcMaxDim, tcK_, tcN_, tcEntries_)) {
cgMB = estimateMB(false, true, false, 0);
bothMB = estimateMB(true, true, pairOk, pairEntries);
}
haveFallback_ = false;
if (useCG_) {
bool want = opt_.cg_fallback == CgFallback::On ||
@@ -732,8 +795,40 @@ private:
buildPrecBlocks(P_, exclusive);
}
cgMaxit_ = (uint32_t)opt_.cg_max_iters;
if (tcN_) {
// Chunks of at most 128 entries of one cluster pair, their offsets
// past the pair-Schur entries they follow on the device.
std::vector<uint32_t> key;
buildCoarseEntries(P_, tcK_, tcEnt_, key);
const uint32_t base = (uint32_t)(P_.pair_entries.size() / 2);
tcChk_.clear();
for (size_t kk = 0; kk + 1 < key.size(); kk++)
for (uint32_t o = key[kk]; o < key[kk + 1]; o += 128) {
tcChk_.push_back(base + o);
tcChk_.push_back(std::min(128u, key[kk + 1] - o));
}
tcChunks_ = (uint32_t)(tcChk_.size() / 2);
}
}
static constexpr uint32_t kTcMaxDim = 4096;
// One LM iteration of each path in the budget's units. The dense assembly
// is quadratic in track length: a 1068-image capture whose tracks average
// 70 observations spent 3.7 s an iteration there, against 0.4 s on CG.
double denseWork(bool pair, uint64_t pairEntries) const {
const double n = P_.n_dim;
const double schur = pair ? kWPairEntry * wide_ * wide_ * (double)pairEntries
: kWSchurObs * wide_ * wide_ * meanTrackT_ / kSchurObsT *
P_.num_obs;
return schur + kWFlop * 2 * n * n * n / 3 + kWJac * wide_ * P_.num_obs;
}
double cgWork() const {
return kWJac * wide_ * P_.num_obs + kWCamDiag * wide_ * wide_ * P_.num_obs +
kCgItersGuess * (kWGather + kWScatter) * wide_ * P_.num_obs;
}
static constexpr double kCgItersGuess = 40;
// device-buffer footprint of a path combination, in MB (mirrors init())
double estimateMB(bool withDense, bool withCG, bool pairTables, uint64_t pairEntries) const {
const double rs = (double)realSize(opt_.real);
@@ -753,6 +848,11 @@ private:
if (pairTables)
b += 8.0 * pairEntries * 1.01 + 6 * no * rs; // pair entries + Y
}
if (withCG && tcN_) {
const double tn = tcN_, tc = tn * (tn + 1) / 2 + 42.0 * P_.num_frames;
b += (std::max(withDense ? packed : 0.0, tc) - (withDense ? packed : 0.0)) * rs;
b += std::max(0.0, 34.0 * tn - n) * rs + 8.0 * tcEntries_ * 1.01;
}
if (withCG)
b += (4 * n + 3 * np + ni * (double)bBlk_ +
(ni + (double)P_.members.size() + (double)P_.groups.size()) * kCamBlk) * rs +
@@ -769,7 +869,10 @@ private:
static constexpr double kWLaunch = 2000, kWPoint = 0.25, kWVec = 1, kWImage = 5;
static constexpr double kWCost = 1.5, kWJac = 5.5, kWDp = 0.6, kWYPrep = 0.3;
static constexpr double kWCamDiag = 12.7, kWGather = 0.65, kWScatter = 1.2;
static constexpr double kWSchurObs = 40, kWPairEntry = 3, kWFlop = 2.6e-3;
static constexpr double kWSchurObs = 40, kWPairEntry = 3, kWFlop = 2.6e-3, kWTcSchur = 6;
// schur_obs walks the track per observation: kWSchurObs is at the 5.8
// observations of that capture's sum t^2 / sum t.
static constexpr double kSchurObsT = 5.8;
// Until a submit has been timed, a device is taken to be 64x slower.
static constexpr double kPriorRate = 1e9 / 64;
@@ -929,22 +1032,9 @@ private:
void recordCholesky() {
const uint32_t n = P_.n_dim, bs = 32;
const uint32_t nb = (n + bs - 1) / bs;
const double tile = 2.0 * bs * bs * bs * kWFlop;
recordFactor(n);
Push p;
p.u0 = n;
p.u1 = 0;
room(kWLaunch + tile);
ctx_.dispatch(cb_, "chol_diag", 1, p);
ctx_.barrier(cb_);
for (uint32_t k = 0; k + 1 < nb; k++) {
p.u1 = k;
uint32_t below = nb - 1 - k;
launch("chol_panel", below, 1, tile, p, atBase);
ctx_.barrier(cb_);
p.u2 = below * (below + 1) / 2;
launch("chol_update", p.u2, 1, tile, p, atBase);
ctx_.barrier(cb_);
}
for (uint32_t k = 0; k < nb; k++) {
p.u1 = k;
p.u2 = (k + 1) * bs < n ? n - (k + 1) * bs : 0;
@@ -961,6 +1051,95 @@ private:
}
}
// Factor the leading n x n packed triangle of u_S in place; `rel` > 0
// replaces a pivot under that fraction of the diagonal tc_reg saved with it.
void recordFactor(uint32_t n, float rel = 0) {
const uint32_t bs = 32;
const uint32_t nb = (n + bs - 1) / bs;
const double tile = 2.0 * bs * bs * bs * kWFlop;
Push p;
p.u0 = n;
p.u1 = 0;
p.u3 = rel > 0 ? 1 : 0;
p.f0 = rel;
room(kWLaunch + tile);
ctx_.dispatch(cb_, "chol_diag", 1, p);
ctx_.barrier(cb_);
for (uint32_t k = 0; k + 1 < nb; k++) {
p.u1 = k;
uint32_t below = nb - 1 - k;
launch("chol_panel", below, 1, tile, p, atBase);
ctx_.barrier(cb_);
p.u2 = below * (below + 1) / 2;
launch("chol_update", p.u2, 1, tile, p, atBase);
ctx_.barrier(cb_);
}
}
// A_c = P^T S P from this iteration's B and W; its Cholesky factor is then
// inverted in place, so an application is two matrix-vector products.
void recordCoarse() {
const uint32_t packedTc = tcN_ * (tcN_ + 1) / 2;
Push p;
p.u0 = P_.num_frames;
p.u2 = tcK_;
p.u3 = packedTc;
room(2 * kWLaunch + (P_.num_frames + P_.num_images) * kWImage);
ctx_.dispatch(cb_, "tc_basis", (P_.num_frames + 63) / 64, p);
ctx_.fillZero(cb_, bS_, 0, (VkDeviceSize)packedTc * realSize(opt_.real));
ctx_.barrier(cb_);
p.u0 = P_.num_images;
ctx_.dispatch(cb_, std::string("tc_bpart") + cgSuffix_, (P_.num_images + 63) / 64, p);
p.u0 = tcChunks_;
p.u1 = P_.num_pair_chunks;
launch("tc_schur", tcChunks_, 1,
kWTcSchur * (double)tcEntries_ / std::max(1u, tcChunks_), p, atBase);
ctx_.barrier(cb_);
Push q;
q.u0 = tcN_;
q.f0 = opt_.real == RealCfg::F32 ? 1e-4f : 1e-8f;
room(kWLaunch + tcN_ * kWVec);
ctx_.dispatch(cb_, "tc_reg", (tcN_ + 255) / 256, q);
ctx_.barrier(cb_);
recordFactor(tcN_, opt_.real == RealCfg::F32 ? 1e-3f : 1e-6f);
const uint32_t bs = 32, nb = (tcN_ + bs - 1) / bs;
const double tile = 2.0 * bs * bs * bs * kWFlop;
Push d;
d.u0 = tcN_;
room(kWLaunch + nb * tile);
ctx_.dispatch(cb_, "tc_dinv", nb, d);
ctx_.barrier(cb_);
d.u2 = 2 * tcN_; // scratch rows follow the two vectors in u_y
for (uint32_t i = 1; i < nb; i++) {
d.u1 = i;
room(2 * kWLaunch + i * (i + 1) / 2.0 * tile);
ctx_.dispatch(cb_, "tc_inv_row", i, d);
ctx_.barrier(cb_);
ctx_.dispatch(cb_, "tc_inv_copy", (bs * bs * i + 255) / 256, d);
ctx_.barrier(cb_);
}
}
// z += P A_c^-1 P^T r, after the block-Jacobi part of the preconditioner.
void recordCoarseApply() {
Push p;
p.u0 = tcN_;
p.u1 = P_.num_frames;
p.u2 = tcK_;
p.u3 = tcN_ * (tcN_ + 1) / 2;
room(4 * kWLaunch + (6.0 * P_.num_frames + (double)tcN_ * tcN_) * kWVec);
ctx_.dispatch(cb_, "tc_restrict", (tcN_ + 255) / 256, p);
ctx_.barrier(cb_);
ctx_.dispatch(cb_, "tc_lmul", (tcN_ + 7) / 8, p);
ctx_.barrier(cb_);
ctx_.dispatch(cb_, "tc_ltmul", (tcN_ + 31) / 32, p);
ctx_.barrier(cb_);
p.u0 = P_.pose_dim;
ctx_.dispatch(cb_, "tc_prolong", (P_.pose_dim + 255) / 256, p);
ctx_.barrier(cb_);
}
void recordCost() {
ctx_.fillZero(cb_, bCost_);
ctx_.barrier(cb_);
@@ -1041,7 +1220,7 @@ private:
} else if (path == LinSolve::DenseObs) {
p.u0 = P_.num_obs;
launch(std::string("schur_obs") + schurSuffix_, P_.num_obs, 128,
kWSchurObs * wide_ * wide_, p, atBase);
kWSchurObs * wide_ * wide_ * meanTrackT_ / kSchurObsT, p, atBase);
} else {
p.u0 = P_.num_cam_chunks;
p.u1 = P_.prec_exclusive ? 1 : 0;
@@ -1054,6 +1233,7 @@ private:
p.u0 = P_.num_prec_blocks;
room(kWLaunch + P_.num_prec_blocks * kWImage);
ctx_.dispatch(cb_, "cg_prec_fact", (P_.num_prec_blocks + 255) / 256, p);
if (tcBuild_) recordCoarse();
}
}
ctx_.barrier(cb_);
@@ -1086,6 +1266,7 @@ private:
pc.u1 = 0; // flag was just cleared
ctx_.dispatch(cb_, "cg_prec_apply", nib, pc);
ctx_.barrier(cb_);
if (tcUse_) recordCoarseApply();
Push pr;
pr.u0 = n;
pr.u1 = 1;
@@ -1098,6 +1279,7 @@ private:
pf.u1 = 0;
pf.u2 = npart;
pf.f0 = (float)opt_.cg_tol;
pf.f1 = (float)opt_.cg_model_tol;
ctx_.dispatch(cb_, "cg_fin", 1, pf);
ctx_.barrier(cb_);
ctx_.dispatch(cb_, "cg_copy", ng, pn);
@@ -1138,6 +1320,7 @@ private:
ctx_.barrier(cb_);
ctx_.dispatch(cb_, "cg_prec_apply", nib, pc);
ctx_.barrier(cb_);
if (tcUse_) recordCoarseApply();
pr.u1 = 1;
ctx_.dispatch(cb_, "cg_red2", ng, pr);
ctx_.barrier(cb_);
@@ -1266,6 +1449,7 @@ private:
const char* cgSuffix_ = "_w"; // ... and of the CG ones
uint32_t bBlk_ = kCamBlk; // per-image B block stride at that tier
double wide_ = 1; // widest camera block over the rig tier's 24
double meanTrackT_ = kSchurObsT; // sum t^2 / sum t over the tracks
SolverStats stats_;
std::unique_ptr<VkContext> owned_; // null when running on a shared context
VkContext& ctx_;
@@ -1276,6 +1460,21 @@ private:
bool cgAllocated_ = false; // CG buffers/tables exist
bool haveFallback_ = false;
uint32_t cgMaxit_ = 100;
// Coarse correction: frames per cluster and the coarse dimension (0: off),
// and whether this iteration's solve uses it.
uint32_t tcK_ = 0, tcN_ = 0;
uint64_t tcEntries_ = 0;
uint32_t tcChunks_ = 0;
std::vector<uint32_t> tcEnt_, tcChk_; // follow the pair-Schur tables on the device
bool tcUse_ = false, tcBuild_ = false, tcHave_ = false, tcOff_ = false;
int tcAge_ = 0;
double tcLambda_ = 0;
double lastCg_ = 0; // iterations the last CG solve took
static constexpr double kTcMinIters = 12;
// Below ~1e-9 rounding outweighs the damping on a badly scaled camera (Gram
// entries of 1e23, from a point at depth 2e-9 in a 22042-image model) and S
// goes indefinite; 1e-8 is still Gauss-Newton to eight digits.
static constexpr double kMinDamping = 1e-8;
GpuBuffer bObs_, bObsImage_, bObsPoint_, bImageInfo_, bGroupInfo_, bMemberInfo_;
GpuBuffer bPoses_, bExts_, bIntr_, bPoints_, bObsRanges_, bModelObs_, bJcOff_;
+256 -7
View File
@@ -12,6 +12,7 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <limits>
#include <string>
#include <vector>
@@ -109,12 +110,39 @@ public:
damping,
reuse ? " (reuse)" : "");
const bool cg = useCG_;
// as sfm/ba/Solver.h: A_c when CG is slow, rebuilt every third
// solve or once the damping has moved tenfold
tcUse_ = cg && tcN_ && !tcOff_ && lastCg_ > kTcMinIters;
tcBuild_ = tcUse_ && (!tcHave_ || tcAge_ >= 2 ||
std::fabs(std::log(damping / tcLambda_)) > std::log(10.0));
if (tcBuild_) {
tcHave_ = true;
tcAge_ = 0;
tcLambda_ = damping;
} else if (tcUse_) {
tcAge_++;
}
double newCost = iterate(damping, reuse, cg);
// as sfm/ba/Solver.h: a CG that stopped before its first step is
// retried without the coarse correction, then taken as a failed step
if (cg && cgIters_ == 0 && !cgConverged_ && tcUse_) {
tcUse_ = tcBuild_ = false;
restore();
newCost = iterate(damping, true, cg);
if (cgIters_ > 0 || cgConverged_) {
tcOff_ = true;
sfm::slog::diag(sfm::slog::Tag::Map,
"[cpu] coarse correction broke down; continuing without it");
}
}
if (cg && cgIters_ == 0 && !cgConverged_)
newCost = std::numeric_limits<double>::infinity();
stats_.iterations = it + 1;
if (cg) {
stats_.cg_solves++;
stats_.cg_iters_total += cgIters_;
lastCg_ = cgConverged_ ? cgIters_ : 1e9;
if (cgConverged_) {
consec_fallbacks = 0;
cgMaxit_ = std::min<uint32_t>(
@@ -150,7 +178,7 @@ public:
if (++noimprov >= opt_.patience) { cost = newCost; break; }
} else {
noimprov = 0;
damping *= 1.0 / 3.0;
damping = std::max(damping / 3.0, 1e-8); // kMinDamping in sfm/ba/Solver.h
}
cost = newCost;
stats_.accepted++;
@@ -291,6 +319,9 @@ private:
b += (double)DenseSpd::elems(n_) * 8 + 2.0 * n * DenseSpd::kBlock * 8;
b += (double)nthreads_ * m_ * n * 8;
}
if (withCG && tcN_)
b += ((double)DenseSpd::elems(tcN_) + 2.0 * tcN_ * DenseSpd::kBlock +
42.0 * P_.num_frames + tcN_) * 8 + 8.0 * tcEntries_;
if (withCG) {
const double nblk = exclusive_ ? ni : (double)P_.num_frames + nSharedBlk_;
b += (4 * n + 3 * np) * 8 + (double)kCamBlk * (ni + nblk) * 8 +
@@ -300,13 +331,34 @@ private:
return b / (1024.0 * 1024.0);
}
// One LM iteration of each path in seconds on 8 cores, fitted where dense
// assembly's quadratic cost in track length shows: 7.4 s dense against 1.5 s
// CG, same cost, on 1068 images averaging 70 a track (GPU's: Solver.h).
double denseSeconds() const {
double t1 = 0, t2 = 0;
for (uint32_t p = 0; p < nPts_; p++) {
const double t = P_.obs_ranges[p + 1] - P_.obs_ranges[p];
t1 += t;
t2 += t * t;
}
double dof2 = 0;
for (uint32_t i = 0; i < nImg_; i++) dof2 = std::max(dof2, (double)dof_[i] * dof_[i]);
const double n = n_;
return 9e-11 * t2 * dof2 + n * n * n / 3e11;
}
double cgSeconds() const {
double dof = 0;
for (uint32_t i = 0; i < nImg_; i++) dof = std::max(dof, (double)dof_[i]);
return (7e-9 + 20 * 1.4e-9) * nObs_ * dof;
}
void decidePaths() {
const double budget =
opt_.vram_budget_mb > 0 ? opt_.vram_budget_mb : defaultBudgetMB();
const bool cgOk = nObs_ > 0;
const double denseMB = estimateMB(true, false);
const double cgMB = estimateMB(false, true);
const double bothMB = estimateMB(true, true);
double cgMB = estimateMB(false, true);
double bothMB = estimateMB(true, true);
switch (opt_.solver) {
case SolverSel::Dense: useCG_ = false; break;
@@ -316,9 +368,14 @@ private:
"[cpu] warning: no observations, falling back to dense");
break;
case SolverSel::Auto:
useCG_ = cgOk && (n_ > kDenseMaxDim || denseMB > budget);
useCG_ = cgOk && (n_ > kDenseMaxDim || denseMB > budget ||
cgSeconds() < 0.5 * denseSeconds());
break;
}
if (useCG_ && planCoarse(P_, kTcMaxDim, tcK_, tcN_, tcEntries_)) {
cgMB = estimateMB(false, true);
bothMB = estimateMB(true, true);
}
haveFallback_ = false;
if (useCG_)
haveFallback_ = opt_.cg_fallback == CgFallback::On ||
@@ -340,6 +397,21 @@ private:
buildCamTables(P_); // the dense path walks the same per-image lists
if (useCG_) buildPrecBlocks(P_, exclusive_);
cgMaxit_ = (uint32_t)opt_.cg_max_iters;
if (tcN_) {
buildCoarseEntries(P_, tcK_, tcEnt_, tcKey_);
const uint32_t nc = tcN_ / 7;
std::vector<uint64_t> w(nc + 1, 0);
for (uint32_t c = 0; c < nc; c++)
w[c + 1] = w[c] + tcK_ + tcKey_[(uint64_t)(c + 1) * (c + 2) / 2] -
tcKey_[(uint64_t)c * (c + 1) / 2];
splitByWeight(w, taskCount((int64_t)w[nc], 1 << 12, nthreads_), tcSplit_);
frameImg_.assign(P_.num_frames + 1, nImg_);
for (uint32_t i = nImg_; i-- > 0;) frameImg_[frame_[i]] = i;
tcA_.init(tcN_);
tcP_.assign(42 * (size_t)P_.num_frames, 0.0);
tcY_.assign(tcN_, 0.0);
tcDiag_.assign(tcN_, 0.0);
}
}
void allocate() {
@@ -410,6 +482,8 @@ private:
cgM_.capacity() + cgGIntr_.capacity() + cgSpIntr_.capacity() +
cgGrp_.capacity()) *
8;
b += tcA_.bytes() + (tcP_.capacity() + tcY_.capacity()) * 8 +
(tcEnt_.capacity() + tcKey_.capacity()) * 4;
b += S_.bytes() + (P_.cam_obs.capacity() + P_.cam_obs_ranges.capacity() +
P_.cam_chunks.capacity() + P_.prec_blocks.capacity()) * 4;
return (double)b / (1024.0 * 1024.0);
@@ -470,6 +544,7 @@ private:
if (cg) {
cgCamDiag(damping);
cgPrecFactor();
if (tcBuild_) coarseBuild();
prof_.schur += lap();
cgIters_ = runPCG(cgMaxit_);
} else {
@@ -857,10 +932,16 @@ private:
const uint32_t dof = P_.prec_blocks[4 * b + 1] + P_.prec_blocks[4 * b + 3];
if (!dof) continue;
double* L = &cgM_[(size_t)kCamBlk * b];
// as cg_prec_fact: a failed pivot is floored and decoupled
double f = 1e-30;
for (uint32_t j = 0; j < dof; j++)
if (L[pidx(j, j)] > f) f = L[pidx(j, j)];
f = std::max(1e-10 * f, 1e-30);
for (uint32_t j = 0; j < dof; j++) {
const double d = std::sqrt(std::max(L[pidx(j, j)], 1e-30));
const bool ok = L[pidx(j, j)] > f;
const double d = std::sqrt(ok ? L[pidx(j, j)] : f);
L[pidx(j, j)] = d;
for (uint32_t i = j + 1; i < dof; i++) L[pidx(i, j)] /= d;
for (uint32_t i = j + 1; i < dof; i++) L[pidx(i, j)] = ok ? L[pidx(i, j)] / d : 0.0;
for (uint32_t c = j + 1; c < dof; c++)
for (uint32_t i = c; i < dof; i++)
L[pidx(i, c)] -= L[pidx(i, j)] * L[pidx(c, j)];
@@ -903,6 +984,154 @@ private:
for (uint32_t i = 0; i < dof; i++) z[cols[i]] = y[i];
}
});
if (tcUse_) coarseApply(r, z);
}
// ================
// coarse correction (README.md, "Coarse correction")
// ================
// P_f: pose deltas of a similarity motion (w, tau, s) of the world,
// d(angle-axis) = -Jr^-1 w and dt = s t - R tau; 6x7 row-major per frame.
void coarseBasis() {
const uint32_t nf = P_.num_frames;
const int nt = taskCount(nf, 1024, nthreads_);
pool_->run(nt, nthreads_, [&](int task, int) {
int64_t lo, hi;
taskRange(nf, nt, task, lo, hi);
for (int64_t f = lo; f < hi; f++) {
const double* q = &P_.poses[6 * (size_t)f];
double* B = &tcP_[42 * (size_t)f];
std::fill(B, B + 42, 0.0);
const double th2 = q[0] * q[0] + q[1] * q[1] + q[2] * q[2], th = std::sqrt(th2);
const double K[9] = {0, -q[2], q[1], q[2], 0, -q[0], -q[1], q[0], 0};
double K2[9];
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
K2[3 * i + j] = K[3 * i] * K[j] + K[3 * i + 1] * K[3 + j] + K[3 * i + 2] * K[6 + j];
const bool nearZero = th < 1e-3;
const double s1 = nearZero ? 1.0 : std::sin(th) / th;
const double c1 = nearZero ? 0.5 : (1 - std::cos(th)) / th2;
const double c2 =
nearZero ? 1.0 / 12 : 1 / th2 - (1 + std::cos(th)) / (2 * th * std::sin(th));
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
B[7 * i + j] = -((i == j) + 0.5 * K[3 * i + j] + c2 * K2[3 * i + j]);
B[7 * (3 + i) + 3 + j] = -((i == j) + s1 * K[3 * i + j] + c1 * K2[3 * i + j]);
}
B[7 * (3 + i) + 6] = q[3 + i];
}
}
});
}
// G = sum over the run starting at observation o of P_f^T Jc_pose^T Jp.
void coarseRunG(uint32_t o, uint32_t end, double* G) const {
std::fill(G, G + 21, 0.0);
const uint32_t c = frame_[P_.obs_image[o]] / tcK_;
for (; o < end && frame_[P_.obs_image[o]] / tcK_ == c; o++) {
const uint32_t img = P_.obs_image[o], dof = dof_[img];
const double* Pf = &tcP_[42 * (size_t)frame_[img]];
const double* Jc = &Jc_[P_.jc_off[o]];
const double* Jp = &Jp_[6 * (size_t)o];
for (int i = 0; i < 7; i++) {
double q0 = 0, q1 = 0;
for (int a = 0; a < 6; a++) {
q0 += Pf[7 * a + i] * Jc[a];
q1 += Pf[7 * a + i] * Jc[dof + a];
}
for (int j = 0; j < 3; j++) G[3 * i + j] += q0 * Jp[j] + q1 * Jp[3 + j];
}
}
}
// A_c = P^T S P, factored. A task owns a range of block rows, so every
// write is its own: the B part from its frames' images, the Schur part
// from the run pairs keyed to its rows.
void coarseBuild() {
coarseBasis();
tcA_.zero(*pool_, nthreads_);
const int nt = (int)tcSplit_.size() - 1;
pool_->run(nt, nthreads_, [&](int task, int) {
for (uint32_t c = tcSplit_[task]; c < tcSplit_[task + 1]; c++) {
const uint32_t f1 = std::min(P_.num_frames, (c + 1) * tcK_);
for (uint32_t img = frameImg_[c * tcK_]; img < frameImg_[f1]; img++) {
const double* Bb = &cgB_[(size_t)kCamBlk * img];
const double* Pf = &tcP_[42 * (size_t)frame_[img]];
double BP[42];
for (int r = 0; r < 6; r++)
for (int j = 0; j < 7; j++) {
double v = 0;
for (int l = 0; l < 6; l++)
v += Bb[r >= l ? pidx(r, l) : pidx(l, r)] * Pf[7 * l + j];
BP[7 * r + j] = v;
}
for (uint32_t i = 0; i < 7; i++) {
double* row = tcA_.row(7 * c + i) + 7 * c;
for (uint32_t j = 0; j <= i; j++) {
double v = 0;
for (int l = 0; l < 6; l++) v += Pf[7 * l + i] * BP[7 * l + j];
if (std::isfinite(v)) row[j] += v;
}
}
}
double G[21], GW[21];
for (uint32_t cv = 0; cv <= c; cv++) {
const uint64_t key = (uint64_t)c * (c + 1) / 2 + cv;
for (uint32_t e = tcKey_[key]; e < tcKey_[key + 1]; e++) {
const uint32_t ou = tcEnt_[2 * (size_t)e], ov = tcEnt_[2 * (size_t)e + 1];
const uint32_t p = P_.obs_point[ou], end = P_.obs_ranges[p + 1];
const double* W = &W_[9 * (size_t)p];
coarseRunG(ou, end, G);
for (int i = 0; i < 7; i++)
for (int j = 0; j < 3; j++)
GW[3 * i + j] = G[3 * i] * W[j] + G[3 * i + 1] * W[3 + j] +
G[3 * i + 2] * W[6 + j];
if (ov != ou) coarseRunG(ov, end, G);
for (uint32_t i = 0; i < 7; i++) {
double* row = tcA_.row(7 * c + i) + 7 * cv;
for (uint32_t j = 0; j < 7 && 7 * cv + j <= 7 * c + i; j++) {
const double v = GW[3 * i] * G[3 * j] + GW[3 * i + 1] * G[3 * j + 1] +
GW[3 * i + 2] * G[3 * j + 2];
if (std::isfinite(v)) row[j] -= v;
}
}
}
}
// the global similarity is a gauge freedom: A_c is singular up
// to the damping
for (uint32_t i = 0; i < 7; i++) {
double& d = tcA_.row(7 * c + i)[7 * c + i];
tcDiag_[7 * c + i] = d;
d *= 1 + 1e-8;
}
}
});
tcA_.factor(*pool_, nthreads_, tcDiag_.data(), 1e-6);
}
void coarseApply(const std::vector<double>& r, std::vector<double>& z) {
const uint32_t nf = P_.num_frames;
std::fill(tcY_.begin(), tcY_.end(), 0.0);
for (uint32_t f = 0; f < nf; f++) {
const double* Pf = &tcP_[42 * (size_t)f];
double* y = &tcY_[7 * (f / tcK_)];
for (int j = 0; j < 7; j++) {
double v = 0;
for (int a = 0; a < 6; a++) v += Pf[7 * a + j] * r[6 * (size_t)f + a];
y[j] += v;
}
}
tcA_.solve(tcY_.data(), *pool_, nthreads_);
for (uint32_t f = 0; f < nf; f++) {
const double* Pf = &tcP_[42 * (size_t)f];
const double* y = &tcY_[7 * (f / tcK_)];
for (int a = 0; a < 6; a++) {
double v = 0;
for (int j = 0; j < 7; j++) v += Pf[7 * a + j] * y[j];
z[6 * (size_t)f + a] += v;
}
}
}
void cgGather() {
@@ -1016,6 +1245,8 @@ private:
bool conv = !(std::isfinite(rho) && rho > 0.0);
uint32_t iters = 0;
cgP_ = cgZ_;
double Q = 0; // the quadratic model at g, for cg_fin's Nash-Sofer test
bool qstop = false;
while (!conv && iters < maxit) {
cgGather();
cgMatvec();
@@ -1025,6 +1256,8 @@ private:
break;
}
const double alpha = rho / pAp;
const double dq = -0.5 * rho * alpha;
Q += dq;
const int nt = taskCount(n_, 1 << 14, nthreads_);
pool_->run(nt, nthreads_, [&](int t, int) {
int64_t lo, hi;
@@ -1044,13 +1277,17 @@ private:
conv = true;
break;
}
if (opt_.cg_model_tol > 0 && Q < 0 && iters * dq / Q < opt_.cg_model_tol) {
conv = qstop = true;
break;
}
pool_->run(nt, nthreads_, [&](int t, int) {
int64_t lo, hi;
taskRange(n_, nt, t, lo, hi);
for (int64_t i = lo; i < hi; i++) cgP_[i] = cgZ_[i] + beta * cgP_[i];
});
}
cgConverged_ = conv && rr <= tol2;
cgConverged_ = conv && (rr <= tol2 || qstop);
return iters;
}
@@ -1139,6 +1376,18 @@ private:
std::vector<double> poses0_, exts0_, intr0_, points0_;
std::vector<double> sbuf_, sgbuf_, part_;
std::vector<double> cgR_, cgZ_, cgP_, cgSp_, cgV_, cgB_, cgM_, cgGIntr_, cgSpIntr_, cgGrp_;
// Coarse correction: frames per cluster, dimension (0: off), the run-pair
// entries by cluster pair, block rows per task, and each frame's first image.
uint32_t tcK_ = 0, tcN_ = 0;
uint64_t tcEntries_ = 0;
std::vector<uint32_t> tcEnt_, tcKey_, tcSplit_, frameImg_;
std::vector<double> tcP_, tcY_, tcDiag_;
DenseSpd tcA_;
bool tcUse_ = false, tcBuild_ = false, tcHave_ = false, tcOff_ = false;
int tcAge_ = 0;
double tcLambda_ = 0, lastCg_ = 0;
static constexpr uint32_t kTcMaxDim = 4096;
static constexpr double kTcMinIters = 12;
std::vector<uint32_t> asmSplit_, cgSplit_;
DenseSpd S_;
struct { double jac = 0, prep = 0, schur = 0, lin = 0, back = 0, cost = 0; } prof_;
+11 -5
View File
@@ -89,11 +89,10 @@ inline Mat3 inverse3(const Mat3& A, bool* ok = nullptr) {
// Cyclic Jacobi eigen-decomposition of a symmetric n x n matrix (row-major in
// `A`, destroyed). Eigenvalues -> w[n], eigenvectors -> columns of V[n*n].
// Not sorted.
inline void jacobiEigenSymmetric(std::vector<double>& A, int n, std::vector<double>& w,
std::vector<double>& V) {
w.assign(n, 0);
V.assign((size_t)n * n, 0);
// Not sorted. This form allocates nothing, for callers in a hot loop.
inline void jacobiEigenSymmetric(double* A, int n, double* w, double* V) {
for (int i = 0; i < n; i++) w[i] = 0;
for (size_t i = 0; i < (size_t)n * n; i++) V[i] = 0;
for (int i = 0; i < n; i++) V[(size_t)i * n + i] = 1.0;
// Convergence is measured against the matrix's own scale. The old test was
@@ -149,6 +148,13 @@ inline void jacobiEigenSymmetric(std::vector<double>& A, int n, std::vector<doub
for (int i = 0; i < n; i++) w[i] = A[(size_t)i * n + i];
}
inline void jacobiEigenSymmetric(std::vector<double>& A, int n, std::vector<double>& w,
std::vector<double>& V) {
w.assign(n, 0);
V.assign((size_t)n * n, 0);
jacobiEigenSymmetric(A.data(), n, w.data(), V.data());
}
// Exact null space of an m x 9 matrix with m < 9, by Householder QR of A^T.
//
// A minimal RANSAC sample gives a constraint matrix that is rank deficient *by
+2 -2
View File
@@ -44,14 +44,14 @@ inline Vec3 triangulateDLT(const Mat34& P1, const Mat34& P2, const Vec3& b1, con
double A[4][4];
dltRows(A[0], A[1], b1, P1);
dltRows(A[2], A[3], b2, P2);
std::vector<double> AtA(16, 0.0);
// On the stack: retriangulation calls this millions of times a pass.
double AtA[16], w[4], V[16];
for (int i = 0; i < 4; i++)
for (int j = 0; j < 4; j++) {
double s = 0;
for (int r = 0; r < 4; r++) s += A[r][i] * A[r][j];
AtA[i * 4 + j] = s;
}
std::vector<double> w, V;
jacobiEigenSymmetric(AtA, 4, w, V);
int mi = 0;
for (int i = 1; i < 4; i++)
+6 -2
View File
@@ -493,9 +493,13 @@ inline double runGlobalBA(Reconstruction& rec, const BundleOptions& bopt) {
if (MapProf::enabled())
slog::diag(slog::Tag::Map,
"[prof] BA #%ld: %u img %u pt %u obs | build %.3f init %.3f solve %.3f "
"write %.3f s | %d LM iters",
"write %.3f s | %d LM iters, %s%s",
(long)g_map_prof.n_ba, P.num_images, P.num_points, P.num_obs, t_build, t_init,
t_solve, t_write, stats.iterations);
t_solve, t_write, stats.iterations, stats.solver,
stats.cg_solves ? (" " + std::to_string((int)std::lround(
stats.cg_iters_total / stats.cg_solves)) +
" its/solve").c_str()
: "");
return stats.final_cost;
}
+294 -86
View File
@@ -1244,6 +1244,21 @@ public:
size_t ra = find(a->second), rb = find(b->second);
if (ra != rb) parent[ra] = rb;
}
// A calibrated rig holds a frame's images at one relative pose, which ties
// them as surely as an agreeing pair: back-to-back fisheyes share no
// matches, and a 4000-image capture split into its lenses and merged back.
if (rigs_) {
std::map<std::pair<uint32_t, uint32_t>, size_t> frame_of;
for (size_t i = 0; i < ids.size(); i++) {
const RigSlot sl = rigs_->slot(ids[i]);
if (!sl.valid() || m.rig_detached.count(ids[i]) || sl.rig >= m.rigs.size() ||
!m.rigs[sl.rig].usable(sl.member))
continue;
auto it = frame_of.emplace(std::make_pair(sl.rig, sl.frame), i).first;
size_t ra = find(i), rb = find(it->second);
if (ra != rb) parent[ra] = rb;
}
}
std::sort(st.fractions.begin(), st.fractions.end());
std::map<size_t, std::vector<uint32_t>> groups;
@@ -1763,6 +1778,7 @@ private:
missing);
}
// Sort, report and return. Split out only because run() has two exits.
std::vector<Reconstruction> finishRun(std::vector<Reconstruction>& models,
std::chrono::steady_clock::time_point prof_start) {
@@ -2222,11 +2238,19 @@ private:
// reprojErr() against the index. Same arithmetic, same cheirality rules.
double reprojErrAt(const ModelIndex& mi, uint32_t img, uint32_t f, const Vec3& X) const {
const Camera& cam = *mi.cam[img];
return reprojErrAt(mi, img, f, X, cam.wideFov() ? cam.bearing(kp(img, f)) : Vec3{});
}
// ... given the feature's bearing, which only a wide lens's cheirality reads.
// A fisheye bearing is an iterative inversion, and retriangulating an
// every-frame video asked for the same ones millions of times a pass.
double reprojErrAt(const ModelIndex& mi, uint32_t img, uint32_t f, const Vec3& X,
const Vec3& bearing) const {
const Pose& p = mi.img[img]->pose;
const Camera& cam = *mi.cam[img];
Vec3 pc = mul(p.R, X) + p.t;
if (cam.wideFov()) {
if (pc.dot(cam.bearing(kp(img, f))) <= 0) return 1e30;
if (pc.dot(bearing) <= 0) return 1e30;
} else if (pc.z < 1e-8) {
return 1e30;
}
@@ -2238,11 +2262,16 @@ private:
// triangulatePair() against the index.
bool triangulatePairAt(const ModelIndex& mi, uint32_t a, uint32_t fa, uint32_t b, uint32_t fb,
Vec3& X, double err_scale = 1.0) const {
return triangulatePairAt(mi, a, fa, mi.cam[a]->bearing(kp(a, fa)), b, fb,
mi.cam[b]->bearing(kp(b, fb)), X, err_scale);
}
bool triangulatePairAt(const ModelIndex& mi, uint32_t a, uint32_t fa, const Vec3& ba,
uint32_t b, uint32_t fb, const Vec3& bb, Vec3& X,
double err_scale = 1.0) const {
const Pose& pa_ = mi.img[a]->pose;
const Pose& pb_ = mi.img[b]->pose;
const Camera& ca = *mi.cam[a];
const Camera& cb = *mi.cam[b];
Vec3 ba = ca.bearing(kp(a, fa)), bb = cb.bearing(kp(b, fb));
Mat34 Pa{pa_.R[0], pa_.R[1], pa_.R[2], pa_.t.x, pa_.R[3], pa_.R[4], pa_.R[5], pa_.t.y,
pa_.R[6], pa_.R[7], pa_.R[8], pa_.t.z};
Mat34 Pb{pb_.R[0], pb_.R[1], pb_.R[2], pb_.t.x, pb_.R[3], pb_.R[4], pb_.R[5], pb_.t.y,
@@ -2253,9 +2282,9 @@ private:
if (!inFront(ca, pa, ba, 1e-6) || !inFront(cb, pb, bb, 1e-6)) return false;
double ang = triangulationAngle(X, cameraCenter(pa_), cameraCenter(pb_));
if (ang * 180.0 / M_PI < opt_.min_tri_angle_deg) return false;
if (reprojErrAt(mi, a, fa, X) > err_scale * (opt_.max_reproj_error * mi.pixel_scale[a]))
if (reprojErrAt(mi, a, fa, X, ba) > err_scale * (opt_.max_reproj_error * mi.pixel_scale[a]))
return false;
if (reprojErrAt(mi, b, fb, X) > err_scale * (opt_.max_reproj_error * mi.pixel_scale[b]))
if (reprojErrAt(mi, b, fb, X, bb) > err_scale * (opt_.max_reproj_error * mi.pixel_scale[b]))
return false;
return true;
}
@@ -2977,12 +3006,16 @@ private:
return false;
}
// Candidates are tried on a reset model (resetModel precedes initialize) and
// touch only their two images: sweeping every image cost a 3000-frame video,
// which rejects every neighbour pair, 80 MB of stores per candidate.
void rollbackInit(uint32_t a, uint32_t b) {
rec_.points3D.clear();
rec_.next_point3D_id = 1;
for (auto& kv : rec_.images) {
kv.second.registered = false;
std::fill(kv.second.point3D_ids.begin(), kv.second.point3D_ids.end(), kInvalidPoint3D);
for (uint32_t i : {a, b}) {
Image& im = rec_.images[i];
im.registered = false;
std::fill(im.point3D_ids.begin(), im.point3D_ids.end(), kInvalidPoint3D);
}
}
@@ -3969,66 +4002,179 @@ private:
triangulateForImageAt(mi, img, err_scale);
}
using WorldRays = std::vector<std::vector<float>>; // by image id, 3 per feature
struct NewTrack {
Vec3 X;
uint32_t f = 0; // the feature of the image it was made for
std::vector<TrackElement> track;
};
// The index is a whole-model scan, so a caller that runs this over every
// registered image (completeAndRetriangulate) builds it once.
void triangulateForImageAt(const ModelIndex& mi, uint32_t img, double err_scale) {
Image& me = *mi.img[img];
std::vector<Correspondence> obs; // reused across features
std::vector<TrackElement> track;
for (uint32_t f = 0; f < feats_[img].count(); f++) {
if (me.point3D_ids[f] != kInvalidPoint3D) continue;
// candidate observations: registered, feature not yet on a 3D point
obs.clear();
for (const Correspondence& c : graph_.at(img, f))
if (mi.img[c.image_id]->registered &&
mi.img[c.image_id]->point3D_ids[c.feature_idx] == kInvalidPoint3D)
obs.push_back(c);
if (obs.empty()) continue;
// Triangulate with the correspondence of maximum parallax.
Vec3 bestX;
double bestAng = -1;
const Correspondence* bestC = nullptr;
for (const Correspondence& c : obs) {
Vec3 X;
if (!triangulatePairAt(mi, img, f, c.image_id, c.feature_idx, X, err_scale))
continue;
double ang = triangulationAngle(X, cameraCenter(me.pose),
cameraCenter(mi.img[c.image_id]->pose));
if (ang > bestAng) { bestAng = ang; bestX = X; bestC = &c; }
}
if (!bestC) continue;
// Build the track: this obs + every candidate that reprojects well.
track.clear();
track.push_back({img, f});
for (const Correspondence& c : obs) {
if (mi.img[c.image_id]->point3D_ids[c.feature_idx] != kInvalidPoint3D) continue;
if (reprojErrAt(mi, c.image_id, c.feature_idx, bestX) <=
err_scale * (opt_.max_reproj_error * mi.pixel_scale[c.image_id]))
track.push_back({c.image_id, c.feature_idx});
}
if (track.size() < 2) continue;
// Guard against two features from the same image on one track.
std::sort(track.begin(), track.end(),
[](const TrackElement& a, const TrackElement& b) {
return a.image_id < b.image_id;
});
track.erase(std::unique(track.begin(), track.end(),
[](const TrackElement& a, const TrackElement& b) {
return a.image_id == b.image_id;
}),
track.end());
if (track.size() < 2) continue;
rec_.addPoint3D(bestX, track);
// Every track element is a registered image's feature joining a
// point. (Also reached from completeAndRetriangulate, where the
// counts drift against its direct track edits -- harmless, no
// ranking happens before the post-refine rebuildScores().)
for (const TrackElement& e : track) attachObservation(e.image_id, e.point2D_idx);
}
const uint32_t n = feats_[img].count();
const size_t kBlock = 256;
std::vector<std::vector<NewTrack>> made((n + kBlock - 1) / kBlock);
parallelFor(n, kBlock, [&](size_t lo, size_t hi, std::vector<uint8_t>&) {
std::vector<Correspondence> obs;
NewTrack t;
for (size_t f = lo; f < hi; f++)
if (featureTrack(mi, img, (uint32_t)f, err_scale, obs, t))
made[lo / kBlock].push_back(t);
});
commitCollected(mi, img, made, err_scale);
}
// Commit tracks featureTrack collected against an earlier state, in order;
// one whose elements a commit since has taken is made again from now.
void commitCollected(const ModelIndex& mi, uint32_t img,
const std::vector<std::vector<NewTrack>>& made, double err_scale) {
std::vector<Correspondence> obs;
NewTrack again;
for (const std::vector<NewTrack>& block : made)
for (const NewTrack& t : block) {
bool clash = false;
for (const TrackElement& e : t.track)
clash = clash || mi.img[e.image_id]->point3D_ids[e.point2D_idx] != kInvalidPoint3D;
if (!clash) commitNewTrack(t);
else if (featureTrack(mi, img, t.f, err_scale, obs, again)) commitNewTrack(again);
}
}
// The point triangulateForImageAt makes for feature f of img, if any. Read
// only: a later claim changes the answer only by taking one of `out`'s
// elements (the best pair's is among them), so a snapshot's answer checks.
bool featureTrack(const ModelIndex& mi, uint32_t img, uint32_t f, double err_scale,
std::vector<Correspondence>& obs, NewTrack& out,
const WorldRays* rays = nullptr) const {
const Image& me = *mi.img[img];
if (me.point3D_ids[f] != kInvalidPoint3D) return false;
// candidate observations: registered, feature not yet on a 3D point
obs.clear();
for (const Correspondence& c : graph_.at(img, f))
if (mi.img[c.image_id]->registered &&
mi.img[c.image_id]->point3D_ids[c.feature_idx] == kInvalidPoint3D)
obs.push_back(c);
if (obs.empty()) return false;
const Vec3 bf = mi.cam[img]->bearing(kp(img, f));
std::vector<Vec3>& bo = obs_bearing_scratch();
bo.assign(obs.size(), Vec3{0, 0, 0});
auto bearingOf = [&](size_t k) -> const Vec3& {
if (bo[k].x == 0 && bo[k].y == 0 && bo[k].z == 0)
bo[k] = mi.cam[obs[k].image_id]->bearing(kp(obs[k].image_id, obs[k].feature_idx));
return bo[k];
};
// Triangulate with the correspondence of maximum parallax. Rays closer
// than the minimum angle less both reprojection tolerances cannot:
// 94% of a dual-fisheye video's candidates, frames 1/30 s apart.
Vec3 ra = mul(transpose(me.pose.R), bf);
ra = ra * (1.0 / ra.norm());
const double tol_a = rayTolerance(mi, img, err_scale);
Vec3 bestX;
double bestAng = -1;
const Correspondence* bestC = nullptr;
for (size_t k = 0; k < obs.size(); k++) {
const Correspondence& c = obs[k];
const double lim = opt_.min_tri_angle_deg * M_PI / 180.0 - tol_a -
rayTolerance(mi, c.image_id, err_scale);
if (lim > 0) {
Vec3 rb;
if (rays) {
const float* r = &(*rays)[c.image_id][3 * (size_t)c.feature_idx];
rb = {r[0], r[1], r[2]};
} else {
rb = mul(transpose(mi.img[c.image_id]->pose.R), bearingOf(k));
rb = rb * (1.0 / rb.norm());
}
if (ra.dot(rb) > std::cos(lim)) continue;
}
Vec3 X;
if (!triangulatePairAt(mi, img, f, bf, c.image_id, c.feature_idx, bearingOf(k), X,
err_scale))
continue;
double ang = triangulationAngle(X, cameraCenter(me.pose),
cameraCenter(mi.img[c.image_id]->pose));
if (ang > bestAng) { bestAng = ang; bestX = X; bestC = &c; }
}
if (!bestC) return false;
// Build the track: this obs + every candidate that reprojects well.
std::vector<TrackElement>& track = out.track;
track.clear();
track.push_back({img, f});
for (size_t k = 0; k < obs.size(); k++) {
const Correspondence& c = obs[k];
if (reprojErrAt(mi, c.image_id, c.feature_idx, bestX, bearingOf(k)) <=
err_scale * (opt_.max_reproj_error * mi.pixel_scale[c.image_id]))
track.push_back({c.image_id, c.feature_idx});
}
if (track.size() < 2) return false;
// Guard against two features from the same image on one track.
std::sort(track.begin(), track.end(),
[](const TrackElement& a, const TrackElement& b) {
return a.image_id < b.image_id;
});
track.erase(std::unique(track.begin(), track.end(),
[](const TrackElement& a, const TrackElement& b) {
return a.image_id == b.image_id;
}),
track.end());
if (track.size() < 2) return false;
out.X = bestX;
out.f = f;
return true;
}
// Registered images' feature rays, world frame, unit, float: a fisheye bearing
// is an iterative inversion and the filter wanted 166M a pass. Float error is
// far inside rayTolerance's slack; what passes is decided on exact bearings.
WorldRays worldRays(const ModelIndex& mi) {
WorldRays out(db_.images.size());
std::vector<uint32_t> imgs;
for (uint32_t i = 0; i < db_.images.size(); i++)
if (mi.img[i] && mi.img[i]->registered) imgs.push_back(i);
parallelFor(imgs.size(), 4, [&](size_t lo, size_t hi, std::vector<uint8_t>&) {
for (size_t j = lo; j < hi; j++) {
const uint32_t i = imgs[j];
const Mat3 Rt = transpose(mi.img[i]->pose.R);
const uint32_t n = feats_[i].count();
std::vector<float>& r = out[i];
r.resize(3 * (size_t)n);
for (uint32_t f = 0; f < n; f++) {
Vec3 w = mul(Rt, mi.cam[i]->bearing(kp(i, f)));
w = w * (1.0 / w.norm());
r[3 * (size_t)f] = (float)w.x;
r[3 * (size_t)f + 1] = (float)w.y;
r[3 * (size_t)f + 2] = (float)w.z;
}
}
});
return out;
}
// The angle a reprojection tolerance can move a ray by: twice the tolerance
// over the focal, for a lens whose rim compresses the pixel scale.
double rayTolerance(const ModelIndex& mi, uint32_t img, double err_scale) const {
const Camera& c = *mi.cam[img];
return 2.0 * err_scale * opt_.max_reproj_error * mi.pixel_scale[img] / std::min(c.fx, c.fy);
}
// featureTrack's candidate bearings; each worker thread sizes and uses its own.
static std::vector<Vec3>& obs_bearing_scratch() {
static thread_local std::vector<Vec3> v;
return v;
}
void commitNewTrack(const NewTrack& t) {
rec_.addPoint3D(t.X, t.track);
// Each element joins a point. Completion's direct track edits leave these
// counts behind -- harmless: nothing ranks before rebuildScores().
for (const TrackElement& e : t.track) attachObservation(e.image_id, e.point2D_idx);
}
// ---- global refinement: BA + filtering + image de-registration ----
//
// COLMAP's IterativeGlobalRefinement in miniature: bundle-adjust, filter,
@@ -4284,39 +4430,101 @@ private:
void completeAndRetriangulate() {
const double err = opt_.retri_scale * opt_.max_reproj_error; // x pixel_scale below
ModelIndex mi = indexModel();
// "Is this image already on the track" as a dense flag rather than a
// std::set rebuilt (and heap-allocated) per point: there are hundreds
// of thousands of points and the sets were short-lived and tiny.
// Cleared by unsetting only what was set, so it stays O(track).
std::vector<std::pair<uint64_t, Point3D*>> pts;
pts.reserve(rec_.points3D.size());
for (auto& kv : rec_.points3D) pts.emplace_back(kv.first, &kv.second);
std::vector<uint32_t> imgs;
for (const auto& kv : rec_.images)
if (kv.second.registered) imgs.push_back(kv.first);
// Collected in parallel against the pass's starting state, committed in
// the serial order; an item whose claims an earlier commit took is redone
// then, so the result is the serial pass's (src/sfm/README.md, "Retriangulation").
std::vector<std::vector<TrackElement>> added(pts.size());
parallelFor(pts.size(), 256, [&](size_t lo, size_t hi, std::vector<uint8_t>& on_track) {
for (size_t i = lo; i < hi; i++) collectCompletion(mi, *pts[i].second, err, on_track,
added[i]);
});
std::vector<uint8_t> on_track(db_.images.size(), 0);
for (auto& kv : rec_.points3D) {
Point3D& pt = kv.second;
for (const TrackElement& e : pt.track) on_track[e.image_id] = 1;
for (size_t ti = 0; ti < pt.track.size(); ti++) {
const TrackElement e = pt.track[ti]; // copy: track grows below
for (const Correspondence& c : graph_.at(e.image_id, e.point2D_idx)) {
Image& oi = *mi.img[c.image_id];
if (!oi.registered || on_track[c.image_id] ||
oi.point3D_ids[c.feature_idx] != kInvalidPoint3D)
continue;
if (reprojErrAt(mi, c.image_id, c.feature_idx, pt.xyz) <=
err * mi.pixel_scale[c.image_id]) {
pt.track.push_back({c.image_id, c.feature_idx});
on_track[c.image_id] = 1;
oi.point3D_ids[c.feature_idx] = kv.first;
}
}
std::vector<TrackElement> redo;
for (size_t i = 0; i < pts.size(); i++) {
Point3D& pt = *pts[i].second;
bool clash = false;
for (const TrackElement& e : added[i])
clash = clash || mi.img[e.image_id]->point3D_ids[e.point2D_idx] != kInvalidPoint3D;
if (clash) collectCompletion(mi, pt, err, on_track, redo);
for (const TrackElement& e : clash ? redo : added[i]) {
pt.track.push_back(e);
mi.img[e.image_id]->point3D_ids[e.point2D_idx] = pts[i].first;
}
for (const TrackElement& e : pt.track) on_track[e.image_id] = 0;
}
for (auto& kv : rec_.images)
if (kv.second.registered) triangulateForImageAt(mi, kv.first, opt_.retri_scale);
std::vector<std::vector<std::vector<NewTrack>>> made(imgs.size());
{
const WorldRays rays = worldRays(mi);
parallelFor(imgs.size(), 4, [&](size_t lo, size_t hi, std::vector<uint8_t>&) {
std::vector<Correspondence> obs;
NewTrack t;
for (size_t i = lo; i < hi; i++) {
made[i].resize(1);
for (uint32_t f = 0; f < feats_[imgs[i]].count(); f++)
if (featureTrack(mi, imgs[i], f, opt_.retri_scale, obs, t, &rays))
made[i][0].push_back(t);
}
});
}
for (size_t i = 0; i < imgs.size(); i++)
commitCollected(mi, imgs[i], made[i], opt_.retri_scale);
if (opt_.merge_tracks) {
ProfTimer pt(g_map_prof.merge);
g_map_prof.n_merged += mergeTracks(mi, opt_.retri_scale);
}
}
// The observations track completion adds to `pt`: every correspondence of
// an element, transitively, that is free, on an image the track lacks, and
// reprojects within `err`. `on_track` is all zero on entry and on return.
void collectCompletion(const ModelIndex& mi, const Point3D& pt, double err,
std::vector<uint8_t>& on_track, std::vector<TrackElement>& add) const {
add.clear();
for (const TrackElement& e : pt.track) on_track[e.image_id] = 1;
const size_t n0 = pt.track.size();
for (size_t ti = 0; ti < n0 + add.size(); ti++) {
const TrackElement e = ti < n0 ? pt.track[ti] : add[ti - n0];
for (const Correspondence& c : graph_.at(e.image_id, e.point2D_idx)) {
const Image& oi = *mi.img[c.image_id];
if (!oi.registered || on_track[c.image_id] ||
oi.point3D_ids[c.feature_idx] != kInvalidPoint3D)
continue;
if (reprojErrAt(mi, c.image_id, c.feature_idx, pt.xyz) <=
err * mi.pixel_scale[c.image_id]) {
add.push_back({c.image_id, c.feature_idx});
on_track[c.image_id] = 1;
}
}
}
for (const TrackElement& e : pt.track) on_track[e.image_id] = 0;
for (const TrackElement& e : add) on_track[e.image_id] = 0;
}
// fn(lo, hi, scratch) over [0, n) in blocks, on the mapper's threads; each
// worker gets a zeroed flag buffer the size of the image table.
template <class F>
void parallelFor(size_t n, size_t block, F&& fn) {
const unsigned hc = std::thread::hardware_concurrency();
int nt = opt_.threads > 0 ? opt_.threads : (hc > 0 ? (int)hc : 1);
nt = std::max(1, std::min<int>(nt, (int)((n + block - 1) / block)));
std::atomic<size_t> next{0};
auto worker = [&] {
std::vector<uint8_t> scratch(db_.images.size(), 0);
for (size_t b; (b = next.fetch_add(block)) < n;) fn(b, std::min(b + block, n), scratch);
};
if (nt == 1) return worker();
std::vector<std::thread> pool;
for (int t = 0; t < nt; t++) pool.emplace_back(worker);
for (std::thread& t : pool) t.join();
}
size_t countObservations() const {
size_t n = 0;
for (const auto& kv : rec_.points3D) n += kv.second.track.size();
+304 -7
View File
@@ -34,6 +34,8 @@ static const uint kCgRR = 4; // ||r||^2
static const uint kCgTol2 = 5; // eta^2 ||r0||^2
static const uint kCgConv = 6; // > 0.5: loop kernels become no-ops
static const uint kCgIters = 7; // completed CG iterations
static const uint kCgQ = 8; // quadratic model 1/2 x^T S x - g^T x at x
static const uint kCgDQ = 9; // ... and its change over the last iteration
// Packed block strides (lower triangles): preconditioner blocks are at most
// kPrecDof wide, per-image B blocks as wide as the tier.
@@ -174,8 +176,9 @@ void cgCamDiagImpl<let MD : int>(uint wgx, uint gi) {
atomicAccum(u_g_a, cols[gi], -gacc);
}
// Factor each block in place (packed lower Cholesky, guarded like chol_diag).
// One thread per block; blocks are at most kPrecDof wide. u0 = num blocks
// Factor each block in place (packed lower Cholesky). A block can be all
// cancellation noise (Gram entries of 2e23 on a 22042-image capture): a pivot
// under 1e-10 of its largest diagonal is floored and decoupled. u0 = blocks
[shader("compute")] [numthreads(256, 1, 1)]
void cg_prec_fact(uint3 t : SV_DispatchThreadID) {
uint b = t.x;
@@ -186,10 +189,15 @@ void cg_prec_fact(uint3 t : SV_DispatchThreadID) {
uint cb = kCamBlk * b;
Real L[kCamBlk];
for (uint i = 0; i < dof * (dof + 1) / 2; i++) L[i] = u_cg_M[cb + i];
Real f = Real(1e-30);
for (uint j = 0; j < dof; j++)
if (L[pidx(j, j)] > f) f = L[pidx(j, j)];
f = max(Real(1e-10) * f, Real(1e-30));
for (uint j = 0; j < dof; j++) {
Real d = sqrt(max(L[pidx(j, j)], Real(1e-30)));
bool ok = L[pidx(j, j)] > f;
Real d = sqrt(ok ? L[pidx(j, j)] : f);
L[pidx(j, j)] = d;
for (uint i = j + 1; i < dof; i++) L[pidx(i, j)] = L[pidx(i, j)] / d;
for (uint i = j + 1; i < dof; i++) L[pidx(i, j)] = ok ? L[pidx(i, j)] / d : Real(0.0);
for (uint c = j + 1; c < dof; c++)
for (uint i = c; i < dof; i++)
L[pidx(i, c)] = L[pidx(i, c)] - L[pidx(i, j)] * L[pidx(c, j)];
@@ -394,9 +402,9 @@ void cg_red2(uint3 t : SV_DispatchThreadID, uint3 wg : SV_GroupID, uint gi : SV_
}
}
// Final partial reduction + scalar updates; one workgroup.
// u0 = partial count, u1 = mode (0: init rho/tol, 1: alpha, 2: beta/converge),
// u2 = partial stride, f0 = eta (mode 0: relative residual tolerance)
// Final partial reduction + scalar updates; one workgroup. u0 = partials,
// u1 = mode (0: init rho/tol, 1: alpha, 2: beta/converge), u2 = partial stride,
// f0 = relative residual tolerance (mode 0), f1 = Nash-Sofer tolerance (0: off)
[shader("compute")] [numthreads(256, 1, 1)]
void cg_fin(uint gi : SV_GroupIndex) {
if (u1 != 0 && cgDone()) return;
@@ -423,6 +431,7 @@ void cg_fin(uint gi : SV_GroupIndex) {
u_cg_scal[kCgRR] = B;
u_cg_scal[kCgTol2] = Real(f0) * Real(f0) * B;
u_cg_scal[kCgIters] = Real(0.0);
u_cg_scal[kCgQ] = Real(0.0);
// zero RHS (or a broken preconditioner) => keep the zero step
u_cg_scal[kCgConv] = (isfinite(A) && A > Real(0.0)) ? Real(0.0) : Real(1.0);
} else if (u1 == 1) { // A = p.Sp
@@ -430,6 +439,10 @@ void cg_fin(uint gi : SV_GroupIndex) {
bool ok = isfinite(A) && A > Real(0.0);
u_cg_scal[kCgPAp] = A;
u_cg_scal[kCgAlpha] = ok ? rho / A : Real(0.0);
// the step along p lowers the model by alpha rho / 2
Real dq = ok ? Real(-0.5) * rho * rho / A : Real(0.0);
u_cg_scal[kCgDQ] = dq;
u_cg_scal[kCgQ] = u_cg_scal[kCgQ] + dq;
if (!ok) u_cg_scal[kCgConv] = Real(1.0); // lost positive-definiteness
} else { // A = new r.z, B = new ||r||^2
Real rho = u_cg_scal[kCgRho];
@@ -439,6 +452,15 @@ void cg_fin(uint gi : SV_GroupIndex) {
u_cg_scal[kCgIters] = u_cg_scal[kCgIters] + Real(1.0);
if (!(B > u_cg_scal[kCgTol2]) || !isfinite(A) || !(A > Real(0.0)))
u_cg_scal[kCgConv] = Real(1.0);
// Nash-Sofer, as Ceres stops LM's CG: i (Q_i - Q_i-1) / Q_i under f1.
// 40% fewer iterations than the residual test alone on a 4102-image
// capture, same final cost. Marked as the residual test being met.
Real q = u_cg_scal[kCgQ];
if (f1 > 0.0f && q < Real(0.0) &&
u_cg_scal[kCgIters] * u_cg_scal[kCgDQ] / q < Real(f1)) {
u_cg_scal[kCgConv] = Real(1.0);
u_cg_scal[kCgTol2] = B;
}
}
}
@@ -461,3 +483,278 @@ void cg_updp(uint3 t : SV_DispatchThreadID) {
if (i >= u0) return;
u_cg_p[i] = u_cg_z[i] + u_cg_scal[kCgBeta] * u_cg_p[i];
}
// ---------------------------------------------------------------------
// coarse correction: M^-1 = block Jacobi + P A_c^-1 P^T
// ---------------------------------------------------------------------
// P: a similarity motion (7 dofs) per cluster of u2 frames, as pose deltas
// (README.md, "Coarse correction"). The CG path leaves u_S and u_y free: A_c
// goes in u_S with P (6x7 per frame) from u3; tables follow the pair-Schur ones.
static const uint kTcDof = 7;
uint tcCluster(uint o) { return u_image_info[u_obs_image[o]].x / u2; }
// d(angle-axis) = -Jr^-1 w and dt = s t - R tau for a world similarity
// (w, tau, s), in fp32: the basis only has to span the slow modes, not be
// exact. u0 = num_frames
[shader("compute")] [numthreads(64, 1, 1)]
void tc_basis(uint3 t : SV_DispatchThreadID) {
uint f = t.x;
if (f >= u0) return;
float a[3], tr[3];
for (int i = 0; i < 3; i++) {
a[i] = rToFloat(u_poses[6 * f + i]);
tr[i] = rToFloat(u_poses[6 * f + 3 + i]);
}
float th2 = a[0] * a[0] + a[1] * a[1] + a[2] * a[2], th = sqrt(th2);
float K[9] = {0, -a[2], a[1], a[2], 0, -a[0], -a[1], a[0], 0};
float K2[9];
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
K2[3 * i + j] = K[3 * i] * K[j] + K[3 * i + 1] * K[3 + j] + K[3 * i + 2] * K[6 + j];
bool nearZero = th < 1e-3f;
float s1 = nearZero ? 1.0f : sin(th) / th;
float c1 = nearZero ? 0.5f : (1.0f - cos(th)) / th2;
float c2 = nearZero ? 1.0f / 12.0f : 1.0f / th2 - (1.0f + cos(th)) / (2.0f * th * sin(th));
uint b = u3 + 42 * f;
for (int i = 0; i < 3; i++)
for (int j = 0; j < 7; j++) {
float rot = j < 3 ? -(float(i == j) + 0.5f * K[3 * i + j] + c2 * K2[3 * i + j]) : 0.0f;
float R = j >= 3 && j < 6 ? float(i == j - 3) + s1 * K[3 * i + j - 3] + c1 * K2[3 * i + j - 3]
: 0.0f;
u_S[b + 7 * i + j] = Real(rot);
u_S[b + 7 * (3 + i) + j] = Real(j == 6 ? tr[i] : -R);
}
}
// A_c += P_f^T B_img P_f over the pose rows of each image's damped Gram block.
// u0 = num_images, u2 = frames per cluster, u3 = basis offset
void tcBpartImpl<let MD : int>(uint img) {
if (img >= u0) return;
uint f = u_image_info[img].x, c = f / u2;
uint cb = bblk<MD>() * img, pb = u3 + 42 * f;
Real BP[42];
for (uint r = 0; r < 6; r++)
for (uint j = 0; j < 7; j++) {
Real s = Real(0.0);
for (uint l = 0; l < 6; l++)
s = s + u_cg_B[cb + (r >= l ? pidx(r, l) : pidx(l, r))] * u_S[pb + 7 * l + j];
BP[7 * r + j] = s;
}
for (uint i = 0; i < 7; i++)
for (uint j = 0; j <= i; j++) {
Real s = Real(0.0);
for (uint l = 0; l < 6; l++) s = s + u_S[pb + 7 * l + i] * BP[7 * l + j];
if (isfinite(s)) atomicAccum(u_S_a, pidx(kTcDof * c + i, kTcDof * c + j), s);
}
}
// G = sum over the run starting at observation o of P_f^T Jc_pose^T Jp (7x3).
void tcRunG(uint o, uint t1, out Real G[21]) {
for (int i = 0; i < 21; i++) G[i] = Real(0.0);
uint c = tcCluster(o);
for (; o < t1 && tcCluster(o) == c; o++) {
uint pb = u3 + 42 * u_image_info[u_obs_image[o]].x, jb = u_jc_off[o];
for (uint i = 0; i < 7; i++) {
Real q0 = Real(0.0), q1 = Real(0.0);
for (uint a = 0; a < 6; a++) {
q0 = q0 + u_S[pb + 7 * a + i] * u_Jc[jb + 2 * a];
q1 = q1 + u_S[pb + 7 * a + i] * u_Jc[jb + 2 * a + 1];
}
for (uint j = 0; j < 3; j++)
G[3 * i + j] = G[3 * i + j] + q0 * u_Jp[6 * o + 2 * j] + q1 * u_Jp[6 * o + 2 * j + 1];
}
}
}
// A_c[cu, cv] -= sum G_u W G_v^T, one warp per chunk of one cluster pair's
// entries: lanes stage their 7x7 products in shared and the warp folds them
// into 49 sums, written once. u0 = chunks, u1 = first chunk, u2/u3 as above
[shader("compute")] [numthreads(kPairWg, 1, 1)]
void tc_schur(uint3 wg : SV_GroupID, uint gi : SV_GroupIndex) {
uint wgx = wg.x + u4;
if (wgx >= u0) return;
uint2 ck = u_pair_chunks[u1 + wgx];
uint2 e0 = u_pair_entries[ck.x];
uint cu = tcCluster(e0.x), cv = tcCluster(e0.y);
Real acc0 = Real(0.0), acc1 = Real(0.0);
for (uint b0 = 0; b0 < ck.y; b0 += kPairWg) {
GroupMemoryBarrierWithGroupSync();
if (b0 + gi < ck.y) {
uint2 e = u_pair_entries[ck.x + b0 + gi];
uint p = u_obs_point[e.x], t1 = u_obs_ranges[p + 1];
Real G[21], GW[21];
tcRunG(e.x, t1, G);
for (uint j = 0; j < 3; j++) {
Real w0 = u_W[9 * p + j], w1 = u_W[9 * p + 3 + j], w2 = u_W[9 * p + 6 + j];
for (uint i = 0; i < 7; i++)
GW[3 * i + j] = G[3 * i] * w0 + G[3 * i + 1] * w1 + G[3 * i + 2] * w2;
}
if (e.y != e.x) tcRunG(e.y, t1, G);
for (uint i = 0; i < 7; i++)
for (uint j = 0; j < 7; j++) {
Real v = GW[3 * i] * G[3 * j] + GW[3 * i + 1] * G[3 * j + 1]
+ GW[3 * i + 2] * G[3 * j + 2];
g_chol[49 * gi + 7 * i + j] = isfinite(v) ? v : Real(0.0);
}
} else {
for (uint i = 0; i < 49; i++) g_chol[49 * gi + i] = Real(0.0);
}
GroupMemoryBarrierWithGroupSync();
uint nl = min(kPairWg, ck.y - b0);
for (uint l = 0; l < nl; l++) {
acc0 = acc0 + g_chol[49 * l + gi];
if (gi + kPairWg < 49) acc1 = acc1 + g_chol[49 * l + gi + kPairWg];
}
}
for (uint k = 0; k < 2; k++) {
uint el = gi + kPairWg * k;
if (el >= 49) continue;
uint R = kTcDof * cu + el / 7, C = kTcDof * cv + el % 7;
if (R >= C) atomicAccum(u_S_a, pidx(R, C), -(k == 0 ? acc0 : acc1));
}
}
// A_c diagonal *= 1 + f0, and saved in u_y for the factor's pivot floor: the
// global similarity is a gauge freedom, so A_c is singular up to the damping.
// u0 = coarse dim
[shader("compute")] [numthreads(256, 1, 1)]
void tc_reg(uint3 t : SV_DispatchThreadID) {
uint i = t.x;
if (i >= u0) return;
Real d = u_S[pidx(i, i)];
u_y[i] = d;
u_S[pidx(i, i)] = d * (Real(1.0) + Real(f0));
}
// ---- L -> L^-1 in place, so that each application is two matrix-vector
// products rather than 2 n / 32 dependent triangular-solve dispatches ----
// Invert every 32x32 diagonal block, one thread per column. u0 = n
[shader("compute")] [numthreads(kBs, 1, 1)]
void tc_dinv(uint3 wg : SV_GroupID, uint gi : SV_GroupIndex) {
uint base = wg.x * kBs, m = min(kBs, u0 - base), c = gi;
for (uint r = c; r < m; r++)
g_chol[r * kBs + c] = u_S[pidx(base + r, base + c)];
GroupMemoryBarrierWithGroupSync();
// column c of the inverse, into the upper half of the pool
if (c < m) {
g_chol[kSh2 + c * kPitch + c] = Real(1.0) / g_chol[c * kBs + c];
for (uint r = c + 1; r < m; r++) {
Real s = Real(0.0);
for (uint k = c; k < r; k++) s = s + g_chol[r * kBs + k] * g_chol[kSh2 + k * kPitch + c];
g_chol[kSh2 + r * kPitch + c] = -s / g_chol[r * kBs + r];
}
}
GroupMemoryBarrierWithGroupSync();
for (uint r = c; r < m; r++)
u_S[pidx(base + r, base + c)] = g_chol[kSh2 + r * kPitch + c];
}
// Block row u1 of the inverse, X_ij = -X_ii sum_{j<=k<i} L_ik X_kj, into
// scratch rows u_y[u2 + r * n + col] while the row's L blocks are still read.
// One 32x32 workgroup per block j < i. u0 = n
[shader("compute")] [numthreads(kBs, kBs, 1)]
void tc_inv_row(uint3 wg : SV_GroupID, uint3 lt : SV_GroupThreadID) {
uint n = u0, i = u1, j = wg.x;
uint r = lt.y, c = lt.x;
uint bi = i * kBs, bj = j * kBs;
Real acc = Real(0.0);
for (uint k = j; k < i; k++) {
uint bk = k * kBs;
GroupMemoryBarrierWithGroupSync();
g_chol[r * kBs + c] = bi + r < n ? u_S[pidx(bi + r, bk + c)] : Real(0.0);
g_chol[kSh2 + r * kPitch + c] = bk + r >= bj + c ? u_S[pidx(bk + r, bj + c)] : Real(0.0);
GroupMemoryBarrierWithGroupSync();
for (uint m = 0; m < kBs; m++)
acc = acc + g_chol[r * kBs + m] * g_chol[kSh2 + m * kPitch + c];
}
GroupMemoryBarrierWithGroupSync();
g_chol[kSh2 + r * kPitch + c] = acc;
g_chol[r * kBs + c] = bi + r < n && c <= r ? u_S[pidx(bi + r, bi + c)] : Real(0.0);
GroupMemoryBarrierWithGroupSync();
Real x = Real(0.0);
for (uint m = 0; m <= r; m++) x = x + g_chol[r * kBs + m] * g_chol[kSh2 + m * kPitch + c];
if (bi + r < n) u_y[u2 + r * n + bj + c] = -x;
}
// Scratch row -> block row u1. u0 = n, one thread per element.
[shader("compute")] [numthreads(256, 1, 1)]
void tc_inv_copy(uint3 t : SV_DispatchThreadID) {
uint n = u0, bi = u1 * kBs;
uint r = t.x / bi, col = t.x - r * bi;
if (r >= kBs || bi + r >= n) return;
u_S[pidx(bi + r, col)] = u_y[u2 + r * n + col];
}
// ---- application ----
// v = P^T r into u_y[0, u0), one thread per coarse dof. u1 = num_frames
[shader("compute")] [numthreads(256, 1, 1)]
void tc_restrict(uint3 t : SV_DispatchThreadID) {
if (cgDone()) return;
uint q = t.x;
if (q >= u0) return;
uint c = q / kTcDof, j = q - kTcDof * c;
uint fend = min(u1, (c + 1) * u2);
Real s = Real(0.0);
for (uint f = c * u2; f < fend; f++)
for (uint a = 0; a < 6; a++) s = s + u_S[u3 + 42 * f + 7 * a + j] * u_cg_r[6 * f + a];
u_y[q] = s;
}
// w = X v into u_y[u0, 2 u0), one warp per row. u0 = coarse dim
[shader("compute")] [numthreads(256, 1, 1)]
void tc_lmul(uint3 t : SV_DispatchThreadID, uint gi : SV_GroupIndex) {
if (cgDone()) return;
uint row = t.x / 32, lane = gi % 32, w = gi / 32;
Real s = Real(0.0);
if (row < u0)
for (uint j = lane; j <= row; j += 32) s = s + u_S[pidx(row, j)] * u_y[j];
g_sh[gi] = s;
GroupMemoryBarrierWithGroupSync();
for (uint h = 16; h > 0; h >>= 1) {
if (lane < h) g_sh[gi] = g_sh[gi] + g_sh[gi + h];
GroupMemoryBarrierWithGroupSync();
}
if (lane == 0 && row < u0) u_y[u0 + row] = g_sh[32 * w];
}
// z = X^T w into u_y[0, u0): 32 columns per workgroup, so each packed row
// segment is one coalesced read, and 8 row lanes summed in shared.
[shader("compute")] [numthreads(256, 1, 1)]
void tc_ltmul(uint3 wg : SV_GroupID, uint gi : SV_GroupIndex) {
if (cgDone()) return;
uint cc = gi % 32, rl = gi / 32;
uint col = wg.x * 32 + cc;
Real s = Real(0.0);
if (col < u0)
for (uint r = wg.x * 32 + rl; r < u0; r += 8)
if (r >= col) s = s + u_S[pidx(r, col)] * u_y[u0 + r];
g_sh[gi] = s;
GroupMemoryBarrierWithGroupSync();
if (rl == 0 && col < u0) {
Real z = Real(0.0);
for (uint k = 0; k < 8; k++) z = z + g_sh[32 * k + cc];
u_y[col] = z;
}
}
// z += P y over the pose columns. u0 = pose_dim
[shader("compute")] [numthreads(256, 1, 1)]
void tc_prolong(uint3 t : SV_DispatchThreadID) {
if (cgDone()) return;
uint i = t.x;
if (i >= u0) return;
uint f = i / 6, a = i - 6 * f, c = f / u2;
Real s = Real(0.0);
for (uint j = 0; j < 7; j++) s = s + u_S[u3 + 42 * f + 7 * a + j] * u_y[kTcDof * c + j];
u_cg_z[i] = u_cg_z[i] + s;
}
[shader("compute")] [numthreads(64, 1, 1)]
void tc_bpart_w(uint3 t : SV_DispatchThreadID) { tcBpartImpl<kDofWide>(t.x); }
[shader("compute")] [numthreads(64, 1, 1)]
void tc_bpart_x(uint3 t : SV_DispatchThreadID) { tcBpartImpl<kDofRig>(t.x); }
+11 -2
View File
@@ -12,6 +12,15 @@
// Included at the end of ba.slang; shares its bindings and push constants.
static const uint kBs = 32;
// Pivot from trailing diagonal x (NaN included): at least 1e-30; with u3 set, one
// under f0 of the row's original diagonal (in u_y) becomes that diagonal, which
// bounds the factor however indefinite rounding made it (cg.slang's coarse one).
Real pivot(Real x, uint row) {
if (u3 == 0) return sqrt(x > Real(1e-30) ? x : Real(1e-30));
Real d = u_y[row];
return sqrt(x > Real(f0) * d ? x : max(d, Real(1e-30)));
}
static const uint kPitch = 33; // padded shared row pitch (bank-conflict-free)
static const uint kShV = kSh2 + 32 * 33; // RHS vector slot in the shared pool
@@ -30,7 +39,7 @@ void chol_diag(uint3 t : SV_DispatchThreadID) {
for (uint j = 0; j < m; j++) {
GroupMemoryBarrierWithGroupSync();
if (r == j)
g_chol[j * kPitch + j] = sqrt(max(g_chol[j * kPitch + j], Real(1e-30)));
g_chol[j * kPitch + j] = pivot(g_chol[j * kPitch + j], base + j);
GroupMemoryBarrierWithGroupSync();
if (r > j && r < m)
g_chol[r * kPitch + j] = g_chol[r * kPitch + j] / g_chol[j * kPitch + j];
@@ -128,7 +137,7 @@ void chol_update(uint3 wg : SV_GroupID, uint3 lt : SV_GroupThreadID) {
for (uint j = 0; j < m2; j++) {
GroupMemoryBarrierWithGroupSync();
if (r == j && c == j)
g_chol[j * kPitch + j] = sqrt(max(g_chol[j * kPitch + j], Real(1e-30)));
g_chol[j * kPitch + j] = pivot(g_chol[j * kPitch + j], bi + j);
GroupMemoryBarrierWithGroupSync();
if (c == j && r > j && r < m2)
g_chol[r * kPitch + j] = g_chol[r * kPitch + j] / g_chol[j * kPitch + j];
+2
View File
@@ -433,6 +433,7 @@ void testAgainstReference(uint32_t model, uint32_t groups, const char* loss, boo
opt.verbose = false;
opt.solver = cg ? SolverSel::CG : SolverSel::Dense;
opt.cg_tol = 1e-12;
opt.cg_model_tol = 0;
opt.cg_max_iters = 4000;
opt.cg_fallback = CgFallback::Off;
@@ -512,6 +513,7 @@ void testFullSolve(uint32_t model, uint32_t groups, uint32_t rig = 0) {
opt.verbose = false;
opt.solver = k ? SolverSel::CG : SolverSel::Dense;
opt.cg_tol = 1e-10;
opt.cg_model_tol = 0;
opt.cg_max_iters = 2000;
opt.cg_fallback = CgFallback::Off;
bacpu::Solver s(P, opt);
+1
View File
@@ -52,6 +52,7 @@ SolverOptions baseOptions(RealCfg real, int device, bool cg) {
o.verbose = false;
o.solver = cg ? SolverSel::CG : SolverSel::Dense;
o.cg_tol = 1e-10;
o.cg_model_tol = 0;
o.cg_max_iters = 3000;
o.cg_fallback = CgFallback::Off;
return o;