fix: 64-bit randint could return high; correct the integer fills - #172
vchamarthi wants to merge 4 commits into
Conversation
|
@vlad-perevezentsev |
Looks separate |
| } | ||
| } | ||
|
|
||
| void irk_rand_uint64_vec(irk_state *state, |
There was a problem hiding this comment.
There is near-twin irk_discrete_uniform_long_vec (used by randint_untyped) keeps the old single-path rejection loop and the pre-fix VSL_RNG_METHOD_UNIFORM_STD constant.
It'd be nice to research on performance improvement there also in the follow-up PR.
Can you @vchamarthi work with @vlad-perevezentsev to fix the full-range scalar draw also for the other BRNGs? |
I will fix this in the next PR after merging this one |
There was a problem hiding this comment.
LGTM
Thank you @vchamarthi
Just a few minor points
| ) | ||
| def test_narrow_width_full_range_in_bounds(self, dtype): | ||
| # Narrow fills stage in tiles; cover the 4096 tile boundary and beyond. | ||
| hi = 2 if dtype == "bool" else int(np.iinfo(dtype).max) |
There was a problem hiding this comment.
| hi = 2 if dtype == "bool" else int(np.iinfo(dtype).max) | |
| hi = 2 if dtype == "bool" else int(np.iinfo(dtype).max + 1) |
to cover the full range
| # Ranges at or above INT_MAX take the masked branch, which | ||
| # test_in_bounds_fuzz never reaches (it only uses high <= 16). | ||
| for dtype in ("int64", "uint64"): | ||
| if low + high > np.iinfo(dtype).max: |
There was a problem hiding this comment.
| if low + high > np.iinfo(dtype).max: | |
| if high > np.iinfo(dtype).max + 1: |
| @@ -1808,18 +1809,7 @@ void irk_rand_uint8_vec(irk_state *state, | |||
| } | |||
|
|
|||
| assert(lo < hi); | |||
There was a problem hiding this comment.
looks identical in all five narrow fills so they can move into irk_rand_narrow_fill
note:
lo is an int there so the fill needs a cast
if (lo == hi) {
DIST_PRAGMA_VECTOR
for (i = 0; i < len; ++i)
res[i] = (T)lo;
return;
}
assert(lo < hi);There was a problem hiding this comment.
Then if (len < 1) above on 1800 line looks redundant because the constant fill and while (len > 0)already do nothing forlen <= 0`
There was a problem hiding this comment.
For irk_rand_bool_vec the remaining assert((lo == 0) && (hi == 1)) can either stay or go.
Once lo == hi is handled inside the helper assert(lo < hi) already implies it for npy_bool
If you keep it it has to become assert((lo == hi) || ((lo == 0) && (hi == 1))) because it is now reachable for lo == hi as well
What
Four fixes in the integer fills, all in
mkl_distributions.cpp.1.
randintcould returnhigh. In the masked branch of the 64-bit fill,rngwas incremented before the mask was derived from it:Fixed by leaving
rngas the inclusive maximum and usingrng + 1only whereviRngUniformwants an exclusive bound.The same surplus mask bit also forced roughly half of all draws to be rejected
and redrawn whenever
rng + 1was a power of two. With the mask correct,mask == rngmeans nothing can be rejected, so those requests now fill theresult buffer directly.
2. The integer fills handled at most two
MKL_INT_MAXchunks. They usedifwhere the other 43 chunking sites in the same file usewhile, so a largerrequest recursed once, fell through, and passed a count that overflows MKL's
32-bit
int.NDEBUGcompiles out theassert, so the error was swallowed andthe tail was never written.
3.
multinomialdecremented its chunk counter by elements, not draws(
len -= k * MKL_INT_MAX), skipping work and leaving the tail of large outputsunwritten.
4. The fills narrower than
intstaged the whole request.viRngUniformonly emits 32-bit integers, so a
uint8fill allocated four bytes per elementto produce one, then read that buffer back to narrow it: 40 MB allocated to
write 10 MB of output. Now staged one cache-resident tile at a time.
Numbers
Xeon Gold 6338, single thread, ns/element, min of 2 runs. Intel-channel
mkl/mkl-devel2026.1.0-intel_236, meson build. Baseline and patchedinstalled in separate conda envs.
uint64, power-of-two range 2**32uint64, power-of-two range 2**48uint64,iinfo.maxupper boundbool, 10Muint8, 10Muint16, 10Muint32, 10M (control)Streams change
Only power-of-two ranges at or above
INT_MAXchange, and only because the oldmask there was wrong. Of 12 seeded outputs compared, exactly one hash moved:
randint(0, 2**32, dtype='uint64'). The other 11 are bit-identical, includingevery narrow dtype,
uint32,standard_normalandmultinomial. Thenarrow-int tiling is bit-identical by construction: a VSL stream is sequential,
so tiling draws the same values in the same order.
Testing
master, so they are invariant guards rather than fitted to this change.
randint(0, 2**31), 8e9 draws perdtype: 3 hits on default
int, 3 onint64, 2 onuint64. After the fix, 0on all three.
6b7962f.Notes for reviewers
test_randint_in_bounds_fuzzonly useshighin{4, 8, 16}, all of whichtake the 32-bit path, so the masked 64-bit branch had no coverage. The new
test covers ranges at and above
INT_MAX.asserts the bound invariant rather than trying to reproduce it.
above two
MKL_INT_MAXchunks (~4.4 GB forbool, ~26 GB formultinomial). Worth a second opinion on whether thewhileconversion iscorrect in all eight fills.
irk_discrete_uniform_long_vecalready derived its mask from the inclusivemaximum, which is the pattern this restores in the 64-bit fill.