Train transformers faster by shrinking the math and fusing the steps
Transformers spend a lot of time on many small, repeated operations: GEMMs (general matrix, matrix multiplies), layer norm, activation, repeat. On GPU that adds up to many tiny kernel launches and extra memory traffic, overhead that can dominate runtime. NVIDIA’s Transformer Engine (TE) attacks both cost centers, it fuses small ops into larger GPU kernels and runs compute at lower precision (BF16 or FP8) when hardware supports it. The result is the same architecture with lower ms/step and lower peak GPU memory.
There’s a runnable notebook that wires TE into a compact GPT-style model so you can test the full stack end-to-end. Install the package, detect device capabilities, swap in fused te.* modules, configure an FP8 recipe, run a short training loop and a bench harness, inspect FP8 metadata, and validate generation. Treat this as a smoke test for production experiments, measure on your hardware and iterate.
Quick practical checklist
- Install: pip install transformer_engine[pytorch]
- Gate on device capability: check your GPU programmatically (torch.cuda.get_device_capability() or torch.cuda.get_device_properties(device)) and verify FP8/tensor-core support against NVIDIA Transformer Engine documentation. Family names (H100, L4, Ada, Blackwell) are useful reminders, but mapping to compute capability varies by architecture.
- Keep a pure‑PyTorch fallback so the code runs on older or unsupported hardware and to preserve a conservative path for correctness checks.
What the notebook runs, precise, runnable settings
Key knobs used in the example model and the short runs you can copy into Colab or a node:
- VOCAB = 96, D_MODEL = 768, N_HEADS = 12, N_LAYERS = 4, FFN = 3072, SEQ = 256
- Random seed: torch.manual_seed(1234)
- Training: 60 steps, optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
- Example batch sizes: default bsz = 16; benchmark uses bsz = 32
- Benchmark harness: bench(iters=30, warmup=10), measures forward+backward+optim ms/step and peak device memory
- Autoregressive validation: prompt_len = 8, gen_len = 24
How hardware gating works (practical guidance)
The notebook checks device capability numerically and sets runtime flags. That programmatic approach is the reliable method. Query torch.cuda.get_device_capability() and then consult NVIDIA Transformer Engine release notes for exact FP8/tensor-core support. Don’t rely solely on human-readable GPU names. The same family name can map to different compute capability numbers across driver and toolchain versions.
FP8 and fused kernels, what the notebook does
When TE is available the code replaces standard PyTorch building blocks with fused Transformer Engine modules such as te.Linear, te.LayerNorm, te.LayerNormLinear, te.LayerNormMLP and te.TransformerLayer. Fused kernels reduce kernel-launch overhead and memory traffic by combining several small ops into fewer, larger GPU kernels.
When FP8 is supported the notebook wraps forward passes with te.fp8_autocast under an FP8 recipe, otherwise it falls back to BF16 (or FP32 if TE is not importable). The exact FP8 recipe used in the example is:
recipe.DelayedScaling(fp8_format=recipe.Format.HYBRID, amax_history_len=16, amax_compute_algo=”max”)
HYBRID blends behaviors from E4M3 and E5M2 to balance mantissa precision and exponent range. The recipe keeps a recent amax_history (length 16 here) to compute safe scaling. When FP8 is enabled, TE manages casting, scaling, and tensor placement automatically via fp8_autocast.
Note: during training TE typically keeps optimizer state (AdamW momentum/variance) and master weights at higher precision (FP32) for numerical stability. fp8_model_init() is intended primarily as an inference and weight-storage optimization and needs careful validation before being applied to training workflows.
Inspecting FP8 state at runtime
Transformer Engine stores per-module FP8 metadata under a fp8_meta attribute. You can inspect the forward scaling state and the amax history for any submodule that participated in FP8. The notebook reads entries such as scaling_fwd → scale and amax_history and prints the first values so you can watch how scaling adapts during training and detect saturations or collapsed scales.
Benchmarks: what is measured (and what you must run yourself)
The example benchmark measures end-to-end ms/step for forward+backward+optimizer and peak GPU memory for two modes, BF16 (or FP32 when TE is off) and FP8 (when available). The notebook prints a concise summary like:
“>> Benchmark (batch 32, seq 256, fwd+bwd+optim):”
” BF16: {ms_hi:7.1f} ms/step | peak mem {mem_hi:.2f} GB”
” FP8 : {ms_f8:7.1f} ms/step | peak mem {mem_f8:.2f} GB”
” Speedup: {ms_hi/ms_f8:.2f}x (gains grow with model size, try D_MODEL=2048, N_LAYERS=12)”
Two important points:
- The notebook provides the measurement scaffold but does not publish fixed numeric speedups, you must run the bench on your target GPU to get real numbers for your stack (device, driver, CUDA, PyTorch, transformer_engine version).
- Relative gains typically grow with model size and different shapes. A compact example (D_MODEL=768, N_LAYERS=4) shows modest benefits compared to what you may see at larger scale.
Why DelayedScaling and amax_history matter
FP8 offers limited dynamic range compared to BF16 and FP32. A single global loss scale is often insufficient because activations and gradients vary across layers and operations. DelayedScaling maintains per‑tensor amax_history and derives per‑tensor scaling factors from recent maxima. This reduces runtime scaling overhead and improves stability in many cases, but it is not a guarantee, monitor amax_history and saturation signals as you tune recipes. For more detail see the NVIDIA Transformer Engine FP8 primer.
What the notebook validates
The example trains for 60 steps on deterministic synthetic arithmetic sequences and then runs greedy autoregressive generation to show the model learned the pattern. Training prints progress such as:
” step {step:3d} | loss {loss:.4f} | {(time.time()-t0)/step*1000:.0f} ms/step”
After training it prints a final loss indicator like:
” >> Final loss: {loss:.4f} (random guess would be ~{math.log(VOCAB):.2f})”
The generation output checks that the model continues the arithmetic stride, a small but useful correctness check for the FP8 path versus the BF16/FP32 fallback.
Exact “things to try next” lines the notebook prints
* Scale up: D_MODEL=2048, N_LAYERS=12 -> FP8 speedup becomes dramatic
* recipe.Format.E4M3 vs HYBRID; amax_history_len=1024
* te.LayerNormMLP / te.LayerNormLinear in your own architectures
* fp8_model_init() to store weights themselves in FP8 for inference
Production caveats and operational checklist
- Device compatibility: check programmatically. Use torch.cuda.get_device_capability() and confirm FP8/tensor-core support against NVIDIA Transformer Engine release notes. Human-readable GPU names are only approximate mappings to compute capability numbers.
- Pin versions for reproducibility. Pin transformer_engine, PyTorch, CUDA toolkit, and driver versions. TE kernels are sensitive to compiled targets (TORCH_CUDA_ARCH_LIST) and toolkit compatibility.
- Optimizer and master weights stay higher precision. Optimizer states and master weights are typically maintained in FP32 during training for stability, FP8 is not a blind replacement for optimizer state.
- Test multi‑GPU/DDP and checkpointing. Validate that FP8 metadata (amax_history, scaling_fwd) and checkpoint formats behave properly across allreduce, save/load, and resume in your distributed setup.
- Monitor FP8 signals. Watch fp8_meta.scaling_fwd and amax_history for repeated saturations or collapsed scales, these are diagnostic indicators you should log and react to.
- Reproducibility is trickier with mixed precision. torch.manual_seed is necessary but not sufficient. If you need strict reproducibility, set torch.backends.cudnn.deterministic=True, consider disabling TF32, and document all flags in your benchmark logs.
What to log when you benchmark
- Device name and compute capability (torch.cuda.get_device_properties(device) output)
- Driver, CUDA runtime, PyTorch, and transformer_engine versions
- ms/step (fwd+bwd+optim), mean and stdev, and peak GPU memory (GB)
- fp8_meta summary: per-module amax_history statistics and any saturation counts
- Random seed and any deterministic flags (cudnn.deterministic, TF32) used for the run
Recommended experiments before committing FP8 to production
- Run the notebook bench on your target hardware and publish ms/step and peak memory alongside environment details.
- Scale model geometry (for example, try D_MODEL=2048, N_LAYERS=12) and compare BF16 vs FP8, gains often increase with model size.
- Sweep FP8 recipes, recipe.Format.E4M3, E5M2, and HYBRID, and vary amax_history_len (16 → 1024) while monitoring amax_history and loss curves.
- For inference-only savings, try fp8_model_init() and evaluate downstream metrics (perplexity, accuracy) before committing to FP8 weight storage.
Key takeaways, questions you’ll want answered before you roll this out
-
Will my GPU run TE and FP8?
Check torch.cuda.get_device_capability() and verify FP8/tensor‑core support against the NVIDIA Transformer Engine documentation. Numeric capability checks are reliable. Family-name mappings (H100, L4, Ada, Blackwell) are approximate and vary by toolchain.
-
How do I install and test it quickly?
Install with pip install transformer_engine[pytorch], toggle the TE/FP8 path based on device capability, and run the notebook’s short synthetic training and bench harness to validate functionality on your node.
-
Is FP8 numerically safe for full training?
FP8 can work, but it requires per‑tensor scaling (DelayedScaling and amax_history are common strategies). Validate on full training curves and monitor fp8_meta closely. A tiny synthetic run is not sufficient evidence for safe parity with BF16/FP32 at scale.
-
How big are the speedups?
The notebook reports ms/step and peak memory but doesn’t publish fixed numbers. Expect larger relative benefits as models grow, run the bench on your hardware and report device and environment details to know what to expect.
Final perspective
NVIDIA Transformer Engine combines two practical levers, fused kernels to cut kernel‑launch and memory traffic overhead and lower precision (BF16/FP8) to reduce bandwidth and increase tensor‑core utilization. The notebook stitches these pieces into a hands‑on demo so teams can run a quick smoke test on their GPUs, inspect fp8_meta for numeric health, and iterate on recipes and model size.
For teams building AI for business, conversational assistants, sales automation models, task-specific ChatGPT-like agents, lower training cost and faster iteration translate directly into faster product cycles and cheaper experimentation. But FP8 is an advanced performance lever. Validate it with metrics, monitor FP8 metadata, test distributed and resume behavior, and pin your stack before you flip it into production.