Numba: back-port compilation speedup for large tuple/ number of arguments - #2361
Conversation
4928dca to
a38edaa
Compare
|
Adversarial regression:
import numba
import pytensor.link.numba.dispatch # noqa: F401 -- installs the peephole patch
N = 40 # CPython emits CALL_FUNCTION_EX once a call has >30 positional argsparams = ", ".join(f"a{i}" for i in range(N))
ns = {}
exec(
f"def callee({params}, k):\n"
f" return a0 + k\n"
f"def caller({params}):\n"
f" return jitted_callee({params}, k=1.0)\n",
ns,
)
ns["jitted_callee"] = numba.njit(ns["callee"])
print(numba.njit(ns["caller"])(*[float(i) for i in range(N)])) # expected: 1.0You need to put the patch in the right location or smth We also need to see if this patch is compatible with all numba versions in our supported range. |
a38edaa to
9da1e5b
Compare
|
Confirmed, thanks — the collapse left the tuple behind a single-use Var alias, and the CALL_FUNCTION_EX peephole only accepts a vararg defined by a On version range: verified standalone against numba 0.58.1 (py3.11), 0.61.2, 0.65.1 and 0.66.0 — i.e. both ends and the middle of our On the wariness: agreed it's not free — this follows the same import-side-effect mold as |
|
pointer add is already merged upstream so it's a backport mostly. The unique_ids they acknowledge it's a limitation but are still looking for how to tackle. Let's give them a few days to review your PR over there to see if they don't have any qualms over the approach. Maybe there's a less intrusive approach. Your original one of changing our codegen was more limited but less intrusive. Although it touched other backends as they share the logic (easy to fix that though) Otherwise the gains make sense. |
d2ee9e2 to
9da1e5b
Compare
|
Dropped the second commit (inlining the collapsed tuple into direct call arguments). Measured on the n=80 repro it is worth 0.2% — 324.2 / 323.6 s without it vs 322.7 / 323.4 s with it — so it was carrying risk for no gain, including the corresponding semantic change upstream. What is left is the collapse itself plus the alias forwarding that fixes your kwargs repro, which is the part that actually matters. Same drop applied to numba/numba#10782, where it also removes the only behaviour change and the edit to numba's own test suite. |
|
@ricardoV94 numba PR seems to have maintainer blessing now: |
9da1e5b to
caffde1
Compare
|
Bot summary of my nudging: Could we use a faithful backport of the merged Numba fix here? Copying the upstream peephole implementation and monkey-patching it should be straightforward, subject to checking compatibility with our supported Numba versions. This custom pass has broader scope and still leaves quadratic tuple chains after starred unpacking. For temporary code, I would prefer the upstream implementation with a couple of focused regression tests, removed together with the patch. |
caffde1 to
fbd361a
Compare
|
Done — this is now a faithful backport rather than a custom pass. Your two technical objections both go away with the upstream implementation: it coalesces runs of appends around On the version question: the patch installs only when the running numba predates the fix, detected by looking for the coalescing in Also rebased onto main — the earlier red CI was numpy 2.5 drift fixed by #2376, unrelated to this patch. |
|
Can we use numba versioning check to include or not the patch, instead of checking the code. we know that if larger than today's release it isn't needed, before or equal yes? we should likely do the same to pointer_add (gotta check when was it released). |
CPython emits BUILD_LIST + LIST_APPEND per item + LIST_TO_TUPLE for any call or tuple display past 30 items (STACK_USE_GUIDELINE). numba turned each appended item into a one-element tuple joined to an accumulator by a binary add, so the IR held a tuple of every prefix length and both it and the LLVM lowered from it were quadratic in the item count — generated fgraph functions hit this for every wide node. peep_hole_list_to_tuple is copied verbatim from numba master (merged 2026-08-27) rather than reimplemented, so the behaviour matches what numba will ship, including runs of appends around starred unpacking. It is installed only on numba < 0.68, where the fix ships, and the module is deleted once that is our minimum. Verified on 0.58.1, 0.61.2, 0.65.1, 0.66.0 and a master build. _patch_pointer_add gets the same treatment: its fix landed in 0.67.0, so it now applies only below that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fbd361a to
97f73d4
Compare
|
Switched to version checks on both, using The release boundaries, checked against numba's tags rather than guessed: the list_to_tuple fix merged 2026-08-27, after 0.67.0 was tagged on 2026-08-10, so it ships in 0.68 — patch applies on Verified the gates at the boundaries by loading both modules against four interpreters: 0.58.1 both patched, 0.66.0 both patched, 0.67.0 pointer_add off / list_to_tuple on, master build both off. Tests still green. |
Problem
Numba's
peep_hole_list_to_tuplerewrites the bytecode CPython emits for >30-item calls and tuple displays (STACK_USE_GUIDELINE) into incremental tuple concatenation — an IR tuple of every prefix length — which types and lowers as O(n²) LLVM IR plus NRT refcount churn, with a hard cliff at 31 items (21× module-size jump). Generated fgraph functions make such calls for every >30-input node (MakeVector,Scan, wide fused kernels), so compile time and memory blow up quadratically in node arity.Fix
_patch_list_to_tuplewraps the peephole, collapses the concatenation chain into a singlebuild_tuple, and inlines that tuple back into direct call arguments (so no aggregate forms at either side of a wide call boundary) — the same patch-until-upstream-releases mold as_patch_pointer_add. The bug is fixed upstream in numba/numba#10782, but numba releases roughly twice a year and pytensor supports already-released versions, so the workaround is warranted until the minimum supported numba ships the fix; the module then just gets deleted.Results
Interleaved with
mainin one session on an otherwise-idle 8-core/16-thread box (Ryzen 7 PRO 4750U, BLAS/OMP threads pinned to 1), cold caches (absolute times here are load-sensitive, so only same-session comparisons are quoted): on the n=80 additive repro this patch alone takes 364.9 s / 2696 MB to 323.3 s / 1689 MB — most of the win is memory, because the caller-side bytecode chain is only one of the two quadratics. Combined with #2362, which removes the wide-tuple callee inJoin: 105.9 s / 933 MB. The two are complementary — this PR carries the memory win, #2362 the compile-time one. Pure-numba: a 160-argument jitted call compiles ~3x faster and its caller module is 34x smaller.