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 192 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/27
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 Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingModel 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 & AutoscalingDistributed 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
LearnInference & Production ScaleLow-Precision GPU Programming
🚀HardInference Optimization

Low-Precision GPU Programming

Build numerically sound FP16, BF16, TF32, FP8, and MXFP8 GPU paths by making scale, accumulation, layout, and kernel contracts explicit.

19 min read
Learning path
Step 149 of 192 in the full curriculum
GEMM From Naive CUDA to Tensor CoresModel Quantization: GPTQ, AWQ & GGUF

Personalize this lesson

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

A GPU operation gets faster after you switch it to a smaller number format. Then one request returns a plausible vector with a 98% error. Nothing crashes. Every value is finite. The narrow input format clipped a spike before the wider accumulator ever saw it.

The previous GEMM lesson tuned a general matrix multiplication (GEMM) while keeping operand, accumulator, and output types separate. The mixed-precision training lesson showed how scaling keeps gradients inside a 16-bit format's range. Here, those ideas become an explicit numeric contract for each tensor and kernel.

That incident separates low-precision programming from changing a dtype argument. A production path needs four compatible contracts:

  1. Encoding: which bit patterns represent finite values, zeros, infinities, and NaNs?
  2. Scaling: which values share a scale, when is it measured, and how is it stored?
  3. Arithmetic: which format enters the multiplier, accumulates partial sums, and stores the epilogue?
  4. Kernel application binary interface (ABI): which layouts, alignments, architecture targets, and scale orientations does the implementation accept?

The running operation is a small Y=XWY=XWY=XW. XXX has two rows of 32 activations. One row stays near 10−310^{-3}10−3 to 111; the other reaches 500500500. WWW is a 32-element vector. We'll compare 8-bit floating-point (FP8) inputs with a 32-bit floating-point (FP32) accumulator. This setup is small enough to audit every conversion but has the same failure mode as a large projection: one global scale lets the large row erase detail in the small row.

A tensor-core kernel accepts FP8 inputs and accumulates into FP32. Does FP32 accumulation guarantee a correct answer?

Answer

No. FP32 accumulation preserves the products that reach it, but it can't recover values already rounded to zero or clipped during FP8 conversion. Input scaling and conversion semantics remain part of correctness.


Read formats as range plus spacing

A format name says how many exponent and fraction bits are available. More exponent bits buy dynamic range. More fraction bits buy precision near a fixed magnitude. The table compares FP32, NVIDIA TensorFloat-32 (TF32), bfloat16 (BF16), IEEE half precision (FP16), and two FP8 encodings.

FormatStored layoutLargest finite magnitudeSmallest positive normalTypical GPU role
FP321 sign, 8 exponent, 23 fractionabout 3.40×10383.40\times10^{38}3.40×1038about 1.18×10−381.18\times10^{-38}1.18×10−38reference math, accumulation, sensitive reductions
TF32FP32 storage; multiplier reads 8 exponent and 10 fraction bitsFP32 rangeFP32 normal rangefaster FP32 matrix math on supported NVIDIA tensor cores
BF161 sign, 8 exponent, 7 fractionabout 3.39×10383.39\times10^{38}3.39×1038about 1.18×10−381.18\times10^{-38}1.18×10−38broad-range activations, weights, gradients
FP161 sign, 5 exponent, 10 fraction65,504about 6.10×10−56.10\times10^{-5}6.10×10−5higher local precision, narrower range
FP8 E4M31 sign, 4 exponent, 3 fraction4482−62^{-6}2−6forward tensors when precision matters more than range
FP8 E5M21 sign, 5 exponent, 2 fraction57,3442−142^{-14}2−14gradients or tensors needing more range

The Open Compute Project (OCP) 8-bit floating-point specification, shortened to OFP8, defines these two encodings. OFP8 E4M3 reaches subnormals down to 2−92^{-9}2−9 and uses its extreme exponent patterns for finite numbers, leaving two NaN encodings and no infinity. OFP8 E5M2 reaches subnormals down to 2−162^{-16}2−16 and retains IEEE-style infinities and NaNs.[1]Reference 1OCP 8-bit Floating Point Specification (OFP8), Revision 1.0https://www.opencompute.org/documents/ocp-8-bit-floating-point-specification-ofp8-revision-1-0-2023-12-01-pdf-1

Two comparisons prevent common design mistakes:

  • FP16 versus BF16: both store 16 bits. FP16 has three more fraction bits, while BF16 inherits FP32's eight exponent bits. FP16 resolves nearby values more finely; BF16 survives much larger and smaller magnitudes.
  • TF32 versus a tensor dtype: TF32 is an NVIDIA compute mode for FP32 matrix operations, not an 11-bit storage type. FP32 tensors enter a tensor-core multiplier with reduced fraction precision, while products normally accumulate in FP32. Current PyTorch documentation exposes explicit TF32 precision controls and notes that defaults differ between matrix multiplication and convolution.[2]Reference 2Numerical Accuracyhttps://docs.pytorch.org/docs/stable/notes/numerical_accuracy.html

Roundoff grows with magnitude. Around 1, E4M3 spacing is 2−3=0.1252^{-3}=0.1252−3=0.125; around 256, its spacing is 32. Scaling moves values into a useful part of that nonuniform grid.

Follow one value through the kernel

For each operand, use scale s>0s>0s>0 and encode

q=cast⁡F(xs),x^=sqq = \operatorname{cast}_{F}\left(\frac{x}{s}\right), \qquad \hat{x}=s qq=castF​(sx​),x^=sq

where FFF is the low-precision format. The GEMM then computes products from reconstructed values and accumulates them in the declared accumulator type:

Yij=cast⁡O(∑kX^ikW^kj)Y_{ij}=\operatorname{cast}_{O}\left(\sum_k \hat{X}_{ik}\hat{W}_{kj}\right)Yij​=castO​(∑k​X^ik​W^kj​)

The output cast OOO is another rounding boundary. A BF16 epilogue can hide improvement gained from an FP32 accumulator if the next operator only receives BF16.

Diagram showing BF16 or FP32 X, measure amax, choose X scale, and scale + FP8 cast.
BF16 or FP32 X, measure amax, choose X scale, and scale + FP8 cast.

Treat every arrow as owned state. If a framework exports QX but omits SX, another runtime sees codes without their units. If it keeps a rowwise scale but launches a kernel expecting tensorwise scale, the bytes are valid and the numbers are wrong.

Why is an FP8 tensor plus its scale closer to a typed value than the FP8 bytes alone?

Answer

The FP8 code q represents x divided by s. Reconstructing the application value requires x_hat = s times q, so scale, granularity, and axis are part of the tensor's numeric meaning.


Conversion has three visible failure modes

Saturation handles values beyond the finite range. Under OFP8 saturating conversion, 500 cast directly to E4M3 becomes 448. Under non-saturating conversion, an E4M3 overflow becomes NaN, while E5M2 overflow becomes infinity. CUDA's FP8 conversion API exposes saturation modes and round-to-nearest-even behavior.[1]Reference 1OCP 8-bit Floating Point Specification (OFP8), Revision 1.0https://www.opencompute.org/documents/ocp-8-bit-floating-point-specification-ofp8-revision-1-0-2023-12-01-pdf-1[3]Reference 3FP8 Conversion and Data Movementhttps://docs.nvidia.com/cuda/cuda-math-api/cuda_math_api/group__CUDA__MATH__FP8__MISC.html

Underflow handles values below the smallest subnormal. OFP8 conversion rounds them to signed zero. Values between the smallest normal and smallest subnormal can survive as subnormals, but with reduced significant precision. Hardware may flush some denormals. PyTorch documents a specific ROCm caveat: on MI200-class products, FP16 and BF16 V_DOT2 and matrix fused multiply-add instructions flush input and output denormals; its other supported AMD GPUs don't have this behavior.[1]Reference 1OCP 8-bit Floating Point Specification (OFP8), Revision 1.0https://www.opencompute.org/documents/ocp-8-bit-floating-point-specification-ofp8-revision-1-0-2023-12-01-pdf-1[2]Reference 2Numerical Accuracyhttps://docs.pytorch.org/docs/stable/notes/numerical_accuracy.html

Rounding chooses a nearby representable value. OFP8 conversion requires round-to-nearest, ties-to-even. The rule is deterministic, but repeated casts can still bias a computation when values repeatedly land on one side of a grid boundary.[1]Reference 1OCP 8-bit Floating Point Specification (OFP8), Revision 1.0https://www.opencompute.org/documents/ocp-8-bit-floating-point-specification-ofp8-revision-1-0-2023-12-01-pdf-1

Clipping telemetry and NaN checks observe different failures. Saturation can produce a finite but badly wrong result. A health check that only counts NaNs misses it.

Worked failure: stale scale, finite answer

Suppose delayed E4M3 scaling uses yesterday's amax of 10:

sold=10448s_{old}=\frac{10}{448}sold​=44810​

Today, a value spikes to 500. Its scaled magnitude is 22,400, so saturating conversion clamps it to 448. Reconstruction returns

x^=448⋅10448=10\hat{x}=448\cdot \frac{10}{448}=10x^=448⋅44810​=10

The value is finite and has the right sign, but its relative error is 98%. FP32 accumulation faithfully sums the wrong product.

Scale granularity sets who competes for range

GranularityScale count for X∈RM×KX\in\mathbb{R}^{M\times K}X∈RM×KBenefitCost or risk
Tensorwise1minimal metadata and simple kernelsone outlier controls every value
RowwiseMMMseparates activation rows or tokensaxis-specific layout and scale loads
ColumnwiseKKKuseful when contraction or output channels differisn't interchangeable with rowwise packing
Groupwiseroughly MK/gMK/gMK/glocalizes outliers within groups of size gggmore metadata and indexing work
MXFP8 blockwiseone E8M0 scale per 32 elementsstandardized local power-of-two scaleblock alignment, padding, and orientation constraints

The best granularity follows the distribution and access pattern. Finer scaling reduces competition between outliers and small values, but kernels must fetch and apply more metadata. A theoretically accurate grouping can lose overall throughput if it breaks coalesced access or lacks a fused kernel.

Current scaling and delayed scaling

An amax is the largest absolute value in a scale group. That measurement is an observation, not a scale by itself.

Current scaling measures the tensor being converted, derives a scale, then casts it. NVIDIA Transformer Engine describes one FP32 scale per tensor for its current-scaling FP8 recipe. The extra amax pass means the input is read twice: once to reduce amax and again to scale and cast.[4]Reference 4Using FP8 and FP4 with Transformer Enginehttps://docs.nvidia.com/deeplearning/transformer-engine/user-guide/examples/fp8_primer.html

Delayed scaling chooses today's scale from prior amax values, so a fused kernel can cast today's input while collecting an amax for a future step. An amax history may choose its maximum or its most recent sample. The saved read improves execution, but a sudden spike can clip under a stale scale. A stale historical maximum can cause the opposite problem: no clipping, yet common values occupy too little of the FP8 grid.[4]Reference 4Using FP8 and FP4 with Transformer Enginehttps://docs.nvidia.com/deeplearning/transformer-engine/user-guide/examples/fp8_primer.html

Use the recipe name together with its state:

text
1fp8_contract = { 2 format: E4M3, 3 granularity: tensorwise, 4 scale_rule: current, 5 rounding: nearest_even, 6 overflow: saturate, 7 accumulator: FP32, 8 output: BF16 9}

Transformer Engine's common hybrid recipe uses E4M3 for forward activations and weights, then E5M2 for gradients because gradients need more range. That convention is a recipe choice, not a property that forces every inference tensor into E4M3.[4]Reference 4Using FP8 and FP4 with Transformer Enginehttps://docs.nvidia.com/deeplearning/transformer-engine/user-guide/examples/fp8_primer.html[5]Reference 5FP8 Formats for Deep Learning.https://arxiv.org/abs/2209.05433

A delayed-scaling run has no NaNs, but clip count jumps after a distribution shift. Which state should be inspected first?

Answer

Inspect the scale derived from amax history against the current amax. A stale scale can saturate new spikes into finite maximum values, so NaN count stays zero while error grows.


MXFP8 makes scale ownership part of the format

The OCP Microscaling (MX) specification describes an MX tensor as element values PiP_iPi​, a shared scale type XXX, and block size kkk. Microscaling FP8 (MXFP8) uses 32 E4M3 or E5M2 elements with one E8M0 scale per block. E8M0 stores a power-of-two exponent, so scale multiplication can be implemented as exponent adjustment.[6]Reference 6OCP Microscaling Formats (MX) Specification, Version 1.0https://www.opencompute.org/documents/ocp-microscaling-formats-mx-v1-0-spec-final-pdf

One 32-value block contains 256 element bits plus 8 scale bits, an effective payload of 8.258.258.25 bits per value before padding or container overhead. Other standardized MX members trade more precision for density, including MXFP6, MXFP4, and MXINT8.[6]Reference 6OCP Microscaling Formats (MX) Specification, Version 1.0https://www.opencompute.org/documents/ocp-microscaling-formats-mx-v1-0-spec-final-pdf

MX changes the interface, not the need for a kernel contract:

  • The contraction dimension is typically padded to a multiple of 32.
  • Scale and element bytes need their specified physical layout.
  • Rowwise and columnwise quantizations of a matrix are different numeric objects. NVIDIA's MXFP8 documentation warns that a packed rowwise tensor can't be transposed to obtain the columnwise representation; both orientations must be quantized independently from higher precision data.[4]Reference 4Using FP8 and FP4 with Transformer Enginehttps://docs.nvidia.com/deeplearning/transformer-engine/user-guide/examples/fp8_primer.html
  • The OCP general dot-product definition says its result should be FP32, while internal precision and operation order remain implementation-defined. Bitwise equality across kernels isn't implied.[6]Reference 6OCP Microscaling Formats (MX) Specification, Version 1.0https://www.opencompute.org/documents/ocp-microscaling-formats-mx-v1-0-spec-final-pdf

Run the two-row experiment

This exercise uses PyTorch's OCP E4M3 dtype. Tensorwise scaling uses the exact amax ratio. The MXFP8-like path uses one conservative power-of-two scale per 32-value row so every block maximum fits inside E4M3. OCP permits multiple scale-selection algorithms, so the exercise demonstrates granularity rather than claiming one universal MX encoder.

Predict what happens to 0.001 when it shares one scale with 500, then run the code.

fp8-scale-granularity.py
1import math 2import torch 3 4torch.set_printoptions(precision=6, sci_mode=False) 5 6pattern_small = [0.001, 0.03, 0.117, 0.5, 1.0, -0.25, 0.0625, -0.015625] 7pattern_large = [4.0, 16.0, 64.0, 500.0, -2.0, -8.0, -32.0, -250.0] 8weight_pattern = [0.5, -0.25, 0.125, -0.0625, 0.03125, -0.5, 0.25, -0.125] 9 10x = torch.tensor([pattern_small * 4, pattern_large * 4], dtype=torch.float32) 11w = torch.tensor(weight_pattern * 4, dtype=torch.float32).reshape(32, 1) 12fp8_max = torch.finfo(torch.float8_e4m3fn).max 13 14def roundtrip(values: torch.Tensor, scale: torch.Tensor) -> torch.Tensor: 15 return (values / scale).to(torch.float8_e4m3fn).float() * scale 16 17tensor_scale_x = x.abs().max() / fp8_max 18tensor_scale_w = w.abs().max() / fp8_max 19x_tensor = roundtrip(x, tensor_scale_x) 20w_tensor = roundtrip(w, tensor_scale_w) 21 22def conservative_power_of_two_scale(block: torch.Tensor) -> float: 23 ratio = block.abs().max().item() / fp8_max 24 return 2.0 ** math.ceil(math.log2(ratio)) 25 26mx_scales_x = torch.tensor( 27 [conservative_power_of_two_scale(row) for row in x], dtype=torch.float32 28).reshape(2, 1) 29mx_scale_w = torch.tensor(conservative_power_of_two_scale(w), dtype=torch.float32) 30x_mx = roundtrip(x, mx_scales_x) 31w_mx = roundtrip(w, mx_scale_w) 32 33reference_y = (x @ w).squeeze() 34tensor_y = (x_tensor @ w_tensor).squeeze() 35mx_y = (x_mx @ w_mx).squeeze() 36 37print(f"tensor_scale_x={tensor_scale_x.item():.6f}") 38print(f"mxfp8_scales_x={mx_scales_x.squeeze().tolist()}") 39print( 40 "x[0,0]: " 41 f"reference={x[0, 0].item():.8f} " 42 f"tensor_fp8={x_tensor[0, 0].item():.8f} " 43 f"mxfp8={x_mx[0, 0].item():.8f}" 44) 45print( 46 "x[1,3]: " 47 f"reference={x[1, 3].item():.8f} " 48 f"tensor_fp8={x_tensor[1, 3].item():.8f} " 49 f"mxfp8={x_mx[1, 3].item():.8f}" 50) 51print(f"reference_y=[{reference_y[0]:.6f}, {reference_y[1]:.6f}]") 52print(f"tensor_fp8_y=[{tensor_y[0]:.6f}, {tensor_y[1]:.6f}]") 53print(f"mxfp8_y=[{mx_y[0]:.6f}, {mx_y[1]:.6f}]") 54print(f"tensor_abs_error={(tensor_y - reference_y).abs().tolist()}") 55print(f"mxfp8_abs_error={(mx_y - reference_y).abs().tolist()}")
Output
1tensor_scale_x=1.116071 2mxfp8_scales_x=[0.00390625, 2.0] 3x[0,0]: reference=0.00100000 tensor_fp8=0.00000000 mxfp8=0.00097656 4x[1,3]: reference=500.00000000 tensor_fp8=500.00000000 mxfp8=512.00000000 5reference_y=[0.600812, 7.750000] 6tensor_fp8_y=[0.583104, 7.568359] 7mxfp8_y=[0.601562, 7.750000] 8tensor_abs_error=[0.017708778381347656, 0.181640625] 9mxfp8_abs_error=[0.0007500052452087402, 0.0]

The tensorwise scale is about 1.116, so the smallest nonzero reconstructed E4M3 value is 1.116×2−9≈0.002181.116\times2^{-9}\approx0.002181.116×2−9≈0.00218. The first activation, 0.001, falls below half that first step and rounds to zero. Its row loses almost 3% in the final dot product.

The first block's power-of-two scale is 2−82^{-8}2−8. Its smallest subnormal reconstructs as 2−17≈7.63×10−62^{-17}\approx7.63\times10^{-6}2−17≈7.63×10−6, so 0.001 survives as 0.00097656. The large row gets its own scale of 2.0, but its 500 still rounds to 512. The second dot product happens to be exact because positive and negative rounding errors cancel for this weight vector, not because block scaling preserves every operand.

Side-by-side tables compare one tensorwise E4M3 scale with one power-of-two scale per 32-value row. With global scale 1.116071, 0.001 reconstructs as zero. With row scales 0.00390625 and 2, 0.001 reconstructs as 0.00097656 while 500 rounds to 512; the second dot product is exact only because rounding errors cancel for this weight vector.
One scale forces both rows onto the same E4M3 grid, erasing 0.001. Separate 32-value scales recover the small row, while 500 still rounds to 512; the exact second dot product comes from error cancellation, not lossless encoding.

The result doesn't prove MXFP8 always wins. It isolates one mechanism. On a distribution with uniform magnitude, per-block metadata may add traffic without enough accuracy benefit. Measure error and kernel throughput together.

Accumulation is a separate precision decision

A multiply format and an accumulator format solve different problems. FP8 or FP16 operands reduce storage and can expose higher-throughput tensor-core paths. FP32 accumulation reduces loss while many products are added.

FP32 accumulation still has limits:

  • Products are formed from already rounded inputs.
  • Some libraries permit reduced-precision reductions for FP16 or BF16 GEMMs on supported hardware. PyTorch exposes flags to disable those paths when numerical error matters more than speed.[2]Reference 2Numerical Accuracyhttps://docs.pytorch.org/docs/stable/notes/numerical_accuracy.html
  • Split-K or parallel reductions change addition order. Floating-point addition isn't associative, so two correct kernels may differ in low bits.
  • Bias, activation, residual addition, and output cast can each introduce another precision boundary.

Record a full signature such as E4M3 x E4M3 -> FP32 accumulate -> BF16 output. Saying "FP8 GEMM" leaves the most important correctness choices unstated.

Hardware support is a dated claim

The table below is a documentation snapshot verified 2026-08-29. Architecture capability, installed toolkit, framework release, and chosen kernel all have to agree.

EcosystemDocumented architecture support in this snapshotBoundary to keep explicit
NVIDIA TF32Ampere and later in current PyTorch guidanceenabled state and library heuristic affect whether a specific operation uses TF32
NVIDIA FP8CUDA capability tables list FP8 from compute capability 8.9; Transformer Engine documents FP8 on Ada, Hopper, and Blackwella dtype object or device capability doesn't promise every operator has an FP8 kernel
NVIDIA MXFP8Transformer Engine documents native MXFP8 on Blackwell SM 10.0 and 10.3block layout, dual orientation, and recipe support are kernel-specific
AMD FP8ROCm lists matrix-core FP8 on CDNA3, CDNA4, and RDNA4 familiesMI300-class CDNA3 paths use FNUZ; MI350-class CDNA4 paths use OCP FP8; RDNA4 and library support need path-specific checks

CUDA's current programming guide supplies the NVIDIA capability table.[7]Reference 7CUDA Programming Guide.https://docs.nvidia.com/cuda/cuda-programming-guide/ AMD's current precision matrix names example products and separates language types, matrix-core support, and library support.[8]Reference 8Data Types and Precision Supporthttps://rocm.docs.amd.com/en/latest/reference/precision-support.html

Interoperability needs particular care on AMD. FNUZ E4M3 and E5M2 encodings differ from OCP variants in special values and signed-zero behavior. Identical raw bytes can mean different numbers. PyTorch exposes OCP-style and FNUZ dtype names, plus E8M0 and packed low-bit types, but its own dtype documentation warns that shell dtype availability doesn't imply broad operator support.[9]Reference 9Tensor Attributeshttps://docs.pytorch.org/docs/stable/tensor_attributes.html

Write the kernel contract before choosing the kernel

Use a contract review that another engineer could implement without guessing:

FieldExample decisionFailure when omitted
Logical shapesX:[M,K]X:[M,K]X:[M,K], W:[K,N]W:[K,N]W:[K,N], K mod 32=0K\bmod32=0Kmod32=0wrong padding or tail handling
Element encodingsOCP E4M3 for both operandsOCP/FNUZ byte mismatch
Scale semanticsE8M0, one per 32 contiguous KKK valueswrong axis or stale metadata
Operand layoutsrow-major XXX, kernel-packed WWWvalid allocation, scrambled values
Scale layoutsrowwise XXX, columnwise WWWtransposed scale association
AccumulatorFP32unexplained reduction drift
EpilogueFP32 bias, GELU, BF16 storehidden output rounding
Overflow and roundingsaturate, nearest-evenNaN versus clamp disagreement
Alignmentpointer, stride, and tile constraintsfallback, fault, or slow path
Architecturecompiled target and minimum runtimeunsupported instruction or silent fallback

Treat framework, compiler, and kernel boundaries as serialization boundaries. Pass dtype tags, scales, orientation, padding, and accumulator mode together. Reject ambiguous inputs at dispatch rather than inferring them from tensor shape.

A framework exposes float8_e4m3fn, and the target GPU advertises FP8 instructions. Is that enough to dispatch an arbitrary FP8 operator?

Answer

No. Dtype registration and instruction support are necessary but not sufficient. The framework still needs a kernel for the exact operation, layout, scale recipe, accumulator, architecture, and runtime version.


Diagnose the numeric boundary

Start from the first boundary where the low-precision path diverges from an FP32 reference.

SymptomLikely boundaryTargeted probeTypical correction
Raw FP8 codes pinned at ±448E4M3 conversionclip count and pre-cast amaxrefresh scale or use finer granularity
Many exact zeros in one rowscale grouping or underflowzero rate by row/block before GEMMrowwise or blockwise scale
NaNs appear only with non-saturating castoverflow policycompare saturating and non-saturating conversionsspecify policy and fix range
Error starts after reductionaccumulator or split-K orderstore FP32 partials and compare reduction modeswider reduction or stable algorithm
Correct on one vendor, wrong on anotherencoding ABIdecode known byte patterns, especially zero/NaNconvert OCP and FNUZ explicitly
Correct values, disappointing speedlayout or kernel dispatchprofiler kernel name, bytes moved, fallback logpack expected layout or choose supported recipe
MX result breaks after transposescale orientationcompare fresh columnwise quantizationquantize each required orientation independently

A useful incident capture includes input histograms, amax per scale group, selected scales, clip and zero counts, accumulator mode, output error against FP32, actual kernel symbol, and device/runtime versions. A single aggregate relative error hides where the contract failed.

Ship low precision as a measured contract

Low-precision GPU code is safe when every lossy boundary is named and observed:

  • Pick format from range and spacing, not bit width alone.
  • Make scale value, granularity, axis, update rule, and history explicit.
  • Count saturation and underflow even when outputs remain finite.
  • Separate operand encoding from accumulator and epilogue precision.
  • Version hardware claims and verify actual kernel dispatch.
  • Compare accuracy and throughput with the same shapes, layouts, and workload distribution used in serving.
Complete the lesson

Mastery Check

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

1.An E4M3 GEMM receives x = 500 with scale s = 1 and uses saturating conversion plus FP32 accumulation. Which value enters the product, and why can't FP32 accumulation repair it?

Correct answer: 448 enters the product; saturation already discarded the difference before accumulation.

OFP8 E4M3 has maximum finite magnitude 448. Saturating conversion clamps 500 to 448. The wide accumulator only sums products formed from that reconstructed operand, so it has no information about the lost 52.

2.A tensor contains stable values near 0.01 and rare values near 400. Which change most directly protects the small values while retaining E4M3?

Correct answer: Split the tensor into rowwise or blockwise scale groups so outliers control less unrelated data.

One global scale makes small and large regimes compete for the same FP8 range. Finer scale groups localize each outlier's influence. Accumulator precision can't restore inputs rounded to zero, and non-saturating overflow produces NaN or infinity rather than better finite values.

3.Why might E5M2 be chosen for a gradient tensor while E4M3 is chosen for a forward activation?

Correct answer: E5M2 has five exponent bits and much wider range, while E4M3 spends one more bit on fraction precision.

E5M2 reaches 57,344 and retains infinities, giving gradients room for larger dynamic range. E4M3 reaches 448 but has three fraction bits, so it offers finer spacing within its range. Scaling granularity is independent of this encoding choice.

4.A delayed E4M3 recipe derives scale from historical amax = 10, then current amax jumps to 500. Saturating reconstruction of the maximum returns 10. Which telemetry best exposes the failure?

Correct answer: Current amax versus historical scale plus the per-group clip count.

Delayed scaling can lag a distribution shift. Saturation keeps outputs finite, so NaN count stays quiet. Comparing current range with the scale source and counting clipped values identifies the stale-scale boundary.

5.An MXFP8 block stores 32 FP8 elements and one 8-bit E8M0 scale. Ignoring padding and container overhead, what is its effective payload per element?

Correct answer: 8.25 bits, because 256 element bits plus 8 scale bits are shared across 32 values.

The block payload is 32 times 8 element bits plus one 8-bit scale, or 264 bits. Dividing by 32 gives 8.25 bits per value before alignment, padding, and container metadata.

6.A kernel has a packed rowwise MXFP8 matrix but needs the same logical matrix as a columnwise operand. Which operation preserves the numeric contract?

Correct answer: Quantize the required columnwise orientation independently from higher-precision source data.

MX scales belong to consecutive groups along a particular orientation. A transpose changes group membership, so transposing packed codes and scales doesn't recreate the columnwise quantization. Both orientations need their own quantization.

7.The same FP8 byte buffer decodes differently on an AMD CDNA3 path and a path expecting OCP E4M3. What is the strongest first hypothesis?

Correct answer: The buffer uses FNUZ encoding, whose zeros and special values aren't byte-compatible with OCP E4M3.

ROCm distinguishes FNUZ FP8 types on CDNA3 from OCP FP8 types on newer listed architectures. Raw bytes aren't a portable interchange contract unless the exact encoding tag travels with them.

8.A framework defines an E4M3 dtype and the GPU advertises FP8 tensor-core instructions, yet an operation falls back to BF16. Which conclusion is most accurate?

Correct answer: Instruction and dtype support aren't sufficient; the exact operator, layout, scale recipe, toolkit, and framework version need a matching kernel.

Capability exists at several layers. Hardware instructions, compiler target, runtime, framework dispatch, operand layout, and recipe must line up. Missing one layer can cause a safe fallback even though the dtype object and device capability exist.

8 questions remaining.

Next Step
Continue to Model Quantization: GPTQ, AWQ & GGUF

It moves from floating-point execution formats to model artifacts that compress weights for deployment. Low-precision kernel contracts explain whether those quantized bytes produce a real memory or throughput win.

PreviousGEMM From Naive CUDA to Tensor Cores
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

OCP 8-bit Floating Point Specification (OFP8), Revision 1.0

Open Compute Project · 2023

https://www.opencompute.org/documents/ocp-8-bit-floating-point-specification-ofp8-revision-1-0-2023-12-01-pdf-1

Numerical Accuracy

PyTorch Contributors · 2026 · Official PyTorch Documentation

https://docs.pytorch.org/docs/stable/notes/numerical_accuracy.html

FP8 Conversion and Data Movement

NVIDIA · 2026 · CUDA Math API

https://docs.nvidia.com/cuda/cuda-math-api/cuda_math_api/group__CUDA__MATH__FP8__MISC.html

Using FP8 and FP4 with Transformer Engine

NVIDIA · 2026

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

FP8 Formats for Deep Learning.

Micikevicius, P., et al. · 2022

https://arxiv.org/abs/2209.05433

OCP Microscaling Formats (MX) Specification, Version 1.0

Open Compute Project · 2023

https://www.opencompute.org/documents/ocp-microscaling-formats-mx-v1-0-spec-final-pdf

CUDA Programming Guide.

NVIDIA · 2026

https://docs.nvidia.com/cuda/cuda-programming-guide/

Data Types and Precision Support

AMD · 2026 · ROCm Documentation

https://rocm.docs.amd.com/en/latest/reference/precision-support.html

Tensor Attributes

PyTorch Contributors · 2026 · Official PyTorch Documentation

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

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.