fix(agglomeration): tolerate float rounding in stale-edge rescore#5
Merged
Conversation
MeanAffinity edge scores are recomputed from running/area-weighted means in float32, so a stale edge's rescore can dip ~1 ULP (5.96e-08) below its queued score and abort on `assert(newScore >= score)`. This surfaces under heavy over-fragmentation (watershed size_threshold=0): high-affinity (mean~0.99), tiny-contact edges where the running-mean rounding is most jittery right where 1-mean is tiny. Observed 23 such 1-ULP violations out of ~48M edges -- one is enough to abort the process. Tolerate sub-epsilon rounding noise; a real non-monotonicity exceeds 1e-5 by orders of magnitude (and the discretize_queue bucket width ~1/256). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Agglomeration aborts with:
It happens with the default
OneMinus<MeanAffinity>scoring, and is data/parameter dependent ("fails a lot on 16nm seg, some succeed").Root cause — 1-ULP floating-point rounding
When a stale edge is popped, its score is recomputed (
scoreEdge) from the running/area-weighted mean-affinity formulas, which are float32 and order-dependent. The recomputed score can land one ULP below the score the edge was queued at, and the strictassert(newScore >= score)aborts the whole process.Instrumenting a live reproduction (replacing the assert with a logger) showed the truth: every violation has
gap = 5.96046e-08exactly (= 2^-24, one float32 ULP), on high-affinity (mean ≈ 0.99), tiny-contact (count = 3-6) edges:Only 23 such edges out of ~48M — but one
abort()kills the run. It is not an algorithmic non-monotonicity;MeanAffinityProviderand this assert are unchanged since the original upstream. Heavy over-fragmentation (watershedsize_threshold=0→ millions of tiny, high-affinity supervoxels) is what makes the running-mean rounding land on the wrong side of the last bit, right where1 - meanis tiny.Fix
Tolerate sub-epsilon rounding noise:
1e-5is far above the observed 6e-8 wiggle yet far below thediscretize_queuebucket width (~1/256 ≈ 3.9e-3), so any real non-monotonicity still trips it. In release (NDEBUG) builds the assert is compiled out anyway; the edge is simply re-queued at its recomputed score.Verification
A deterministic reproduction (cutout1 affs,
size_threshold=0watershed → 11.7M SVs, agglomerate @ 0.35) goes fromabort()→ clean completion, 0 violations with this change.🤖 Generated with Claude Code