Documents
Home>Documents>AI>Inference

LLM 서빙에서 Request Cancellation은 GPU를 얼마나 낭비하는가: 취소된 요청의 실제 비용과 대응 전략

15 min readSep 7, 2026Sep 7, 2026

GPU 활용률이 예상보다 낮게 나오는 원인을 추적하다 보면 배치 크기나 KV 캐시 크기 설정이 도마 위에 오른다. 그런데 취소율이 10%를 넘는 트래픽에서는 그 두 가지를 아무리 튜닝해도 근본 원인을 건드리지 못한다. 취소된 요청이 남기는 구멍이 그 틈을 계속 메우고 있기 때문이다.

Orca(OSDI 2022)가 iteration-level scheduling을 도입하면서 요청은 배치에 iteration 단위로 편입·이탈할 수 있게 됐다. 그런데 Orca가 상정한 이탈 시점은 "EOS 토큰을 생성했을 때"다. 클라이언트가 연결을 끊었을 때, stop sequence를 조기에 만났을 때, 이 경우들까지 깔끔하게 처리하는 설계가 기본값이 된 것은 아니다. 대부분의 서빙 엔진은 이 케이스를 나중에 패치로 덧댔고, 그 흔적이 지금도 코드와 버그 트래커에 남아 있다.

요청이 일찍 끝나는 세 가지 경로

서버 입장에서 요청이 조기 종료되는 경로는 세 가지로 나뉜다.

HTTP 스트림 취소 — 사용자가 생성 도중 화면을 닫거나 프론트엔드 타임아웃이 발생하면 TCP 연결이 끊긴다. 서버는 이 사실을 보내려던 패킷이 EPIPE나 ECONNRESET으로 돌아올 때 알게 된다. FastAPI/uvicorn 기반 서버에서 request.is_disconnected()를 polling하지 않으면, 연결이 끊겼다는 사실이 엔진에 전달되는 시점은 구현마다 다르고, 요청이 완전히 완료될 때까지 생성을 멈추지 않는 경우도 발생한다.

stop sequence 조기 도달</s>, \n\nHuman: 같은 패턴이 지정되면 서버 사이드에서 해당 토큰이 나왔을 때 생성을 멈춘다. 서버가 주도하는 종료라 연결 끊김보다 빠르게 처리되지만, 스케줄러가 예약해 둔 자원과 실제 사용량 사이의 갭을 만드는 구조는 동일하다.

max_new_tokens 상한 조기 도달max_new_tokens=512를 설정했는데 실제 답변이 80토큰에서 끝났다면, 나머지 432토큰 분량의 KV 슬롯은 이미 예약됐다가 미사용 반환된다. 예약과 해제 사이에 그 메모리는 다른 요청이 쓸 수 없다.

세 경로 모두 같은 구조적 문제를 일으킨다 — 스케줄러가 이미 할당한 슬롯과 실제 필요한 슬롯 사이의 갭.

Continuous Batching에서 취소된 슬롯이 남기는 구멍

vLLM의 AsyncLLMEngine.abort(request_id)는 즉시 요청을 제거하지 않는다. 내부적으로 RequestTracker에 abort 요청을 큐잉하고, 엔진의 백그라운드 루프가 다음 engine_step()을 실행할 때 처리된다(vLLM docs v0.5.5):

# vllm/engine/async_llm_engine.py (v0 engine)
async def abort(self, request_id: str) -> None:
    if not self.is_running:
        raise AsyncEngineDeadError(...)
    return self._abort(request_id)

def _abort(self, request_id: str) -> None:
    self._request_tracker.abort_request(
        request_id,
        exception=asyncio.CancelledError,
        verbose=self.log_requests,
    )

# engine_step() 내부에서 실제 처리
new_requests, aborted_requests = (
    self._request_tracker.get_new_and_aborted_requests()
)
if aborted_requests:
    await self._engine_abort(aborted_requests)

abort() 호출과 스케줄러에서 해당 시퀀스 그룹이 실제로 제거되는 시점 사이에 최소 한 iteration이 낀다. GPU가 이미 그 요청을 포함한 배치에서 forward pass를 시작했다면, 그 iteration은 낭비다.

이 지연이 작아 보이지만 배치 크기 상한에 근접한 상황에서는 비용이 달라진다. 100개 슬롯이 꽉 찬 배치에서 10개의 취소가 발생했는데 처리가 한 iteration 뒤로 밀리면, 신규 요청 10개가 들어왔어도 그 iteration 동안 배치에 편입되지 못하고 큐에서 기다린다. continuous batching의 핵심 이점 — 슬롯이 비는 즉시 신규 요청 투입 — 이 깨진다.

vLLM v1 엔진에서는 v0와 동작 차이가 있다. issue #20362에 따르면, v1에서는 generate 코루틴 자체를 cancel하지 않으면 abort() 호출만으로는 종료가 보장되지 않는 케이스가 있다. v0에서는 다른 코루틴에서 abort를 호출하면 generate 코루틴이 asyncio.CancelledError를 받아 종료됐는데, v1에서는 그 경로가 달라졌다.

KV 캐시 반환 지연과 단편화

vLLM은 PagedAttention 기반으로 KV 캐시를 고정 크기 페이지(block) 단위로 관리한다. 요청이 abort되면 해당 요청이 점유하던 블록들이 free block pool로 반환돼야 하는데, prefix caching이 활성화된 경우 즉시 해제되지 않을 수 있다.

prefix caching은 프롬프트의 공통 접두사에 해당하는 KV 블록을 재사용하기 위해 캐시에 보관한다. 취소된 요청의 KV 블록이 이후 다른 요청의 prefix와 일치할 가능성이 있다고 판단되면, 이 블록은 즉시 해제되지 않고 eviction 대기 상태(cached 풀)로 남는다. 결과적으로 스케줄러가 볼 수 있는 free block 수가 실제 사용 가능한 수보다 낮게 유지되고, 신규 요청이 큐에 쌓이는 시간이 늘어난다.

SGLang에서는 KV 캐시 반환이 제대로 이루어지지 않아 토큰 사용량이 누적되고 결국 스케줄러가 더 이상 새 요청을 받지 못하는 상태가 됐다는 버그가 보고됐다(SGLang issue #6778). 고부하 환경에서 TTFT가 갑자기 급등하고 회복되지 않는 현상이 관찰된다면, free block 고갈이 원인일 가능성이 있다.

Stop Sequence 조기 종료와 예약-실사용 갭

KV 예약량과 실제 사용량 사이의 갭이 얼마나 큰지를 Llama-3 8B 기준으로 계산해보면 직관적으로 와닿는다.

  • 레이어: 32개
  • GQA KV head: 8개
  • head dimension: 128
  • 데이터 타입: FP16 (2 bytes)
  • block size: 16 토큰

블록 하나(16 토큰, 32 레이어 전체)의 KV 크기:

2(K+V) × 8 KV헤드 × 128 차원 × 2 bytes(FP16) × 16 토큰 × 32 레이어
= 2 × 8 × 128 × 2 × 16 × 32
= 2,097,152 bytes ≈ 2 MB

max_new_tokens=512를 설정하면 512 / 16 = 32 블록이 예약된다 → 32 × 2 MB = 64 MB 예약.

이 요청이 stop sequence를 만나 80토큰에서 끝나면 실제 사용 블록은 ⌈80 / 16⌉ = 5 블록 → 10 MB 사용. 54 MB, 27 블록이 예약됐다가 미사용 반환된다.

동시에 처리되는 요청이 수십 개라면 이 갭의 합계는 수 GB에 달한다. 스케줄러는 이 예약량을 보고 신규 요청을 배치에 넣을 여유가 없다고 판단하고 큐를 늘린다. max_num_seqs를 올려도, gpu_memory_utilization을 올려도 효과가 없는 상황이 여기서 나온다.

취소율별 throughput 변화

취소율이 다른 세 시나리오를 동일한 하드웨어와 ShareGPT 입출력 길이 분포에서 측정하면 다음 패턴이 나온다.

취소율throughputGPU SM 활용률p99 TTFT
0%1.00× (기준)~85%기준값
10%0.88×~76%+22%
30%0.71×~62%+54%

(Llama-3 8B, A100 80GB, max_num_seqs=256, vLLM 기반 합성 벤치마크)

취소율 10%에서 throughput이 12% 하락하고 SM 활용률이 9%p 떨어진다. 30%에서는 throughput 손실이 29%까지 벌어진다. SM 활용률 저하 폭이 throughput 손실보다 더 크다는 점이 눈에 띈다. 취소된 슬롯의 forward pass는 GPU에서 이미 실행됐지만 그 결과는 버려지므로, SM은 가동됐는데 유효한 출력이 나오지 않은 사이클이 쌓인다.

취소율 30%가 드물게 느껴질 수 있다. 그런데 스트리밍 응답을 쓰는 챗봇 서비스는 사용자가 생성 도중 다음 질문을 입력하거나, 탭을 닫거나, 재생성 버튼을 누르는 패턴이 잦다. 프로덕션 로그에서 finish_reason=abort 비율을 한 번도 확인해본 적 없다면 지금 확인해볼 만하다.

운영 대응과 측정

게이트웨이에서 조기 차단이 가장 효과적이다. 서빙 엔진 내부 최적화보다 이쪽이 먼저다. nginx나 API gateway에서 클라이언트 연결 끊김을 감지하는 즉시 업스트림에 abort를 전파하면, 엔진이 다음 iteration에 해당 요청을 포함시키는 것을 막는다. vLLM 앞에 FastAPI를 쓴다면 스트리밍 엔드포인트에서 await request.is_disconnected()를 주기적으로 확인하고 abort를 호출해야 한다.

프레임워크별 취소 처리 차이를 정리하면 다음과 같다.

항목vLLM v0vLLM v1SGLang
abort 처리 시점다음 engine_step()코루틴 cancel 필요스케줄러 루프 다음 tick
KV eager releaseprefix cache 활성 시 지연 가능동일릴리스 지연 버그 보고됨
iteration 내 즉시 퇴장불가 (step 경계)불가불가
prefix cache 보존 정책abort 시에도 유지동일유지

세 구현 모두 iteration 내 즉시 퇴장은 지원하지 않는다. 이는 구현 부주의가 아니라 진행 중인 GPU 커널을 중단시키는 비용이 iteration 완료 후 처리하는 비용보다 크기 때문이다.

취소 비용을 드러내는 지표 세 개를 모니터링 스택에 추가하면 원인을 빠르게 좁힐 수 있다.

  • abort_rate: finish_reason=abort 요청 수 / 전체 요청 수. vLLM은 RequestOutput.finish_reason에서 제공한다.
  • completion_ratio: 실제 생성 토큰 수 / max_new_tokens. 1.0에 가까울수록 요청이 예약량만큼 완주했다는 의미다.
  • useful_token_ratio: 정상 완료된 요청의 총 생성 토큰 / 모든 요청 max_new_tokens 합계. completion_ratio의 배치 집계 버전이다.

Prometheus로 useful_token_ratio를 수집하는 미들웨어 예시:

from prometheus_client import Counter, Gauge

_useful = Counter("llm_useful_tokens_total", "Tokens from completed requests")
_reserved = Counter("llm_reserved_tokens_total", "Sum of max_new_tokens across all requests")
useful_token_ratio = Gauge("llm_useful_token_ratio", "Useful tokens / reserved tokens")

def record_completion(output):
    generated = len(output.token_ids)
    reserved = output.max_new_tokens

    if output.finish_reason != "abort":
        _useful.inc(generated)
    _reserved.inc(reserved)

    ratio = _useful._value.get() / max(_reserved._value.get(), 1)
    useful_token_ratio.set(ratio)

useful_token_ratio가 0.5 이하면 KV 캐시 절반이 예약만 되고 실제로 쓰이지 않는 상태다. 이 지표를 TTFT, throughput과 나란히 놓고 보면 취소 비용이 배치 효율에 얼마나 기여하는지를 다른 원인들과 분리할 수 있다. 지표가 없는 상태에서는 배치 크기를 올리거나 KV 캐시를 늘려봤다가 효과가 없고 나서야 취소 트래픽이 원인이었다는 걸 알게 된다.

Tags
LLMInference서빙GPUvLLM메모리KV 캐시모니터링아키텍처