엔지니어링 노트설치·운영과 품질

vLLM 동시성 상한은 짧은 프롬프트가 아니라 운영 입력 길이로 잰다

자체 호스팅 모델을 vLLM으로 서빙할 때 1.1K 토큰짜리 프롬프트로 벤치를 돌리면, 동시에 처리할 요청 수(max_num_seqs)를 4에서 32로 올리라는 결론이 나옵니다. 그렇게 올리면 전체 처리량이 92% 늘기 때문입니다. 같은 서버를 업무 질의의 실제 입력 길이로 다시 재면 결론이 뒤집힙니다. 4에서 8로만 올려도 처리량은 4% 느는 데 그치고, 첫 토큰이 나오기까지 걸리는 시간(TTFT)은 65% 늘며, 응답 하나가 토큰을 내는 속도는 56% 떨어집니다.

이 글은 왜 입력 길이가 결론을 뒤집는지, 벤치 입력을 어떻게 골라야 하는지, 그리고 동시성 상한 4에 맞춰 정한 나머지 값을 다룹니다. 가중치 정밀도, 한 번에 처리할 입력 토큰 수, 서버 출력 상한, 그리고 OwlAI가 이 서버에 요청을 보내는 수입니다. 수치는 모두 참조 구성에서 잰 값입니다. RTX 4090 두 장(합계 48 GB)에 35B급 MoE 모델(토큰마다 활성 파라미터 3B)의 Int4(GPTQ) 양자화본을 텐서 병렬 2로 올렸고, vLLM 0.26.0에서 쟀습니다.

짧은 입력과 운영 길이 입력에서 동시성 상한을 올린 결과
그림 1. 짧은 입력과 운영 길이 입력에서 동시성 상한을 올린 결과

입력이 길면 처리량은 동시 2~4에서 더 늘지 않는다

요청 하나는 두 단계로 처리됩니다. 입력 처리(프리필)는 입력 토큰 전체의 키·값을 계산해 KV 캐시에 채우고, 디코드는 그 뒤로 출력 토큰을 하나씩 만듭니다. 디코드 한 걸음은 토큰 하나를 만들려고 가중치를 GPU 메모리에서 통째로 읽어야 해서, 연산보다 메모리 대역폭이 먼저 찹니다. 여러 요청을 한 걸음에 묶으면 같은 읽기 한 번으로 토큰 여러 개를 만들 수 있습니다. 입력이 짧아 디코드가 대부분인 부하에서 동시성을 올릴수록 처리량이 느는 이유입니다.

입력이 길면 사정이 다릅니다. 참조 구성의 프리필은 초당 약 11,400토큰에서 연산이 이미 꽉 찹니다. 4만 토큰짜리 입력 하나를 처리하는 데 GPU가 3초 넘게 쉬지 않고 계산하는 셈이라, 요청을 더 받아도 이 계산을 나눠 할 여유가 없습니다. 더 받은 요청은 앞 요청의 프리필이 끝나기를 기다리므로 TTFT가 늘어납니다. 이미 답을 내던 응답도 남의 프리필과 GPU를 나눠 쓰느라 토큰 내는 속도가 떨어집니다.

업무 질의의 입력은 깁니다. 표 스키마, 문서 발췌, 도구 결과, 대화 맥락이 한 요청에 쌓이기 때문입니다. 실운영 환경에서 관측한 입력 길이의 중앙값은 39,812토큰이었고, 61.7%가 32K를 넘었습니다. 이 길이에서 전체 처리량은 동시 2~4에서 평평해집니다. 그래서 상한을 4로 두었습니다. 8로 올리면 얻는 것은 처리량 4%이고, 잃는 것은 모든 사용자가 첫 글자를 보기까지 더 기다리는 시간입니다.

벤치 입력은 운영 기록의 입력 길이에서 고른다

벤치 결과가 운영과 맞으려면 측정 조건이 운영 부하를 닮아야 합니다. 참조 구성의 숫자를 낸 측정에서 지킨 조건은 다음과 같습니다.

  • 입력 길이는 운영 기록에서 정합니다. OwlAI는 LLM 호출마다 입력 토큰 수를 사용량 원장에 남기므로, 그 분위수를 먼저 보고 벤치 프롬프트 길이를 맞춥니다.
  • 요청마다 프롬프트 앞부분을 다르게 줍니다. 접두사 캐싱이 켜진 서버에 같은 프롬프트를 동시에 보내면 요청들이 KV 캐시를 함께 써서 처리량이 부풀고, 31K 입력의 TTFT가 0.13초로 찍힙니다.
  • 같은 조건을 여러 번 잽니다. temperature 0이어도 배치 구성에 따라 결과가 흔들려서, 한 번 잰 값으로는 결론을 내지 않습니다.
  • max_num_seqs는 크게 열어 두고 클라이언트 쪽 동시 요청 수를 바꿔 가며 잽니다. 이 값은 스케줄러의 상한일 뿐이라 값마다 서버를 다시 띄운 것과 같은 곡선이 나오고, 한 번에 3~4분 걸리는 재기동을 건너뜁니다.
  • 동시 요청 수 × 평균 컨텍스트가 KV 캐시에 담을 수 있는 토큰 수를 넘는지 봅니다. 넘으면 vLLM이 진행 중인 요청을 내려놓았다가 다시 계산하는 선점(preemption)을 시작합니다.

동시 4개를 창 끝까지 받으려면 정밀도보다 KV 캐시 자리가 먼저다

상한 4를 정하면 GPU 메모리를 어떻게 나눌지도 따라 정해집니다. 요청 넷이 모두 컨텍스트 창 262,144토큰 가까이 쓰더라도 선점 없이 담기려면 KV 캐시에 창 네 개 이상이 들어가야 합니다.

이 모델의 공식 양자화본은 FP8이고, 정밀도만 보면 Int4보다 낫습니다. 그런데 FP8 가중치는 34.9 GiB라서 이 GPU에 올리면 KV 캐시에 카드당 5 GB 정도만 남습니다. Int4 가중치는 22.7 GiB이고 KV 캐시를 카드당 약 10 GiB 남깁니다. Int4 구성의 KV 캐시는 창을 다 채운 요청 7.58개분을 담고, FP8은 KV 캐시가 그 절반쯤이라 어림셈으로 창 네 개에 못 미칩니다. 그래서 이 GPU에서는 Int4를 골랐습니다. GPU 메모리가 넉넉해 FP8 가중치를 올리고도 창 네 개가 남는 환경이라면 FP8을 고릅니다.

FP8 과 Int4 에서 GPU 메모리를 가중치와 KV 캐시로 나눈 비교
그림 2. FP8 과 Int4 에서 GPU 메모리를 가중치와 KV 캐시로 나눈 비교

한 번에 프리필할 토큰 수(max_num_batched_tokens)는 8192로 둡니다. 32768로 올리면 큰 묶음으로 프리필할 수 있을 것 같지만, 처리량은 초당 82토큰에서 84토큰으로 거의 그대로였고 KV 캐시는 9.7 GiB에서 8.1 GiB로 17% 줄었습니다. 프리필이 이미 연산을 다 쓰고 있어서 묶음을 키워도 빨라지지 않고, 큰 묶음에 쓸 작업 메모리만 KV 캐시 몫에서 빠져나갑니다.

네 자리 중 하나를 붙잡는 반복 생성은 서버 출력 상한이 끊는다

동시성 상한이 4이면 서버가 동시에 생성하는 응답 자리도 넷입니다. 이 가운데 하나가 오래 묶이면 서버 전체 처리 능력의 4분의 1이 그동안 빠집니다.

모델이 같은 구절을 되풀이하다 멈추지 않는 경우가 드물게 있습니다. 모델의 생성 설정 파일에 출력 상한이 없으면 vLLM의 실제 상한은 「창 − 프롬프트」라서, 반복에 빠진 요청은 26만 토큰 창 끝까지 생성하며, 한 건이 수십 분 동안 자리 하나를 붙잡습니다.

그래서 vLLM 설정의 override_generation_config에 서버 기본값 두 개를 넣었습니다.

설정 값 하는 일
max_new_tokens 32768 서버 전역 출력 상한. 요청이 더 큰 값을 보내도 작은 쪽으로 묶인다. 반복을 끝내는 것은 이 값이다
presence_penalty 1.5 이미 나온 토큰을 다시 고를 확률을 낮춰 반복에 빠지는 일을 줄인다

32768은 모델 카드가 대부분의 질의에 권하는 출력 길이이고, 1.5는 모델 카드가 생각 모드의 일반 작업에 권하는 값입니다. presence_penalty는 vLLM 판에 따라 서버 기본값으로 받지 않고 조용히 버립니다. 시험한 판 중 0.29는 버렸고 0.32부터 받았습니다. 판을 올렸다면 기동 로그에 「Default vLLM sampling parameters have been overridden」 줄이 있고 그 안에 두 값이 다 있는지 확인합니다. 출력 상한에서 잘린 응답은 오류 없이 돌아오므로, OwlAI는 이런 응답을 따로 WARNING으로 남깁니다(「구축형 제품은 JSON 로그 하나로 관측하고, 요청 ID로 워커까지 잇는다」).

OwlAI는 서버 자리 수만큼만 동시에 보낸다

서버 상한보다 많이 보낸 요청은 서버 앞에서 줄을 설 뿐입니다. 그 줄에는 다른 사용자의 채팅 턴도 함께 섭니다. 그래서 OwlAI도 요청을 한꺼번에 여러 개 보내는 곳에서는 대상이 자체 호스팅 모델인지 봅니다.

딥 리서치 보고서를 다듬는 단계가 그런 곳입니다. 이 단계는 보고서의 문단과 목록 항목을 블록으로 나눠 블록마다 모델을 부릅니다. 클라우드 모델이면 블록 12개를 동시에 부르고, 자체 호스팅 모델이면 4개만 부릅니다. 클라우드 모델은 블록 호출 하나가 평균 2초로 짧아서, 동시 수를 늘릴수록 보고서가 나오기 직전의 대기가 줄어듭니다. 실측에서 가장 늦은 블록을 기다린 시간은 동시 4에서 18.2초, 12에서 6.5초였고 16에서는 5.7초로 거의 줄지 않았습니다. 자체 호스팅 모델은 서버 자리가 넷이라 그보다 많이 보내면 남의 요청 앞을 막습니다. 두 동시 수는 설정으로 바꿀 수 있으므로, 서버의 max_num_seqs를 바꿨다면 자체 호스팅 쪽 값도 같이 맞춥니다.

설계에서 고른 것

처리량 대신 첫 토큰까지의 시간을 골랐습니다. 상한을 8로 올리면 같은 GPU로 4%를 더 처리합니다. 대신 모든 요청의 TTFT가 65% 늘고 응답마다 토큰 속도가 절반 아래로 떨어집니다. 업무 질의는 사람이 화면 앞에서 답을 기다리므로 늦게 시작하는 답의 비용이 처리량 4%보다 큽니다. 줄이 길어질 만큼 사용자가 많다면 상한을 올리는 대신 GPU를 더 둡니다.

정밀도 한 단계 대신 KV 캐시 자리를 골랐습니다. 공식 FP8이 Int4보다 원래 모델에 가깝습니다. 그래도 48 GB에서 FP8을 고르면 긴 요청 넷을 함께 담지 못해 선점이 생기고, 긴 입력이 대부분인 부하에서는 그 손해가 정밀도 차이보다 큽니다.

출력 상한은 요청마다 맡기는 대신 서버 전역에 걸었습니다. 대가는 32,768토큰보다 긴 정상 출력도 잘린다는 점입니다. 클라이언트가 보낸 값에만 기대면 상한을 빠뜨린 호출 하나가 자리를 수십 분 붙잡을 수 있습니다. 서버에 걸면 어느 호출이든 같은 상한 안에서 끝납니다.

투기적 디코딩(MTP)으로 얻을 생성 속도 대신 기동 안정성을 골랐습니다. vLLM 0.26에서 양자화 MoE 모델과 함께 켜면 서버가 기동하지 못했습니다.

입력이 긴 업무 질의를 소비자급 GPU 한두 장으로 서빙한다면 이 값들을 출발점으로 삼을 수 있습니다. 입력이 짧은 대화가 대부분이라면 디코드가 부하를 차지하므로, 자기 운영 기록의 입력 길이로 동시성 곡선부터 다시 잽니다.

어떤 문제를 해결하나요?

설치·운영과 품질의 다른 글