Skip to content

Add BLAS dispatches and Dot-based lowering for the JIT backend - #2406

Open
jessegrabowski wants to merge 21 commits into
pymc-devs:mainfrom
jessegrabowski:numba-blas-gemm-ger
Open

jessegrabowski wants to merge 21 commits into
pymc-devs:mainfrom
jessegrabowski:numba-blas-gemm-ger

Conversation

@jessegrabowski

Copy link
Copy Markdown
Member

All of this measures netural against pymc-model-catalog. I saw 10-15% speedup in the backward pass of linear layers in pytensor-ml. Rewriting graphs of the form a + B @ C into GEMM isn't perfect, it still requires the user to put parenthesis. We can try to tune it up in follow-up work, but it's not super clear to me it's an obvious win.

@jessegrabowski jessegrabowski added enhancement New feature or request linalg Linear algebra labels Sep 2, 2026
@jessegrabowski jessegrabowski changed the title dd BLAS dispatches and Dot-based lowering for the JIT backe Add BLAS dispatches and Dot-based lowering for the JIT backend Sep 2, 2026
Comment thread pytensor/link/jax/dispatch/blas.py Outdated
return batched_dot


@jax_funcify.register(Gemm)

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 is an argument to not bother with these ops in jax, like we don't bother with Fusion/Inplace?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This feels a lot more straight-forward though. We canonicalize to one form them represent it however the backend is able to. In this case it's just a naive form.

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 is not a canonicalization, it's a specialization. It doesn't make sense to specialize (and slow compilation) if we're throwing it away next.

With the very same argument you'd say do fusion and inplace rewrites in jax

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Went with your way

return expm, cache_version


def _gemm(A, B, C, transa=False, transb=False, alpha=1.0, beta=0.0):

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.

there was a big slowdown in gemv(?) C-code with negative strides, where a copy could be avoided. we may want to do the same trick for numba



@overload(_ger)
def _ger_impl(alpha, x, y, A):

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.

isn't ger the more useless one, compared to gemv?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I was getting good results from ger and gemv across the board, but gemm was pretty useless unless the matrices were specific shapes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'll post some real benchmarks.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Benchmarks say gemv: #2406 (comment)

Comment thread pytensor/link/numba/dispatch/blas.py Outdated
Comment on lines +29 to +33
b = beta.item()
if b == 1.0:
out += Z
elif b != 0.0:
out += b * Z

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.

isn't this defeat the point of the scalar/mul fusion of gemm?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done, one loop

def dot(x, y, out=None):
if out is None:
out = np.empty((x.shape[0], y.shape[1]), dtype=numba_dot_dtype)
return _gemm(x, y, out, False, False, 1.0, 0.0)

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.

I'd be surprised if numba doesn't emit blas for np.dot already

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

it does, but it doesn't have support for inplace, alpha, or beta.

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.

np.dot(out=x)?

alpha and beta I suspect are just cuteness. We probably make better use with fusion/reduction downstream than trying very hard to merge with blas

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, reverted

if numba_dot_dtype in _GEMM_DTYPES:
# `gemm` reads each operand's memory order as a transpose flag, so an
# operand that reaches here transposed costs nothing, where `np.dot` would
# have to be handed a contiguous copy of 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.

sus comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Removed

Comment thread pytensor/link/utils.py
return destroy_dependencies


def get_static_scalar(node: Apply | None, input_index: int) -> float | None:

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.

I don't think this helper earns its keep, the logic to make it general is more complex than inlining it

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Kept, five callers

Comment thread pytensor/tensor/rewriting/blockwise.py Outdated
)


@node_rewriter([AllocEmpty])

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.

should apply to alloc of zeros as well

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done

destroyers = dict.fromkeys(
client
for client, input_index in clients[1:]
if not isinstance(client.op, Output)

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.

is this if not needed?

@ricardoV94 ricardoV94 left a comment

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.

the refactor and bootstrap code for the c impl dispatcher looks neat.

OTOH I'm concerned IFF we are making the blas pipeline run by default in jax and numba? For jax it reads just like rewrite overhead, since we end up emitting the naive code. For numba I'd need a more exhaustive reproducible benchmark than "it speeds up pytensor-ml by 10%", as this is a fundamental change that touches most graphs we work with. Or proving that np.dot always lowers to blas by numba anyway and we are just skipping some indirection (which ones?).

Comment thread pytensor/link/numba/dispatch/blas.py Outdated
Comment on lines +93 to +99
def ger(A, alpha, x, y):
# `A` is only broadcast against the outer product, so the buffer the update
# writes into takes the product's shape rather than `A`'s. Copying also leaves
# `A` intact, which is the whole difference between this op and its inplace form.
out = np.empty((x.shape[0], y.shape[0]), dtype=dtype)
out[:] = A
return _ger(alpha.item(), x, y, out)

@ricardoV94 ricardoV94 Sep 8, 2026

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.

Should this be return _ger(alpha.item(), x, y, A.copy())? or copy_asfortran_order?

@jessegrabowski

Copy link
Copy Markdown
Member Author

Evaluation time on the numba backend, mean +/- stddev of a pytest-benchmark run, main at 44373b4 against this branch. Speedup is main over branch, so above 1x is a win. Each grid is graph in {forward, backward, forward_backward} x output shape in {square, tall (4n rows), wide (4n cols)} x n in {16, 128, 512} x unbatched or batched over a leading axis of 8. product is the bare product, accumulate is Z + alpha * product, the form the fusion rewrites match. The backward graph is the pullback with a supplied cotangent. backward under accumulate is skipped because the additive Z makes it identical to product.

A bare Dot still lowers to np.dot on this branch, so the unbatched product rows measure unchanged code and show the noise floor. BatchedDot now calls gemm per batch slice, writing into the output and reading transposed slices through the BLAS flag, where before each slice went through np.dot's own copy and allocate. Every batched speedup below, on all three functions, comes from that change.

Gemm

Z + alpha * (A @ B) compiles to one Gemm when unbatched. The batched accumulate stays BatchedDot + Elemwise, so its speedup is the BatchedDot change alone.

Every batched row wins, 1.4x to 3.0x, with a median of 1.8x on forward and about 2.1x on backward and forward+backward. 46 of the 54 batched cells clear one stddev.

The unbatched Gemm itself loses. Where it fires (forward-*-unbatched-accumulate) it is 0.83x at square-128, 0.88x at wide-128 and 0.95x at wide-512, all outside noise, and it is never significantly ahead. At these sizes np.dot plus the fused Elemwise add is cheaper than the single BLAS call that replaces them. The unbatched forward+backward accumulate rows are flat because only one of their three products becomes a Gemm, and the unbatched product rows are flat because they run unchanged code.

test                                                              main (us)            branch (us)   speedup
forward-square-16-unbatched-product                            1.6 +/-    0.3           1.7 +/-    0.2     0.97x
forward-square-128-unbatched-product                          12.5 +/-    1.5          12.6 +/-    1.2     1.00x
forward-square-512-unbatched-product                         475.9 +/-   18.4         475.0 +/-   25.2     1.00x
forward-tall-16-unbatched-product                              1.8 +/-    0.4           1.8 +/-    0.3     1.01x
forward-tall-128-unbatched-product                            44.1 +/-    8.0          42.8 +/-    2.9     1.03x
forward-tall-512-unbatched-product                          1881.8 +/-  315.3        1644.3 +/-  101.1     1.14x
forward-wide-16-unbatched-product                              1.8 +/-    1.6           1.8 +/-    0.5     1.04x
forward-wide-128-unbatched-product                            49.2 +/-    4.4          47.9 +/-    3.4     1.03x
forward-wide-512-unbatched-product                          2031.3 +/-  100.3        1990.2 +/-   83.0     1.02x
backward-square-16-unbatched-product                           2.5 +/-    0.8           2.5 +/-    0.5     0.99x
backward-square-128-unbatched-product                         41.1 +/-   25.1          57.4 +/-   30.6     0.72x
backward-square-512-unbatched-product                        965.0 +/-  100.0         963.7 +/-   92.4     1.00x
backward-tall-16-unbatched-product                             2.7 +/-    0.7           2.7 +/-    0.5     0.98x
backward-tall-128-unbatched-product                           88.8 +/-   18.0          93.6 +/-   23.0     0.95x
backward-tall-512-unbatched-product                         3695.3 +/-  197.1        3601.5 +/-  161.9     1.03x
backward-wide-16-unbatched-product                             2.7 +/-    1.5           2.7 +/-    7.1     0.99x
backward-wide-128-unbatched-product                          180.1 +/-   48.8         191.1 +/-   45.7     0.94x
backward-wide-512-unbatched-product                         4907.0 +/- 1412.7        3859.8 +/-  166.1     1.27x
forward_backward-square-16-unbatched-product                   3.3 +/-    0.6           3.3 +/-    0.7     1.00x
forward_backward-square-128-unbatched-product                 70.0 +/-   41.3          65.0 +/-   26.0     1.08x
forward_backward-square-512-unbatched-product               1646.5 +/-  155.3        1614.2 +/-  128.2     1.02x
forward_backward-tall-16-unbatched-product                     3.8 +/-    0.7           3.8 +/-    0.8     1.00x
forward_backward-tall-128-unbatched-product                  188.4 +/-   40.6         192.1 +/-   34.4     0.98x
forward_backward-tall-512-unbatched-product                 6167.7 +/-  281.3        5881.5 +/-  239.6     1.05x
forward_backward-wide-16-unbatched-product                     3.8 +/-    1.2           3.6 +/-    0.6     1.03x
forward_backward-wide-128-unbatched-product                  263.1 +/-   80.0         275.4 +/-   54.1     0.96x
forward_backward-wide-512-unbatched-product                 7251.4 +/-  502.7        6563.2 +/-  258.9     1.10x
forward-square-16-batched-product                              5.8 +/-    0.8           3.6 +/-    0.6     1.60x
forward-square-128-batched-product                           181.4 +/-   26.2          80.9 +/-    4.9     2.24x
forward-square-512-batched-product                          5345.2 +/-  360.2        2956.4 +/-   83.4     1.81x
forward-tall-16-batched-product                               11.9 +/-    9.4           5.1 +/-    1.2     2.35x
forward-tall-128-batched-product                             781.0 +/-   74.8         519.7 +/-   72.8     1.50x
forward-tall-512-batched-product                           23906.0 +/-  771.4       13183.2 +/-  367.9     1.81x
forward-wide-16-batched-product                               11.0 +/-   15.2           4.1 +/-    0.7     2.70x
forward-wide-128-batched-product                             848.6 +/-   89.5         591.5 +/-   83.4     1.43x
forward-wide-512-batched-product                           28609.5 +/- 5470.0       16272.0 +/-  323.0     1.76x
backward-square-16-batched-product                            14.0 +/-   11.6           6.3 +/-    0.8     2.24x
backward-square-128-batched-product                          550.8 +/-  114.1         200.1 +/-   41.1     2.75x
backward-square-512-batched-product                        18897.1 +/- 1099.1        7068.7 +/-  170.4     2.67x
backward-tall-16-batched-product                              22.7 +/-    3.1           8.2 +/-    2.3     2.78x
backward-tall-128-batched-product                           1805.6 +/-  232.5         860.5 +/-   96.0     2.10x
backward-tall-512-batched-product                          56568.8 +/- 1059.1       28614.6 +/-  375.0     1.98x
backward-wide-16-batched-product                              21.3 +/-    1.9           8.3 +/-    1.4     2.56x
backward-wide-128-batched-product                           1941.9 +/-  205.5        1011.8 +/-  132.2     1.92x
backward-wide-512-batched-product                          61211.0 +/-  811.2       30089.2 +/-  378.0     2.03x
forward_backward-square-16-batched-product                    18.5 +/-    2.6           9.4 +/-    5.1     1.97x
forward_backward-square-128-batched-product                  916.9 +/-  222.5         444.2 +/-   68.1     2.06x
forward_backward-square-512-batched-product                26645.1 +/- 1245.6       12723.5 +/-  312.3     2.09x
forward_backward-tall-16-batched-product                      39.3 +/-   13.6          13.1 +/-    2.7     2.99x
forward_backward-tall-128-batched-product                   2940.7 +/-  240.3        1708.7 +/-  212.6     1.72x
forward_backward-tall-512-batched-product                 103018.4 +/- 21060.3       47073.2 +/- 1179.6     2.19x
forward_backward-wide-16-batched-product                      36.4 +/-   41.8          12.3 +/-    2.4     2.97x
forward_backward-wide-128-batched-product                   3514.3 +/-  453.6        1968.2 +/-  239.1     1.79x
forward_backward-wide-512-batched-product                  98923.9 +/- 5604.2       51719.2 +/-  611.2     1.91x
forward-square-16-unbatched-accumulate                         1.9 +/-    0.4           1.9 +/-    0.4     1.00x
forward-square-128-unbatched-accumulate                       16.1 +/-    1.5          19.4 +/-    1.7     0.83x
forward-square-512-unbatched-accumulate                      521.5 +/-   32.9         548.0 +/-   32.7     0.95x
forward-tall-16-unbatched-accumulate                           2.2 +/-    0.7           2.4 +/-    0.4     0.93x
forward-tall-128-unbatched-accumulate                        108.3 +/-  592.6          64.7 +/-    4.0     1.67x
forward-tall-512-unbatched-accumulate                       2089.8 +/-  196.8        1972.8 +/-   79.0     1.06x
forward-wide-16-unbatched-accumulate                           2.2 +/-    1.0           2.3 +/-    0.6     0.95x
forward-wide-128-unbatched-accumulate                         60.3 +/-    5.1          68.4 +/-    4.7     0.88x
forward-wide-512-unbatched-accumulate                       2263.3 +/-  119.9        2384.3 +/-   67.9     0.95x
forward_backward-square-16-unbatched-accumulate                3.6 +/-    2.1           3.7 +/-    0.8     0.97x
forward_backward-square-128-unbatched-accumulate              76.3 +/-   70.0         115.5 +/-   29.0     0.66x
forward_backward-square-512-unbatched-accumulate            1759.8 +/-  158.1        1676.3 +/-  130.3     1.05x
forward_backward-tall-16-unbatched-accumulate                  4.3 +/-    0.9           4.6 +/-    0.9     0.94x
forward_backward-tall-128-unbatched-accumulate               196.0 +/-   35.3         214.3 +/-   34.7     0.91x
forward_backward-tall-512-unbatched-accumulate              6849.2 +/-  744.5        6169.8 +/-  180.5     1.11x
forward_backward-wide-16-unbatched-accumulate                  4.3 +/-    1.5           4.5 +/-    1.0     0.95x
forward_backward-wide-128-unbatched-accumulate               263.9 +/-   51.1         278.2 +/-   49.7     0.95x
forward_backward-wide-512-unbatched-accumulate              7426.5 +/-  535.2        7005.0 +/-  297.3     1.06x
forward-square-16-batched-accumulate                           6.2 +/-    0.8           4.1 +/-    0.6     1.52x
forward-square-128-batched-accumulate                        240.0 +/-  560.0         125.4 +/-   58.0     1.91x
forward-square-512-batched-accumulate                       5746.6 +/-  195.2        3450.6 +/-   56.5     1.67x
forward-tall-16-batched-accumulate                            13.7 +/-    2.1           7.0 +/-    1.2     1.95x
forward-tall-128-batched-accumulate                         1179.1 +/- 1175.0         613.7 +/-   72.6     1.92x
forward-tall-512-batched-accumulate                        27072.6 +/- 2899.8       14850.8 +/-  317.4     1.82x
forward-wide-16-batched-accumulate                            12.8 +/-    1.5           5.9 +/-    1.0     2.15x
forward-wide-128-batched-accumulate                          959.4 +/-  115.4         678.0 +/-   71.8     1.42x
forward-wide-512-batched-accumulate                        28028.3 +/-  572.2       17779.4 +/-  167.3     1.58x
forward_backward-square-16-batched-accumulate                 19.6 +/-    3.1          10.1 +/-    1.3     1.94x
forward_backward-square-128-batched-accumulate               959.7 +/-  173.4         456.2 +/-   80.1     2.10x
forward_backward-square-512-batched-accumulate             26849.1 +/-  653.5       13355.1 +/-  327.1     2.01x
forward_backward-tall-16-batched-accumulate                   42.0 +/-   14.9          16.0 +/-    1.7     2.62x
forward_backward-tall-128-batched-accumulate                3167.4 +/-  295.6        1876.7 +/-  217.7     1.69x
forward_backward-tall-512-batched-accumulate               90600.7 +/- 4007.7       47907.5 +/-  621.3     1.89x
forward_backward-wide-16-batched-accumulate                   45.2 +/-  380.3          15.2 +/-    2.0     2.98x
forward_backward-wide-128-batched-accumulate                3733.4 +/-  368.7        2118.6 +/-  233.4     1.76x
forward_backward-wide-512-batched-accumulate               96841.7 +/- 3932.1       53645.2 +/-  614.0     1.81x

Ger

Z + alpha * outer(x, y) compiles to one Ger when unbatched. Before this branch numba had no Ger at all, so main runs Dot + Elemwise. The batched outer product is an Elemwise broadcast on both sides, so those rows measure unchanged code.

The unbatched Ger wins (forward-*-unbatched-accumulate), 1.2x to 2.5x with a median of 1.8x and six of nine cells significant, and the unbatched forward+backward accumulate keeps a 1.15x median from it. Batched backward is 1.2x median from the BatchedDot change, since the pullback of an outer product is two matrix-vector products.

Nothing loses. Batched forward is unchanged Elemwise code and sits at parity. Its wide-512 rows read 0.81x and 0.87x in the full run, so both were re-timed in isolation on each commit and those reruns are the rows shown. Every other row is flat.

test                                                              main (us)            branch (us)   speedup
forward-square-16-unbatched-product                            1.2 +/-    0.2           1.1 +/-    0.1     1.01x
forward-square-128-unbatched-product                           2.8 +/-    0.9           2.8 +/-    0.7     1.00x
forward-square-512-unbatched-product                         119.2 +/-   33.7         124.4 +/-  142.7     0.96x
forward-tall-16-unbatched-product                              1.5 +/-    0.3           1.5 +/-    0.3     1.00x
forward-tall-128-unbatched-product                             6.1 +/-    1.4           6.1 +/-    1.6     1.00x
forward-tall-512-unbatched-product                            76.7 +/-    4.8          74.2 +/-    5.3     1.03x
forward-wide-16-unbatched-product                              1.5 +/-    0.5           1.5 +/-    0.3     1.01x
forward-wide-128-unbatched-product                             6.8 +/-    1.2           7.1 +/-    1.3     0.95x
forward-wide-512-unbatched-product                            75.1 +/-    4.1          75.1 +/-    8.7     1.00x
backward-square-16-unbatched-product                           1.5 +/-    0.4           1.4 +/-    0.2     1.05x
backward-square-128-unbatched-product                          2.7 +/-    0.5           2.7 +/-    0.6     1.01x
backward-square-512-unbatched-product                          9.8 +/-    1.3          10.1 +/-    2.0     0.97x
backward-tall-16-unbatched-product                             2.3 +/-    0.4           2.4 +/-    0.6     0.98x
backward-tall-128-unbatched-product                            4.3 +/-    0.7           4.3 +/-    0.6     0.99x
backward-tall-512-unbatched-product                           32.2 +/-    3.6          36.8 +/-    4.6     0.87x
backward-wide-16-unbatched-product                             2.1 +/-    0.5           2.2 +/-    0.6     0.99x
backward-wide-128-unbatched-product                            4.1 +/-    0.7           4.1 +/-    0.8     0.99x
backward-wide-512-unbatched-product                           38.5 +/-    3.0          38.1 +/-    5.1     1.01x
forward_backward-square-16-unbatched-product                   1.8 +/-    0.4           2.1 +/-   47.6     0.85x
forward_backward-square-128-unbatched-product                 11.9 +/-   12.8          12.7 +/-   10.0     0.94x
forward_backward-square-512-unbatched-product                274.9 +/-   68.0         270.8 +/-   64.7     1.02x
forward_backward-tall-16-unbatched-product                     3.6 +/-    1.1           3.6 +/-    0.9     1.00x
forward_backward-tall-128-unbatched-product                   32.7 +/-   24.7          44.0 +/-   14.0     0.74x
forward_backward-tall-512-unbatched-product                  707.5 +/-  118.3         708.8 +/-  127.8     1.00x
forward_backward-wide-16-unbatched-product                     3.4 +/-    1.0           3.4 +/-    0.9     1.00x
forward_backward-wide-128-unbatched-product                   48.8 +/-  194.3          49.2 +/-   14.3     0.99x
forward_backward-wide-512-unbatched-product                  691.9 +/-  117.2         693.7 +/-  116.0     1.00x
forward-square-16-batched-product                              1.2 +/-    0.7           1.2 +/-    0.2     1.07x
forward-square-128-batched-product                             9.8 +/-    1.8           9.8 +/-    1.9     1.00x
forward-square-512-batched-product                           138.1 +/-   16.0         147.1 +/-   13.7     0.94x
forward-tall-16-batched-product                                1.6 +/-    0.4           1.7 +/-    0.4     0.99x
forward-tall-128-batched-product                             227.4 +/-   51.6         206.6 +/-   63.7     1.10x
forward-tall-512-batched-product                            4241.0 +/-  518.4        4525.7 +/-  637.9     0.94x
forward-wide-16-batched-product                                1.6 +/-    0.5           1.6 +/-    0.3     1.00x
forward-wide-128-batched-product                             191.5 +/-   55.1         215.3 +/-   51.5     0.89x
forward-wide-512-batched-product                            4475.9 +/-  601.7        4413.7 +/-  640.3     1.01x
backward-square-16-batched-product                             2.6 +/-    0.6           2.3 +/-    0.6     1.12x
backward-square-128-batched-product                           11.3 +/-    1.4           8.6 +/-    1.3     1.31x
backward-square-512-batched-product                          141.6 +/-    8.9         144.7 +/-   25.8     0.98x
backward-tall-16-batched-product                               7.9 +/-    1.1           5.7 +/-    1.4     1.38x
backward-tall-128-batched-product                             28.1 +/-    2.7          20.9 +/-    2.7     1.35x
backward-tall-512-batched-product                           1000.3 +/-   41.3         986.4 +/-   60.1     1.01x
backward-wide-16-batched-product                               5.5 +/-    0.7           4.7 +/-    0.9     1.16x
backward-wide-128-batched-product                             25.9 +/-    2.3          19.3 +/-    2.8     1.34x
backward-wide-512-batched-product                           1018.7 +/-   49.4        1064.0 +/-   93.8     0.96x
forward_backward-square-16-batched-product                     3.2 +/-    0.6           2.8 +/-    0.6     1.13x
forward_backward-square-128-batched-product                   32.5 +/-    2.9          30.4 +/-    3.0     1.07x
forward_backward-square-512-batched-product                  438.7 +/-   29.2         429.1 +/-   32.4     1.02x
forward_backward-tall-16-batched-product                      10.2 +/-    2.0           7.8 +/-    1.2     1.31x
forward_backward-tall-128-batched-product                    417.5 +/-  101.1         424.6 +/-  119.8     0.98x
forward_backward-tall-512-batched-product                   9306.8 +/-  710.4        9882.7 +/-  979.7     0.94x
forward_backward-wide-16-batched-product                       7.8 +/-    1.9           6.8 +/-    1.3     1.16x
forward_backward-wide-128-batched-product                    445.6 +/-   89.7         406.7 +/-  102.0     1.10x
forward_backward-wide-512-batched-product                   9800.6 +/-  861.0        9437.0 +/-  776.5     1.04x
forward-square-16-unbatched-accumulate                         1.3 +/-    0.4           1.1 +/-    0.5     1.15x
forward-square-128-unbatched-accumulate                        6.8 +/-    1.6           3.3 +/-    0.7     2.04x
forward-square-512-unbatched-accumulate                      160.1 +/-   35.9         125.1 +/-   78.2     1.28x
forward-tall-16-unbatched-accumulate                           1.8 +/-    0.5           1.3 +/-    0.2     1.35x
forward-tall-128-unbatched-accumulate                         19.5 +/-    2.2           8.5 +/-    1.3     2.28x
forward-tall-512-unbatched-accumulate                        258.4 +/-   16.5         144.7 +/-   16.0     1.79x
forward-wide-16-unbatched-accumulate                           1.7 +/-    0.2           1.2 +/-    0.2     1.39x
forward-wide-128-unbatched-accumulate                         21.6 +/-    2.1           8.6 +/-    2.4     2.51x
forward-wide-512-unbatched-accumulate                        248.7 +/-   14.0         123.3 +/-   13.3     2.02x
forward_backward-square-16-unbatched-accumulate                2.2 +/-    0.5           2.0 +/-    0.5     1.09x
forward_backward-square-128-unbatched-accumulate              13.5 +/-    2.2          14.6 +/-    9.7     0.92x
forward_backward-square-512-unbatched-accumulate             336.8 +/-   71.5         286.3 +/-   81.2     1.18x
forward_backward-tall-16-unbatched-accumulate                  4.0 +/-    1.0           3.5 +/-    0.8     1.14x
forward_backward-tall-128-unbatched-accumulate                55.0 +/-   24.9          49.9 +/-   15.5     1.10x
forward_backward-tall-512-unbatched-accumulate               933.2 +/-  124.7         760.7 +/-  128.5     1.23x
forward_backward-wide-16-unbatched-accumulate                  3.8 +/-    0.6           3.3 +/-    0.6     1.16x
forward_backward-wide-128-unbatched-accumulate                62.7 +/-  193.0          49.0 +/-   13.3     1.28x
forward_backward-wide-512-unbatched-accumulate               907.2 +/-  126.4         711.6 +/-  126.5     1.27x
forward-square-16-batched-accumulate                           1.4 +/-    0.5           1.3 +/-    0.2     1.04x
forward-square-128-batched-accumulate                         15.7 +/-    3.0          15.8 +/-   64.3     0.99x
forward-square-512-batched-accumulate                        273.9 +/-   29.9         276.6 +/-   84.8     0.99x
forward-tall-16-batched-accumulate                             2.1 +/-    0.7           2.1 +/-    0.5     1.01x
forward-tall-128-batched-accumulate                          266.1 +/-   48.0         236.0 +/-   62.6     1.13x
forward-tall-512-batched-accumulate                         5274.0 +/-  460.4        5243.8 +/-  618.5     1.01x
forward-wide-16-batched-accumulate                             2.0 +/-    0.3           2.1 +/-    0.6     0.97x
forward-wide-128-batched-accumulate                          228.6 +/-   56.3         241.0 +/-   47.8     0.95x
forward-wide-512-batched-accumulate                         5421.6 +/-  579.6        5303.3 +/-  576.0     1.02x
forward_backward-square-16-batched-accumulate                  3.5 +/-    0.7           3.3 +/-    1.0     1.06x
forward_backward-square-128-batched-accumulate                82.3 +/-   34.5          80.0 +/-   30.3     1.03x
forward_backward-square-512-batched-accumulate              1928.9 +/-  243.5        2026.3 +/-  222.2     0.95x
forward_backward-tall-16-batched-accumulate                   13.8 +/-    7.4          11.2 +/-    1.8     1.23x
forward_backward-tall-128-batched-accumulate                 596.5 +/-  116.5         600.0 +/-  126.0     0.99x
forward_backward-tall-512-batched-accumulate               12037.0 +/- 1033.4       11994.5 +/-  999.4     1.00x
forward_backward-wide-16-batched-accumulate                   10.1 +/-    1.6          11.0 +/-    8.6     0.93x
forward_backward-wide-128-batched-accumulate                 595.4 +/-  119.5         573.1 +/-  112.2     1.04x
forward_backward-wide-512-batched-accumulate               12517.6 +/- 1081.7       12163.5 +/-  991.9     1.03x

Gemv

Z + alpha * (A @ x) compiles to one Gemv when unbatched. The batched backward is where the branch changes most: the pullback of a matvec with respect to A is a rank-1 BatchedDot over (B, M, 1) @ (B, 1, K), and np.dot per slice paid a copy and a degenerate gemm for each of those.

Batched backward wins by the most of anything here, 1.5x to 17x with a median of 4.1x and all nine cells significant, and batched forward+backward follows at 1.7x to 12x, median 2.9x. The largest ratios are at square-512, where main spends 3.6 ms on a 2 MB rank-1 update. Batched forward is a milder 1.0x to 1.9x.

Nothing loses. The unbatched Gemv (forward-*-unbatched-accumulate) sits at a median of 1.00x, so at n <= 512 it neither costs nor gains anything. All other unbatched rows are flat.

test                                                              main (us)            branch (us)   speedup
forward-square-16-unbatched-product                            1.1 +/-    0.9           1.0 +/-    0.3     1.05x
forward-square-128-unbatched-product                           1.9 +/-    0.7           1.8 +/-    0.3     1.04x
forward-square-512-unbatched-product                           6.7 +/-    1.5           6.7 +/-    1.2     1.00x
forward-tall-16-unbatched-product                              1.7 +/-    0.6           1.6 +/-    0.4     1.03x
forward-tall-128-unbatched-product                             3.2 +/-   22.5           2.9 +/-    0.5     1.10x
forward-tall-512-unbatched-product                            25.8 +/-    5.8          20.7 +/-    3.3     1.25x
forward-wide-16-unbatched-product                              1.5 +/-    0.3           1.5 +/-    0.4     0.99x
forward-wide-128-unbatched-product                             2.8 +/-    0.8           2.7 +/-    0.5     1.06x
forward-wide-512-unbatched-product                            21.9 +/-    3.3          21.4 +/-    2.8     1.02x
backward-square-16-unbatched-product                           1.5 +/-    0.3           1.6 +/-    0.6     0.96x
backward-square-128-unbatched-product                          3.7 +/-    1.0           3.7 +/-    0.8     0.98x
backward-square-512-unbatched-product                        119.5 +/-   32.6         128.1 +/-   23.5     0.93x
backward-tall-16-unbatched-product                             2.7 +/-    0.7           2.8 +/-    0.9     0.99x
backward-tall-128-unbatched-product                            7.7 +/-    2.0           7.7 +/-    1.8     1.00x
backward-tall-512-unbatched-product                          660.3 +/-   95.2         570.7 +/-   92.5     1.16x
backward-wide-16-unbatched-product                             2.8 +/-    0.5           2.8 +/-    1.0     1.00x
backward-wide-128-unbatched-product                            9.5 +/-   29.4           8.7 +/-    1.9     1.09x
backward-wide-512-unbatched-product                          691.5 +/-   72.5         575.7 +/-  120.1     1.20x
forward_backward-square-16-unbatched-product                   1.9 +/-    0.6           1.8 +/-    0.6     1.04x
forward_backward-square-128-unbatched-product                  4.8 +/-    1.4           4.8 +/-    1.4     1.01x
forward_backward-square-512-unbatched-product                134.7 +/-   29.1         136.2 +/-   93.4     0.99x
forward_backward-tall-16-unbatched-product                     3.6 +/-    0.9           3.6 +/-    1.9     1.00x
forward_backward-tall-128-unbatched-product                   10.2 +/-    1.5          10.2 +/-    1.5     1.00x
forward_backward-tall-512-unbatched-product                  534.2 +/-  120.7         553.2 +/-  102.9     0.97x
forward_backward-wide-16-unbatched-product                     3.2 +/-    0.6           3.3 +/-    1.2     0.99x
forward_backward-wide-128-unbatched-product                   10.4 +/-    1.9          10.8 +/-    1.4     0.96x
forward_backward-wide-512-unbatched-product                  538.5 +/-   95.3         550.7 +/-  113.0     0.98x
forward-square-16-batched-product                              1.7 +/-    0.6           1.5 +/-    0.3     1.16x
forward-square-128-batched-product                             7.6 +/-    1.5           5.3 +/-    0.8     1.44x
forward-square-512-batched-product                           105.3 +/-  373.4          82.6 +/-    6.3     1.28x
forward-tall-16-batched-product                                5.8 +/-    1.6           3.5 +/-    0.5     1.64x
forward-tall-128-batched-product                              22.5 +/-   68.0          13.4 +/-    1.5     1.67x
forward-tall-512-batched-product                             582.0 +/-   45.7         524.0 +/-   30.9     1.11x
forward-wide-16-batched-product                                3.1 +/-    0.7           2.8 +/-    0.5     1.10x
forward-wide-128-batched-product                              22.7 +/-  403.2          11.9 +/-    1.5     1.92x
forward-wide-512-batched-product                             499.5 +/-   24.5         487.0 +/-   21.0     1.03x
backward-square-16-batched-product                             3.1 +/-    0.5           2.1 +/-    0.4     1.50x
backward-square-128-batched-product                           92.7 +/-   36.1          15.3 +/-    2.5     6.06x
backward-square-512-batched-product                         3602.6 +/-  995.2         210.3 +/-   22.6    17.13x
backward-tall-16-batched-product                               8.7 +/-    2.1           4.4 +/-    0.8     1.98x
backward-tall-128-batched-product                            639.4 +/-   77.9         241.9 +/-   62.9     2.64x
backward-tall-512-batched-product                          23571.4 +/- 3980.5        5756.5 +/-  455.7     4.09x
backward-wide-16-batched-product                              10.9 +/-    4.2           4.1 +/-    1.5     2.65x
backward-wide-128-batched-product                           1764.6 +/-  245.4         246.0 +/-   56.2     7.17x
backward-wide-512-batched-product                          30542.7 +/- 2028.4        6213.7 +/-  707.2     4.92x
forward_backward-square-16-batched-product                     5.0 +/-   10.9           2.8 +/-    0.7     1.82x
forward_backward-square-128-batched-product                  116.0 +/-   49.6          20.7 +/-    3.6     5.61x
forward_backward-square-512-batched-product                 3407.9 +/-  421.1         281.4 +/-   22.7    12.11x
forward_backward-tall-16-batched-product                      13.4 +/-    1.7           7.9 +/-    1.2     1.71x
forward_backward-tall-128-batched-product                    652.2 +/-  105.7         274.4 +/-   58.6     2.38x
forward_backward-tall-512-batched-product                  17788.1 +/-  866.0        6245.3 +/-  636.4     2.85x
forward_backward-wide-16-batched-product                      12.5 +/-    1.6           6.4 +/-    1.1     1.96x
forward_backward-wide-128-batched-product                    784.1 +/-  150.5         241.8 +/-   55.7     3.24x
forward_backward-wide-512-batched-product                  19561.9 +/-  829.2        6182.8 +/-  576.9     3.16x
forward-square-16-unbatched-accumulate                         1.2 +/-    0.4           1.2 +/-    0.4     1.02x
forward-square-128-unbatched-accumulate                        2.0 +/-    0.6           2.0 +/-    0.3     0.99x
forward-square-512-unbatched-accumulate                        6.5 +/-    1.5           6.6 +/-    1.0     0.99x
forward-tall-16-unbatched-accumulate                           1.9 +/-    0.7           2.0 +/-    0.4     0.94x
forward-tall-128-unbatched-accumulate                          3.2 +/-    1.5           3.1 +/-    0.6     1.01x
forward-tall-512-unbatched-accumulate                         25.2 +/-    8.0          21.7 +/-    3.7     1.16x
forward-wide-16-unbatched-accumulate                           1.7 +/-    1.9           1.7 +/-    0.3     1.00x
forward-wide-128-unbatched-accumulate                          5.3 +/-  153.8           2.9 +/-    0.4     1.79x
forward-wide-512-unbatched-accumulate                         22.1 +/-    2.8          22.6 +/-    2.6     0.98x
forward_backward-square-16-unbatched-accumulate                2.2 +/-    0.6           2.1 +/-    0.7     1.03x
forward_backward-square-128-unbatched-accumulate               5.2 +/-    1.0           5.2 +/-    1.0     1.01x
forward_backward-square-512-unbatched-accumulate             129.6 +/-   32.7         134.5 +/-   35.4     0.96x
forward_backward-tall-16-unbatched-accumulate                  4.0 +/-    1.0           4.1 +/-    1.2     0.98x
forward_backward-tall-128-unbatched-accumulate                10.9 +/-    2.1          10.8 +/-    1.9     1.01x
forward_backward-tall-512-unbatched-accumulate               541.7 +/-  121.7         562.3 +/-  105.5     0.96x
forward_backward-wide-16-unbatched-accumulate                  3.6 +/-    0.5           3.8 +/-    0.7     0.96x
forward_backward-wide-128-unbatched-accumulate                10.8 +/-    1.2          11.2 +/-    1.5     0.96x
forward_backward-wide-512-unbatched-accumulate               561.6 +/-  113.3         552.5 +/-  113.0     1.02x
forward-square-16-batched-accumulate                           1.9 +/-    0.7           1.6 +/-    0.4     1.21x
forward-square-128-batched-accumulate                          7.9 +/-    1.7           5.7 +/-    0.7     1.38x
forward-square-512-batched-accumulate                         86.6 +/-   18.9          70.7 +/-    6.8     1.22x
forward-tall-16-batched-accumulate                             6.0 +/-    1.4           3.8 +/-    0.6     1.57x
forward-tall-128-batched-accumulate                           20.9 +/-    2.7          14.4 +/-    3.7     1.45x
forward-tall-512-batched-accumulate                          594.8 +/-   46.7         521.0 +/-   29.3     1.14x
forward-wide-16-batched-accumulate                             3.5 +/-   10.6           3.0 +/-    0.6     1.16x
forward-wide-128-batched-accumulate                           18.6 +/-  230.4          12.6 +/-    1.8     1.48x
forward-wide-512-batched-accumulate                          491.7 +/-   18.9         488.6 +/-   21.0     1.01x
forward_backward-square-16-batched-accumulate                  4.5 +/-    1.3           3.2 +/-    1.1     1.43x
forward_backward-square-128-batched-accumulate                83.3 +/-   10.0          21.5 +/-    2.9     3.88x
forward_backward-square-512-batched-accumulate              3332.6 +/-  340.0         284.5 +/-   26.8    11.71x
forward_backward-tall-16-batched-accumulate                   14.0 +/-    1.6           8.4 +/-    1.3     1.67x
forward_backward-tall-128-batched-accumulate                 699.5 +/-  264.0         270.1 +/-   67.6     2.59x
forward_backward-tall-512-batched-accumulate               17825.0 +/-  499.5        6298.5 +/-  584.6     2.83x
forward_backward-wide-16-batched-accumulate                   12.9 +/-    1.8           6.8 +/-    1.0     1.90x
forward_backward-wide-128-batched-accumulate                 748.8 +/-  109.5         245.4 +/-   58.8     3.05x
forward_backward-wide-512-batched-accumulate               19470.0 +/-  781.3        6293.3 +/-  516.7     3.09x

SGD step on a two-layer MLP

tanh(X @ W1 + b1) @ W2 + b2 with a squared loss and a plain p - lr * g update on shared weights. The two weight updates are W - lr * (X.T @ dY), which the extended rewrite folds into an inplace Gemm on W. The forward X @ W + b stays a Dot because the bias only broadcasts against the product. Sizes are (batch, n_in, n_hidden): small (64, 32, 64), medium (256, 128, 256), large (1024, 512, 512).

Neutral, 0.97x to 1.04x, all inside one stddev. The fused inplace Gemm update saves an allocation and a pass over W per step, but at these sizes the two forward Dots and the elementwise work dominate, and the Gemm table above shows the unbatched Gemm is no faster than np.dot. This is the graph where I saw the 10-15% win on the backward pass earlier. That win came from lowering a bare Dot through gemm with transpose flags, and #2348 on main has since delivered the same copy elimination on its own, so the branch no longer changes the linear layer's backward.

test                main (us)            branch (us)   speedup
small           26.2 +/-    2.9          26.4 +/-    2.1    0.99x
medium         371.9 +/-   16.0         383.7 +/-   19.3    0.97x
large         5449.3 +/-  166.3        5231.6 +/-  135.8    1.04x

@ricardoV94

ricardoV94 commented Sep 14, 2026

Copy link
Copy Markdown
Member

I'm running an adversarial check on those claims. I am highly skeptical of the "when batched we now avoid copy" and all that arguing because we were making use of np.dot(out=...) argument for Blockwise already. The GER I'm also not sure is a fair comparison vs np.dot instead of a pure elemwise impl. GEMV is the most believable (and also nicest looking) bench, I hope it holds.

@ricardoV94

ricardoV94 commented Sep 14, 2026

Copy link
Copy Markdown
Member

Bot analysis and guided reply, It argues against BLAS pipeline on the pytensor graph vs just giving the right hints? Wanna confirm these results on your side @jessegrabowski and push back again?


I reproduced the large batched-matvec backward speedups. My main takeaways are that ordinary GEMM is roughly a wash, the GEMV workload benefits mainly from avoiding input copies, and GER should be dropped from the Numba backend (I was on the fence on C, I think also there) in favor of fused Elemwise outer products.

These checks use full pytensor.function(..., mode="NUMBA") calls, Numba 0.65.1, float64, single-thread OpenBLAS, and 24 interleaved rounds on a Ryzen 5 PRO 7540U laptop. Compilation, warm-up, numerical checks, and allocation profiling are outside timing. Batched comparisons use base 44373b464 and PR 1b0569527; unbatched comparisons retain the previous Dot + Elemwise path by excluding blas_fusion. Every comparison measures its alternatives together; no historical timings are used.

GEMM: ordinary dense matrix-matrix accumulation showed no substantial improvement over the previous code: approximately 4.52 ms before versus 4.53 ms with the PR for the tested 512² case. The large regression below is specifically an outer-product case.

GEMV: the batched backward gains are real, but a layout guard around the existing np.dot(out=...) recovers them without custom BLAS dispatch. For y = (A @ x[:, :, None])[:, :, 0], differentiating with respect to A and x already makes the matrix gradient an Elemwise outer product on both commits. The expensive product is the vector gradient, A.T @ g.

Numba infers those transposed matrix slices as arbitrary-layout arrays and copies them before calling BLAS, even though each slice is F-contiguous at runtime. I verified this in the actual compiled function's types and LLVM. Adding out= alone leaves those matrix copies. A C/F guard using asfortranarray or ascontiguousarray avoids them; the pointer check confirms no copy for these slices.

The guard inside Blockwise's core Dot lowering looks like this (b and out are already contiguous here):

if a.flags.f_contiguous:
    np.dot(np.asfortranarray(a), b, out)
else:
    np.dot(np.ascontiguousarray(a), b, out)

The helpers give Numba a C/F-typed operand; checking the flags alone leaves its inferred type unchanged and still triggers the copy.

For the full backward function with eight 512² matrices, one interleaved run gives:

Path Time (ms)
Previous default BatchedDot 17.19
Previous Blockwise(Dot), already using out 17.54
Same Blockwise(Dot), adding only the layout guard 1.79
PR Blockwise(Gemv), starting from empty 3.42

Guarded dot matches PR Gemv at 128² and beats it at 512² in these checks. Blockwise(Gemv) also retains the gains when starting from zeros. Unbatched Gemv itself was roughly neutral. BatchedDot is therefore unnecessary for retaining these gains and can be removed in a follow-up PR, provided its Blockwise replacement handles input layout.

GER: I would remove this specialization and lower outer-product updates to fused broadcast multiplication, including alpha/beta. For a 512² update with the accumulator unchanged, the previous path takes 100.2 µs, PR Ger 44.6 µs, and fused Elemwise 43.8 µs. The non-inplace Ger implementation is itself a fused loop. Actual inplace BLAS GER also loses to inplace Elemwise here: 31.6 versus 28.7 µs.

This also addresses the major regression: 0.7*A + alpha*outer(x, y) selects GEMM and takes 264.7 µs, versus 102.8 µs previously and 43.5 µs with fused Elemwise. Outer products should stay in the fusion path even when beta differs from one.

Since Numba's np.dot already calls BLAS, these results favor fixing its input-layout handling and using ordinary fusion. What additional measured benefit over that baseline justifies the custom BLAS layer? BLAS-compatible noncontiguous layouts may offer one, but the large speedups reproduced here do not require it.

@jessegrabowski

Copy link
Copy Markdown
Member Author

I reproduced your timings. So the PR is much less of a win than I was hoping.

What the guard can't do is a slice that's neither C nor F. matmul(A[:, :, :n], X) with A (8, n, 2n):

                  n=128    n=512
PR (lda)           83.6    3652
np.dot guarded    141.2    5584
np.dot plain      233.2    8020

np.dot has no lda, so the guard copies. Unbatched it's 10-13% at n >= 512.

Inplace accumulation Z <- Z + alpha * A @ X (both paths inplace) is also a small win at moderate sizes:

                    n=128    n=512    n=2048
Gemm{inplace}        20.3      402    24003
np.dot + Elemwise    15.9      484    25863

My preference is to merge it as-is because that's less work for me and it seems like a push at worst with maybe some fringe benefits. The machinery is definitely cleaner. I'm open to making changes if you insist.

Introduce the singledispatch c_funcify registry returning detached CImpl
implementations, resolve CLinker through it, and route OpWiseCLinker, the
VM, and DebugMode to the dispatched C thunk with a Python fallback.
The deleted make_c_gemv_destructive also duplicated a shared AllocEmpty so each Gemv could destroy its own buffer, so test_multiple_inplace fails until the generic replacement lands two commits later.
Without this the rewrite that introduces Ger regresses against the elemwise it replaces, since numba falls back to object mode for it.
Copying the accumulator in and letting BLAS scale it on top touches the output twice, which cost more than the elemwise dot-and-add these ops replace.
@ricardoV94

ricardoV94 commented Sep 14, 2026

Copy link
Copy Markdown
Member

blas can handle strided inputs? Is that general across Blas implementations? I was under the impression from the C work that it must be contiguous and the only clever thing we could do is handle negative [::-1] strides.

On my end we can go ahead but I'd not include any blas rewrites in numba vs just having the better dot/batched dot dispatch.

@ricardoV94

Copy link
Copy Markdown
Member

One other advantage of our own dot dispatch is that we avoid the numba spurious contiguity warning?

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

Labels

enhancement New feature or request linalg Linear algebra

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants