Skip to content

perf: hoist the randint dtype-bounds table out of the per-call path - #199

Open
intel-python-devops wants to merge 8 commits into
masterfrom
chore/agentic-sweep-21
Open

intel-python-devops wants to merge 8 commits into
masterfrom
chore/agentic-sweep-21

Conversation

@intel-python-devops

@intel-python-devops intel-python-devops commented Oct 19, 2026 •

Copy link
Copy Markdown

Note

This pull request is entirely AI-generated. Please review thoroughly.

mkl_random/mklrand.pyx: _choose_randint_type runs on every randint call. Most of its cost was per-call overhead, not generation:

  • it rebuilt the 9-entry _randint_bounds dict on every call. Cython already folds the 2**n constants and caches the tuples, so this was just the dict construction, ~0.1–0.2 µs;
  • it keyed the dict by np.dtype(dtype).name. That is a Python-level property in NumPy and costs ~1.1 µs, about 40% of a randint(0, 10) call.

The table is now a module-level constant keyed by the np.dtype object. A dtype hash lookup takes ~25 ns, and aliases such as long, longlong and intc resolve to the same entry. Each entry also stores the names of the _rand_* and _rand_*_broadcast methods, so a call is one dict lookup and two getattrs, with no f-strings. .name is now only used to build the TypeError message for unsupported dtypes. Generated values, dispatch and validation are unchanged.

master this PR
randint(0, 10) ~3.05 µs ~1.45 µs
randint(0, 10, size=1) ~5.2 µs ~3.5 µs

Large outputs (1–30 ms calls, scalar and array bounds, int64/int32/uint8/bool) show no measurable change, and for a fixed seed the output is identical to master. The gain is only in per-call overhead.

@vlad-perevezentsev

vlad-perevezentsev commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

I pushed a follow-up commit on top of the original change 88c3bc5.
It also keys the bounds table by the np.dtype object instead of dtype.name.
dtype.name is a Python-level property in NumPy (~1.1 µs) and was the main per-call cost. The dict rebuild was only ~0.25 µs, since Cython already folds the 2**n constants. With this, randint(0, 10) takes ~1.45 µs, down from ~3.05 µs on master. Large outputs are unaffected: no timing change, and for a fixed seed the output matches master.

I also shortened the comments and updated the description. The previous one said randint_untyped goes through _choose_randint_type, which it doesn't, and that 2**n was evaluated on every call, which it wasn't.

@antonwolfy antonwolfy added this to the 1.7.0 release milestone Oct 20, 2026
Comment thread mkl_random/mklrand.pyx Resolved
Comment thread mkl_random/mklrand.pyx Resolved
Comment thread mkl_random/mklrand.pyx Resolved
jharlow-intel and others added 4 commits October 28, 2026 12:31
…og entry

_choose_randint_type now reads both _rand_* method names from the table
instead of formatting them on every call.
The table holds method names, not methods, and is keyed by np.dtype
so that dtype aliases share an entry. The changelog entry now names the
user-visible effect instead of implementation details.
@antonwolfy antonwolfy changed the title perf: hoist the randint dtype-bounds table out of the per-call hot pat perf: hoist the randint dtype-bounds table out of the per-call path Oct 29, 2026

@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 looks good now.

I pushed d0ecacb on top. It rewords the comment above _randint_bounds: the table holds method names, and it is keyed by np.dtype so that aliases share an entry. It also makes the changelog entry describe the user-visible effect. I fixed the truncated PR title and two sentences in the description: the table stores method names, not the dtype name, and the dict rebuild is ~0.1–0.2 µs. The first commit's headline is still cut off ("...hot pat"), so please squash-merge, which uses the fixed title as the headline.

I rebuilt 583b424 and its base 2691015 and compared them (d0ecacb only touches a comment and the changelog):

  • the test suite passes on both (380 passed, 24 skipped);
  • about 2,270 seeded calls across all dtypes, sizes, full-range bounds, array bounds and brngs give identical output and generator state;
  • about 1,100 dtype-like inputs behave the same through every public randint, including aliases, dtypes with metadata and non-native byte order;
  • dtype hashing and equality agree on NumPy 1.26.4, 2.2.6 and 2.5.4, so the dtype-keyed lookup works on all supported versions.

Timings with NumPy 2.5.4, each build in its own process pinned to one P-core, 7 alternating rounds, median of each round's best time:

call base PR change
randint(0, 10) 1.92 µs 0.81 µs −58%
randint(0, 10, size=1) 3.22 µs 2.02 µs −37%
randint(0, 10, dtype=np.uint8) 1.90 µs 0.76 µs −60%
randint(0, 10, dtype='int32') 1.89 µs 0.71 µs −62%
array bounds, 3 elements 13.4 µs 11.9 µs −11%
0.5–30 ms calls (int64/int32/uint8/bool, scalar and array bounds) within noise (0.99–1.01×)

np.dtype(dt).name alone takes ~0.77 µs here, so it accounts for most of the saving, as the description says.

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.

4 participants