Optimize PyG with torch.compile
Source title: Speeding Up Graph Learning Models with PyG and torch.compile
Authors: Akihiro Nitta and Matthias Fey
Published: April 2025
Why compilation matters for graph learning
PyTorch 2.0 introduced torch.compile, a JIT compiler that fuses GPU kernels, eliminates Python overhead, and can speed up standard deep learning workloads.
Graph neural networks (GNNs) present unique challenges that make compilation less straightforward than calling torch.compile(model).
The core difficulty is irregular data. Every GNN mini-batch can have a different number of nodes, edges, and neighbors. Neighbor sampling produces subgraphs of varying shapes across iterations. That variability triggers re-compilations, graph breaks, and host-device synchronization that can erase the performance gains from compilation.
Kumo’s production stack for Relational Deep Learning (RDL) constructs a large heterogeneous graph from relational database tables. Each table becomes a node type, each row becomes a node, and primary key-foreign key pairs define edges. The system uses PyTorch Geometric (PyG) for message passing, PyTorch Frame for multi-modal feature encoding, and PyTorch Lightning for training orchestration.
This guide covers the optimizations that produced 30-35% training speedups on real workloads without sacrificing model accuracy. Each technique applies to PyG-based pipelines.
Graph learning workloads have irregular, variable-sized inputs that make torch.compile harder to apply than on standard vision or language models.
The optimizations in this guide address that irregularity.
Avoid unnecessary re-compilations
The largest source of wasted time with torch.compile in graph learning is re-compilation.
By default, PyTorch compiles an optimized kernel for the exact input shapes it sees.
When the next mini-batch has different shapes, which happens every iteration in GNNs due to neighbor sampling, PyTorch re-compiles from scratch.
That can make training slower than eager mode.
Dynamic input shapes
In a standard image pipeline, every batch has the same tensor dimensions. In graph learning, the number of sampled nodes and edges changes per batch. Without intervention, each new shape triggers a full re-compilation.
Use torch.compile(dynamic=True).
This tells the compiler to generate kernels that handle variable shapes from the start, using symbolic shape analysis instead of hard-coding dimensions.
The compiled kernel works for different input sizes without re-compiling.
Think of the default mode as a conveyor belt sized exactly for the first box it sees, which must be rebuilt for every differently sized box.
dynamic=True creates an adjustable belt that handles different box sizes from the start.
Learning-rate schedulers
torch.compile treats floating-point scalar arguments as compile-time constants.
When a learning-rate scheduler updates a floating-point learning rate at each step, the compiler can treat it as a new constant and re-compile.
Wrap the learning rate in a tensor instead.
The tensor is treated as a runtime value, so scheduler updates no longer trigger re-compilation.
Use torch.compile(dynamic=True) for variable input shapes and a tensor learning rate for scheduler compatibility.
Both are one-line changes that remove common sources of re-compilation in GNN training.
Graph breaks and CUDA Graphs
Even after re-compilations are removed, torch.compile can underperform when the computation graph is fragmented.
The main considerations are graph breaks and whether to enable CUDA Graphs.
Graph breaks
A graph break occurs when torch.compile encounters an operation it cannot trace through.
The compiler splits the computation into separately compiled subgraphs, preventing kernel fusion across the break boundary.
In PyG, tuple keys in ModuleDict lookups for heterogeneous graphs can cause graph breaks.
Converting tuple keys to strings lets the compiler trace through the full forward pass.
Staying on the latest PyG release also incorporates upstream torch.compile compatibility improvements.
CUDA Graphs: when not to use them
CUDA Graphs record a sequence of GPU kernels and replay them as one unit, removing per-kernel launch overhead. They are useful for fixed-shape workloads such as image classification or transformer inference with fixed sequence lengths.
For GNN training with neighbor sampling, CUDA Graphs are counterproductive. The node count in each mini-batch differs across iterations. CUDA Graphs must re-record the kernel sequence when shapes change, increasing memory usage and slowing training.
The Kumo pipeline disables CUDA Graphs:
Update PyG and replace incompatible patterns such as tuple keys in ModuleDict to fix graph breaks.
Disable CUDA Graphs for neighbor-sampled GNN training because variable mini-batch sizes make repeated kernel recording counterproductive.
Eliminate host-device synchronization
Removing unnecessary synchronization between the CPU (host) and GPU (device) is often the highest-impact optimization category. Each synchronization forces the GPU to finish queued work before the CPU can proceed, creating a pipeline bubble. These points can dominate training time more than computation itself.
Metric accumulation on the CPU
Accumulating metrics such as accuracy on the CPU forces synchronization each iteration.
The int() call transfers data from device to host and blocks until pending GPU kernels finish.
Keeping the running total as a tensor transfers the final accumulated value to the CPU only once per epoch.
torch.repeat_interleave without output_size
GNN message passing often uses torch.repeat_interleave to expand node features along edges.
Without output_size, PyTorch must synchronize to calculate the output tensor size before allocating memory and launching kernels.
When edge counts are known from the graph structure, passing the size lets PyTorch allocate memory and launch kernels without waiting.
Third-party library synchronization
Even well-maintained libraries can introduce hidden synchronization.
Older torchmetrics releases, for example, introduced device synchronization during metric computation.
Profile before and after library upgrades, and pin versions that do not introduce synchronization.
Removing host-device synchronization is like letting a factory assembly line run continuously instead of stopping it to count finished products after every item.
Keep metric accumulation on the GPU, provide output_size to torch.repeat_interleave, and update libraries that introduce hidden synchronization.
The full optimization pipeline
These techniques are layered rather than a single switch. Each addresses a different layer of the stack, and the effects compound.
The RDL architecture has three stages that benefit from the optimizations:
- Feature encoding with PyTorch Frame encodes multi-modal column data including numerical, categorical, text, images, and timestamps into dense embeddings.
- Message passing with PyG performs heterogeneous message passing across the relational graph, propagating information between connected tables.
- Training orchestration with PyTorch Lightning manages the training loop, distributed training, and checkpointing.
Feature encoding involves standard tensor operations that compile cleanly. Message passing has the variable-shape challenges described above. Training orchestration wraps the outer loop and benefits from synchronization removal.
Re-compilation fixes address compiler input, graph-break fixes address compiler output, and synchronization removal addresses runtime behavior. Applying all three layers together produces the full 30-35% speedup.
Benchmark results
All benchmarks ran on a g6.4xlarge instance with PyTorch 2.7.0 and CUDA 12.8. The dataset was the Kaggle H&M recommendation challenge, a real-world e-commerce dataset with users, items, and transactions. The evaluation covered three prediction types.
Speedup results
Across all three tasks, torch.compile with the full optimization pipeline delivered 30% to 35% speedups over eager-mode execution.
The source reports no reduction in predictive accuracy, with compiled models producing identical predictions to their eager-mode counterparts.
The speedups are consistent across classification, regression, and link prediction because the optimizations target infrastructure bottlenecks rather than a task-specific computation. A 35% training speedup turns a ten-hour training run into a 6.5-hour run, saving GPU time and speeding iteration across many experiments.
The combined optimizations delivered 30-35% training speedups across real e-commerce classification, regression, and link-prediction tasks with no reported accuracy loss.
Practical checklist
1. Enable compilation with dynamic shapes
Do not use mode="reduce-overhead" for variable-shape workloads because it enables CUDA Graphs.
2. Prevent learning-rate re-compilation
3. Eliminate graph breaks
- Update PyG to the latest version.
- Convert tuple keys to string keys in custom
ModuleDictusage. - Set
TORCH_LOGS="graph_breaks"to find remaining breaks.
4. Remove host-device synchronization
- Replace
int(tensor)andfloat(tensor)in training loops with tensor-native accumulation. - Pass
output_sizetotorch.repeat_interleavewhenever the size is known. - Upgrade
torchmetricsand other metric libraries.
5. Profile and verify
- Use the PyTorch Profiler to confirm synchronization points are gone.
- Compare eager and compiled training-loss curves to verify numerical equivalence.
- Measure wall-clock time per epoch rather than per step because compilation overhead is amortized over an epoch.
Recommended resources
These are incremental, low-risk changes that do not require changing the model architecture.
Start with torch.compile(dynamic=True), then eliminate re-compilations, graph breaks, and synchronization while profiling each step.