@@ -498,9 +498,10 @@ common rail driver.
498498The following proposed optimizations were deliberately deferred:
499499
500500- A pre-sorted target list would replace an O(P x V) scan, but the measured
501- target-selection phase is only 0.8 ms on the ` 256^3 ` fixture. Its O(V log V)
502- setup, extra memory, and invalidation bookkeeping cannot pay back materially
503- at present.
501+ target-selection phase was only 0.8 ms on the ` 256^3 ` fixture. It was
502+ therefore deferred at this point. The later real-filament MRC follow-up
503+ below showed that this conclusion was workload-specific and implemented an
504+ adaptive ordered selector for large, many-rail components.
504505- Touched-node or generation-based compact state reset could avoid a V-byte
505506 clear per rail, but all rail-path Dijkstra work is only 26.6 ms in the same
506507 profile. Adding a branch/write to every discovery without first isolating
@@ -511,8 +512,8 @@ The following proposed optimizations were deliberately deferred:
511512 different, and keeping the dense oracle structurally independent helps it
512513 catch compact-specific regressions. Only the identical, policy-free bounds
513514 calculation was shared.
514- - Candidate-target and state- reset changes are therefore benchmark follow-ups,
515- not pending correctness fixes.
515+ - State- reset changes remain benchmark follow-ups rather than pending
516+ correctness fixes. Candidate-target selection is revisited below .
516517
517518## Multi-component and multi-label dispatch
518519
@@ -605,3 +606,133 @@ A profile build on the 27-label packed case at one worker reported:
605606The normal optimized build was restored after profiling. Skeleton outputs were
606607exact across worker counts ` 1, 2, 4, 8 ` , and the benchmark validates ` E = V-C ` ,
607608semantic keys, paired binary/label parity, and raw samples before writing JSON.
609+
610+ ## Real filament MRC target-selection follow-up
611+
612+ The synthetic branching tubes used above require only a small number of rails,
613+ so their repeated full-domain target scan was negligible. A real binary mask
614+ with many long, closely packed filaments exposed a different scaling regime.
615+ The input ` examples/skeleton/00004_gt_mask.mrc ` has shape
616+ ` 324 x 1251 x 1251 ` , 10,971,478 foreground voxels, and produces 1,293
617+ 26-connected skeleton components. With ` scale=3.0 ` and eight workers, the
618+ original full-volume call took 194.737 s.
619+
620+ The real-data size sweep is reproducible with:
621+
622+ ``` bash
623+ python development/skeleton/benchmark_teasar_mrc.py
624+ python development/skeleton/benchmark_teasar_mrc.py --include-full
625+ python development/skeleton/benchmark_teasar_mrc.py \
626+ --fractions 0.125 0.25 --threads 1 2 4 8 \
627+ --repeats 3 --warmup 1 --json /tmp/teasar_mrc.json
628+ ```
629+
630+ The harness memory-maps the MRC, takes centered nested crops, copies each crop
631+ to ` uint8 ` outside the timed region, and reports shape, foreground count,
632+ component count (` V - E ` ), graph size, raw samples, median, and minimum. It
633+ compares vertices, edges, and radii array-exactly when several worker counts
634+ are requested. The default is one no-warmup measurement at eight workers for
635+ 12.5%, 25%, and 50% crops; the full volume is opt-in because it was initially
636+ more than a three-minute call.
637+
638+ ### Profile diagnosis
639+
640+ The centered 25% crop has 81 components and 972,194 foreground voxels, but one
641+ component contains 757,982 voxels. On the centered 50% crop, one of 286
642+ components contains 3,346,043 of the 4,356,501 foreground voxels. This
643+ imbalance explains why component-level fan-out alone does not scale well:
644+ when the component count exceeds the thread budget, each component receives a
645+ one-thread local budget and the dominant component determines wall time.
646+
647+ A profile build isolated the dominant 50% component and called the compact
648+ on-the-fly FP64 backend with one worker. The before and after columns use the
649+ same mask, TEASAR parameters, backend, compiler configuration, and worker
650+ count:
651+
652+ | phase | repeated scan | adaptive ordering | change |
653+ | --- | ---: | ---: | ---: |
654+ | input crop / padding | 204.1 ms | 198.9 ms | -2.5% |
655+ | distance transform | 1.652 s | 1.631 s | -1.3% |
656+ | compact domain | 147.4 ms | 149.6 ms | +1.5% |
657+ | DBF compaction | 35.6 ms | 40.5 ms | +13.8% |
658+ | root Dijkstra | 2.808 s | 2.789 s | -0.7% |
659+ | PDRF | 80.2 ms | 80.8 ms | +0.7% |
660+ | target selection | 12.887 s | 628.1 ms | ** -95.1%** |
661+ | rail-path Dijkstra | 3.530 s | 3.612 s | +2.3% |
662+ | invalidation | 465.3 ms | 457.3 ms | -1.7% |
663+ | total | 21.810 s | 9.587 s | ** -56.0%** |
664+
665+ The unchanged neighboring phases isolate the improvement to target selection.
666+ With eight workers after the change, the same component took 8.651 s: EDT was
667+ 427.5 ms, target selection 650.8 ms, root Dijkstra 2.857 s, and rail Dijkstra
668+ 3.765 s. Shortest-path work is therefore the new dominant cost, accounting for
669+ 76.5% of the profiled total.
670+
671+ ### Adaptive target ordering
672+
673+ Every rail previously found its target by scanning all compact nodes and
674+ choosing the farthest active root-field value, an O(P x V) operation for ` P `
675+ rails and ` V ` foreground nodes. The optimized compact backend retains that
676+ linear scan for small and short skeletonizations. Components with at least
677+ 65,536 compact nodes switch only after
678+ ` max(16, bit_width(number_of_nodes)) ` rail selections. At that point the
679+ remaining active nodes are sorted once by descending root distance and
680+ ascending compact node ID. Later selections advance through the array and
681+ skip nodes removed by invalidation.
682+
683+ The secondary compact-ID order exactly preserves the original ascending-scan
684+ tie break. The dense FP64 backend remains unchanged as a correctness oracle. A
685+ connected thick-comb fixture forces more than 16 rails above the size crossover
686+ and verifies array-exact dense/compact vertices, edges, and radii.
687+
688+ Single-run eight-worker measurements on the MRC sweep were:
689+
690+ | crop | shape | foreground | components | before | after | change |
691+ | --- | --- | ---: | ---: | ---: | ---: | ---: |
692+ | 12.5% | ` 40 x 156 x 156 ` | 152,662 | 23 | 100 ms | 104 ms | +4.0% |
693+ | 25% | ` 81 x 312 x 312 ` | 972,194 | 81 | 1.655 s | 1.463 s | -11.6% |
694+ | 50% | ` 162 x 625 x 625 ` | 4,356,501 | 286 | 22.271 s | 10.061 s | ** -54.8%** |
695+ | full | ` 324 x 1251 x 1251 ` | 10,971,478 | 1,293 | 194.737 s | 43.150 s | ** -77.8%** |
696+
697+ The full-volume result is a 4.51x speedup. The smallest crop is below the
698+ ordered-selector crossover for every component, so its small difference is
699+ run-to-run noise on an unchanged target-selection path. The full-volume
700+ baseline is the original example measurement; the optimized value and crop
701+ rows were collected through the new harness.
702+
703+ ### Parallel Dijkstra decision on this data
704+
705+ The removed compact delta-stepping adapter was rebuilt from the last commit
706+ that contained it and tested on the isolated 3,346,043-voxel component. The
707+ complete sequential-heap call took 21.9 s. With eight workers, delta stepping
708+ finished its EDT in 356 ms but the call was stopped after more than 90 s without
709+ completing. It therefore regressed this workload by more than 4x. These
710+ one-source root fields and early-stopping weighted rail paths have narrow
711+ wavefronts and do not amortize proposal generation, sorting, merging, and
712+ synchronization. Compact root and rail solves remain on the optimized
713+ sequential heap; the thread budget continues to apply to EDT and independent
714+ components.
715+
716+ ### Synthetic retention benchmark
717+
718+ The established large binary suite was rerun before and after with one worker,
719+ one warmup, three measured calls, and the deterministic backend-order shuffle:
720+
721+ ``` bash
722+ python development/skeleton/benchmark_teasar.py \
723+ --large --suite binary --threads 1 --repeats 3 --warmup 1
724+ ```
725+
726+ | case | before median | after median | change |
727+ | --- | ---: | ---: | ---: |
728+ | branching tube ` 128^3 ` | 34.57 ms | 33.93 ms | -1.8% |
729+ | branching tube ` 192^3 ` | 111.27 ms | 107.24 ms | -3.6% |
730+ | branching tube ` 256^3 ` | 292.69 ms | 291.82 ms | -0.3% |
731+ | packed binary, 27 components | 53.03 ms | 51.34 ms | -3.2% |
732+
733+ No synthetic case regressed. The ` 128^3 ` and ` 192^3 ` components stay below
734+ the size crossover, while the ` 256^3 ` tube does not generate enough rails to
735+ leave the initial linear mode. Skeleton output remained array-exact across
736+ backends and worker counts. Final verification after the optimization and
737+ benchmark addition: ` 1182 passed ` with third-party pytest plugin autoload
738+ disabled.
0 commit comments