lib: Make random number generation thread safe - #6480
Conversation
|
The code changes look straightforward. Do you have an idea about performance? I agree we need the change in any case, but at least knowing whether we need a better solution in the future would be good. |
|
Good news. This makes the failing rand function tests in #6476 pass at least on Linux. |
As random number generator tracks its internal state as a static variable, it is necessary to guard its updates with mutex to prevent misbehavour when multiple threads change values simultaneously
I haven't tested but locked part is small and should be fast (just update, no IO), thus I would expect impact on speed to be negligible. |
wenzeslaus
left a comment
There was a problem hiding this comment.
I think the locking needs to expand to reading of the shared state, it is both write and read which need to be isolated.
More generally, I'm afraid we didn't finalize the r.mapcalc's rand behavior with multiple threads (and seeds). (Or did we?) If r.mapcalc is using these functions directly as before parallelization, the random numbers placed in the raster will depend on the order of threads, i.e., what rows will come first, but I did not review the relevant code now. (An ultimate fix there would be different "context" objects for the random numbers, per row (for maximum reproducibility) or per thread (for at least seed+nprocs reproducibility).
No IO, but it is called for every cell in r.mapcalc, no? Still better slow with overall faster code than a segfault, and |
multiple threads As random number generation is guarded by a mutex, it makes generation a serial operation – always leading to the same sequence of numbers
|
This is the worst case scenario – code calls random number generation and discards produced numbers (thread creation + locking overhead): Total random calls per run: 100,000,000
|
RN generation now is always serial – outcome order is guaranteed. Consumption is another topic. I lean towards just stating that for fully reproducible outcome one should set parameters for single thread execution. |
|
A random sequence can contain duplicates, so your test is failing, but it is because you don't have to (can't) have that requirement. I think only the comparison of sorted lists gives you a true idea of what happened. |
From library perspective, but never in the actual usage.
So I would say just don't overstate the outcome order guarantees in the doc.
That would be my choice at this point as well.
I agree. |
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Replace the pthread mutex approach with a lock-free compare-and-swap on the whole 48-bit generator state kept in a single C11 atomic integer. One successful compare-and-swap is exactly one step of the standard drand48 linear congruential generator, so the sequence of generated values for a given seed is identical to a single-threaded run and to the previous limb-based implementation. Compared to the mutex: the feature is gated by the compiler (__STDC_NO_ATOMICS__), not by --with-pthread, so it is active in every supported build, including the OpenMP-without-pthread configurations (macOS CI, Docker images) where the race reported in OSGeo#5742 actually occurs; no build system changes or extra link dependencies are needed (the Makefile and CMake changes are dropped, which also avoids defining HAVE_PTHREAD from the Win32-threads CMake package on MSVC); and there is no lock contention (in a microbenchmark the mutex made threaded runs slower than single-threaded at every thread count, while the atomic version is about twice as fast as the mutex when several threads generate numbers at the same time). A single shared generator still cannot scale with the number of threads; per-thread state remains future work. Where C11 atomics are unavailable, the code falls back to the previous plain implementation, which is not thread-safe (unchanged behavior). Output compatibility is guarded by the PRNG regression tests merged in OSGeo#7633 and OSGeo#7634 (lib/gis/tests/lib_gis_lrand48_test.py and raster/r.mapcalc/tests/r_mapcalc_rand_seed_test.py) and was additionally verified against the previous implementation for multiple seeds. The 64-bit reformulation of the generator and its verification were AI-assisted (Claude).
…nd_thread-atomics # Conflicts: # lib/gis/CMakeLists.txt # lib/gis/Makefile
|
Commit 38ed9dc updates the PR to use C11 atomics instead of a pthread mutex to fix #5742 (thread-safe Details: why atomics, how it works, and what was verified (AI-generated)Three problems the mutex approach ran into:
How it works: the generator is the standard drand48 linear congruential generator (LCG): the next state is computed from the current one as X' = (A*X + B) mod 2^48. The previous code kept those 48 bits split across three 16-bit variables and advanced them with hand-written multi-word arithmetic, a leftover from times before 64-bit integers were portable; that split is what made concurrent updates able to interleave and corrupt the state. Now the whole state is a single 48-bit value in one The same C11-atomics-with-fallback approach is used in #7350; if both land, the capability test can be factored into a shared macro. Reproducibility: the sequence of values is serialized and seed-reproducible, but which thread (and so which raster cell) receives which value depends on scheduling. Fully reproducible results still require single-threaded execution; this is now stated in the file documentation. The concern about Test cleanup: the duplicate-values assertion is removed (a correct random sequence can contain duplicates, as the observed The 64-bit reformulation and its verification were AI-assisted (Claude). Verification included a standalone build of this file compared against the previous implementation for 100k steps and the reference sequences for five seeds, an 8-thread multiset check (1M draws), and the regression tests from #7633 and #7634 run against a GRASS build using this file. |
I changed the code to address my previous review.
nilason
left a comment
There was a problem hiding this comment.
Look good to me, the atomic ops are reasonable.
|
Lets merge this, but remember that this is the first step, as we still have other issues to tackle:
|
#7884 is an enhancement built upon this PR. I'm not sure if that answers your question.
MSVC do have support for atomic operations, but with a bit different API compared to C11. I plan to suggest to add a GRASS wrapper API for atomic operations similar to https://github.com/python/cpython/pull/108701/changes. That will hopefully make implementation of atomic operations with broader support easier in GRASS.
cpython do have a wrapper for GCC's internal functions which match C11. This will enable support for GCC 4.8+ according to https://github.com/python/cpython/blob/main/Include/cpython/pyatomic_gcc.h. |
|
With available C11 atomics, main is now without race although potentially slower (price for correctness). Follow-up in #7884 for a more complete fix. |
As random file generation keeps internal state in a static variable, it is prone to failures as multiple threads would attempt to update sate simultaneously. This PR adds a mutex lock around internal state update calls thus preventing clashes when called from multiple threads.
Contains code to be merged as PR #6479
Also contains necessary changes in Make/CMake to compile gis lib with pthread support.
Should fix issues observed in r.mapcalc after thread support was merged #5742