TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Graph Compilers and Runtimes: Operator Fusion, torch.compile, ONNX Runtime, and TensorRT

Status: Draft (2026-10-16), partially measured. torch.compile, memory layout, and cuDNN settings were measured using single-use pods on a single 8GB laptop-grade GPU in the lab (PyTorch 2.9, median of 50 runs × 5 batches after 20 warm-up runs). The principles of fusion were verified using a laptop CPU example (code/03_fusion.py, taking several seconds). ONNX Runtime and TensorRT were not measuredas they were absent from the experimental image (not downloaded). Part 3 of the "Reading Vision Models" series. Precision reduction is covered in Part 2.

Table of Contents

1. Intuition: The Chef and the Refrigerator

While a GPU's compute units are fast, data resides in memory outside the chip (the refrigerator). Eager execution performs "convolution → batch normalization → ReLU → addition" separately. The convolution result goes into the refrigerator, batch normalization takes it out, modifies it, and puts it back, and then ReLU takes it out again and puts it back. For large feature maps, the time spent moving data in and out exceeds the computation time. Operator fusionmeans "taking it out once, doing all four operations, and putting it back only once."

연산 융합과 컴파일러·런타임 융합 전: 연산마다 커널 하나, 메모리를 오간다 합성곱 BN ReLU 더하기 GPU 메모리 — 읽고 쓰기 4번, 커널 부르기 4번 융합 뒤: 한 커널이 값을 칩 안에 둔 채 끝까지 합성곱 + BN(가중치에 접음) + ReLU + 더하기 GPU 메모리 — 읽고 쓰기 1번, 커널 부르기 1번 같은 모델이 GPU 커널이 되기까지 두 길 PyTorch 모델 torch.compile 그래프 잡기 Inductor Triton 커널 생성 ONNX 파일 표준 그래프 ONNX Runtime TensorRT 엔진
Figure: Before fusion, each operation dispatches a kernel that accesses memory back and forth. After fusion, a single kernel keeps the values inside the chip all the way through. Below are the two paths where a PyTorch model becomes a GPU kernel via torch.compile or ONNX.

There are two types of fusion:

BN 접기:  y = γ·(W∗x + b − μ)/√(σ²+ε) + β
        = (γ/√(σ²+ε))·W ∗ x + (γ·(b − μ)/√(σ²+ε) + β)
        = W' ∗ x + b'           ← 합성곱 하나로

2. Try It Yourself (Laptop CPU)

code/03_fusion.py was run using a single laptop CPU thread (median of 7 runs).

Experiment Result
Convolution (32→64, 3×3, input 144×256) + folding BN Max difference 4.77e-06 (FP32 rounding level). Time 5.80 → 5.80ms (no difference)
Element-wise chain relu(x·1.5 + 0.5)·0.9, 16.77 million elements Eager execution 9.7ms → torch.compile 2.4ms (4.1x speedup), zero result difference
Same chain implemented manually via in-place operations 7.4ms
Calling addition on 1.02 million elements 1,000 times separately 0.13 → 0.88ms, approx. 0.8µs per call

3. What Graph Compilers Do

TVM (Chen et al., 2018) receives a model as a graph and classifies operations into four categories: element-wise (injective), reduction, complex operations that can attach element-wise work to the output (complex-out-fusable, such as convolution), and unfusable operations. It groups the first three according to rules. Operator fusion alone provided a 1.2–2x speedup (2015 desktop-class GPU, 4 tasks including convolution+BN+ReLU, Section 3, Figure 4), and achieved 1.6–3.8x end-to-end compared to other frameworks (Section 6.1).

PyTorch 2 (Ansel et al., 2024) 's torch.compile consists of two parts:

On datacenter GPUs, inference for 62 TIMM image models was 2.48x faster in FP32 and 2.77x faster in FP16 compared to eager execution (Table 3). Disabling both fusion and inlining actually made it slower at 0.80x relative to eager execution (Table 4, HuggingFace FP16 inference). This means most of the speedup comes from fusion. Since we could not verify in the text which combination the green "2.27x inference" refers to, we rely only on the values from Tables 3 and 4 here.

4. ONNX and Runtimes: Portable Model Formats

torch.compile runs within PyTorch. If you do not want to keep Python and PyTorch on the server, or if you need to run the model in a browser or phone, you must export the model into a standard graph file.

5. Our Measurements: Applying torch.compile to our Ball Model

We measured TT Score's ball model (BlurBall, 3-frame window, 512×288, batch size 1) on our experimental GPU.

Execution Method FP32 FP16 First Call (Compilation)
Eager execution (current production mode) 15.09ms 9.77ms —
Eager execution, cuDNN auto-tuning disabled 15.08ms 9.75ms —
Eager execution, channels_last (NHWC) 18.62ms 9.02ms —
torch.compile default 12.91ms (1.17x) 4.61ms (2.12x) 10–11 sec
torch.compile reduce-overhead (CUDA graphs) 12.55ms 4.34ms (2.25x) 7–8 sec
torch.compile max-autotune (without CUDA graphs) 12.79ms 4.48ms 41–46 sec

(FP16 uses autocast. Multipliers are relative to eager execution at the same precision.)

Profiling the same execution revealed that a single eager FP32 run invoked 478 CUDA kernels, and out of 14.88ms of active GPU time, convolutions took 8.09ms (72 times) and batch normalization took 3.27ms (71 times, 22%). Since the sum of kernel times roughly matched the wall-clock time (15.09ms), the GPU was never idle. Therefore, the bottleneck was not dispatch overhead, but memory-bound element-wise operations, and we interpret that the compiler eliminated these by fusing them (the number of kernels in the compiled version was not counted).

Here is how to read these results.

6. Limitations

7. Connection to Our Project

A word from Titi. We cooked the exact same dish while opening the refrigerator door less often, and it became twice as fast—even though the ingredients (weights) and recipe (model) stayed the same!

References

Sign in to like

Comments

No comments yet.

Sign in to leave a comment