-
-
Notifications
You must be signed in to change notification settings - Fork 717
Deterministic multithreading usage of itkMersenneTwisterRandomVariate… #5287
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Deterministic multithreading usage of itkMersenneTwisterRandomVariate… #5287
Conversation
…Generator.cxx When using ITK library along with SuperElastix https://github.com/SuperElastix/elastix by running multiple elastix::ELASTIX objects in different threads such as : const auto runElastix = [](){ elastix::ELASTIX elastixFilter; int error = elastixFilter.RegisterImages( fixedImg, movingImg, propertiesMap, outDirPath, false, false, nullptr, nullptr); } std::vector<std::thread> threads; for (int i = 0; i <= 1; ++i) { threads.emplace_back(runElastix); } for (auto &t : threads) { t.join(); } they share the same instance of MersenneTwisterRandomVariateGenerator, which cause that results are nondeterministic. This simple fix stabilize results along multiple threads and does not seems to break anything in ITK.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for contributing a pull request! 🙏
Welcome to the ITK community! 🤗👋☀️
We are glad you are here and appreciate your contribution. Please keep in mind our community participation guidelines. 📜
More support and guidance on the contribution process can be found in our contributing guide. 📖
This is an automatic message. Allow for time for the ITK community to be able to read the pull request and comment
on it.
|
This breaks the singleton mechanics, e.g. setting a seed from one thread will now only apply that seed to that thread. Niels, does this change break any tests in Elastix? |
pull request InsightSoftwareConsortium/ITK#5287 Deterministic multithreading usage of itkMersenneTwisterRandomVariateGenerator.cxx
|
Interesting, thanks @michalmeszaroswork! I just gave it a try: https://github.com/SuperElastix/elastix/tree/michalmeszaroswork-patch-1 (should take an hour or so to pass the test). |
|
Nice job identifying the dependency onto global random. However, change the behavior of this may not be desirable by all even if it does not immediately "break" things. Let us consider another option. The alternative is that each algorithm initializes its own random number generate ( either seeded by a user provided value, clock or the next seed from the global ). Using an algorithm specific generator removes the dependency on the global ( even a thread local is still a global and could cause issues is some cases, especially with certain thread pool or task based threading models.)
|
| if (!m_PimplGlobals->m_StaticInstance) | ||
| thread_local MersenneTwisterRandomVariateGenerator::Pointer threadSafeInstance{}; | ||
| if (!threadSafeInstance) | ||
| { | ||
| m_PimplGlobals->m_StaticInstance = MersenneTwisterRandomVariateGenerator::CreateInstance(); | ||
| m_PimplGlobals->m_StaticInstance->SetSeed(); | ||
| threadSafeInstance = MersenneTwisterRandomVariateGenerator::CreateInstance(); | ||
| threadSafeInstance->SetSeed(); | ||
| } | ||
|
|
||
| return m_PimplGlobals->m_StaticInstance; | ||
| return threadSafeInstance; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: I would name it threadLocalInstance, rather than threadSafeInstance. The original instance m_StaticInstance was thread-safe already, but your essential change is to make it thread local.
Then I would suggest to have it initialized during its declaration, so that it can be const:
const thread_local MersenneTwisterRandomVariateGenerator::Pointer threadLocalInstance = [] {
MersenneTwisterRandomVariateGenerator::Pointer instance = MersenneTwisterRandomVariateGenerator::CreateInstance();
instance->SetSeed();
return instance;
}();
return threadLocalInstance;
Interesting, @blowekamp . I guess for elastix we should then replace or rewrite ITK's random iterators, right? elastix's |
So the initial issue is that if you run the algorithm (elastix) concurrently in multiple threads without setting the seed, then the behavior is randomized? Are there seeds available to set per algorithm or the only option is setting a global seed? Have you looked ImageRegistrationMethodv4? A seed can be set for each instance of the class, and it uses it own MersenneTwisterRandomVariateGenerator to generate seeds for each of its components. Additionally, you may be interested in the method use the the Image Noise filters e.g.(https://github.com/InsightSoftwareConsortium/ITK/blob/master/Modules/Filtering/ImageNoise/include/itkSaltAndPepperNoiseImageFilter.hxx#L46-L54). Here the filter has a seed, and each thread modifies the seed for different per-seed values. This has the effect of each thread having a different random seed, but is reproducible by not cause a non-deterministic race condition for the seed. However the results do very with the number of threads used. |
|
@blowekamp Even when each thread has its own instance of
I guess that might still cause nondeterministic behavior, right? Even if it is "thread-safe" (atomic), it may still affect the behavior when one thread does |
My understanding is that m_StaticDiffer is use to set the seeds sequentially. My recommendations above are to explicitly set the seeds for the algorithm, and not rely on the global state. |
Thanks @blowekamp, I see now, two different generators produce the same random number sequence, when they have the same initial seed: using itk::Statistics::MersenneTwisterRandomVariateGenerator;
const auto generator1 = MersenneTwisterRandomVariateGenerator::New();
const auto generator2 = MersenneTwisterRandomVariateGenerator::New();
generator1->SetSeed(42);
generator2->SetSeed(42);
std::cout << generator1->GetIntegerVariate() << '\n'
<< generator2->GetIntegerVariate() << '\n'
<< generator1->GetIntegerVariate() << '\n'
<< generator2->GetIntegerVariate() << '\n'
<< generator1->GetIntegerVariate() << '\n'
<< generator2->GetIntegerVariate() << '\n';Output:
👍 I think it's a bit unfortunate that the global |
Yes. That makes sense to me. |
|
From the discussion here, it looks like this PR is not a good solution. Have we identified an alternative solution? If so, who should propose a PR? Or should this PR be abandoned? |
I'm still looking into elastix, trying to replace all those |
Stopped using the global instance of MersenneTwisterRandomVariateGenerator. Stopped calling its `GetInstance()`. Added a deterministic default-constructed `m_RandomVariateGenerator` to ElastixBase. Added pointers to this generator to AdvancedImageToImageMetric, ImageRandomSamplerBase, and CMAEvolutionStrategyOptimizer. Added `SetRandomVariateGenerator` member functions to these classes (using a default-constructed generator when `SetRandomVariateGenerator` is not yet called). Also removed the one `MersenneTwisterRandomVariateGenerator::New()` call from elastix, which was for a local generator in `ImageRandomSamplerBase::GenerateRandomNumberList()`. Instead, added `m_Seed` to ImageRandomSamplerBase, and used that seed for a default-constructed local generator. Aims to make the results of running multiple registrations parallel (multi-threaded) within a single process deterministic. Triggered by pull request InsightSoftwareConsortium/ITK#5287 "Deterministic multithreading usage of itkMersenneTwisterRandomVariateGenerator.cxx", Michal Meszaros, Mar 21, 2025.
Stopped using the global instance of MersenneTwisterRandomVariateGenerator. Stopped calling its `GetInstance()`. Added a deterministic default-constructed `m_RandomVariateGenerator` to ElastixBase. Added pointers to this generator to AdvancedImageToImageMetric, ImageRandomSamplerBase, and CMAEvolutionStrategyOptimizer. Added `SetRandomVariateGenerator` member functions to these classes (using a default-constructed generator when `SetRandomVariateGenerator` is not yet called). Also removed the one `MersenneTwisterRandomVariateGenerator::New()` call from elastix, which was for a local generator in `ImageRandomSamplerBase::GenerateRandomNumberList()`. Instead, added `m_Seed` to ImageRandomSamplerBase, and used that seed for a default-constructed local generator. Aims to make the results of running multiple registrations parallel (multi-threaded) within a single process deterministic. Triggered by pull request InsightSoftwareConsortium/ITK#5287 "Deterministic multithreading usage of itkMersenneTwisterRandomVariateGenerator.cxx", Michal Meszaros, Mar 21, 2025.
Stopped using the global instance of MersenneTwisterRandomVariateGenerator. Stopped calling its `GetInstance()`. Added a deterministic default-constructed `m_RandomVariateGenerator` to ElastixBase. Added pointers to this generator to AdvancedImageToImageMetric, ImageRandomSamplerBase, and CMAEvolutionStrategyOptimizer. Added `SetRandomVariateGenerator` member functions to these classes (using a default-constructed generator when `SetRandomVariateGenerator` is not yet called). Also removed the one `MersenneTwisterRandomVariateGenerator::New()` call from elastix, which was for a local generator in `ImageRandomSamplerBase::GenerateRandomNumberList()`. Instead, added `m_Seed` to ImageRandomSamplerBase, and used that seed for a default-constructed local generator. Aims to make the results of running multiple registrations parallel (multi-threaded) within a single process deterministic. Triggered by pull request InsightSoftwareConsortium/ITK#5287 "Deterministic multithreading usage of itkMersenneTwisterRandomVariateGenerator.cxx", Michal Meszaros, Mar 21, 2025.
|
@michalmeszaroswork Thanks again for your work on this topic. Do you agree that with the recent merge of pull request SuperElastix/elastix#1323, your ITK pull request is no longer necessary, for elastix? |
|
Fixes made in SuperElastix/elastix#1323 make this PR unnecessary. |
…Generator.cxx
When using ITK library along with SuperElastix
https://github.com/SuperElastix/elastix
by running multiple elastix::ELASTIX objects in different threads such as :
const auto runElastix = {
elastix::ELASTIX elastixFilter;
int error = elastixFilter.RegisterImages(
fixedImg,
movingImg,
propertiesMap,
outDirPath,
false,
false,
nullptr,
nullptr);
}
they share the same instance of MersenneTwisterRandomVariateGenerator, which cause that results are nondeterministic. This simple fix stabilize results along multiple threads and does not seems to break anything in ITK.
PR Checklist
Refer to the ITK Software Guide for
further development details if necessary.