Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

SfmLevenbergMarquardtOptimizer

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.

StageRuns on
Camera and point upload (once)Host → device
Projection residuals and JacobiansGPU
Reduced camera (Schur) system assemblyGPU
Damped linear solveGPU
Point back-substitutionGPU
Retraction and trial-error evaluationGPU
Lambda policy and convergence checksCPU (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.

SparseLevenbergMarquardtOptimizerSfmLevenbergMarquardtOptimizer (this page)CUDA SFM + GNC
Supported graphsAny graph of finite, unconstrained JacobianFactorsGeneralSFMFactor<PinholeCamera<Cal3Bundler>, Point3> onlySame as this page, plus outliers
LinearizationCPUGPUGPU
Retraction / error evaluationCPUGPUGPU
Per-iteration host↔device trafficJacobian numerics every iterationNoneNone
Structure exploitedGeneric sparsityCamera/point Schur complementCamera/point Schur complement
Linear solversCudss, PcgDenseCholesky, Cudss, PcgSame as this page
Inherits NonlinearOptimizerYesNoVia GncOptimizer
CPU fallbackYes (fallbackOnUnsupported)No — unsupported graphs throwNo

Practical guidance:

  • Bundle adjustment on Cal3Bundler cameras → 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 NonlinearFactorGraph and 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. SfmLevenbergMarquardtParams is 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 optionEffect
GTSAM_ENABLE_CUDA=ONCompiles 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=ONAdditionally 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=ON

Supported 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 ordinary NonlinearFactorGraph.

  • optimizeSfm(data, params) — a free function taking SfmData directly, skipping graph conversion entirely. optimizeSfmWithoutValueDownload is 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:

LinearSolverTypeBehaviorRequires
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
CudssAssembles the reduced camera system as upper-triangular CSR and solves it with cuDSS. Symbolic analysis is computed once and reused.GTSAM_ENABLE_CUDSS
PcgApplies 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:

ParameterTypeDefaultDescription
linear.backendLinearSolverTypeDenseCholeskyReduced-system solver backend
pcg.maxIterationsint250PCG iteration cap
pcg.relativeTolerancedouble1e-6PCG relative residual tolerance
pcg.warmStartbooltrueReuse the previous camera solution as the initial guess
pcg.convergenceCheckIntervalint10Iterations between host-side convergence checks
enableDetailedProfilingboolfalseCollect 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;

  • appliedScalarPermutation when an ordering was supplied;

  • iterationProfiles (with nested attemptProfiles) when enableDetailedProfiling = 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 no iterate().

  • Not a NonlinearOptimizer subclass, 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.txt

Use --profile for the detailed per-iteration breakdown, --cuda-linear-solver cudss|pcg to switch backends, and --output-format csv|json for machine-readable output.

Files

Open In Colab