Three kernels or one
Take three vectors and compute (a + b) × c, then scale the
result. Written down it is one expression. Run unoptimised, it is three separate kernel
launches.
A kernel ends by writing its output to the card's memory, because that is the only place the next kernel can find it. So the intermediate values in the middle of the expression get written out and read straight back in, for no reason other than that a kernel boundary fell between them.
The arithmetic is not the cost
One add, one multiply, one divide per element. Twenty-one thousand lanes dispatch that almost instantly. What they cannot skip is the waiting. Memory sits far from the lanes and runs far slower than they do, so the cores idle while numbers travel back and forth. The expense of this sequence is the six trips, not the three operations.
Fusion
Do all three inside one kernel. Read a, b and c
once, hold the intermediates in registers sitting right beside the lane, write the answer
once. Two trips instead of six, for identical arithmetic.
Drawing the boundary yourself
Nothing about the hardware forced three launches. The boundaries came from the fact that
add, multiply and divide are three library functions,
each written to be a complete kernel on its own. Call them in sequence and you get their
boundaries whether you wanted them or not.
A CUDA kernel is where you draw the boundary instead. You write one function that takes
a, b and c, does the whole expression for one element,
and returns the answer. The intermediates never become tensors. They are values in a
register that exist for a few instructions and are gone. Three stages collapse into one.
What "one kernel" actually costs you
The fused version is faster and it is also less flexible. Each library op was general:
any shape, any dtype, composable with anything. The fused kernel does one specific
sequence. That trade — generality for trips to memory — is the whole reason
torch.compile exists, since it tries to find the fusions automatically
instead of making you hand-write them.
This single move explains a great deal of production deep learning tooling.
torch.compile looks for chains of small operations and welds them together.
FlashAttention is the same idea applied to the attention computation, which is otherwise a
parade of large intermediates written out and read back. Fused optimizers do it for the
update step. Different problems, one technique: stop bouncing intermediates off memory.
The same lesson, one order of magnitude out
Inside one card, an unfused kernel wastes trips to local memory, and that is already enough to dominate the runtime. Between cards, every exchange crosses a network that is slower still by a wide margin. Once we start splitting a model across several GPUs, the question on every page will be the one this page asks: how many times does data have to move, and can we move it less?