서빙 설정을 고르는 판단은 결국 부하 테스트 결과에 기댄다. 그런데 부하 테스트는 설계를 조금만 잘못해도 실제보다 좋은 숫자를 낸다. 길이를 고정하거나, 응답이 와야 다음 요청을 보내거나, 측정 구간을 짧게 잡으면 그렇다. 이 장은 운영 트래픽에서 겪을 지연을 미리 재기 위한 테스트 조건을 다룬다.
길이 분포
테스트 요청의 프롬프트 길이와 출력 길이는 운영 로그에서 뽑는다. 두 길이를 따로 뽑아 섞지 않고 요청 단위로 짝을 지어 함께 뽑는다. 긴 문서를 붙인 요청은 출력이 짧은 요약이고 짧은 질문은 출력이 긴 설명인 것처럼, 두 길이 사이에는 상관이 있기 때문이다. 따로 뽑으면 긴 프롬프트와 긴 출력이 만나는 현실에 없는 요청이 생기거나, 현실에 있는 조합이 빠진다.
로그가 아직 없는 새 서비스라면 비슷한 서비스의 분포를 빌리거나, 예상 사용 사례별로 대표 요청을 만들어 비율대로 섞는다. 어느 쪽이든 평균 길이 하나로 대신하지 않는다. 같은 평균이라도 분포의 꼬리가 길면 긴 요청이 다른 요청을 붙잡는 시간과 KV 캐시 점유가 크게 달라진다.
출력 길이를 재현하려면 모델이 정해진 길이만큼 생성하도록 만들어야 한다. 실제 모델은 같은 프롬프트에도 매번 다른 길이로 답하므로 로그의 출력 길이를 그대로 재현하려면 종료 토큰을 무시하게 하고 최대 생성 길이를 그 값으로 둔다. vLLM의 샘플링 설정에는 종료 토큰이 나와도 생성을 계속하는 ignore_eos와 최대 생성 길이인 max_tokens가 있다. 이렇게 만든 출력은 내용이 의미 없지만 부하 테스트가 재는 것은 길이와 시간이므로 상관없다.
도착 과정
요청을 보내는 방식은 두 가지다.
| 방식 | 동작 | 재는 것 |
|---|---|---|
| 닫힌 루프 (동시 요청 수 고정) | 클라이언트 N개가 각자 요청을 보내고 응답이 끝나면 다음 요청을 보낸다 | 동시 요청 N개일 때의 지연과 처리량 |
| 열린 루프 (요청률 고정) | 응답과 상관없이 정해진 시각마다 요청을 보낸다 | 초당 λ개가 들어올 때의 지연 |
닫힌 루프는 구현이 쉽고 동시 요청 수에 따른 변화를 보기 좋다. 다만 서버가 느려지면 클라이언트도 다음 요청을 늦게 보내므로 과부하가 걸려도 대기열이 끝없이 쌓이지 않는다. 서버가 버티지 못하는 부하에서도 지연이 적당히 늘어나는 것처럼 보인다.
실제 사용자는 서버의 응답을 기다렸다가 다음 사람이 요청하지 않는다. 운영 트래픽은 열린 루프에 가깝다. 요청률이 서버의 처리 능력을 넘으면 대기열이 계속 길어지고 TTFT가 끝없이 늘어나는데, 열린 루프 테스트만 이 현상을 재현한다. 요청 간격은 운영 로그의 도착 시각을 그대로 재생하거나, 독립적인 사용자가 많은 서비스라면 푸아송 과정(간격이 지수 분포)으로 만든다. 요청이 몰리는 순간의 버스트를 재현하려면 로그 재생이 낫다.
리틀의 법칙
두 방식은 리틀의 법칙으로 이어진다. 안정 상태의 시스템에서 시스템 안에 있는 평균 요청 수 L은 도착률 λ와 요청 하나가 시스템에 머무는 평균 시간 W의 곱이다.
L = λ × W
초당 10개의 요청이 들어오고 요청 하나가 평균 8초 동안 생성된다면 서버 안에는 평균 80개의 요청이 동시에 있다. 반대로 닫힌 루프에서 동시 요청 80개로 잰 평균 응답 시간이 8초였다면 그 설정은 초당 10개의 요청을 처리한 셈이다. 요청률 목표가 주어졌을 때 몇 개의 동시 요청을 감당해야 하는지, KV 캐시 메모리가 그만큼을 담을 수 있는지를 이 식으로 먼저 어림한다.
이 식은 시스템이 안정 상태일 때만 성립한다. 도착률이 처리 능력을 넘으면 L이 시간에 따라 계속 커지므로 테스트 결과에서 대기열 길이가 측정 구간 내내 늘어나고 있다면 그 요청률은 이미 용량을 넘은 것이다.
측정 구간
측정은 워밍업, 안정 구간, 마무리로 나눈다. 서버를 띄운 직후의 요청은 커널 준비와 메모리 할당 비용을 치르고, 테스트 시작 직후에는 배치가 아직 차지 않았다. 테스트 끝 무렵에는 새 요청이 줄어 배치가 비어 간다. 지연과 처리량은 안정 구간의 요청만으로 계산한다.
안정 구간은 가장 긴 요청이 여러 번 시작하고 끝날 만큼 길게 둔다. 출력이 수천 토큰인 요청이 섞인 부하에서 1분짜리 측정은 그런 요청을 몇 개 담지 못해 p99가 흔들린다. 같은 조건으로 여러 번 반복해 백분위가 얼마나 흔들리는지도 함께 남긴다.
요청마다 남길 값은 운영 계측과 같다. 프롬프트·출력 토큰 수, 대기열 대기 시간, TTFT, 토큰 사이 간격 전체, 종료 사유를 남기면 결과를 다시 나눠 볼 수 있다. 서버 쪽에서는 실행 중인 요청 수, 대기 중인 요청 수, KV 캐시 사용률, 선점 횟수를 같은 시간축으로 기록한다.
결과 읽기
요청률을 단계적으로 올리며 단계마다 TTFT와 TPOT의 백분위, 목표를 지킨 요청의 비율을 기록한다. 결과를 요청률을 가로축으로 놓고 그리면 처리량은 계속 오르다가 평평해지고, 지연은 한동안 평평하다가 어느 지점에서 가파르게 오른다. 목표를 지킨 요청 비율이 정한 백분위 아래로 떨어지기 직전의 요청률이 그 설정의 굿풋이다.
설정을 비교할 때는 이 굿풋을 나란히 놓는다. 그리고 결과에는 모델, 엔진 버전, 설정, 장비, 길이 분포, 도착 과정을 함께 적는다. 이 가운데 하나만 바뀌어도 숫자가 달라지기 때문에, 조건 없이 남은 숫자는 나중에 비교할 근거가 되지 못한다.