Overview¶
gtsam::cuda::SfmLevenbergMarquardtOptimizer is a batch-only, fully
GPU-resident Levenberg-Marquardt optimizer specialized for BAL-style bundle
adjustment. Projection-factor linearization, Schur-complement assembly, the
damped linear solve, retraction, and nonlinear error evaluation all run on the
device; the host only drives the lambda search and reads back scalar errors.
| Stage | Runs on |
|---|---|
| Camera and point upload (once) | Host → device |
| Projection residuals and Jacobians | GPU |
| Reduced camera (Schur) system assembly | GPU |
| Damped linear solve | GPU |
| Point back-substitution | GPU |
| Retraction and trial-error evaluation | GPU |
| Lambda policy and convergence checks | CPU (scalars only) |
| Optimized values download (once) | Device → host |
Because nothing crosses the PCI bus between iterations, this is the fastest of the CUDA optimizers on the problems it supports — at the cost of supporting only those problems.
Relationship to the other CUDA optimizers¶
GTSAM’s CUDA layer contains three related entry points. They share the same
device linear-solver session (LinearSolverSession, LinearSolverType) and the
same PCG implementation, and differ in how much of the LM loop lives on the
device.
| SparseLevenbergMarquardtOptimizer | SfmLevenbergMarquardtOptimizer (this page) | CUDA SFM + GNC | |
|---|---|---|---|
| Supported graphs | Any graph of finite, unconstrained JacobianFactors | GeneralSFMFactor<PinholeCamera<Cal3Bundler>, Point3> only | Same as this page, plus outliers |
| Linearization | CPU | GPU | GPU |
| Retraction / error evaluation | CPU | GPU | GPU |
| Per-iteration host↔device traffic | Jacobian numerics every iteration | None | None |
| Structure exploited | Generic sparsity | Camera/point Schur complement | Camera/point Schur complement |
| Linear solvers | Cudss, Pcg | DenseCholesky, Cudss, Pcg | Same as this page |
Inherits NonlinearOptimizer | Yes | No | Via GncOptimizer |
| CPU fallback | Yes (fallbackOnUnsupported) | No — unsupported graphs throw | No |
Practical guidance:
Bundle adjustment on
Cal3Bundlercameras → use this optimizer. It is the specialized path and keeps the entire LM loop on the device.Anything else (pose graphs, stereo, IMU, mixed factor types) → use SparseLevenbergMarquardtOptimizer, which accepts a general
NonlinearFactorGraphand falls back to CPU LM when a graph is unsupported.Bundle adjustment with outlier measurements → wrap this optimizer in
GncOptimizer; see GNC with the CUDA SFM optimizer.SfmLevenbergMarquardtParamsis the inner-solver parameter type, so GNC’s re-weighted solves each run fully on the device.
The timeCudaSFMBAL benchmark exposes all three on the same BAL dataset:
--cuda-lm runs this optimizer, --cuda-sparse-lm runs the general sparse
optimizer, and --gnc cuda runs the GNC wrapper.
Build requirements¶
Both CMake options are OFF by default, so a standard GTSAM build does not
compile this class at all.
| CMake option | Effect |
|---|---|
GTSAM_ENABLE_CUDA=ON | Compiles the CUDA layer and makes this optimizer available. Requires the CUDA Toolkit and CMake ≥ 3.17. Sufficient for the DenseCholesky and Pcg backends. |
GTSAM_ENABLE_CUDSS=ON | Additionally enables the cuDSS sparse direct backend. Requires GTSAM_ENABLE_CUDA=ON (configuration fails otherwise) and a cuDSS installation. |
# dense Schur and PCG — CUDA Toolkit is enough
cmake -S . -B build-cuda -DGTSAM_ENABLE_CUDA=ON
# also enable the cuDSS sparse Schur solver
cmake -S . -B build-cuda -DGTSAM_ENABLE_CUDA=ON -DGTSAM_ENABLE_CUDSS=ONSupported graphs¶
Supported graphs contain GeneralSFMFactor<PinholeCamera<Cal3Bundler>, Point3>
factors. Unit, diagonal, isotropic, and supported robust noise models are
converted by convertGeneralSfmGraph. Unsupported graphs throw — there is no
CPU fallback, unlike the general sparse optimizer.
Two front doors are available:
SfmLevenbergMarquardtOptimizer(graph, initialValues, params)— the optimizer-style adapter, taking an ordinaryNonlinearFactorGraph.optimizeSfm(data, params)— a free function takingSfmDatadirectly, skipping graph conversion entirely.optimizeSfmWithoutValueDownloadis the same, minus the final device→host transfer, and is what benchmarks use to isolate solve time.
The class intentionally does not inherit NonlinearOptimizer: the resident
pipeline provides a complete batch solve, not an independently usable
single-iteration iterate() operation. Consequently SfmLevenbergMarquardtParams
still declares using OptimizerType = SfmLevenbergMarquardtOptimizer;, which is
what lets it be selected by parameter type (including as a GncParams inner
solver), but the optimizer is not interchangeable with NonlinearOptimizer
subclasses at runtime.
Solver selection¶
params.linear.backend selects the solver for the camera-only Schur complement:
LinearSolverType | Behavior | Requires |
|---|---|---|
DenseCholesky (default here) | Forms the dense reduced camera matrix and factors it with cuSOLVER. Fastest for the camera counts typical of BAL datasets. | GTSAM_ENABLE_CUDA |
Cudss | Assembles the reduced camera system as upper-triangular CSR and solves it with cuDSS. Symbolic analysis is computed once and reused. | GTSAM_ENABLE_CUDSS |
Pcg | Applies the damped reduced operator matrix-free, with a per-camera block-Jacobi preconditioner; the reduced matrix is never formed. | GTSAM_ENABLE_CUDA |
A supplied Ordering contains camera keys only and is expanded to scalar
column indices. It is meaningful only for Cudss, which is the only backend that
builds the reduced CSR plan; supplying one with any other backend throws
std::invalid_argument.
Usage (C++)¶
#include <gtsam/slam/cuda/SfmLevenbergMarquardt.h>
using namespace gtsam;
using namespace gtsam::cuda;
NonlinearFactorGraph graph = /* BAL-style graph */;
Values initial = /* cameras and points */;
SfmLevenbergMarquardtParams params =
SfmLevenbergMarquardtParams::ceresDefaults();
params.setLinearSolver(LinearSolverType::DenseCholesky);
SfmLevenbergMarquardtOptimizer optimizer(graph, initial, params);
const Values& result = optimizer.optimize();
const SfmLevenbergMarquardtResult& diagnostics = optimizer.result();Working directly from SfmData, e.g. a BAL file:
SfmData data = SfmData::FromBalFile(filename);
SfmLevenbergMarquardtResult result = optimizeSfm(data, params);
const Values& values = result.optimizedValues;Parameters¶
SfmLevenbergMarquardtParams derives from LevenbergMarquardtParams, so all the
usual convergence tolerances, lambda policy, and maximum iteration count apply.
The CUDA-specific additions are:
| Parameter | Type | Default | Description |
|---|---|---|---|
linear.backend | LinearSolverType | DenseCholesky | Reduced-system solver backend |
pcg.maxIterations | int | 250 | PCG iteration cap |
pcg.relativeTolerance | double | 1e-6 | PCG relative residual tolerance |
pcg.warmStart | bool | true | Reuse the previous camera solution as the initial guess |
pcg.convergenceCheckInterval | int | 10 | Iterations between host-side convergence checks |
enableDetailedProfiling | bool | false | Collect per-iteration and per-attempt profiles |
Two named constructors set the LM policy:
SfmLevenbergMarquardtParams::legacyDefaults() (GTSAM’s historical defaults,
also what the default constructor uses) and
SfmLevenbergMarquardtParams::ceresDefaults() (Ceres-style defaults, generally
preferable for bundle adjustment).
Diagnostics¶
optimizer.result() — or the value returned by optimizeSfm — is a
SfmLevenbergMarquardtResult reporting:
the selected backend, linear-system kind, dimension, and nonzero count;
initial and final objective, accepted steps, iterations, inner iterations, and final lambda;
linearSolveStats: analysis / factorization / solve counts, PCG iteration totals, iteration-cap hits, breakdowns, and per-phase seconds;host→device and device→host transfer counts and byte totals;
appliedScalarPermutationwhen an ordering was supplied;iterationProfiles(with nestedattemptProfiles) whenenableDetailedProfiling = true;optimizedValues, the final values.
The timing fields overlap by design — they are not an exclusive partition of wall time and must not be summed as one.
Limitations¶
CUDA support is experimental and opt-in.
Only
GeneralSFMFactor<PinholeCamera<Cal3Bundler>, Point3>graphs are supported; anything else throws, with no CPU fallback.Batch optimizer with fixed structure;
optimize()is called once, and there is noiterate().Not a
NonlinearOptimizersubclass, so it cannot be used polymorphically through that base class.User-supplied orderings apply only to the cuDSS backend.
Small problems are often faster on the CPU, which does not pay CUDA setup cost.
Benchmarking¶
timeCudaSFMBAL (timing/sfm_ba/) loads one or more BAL datasets and compares
CPU LM against the CUDA backends, checking final-objective agreement:
./timing/sfm_ba/timeCudaSFMBAL --cuda-lm \
--cuda-linear-solver dense-cholesky problem.txtUse --profile for the detailed per-iteration breakdown,
--cuda-linear-solver cudss|pcg to switch backends, and
--output-format csv|json for machine-readable output.