📌 Notice
LLM Serving and Optimization Study (=LLMSO)
직접 실습을 통해 LLM 서비스 제공 및 최적화에 대한 지식에 대한 학습 및 실습 내용을 정리한 블로그입니다.
CloudNet@에서 스터디를 진행하고 있습니다.
EKS 관련 이전 스터디 내용은 아래 링크를 통해 확인할 수 있습니다.
LLM을 서비스로 제공할 때는 모델이 답변을 생성할 수 있는지만으로 충분하지 않습니다.
사용자가 첫 Token을 보기까지 얼마나 기다려야 하는지,
전체 답변이 끝날 때까지 얼마나 걸리는지,
요청이 몰렸을 때도 서비스를 유지할 수 있는지,
그리고 그 과정에 GPU 비용이 얼마나 드는지를 함께 생각해야 합니다.
CH5에서는 이 문제를 다음 질문으로 좁혀서 살펴봅니다.
LLM Serving 요청은 어느 단계에서 병목이 생기며,
그 병목은 Compute·Memory·Interconnect 중 무엇과 관련이 있는가?
CH03: 요청이 Serving System 안에서 어떻게 처리되는가?
↓
CH05: 그 과정에서 병목은 어디서 생기는가?
↓
CH06: 병목을 줄이기 위해 어떤 최적화 기법을 쓰는가?
CH03에서 요청의 생명주기와 Serving 구성요소를 살펴봤다면, CH05에서는 각 단계가 어떤 자원을 사용하고 어디에서 제한될 수 있는지
확인합니다. CH06에서는 그 진단 결과를 바탕으로 Batching, Attention, Quantization, Prefix Caching 같은 최적화 기법으로 이어갑니다.
모델이 답변을 생성할 수 있다면, 왜 LLM Serving System을 따로 최적화해야 할까?
LLM을 처음 공부할 때는 다음 코드만 실행하면 서비스가 완성될 것처럼 느껴질 수 있습니다.
response = model.generate(prompt)
하지만 실제 사용자가 만나는 것은 generate() 함수가 아니라, 요청을 받고 모델을 실행하고 답변을 전달하는 전체 시스템입니다.
이번 Step에서는 아직 GPU를 벤치마크하지 않습니다.
먼저 무엇을 빠르게 만들어야 하는지, 그리고 무엇을 기준으로 Serving을 평가해야 하는지부터 정리합니다.
Inference는 모델이 입력을 계산하고 다음 Token을 생성하는 과정입니다.
Serving은 그 추론을 사용자에게 제공하기 위해 필요한 주변 시스템까지 포함합니다.
Inference
└─ 입력을 모델에 전달하고 출력 Token을 계산하는 과정
Serving
└─ 요청 수신
├─ 입력 검증과 요청 관리
├─ 모델과 Weight 준비
├─ 대기열과 Scheduling
├─ Inference 실행
└─ 생성 결과 전달
따라서 모델이 빠르게 Token을 생성하더라도 다음 상황에서는 사용자가 서비스를 느리다고 느낄 수 있습니다.
LLM Serving 최적화는 모델의 연산 속도만 높이는 일이 아니라,
요청의 전체 생명주기에서 병목을 찾아 줄이는 일입니다.
LLM Serving 요청을 단순화하면 다음과 같은 흐름으로 볼 수 있습니다.
사용자 요청
↓
API 수신
↓
Queue와 Scheduling
↓
입력 처리와 Model Execution
↓
첫 Token 생성
↓
남은 Token 생성
↓
전체 응답 전달
이 흐름을 기준으로 보면 “모델이 느리다”는 말만으로는 원인을 충분히 설명할 수 없습니다. 요청 대기, 모델 실행, 응답 생성 중 어디에서 시간이 사용되었는지를 나눠서 봐야 합니다.
LLM 응답 시간은 하나의 숫자로만 보지 않는 것이 좋습니다.
요청 시작 ───────── 첫 Token ───────────── 응답 완료
TTFT E2E Latency
TTFT(Time To First Token)는 사용자가 요청을 보낸 뒤 첫 번째 Token을 확인하기까지 걸리는 시간입니다.
TTFT가 길면 사용자는 다음처럼 느낄 수 있습니다.
아직 응답이 시작되지 않았네.
Queue에서 오래 기다리거나, 긴 입력을 처리하는 Prefill 시간이 길면 첫 Token이 늦어질 수 있습니다.
여기서는 요청 시작부터 전체 응답이 완료될 때까지의 시간을 E2E(End-to-End) Latency라고 부르겠습니다.
첫 Token은 빨리 도착했지만 이후 Token 생성이 오래 걸리면, TTFT는 괜찮아도 E2E Latency는 길 수 있습니다.
따라서 다음 두 문제는 구분해야 합니다.
원문은 LLM Serving 최적화가 중요한 첫 번째 이유로 Customer Experience를 제시합니다.
응답 지연이 길어질수록 사용자의 만족도가 떨어질 수 있기 때문입니다.
다만 지연시간과 만족도의 관계는 단순한 직선이 아닙니다.
이 수치는 이번 환경에서 측정한 결과가 아니라, 원문이 설명을 위해 제시한 사고 실험입니다.
중요한 것은 특정 숫자 자체보다 사용자가 체감하는 개선의 크기와 시스템 비용을 함께 봐야 한다는 점입니다.
또 다른 축은 모델의 응답 품질입니다. 일반적으로 같은 계열의 모델에서는 더 큰 모델이 더 높은 품질을 보일 가능성이 있지만, 더 많은 GPU Memory와 처리 시간을 요구할 수 있습니다.
결국 Serving 설계는 다음 세 요소 사이의 균형을 찾는 문제입니다.
응답 품질
↕
지연시간 ─── 처리량 ─── 비용
“가장 빠른 모델”을 선택하는 것이 언제나 정답은 아닙니다.
사용자가 필요로 하는 품질과 지연시간을 만족하면서, 감당할 수 있는 비용으로 처리하는 것이 목표입니다.
모델을 한 번 학습하는 비용과, 배포한 모델을 계속 실행하는 비용은 성격이 다릅니다.
Training
└─ 주로 초기 투자와 재학습 비용
Inference
└─ 배포 후 요청과 상호작용이 발생할 때마다 계속 발생하는 운영 비용
실제 서비스에서 추론 요청이 계속 증가하고, AI Agent나 복잡한 Workflow에서는 한 번의 작업에도 여러 LLM·Embedding 호출이 발생할 수 있으므로 추론 비용 관리가 중요하다고 설명합니다.
Serving 비용은 단순히 GPU의 시간당 가격만으로 결정되지 않습니다.
같은 하드웨어에서 더 많은 요청을 처리할 수 있다면 필요한 Replica 수를 줄일 수 있습니다. 또는 동일한 요구사항을 더 저렴한 GPU에서 만족할 수 있다면 하드웨어 비용을 줄일 수 있습니다.
다만 이 글에서는 특정 GPU 가격이나 추론 비용을 직접 측정하지 않습니다.
따라서 여기서 말하는 비용 개선은 원리와 설계 방향이며, 실제 비용 절감률로 해석하면 안 됩니다.
평균 트래픽을 처리할 수 있다고 해서 서비스가 항상 안정적인 것은 아닙니다.
장애와 지연은 평균보다 Peak Traffic에서 먼저 드러나는 경우가 많습니다.
요청 증가
↓
Queue 증가
↓
TTFT와 전체 지연시간 증가
↓
Timeout 또는 재시도
↓
추가 요청 증가
원문은 Black Friday와 같은 상황에서 수요가 평소보다 400% 이상 증가할 수 있는 사례를 제시합니다. 이 역시 우리 환경에서 측정한 트래픽 수치가 아니라 Peak Load의 위험을 설명하기 위한 원문 사례입니다.
Peak Load를 처리하려면 GPU 연산 성능만이 아니라 다음 조건을 함께 봐야 합니다.
따라서 Serving 최적화는 성능과 비용뿐 아니라,
어떤 하드웨어 환경에서도 서비스를 실제로 배포할 수 있는가라는
실현 가능성의 문제이기도 합니다.
최적화는 모든 지표를 동시에 무조건 개선하는 마법이 아닙니다.
한 지표를 개선하면 다른 지표에 비용이 생길 수 있습니다.
| 목표 | 기대하는 변화 | 함께 확인할 점 |
|---|---|---|
| TTFT 단축 | 첫 응답을 더 빨리 표시 | Queue, 입력 처리, GPU Memory |
| 전체 지연시간 단축 | 답변 완료 시점을 앞당김 | Decode 시간과 출력 Token 수 |
| 처리량 증가 | 같은 시간에 더 많은 요청 처리 | 동시성, Queue, GPU 사용률 |
| 더 큰 모델 사용 | 응답 품질 향상 가능 | Weight 용량, 비용, 지연시간 |
| 더 저렴한 GPU 사용 | 시간당 비용 절감 가능 | Memory, Bandwidth, Interconnect |
그러므로 최적화를 시작하기 전에 최소한 다음 질문을 정해야 합니다.
다음 상황을 보고 가장 먼저 확인할 지표를 예상해봅니다.
예상 답변은 다음과 같이 정리할 수 있습니다.
| 상황 | 먼저 볼 관점 |
|---|---|
| 첫 Token이 늦음 | Queue, Scheduling, 입력 처리, TTFT |
| 전체 답변이 늦음 | Decode, 출력 Token 수, E2E Latency |
| Peak에서만 느림 | Queue, 동시성, GPU Memory, Replica와 Autoscaling |
이 표는 아직 측정 결과가 아니라, 다음 Step에서 검증할 첫 번째 가설입니다.
LLM Serving 최적화는 모델의 계산만 빠르게 만드는 일이 아닙니다.
이 세 가지 질문을 함께 봐야 “좋은 Serving”을 정의할 수 있습니다.
다음 Step에서는 이 요구사항을 실제 하드웨어 관점으로 옮겨서,
Compute·Memory·Interconnect 중 어디가 병목이 될 수 있는지 살펴봅니다.
TFLOPS가 높은 GPU가 항상 좋은 LLM Serving GPU일까?
결론부터 말하면 그렇지 않습니다.
LLM Serving에서는 GPU의 연산 성능만 보는 순간 중요한 조건을 놓칠 수 있습니다.
모델을 GPU에 올릴 수 있는지,
Weight와 KV Cache를 얼마나 빠르게 읽을 수 있는지,
여러 GPU가 필요한 경우 서로 얼마나 빠르게 통신하는지를 함께 봐야 합니다.
이번 Step에서는 GPU 제품을 추천하지 않습니다.
대신 사양표를 읽고 워크로드의 병목 가설을 세우는 방법을 익힙니다.
GPU 사양표를 처음 보면 FP16, FP8, TFLOPS, GB/s 같은 숫자가 한꺼번에 나옵니다.
숫자를 외우기보다 다음 질문으로 바꿔 읽는 편이 쉽습니다.
| 질문 | 연결되는 Spec | Serving에서 확인하는 것 |
|---|---|---|
| 계산을 얼마나 빠르게 하는가? | Compute, FLOPS, Tensor Core, Precision | Matrix Multiplication과 Attention 처리 |
| 모델을 담을 수 있는가? | GPU Memory Capacity, VRAM | Weight와 KV Cache의 수용 가능 여부 |
| 데이터를 얼마나 빨리 공급하는가? | Memory Bandwidth | HBM에서 연산 유닛으로 Weight·Activation 이동 |
| 여러 GPU가 협력할 수 있는가? | PCIe, NVLink, NVSwitch, InfiniBand | Tensor Parallelism과 Multi-node 통신 |
여기에 데이터센터 관점에서는 Power Consumption도 추가해야 합니다.
같은 처리량을 더 적은 전력으로 달성할 수 있는지는 운영 비용과 랙 배치 가능한 GPU 수에 영향을 줍니다.
FLOPS(Floating-point Operations Per Second)는 1초에 수행할 수 있는 부동소수점 연산 횟수를 나타냅니다. TeraFLOPS는 1초에 1조 단위의 연산을 의미합니다.
LLM에서는 주로 Matrix Multiplication, Attention, MLP 같은 연산의 처리 능력과 연결됩니다.
다만 다음 조건에서는 Compute 수치가 높아도 GPU가 계산 유닛을 충분히 사용하지 못할 수 있습니다.
즉, 높은 TFLOPS는 가능한 계산 속도이지 모든 Serving 요청에서 실제로 달성하는 속도는 아닙니다.
GPU Memory는 두 가지 관점으로 나눠야 합니다.
Memory Capacity는 GPU에 데이터를 얼마나 많이 담을 수 있는지를 뜻합니다.
모델 Weight만 겨우 들어가는 GPU는 실제 Serving에서 여유가 부족할 수 있습니다.
Memory Bandwidth는 GPU Memory에서 데이터를 얼마나 빠르게 읽고 쓸 수 있는지를 나타냅니다.
특히 Decode에서는 매 Token을 생성할 때 Weight와 KV Cache를 반복적으로 읽어야 하므로,
단순한 Compute 성능보다 Memory Bandwidth가 더 중요한 병목이 될 수 있습니다.
하나의 GPU로 모델을 담을 수 없다면 여러 GPU를 함께 사용해야 합니다.
이때 GPU 수만 늘린다고 성능이 자동으로 선형 증가하지는 않습니다.
Tensor Parallelism을 사용하면 GPU 사이에 다음 데이터가 오갑니다.
따라서 GPU 간 통신 경로가 느리면, 각 GPU의 Compute 성능이 남아 있어도
전체 Serving은 통신을 기다리게 됩니다.
Interconnect의 상세한 종류와 topology는 다음 Step에서 따로 다룹니다.
이번 Step에서는 “GPU를 여러 개 사용한다면 연결 방식도 Spec의 일부”라는 점만 기억하면 됩니다.
GPU의 전력 소비는 보통 Watt 또는 TDP로 표현합니다.
전력은 단순히 전기요금만의 문제가 아닙니다.
따라서 대규모 운영에서는 GPU의 절대 성능뿐 아니라 Performance per Watt도 중요한 판단 기준입니다.
H100 SXM과 H100 NVL을 비교한 표가 있습니다.
이를 “어느 GPU가 무조건 우수한가”가 아니라 “어떤 축에서 차이가 나는가”를 읽는 예시로 사용하겠습니다.
| 항목 | H100 SXM | H100 NVL |
|---|---|---|
| GPU Memory | 80 GB | 94 GB |
| GPU Memory Bandwidth | 3.35 TB/s | 3.9 TB/s |
| FP16 Tensor Core | 1,979 teraFLOPS | 1,671 teraFLOPS |
| Interconnect 예시 | NVLink 900 GB/s | NVLink 600 GB/s |
이 표만 보면 H100 SXM은 Compute와 Interconnect 측면에서,
H100 NVL은 Memory Capacity와 Bandwidth 측면에서 강점이 있는 것처럼 읽을 수 있습니다.
하지만 이것만으로 Serving 성능을 단정할 수는 없습니다.
를 함께 확인해야 합니다.
Serving 관점으로 정리하면 다음과 같습니다.
| GPU 요소 | 피자 가게 | LLM Serving에서 의미 |
|---|---|---|
| Compute | 오븐의 화력 | 계산을 수행하는 속도 |
| Memory Capacity | 재료를 보관하는 냉장고 | Weight와 KV Cache 저장 공간 |
| Memory Bandwidth | 재료를 오븐으로 옮기는 속도 | 연산 유닛으로 데이터 공급 |
| Interconnect | 주방 사이의 통로 | GPU 간 데이터 전달 |
| Power | 주방의 전력·냉각 한도 | 실제 배치 가능한 처리 용량 |
냉장고가 작으면 모델을 올릴 수 없습니다.
오븐이 아무리 강해도 재료가 늦게 오면 오븐은 놀게 됩니다.
주방 사이의 통로가 좁으면 여러 오븐을 사용해도 서로 결과를 전달하는 데 시간이 걸립니다.
다음 상황에서 먼저 볼 Spec을 예상해봅니다.
| 상황 | 먼저 볼 Spec |
|---|---|
| 모델이 들어가지 않음 | Memory Capacity |
| Decode 처리량이 낮음 | Memory Bandwidth와 Batch |
| GPU 간 통신이 느림 | Interconnect와 Topology |
| 배치·냉각 한도 초과 | Power와 Performance per Watt |
이 표는 아직 GPU를 직접 측정한 결과가 아니라,
다음 Step에서 사용할 병목 가설을 세우는 기준표입니다.
LLM Serving GPU를 고를 때는 다음 네 축을 함께 봐야 합니다.
Compute
+ Memory Capacity
+ Memory Bandwidth
+ Interconnect
+ Power
특히 “TFLOPS가 높다”는 한 가지 사실만으로는 모델이 올라가는지,
Decode가 빠른지, Multi-GPU Serving이 효율적인지 알 수 없습니다.
다음 Step에서는 이 중 Interconnect를 더 자세히 살펴봅니다.
PCIe·NVLink·NVSwitch·InfiniBand가 각각 어느 통신 범위를 담당하는지,
그리고 GPU를 늘려도 성능이 늘지 않을 수 있는 이유를 확인하겠습니다.
GPU를 여러 개 사용하면 성능도 GPU 개수만큼 늘어날까?
모델이 너무 크거나 한 GPU의 처리량이 부족하면 여러 GPU를 함께 사용할 수 있습니다. 하지만 여러 GPU는 하나의 큰 GPU처럼 자동으로 동작하지 않습니다.
GPU 사이에서 Activation, Partial Result, Synchronization 정보가 계속 이동하기 때문입니다.
그래서 Multi-GPU Serving에서는 GPU의 개수뿐 아니라
GPU 사이의 통신 경로와 대역폭을 함께 봐야 합니다.
먼저 GPU가 어디에 있는지를 구분합니다.
Intra-node
└─ 같은 서버 안의 GPU 사이 통신
├─ PCIe
├─ NVLink Bridge
└─ NVLink + NVSwitch
Inter-node
└─ 서로 다른 서버의 GPU 사이 통신
├─ InfiniBand
├─ RoCE
└─ RDMA / GPUDirect RDMA
일반적으로 같은 서버 안의 전용 GPU 연결이 서버 사이의 네트워크보다 짧고 빠릅니다.
모델을 여러 노드로 나눌수록 성능뿐 아니라 운영 복잡도도 커질 수 있습니다.
PCIe는 일반적인 서버 확장 슬롯을 통해 GPU와 CPU 또는 GPU 사이를 연결합니다.
중요한 점은 “PCIe가 나쁘다”가 아니라 워크로드의 통신량과 맞는가입니다.
각 GPU에서 서로 독립적인 모델을 실행한다면 통신이 적기 때문에 PCIe만으로도 충분할 수 있습니다.
NVLink Bridge는 PCIe 폼팩터의 GPU 일부를 더 빠른 전용 링크로 연결하는 구성입니다.
이 장의 H100 예시에서는 두 GPU를 NVLink Bridge로 연결하는 구성과,
네 GPU를 구성할 때 모든 GPU 쌍이 같은 경로를 갖지는 않는다는 점을 설명합니다.
이 구조를 읽을 때는 다음을 확인해야 합니다.
GPU가 네 개라는 사실보다 통신이 필요한 GPU들이 실제로 어떤 링크로 연결되어 있는지가 더 중요합니다.
NVLink는 NVIDIA GPU 사이의 고속 연결이고,
NVSwitch는 여러 NVLink GPU를 스위치 구조로 연결해 통신 경로를 확장합니다.
이를 단순화하면 다음처럼 볼 수 있습니다.
GPU A ─┐
GPU B ─┼─ NVSwitch ─┼─ GPU C
GPU D ─┘ └─ GPU E
전용 스위치가 있으면 GPU 사이의 통신 경로를 더 균일하게 설계할 수
있습니다. 대신 전용 하드웨어 비용과 시스템 구성 복잡도가 증가합니다.
단일 노드의 PCIe·NVLink·NVSwitch와 노드 간 InfiniBand/RDMA 경로 비교
한 서버에 담을 수 있는 GPU 수에는 물리적·전력·냉각 제약이 있습니다.
모델이 더 커지면 여러 서버에 걸쳐 모델을 나눌 수 있고, 이때
InfiniBand나 RoCE 같은 네트워크와 RDMA가 중요해집니다.
RDMA(Remote Direct Memory Access)는 CPU의 개입을 줄이고 다른 노드의
메모리에 직접 접근하는 방식입니다. GPU Serving에서는 GPUDirect RDMA와
같은 기술이 GPU 간 데이터 이동을 줄이는 데 사용될 수 있습니다.
다만 Inter-node Serving은 다음 비용을 함께 가져옵니다.
그래서 가능한 경우 먼저 한 노드 안에서 모델을 구성하고, 모델 크기나
처리량 요구사항 때문에 필요한 경우에만 여러 노드로 확장하는 방향을
검토합니다.
이 장의 자료에는 H100 구성에 대한 다음과 같은 예시가 있습니다.
| Setup | 자료에서 제시한 예시 대역폭 |
|---|---|
| 같은 노드, NVLink/NVSwitch | 900 GB/s |
| 같은 노드, NVLink Bridge | 600 GB/s |
| 같은 노드, PCIe | 128 GB/s |
| 노드 간 연결 | 50 GB/s |
이 숫자를 현재 모든 환경에 적용하면 안 됩니다. 링크 수, 방향성,
토폴로지, 실제 메시지 크기, 통신 패턴에 따라 관측값이 달라집니다.
여기서 중요한 결론은 절대 숫자보다 같은 노드의 전용 링크와 노드 간
네트워크가 서로 다른 병목을 가진다는 점입니다.
다음 세 가지 상황에서 어떤 구성을 먼저 검토할지 생각해봅니다.
| 상황 | 먼저 검토할 연결 |
|---|---|
| 독립적인 모델 실행 | PCIe도 가능한지 확인 |
| 같은 노드의 Tensor Parallelism | NVLink·NVSwitch와 Topology 확인 |
| 여러 서버의 Model Sharding | InfiniBand/RoCE, RDMA와 네트워크 구성 확인 |
정답은 GPU 수가 아니라 통신 패턴에서 결정됩니다.
Multi-GPU Serving에서 기억할 문장은 다음과 같습니다.
GPU를 추가하면 Compute와 Memory Capacity는 늘어날 수 있지만,
GPU 사이의 통신 비용도 함께 늘어날 수 있다.
따라서 다음 순서로 판단합니다.
다음 Step에서는 GPU에 모델을 올리는 과정으로 내려갑니다.
모델이 실제로 Storage에서 GPU Memory까지 어떻게 이동하고,
Weight 크기를 어떤 공식으로 먼저 추정할 수 있는지 살펴보겠습니다.
모델이 파일로 존재한다면, 왜 GPU Memory에 미리 올려두어야 할까?
LLM Serving을 시작할 때 가장 먼저 만나는 제약은 모델을 실행할 GPU Memory가 충분한지 여부입니다.
모델을 요청이 들어올 때마다 읽어 올 수는 없기 때문에,
일반적인 Serving System은 시작 과정에서 Weight를 준비하고 실행 중 재사용합니다.
모델 Weight는 보통 다음 경로를 거쳐 GPU에 올라갑니다.
Model Storage
↓
CPU Memory
↓
GPU Memory
↓
Runtime 초기화·컴파일·Warmup
↓
요청 처리 가능
이 과정은 단순한 파일 복사가 아닙니다.
이 중 어느 한 곳이 느리면 서버가 Ready 상태가 되는 시간이 길어집니다.
모델 Weight가 CPU Memory나 Disk에만 있다면 요청이 올 때마다 GPU로 전송해야 합니다.
하지만 CPU Memory와 Disk는 GPU Memory보다 데이터 공급 속도가 느리기 때문에 실시간 Serving에 적합하지 않습니다.
이 장의 자료에는 다음과 같은 대역폭 범위 비교가 있습니다.
| 위치 | 자료에서 제시한 예시 대역폭 |
|---|---|
| SSD | 0.5–14 GB/s |
| CPU Memory | 50–200 GB/s |
| GPU Memory | 300 GB/s–3 TB/s |
이 수치는 장치·세대·구성에 따라 달라지는 범위 예시입니다.
중요한 점은 정확한 숫자보다 실행 중 반복해서 읽는 Weight를 GPU Memory에 캐싱하는 이유입니다.
한 번 로드한 Weight를 GPU에 유지하면 각 요청은 모델을 다시 준비하지 않고 입력 처리와 Token 생성에 집중할 수 있습니다.
아닙니다. 모델을 GPU에 올리는 순간 필요한 공간은 최소한 다음과 같습니다.
GPU Memory
├─ Model Weight
├─ KV Cache
├─ Activation
├─ Temporary Buffer
└─ Runtime Reserved Memory
이번 Step에서는 가장 먼저 계산할 수 있는 Model Weight 크기를 다룹니다.
KV Cache와 실행 중 메모리는 다음 Step에서 이어서 살펴봅니다.
모델의 파라미터 수와 데이터 타입을 알면 Weight 크기를 대략 추정할 수 있습니다.
Weight Memory
≈ Parameter Count × Bytes per Parameter
정밀도별로 파라미터 하나가 차지하는 바이트 수는 다음처럼 생각할 수 있습니다.
| Precision | Bit 수 | 파라미터당 대략적인 크기 |
|---|---|---|
| FP32 | 32 bit | 4 bytes |
| FP16 / BF16 | 16 bit | 2 bytes |
| INT8 / FP8 | 8 bit | 1 byte |
정밀도를 낮추면 Weight Memory가 줄어들 수 있지만,
정확도·Kernel 지원·실제 처리 성능은 별도로 확인해야 합니다.
메모리가 절반이 되었다고 Serving 시간이 항상 절반이 되는 것은 아닙니다.
예시와 같이 약 70억 개의 파라미터를 BF16으로 저장한다고 가정합니다.
BF16은 파라미터당 2 bytes를 사용합니다.
7,000,000,000 parameters × 2 bytes
≈ 14,000,000,000 bytes
≈ 14 GB
따라서 모델 Weight만 대략 14GB가 필요하다고 추정할 수 있습니다.
실제 파일 크기가 계산값과 정확히 같지 않은 이유는 다음과 같습니다.
그러므로 이 공식은 GPU를 고르기 위한 첫 번째 하한선 추정으로 사용하는 것이 좋습니다.
다음 모델의 Weight Memory를 계산해봅니다.
| 모델 | Precision | Weight Memory 대략값 |
|---|---|---|
| 7B | FP16/BF16 | 14 GB |
| 7B | INT8/FP8 | 7 GB |
| 70B | BF16 | 140 GB |
이 표는 Weight만 계산한 값입니다.
실제 Serving을 위해서는 KV Cache, Activation, Runtime Reserved Memory를 더할 공간이 필요합니다.
Model Loading에서 먼저 기억할 것은 다음 세 가지입니다.
다음 Step에서는 모델이 GPU에 들어갔는데도 OOM이 발생하는 이유를
살펴봅니다. 답은 요청마다 커지는 KV Cache와 실행 중 메모리에 있습니다.
Model Weight가 GPU에 들어갔는데, 왜 요청이 늘면 OOM이 발생할까?
모델을 GPU에 올리는 것과 여러 요청을 안정적으로 처리하는 것은 다른 문제입니다.
실행 중에는 각 요청의 Context와 생성 상태를 저장하기 위한 KV Cache가 계속 필요하기 때문입니다.
Transformer가 다음 Token을 만들 때 이전 Token과 관련된 Attention 계산을 반복하면 같은 계산을 계속 수행해야 합니다.
처음 입력 처리
↓
Key·Value 계산
↓
KV Cache에 저장
↓
다음 Token 생성 때 재사용
이 방식은 계산을 줄여 Serving을 빠르게 만들 수 있지만, 저장 공간을 사용합니다.
요청 수와 Context Length가 커질수록 KV Cache도 함께 커집니다.
기본적인 Multi-Head Attention(MHA)을 기준으로 Token 하나의 KV Cache를 다음처럼 근사할 수 있습니다.
KV Cache per Token
≈ 2 × Number of Layers
× Number of KV Heads
× Head Dimension
× Bytes per Element
앞의 2는 Key와 Value를 함께 저장하기 때문에 붙습니다.
본문의 Llama-2-7B 예시는 MHA를 사용하므로 Attention Head 수와 KV Head 수가 같다고 볼 수 있습니다.
하지만 GQA나 MQA에서는 KV Head 수가 달라질 수 있습니다.
따라서 일반적인 공식에서는 Attention Head가 아니라 KV Head 수를 사용해야 합니다.
다음 조건을 가정합니다.
2 × 32 × 32 × 128 × 2
= 524,288 bytes
≈ 0.5 MiB per Token
Token 하나만 보면 작아 보이지만,
여러 요청과 긴 Context가 동시에 존재하면 크기가 빠르게 누적됩니다.
전체 KV Cache는 다음처럼 생각할 수 있습니다.
Total KV Cache
≈ KV Cache per Token
× Maximum Batch Size
× Sequence Length
앞의 예시에서 Sequence Length 4,096과 Batch Size 16을 사용하면:
0.5 MiB × 4,096 × 16
≈ 32 GiB
Model Weight가 약 14GB였더라도 KV Cache만 약 32GiB가 필요할 수 있습니다.
여기에 Activation과 Runtime Buffer까지 더해지므로,
Weight만 GPU에 들어간다고 해서 충분하지 않습니다.
Serving 중 GPU Memory 사용량을 단순화하면 다음과 같습니다.
전체 GPU Memory
├─ Model Weight: 서버 실행 동안 비교적 고정
├─ KV Cache: 요청 수와 Sequence Length에 따라 증가
├─ Activation: 현재 계산에 필요한 중간 결과
├─ Temporary Buffer: Kernel과 Runtime이 임시로 사용하는 공간
└─ 여유 공간: 피크 시 OOM을 피하기 위한 안전 영역
Idle 상태에서는 Weight가 대부분을 차지할 수 있습니다.
하지만 여러 요청을 처리하면 KV Cache와 중간 Buffer가 증가하여 Execution 상태의 Peak Memory가 더 커집니다.
같은 GPU에서 Batch Size를 키우면 동시에 더 많은 요청을 처리할 수 있습니다.
하지만 KV Cache가 함께 증가하므로 다음 문제가 발생할 수있습니다.
따라서 Serving 설정에서 다음과 같은 개념을 함께 확인합니다.
특정 프레임워크의 옵션 이름을 외우는 것보다,
동시 요청·Context·KV Cache 사이의 관계를 이해하는 것이 먼저입니다.
앞의 Llama-2-7B 예시에서 다음 상황을 비교해봅니다.
| Batch Size | Sequence Length | KV Cache 추정 |
|---|---|---|
| 1 | 4,096 | 약 2 GiB |
| 4 | 4,096 | 약 8 GiB |
| 16 | 4,096 | 약 32 GiB |
| 16 | 8,192 | 약 64 GiB |
이 표는 Weight와 Activation을 제외한 KV Cache만의 단순 추정입니다.
Batch와 Sequence Length 중 하나만 두 배가 되어도 KV Cache가 두 배가 되는 구조를 확인하는 것이 목적입니다.
실제 KV Cache 크기는 다음 요인에 따라 달라질 수 있습니다.
따라서 “모델 크기의 두 배 GPU를 확보하라”는 이 장의 경험칙은 시작점일 뿐입니다.
모든 모델과 워크로드에 적용되는 법칙은 아닙니다.
Model Weight가 GPU에 들어가는 것은 Serving의 시작 조건일 뿐입니다.
다음 Step에서는 메모리 용량을 넘어, GPU가 실제로
연산을 기다리는지 데이터 이동을 기다리는지를 판단하는 도구를
살펴봅니다.
그 도구가 Arithmetic Intensity와 Roofline Model입니다.
GPU의 TFLOPS가 충분히 높은데도 Serving이 느릴 수 있을까?
그럴 수 있습니다.
GPU가 계산할 수 있는 속도와 GPU에 데이터를 공급하는 속도는 서로 다른 제약입니다.
모델 Weight가 GPU Memory에 올라와 있어도,
실행 중에는 데이터가 Memory에서 연산 유닛까지 이동해야 합니다.
이번 Step에서는 이 둘 중 어느 쪽이 병목인지 생각하는 도구로
Arithmetic Intensity와 Roofline Model을 소개합니다.
모델을 로드하는 과정과 모델을 실행하는 과정은 구분해야 합니다.
GPU 내부의 데이터 흐름을 단순화하면 다음과 같습니다.
GPU Memory(HBM/GDDR)
↓
L2·L1 Cache / Shared Memory
↓
Register
↓
Compute Unit
On-chip Memory인 SRAM은 작지만 빠르고,
GPU Memory인 HBM이나 GDDR은 상대적으로 큰 용량을 제공합니다.
실행 중 필요한 데이터가 GPU Memory에서 계속 읽혀야 한다면 Memory Bandwidth가 중요한 제한이 될 수 있습니다.
Arithmetic Intensity는 이동한 데이터 양에 비해 얼마나 많은 연산을 수행하는지를 나타냅니다.
Arithmetic Intensity
= Number of FLOPs / Data Movement in Bytes
두 가지 경우를 비교해봅니다.
| 형태 | 의미 | 예상 병목 |
|---|---|---|
| 낮은 Arithmetic Intensity | 데이터를 많이 이동하지만 연산은 적음 | Memory Bandwidth |
| 높은 Arithmetic Intensity | 적은 데이터를 재사용하며 연산이 많음 | Compute |
낮은 Arithmetic Intensity의 Workload에서는 Compute 성능이 남아 있어도 데이터가 늦게 도착해 연산 유닛이 기다릴 수 있습니다.
Roofline Model은 하드웨어의 Compute 한계와 Memory Bandwidth 한계를 하나의 그래프로 함께 보는 단순화된 모델입니다.
출처 - https://docs.nersc.gov/tools/performance/roofline/
- x축: Arithmetic Intensity, FLOPs per Byte
- y축: Attainable Performance
- 기울어진 구간: Memory Bandwidth가 제한하는 영역
- 수평 구간: Compute가 제한하는 영역
- 두 구간이 만나는 지점: Crossover Point
Arithmetic Intensity가 Crossover Point보다 낮으면 데이터를 더 빨리 공급하는 것이 우선일 수 있습니다.
반대로 충분히 높으면 Compute 성능을 더 사용하는 방향이 중요해집니다.
이 장에서는 L40S의 FP16 Tensor Core 성능과 Memory Bandwidth를 이용해
이론적인 Crossover Point를 계산합니다.
362 TeraFLOPS / 864 GB/s
≈ 419 FLOPs/Byte
이 계산은 실제 애플리케이션 성능이 아니라 다음 조건을 둔 이상적인
근사입니다.
따라서 419 FLOPs/Byte라는 숫자를 모든 Workload의 절대 기준으로 사용하면 안 됩니다.
여기서는 Roofline을 읽는 방법을 익히는 것이 목적입니다.
LLM의 Transformer Block에서는 Matrix Multiplication이 큰 비중을 차지합니다.
다음 형태를 생각해봅니다.
Input[M, K] × Weight[K, N] = Output[M, N]
단순화하면 연산 횟수와 데이터 이동량은 다음과 같습니다.
Operations
≈ 2 × M × N × K
Data Movement
≈ Bytes per Element × (M × K + K × N + M × N)
FP16처럼 Element 하나를 2 bytes로 저장한다고 가정하면:
Arithmetic Intensity
≈ M × N × K / (M × K + K × N + M × N)
분자와 분모에서 2가 서로 약분된 결과입니다.
M=N=K라고 단순화하면 이 장의 예시는 다음과 같습니다.
| 행렬 크기 | Arithmetic Intensity | L40S 예시 기준 해석 |
|---|---|---|
| 64 | 약 21 FLOPs/Byte | Memory Bandwidth 영역 |
| 512 | 약 170 FLOPs/Byte | Memory Bandwidth 영역 |
| 4,096 | 약 1,365 FLOPs/Byte | Compute 영역 |
행렬이 커질수록 같은 데이터를 더 많이 재사용할 수 있어
Arithmetic Intensity가 커집니다.
다음 질문에 답해봅니다.
예상 답변은 다음과 같습니다.
GPU 성능을 볼 때는 “TFLOPS가 얼마인가?”에서 멈추지 말고
다음 질문으로 이어가야 합니다.
이 Workload는 데이터를 기다리는가, 아니면 계산을 기다리는가?
Arithmetic Intensity는 그 질문을 정량적으로 생각하게 해주고,
Roofline Model은 하드웨어의 Compute와 Memory 한계를 한 화면에서
비교하게 해줍니다.
다음 Step에서는 이 도구를 실제 LLM Serving의 두 단계,
Prefill과 Decode에 적용합니다.
같은 모델을 실행하는데, 왜 입력을 처리하는 단계와 답변을 생성하는 단계의 병목이 다를까?
LLM Serving은 크게 두 단계로 나눠 생각할 수 있습니다.
두 단계는 같은 Transformer Layer를 지나지만 한 번에 처리하는 Token의 수와 Tensor Shape가 다릅니다.
이 차이가 Arithmetic Intensity와 GPU 활용 패턴을 바꿉니다.
LLM의 입력 Tensor를 단순화하면 다음과 같습니다.
[Batch Size, Sequence Length, Hidden Dimension]
Batch Size가 1이면 입력을 다음과 같이 2차원 Matrix로 단순화할 수 있습니다.
[Sequence Length, Hidden Dimension]
이때 Sequence Length가 Matrix의 행 수와 비슷한 역할을 하게 됩니다.
Prefill은 사용자가 보낸 Prompt를 모델에 처음 통과시키는 단계입니다.
이 단계에서 모델은:
1. 입력 Token 전체에 대한 Attention과 Layer 계산을 수행합니다.
2. 이후 Decode에 사용할 첫 번째 KV Cache를 생성합니다.
Prompt가 길면 한 번에 처리할 Token 수가 많아집니다.
따라서 Matrix의 행 수에 해당하는 Sequence Length가 커지고,
앞 Step의 단순화된 모델에서는 Arithmetic Intensity도 높아질 수 있습니다.
결과적으로 긴 Prompt의 Prefill은 Compute-bound에 가까워질 가능성이 있습니다.
하지만 모든 Prefill이 항상 Compute-bound라는 뜻은 아닙니다.
Batch, Kernel, Attention 구현, Memory 재사용에 따라 달라집니다.
Decode는 이미 계산한 KV Cache를 이용해 다음 Token을 반복적으로 생성하는 단계입니다.
단순화된 Batch Size 1 모델에서는 한 번에 새로 처리하는 Token 수가
1이므로:
s_prefill = Sequence Length
s_decode = 1
Hidden Dimension은 같지만 Decode의 행 수가 매우 작아집니다.
이 때문에 한 번 읽은 Weight를 큰 Matrix 연산으로 재사용하기 어렵고,
Weight와 KV Cache를 읽는 Memory Bandwidth의 영향이 커질 수 있습니다.
이 장에서는 이러한 단순화 아래 Decode가 낮은 Arithmetic Intensity를 보이며
Memory Bandwidth-bound에 가깝다고 설명합니다.
앞 Step의 Matrix Multiplication 공식을 LLM의 간단한 형태에 적용합니다.
M = s
N = K = h
Arithmetic Intensity
≈ s × h × h / (s × h + s × h + h × h)
여기서 s는 Sequence Length, h는 Hidden Dimension입니다.
앞서 살펴본 L40S 이론 예시처럼 h=4,096을 사용하면 다음과 같이 비교할 수 있습니다.
| Sequence Length s | Prefill Arithmetic Intensity | Decode Arithmetic Intensity | 단순화한 해석 |
|---|---|---|---|
| 64 | 약 62.06 | 약 1.0 | 둘 다 Memory Bandwidth 영향이 큼 |
| 512 | 약 409.60 | 약 1.0 | Prefill은 경계에 가까워짐 |
| 4,096 | 약 1,365.33 | 약 1.0 | Prefill은 Compute 쪽, Decode는 Memory 쪽 |
이 표의 Decode 값은 Batch Size 1과 s_decode=1을 고정한 결과입니다.
실제 Serving에서는 Continuous Batching과 동시 요청 수가 Shape를 바꾸므로
Decode가 모든 상황에서 항상 같은 병목이라는 의미는 아닙니다.
Prefill과 Decode를 앞에서 배운 지표와 연결하면 다음과 같이 생각할 수
있습니다.
| 증상 | 우선 확인할 구간 | 관련 지표 |
|---|---|---|
| 첫 Token이 늦게 나옴 | Queue와 Prefill | TTFT |
| 첫 Token 후 생성 간격이 김 | Decode | Inter-Token Latency |
| 전체 응답이 늦게 끝남 | Decode와 응답 전달 | E2E Latency |
| 긴 Prompt에서만 느림 | Prefill과 Context 처리 | TTFT, Prefill Time |
| 동시 요청이 늘 때 악화 | Batch·KV Cache·Memory | TTFT, Throughput, GPU Memory |
이 표는 원인을 확정하는 진단표가 아니라, 측정 순서를 정하는 가설입니다.
TTFT가 길다고 해서 항상 Prefill만의 문제라고 단정할 수는 없습니다.
Queue와 Model Loading도 TTFT에 포함될 수 있기 때문입니다.
다음 상황에서 어느 단계의 가설을 먼저 세울지 생각해봅니다.
| 상황 | 첫 번째 가설 |
|---|---|
| 긴 Prompt에서 TTFT 증가 | Prefill과 입력 처리량 |
| 긴 출력에서 E2E 증가 | Decode와 출력 Token 생성 |
| 동시 요청 증가로 모두 악화 | Queue, Batch, KV Cache, Memory |
실제 환경에서는 이 가설을 TTFT, Inter-Token Latency, E2E, GPU Memory,
Queue Length 등의 측정으로 확인해야 합니다.
Prefill과 Decode는 같은 모델의 실행 단계지만 입력 Shape가 다릅니다.
다음 Step에서는 이런 하드웨어 제약을 다른 Accelerator와 Memory Wall 관점으로 넓혀 보고,
마지막에 운영 사례를 선택적으로 정리합니다.
NVIDIA GPU 외의 Accelerator가 있다면, 무엇을 기준으로 비교해야 할까?
앞의 Step에서는 GPU의 Memory, Compute, Interconnect가 LLM Serving의 병목에 어떻게 연결되는지 살펴봤습니다.
하지만 LLM 추론을 위한 하드웨어는 GPU 하나로 끝나지 않습니다.
다만 “어느 칩이 가장 좋은가?”라는 순위표를 만드는 것이 이번 Step의 목표는 아닙니다.
워크로드와 소프트웨어 환경에 따라 선택 기준이 달라지기 때문입니다.
이 장에서는 다음과 같은 Accelerator를 소개합니다.
| 분류 | 예시 | 먼저 확인할 점 |
|---|---|---|
| 범용 GPU 대안 | AMD MI300X, Intel Gaudi2 | 모델·Kernel·소프트웨어 호환성 |
| 클라우드 전용 Accelerator | Google TPU, Amazon Inferentia | 특정 클라우드와 SDK 의존성 |
| 지역·시장별 대안 | Huawei Ascend NPU | 공급망과 지역 제약 |
| 특화된 추론 칩 | Groq, Cerebras, SambaNova 등 | 특정 지연시간·모델 구조에 대한 적합성 |
이 표는 이 장에서 제시한 분류를 학습용으로 정리한 것입니다.
제품의 현재 성능이나 시장 점유율을 비교하는 최신 표가 아닙니다.
새로운 칩을 볼 때는 GPU 이름 대신 다음 질문을 반복합니다.
| 질문 | 확인할 내용 |
|---|---|
| 모델이 들어가는가? | Memory Capacity와 모델 분할 필요성 |
| 데이터를 빨리 공급하는가? | Memory Bandwidth와 On-chip Memory |
| 연산을 충분히 활용하는가? | Precision, Tensor 연산, Kernel 지원 |
| 여러 칩이 협력하는가? | Interconnect와 통신 라이브러리 |
| 실제로 운영할 수 있는가? | SDK, Framework, 인력, 공급, 전력, 비용 |
특히 NVIDIA GPU에서 다른 Accelerator로 바꾸는 일은 하드웨어만 교체하는 작업이 아닐 수 있습니다.
까지 함께 바뀔 수 있습니다.
여기서 주목할 문제는 Memory Wall입니다.
시간이 지나면서 GPU와 Accelerator의 Peak FLOPS는 빠르게 증가했지만,
데이터를 이동시키는 Memory Bandwidth와 Interconnect Bandwidth는 같은 속도로 증가하지 않을 수 있습니다.
연산 성능 증가 속도
>
데이터 이동 성능 증가 속도
↓
계산 유닛이 데이터를 기다리는 시간 증가
↓
Memory Wall
이 관점은 Step 06의 Arithmetic Intensity와 연결됩니다.
계산량이 많아도 데이터 이동이 따라오지 못하면 GPU의 Peak Compute를 실제로 활용하지 못할 수 있습니다.
On-chip SRAM은 용량이 작고 비싸지만 연산 유닛 가까이에 위치해 지연시간을 줄일 수 있습니다.
이 방향은 다음과 같은 장점을 기대할 수 있습니다.
반면 모델 크기가 커질수록 여러 칩으로 분할해야 할 수 있고,
칩 비용과 소프트웨어 제약이 커질 수 있습니다.
다른 방향은 GPU와 CPU, GPU와 GPU를 고대역폭·저지연 Interconnect로 긴밀하게 결합하는 것입니다.
이 장에서는 Grace CPU, Blackwell GPU, GB200 NVL72와 같은 Rack-scale 구성을 사례로 소개합니다.
이런 구조에서는 많은 GPU를 하나의 통신 도메인으로 묶어 대형 모델과 MoE 모델을 처리하려고 합니다.
하지만 이 역시 단순히 GPU 개수를 늘리는 문제는 아닙니다.
를 함께 설계해야 합니다.
⚠️ 이 Step에 등장하는 Accelerator와 Rack-scale 구성은 이 장에서 인용한 학습용 사례입니다.
최신 제품 사양·가격·시장 점유율·성능 순위로 사용하려면 별도의 공식 자료 확인이 필요합니다.
다음 상황에서 어떤 축을 가장 먼저 확인할지 생각해봅니다.
| 상황 | 먼저 확인할 축 |
|---|---|
| 모델이 들어가지 않음 | Memory Capacity와 Sharding |
| Decode가 느림 | Memory Bandwidth와 On-chip Memory |
| Migration 부담이 큼 | Software Ecosystem과 Kernel 호환성 |
| 초저지연이 최우선 | SRAM 구조와 고정된 실행 방식 |
이 연습의 목적은 특정 칩을 고르는 것이 아니라,
워크로드의 병목과 하드웨어의 강점을 연결하는 습관을 만드는 것입니다.
LLM Serving 하드웨어를 선택할 때는 다음 순서로 생각합니다.
결국 좋은 Accelerator는 절대적으로 정해져 있지 않고,
자신의 Workload와 운영 조건에 맞는 Accelerator입니다.
이번 장에서는 LLM Serving을 단순히 “모델이 Token을 생성하는 과정”으로 보지 않고,
하드웨어와 요청 흐름이 결합된 시스템으로 살펴봤습니다.
Serving의 목표는 무조건 가장 빠른 응답이 아닙니다.
Customer Experience, Cost Efficiency, Scalability·Peak Load·Feasibility를 함께 봐야 합니다.
GPU는 TFLOPS 하나로 판단할 수 없습니다.
Compute
+ Memory Capacity
+ Memory Bandwidth
+ Interconnect
+ Power
모델이 GPU에 들어가는지,
실행 중 Weight와 KV Cache를 공급할 수 있는지,
여러 GPU가 통신을 감당할 수 있는지를 함께 확인해야 합니다.
Model Loading에서는 Weight가 Storage에서 CPU Memory를 거쳐 GPU Memory로 이동합니다.
Weight 크기는 Parameter Count와 Precision으로 먼저 추정할 수 있지만,
실제 Serving에서는 KV Cache·Activation·Runtime Buffer가 추가로 필요합니다.
특히 KV Cache는 Batch Size와 Sequence Length가 커질수록 증가하므로,
모델이 GPU에 들어간다는 사실만으로 동시 요청 처리 가능성을 판단할 수 없습니다.
Arithmetic Intensity와 Roofline Model을 사용하면 GPU Workload가
Compute-bound인지 Memory Bandwidth-bound인지 생각할 수 있습니다.
Prefill과 Decode는 같은 모델을 실행하지만 처리하는 Tensor Shape가 다르므로 병목도 달라질 수 있습니다.
이 결론은 Batch Size, Kernel, Attention 구조, Runtime 구현에 따라 달라질 수 있으므로 실제 측정 없이 절대적인 규칙으로 사용하면 안 됩니다.
Serving 문제가 생겼을 때 다음 순서로 질문합니다.
사용자 증상
↓
TTFT인가, E2E인가, Throughput인가?
↓
Queue·Model Loading·Prefill·Decode 중 어디인가?
↓
Memory Capacity·Memory Bandwidth·Compute·Interconnect 중 무엇인가?
↓
설정·Kernel·Batch·Precision·Hardware 중 무엇을 바꿀 것인가?
이 흐름이 있으면 새로운 Framework나 Accelerator가 등장해도,
이름을 먼저 외우기보다 문제와 병목을 기준으로 이해할 수 있습니다.
이번 CH05 정리는 다음을 직접 측정한 벤치마크가 아닙니다.
앞서 인용한 수치와 사례는 개념을 설명하기 위한 예시로 구분했습니다.
실제 환경의 결론을 내리려면 동일한 모델·Precision·Batch·Context·Runtime 조건에서 별도의 측정이 필요합니다.
CH05에서 만든 기초 위에 다음 장에서는 LLM Serving을 더 효율적으로 만드는 구체적인 기법을 다루게 됩니다.
이제부터 중요한 것은 새로운 기법을 많이 아는 것이 아니라,
어떤 병목을 해결하기 위해 그 기법을 선택하는가를 설명하는 것입니다.