연구 환경에서 잘 돌던 모델이 서비스에 붙는 순간 다른 문제가 시작된다. 정확도가 아니라 응답 시간과 GPU 비용이다. 두 가지는 대개 같은 뿌리에서 나온다. GPU가 놀고 있거나, 안 해도 될 일을 하고 있다는 것.
최적화에는 순서가 있다. 효과가 큰 것부터, 그리고 되돌리기 쉬운 것부터다.
1단계: 프로파일링 — 병목을 찾지 않으면 최적화도 없다
가장 흔한 실수가 이 단계를 건너뛰는 것이다. 추론 레이턴시는 여러 조각의 합이다.
- 요청 파싱과 전처리(이미지 디코딩, 리사이즈, 정규화)
- CPU → GPU 데이터 전송
- 모델 순전파
- GPU → CPU 결과 회수
- 후처리와 응답 직렬화
전처리와 전송이 절반 이상을 차지하는 경우가 드물지 않다. 이 상태에서 모델을 양자화해봐야 전체 시간은 거의 안 준다.
PyTorch 프로파일러로 CPU와 GPU 활동을 함께 본다.
import torch
from torch.profiler import profile, ProfilerActivity
with profile(
activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
record_shapes=True,
) as prof:
for _ in range(10):
with torch.no_grad():
model(sample_input)
print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=20))
볼 것은 두 가지다. GPU 커널이 실제로 도는 시간의 비율과, GPU가 놀고 있는 구간이다. 후자가 크다면 데이터 공급이 못 따라가는 것이고, 그건 모델 문제가 아니다.
측정할 때 주의할 점 하나. GPU 연산은 비동기라서 그냥 시간을 재면 실제보다 짧게 나온다. torch.cuda.synchronize()로 동기화한 뒤 재거나, 프로파일러를 쓴다. 그리고 첫 몇 회는 워밍업으로 버린다.
2단계: 정밀도를 낮춘다 (FP16 / INT8)
병목이 모델 연산에 있다고 확인됐다면 여기부터다. 가장 적은 노력으로 가장 큰 효과가 나는 구간이다.
FP32 → FP16은 보통 첫 선택이다. 메모리 사용량이 절반으로 줄고, 최신 GPU의 텐서 코어가 가속하며, 정확도 손실은 대부분의 비전·언어 모델에서 무시할 만하다.
model = model.half().eval().cuda()
with torch.no_grad():
out = model(x.half().cuda())
INT8 양자화는 더 크게 줄지만 정확도 손실이 눈에 띌 수 있다. 그래서 검증 데이터로 양자화 전후 정확도를 반드시 비교하고 넘어간다. 이 확인을 생략했다가 서비스 품질이 떨어진 걸 뒤늦게 아는 경우가 많다.
한 단계 더 나아가면 ONNX로 내보내 추론 전용 런타임에서 실행하는 방법이 있다. 연산자 융합과 그래프 최적화가 적용되어 같은 하드웨어에서 더 빨라진다.
# ONNX 모델을 FP16 엔진으로 빌드하는 예
trtexec --onnx=model.onnx --saveEngine=model.plan --fp16
빌드 시 입력 크기를 고정하면 최적화 여지가 커진다. 가변 크기가 필요하면 최소·최적·최대 크기를 지정하는 방식으로 범위를 좁혀준다.
3단계: 동적 배칭 — GPU를 놀게 두지 않는다
단일 요청을 하나씩 처리하면 GPU 사용률이 낮게 유지된다. GPU는 여러 입력을 한 번에 처리할 때 효율이 좋기 때문이다.
동적 배칭은 짧은 시간 동안 들어온 요청을 모아 한 번에 처리한다. 개별 요청은 대기 시간이 조금 늘지만 전체 처리량이 크게 오른다.
전용 추론 서버(Triton 등)를 쓰면 설정으로 켤 수 있다.
max_batch_size: 32
dynamic_batching {
preferred_batch_size: [ 8, 16 ]
max_queue_delay_microseconds: 2000
}
max_queue_delay가 핵심 손잡이다. 길게 잡을수록 배치가 커져 처리량이 오르고 지연도 늘어난다. SLA에서 허용하는 지연의 10~20% 정도를 이 대기에 쓰는 선에서 맞추는 것이 실용적인 출발점이다.
FastAPI 같은 일반 웹 프레임워크에 모델을 직접 얹은 구조라면, 여기서 전용 추론 서버로 옮기는 것만으로 처리량이 크게 달라진다. 다만 운영 복잡도가 올라가므로 트래픽이 실제로 있을 때 넘어가는 것이 맞다.
4단계: 인프라 — 사양보다 사용률
마지막은 하드웨어 선택이다. 여기서 자주 나오는 오해가 “더 비싼 GPU가 답”이라는 것이다.
- GPU 사용률이 낮은 상태에서 상위 GPU로 바꾸면 비용만 오르고 지연은 그대로다. 먼저 배칭과 파이프라인으로 사용률을 올린다
- 모델이 VRAM에 겨우 들어가는 상황이라면 메모리가 큰 쪽으로 바꾸는 것이 배치 크기를 키워 처리량을 올린다
- 트래픽이 불규칙하다면 상시 GPU 인스턴스보다 오토스케일링이나 서버리스 추론이 비용 면에서 유리하다
그리고 전처리를 GPU로 올리는 것을 검토한다. 이미지 디코딩과 리사이즈를 CPU에서 하면 그것이 병목이 되는 경우가 많다. GPU 기반 전처리로 옮기면 1단계에서 발견한 “GPU가 노는 구간”이 줄어든다.
요약 체크리스트
- 프로파일링 — 병목이 모델인지 전처리인지 전송인지 확인
- FP16 적용 후 정확도 확인
- 필요하면 ONNX 내보내기 + 추론 런타임
- 동적 배칭으로 GPU 사용률 올리기
- 전처리 최적화 (GPU 이전, 비동기 로딩)
- 그다음에 인스턴스 사양 조정
- INT8은 정확도 검증을 통과한 경우에만
순서를 거꾸로 하면(먼저 비싼 GPU를 사고, 나중에 프로파일링) 돈은 쓰고 문제는 그대로인 결과가 나온다.
⚠️ 라이브러리 버전과 하드웨어 세대에 따라 최적 설정이 달라진다. 이 글은 접근 순서를 다루며, 실제 수치는 반드시 자신의 모델과 환경에서 측정해 확인할 것.

Leave a Reply