Skip to content

fix(billing): bill a ufunc on the shape it writes when out= or where= is larger - #288

Open
AndreasHad04 wants to merge 1 commit into
AIcrowd:mainfrom
AndreasHad04:fix/275-ufunc-out-where-broadcast
Open

AndreasHad04 wants to merge 1 commit into
AIcrowd:mainfrom
AndreasHad04:fix/275-ufunc-out-where-broadcast

Conversation

@AndreasHad04

@AndreasHad04 AndreasHad04 commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #275.

What was wrong

numpy broadcasts a ufunc's operands against out= (and where=) and evaluates the loop over every output element, but the bill followed the operands. exp(x (2048,), out=O (512, 2048)) was billed for 2,048 elements while numpy computed 1,048,576; the same held for binary ufuncs, for where= masks (with or without out=), and for matmul(X (128, 128), X, out=O (256, 128, 128)), which was billed as one product instead of 256.

What it bills now

  • Pointwise ufuncs (_counted_unary, _counted_binary and their multi-output forms modf, frexp, divmod) and clip: numel(broadcast(operands, out, where)) cells instead of numel(broadcast(operands)). The operands' symmetry is carried into that frame with broadcast_group, so the call costs exactly what the same op costs on the operands broadcast to the destination's shape.
  • Gufunc contractions routed through _einsum_routed_binary (matmul, vecdot, matvec, vecmat): the contraction cost times numel(out) / numel(result without out=), i.e. once per batch element the destination adds. This scales mu and alpha alike, so the complex-factor override is unchanged. Non-ufunc ops such as dot refuse a larger destination and are not touched.

A destination or mask of the operands' own shape, where=True, and an op without a native out= (it computes over its operand and is copied) bill exactly as before. The cost-model docs now say that numel(output) is the shape numpy writes, and note the batch rule under the contraction table.

Tests

New tests/test_out_where_broadcast_billing.py (12 tests), using the issue's shapes: exp and multiply with a (512, 2048) out=, exp with a (512, 2048) where= mask with and without out=, matmul with a (256, 128, 128) out=, vecdot with a batched out=, and modf/divmod/clip with larger destinations. Five more cover vecdot with keepdims=True (with and without axis=) and matvec/vecmat with an axes= that moves the result's core axis: the batch axes a larger out= adds are counted wherever numpy puts that axis. Each asserts the bill equals the same op on the operands materialised at the destination's shape (and, where applicable, the batch count times the plain call), with identical values. A control test checks that same-shape destinations, same-shape masks, where=True, an out= matching a broadcast binary output and a same-shape matmul destination are unchanged. 11 of the 12 fail without the source change.

Results

  • New tests: 12 passed.
  • Full default suite (uv run pytest): 10714 passed, 125 skipped, 2 xfailed.
  • tests/parity fast gate: 135 passed.
  • ruff check, ruff format --check, pyright on the changed files: clean.

… is larger (AIcrowd#275)

numpy broadcasts a ufunc's operands against out= (and where=) and
evaluates the loop over every output element, but the bill followed the
operands: exp(x (2048,), out=O (512, 2048)) was billed for 2,048
elements while numpy computed 1,048,576, and matmul(X, X,
out=O (256, 128, 128)) was billed as one product instead of 256.

The pointwise factories (unary, binary and their multi-output forms)
and clip now bill numel(broadcast(operands, out, where)), carrying the
operands' symmetry into that frame with broadcast_group, so the call
costs what the same op costs on the operands broadcast to the
destination. The gufunc contractions routed through
_einsum_routed_binary (matmul, vecdot, matvec, vecmat) multiply their
cost by the number of batch elements out= adds. A destination or mask
of the operands' own shape bills exactly as before, as does an op
without a native out= (it computes over its operand and is copied).
@AndreasHad04
AndreasHad04 force-pushed the fix/275-ufunc-out-where-broadcast branch from d74cad1 to 09bf498 Compare October 20, 2026 21:44

This branch has not been deployed

No deployments
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.

ufuncs with an out= (or where=) larger than the inputs bill the input size, not the broadcast write

1 participant