AI·

vLLM 요약이 너무 느렸다 — 배치와 멀티모델 큐 튜닝

수천 건의 문서를 로컬·사내 LLM으로 요약하며 처리량을 끌어올린 기록. 모델을 함께 묶지 않는 독립 큐, 워커·claim·flush 다이얼 맞추기, max_tokens의 함정, 그리고 걸린 시간을 로그로 드러내기까지 정리합니다.

원문 미러가 준비됐으니(지난 글), 이제 이걸 LLM으로 요약해 위키로 만들 차례였습니다. 문제는 양이었습니다. 수천 건을 요약해야 했고, 처음 돌렸을 때 속도가 답이 안 나왔습니다. 이 글은 그 처리량을 끌어올린 기록입니다.

수천 건을 요약해야 했다

요약 작업(ingest)은 이런 모양입니다.

  • synced/의 원문 문서를 하나씩 읽는다
  • 사내/로컬 LLM에 "이 문서를 사서처럼 요약하고, 관련 문서와 연결해 달라"고 요청한다
  • 결과를 wiki/에 쓴다

건당 몇 초씩만 잡아도 수천 건이면 몇 시간입니다. 게다가 로컬 GPU에 올린 모델 하나로 순차 처리하면 처리량이 처참했습니다.

첫 병목: 한 모델로는 답이 없다

가장 먼저 눈에 띈 건 로컬 모델의 처리량이었습니다. 워크스테이션 GPU(RTX A6000급) 한 장에 올린 모델 하나로는 초당 몇 건을 넘기기 어려웠습니다.

다행히 쓸 수 있는 모델이 더 있었습니다. 사내 게이트웨이로 서빙되는 오픈 모델 몇 종이 놀고 있었죠. 여러 모델을 동시에 요약에 투입하면 처리량을 배로 늘릴 수 있었습니다.

여기서 첫 설계 실수가 나옵니다.

모델을 함께 묶지 말자 — 독립 큐

처음엔 두 모델을 한 배치로 묶어서 돌렸습니다. 32건을 뽑아 절반씩 두 모델에 나눠 주고, 둘 다 끝나면 다음 32건을 뽑는 식이었죠.

문제는 모델마다 속도가 다르다는 것이었습니다. 빠른 모델이 자기 몫을 끝내도, 느린 모델이 끝날 때까지 놀면서 기다려야 했습니다. 배치의 속도는 늘 가장 느린 모델에 묶였습니다.

그래서 구조를 바꿨습니다. 공용 큐 하나를 두고, 각 모델이 끝나는 대로 알아서 다음 걸 꺼내 가게 했습니다.

공용 큐 (수천 건) 끝나면 꺼감 끝나면 꺼감 빠른 모델 느린 모델
모델을 배치로 묶지 않고, 각자 끝나는 대로 공용 큐에서 꺼내 간다

이러면 빠른 모델은 계속 일합니다. 느린 모델은 느린 만큼만 가져갑니다. 서로를 기다리지 않으니 전체 처리량 = 두 모델의 합에 가까워집니다. 배치를 억지로 맞추던 때보다 눈에 띄게 빨라졌습니다.

워커·claim·flush 다이얼 맞추기

큐 구조를 잡고 나선 세 개의 손잡이를 조정했습니다.

  • 모델당 워커 수: 한 모델에 동시에 몇 개의 요청을 태울지. 늘리면 GPU가 더 바빠지지만, 너무 높이면 큐가 밀립니다. 모델당 4로 안착했습니다.
  • claim 크기: 워커가 한 번에 큐에서 몇 건을 집어올지. 너무 작으면 잠금 경합이, 너무 크면 편중이 생깁니다. 32로 잡았습니다.
  • flush 주기: 요약 결과를 얼마나 자주 디스크에 반영할지. 30초마다 모아서 씁니다. 매건 쓰면 I/O가 병목이고, 너무 몰아 쓰면 중간에 죽었을 때 손실이 큽니다.
ingest 시작: 모델 2종 · 모델당 워커 4 · claim 32 · flush 30초

숫자 자체보다 중요한 건, 이 값들을 언제든 바꿀 수 있게 밖으로 빼 둔 것이었습니다. 하드웨어나 모델이 바뀌면 다시 맞춰야 하니까요.

max_tokens의 함정

한번은 요약이 비정상적으로 길게 나와서 느려진 적이 있었습니다. 원인은 max_tokens였습니다.

이건 응답의 최대 길이를 토큰 단위로 제한하는 값입니다. 너무 크게 잡으면 모델이 필요 이상으로 장황하게 늘어놓고, 그만큼 생성 시간도 늘어납니다. 요약은 "짧게"가 목적이니, max_tokens를 적절히 조여서 출력 길이와 속도를 함께 잡았습니다.

보이지 않으면 못 고친다 — 처리시간 로그

튜닝하면서 가장 답답했던 건 "지금 빠른 건지 느린 건지 감이 안 온다"는 것이었습니다. 그래서 걸린 시간을 로그에 드러냈습니다.

ingest 완료: 8개 · 3.7초 · 2.18개/초

건수·소요 시간·초당 처리량을 매 플러시마다 찍으니, 손잡이를 돌릴 때마다 효과가 숫자로 보였습니다. 로그를 읽기 좋게 정리한 것도 이때입니다 — 흔한 라이브러리 로그 대신, 타임스탬프·레벨·메시지가 정렬된 형태로 바꿔서 한눈에 흐름을 읽을 수 있게 했습니다.

결과

  • 모델을 묶지 말고 독립 큐로 — 서로 기다리지 않게.
  • 워커·claim·flush는 밖으로 빼서 상황에 맞게 조정.
  • max_tokens로 출력 길이와 속도를 함께 관리.
  • 걸린 시간을 로그로 드러내야 튜닝이 가능하다.

이제 위키가 채워졌습니다. 다음은 이 위키를 실제로 써먹을 차례입니다. 다음 글에서는 위키 데이터를 바탕으로 매일 아침 스크럼을 보내는 슬랙봇을 만듭니다.