C++ Quick Start#

This page is the quick start for the cuNLS C++ API. For the Python version (pycunls), see Quick Start.

Important

Capacity vs. active count. Factor and state batches are constructed with their capacity (how many factors / states their buffers hold) and start with zero active entries: call SetNumActiveFactors(n) / SetNumActiveStates(n) before solving, and again whenever the problem size changes. See Capacity and active count.

This section shows a minimal end-to-end C++ setup:

  1. Install the cuNLS C++ library

  2. Write a tiny app

  3. Compile and run it against the installed library

Step 1: Install cuNLS#

Use C++ Installation (C++ Installation) and make sure you have an install prefix (example: /tmp/cunls_install).

Step 2: Create a minimal source file#

Create main.cpp. The program solves the simplest possible nonlinear least-squares problem: a single scalar variable \(x\) pulled toward a target value \(o = 2\) by a prior factor with residual \(r = x - o\). The cost is \(\tfrac{1}{2}\|x - o\|^2\), so the optimal solution is \(x^* = 2\).

Includes and CUDA stream. All cuNLS public headers are available through the umbrella header cunls/cunls.h. A CUDA stream is required by every cuNLS API call — it controls asynchronous GPU execution.

#include <cuda_runtime.h>
#include <iostream>
#include <vector>
#include "cunls/cunls.h"

int main() {
  // cuNLS operations are asynchronous; a CUDA stream serializes GPU work.
  cudaStream_t stream = nullptr;
  cudaStreamCreate(&stream);

Prepare host data and upload to the GPU. We define one scalar state \(x = 0\) (the initial guess) and one observation \(o = 2\) (the target). dvector (see Common API) is a thin RAII wrapper around cudaMalloc / cudaMemcpy that uploads host vectors to device memory on construction.

// Initial guess: x = 0.  Target observation: o = 2.
std::vector<float> h_state = {0.0f};
std::vector<float> h_obs   = {2.0f};

// Upload both vectors to GPU memory.
cunls::dvector<float> d_state(h_state);
cunls::dvector<float> d_obs(h_obs);

Create the state batch. A VectorStateBatch<1> (see State API) wraps the device memory as a batch of 1-dimensional Euclidean states. The template argument 1 means each state has one float. The second constructor argument is the batch’s capacity: the number of states the buffer holds (here just one). A batch starts with 0 active states; SetNumActiveStates sets how many of them the solver uses, and must be called before the first solve. The capacity is fixed, so the same batch can be reused for problems of any size up to it.

// Wrap the device state memory in a VectorStateBatch with one state of
// dimension 1.  The solver will update this memory in-place.
cunls::VectorStateBatch<1> state_batch(d_state.data(), /*capacity=*/1);
state_batch.SetNumActiveStates(1);  // active states: zero until set

Create the factor batch. A PriorFactorBatch<manifold::Vector<1>> (see Factor API) computes the residual \(r = x - o\) and Jacobian \(J = I\) for each factor. The constructor takes a device pointer to the observation vectors and the capacity (the number of observations the buffer holds); like the state batch, it starts with 0 active factors until SetNumActiveFactors is called. PriorFactorBatch<Manifold> is the manifold-generic facade — prefer it over the per-manifold class it wraps (PriorVectorFactorBatch<Dim> here) so the same factor name works regardless of which manifold the state lives on.

// Build a prior factor that penalizes deviation from the observation.
// Residual: r = x - o,  Jacobian: J = I.
cunls::PriorFactorBatch<cunls::manifold::Vector<1>> prior(
    reinterpret_cast<const cunls::Vector<1>*>(d_obs.data()),
    /*capacity=*/1);
prior.SetNumActiveFactors(1);  // active factors: zero until set

Wire state pointers and assemble the problem. The state-pointer vector tells the solver which state each factor reads. For a prior factor with one state input, there is exactly one pointer per factor. Problem (see Minimizer API) collects all state and factor batches into a single factor graph.

// Each factor needs a list of device pointers to its input states.
// The prior factor reads one state, so we provide one pointer.
std::vector<float*> state_ptrs = {state_batch.StateDevicePtr(0)};

// Assemble the factor graph.
cunls::Problem problem;
problem.AddStateBatch(&state_batch);
problem.AddFactorBatch(&prior, state_ptrs);

Run the solver. LevenbergMarquardtMinimizer (see Minimizer API) solves the damped normal equations \((J^T J + \lambda D)\,\Delta x = -J^T r\) at each iteration, adapting \(\lambda\) based on step quality. Minimize updates the state memory in-place and returns a MinimizerSummary with solve statistics.

// Solve with default LM settings.  The state (d_state) is updated
// in-place on the GPU.
cunls::LevenbergMarquardtMinimizer minimizer;
auto summary = minimizer.Minimize(stream, problem);

Inspect results.

  std::cout << "Iterations: "   << summary.num_iterations << "\n";
  std::cout << "Initial cost: " << summary.initial_cost   << "\n";
  std::cout << "Final cost: "   << summary.final_cost     << "\n";

  cudaStreamDestroy(stream);
  return 0;
}

Step 3: Create CMakeLists.txt#

cmake_minimum_required(VERSION 3.22)
project(cunls_quick_start LANGUAGES CXX CUDA)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

if(NOT DEFINED CUNLS_INSTALL_DIR)
  message(FATAL_ERROR "Set CUNLS_INSTALL_DIR to cuNLS install prefix.")
endif()

find_package(CUDAToolkit REQUIRED)
find_library(CUNLS_LIBRARY cunls PATHS "${CUNLS_INSTALL_DIR}/lib" REQUIRED NO_DEFAULT_PATH)

add_executable(minimal main.cpp)
target_include_directories(minimal PRIVATE "${CUNLS_INSTALL_DIR}/include")
target_link_libraries(minimal PRIVATE "${CUNLS_LIBRARY}" CUDA::cudart)
set_target_properties(minimal PROPERTIES
  BUILD_RPATH "${CUNLS_INSTALL_DIR}/lib"
  INSTALL_RPATH "${CUNLS_INSTALL_DIR}/lib"
)

Step 4: Build and run#

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCUNLS_INSTALL_DIR=/tmp/cunls_install
cmake --build build -j
./build/minimal

You should see the final cost decrease toward zero.