Skip to content

perf(linear): compute JacobianFactor::gradient() directly, skip HessianFactor conversion#2510

Merged
dellaert merged 4 commits into
borglab:developfrom
jashshah999:perf/jacobian-gradient-direct
May 8, 2026
Merged

perf(linear): compute JacobianFactor::gradient() directly, skip HessianFactor conversion#2510
dellaert merged 4 commits into
borglab:developfrom
jashshah999:perf/jacobian-gradient-direct

Conversation

@jashshah999
Copy link
Copy Markdown
Contributor

Problem

JacobianFactor::gradient(Key, VectorValues) converts the entire factor to a HessianFactor to compute a single gradient entry (flagged by an existing TODO at line 881):

Vector JacobianFactor::gradient(Key key, const VectorValues& x) const {
  // TODO: optimize it for JacobianFactor without converting to a HessianFactor
  HessianFactor hessian(*this);
  return hessian.gradient(key, x);
}

The HessianFactor constructor computes A'A which is O(n*m^2) for an n-row, m-column Jacobian. This is unnecessary when only one key's gradient is needed.

Fix

Compute the gradient directly:

  1. Evaluate unwhitened residual e = A*x - b by iterating over Jacobian blocks — O(n*m)
  2. Apply noise model: e = Sigma^{-1} * e (double whiten)
  3. Return A_k' * e for the requested key — O(n*d_k)

Total cost: O(nm) vs O(nm^2) for the old path. No heap allocation for the HessianFactor.

Tests

Two new tests in testJacobianFactor.cpp:

  • gradient: isotropic noise model, verifies numerical equivalence with the HessianFactor path
  • gradient_no_noise: no noise model (unit covariance), same verification

…sianFactor

JacobianFactor::gradient(Key, VectorValues) was converting the entire
factor to a HessianFactor (O(n*m^2) for A'A computation) just to read
one gradient entry. This was flagged by a TODO in the code.

The gradient for key k is A_k' * Sigma^{-1} * (A*x - b), which can be
computed in O(n*m) by evaluating the residual directly and multiplying
by the transpose of the requested block. This avoids the HessianFactor
allocation and the full Hessian matrix product.

Adds two tests verifying numerical equivalence with the HessianFactor
path, with and without a noise model.
@dellaert dellaert requested a review from Copilot April 27, 2026 20:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR optimizes JacobianFactor::gradient(Key, VectorValues) by computing a single-key gradient directly from Jacobian blocks, avoiding conversion to HessianFactor and its AᵀA construction cost.

Changes:

  • Reimplemented JacobianFactor::gradient() to compute the residual and project onto a requested key’s Jacobian block.
  • Added unit tests comparing the new path to the existing HessianFactor-based gradient for isotropic and no-noise cases.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
gtsam/linear/JacobianFactor.cpp Replaces HessianFactor conversion with direct residual/gradient computation.
gtsam/linear/tests/testJacobianFactor.cpp Adds tests validating gradient equivalence against HessianFactor.

Comment thread gtsam/linear/JacobianFactor.cpp Outdated
Comment thread gtsam/linear/JacobianFactor.cpp Outdated
Comment thread gtsam/linear/tests/testJacobianFactor.cpp
Comment thread gtsam/linear/tests/testJacobianFactor.cpp
The previous implementation applied whitenInPlace twice, computing
R*R*e instead of R^T*R*e. These differ for non-symmetric R (any
non-diagonal covariance). Fix: whiten both A_k and e once, then
compute A_k_w^T * e_w = A_k^T * R^T * R * e.

Also added bounds check for missing key and a test with non-diagonal
covariance.
@jashshah999
Copy link
Copy Markdown
Contributor Author

Pushed a fix for the noise model handling. The previous version applied whitenInPlace twice, computing R*R*e instead of R^T*R*e. These differ when R is non-symmetric (any non-diagonal covariance). Now whitens both A_k and e once: (R*A_k)^T * (R*e) = A_k^T * R^T*R * e.

Also added a bounds check for missing keys and a test with non-diagonal (Gaussian) covariance that would have caught this.

@dellaert
Copy link
Copy Markdown
Member

dellaert commented May 8, 2026

@jashshah999 please "resolve" copilot comments you addressed and/or are irrelevant.
The CI failed on some platforms. Please check. If these are CI hiccups, please merge in develop to re-trigger CI.

@jashshah999
Copy link
Copy Markdown
Contributor Author

Resolved all Copilot threads (replied to each). Also merged develop to re-trigger CI.

Copy link
Copy Markdown
Member

@dellaert dellaert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice. We'll merge when CI passes.

@dellaert dellaert merged commit 1a9792a into borglab:develop May 8, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants