Skip to content

Fix einsum not broadcasting batch dimensions in batched tensordot - #4125

Open
Adityaj0 wants to merge 1 commit into
ml-explore:mainfrom
Adityaj0:fix-einsum-batch-broadcast
Open

Fix einsum not broadcasting batch dimensions in batched tensordot#4125
Adityaj0 wants to merge 1 commit into
ml-explore:mainfrom
Adityaj0:fix-einsum-batch-broadcast

Conversation

@Adityaj0

Copy link
Copy Markdown
Contributor

Fixes #4124.

Proposed changes

batch_tensordot broadcast the contracting dimensions of its two operands to a common size but left the batch dimensions alone, then built the output shape from the first operand only:

for (auto ax : a_batch) {
  out_shape.push_back(a.shape(ax));
}

When the first operand had a size 1 batch dimension, matmul broadcast it against the second operand while out_shape kept the 1, so the final reshape got the wrong element count:

mx.einsum("...ij,...jk->...ik", mx.zeros((1, 3, 4)), mx.zeros((2, 4, 5)))
# ValueError: [reshape] Cannot reshape array of size 30 into shape (1,3,5).

The reverse operand order worked, because the output shape was then taken from the larger operand. That asymmetry is why it went unnoticed. It is not specific to ellipses either, "bij,bjk->bik" with the same shapes fails identically.

The fix broadcasts the batch dimensions the same way the contracting dimensions already are, in the same block. a_batch and b_batch are parallel by construction (b_batch is built by looking up each a_batch label in b's subscript), so they can be zipped exactly like a_contract / b_contract.

operands NumPy before after
(1,3,4), (2,4,5) (2,3,5) error (2,3,5)
(2,3,4), (1,4,5) (2,3,5) (2,3,5) (2,3,5)
(1,4), (5,4) -> ... (5,) error (5,)
(5,4), (1,4) -> ... (5,) (5,) (5,)

Tests

Added test_ellipses_broadcast to python/tests/test_einsum.py covering both operand orders, multiple leading batch dimensions, a reduced (->...) output, and the explicit-label spelling, each checked against NumPy for both shape and values.

I also removed an unconditional return in test_broadcasting that made the four lines after it dead code. To be clear about scope: those assertions pass on main as well, so that is an unrelated cleanup rather than something this fix enables. I checked before touching it. It was simply hiding coverage right next to the bug.

  • I have read the CONTRIBUTING document
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the necessary documentation (not needed, no API change)
  • I have run pre-commit run --all-files to format my code and installed pre-commit prior to committing changes

Verified with a CPU-only build (-DMLX_BUILD_METAL=OFF): test_einsum, test_ops, test_autograd, test_vmap, test_linalg, test_blas, test_nn, test_array and test_reduce pass (464 tests).

batch_tensordot broadcast the contracting dimensions of its two operands
to a common size but left the batch dimensions alone, then built the
output shape from the first operand only:

    for (auto ax : a_batch) {
      out_shape.push_back(a.shape(ax));
    }

When the first operand had a size 1 batch dimension, matmul broadcast it
against the second operand while out_shape kept the 1, and the final
reshape failed:

    mx.einsum("...ij,...jk->...ik", mx.zeros((1, 3, 4)), mx.zeros((2, 4, 5)))
    ValueError: [reshape] Cannot reshape array of size 30 into shape (1,3,5).

The reverse order worked because the output shape was then taken from the
larger operand, which hid the bug. It reproduces with explicit labels too,
for example "bij,bjk->bik", so it is not specific to ellipses.

Broadcast the batch dimensions the same way the contracting dimensions
are already broadcast.

Also removes an unconditional return in test_broadcasting that made the
rest of that test dead code. Those assertions pass on main, so this is an
unrelated cleanup, but it was hiding coverage next to the bug.
@JasonHonKL

Copy link
Copy Markdown
Contributor

Will this cause some performance issue ? @Adityaj0

@zcbenz zcbenz added the await verification This pull request is non-trivial and requires a human expert to verify its correctness. label Aug 10, 2026
@Adityaj0

Copy link
Copy Markdown
Contributor Author

Hi @JasonHonKL no, because the fix only adds lazy batch dimension broadcasting in the existing batched tensordot path, so it should have negligible overhead beyond a small amount of shape bookkeeping.

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

Labels

await verification This pull request is non-trivial and requires a human expert to verify its correctness.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

einsum fails to broadcast size 1 batch dimensions when the smaller operand comes first

3 participants