Skip to content

Stop passing degenerate parameters to the MKL discrete generators - #195

Open
vlad-perevezentsev wants to merge 10 commits into
masterfrom
fix/degenerate-params-uninitialized-output
Open

vlad-perevezentsev wants to merge 10 commits into
masterfrom
fix/degenerate-params-uninitialized-output

Conversation

@vlad-perevezentsev

Copy link
Copy Markdown
Collaborator

MKL's discrete generators reject degenerate parameter values that NumPy accepts. mkl_random passed them through and only checked the status code with an assert, which meson.build disables via -DNDEBUG, so the
untouched output buffer was handed back as random values.

Fixed

  • poisson with a scalar lam == 0, and with array-valued lam and method="PTPE", returned uninitialized memory; it now returns zeros, as the default array path and NumPy already did.
  • negative_binomial with p == 1 returned uninitialized memory; it now returns zeros, as NumPy does.
  • geometric with p being NaN returned uninitialized memory.

Changed

  • negative_binomial now rejects p == 0, which MKL does not support, with
    a ValueError; the documented range of p becomes (0, 1].
  • poisson, negative_binomial, geometric and logseries now reject
    NaN parameters with a ValueError, as NumPy does.

Notes

  • The guards fill zeros without drawing from the stream, so results for valid parameters are unchanged and the existing regression tests pass untouched.
  • Pre-existing, found while checking review feedback on Document MKLRandomState in the API reference and align docstrings with NumPy #165.
  • Not here: geometric is shifted by one relative to NumPy for every p (MKL counts failures, NumPy counts trials), which is a breaking change and gets its own PR.

viRngPoisson requires lam > 0, so lam == 0 made it fail with "Parameter 5
was incorrect on entry to viRngPoisson" and leave the output buffer
untouched, which was then handed back as random values. The assert on the
MKL status code cannot catch that: meson.build defines NDEBUG, so every
assert in mkl_distributions.cpp is a no-op in shipped builds. The scalar
path hit this directly, and so did array-valued lam with method="PTPE",
which calls the scalar kernel per element; only the default POISNORM
array path was correct, since viRngPoissonV accepts a zero lambda.

The distribution is degenerate at lam == 0, so the kernels now fill zeros
without touching the stream, which is what the array path and NumPy
already returned. Streams for a positive lam are unaffected.

NaN lam passed validation as well, because `lam < 0` is false for NaN,
and MKL then returned arbitrary values. It now raises ValueError with
NumPy's wording. The isnan check runs first, as the comparison ufuncs
warn on NaN input.
viRngNegbinomial requires 0 < p < 1, and both endpoints reached it. p == 1
only reported "INTERNAL ERROR: Condition -6", and p == 0 passed a check
that tested `p < 0`, so in both cases the untouched output buffer was
handed back as random values, on the scalar and on the array path alike.
The assert on the MKL status code is a no-op, as meson.build defines
NDEBUG.

p == 1 means no trial ever fails, so the number of failures is zero and
the kernel now fills zeros without touching the stream, as NumPy returns.
p == 0 has no finite answer, NumPy's own Generator rejects it, and it now
raises ValueError, which makes the documented range of p (0, 1]. NaN was
unchecked for both n and p, so it is rejected together with them.
Neither validated NaN: `p <= 0.0`, `p > 1.0` and `p >= 1.0` are all false
for NaN, so it reached the kernels. geometric then wrote nothing and
handed back uninitialized memory as random values, since viRngGeometric
is only called for 0 < p < 1; logseries silently returned ones. NumPy
raises ValueError for both.

The checks are merged into one condition per path, with isnan first, as
the comparison ufuncs warn on NaN input.

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is a nice catch! The scalar and array checks now agree with each other and with NumPy at the boundaries, and the C guards follow the existing p == 1 handling in irk_geometric_vec, so valid-parameter streams are untouched. A couple of small things inline.

  • Not for this PR, but while you're in this area: binomial with array p still lets NaN through to viRngBinomial, since only the scalar path checks isnan. noncentral_chisquare(0.5, 0) calls viRngPoisson(PTPE, ..., lambda=0) directly in irk_noncentral_chisquare_vec, bypassing the new guard. Worth a follow-up issue?

Comment thread CHANGELOG.md Outdated
Comment thread mkl_random/tests/test_random.py Outdated
Per review: with lam = 5 for the middle element, the assertion on the zero
elements would also hold if viRngPoissonV had rejected the whole call and
left the zero-filled output buffer untouched, which is exactly the failure
mode this test is about. A lam of 1000 cannot produce a zero, so the
element now proves the call went through.

That viRngPoissonV accepts a zero lambda was checked directly, not assumed:
poisson([0, 1000, 0]) returns [0, 1005, 0] with nothing on stderr, while
the scalar and PTPE paths printed "Parameter 5 was incorrect on entry to
viRngPoisson" before this branch.
Per review: gh-193 is the unrelated set_state buffer-length issue. The
entries were written before the PR existed and kept the guessed number.
Comment thread mkl_random/tests/test_random.py Outdated
Per review: both RandomState and Generator document p in [0, 1) and return
1 there, so rejecting it was a visible difference when mkl_random is
patched into numpy.random. The sampler needs no special case, as noted in
the review: with p == 0, r = log(1 - p) is zero and the acceptance test
V >= p always holds, so irk_logseries_vec already fills ones. Checked:
logseries(0.0, 5), logseries(np.zeros(5)) and a mixed array all return ones
where p is zero, matching NumPy.

The docstring prose said 0 < p < 1 and now matches the code.
negative_binomial keeps rejecting p == 0: MKL has no such distribution
there and the number of failures has no finite answer.
@antonwolfy antonwolfy added this to the 1.6.0 release milestone Oct 16, 2026
Same point as the review on gh-196: assert_array_equal ignores dtypes by
default, so the int32 expectations were not actually checked and an int64
or float result would have passed. strict=True also pins the shape and
needs NumPy 1.24, below the 1.26.4 minimum.
vlad-perevezentsev added a commit that referenced this pull request Oct 16, 2026
gh-194 is the unrelated np.long issue. The entries were written before the
PR existed and kept the guessed number, the same slip as in gh-195.
Comment thread mkl_random/tests/test_random.py Outdated
Comment thread mkl_random/mklrand.pyx
Per review: vec_Poisson_array allocates the output with PyArray_SimpleNew,
and the scalar path with np.empty, so a rejected call leaves whatever was
in memory rather than zeros. The check itself is unaffected.
Per review, older than this PR but in the lines being touched: the
Parameters block described loc and scale, which the function does not take,
and never mentioned p, while Returns claimed the values lie in [0, n]. The
support is the positive integers.

gh-165 rewrites this block as well, so that PR keeps the final wording; the
range there needs to read [0, 1) after this one.

@antonwolfy antonwolfy left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for turning those around so quickly!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants