Phase 04 — torch.compile & FX Graph Optimization
Difficulty: ⭐⭐⭐⭐⭐
Estimated Time: 3 weeks (70–90 hours)
Roles Supported: Framework engineers, NPU backend engineers — understanding torch.compile is mandatory for writing ops that work at production scale
Why This Phase Exists
torch.compile is the future of PyTorch performance. Since PyTorch 2.0, every major model deployment at Google, Meta, and NVIDIA goes through some form of graph compilation. At Qualcomm, the ML framework team's job is to make their NPU backend a first-class torch.compile target.
This requires deep understanding of three systems: TorchDynamo (how Python bytecode becomes an FX graph), AOTAutograd (how the joint forward+backward graph is constructed), and TorchInductor (how FX graphs are lowered to CUDA/C++/Triton kernels). If you can write a custom torch.compile backend that correctly handles 90% of LLaMA's operations, you are the kind of engineer Qualcomm is hiring.
Concepts
- TorchDynamo: Python bytecode analyzer; installs a frame evaluation callback; traces tensor-producing operations into an FX graph; inserts guards for assumptions made during tracing
- Graph breaks: when TorchDynamo encounters unsupported Python constructs (dynamic control flow on tensor values, non-PyTorch library calls), it splits the computation into multiple FX graphs separated by Python execution
- FX Graph IR: a DAG of
Nodeobjects with ops:placeholder,call_function,call_module,call_method,output; serializable, manipulable, inspectable - AOTAutograd: ahead-of-time autograd — computes the joint forward+backward graph before execution; enables compiling the backward pass alongside the forward
- TorchInductor: default backend; generates Triton kernels (GPU) or C++/OpenMP (CPU); performs operator fusion, memory layout optimization, loop tiling
- FX graph transformations:
Interpreter(rerun with custom op dispatch),Transformer(node replacement), pattern matching viatorch.fx.subgraph_rewriter - Operator fusion: combining multiple ops into a single kernel (e.g., conv+BN, LayerNorm+dropout, matmul+bias+ReLU) reduces memory bandwidth pressure
- Dead code elimination (DCE): remove graph nodes whose output is never used
- Constant folding: evaluate subgraphs with only constant inputs at compile time
- Shape specialization vs dynamic shapes:
torch.compilespecializes on input shapes by default (fast) or can usedynamic=Truefor shape-agnostic compilation (slower, more general) - Custom backends: any callable
(fx.GraphModule, example_inputs) → callablecan be registered as atorch.compilebackend - Triton: Python-embedded GPU kernel language; used by TorchInductor; enables writing performance-critical kernels without CUDA C++
Labs
Lab 01 — FX Graph Transformation Passes
| Field | Value |
|---|---|
| Goal | Write 4 FX graph transformation passes: (1) Conv2d + BatchNorm fusion, (2) constant folding, (3) dead node elimination, (4) Op replacement (swap relu for hardswish for NPU compatibility) |
| Concepts | torch.fx.symbolic_trace, torch.fx.Interpreter, torch.fx.Transformer, node pattern matching, torch.nn.utils.fusion.fuse_conv_bn_eval, GraphModule.recompile() |
| Steps | 1. Implement ConvBNFuser(fx.Transformer) that fuses sequential Conv2d+BN; 2. Implement ConstantFolder(fx.Interpreter) that evaluates constant subgraphs; 3. Implement DCEPass that removes nodes with zero users; 4. Implement OpReplacer for configurable op substitution; 5. Compose passes into a PassManager with before/after accuracy validation; 6. Benchmark ResNet-50: passes applied vs baseline (latency + accuracy) |
| Stack | PyTorch 2.3+, torchvision |
| Datasets | ImageNet val subset (1000 images) |
| Output | PassManager with all 4 passes; ResNet-50 benchmark: latency reduction with <0.1% accuracy drop; pass unit tests |
| How to Test | pytest test_lab.py — checks: (1) Conv+BN fusion reduces node count, (2) fused model accuracy matches original within 1e-5, (3) constant subgraphs are correctly evaluated, (4) dead nodes are eliminated |
| Talking Points | What is the mathematical equivalence behind conv+BN fusion? Why can constant folding improve latency? What makes DCE safe (when might DCE be incorrect)? |
| Resume Bullet | Wrote 4 FX graph optimization passes (Conv-BN fusion, constant folding, DCE, op replacement); demonstrated 12% latency improvement on ResNet-50 with <0.1% accuracy delta |
| Extensions | Add quantization-aware fusion (conv+bn+relu → quantized fused op); add symbolic shape analysis; implement algebraic simplification (x + 0 = x) |
Lab 02 — Custom torch.compile Backend
| Field | Value |
|---|---|
| Goal | Write a custom torch.compile backend (npu_simulator) that lowers an FX graph to a mock NPU instruction set (a simple JSON-based IR), with fallback to PyTorch CPU for unsupported ops |
| Concepts | @register_backend, fx.GraphModule, example_inputs, lowering, fallback partitioning, torch.fx.passes.operator_support, BackendPatternBase |
| Steps | 1. Define 15-op NPU ISA in JSON (matmul, add, relu, softmax, layernorm, etc.); 2. Implement FX→NPU IR lowering (supported ops → NPU IR nodes, unsupported → CPU fallback); 3. Implement NpuSimulator that executes the JSON IR; 4. Register backend via @register_backend; 5. Test with torch.compile(model, backend="npu_simulator"); 6. Measure partition quality: % ops handled by NPU vs CPU fallback on LLaMA-tiny |
| Stack | PyTorch 2.3+, torch._dynamo, torch.fx |
| Datasets | Synthetic (random inputs) |
| Output | Registered npu_simulator backend; LLaMA-tiny running under torch.compile(backend="npu_simulator") with >85% ops handled by NPU; op coverage report |
| How to Test | pytest test_lab.py — checks: backend registered correctly, output matches torch.compile(backend="eager") within 1e-4, partition report generated, fallback rate < 15% for standard models |
| Talking Points | How does torch.compile determine which ops go to which backend? What is operator_support.OperatorSupport? When should you prefer a graph break over a fallback? |
| Resume Bullet | Wrote custom torch.compile backend targeting mock NPU ISA; achieved 87% op coverage for LLaMA-style models with automatic CPU fallback for unsupported ops |
| Extensions | Add profiling mode (timing each NPU op); implement operator decomposition (break unsupported ops into supported primitives); add memory layout optimization (NHWC vs NCHW) |
Lab 03 — Triton Fused Kernel + Inductor Lowering
| Field | Value |
|---|---|
| Goal | Write a fused LayerNorm+GeLU Triton kernel and register it as a TorchInductor lowering so that torch.compile automatically uses your kernel whenever it encounters this pattern |
| Concepts | Triton programming model, blocked tile computation, SRAM vs DRAM, Inductor lowering via @torch._inductor.lowering.register_lowering, FX pattern matching, performance benchmarking |
| Steps | 1. Implement layernorm_gelu_kernel in Triton: fused computation that reads x once, computes LayerNorm, applies GeLU, writes output (avoids two DRAM passes); 2. Verify correctness vs F.layer_norm + F.gelu within 1e-4; 3. Benchmark vs sequential and vs torch.compile default; 4. Register as Inductor lowering; 5. Verify torch.compile(model) now uses your kernel automatically via Triton's @triton.jit |
| Stack | Triton 2.3+, PyTorch 2.3+, CUDA GPU required |
| Datasets | Synthetic: torch.randn(batch, seq, d_model) |
| Output | layernorm_gelu_kernel achieving ~1.4× speedup over sequential on batch=32,seq=2048,d=4096; registered as Inductor lowering with correctness verification |
| How to Test | pytest test_lab.py — checks: Triton output matches PyTorch within 1e-4, kernel handles edge cases (d_model not divisible by BLOCK_SIZE), perf > 1.2x on target shape, Inductor lowering fires correctly |
| Talking Points | How does Triton's blocked programming model map to GPU warp/block hierarchy? What determines BLOCK_SIZE choice? What is memory coalescing and why does your kernel achieve it? |
| Resume Bullet | Wrote fused LayerNorm+GeLU Triton kernel registered as TorchInductor lowering; achieved 1.43× speedup over sequential ops at transformer-scale dimensions (seq=2048, d=4096) |
| Extensions | Add FP8 input support; implement fused MHA (Q,K,V projection + attention in one kernel); contribute to PyTorch's Triton op library |
Deliverables Checklist
- 4 FX graph transformation passes with benchmark results
-
Custom
torch.compilebackend with op coverage report - Triton fused kernel registered as Inductor lowering
-
All
test_lab.pysuites pass
Interview Relevance
- "Walk me through what happens when I call
torch.compile(model)(x)." — you can describe the full stack from Python bytecode → FX → AOTAutograd → Inductor → CUDA/Triton - "How would you write a
torch.compilebackend for the Qualcomm HTP?" — Lab 02 gives you the complete template - "What is a graph break and why does it hurt performance?" — you've encountered them in Lab 02 during op coverage analysis
- "Write a Triton kernel for softmax." — Lab 03 gives you the mental model for tiling and warp-level reduction