Skip to content

Commit fc15a07

Browse files
authored
Merge pull request #6337 from hjmjohnson/supersede-6321-pslsif-platform-multithreader
BUG: Fix ParallelSparseFieldLevelSet deadlock with PlatformMultiThreader (supersedes #6321)
2 parents 51f9bcf + dc83d3c commit fc15a07

3 files changed

Lines changed: 341 additions & 0 deletions

File tree

Modules/Segmentation/LevelSets/include/itkParallelSparseFieldLevelSetImageFilter.hxx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ ParallelSparseFieldLevelSetImageFilter<TInputImage, TOutputImage>::ParallelSpars
125125
, m_IsoSurfaceValue(m_ValueZero)
126126
{
127127
this->SetRMSChange(static_cast<double>(m_ValueOne));
128+
// Neighbor-blocking inter-phase sync requires a dedicated OS thread per work unit.
129+
this->DynamicMultiThreadingOff();
130+
this->SetMultiThreader(PlatformMultiThreader::New());
128131
}
129132

130133
template <typename TInputImage, typename TOutputImage>

Modules/Segmentation/LevelSets/test/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ set(
3333

3434
createtestdriver(ITKLevelSets "${ITKLevelSets-Test_LIBRARIES}" "${ITKLevelSetsTests}")
3535

36+
set(
37+
ITKLevelSetsGTests
38+
itkParallelSparseFieldLevelSetImageFilterRobustnessGTest.cxx
39+
)
40+
creategoogletestdriver(ITKLevelSets "${ITKLevelSets-Test_LIBRARIES}" "${ITKLevelSetsGTests}")
41+
3642
itk_add_test(
3743
NAME itkThresholdSegmentationLevelSetImageFilterTest
3844
COMMAND
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
/*=========================================================================
2+
*
3+
* Copyright NumFOCUS
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0.txt
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*=========================================================================*/
18+
19+
// Robustness test: the neighbor-only inter-phase sync deadlocks under
20+
// pool/TBB MultiThreader backends when work units exceed worker-pool size.
21+
// Only same-work-unit-count runs produce bit-identical output. See the PR
22+
// for the full analysis and scenario rationale.
23+
24+
#include "gtest/gtest.h"
25+
26+
#include "itkLevelSetFunction.h"
27+
#include "itkParallelSparseFieldLevelSetImageFilter.h"
28+
29+
#include <algorithm>
30+
#include <chrono>
31+
#include <cmath>
32+
#include <condition_variable>
33+
#include <future>
34+
#include <iostream>
35+
#include <mutex>
36+
#include <thread>
37+
#include <vector>
38+
39+
namespace PSFLSIFR
40+
{
41+
42+
constexpr unsigned int DIM = 32;
43+
constexpr int RADIUS = DIM / 4;
44+
45+
// Abort the process if body does not finish within the deadline, so a
46+
// ParallelizeArray dispatch deadlock fails the test instead of hanging the
47+
// driver. The watchdog runs on its own OS thread, outside the work-unit pool
48+
// that the deadlock pins.
49+
template <typename TFunctor>
50+
void
51+
runWithDeadline(unsigned int seconds, TFunctor && body)
52+
{
53+
std::mutex m;
54+
std::condition_variable cv;
55+
bool done = false;
56+
std::thread watchdog([&] {
57+
std::unique_lock<std::mutex> lk(m);
58+
if (!cv.wait_for(lk, std::chrono::seconds(seconds), [&] { return done; }))
59+
{
60+
std::cerr << "Deadline exceeded (" << seconds << "s): ParallelizeArray dispatch deadlock\n";
61+
std::abort();
62+
}
63+
});
64+
body();
65+
{
66+
const std::lock_guard<std::mutex> lk(m);
67+
done = true;
68+
}
69+
cv.notify_all();
70+
watchdog.join();
71+
}
72+
73+
float
74+
sphere(unsigned int x, unsigned int y, unsigned int z)
75+
{
76+
float dis = (x - float{ DIM } / 2.0f) * (x - float{ DIM } / 2.0f) +
77+
(y - float{ DIM } / 2.0f) * (y - float{ DIM } / 2.0f) +
78+
(z - float{ DIM } / 2.0f) * (z - float{ DIM } / 2.0f);
79+
dis = RADIUS - std::sqrt(dis);
80+
return -dis;
81+
}
82+
83+
float
84+
cube(unsigned int x, unsigned int y, unsigned int z)
85+
{
86+
const float X = itk::Math::Absolute(x - float{ DIM } / 2.0f);
87+
const float Y = itk::Math::Absolute(y - float{ DIM } / 2.0f);
88+
const float Z = itk::Math::Absolute(z - float{ DIM } / 2.0f);
89+
float dis = -std::sqrt((X - RADIUS) * (X - RADIUS) + (Y - RADIUS) * (Y - RADIUS) + (Z - RADIUS) * (Z - RADIUS));
90+
if (!((X > RADIUS) && (Y > RADIUS) && (Z > RADIUS)))
91+
{
92+
dis = RADIUS - (std::max(std::max(X, Y), Z));
93+
}
94+
return -dis;
95+
}
96+
97+
void
98+
fill_image(itk::Image<float, 3> * im, float (*f)(unsigned int, unsigned int, unsigned int))
99+
{
100+
itk::Image<float, 3>::IndexType idx;
101+
for (unsigned int x = 0; x < DIM; ++x)
102+
{
103+
idx[0] = x;
104+
for (unsigned int y = 0; y < DIM; ++y)
105+
{
106+
idx[1] = y;
107+
for (unsigned int z = 0; z < DIM; ++z)
108+
{
109+
idx[2] = z;
110+
im->SetPixel(idx, f(x, y, z));
111+
}
112+
}
113+
}
114+
}
115+
116+
class MorphFunction : public itk::LevelSetFunction<itk::Image<float, 3>>
117+
{
118+
public:
119+
using Self = MorphFunction;
120+
using Superclass = itk::LevelSetFunction<itk::Image<float, 3>>;
121+
using Pointer = itk::SmartPointer<Self>;
122+
itkOverrideGetNameOfClassMacro(MorphFunction);
123+
itkNewMacro(Self);
124+
125+
void
126+
SetDistanceTransform(itk::Image<float, 3> * d)
127+
{
128+
m_DistanceTransform = d;
129+
}
130+
131+
protected:
132+
~MorphFunction() override = default;
133+
MorphFunction()
134+
{
135+
RadiusType r;
136+
r[0] = r[1] = r[2] = 1;
137+
Superclass::Initialize(r);
138+
}
139+
140+
private:
141+
itk::Image<float, 3>::Pointer m_DistanceTransform;
142+
ScalarValueType
143+
PropagationSpeed(const NeighborhoodType & nbh, const FloatOffsetType &, GlobalDataStruct *) const override
144+
{
145+
return m_DistanceTransform->GetPixel(nbh.GetIndex());
146+
}
147+
};
148+
149+
class MorphFilter : public itk::ParallelSparseFieldLevelSetImageFilter<itk::Image<float, 3>, itk::Image<float, 3>>
150+
{
151+
public:
152+
ITK_DISALLOW_COPY_AND_MOVE(MorphFilter);
153+
using Self = MorphFilter;
154+
using Pointer = itk::SmartPointer<Self>;
155+
itkOverrideGetNameOfClassMacro(MorphFilter);
156+
itkNewMacro(Self);
157+
itkSetMacro(Iterations, unsigned int);
158+
159+
void
160+
SetDistanceTransform(itk::Image<float, 3> * im)
161+
{
162+
auto * func = dynamic_cast<MorphFunction *>(this->GetDifferenceFunction().GetPointer());
163+
if (func == nullptr)
164+
{
165+
itkGenericExceptionMacro("MorphFunction cast failed");
166+
}
167+
func->SetDistanceTransform(im);
168+
}
169+
170+
protected:
171+
~MorphFilter() override = default;
172+
MorphFilter()
173+
{
174+
auto p = MorphFunction::New();
175+
p->SetPropagationWeight(-1.0);
176+
p->SetAdvectionWeight(0.0);
177+
p->SetCurvatureWeight(1.0);
178+
this->SetDifferenceFunction(p);
179+
}
180+
181+
private:
182+
unsigned int m_Iterations{ 0 };
183+
bool
184+
Halt() override
185+
{
186+
return this->GetElapsedIterations() == m_Iterations;
187+
}
188+
};
189+
190+
using ImageType = itk::Image<float, 3>;
191+
192+
ImageType::Pointer
193+
make_init_image()
194+
{
195+
auto im = ImageType::New();
196+
ImageType::SizeType sz{ DIM, DIM, DIM };
197+
ImageType::RegionType r{ sz };
198+
im->SetRegions(r);
199+
im->Allocate();
200+
fill_image(im, sphere);
201+
return im;
202+
}
203+
204+
ImageType::Pointer
205+
make_target_image()
206+
{
207+
auto im = ImageType::New();
208+
ImageType::SizeType sz{ DIM, DIM, DIM };
209+
ImageType::RegionType r{ sz };
210+
im->SetRegions(r);
211+
im->Allocate();
212+
fill_image(im, cube);
213+
itk::ImageRegionIterator<ImageType> it(im, im->GetRequestedRegion());
214+
for (it.GoToBegin(); !it.IsAtEnd(); ++it)
215+
{
216+
it.Value() = it.Value() / std::sqrt((5.0f + itk::Math::sqr(it.Value())));
217+
}
218+
return im;
219+
}
220+
221+
ImageType::Pointer
222+
run_one(unsigned int workUnits, unsigned int iterations)
223+
{
224+
auto init = make_init_image();
225+
auto target = make_target_image();
226+
auto mf = MorphFilter::New();
227+
mf->SetDistanceTransform(target);
228+
mf->SetIterations(iterations);
229+
mf->SetInput(init);
230+
mf->SetNumberOfWorkUnits(workUnits);
231+
mf->SetNumberOfLayers(3);
232+
mf->SetIsoSurfaceValue(0.0);
233+
mf->Update();
234+
ImageType::Pointer out = mf->GetOutput();
235+
out->DisconnectPipeline();
236+
return out;
237+
}
238+
239+
double
240+
image_summary(ImageType * img)
241+
{
242+
double sum = 0.0;
243+
itk::ImageRegionConstIterator<ImageType> it(img, img->GetRequestedRegion());
244+
for (it.GoToBegin(); !it.IsAtEnd(); ++it)
245+
{
246+
sum += static_cast<double>(it.Get()) * static_cast<double>(it.Get());
247+
}
248+
return std::sqrt(sum / img->GetRequestedRegion().GetNumberOfPixels());
249+
}
250+
251+
bool
252+
images_identical(ImageType * a, ImageType * b)
253+
{
254+
if (a->GetRequestedRegion() != b->GetRequestedRegion())
255+
{
256+
return false;
257+
}
258+
itk::ImageRegionConstIterator<ImageType> ai(a, a->GetRequestedRegion());
259+
itk::ImageRegionConstIterator<ImageType> bi(b, b->GetRequestedRegion());
260+
for (ai.GoToBegin(), bi.GoToBegin(); !ai.IsAtEnd(); ++ai, ++bi)
261+
{
262+
if (ai.Get() != bi.Get())
263+
{
264+
return false;
265+
}
266+
}
267+
return true;
268+
}
269+
270+
} // namespace PSFLSIFR
271+
272+
// Scenario 1: repeatedly cycle work-unit counts to amplify the rare per-run
273+
// pool-starvation deadlock into a near-certain per-test failure.
274+
TEST(ParallelSparseFieldLevelSetRobustness, SweepRepeat)
275+
{
276+
using namespace PSFLSIFR;
277+
runWithDeadline(30, [] {
278+
const std::vector<unsigned int> sweep{ 1, 2, 4, 8, 11, 16, 32 };
279+
constexpr unsigned int kSweepRepeats = 20;
280+
constexpr unsigned int kSweepIterations = 30;
281+
for (unsigned int rep = 0; rep < kSweepRepeats; ++rep)
282+
{
283+
for (const unsigned int wu : sweep)
284+
{
285+
auto out = run_one(wu, kSweepIterations);
286+
const double summary = image_summary(out);
287+
EXPECT_TRUE(std::isfinite(summary));
288+
EXPECT_GT(summary, 0.0);
289+
EXPECT_LT(summary, 100.0);
290+
}
291+
}
292+
});
293+
}
294+
295+
// Scenario 2: repeated runs at a fixed work-unit count must be bit-identical.
296+
TEST(ParallelSparseFieldLevelSetRobustness, Determinism)
297+
{
298+
using namespace PSFLSIFR;
299+
runWithDeadline(30, [] {
300+
auto run0 = run_one(11, 100);
301+
for (unsigned int rep = 1; rep < 3; ++rep)
302+
{
303+
auto runN = run_one(11, 100);
304+
EXPECT_TRUE(images_identical(run0, runN)) << "run " << rep << " differs from run 0";
305+
}
306+
});
307+
}
308+
309+
// Scenario 3: eight concurrent std::async pipelines (88 work units) contend
310+
// for the core-bounded worker pool, probing the dispatch-starvation deadlock.
311+
TEST(ParallelSparseFieldLevelSetRobustness, ConcurrentMultiPipeline)
312+
{
313+
using namespace PSFLSIFR;
314+
runWithDeadline(30, [] {
315+
constexpr unsigned int kConcurrentReps = 6;
316+
constexpr unsigned int kConcurrentPipelines = 8;
317+
for (unsigned int rep = 0; rep < kConcurrentReps; ++rep)
318+
{
319+
std::vector<std::future<ImageType::Pointer>> futures;
320+
futures.reserve(kConcurrentPipelines);
321+
for (unsigned int p = 0; p < kConcurrentPipelines; ++p)
322+
{
323+
futures.emplace_back(std::async(std::launch::async, [] { return run_one(11, 60); }));
324+
}
325+
for (auto & f : futures)
326+
{
327+
ImageType::Pointer out = f.get();
328+
EXPECT_TRUE(out.IsNotNull());
329+
}
330+
}
331+
});
332+
}

0 commit comments

Comments
 (0)