Repository navigation
PERF: Build N4 B-spline fit points once instead of in each iteration - #7075
hjmjohnson merged 1 commit into
Conversation
|
@ntustison could you review this when you have a chance? It is one of the upstream items from ANTsX/ANTs#2037. |
hjmjohnson
left a comment
There was a problem hiding this comment.
Thanks @gdevenyi. Hoisting the point construction out of the iteration loop is worth doing, but the cached PointSet does not survive the release-data path, and the change needs an in-tree test before merge.
- Please add a GoogleTest that runs N4 with
UseMaskLabelon and a mask label set, a confidence image, and a second run withDataObject::GlobalReleaseDataFlagon, and compares the result against a fixed baseline. No current N4 test sets a mask label or a confidence image, and the bit-identity check is out of tree, so the equivalence should be pinned by an in-tree test.
Other details are inline.
7529e85 to
76f7e68
Compare
|
Thanks @hjmjohnson. I force-pushed 76f7e68 with the inline changes. Three new GoogleTests are in
All three pass with I couldn't add the |
|
Follow-up to the |
Round-1 requested changes are addressed; dismissing my change request.
[Medium impact] The change affects runtime and memory use within N4 bias correction. The remaining raw-buffer copy in the test must meet the repository’s image-access requirement before merging. Findings
|
|
|
||
| auto & values = fitPoints.values->CastToSTLContainer(); | ||
| const auto & bufferIndices = fitPoints.bufferIndices; | ||
| const RealType * const fieldBuffer = fieldEstimate->GetBufferPointer(); |
There was a problem hiding this comment.
Raw reads bypass image conventions
The new gather reads pixels through GetBufferPointer(), which violates the project directive “Use iterators, not raw buffer access.” Use MakeImageBufferRange(fieldEstimate) for the indexed reads instead. The new output copy in itkN4BiasFieldCorrectionImageFilterGTest.cxx also needs range-based access. This repository requirement must be satisfied before merging.
Context Used: AGENTS.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Fixed in b8322a4: the gather now reads through MakeImageBufferRange(fieldEstimate), the same way GenerateData reads the mask and confidence images.
76f7e68 to
4c2f1b4
Compare
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues are recorded for the N4 bias-correction optimization.
No open issues 🎉
What changed in this PR
This PR reduces repeated work in N4 bias correction by building B-spline fit points and weights once per filter execution, then gathering only residual values in parallel during each iteration. It complements the B-spline fitting optimization in #7072.
Changes:
- Reuse fit coordinates, weights, and voxel buffer indices throughout
GenerateData. - Parallelize residual-value gathering while creating a fresh point set for each fit.
- Add regression tests for mask labels, confidence images, and baseline output values.
| File | Description |
|---|---|
| Modules/Filtering/BiasCorrection/test/itkN4BiasFieldCorrectionImageFilterGTest.cxx | Adds mask/confidence equivalence and baseline-output tests. |
| Modules/Filtering/BiasCorrection/include/itkN4BiasFieldCorrectionImageFilter.hxx | Builds reusable fit data once and gathers residual values in parallel. |
| Modules/Filtering/BiasCorrection/include/itkN4BiasFieldCorrectionImageFilter.h | Defines FitPoints and updates the bias-estimation method signature. |
Review effort: Balanced
hjmjohnson
left a comment
There was a problem hiding this comment.
The new head is a rebase of the round-2 code (git range-diff shows the patch unchanged), so the spec side is still clean: bit-identity holds by reading, and every round-1 thread checks out. Three should-fix items remain on the standards side, inline. None is blocking.
Also, Greptile's open P2 on the raw GetBufferPointer() gather (hxx:517) holds: Documentation/AI/conventions.md says "Use iterators, not raw buffer access", and GenerateData already uses MakeImageBufferRange for the mask and confidence images. const auto fieldRange = MakeImageBufferRange(fieldEstimate); with fieldRange[bufferIndices[k]] is a drop-in. Please fix it or answer that thread.
Non-blocking:
- "with #7072" in the body is out of date now that #7072 has merged.
- The PR body still ends with the "Generated with Claude Code" footer; the AI disclosure in
<details>is enough (non-blocking).
This review ran as /code-review max (Standards and Spec axes) on Opus 5.5. Once these are addressed I will re-check, then request a Greptile review and mark the PR ready.
| * the estimate and adds the resulting control point values to the total | ||
| * bias field estimate. | ||
| */ | ||
| /** The fit points of the included voxels, which depend only on the mask, the confidence image and the geometry. */ |
There was a problem hiding this comment.
Should-fix: FitPoints sits between UpdateBiasFieldEstimate's doc block (lines 409-413) and the method, so Doxygen attaches that block to the struct and the method is left undocumented. The struct comment is also the commit-body sentence round 1 asked to keep out of the source. Please move the struct above the doc block at line 409, so the doc block sits directly on the method, with a short comment:
| /** The fit points of the included voxels, which depend only on the mask, the confidence image and the geometry. */ | |
| /** Fit points of the included voxels. */ |
There was a problem hiding this comment.
Done in b8322a4: FitPoints now sits above the doc block, with your one-line comment, so the doc block attaches to UpdateBiasFieldEstimate.
| const auto confidence = MakeConfidence(); | ||
| const auto output = RunN4({ labelMask, true, 1, confidence }); | ||
|
|
||
| // Values from ITK main before the fit points were built once per filter update. |
There was a problem hiding this comment.
Should-fix: comments describe what the code is, not what it replaced (prose-budget.md, references to prior behavior):
| // Values from ITK main before the fit points were built once per filter update. | |
| // Regression baseline. |
There was a problem hiding this comment.
Done, using your suggestion.
|
|
||
| const typename ImporterType::OutputImageType * parametricFieldEstimate = importer->GetOutput(); | ||
|
|
||
| auto & values = fitPoints.values->CastToSTLContainer(); |
There was a problem hiding this comment.
Nit: this writes through const FitPoints &; it compiles only because the SmartPointer is const and its target is not. Taking FitPoints & would make the signature say the values are filled here.
There was a problem hiding this comment.
Done: UpdateBiasFieldEstimate now takes FitPoints &.
4c2f1b4 to
b8322a4
Compare
|
Thanks @hjmjohnson. Force-pushed b8322a4 with the three items and greptile's buffer-range point (all inline). The PR body no longer refers to #7072 as pending. |
b8322a4 to
683fed9
Compare
The fit points and weights depend only on the mask, the confidence image and the input geometry. Build their containers once in GenerateData, and in each iteration gather only the residual field values, in parallel, into a fresh point set that shares the containers, so releasing the fit's input cannot empty them. The points, weights and values are unchanged, so the output is bit-identical. GoogleTests compare a mask label with a binary mask, a binary confidence image with a mask, and a mask label with a confidence image against values from main.
683fed9 to
3feef1f
Compare
Build the N4 B-spline fit points and weights once per
GenerateData, and in each iteration only gather the residual field values into the point data, in parallel. The output is bit-identical; onmain(which now includes #7072), full-resolution N4 is 20 % faster.Details
The points and weights depend only on the mask, the confidence image and the input geometry. Each iteration rebuilt them in a serial loop with a mask test and
TransformIndexToPhysicalPointper voxel (ANTsX/ANTs#2037, item N4-3). The points are built from an image with the input origin, spacing and buffered region and identity direction, as the importer did, so their coordinates are unchanged, also on oblique images. The members are released at the end ofGenerateData.Cost: the points, the weights and a
SizeValueTypebuffer index per included voxel now stay allocated for the whole run, not for one iteration at a time.Measurements
AMD Ryzen 9 9950X, GCC 13.3, Release, 16 threads. N4 on MNI152 2009c T1 (1 mm, no shrink, 193 x 229 x 193), 3 levels x 15 iterations, four runs (no mask; mask; mask and confidence image; oblique direction), two repeats:
mainwith #7072mainwith #7072 + this PRAt shrink 2 on
mainbefore #7072 merged, the gain was within noise (43.8 s → 43.3 s), because the fit dominated there.Test results
ctest -R 'N4|BiasCorrection': 9/9 pass. The build adds no warnings.Update()after removing the mask, withcmpagainstmain: identical, for this PR alone and stacked on PERF: Compute B-spline fit kernel values once per axis per point #7072.AI assistance