AEGLOSLAB 엔비디아 기술 스택

제4부 데이터센터와 스케일

4.2 Hopper I — 트랜스포머 엔진·FP8

4.2.1 이 장에서 배우는 것 / 선수 지식

4.1에서 세대를 조망했다. 이제 Hopper 세대를 깊게 판다 — 그 핵심 신기능은 트랜스포머 학습·추론을 위한 FP8 + 트랜스포머 엔진(Transformer Engine, TE) 이다. FP8은 텐서코어 처리량을 FP16의 약 2배로(시점·연산 의존) 끌어올리고 메모리·대역폭을 절반으로 줄이지만, 표현 범위가 너무 좁아 순진하게 캐스팅하면 오버플로·언더플로로 학습이 망가진다. TE는 이것을 텐서별 동적 스케일링으로 자동 관리하는 하드웨어+라이브러리다. 이 장은 FP8의 메커니즘과 TE를 켜는 법을 익힌다.

배우는 것:

  • FP8 두 포맷(E4M3·E5M2)의 구조(1.13 연장)와 각각이 forward·gradient에 쓰이는 이유.
  • FP8의 좁은 동적 범위 문제와, 트랜스포머 엔진의 per-tensor 동적 스케일링(amax 추적·delayed scaling) 이 그것을 푸는 원리.
  • 무엇을 FP8로 내리고 무엇을 고정밀로 남기는가(혼합), master weight·FP32 누적과의 관계.
  • transformer_engine으로 FP8 학습을 켜고(fp8_autocast·recipe), 정확도·발산을 진단한다.

선수 지식: 1.13~1.14(부동소수 포맷·혼합정밀도·스케일링 — 이 장은 그 FP8 심화), 1.11~1.12(텐서코어·세대별 가속), 1.9~1.10(대역폭·메모리 바운드 — FP8의 메모리 이득), 3.1(GEMM). 환경은 0.3. FP8 처리량 배수·지원 연산·정확도는 세대·모델·시점 의존 → 확인 필요. 코드는 개략이며 정확한 API는 설치된 Transformer Engine 버전으로.

4.2.2 왜 FP8인가 — 그리고 왜 어려운가

정밀도를 낮추면(1.13) 텐서코어가 더 빠르고(같은 실리콘에 더 많은 MAC), 메모리·대역폭이 준다. FP16/BF16에서 한 단계 더 내려간 FP8(8비트)은:

구분내용
이득텐서코어 처리량 ~2배(FP16 대비, 세대·연산 의존). 메모리·HBM 트래픽 절반 → 메모리 바운드 구간 가속(1.10), 더 큰 배치·모델
대가8비트 = 256개 값밖에 표현 못 함 → 동적 범위·정밀도가 매우 좁다

문제의 핵심 — 좁은 동적 범위. FP16조차 작은 그래디언트가 언더플로해 loss scaling이 필요했다(1.14). FP8은 훨씬 심하다. 텐서의 값들이 FP8이 표현 가능한 좁은 범위를 벗어나면:

값이 너무 큼 FP8 최대 초과 ∞/포화(오버플로) 값이 너무 작음 FP8 최소 미만 0(언더플로) 정보 손실

트랜스포머의 텐서들은 레이어·스텝마다 크기(amax)가 제각각이고 시시각각 변한다.

그래서 스케일링 없이 FP8로 캐스팅하면 안 된다. 텐서마다·시점마다 적절한 스케일(곱하는 상수) 을 곱해 값들을 FP8 sweet spot으로 옮긴 뒤 캐스팅하고, 연산 후 되돌려야 한다. 이 스케일을 자동·동적으로 관리하는 것이 트랜스포머 엔진이다.

4.2.3 FP8 두 포맷 — E4M3와 E5M2

8비트를 지수(exponent)와 가수(mantissa)로 어떻게 나누느냐로 두 포맷이 있다(1.13의 포맷 분해 연장):

E4M3 S E E E E M M M 부호1 지수4 가수3 → 범위 좁고 정밀도 높음 (값 표현 정밀) E5M2 S E E E E E M M 부호1 지수5 가수2 → 범위 넓고 정밀도 낮음 (큰/작은 값 도달)
포맷지수/가수특성주 용도
E4M34/3동적 범위 좁음, 상대정밀 높음forward(weight·activation)
E5M25/2동적 범위 넓음, 정밀 낮음gradient(backward)

왜 이렇게 나누나: forward의 weight·activation은 값 분포가 비교적 모여 있어 정밀도(가수)가 중요 → E4M3. gradient는 크기 편차가 커(아주 크거나 작은 값 공존) 넓은 범위가 중요 → E5M2. 즉 FP16의 단일 포맷과 달리 FP8은 방향(forward/backward)별로 다른 포맷을 쓴다. (정확한 비트 배치·특수값 처리는 표준·구현 의존, 확인 필요.)

연결: 1.13에서 "지수↔범위, 가수↔정밀도"의 트레이드오프를 봤다. FP8은 그 트레이드오프가 지나치게 엄격해 한 포맷으로 수용하지 못하고 둘로 분할한 사례다.

4.2.4 트랜스포머 엔진 — per-tensor 동적 스케일링

TE의 일은 각 FP8 텐서에 곱할 스케일을 시시각각 정해, 값들이 FP8 범위에 정확히 맞게 하는 것이다.

amax 기반 스케일. 텐서의 절대값 최대(amax)를 알면, "amax가 FP8 최대값 근처에 오도록" 하는 스케일을 정할 수 있다:

  • $\mathrm{scale} \approx \mathrm{FP8\_MAX} / \mathrm{amax}(\mathrm{tensor})$ (값들을 FP8 상한에 맞춰 키움)
  • FP8 텐서 $= \mathrm{round\_to\_fp8}(\mathrm{tensor} \times \mathrm{scale})$
  • 연산(GEMM)은 FP8로, 결과는 FP32 누적 후 $\times (1/\mathrm{scale})$ 로 원복(또는 다음 단계 스케일과 합성)

amax보다 작은 스케일을 쓰면 큰 값이 포화, 더 크게 쓰면 작은 값이 언더플로. amax에 맞추면 범위를 최대한 활용한다.

delayed scaling — amax 히스토리. 문제: 이번 텐서의 amax는 텐서를 만든 뒤에야 안다(닭-달걀). 매번 텐서를 두 번 순회하면(amax 측정 → 스케일 적용) 느리다. TE는 과거 스텝들의 amax 히스토리를 들고 다니며 그걸로 이번 스케일을 예측(예: 최근 N 스텝 amax의 최대)한다 — 한 번에 캐스팅. 분포가 급변하지 않는다는 가정이고, 스파이크에 한 박자 늦게 반응한다.

측정과 적용을 분리 delayed 텐서 생성 이번 amax 측정해 히스토리에 기록 다음 스텝 스케일 = f (히스토리)

장점은 한 번에 캐스팅(빠름)이고, 주의점은 amax 급변 시 한 스텝 지연 → 일시 포화/언더플로 가능이다.

무엇을 FP8로, 무엇을 고정밀로. 전부 FP8로 하지 않는다:

정밀도대상이유
FP8GEMM(Linear·attention의 행렬곱)텐서코어 이득이 큰 곳
고정밀(FP16/BF16/FP32)LayerNorm·softmax·residual·optimizer·master weight수치적으로 민감하거나 reduction이라 FP8이 위험

master weight는 FP32(또는 BF16)로 유지한다 — FP8은 '곱셈 입력'으로만 쓰고, 누적은 항상 고정밀이다(1.14).

핵심: TE는 "GEMM은 FP8, 주변은 고정밀, 스케일은 TE가 동적 관리" 라는 혼합을 자동화한다 — 사용자가 amax·스케일을 수동으로 조정하지 않게.

4.2.5 워크스루 A — 스케일 수계산

왜 스케일이 필요한지 숫자로. 가상의 E4M3 FP8 표현 가능 최대값을 FP8_MAX ≈ 448(구현 의존, 예시)이라 하자. 어떤 activation 텐서의 값 범위가 [-0.02, 0.03](작음).

스케일 없이 캐스팅: 값 0.03을 FP8로? FP8의 가장 작은 정상값 근처라 정밀도가 거의 없다(몇 개 안 되는 값에 양자화) — 사실상 0~몇 단계로 압축됨 → 언더플로·정보 손실.

amax 스케일: amax = 0.03. scale = 448/0.03 ≈ 14933. 텐서 × scale → 범위가 [-298, 448]로 FP8 범위를 꽉 채움 → 양자화 단계가 값 사이에 촘촘 → 정밀 보존. GEMM 후 × (1/scale)로 원복.

×14933 ×1/14933 원래 [-0.02, 0.03] [-298, 448] (FP8에 잘 맞음) 캐스팅 후 GEMM (FP8×FP8→FP32 누적) 결과 원복 (또는 다음 스케일과 합성)

스케일이 없었다면 0.03이 FP8 표현 바닥에서 0 근처로 압축되어 학습 신호를 잃는다.

반대로 값이 큰 텐서(amax=1000 > 448)면 scale = 448/1000 = 0.448 < 1로 줄여서 포화를 막는다. 스케일은 텐서 크기에 따라 키우거나 줄여 항상 FP8 범위에 맞추는 동적 상수다. TE가 이것을 amax 히스토리로 자동 결정한다.

4.2.6 워크스루 B — Transformer Engine으로 FP8 학습

transformer_engine(PyTorch 통합)으로 FP8을 켠다. TE의 Linear·LayerNormLinear 등으로 모델을 구성하고 fp8_autocast로 감싼다(개략, API는 버전 의존):

import torch
import transformer_engine.pytorch as te
from transformer_engine.common.recipe import DelayedScaling, Format

# 1) 모델을 TE 레이어로 (일반 nn.Linear 대신 te.Linear)
model = te.TransformerLayer(
    hidden_size=4096, ffn_hidden_size=16384, num_attention_heads=32,
).cuda()

# 2) FP8 recipe: 포맷·amax 히스토리 길이·스케일 정책
fp8_recipe = DelayedScaling(
    fp8_format=Format.HYBRID,    # forward E4M3, backward E5M2 (HYBRID)
    amax_history_len=16,         # 최근 16스텝 amax로 스케일 결정
    amax_compute_algo="max",     # 히스토리에서 max를 스케일 기준으로
)

optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)  # master weight는 고정밀 유지

for step, (x, target) in enumerate(loader):
    # 3) 이 컨텍스트 안의 te.Linear GEMM이 FP8로 실행, 스케일은 TE가 관리
    with te.fp8_autocast(enabled=True, fp8_recipe=fp8_recipe):
        out = model(x.cuda())          # GEMM은 FP8, norm·softmax는 고정밀
        loss = loss_fn(out, target.cuda())
    loss.backward()                     # gradient는 E5M2 경로
    optimizer.step(); optimizer.zero_grad()
    # TE가 내부적으로 amax를 갱신해 다음 스텝 스케일을 정함

읽는 법:

  • Format.HYBRID — forward E4M3·backward E5M2(4.2.3). 순수 E4M3 등 다른 recipe도 있음.
  • amax_history_len — delayed scaling의 히스토리 깊이. 짧으면 민감·노이즈, 길면 안정하나 급변에 둔감.
  • fp8_autocast 컨텍스트 안의 te 레이어 GEMM만 FP8. 바깥·민감 연산은 고정밀.
  • master weight·optimizer 상태는 FP8이 아니다 — 고정밀로 유지하고 FP8은 곱셈 입력으로만(1.14 혼합정밀 원칙의 연장).

검증. loss 곡선을 BF16 기준과 비교. FP8이 초반 발산하거나 정확도가 크게 떨어지면 recipe·민감 레이어를 조정(4.2.9). "FP8 켜서 빨라졌지만 정확도 동등"을 측정으로 확인해야 한다 — 자동으로 보장되지 않는다.

4.2.7 중간 점검 (체크포인트)

  • [ ] FP8 이득(처리량~2배·메모리 절반)과 핵심 난점(좁은 동적 범위→스케일 필수)을 함께 말한다.
  • [ ] E4M3·E5M2의 차이와 forward·backward 배정 이유를 설명한다.
  • [ ] amax 기반 per-tensor 스케일과 delayed scaling(히스토리)이 무엇을 해결·교환하는지 4.2.5 수계산으로 보인다.
  • [ ] 무엇을 FP8로 내리고(GEMM) 무엇을 고정밀로 남기는지(norm·softmax·master weight) 구분한다.

증상→의심: FP8 학습 초반 loss 발산·NaN → 스케일 부적합·amax 스파이크(히스토리·민감 레이어 점검), master weight를 FP8로 둠. FP8인데 안 빨라짐 → GEMM이 작아 텐서코어 이득 미미하거나 FP8 미지원 연산이 폴백. 정확도만 떨어지고 속도는 OK → 민감 레이어까지 FP8(일부를 고정밀로 되돌림). 처리량 배수가 기대 이하 → 메모리 바운드라 정밀도보다 대역폭이 병목(이미 이득 본 것일 수도).

4.2.8 트레이드오프 — FP8을 켤 것인가

상황FP8 권고비고
대규모 트랜스포머 사전학습(Hopper+)강함 — 비용·시간 절감 큼TE로 검증하며. 민감 레이어 주의
추론(특히 LLM)강함 — 처리량·메모리양자화 검증(5.4~5.9)
소규모·민감 모델신중 — 이득 작고 정확도 위험BF16로 충분할 때 많음
Hopper 이전 세대불가/제한FP8 텐서코어 경로 없음

견해: Hopper+에서 대규모 LLM은 FP8을 적극 시도할 가치가 크다(같은 예산으로 더 큰·빠른 학습). 단 "무비용 2배"가 아니다 — TE recipe·민감 레이어·정확도 검증에 공을 들여야 하고, 모델·데이터에 따라 일부는 BF16로 남겨야 안정적이다. BF16로 충분하고 규모가 작으면 FP8의 검증 비용이 이득을 안 넘을 수 있다.

비NVIDIA 정직: FP8은 NVIDIA 단독이 아니라 업계 표준화(OCP FP8, E4M3/E5M2) 흐름이며 AMD·Intel·다른 가속기도 채택한다. NVIDIA의 차별점은 트랜스포머 엔진의 자동 스케일링 통합(amax 관리를 하드웨어·라이브러리가 대신)과 텐서코어 구현 성숙도다. FP8 포맷은 이식 가능, 자동화 편의는 스택 의존.

4.2.9 흔한 함정

  • 스케일링 없이 FP8로 캐스팅. 스케일 없이 캐스팅하면 오버플로·언더플로로 학습이 망가진다. TE(또는 동등한 스케일 관리) 없이 FP8을 수동으로 켜지 말 것.
  • master weight·optimizer를 FP8로. weight 갱신·모멘텀은 작은 값 누적이라 FP8이면 무너진다(1.14). FP8은 GEMM 입력으로만, 상태는 고정밀.
  • 민감 연산까지 FP8. LayerNorm·softmax·residual·loss는 수치 민감·reduction이라 FP8이 위험. TE 기본이 이들을 고정밀로 두는 이유 — 임의로 FP8로 내리지 말 것.
  • amax 스파이크에 delayed scaling 둔감. 분포가 급변하는 스텝(워밍업·LR 변경·드문 입력)에 히스토리 기반 스케일이 한 박자 늦어 포화·언더플로. 히스토리 길이·알고리즘을 조정하거나 그 구간을 주시.
  • 정확도 검증 생략. "FP8 켜서 빨라졌다"만 보고 정확도를 BF16 기준과 비교 안 하면, 조용한 품질 저하를 놓친다. loss·평가지표를 항상 대조.
  • FP8 미지원 연산 폴백을 이득으로 착각. 일부 연산은 FP8 경로가 없어 고정밀로 폴백 — 속도가 기대만 못 하면 어디가 FP8로 실제 실행되는지 프로파일(2.14).
  • 세대 혼동. Hopper 이전엔 FP8 텐서코어가 없다. 코드가 동작해도 FP8 가속이 아닐 수 있다 — 타깃 GPU 세대 확인(4.1).

4.2.10 연습문제

연습 1 (포맷 배정). E4M3와 E5M2 중 (a) forward activation, (b) backward gradient에 각각 어느 것을 쓰고 왜인지, 두 텐서의 값 분포 특성으로 설명하라.

정답

(a) forward activation·weight → E4M3(지수4·가수3). 이 텐서들은 값이 비교적 모여 있어 상대 정밀도(가수) 가 중요하고 극단적 동적 범위는 덜 필요 → 가수를 더 준 E4M3. (b) backward gradient → E5M2(지수5·가수2). gradient는 아주 큰 값과 아주 작은 값이 공존해 넓은 동적 범위(지수) 가 중요 → 지수를 더 준 E5M2. 한 포맷으로 둘 다 수용하지 못해 방향별로 나눈다.

연습 2 (스케일 계산). FP8 최대값을 448로 가정. activation 텐서 amax가 (a) 0.05, (b) 1200일 때 각각의 스케일과, 스케일 없이 캐스팅하면 무슨 일이 나는지.

정답

(a) scale = 448/0.05 = 8960(>1, 키움). 스케일 없으면 0.05가 FP8 표현 바닥에서 양자화돼 거의 0~몇 단계로 압축됨 → 언더플로·정보 손실. (b) scale = 448/1200 ≈ 0.373(<1, 줄임). 스케일 없으면 1200이 FP8 최대 448을 넘어 오버플로·포화(전부 448/∞). 둘 다 스케일이 값을 FP8 범위로 옮겨 해결.

연습 3 (delayed scaling). delayed scaling이 "amax 측정과 스케일 적용을 분리"하는 이유(성능)와, 그 대가(언제 문제)를 설명하라. 히스토리 길이를 늘리면·줄이면 각각 어떻게 되나?

정답

이유: 이번 텐서의 amax는 텐서를 만든 뒤에야 알 수 있어, 정확히 하려면 텐서를 두 번 순회해야(측정→적용) 해 느리다. delayed scaling은 과거 스텝 amax 히스토리로 이번 스케일을 미리 정해 한 번에 캐스팅(빠름). 대가: 분포가 급변하는 스텝(워밍업·LR 변경·드문 입력)엔 한 박자 늦게 반응 → 일시 포화/언더플로. 히스토리 길게 = 안정적이나 급변에 둔감(지연↑), 짧게 = 민감해 빨리 따라가나 노이즈에 흔들림. 분포 안정성에 맞춰 조정.

연습 4 (무엇을 FP8로). Transformer 레이어에서 (a) QKV·FFN의 GEMM, (b) LayerNorm, (c) softmax, (d) master weight, (e) AdamW 모멘텀 각각을 FP8로 둘지 고정밀로 둘지와 이유.

정답

(a) GEMM → FP8: 텐서코어 이득이 가장 큰 곳, 곱셈 입력이라 FP8 적합(누적은 FP32). (b) LayerNorm → 고정밀: 평균·분산 reduction이라 작은 값 누적·수치 민감. (c) softmax → 고정밀: exp·정규화가 수치 민감(오버플로·정밀). (d) master weight → 고정밀(FP32/BF16): 갱신 시 작은 변화 누적, FP8이면 무너짐. (e) 모멘텀 → 고정밀: optimizer 상태는 작은 값 장기 누적이라 FP8 부적합. 요약: GEMM만 FP8, 나머지·상태는 고정밀(TE 기본).

연습 5 (검증 절차). "FP8로 학습을 켰다"를 production에 넣기 전 거쳐야 할 검증 단계를 3가지 이상 쓰라.

정답

① 정확도 대조: 동일 설정 BF16 기준선과 loss 곡선·다운스트림 평가지표를 비교, 동등 범위인지 확인. ② 안정성: 워밍업·LR 변경 구간에서 NaN·발산·loss 스파이크가 없는지(amax 히스토리·민감 레이어 점검). ③ 속도 실측: Nsight Compute(2.14)로 GEMM이 실제 FP8 텐서코어로 실행되는지, 폴백·미지원 연산이 없는지 확인 — 처리량 이득이 실재하는지. 추가: ④ 민감 레이어를 BF16로 남긴 변형과 비교, ⑤ 장기 학습 재현성·체크포인트 호환.

4.2.11 요약 · 다음 장

  • FP8은 텐서코어 처리량~2배·메모리 절반을 주지만 동적 범위가 너무 좁아 순진한 캐스팅은 오버플로·언더플로로 학습을 저해한다 — 스케일이 필수.
  • 포맷이 둘: E4M3(정밀, forward)·E5M2(범위, gradient). 방향별로 다른 포맷을 쓴다.
  • 트랜스포머 엔진은 per-tensor 동적 스케일링(amax 히스토리·delayed scaling)으로 스케일을 자동 관리하고, GEMM만 FP8·norm/softmax/master weight는 고정밀이라는 혼합을 자동화한다.
  • FP8은 "무비용 2배"가 아니라 검증이 필요한 최적화 — 정확도·안정성·실제 FP8 실행을 측정으로 확인. Hopper+ 전용.

다음 장: 4.3 Hopper II — TMA·스레드블록 클러스터·DSMEM. FP8이 정밀도 축의 진화라면, 이번엔 실행·메모리 모델의 진화다. 대량 비동기 복사(TMA), 블록들을 묶는 스레드블록 클러스터, 블록 간 공유메모리(DSMEM)가 3.4의 파이프라인·1.7의 온칩 메모리를 어떻게 한 단계 끌어올리는지 본다.