고성능 LLM 추론 (High-performance LLM inference)
고성능 LLM 추론 (High-performance LLM inference)
Modal에서 LLM 추론을 빠르게 돌리려면 어떤 기술들이 필요한지 한데 모아 둔 고급 가이드예요. 오픈 가중치 모델과 오픈소스 추론 엔진이 많이 발전해서, 이제는 외부 제공사에 의존하지 않고 내부에서 많은 생성형 AI 애플리케이션을 직접 돌리는 게 경제적으로도 충분히 가능해졌죠. 다만 처음부터 좋은 성능과 비용이 나오는 건 아니고, 워크로드에 맞춰 조금은 생각하고 튜닝해 줘야 해요. LLM 추론은 엔지니어들이 늘 Optimization 해 왔던 웹 서빙이나 데이터베이스 워크로드와는 성격이 꽤 다르거든요.
출처: https://modal.com/docs/guide/high-performance-llm-inference
워크로드에 따라 달라지는 핵심 성능 지표
이 가이드는 어떤 성능 기준이 중요한지에 따라 추천을 나눠요. 크게 세 가지 관점이 있죠.
- 처리량(throughput): 많은 병렬 요청으로 이루어진 큰 '잡(job)'이 전부 끝나야 완료되는 워크로드
- 지연 시간(latency): 사람이 기다리는 시간 단위로 개별 요청을 최대한 빨리 응답해야 하는 서빙
- 콜드 스타트 시간(cold start time): 지연과 처리량에 민감한 구성 요소가 섞여 있는 폭발적(bursty) 워크로드
기본적인 LLM 서버만 빠르게 띄워 보고 싶다면 llm_inference 예제를, 고처리량·저지연·저콜드스타트 각각은 vllm_throughput, sglang_low_latency, sglang_snapshot 예제를 보면 돼요. 벤치마크 기준선과 측정법은 Modal의 LLM Engineer's Almanac에서 확인할 수 있어요.
고처리량 LLM 추론 (TPS) 달성하기
전형적인 고처리량 워크로드로는 데이터베이스 백필(backfill)을 꼽아요. 트리거가 발생하면 수백 개 이상의 행을 처리해서, 예를 들어 분석 파이프라인의 감성 점수나 오프라인 평가에 쓸 생성 결과물을 만들어 내는 식이죠. 어떤 개별 행의 결과를 기다리는 사람이나 시스템은 없어요.
이 경우 성능은 초당 토큰(TPS, tokens per second)으로 측정되는 처리량으로 정의돼요. 처리량이 곧 잡 전체의 종단 간 지연 시간을 정하고, 대부분의 배포에서 비용까지 좌우하죠.
고처리량 추론은 대부분 큰 컨텍스트에 작은 출력이라서 프리필(prefill) 시간이 지배적인 경우가 많아요. 여기에 배치(batching)로 산술 강도(arithmetic intensity)를 높이면 결국 계산 바운드(compute-bound) 워크로드가 되죠. GPU는 본래 최대 처리량을 내도록 설계된 하드웨어라서, 일반적으로 고처리량은 저지연보다 달성하기 쉬워요.
이런 잡에는 vLLM 추론 서버를 추천해요. vLLM은 프리필과 디코드(생성) 작업을 더 잘 스케줄링해서 처리량이 높아지는 편이거든요. FP4 같은 4비트 부동소수점은 Blackwell 이상 GPU에서만 지원되니, 좀 더 성숙한 8비트 FP8(호퍼 이상 지원)을 쓰는 걸 권장해요.
처리량 관점에서는 복제본(replica)당 16비트 FLOP/달러 비용이 A100·H100·B200에서 거의 비슷해요. 최신 GPU가 초당 처리량은 높아도 단위 비용이 비슷하니, 덜 쓰는 GPU가 저활용 비용이 낮고 대량 공급도 잘 되는 편이라 오히려 이점이 있죠.
또 하나, Modal에는 잡 큐(job queue) 원시 요소가 있어서 수백만 개의 대기 입력과 최대 일주일짜리 잡을 다룰 수 있어요. 기본 LLM 추론은 modal.Cls를 .spawn으로 호출해서 제공하고, 각 호출은 modal.FunctionCall 식별자 문자열을 반환받아 결과를 일주일까지 조회할 수 있죠. 추론 시스템이 초당 400개 이상의 작업을 처리할 수 있다면 여러 작업을 하나의 함수 입력으로 묶어 최대 처리량을 맞추는 걸 권장해요.
LLM 추론 지연 시간 최소화하기 (TTFT/TPOT/TTLT)
전형적인 저지연 워크로드로는 챗봇을 꼽아요. 요청 하나하나가 기다리는 사용자를 의미하고, 사용자는 수백 밀리초 단위로 반응하죠. 성능은 지연 시간으로 정의하고, 스트리밍 여부에 따라 첫 토큰까지의 시간(TTFT)이나 출력 토큰당 시간(TPOT), 마지막 토큰까지의 시간(TTLT)으로 측정해요. 대부분의 챗봇처럼 스트리밍되는 애플리케이션이라면 TTFT가 가장 중요해요.
현대 Transformer 모델은 순차적이라 응답을 직렬로 생성해요. 그래서 긴 디코드 구간은 짧은 프리필과는 다른 하드웨어 성능을 요구하죠. 디코드는 보통 **메모리 바운드(memory-bound)**라서, 토큰마다 로드하는 메모리 양을 줄이거나 메모리 대역폭을 늘리는 방식이 효과가 있어요.
토큰당 메모리를 줄이는 기법은 둘이 대표적이에요.
- 더 작고 과감하게 양자화한 모델은 메모리를 덜 차지함
- 스펙큘레이티브 디코딩(speculative decoding)으로 드래프트 모델을 통해 여러 토큰을 한 번에 생성
스펙큘레이티브 디코딩 기법은 단순한 n-gram 추측부터 여러 모델이 서로의 토큰을 만들어 주는 스택까지 매우 다양해요. Modal은 그중 EAGLE-3 방식이 오버헤드 대비 개선 효과가 가장 좋다고 봐요. 일부러 70B 파라미터 아래 모델을 FP4로 돌리는 건 드물고(special한 GPT-OSS 같은 예외 제외), 초기 2026년 시점 기준으로 저지연 전용 오픈소스 Blackwell 최적화 커널도 아직 부족해요. 그래서 일반적으로 H100/H200에서 FP8 양자화 모델을 추천해요.
저지연 워크로드에는 SGLang 추론 엔진을 추천해요. SGLang은 특히 작은 모델에서 CPU를 기다리며 GPU가 놀게 되는 호스트 오버헤드가 더 낮아요. 수백 밀리초 어음의 지연 예산에서는 네트워크 지연과 프록시 오버헤드도 신경 써야 하는데, Modal의 초저지연 지역 기반 웹 서버 배포(Modal Servers)를 쓰면 네트워크 오버헤드를 100ms 아래로 낮출 수 있어요.
폭발적 워크로드에서의 고성능 추론 (콜드 스타트 시간)
순수 처리량과 순수 지연 사이에 있는 마지막 주요 클래스예요. 대표적인 게 '워크플로우'인데, LLM 추론이 워크플로우의 한 단계이고 어떤 때는 사람이 대화형으로, 어떤 때는 비동기로 대량 실행되죠. 이런 앱의 핵심 관심사는 높은 피크 대 평균 부하 비율을 처리하는 거예요. 예를 들어 대부분은 초당 0건이다가 잠깐 10건, 또 100건으로 치솟았다 다시 0으로 떨어지는 식이죠.
핵심 성능 기준은 새 복제본이 뜨고 요청을 받기 시작할 때까지 걸리는 콜드 스타트 시간이에요. 클라우드에서 직접 자원을 요청하는 방식은 몇 분에서 수십 분이 걸리지만, Modal은 커널 단에서부터 컨테이너 시작까지 1초 미만의 지연을 목표로 설계됐어요.
폭발적 워크로드에서 유용한 기법을 몇 가지 소개할게요.
- 작은 모델 쓰고 과감하게 양자화:
Modal Volume에서 모델을 1~2GB/s 속도로 로드할 수 있어서, 모델 가중치 1GB당 콜드 스타트 지연이 거의 1초씩 발생해요. 정수·삼진 양자화같이 더 과감한 압축 포맷이 특히 도움이 돼요. - 컴파일 단계 건너뛰기: CUDA Graph 캡처, JIT 커널, Torch 컴파일은 지연·처리량엔 좋지만 캐시가 잘 안 되어 매 부팅마다 큰 지연 패널티를 줘요. 몇 초에서 수십 분까지 벌어질 수 있으니 주의해야 해요.
- 스냅샷에서 복원: JIT 컴파일 같은 시작 작업이 어쩔 수 없을 땐
Memory Snapshots를 써요. 요청 처리 직전 컨테이너의 전체 메모리 상태를 디스크에 직렬화해 뒀다가, 다음 시작 때 역직렬화만 하면 되죠. GPU 메모리 스냅샷도 지원해서 콜드 스타트를 최대 10배까지 줄였다고 해요.
어느 최적화가 통하는지는 워크로드가 저지연 쪽인지 고처리량 쪽인지에 따라 달라져요. 다만 스펙큘레이티브 디코딩은 고처리량 영역에서 성능을 해치니 일반적으로는 나쁜 선택이에요. vLLM과 SGLang 사이에서 특별한 추천은 없는데, vLLM은 새 모델을 더 빨리 따라가고 SGLang은 해킹하고 확장하기 조금 더 쉬운 편이라는 차이가 있어요.
폭발적 워크로드는 일반 Functions를 원격 Python 호출로 부르거나 Web Functions를 HTTP로 호출해서 서빙할 수 있어요. @modal.concurrent 데코레이터는 max_inputs(상한)와 target_inputs(목표)를 함께 설정할 수 있는데, 상한을 목표보다 높게 두면 기존 용량으로 부하 증가를 흡수할 수 있어요. 단, 추론 서버가 내부 대기 없이 max_inputs만큼 큰 배치를 처리하도록 설정해 두어야 해요.
더 알아보기
- Modal 예제: 기본 LLM 서버, 고처리량 vLLM, 저지연 SGLang, 스냅샷
- 콜드 스타트 성능 가이드
- 메모리 스냅샷
- 잡 큐