Quant Trading Bot Devlog

Read in English

27B dense 모델을 RTX 5090 한 장에서 2배 빠르게 - 병목은 동시성 숫자가 아니라 KV 캐시였다

100종목 야간 배치를 6시간대에서 3시간 안쪽으로 줄인 과정과, 그 숫자를 믿으면 안 되는 이유들

밤마다 도는 100종목 분석 배치에 27B dense 모델을 쓰기로 했습니다. 이 모델을 RTX 5090 한 장에서 얼마나 빠르게 돌릴 수 있는지를 실험했고, 지금까지의 결과를 정리합니다.

결과부터 말하면, 프로덕션 기준이던 종목당 약 226초를 재생 실험에서 약 102초까지 줄였습니다. 다만 이 102초는 24종목, 반복 1회짜리 값이라 아직 프로덕션 실측이 아닙니다. 그 한계까지 같이 적겠습니다.

이 배치가 실제로 하는 일

숫자를 재현하려는 분들을 위해, 먼저 워크로드를 적어둡니다.

종목 하나를 분석하는 데 LLM 호출이 17번 일어납니다(멀티 에이전트 파이프라인). 노드별 평균은 이렇습니다.

노드 평균 입력 평균 출력 평균 지연(슬롯 3개)
Market 분석 13.3k 토큰 3.0k 토큰 68.4초
Sentiment 분석 2.95k 0.87k 19.7초
Portfolio Manager 9.2k 0.52k 12.9초
Trader 1.2k 0.35k 7.9초

종목당 합계는 입력 약 188k 토큰, 출력 약 25.8k 토큰입니다. 시간을 쪼개면 prefill이 약 38초, decode가 약 540초라서 decode가 93%입니다. 그래서 prefill 최적화의 여지가 작다고 판단했습니다.

입력이 가장 길어지는 노드는 Conservative Analyst입니다. 프로덕션 9밤 동안 관측한 최대 입력이 33,487 토큰이었고(Bear 33,451, Bull 32,699), 그래서 서버의 최대 길이를 34,816으로 잡았습니다. 이미 빠듯해서 이 값을 더 줄일 수 없습니다. 넘으면 400 에러로 그 호출이 실패합니다.

샘플링 세팅

샘플링은 temperature 0.7, top_p 0.8, top_k 20, presence_penalty 1.5입니다. 모델 카드의 non-thinking(추론 끔) 모드 권장값을 그대로 썼습니다.

09-24 이전 프로덕션 9밤은 달랐습니다. 체크포인트 기본값(1.0/0.95/20)에 temperature만 0.8로 덮어서, 실효 샘플링이 0.8 / 0.95 / 20 / presence 0이었습니다. 세 축을 한꺼번에 바꾼 셈입니다.

효과는 출력 꼬리에서 가장 선명했습니다.

프로덕션 9밤(29,630호출) 새 샘플링 블라인드 런(1,700호출)
출력 p50 / p90 / p99 1,530 / 2,837 / 5,148 토큰 1,337 / 2,670 / 4,402 토큰
6,000토큰 초과 호출 104건(0.35%) 2건(0.12%)
출력 최대 26,518 토큰 9,291 토큰

presence_penalty가 폭주하는 꼬리를 잘라낸 것으로 보입니다. 다만 세 축이 동시에 바뀌어서 어느 축 덕분인지는 분리하지 못했습니다.

출발점은 XTX에서 21.7시간이었다

이 모델을 처음 돌린 건 두 달 전 7900 XTX 한 장(새 창)이었습니다. llama.cpp에 4비트 양자화(Q4_K_M)로 돌리자 100종목에 30시간 안팎이 걸렸습니다.

여기에 MTP(투기적 디코딩) 별도 헤드를 붙이자 약 21.7시간으로 줄었습니다. 디코드 속도가 초당 35토큰대에서 57토큰대로 올라간 덕분입니다. 그래도 야간 창에 들어가는 목표(10시간 안쪽)의 두 배가 넘었습니다.

이때 하나 배운 게 있습니다. 벽시계 시간만으로 비교하면 틀린다는 점입니다. 어떤 런은 출력이 길게 나온 만큼 3시간 넘게 과대 측정됐습니다. 그래서 그 뒤로는 벽시계 대신 prefill/decode 처리량으로 팔을 비교했습니다.

5090으로 옮기고 스택을 바꿨다

5090으로 옮기면서 서빙 스택도 llama.cpp에서 vLLM으로 바꿨습니다. 양자화는 4비트 부동소수(NVFP4) 체크포인트를 쓰고, KV 캐시는 fp8로 줄였습니다.

첫 프로덕션 설정은 동시 처리 슬롯 3개였습니다. 이 설정으로 9밤 동안 돌린 프로덕션 실측이 종목당 약 226초, 100종목 기준 약 6.3시간이었습니다. 목표는 통과했지만 여유가 크지 않았습니다.

그 전에 동시 슬롯을 14개로 크게 잡은 첫 시도는 메모리 부족으로 접었습니다. 이때는 숫자만 보고 "슬롯을 늘리면 빨라지겠지"라고 생각했습니다.

동시성을 늘렸더니 1.77배 빨라졌다

이번엔 같은 입력을 얼려서(재생 실험) 설정만 바꿔 비교했습니다. 슬롯 3개인 기준선은 100종목을 다 돌려 종목당 204.5초(5.68시간)가 나왔습니다.

슬롯을 14개로 늘린 런은 28종목에서 종목당 115.8초(3.22시간)였습니다. 기준선의 1.77배 빠릅니다. 집계 처리량으로 보면 초당 126토큰에서 233토큰으로 올랐습니다.

그런데 서버 로그를 보니 이상했습니다. 동시에 실제로 돌고 있는 요청이 14개가 아니라 6~9개였습니다. 대기열이 늘 5~9개 쌓여 있었고, 한 호출의 지연은 중앙값 기준 32초에서 70초로 늘었습니다.

병목은 슬롯 수가 아니라 KV 캐시였다

원인은 KV 캐시 용량이었습니다. 슬롯 설정이 14여도 캐시가 다 차면 새 요청이 들어가지 못합니다. 캐시 사용률이 최대 100%까지 찼습니다.

이 모델은 완전한 dense가 아니라 하이브리드 구조입니다. 64개 층 중 48개가 선형 어텐션이고 16개만 전체 어텐션인데, 그래서 KV 계산이 단순한 dense와 다릅니다. "슬롯 14개 = 14배 동시성"이라는 직관이 여기서 깨집니다.

그래서 설정한 동시성 숫자는 의미가 없고, 실제로 실현된 동시성을 봐야 한다는 걸 배웠습니다. 로그에서 Running 요청 수를 보지 않았다면 "14-way로 1.77배"라고 잘못 기록할 뻔했습니다.

MTP를 붙였더니 2배가 됐다

다음은 투기적 디코딩입니다. 이번엔 모델에 내장된 MTP 헤드를 vLLM에서 켜고, 투기 토큰은 2개로 했습니다. 동시에 슬롯을 8개로 줄이고 GPU 메모리 사용률을 0.95로 올렸습니다.

24종목을 돌린 결과 종목당 102.0초(2.83시간)가 나왔습니다. 기준선의 2.0배이고, MTP 없이 슬롯 14개인 런보다도 12% 빨랐습니다. 집계 처리량은 로그 최댓값 기준 초당 437토큰까지 갔고, 투기 토큰 수용률은 창별로 65~79%였습니다.

이 런에서도 설정은 슬롯 8개인데 실현 동시성은 4~6개였고 KV 사용률은 최대 100%였습니다. 병목이 여전히 KV라는 뜻입니다. 시간을 쪼개 보면 디코드가 약 93%라서, prefill 쪽 최적화는 여지가 작습니다.

이 숫자를 믿으면 안 되는 이유

지금 채택한 설정은 이 102초입니다. 하지만 이 숫자에는 한계가 분명합니다.

측정 방식에서도 실수를 한 번 했습니다. 종목당 초를 이미 동시성이 반영된 벽시계로 계산했는데, 슬롯 수로 한 번 더 나누는 계산을 하려 했습니다. 이중으로 나누면 속도를 몇 배씩 과대평가합니다. 다행히 리뷰에서 걸렸습니다.

품질 쪽 수치

속도만 빨라지고 결과가 나빠졌다면 의미가 없으니, 지금 아는 만큼의 품질 수치도 적어둡니다. 결론부터 말하면 품질은 아직 확인하지 못했다입니다.

그래서 배치 속도를 올린 뒤의 계획은 종목 하나를 여러 번 뽑아 평균 점수를 쓰는 것입니다. 단일 런의 등급으로 IC를 재면 검출력이 노이즈에 먹힙니다.

시도했다가 접은 것들

앞으로

다음 주 첫 프로덕션 밤에 실제 100종목 시간을 재보고, 그 값이 재생 실험의 2.83시간과 얼마나 다른지 확인합니다. 그 차이가 이 글의 진짜 결론입니다.

남는 시간은 종목 하나를 여러 번 뽑아 평균을 내는 쪽에 쓸 계획입니다. 프로덕션 결과가 나오면 후속 글로 정리하겠습니다.

같은 방식으로 R9700에서 35B MoE 모델을 튜닝한 기록은 다음 글(새 창)에 있습니다. 백엔드부터 동시성, MTP까지 결론이 꽤 다르게 나왔습니다.