Skip to content

WASM: browser support for QuantEcon.py (Numba + JupyterLite) — tracking issue #925

Open
1 of 11 issues completed
Open

Description

@mmcky

Note

Updated 2026-09-09. Revised after an adversarial re-check of every claim in this body against live state. Since 2026-08-31: PR #937 merged, so #926 is closed and Gap 1 is resolved; PR #947 removed the sympy dependency; #950 (WASM testbed site) was opened and linked as a sub-issue on 09-07 and is now carried in the plan; and item 1 of #931 was falsified — network calls work in the browser (@oyamad, 2026-09-06), so PR #948 needs rework. Two claims in the previous revision are corrected below (coverage is not packaged for WASM; the dependency list). Every row in the Gaps and Work plan tables now carries an evidence marker. The revision log is in the comments.

Evidence convention

The failure in #931 was a claim about browser behaviour written from first principles rather than from a run, and it reached a merge-ready PR before anyone tested it. To make that class of error visible, every claim below carries one of three markers. Add the marker whenever you add a claim, and cite the run or the file.

Marker Meaning
[browser] Observed in a real xeus-python kernel; the comment or run is linked
[source] Verified by reading the code, a patch, or a recipe in this repo or upstream
[inferred] Reasoned from platform properties, not observed. Treat as a hypothesis, and do not build a PR on it without a browser check

Background

On 11 August 2026 QuantStack announced Numba in the Browser (Anutosh Bhat): a genuine Numba JIT running entirely in the browser. Python functions are compiled through llvmlite to WebAssembly, linked in-process with LLD, and loaded as Emscripten side modules into a JupyterLite kernel — no server anywhere. Numba is now published on emscripten-forge (numba 0.67.0 build 1 + 10 emscripten patches; llvmlite 0.49.0 + 4 patches), and the announcement showcases the economics stack built on top of it (EconForge's interpolation.py and Dolo.py, PyMC/PyTensor). QuantEcon.py is the obvious next citizen of that ecosystem, and its authors explicitly invite the wider Numba ecosystem to validate their packages.

Concretely, "browser support" for QuantEcon.py means: works in JupyterLite with the xeus-python kernel against the emscripten-forge + conda-forge channels. This is not the classic Pyodide kernel — Numba remains unavailable there (see numba/numba#3284, pyodide/pyodide-recipes#192).

Where we stand (verified 2026-10-05)

Re-verified 2026-10-05: the project stays active, and two items wait on upstream. Every row of the table below was re-checked against live state; rows that changed are re-dated, and the others stand as written on 2026-09-09. What moved: the import-time compilation fix landed (PR #957, merged 2026-09-23, @oyamad's implementation) and shipped in v0.12.0 on 2026-09-24, so the four gufuncs in Gap 3 no longer compile during import quantecon; #930 stays open because the browser import time is still unmeasured, and the Gap 3 and #930 rows further down predate this. What has not: the testbed site (#950), the lead item, has not started, and no browser run has yet come from this repository (the WASM smoke suite workflow runs the suite natively; its browser job is #933). The two upstream waits: #944 waits on emscripten-forge/recipes#6309, which has had no maintainer reply since 2026-08-20; #927 waits on an upstream fix for the jitted-generator hang, and the report that would ask for it is still a draft in the #927 thread, so the first move there is ours. Everything else is unblocked: #950 first, then the browser checks that #929 (PR #952) and #930 are held for.

Fact Value Evidence
main bcc18b5; twelve commits since the last revision, one of them WASM work (#957, as 79360d6). Released as v0.12.0 on 2026-09-24 [source] GitHub compare 0c7c816...main, 2026-10-05
Sub-issues 11 linked, 1 closed (#926 via PR #937); unchanged since 2026-09-09 [source] GitHub, 2026-10-05
Open PRs #948 (#931, now conflicts with main, needs rework — its premise is falsified); #952 (#929, CLEAN, premise unverified in browser) [source] GitHub, 2026-10-05
Closed PRs #957 merged 2026-09-23 (part of #930, which stays open); #943 closed unmerged 2026-09-10, replaced by #957; #937 merged 2026-09-09 (closed #926); #938 merged; #935, #942 closed unmerged [source] GitHub, 2026-10-05
Runtime dependencies Four: numba, numpy, requests, scipy. sympy removed by PR #947 (2026-09-09) [source] pyproject.toml @ bcc18b5
Branches in this repo wasm-930-dynamic-gufuncs (7c512ed, oyamad, 2026-08-23); its implementation landed as PR #957, so the branch can go [source] GitHub, 2026-10-05
emscripten-forge numba 0.67.0 build 1, 10 patches; recipe unchanged since 9a2db20 (2026-08-18), re-checked 2026-10-05 [source] recipe history, 2026-10-05
emscripten-forge/recipes#6309 (#944) open; no maintainer reply since oyamad's repro on 2026-08-20 [source] GitHub, 2026-10-05
Jitted-generator hang (#927) root cause confirmed by symbol probe and a monkeypatch that makes the generator return; still no upstream report filed [browser] #927 thread, 2026-08-19; no upstream report found on 2026-10-05 [source]
numba/numba#10128 open; the candidate fix at numba/numba#10545 is open and unmerged [source] GitHub, 2026-10-05
Browser runs from this repo's CI still none. All browser evidence comes from the public numba-ecosystem demo, run by hand [source] #928 results table
#928 evidence table 5 of 14 rows have browser evidence; 9 remain "not yet run" [source] #928, 2026-09-09

The starting position is strong. QuantEcon.py is pure Python (noarch on conda-forge, universal wheel), every runtime dependency is already resolvable for the browser, and all ~90 jitted callables are nopython-mode with lazy specialisation — exactly the model the WASM engine implements. import quantecon already works in the xeus-python kernel, _numba_linalg_solve / np.linalg.solve inside @njit return correct results there, and the requests / urllib network paths work too [browser].

The library's widespread cache=True discipline should become an asset under JupyterLite's persistent cross-session compilation cache (patch 0006), but until emscripten-forge/recipes#6309 is resolved it is a liability: a cache=True function compiled in a session where a cache=True callee was restored from a previous session crashes with RuntimeError: no compiled object yet (#944). This affects every cached call chain (simplex_grid, the pivoting-based solvers including lemke_howson), not just comb_jit.

The constraints of the in-browser Numba build, read from the ten emscripten-forge patches and the recipe's tests — all [source]:

Gaps

# Gap Evidence Severity
1 gini_coefficient uses @njit(parallel=True) + prange Resolved. PR #937 merged 2026-09-09 replaced it with the O(n log n) sorted closed form; #926 is closed. The browser behaviour of the new implementation has not been observed [source] Closed
2 support_enumeration and vertex_enumeration hang because jitted generator functions hang on the emscripten-forge build: numba requests the symbol numba_make_generator but patch 0002 exports only Numba_make_generator. Confirmed by a ctypes symbol probe and by a PythonAPI._get_function redirect that makes the generator return [0, 1, 2]. vertex_enumeration is expected to fail by the same mechanism but has not been observed [browser] for support_enumeration and the root cause; [inferred] for vertex_enumeration Blocked on upstream
3 import quantecon eagerly compiles four guvectorize ufuncs (_probvec_parallel, _probvec_cpu, _sample_without_replacement, _ints_arr_to_bits) plus one eagerly-signed @jit, comb_jit, carrying types.intp(types.intp, types.intp). ufunc caching is disabled on Emscripten (patch 0007) so the four gufuncs recompile every session. Native cost measured 2026-09-09: 1.85 s cold vs 0.65 s warm, so ~1.2 s is eager compilation. Browser cost still unmeasured; test_import_time in the smoke suite is the instrument [source] inventory; native timing measured; browser unmeasured UX / performance
4 32-bit intp on wasm32: the comb_jit / simplex_grid overflow guard trips at 2³¹−1 instead of 2⁶³−1. Correct by design (the value is an array size); simplex_index is pure Python and cannot wrap, and the tests are already intp-keyed. Only the docstrings need to state the platform-dependent boundary (#929) [inferred] — one line in a kernel (np.iinfo(np.intp).max) settles it, and PR #952 should not merge before that Documentation
5 A cache=True function compiled in a session where a cache=True callee was restored from the persistent cache fails with RuntimeError: no compiled object yet (patch 0006, emscripten-forge/recipes#6309). Deterministic for simplex_grid via num_compositions_jit → comb_jit; also exposes k_array_rank_jit → comb_jit and the _pivoting / _lex_min_ratio_test callers (lemke_howson, linprog_simplex, lcp_lemke). Only reproduces on a warm cache [browser] #944 Blocked on upstream
6 New. quantecon/util/array.py runs with objmode(): inside @jit(nopython=True) to emit a DeprecationWarning from the public searchsorted helper. objmode lifts back into the CPython interpreter from compiled code and requires the GIL; none of the ten emscripten patches mentions it. Previously filed under "what does not need to change", which was too generous — the "no object-mode @jit" claim is true of the decorator but skips the context manager [inferred] — row 14 of #928's table, "not yet run" Unknown until run
7 Withdrawn. Network utilities were listed as failing in the browser because WASM has no sockets. This is false. GAMReader.from_url and requests.get both work: urllib3 has shipped native Emscripten support since 2.2.0 and enables it automatically when sys.platform == "emscripten". See #931 and PR #948 [browser] @oyamad, 2026-09-06 Not a gap

Work plan

Phase Issue Work item Status (2026-09-09)
0 — Prove it #928 JupyterLite proof-of-concept deployment + browser smoke suite Config, ci/wasm/smoke_test.py and the Playwright harness on main (PR #938). Results table has 5 of 14 rows filled, all from the public demo. Nothing has run in the browser from this repo
0 #950 Priority raised 2026-09-09 — this is now the lead item of the campaign. WASM testbed site under the QuantEcon org: a JupyterLite site on Pages whose environment can include locally patched packages, with the smoke notebooks preloaded Not started. Two jobs, and the second is why it moved up: it replaces "paste cells into someone else's demo" as our evidence source, and it is the bench on which we design and test the patches we contribute upstream. A C-level fix cannot be validated by monkeypatching a live kernel — it needs a rebuilt package running in a browser, which is exactly what #950 provides
1 — Code fixes #926 Replace the O(n²) parallel loop in gini_coefficient with the O(n log n) sorted closed form Done. PR #937 merged 2026-09-09; #926 closed. Note the smoke suite's test_gini_coefficient still carries a strict=True xfail for Emscripten that is now wrong on two counts — see #933
1 #927 Upstream the jitted-generator hang with a minimal repro; decide whether support_enumeration / vertex_enumeration need Emscripten-gated non-generator paths Blocked on upstream. The draft report in the #927 thread is still unfiled after 19 days, and anutosh491's offer to take it is still unanswered. The repro has not been re-run on 0.67.0 build 1
1 #929 Document the platform-dependent intp overflow boundary (docs only) PR #952 open and CLEAN. Hold for a one-line browser check of np.iinfo(np.intp).max before merging — the claim is very likely right, which is exactly why confirming it is cheap
1 #930 Reduce import-time eager Numba compilation PR #943 CLEAN, unreviewed since 2026-08-22. oyamad's alternative on wasm-930-dynamic-gufuncs still has no PR. Decision needed on which implementation goes forward. Native timing now recorded (Gap 3); the browser gate is still open
1 #931 Document browser limitations; raise helpful platform-specific errors Item 1 falsified — see Gap 7. PR #948 implements guards that would remove working functionality and must be cut back to the probvec docstring note (item 2), which is correct and should land. Item 3 (demote requests) is dependency hygiene with no WASM rationale and is entangled with the separate question of deprecating util/notebooks.py in 0.12.0 — move it out of this tracker. Item 4 (docs page) stands
1 #944 cache=True caller linking a cache-restored callee fails with "no compiled object yet" Blocked on emscripten-forge/recipes#6309, no maintainer response in 20 days. Decision stands: wait for upstream, no library-side change
2 — Distribution #932 Contribute a quantecon recipe to emscripten-forge No recipe yet; quantecon already installs from conda-forge noarch. Justify or drop: the package is pure Python with no build step, so state what the recipe buys (solve speed, pinned coverage, visibility) before spending effort on it
3 — CI #933 Add a WebAssembly/JupyterLite job to CI Native job on main (PR #938); browser runner PR not started. Tasks 1–6 in the body, plus two new ones: the gini strict=True xfail will hard-fail the first browser run, and coverage is not packaged for WASM
4 — Ecosystem #934 Demo notebook, lectures without a kernel server, WASM SIMD exploration After the intermediate release and #932. The JAX-on-emscripten funding conversation with QuantStack is tracked separately — see "Explicitly not doing"

Sequencing. #950 leads. Two arguments put it at the front. The first is defensive: reasoning is currently cheaper than testing, which is what produced #931 — five issues that depend on browser evidence were written while the harness sat unbuilt, two became PRs, and one rested on a false premise. The second is the stronger one. Two of this campaign's five gaps are blocked on upstream fixes we have already diagnosed but cannot build or test (#927, #944). Being able to compile a patched numba and exercise it in a real kernel turns us from a reporter of upstream bugs into a contributor of upstream fixes — with browser evidence attached to the PR. That is a different relationship with emscripten-forge, and it is worth more than any single item further down this plan. #933 remains complementary rather than an alternative: it gives regression CI against the stock stack, while #950 gives the interactive, deep-linkable kernel and the patched-build bench. Both before more documentation written from inference.

Front of the plan (next session):

  1. Build WASM: Host a WASM testbed site under the QuantEcon org #950. The lead item. It converts the 9 "not yet run" rows into evidence, removes the dependency on a third-party demo, and — the reason it outranks everything else — gives us a bench on which to design and test upstream patches. The first candidate is ready: WASM: support_enumeration and vertex_enumeration hang in the browser — root cause is jitted generators (numba.np.linalg._LAPACK verified working) #927's generator alias. A source audit on 2026-09-09 confirms the fix is a single alias and that it is complete — _dynfunc.c_helpers contains exactly one entry, make_generator, and numba/core/base.py registers every helper from both helper dicts as "numba_" + key, so Numba_make_generator is the only helper whose registered name differs in case from its C symbol (_helperlib's 158 are already lowercase in C; NRT's 25 register as NRT_<name> and match). Once WASM: Host a WASM testbed site under the QuantEcon org #950 exists that patch can be built, run, and sent upstream with evidence rather than offered as a diagnosis.
  2. Rework PR WASM: raise OSError for network calls on Emscripten (#931) #948 to the probvec docstring only, and correct item 1 of WASM: document browser limitations and raise helpful platform-specific errors #931 so the next person is not sent down the same path.
  3. Fix the gini strict=True xfail in ci/wasm/smoke_test.py before WASM: add a WebAssembly/JupyterLite smoke-test job to CI #933's runner executes, or the first correct browser run reports red.
  4. File the jitted-generator report upstream (WASM: support_enumeration and vertex_enumeration hang in the browser — root cause is jitted generators (numba.np.linalg._LAPACK verified working) #927) — take anutosh491's offer or file from the draft; re-run the repro on 0.67.0 build 1 first.
  5. Decide the WASM: reduce import-time eager Numba compilation #930 implementation — PR WASM: defer eager Numba gufunc/jit compilation to first use (#930) #943 as rebased, or a PR from wasm-930-dynamic-gufuncs. Both satisfy the Race condition in GUFunc caching can lead to segmentation faults numba/numba#10128 constraint. Then either record the browser import timing or waive the gate explicitly.

Explicitly not doing

What does not need to change

No C extensions, no build step, no subprocess/multiprocessing/threading anywhere in the runtime package [source], no @jitclass, no generated_jit, no threading-layer APIs. All four runtime dependencies resolve in the browser ecosystem today. One correction to the previous revision: the test tooling is not entirely packaged for WASM — pytest and pandas are emscripten-forge recipes, but coverage is not, and conda-forge's coverage is not noarch because it builds a C tracer per platform [source]. #933 should plan for the pure-Python tracer or accept no coverage from the browser job.

Sources

Activity

  1. anutosh491 commented on Aug 13, 2026

    @anutosh491

    Hi @mmcky ,

    Author of the blog mentioned here :)

    Feel free to reach out to the me or the emscripten-forge team at QuantStack in case you would like to collaborate on some of the packaging work here @SylvainCorlay @MMesch !

  2. mmcky commented on Aug 13, 2026

    @mmcky
    ContributorAuthor

    @anutosh491 amazing work! Great blog post -- and well done to the whole team behind this. Thanks @SylvainCorlay for reposting the blog post on LinkedIn too. It would be great to link up with the team and learn more. Thanks for saying 👋

  3. SylvainCorlay commented on Aug 13, 2026

    @SylvainCorlay

    It would be great to link up with the team and learn more. Thanks for saying 👋

    We'd be happy to!

  4. 5 remaining items

  5. mmcky commented on Aug 21, 2026

    @mmcky
    ContributorAuthor

    @anutosh491 thank you — and thank you for patch 0010 (emscripten-forge/recipes#6293); we see numba 0.67.0 build 1 on emscripten-forge-4x. To answer the question properly: gini_coefficient is our only @njit(parallel=True) user so this will be useful.

    I see two blockers so far for us upstream:

    Your help on this is amazing! Thank you. Any feedback is welcome and we can work on workarounds locally if needed.

  6. mmcky commented on Aug 21, 2026

    @mmcky
    ContributorAuthor

    Correction to the release-sequencing note above (2026-08-19), now that the Phase 1 sub-issues have settled: the release-gating code changes are #926 (PR #937, approved, awaiting merge) and #930 (PR #943, after a rebase onto main that drops #942's int64 commit and keeps comb_jit as intp). #929 and #931 are documentation. #927 (jitted-generator hang) and #944 (see emscripten-forge/recipes#6309) are blocked on upstream and must not gate the intermediate release — they stay in milestone 8 but are scoped out of the release checklist, so the release fires when #926 and #930 close, not when every Phase 1 issue does.

  7. pinned this issue on Aug 21, 2026
  8. anutosh491 commented on Aug 27, 2026

    @anutosh491

    Hey @mmcky ,

    I just got jax jit working in Jupyterlite yesterday

    Screen.Recording.2026-08-25.at.2.42.24.PM.mp4
    Screen.Recording.2026-08-25.at.9.35.27.PM.mp4

    Is this something QuantEcon would like to make use of ?

    if yes, I would like to know if there might be interest from y'all to fund pacakging jax on emscripten-forge !?
    There are quite some patches to maintain here and this would be even more challenging that our numba recipe. Just thought of asking if this is something that interests y'all.

  9. mmcky commented on Aug 27, 2026

    @mmcky
    ContributorAuthor

    @anutosh491 WOW! That's amazing! Yes absolutely something we would like to use :-).

    How does this magic work?!

    I imagine this is a cpu link (rather than gpu passthrough). But that would unlock a lot for teaching jax on our jax.quantecon.org series. I'd love to run some benchmarks on this.

  10. mmcky commented on Aug 27, 2026

    @mmcky
    ContributorAuthor

    if yes, I would like to know if there might be interest from y'all to fund pacakging jax on emscripten-forge !?

    Let's link up and discuss this possibility. I'm our only full time technical (programming + CS tasks) resource but we have a network of RAs I could work with.

  11. anutosh491 commented on Aug 27, 2026

    @anutosh491

    Hey Matt,

    Thanks a lot for the email. Waiting for Sylvain to suggest a suitable time there.

    Now that you've highlighted benchmarks, people have already started playing with my jax build and have told me what works and what doesn't. So I do have an idea of what things to improve for jax :)

    See comments :
    pyodide/pyodide-recipes#187 (comment)
    pyodide/pyodide-recipes#187 (comment)

  12. mmcky commented on Aug 31, 2026

    @mmcky
    ContributorAuthor

    Revision log, 2026-08-31 (body re-stamped verified 2026-08-31 14:53 AEST; previous stamp 2026-08-21). This is a trace sweep across the sub-issues, PRs, main, emscripten-forge and numba since 2026-08-21 — no code or issue work was done in the session that wrote it, so everything below is attributed to whoever's trace it is.

    What happened since 2026-08-21

    What the body now says differently

    Resume pointer — in order: (1) merge PR #937, which closes #926; (2) decide the #930 implementation (PR #943 vs a PR from wasm-930-dynamic-gufuncs) and either record the import timing or waive that gate; (3) file the jitted-generator repro on emscripten-forge/recipes — take anutosh491's offer or file from the #927 draft, after re-running it on 0.67.0 build 1; (4) open the #933 runner PR from the parked job. Sub-issue progress is 0/10; the dashboard reads this tracker as untyped because it has no Project issue type.

  13. added theissue type on Aug 31, 2026
  14. mmcky commented on Sep 9, 2026

    @mmcky
    ContributorAuthor

    Revision log — 2026-09-09. The body above was rewritten after an adversarial re-check of every claim in it against live state. The previous text is in the edit history. What changed, and why.

    A claim was falsified. #931's first item held that the network utilities cannot work in the browser because WASM has no sockets. @oyamad demonstrated on 2026-09-06 that GAMReader.from_url and requests.get both work in xeus-python. urllib3 has shipped native Emscripten support since 2.2.0 and auto-enables it when sys.platform == "emscripten"; requests inherits it; emscripten-forge retired requests-wasm-polyfill as redundant. That claim reached a merge-ready PR (#948) before anyone tested it, and the guard it produced fires on precisely the condition that enables the browser transport. It is now Gap 7, marked withdrawn, and #931 is corrected.

    An evidence convention was added, because the above is a class of error rather than a one-off. Every claim now carries [browser], [source] or [inferred], with the run or the file cited. #927 was already doing this — its "by mechanism; not yet observed" note on vertex_enumeration is the model, and #927 is correspondingly the one item nobody has had to re-litigate.

    Three corrections to the previous revision. coverage is not packaged for WASM (not among emscripten-forge's 639 recipes; conda-forge's is not noarch because it builds a C tracer), so the "test tooling is packaged for WASM" claim is wrong for it. The dependency list is now four, not five — PR #947 removed sympy. And objmode was filed under "what does not need to change" on the strength of "no object-mode @jit"; that is true of the decorator but skips with objmode(): inside a nopython jit in quantecon/util/array.py, which is now Gap 6.

    Two things measured or resolved. Gap 1 is closed — PR #937 merged, so #926 is closed. And #930's import-time gate, unrecorded since 13 August, now has a native reference point: import quantecon at 0c7c816 takes 1.85 s cold against 0.65 s with the Numba on-disk cache warm, so about 1.2 s is eager compilation. The inventory is exactly as claimed — four gufuncs plus the eagerly-signed comb_jit. Native seconds are not browser seconds; this is a lower bound that confirms the item is real, not the gate itself.

    #950 joins the plan. It has been a linked sub-issue since 2026-09-07, but the body and the work plan never absorbed it. It matters more than its age suggests: it is the only way to exercise a patched numba build, which is what #927's candidate generator alias needs, since a C-level fix cannot be validated by monkeypatching a live kernel.

    Sequencing changed. #950 and #933 move ahead of the documentation work. They are complementary — #933 gives regression CI on the stock stack, #950 gives a deep-linkable kernel where a claim is checkable in seconds. The reason to prioritise them is structural: reasoning is currently cheaper than testing, #928's evidence table still has 9 of 14 rows marked "not yet run", and no browser run has ever come from this repository's CI. Five issues that depend on browser evidence were written in the meantime and two became PRs. Notes have gone to #931 and #933 with the specifics, including a strict=True xfail on gini that will hard-fail the first browser run.

  15. mmcky commented on Oct 4, 2026

    @mmcky
    ContributorAuthor

    Revision log — 2026-10-05. The Where we stand section was re-verified row by row against live state and re-dated; nothing else in the body changed. Recorded: PR #957 merged on 2026-09-23 and shipped in v0.12.0, #943 closed in its favour, and #948 now conflicts with main. The section now names the two upstream waits (#944 on emscripten-forge/recipes#6309, and #927, whose upstream report is still unfiled) and says that #950 remains the lead item, not started. The Gap 3 and #930 work-plan rows predate #957 and are flagged as such rather than rewritten.

  16. mmcky commented on Oct 17, 2026

    @mmcky
    ContributorAuthor

    For the record: QuantEcon/QuantEcon.py-wasm#13 notes that quantecon_wasm still carries copies of the LQMarkov and ARMA bugs fixed in v0.13.1 (#983, #976), and asks whether to port the fixes there or retire that package in favour of the browser support tracked here.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions