모델 운영

MLOPS / 63번째 글

서빙 벤치마크 — 숫자 하나로 말하면 반드시 틀린다

「초당 40건 처리」나 「지연 900ms」는 그 자체로는 아무 말도 하지 않습니다. 무엇을 함께 적어야 재현되는 수치가 되는지, 부하를 어떻게 걸어야 실제와 같아지는지 정리합니다.

PALDYN Team11 MIN READ

지난 글까지 서빙 설정을 여러 번 바꿨다. 어댑터를 함께 태우고, 복제본을 자동으로 늘리고, 카드를 나눠 썼다. 그때마다 따라오는 질문이 하나 있다. 정말 좋아졌는가. 이 질문에 답하려고 재는데, 재는 방식이 어긋나 있으면 바꾼 것과 무관한 숫자를 비교하게 된다.

지표는 넷이고 각각 다른 것을 말한다

먼저 이름부터 정리해 둔다. 스트리밍으로 토큰을 흘리는 서비스에서는 「응답 시간」 하나로 뭉뚱그릴 수 없다.

지표 무엇을 재나 누가 신경 쓰나
첫 토큰 지연(TTFT) 요청부터 첫 글자가 보일 때까지 사용자. 화면이 멈춰 보이는 시간이다
토큰 간 지연(ITL) 글자와 글자 사이 사용자. 읽는 속도보다 느리면 답답하다
전체 응답 시간 요청부터 마지막 토큰까지 뒤에서 부르는 프로그램. 다 받아야 다음으로 간다
처리량 초당 처리한 요청 수·토큰 수 비용을 내는 쪽

이 넷이 서로 반대로 움직인다는 점이 벤치마크를 어렵게 만든다. 배치를 키우면 처리량은 오르고 지연은 나빠진다. 연속 배칭에서 이미 본 맞바꿈이다.

처리량과 지연은 한 곡선 위에 있다

그래서 「우리 서버는 초당 40건을 처리한다」는 문장은 절반만 참이다. 지연을 얼마까지 허용하느냐에 따라 그 숫자가 달라지기 때문이다.

처리량과 지연은 한 곡선 위의 두 좌표다

부하를 조금씩 올리며 재면 곡선 하나가 나온다. 왼쪽에서는 부하를 올려도 지연이 거의 안 변하다가, 어느 지점부터 급하게 꺾인다. 이 꺾이는 자리를 무릎(knee)이라 부른다. 여기가 대기열이 자라기 시작하는 지점이고, 운영 지점은 이 앞에 두는 것이 맞다.

재는 방식이 여기서 정해진다. 한 번 돌려 숫자 하나를 얻는 것이 아니라, 부하 단계를 여러 개 두고 곡선을 그린다. 그리고 한 단계에서 나온 값들끼리 묶어 적는다.

- 부하: 20 req/s
  TTFT_p50: 180ms
  TTFT_p95: 310ms
  ITL_p95: 24ms
  처리량: 20.0 req/s
  GPU_이용률: 61%
- 부하: 40 req/s
  TTFT_p50: 240ms
  TTFT_p95: 900ms
  ITL_p95: 31ms
  처리량: 39.8 req/s
  GPU_이용률: 82%

「처리량 40, 지연 900ms」를 따로 적으면 둘이 같은 실행에서 나온 값인지 알 수 없다. 실제로 흔한 사고가 처리량은 최대 부하에서, 지연은 최소 부하에서 가져와 나란히 싣는 것이다. 둘 다 참인 숫자지만 함께 성립하지는 않는다.

부하를 어떻게 거는가가 결과를 바꾼다

부하 생성기가 요청을 내보내는 방식에 두 갈래가 있고, 둘이 아주 다른 것을 잰다.

부하 생성기가 요청을 내보내는 두 가지 방식

닫힌 루프는 가상 사용자 NN명이 각자 응답을 받은 뒤에 다음 요청을 보내는 방식이다. 구현이 쉽고 대부분의 도구가 기본으로 쓴다. 문제는 서버가 느려지면 부하도 같이 줄어든다는 것이다. 서버가 감당 못 하는 구간을 아예 만들지 못하므로, 용량의 한계를 재려던 시험이 「서버가 편안한 지점」만 되풀이해 잰다.

열린 루프는 시계에 맞춰 정해진 속도로 계속 보낸다. 실제 사용자는 우리 서버 사정을 봐주지 않으므로 이쪽이 현실에 가깝다. 서버가 밀리면 대기열이 자라고, 그 자란 만큼이 지연에 나타난다.

여기서 한 가지를 더 지켜야 한다. 열린 루프에서도 지연을 「실제로 보낸 시각」부터 재면 밀린 시간이 사라진다. 생성기 자신이 밀려서 100ms 늦게 보냈다면 그 100ms도 사용자가 기다린 시간이다. 보내려던 시각부터 세야 한다. 이 함정에는 이름도 붙어 있다 — 협조적 누락(coordinated omission)이라 부른다. 부하 생성기가 서버 사정에 맞춰 주는 바람에 나쁜 구간이 통계에서 빠지는 현상이다.

워크로드를 안 적으면 재현이 안 된다

같은 서버, 같은 부하인데 결과가 두 배 차이 나는 일이 흔하다. 대개 요청의 모양이 달라서다.

무엇이 왜 결과를 바꾸나
입력 길이 첫 토큰 지연은 입력 길이에 거의 비례한다
출력 길이 전체 시간과 배치 점유 시간을 정한다
길이의 분포 평균이 같아도 편차가 크면 지연 꼬리가 두꺼워진다
앞부분 공유율 시스템 프롬프트가 같으면 프리픽스 캐시가 걸려 확 빨라진다
도착 분포 균등 간격과 포아송은 대기열 길이가 다르다
스트리밍 여부 첫 토큰 지연은 스트리밍일 때만 의미가 있다

「입력 512 · 출력 128 고정」으로 잰 결과는 실제 트래픽과 거의 무관하다. 로그에서 길이 분포를 뽑아 그대로 쓰는 것이 가장 확실한 방법이다. 그럴 수 없다면 최소한 분포를 적어 두고, 앞부분 공유율은 반드시 밝힌다 — 같은 프롬프트를 천 번 보내 얻은 숫자는 캐시 적중률 100%의 성능이라 실제의 몇 배가 나온다.

자주 새는 자리

새는 곳 무슨 일이 일어나나 막는 법
워밍업 없음 첫 요청들이 컴파일·캐시 준비를 다 뒤집어쓴다 30초쯤 돌린 뒤부터 집계
평균만 봄 열 명 중 한 명이 5초를 기다려도 평균은 멀쩡하다 p50·p95·p99를 함께
실행이 짧음 오토스케일링·캐시 적중률이 자리 잡기 전에 끝난다 부하 단계마다 3~5분
클라이언트 병목 생성기가 먼저 포화돼 서버가 한가해 보인다 생성기의 CPU와 응답 대기 수를 함께 기록
토크나이저 차이 「초당 토큰」이 모델마다 다른 단위가 된다 모델 간 비교는 문자 수나 요청 수로
실패를 안 셈 밀려서 거절된 요청이 통계에서 빠져 지연이 좋아 보인다 오류율을 항상 함께 적는다

마지막 줄이 특히 조용하다. 서버가 과부하에서 요청을 거절하기 시작하면 남은 요청은 빨라진다. 지연 그래프만 보면 오히려 좋아진 것처럼 보이므로, 오류율을 옆에 놓지 않으면 포화를 성능 개선으로 읽는다.

무엇을 기록해 둘 것인가

몇 주 뒤에 「그때 그 숫자」와 비교하려면 결과만으로는 부족하다. 함께 남겨야 재현이 된다.

  • 모델과 정밀도: 이름, 양자화 여부, 가중치 버전
  • 엔진과 설정: 엔진 이름과 버전, 최대 배치, KV 캐시 비율, 텐서 병렬 수
  • 하드웨어: 카드 종류와 장수, 나눠 쓰고 있다면 그 방식
  • 워크로드: 입출력 길이 분포, 앞부분 공유율, 도착 분포
  • 부하 방식: 열린 루프인지 닫힌 루프인지, 지연을 어느 시각부터 셌는지
  • 집계: 워밍업 제외 구간, 퍼센타일, 오류율

길어 보이지만 대부분 실행 스크립트에서 자동으로 뽑아 붙일 수 있는 것들이다. 손으로 적기 시작하면 빠지는 항목이 생기므로, 결과 파일에 설정을 함께 써 넣는 것을 습관으로 두는 편이 낫다.

정리

벤치마크의 목적은 자랑할 숫자를 얻는 것이 아니라 어디까지 밀어도 되는지 아는 것이다. 그러려면 곡선을 그려 무릎을 찾고, 그 앞에 운영 지점을 두고, 다음에 설정을 바꿨을 때 같은 방식으로 다시 그려 비교하면 된다.

숫자 하나만 남기면 비교할 수 없다. 조건을 함께 적은 곡선 하나가 훨씬 오래 쓸모 있다.


읽어주셔서 감사합니다. 😊

LATEST

모델 운영의 최신 글

모델 운영2026.09.04

KV 캐시 양자화 — 가중치보다 이쪽이 먼저 넘친다

긴 문맥에서 GPU 메모리를 실제로 잡아먹는 것은 가중치가 아니라 KV 캐시입니다. 캐시 크기를 계산하는 법, K와 V를 다르게 다뤄야 하는 이유, 어디까지 줄여도 되는지를 정리합니다.

16 MIN
모델 운영2026.09.04

양자화 보정 — 데이터 128개가 모델 품질을 정한다

양자화에서 스케일을 정하는 절차가 보정입니다. 무엇을 재는지, 데이터를 어디서 몇 개 뽑아야 하는지, 자르는 지점을 어떻게 고르는지, 그리고 잘못된 보정이 어떤 모양으로 드러나는지를 정리합니다.

21 MIN
모델 운영2026.09.03

과제 특화 증류 — 큰 모델의 답을 작은 모델에 옮긴다

범용 성능이 아니라 우리 과제 하나만 잘하는 작은 모델을 만드는 방법입니다. 교사에게 무엇을 받아야 하는지, 데이터를 어떻게 모으고 거르는지, 손익 분기가 어디인지를 정리합니다.

15 MIN