서빙 설정을 조정해 보면 처리량을 올리는 수단 대부분이 지연을 함께 늘린다. 둘이 같은 값, 즉 한 스텝에 함께 계산하는 요청 수로 움직이기 때문이다. 그래서 튜닝은 둘 중 하나를 먼저 고정하고 나머지를 최대화하는 문제가 되고, 대화형 서비스에서는 지연 쪽을 고정한다.
배치와 처리량
디코드 한 스텝은 대역폭에 묶여 있다. 스텝마다 가중치 전체를 메모리에서 읽어야 하고 그 시간이 스텝 시간의 하한을 정한다. 그런데 읽어 온 가중치에 요청 하나의 토큰만 곱하든 요청 32개의 토큰을 함께 곱하든 가중치를 읽는 양은 같다. 엔진이 한 스텝에 요청 B개를 묶으면 스텝 시간은 거의 그대로인 채 그 스텝에서 토큰이 B개 나온다.
처리량(토큰/s) ≈ B ÷ 스텝 시간
그래서 묶는 요청 수가 늘면 처리량은 그 수에 거의 비례해 오른다. 연산 강도는 B 근처로 올라가므로 B가 능선(H100 SXM, BF16 기준 약 295)에 가까워질 때까지는 연산기에 여유가 있다.
다만 요청마다 쌓인 KV 캐시도 스텝마다 읽어야 한다. 가중치는 요청 수와 상관없이 한 번만 읽지만 KV 캐시는 요청마다 따로 있어서, 묶은 요청이 많고 문맥이 길수록 스텝마다 읽는 바이트가 가중치에 더해 계속 늘어난다. 처리량이 B에 정확히 비례하지 않고 점점 완만해지는 것은 이 때문이다.
배치와 지연
처리량이 오르는 동안 요청 하나하나가 겪는 시간은 반대로 나빠진다. 같은 배치를 요청 하나의 쪽에서 보면 지연이 세 곳에서 늘어난다.
스텝이 길어진다. 묶은 요청이 늘면 KV 캐시를 읽는 바이트와 연산이 함께 늘어 스텝 하나가 조금씩 길어진다. 요청 하나는 스텝마다 토큰 하나를 받으므로 스텝 시간이 곧 그 요청의 토큰 간격이다. 처리량이 오르는 동안 요청 하나하나의 TPOT도 함께 오른다.
새 요청의 프리필이 끼어든다. 새 요청은 프롬프트를 읽어야 생성을 시작할 수 있다. 프리필은 연산 천장에 걸리는 무거운 계산이라, 디코드와 같은 스텝에 들어가면 그 스텝의 디코드 요청들이 프리필이 끝날 때까지 다음 토큰을 받지 못한다. 긴 프롬프트가 자주 들어오는 부하에서는 이 효과가 TPOT의 꼬리를 키운다.
대기열이 생긴다. 한 번에 묶을 수 있는 요청 수에는 상한이 있다. 엔진 설정의 상한일 수도 있고 KV 캐시를 담을 메모리의 한계일 수도 있다. 자리가 차면 새 요청은 프리필도 시작하지 못하고 대기열에서 기다리며, 이 시간은 그대로 TTFT에 더해진다.
처리량은 한 번에 얼마나 많이 모아 계산했느냐로 오르고 지연은 요청 하나가 그렇게 모이는 동안 얼마나 기다렸느냐로 는다. 둘 다 한 스텝에 묶는 요청 수로 움직이므로 그 수를 올리면 처리량과 지연이 함께 오른다.
지연 목표
지연 목표(SLO)는 지표, 백분위, 시간 창을 함께 정해야 검증할 수 있다. 대화형 서비스라면 “피크 시간대 TTFT p99 1초 이하, TPOT p99 100ms 이하”처럼 적는다. 이 값은 예시이고 실제 값은 사용 맥락이 정한다. 사람이 화면에서 응답을 읽는 서비스와, 다른 프로그램이 응답을 받아 다음 단계를 진행하는 에이전트 호출은 기다릴 수 있는 시간부터 크게 차이 난다.
목표를 먼저 정하면 튜닝 문제가 하나로 좁혀진다. 이 목표를 지키면서 장비 한 대가 받아 낼 수 있는 최대 부하는 얼마인가. 처리량을 먼저 정하면 이 질문에 답할 수 없다. 초당 토큰 수에는 그 수치를 내는 동안 요청들의 지연이 어디까지 밀려났는지가 담겨 있지 않기 때문이다.
목표에 TTFT와 TPOT를 따로 두는 데에도 이유가 있다. 설정 하나가 두 지표를 반대 방향으로 움직이는 경우가 많다. 프리필을 우선하면 TTFT가 좋아지는 대신 진행 중인 요청의 TPOT가 흔들리고, 디코드를 우선하면 TPOT는 고르게 유지되지만 새 요청의 TTFT가 늘어난다. 하나로 합친 목표로는 이런 설정을 비교할 수 없다.
굿풋
DistServe는 이 생각을 굿풋(goodput)이라는 지표로 정리했다. TTFT와 TPOT 목표를 모두 지키면서 GPU 한 장이 처리할 수 있는 최대 요청률이다. 목표를 넘긴 요청은 아무리 많이 처리해도 굿풋에 들어가지 않는다.
굿풋으로 보면 처리량 그래프와 모양이 달라진다. 배치 상한을 올리면 초당 토큰 수는 계속 오르지만 어느 지점부터는 목표를 넘기는 요청이 늘어 굿풋이 오히려 떨어진다.
그래서 설정을 비교할 때는 동시 요청 수를 올려 가며 목표를 지킨 요청의 비율을 함께 기록하고, 그 비율이 정한 백분위 아래로 떨어지기 직전의 부하를 그 설정의 용량으로 삼는다. 배치 상한, 프리필을 나누는 크기, KV 캐시에 할당하는 메모리 비율처럼 서로 얽힌 설정을 고를 때도 기준은 굿풋 하나로 둔다.
대화형 요청과 오프라인 작업
같은 모델을 쓰는 작업 중에도 지연 목표가 없는 작업이 있다. 문서 일괄 요약, 데이터 라벨링, 평가 실행처럼 결과가 정해진 기한 안에 모이기만 하면 되는 작업이다. 이런 작업은 처리량만 보면 되므로 배치를 최대한 키워 연산기를 채우는 편이 유리하다.
두 종류의 부하를 같은 장비에 섞으면 오프라인 작업의 큰 배치와 긴 프롬프트가 대화형 요청의 TPOT를 끌어올린다. 다루는 방법은 두 가지다.
- 시간이나 장비로 분리한다. 대화형 트래픽이 적은 시간대에 오프라인 작업을 몰거나 별도 장비에서 돌린다
- 같은 장비 안에서 우선순위를 둔다. 대화형 요청이 먼저 스텝에 들어가게 하고, 오프라인 요청은 동시 처리 수에 상한을 두어 대화형 요청이 목표를 지키고 남는 자리만 쓰게 한다
어느 쪽이든 판단 기준은 같다. 대화형 요청의 지연 목표를 먼저 지키고 오프라인 작업의 처리량은 그 목표를 지키고 남는 용량 안에서 최대화한다.