제1부 GPU 아키텍처
1.13 수치 정밀도 I — 부동소수 포맷
1.13.1 이 장에서 배우는 것 / 선수 지식
딥러닝 가속에서 쓰이는 수치 포맷을 비트 단위로 이해하는 장이다. 텐서코어가 이 포맷들을 어떻게 먹는지는 1.11~1.12에서 봤고, 이를 쓰는 법(혼합정밀도 학습·양자화·희소성)은 1.14, 추론 양자화 실무는 5.4다. 이 장은 그 사이의 수치적 토대 — 포맷 자체.
학습 목표:
- FP32·TF32·FP16·BF16·FP8(E4M3·E5M2)·INT8·FP4의 비트 레이아웃(부호·지수·가수) 을 외우지 않고 유도한다.
- 각 포맷의 동적 범위(지수) 와 정밀도(가수) 트레이드오프를 수치로 설명한다.
- 한 실수값을 여러 포맷으로 인코딩하며 오차가 값 크기에 어떻게 의존하는지 직접 본다.
선수 지식: IEEE 754 부동소수의 대략적 형태(부호·지수·가수), 텐서코어가 저정밀 입력·고정밀 누산을 한다는 점(1.11), 메모리바운드·대역폭(1.10).
버전 주의: 포맷의 표준(비트 폭·분배)은 안정적이지만, 어떤 하드웨어 세대가 어떤 포맷을 가속하는지는 세대 의존(1.12). "시점 의존, 확인 필요".
1.13.2 왜 비트를 줄이는가
딥러닝 연산의 두 비용은 연산량(FLOPs) 과 데이터 이동(바이트) 이다. 수치 비트를 줄이면 둘 다 직접 내려간다.
비트를 절반으로 (FP32→FP16/BF16):
· 메모리 점유 1/2 → 더 큰 모델·배치
· 대역폭 소모 1/2 → 메모리바운드 커널 2배 (1.10)
· 텐서코어 처리량 ↑ (세대별, 1.11)
비트↓ → 처리량↑·메모리↓ BUT 표현력↓ → 오차↑
핵심 긴장: 비트를 줄이면 빠르고 작아지지만, 표현할 수 있는 값의 집합이 듬성듬성해진다. 그 듬성함이 모델 품질을 깨지 않는 한도 내에서 최대한 비트를 줄이는 것 — 이것이 정밀도 엔지니어링의 전부다.
부동소수 한 워드는 세 필드로 값을 만든다:
값 ≈ (-1)^부호 × 1.가수 × 2^(지수 - 바이어스)
sign mantissa exponent
부호 1비트 "얼마나 촘촘히" "얼마나 멀리까지"
= 정밀도 = 동적 범위
- 지수 비트(E) 가 많을수록 → 표현 가능한 범위가 넓다. 범위는 대략
2^(2^E)규모로 지수적으로 커진다. - 가수 비트(M) 가 많을수록 → 인접한 두 표현값 사이가 촘촘하다 = 상대 정밀도가 높다. 가수 M비트면 상대 오차 상한 ≈
2^-(M+1).
같은 16비트를 지수에 더 줄지(BF16) 가수에 더 줄지(FP16)가 곧 "범위 우선이냐 정밀도 우선이냐"의 선택이다.
1.13.3 비트 레이아웃 — 포맷 도감
각 포맷은 S(부호) / E(지수) / M(가수) 비트로 정의된다(총비트 = 1 + E + M).
| 포맷 | 총비트 | S | E | M | 바이어스 | 표준/출처 |
|---|---|---|---|---|---|---|
| FP32 | 32 | 1 | 8 | 23 | 127 | IEEE 754 |
| TF32 | 19* | 1 | 8 | 10 | 127 | NVIDIA (Ampere+) |
| FP16 | 16 | 1 | 5 | 10 | 15 | IEEE 754 |
| BF16 | 16 | 1 | 8 | 7 | 127 | Google/산업 표준 |
| FP8 E4M3 | 8 | 1 | 4 | 3 | 7 | OCP FP8 |
| FP8 E5M2 | 8 | 1 | 5 | 2 | 15 | OCP FP8 |
| FP4 E2M1 | 4 | 1 | 2 | 1 | 1 | OCP MX (Blackwell) |
* TF32는 저장은 FP32(32비트), 텐서코어 연산 시 가수를 10비트로 잘라 쓰는 "연산 모드"다. 메모리 절감 포맷이 아니라 정밀도-처리량 절충 연산 포맷.
한눈에 읽히는 사실:
- BF16 = FP32의 가수를 23→7로 자른 것. 지수(E8)가 같으므로 범위가 FP32와 동일. FP32↔BF16 변환은 하위 16비트 버림뿐이라 비용이 낮다.
- FP16 = 지수를 E5로 줄이고 가수를 M10으로 늘림. 16비트에서 정밀도는 BF16보다 높지만 범위가 좁다(최대 ~65504). 학습 그래디언트가 범위를 넘으면 inf → 손실 스케일링 필요(1.14).
- E5M2 = "FP16의 가수 비트만 더 줄인 8비트" → 범위 넓음, 정밀도 매우 거침. E4M3 = 범위를 좁히고 가수 1비트 더 → 정밀도 우선 8비트.
- FP8·FP4는 비트가 너무 적어 스케일로 값을 끌어와야 의미가 있다(1.14).
1.13.3.1 동적 범위 vs 정밀도
| 포맷 | 최대 유한값(≈) | 최소 정규값(≈) | 상대 정밀도(≈2^-(M+1)) | 십진 유효자릿수 |
|---|---|---|---|---|
| FP32 | 3.4e38 | 1.2e-38 | 6e-8 | ~7 |
| TF32 | 3.4e38 | 1.2e-38 | 5e-4 | ~3 |
| FP16 | 65504 | 6.1e-5 | 5e-4 | ~3 |
| BF16 | 3.4e38 | 1.2e-38 | 4e-3 | ~2.5 |
| E4M3 | 448 | 0.016 | 0.06 | ~1 |
| E5M2 | 57344 | 6.1e-5 | 0.125 | <1 |
| E2M1 | 6 | 1.0(정규) | 0.25 | <1 |
주의: E4M3·E5M2의 특수값 처리는 IEEE와 다르다. OCP FP8에서 E4M3는 inf가 없고(범위를 늘리려 inf 인코딩을 유한값으로 전용, 그래서 최대 448), NaN만 둔다. E5M2는 IEEE식 inf·NaN. 개략값이며 표준 문서로 확인.
위 그림의 각 막대 성격 — FP32: 범위 넓음, 촘촘. BF16: 범위=FP32, 거침. FP16: 범위 좁음, 비교적 촘촘. E5M2: 범위 넓음(8b치곤), 매우 거침. E4M3: 범위 좁음, 8b 중 정밀. E2M1: 스케일 없으면 거의 무의미.
이 그림이 포맷 선택의 기준이다: BF16은 FP16보다 범위가 압도적으로 넓어 학습 오버플로 걱정이 적다. 대신 가수 7비트라 FP16보다 거칠다. 학습 그래디언트는 "값이 가끔 크게 벗어난다"가 "마지막 한 자리가 정확하다"보다 중요하므로 BF16이 학습 기본으로 자리잡았다.
1.13.3.2 정수 포맷 — INT8
INT8은 부동소수가 아니라 고정소수 정수다. 부호 있으면 -128..127. 실수를 표현하려면 스케일(과 제로포인트) 을 곱해 해석한다(1.14).
- INT8 (대칭, scale $s$): 실수 $r \approx q \times s$, $q \in [-127, 127]$
- INT8 (비대칭, $s$, $\mathrm{zp}$): 실수 $r \approx (q - \mathrm{zp}) \times s$, $q \in [0, 255]$
INT8의 "범위·정밀도"는 포맷이 아니라 고른 스케일이 결정한다 — 균등 간격 256단계를 어느 구간에 배치할지의 문제. FP8과 대비: FP8은 지수가 있어 0 근처를 더 촘촘하게 배치하지만 INT8은 전 구간 균등이다.
1.13.4 한 값을 여러 포맷으로 — 표현 메커니즘
값 x = 0.1을 BF16으로 인코딩해 본다.
- $0.1 = 0.0001100110011001100\ldots_2$ (이진 무한소수 → 반올림 필연)
- 정규화: $1.100110011001100\ldots_2 \times 2^{-4}$
- 부호 $S = 0$, 지수 $E = (-4)+127 = 123 = 01111011_2$
- 가수 $M = 1001101$ (7비트, 소수부 반올림 — 버린 부분이 절반 초과라 올림)
- BF16 비트: 0 01111011 1001101
가수를 7비트로 자르며 뒷부분을 버린다 → 복원하면 정확히 0.1이 아니라 근처 격자점. FP16(가수 10비트)이면 3비트 더 살아 더 가깝다. FP8 E4M3(가수 3비트)이면 오차가 크다.
0.1을 각 포맷으로 (개략 — 실제 비트 격자점):
| 포맷 | 0.1 표현값(≈) | 오차 |
|---|---|---|
| FP32 | 0.100000001490 | ~1e-9 |
| FP16 | 0.0999755859 | ~2e-5 |
| BF16 | 0.10009765625 | ~1e-4 (가수 7비트라 FP16보다 거침) |
| E4M3 | 0.1015625 | ~2e-3 |
같은 16비트인데 FP16이 BF16보다 0.1을 정확히 표현한다. 0.1은 범위가 좁은 값이라 FP16의 좁은 범위가 문제 안 되고 가수가 많은 쪽이 이긴다. 반대로 x = 70000은 FP16에서 inf(65504 초과)지만 BF16은 멀쩡하다. 어느 포맷이 "더 정확"하냐는 값의 크기에 의존한다 — 범위 안이면 가수 많은 쪽, 범위를 넘나들면 지수 많은 쪽.
ULP 관점: 어떤 값 근처에서 인접한 두 표현값 간격 = 2^지수 × 2^-M. 가수 M이 작으면 간격이 크다 = 양자화 오차가 크다. FP8의 오차가 큰 이유는 M이 2~3비트라 간격이 거칠기 때문이고, 그래서 스케일로 값을 촘촘한 구간으로 끌어와야 한다(1.14).
1.13.5 워크스루 — BF16 인코딩·오차
검증 가능한 작은 실험. 외부 의존 없는 순수 Python으로 BF16 인코딩을 풀어본다.
# bf16_encode.py
import struct
def to_bf16(x):
b = struct.unpack('<I', struct.pack('<f', x))[0] # FP32 비트
rounding = 0x7FFF + ((b >> 16) & 1) # ties-to-even
b = (b + rounding) & 0xFFFF0000 # 상위 16비트만
return struct.unpack('<f', struct.pack('<I', b))[0]
for x in [0.1, 1/3, 70000.0, 1e-4, 3.0e38]:
y = to_bf16(x); err = abs(x - y) / (abs(x) if x else 1)
print(f"{x:>12.6g} -> BF16 {y:>14.6g} 상대오차 {err:.2e}")
예상(개략):
0.1 -> BF16 0.100098 상대오차 9.77e-04
0.333333 -> BF16 0.333984 상대오차 1.95e-03
70000 -> BF16 70144 상대오차 2.06e-03 ← FP16이면 inf
0.0001 -> BF16 0.000100136 상대오차 1.36e-03 ← FP16이면 더 정밀
3e+38 -> BF16 3.00406e+38 상대오차 1.35e-03 ← FP16은 inf
읽을 점: BF16 상대오차가 어떤 값이든 대략 ~1e-3 수준(상한 $2^{-8} \approx4\mathrm{e}-3$)으로 일정하다 — 부동소수의 상대 정밀도는 크기와 무관(0.1이든 3e38이든 같은 자릿수). 반면 70000·3e38은 BF16이 멀쩡히 표현 — FP16이라면 둘 다 inf. "범위는 BF16, 정밀도는 FP16"의 실증이다. (PyTorch가 있으면 x.to(torch.float16/bfloat16/float8_e4m3fn)으로 같은 비교를 할 수 있다.)
1.13.6 중간 점검 (체크포인트)
- [ ] 임의 포맷의
S/E/M을 보고 범위(지수)와 상대 정밀도(가수) 를 즉석에서 추정한다. - [ ] "BF16과 FP16 중 70000을 표현하는 것은?"에 BF16이라 답하고 이유(E8 vs E5)를 제시한다.
- [ ] TF32가 "저장 포맷이 아니라 연산 모드"임을 안다(메모리 절감 없음).
- [ ] 같은 16비트라도 값 크기에 따라 FP16/BF16의 우열이 갈림을 설명한다.
1.13.7 흔한 함정
- "FP16과 BF16은 둘 다 16비트니 같다" — 아니다. 범위(E5 vs E8)가 본질적으로 다르다. FP16은 65504 초과에서 inf, BF16은 안전. 학습 안정성이 갈린다.
- TF32를 "저장 포맷"으로 착각. TF32는 32비트로 저장되는 연산 모드다. 메모리 절감 효과 없음. 메모리를 줄이려면 BF16/FP16.
- FP8/INT8을 스케일 없이 캐스팅. 비트가 너무 적어 분포를 표현 범위로 끌어오지 않으면 거의 무의미(1.14).
- 누산 정밀도 가정. 텐서코어가 FP8/FP16 입력을 받아도 누산은 FP32. 출력 정밀도를 입력 정밀도로 오해하면 오차 분석이 틀어진다.
1.13.8 연습문제
연습 1 (비트 유도). E4M3의 최대 유한값이 왜 448인가? OCP E4M3가 inf 인코딩을 유한값으로 전용한다는 점을 반영해 유도하라.
정답
E4(바이어스 7). 보통 지수 모두-1을 inf/NaN에 쓰지만 OCP E4M3은 inf를 없애 그 패턴을 유한값에 쓴다. 표준은 특정 인코딩을 최대값으로 정의해 $1.75 \times2^{8} = 448$(NaN은 별도 패턴). 핵심: "inf를 포기해 범위를 늘렸다"(256→448). 정확한 인코딩은 OCP FP8 문서로 확인.
연습 2 (포맷 선택). 한 텐서가 [0.002, 0.005, 71000]을 담는다. (a) FP16·(b) BF16·(c) per-tensor INT8 각각 어떤 문제가 생기나?
정답
(a) FP16: 71000 > 65504 → inf. (b) BF16: 셋 다 표현되나 작은 값은 가수 7비트라 거침. (c) INT8 per-tensor: $s=71000/127\approx559$ → 0.002·0.005는 q=0으로 소실됨. 처방: per-channel/그룹 스케일, 크기대별 분리, outlier 별도 처리(1.14).
연습 3 (ULP·오차). FP16에서 값 4.0 근처의 ULP를 구하라. 같은 4.0을 BF16으로 표현하면 ULP는?
정답
4.0 = 2^2. FP16(M10): ULP = 2^2 × 2^-10 = 2^-8 ≈ 3.9e-3. BF16(M7): ULP = 2^2 × 2^-7 = 2^-5 ≈ 3.1e-2. BF16이 약 8배 거칠다(같은 값에서 가수가 3비트 적으므로 2^3배).
1.13.9 요약 · 다음 장
- 부동소수 포맷은 지수(범위)와 가수(정밀도) 의 비트 배분이 전부다. BF16=범위 우선(E8), FP16=정밀도 우선(E5) — 같은 16비트의 다른 선택.
- FP8(E4M3 정밀·E5M2 범위)·FP4·INT8은 비트가 적어 스케일로 값을 표현 범위에 끌어와야 쓸모가 있다.
- 어느 포맷이 더 정확하냐는 값의 크기에 의존한다. TF32는 저장이 아니라 연산 모드. 누산은 항상 FP32.
제1부의 마지막 장: 1.14 수치 정밀도 II — 혼합정밀도·양자화 기초·구조적 희소성. 이 포맷들을 실제로 쓰는 법 — FP32 마스터 가중치·손실 스케일링·양자화 affine 매핑·2:4 희소성을 본다.