Repository navigation
Fix matmul() broadcasting for operands of different ranks - #239
Merged
ekzhang merged 3 commits intoOct 25, 2026
Merged
Conversation
np.matmul() put the batch dims in the wrong place when the operands had different ranks and the right one had batch dims. For example (2, 3) @ (4, 3, 2) returned shape [2, 4, 2] instead of NumPy's (4, 2, 2), and (2, 2) @ (2, 2, 2) returned the right shape with the leading two axes swapped. Prepend 1s to the shape of the operand with fewer dims, so batch dims are aligned from the right and broadcast as in NumPy and JAX. Co-Authored-By: Claude Opus 5.5 <[email protected]>
ekzhang
reviewed
Oct 23, 2026
| } else if (y.ndim > 2 && y.ndim < x.ndim) { | ||
| y = y.reshape([...rep(x.ndim - y.ndim, 1), ...y.shape]); | ||
| } | ||
| const numBatchDims = Math.min(Math.max(x.ndim, 2), y.ndim) - 2; |
Owner
There was a problem hiding this comment.
Thanks! Can you just implement this fix without your extra reshaping by transposing the output of lax.dot() if numBatchDims >= 1 and x.ndim != y.ndim,
from:
batch, x, y
to:
x, y, batch
ekzhang
enabled auto-merge (squash)
October 25, 2026 17:45
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
np.matmul()puts the batch dims in the wrong place when the operands have different ranks and the right one has batch dims (2D @ 3D+, or 3D+ @ 3D+ with different ranks). Sometimes the shape still looks right and only the values are wrong:NumPy and
jax.numpy.matmulalign batch dims from the right and broadcast them, which is also whatnp.einsum("...ij,...jk->...ik", a, b)already does here.Cause
src/library/numpy.ts:1925passesmin(max(x.ndim, 2), y.ndim) - 2trailing batch dims of each operand tolax.dot(). When the ranks differ, the leading dims that only the higher-rank operand has are passed as free dims, andlax.dot()returns[...batch, ...lhs free, ...rhs free], so they come out after the shared batch dims, or after the rows ofxwhen they belong toy. This has been the case since #16 movedmatmul()ontolax.dot().Fix
Prepend 1s to the shape of the operand with fewer dims, so both have the same batch rank and
lax.dot()broadcasts size-1 batch dims as it already does for equal ranks. A 1Dxor a 2Dyhas no batch dims to align, so those paths are unchanged; for(..., N, K) @ (K, M), the usual dense-layer shape, the traced jaxpr is identical to before.MatMulinpackages/onnxcallsnp.matmul()directly, so it picks up the fix.Two tests are added to the
jax.numpy.matmul()suite: a 2D matrix against a stack of matrices, and a 4D @ 3D product that broadcasts size-1 batch dims.Verification
I could not install the dependencies (no npm registry access here), so vitest,
pnpm check,pnpm lintandpnpm format:checkwere not run. Instead I loadedsrc/directly in Node 22, with a small loader for extensionless imports andusing, on the cpu and wasm backends:jit(): 97/150 match on main on each device, with every failure in 2D @ 3D/4D, 3D @ 4D or 4D @ 3D; 150/150 match with the fix.valueAndGrad()with respect to both operands for 9 mixed-rank cases, against analytic gradients computed in NumPy, eager and underjit(): 2/9 match on main, 9/9 with the fix.vmap()agrees with the unmapped call, both inputs are consumed exactly once, and incompatible batch dims still throw.test/numpy.test.tsthrough a minimal stand-in for the vitest API. The two new tests fail on main on cpu and wasm (expected [2,3,2] to equal [3,2,2],expected [3,2,1,1] to equal [2,3,1,1]) and pass with the fix. Thenumpy,numpy-linalg,lax-linalg,einsum,refcount,basicandtracingtest files give the same per-test results before and after apart from those four; the only failures, on both, areFloat16Array is not definedfrom Node 22.WebGPU and WebGL were not available, so the new tests have not run there. The second one multiplies int32 arrays, as the existing einsum tests already do on every device.
🤖 Generated with Claude Code