📌 Notice
LLM Serving and Optimization Study (=LLMSO)
직접 실습을 통해 LLM 서비스 제공 및 최적화에 대한 지식에 대한 학습 및 실습 내용을 정리한 블로그입니다.
CloudNet@에서 스터디를 진행하고 있습니다.
EKS 관련 이전 스터디 내용은 아래 링크를 통해 확인할 수 있습니다.
LLM Serving 최적화를 공부하다 보면 Prefix Caching, AWQ, Speculative Decoding,
Tensor Parallel처럼 바로 적용해 보고 싶은 이름이 먼저 보입니다.
하지만 같은 기법도 Prompt 길이와 반복성, 동시 요청 수, GPU Memory와 Interconnect에 따라 결과가 달라집니다.
그래서 이번 장의 시작점은 설정이 아니라 질문입니다.
사용자가 기다리는 첫 Token을 줄이고 싶은지,
한 GPU가 처리하는 전체 Token을 늘리고 싶은지,
더 큰 Model을 Memory 안에 넣고 싶은지부터 구분해야 합니다.

최적화 실험은 다음 순서로 돌아갑니다.
이 글의 원본에는 Qwen3-14B와 vLLM을 사용한 과거 실행 결과가 남아 있습니다.
다만 Model revision, Runtime version, 원본 Result JSON이 완전하지 않고 서로 다른 Hardware 기록도 섞여 있습니다.
이번 작성 과정에서는 GPU Benchmark를 다시 실행하지 않았으므로 해당 숫자는 Historical로만 읽습니다.
현재 Checkout에서 직접 실행한 것은 원문 숫자의 산술 일관성 검사입니다.
수치가 맞게 나누어지는지는 확인할 수 있지만,
그 수치가 지금도 재현되는지나 특정 최적화가 원인인지는 확인할 수 없습니다.
앞으로 Hardware와 Traffic 계약을 고정하고, 지표를 읽고, Baseline Memory를 해석한 뒤 Prefix Cache와 Quantization을 살펴봅니다.
마지막에는 Workload별 튜닝과 Profiling, Tensor Parallel과 Replica 선택까지 같은 측정 기준으로 연결합니다.
GPU 사용률이 낮다는 사실만 보고 어떤 설정을 바꿔야 할까?
아직은 아무 설정도 바꾸면 안 됩니다.
GPU 사용률은 관찰값이고,
사용자가 겪는 문제가 첫 Token 대기인지 Decode 속도인지 낮은 처리량인지부터 정해야 합니다.
성능을 높인다는 목표는 실험을 설계하기에 너무 넓습니다.
다음처럼 대상과 부하, 지표를 함께 적어야 비교가 가능합니다.
p95 TTFT를 줄입니다.Output tok/s를 높입니다.Request당 비용을 낮춥니다.여기서 중요한 것은 평균값 하나가 아니라 통과 조건입니다.
예를 들어 대화형 서비스라면 다음처럼 적을 수 있습니다.
p95 TTFT <= 1.5 s
p95 TPOT <= 40 ms/token
request error rate < 1%
위 조건을 지키는 범위에서 Output tok/s를 최대화한다.
이렇게 쓰면 Batch를 키워 TPS가 올랐더라도 p95 TTFT가 1.5초를 넘는 순간 채택하지 않을 수 있습니다.
반대로 Offline 요약 작업이라면 TTFT보다 완료 시간과 GPU 시간당 처리 Token이 더 중요할 수 있습니다.
처리량과 지연시간은 서로 독립된 점수가 아닙니다.
Batch를 크게 만들면 GPU 효율과 전체 처리량은 좋아질 수 있지만,
Queue에서 기다리는 시간이 늘어 TTFT가 나빠질 수 있습니다.
같은 느리다는 말도 실제로는 서로 다른 문제를 가리킵니다.
| 사용자가 보는 증상 | 먼저 나눠 볼 구간 | 의심할 자원이나 정책 |
|---|---|---|
| 첫 글자가 늦게 나타남 | Queue, Tokenization, Prefill | Arrival burst, 긴 Prompt, CPU 준비, Cache miss |
| 글자는 바로 나오지만 생성이 느림 | Decode | 작은 Batch, Memory bandwidth, 동기화 |
| 평소에는 빠르지만 몰릴 때 급격히 느려짐 | Queue와 Scheduler | Admission, Batch budget, KV 부족 |
| Server는 바쁜데 완료 요청이 적음 | 전체 경로 | 긴 Output, 실패와 Retry, 비효율적인 Kernel |
GPU Utilization이 낮다는 이유만으로 Batch를 키우면 첫 번째와 세 번째 증상을 더 악화시킬 수 있습니다.
먼저 어떤 시간 구간을 줄일지 정한 뒤,
그 구간과 연결되는 설정만 후보로 올립니다.
원본은 AWS g6e.2xlarge의 L40S를 설명하는 문장과 RTX 4070 16GB 출력이 같은 구간에 섞여 있습니다.
이 둘을 하나의 실험 환경으로 합치면 안 됩니다.
실험 기록에는 적어도 다음 항목이 필요합니다.
Hardware 사양표는 최대 성능을 말해 주지만 현재 Run의 상태를 보장하지 않습니다.
같은 GPU라도 Power limit과 Clock,
다른 Process의 Memory 점유,
PCIe 배치와 온도에 따라 결과가 달라집니다.
nvidia-smi는 이 Preflight를 시작하기에 좋은 도구입니다.
하지만 순간적인 GPU Utilization과 Memory 사용량만으로 Scheduler 대기, Memory bandwidth, Kernel 병목을 구분할 수는 없습니다.
nvidia-smi --query-gpu=name,uuid,memory.total,driver_version,power.limit,temperature.gpu --format=csv
nvidia-smi topo -m
위 명령은 실행 계획 예시이며 이번 작성 과정에서는 실행하지 않았습니다.
Cloud GPU에서 실행한다면 Account, Region, Instance ID, Owner, TTL과 종료 확인까지 함께 남겨야 합니다.
M1 Air 실습에서는 이 목록을 Apple Silicon에 맞게 바꿉니다.
Chip과 16GB Unified Memory,
macOS와 vLLM-Metal·MLX Version,
전원·저전력 모드,
Memory Pressure와 동시에 실행 중인 App을 기록합니다.
M1의 숫자를 NVIDIA 결과에 이어 붙이지 않습니다.
같은 질문과 Traffic을 사용해 학습할 수는 있지만,
Metal과 CUDA는 Runtime과 Kernel 경로가 다른 별도의 Hardware 계약입니다.
최적화의 입력은 Model 설정이 아니라 SLO, Workload, Hardware 계약입니다.
이 셋이 고정되지 않으면 빠른 숫자를 얻을 수는 있어도,
어떤 변경이 왜 효과가 있었는지는 설명할 수 없습니다.
공개 대화 Dataset으로 높은 TPS가 나왔다면 실제 서비스도 같은 속도일까?
그렇게 말할 수 없습니다.
Serving Runtime이 받는 부하는 Prompt 내용보다 Token 길이, 요청 도착 간격, 동시성, 반복 Prefix와 Output 길이에 직접 영향을 받습니다.

예를 들어 고객 문의 Chat은 짧은 질문과 중간 길이 답변이 반복될 수 있습니다.
RAG는 검색 문서 때문에 Input이 길어지고,
보고서 생성은 Output이 길어질 수 있습니다.
Agent 요청은 짧은 응답 여러 번과 Tool 호출이 이어집니다.
이 네 가지를 평균 Prompt 500 Token이라는 한 숫자로 합치면 어떤 단계가 바쁜지 사라집니다.
Input과 Output 길이의 p50·p95,
요청 종류별 비율을 따로 남겨야 합니다.
| 항목 | 왜 필요한가 |
|---|---|
| Input·Output Token 분포 | Prefill과 Decode의 계산 비중이 달라집니다. |
| Arrival rate와 Burst | 같은 평균 RPS라도 Queue가 달라집니다. |
| Max concurrency | Runtime이 동시에 묶을 수 있는 요청 수를 제한합니다. |
| Prefix uniqueness | Cache hit 가능성과 재사용 범위를 바꿉니다. |
| Sampling과 Stop 조건 | 실제 생성 Token 수와 실행 시간을 바꿉니다. |
| Request 순서와 Seed | 반복 실행의 비교 가능성을 만듭니다. |
평균 길이가 같아도 분포가 다르면 Scheduler가 보는 부하는 달라집니다.
모든 요청이 Input 500 Token인 경우와,
대부분 100 Token이지만 일부가 4,100 Token인 경우는 평균이 비슷해도 긴 요청이 Batch와 Tail latency에 미치는 영향이 다릅니다.
Request rate는 새 요청이 얼마나 자주 도착하는지를 말합니다.
Concurrency는 동시에 처리 중이거나 기다리는 요청의 수입니다.
한 요청이 10초 걸리는 상황에서 초당 1건이 계속 도착하면 동시 요청은 자연스럽게 쌓입니다.
반대로 Client가 이전 요청이 끝날 때까지 기다리는 Closed-loop 방식은 Server가 느려질수록 새 요청도 덜 보내므로 과부하 구간을 감출 수 있습니다.
실서비스의 Arrival을 재현하려면 Open-loop와 Closed-loop 중 무엇을 사용했는지 기록합니다.
평균 RPS가 같더라도 Burst가 있으면 Queue와 p99 TTFT가 완전히 달라질 수 있습니다.
원본은 자연 대화형 ShareGPT와 동일한 긴 Prefix를 반복하는 Synthetic Traffic을 사용합니다.
두 Dataset은 서로 다른 질문에 답합니다.
ShareGPT는 길이와 대화 형태가 섞인 공개 Proxy이고,
Prefix Repetition은 Cache reuse가 많은 경로를 의도적으로 강조합니다.
원본에 기록된 100개 ShareGPT Sample은 Prompt 평균 232.6 Token,
Output 평균 220.61 Token입니다.
이 숫자는 수집된 Sample의 Historical 통계일 뿐 전체 Dataset이나 현재 서비스 Traffic의 분포를 대표하지 않습니다.
둘 사이의 TPS 차이를 보려면 요청 수, Request rate, Concurrency, Token 길이를 맞춰야 합니다.
원본의 Historical 기록은 ShareGPT 2,000건·10 RPS와 Prefix Repetition 1,000건·5 RPS를 비교하므로,
기록된 약 2.37배를 Prefix Cache의 순수 효과라고 부를 수 없습니다.
공개 Dataset도 그대로 Production Benchmark가 되지는 않습니다.
License와 원본 URL, Download 시각, Checksum을 남기고 개인정보와 외부 지시문을 제거해야 합니다.
CH09 Raw에는 대규모 ShareGPT Sample이 복사되어 있고 일부 Prompt에 이름, 연락처, URL 같은 정보가 섞여 있습니다.
이 글에는 Sample 문장을 복제하지 않습니다.
실습에서도 원본을 바로 사용하지 않고,
승인된 Fixture로 Sanitization한 뒤 Hash를 고정합니다.
실습 Fixture는 원문을 많이 모으는 것보다 질문을 분리하는 편이 좋습니다.
balanced-chat: Input과 Output이 비슷한 대화형 요청long-prefill: 긴 Input과 짧은 Outputlong-decode: 짧은 Input과 긴 Outputshared-prefix: 긴 System Prompt가 같고 사용자 질문만 다름먼저 작은 Fixture로 결과 방향을 확인하고,
그다음 실제 Length Histogram을 반영한 큰 Dataset으로 Saturation curve를 측정합니다.
긴 Prefix를 공유하는 요청이 빨라졌다면 먼저 Cache reuse 기회가 있는 Workload에서 관찰된 결과라고 적습니다.
그다음 Runtime의 Cache hit와 eviction을 확인해야 실제 재사용을 설명할 수 있습니다.
같은 Dataset을 여러 번 실행하는 것 자체가 Prefix Cache를 데울 수도 있습니다.
Cache 효과를 의도하지 않았다면 Server를 재시작하거나 Cache를 초기화하고,
Seed와 실행 순서를 바꿨는지 함께 남깁니다.
Benchmark Dataset은 성능 측정의 배경 자료가 아닙니다.
어떤 최적화가 유리해지는지를 결정하는 실험 변수입니다.
Total TPS가 올랐는데 사용자가 더 오래 기다릴 수도 있을까?
가능합니다.
처리량은 시스템 전체가 처리한 양을 말하고,
Latency는 개별 요청이 기다린 시간을 말하기 때문입니다.
요청이 서버에 도착한 뒤 첫 Token을 받을 때까지의 시간이 TTFT(Time to First Token)입니다.
여기에는 Network, Queue, Tokenization, Scheduling, Prefill과 첫 Sampling이 함께 들어갈 수 있습니다.
따라서 TTFT를 Prefill Kernel 시간과 같은 값으로 두면 안 됩니다.
첫 Token 이후 생성 Token 사이의 간격은 ITL(Inter-Token Latency)로 볼 수 있습니다.
여러 Output Token의 간격을 요약한 TPOT(Time per Output Token)은 Decode 체감 속도를 설명하는 데 유용합니다.
마지막 Token까지 걸린 E2E Latency는 Queue부터 생성 완료까지 전체 시간을 담습니다.
표준 Decode에서 요청이 출력 Token N개를 만들었다면 다음 관계로 이해할 수 있습니다.
E2E latency ≈ TTFT + (N - 1) × TPOT
예를 들어 TTFT가 100ms이고 출력 100개의 TPOT가 20ms라면,
대략적인 E2E는 100 + 99 × 20 = 2,080ms입니다.
TTFT가 50ms 줄어도 긴 답변의 TPOT가 5ms 늘면 전체 완료 시간은 오히려 길어질 수 있습니다.
그래서 첫 반응과 이후 생성 속도를 함께 봅니다.
ITL과 TPOT도 항상 같지는 않습니다.
ITL은 실제 Streaming Chunk 사이의 간격을 모으고,
TPOT는 첫 Token 이후 시간을 출력 Token 수로 나눕니다.
Speculative Decoding처럼 한 Chunk에 여러 Token이 담기면 두 지표의 의미가 더 달라집니다.
Request/s: 단위 시간에 완료된 요청 수입니다.Input tok/s: Prefill에 들어간 Token 처리량입니다.Output tok/s: 사용자에게 생성한 Token 처리량입니다.Total tok/s: Input과 Output을 합한 값입니다.Total TPS는 긴 Input을 많이 읽은 실험에서 높아질 수 있습니다.
Chat 응답의 생성 속도를 비교할 때는 Output TPS와 TPOT를 함께 봐야 합니다.
request_throughput = completed_requests / duration
input_tps = input_tokens / duration
output_tps = output_tokens / duration
total_tps = (input_tokens + output_tokens) / duration
Input이 아주 긴 Benchmark는 Model이 답변을 빠르게 생성하지 못해도 Total TPS가 크게 보일 수 있습니다.
반대로 Output TPS가 높아도 실패한 요청을 제외한 성공 요청만 계산했다면 실제 서비스 Capacity를 과대평가할 수 있습니다.
SLO를 지킨 처리량을 별도로 보고 싶다면 Goodput 관점이 유용합니다.
goodput = SLO를 모두 만족한 완료 요청 수 / duration
TPS는 Hardware가 얼마나 일했는지를 보여 주고,
Goodput은 그중 사용 가능한 결과가 얼마나 되었는지를 보여 줍니다.
평균 TTFT가 같아도 일부 요청이 Queue에서 오래 기다릴 수 있습니다.
그래서 p50, p95, p99와 최대값을 보고,
성공·실패·Timeout·OOM 건수를 같은 표에 넣습니다.
p99는 요청 100개 중 하나가 느리다는 단순한 뜻으로만 읽으면 안 됩니다.
Sample이 작으면 Percentile이 몇 개의 요청에 크게 흔들리고,
긴 Prompt나 특정 Tenant가 Tail을 독점할 수도 있습니다.
전체 Percentile을 본 뒤 Input 길이와 요청 종류,
Cache hit 여부로 다시 나누어야 느린 집단을 찾을 수 있습니다.
Warm-up, Compile, Cold Cache를 측정 시간에 포함했는지도 적어야 합니다.
Profiler를 붙인 실행은 Overhead가 생기므로 일반 Benchmark와 같은 표에서 직접 비교하지 않습니다.
Client가 측정한 TTFT와 Server 내부 Prefill 시간도 구분합니다.
Client TTFT에는 HTTP와 Queue가 들어가므로 사용자 경험을 보여 주지만,
Kernel 변경의 효과를 설명하려면 Server Timeline이 추가로 필요합니다.

좋은 결과는 단일 숫자가 아니라 작은 계약입니다.
예를 들어 Output TPS는 증가했고 p95 TTFT와 오류율은 SLO 안이며 Quality Gate를 통과했다처럼 적습니다.
이 계약이 있어야 처리량을 위해 Tail latency나 품질을 몰래 희생하지 않습니다.
Model이 GPU에 올라갔는데 남은 Memory는 왜 중요한가?
LLM Serving의 GPU Memory는 Weight만 담지 않습니다.
KV Cache, Activation과 Workspace, CUDA Graph, Runtime 여유도 같은 공간을 나누어 사용합니다.
Memory Budget은 다음처럼 생각할 수 있습니다.
available device memory
= model weights
+ KV cache
+ activations and temporary workspace
+ runtime and graph reserve
+ safety headroom
Weight만 겨우 들어간 상태는 실행 가능일 수는 있어도 서빙 가능하다고 보기 어렵습니다.
새 요청이 들어올 때 KV Block을 확보하지 못하면 Preemption이나 Recompute가 늘고,
결국 Tail latency와 처리량이 함께 흔들릴 수 있습니다.
Model Weight는 요청이 없어도 비교적 고정된 Memory를 차지합니다.
반면 KV Cache는 활성 Sequence의 길이와 수에 따라 늘어납니다.
따라서 Weight가 GPU를 거의 채우면 Server가 시작되더라도 긴 Context나 많은 동시 요청을 수용하기 어렵습니다.
한 Token의 KV 크기는 Model 구조와 dtype에 따라 달라집니다.
개념적으로는 다음 항목의 곱으로 증가합니다.
KV bytes per token
≈ layers × 2(K와 V) × kv_heads × head_dim × bytes_per_element
Multi-Head Attention보다 KV Head가 적은 GQA와 MQA는 Token당 KV 크기를 줄일 수 있습니다.
하지만 정확한 용량은 Sliding Window와 KV dtype,
Block 크기와 Runtime 구현까지 포함해 Server가 출력한 값으로 확인해야 합니다.
원본의 Baseline 기록은 Model Weight와 KV Cache 크기, KV Token capacity를 함께 남깁니다.
이런 정보는 Throughput 숫자보다 먼저 봐야 합니다.
Runtime이 요청을 Batch로 묶으려 해도 KV Block을 확보하지 못하면 더 많은 Sequence를 실행할 수 없기 때문입니다.
이론적인 동시성 상한은 다음처럼 생각할 수 있습니다.
theoretical_concurrency ≈ floor(kv_token_capacity / tokens_per_active_request)
예를 들어 KV Token capacity가 72,064이고 활성 요청 하나가 Input과 생성 중인 Output을 합쳐 2,048 Token을 유지한다면,
산술상 상한은 floor(72,064 / 2,048) = 35입니다.
실제 요청 길이가 모두 2,048로 고정되지 않고 Runtime 여유와 Scheduling이 필요하므로,
이 값을 동시 사용자 35명 보장이라고 쓰면 안 됩니다.
이 값은 상한일 뿐 실제 동시성이 아닙니다.
Block Fragmentation, Scheduler 정책, 긴 Prompt 편차, 다른 Runtime Memory와 Request 도착률이 실제 값을 더 낮출 수 있습니다.
| 영역 | 기록할 값 |
|---|---|
| Model | 정확한 revision, dtype, quantization, chat template |
| Runtime | vLLM·PyTorch·CUDA·Driver·Container version |
| Memory | Weight, KV Cache, Runtime reserve, Peak usage |
| Traffic | Dataset checksum, length 분포, rate, concurrency |
| 결과 | TPS, TTFT·TPOT·E2E 분포, 오류와 OOM |
Baseline은 아무 최적화도 하지 않은 느린 상태가 아닙니다.
다음 변경이 무엇을 바꾸었는지 판단할 수 있게 만드는 기준선입니다.
서버 로그에 Memory가 출력되었다는 사실과 그 Memory가 실제로 얼마나 사용되었는지도 구분해야 합니다.
할당 가능한 KV capacity가 커도 부하가 작으면 Batch와 Throughput은 늘지 않습니다.
M1 Air에서는 GPU 전용 VRAM 대신 CPU와 GPU가 16GB Unified Memory를 함께 사용합니다.
Model Process가 8GB를 사용한다는 이유만으로 나머지 8GB를 KV Cache에 쓸 수 있는 것은 아닙니다.
macOS와 Browser,
IDE도 같은 Memory Pool을 사용하므로 Activity Monitor의 Memory Pressure와 Swap을 함께 봅니다.
이 차이는 Memory 식을 버려야 한다는 뜻이 아닙니다.
GPU Memory Budget을 장치 전체 Unified Memory Budget으로 바꾸고,
OS Headroom까지 실험 계약에 넣어야 한다는 뜻입니다.
같은 긴 System Prompt를 반복한다면 매 요청마다 Prefill을 다시 해야 할까?
Tokenized Prefix와 Model 상태가 동일하고 Cache Entry가 살아 있다면,
이미 계산한 Prefix KV Block을 재사용할 여지가 있습니다.
뒤에 붙는 사용자 Suffix만 새로 Prefill하면 되므로 TTFT와 Prefill 계산량을 줄일 수 있습니다.
하지만 문장이 비슷하다는 조건만으로 Cache hit가 되지는 않습니다.
Tokenizer, Chat template, Model revision, Adapter, Cache key와 Block boundary가 실제로 일치해야 합니다.
다음 두 요청은 System Prompt와 앞 대화가 같고 마지막 질문만 다릅니다.
Request A = [system + document + history] + [question A]
Request B = [system + document + history] + [question B]
Request A를 처리하면서 공통 구간의 KV Block을 보관했다면,
Request B는 그 Block 뒤의 질문만 새로 Prefill할 수 있습니다.
여기서 줄어드는 것은 공통 Prefix의 Prefill 계산입니다.
Request B가 답변을 200 Token 생성한다면 그 Decode는 여전히 수행해야 하므로,
Prefix Cache가 긴 Output의 TPOT까지 같은 비율로 줄여 주는 것은 아닙니다.
vLLM은 Token Block과 앞선 Prefix의 Hash를 연결해 Cache Block을 찾습니다.
중간의 Token 하나가 달라지면 이후 Block의 연결도 달라질 수 있습니다.
사람이 보기에 같은 문장이라도 Chat template의 공백이나 Special Token,
Adapter와 Model revision이 다르면 같은 Cache Entry로 취급하면 안 됩니다.

원본은 ShareGPT 부하에서 Total 474.38 tok/s,
긴 Prefix를 반복하는 부하에서 Total 1,123.13 tok/s를 기록합니다.
산술상 약 2.368배입니다.
이 비율은 이번 작성 과정에서 재현한 Benchmark가 아닙니다.
두 실행은 요청 수와 Request rate가 다르고,
원본 Result JSON과 Cache hit trace도 남아 있지 않습니다.
따라서 Prefix 반복 부하에서 더 높은 기록이 있었다까지만 말할 수 있습니다.
2.368배라는 숫자에는 Cache 외에도 요청 수와 도착률,
길이 분포와 Server의 Warm 상태가 함께 들어 있습니다.
비율 계산이 맞는 것과 원인 설명이 맞는 것은 별개의 검증입니다.
Cold와 Warm도 한 단어로 끝내지 않습니다.
cold server: Process를 새로 시작해 Model과 Runtime 준비부터 포함합니다.warm model, empty prefix cache: Model 준비는 끝났지만 비교할 Prefix는 없습니다.warm prefix: 같은 Prefix Block이 Cache에 남아 있습니다.eviction run: 다른 요청으로 Cache를 채운 뒤 재사용 가능성을 다시 봅니다.이 상태들이 섞이면 첫 실행의 Compile 비용을 Cache miss로 잘못 해석하거나,
이전 Benchmark가 남긴 Prefix를 새로운 최적화 효과로 볼 수 있습니다.
Prefix Cache는 반복 요청이 많을수록 무조건 좋은 전역 설정이 아닙니다.
재사용이 거의 없으면 Hash lookup과 Memory 점유만 늘 수 있고,
긴 Prefix가 자주 바뀌면 Cache Entry가 빠르게 밀려날 수 있습니다.
Hit rate만 높아도 충분하지 않습니다.
짧은 Prefix 여러 개가 Hit한 경우보다 아주 긴 Prefix 하나를 재사용한 경우가 더 많은 계산을 아낄 수 있습니다.
그래서 Hit query 수와 함께 재사용한 Token 수,
KV Block의 Lifetime과 Eviction,
TTFT 분포를 연결해 봅니다.
M1 실습에서는 Server를 재시작한 뒤 unique와 shared Fixture를 각각 실행합니다.
두 Client 결과의 TTFT 차이를 먼저 보고,
Server Metric이나 Log에서 실제 Cache Hit가 있었는지 확인한 뒤에만 원인을 붙입니다.
결국 질문은 Prefix Cache를 켰는가가 아니라,
우리 Traffic에서 재사용된 Token이 얼마이고 그 대가로 어떤 Memory와 운영 비용을 썼는가입니다.
Weight가 작아지면 Model 계산도 같은 비율로 빨라질까?
반드시 그렇지는 않습니다.
낮은 Bit Weight를 읽는 Memory bandwidth는 줄 수 있지만,
지원 Kernel과 Dequantization 경로, Batch 크기, Hardware에 따라 실제 연산 시간은 달라집니다.

Quantization은 모든 숫자를 한꺼번에 줄이는 기능이 아닙니다.
Weight만 4bit로 저장하고 계산 시 더 높은 정밀도로 풀어 쓰는 방식이 있고,
Activation이나 KV Cache까지 낮은 정밀도로 다루는 방식도 있습니다.
따라서 4bit Model이라는 이름만으로 Weight와 Activation,
KV Cache가 모두 4bit라고 가정하면 안 됩니다.
Artifact 형식과 실제 Kernel이 무엇을 양자화하는지 확인해야 합니다.
14B Parameter를 단순 계산하면 BF16 Weight는 14B × 2 byte ≈ 28GB,
4bit Weight는 이상적으로 14B × 0.5 byte ≈ 7GB입니다.
실제 Artifact에는 Scale과 Group Metadata,
양자화하지 않은 Layer와 Runtime Buffer가 있어 이론값과 달라집니다.
원본의 Qwen3-14B Historical 기록을 산술로 다시 계산하면 다음과 같습니다.
| 항목 | Baseline | AWQ | 계산상 변화 |
|---|---|---|---|
| Weight Memory | 27.5185 GiB | 9.3619 GiB | 65.98% 감소 |
| KV Cache | 11.00 GiB | 29.15 GiB | 18.15 GiB 증가 |
| KV Token capacity | 72,064 | 191,056 | 2.651배 |
| Weight + KV | 38.5185 GiB | 38.5119 GiB | 거의 동일 |
여기서 직접 관찰된 것은 기록된 Memory 배분과 Token capacity입니다.
Weight가 줄어 남은 공간을 Runtime이 KV Cache로 사용했다는 해석은 표와 맞습니다.
Weight와 KV의 합이 두 구성에서 약 38.5GiB로 비슷하다는 점이 중요합니다.
양자화로 확보한 Memory를 비워 둔 것이 아니라,
Runtime이 더 많은 KV Token을 담는 Capacity로 재배치한 기록이기 때문입니다.
하지만 KV capacity가 2.651배가 되었다고 Throughput이 자동으로 2.651배가 되는 것은 아닙니다.
요청이 충분해야 Batch가 커지고,
Scheduler와 Kernel이 그 Batch를 효율적으로 처리해야 합니다.
원본은 AWQ 실행에서 Total 1,280 tok/s를 기록하고 이전 474.38 tok/s와 비교합니다.
산술상 약 2.70배이지만 정확한 Model revision, Traffic 결과 파일, Runtime version과 Quality 결과가 없습니다.
그러므로 이 숫자를 AWQ의 보편적 속도 향상이나 KV Cache의 순수 효과로 사용하지 않습니다.
낮은 Bit Weight는 Device Memory에서 읽어야 할 Byte를 줄일 수 있습니다.
Decode가 Weight를 반복해서 읽는 Memory-bandwidth-bound 구간이라면 이 변화가 도움이 될 수 있습니다.
반면 Hardware에 맞는 Kernel이 없거나 Dequantization과 Packing 비용이 크면 Latency가 나빠질 수 있습니다.
작은 Batch에서는 준비 비용이 더 크게 보이고,
큰 Batch에서는 다른 연산이 병목이 될 수 있습니다.
따라서 확인 순서는 다음과 같습니다.
Quantization을 채택하려면 성능 표와 별도로 Quality Gate가 필요합니다.
업무별 Golden set, 한국어 표현, 긴 Context, Tool call과 Structured output을 같은 Sampling 조건에서 비교합니다.
성능 Gate에서는 TTFT, TPOT, Output TPS, GPU Memory, Power와 Cost를 봅니다.
품질과 성능을 모두 통과해야 더 작은 Model Artifact가 더 나은 Serving 구성이 됩니다.
Quality Gate는 일반적인 Benchmark 점수 하나보다 서비스 실패 모양을 직접 포함해야 합니다.
한국어 조사와 숫자 보존,
JSON Schema,
Tool 이름과 Argument,
긴 문서의 핵심 사실을 Golden set으로 고정할 수 있습니다.
M1 Air 실습은 0.5B Model의 BF16과 4bit를 비교합니다.
이 실습의 목적은 큰 Model을 Laptop에 넣었다고 과장하는 것이 아니라,
같은 16GB Unified Memory 안에서 Artifact 크기와 Runtime Memory,
Latency와 품질이 어떻게 함께 바뀌는지 관찰하는 것입니다.
GPU 사용률이 낮으면 Speculative Decoding을 켜면 될까?
먼저 낮은 사용률이 어떤 구간에서 생겼는지 확인해야 합니다.
긴 Prompt를 처리하는 Prefill과 한 Token씩 반복하는 Decode는 계산 모양이 다르고,
Queue·Cache·CPU 준비 시간도 GPU가 쉬어 보이는 구간을 만들 수 있습니다.
설정 이름보다 먼저 요청의 시간축에 표시합니다.
arrival → queue → tokenize → prefill → first token → decode steps → complete
TTFT만 높고 긴 Input 요청이 Tail을 차지한다면 Prefill 쪽 가설이 먼저입니다.
TTFT는 안정적인데 TPOT가 높다면 Decode와 Memory bandwidth를 봅니다.
Queue가 늘면서 GPU에 빈 구간이 보이면 CPU 준비와 Scheduler,
작은 Batch와 동기화를 의심할 수 있습니다.
KV 사용률과 Preemption이 함께 증가한다면 계산 Kernel보다 Memory Budget이 먼저일 수 있습니다.
긴 문서와 긴 System Prompt가 많다면 Prefill 비용과 TTFT가 먼저 보입니다.
Chunked Prefill은 긴 Prefill 작업을 나누어 Decode와 섞을 여지를 만들고,
공유 Prefix가 많다면 Prefix Cache나 외부 KV Cache 계층을 검토할 수 있습니다.
이때 목표는 Prefill을 없앤다가 아닙니다.
Decode 요청이 긴 Prefill 뒤에서 굶지 않게 하고,
실제로 재사용 가능한 Prefix만 저장하는 것입니다.
Chunked Prefill은 긴 Prompt를 여러 Token Chunk로 나누어 Decode 요청 사이에 배치할 수 있게 합니다.
Chunk가 작으면 Decode가 오래 기다리지 않아 ITL에 유리할 수 있지만,
너무 작으면 Scheduling과 Kernel 호출 횟수가 늘 수 있습니다.
반대로 큰 Token Budget은 Prefill을 빨리 밀어낼 수 있지만 진행 중인 Decode를 오래 방해할 수 있습니다.
max_num_batched_tokens는 큰 값과 작은 값 중 하나가 정답인 설정이 아니라,
TTFT와 ITL 사이에서 Workload에 맞는 지점을 찾는 실험 변수입니다.
Output이 길고 작은 Batch Decode가 반복된다면 Memory bandwidth와 순차 Iteration 수가 중요해질 수 있습니다.
Speculative Decoding은 작은 Draft가 여러 Token을 제안하고 Target이 검증하여,
비싼 Target Step의 수를 줄일 가능성을 만듭니다.
예를 들어 Draft가 네 Token을 제안하더라도 Target이 첫 Token만 받아들이면 나머지는 폐기됩니다.
한 번의 Target 검증에서 평균 몇 Token이 확정되었는지가 실제 이득을 결정합니다.
accepted tokens per target verification
= accepted draft tokens + target이 보장하는 다음 token
Draft 자체의 Weight와 KV Cache도 Memory를 사용합니다.
Target이 이미 Memory 한계에 가까우면 순차 Step은 줄어도 동시 요청 수가 줄 수 있습니다.
Acceptance가 낮으면 Draft와 검증, 폐기 비용이 이득을 넘을 수 있습니다.
따라서 Draft Model 이름보다 Acceptance rate, 확정 Token 수, TTFT·TPOT 변화와 추가 Memory를 함께 측정합니다.
원인을 찾을 때는 도구를 거꾸로 사용하지 않습니다.
이 순서를 지키면 Profiler에서 가장 큰 막대를 바로 Root cause라고 부르는 실수를 줄일 수 있습니다.
큰 Kernel은 원래 많은 일을 담당해서 클 수 있고,
CPU Synchronization은 앞선 GPU 작업을 기다리는 결과로 길어질 수도 있습니다.
관찰한 시간 비중과 원인 가설,
변경 후 일반 Run에서 개선된 SLO를 세 단계로 연결해야 합니다.
Profiler를 붙인 실행은 일반 실행보다 느릴 수 있습니다.
원본의 RTX 4070·Nsight 기록도 Report Artifact와 전체 명령이 없어 Historical 단서로만 남깁니다.
특정 Kernel 시간이 줄었다는 기록을 Qwen3-14B AWQ 결과의 원인으로 확장하지 않습니다.
| 관찰 | 가능한 가설 | 다음 증거 |
|---|---|---|
| TTFT p99만 상승 | Queue burst, 긴 Prefill, Cache miss | Request timeline, length 분포, Cache metric |
| GPU Utilization이 톱니 모양 | CPU 준비, 작은 Batch, 동기화 | Nsight Systems Timeline |
| Weight Memory 감소 | 더 큰 KV Budget 가능 | Runtime Memory log, KV capacity |
| TP에서 TPS 정체 | Collective 비용 가능 | Topology, NCCL trace, GPU별 timeline |
M1 Air에서는 Nsight와 CUDA Timeline을 수집하지 않습니다.
먼저 Loopback Client의 TTFT·E2E·Output TPS와 Process Memory,
Memory Pressure를 기록합니다.
이 증거로 Metal Kernel의 병목을 확정할 수는 없지만,
Prefix와 Quantization 변경이 사용자 관찰 지표를 바꾸었는지는 확인할 수 있습니다.
원인이 Apple GPU 내부에 있다고 주장하려면 Metal에 맞는 별도의 Profiler Evidence가 필요합니다.
nvidia-smi는 어디를 볼지 알려 주는 계기입니다.
원인을 확정하는 마지막 증거는 아닙니다.
GPU 네 장이면 한 장보다 네 배 빨라질까?
GPU 수만으로는 답할 수 없습니다.
한 요청의 Weight와 연산을 나누는 Tensor Parallel과,
Model을 여러 번 복제해 독립 요청을 처리하는 Horizontal Scaling은 다른 문제를 풉니다.

Tensor Parallel은 한 Layer의 Weight와 연산을 여러 GPU에 나눕니다.
Model이 한 GPU Memory에 들어가지 않거나 한 요청의 Service time을 줄여야 할 때 후보가 됩니다.
두 GPU로 단순화하면 같은 Input Activation을 받아 각 GPU가 자신이 가진 Weight 조각을 계산합니다.
그다음 Partial Result를 합쳐 다음 Layer가 사용할 하나의 결과를 만듭니다.
same activation X
├─ GPU 0 × weight shard 0 → partial result 0
└─ GPU 1 × weight shard 1 → partial result 1
↓
collective communication
↓
next layer input
한 Layer만 나눈 뒤 끝나는 것이 아니라 Model의 여러 Layer에서 이 동기화가 반복됩니다.
각 GPU의 계산 시간이 줄어든 양보다 Collective 대기와 전송 시간이 크면 GPU를 추가해도 느려질 수 있습니다.
각 GPU가 만든 부분 결과를 Collective로 합쳐야 하므로 Interconnect가 중요합니다.
PCIe만 사용하는 구성과 NVLink·NVSwitch가 있는 구성은 같은 TP 크기라도 통신 비용이 다릅니다.
TP가 Latency를 항상 줄인다고 가정하지 않고 실제 Topology에서 측정해야 합니다.
Model Weight가 W이고 TP 크기가 N이라고 해서 GPU당 Memory가 정확히 W/N만 필요한 것도 아닙니다.
KV Cache와 Activation,
통신 Buffer와 Runtime Reserve가 남습니다.
목적이 Model fit이라면 Server가 시작되는지만 보지 않고 목표 Context와 Concurrency까지 들어가는지 확인합니다.
Model이 GPU 한 장에 들어간다면 GPU마다 독립 Worker를 두고 요청을 나눌 수 있습니다.
한 요청의 Model 계산을 나누지는 않지만,
동시에 들어온 독립 요청을 병렬로 처리해 총 Throughput을 늘립니다.
부하가 충분한 상황에서는 Queue 길이가 줄어 TTFT도 좋아질 수 있습니다.
따라서 Replica는 Latency를 개선하지 않는다고 단정하지 않고,
Service time과 Queueing time을 나누어 봅니다.
Replica에는 Routing 비용도 생깁니다.
Prefix Cache locality, Session affinity, Backpressure와 장애 시 재분배가 실제 효율을 바꿉니다.
Replica 네 개가 있어도 Router가 한 Replica에 요청을 몰아주면 Aggregate Capacity를 사용할 수 없습니다.
반대로 매 요청을 무작위로 보내면 같은 System Prompt의 Prefix Cache가 네 곳으로 흩어질 수 있습니다.
Queue 길이만 보고 보내는 방식과 Cache locality를 보존하는 방식 사이에도 Trade-off가 생깁니다.
한 Replica가 실패했을 때 남은 세 곳에 부하가 몰리는 상황까지 Saturation 실험에 포함합니다.
원본은 4-GPU TP에서 3,926 TPS,
네 개 Independent Replica의 합계로 9,816 TPS를 기록합니다.
산술상 Replica 합계는 TP의 2.50배이며 거의 3배가 아닙니다.
또 다른 기록은 TTFT 66ms에서 33ms로 50% 감소했다고 적습니다.
하지만 Model, Traffic, Concurrency와 Result Artifact가 완전하지 않으므로 현재 성능 주장으로 사용하지 않습니다.
확장 효율은 단순한 GPU 수보다 다음 비율로 보는 편이 낫습니다.
scaling efficiency
= throughput_N / (N × throughput_1)
한 GPU에서 1,000 tok/s이고 네 GPU에서 3,000 tok/s라면 Aggregate Throughput은 세 배가 되었지만,
Linear Scaling 4,000 tok/s를 기준으로 한 효율은 75%입니다.
나머지 25%를 모두 통신 손실이라고 단정하지 않습니다.
Traffic이 충분하지 않았거나 Router가 고르게 분배하지 못했을 수도 있고,
CPU와 Network가 먼저 포화되었을 수도 있습니다.
다시 비교할 때는 다음 단위를 모두 남깁니다.
선택 기준을 줄이면 다음과 같습니다.
| 제약 | 먼저 검토할 구성 | 이유 |
|---|---|---|
| Model이 한 GPU에 들어가지 않음 | TP 또는 PP | Weight와 Layer를 나눠 Model fit 문제를 풉니다. |
| 한 요청의 Service time이 너무 김 | TP 후보 | 통신 비용보다 계산 분할 이득이 큰지 측정합니다. |
| 독립 요청이 많고 Model은 한 GPU에 들어감 | Replica | 요청을 나누고 Cache locality와 Queue를 관리합니다. |
| GPU 연결이 PCIe뿐임 | Single GPU·PP·Replica와 함께 비교 | TP Collective 비용이 이득을 넘을 수 있습니다. |
M1 Air 한 대에는 Tensor Parallel로 나눌 여러 GPU가 없습니다.
이 Step의 M1 학습 목표는 분산 숫자를 재현하는 것이 아니라,
작은 Model이 한 장치에 들어갈 때 왜 Replica나 TP가 필요하지 않은지 설명하는 것입니다.
실제 TP와 Replica 비교는 NVIDIA Reference Track에서 같은 GPU 시간과 Traffic으로 실행합니다.
GPU를 늘리는 것은 Hardware 선택이 아니라 Scheduling과 통신 구조를 바꾸는 일입니다.
Model fit, 한 요청의 Latency, 독립 요청의 Throughput 중 어떤 제약을 풀지 먼저 정해야 합니다.
CH09에서 가장 중요한 변화는 Option을 더 많이 알게 된 것이 아닙니다.
숫자를 보기 전에 어떤 질문에 답하는 실험인지 먼저 적게 된 것입니다.
Hardware를 확인하고 Traffic을 고정한 뒤,
TTFT와 TPOT, TPS와 Error를 같은 시간축에서 봅니다.
Baseline Memory를 읽고 Prefix Cache와 Quantization이 바꾼 자리를 확인하며,
Metric에서 Trace와 Kernel로 필요한 만큼만 내려갑니다.
마지막으로 GPU를 늘릴 때도 더 많은 GPU라는 한 문장으로 묶지 않습니다.
Model을 한 GPU에 넣기 위한 Tensor Parallel인지,
독립 요청의 Queue를 나누기 위한 Replica인지 목적을 구분합니다.
원본의 Historical 결과는 좋은 질문을 남겼지만 그대로 결론이 되지는 못했습니다.
Prefix 2.368배, AWQ 2.70배, Replica 2.50배 같은 숫자는 각 Run의 조건과 Artifact가 복원되어야 다시 검증할 수 있습니다.
앞으로 최적화를 채택할 때는 다음 다섯 가지를 함께 남기려 합니다.
좋은 최적화는 가장 큰 숫자를 만드는 설정이 아닙니다.
다음 사람이 같은 조건에서 다시 확인하고,
Traffic이 달라졌을 때 안전하게 되돌리거나 새 Baseline을 만들 수 있는 변경입니다.