인지야공

인지야공/딥러닝 기초 정리/22번째 글

추론 서빙 — 처리량과 지연, 그리고 빈자리

실행: python NN_40_serving.py (검증 환경: torch 2.8.0+cu129, RTX 5080) [A]와 [B]는 한 GPU 실측, [C]는 모의실험이다. 필요한 공학: 산술 강도 노트.


학습 이야기를 오래 했으니 이번엔 쓰는 쪽이다. 모델은 한 번 만들고 수억 번 돌린다. 그래서 추론 비용이 사실상 전체 비용이고, 그 비용을 정하는 것은 모델 구조가 아니라 어떻게 묶어서 돌리느냐다.


1. 두 가지 국면 — 그림으로 먼저

생성은 성격이 다른 두 단계로 나뉜다.

프리필과 디코드 프리필은 질문의 모든 토큰을 한 번에 통과시키므로 행렬이 두껍고 계산에 묶인다. 디코드는 한 번에 토큰 하나만 통과시키므로 행렬이 가늘고, 그 한 토큰을 위해 가중치 전체를 읽어야 해서 읽기에 묶인다. 여러 사람을 묶으면 같은 가중치 한 번을 읽어 여럿을 처리한다. ① 프리필 — 질문을 한 번에 읽는다 ② 디코드 — 한 글자씩 뱉는다 토큰이 많다 → 계산에 묶인다 (105 TFLOP/s) 토큰이 하나다 → 읽어 오기에 묶인다 (0.3 TFLOP/s) 토큰 2048 가중치 전체 결과 토큰 1 가중치 전체 같은 가중치 한 번 읽고 여러 사람을 처리

프리필은 질문 전체를 한 번에 통과시킨다. 행렬이 두꺼워 GPU가 제 실력을 낸다. 디코드는 토큰 하나를 위해 가중치 전체를 읽어야 한다 — 계산할 것은 거의 없는데 읽어 올 것은 그대로다. 이것이 산술 강도가 낮다는 말의 뜻이고, 추론 최적화의 거의 모든 것이 여기서 나온다.


2. 직접 재 보기 B — 같은 GPU, 350배 차이

먼저 두 국면을 나란히 쟀다(12층, 은닉 2048, fp16).

추론 서빙

한 스텝토큰당실제 성능산술 강도
프리필 (질문 512토큰)0.664 ms1.3 µs77.6 TFLOP/s512
프리필 (질문 2048토큰)1.955 ms1.0 µs105.5 TFLOP/s2048
디코드 (한 글자, 혼자)0.319 ms318.9 µs0.3 TFLOP/s1
디코드 (한 글자, 32명 묶음)0.257 ms8.0 µs12.6 TFLOP/s32

같은 GPU가 105.5 TFLOP/s를 내기도 하고 0.3 TFLOP/s를 내기도 한다 — 350배 차이다. 토큰 하나를 뱉는 데 318.9 µs가 걸리는데, 그중 계산은 거의 없다. 가중치를 메모리에서 끌어오는 시간이다.

그래서 32명을 묶으면 토큰당 8.0 µs로 40배 빨라진다. 한 번 읽어 온 가중치로 32명을 처리하기 때문이고, 산술 강도가 1에서 32로 오른 것이 그 뜻이다. 묶는 것이 곧 성능이다.


3. 직접 재 보기 A — 배치를 키워도 대기가 안 늘어난다

“처리량을 올리면 지연이 늘어난다”는 것이 보통의 직감이다. 정말 그런지 쟀다.

배치 크기1832128256
한 스텝 (= 한 사람의 대기)0.321 ms0.243 ms0.246 ms0.228 ms0.368 ms
처리량3,120 토큰/초32,971130,006561,933696,195

처리량은 223배가 됐는데 대기는 1.1배다. 8명에서 128명까지는 오히려 대기가 줄기까지 했다.

이유는 2절이다. 디코드는 메모리에 묶여 있어서, 자리를 채워도 걸리는 시간이 거의 그대로다. 빈 트럭이나 꽉 찬 트럭이나 한 번 왕복하는 시간은 같은 것과 같다. 맞바꿈이 시작되는 것은 계산이 다시 병목이 되는 지점부터이고, 여기서는 256에서 그 조짐이 보인다(0.228 → 0.368 ms).

그러니 추론 서빙의 첫 번째 규칙은 “자리를 채워라”다. 비어 있는 배치 슬롯은 공짜로 얻을 수 있었던 처리량을 그냥 버리는 것이다.


4. 직접 재 보기 C — 빈자리는 어디서 생기나

문제는 요청마다 생성 길이가 다르다는 것이다. 어떤 사람은 20토큰, 어떤 사람은 1,300토큰을 받는다. 한 묶음을 통째로 돌리고 다 끝나야 다음 묶음을 받는 방식(정적 배칭)이라면, 가장 긴 요청이 끝날 때까지 나머지 자리가 놀게 된다.

요청 600개(길이 중앙값 70토큰, 최대 1,327토큰), 슬롯 32개로 모의실험했다.

총 스텝슬롯 점유율
정적 배칭9,60220.3%
연속 배칭2,44579.9%

정적 배칭은 슬롯의 20.3%만 쓰고 있었다. 끝난 자리에 다음 요청을 바로 채우는 것(연속 배칭)만으로 점유율이 79.9%가 되고 전체가 3.93배 빨라진다. 모델도 GPU도 그대로인데, 비는 자리를 안 비워 두는 것 하나로 얻은 이득이다.

자리가 차고 비는 모습

위(빨강)가 정적 배칭이다. 한 묶음이 시작될 때는 꽉 차 있다가 짧은 요청들이 먼저 끝나면서 점점 비어 가고, 가장 긴 하나가 끝날 때까지 그 상태로 머문다. 아래(파랑)는 빈자리가 생기는 즉시 채워져 계속 꽉 차 있다.

이것이 오늘날 추론 서버(vLLM, TGI 등)가 하는 일의 핵심이다. 여기에 KV 캐시 편의 메모리 문제가 겹치는데, 캐시를 페이지 단위로 쪼개 관리하는 기법(PagedAttention)이 바로 자리를 더 촘촘히 채우기 위한 장치다.


5. 흔한 오해와 한계

  1. “배치를 키우면 느려진다” — 디코드 구간에서는 거의 안 그렇다(3절, 223배 처리량에 대기 1.1배). 계산이 병목이 되는 지점부터 맞바꿈이 시작된다.
  2. “GPU 성능은 하나의 숫자” — 아니다. 같은 카드가 105.5와 0.3 TFLOP/s 사이를 오간다(2절). 벤치마크 숫자를 볼 때 어느 국면의 값인지를 물어야 한다.
  3. “모델을 작게 만드는 게 제일 급하다” — 4절에서는 모델을 건드리지 않고 3.93배를 얻었다. 지식 증류 편·양자화 편 이전에 볼 것이 남아 있을 수 있다.
  4. “점유율이 높으면 다 좋다” — 자리를 너무 많이 채우면 KV 캐시가 넘치고, 긴 요청이 짧은 요청의 응답을 늦춘다. 공정성과 꼬리 지연은 별도의 문제다.
  5. 이 글의 실험 — 12층짜리 작은 모델과 단순한 모의실험이다. 350배·3.93배 같은 수는 이 설정의 값이고, 요점은 디코드는 읽기에 묶여 있고, 그래서 묶는 것이 곧 성능이며, 빈자리가 곧 손해라는 구조다.

6. 한 문단 요약

추론은 성격이 다른 두 국면으로 나뉜다. 프리필은 토큰이 많아 계산에 묶이고(105.5 TFLOP/s), 디코드는 토큰 하나를 위해 가중치 전체를 읽어야 해서 읽기에 묶인다(0.3 TFLOP/s) — 같은 GPU에서 350배 차이다. 이 성질 덕에 배치를 1에서 256으로 키워도 처리량은 223배가 되는 동안 대기는 1.1배만 늘었다. 즉 추론 서빙의 승부는 자리를 채우는 것에 걸려 있다. 그런데 요청마다 생성 길이가 제각각이라, 한 묶음을 통째로 기다리는 정적 배칭은 슬롯의 20.3%만 쓰고 있었다. 끝난 자리를 바로 채우는 것만으로 점유율이 79.9%가 되고 전체가 3.93배 빨라진다 — 모델도 GPU도 그대로 둔 채로.


참고

표시는 이 브라우저에만 남는다. 서버로 가는 것은 없다.