Skip to content

WASM: defer Numba gufunc compilation to first call (#930) - #957

Open
mmcky wants to merge 4 commits into
mainfrom
wasm/930-lazy-gufuncs
Open

mmcky wants to merge 4 commits into
mainfrom
wasm/930-lazy-gufuncs

Conversation

@mmcky

@mmcky mmcky commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Defers the four eagerly-compiled Numba gufuncs to first call, so they are no longer compiled during import quantecon. Part of #930, part of #925 (Phase 1).

This is @oyamad's implementation from the wasm-930-dynamic-gufuncs branch, opened as a PR in place of #943, which is closed. His commit is preserved as authored; the second commit carries two small adjustments described below.

What changes

Callable File Change
_probvec_parallel random/utilities.py explicit-signature gufunc built by a memoised factory on first call
_probvec_cpu random/utilities.py same
_sample_without_replacement random/utilities.py dynamic (signature-less) gufunc; caller supplies the output array
_ints_arr_to_bits game_theory/vertex_enumeration.py same

The explicit signature is kept for both _probvec targets deliberately, and the code now says why: the parallel target requires it, and an explicit-signature gufunc must not be mixed with a dynamic one of the same kernel under cache=True, because they share an incompatible cache entry (numba#10128). That constraint was previously undocumented.

No public API change. qe.random.probvec(..., parallel=...), qe.random.sample_without_replacement(...) and qe.game_theory.vertex_enumeration(...) are untouched.

The two adjustments

  • functools.cachefunctools.lru_cache(maxsize=None). The former is an alias for the latter, added in 3.9 for convenience only, so this is identical memoisation without a version floor.
  • The requires-python bump to >=3.9 is reverted. The floor is stale at >=3.7 while CI tests 3.12–3.14, but fixing that is DOC: repo-wide documentation consistency audit #864's business and does not belong in a WASM change.

Verification

  • Full suite: 678 passed
  • flake8 --select=F401,F405,E231 quantecon: clean
  • Deferral confirmed directly: after import quantecon, both _probvec factories report cache_info().currsize == 0; after one probvec call they are populated
  • Native warm-cache import quantecon, best of 7: 0.70 s on main → 0.65 s here

The measurement this still needs

#930's acceptance criterion 1 — warm-cache import quantecon timed in JupyterLite, before and after — is not satisfied by the native number above, and remains open. It matters more than it looks: emscripten-forge Numba patch 0007 forces cache=False for every @guvectorize, so in the browser these four compiles are still paid once per session either way. This change moves that cost from import to first call rather than removing it, which is a clear win only if most notebooks never reach probvec, sample_without_replacement or vertex_enumeration.

For that reason this PR says Part of #930, not "Closes". Either a manual JupyterLite timing settles criterion 1, or a maintainer waives it explicitly on #930, as that issue requires.

Not fixed here

#944 (RuntimeError: no compiled object yet when a cache-miss caller links a cache-restored callee, reported upstream as emscripten-forge/recipes#6309) is unaffected. This change narrows the exposure but does not resolve it, and should not be cited as doing so.

Part of #930. Replaces #943.

🤖 Generated with Claude Code

oyamad and others added 2 commits September 9, 2026 19:27
`import quantecon` compiled four `guvectorize` functions eagerly because
of their explicit type signatures. In the in-browser (Emscripten) Numba
build, where `cache=True` is disabled for gufuncs, that cost was paid on
every session on the import path. Defer all four to first call:

* `_ints_arr_to_bits` and `_sample_without_replacement` become dynamic
  (signature-less) gufuncs; the callers now supply the output array.

* `_probvec_parallel` and `_probvec_cpu` are constructed on first call
  by `functools.cache`d factories, keeping their explicit signatures.
  The parallel target requires one, and the cpu variant keeps one too:
  an explicit-signature gufunc and a dynamic gufunc of the same kernel
  share an incompatible cache entry when `cache=True`, which crashes
  the process.

Public API is unchanged. `functools.cache` requires Python 3.9, so
`requires-python` is raised from 3.7 to 3.9.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two adjustments to the cherry-picked implementation, neither changing
its behaviour.

`functools.cache` is an alias for `functools.lru_cache(maxsize=None)`
added in 3.9 purely for convenience, so using `lru_cache` directly gives
identical memoisation without imposing a version floor.

That removes the need for the `requires-python` bump to 3.9, which is
reverted here: the floor is stale at ">=3.7" while CI tests 3.12-3.14,
but correcting it is #864's business and does not belong in a WASM
change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The changes are localized, call sites appear consistently updated, and no correctness issues were found in the modified regions.

Pull request overview

Defers several Numba guvectorize compilations from import time to first call to reduce import quantecon overhead (especially relevant for WASM/JupyterLite where persistent ufunc caching is unavailable).

Changes:

  • Replaces module-level _probvec_parallel / _probvec_cpu gufunc objects with lazily-constructed, lru_cache-memoized factories.
  • Converts _sample_without_replacement and _ints_arr_to_bits to dynamic (signature-less) gufuncs, with callers supplying the output arrays.
  • Updates internal call sites to use the new factory / output-supplied calling patterns.
File summaries
File Description
quantecon/random/utilities.py Lazily constructs the _probvec_* gufuncs; makes _sample_without_replacement a dynamic gufunc with caller-supplied output.
quantecon/game_theory/vertex_enumeration.py Updates _ints_arr_to_bits to a dynamic gufunc and updates the caller to provide an output buffer.
quantecon/game_theory/random.py Updates _probvec_cpu usage to call the new memoized factory.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread quantecon/game_theory/random.py
@coveralls

coveralls commented Sep 9, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 93.976% (+0.009%) from 93.967% — wasm/930-lazy-gufuncs into main

oyamad and others added 2 commits September 10, 2026 10:54
Resolves two docstring conflicts with #864 by keeping this branch's
text: the `_sample_without_replacement` docstring (rewritten here;
already spells "compiled") and the `_ints_arr_to_bits` example (the
1-D-to-scalar form removed here no longer needs its repr updated).
`requires-python` takes main's ">=3.12".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@oyamad

oyamad commented Sep 10, 2026

Copy link
Copy Markdown
Member

@mmcky Thanks!
Current main merged into this branch where requires-python = ">=3.12", I replaced ``functools.lru_cache(maxsize=None)with the shorterfunctools.cache`.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants