Repository navigation
fix: do not count nnU-Net's ignore label as background in the EM histogram - #141
Open
Hendrik-code wants to merge 1 commit into
Open
Hendrik-code wants to merge 1 commit into
Hendrik-code wants to merge 1 commit into
Conversation
…ogram `em_subdivide_labels` took each label's voxel count from a histogram of `label_map.clamp_min(0)` and then selected that label's voxels with `label_map == p`. A negative value is not a label -- nnU-Net writes -1 for voxels outside the annotated region -- so every -1 was counted as background while the selection excluded it. The count was therefore larger than the number of voxels gathered, and the `randint(0, count)` subsample that fits the background GMM could draw an index past the end. On CPU that raises `IndexError`. On CUDA it is an asynchronous `index out of bounds` device-side assert, reported at whichever kernel runs next, and it only fires when an oversized index is actually drawn: for a 128^3 patch with six ignored voxels and the shipped `max_fit_voxels: 100000` it is about two runs in five. It presented as an occasional crash pointing at innocent code. Reproduced on a preprocessed Dataset804 patch: the histogram reported 1219605 background voxels where `label == 0` selects 1219599, the difference being the six ignored voxels. Not reachable from `nnUNetTrainerDAExt`, which puts `RemoveLabelTansform(-1, 0)` in the dataloader ahead of the GPU augmentations, so no published training is affected. It is reachable from `RandomSynthSegGPU` and `em_subdivide_labels` directly, which take a label map and document no such precondition. The histogram now excludes negative values, and `em_subdivide_labels` returns one undivided region when no non-negative label is present at all -- previously unreachable, because the clamp mapped such a patch to background. Introduced in 20360d8 ("perf: remove the per-sample loops and host stalls from the augmentations"), which replaced `cnt = int(mask.sum())` with the batched histogram. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This branch has not been deployed
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.
The bug
em_subdivide_labels(SynthSeg EM label completion) takes each label's voxel count from a histogram oflabel_map.clamp_min(0), then selects that label's voxels withlabel_map == p.A negative value is not a label: nnU-Net writes
-1for voxels outside the annotated region. The clamp counted every-1as background; the selection that follows excludes them. So the count exceeded the number of voxels gathered, and therandint(0, count)subsample that fits the background GMM could draw an index past the end ofx:On CPU that raises
IndexError. On CUDA it is an asynchronousindex out of boundsdevice-side assert, reported at whichever kernel happens to run next — in my case inside_em_gmm_1d, nine frames from the actual fault. It only fires when an oversized index is actually drawn, so it is probabilistic: for a 128³ patch with six ignored voxels and the shippedem_max_fit_voxels: 100000, about two runs in five.Reproduced on a preprocessed Dataset804 patch: the histogram reports 1219605 background voxels where
label == 0selects 1219599, the difference being exactly the six ignored voxels.Scope
No published training is affected.
nnUNetTrainerDAExt.get_training_transformsputsRemoveLabelTansform(-1, 0)in the CPU dataloader pipeline, ahead of the GPU augmentations, so a GPU transform is never handed a negative label in training.It is reachable from
RandomSynthSegGPUand fromem_subdivide_labelsdirectly, both of which take a label map and document no such precondition. I hit it in an offline harness that reads the preprocessed.npyfiles straight off disk.Introduced in 20360d8 ("perf: remove the per-sample loops and host stalls from the augmentations"), which replaced the correct
cnt = int(mask.sum())with the batched histogram.The fix
_label_counts_per_sampleexcludes negative values from the histogram, keeping the single-bincountshape that commit was after. The clamp stays on the row offset only, so a negative label cannot land in another sample's bins.em_subdivide_labelsreturns one undivided region when no non-negative label is present at all. That case was previously unreachable — the clamp turned an all-ignore patch into all-background — and would otherwise index an emptyparents.Behaviour is unchanged for any label map without negative values, which is every label map the trainer produces.
Tests
New
unit_tests/test_synthseg_em_ignore_label.py, four tests. The first pins the invariant the crash violated (counted == selected) and is deterministic; the others exercise the call that raised, and the all-ignore patch.Verified both directions on this branch:
4 passed, 4 subtests passedfunctional.pyreverted:5 failed, 1 passed—IndexError: index 2272 is out of bounds for dimension 0 with size 1840Full suite:
473 passed, 13 skipped, 1523 subtests passed.🤖 Generated with Claude Code