feat(isam2): adaptive variable reordering triggered by fill-in growth#2509
feat(isam2): adaptive variable reordering triggered by fill-in growth#2509jashshah999 wants to merge 2 commits intoborglab:developfrom
Conversation
In long-running iSAM2 sessions the Bayes tree accumulates fill-in because the variable ordering is only refreshed for the affected subtree on each incremental update. Over thousands of steps the total number of nonzero entries (nnz) in the tree can grow well beyond what a fresh COLAMD ordering would produce, degrading both memory and solve time. This commit adds: - ISAM2::treeNnz() — compute total nnz across all cliques - ISAM2Params::enableAdaptiveReorder (default: false) — opt-in flag - ISAM2Params::adaptiveReorderThreshold (default: 2.0) — ratio of current nnz to baseline nnz that triggers a full batch re-elimination with fresh COLAMD ordering (reuses the existing recalculateBatch path) - ISAM2Result::treeNnz — reported every update for monitoring - ISAM2Result::batchReorderTriggered — flag indicating a reorder happened The baseline nnz is recorded after each batch reorder (including the first update and whenever the existing 65% batch threshold fires). When the current nnz exceeds threshold * baseline, all keys are marked so that recalculate() takes the batch path on the current step. Disabled by default so there is no behavior change for existing users. Three new tests in testGaussianISAM2.cpp: nnz tracking, triggered reorder with a low threshold, and verification that the feature is off by default.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds an opt-in “adaptive reorder” heuristic for iSAM2 that monitors Bayes tree fill-in (nnz) over time and triggers a full batch reorder when nnz grows beyond a configurable threshold.
Changes:
- Add
treeNnzreporting andbatchReorderTriggeredsignaling inISAM2Result. - Add adaptive reorder knobs to
ISAM2Paramsand implementISAM2::treeNnz()plus reorder triggering logic inISAM2::update(). - Add unit tests covering nnz tracking, reorder triggering behavior, and default-disabled behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/testGaussianISAM2.cpp | Adds tests for nnz tracking and adaptive reorder behavior. |
| gtsam/nonlinear/ISAM2Result.h | Extends the update result with nnz telemetry + reorder flag and prints/getters. |
| gtsam/nonlinear/ISAM2Params.h | Introduces new adaptive reorder parameters, defaults, and print output. |
| gtsam/nonlinear/ISAM2.h | Adds stored baseline nnz state and exposes treeNnz() API. |
| gtsam/nonlinear/ISAM2.cpp | Implements treeNnz() and adaptive reorder triggering + baseline refresh logic. |
Comments suppressed due to low confidence (1)
gtsam/nonlinear/ISAM2.cpp:1
ISAM2::equalsdoes not include the newly addednnzAfterLastReorder_. Since this value influences future adaptive reorder decisions, two instances can be ‘equal’ per this method while exhibiting different behavior on subsequentupdate()calls. Consider includingnnzAfterLastReorder_in the comparison (or document clearly why it is intentionally excluded and safe to ignore).
/* ----------------------------------------------------------------------------
| // With threshold 1.01, the loop closure fill-in should trigger a reorder | ||
| // on this or the next update | ||
| bool triggered = result.batchReorderTriggered; | ||
| if (!triggered) { | ||
| // Try one more update to see if fill-in was detected | ||
| graph = NonlinearFactorGraph(); | ||
| graph.emplace_shared<BetweenFactor<Pose2>>(5, 10, | ||
| Pose2(5.0, 0.0, 0.0), odoNoise); | ||
| result = isam.update(graph, init); | ||
| triggered = result.batchReorderTriggered; | ||
| } | ||
|
|
||
| // At some point the reorder should have triggered | ||
| // (If not, the nnz didn't grow — which is also fine, means no fill-in) | ||
| EXPECT(result.treeNnz > 0); |
There was a problem hiding this comment.
Fixed. Added EXPECT(triggered) assertion.
| // 8. Check whether adaptive reorder should force a batch recalculation. | ||
| // If fill-in has grown past the threshold, mark all keys so that | ||
| // recalculate() takes the batch path. | ||
| if (params_.enableAdaptiveReorder && nnzAfterLastReorder_ > 0 && | ||
| !roots_.empty()) { | ||
| const size_t currentNnz = treeNnz(); | ||
| if (currentNnz > | ||
| static_cast<size_t>(params_.adaptiveReorderThreshold * | ||
| static_cast<double>(nnzAfterLastReorder_))) { | ||
| for (const auto& [key, _] : variableIndex_) { | ||
| result.markedKeys.insert(key); | ||
| } | ||
| result.batchReorderTriggered = true; | ||
| } | ||
| } |
There was a problem hiding this comment.
Fixed. Flag is now set in both the adaptive and 65% threshold paths.
| // Update baseline when: first update, adaptive reorder fired, or the | ||
| // existing batch threshold was hit (variablesReeliminated == all vars). | ||
| if (nnzAfterLastReorder_ == 0 || result.batchReorderTriggered || | ||
| result.variablesReeliminated >= theta_.size()) { |
There was a problem hiding this comment.
Fixed. Using batchReorderTriggered flag directly.
| if (currentNnz > | ||
| static_cast<size_t>(params_.adaptiveReorderThreshold * | ||
| static_cast<double>(nnzAfterLastReorder_))) { |
There was a problem hiding this comment.
Fixed. Using floating-point ratio comparison.
|
Could you clarify how the threshold is set in the PR comments and ( if needed) in the comments in the code? The PR comment makes reference to a “baseline” NNZ but it’s unclear how that is obtained. |
|
The baseline nnz is recorded after each full batch re-elimination — the first Happy to add a clarifying comment in the code if you'd like. |
|
To clarify the threshold: the baseline NNZ is captured once during the first full batch elimination, then refreshed after each subsequent batch reorder. The |
|
Hi @jashshah999 . Yes, please make this clear in the code. Also, please evaluate the copilot comments above for accuracy, and “resolve” all of them by either addressing or deeming them irrelevant. They often point to real issues. |
- Set batchReorderTriggered in the existing 65% threshold path too, matching the ISAM2Result documentation - Use floating-point ratio comparison instead of size_t cast to avoid truncation for large baselines - Use batchReorderTriggered flag for baseline refresh instead of the brittle variablesReeliminated proxy - Add EXPECT(triggered) assertion in test
|
Addressed all four copilot comments:
Also added a comment clarifying the baseline NNZ mechanism. |
Problem
In long-running iSAM2 sessions the Bayes tree accumulates fill-in because the variable ordering is only refreshed for the affected subtree on each incremental update. Over thousands of steps the total number of nonzero entries (nnz) in the tree grows beyond what a fresh COLAMD ordering would produce, degrading both memory usage and solve time.
The existing batch fallback (triggered when >65% of variables are affected) only fires on large loop closures. Gradual fill-in accumulation from incremental updates goes undetected.
Solution
Track the Bayes tree nnz after each full reorder and automatically trigger a batch re-elimination with fresh COLAMD ordering when fill-in grows past a configurable threshold.
New parameters in
ISAM2Params:enableAdaptiveReorder(default:false) -- opt-in, no behavior change for existing usersadaptiveReorderThreshold(default:2.0) -- trigger batch reorder when current nnz exceeds this multiple of the baseline nnzNew fields in
ISAM2Result:treeNnz-- total Bayes tree nnz, reported every update regardless of whether adaptive reorder is enabled, useful for monitoring fill-inbatchReorderTriggered-- flag indicating a reorder happened on this stepNew method on
ISAM2:treeNnz()-- compute total nnz across all cliques (uses existingISAM2Clique::nnz_internal)How it works
update(), the current nnz is recorded as the baselinerecalculate(), ifenableAdaptiveReorderis true, the current nnz is compared toadaptiveReorderThreshold * baselinerecalculate()takes the existingrecalculateBatch()path (full COLAMD reorder + re-elimination)No new algorithms -- this reuses the existing
recalculateBatch()infrastructure. The only new computation istreeNnz(), which is O(number of cliques).Tests
Three new tests in
testGaussianISAM2.cpp:AdaptiveReorder_NnzTracking: verifies nnz is populated after first update and grows with added factorsAdaptiveReorder_Triggered: uses threshold=1.01 with a loop closure to exercise the reorder pathAdaptiveReorder_DisabledByDefault: verifies no reorder with default params