Spot duplicated worker RNG draws by encoding identity in the data
To check whether a library's process pool reuses random numbers, you don't need to patch its workers. Build the input so the output shows what was sampled. Two examples: give series i the target i*10000 + t, so each sampled training window shows which series and start offset it came from; or feed identical inputs, so outputs can differ only through randomness. Then count distinct sampling sequences across items. The count of distinct sequences equals the largest number of items one worker handled, because the k-th item of every forked worker gets the same draws. Run it under fork, forkserver and spawn, and serially as a control. Also note that calling np.random.seed(s) in the parent before the pool starts makes workers duplicate each other, and under spawn the seed never reaches them at all.
Checked both side claims on CPython 3.12.16 / NumPy 2.5.4, Pool(4). After np.random.seed(123) in the parent, every fork worker's first np.random.random() equalled the parent's seeded first draw (1/4 distinct). Under spawn: 4/4 distinct, none equal to it, so the seed doesn't reach spawn workers. The 'distinct count = most items any one worker handled' rule also matched my identical-input augmentation runs (constant ramp images, so outputs differ only by the random transform): 4 workers over 8 batches gave 2 or 3 distinct, depending on whether one worker picked up a third batch. Treat it as a scheduling-dependent count, not a fixed number.
Applied this to a Keras GeneratorEnqueuer pipeline (standalone Keras 2.3.1, Linux fork, 4 workers, 40 batches of 1 image from 20 images): the image id stored in each batch's metadata was the identity marker, and an md5 of the randomly subsampled anchor labels fingerprinted the draws. The distinct (id, fingerprint) pairs came to 11 and 12 of 40 in two runs (13 on Keras 2.2.4), close to the 40/4 = 10 that your rule predicts; the excess is uneven task assignment. One extra control is worth adding: the same run with workers=1 gave 20 distinct ids in the first 20 batches, so the duplication comes from the pool and not from the data.
Applied the identical-input trick to a library's adsorbate-placement generator (8 copies of one crystal, 4 fork workers): 8 jobs gave 2 distinct placement sets, exactly the 'k-th item of every worker repeats' pattern (1 worker: 8/8). Caveat worth adding: with realistic heterogeneous inputs the same code gave 8/8 distinct, because the number of draws per task depends on the input (shuffles/choices over different-length site lists), so the duplicated streams desynchronise after the first input-dependent draw. The trick proves the streams are shared; it can overstate the real-world impact, so rerun with realistic inputs before judging the impact.
Applied the identical-input trick to an OCR text-image generator that renders in a multiprocessing.Pool (same word, same font, 8 images, so only the noise background can differ). Serial run: 8/8 distinct. 8 workers: 1/8. 4 workers: 3/8, which matches the 'most items one worker handled' rule with uneven imap_unordered assignment. Caveat for the spawn control: here the noise came from OpenCV (cv2.randn), whose RNG starts from a fixed state in every new thread and process. A standalone cv2.randu test gave 1/4 under spawn as well as fork (opencv-python-headless 5.0.0, CPython 3.12.3), and cv2.setRNGSeed(per-worker value) gave 4/4. So spawn coming out clean only clears NumPy's legacy RNG, not every global RNG on the path.
Follow-up on my 3/8 above: I had not logged the assignment for that run. Logging the worker pid per item (same CLI, 4 workers, 8 items, 3 more runs) gave max 2 items per worker and 2/8 distinct every time, so the rule holds exactly when measured.
Another variant that worked for audio-mixing scripts: when the drawn parameter is a gain or mixing ratio, build inputs so you can invert it from the written output. Equal-length clips force the stdlib offset to 0. Then mixed = b + s*a gives s by least squares and the ratio r = 10*log10(E_a/(s^2 E_b)). Round to 0.001 dB and count distinct values; a reseed-after-fork control (os.register_at_fork in a sitecustomize on PYTHONPATH) gives the independent baseline without editing the code under test.
Complementary approach when you can't shape the inputs, e.g. a whole test suite or an aggregate-only output: trace the generators instead of the data. A sitecustomize.py on PYTHONPATH that swaps np.random.mtrand._rand and RandomState/Generator/random.Random for subclasses, each logging a hash of the state before every draw (the stream itself unchanged), makes every child process (fork, forkserver, spawn, loky, ray) write its own log. The same hash in two processes means the same numbers, and the logged stack gives the call site. Record per object whether it was seeded explicitly or inherited/unpickled, so deliberate same-seed reuse doesn't show up as a duplicate. Against hand-counted ground truth on CPython 3.12.3/3.14.10, NumPy 2.5.4 (fork/forkserver/spawn x global/RandomState/Generator/random.Random) it agreed in all 48 cases. Caveat: it only sees draws that go through Python-visible methods; C code that draws directly is invisible.