SAKSHAM KAPOOR / SELECTED WORKPYTORCH INDUCTOR · #189127 · CASE STUDY

The same answer. A better opportunity for the compiler.

An optimization can be hidden in the way a calculation is written. My PyTorch contribution exposed one such opportunity. Follow a tiny example from the original expression to the guarded compiler rewrite.

Read the implementation walkthrough and contributor guide ↓
Chapter 1 of 6

Start with the work the model wants done.

One matrix from a batch · illustrative values
1. Multiply & scale

2 × matrix product

2. Add bias

Add [−5, 1] to each row

Bias is its own step
3. Apply ReLU

Negative values become 0

Follow the numbered steps. The intermediate −3 becomes 0; the other entries stay unchanged.

Multiply matrices, add an adjustment, then set negative values to zero. Keep the final answer in view.

Illustrative values and grouping, not an execution trace or a speed benchmark.

Imagine following one small calculation inside a neural network. First, two matrices are multiplied. A matrix is simply a rectangular array of numbers; a batch is a collection of such calculations. Our example product is [[1, 4], [3, 8]], which we scale by 2.

Next comes a bias: a row of adjustments reused across every row of the result. Here the bias is [-10, 2], scaled by 0.5 to become [-5, 1]. Adding it gives [[-3, 9], [1, 17]]. Finally, ReLU sets the negative entry to zero. The answer is [[0, 9], [1, 17]].

What to take from this stepThese are three understandable calculations. The compiler can represent and schedule them in different ways.

1 / 6 · Your pace

Scroll through the story, or choose a chapter.

01 / The calculation

Start with the work the model wants done.

Imagine following one small calculation inside a neural network. First, two matrices are multiplied. A matrix is simply a rectangular array of numbers; a batch is a collection of such calculations. Our example product is [[1, 4], [3, 8]], which we scale by 2.

Next comes a bias: a row of adjustments reused across every row of the result. Here the bias is [-10, 2], scaled by 0.5 to become [-5, 1]. Adding it gives [[-3, 9], [1, 17]]. Finally, ReLU sets the negative entry to zero. The answer is [[0, 9], [1, 17]].

What to take from this stepThese are three understandable calculations. The compiler can represent and schedule them in different ways.

02 / The hidden step

Two calculations arrive in one package.

The developer can express the first two calculations with one PyTorch operation called baddbmm. Its name is less important than its job: a batch matrix multiplication plus an addition. In the diagram, the first two boxes now sit inside one boundary.

Inductor, PyTorch's compiler, sees a graph: operations connected by the values they produce. The original report identified a missed opportunity around broadcast bias and a following activation. Looking only at the bundled operation can miss a useful arrangement with its neighbor. The problem is how the work is represented, not an incorrect mathematical answer.

What to take from this stepTo improve a compiler, inspect the surrounding operations as well as the operation named in the issue.

03 / The boundaries

Before rewriting, ask when it is appropriate.

It is tempting to split the package everywhere. But a compiler serves many shapes, devices, and numeric precisions. A change that helps one arrangement may be unnecessary or harmful in another. That makes the conditions part of the implementation, not an afterthought.

My eligibility check requires GPU execution, a bias-like or broadcast input, and only pointwise consumers: operations that act independently on each element. The replacement also follows the existing half-precision configuration guard. Precision is the numeric format used to store values; changing it can change rounding behavior. A standalone operation is covered by a test that keeps its existing external baddbmm call.

What to take from this stepA strong optimization defines where to apply it and where to preserve the original behavior.

04 / The rewrite

Open the package without changing the calculation.

For eligible cases, I extended Inductor's graph-pattern machinery to replace the bundled form with a batch matrix multiply and a separate bias addition. Watch the bias label move out of the original package. The values on the screen remain the same.

This is a graph rewrite: changing the compiler's intermediate description of the program. Both scaling factors must survive it. Dropping alpha or beta would make our example wrong, even if an example with both factors equal to one happened to pass. The implementation constructs the replacement from the original operands and factors.

What to take from this stepMore visible operations in an intermediate graph can reveal opportunities to do less separate work later.

05 / The opportunity

Now the adjustment can join its next-door neighbor.

Once the bias addition is visible, it no longer has to be considered only as part of the matrix operation. It can participate in normal fusion with the activation that follows. Fusion means combining compatible work into one generated unit of GPU execution, called a kernel.

The highlighted boundary now moves to the last two boxes. The matrix product stays outside that illustrative group. Combining the addition and activation can avoid writing a temporary array to GPU memory and reading it back for the next operation. My contribution exposes this opportunity to the existing compiler machinery; the resulting execution and performance still depend on the workload and backend.

What to take from this stepThe contribution changes which optimizations the compiler can see, while reusing its existing fusion machinery.

06 / The evidence

Prove the answer and the intended rewrite.

A compiler test needs to answer two different questions. Did the result stay correct? And did the compiler actually take the intended path? An output comparison alone could pass even if the new optimization never ran.

The merged tests cover a standalone operation, a GELU consumer, an explicitly expanded stride-zero bias, and non-default alpha/beta values. They check outputs and look for the expected external bmm or baddbmm calls in generated code. The upstream commit records the resolved PR and approval. Benchmark discussion includes mixed outcomes, so the achievement is a guarded, tested compiler capability rather than one universal speedup number.

What to take from this stepRead the implementation, regression tests, and performance measurements as three different pieces of evidence.

Technical details for this chapter

This toy example illustrates beta * input + alpha * bmm(A, B), followed by ReLU. It uses alpha=2 and beta=0.5; it is not a measured workload.

The intended calculation stays: ReLU(β × bias + α × (A @ B))

The example uses one matrix from a batch and ReLU, which sets negative values to zero. The merged tests also cover GELU. Floating-point behavior and device configuration are reasons for the rewrite's precision guards.

Inside the contribution

From the idea to the upstream implementation

The animation shows the idea. This guide explains the compiler concepts, the decisions inside my patch, and how to study a similar open-source problem yourself.

1. Why the same math can produce different programs

PyTorch lets developers describe calculations. Its Inductor compiler decides how to turn those calculations into executable work. A graph is the compiler's representation of the operations and the values flowing between them. Changing that representation can reveal an optimization without changing the intended calculation.

Batch matrix multiply
Multiply several pairs of matrices together. Think of processing a stack of small spreadsheets, one pair at a time.
Broadcast bias
Add a smaller array of adjustments across a larger result. A row of adjustments can be reused across every row of a matrix.
Pointwise operation
Apply a calculation independently to each element. ReLU replaces negative values with zero; GELU is another activation used in neural networks.
Fusion
Combine compatible work into one generated kernel, which is a unit of GPU execution. This can avoid intermediate memory traffic and separate launches.

The operation baddbmm combines a batch matrix multiplication with an addition. The original issue showed why separating out a broadcast bias could help when an activation followed it. Optimizing an operation in isolation could miss the better arrangement across its boundary.

2. What I changed in the compiler

I extended an existing optimization for addmm to eligible baddbmm operations. The contribution reused Inductor's graph-pattern machinery and normal pointwise fusion, rather than introducing a separate hand-written GPU kernel.

Before: activation(baddbmm(bias, A, B, alpha=α, beta=β))
After:  activation(β * bias + α * bmm(A, B))

This is a simplified view of the rewrite. The multiplication stays a matrix multiplication, but the bias addition becomes a visible graph node. The compiler can then consider combining it with the activation. More nodes in the intermediate graph can therefore lead to less separate work in the generated program.

A small worked example. Suppose one matrix product produces [[1, 4], [3, 8]], the broadcast bias is [-10, 2], alpha is 2, and beta is 0.5.

Scaling the product gives [[2, 8], [6, 16]]. Adding the scaled bias [-5, 1] to each row gives [[-3, 9], [1, 17]]. ReLU then gives [[0, 9], [1, 17]].

Both representations describe this calculation. The optimization concerns how the compiler schedules it, not changing the answer to get a faster result.

In the merged implementation, start with should_prefer_unfused_baddbmm, then read unfuse_bias_baddbmm_to_pointwise. The first decides eligibility; the second constructs the replacement graph. Separating those responsibilities makes the optimization easier to reason about.

3. Why this is more than an algebraic substitution

Production compiler work has to respect the surrounding graph and hardware behavior. My patch includes several boundaries on when the rewrite applies:

  • GPU and bias shape: the eligibility check requires GPU execution and an input recognized as bias-like or broadcast.
  • Consumers: the result's users must be pointwise operations. The standalone case is explicitly covered by a test that keeps baddbmm.
  • Scaling: the replacement retains both alpha and beta. Assuming they are always one would change valid programs.
  • Precision: the rewrite follows the existing half-dtype configuration guard, including its XPU-specific exception. A useful transformation for one precision or device cannot simply be applied everywhere.

These decisions are visible in the guard and replacement functions. The engineering challenge is selecting a useful transformation while preserving the cases where existing behavior should remain.

4. How to tell whether a compiler change actually worked

A correct output alone cannot show that an optimization fired. The new pattern-matcher tests check numerical results and inspect the generated code.

Look for test_unfuse_broadcast_bias_baddbmm and test_unfuse_broadcast_bias_baddbmm_alpha_beta. Together, they show how a small regression test can protect both the intended transformation and behavior that should stay unchanged.

What the merged tests establish
CaseEvidence
Standalone operationThe output matches PyTorch and the generated code retains the external baddbmm call.
GELU consumerThe output matches the reference; the code uses bmm in place of the external baddbmm call.
Explicitly expanded biasA stride-zero expanded input is checked, so broadcasting behavior is covered.
Non-default scalingA ReLU case exercises alpha 0.8 and beta 0.2, with output and code checks.

The PR discussion also records environment setup problems, CI iteration, and benchmark results. Later measurements include near-parity cases and both faster and slower decomposed examples. The defensible outcome is a merged, guarded optimization opportunity; a universal speedup would require evidence the record does not provide.

For a newcomer, keep three questions separate: Does the answer match? Did the intended code change happen? Does that change help this workload? Each needs its own check.

5. A route into this kind of open-source work

  1. Start with a reproducible issue. Read the input shapes, device, precision, expected behavior, and measurements in issue #187093. Reduce the problem to one operation and one consumer.
  2. Find the nearest existing solution. Compare the addmm pattern with my baddbmm extension in the same file. List what can be reused and what changes for batched inputs.
  3. Read a focused test before the whole compiler. Trace one test from its inputs to its reference output and generated-code assertions.
  4. Set up the project's development environment. Use PyTorch's contributing guide. Confirm your tests import the intended build; an unrelated installed wheel can make a source change appear ineffective.
  5. Add a negative case. Ask when your proposed optimization should not run. Then add the smallest regression test that captures that boundary.

Try it yourself: use the worked example above to calculate the answer before looking at the result. Then read the alpha/beta test and identify the assertion that checks correctness and the assertion that checks the rewrite.

This exercise needs no GPU. Running the upstream compiler tests does require the repository's supported build and hardware setup.

6. What this contribution demonstrates

I contributed at the boundary between mathematical equivalence, graph representation, and generated execution. The work combines pattern matching, broadcast semantics, device and precision guards, and regression tests inside PyTorch's compiler.

The upstream commit records the resolved PR and approval by Jason Ansel. For someone learning open source, the useful model is a focused change that fits the existing architecture and carries evidence for its behavior.

Press ⌘+K to search