Repository navigation
Conversation
Default work units per thread differ by multithreader (Platform 1, Pool 4, TBB 16), and a filter could only override them with a fixed count. A non-zero ratio sets the default NumberOfWorkUnits to ratio times the threader's maximum number of threads, so filters with a high cost per work unit can set a low ratio in their constructor. Zero, the default, keeps the threader's default. An explicitly set NumberOfWorkUnits is kept. Closes InsightSoftwareConsortium#950.
Each work unit allocates, fills, and merges a whole histogram, which dominates run time for multi-component histograms under Pool (4 work units per thread) and TBB (16). 24 threads, ms, 128^3 / 256^3, threader default -> two per thread: 3 x 64 bins: Pool 214 -> 166 / 364 -> 292, TBB 527 -> 146 / 683 -> 267 2 x 256 bins: Pool 53 -> 45 / 164 -> 156, TBB 120 -> 43 / 225 -> 162 1 x 256 bins: Pool 8.9 -> 10.5 / 64 -> 75, TBB 7.9 -> 9.3 / 64 -> 78
SetWorkUnitToThreadRatio assigned m_NumberOfWorkUnits directly, so composite filters that override SetNumberOfWorkUnits to configure their internal filters, such as GrayscaleErodeImageFilter, kept the old count. The default is still stored unclamped, as SetMultiThreader does.
BinaryImageToLabelMapFilter adds a line per work-unit boundary to its serial label-equivalence pass; LabelImageToLabelMapFilter builds one label map per work unit and merges them serially. Use one and two work units per thread. The composites forward their count to these filters, so they get the same ratio. LabelMapFilter subclasses take label objects from a shared queue, so their count does not matter. 24 threads, ms, threader default -> new, Pool / TBB: BinaryImageToLabelMap, 128^3 840 -> 791 / 923 -> 456 LabelImageToLabelMap, 8000 labels 39 -> 33 / 73 -> 39 BinaryFillhole, 160^3 3400 -> 2090 / 3470 -> 2100 LabelShapeOpening, 8000 labels 53 -> 42 / 51 -> 42
These classes do work per work unit that grows with the kernel, lattice, or label count: box mean and sigma, anchor and van Herk/Gil-Werman morphology recompute a kernel-radius halo, the B-spline scattered data fit allocates and reduces whole lattices, and label statistics merge per-label results. Binary opening and closing by reconstruction forward their count to the binary reconstruction filters. Other Filtering classes were within noise or slower with fewer units. 24 threads, ms, threader default -> new, Pool / TBB: BoxMean radius 5, 160^3 117 -> 52 / 228 -> 49 AnchorOpen box radius 5, 128^3 190 -> 74 / 405 -> 71 BSpline fit, 40^3 control points 5550 -> 4530 / 5300 -> 4330 LabelStatistics, 8000 labels, hist 72 -> 46 / 137 -> 57
ConnectedComponentImageFilter adds a line per work-unit boundary to its serial label-equivalence pass. BlockMatchingImageFilter gives the last work unit all remaining feature points, so with fewer points than work units it runs on one thread. The curvature and elastic VariationalRegistration regularizers also use the count as their FFT thread count. Level set, PDE deformable registration, label voting, displacement field, Core, and Numerics classes were within noise. 24 threads, ms, threader default -> new, Pool / TBB: ConnectedComponent, 160^3 1070 -> 920 / 1160 -> 491 BlockMatching, 100 points 1.43 -> 1.36 / 8.4 -> 1.5 ElasticRegularizer, 64^3 14.3 -> 12.0 / 17.2 -> 12.6
The 1-D FFTW filters create an FFTW plan and two line buffers for each work unit when plans are first computed or the line length changes. The N-D FFTW filters size FFTW threads by the maximum number of threads, so their work-unit count does not matter. 24 threads, 8 cases of 128^3 and 1024^2 images, summed ms, threader default -> two per thread: New filter per Update: Pool 26.9 -> 26.6, TBB 46.7 -> 27.3 Reused filter, cached plans: Pool 7.1 -> 6.8, TBB 5.5 -> 6.6
|
Threading is complicated. And this looks like extensive work that does identify some key filters that do not scale with will numerous work units. I am in favor of adding a per filter option to set the default WorkUnitToThreadRatio, or WorkUnitMultiplier. I also thinks that there should be some kind of "GlobalDefault" settable value for this as well, or perhaps it's per multithreaded. I think this is easier to do. However to set the defaults for these (the globals) is going to be harder. It will require the community setting clear criteria for the defaults, and then discussion and evaluation of the purposes set out. For example getting frequent progress updates may come at the expense of performance. |
|
Good idea. As the multi-threaders already have this, it would make more sense to have this per multi-threader. We would only have to make the current thing user-settable. Changing the defaults would benefit from extensive benchmarking on multiple machines and workloads, as well as other kinds of community input and discussion. Both can be done as follow-ups to this PR, especially the discussion about defaults. |
[Medium impact] The PR changes default scheduling for selected filters across several ITK workflows. Fix the ratio setter, the test build without Pool, and the required includes before merging. Findings
|
PoolMultiThreader used 4 work units per thread and TBBMultiThreader 16, both hard-coded. SetDefaultWorkUnitPerThreadRatio lets applications tune these defaults for all filters at once; zero restores the usual value. MultiThreaderBase declares the interface and ignores the setter, which Platform and Single inherit; Pool and TBB keep a global ratio each. ProcessObject now records the threader's defaults in SetMultiThreader, because executing a filter changes the threader's live work-unit and thread counts. A flag records explicit SetNumberOfWorkUnits calls, so a ratio change keeps them even when they equal the default.
bf6488a to
b8ec2b7
Compare
Add
ProcessObject::SetWorkUnitToThreadRatioso a filter can override its threader's default work units per thread, and set it in 37 classes that are faster with fewer work units. Closes #950.Design
NumberOfWorkUnitsto ratio ×GetMultiThreader()->GetMaximumNumberOfThreads(), clamped to [1,ITK_MAX_THREADS]. Filters set it in their constructor.NumberOfWorkUnitsis kept. Changing the multithreader keeps the existingSetMultiThreaderrule.SetNumberOfWorkUnits, so composite filters that forward it to internal filters (e.g.GrayscaleErodeImageFilter) see it.Classes changed
BoxMeanImageFilter,BoxSigmaImageFilterAnchorErodeDilateImageFilter,AnchorOpenCloseImageFilter,VanHerkGilWermanErodeDilateImageFilter(and their Erode/Dilate/Open/Close subclasses)BSplineScatteredDataPointSetToImageFilterBinaryImageToLabelMapFilter,ConnectedComponentImageFilterBinaryFillhole,BinaryGrindPeak,BinaryShape/StatisticsOpening,BinaryShape/StatisticsKeepNObjects,BinaryImageToShape/StatisticsLabelMap,BinaryReconstructionByDilation/Erosion),BinaryOpening/ClosingByReconstructionImageFilterBlockMatchingImageFilterVariationalRegistrationCurvatureRegularizer,VariationalRegistrationElasticRegularizerImageToHistogramFilter(andMaskedImageToHistogramFilter)LabelStatisticsImageFilterLabelImageToLabelMapFilterand 8 label composites (LabelShape/StatisticsOpening,LabelShape/StatisticsKeepNObjects,Shape/StatisticsRelabel,LabelImageToShape/StatisticsLabelMap)FFTWForward1DFFTImageFilter,FFTWInverse1DFFTImageFilter,FFTWComplexToComplex1DFFTImageFilterBenchmarks (24-thread Ryzen 9 5900, MSVC 14.51 Release, Windows)
Median of 5-15 runs, ms, threader default → new default.
BoxMeanradius 5, 160³ uint16AnchorOpenbox radius 5, 128³ uint8VanHerkGilWermanDilatebox radius 2, 128³GrayscaleErodebox radius 2 (internal Anchor)BSplineScatteredDatafit, 40³ control pointsImageToHistogram3 × 64 bins, 128³LabelStatistics8000 labels, histogramsLabelImageToLabelMap8000 labelsBinaryImageToLabelMap128³BinaryFillhole160³ConnectedComponent160³BlockMatching100 pointsVariationalRegistrationElasticRegularizer64³, FFTWVariationalRegistrationCurvatureRegularizer64³, FFTWEach case was also swept over ratios 1, 2, 4 under Platform, Pool, and TBB to pick the ratio.
Classes left at the threader default
About 110 cases covering every module with threaded code, chosen after reading all ~400 threaded source files for per-work-unit cost. Within noise or slower with fewer work units, so not changed:
NeighborhoodOperatorImageFilter, recursive Gaussian.MovingHistogramDilate,MaskedRank.Statistics,MinimumMaximum,SimilarityIndex,LabelOverlapMeasures, projections, Otsu.SignedMaurerDistanceMap.RelabelComponent.GetMaximumNumberOfThreads), PocketFFT 1-D, FFT convolution,VariationalRegistrationDiffusionRegularizer.BSplineControlPointImageFilter: ratio 1 helps only when the lattice is large relative to the output, and is ~10% slower for a small lattice with a large output (the N4 case).Known trade-offs
LabelStatisticsImageFilterwith few labels is up to 0.5 ms slower at 160³.PlatformMultiThreaderstarts two threads per core for those classes.Testing
itkProcessObjectWorkUnitToThreadRatioGTest(default, ratio from constructor, ratio change, explicit count kept, forwarding through the virtual setter), against Platform, Pool, Single, and TBB.Module_ITKFFTW,ITK_USE_FFTWD,ITK_USE_FFTWF: all 95 ITKFFT and VariationalRegistration tests pass.pre-commitpasses on all changed files.AI assistance
🤖 Generated with Claude Code