LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnAdvanced Training & AdaptationMixed Precision Training
⚡HardFine-Tuning & Training

Mixed Precision Training

Choose FP16 or BF16 for an SFT run by measuring range, update resolution, memory, and held-out policy quality instead of assuming faster math is an upgrade.

28 min read
Learning path
Step 110 of 196 in the full curriculum
Supervised Fine-Tuning PipelineDistributed Training: FSDP & ZeRO

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

The last chapter turned access-policy supervised fine-tuning (SFT) into a real training system: a response-token loss, a batch budget, resumable checkpoints, and a held-out export rule on policy_pass_rate. Now the same run needs an arithmetic decision.

Suppose a naive FP16 candidate keeps update state in FP16. A weight near 1.0 can miss its 1e-4 update.

A different batch can fail in forward: an activation near 1e5 crosses FP16's 65504 ceiling. Nothing about the model's task changed; the number system did.

Before changing dtype, trace which values disappear, which values overflow, and which quality gates move.

Mixed precision training runs selected expensive operations in a compact floating-point format while keeping higher precision where training is fragile.

Current PyTorch AMP (Automatic Mixed Precision) examples create the model and optimizer in default precision, then let autocast choose an operation-specific dtype inside the forward region.[1]Reference 1Automatic Mixed Precision package - torch.amphttps://docs.pytorch.org/docs/stable/amp.html

Don't declare BF16 good or FP16 bad in the abstract. See what each format can lose, then measure the survivors under the same validation and hardware conditions.

A floating-point number has two limits

A floating-point value is a sign, a scale, and some significant digits. Its exponent controls range: how tiny or huge a magnitude it can represent. Its fraction (often called the mantissa in training discussions) controls resolution: how close two neighboring values can be.

Before reading the table, predict the failure for each value in the opening trace. Which format has enough range for 1e5? Which format has enough spacing near 1.0 for a 1e-4 step? Those are separate questions.

FormatBitsExponent bitsFraction bitsMain training consequence
FP3232823Wide range and fine update resolution, with higher storage cost
FP1616510Compact, but small gradients can underflow and large values can overflow
BF161687Compact with FP32-like range, but coarser nearby resolution

The easy mistake is treating "16-bit" as one thing. BF16 improves range relative to FP16; it doesn't improve nearby resolution. BF16 has fewer fraction bits than FP16. That's why BF16 compute still normally updates FP32 parameters.

PyTorch exposes the exact limits with torch.finfo. Run this on CPU; you don't need an accelerator to inspect the number system.

format-limits.py
1import torch 2 3formats = ( 4 ("FP32", torch.float32), 5 ("FP16", torch.float16), 6 ("BF16", torch.bfloat16), 7) 8 9print("format epsilon_at_1 smallest_normal largest_finite") 10for label, dtype in formats: 11 info = torch.finfo(dtype) 12 print(f"{label:<6} {info.eps:>12.1e} {info.tiny:>15.1e} {info.max:>14.1e}") 13 14print(f"bf16_min_normal_matches_fp32={torch.finfo(torch.bfloat16).tiny == torch.finfo(torch.float32).tiny}") 15print(f"bf16_resolution_coarser_than_fp16={torch.finfo(torch.bfloat16).eps > torch.finfo(torch.float16).eps}")
Output
1format epsilon_at_1 smallest_normal largest_finite 2FP32 1.2e-07 1.2e-38 3.4e+38 3FP16 9.8e-04 6.1e-05 6.6e+04 4BF16 7.8e-03 1.2e-38 3.4e+38 5bf16_min_normal_matches_fp32=True 6bf16_resolution_coarser_than_fp16=True

epsilon_at_1 is the gap between 1.0 and the next representable value near it. smallest_normal and largest_finite describe range. FP16 is finer than BF16 near 1.0, and far narrower in range.

Two log-scale comparisons of FP32, FP16, and BF16: FP32 and BF16 span roughly 1.2e-38 to 3.4e38, while FP16 has a 6.1e-5 normal floor, a roughly 6.0e-8 subnormal floor, and a 6.6e4 maximum, so a 1.2e-8 gradient underflows and a 1e5 activation overflows only in FP16; near 1.0, a 1e-4 update survives FP32 spacing but rounds away in both FP16 and BF16.
The upper plot is magnitude range. The lower plot is spacing near 1.0. BF16 covers the two range examples; only FP32 keeps the 1e-4 parameter update.

If BF16 has FP32-like range, why don't we store model updates only in BF16?

Answer

Range and resolution are different. BF16 can represent very small standalone magnitudes, but its fraction has only 7 bits, so a tiny change to a weight near 1.0 can still round away. FP32 update state preserves those accumulated changes.

Tiny updates need an FP32 home

Take one access-policy classifier weight sitting at 1.0. One optimizer step wants to subtract 0.0001. That update is much larger than the smallest BF16 magnitude, and still smaller than the spacing between BF16 values near 1.0.

Predict the stored value before running the cell. A format can represent 1e-4 as a standalone number and still lose it when subtracting from a weight near 1.0.

tiny-parameter-update.py
1weight = torch.tensor([1.0], dtype=torch.float32) 2update = torch.tensor([1.0e-4], dtype=torch.float32) 3 4for label, dtype in formats: 5 before = weight.to(dtype) 6 after = before - update.to(dtype) 7 changed = bool(after.item() != before.item()) 8 print(f"{label}: stored_after_step={after.item():.8f}, update_survived={changed}") 9 10print("lesson=BF16 protects range; FP32 protects small accumulated updates")
Output
1FP32: stored_after_step=0.99989998, update_survived=True 2FP16: stored_after_step=1.00000000, update_survived=False 3BF16: stored_after_step=1.00000000, update_survived=False 4lesson=BF16 protects range; FP32 protects small accumulated updates

Both 16-bit parameter values lose this update.

The original mixed-precision recipe kept an FP32 master copy of the weights so small updates accumulate instead of disappearing.[2]Reference 2Mixed Precision Training.https://arxiv.org/abs/1710.03740 Ordinary PyTorch AMP gets the same protection by leaving parameters in default precision and autocasting eligible forward operations, rather than converting parameter storage before the optimizer step.[1]Reference 1Automatic Mixed Precision package - torch.amphttps://docs.pytorch.org/docs/stable/amp.html

Range decides whether gradients exist at all

Resolution is one failure. Range is another. Compare an extremely small gradient and a large activation-like value when stored in FP16 and BF16.

The useful prediction is asymmetric: FP16 should lose the small gradient and overflow the large value, while BF16 should keep both magnitudes with some rounding.

gradient-range.py
1values = torch.tensor([1.2e-8, 1.0e5], dtype=torch.float32) 2 3for label, dtype in (("FP16", torch.float16), ("BF16", torch.bfloat16)): 4 cast = values.to(dtype) 5 print( 6 f"{label}: small={cast[0].item():.2e}, " 7 f"large={cast[1].item():.2e}, " 8 f"all_finite={bool(torch.isfinite(cast).all())}" 9 ) 10 11print("fp16_loses_small_and_large=True") 12print("bf16_keeps_range_in_this_example=True")
Output
1FP16: small=0.00e+00, large=inf, all_finite=False 2BF16: small=1.20e-08, large=9.98e+04, all_finite=True 3fp16_loses_small_and_large=True 4bf16_keeps_range_in_this_example=True

FP16's smallest positive normal number is roughly 6.1×10−56.1 \times 10^{-5}6.1×10−5, and its subnormal floor is about 6.0×10−86.0 \times 10^{-8}6.0×10−8. A true gradient of 1.2×10−81.2 \times 10^{-8}1.2×10−8 becomes zero in FP16. At the other end, 100000 is beyond FP16's largest finite value of 65504, so it becomes Inf.

BF16 keeps the 8-bit exponent width of FP32, so it has a similar range and these two magnitudes stay representable, although rounded. The BF16 training study treats that wider range as the main stability advantage over FP16.[3]Reference 3A Study of BFLOAT16 for Deep Learning Training.https://arxiv.org/abs/1905.12322

Loss scaling can rescue the tiny backward gradient. It can't repair a forward activation that already overflowed.

FP16 uses loss scaling to rescue small gradients

For FP16, loss scaling moves gradient magnitudes into a representable interval during backpropagation. Multiply the loss by a scale SSS; the chain rule multiplies each gradient by SSS too. After backward, divide the gradients by SSS in FP32 before the optimizer step. The intended update hasn't changed.

Gradient clipping belongs after that unscale step. Clipping a gradient of 0.02 while it's still multiplied by 1024 treats it as 20.48, so a threshold of 1.0 would shrink a gradient that was already safely below the intended limit.

For a true gradient of 1.2×10−81.2 \times 10^{-8}1.2×10−8 and a teaching scale S=1024S = 1024S=1024, predict the order before reading the table. The scale must enter before the low-precision backward pass. Multiplying a gradient after FP16 already rounded it to zero can't recover the lost value.

OperationValueFP16 outcome
Cast unscaled gradient1.2×10−81.2 \times 10^{-8}1.2×10−8Rounds to zero
Multiply by S=1024S=1024S=1024 during backward1.23×10−51.23 \times 10^{-5}1.23×10−5Finite (subnormal)
Convert to FP32 and divide by SSSapproximately 1.2×10−81.2 \times 10^{-8}1.2×10−8Ready for the FP32 update

That 1.23×10−51.23 \times 10^{-5}1.23×10−5 value sits below FP16's normal floor of 6.1×10−56.1 \times 10^{-5}6.1×10−5, so the rescue uses subnormals. A larger scale, including PyTorch's default GradScaler start of 216=655362^{16} = 65536216=65536, would push the same gradient into the normal range. The mechanism is the same either way.

loss-scaling-rescue.py
1true_grad = torch.tensor([1.2e-8], dtype=torch.float32) 2scale = 1024.0 3 4plain_fp16 = true_grad.to(torch.float16) 5scaled_fp16 = (true_grad * scale).to(torch.float16) 6recovered_fp32 = scaled_fp16.to(torch.float32) / scale 7 8print(f"plain_underflowed={plain_fp16.item() == 0.0}") 9print(f"scaled_visible={scaled_fp16.item() > 0.0}") 10print(f"recovered_grad={recovered_fp32.item():.2e}") 11print(f"recovery_relative_error={abs(recovered_fp32.item() - true_grad.item()) / true_grad.item():.3%}")
Output
1plain_underflowed=True 2scaled_visible=True 3recovered_grad=1.20e-08 4recovery_relative_error=0.077%

Scaling too far causes overflow

A scale that saves the smallest gradient may overflow a larger gradient in the same step. Dynamic scaling therefore has two outcomes: apply a finite, descaled update, or skip an overflowed step and reduce the scale.

Before running the next cell, choose the outcome for the quiet pair and the spiky pair at S = 1024. The tiny gradient needs the scale, but the 1e2 spike becomes 1.024e5, past FP16's maximum.

overflow-backoff.py
1def scaled_step_status(gradients: torch.Tensor, scale: float) -> tuple[str, float]: 2 scaled = (gradients * scale).to(torch.float16) 3 if not bool(torch.isfinite(scaled).all()): 4 return "SKIP_OVERFLOW", scale / 2 5 return "APPLY_DESCALED_UPDATE", scale 6 7quiet_step = torch.tensor([1.2e-8, 2.0e-2], dtype=torch.float32) 8spiky_step = torch.tensor([1.2e-8, 1.0e2], dtype=torch.float32) 9 10quiet_status, quiet_next_scale = scaled_step_status(quiet_step, 1024.0) 11spiky_status, spiky_next_scale = scaled_step_status(spiky_step, 1024.0) 12 13print(f"quiet_step={quiet_status}, next_scale={quiet_next_scale:.0f}") 14print(f"spiky_step={spiky_status}, next_scale={spiky_next_scale:.0f}") 15print("invariant=never_apply_nonfinite_gradients")
Output
1quiet_step=APPLY_DESCALED_UPDATE, next_scale=1024 2spiky_step=SKIP_OVERFLOW, next_scale=512 3invariant=never_apply_nonfinite_gradients

The quiet step keeps scale 1024. The spiky step skips the update and halves the scale. That control flow is what GradScaler automates:

Diagram showing scale loss by S, FP16 backward, all grads finite?, and unscale, clip, step.
scale loss by S, FP16 backward, all grads finite?, and unscale, clip, step.
Log-magnitude loss-scaling plot where multiplying gradients by 1024 shifts every value 3.01 decades: a 1.2e-8 tiny gradient moves into FP16 range at 1.23e-5, a 2e-2 quiet gradient remains finite at 20.5, and a 1e2 spike becomes 1.024e5 above FP16 maximum 65504; the finite batch is unscaled and applied at scale 1024, while the overflowed batch is skipped and backs off to scale 512.
The same scale rescues the tiny gradient and overflows the spike. Dynamic scaling applies only the finite, unscaled batch and halves the next scale after the rejected batch.

In current PyTorch, torch.amp.GradScaler performs this scale, unscale, finite-check, skip, and update control flow for FP16 training.[1]Reference 1Automatic Mixed Precision package - torch.amphttps://docs.pytorch.org/docs/stable/amp.html

Its default starting scale is 65536.0, it doubles after 2000 finite steps, and it multiplies by 0.5 after overflow. If you inspect or clip gradients, call scaler.unscale_(optimizer) first so thresholds apply to true gradient magnitudes.[1]Reference 1Automatic Mixed Precision package - torch.amphttps://docs.pytorch.org/docs/stable/amp.html

Loss scaling isn't a general extension of FP16 range. It rescues small backward gradients that would underflow, but it can't make a forward activation above 65504 representable.[1]Reference 1Automatic Mixed Precision package - torch.amphttps://docs.pytorch.org/docs/stable/amp.html

PyTorch also warns that GradScaler may reduce its scale below 1 for overflow-prone models, so don't assume the scale always grows or stays above 1.[1]Reference 1Automatic Mixed Precision package - torch.amphttps://docs.pytorch.org/docs/stable/amp.html A BF16-pretrained model can hit that activation wall when its activations don't fit FP16's 65504 cap.

An FP16 step overflows after backward. Should the optimizer apply the finite gradients and drop only the non-finite tensors?

Answer

No. Skip the whole optimizer step, reduce the loss scale, and retry on a later batch. Applying a partial update changes the optimizer contract. Clipping must also happen only after gradients are unscaled to their true magnitudes.

Compute low, update high

Loss scaling protects FP16 gradients from range failure. It doesn't make 16-bit parameter storage appropriate for tiny updates. Keep the FP32 update path.

Ordinary AMP does that without a separate master-weight tensor: parameters stay FP32, autocast selects lower precision for eligible compute, and the optimizer updates the FP32 parameters directly.[1]Reference 1Automatic Mixed Precision package - torch.amphttps://docs.pytorch.org/docs/stable/amp.html

The 2018 recipe instead copied FP32 master weights into a low-precision compute copy each step.[2]Reference 2Mixed Precision Training.https://arxiv.org/abs/1710.03740 Both approaches keep high-precision update state.

They don't keep the same stored parameter dtype, which matters as soon as you start counting bytes.

The boundary to watch is the optimizer update. Compute can use fewer bits, but the value that accumulates tiny changes needs the FP32 path.

This is the CUDA shape you'd use for a real access-policy fine-tune. It isn't marked executable here because it needs an accelerator and a model workload. GradScaler(..., enabled=False) turns scale, unscale, and step into the ordinary FP32 control flow, so BF16 and FP16 can share one loop:

cuda-amp-training-shape.py
1dtype = torch.bfloat16 # compare against torch.float16 in a controlled run 2scaler = torch.amp.GradScaler("cuda", enabled=(dtype == torch.float16)) 3 4for batch, target in dataloader: 5 optimizer.zero_grad(set_to_none=True) 6 with torch.autocast(device_type="cuda", dtype=dtype): 7 logits = model(batch.cuda()) 8 loss = criterion(logits, target.cuda()) 9 10 scaler.scale(loss).backward() 11 scaler.unscale_(optimizer) 12 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) 13 scaler.step(optimizer) 14 scaler.update()

For BF16, skipping GradScaler is a common policy because BF16's exponent range avoids the FP16 failure that loss scaling targets. It isn't a guarantee that every BF16 job is stable. Bad data, unstable losses, overly large learning rates, or sensitive kernels can still produce non-finite values.

GradScaler and gradient accumulation share one optimizer-step boundary

The sketch above assumes one micro-batch per optimizer step. Real SFT jobs often use gradient accumulation: several micro-batches contribute to gradients before a single optimizer.step. Before looking at the table, identify that one logical boundary. The scaler contract follows the optimizer step, not each micro-batch.

CallOnce per micro-batchOnce per optimizer step
scaler.scale(micro_loss).backward()YesNo
scaler.unscale_(optimizer)NoYes, after the window finishes and before clip
clip gradientsNoYes
scaler.step(optimizer)NoYes
scaler.update()NoYes

If you step/update every micro-batch, the effective batch size collapses and the scale adapts on incomplete gradients. If you unscale before the accumulation window finishes, later micro-batches write into already-unscaled grad buffers. Count skipped non-finite optimizer steps in telemetry. A silent skip storm is a failed precision experiment even when the run "completes."

When loss is averaged over the accumulation window, keep the usual loss / accum_steps scaling and let GradScaler multiply that micro-loss. Don't also divide the GradScaler scale by hand.

Memory savings need accounting, not slogans

When autocast runs an eligible operation in BF16 or FP16, its saved low-precision activation values use two bytes rather than four. Other operations stay in or return FP32 for numerical safety. Total training memory doesn't necessarily halve, because FP32 parameters and optimizer moments may remain unchanged.

Predict the total before looking at the inventory. If only the 800 million saved activations switch from four bytes to two, the activation line halves, but the parameter, gradient, and Adam lines stay put.

The next inventory is deliberately small: 100 million parameters, their gradients, two Adam moment buffers, and 800 million stored activation values. To make the arithmetic visible, assume those inventoried activations were saved in low precision for the AMP candidate. A real profile may retain FP32 values for some operations. This is a budget calculation, not a measured GPU profile.

memory-budget.py
1def gib(values: int, bytes_per_value: int) -> float: 2 return values * bytes_per_value / (1024 ** 3) 3 4parameter_values = 100_000_000 5activation_values = 800_000_000 6 7fp32_budget = { 8 "parameters": gib(parameter_values, 4), 9 "gradients": gib(parameter_values, 4), 10 "adam_moments": gib(parameter_values * 2, 4), 11 "activations": gib(activation_values, 4), 12} 13amp_budget = { 14 **{name: value for name, value in fp32_budget.items() if name != "activations"}, 15 "activations": gib(activation_values, 2), 16} 17 18print(f"fp32_total_gib={sum(fp32_budget.values()):.2f}") 19print(f"amp_total_gib={sum(amp_budget.values()):.2f}") 20print(f"activation_saving_gib={fp32_budget['activations'] - amp_budget['activations']:.2f}") 21print(f"total_reduction={(1 - sum(amp_budget.values()) / sum(fp32_budget.values())):.1%}") 22print("lesson=half_size_activations_do_not_imply_half_total_memory")
Output
1fp32_total_gib=4.47 2amp_total_gib=2.98 3activation_saving_gib=1.49 4total_reduction=33.3% 5lesson=half_size_activations_do_not_imply_half_total_memory

Saved activations use two bytes instead of four, yet total measured memory falls by only one third. Is mixed precision broken?

Answer

No. Parameters, gradients, and Adam moments may still use FP32 storage, so only one part of the inventory halved. Profile each component before deciding whether activation casting, sharding, checkpointing, or a smaller batch targets the real bottleneck.

Component memory accounting for 100 million parameters, 100 million gradients, two Adam moment buffers, and 800 million saved activations: FP32 totals 4.47 GiB, while an AMP candidate totals 2.98 GiB because only activations halve from 2.98 to 1.49 GiB, a 33.3 percent reduction. Separate 100-million-gradient communication bars show 0.37 GiB for FP32 reduction and 0.19 GiB for BF16 reduction, a 50 percent payload cut that doesn't follow from compute dtype.
Halving activation bytes reduces this full inventory by one third, not one half. The payload bars are a separate question: reduction dtype isn't implied by compute dtype.

The 2018 paper could say mixed precision "nearly halves" training memory because activations dominated and weights, activations, and gradients were stored in FP16 beside an FP32 master copy.[2]Reference 2Mixed Precision Training.https://arxiv.org/abs/1710.03740

Ordinary AMP, as in the budget above, often leaves parameters and Adam moments in FP32. The next chapter's 16-byte-per-parameter count is yet another recipe: low-precision parameter and gradient copies plus an FP32 master and FP32 Adam moments.

Write down which copies exist before using a bytes-per-parameter slogan.

For large models, sharding methods such as ZeRO and Fully Sharded Data Parallel (FSDP) address the parameter, gradient, and optimizer-state memory that activation casting alone doesn't remove.[4]Reference 4ZeRO: Memory Optimizations Toward Training Trillion Parameter Models.https://arxiv.org/abs/1910.02054[5]Reference 5PyTorch FSDP: Experiences on Scaling Fully Sharded Data Parallel.https://arxiv.org/abs/2304.11277

Distributed jobs add a communication dtype

When workers exchange gradients, the network payload has its own precision policy. Current PyTorch FSDP mixed-precision configuration exposes param_dtype for forward and backward computation and reduce_dtype for gradient reduction. The two fields may differ.[6]Reference 6FullyShardedDataParallelhttps://docs.pytorch.org/docs/stable/fsdp.html

communication-budget.py
1gradient_values = 100_000_000 2fp32_reduce_gib = gib(gradient_values, 4) 3bf16_reduce_gib = gib(gradient_values, 2) 4 5print(f"gradient_payload_fp32_gib={fp32_reduce_gib:.2f}") 6print(f"gradient_payload_bf16_gib={bf16_reduce_gib:.2f}") 7print(f"payload_reduction={(1 - bf16_reduce_gib / fp32_reduce_gib):.0%}") 8print("warning=compute_dtype_does_not_prove_reduce_dtype")
Output
1gradient_payload_fp32_gib=0.37 2gradient_payload_bf16_gib=0.19 3payload_reduction=50% 4warning=compute_dtype_does_not_prove_reduce_dtype

A job can use BF16 for matrix computations and still communicate FP32 gradient payloads. A trustworthy run record therefore separates compute_dtype, update_storage_dtype, and reduce_dtype rather than logging a single mixed_precision=true flag.

Decide from runs, not format preference

Once the arithmetic is clear, the question becomes attribution: did the dtype change the result, or did the run change around it? Freeze the access-policy artifact, data, seeds, eval suite, and hardware, then record the rest of the comparison contract before timing anything.

BoundaryRecord before comparing runs
HardwareAccelerator model and count, interconnect or topology, and driver/CUDA versions
SoftwarePyTorch, kernel or Transformer Engine versions, compiler settings, and launch path
WorkloadModel shape, micro-batch and sequence lengths, accumulation window, warmup, and steady-state sample
PrecisionCompute, update-storage, and reduction dtypes, plus FP8 format and scaling recipe when used
BaselineExact FP32, BF16, or FP16 implementation, optimizer settings, and equivalent tuning
CorrectnessNon-finite and skipped steps, tolerances, loss curves, held-out format_pass_rate, and policy_pass_rate slices

That record turns a speed or memory number into evidence someone else can reproduce. The small contract below carries the artifact, baseline, data, seed, evaluation, hardware, and release gates through the example.

precision-contract.py
1from dataclasses import dataclass 2 3@dataclass(frozen=True) 4class PrecisionContract: 5 experiment: str 6 artifact: str 7 baseline: str 8 candidates: tuple[str, ...] 9 data_fingerprint: str 10 seed_policy: str 11 eval_suite: str 12 hardware_profile: str 13 required_policy_pass_rate: float 14 permitted_nonfinite_steps: int 15 16contract = PrecisionContract( 17 experiment="access-policy-sft-precision", 18 artifact="access-policy-assistant-sft", 19 baseline="fp32", 20 candidates=("fp16_unscaled", "fp16_scaled", "bf16"), 21 data_fingerprint="access-policy-sft@sha256:fixture-7", 22 seed_policy="seeds=11,17,23", 23 eval_suite="policy-compliance@sha256:suite-4", 24 hardware_profile="a100-80gb-single-gpu", 25 required_policy_pass_rate=0.93, 26 permitted_nonfinite_steps=0, 27) 28 29print(f"experiment={contract.experiment}") 30print(f"artifact={contract.artifact}") 31print(f"baseline={contract.baseline}") 32print(f"candidates={','.join(contract.candidates)}") 33print(f"metric_gate=policy_pass_rate>={contract.required_policy_pass_rate:.2f}") 34print(f"nonfinite_steps_gate={contract.permitted_nonfinite_steps}")
Output
1experiment=access-policy-sft-precision 2artifact=access-policy-assistant-sft 3baseline=fp32 4candidates=fp16_unscaled,fp16_scaled,bf16 5metric_gate=policy_pass_rate>=0.93 6nonfinite_steps_gate=0

The 0.93 floor matches the SFT chapter's step-200 policy_pass_rate. Keep the previous export rule too: format_pass_rate still has to clear 0.98 before policy rate is allowed to decide. Precision work doesn't get to invent a friendlier metric.

RL can expose a BF16 train-inference mismatch

BF16's wide exponent range makes it a strong default candidate for many training jobs, including this SFT run. Range isn't the only concern.

Reinforcement-learning fine-tuning compares or updates a training policy using trajectories generated by an inference policy. Different kernels, batching shapes, or execution paths can produce nearby FP32 logits before casting.

BF16's coarse spacing may round those nearby values to different sides of a representable boundary, so rollout probabilities no longer match the probabilities used during training.

Qi et al. report this failure mode in LLM reinforcement-learning fine-tuning and find that using FP16 consistently reduced the train-inference mismatch and improved optimization in their tested tasks, algorithms, and frameworks.[7]Reference 7Defeating the Training-Inference Mismatch via FP16https://arxiv.org/abs/2510.26788

That's evidence for an RL-specific candidate, not a universal reversal of the BF16 guidance above. FP16 still needs loss scaling for small gradients, finite forward values, and the same controlled quality checks as every precision policy.

The toy below doesn't reproduce a full RL run. It isolates the rounding boundary. Two execution paths produce first logits 1.0038 and 1.0040. FP16 rounds both to the same value, while BF16 rounds one down and the other up, creating a probability gap.

compare-policy-rounding.py
1import torch 2 3training_logits = torch.tensor([1.0038, 1.0], dtype=torch.float32) 4rollout_logits = torch.tensor([1.0040, 1.0], dtype=torch.float32) 5 6def policy_gap(dtype: torch.dtype) -> tuple[list[float], list[float], float]: 7 training_cast = training_logits.to(dtype).to(torch.float32) 8 rollout_cast = rollout_logits.to(dtype).to(torch.float32) 9 training_probability = torch.softmax(training_cast, dim=0)[0] 10 rollout_probability = torch.softmax(rollout_cast, dim=0)[0] 11 gap = abs(training_probability.item() - rollout_probability.item()) 12 return training_cast.tolist(), rollout_cast.tolist(), gap 13 14for label, dtype in (("FP16", torch.float16), ("BF16", torch.bfloat16)): 15 training_cast, rollout_cast, gap = policy_gap(dtype) 16 print( 17 f"{label}: training={training_cast[0]:.7f}, " 18 f"rollout={rollout_cast[0]:.7f}, probability_gap={gap:.7f}" 19 ) 20 21assert policy_gap(torch.float16)[2] == 0.0 22assert policy_gap(torch.bfloat16)[2] > 0.0
Output
1FP16: training=1.0039062, rollout=1.0039062, probability_gap=0.0000000 2BF16: training=1.0000000, rollout=1.0078125, probability_gap=0.0019531

An RL precision contract should therefore add a direct train-versus-rollout comparison: record logits or log probabilities for matched prompts, measure divergence under the actual training and generation stacks, and check reward and held-out quality across seeds. Promote scaled FP16 over BF16 only when it closes that measured mismatch without introducing overflow or quality regressions.

The next fixture brings the SFT lesson back to experiment tracking. The numbers are illustrative recorded outcomes, not benchmark claims.

They show the review rule you should apply after running the same classifier, data fingerprint, seed policy, held-out policy_pass_rate, and target-GPU profile for every precision configuration. One extra BF16 run changes hardware on purpose so the comparison filter has something to reject.

Before reading the output, predict which candidate is ineligible even though its examples-per-second value is highest. A changed accelerator breaks the comparison before speed enters the decision.

precision-run-decision.py
1@dataclass(frozen=True) 2class PrecisionRun: 3 run_id: str 4 policy: str 5 artifact: str 6 data_fingerprint: str 7 seed_policy: str 8 eval_suite: str 9 hardware_profile: str 10 policy_pass_rate: float 11 nonfinite_steps: int 12 peak_memory_gib: float 13 examples_per_second: int 14 evidence: str 15 16def fixture_run( 17 run_id: str, 18 policy: str, 19 policy_pass_rate: float, 20 nonfinite_steps: int, 21 peak_memory_gib: float, 22 examples_per_second: int, 23 *, 24 hardware_profile: str = contract.hardware_profile, 25) -> PrecisionRun: 26 return PrecisionRun( 27 run_id=run_id, 28 policy=policy, 29 artifact=contract.artifact, 30 data_fingerprint=contract.data_fingerprint, 31 seed_policy=contract.seed_policy, 32 eval_suite=contract.eval_suite, 33 hardware_profile=hardware_profile, 34 policy_pass_rate=policy_pass_rate, 35 nonfinite_steps=nonfinite_steps, 36 peak_memory_gib=peak_memory_gib, 37 examples_per_second=examples_per_second, 38 evidence="illustrative_fixture", 39 ) 40 41runs = ( 42 fixture_run("run_fp32", "fp32", 0.93, 0, 4.47, 800), 43 fixture_run("run_fp16_plain", "fp16_unscaled", 0.88, 3, 2.98, 1240), 44 fixture_run("run_fp16_scaled", "fp16_scaled", 0.93, 0, 2.98, 1190), 45 fixture_run("run_bf16", "bf16", 0.93, 0, 2.98, 1310), 46 fixture_run( 47 "run_bf16_other_hardware", 48 "bf16", 49 0.93, 50 0, 51 2.98, 52 1770, 53 hardware_profile="h100-80gb-single-gpu", 54 ), 55) 56 57def comparable_to_contract(run: PrecisionRun) -> bool: 58 return ( 59 run.artifact == contract.artifact 60 and run.data_fingerprint == contract.data_fingerprint 61 and run.seed_policy == contract.seed_policy 62 and run.eval_suite == contract.eval_suite 63 and run.hardware_profile == contract.hardware_profile 64 ) 65 66def passes_gates(run: PrecisionRun) -> bool: 67 return ( 68 run.policy_pass_rate >= contract.required_policy_pass_rate 69 and run.nonfinite_steps <= contract.permitted_nonfinite_steps 70 ) 71 72comparable_candidates = [ 73 run for run in runs if run.policy != contract.baseline and comparable_to_contract(run) 74] 75gate_eligible = [run.run_id for run in comparable_candidates if passes_gates(run)] 76rejected_gates = [run.run_id for run in comparable_candidates if not passes_gates(run)] 77excluded_noncomparable = [run.run_id for run in runs if not comparable_to_contract(run)] 78 79print(f"gate_eligible_runs={','.join(gate_eligible)}") 80print(f"rejected_gate_runs={','.join(rejected_gates)}") 81print(f"excluded_noncomparable_runs={','.join(excluded_noncomparable)}") 82print("decision=BLOCKED_FIXTURE_ONLY_RUN_MEASURED_PROFILE") 83print("next_metrics=examples_per_second,peak_memory,policy_pass_rate,nonfinite_steps")
Output
1gate_eligible_runs=run_fp16_scaled,run_bf16 2rejected_gate_runs=run_fp16_plain 3excluded_noncomparable_runs=run_bf16_other_hardware 4decision=BLOCKED_FIXTURE_ONLY_RUN_MEASURED_PROFILE 5next_metrics=examples_per_second,peak_memory,policy_pass_rate,nonfinite_steps

Don't promote BF16 because it sounds newer. Both scaled FP16 and BF16 pass this small fixture, and both require real measurements under the declared contract.

The faster run_bf16_other_hardware value can't rank against them because its accelerator changed. BF16 is often simpler to operate because it commonly avoids loss scaling, but only a comparable controlled run can justify promotion.

FP8 is a later optimization, not a default answer

The fixture leaves two candidates. The next temptation is an 8-bit path. FP8 can cut operand storage again, but it isn't a smaller FP16 switch: its encoding, scale granularity, conversion rules, and kernel path all become part of the training contract.

FP8 has two complementary encodings. E4M3 uses 4 exponent bits and 3 fraction bits, reaches a maximum finite magnitude of 448, and doesn't encode infinity. E5M2 uses 5 exponent bits and 2 fraction bits, reaches 57,344, and keeps infinity encodings.

E4M3 buys nearby resolution; E5M2 buys range. Those limits come from the OCP FP8 specification and the original FP8 formats paper.[8]Reference 8FP8 Formats for Deep Learning.https://arxiv.org/abs/2209.05433

The encoding still doesn't tell you how a training stack scales a tensor. Transformer Engine's tensor-scaled hybrid recipe uses E4M3 for forward activations and weights, then E5M2 for backward gradients.[9]Reference 9Using FP8 and FP4 with Transformer Enginehttps://docs.nvidia.com/deeplearning/transformer-engine/user-guide/examples/fp8_primer.html

Its current-scaling recipe computes an absolute maximum (amax) from the current tensor; delayed scaling estimates that scale from amax history. Both use one FP32 scale per tensor, but current scaling reads the tensor once to find amax and again to scale and cast it, while delayed scaling trades that read for history that can lag a new spike.[9]Reference 9Using FP8 and FP4 with Transformer Enginehttps://docs.nvidia.com/deeplearning/transformer-engine/user-guide/examples/fp8_primer.html

MXFP8 changes the scale granularity rather than pretending one global scale fits every value. The OCP MX specification defines MXFP8 as FP8 elements with one E8M0 power-of-two scale shared by each block of 32 values.

Transformer Engine's MXFP8 recipe uses E4M3 by default, supports a hybrid E4M3-forward/E5M2-backward option, and requires Blackwell (SM100) or later for its native path.[9]Reference 9Using FP8 and FP4 with Transformer Enginehttps://docs.nvidia.com/deeplearning/transformer-engine/user-guide/examples/fp8_primer.html

RecipeScale contractFailure or cost to measure
FP8 current scalingOne FP32 scale from the tensor's current amax; scale, then castTwo tensor reads and outlier-driven range changes
FP8 delayed scalingOne FP32 scale predicted from amax history, then history updateStale history can saturate a new spike
MXFP8 block scalingOne E8M0 power-of-two scale per 32 consecutive values; E4M3 is the usual element formatBlock dimensions, layout, transpose, and native-kernel support

Storage format and accumulation are separate questions. The FP8 paper expects math to produce wider outputs, while OCP MX says a general dot product should return Float32.[8]Reference 8FP8 Formats for Deep Learning.https://arxiv.org/abs/2209.05433

OCP leaves internal precision and operation order implementation-defined. Transformer Engine documents round-to-nearest-even for current-scaling conversion.[9]Reference 9Using FP8 and FP4 with Transformer Enginehttps://docs.nvidia.com/deeplearning/transformer-engine/user-guide/examples/fp8_primer.html

OCP also distinguishes saturation from overflow: an out-of-range E4M3 value can clamp to 448 under saturation or become NaN under overflow, while E5M2 can clamp to 57,344 or become infinity. Count saturation, non-finite values, scale history, and held-out errors; a finite saturated value can still be wrong.[9]Reference 9Using FP8 and FP4 with Transformer Enginehttps://docs.nvidia.com/deeplearning/transformer-engine/user-guide/examples/fp8_primer.html

Hardware and software decide whether those rules are cheap or even available. Verify accelerator model and count, topology, CUDA and PyTorch versions, Transformer Engine version, tensor dimensions, kernel layout, and any fallback path before calling an FP8 run comparable. A lower-bit label doesn't prove a native Tensor Core path or a faster step.

Don't add FP8 to a training proposal until BF16 or scaled FP16 is measured under the evidence contract above.

Keep model, optimizer, data, seeds, batch shapes, and evaluation fixed. Compare step time, examples per second, peak memory, scaling overhead, saturation and skipped-step counts, loss curves, and the held-out format_pass_rate and policy_pass_rate gates. Precision work should reduce measured cost without creating unexplained convergence risk.

Common pitfalls

A precision failure usually points to a boundary that moved. Start with the symptom, identify which value or state crossed it, then check the smallest change that restores the intended contract.

BF16 is mistaken for an FP32 optimizer replacement

A BF16-only parameter update can stop improving loss even while every gradient is finite. Wide exponent range doesn't provide fine spacing near the current weight, so keep FP32 update state under ordinary AMP and log the storage policy.

FP16 silently loses gradients

Training can look stable while policy_pass_rate lags the FP32 baseline when small unscaled FP16 gradients underflow to zero. Use GradScaler, track non-finite or skipped steps, and compare the declared held-out metric against the same baseline.

Gradient clipping sees scaled values

If clipping behaves erratically or training diverges under FP16 AMP, inspect whether the run clips before scaler.unscale_(optimizer). Unscale first, then apply gradient clipping, and let the scaler perform or skip the optimizer step.

GradScaler steps every micro-batch

If effective batch size collapses or later micro-batches write into already-unscaled gradients, unscale_ / step / update ran inside the accumulation window. Scale and backward every micro-batch, then unscale, clip, step, and update once per optimizer step.

Memory claims omit optimizer state

"Half-memory" planning fails when only activation dtype changed while FP32 parameters and Adam moments remain large. Log a component-level memory profile or accounting budget instead of a dtype slogan.

Distributed bandwidth remains high

BF16 compute can be enabled while cross-worker traffic remains a bottleneck because reduction payloads stayed FP32. Inspect and record reduce_dtype separately, then measure held-out quality and communication changes before promotion.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.Which precision choice best matches a workload that needs FP32-like exponent range but can tolerate coarser mantissa resolution?

Correct answer: Use BF16 for the lower-precision path and retain FP32 where update accuracy or reduction stability requires it.

BF16 keeps a wide exponent range close to FP32 while sacrificing mantissa precision. Mixed precision is a contract, so sensitive accumulations or updates can remain FP32 rather than assuming every tensor should share one dtype.

2.A small FP32 gradient repeatedly disappears when cast to FP16, while the model is otherwise stable. What is the targeted remedy?

Correct answer: Scale the loss before backpropagation, then unscale gradients before clipping and the optimizer step.

Loss scaling moves small gradients into a representable range during backpropagation. They must be unscaled before clipping or updating so thresholds and optimizer math operate in the original scale.

3.What should a precision audit predict for values near 1.2e-8 and 1e5?

Correct answer: FP16 can underflow the small value and overflow the large one, while BF16 generally preserves the range with coarser precision.

The article uses these magnitudes to expose FP16's narrow dynamic range. BF16 has less precision but a much wider exponent range, so it is less vulnerable to these range failures.

4.A scaled backward pass produces a nonfinite gradient in one parameter group. What should the optimizer boundary do?

Correct answer: Skip the whole optimizer step, adjust the scale according to policy, and continue with a later batch.

A partially applied step can leave optimizer state inconsistent across parameters. Loss scaling logic treats a nonfinite result as a failed step and skips the update as a unit.

5.With gradient accumulation over several microbatches, when should a GradScaler update and optimizer step occur?

Correct answer: At the optimizer boundary after all microbatches are accumulated, with unscale and clipping applied to the accumulated gradients.

Accumulation changes where one logical optimizer update occurs. Scaling, unscaling, finite checks, clipping, and scale updates need to align with that boundary rather than treating each microbatch as a separate optimizer step.

6.An AMP memory report drops from 4.47 GB to 2.98 GB. What reduction should be reported?

Correct answer: About 33.3%, because the drop is 1.49 GB relative to the 4.47 GB FP32 baseline.

The relative reduction is (4.47 - 2.98) / 4.47, approximately one third. Mixed precision does not imply a 50% end-to-end reduction because some state and buffers remain in higher precision.

7.Why might a distributed training setup choose BF16 compute but FP32 gradient reduction?

Correct answer: Compute and reduction dtype are separate precision decisions; FP32 accumulation can reduce collective-rounding error.

The compute path and collective accumulation have different numerical risks. Keeping reduction in FP32 can improve stability without forcing every matrix operation or activation to use FP32.

8.An RL policy trains in BF16 but serves in a different precision path, and reward drops only after deployment. What is the first diagnostic?

Correct answer: Run matched train and inference precision fixtures and compare logits, sampling, and reward before tuning.

RL is sensitive to small distribution changes because they alter trajectories. A precision mismatch must be measured on the same inputs and sampling contract before deciding whether to change scales or tolerances.

9.What makes a mixed-precision result comparable across runs?

Correct answer: Fix the data, seed, evaluation protocol, hardware context, and precision settings, then gate the relevant metrics.

Precision comparisons need a stable contract. Otherwise changes in data order, evaluation, hardware, or dtype can be mistaken for a precision effect and hide regressions behind an aggregate score.

9 questions remaining.

Next Step
Continue to Distributed Training: FSDP & ZeRO

You can now choose FP16 or BF16 from measured stability, and you know that ordinary AMP often leaves FP32 parameter and <span data-glossary="optimizer-state">optimizer state</span> behind. Distributed training shards those leftover states, keeps `param_dtype` and `reduce_dtype` as separate fields, and uses a bytes-per-parameter count that depends on which copies actually exist.

PreviousSupervised Fine-Tuning Pipeline
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Automatic Mixed Precision package - torch.amp

PyTorch Contributors · 2026

https://docs.pytorch.org/docs/stable/amp.html

Mixed Precision Training.

Micikevicius, P., et al. · 2018

https://arxiv.org/abs/1710.03740

A Study of BFLOAT16 for Deep Learning Training.

Kalamkar, D., et al. · 2019

https://arxiv.org/abs/1905.12322

ZeRO: Memory Optimizations Toward Training Trillion Parameter Models.

Rajbhandari, S., et al. · 2020 · SC 2020

https://arxiv.org/abs/1910.02054

PyTorch FSDP: Experiences on Scaling Fully Sharded Data Parallel.

Zhao, Y., et al. · 2023 · VLDB 2023

https://arxiv.org/abs/2304.11277

FullyShardedDataParallel

PyTorch Contributors · 2026

https://docs.pytorch.org/docs/stable/fsdp.html

Defeating the Training-Inference Mismatch via FP16

Qi, P., Liu, Z., Zhou, X., Pang, T., Du, C., Lee, W. S., & Lin, M. · 2025

https://arxiv.org/abs/2510.26788

FP8 Formats for Deep Learning.

Micikevicius, P., et al. · 2022

https://arxiv.org/abs/2209.05433

Using FP8 and FP4 with Transformer Engine

NVIDIA · 2026

https://docs.nvidia.com/deeplearning/transformer-engine/user-guide/examples/fp8_primer.html

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.