연재 중집필 중인 책입니다. 아직 본문이 비어 있거나 채워지는 중인 장이 있습니다.

제 2 장

프리필과 디코드

생성 추론은 프롬프트를 한 번에 읽는 단계와 토큰을 하나씩 만드는 단계로 나뉜다. 두 단계는 같은 가중치로 같은 종류의 연산을 하지만, 메모리에서 읽은 바이트 하나로 연산을 몇 번 하느냐가 수백 배 차이 나서 프리필은 연산 성능에, 디코드는 메모리 대역폭에 먼저 걸린다. 서빙에서 배치·스케줄링·메모리 관리를 고르는 판단은 대부분 이 차이에서 출발한다.

토큰 생성 과정

요청이 들어오면 엔진은 프롬프트의 토큰 전부를 한꺼번에 모델에 넣는다. 층마다 각 토큰의 키(K)와 값(V) 벡터를 계산해 메모리에 저장하고 마지막 층의 출력으로 첫 토큰을 고른다. 이 단계를 프리필(prefill)이라고 부른다.

첫 토큰이 나온 뒤로는 방금 만든 토큰 하나만 모델에 넣는다. 앞선 토큰들의 키와 값은 이미 저장돼 있으므로 다시 계산하지 않고 읽어 오기만 하고, 새 토큰의 키와 값을 덧붙인 다음 그다음 토큰을 고른다. 종료 토큰이 나오거나 길이 상한에 이를 때까지 이 과정을 되풀이하는데 이 단계를 디코드(decode)라고 부른다. 키와 값을 모아 둔 저장 공간이 KV 캐시다.

두 단계 모두 모델의 가중치 전부를 한 번씩 거친다. 프리필은 토큰 수천 개를 한 번의 행렬 곱으로 처리하고 디코드는 요청 하나당 토큰 하나를 처리한다. 가중치를 읽는 양은 같은데 그 가중치로 하는 연산의 양이 토큰 수만큼 벌어진다.

연산 천장과 대역폭 천장

GPU의 성능에는 상한이 두 개 있다. 초당 수행할 수 있는 연산 수와 메모리에서 초당 읽어 올 수 있는 바이트 수다. 어떤 계산이 어느 상한에 먼저 걸리는지는 메모리에서 읽은 바이트 하나당 연산 횟수가 정한다. 루프라인 모델은 이 값을 연산 강도(operational intensity)라고 부르고, 연산 강도가 낮은 구간에서는 성능이 대역폭에 비례해 오르다가 일정 지점부터 연산 성능에서 평평해지는 그림으로 상한을 나타낸다.

H100 SXM의 메모리 대역폭은 초당 3.35TB다. BF16 텐서 코어 성능은 사양표에 1,979 TFLOPS로 적혀 있는데, 이 값은 2:4 구조적 희소성을 가정한 수치라 일반 행렬 곱의 상한은 그 절반인 약 989 TFLOPS로 잡는다. 두 상한이 만나는 능선은 다음과 같다.

989 × 10¹² FLOP/s ÷ 3.35 × 10¹² 바이트/s ≈ 295 FLOP/바이트

바이트 하나를 읽어 연산을 295번보다 적게 하는 계산은 대역폭 천장에 먼저 걸린다. 연산기가 아무리 빨라도 계산할 값이 메모리에서 그보다 빨리 오지 않기 때문이다. 295번보다 많이 하는 계산은 연산 천장에 걸린다.

디코드 한 스텝의 하한

Llama 3.1 8B Instruct의 설정은 층 32개, 은닉 차원 4,096, MLP 중간 차원 14,336, 어휘 128,256개다. 행렬 크기를 곱해 더하면 파라미터는 약 80억 3천만 개이고 BF16은 파라미터 하나에 2바이트를 쓰므로 가중치는 약 16GB다.

디코드에서 요청 하나가 토큰 하나를 만들려면 이 16GB를 메모리에서 한 번 다 읽는다. 그동안 하는 연산은 파라미터 하나당 곱셈과 덧셈 한 번씩, 2 FLOP이다. 2바이트를 읽어 2번 계산하므로 연산 강도는 1 안팎으로, 능선인 295에 한참 못 미친다. 그래서 디코드 한 스텝의 하한은 연산이 아니라 가중치를 읽는 시간이 정한다.

16.06 × 10⁹ 바이트 ÷ 3.35 × 10¹² 바이트/s ≈ 4.8ms

요청 하나만 생성할 때 이 조합이 낼 수 있는 속도는 이론상 초당 약 200토큰이다. KV 캐시를 읽는 시간과 커널 실행 비용을 뺀 값이라 실제로는 이보다 느리다. 같은 4.8ms 동안 수행한 연산은 160억 FLOP으로, 초당으로 환산하면 약 3.3 TFLOPS다. 연산 성능 상한의 0.3%에 그친다.

엔진이 동시에 생성 중인 요청 B개를 한 스텝에 묶으면 가중치는 한 번만 읽고 요청마다 토큰을 하나씩 만든다. 읽는 바이트는 거의 그대로이고 연산은 B배가 되므로 연산 강도는 B 근처로 오른다. 요청 40개를 묶어도 연산 강도는 능선의 7분의 1 수준이라 디코드는 여전히 대역폭에 묶여 있다.

프리필 한 번의 하한

프리필은 반대다. 프롬프트가 2,000토큰이면 가중치를 한 번 읽어 토큰 2,000개에 함께 곱한다. 읽는 양은 디코드 한 스텝과 같은 16GB인데 연산은 2,000배이므로 연산 강도가 2,000 근처까지 올라 능선을 넘는다. 이때는 연산 천장이 하한을 정한다.

2 FLOP × 80.3억 파라미터 × 2,000토큰 ≈ 3.2 × 10¹³ FLOP
3.2 × 10¹³ FLOP ÷ 989 × 10¹² FLOP/s ≈ 32ms

어텐션 연산을 빼고 연산기를 100% 쓴다고 가정한 값이라 실제 프리필은 이보다 오래 걸린다. 크기만 보면 2,000토큰 프롬프트 하나를 처리하는 시간은 디코드 스텝 예닐곱 번에 해당한다. 엔진이 이 프리필을 디코드와 같은 스텝에 넣으면 그 스텝에 묶인 다른 요청들은 프리필이 끝날 때까지 다음 토큰을 받지 못하고, 사용자에게는 토큰 간격이 튀는 것으로 보인다.

GPU 사용률의 정의

nvidia-smi가 보여 주는 GPU 사용률은 NVML이 돌려주는 값이다. NVML 문서는 이 값을 지난 샘플 구간 동안 GPU에서 커널이 하나 이상 실행되고 있던 시간의 비율로 정의한다. 커널이 SM을 몇 개나 쓰는지, 연산기가 얼마나 바쁜지는 이 비율에 들어가지 않는다.

추론 엔진은 디코드 스텝마다 층별로 커널을 연달아 실행하므로 요청이 끊이지 않으면 사용률은 쉽게 100%에 이른다. 위 계산처럼 연산기를 0.3%만 쓰는 배치 1 디코드도 100%로 표시된다. 이 숫자를 장비가 포화됐다는 근거로 읽고 GPU를 늘리면 같은 장비에서 배치를 키워 얻을 수 있던 처리량을 놓친다.

장비가 실제로 어느 상한에 가까운지는 DCGM의 프로파일링 지표로 본다.

지표정의 (DCGM 문서)읽는 법
DCGM_FI_PROF_SM_ACTIVESM에서 워프가 하나 이상 활성이던 시간 비율의 전체 SM 평균커널이 SM을 얼마나 넓게 쓰는지
DCGM_FI_PROF_PIPE_TENSOR_ACTIVE텐서 파이프가 활성이던 사이클 비율행렬 연산기가 연산 천장에 얼마나 가까운지
DCGM_FI_PROF_DRAM_ACTIVE디바이스 메모리와 데이터를 주고받은 사이클 비율대역폭 천장에 얼마나 가까운지

디코드 위주의 부하에서는 DRAM 활성도가 높고 텐서 파이프 활성도가 낮게 나오며, 배치를 키울수록 텐서 파이프 활성도가 따라 오른다.

단계 분리 설계

프리필은 연산에, 디코드는 대역폭에 걸린다는 관찰은 서빙 시스템 설계의 출발점이 됐다. Splitwise는 프롬프트 계산을 연산 집약적인 단계로, 토큰 생성을 메모리 집약적인 단계로 나누고 두 단계를 별도의 장비에서 실행하는 구성을 제안한다. 같은 장비에서 두 단계를 함께 돌리는 엔진도 프리필을 잘게 나눠 디코드 사이에 끼워 넣거나 두 단계의 우선순위를 조정하는 방식으로 이 차이를 다룬다.

측정의 기준2 / 18