LLM 추론 서버를 운영하다 보면 GPU를 빌려놓고도 실제 활용률이 기대보다 낮게 나오는 일이 많습니다. continuous batching을 적용하면 padding 낭비를 줄일 수 있지만, 그것만으로 끝나지 않습니다. Hugging Face가 공개한 글은 synchronous continuous batching에서 CPU와 GPU가 번갈아 쉬는 구조를 설명하고, asynchronous batching으로 이 유휴 시간을 줄이는 방법을 정리합니다. 운영자에게 중요한 메시지는 단순합니다. 모델을 바꾸지 않아도 스케줄링과 스트림 설계만으로 비용을 줄일 여지가 있습니다.
출처: Hugging Face Blog, “Unlocking asynchronicity in continuous batching”, 2026-05-14.
Continuous batching은 서로 다른 요청을 한 배치 안에 계속 넣고 빼면서 GPU를 더 촘촘하게 쓰는 기법입니다. fixed batching처럼 모든 요청 길이가 끝날 때까지 기다리지 않기 때문에, 긴 응답 하나 때문에 짧은 응답들이 같이 묶여 낭비되는 문제를 줄입니다. 하지만 기본 구현이 동기적이면 다른 낭비가 남습니다.
Hugging Face 글은 batch size 32, 8B 모델로 8K token을 생성하는 프로파일에서 전체 300.6초 중 24.0%가 GPU가 CPU를 기다리며 idle 상태였다고 설명합니다. CPU는 batch를 준비하고, KV cache table을 업데이트하고, 완료된 요청을 제거하고, 새 요청을 넣고, sampling 결과를 처리합니다. GPU는 forward pass를 수행합니다. 동기 구조에서는 GPU가 계산하는 동안 CPU가 기다리고, CPU가 다음 배치를 준비하는 동안 GPU가 기다립니다.
이 병목은 모델 성능 문제가 아닙니다. 같은 모델, 같은 하드웨어에서도 CPU batch preparation과 GPU compute를 겹치게 만들면 개선 여지가 생깁니다. 그래서 asynchronous batching의 목표는 batch N이 GPU에서 계산되는 동안 CPU가 batch N+1을 준비하게 만드는 것입니다.
비동기 batching을 이해하려면 CUDA stream과 event 개념이 필요합니다. CUDA stream은 GPU 작업의 ordered queue입니다. 같은 stream 안의 작업은 순서대로 실행되지만, 서로 다른 stream의 작업은 독립적으로 실행될 수 있습니다. 일반적인 PyTorch 코드는 default stream을 많이 쓰는데, default stream은 동기화 성격이 강해서 concurrency를 만들기 어렵습니다.
Hugging Face 글에서는 세 가지 stream을 구분합니다. CPU에서 GPU로 입력을 보내는 H2D stream, GPU forward를 실행하는 compute stream, GPU 결과를 CPU로 가져오는 D2H stream입니다. 이 셋을 나누면 CPU가 작업을 enqueue한 뒤 바로 다음 일을 할 수 있습니다. 다만 stream이 독립적이기 때문에 순서를 명시하지 않으면 compute가 입력 전송 완료 전에 실행되거나, output transfer가 compute 완료 전에 실행되는 문제가 생깁니다.
여기서 CUDA event를 씁니다. H2D 전송이 끝나면 h2d_done event를 기록하고, compute stream은 이 event를 기다립니다. compute가 끝나면 compute_done event를 기록하고, D2H stream은 이를 기다립니다. CPU는 이 작업들을 enqueue하고 다음 준비를 진행합니다. 마지막에 결과가 필요할 때만 D2H 완료 event를 synchronize합니다.
운영 중인 서버에 바로 stream-level 최적화를 넣는 것은 쉽지 않습니다. serving framework가 내부적으로 이미 많은 최적화를 하고 있기 때문입니다. vLLM, TGI, SGLang, TensorRT-LLM 같은 스택을 쓴다면 먼저 해당 프레임워크의 scheduling, paged attention, async output processing 옵션을 확인해야 합니다. 직접 구현한 PyTorch serving이라면 비동기 batching의 효과가 더 클 수 있습니다.
적용 전에는 프로파일이 먼저입니다. GPU utilization이 낮다는 이유만으로 비동기화를 넣으면 안 됩니다. 병목이 CPU scheduling인지, tokenizer인지, network IO인지, KV cache memory pressure인지, batch admission policy인지 확인해야 합니다. Nsight Systems, PyTorch profiler, framework metrics를 통해 CPU active time, GPU kernel time, H2D/D2H transfer time, queue wait time을 분리합니다.
만약 CPU batch preparation이 실제 병목이라면, 작업을 세 단계로 쪼갭니다. 첫째, token sampling과 request state update를 빠르게 만들고 Python object churn을 줄입니다. 둘째, H2D/compute/D2H 경로를 default stream에서 분리합니다. 셋째, batch N 결과를 기다리는 동안 batch N+1 후보를 미리 준비합니다. 이때 가장 어려운 부분은 batch N+1이 batch N의 sampling 결과에 의존한다는 점입니다. speculative하게 준비할 수 있는 부분과 반드시 결과를 기다려야 하는 부분을 분리해야 합니다.
GPU 비용을 줄이려면 tokens/sec만 보지 말고 dollar per successful response를 봐야 합니다. 예를 들어 H200이 시간당 5달러라면 하루 종일 돌렸을 때 120달러입니다. GPU가 24% idle이면 단순 계산으로 상당한 비용이 공중에 떠 있습니다. 하지만 최적화가 복잡해져 장애가 늘거나 tail latency가 나빠지면 실제 비용은 다시 올라갑니다.
따라서 평가 지표는 네 가지를 함께 봐야 합니다. 첫째, throughput입니다. 초당 output token 또는 requests/sec를 측정합니다. 둘째, latency입니다. p50뿐 아니라 p95, p99가 중요합니다. 셋째, GPU utilization과 memory입니다. utilization이 올라가도 memory fragmentation이 심하면 OOM이 늘 수 있습니다. 넷째, correctness입니다. 비동기화로 순서 관리가 틀어지면 request와 output이 섞이는 치명적인 버그가 생깁니다.
특히 multi-tenant inference에서는 공정성도 중요합니다. 긴 요청이 짧은 요청을 밀어내거나, 특정 고객 트래픽이 batch를 독점하면 SLA 문제가 됩니다. scheduler policy는 성능 최적화와 제품 정책이 만나는 지점입니다. 단순히 GPU를 100% 쓰는 것이 목표가 아니라, 사용자별 지연 시간과 비용을 동시에 맞추는 것이 목표입니다.
첫 번째 실패는 default stream을 일부 남겨두는 것입니다. 특정 연산 하나가 default stream에 들어가면 다른 stream 작업과 암묵적으로 동기화되어 concurrency가 깨질 수 있습니다. PyTorch 연산, memory copy, custom kernel이 어느 stream에서 실행되는지 확인해야 합니다.
두 번째 실패는 non-blocking transfer를 켰지만 memory pinning이나 event synchronization을 제대로 하지 않는 것입니다. CPU가 곧바로 다음 작업으로 넘어가도 데이터가 준비되지 않으면 잘못된 결과가 나옵니다. 이벤트 기반 의존성을 명확히 해야 합니다.
세 번째 실패는 Python 레벨 병목을 무시하는 것입니다. batch preparation이 Python list와 dict 조작에 묶여 있으면 stream을 나눠도 기대만큼 빨라지지 않습니다. hot path는 tensor operation, preallocated buffer, compiled extension으로 옮길 필요가 있습니다.
Continuous batching 비동기화의 핵심은 “GPU를 더 바쁘게 만들자”가 아닙니다. CPU와 GPU의 대기 시간을 줄여 같은 하드웨어에서 더 많은 유효 응답을 만드는 것입니다. 모델 교체보다 덜 화려하지만, 추론 비용이 큰 서비스에서는 이런 운영 최적화가 실제 마진을 만듭니다.