Numba: work around quadratic lowering of >30-argument calls - #2361
Numba: work around quadratic lowering of >30-argument calls#2361velochy wants to merge 2 commits into
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. |
numba's peep_hole_list_to_tuple rewrites CPython's >30-item LIST_APPEND bytecode (STACK_USE_GUIDELINE) into a tuple of every prefix length, which types and lowers as O(n^2) LLVM IR plus NRT refcount churn — generated fgraph functions make such calls routinely. Collapse the chain into a single build_tuple after the peephole runs. Fixed upstream in numba/numba#10782; this patch covers already-released numba versions, in the same mold as _patch_pointer_add. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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. |
A wide vararg call round-trips every argument through one wide tuple on both sides of the boundary; with direct arguments and a fixed-arity callee no aggregate forms at all. Also fixes the use counter treating an alias assignment's target as a use, which blocked forwarding through double aliases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 1-core box, 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.