Cut the single-core time out of mapping, and make the Ceres backend configurable - #5
Closed
kevintsq wants to merge 5 commits into
Closed
Cut the single-core time out of mapping, and make the Ceres backend configurable#5kevintsq wants to merge 5 commits into
kevintsq wants to merge 5 commits into
Conversation
Bind the rest of BundleAdjustmentDiagnostics. Every field is populated in PopulateResult, but only three were exposed, so the BA failure path in adjuster.py raised AttributeError on termination_type instead of logging the warning and restoring the last valid reconstruction. Keep load_mapping_runtime() ahead of every torch import: libtorch_cpu.so exports its own statically linked BLAS/LAPACK, and when torch loads first those symbols win global resolution for SuiteSparse/Ceres, so CHOLMOD reports "matrix not positive definite" and bundle adjustment fails. Read retrieval descriptors with the h5py dataset spelling instead of calling Dataset.__array__() by hand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rebuilding vidmap_native currently produces a module that fails to import. COLMAP's static libraries reference Abseil's logging internals but its exported configuration does not list Abseil, so the module is left with undefined absl::log_internal symbols; and when Ceres resolves to a shared build outside the loader's search path, libceres cannot be found at import time. Link Abseil explicitly when it is available and keep the link-time library directories in the installed RPATH. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing on the mapping path ever sampled point color, so every written points3D.bin carried RGB (0, 0, 0) and COLMAP rendered the cloud black. Only the HTML visualization called extract_colors_for_all_images, which is why the same run looked colored there and blank in COLMAP. Sample colors from the run's recorded RGB root just before write. Color is cosmetic, so a missing or unreadable image root degrades to a warning rather than discarding a finished reconstruction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sampling process-tree CPU once a second over a 617-image mapping run showed 740s of its 1218s wall clock running on roughly one core, almost all of it in the Python that builds each bundle-adjustment problem: every BA round spent 52-62s preparing before a 35s native solve. The dominant cost was small_triangulation_angle_mask, called once per image while rebuilding a set over all 380k track ids on every call. The filter is per-track independent, so compute the small-angle ids once per solve and mask per image. Snapshot point coordinates and track lengths into one Point3DTable per solve as well, so per-image lookups are a binary search instead of a pybind call per observation; rows stay in reconstruction order because that order reaches Ceres through variable_point3D_ids. Also stop rebuilding every pycolmap point after a positioning pass, which only moves coordinates, and stop materializing all track records for a replay summary that is disabled by default. BA preparation drops to 6-8s per round; the run finishes in 681s with 250s on one core. Reconstructions stay inside the pipeline's own run-to-run spread: on an 89-image scene the baseline varies by 0.10% of scene radius between repeats, and before-versus-after differs by 0.05%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The large global solves hard-coded a direct sparse Schur factorization, whose factorization runs on one thread; the measured solves reach only 6-8 of 32 cores. Rotation averaging was pinned to a single thread with no way to change it. Expose linear_solver, preconditioner and use_cuda for bundle adjustment and global positioning, and num_threads for rotation averaging. Every default reproduces today's behavior: the direct sparse solve with CUDA off, and one thread for rotation averaging, which keeps that stage byte-identical as its pinned random seed intends. Drop global positioning's CLUSTER_TRIDIAGONAL preconditioner, which SPARSE_SCHUR ignores; timing it explicitly confirms no difference. Requesting CUDA is rejected with a clear message when Ceres was built without it, and sparse CUDA only exists from Ceres 2.2 on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Superseded by #6 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Mapping spent most of its wall clock on one core. Sampling process-tree CPU once a
second across a 617-image run showed 740s of 1218s (61%) running on roughly a single
core, almost none of it inside the native solvers. This branch removes the bulk of that
serial work, makes the Ceres solver backend configurable, fixes point colors, and repairs
the native build.
The same scene now finishes in 681s with 250s (37%) on one core.
What was slow
Every bundle-adjustment round spent 52–62s building the problem in Python before handing
a 35s solve to Ceres — preparation cost more than the solve itself, eight rounds in a row.
The dominant term was
small_triangulation_angle_mask, called once per image inside theper-image loop, each call materializing all 380k track ids into a Python list and then a
set. That work is loop-invariant, and the underlying filter is per-track independent, so
one pass over the problem answers every per-image query.
Two smaller serial costs sat alongside it: per-observation pybind lookups for point
coordinates and track lengths (~1.25M calls per round), and a full rebuild of every
pycolmap point after each positioning pass, which only moves coordinates.
Changes
Fix the native extension build against the installed COLMAP. Rebuilding
vidmap_nativecurrently yields a module that cannot be imported: COLMAP's staticlibraries reference Abseil's logging internals without listing Abseil in their exported
configuration, leaving undefined
absl::log_internalsymbols, and a shared Ceres outsidethe loader's search path is not found at import time. Links Abseil when available and
keeps link-time library directories in the installed RPATH.
Extract point colors before writing a reconstruction. Nothing on the mapping path
sampled color, so every
points3D.bincarried RGB(0, 0, 0)and COLMAP rendered thecloud black — only the HTML visualization called
extract_colors_for_all_images, whichis why the same run looked colored there and blank in COLMAP. Color is cosmetic, so a
missing image root degrades to a warning rather than discarding a finished
reconstruction.
Cut the single-threaded Python work between native solves. Hoists the small-angle id
computation out of the per-image loop; snapshots point coordinates and track lengths into
one
Point3DTableper solve so per-image lookups are a binary search. Rows stay inreconstruction order because that order reaches Ceres through
variable_point3D_ids.Positioning passes now publish values instead of rebuilding all points, and the replay
track snapshot is only materialized when replay is enabled.
Make the Ceres linear solver and thread counts configurable. The global solves
hard-coded a direct sparse Schur factorization, whose factorization step is
single-threaded; rotation averaging was pinned to one thread with no way to change it.
Adds
linear_solver,preconditioneranduse_cudafor bundle adjustment and globalpositioning, plus
num_threadsfor rotation averaging.Every default reproduces current behavior: the direct sparse solve with CUDA off, and one
thread for rotation averaging, which keeps that stage byte-identical as its pinned random
seed intends. Requesting CUDA is rejected with a clear message when Ceres was built
without it. Also drops global positioning's
CLUSTER_TRIDIAGONALpreconditioner, whichSPARSE_SCHURignores — timing it explicitly confirmed no difference.Verification
Multi-threaded Ceres is not bit-reproducible here, so results are compared against the
pipeline's own run-to-run spread after removing the similarity gauge:
unmodified code; before-versus-after differs by 0.05%. Reprojection error 0.94155
vs 0.94173, same 89 registered images.
unchanged.
Notes for reviewers
iterative_schuranduse_cudaare wired but off by default and not yet recommended.On the 89-image scene
iterative_schurwas slower (3m43 vs 1m30) and converged to amaterially different solution (scale off by 2x), so it needs tuning before it is
useful.
EigenSparse only over the SuiteSparse build COLMAP was compiled against. That choice
is worth 2x on global positioning (212s @ 3.8 cores vs 105s @ 7.7 cores), so watch
the
-- Found Ceres version:configure line and passCeres_DIRif it picks the wrongone.
Expose full BA diagnostics and tidy descriptor loadingcommit.🤖 Generated with Claude Code