Conference Presentation, Keynote
Making GPUs Actually Fast: A Deep Dive into Training Performance
- CPUs are approximately 1,000 times faster than GPUs for single floating-point operations (1 nanosecond vs. 1 microsecond) but lack the massive parallelism required for matrix multiplication.
- For 2048x2048 matrix multiplication, GPUs achieve a 100x speedup over CPUs (200 microseconds vs. 28 milliseconds) by parallelizing output elements across threads.
- The standard PyTorch training loop involves a data loader, forward pass, loss calculation, gradient computation (
loss.backward()), and optimizer step. - GPU utilization is inefficient when the "blue bar" (CUDA hardware line) has gaps, indicating idle time caused by CPU-GPU synchronization.
- Synchronizations occur when copying data back to the CPU for metrics, checking
torch.isnan(due to boolean coercion), or holding CPU memory stable during asynchronous CPU-to-GPU transfers. - Synchronizations stall the pipeline because the CPU cannot launch new kernels while waiting for the GPU to finish the transfer or for memory to be pinned.
- Training loop performance can be improved by computing metrics entirely on the GPU via kernels and deferring data transfer until the end of the epoch.
- Asynchronous
isNanchecks can be implemented using background threads or pipelines to prevent the main thread from blocking on data synchronization. - Memory pinning (
pin_memory=True) combined withnon_blocking=Trueallows Direct Memory Access (DMA) for asynchronous CPU-to-GPU transfers without CPU involvement. - GPU architecture consists of Streaming Multiprocessors (SMs); an H100 contains 132 SMs that cannot efficiently communicate with each other.
- Memory hierarchy speeds on an H100 are: Shared Memory (26 TB/s) > L2 Cache (7 TB/s) > Global Memory (3 TB/s).
- Efficient kernel design prioritizes data residency in shared memory to minimize traffic from global memory.
- Three primary bottlenecks in GPU kernel performance are compute limits, memory bandwidth, and kernel launch overhead (5–10 microseconds per launch).
- Kernel fusion combines multiple small operations (e.g., LayerNorm, ReLU, addition) into a single kernel to amortize memory loads and reduce launch overhead.
torch.compileautomatically traces model operations to identify fusion opportunities, though it may create "graph breaks" around custom operators, dictionary comprehensions, or hidden synchronization points.torch.compilerelies on Triton templates which may achieve 80–90% of NVIDIA's hand-optimized kernel performance, but fails to parallelize Python for-loops effectively.- Custom CUDA kernels can resolve scenarios where automated fusion fails, such as summing thousands of independent tensors inside a Python loop.
- Custom kernel strategy involves assigning one tensor to one SM, using thread indexing for partial sums, and performing reductions via shared memory and warp-level operations.
- A custom kernel reduced the time for summing multiple tensors from 20 milliseconds to 30 microseconds, a nearly 1,000x performance improvement.
- Development hierarchy at Jane Street: Pure PyTorch > Triton (Python DSL, 80-90% performance) > CUDA C++ (for absolute performance or hardware-specific features).
- PTX (PTX Intermediate Representation) is used sparingly, typically only for injecting specific instructions into existing CUDA kernels.