AEGLOSLAB 엔비디아 기술 스택

제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).

포맷총비트SEM바이어스표준/출처
FP32321823127IEEE 754
TF3219*1810127NVIDIA (Ampere+)
FP1616151015IEEE 754
BF1616187127Google/산업 표준
FP8 E4M381437OCP FP8
FP8 E5M2815215OCP FP8
FP4 E2M141211OCP MX (Blackwell)

* TF32는 저장은 FP32(32비트), 텐서코어 연산 시 가수를 10비트로 잘라 쓰는 "연산 모드"다. 메모리 절감 포맷이 아니라 정밀도-처리량 절충 연산 포맷.

부호(S) 지수(E) 가수(M) FP32 1 + 8 + 23 TF32 1 + 8 + 10 (저장은 FP32) FP16 1 + 5 + 10 BF16 1 + 8 + 7 E4M3 1 + 4 + 3 E5M2 1 + 5 + 2 E2M1 1 + 2 + 1

한눈에 읽히는 사실:

  • 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))십진 유효자릿수
FP323.4e381.2e-386e-8~7
TF323.4e381.2e-385e-4~3
FP16655046.1e-55e-4~3
BF163.4e381.2e-384e-3~2.5
E4M34480.0160.06~1
E5M2573446.1e-50.125<1
E2M161.0(정규)0.25<1

주의: E4M3·E5M2의 특수값 처리는 IEEE와 다르다. OCP FP8에서 E4M3는 inf가 없고(범위를 늘리려 inf 인코딩을 유한값으로 전용, 그래서 최대 448), NaN만 둔다. E5M2는 IEEE식 inf·NaN. 개략값이며 표준 문서로 확인.

10−38 10−19 100 1019 1038 FP32 BF16 FP16 E5M2 E4M3 E2M1

위 그림의 각 막대 성격 — 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 표현값(≈)오차
FP320.100000001490~1e-9
FP160.0999755859~2e-5
BF160.10009765625~1e-4 (가수 7비트라 FP16보다 거침)
E4M30.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 희소성을 본다.