TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Reducing Numbers: FP16, BF16, INT8 Quantization, Post-Training Quantization, Quantization-Aware Training, and Calibration Data

Status: Draft (2026-10-16), partially measured. GPU measurements were performed in a single-use pod on an 8GB class laptop GPU in the home lab (median of iterations after warm-up). Accuracy was compared using a single public dataset OpenTTGames test match (388 frames at 60fps with markers). INT8 was run using PyTorch's post-training quantization on the CPU of the same pod (4-core limit). The principles of formats and calibration were verified with a laptop CPU example (code/02_quant_error.py, around 1 second). Part 2 of the series 「Reading Vision Models 3」. Latency budget is in Part 1.

Table of Contents

1. Why Reducing Bits Makes It Faster

Most neural network inference time is spent on two things: fetching numbers from memory, and multiplying and adding them. Reducing a number from 32 bits to 16 bits halves the bytes to fetch. Moreover, modern GPUs and phone chips have dedicated units that perform many 16-bit or 8-bit multiplications at once. According to NVIDIA's 2018-generation GPU specifications, INT8 compute throughput is 16x that of FP32, and FP16 is 8x (Wu et al. 2020, Table 1 — specification ratios, not actual measurements).

The trade-off is accuracy. Writing numbers with less precision introduces rounding errors, and a narrow range causes large numbers to overflow.

숫자 형식과 INT8 눈금 비트 나눔(가로 길이 = 비트 수) FP32 지수 8 가수 23 FP16 지수 5 가수 10 최대 65,504 BF16 지수 8 가수 7 범위 = FP32 INT8 정수 8 −128 … 127 s = 부호. 지수는 범위를, 가수는 촘촘함을 정한다 INT8 양자화: 실수 r 을 0–255 눈금 q 에 맞춘다 q=0 q=255 잘림 최솟값 상한 가장 가까운 눈금으로 반올림 → 255 r = S · (q − Z), S = (상한 − 최솟값) ÷ 255 범위를 넓게 잡으면 잘림은 줄지만 눈금 간격 S 가 커진다 범위를 좁게 잡으면 눈금은 촘촘하지만 큰 값이 잘린다
Figure: Bit allocation for FP32, FP16, BF16, and INT8, and how INT8 quantization fits real numbers to a 0–255 scale while clipping values exceeding the upper limit. Choosing a wider range reduces clipping but makes the grid coarser.

2. Two 16-Bit Formats: FP16 and BF16

Floating-point numbers are divided into sign, exponent, and mantissa. The exponent determines "how large or small the numbers can get," and the mantissa determines "how fine-grained the precision is."

Format Sign, Exponent, Mantissa Largest Number Spacing to Next Number (eps)
FP32 1 · 8 · 23 3.4×10³⁸ 1.19×10⁻⁷
FP16 1 · 5 · 10 65,504 9.77×10⁻⁴
BF16 1 · 8 · 7 3.4×10³⁸ 7.81×10⁻³

(code/02_quant_error.out.txt PyTorch values for . FP16 is fine-grained but has a narrow range, whereas BF16 has the same range as FP32 but is coarser. Trying them directly, 70,000 becomes inf in FP16 and 70,144 in BF16, while 1e-8 becomes 0 in FP16 and 1.00117e-8 in BF16. The relative error for a 256×256 matrix multiplication was 3.6×10⁻⁴ for FP16 and 2.9×10⁻³ for BF16.

In training, this range issue is significant. Micikevicius et al.'s "Mixed Precision Training" (2018) solved the problem where gradients smaller than 2⁻²⁴ become zero in FP16 by using loss scaling, multiplying the loss by a large number before dividing and maintaining a separate FP32 copy of the weights. With this approach, ResNet-50's ImageNet top-1 matched FP32 at 75.92 versus mixed precision at 76.04 (Table 1). BF16 has the same range as FP32, allowing it to be used without loss scaling, and Kalamkar et al. (2019) trained ResNet-50 with BF16 to achieve 74.7, matching FP32 (comparing similar evaluation methods, Section 4.1.2).

In inference, pre-trained weights are used, so the range issue is less severe. As long as activation values do not exceed 65,504, FP16 is often sufficient.

3. INT8: Fitting Real Numbers to a Grid

8-bit integers have only 256 slots. Jacob et al.'s (2018) affine quantization writes real numbers r as follows.

r = S · (q − Z)
S: 눈금 한 칸의 크기(실수),  Z: 실수 0 에 해당하는 정수(영점),  q: 0–255 의 정수
범위 [최솟값, 상한] 을 정하면  S = (상한 − 최솟값) / 255,  Z = round(−최솟값 / S)

The key point is setting Z so that the real number 0 is represented precisely as a single integer. Padding filled with zeros and zeros after ReLU are then represented without error. Multiplication is performed by accumulating uint8 × uint8 into int32, multiplying by the scale factor just once at the end, and reducing back to 8 bits, allowing the entire inference to be done solely with integers. The same paper quantizes graphs by folding batch normalization into convolutions beforehand (Part 3).

Results (Jacob et al. 2018) Real INT8 Condition
ResNet-50 ImageNet top-1 76.4 74.9 Table 4.1
MobileNet SSD (Width 50%) COCO mAP · Latency 16.7 · 121ms 16.6 · 61ms 2017 mobile big core, single thread (Table 4.4)
Face Detection (Width 25%) Latency 44ms (approx. 23fps) 28ms (approx. 36fps) Same mobile big core (Table 4.6)

The main point of this paper is that within the same 33ms budget, the INT8 model was about 10% more accurate than the real-number model (Section 4.2.1, mobile small core). This is because the time saved by lowering precision can be used to run a larger model.

How Many Grid Points: Per-Tensor vs. Per-Channel

If a single S is used for the entire weight tensor, a large value in even one channel will widen the scale to fit that channel, causing the rest to collapse. Specifying a separate S for each output channel (per-channel) eliminates this issue. Trying it directly (256×256 matrix multiplication, weights only in INT8):

Weights Per-Tensor Per-Channel
Uniform weights 1.06×10⁻² 7.13×10⁻³
One channel 20x 7.25×10⁻² (other channels 1.13×10⁻¹) 6.32×10⁻³

The same thing happens in real models. In Krishnamoorthi's (2018) whitepaper, converting MobileNet-v1 to 8 bits without training resulted in a top-1 accuracy of 0.001 for per-tensor scaling (completely broken), while per-channel scaling achieved 0.703 (compared to 0.709 for real numbers) (Table 3). Depthwise convolutions (Part 4) exhibit large variations in value magnitudes across channels, making them particularly vulnerable to this issue.

4. Post-Training Quantization (PTQ) and Calibration Data

Post-Training Quantizationpasses several hundred representative inputs through a trained model to measure the range of activation values per layer (calibration) and determines S and Z from those ranges. No training is required. Nagel et al.'s (2021) whitepaper noted that 500–1,000 unlabeled images are usually sufficient, and reported that following standard procedures kept models within 0.7% of FP32 for W8A8 (8-bit weights and activations).

The difficult part is how far to set the range. Using the minimum and maximum values directly causes the grid to become coarse due to a single rare large value. Narrowing the range by clipping the top end slightly (e.g., the 99.99th percentile) makes the grid finer, but the clipped values become inaccurate. We tested this with two types of activation values (calibration and test samples were drawn separately).

Activations Calibration Batch Min–Max 99.99th Percentile 99.9th Percentile
(A) Long-tailed distribution 1 Upper bound 23.1, error 0.057 21.0, 0.067 11.0, 0.158
(A) 64 Upper bound 215.1, error 0.144 22.7, 0.059 10.3, 0.170
(B) Rare large values are signals 64 Upper bound 109.4, error 0.076 51.0, 0.180 4.1, 0.707

Therefore, calibration methods must be evaluated per model. In the table from Wu et al. (2020), PTQ on EfficientNet-B0 yielded 22.3% for min–max, 72.1% for entropy, and 70.9% for the 99.9th percentile (FP32 was 76.9%), while Inception v4 achieved 0.1% for min–max and 79.6% for the 99.99th percentile (FP32 was 79.7%) (ImageNet top-1, 1,024 calibration images, Table 5).

5. Quantization-Aware Training (QAT)

If PTQ falls short, quantization is simulated starting from training. Forward passes calculate values using "quantized-and-dequantized" weights and activations (fake quantization), while backward passes let gradients flow directly as if there were no rounding. In effect, the model learns to shift its weights to fit the grid. In Krishnamoorthi's whitepaper, MobileNet-v1, which collapsed with per-tensor scaling (0.001), recovered to 0.70 with QAT (Table 4), and in Wu et al., EfficientNet-B0 reached 76.95% with QAT, surpassing the best PTQ result (72.1%) (fine-tuned for about 10% of the original training schedule, Table 7). The trade-off is the requirement for a training pipeline and data.

6. Our Measurements: FP16 for the Ball Model

Speed

The TT Score ball model (BlurBall, 3-frame window, 512×288) was measured on an experimental GPU (batch size 1, median of 250 runs after 20 warm-up runs).

Precision Single Window
FP32 (Production mode) 15.09ms
FP16, autocast (automatic 16-bit per operation) 9.77ms (1.54x)
FP16, entire model in half precision 9.49ms (1.59x)

On the same GPU, the transformer encoder (EfficientTAM-S, Part 4) was 3.1x faster. The ball model is a convolutional network that maintains high resolution all the way through, so the share of dedicated 16-bit hardware acceleration is relatively small, and element-wise operations like batch normalization and ReLU remain. In Part 3, fusing these with a compiler brought it down to 4.3ms.

Accuracy: Does the Ball Position Change?

On a single test match from OpenTTGames featuring 388 frames at 60fps with ball position markers (all containing a ball), FP32 and FP16 were run on the same inputs using production settings (heatmap of the last frame, connected component analysis with a threshold of 0.5). Results within 5px of the ground truth (based on 512×288) were counted as correct. Inputs compressed and decompressed using JPEG quality 70 (same conditions as sent by the browser) were also run with FP32.

Input & Precision Hits / False Positives / Misses Precision · Recall Frames with identical detection status to FP32 Frames where the heatmap maximum position shifted
Original · FP32 384 / 2 / 4 0.995 · 0.990 — —
Original · FP16 (autocast) 384 / 2 / 4 0.995 · 0.990 100% 4.6% (up to 2px)
Original · FP16 (half) 384 / 2 / 4 0.995 · 0.990 100% 0.8% (up to 1px)
JPEG 70 · FP32 377 / 1 / 11 0.997 · 0.972 97.9% 34.3%

FP16 did not change a single judgment across these 388 frames. The difference in heatmap values was around 0.0001 at the center. In contrast, JPEG compression increased missed balls from 4 to 11 and shifted the maximum position by a third of a frame. Rather than halving precision, compressing the input caused a much larger error. Since production already receives JPEGs, we believe enabling FP16 will not cause significant changes compared to now. However, this is based on a single match and condition, and we did not measure degradation conditions (darkness, blur) or production GPUs (not measured).

7. Our Measurement: Post-Training INT8 Quantization of the Ball Model (CPU)

We could not measure INT8 on the GPU this time because it requires a runtime like TensorRT (Part 3). Instead, we converted the same model to INT8 on the CPU using PyTorch's post-training quantization (FX graph mode, x86 integer engine). Calibration frames were selected from the beginning of the match and evaluation frames from the end, ensuring no overlap.

220 evaluation frames (215 with balls), 146 calibration candidates, limited to 4 CPU cores, showing the median time per window run in batches of 4.

Method Calibration Frames Hits / False Positives / Misses Recall Max Heatmap Difference (vs. FP32, Median) Time per CPU Window
FP32 — 208 / 3 / 7 0.967 — 519.9ms
INT8, Default (activation histogram, per-channel weights) 1 206 / 3 / 9 0.958 0.064 72.2ms
Same 8 207 / 3 / 8 0.963 0.056 71.3ms
Same 64 208 / 3 / 7 0.967 0.057 63.1ms
INT8, Min-Max, Single Weight Tensor 8 207 / 3 / 8 0.963 0.086 71.4ms
INT8, Min-Max, Per-Channel Weights 8 208 / 2 / 7 0.967 0.084 72.5ms

8. Limitations

9. Connection to Our Project

The next post covers compilers and runtimes that change the execution methodwhile keeping precision intact.

A word from TT. We halved the numbers, but the ball position stayed the same. Compressing the images caused more distortion instead. You never know where errors occur until you measure them!

References

Sign in to like

Comments

No comments yet.

Sign in to leave a comment