devlos
Devlos Archive
devlos
전체 방문자
오늘
어제
10-13 00:01

최근 글

  • 분류 전체보기 (131)
    • 프로젝트 (1)
    • MSA 설계 & 도메인주도 설계 (9)
    • AI (15)
      • [LLMSO] Hands-On LLM 서빙과 최적.. (15)
    • 클라우드 컴퓨팅 & NoSQL (96)
      • [K8S Deploy] K8S 디플로이 스터디 (8)
      • [Cilium Study] 실리움 스터디 (8)
      • [KANS] 쿠버네티스 네트워크 심화 스터디 (12)
      • [T101] 테라폼 4기 스터디 (8)
      • [CICD] CICD 맛보기 스터디 (3)
      • [T101] 테라폼 기초 입문 스터디 (6)
      • [AEWS] Amazon EKS 워크숍 스터디 (7)
      • [PKOS] 쿠버네티스 실무 실습 스터디 (7)
      • Kubernetes (13)
      • Docker (7)
      • Redis (1)
      • Jenkins (3)
      • Terraform (1)
      • Ansible (4)
      • Kafka (1)
    • 프로그래밍 (7)
      • Spring Boot (5)
      • Broker (1)
    • 성능과 튜닝 (1)
    • ALM (0)
    • 기타 (2)

인기 글

태그

  • CloudNet@
  • Kubernetes
  • kOps
  • 데브옵스
  • K8s
  • t101 4기
  • ansible
  • DevOps
  • LLM 서빙
  • LLMSO
  • 쿠버네티스 스터디
  • MSA
  • docker
  • 도커
  • 쿠버네티스

티스토리

최근 댓글

hELLO · Designed By 정상우.
devlos

Devlos Archive

[5주차-LLMSO]CH9. LLM Optimization in Practice (2026.08.30)
AI/[LLMSO] Hands-On LLM 서빙과 최적화 스터디

[5주차-LLMSO]CH9. LLM Optimization in Practice (2026.08.30)

2026. 9. 5. 10:37
반응형
CH9 실전 LLM 최적화 — 처리량은 어디에서 오는가
CH9 · 실전 최적화 들어가며 1 최적화 계획 2 하드웨어 점검 3 벤치마크 트래픽 4 평가 지표 5 메모리 읽기 6 베이스라인 7 양자화 8 추가 튜닝 9 분산 서빙 10 트레이드오프 마치며
LLMSO 5주차 · CH9 LLM Optimization in Practice

처리량이 올랐다는 건
계산이 빨라진 걸까,
덜 계산한 걸까.

Qwen3-14B 한 대를 vLLM으로 띄워놓고, 여덟 단계에 걸쳐 처리량을 2.7배까지 끌어올리는 과정입니다. 그리고 그 2.7배가 어디에서 왔는지를 프로파일러로 끝까지 따라가 봅니다.

들어가며

읽는 시간 35분 대상 vLLM은 띄워봤지만 튜닝은 안 해본 개발자 사전 지식 GPU·KV 캐시라는 말을 들어본 정도 그림 10개

서빙 최적화 문서를 읽다 보면 항상 같은 곳에서 막힙니다. "AWQ를 켰더니 처리량이 2.7배가 됐다"까지는 읽히는데, 왜 2.7배인지가 안 읽힙니다. 4비트로 줄였으니 계산이 4배 빨라진 걸까요? 그렇게 생각하면 그다음 실험이 전부 어긋납니다.

이 장은 그 어긋남을 세 번 정면으로 깨뜨립니다. 같은 모델·같은 설정인데 트래픽만 바꿔도 처리량이 2.4배 차이가 나고, 양자화를 해도 GPU가 실제로 계산한 시간은 거의 그대로이며, GPU를 네 장 붙이면 어떤 서버에서는 빨라지고 어떤 서버에서는 오히려 느려집니다.

결론부터 세 줄
  1. 처리량이 오르는 진짜 경로는 "메모리가 남는다 → KV 캐시가 커진다 → 배칭이 촘촘해진다 → 스케줄링이 효율화된다"입니다. 연산이 빨라진 게 아닙니다.
  2. GPU를 더 붙일지는 GPU 개수가 아니라 GPU 사이를 잇는 선(NVLink냐 PCIe냐)이 결정합니다. PCIe만 있는 서버에서는 한 장이 네 장보다 빠릅니다.
  3. 처리량만 목표라면 분산 서빙보다 단순 복제가 3배 유리할 수 있습니다. 분산 서빙의 값어치는 처리량이 아니라 지연시간과 모델 크기에 있습니다.

접는 상자(▸)는 안 열어도 됩니다. 안에는 실제 명령어, 로그 원문, 계산식이 들어 있습니다. 본문만 읽어도 이야기는 끝까지 이어집니다.

그림은 눈으로 보는 게 아니라 만지는 겁니다. 슬라이더를 극단까지 밀어보세요. 노란 상자에 "해볼 것"이 적혀 있습니다.

이 페이지의 장 번호와 책의 장 번호는 다릅니다. 책을 가리킬 때는 "책 6장"처럼 앞에 '책'을 붙였습니다.

다루는 것

vLLM + Qwen3-14B를 예제로 한 8단계 최적화 절차 — 하드웨어 점검, 벤치마크 트래픽 설계, 지표 정의, 베이스라인 측정, AWQ 양자화, 워크로드별 튜닝, 분산 서빙 비교, 그리고 다섯 가지 트레이드오프입니다.

다루지 않는 것

양자화가 정확도를 얼마나 떨어뜨리는지는 다루지 않습니다(책 6장 소관입니다). CUDA 커널을 직접 짜는 법, 멀티 노드 구성(prefill–decode 분리), 그리고 LMCache·speculative decoding의 실습 코드도 여기 없습니다(책 7장). 이 페이지는 측정하고 해석하는 방법에 집중합니다.

숫자를 그대로 믿지 마세요

이 장에 나오는 모든 수치는 특정 GPU · 특정 트래픽에서 나온 하나의 사례입니다. 저자들도 마지막에 "이 실습 자체가 전형적인 정적 최적화였다"고 고백합니다. 가져갈 것은 474나 1,280 같은 숫자가 아니라, 그 숫자를 만들어낸 인과의 사슬입니다.

먼저 목표를 정합니다 — 무엇을 올리려고 하는 건지부터.

01최적화 계획 — 무엇을 올릴 것인가

한 줄 요약: 목표를 "처리량"으로 못 박고, 지연시간은 허용 범위 안에서만 지킵니다.

왜 이걸 배우나
"그냥 빠르게 만들면 되는 거 아닌가? 목표를 왜 따로 정하지?"

"빠르다"는 말이 두 가지를 동시에 가리키기 때문입니다. 초당 몇 토큰을 처리하느냐(처리량)와 내 요청 하나가 몇 밀리초 만에 답하느냐(지연시간)는 서로 다른 값이고, 자주 서로를 밀어냅니다.

둘 중 무엇을 올릴지 정하지 않고 튜닝을 시작하면, 숫자가 올라가도 좋아진 건지 나빠진 건지 판단할 수가 없습니다.

이 장에 나오는 낯선 말
throughput (처리량)
단위 시간에 처리한 토큰 수. 보통 TPS(tokens per second)로 씁니다. 대부분의 LLM 과금이 토큰 기준이라, 처리량이 곧 토큰당 비용입니다.
latency (지연시간)
요청 하나가 기다린 시간. 첫 글자가 나올 때까지(TTFT), 글자와 글자 사이(ITL) 등으로 나눠 봅니다.
batching (배칭)
여러 요청을 한 번에 묶어 GPU에 넣는 것. 묶으면 GPU가 놀지 않아 처리량이 오르지만, 묶이기를 기다리는 시간이 생깁니다.
concurrency (동시성)
서버가 동시에 붙잡고 있는 요청 수. 배칭의 재료입니다.

비유 — 엘리베이터

1층 로비의 엘리베이터를 생각해 보세요. 사람이 오는 대로 즉시 출발시키면 기다림은 없지만 한 번에 한 명씩만 실어 나릅니다. 반대로 정원이 찰 때까지 문을 붙잡고 있으면 한 번에 열 명을 실어 나르지만, 먼저 탄 사람은 그만큼 문 앞에서 기다립니다.

처리량을 올린다는 건 엘리베이터를 꽉 채운다는 뜻이고, 지연시간을 줄인다는 건 문을 빨리 닫는다는 뜻입니다. 이 장은 "승객이 못 참을 만큼은 기다리게 하지 않으면서 최대한 꽉 채우기"를 목표로 삼습니다.

작은 숫자로 한 번

같은 GPU 한 장에서 서버 A는 초당 500토큰, 서버 B는 초당 1,500토큰을 처리한다고 해봅시다.

# GPU 시간당 비용이 $1.5로 같다고 하면 서버 A : 500 tok/s × 3600s = 1,800,000 tok → $1.5 / 180만 토큰 서버 B : 1500 tok/s × 3600s = 5,400,000 tok → $1.5 / 540만 토큰 # 토큰 100만 개당 비용 서버 A : $0.83 서버 B : $0.28 ← 같은 하드웨어, 1/3 비용

하드웨어를 한 대도 더 사지 않고 비용이 3분의 1이 됩니다. 이것이 처리량을 첫 번째 목표로 두는 이유입니다.

그림 1

동시성을 올리면 무슨 일이 일어나는가

↔ 슬라이더
해볼 것 — ① 동시성을 1에서 10까지 올리며 초록 처리량 곡선이 가파르게 오르는 걸 보세요. ② 40을 넘기면 처리량은 거의 평평해지는데 주황 지연시간만 계속 올라갑니다. ③ 64까지 끝까지 밀면 "더 이상 얻을 게 없는데 사용자만 기다리는" 구간이 어떻게 생기는지 보입니다.
대략적인 수치입니다

동시성 10에서 474 TPS · TTFT 104ms · ITL 43ms라는 실측값에 곡선을 맞춘 모형입니다. 다른 지점은 실측이 아닙니다.

그림에 나오는 말 동시 요청 수 (concurrency) 서버가 같은 시각에 붙잡고 있는 요청 수 · 총 처리량 (TPS) 초당 처리한 입력+출력 토큰 수 · TTFT Time To First Token — 요청 후 첫 토큰이 나올 때까지의 시간 · ITL Inter-Token Latency — 토큰과 토큰 사이의 시간

여덟 단계 계획

  1. 하드웨어 점검GPU 메모리 크기, 대역폭, 인터커넥트를 먼저 확인합니다. 나중에 결과를 해석할 근거가 됩니다.
  2. 벤치마크 트래픽 생성실제 트래픽을 닮은 데이터셋을 고릅니다. 여기서 최적화 방향이 갈립니다.
  3. 평가 지표 정의비교 기준을 미리 못 박습니다. 나중에 정하면 유리한 숫자만 고르게 됩니다.
  4. 서빙 서버 구성기본 설정 그대로 vLLM을 띄우고, 로그에서 메모리 분배를 읽습니다.
  5. 베이스라인 벤치마크튜닝 없는 상태의 숫자를 확보합니다. 모든 비교의 원점입니다.
  6. 양자화 벤치마크AWQ 4비트로 같은 실험을 반복해 차이를 봅니다.
  7. 추가 기법 적용워크로드 성격에 맞는 캐싱·배칭 옵션을 고릅니다.
  8. 분산 서빙 벤치마크GPU를 2장·4장으로 늘려보고, 하드웨어가 다르면 결론이 뒤집히는 걸 확인합니다.
실습 환경 — GPU가 없어도 따라갈 수 있습니다

원문 실습은 AWS g6e.2xlarge(NVIDIA L40S, VRAM 약 46GB)에서 진행합니다. 멀티 GPU 구간에서는 g6e.12xlarge(L40S ×4)와 p4d.24xlarge(A100 ×8)를 씁니다.

대안으로는 Runpod A40(48GB)이 시간당 $0.44 수준이고, RTX 4070(16GB) 같은 데스크톱이라면 모델을 Qwen3-4B → Qwen3-4B-AWQ로 낮춰 같은 논리를 재현할 수 있습니다(8장에 그 결과가 있습니다).

저자들이 각 단계의 실행 결과가 담긴 노트북(model_optimization_in_practice.ipynb)을 함께 제공하므로, GPU 없이도 과정과 출력은 그대로 따라갈 수 있습니다.

1장 세 줄 요약
  1. 정의 — 처리량은 초당 토큰 수, 지연시간은 요청 하나가 기다린 시간. 목표는 "처리량 우선, 지연시간은 허용 범위 유지".
  2. 성질 — 배칭은 처리량을 올리고 지연시간을 늘립니다. 동시성을 올릴수록 처리량 이득은 줄고 지연 손해는 늘어납니다.
  3. 쓰임 — 대부분의 과금이 토큰 기준이므로, 처리량은 곧 토큰당 비용입니다. 3배 처리량 = 3분의 1 비용.

목표가 정해졌으니, 이제 무대를 확인할 차례입니다 — 어떤 GPU 위에서 하는 실험인가.

02Step 1 — 코드를 짜기 전에 GPU부터 본다

한 줄 요약: nvidia-smi 한 줄이 나중에 "왜 느린가"를 가르는 진단 기준이 됩니다.

왜 이걸 배우나
"어차피 vLLM이 알아서 잡아 쓰는데, GPU 스펙을 왜 미리 봐야 하지?"

나중에 느린 이유를 구분하기 위해서입니다. "GPU 사용률이 40%인데 처리량이 낮다"와 "GPU 사용률이 97%인데 처리량이 낮다"는 완전히 다른 병입니다. 앞의 것은 배칭·스케줄링 문제고, 뒤의 것은 메모리나 커널 문제입니다.

기준선이 없으면 이 구분을 할 수 없습니다. 그래서 벤치마크 전 사전 점검(pre-flight check)을 먼저 합니다.

이 장에 나오는 낯선 말
performance state (P-state)
GPU의 전력·클럭 상태. P8은 유휴, P0/P1은 최고 속도로 일하는 중입니다. 벤치마크를 돌리는데 P8이면 요청이 안 들어오고 있다는 뜻입니다.
GPU utilization (사용률)
GPU가 일을 하고 있던 시간의 비율. "얼마나 열심히"가 아니라 "얼마나 자주"에 가깝습니다.
compute capability
GPU 세대를 나타내는 숫자(예: 8.9). 어떤 커널·양자화 방식을 쓸 수 있는지가 여기서 갈립니다.
VRAM
GPU에 붙어 있는 메모리. 모델 가중치와 KV 캐시가 여기를 나눠 씁니다. 5장의 주인공입니다.

비유 — 병원 도착 직후의 활력징후

환자가 오면 의사는 증상을 듣기 전에 체온·맥박·혈압부터 잽니다. 열이 39도인 상태에서 "머리가 아프다"는 말은 정상 체온일 때와 전혀 다른 의미를 갖기 때문입니다.

nvidia-smi가 GPU의 활력징후입니다. 전력·사용률·메모리·온도를 한 화면에서 보여주고, 이 네 개의 조합이 진단을 만듭니다.

작은 숫자로 한 번

# 실습 장비: L40S 한 장 총 VRAM : 46,068 MiB (약 46 GB) 현재 사용 중 : 0 MiB ← 아무것도 안 올라가 있음 Performance : P8 ← 유휴 상태 전력 : 15W / 285W # 이 상태에서 Qwen3-14B(bf16)를 올리면 가중치만 27.5 GB. # 남는 18.5 GB로 KV 캐시와 그 밖의 것을 다 해결해야 합니다. → 5장
그림 2

사용률과 전력, 두 숫자로 병을 가른다

↔ 슬라이더 2개
해볼 것 — ① 사용률을 낮게(20%), 전력도 낮게 두고 진단을 읽어보세요. ② 사용률만 그대로 두고 전력을 90%까지 올리면 진단이 바뀝니다. ③ 둘 다 90% 이상으로 밀면 "정상 포화" — 이때는 설정이 아니라 하드웨어를 바꿔야 합니다.
관찰 포인트

같은 "느리다"는 증상이 네 개의 다른 진단으로 갈립니다. 사용률 하나만 보면 이 구분을 못 합니다.

그림에 나오는 말 GPU 사용률 GPU가 일을 하고 있던 시간의 비율 · 전력 (정격 대비) 최대 소비 전력 대비 지금 쓰고 있는 비율 · P-state GPU의 전력·클럭 상태. P8은 유휴, P0/P1은 최고 속도
실제 명령과 출력

필요한 항목만 골라 CSV로 뽑을 수도 있습니다.

nvidia-smi --query-gpu=name,compute_cap,memory.free,memory.used,memory.total --format=csv

name, compute_cap, memory.free [MiB], memory.used [MiB], memory.total [MiB]
NVIDIA L40S, 8.9, 46050 MiB, 18 MiB, 46068 MiB

확인할 네 가지는 이렇습니다.

CUDA·드라이버 버전 — 쓰려는 서빙 프레임워크와 호환되는지.
P-state — P8이면 유휴, P0/P1이면 최고 속도로 동작 중.
전력과 사용률 — 이 둘의 조합이 진단입니다(위 그림).
메모리 — 지금 몇 GB가 비어 있는지. 모델이 들어갈 자리가 있는지.

2장 세 줄 요약
  1. 정의 — nvidia-smi는 GPU의 활력징후. 이름·메모리·P-state·전력·사용률을 한 번에 보여줍니다.
  2. 성질 — 사용률과 전력을 같이 봐야 진단이 갈립니다. 사용률↓+부하 있음 = 배칭/스케줄링 문제, 전력↑+처리량↓ = 메모리 병목이나 커널 비효율.
  3. 쓰임 — 벤치마크 전 사전 점검. 총 VRAM 46GB를 확인해 뒀기 때문에 5장에서 "27.5 + 11 = 38.5"라는 분배를 읽어낼 수 있습니다.

무대를 확인했습니다. 이제 무대에 올릴 관객, 즉 트래픽을 만들 차례입니다.

03Step 2 — 어떤 트래픽으로 잴 것인가

한 줄 요약: 데이터셋이 최적화 방향을 결정합니다. 아무 프롬프트나 넣고 재면 실서비스와 다른 답이 나옵니다.

왜 이걸 배우나
"벤치마크는 그냥 부하만 주면 되는 거 아닌가?"

입력이 길고 출력이 짧은 워크로드(prefill 위주)와, 입력이 짧고 출력이 긴 워크로드(decode 위주)는 서로 다른 기법을 요구합니다. 앞의 것은 캐싱으로, 뒤의 것은 speculative decoding으로 개선됩니다.

그러니 내 서비스를 닮지 않은 트래픽으로 튜닝하면, 튜닝한 만큼 엉뚱한 방향으로 갑니다.

이 장에 나오는 낯선 말
prefill / decode
LLM 추론의 두 단계. prefill은 입력 프롬프트를 한꺼번에 읽는 단계, decode는 출력 토큰을 한 개씩 뱉는 단계입니다.
prefix
프롬프트의 앞부분. 시스템 프롬프트나 공통 지시문처럼 여러 요청이 똑같이 갖고 시작하는 부분입니다.
prefix caching
같은 prefix를 다시 계산하지 않고 앞서 계산해 둔 KV를 재사용하는 것. 이 장 전체에서 가장 큰 효과를 내는 기법입니다.
synthetic dataset (합성 데이터셋)
실제 데이터의 성질만 흉내 내서 기계로 만든 데이터. 원하는 성질(반복률 등)을 정확히 조절할 수 있습니다.
request rate / burstiness
초당 몇 개를 보낼지, 그리고 그 도착이 고르게 오는지 몰려서 오는지.

비유 — 도서관 대출대

도서관 대출 창구의 처리 속도를 재려고 합니다. 서로 다른 책을 든 사람 100명이 오는 것과, 같은 교재를 든 수업 수강생 100명이 오는 것은 완전히 다른 실험입니다. 뒤의 경우 직원은 책 정보를 한 번만 찾아 놓고 100명에게 재사용할 수 있으니까요.

이 장은 두 종류의 줄을 모두 세워봅니다. ShareGPT는 제각각인 손님, Prefix Repetition은 같은 교재를 든 손님입니다.

작은 숫자로 한 번

ShareGPT 100개 샘플의 길이 분포는 이렇습니다.

# ShareGPT 100 samples Prompt 평균 232.6 토큰 / 중앙값 141.5 (최소 5, 최대 817) Output 평균 220.6 토큰 / 중앙값 164.5 # 입력 ≈ 출력 → prefill 위주도, decode 위주도 아닌 "균형 잡힌 대화 트래픽"

평균과 중앙값이 크게 벌어져 있다는 점(232.6 vs 141.5)도 중요합니다. 짧은 프롬프트가 다수이고 아주 긴 것 몇 개가 평균을 끌어올리고 있다는 뜻입니다. 실제 대화가 그렇습니다.

그림 3

고유 prefix를 줄이면 재계산이 줄어든다

↔ 슬라이더 + 버튼
해볼 것 — ① 고유 prefix 수를 4에서 1로 줄이면 20개 요청이 모두 같은 색이 되고 재계산이 사라집니다. ② 20까지 끝까지 올리면 요청마다 색이 달라지고 재사용률이 0이 됩니다 — 이게 ShareGPT 쪽입니다. ③ "ShareGPT처럼" 버튼으로 두 트래픽을 번갈아 보세요.
관찰 포인트

재사용률이 오르면 GPU가 해야 할 prefill 계산량 자체가 줄어듭니다. 6장에서 이게 2.4배 처리량 차이로 나타납니다.

그림에 나오는 말 prefix 프롬프트의 앞부분. 여러 요청이 똑같이 갖고 시작하는 구간 · suffix 요청마다 다른 뒷부분 · prefill 입력 프롬프트를 한꺼번에 읽는 단계 · prefix caching 같은 prefix의 계산 결과를 다시 계산하지 않고 재사용하는 것
두 데이터셋과 부하 생성 명령

ShareGPT — 실제 사용자가 LLM과 나눈 대화를 모은 데이터셋. 다양한 문체와 대화 구조가 섞여 있어 실사용자 트래픽에 가깝습니다.

Prefix Repetition — 합성 데이터셋. 각 프롬프트가 공통 prefix로 시작한 뒤 고유한 suffix가 붙습니다. --prefix-repetition-num-prefixes를 줄일수록 반복이 심해져 캐시 재사용 트래픽이 강해집니다.

vllm bench serve \
   --backend vllm --base-url "http://localhost:8000" \
   --model Qwen/Qwen3-14B \
   --dataset-name prefix_repetition \
   --num-prompts 1000 --request-rate 5 \
   --prefix-repetition-prefix-len 256 \
   --prefix-repetition-suffix-len 256 \
   --prefix-repetition-num-prefixes 10 \
   --prefix-repetition-output-len 128 \
   --max-concurrency 10 \
   --save-result --result-filename test_serve_results.txt

벤치마크 전에 inspect_dataset.py로 길이 분포와 샘플 프롬프트를 먼저 확인합니다. 모델을 추론시키는 코드가 아니라, "내가 지금 서버에 무엇을 보내려는가"를 미리 들여다보는 도구입니다.

3장 세 줄 요약
  1. 정의 — ShareGPT는 실제 대화를 모은 데이터셋, Prefix Repetition은 공통 prefix를 인위적으로 넣은 합성 데이터셋입니다.
  2. 성질 — 고유 prefix가 적을수록 재사용률이 오르고, 서버가 실제로 계산해야 할 prefill 토큰이 줄어듭니다.
  3. 쓰임 — 대화형 서비스라면 ShareGPT로 사용자 경험을, Prefix Repetition으로 캐시 동작을 각각 잽니다. 둘은 보완재입니다.

트래픽이 준비됐습니다. 이제 무엇을 숫자로 남길지 미리 정해야 합니다.

04Step 3 — 네 개의 숫자만 본다

한 줄 요약: 총 처리량 · 출력 처리량 · TTFT · ITL. 비교는 이 넷으로만 합니다.

왜 이걸 배우나
"지표는 많을수록 좋은 거 아닌가?"

지표가 많으면 비교가 흐려집니다. 실험이 여덟 번인데 지표가 열두 개면, 어느 실험이 더 나은지 판단할 수 없습니다. 또 지표를 나중에 고르면 유리하게 나온 숫자만 고르게 됩니다.

그래서 실험을 시작하기 전에 네 개로 못 박습니다.

이 장에 나오는 낯선 말
TTFT — Time To First Token
요청을 보내고 첫 글자가 나올 때까지의 시간. 토크나이징·큐 대기·prefill이 전부 여기 들어갑니다. 사용자가 "반응이 빠르다"고 느끼는 지점.
ITL — Inter-Token Latency
글자와 글자 사이의 시간. 스트리밍이 매끄러운지를 결정합니다.
TPOT — Time Per Output Token
출력 토큰 하나당 평균 시간. ITL과 거의 같은 것을 다른 각도에서 본 값입니다. 이 실습에서는 생략합니다.
p99
전체 요청을 느린 순으로 줄 세웠을 때 상위 1%에 해당하는 나쁜 값. "가장 운 나쁜 사용자"가 겪은 시간입니다.

비유 — 카페의 세 가지 시계

카페 사장은 세 개의 시계를 봅니다. 한 시간에 몇 잔을 팔았나(처리량), 주문하고 첫 잔이 나오기까지 몇 분인가(TTFT), 일행 4명 잔이 하나씩 나오는 간격(ITL)입니다.

세 시계가 서로를 밀어냅니다. 주문을 몰아서 한 번에 내리면 시간당 판매량은 오르지만 첫 잔은 늦어집니다. 그리고 평균만 보면 안 됩니다 — 100명 중 1명이 20분을 기다렸다면 그 사람은 다시 오지 않습니다. 그게 p99입니다.

작은 숫자로 한 번

# 입력 700 tok/s, 출력 300 tok/s로 처리 중일 때 Total token throughput = 700 + 300 = 1000 tok/s 시스템 전체 효율 Output token throughput = 300 = 300 tok/s 비용을 좌우하는 디코딩 성능 # 요청 하나의 시간 구성 (출력 200 토큰짜리) TTFT 104ms + ITL 43ms × 199 = 104 + 8,557 ≈ 8.7초 └ 체감 시간의 98%가 decode 구간

출력이 길어지면 TTFT는 거의 의미가 없어지고 ITL이 전부가 됩니다. 반대로 짧은 답변이 대부분인 서비스라면 TTFT가 사용자 경험을 지배합니다.

그림 4

요청 하나의 시간을 뜯어보기

↔ 슬라이더 2개
해볼 것 — ① 출력 토큰 수를 20으로 줄이면 파란 TTFT 막대가 전체에서 큰 비중을 차지합니다. ② 500으로 올리면 TTFT는 눈에 안 보일 정도로 작아집니다. ③ ITL을 43에서 80으로 올려보면 평균은 조금 오르는데 p99가 훨씬 크게 튀는 걸 보세요.
대략적인 수치입니다

다섯 요청의 흔들림은 고정 씨앗 난수로 만든 예시입니다. p99가 평균보다 크다는 성질을 보여주기 위한 모형입니다.

그림에 나오는 말 TTFT Time To First Token — 첫 토큰이 나올 때까지의 시간 · ITL Inter-Token Latency — 토큰과 토큰 사이의 시간 · E2E 요청 하나가 시작부터 끝까지 걸린 전체 시간 · p99 느린 순으로 줄 세웠을 때 상위 1%가 겪은 값
전체 지표 체계 — 이 중 넷만 씁니다
범주주요 지표이 실습에서
처리량총 토큰 TPS, 출력 토큰 TPS, 요청/초총 TPS · 출력 TPS 사용
지연시간TTFT, TPOT, ITL (평균 + P99)평균 TTFT · 평균 ITL 사용
자원 활용도GPU 사용률, 메모리 사용량참고용으로만
워크로드 특성입출력 비율, 동시성, 요청률실험 조건으로 고정
신뢰성 / 비용오류율, 비용 효율생략

평균(mean)은 이상치에 흔들리고, 중앙값(median)은 보통 사용자의 경험을 보여주며, p99는 가장 나쁜 경험을 보여줍니다. SLO를 정할 때는 보통 p99를 씁니다.

4장 세 줄 요약
  1. 정의 — 총 TPS(입력+출력), 출력 TPS, TTFT(첫 토큰까지), ITL(토큰 사이). 이 넷으로 모든 실험을 비교합니다.
  2. 성질 — TTFT는 prefill 효율을, ITL은 decode 효율을 대변합니다. 출력이 길수록 체감 시간은 ITL이 지배합니다.
  3. 쓰임 — vllm bench serve --save-result가 이 값들을 자동으로 남깁니다. 지표를 미리 못 박아야 6장 이후의 비교가 성립합니다.

이제 서버를 띄웁니다. 그리고 로그 다섯 줄에서 이 장 전체의 병목을 발견하게 됩니다.

05Step 4 — 로그 다섯 줄이 병목을 알려준다

한 줄 요약: 46GB 중 27.5GB를 모델이 먹고, KV 캐시에는 11GB만 남습니다. 이 장 전체의 병목입니다.

왜 이걸 배우나
"모델이 GPU에 올라갔으면 된 거 아닌가? 메모리 배분까지 봐야 하나?"

봐야 합니다. KV 캐시에 남은 공간이 곧 동시에 처리할 수 있는 요청 수이기 때문입니다. 캐시가 좁으면 서버는 배치를 줄이거나 캐시를 버려야 하고, 버린 건 다시 계산해야 합니다.

즉 메모리 배분표가 곧 처리량 예보입니다. 벤치마크를 돌리기 전에 이미 결과의 절반이 정해져 있습니다.

이 장에 나오는 낯선 말
KV cache
이미 읽은 토큰의 계산 결과를 저장해 둔 것. 다음 토큰을 만들 때 앞 토큰들을 다시 계산하지 않으려고 들고 있습니다. 요청마다, 토큰마다 자리를 차지합니다.
eviction (축출)
캐시가 꽉 차서 기존 항목을 버리는 것. 버린 걸 나중에 또 쓰게 되면 다시 계산해야 합니다.
recomputation (재계산)
버렸던 KV를 다시 만드는 일. 순수한 낭비입니다.
max concurrency
vLLM이 로그에 찍어 주는 값. "최대 길이 요청 기준으로 몇 개를 동시에 붙잡을 수 있는가"입니다. 1.76x는 두 개도 못 든다는 뜻입니다.
gpu-memory-utilization
전체 VRAM 중 vLLM이 쓰겠다고 잡는 비율. 기본값은 0.9입니다.

비유 — 이삿짐 트럭

46평 트럭에 냉장고(모델 가중치)를 실었더니 27.5평을 차지했습니다. 남은 18.5평에 작업 공간(KV 캐시)을 만들어야 하는데, 실제로 확보된 건 11평입니다.

작업 공간이 좁으면 짐을 한 번에 두 개씩만 옮길 수 있고, 잠깐 내려놨던 짐을 다시 들어 올려야 합니다. 냉장고를 작은 걸로 바꾸면(양자화) 작업 공간이 넓어지고, 그러면 한 번에 다섯 개씩 옮길 수 있게 됩니다. 이게 7장의 이야기입니다.

작은 숫자로 한 번

# vllm.log — Qwen3-14B(bf16) 로딩 직후 Model loading took 27.5185 GiB Available KV cache memory: 11.00 GiB GPU KV cache size: 72,064 tokens Maximum concurrency for 40,960 tokens per request: 1.76x # 읽는 법 46 GB 중 38.5 GB 사용 → 모델이 전체의 약 60%를 차지 KV 캐시 11 GB → 토큰 하나당 약 0.156 MiB → 72,064 토큰 72,064 ÷ 40,960 → 1.76 ← 최대 길이 요청을 두 개도 못 든다

1.76x라는 숫자를 조금 더 밀어붙여 봅시다. 동시에 처리하고 싶은 요청이 10개인데 캐시가 두 개 분량뿐이면, 다음이 연쇄적으로 일어납니다.

KV 캐시 공간 부족 ↓ 배치 축소 또는 캐시 축출 증가 ↓ 디코딩 중 재계산 증가 ↓ GPU 활용 효율 저하 ↓ 토큰 처리량 저하
그림 5

VRAM 46GB를 무엇이 나눠 쓰는가

↔ 슬라이더 2개
해볼 것 — ① 모델 가중치를 27.5에서 9.6으로 내려보세요(= AWQ 4비트). KV 토큰 수와 동시성이 어떻게 뛰는지 보세요. ② gpu-memory-utilization을 0.7까지 내리면 남는 VRAM이 그냥 놀게 됩니다. ③ 가중치를 40GB까지 밀면 KV가 0이 되고 서버가 아예 못 뜹니다.
관찰 포인트

가중치를 18GB 줄이면 KV 토큰이 2.6배가 됩니다. 메모리는 선형으로 줄었는데 캐시는 훨씬 크게 늘어납니다 — 남는 자리를 전부 캐시가 가져가기 때문입니다.

그림에 나오는 말 모델 가중치 모델 자체가 차지하는 GPU 메모리 · KV 캐시 이미 읽은 토큰의 계산 결과를 담아두는 공간 · 최대 동시성 최대 길이 요청 기준으로 동시에 붙잡을 수 있는 개수 · gpu-memory-utilization vLLM이 쓰겠다고 잡는 VRAM 비율. 기본값 0.9
여기서 방향이 정해집니다

모델을 작게 만들면 단순히 VRAM이 절약되는 게 아닙니다. 남는 VRAM이 전부 KV 캐시로 넘어가 동시성이 오르고, 배칭이 촘촘해지고, 처리량이 오릅니다. 이것이 7장(양자화)을 하는 이유입니다.

5장 세 줄 요약
  1. 정의 — vLLM 기동 로그는 모델 가중치 / KV 캐시 / KV 토큰 수 / 최대 동시성을 순서대로 찍어 줍니다.
  2. 성질 — KV 캐시 용량이 곧 동시성 한도입니다. 좁으면 축출 → 재계산 → 처리량 저하로 이어집니다.
  3. 쓰임 — 27.5 + 11 = 38.5 / 46. 모델이 60%를 차지한 이 배분표가 "양자화를 해야 한다"는 결론의 근거입니다.

병목을 확인했으니, 이제 원점이 될 숫자를 뽑습니다.

06Step 5 — 베이스라인, 그리고 2.4배의 정체

한 줄 요약: 모델도 설정도 그대로인데 트래픽만 바꿨더니 처리량이 2.4배가 됐습니다.

왜 이걸 배우나
"설정을 하나도 안 바꿨는데 처리량이 2.4배가 될 수 있나?"

됩니다. 그리고 이 사실이 이 장에서 가장 중요합니다. 처리량은 GPU가 얼마나 빠른가가 아니라, 같은 계산을 얼마나 안 반복하는가로도 결정됩니다.

GPU 사용률은 두 실험 모두 97%였습니다. GPU는 똑같이 열심히 일했는데, 한쪽은 안 해도 될 일을 하고 있었던 겁니다.

이 장에 나오는 낯선 말
baseline (베이스라인)
튜닝 전 원점. 이후 모든 실험은 이 값과 비교합니다.
continuous batching
배치가 다 끝나기를 기다리지 않고, 먼저 끝난 자리에 새 요청을 바로 채워 넣는 배칭. vLLM이 자동으로 합니다.
memory block sharing
같은 내용의 KV를 여러 요청이 한 벌만 두고 나눠 쓰는 것.
max-concurrency
벤치마크 클라이언트가 동시에 열어 두는 요청 수. 서버 쪽 max concurrency와 이름이 비슷하지만 다른 값입니다.

비유 — 같은 자료를 열 번 복사하기

사무실 복사기 앞에 열 명이 줄을 섰습니다. 각자 다른 문서를 들고 있으면 열 번을 다 복사해야 합니다. 그런데 앞 20장이 전부 똑같은 표지라면? 표지는 한 번만 찍어 두고 열 명에게 나눠주면 됩니다.

복사기 속도는 그대로인데 총 처리량은 크게 오릅니다. 이게 prefix caching입니다.

작은 숫자로 한 번

# 같은 서버, 같은 설정. 데이터셋만 교체 ShareGPT Prefix Repetition Total TPS 474 1,123 ← 2.37배 Mean TTFT 104.15 ms 104.64 ms ← 거의 같음 Mean ITL 43.24 ms 43.95 ms ← 거의 같음 GPU 사용률 97% 97% ← 둘 다 포화 # 처리량만 오르고 지연시간은 그대로 → "요청 하나가 빨라진" 게 아니라 # "같은 시간에 더 많이 통과한" 것
그림 6

두 트래픽의 네 지표를 나란히

↔ 버튼 + 슬라이더
해볼 것 — ① 두 버튼을 번갈아 눌러 보세요. 처리량 막대만 크게 움직이고 TTFT·ITL 막대는 거의 안 움직입니다. ② 동시성을 1까지 내리면 두 트래픽의 차이가 거의 사라집니다 — 배칭할 게 없으니 캐시 재사용의 이득도 작아집니다. ③ 32까지 올리면 차이가 최대로 벌어집니다.
대략적인 수치입니다

동시성 10일 때의 값만 실측(474 / 1,123 TPS)입니다. 다른 동시성 값은 곡선을 맞춘 모형입니다.

그림에 나오는 말 총 처리량 (TPS) 입력+출력을 합산한 초당 토큰 수 · 출력 처리량 초당 생성한 출력 토큰 수 · TTFT · ITL 첫 토큰까지의 시간 · 토큰과 토큰 사이의 시간 · ShareGPT 실제 대화를 모은 데이터셋 · Prefix Repetition 공통 prefix를 인위적으로 넣은 합성 데이터셋
벤치마크 명령과 원문 출력
vllm bench serve \
     --backend vllm --base-url "http://localhost:8000" \
     --model Qwen/Qwen3-14B \
     --dataset-name sharegpt \
     --dataset-path ShareGPT_V3_unfiltered_cleaned_split.json \
     --num-prompts 2000 --request-rate 10 --burstiness 1.0 \
     --max-concurrency 10 \
     --save-result --append-result --result-filename test_serve_results.txt
============ Serving Benchmark Result ============
Successful requests:                     2000
Maximum request concurrency:             10
Benchmark duration (s):                  1810.09
Total input tokens:                      446619
Total generated tokens:                  412052
Output Throughput   :  227.64 tok/s
Total Throughput    :  474.38 tok/s
Mean TTFT           :  104.15 ms
Mean ITL            :   43.24 ms
P99 ITL             :   72.15 ms

이 다섯 줄이 모든 후속 실험의 원점입니다. 앞으로 어떤 옵션을 켜든 이 숫자와 비교합니다.

이 장의 교훈 한 줄

같은 계산을 반복하지 않는 것이 가장 좋은 최적화 중 하나입니다. 실제 트래픽에 시스템 프롬프트 공유나 반복 질문이 많다면, 하드웨어를 한 대도 늘리지 않고 처리량을 크게 올릴 수 있습니다.

6장 세 줄 요약
  1. 정의 — 베이스라인은 474 TPS · TTFT 104ms · ITL 43ms(ShareGPT, 동시성 10)입니다.
  2. 성질 — 같은 설정에서 Prefix Repetition으로 바꾸면 1,123 TPS(2.37배). 지연시간은 거의 그대로입니다.
  3. 쓰임 — 처리량 상승의 원인은 연산 가속이 아니라 prefix caching · continuous batching · memory block sharing에 의한 재계산 회피입니다.

트래픽을 바꾸는 건 우리 손에 없을 때가 많습니다. 이번엔 모델 쪽을 건드려 봅니다.

07Step 6 — 양자화, 그리고 2.7배

한 줄 요약: 가중치를 4비트로 줄이면 남는 17GB가 전부 KV 캐시로 갑니다. 처리량 2.7배, TTFT 42% 개선.

왜 이걸 배우나
"4비트로 줄이면 정밀도가 떨어지는데, 왜 오히려 빨라지지?"

속도가 빨라진 게 아니라 자리가 남은 것이기 때문입니다. 가중치가 27.5GB에서 9.6GB로 줄면 그 차이 17GB가 통째로 KV 캐시로 넘어가고, 동시성이 1.76x에서 4.66x로 뜁니다.

"작게 만든다 → 많이 담는다 → 촘촘히 묶는다 → 처리량이 오른다"가 정확한 순서입니다. 여기 "계산이 빨라진다"는 단계는 없습니다.

이 장에 나오는 낯선 말
quantization (양자화)
숫자를 더 적은 비트로 표현하는 것. bf16(16비트)을 4비트로 줄이면 저장 공간이 약 4분의 1이 됩니다.
AWQ — Activation-aware Weight Quantization
가중치를 양자화하되, 중요한 가중치는 덜 뭉개는 방식. 이름 그대로 활성값(activation)을 보고 중요도를 판단합니다.
weight quantization vs activation quantization
앞의 것은 저장·이동량을 줄이고, 뒤의 것은 연산량 자체를 줄입니다. AWQ는 앞쪽입니다.
Marlin kernel
4비트 가중치를 되풀어서(dequantize) 곱하는 전용 커널. 8장에서 다시 나옵니다.

비유 — 사전을 문고판으로 바꾸기

책상 위에 두꺼운 사전이 놓여 있습니다. 문고판으로 바꾸면 찾는 속도가 빨라지는 게 아니라 책상이 넓어집니다. 넓어진 책상에 작업 중인 서류를 더 많이 펼칠 수 있고, 그 결과 하루에 처리하는 일이 늘어납니다.

사전을 줄여서 글자가 조금 흐려진 건 사실입니다(정확도 손실). 그 대가로 책상을 얻은 겁니다.

작은 숫자로 한 번

# 같은 GPU, 같은 트래픽. 모델만 Qwen3-14B → Qwen3-14B-AWQ bf16 AWQ 4bit 변화 모델 가중치 27.5 GB 9.6 GB -65% KV 캐시 토큰 72,064 191,056 ×2.65 최대 동시성 1.76x 4.66x ×2.65 Total TPS (ShareGPT) 474 1,280 ×2.70 Mean TTFT 103.61 ms 59.29 ms -42%

주목할 점은 처리량과 지연시간이 동시에 좋아졌다는 것입니다. 1장에서 둘은 자주 상충한다고 했지만, "낭비를 없애는" 종류의 최적화는 둘을 한꺼번에 개선합니다.

그림 7

비트를 줄이면 어디까지 따라 움직이는가

↔ 슬라이더
해볼 것 — ① 16비트에서 4비트로 내리며 파란 가중치 막대가 줄고 초록 KV 막대가 그 자리를 채우는 걸 보세요. ② 동시성 숫자가 1.76 → 4.66으로 뛰는 지점이 어디인지 확인하세요. ③ 8비트 지점의 값은 실측이 아니라 보간입니다 — 오른쪽 경고를 읽어보세요.
대략적인 수치입니다

16비트와 4비트만 실측입니다(27.5GB/474TPS, 9.6GB/1,280TPS). 가운데 8비트는 두 점 사이를 이은 추정입니다.

그림에 나오는 말 비트 폭 가중치 하나를 몇 비트로 저장하는가. 16비트를 4비트로 줄이는 것이 양자화 · KV 토큰 수 KV 캐시가 담을 수 있는 토큰 개수 · 최대 동시성 동시에 붙잡을 수 있는 요청 수 · TPS · TTFT 초당 처리 토큰 수 · 첫 토큰까지의 시간
양자화도 공짜는 아닙니다

메모리 효율은 크게 좋아지지만 정확도 손실, 토큰 불안정성, 출력 품질 변화가 생길 수 있습니다. 4비트·8비트·FP8 중 무엇을 쓸지는 "성능 이득 대비 품질 저하를 얼마나 감내할 수 있는가"로 결정해야 합니다. 이 실습은 품질 평가를 하지 않았습니다 — 그건 책 6장(양자화 장)의 몫입니다.

인과의 사슬 — 이 순서를 외우면 됩니다
Quantization
   ↓
모델 메모리 감소  (27.5 → 9.6 GB)
   ↓
KV Cache 증가     (72,064 → 191,056 tokens)
   ↓
동시 Request 증가 (1.76x → 4.66x)
   ↓
Batch 효율 증가
   ↓
Throughput 증가   (474 → 1,280 TPS)

이 사슬 어디에도 "행렬곱이 빨라진다"는 항목이 없습니다. AWQ는 가중치 양자화라서 데이터 이동량과 메모리 사용량을 줄이는 데 초점이 있습니다. 연산량 자체를 줄이려면 활성화(activation) 양자화가 필요하고, 그건 책 6장 소관입니다. 8장에서 이 주장을 프로파일러로 직접 검증합니다.

7장 세 줄 요약
  1. 정의 — AWQ는 활성값을 고려한 4비트 가중치 양자화. 가중치를 27.5GB에서 9.6GB로 줄입니다.
  2. 성질 — 확보된 17GB가 KV 캐시로 넘어가 토큰 용량 2.65배, 동시성 2.65배, 처리량 2.7배, TTFT 42% 개선.
  3. 쓰임 — 하드웨어를 늘리지 않고 처리량과 지연시간을 동시에 개선하는 가장 가성비 높은 한 수. 단, 품질 저하 가능성은 별도 검증 필요.

여기까지가 "누구에게나 통하는" 최적화입니다. 이제부터는 내 트래픽에 맞는 선택입니다.

08Step 7 — 워크로드에 맞춰 고르기, 그리고 프로파일러가 밝힌 진짜 이유

한 줄 요약: 기법은 병목을 보고 고릅니다. 그리고 2배의 원인은 GEMM이 아니라 동기화 대기였습니다.

왜 이걸 배우나
"옵션이 이렇게 많은데, 뭘 켜야 하는지 어떻게 알지?"

옵션 값을 찾는 게 아니라 기법을 고르는 문제이기 때문입니다. 입력이 길고 공유 컨텍스트가 많으면 캐싱 계열을, 출력이 길면 speculative decoding 계열을 씁니다. 무엇이 병목인지 모른 채 노브를 돌리면 특정 GPU에만 맞는 설정을 만들어 놓고 끝납니다.

그리고 이 장 후반에서, 지금까지 우리가 믿어 온 인과 사슬을 프로파일러로 검증합니다.

이 장에 나오는 낯선 말
chunked prefill
긴 입력의 prefill을 여러 조각으로 나눠 처리하는 것. 긴 요청 하나가 다른 요청들을 막지 않게 합니다.
speculative decoding
작은 초안 모델로 여러 토큰을 미리 찍어보고 큰 모델이 한꺼번에 검증하는 방식. decode 위주 워크로드에 효과적입니다.
LMCache
계산해 둔 KV를 GPU 밖(CPU·디스크)까지 확장해 재사용하는 캐시. 멀티턴 채팅이나 RAG처럼 공유 컨텍스트가 많을 때 씁니다.
block-size
KV 캐시를 관리하는 블록 단위. 작을수록 자투리 낭비가 줄어 캐시 활용도가 오릅니다.
Nsight Systems / Nsight Compute
NVIDIA 프로파일러. 앞의 것은 전체 타임라인, 뒤의 것은 커널 하나의 속사정을 봅니다.
occupancy (점유율)
GPU의 연산 유닛이 얼마나 채워져 돌고 있는지. 낮다고 무조건 나쁜 건 아닙니다.
cudaEventSynchronize
호스트(CPU)가 GPU 작업이 끝나기를 기다리는 호출. 여기 쌓인 시간이 곧 "기다린 시간"입니다.

비유 — 주방의 병목은 매번 다른 곳에 있다

주문이 밀리는 식당을 개선한다고 해봅시다. 손질(prefill)에서 밀리는 집과 굽기(decode)에서 밀리는 집은 처방이 완전히 다릅니다. 손질에서 밀리면 미리 손질해 둔 재료를 재사용하면 되고(LMCache), 굽기에서 밀리면 보조 요리사가 미리 초벌을 해두는 편이 낫습니다(speculative decoding).

어디서 밀리는지 모르는 채 칼을 새로 사면 아무 일도 일어나지 않습니다.

작은 숫자로 한 번

vLLM의 주요 노브는 크게 두 갈래입니다.

# 메모리 / 캐시 계열 --gpu-memory-utilization 0.9 캐시에 쓸 VRAM 비율 --max-model-len 4096 최대 컨텍스트 길이 --block-size 16 작을수록 캐시 활용도 ↑ # 배칭 / 스케줄링 계열 --max-num-seqs 512 동시 시퀀스 수 --max-num-batched-tokens 16384 배치당 토큰 수 --enable-prefix-caching 3·6장에서 본 그 캐싱 --enable-chunked-prefill 긴 입력을 쪼개서
설정을 과도하게 튜닝하지 마세요

특정 GPU와 특정 트래픽에 완벽히 맞춘 설정은 다른 하드웨어에서 성능이 나쁘거나 아예 실패합니다. 최신 서빙 프레임워크는 기동 시 하드웨어를 보고 합리적인 기본값을 스스로 추론합니다. 실무의 노력은 "완벽한 파라미터 값 찾기"가 아니라 "어떤 기법을 적용할지 판단하기"에 써야 합니다.

그래서, 2.7배는 정말 어디서 왔나

여기서 이야기가 한 번 더 꺾입니다. RTX 4070 Ti SUPER(16GB) 환경에서 Qwen3-4B를 대상으로 같은 실험을 재현하고, Nsight Systems로 GPU 안쪽을 열어봤습니다. 결과는 반직관적이었습니다.

# Qwen3-4B, ShareGPT 100 prompts, max-concurrency 8 (프로파일링 오버헤드 포함) base (bf16) AWQ (4bit) 처리 시간 46.9 s 21.5 s 2.2배 빠름 총 처리량 967 TPS 2,105 TPS 평균 TPOT 15.3 ms 7.0 ms # 그런데 GPU가 실제로 계산한 시간은? 총 GPU 커널 실행 시간 ~7.1 s ~7.1 s ← 거의 동일! # 차이가 난 곳 cudaEventSynchronize 비중 85.2% (25.6s) 74.6% (11.5s) 평균 대기 시간 / 호출 13.92 ms 5.67 ms 2.45배 단축

총 커널 시간은 같은데 체감 속도는 2.2배입니다. 그리고 호출 1회당 동기화 대기 시간의 단축(2.45배)이 클라이언트가 측정한 TPOT 개선(2.19배)과 거의 정확히 일치합니다.

즉 "AWQ 행렬곱이 bf16보다 계산량이 적어서 빨라졌다"가 아니라, KV 캐시 여유가 생겨 디코딩 스케줄링이 촘촘해졌고, 한 스텝의 호스트–GPU 왕복 대기가 짧아졌다는 것이 실제 메커니즘입니다. 5장에서 세운 인과 사슬이 커널 레벨에서 그대로 확인된 셈입니다.

그림 8

계산한 시간 vs 기다린 시간

↔ 버튼 + 슬라이더
해볼 것 — ① base와 AWQ 버튼을 번갈아 누르면 초록(커널) 막대는 거의 안 움직이고 자홍(대기) 막대만 줄어드는 걸 보세요. ② 슬라이더로 평균 대기 시간을 직접 낮춰보면 총 시간이 어떻게 따라오는지 알 수 있습니다. ③ 대기를 0에 가깝게 밀면 남는 건 커널 시간뿐 — 이게 이론적 하한입니다.
관찰 포인트

AWQ는 커널 호출 횟수가 더 많습니다(79,165 → 113,169회). 그런데도 총 시간은 짧습니다 — 한 스텝의 턴어라운드가 짧으면 처리량은 오릅니다.

그림에 나오는 말 GPU 커널 실행 시간 GPU가 실제로 계산에 쓴 시간 · 동기화 대기 (cudaEventSynchronize) CPU가 GPU 작업이 끝나기를 기다린 시간 · TPOT Time Per Output Token — 출력 토큰 하나당 평균 시간 · 커널 launch CPU가 GPU에 작업을 밀어 넣은 횟수
커널 구성과 Nsight Compute 결과
커널 유형base (bf16)AWQ (4bit)
GEMM (bf16/f16, cutlass)~75%35.5% — 커널 하나, 2,024회. 양자화되지 않은 레이어(lm_head 등)로 추정
Marlin GEMM (4bit 역양자화+행렬곱)0%~35.6% — 12,240회. 훨씬 잦고 짧은 커널
FlashAttention15.3%15.7% — 거의 동일(어텐션은 양자화 대상이 아님)

AWQ에서도 약 35%가 여전히 bf16 GEMM으로 남습니다. attention projection이나 lm_head 같은 일부 레이어는 원본 정밀도로 남는 경우가 많기 때문입니다. 책 8장(FP8 Qwen2.5-7B) 실습에서 본 것과 같은 패턴입니다.

Nsight Compute 커널 단위 분석에서는 GEMM 커널의 SM 점유율이 bf16과 4bit(Marlin)에서 사실상 동일했습니다(~16.7%). 완전히 다른 구현인데 점유율이 같다는 건 커널 하나하나의 효율은 양자화로 달라지지 않았다는 뜻입니다.

KV 캐시 쓰기 커널도 base/AWQ가 거의 동일했습니다(7.30μs vs 7.85μs, occupancy 65.9% vs 65.8%). AWQ는 가중치만 4비트로 바꾸고 KV 캐시 자체는 그대로이므로 당연한 결과지만, "양자화가 어텐션·캐시 경로에는 영향을 주지 않는다"를 프로파일러로 직접 확인한 셈입니다.

(참고) RTX 4070 16GB 환경 재현 결과
지표ShareGPT (base)ShareGPT (AWQ)Prefix Rep. (base)Prefix Rep. (AWQ)
모델 가중치7.56 GiB2.5 GiB동일동일
KV 캐시 / 토큰4.74 GiB / 34,51210.45 GiB / 76,064동일동일
최대 동시성4.21x9.29x동일동일
총 처리량 (TPS)1,1782,107 (+79%)2,7333,140 (+13%)
평균 TTFT (ms)66.243.587.249.0
평균 ITL (ms)16.07.517.07.2

책의 논리가 소비자용 GPU에서도 그대로 재현됩니다. 다만 AWQ 상태에서는 캐싱의 추가 이득이 작아집니다(+13%). 이미 GPU가 포화에 가까워졌기 때문으로 해석됩니다.

8장 세 줄 요약
  1. 정의 — 노브는 메모리/캐시 계열과 배칭/스케줄링 계열 두 갈래. 하지만 먼저 고를 것은 값이 아니라 기법입니다.
  2. 성질 — prefill 위주면 LMCache 계열, decode 위주면 speculative decoding 계열. 특정 환경에 과적합된 설정은 이식성을 잃습니다.
  3. 쓰임 — 프로파일러로 확인한 결과, 처리량 개선의 실제 원인은 GEMM 가속이 아니라 동기화 대기 시간 단축이었습니다. 인과 사슬을 커널 레벨에서 검증한 것입니다.

한 장으로 할 수 있는 건 다 했습니다. 이제 GPU를 더 붙여봅니다 — 그리고 결론이 뒤집힙니다.

09Step 8 — GPU를 더 붙이면 빨라지는가

한 줄 요약: GPU 개수가 아니라 GPU 사이를 잇는 선이 결정합니다. PCIe만 있으면 한 장이 네 장을 이깁니다.

왜 이걸 배우나
"GPU를 4장 쓰면 당연히 1장보다 빠르지 않나?"

같은 모델·같은 설정으로 두 종류의 서버에서 실험했더니 정반대 결과가 나왔습니다. L40S 4장이 달린 서버에서는 1장이 가장 빨랐고, A100 8장이 달린 서버에서는 4장이 가장 빨랐습니다.

단일 GPU 추론에서는 L40S가 A100보다 효율이 좋은데도 그렇습니다. 이 역전의 이유가 이 장의 핵심입니다.

이 장에 나오는 낯선 말
tensor parallelism (TP, 텐서 병렬)
모델 한 벌을 여러 GPU에 쪼개서 올리고 함께 계산하는 방식. 매 레이어마다 GPU끼리 결과를 주고받아야 합니다.
interconnect (인터커넥트)
GPU와 GPU를 잇는 통신 경로. NVLink는 넓고 빠르고, PCIe는 상대적으로 좁고 느립니다.
vertical scaling (수직 확장)
GPU 여러 장이 모델 하나를 나눠 맡는 것. TP가 여기 속합니다.
horizontal scaling (수평 확장)
GPU마다 독립된 모델 복제본을 하나씩 올리는 것. 앞에 라우터를 두고 나눠 보냅니다.
replica (레플리카)
독립적으로 돌아가는 모델 인스턴스 한 벌.

비유 — 한 문서를 넷이 나눠 쓰기

보고서 한 편을 네 사람이 나눠 씁니다. 넷이 같은 방에 앉아 있으면 한 문단 쓸 때마다 옆 사람에게 말로 확인하면 되니 빠릅니다(NVLink). 그런데 넷이 서로 다른 층에 앉아 있고 메일로만 소통한다면? 한 문단마다 메일을 주고받느라 혼자 쓰는 것보다 느려집니다(PCIe).

텐서 병렬은 매 레이어마다 GPU끼리 결과를 동기화합니다. 그 통신 비용이 병렬화 이득보다 크면 GPU를 늘릴수록 손해입니다.

작은 숫자로 한 번

# 같은 모델(Qwen3-14B-AWQ), 인터커넥트만 다른 두 서버 g6e.12xlarge (L40S ×4, PCIe만) 1 GPU → 처리량 최고, TTFT 최저 ← 단일 GPU가 승리 2·4 GPU → 통신 오버헤드로 오히려 악화 p4d.24xlarge (A100 ×8, NVLink) 4 GPU → 처리량 최고, TTFT 최저 TTFT : 66 ms (1 GPU) → 33 ms (4 GPU) 약 50% 단축 # 그런데 처리량만 놓고 보면? TP=4 로 모델 하나 : 3,926 TPS GPU마다 독립 인스턴스 4개 : 9,816 TPS ← 거의 3배

NVLink가 있는 서버에서조차, 처리량만 노린다면 텐서 병렬보다 단순 복제가 3배 유리합니다. 그렇다면 분산 서빙은 왜 하는 걸까요?

분산 서빙의 진짜 값어치

딱 두 가지입니다. (1) 모델이 GPU 한 장에 안 들어갈 때, (2) 요청 하나의 지연시간 자체를 줄여야 할 때. p4d에서 1→4 GPU로 TTFT가 66ms에서 33ms로 준 것은 레플리카를 아무리 늘려도 얻을 수 없는 텐서 병렬만의 이득입니다. 복제본을 더 놓는 것은 요청 하나가 빨라지게 만들지 못합니다.

그림 9

GPU 수 × 인터커넥트 — 결론이 뒤집히는 지점

↔ 슬라이더 + 버튼
해볼 것 — ① NVLink 상태에서 GPU를 4에서 1로 내려보면 TP 처리량이 떨어지고 TTFT가 두 배가 됩니다. ② PCIe로 바꾸면 같은 조작에서 TP 처리량이 오히려 떨어집니다. ③ 어느 쪽이든 회색 "독립 복제" 막대가 TP 막대보다 훨씬 높다는 걸 확인하세요 — 단, 복제는 TTFT를 못 줄입니다.
대략적인 수치입니다

실측은 p4d TP=4의 3,926 TPS · 독립 4개의 9,816 TPS · TTFT 66→33ms와 "g6e는 1장이 최고"라는 방향뿐입니다. 나머지 중간값은 그 사이를 이은 모형입니다.

그림에 나오는 말 TP (tensor parallelism) 모델 하나를 여러 GPU에 쪼개서 함께 계산하는 방식 · 복제 (replica) GPU마다 독립된 모델을 하나씩 두는 방식 · TPS · TTFT 초당 처리 토큰 수 · 첫 토큰까지의 시간 · NVLink · PCIe GPU와 GPU를 잇는 통신 경로. NVLink가 훨씬 넓고 빠름
수직 확장 vs 수평 확장 — 언제 무엇을

수직 확장(Vertical / TP) — 여러 GPU가 모델 인스턴스 하나를 처리합니다.
장점: 한 장에 안 들어가는 큰 모델을 올릴 수 있고, 요청당 지연시간을 줄일 수 있습니다.
단점: GPU 간 통신 오버헤드, 구성 복잡도 증가.

수평 확장(Horizontal / Replica) — GPU마다 독립 복제본을 둡니다.
장점: 총 처리량이 크고, 한 대가 죽어도 나머지가 삽니다(장애 격리), 구성이 단순합니다.
단점: 요청 하나의 지연시간은 GPU를 늘려도 줄지 않습니다.

# 2장으로 모델을 나눠 올리기
vllm serve Qwen/Qwen3-14B-AWQ --tensor-parallel-size 2 > vllm.log 2>&1 &
# --tensor-parallel-size 2 하나로 vLLM이 분산 그룹 초기화와 모델 분할을 알아서 합니다.

실무 판단 순서는 이렇습니다. (1) 인터커넥트가 NVLink급인가 PCIe뿐인가 → (2) 목표가 처리량인가 지연시간·모델 크기인가. 이 두 질문에 답하기 전에는 "GPU를 늘릴까"를 물을 수 없습니다.

9장 세 줄 요약
  1. 정의 — 텐서 병렬은 모델 하나를 여러 GPU에 쪼개는 수직 확장, 레플리카는 GPU마다 독립 인스턴스를 두는 수평 확장입니다.
  2. 성질 — TP는 매 레이어마다 GPU 간 동기화가 필요합니다. NVLink면 이득이 통신비를 넘고, PCIe뿐이면 넘지 못합니다.
  3. 쓰임 — 처리량이 목표면 수평 확장(최대 3배 유리), 모델이 안 들어가거나 TTFT를 줄여야 하면 수직 확장. 이 판단이 먼저입니다.

여덟 단계가 끝났습니다. 남은 건 이 여정에서 계속 마주쳤던 선택의 갈림길들입니다.

10다섯 가지 트레이드오프 — 정답은 없습니다

한 줄 요약: "완벽한 설정"을 찾는 게 아니라 "어떤 기법을 쓸지" 정하는 게 최적화입니다.

왜 이걸 배우나
"그래서 결국 뭘 켜야 제일 빠른데?"

이 질문에는 답이 없습니다. 그리고 답이 없다는 사실 자체가 답입니다. 앞선 아홉 장에서 우리는 같은 설정이 상황에 따라 이기기도 지기도 하는 장면을 반복해서 봤습니다.

그래서 마지막으로 할 일은 설정을 외우는 게 아니라, 내 제약 조건을 먼저 파악하는 습관을 갖는 것입니다.

이 장에 나오는 낯선 말
overfitting (과적합)
원래 머신러닝 용어지만 여기서는 설정이 특정 하드웨어·트래픽에만 맞아떨어지는 상태를 가리킵니다.
static optimization (정적 최적화)
파라미터를 고정해 두고 쓰는 방식. 예측 가능하지만 상황 변화에 못 따라갑니다.
adaptive serving (적응형 서빙)
실시간 트래픽·하드웨어 지표를 보고 배치 크기·캐시 정책·스케줄링을 스스로 조정하는 방식. 서빙 프레임워크의 다음 단계입니다.
SLO
서비스 수준 목표. "p99 TTFT 300ms 이하" 같은 지켜야 할 선입니다.

비유 — 자전거 기어

자전거에 "가장 좋은 기어"는 없습니다. 오르막에서는 가벼운 기어가, 평지에서는 무거운 기어가 맞습니다. 초보자가 하는 실수는 평지에서 잘 나가던 기어를 오르막에서도 고집하는 것입니다.

서빙 설정도 같습니다. 그리고 "지형"에 해당하는 것이 워크로드 성격, 하드웨어 인터커넥트, 품질 허용 범위입니다.

다섯 가지 갈림길

트레이드오프이 페이지에서 직접 확인한 장면
처리량 ↔ 지연시간1장 그림에서 동시성을 올릴수록 처리량 이득은 줄고 지연 손해는 늘었습니다. 인터랙티브 서비스라면 TTFT·ITL도 같이 챙겨야 합니다.
메모리 효율 ↔ 모델 품질7장 AWQ 4비트로 메모리를 65% 절감하고 처리량 2.7배. 단, 정확도 손실 가능성은 이 실습에서 측정하지 않았습니다.
하드웨어 활용도 ↔ 유연성8장의 "과최적화 금지" 경고. 특정 GPU·트래픽에 딱 맞춘 설정은 다른 곳에서 실패합니다.
수직 확장 ↔ 수평 확장9장에서 PCIe 서버는 분산이 손해였고, NVLink 서버에서도 독립 레플리카 4개가 TP=4보다 처리량 3배였습니다.
정적 최적화 ↔ 적응형 서빙이 페이지의 실습 전체가 고정 파라미터로 돌린 정적 최적화였습니다. 프레임워크는 자동 튜닝 쪽으로 가고 있습니다.
그림 10

내 상황이면 수직인가 수평인가

↔ 슬라이더 2개 + 버튼
해볼 것 — ① 모델 크기를 GPU VRAM보다 크게 밀면 다른 선택지가 사라집니다 — TP는 그때 선택이 아니라 필수입니다. ② 목표를 "처리량"에 두고 모델을 작게 하면 항상 수평 확장이 이깁니다. ③ 목표를 "지연시간"으로 바꾸면 같은 조건에서 답이 뒤집힙니다.
관찰 포인트

같은 하드웨어인데 목표를 바꾸면 답이 바뀝니다. 그래서 "무엇을 올릴 것인가"(1장)를 먼저 정해야 합니다.

그림에 나오는 말 수직 확장 (TP) 여러 GPU가 모델 하나를 나눠 맡는 것 · 수평 확장 (복제) GPU마다 독립 인스턴스를 두고 앞에서 나눠 보내는 것 · VRAM GPU에 붙어 있는 메모리. 모델 가중치와 KV 캐시가 여기를 나눠 씀
10장 세 줄 요약
  1. 정의 — 다섯 갈림길: 처리량↔지연시간, 메모리↔품질, 활용도↔유연성, 수직↔수평, 정적↔적응형.
  2. 성질 — 어느 것도 한쪽이 항상 옳지 않습니다. 워크로드 성격·하드웨어 인터커넥트·품질 허용 범위가 답을 바꿉니다.
  3. 쓰임 — 실무의 노력 대부분은 파라미터 값이 아니라 어떤 기법을 적용할지 결정하는 데 씁니다.

마치며

처음 던진 질문으로 돌아갑니다. 처리량이 올랐다는 건 GPU가 더 빨리 계산해서인가, 그냥 덜 다시 계산해서인가?

답은 후자입니다. 그것도 세 번 확인했습니다. 트래픽만 바꿨는데 2.4배(6장), 양자화했더니 2.7배(7장), 그리고 프로파일러로 열어보니 GPU 커널 실행 시간은 그대로였습니다(8장). 세 실험 모두 같은 함정을 반박합니다 — "빨라졌다 = 연산이 빨라졌다"는 직관은 여기서 통하지 않습니다.

실제로 작동한 경로는 이것이었습니다. 메모리 여유 → KV 캐시 확보 → 배칭 촘촘해짐 → 스케줄링 효율화 → 스텝당 대기 감소. 이 사슬을 손에 쥐고 있으면, 새로운 옵션을 만났을 때 "이건 어느 고리를 건드리는 기법인가"로 판단할 수 있습니다.

두 번째 질문 — GPU를 더 붙이면 무조건 더 빠른가? 아니었습니다. 같은 모델이 PCIe 서버에서는 한 장일 때 가장 빨랐고, NVLink 서버에서는 네 장일 때 가장 빨랐습니다. 그리고 처리량만 본다면 어느 쪽에서도 단순 복제가 분산 서빙보다 낫습니다.

세 번째 질문 — 여기서 찾은 최적 설정이 다른 환경에서도 최적인가? 저자들 스스로 아니라고 말합니다. 그러니 이 페이지에서 가져갈 것은 474나 1,280이 아니라, 그 숫자를 만들고 해석한 방법입니다.

이 페이지는 CH9: LLM Optimization in Practice의 스터디 정리본입니다. 실습 코드와 각 단계의 실행 결과는 orca3/llm-model-inference ch09에 있습니다. 모델은 Qwen3-14B와 Qwen3-14B-AWQ, 부하 도구는 vllm bench serve입니다.

수치 해석이 틀렸거나, 여러분의 환경에서 다른 결과가 나왔다면 알려주세요.

CH9 · LLM Optimization in Practice — LLMSO 스터디 5주차 학습자료
반응형

'AI > [LLMSO] Hands-On LLM 서빙과 최적화 스터디' 카테고리의 다른 글

[6주차-LLMSO] Scaling LLM Inference with vLLM and AWS Trainium (2026.09.06)  (1) 2026.09.13
[5주차-LLMSO]CH10. Advancements in LLM Serving (2026.08.30)  (1) 2026.09.05
[4주차-LLMSO]CH8. LLM Serving Frameworks (2026.08.23)  (0) 2026.08.30
[4주차-LLMSO]CH7. Advanced LLM Optimization Techniques (2026.08.23)  (0) 2026.08.30
[3주차-LLMSO]CH6. Essential LLM Optimization Techniques (2026.08.16)  (0) 2026.08.21
    devlos
    devlos
    안녕하세요, Devlos 입니다. 새로 공부 중인 지식들을 공유하고, 명확히 이해하고자 블로그를 개설했습니다 :) 여러 DEVELOPER 분들과 자유롭게 지식을 공유하고 싶어요! 방문해 주셔서 감사합니다 😀 - DEVLOS -

    티스토리툴바