Skip to content

Fix numba failure on gradient of chained batched Convolve1d - #2375

Open
juanitorduz wants to merge 2 commits into
pymc-devs:mainfrom
juanitorduz:fix-2360-blockwise-core-shape-guard
Open

Fix numba failure on gradient of chained batched Convolve1d#2375
juanitorduz wants to merge 2 commits into
pymc-devs:mainfrom
juanitorduz:fix-2360-blockwise-core-shape-guard

Conversation

@juanitorduz

Copy link
Copy Markdown
Contributor

Description

Taking the gradient of chained convolve1d calls, where the kernel has been batched with vectorize_graph and the signal left unbatched (the adstock pattern used in pymc-marketing), failed under the numba backend: the Blockwise{Convolve1d} fell back to object mode with a UserWarning, and evaluation then raised ValueError: Scalar check failed (npy_bool). The same graph runs fine with the py/cvm/c|py/JAX linkers.

Two independent bugs were involved, fixed in one commit each:

1. Over-broad bail-out in introduce_explicit_core_shape_blockwise (pytensor/tensor/rewriting/numba.py)

The rewrite skipped any node for which a Blockwise appeared in applys_between(node.inputs, core_shapes). Because that traversal also yields the owners of the blocker variables, a Blockwise whose core shape reads Shape_i of an input produced by another Blockwise was permanently excluded from BlockwiseWithCoreShape wrapping — which is exactly the shape of the chained-convolution gradient (each grad conv consumes the previous conv's output of unknown static length). Per ShapeFeature.get_non_recursive_shape's contract, the shape expressions read only the node's own inputs; the only case that genuinely cannot be introduced is the Shape_i(output) fallback of Blockwise.infer_shape, where computing the core shape would require evaluating the node itself. The guard now bails only on that self-reference. With this change the issue's repro compiles without any object-mode fallback, for both static and dynamic signal shapes, and the gradient matches the cvm result.

2. 0d arrays stored into ScalarType core-input storage in Blockwise.perform (pytensor/tensor/blockwise.py)

_vectorize_node_perform wrapped every core input value in np.asarray(...). For a core input with ScalarType — the boolean full_mode flag of Convolve1d, signature () — the core C thunk's c_extract rejects a 0d array (Scalar check failed (npy_bool)). The bug is latent for every linker but only surfaces when the lazily-built gufunc (impl=None) picks the C thunk, as happens in the numba object-mode fallback which calls perform on an unprepared node (the cvm/py linkers end up rebuilding the gufunc with the Python impl via prepare_node, which is why they worked). Storage now honors the type's own contract (ScalarType.filter returns numpy scalars), with per-input converters precomputed outside the hot loop — the input-side counterpart of the scalar-output handling from #1846. This keeps the object-mode fallback correct for the cases that legitimately remain there (e.g. a boolean mode that varies across batch dimensions).

The fixes are deliberately backend/graph-level rather than Convolve-specific, following the direction from #1522 and #1998 (keep the mode symbolic; no conv-specific mode rewrites; static shapes are a nice-to-have recovered at dispatch time).

Notes for possible follow-ups (not addressed here):

Tests: a numba end-to-end regression test of the issue's graph (object-mode fallback path and fully-rewritten path), unit tests for the corrected guard (including the self-referential case that must still bail), and unit tests for Blockwise.perform with broadcast and batched boolean ScalarType core inputs exercising the core C thunk.

Related Issue

Checklist

Type of change

  • New feature / enhancement
  • Bug fix
  • Documentation
  • Maintenance
  • Other (please specify):

🤖 Generated with Claude Code

@juanitorduz
juanitorduz marked this pull request as draft August 24, 2026 12:28
@juanitorduz

Copy link
Copy Markdown
Contributor Author

The 8 failing CI jobs here are pre-existing on main: the latest run on main fails with the exact same 8 jobs. They reduce to three test failures caused by numpy 2.5.2 (nested-sequence seeding rejected by SeedSequence in test_convolve2d, and searchsorted comparisons on unsorted inputs whose undefined-behavior results changed with numpy 2.5's binary-search rework) — all unrelated to this PR's changes.

Fix proposed in #2376. Once that merges I'll update this branch with main so CI reruns green.

@juanitorduz

Copy link
Copy Markdown
Contributor Author

I think failing test are coming from #2376

@juanitorduz
juanitorduz requested a review from ricardoV94 August 24, 2026 12:44
@juanitorduz
juanitorduz marked this pull request as ready for review August 24, 2026 12:44
@juanitorduz juanitorduz self-assigned this Aug 24, 2026
@juanitorduz
juanitorduz force-pushed the fix-2360-blockwise-core-shape-guard branch from e587dcb to 9b32c97 Compare August 24, 2026 13:38
…form

_vectorize_node_perform wrapped every core input value in np.asarray,
storing 0d ndarrays. For a core input with ScalarType (e.g. the boolean
full_mode flag of Convolve1d, signature "()"), the core C thunk's
c_extract rejects a 0d array with "ValueError: Scalar check failed".
The bug was latent for every linker but only surfaced when the lazily
built gufunc (impl=None) picked the C thunk for the core op, as happens
in the numba object-mode fallback which calls perform on an unprepared
node.

Storage now honors the type's own contract (ScalarType.filter returns
numpy scalars), with per-input converters precomputed outside the loop.
… shapes

introduce_explicit_core_shape_blockwise bailed whenever any Blockwise
appeared in applys_between(node.inputs, core_shapes). Because that
traversal also yields the owners of the blocker variables, a Blockwise
whose core shape reads Shape_i of an input produced by another Blockwise
was permanently excluded from BlockwiseWithCoreShape wrapping and fell
back to object mode. This broke the gradient of chained convolve1d calls
with a kernel batched by vectorize_graph (issue pymc-devs#2360).

Per ShapeFeature.get_non_recursive_shape's contract, the shape
expressions read only the node's own inputs; the only case that cannot
be introduced is the Shape_i(output) fallback of Blockwise.infer_shape,
where the core shape requires evaluating the node itself. Bail only on
that self-reference.

Closes pymc-devs#2360
@juanitorduz
juanitorduz force-pushed the fix-2360-blockwise-core-shape-guard branch from 9b32c97 to d1693aa Compare August 24, 2026 14:25
@jessegrabowski

Copy link
Copy Markdown
Member

I'm less sure on this one, want @ricardoV94 to take a look. My gut says this is addressing the symptom and not the cause. Details about different linkers shouldn't be leaking into the implementation of Blockwise. If one linker is doing something that doesn't conform to the others, we should fix it.

@ricardoV94

ricardoV94 commented Aug 25, 2026

Copy link
Copy Markdown
Member

Doesn't Blockwise explicitly require tensor inputs?

Numba has a thing for downcasting 0d arrays to python scalars, it's a known limitation.

I think the bot missed this as the source of the failure.

If that's the case, I'd stick only with the core shape fix (which I haven't reviewed yet). We can't be defensive about numba scalar downcasting everywhere. Fortunately it only hurts at the boundaries (obj mode or function output)

for node in applys_between(node.inputs, core_shapes)
):
# If Blockwise shows up in the shape graph we can't introduce the core shape
if set(node.outputs) & set(ancestors(core_shapes, blockers=node.inputs)):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This check is too soft. If a core shape rebuilds the same Blockwise but fresh or a new one you'd accept it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If the issue was indeed "Because that traversal also yields the owners of the blocker variables" just patch that. It's a boundary precision question.

If it's not just that the explanation is still lacking

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.

BUG: numba backend fails on gradient of chained batched Convolve1d (Blockwise falls back to object mode, then "Scalar check failed (npy_bool)")

3 participants