모델 운영

MLOPS / 85번째 글

투기적 디코딩 — 초안 모델을 무엇으로 세울 것인가

초안 모델이 앞질러 낸 토큰을 큰 모델이 한 번에 확인하면 출력은 그대로인데 속도가 붙습니다. 수용률과 초안 비용이 이득을 어떻게 정하는지, 별도 모델·n-gram·다중 헤드 중 무엇을 고를지 정리합니다.

PALDYN Team16 MIN READ

지난 글에서 디코드가 계산이 아니라 메모리 대역폭에 막혀 있다는 이야기를 했다. 토큰 하나를 뽑을 때마다 70B 모델의 가중치 전부를 HBM에서 읽어 오는데, 그 읽기로 하는 일은 행렬 곱 몇 번뿐이다. 배치가 1이면 GPU의 계산 유닛은 대부분의 시간을 놀면서 보낸다.

여기에 빈틈이 있다. 가중치를 한 번 읽어 온 김에 토큰을 여러 개 처리해도 시간은 거의 안 늘어난다. 문제는 자기회귀 생성이 원리상 그것을 못 한다는 데 있다 — 두 번째 토큰을 계산하려면 첫 번째 토큰이 정해져 있어야 한다.

투기적 디코딩(speculative decoding)은 이 순서를 깨지 않으면서 빈틈을 쓰는 방법이다. 작고 빠른 모델에게 다음 토큰 몇 개를 미리 찍게 하고, 큰 모델은 그 찍은 것들을 한 번의 forward로 확인만 한다. 맞았으면 여러 개를 한꺼번에 확정하고, 틀린 자리에서 잘라 낸다.

초안 모델이 낸 토큰을 타깃 모델이 한 번에 판정하는 과정

출력이 변하지 않는다는 것이 핵심이다

"작은 모델을 섞어 쓴다"는 말을 들으면 품질이 조금 깎이는 거래를 떠올리게 된다. 투기적 디코딩은 그런 거래가 아니다. 제대로 구현하면 최종 출력의 확률 분포가 타깃 모델 단독으로 뽑았을 때와 정확히 같다.

그것을 보장하는 것이 거절 샘플링(rejection sampling)이다. 어떤 자리에서 초안 모델이 확률 q(x)q(x) 로 토큰 xx 를 냈다고 하자. 타깃 모델이 그 자리에 매기는 확률은 p(x)p(x) 다. 판정은 이렇게 한다.

  • p(x)≥q(x)p(x) \ge q(x) 이면 무조건 받아들인다. 타깃이 초안보다 그 토큰을 더 좋아하니 거절할 이유가 없다.
  • p(x)<q(x)p(x) < q(x) 이면 p(x)/q(x)p(x)/q(x) 의 확률로만 받아들인다.
  • 거절했으면 그 자리는 max⁡(0, p(x)−q(x))\max(0,\, p(x) - q(x)) 를 정규화한 분포에서 새로 뽑는다.

마지막 줄이 보정 항이다. 초안이 과하게 밀었던 토큰의 몫을 덜어 낸 나머지에서 다시 뽑기 때문에, 받아들인 경우와 거절한 경우를 합치면 정확히 pp 가 된다. 그래서 투기적 디코딩은 근사가 아니라 같은 분포를 다른 순서로 계산하는 방법이다. temperature를 0으로 두는 그리디 디코딩이라면 더 단순해진다 — 초안 토큰이 타깃의 argmax와 같은지만 보면 된다.

이 성질이 실무에서 중요한 이유가 있다. 품질 회귀가 없으므로 평가를 다시 돌릴 필요가 없다. 양자화나 증류는 도입할 때마다 기존 평가셋을 전부 다시 돌려 회귀를 확인해야 하지만, 투기적 디코딩은 지연 지표만 보면 된다. 도입 비용이 다른 최적화보다 눈에 띄게 싸다.

이득의 크기는 두 숫자가 정한다

한 걸음에 초안을 kk 개 내고, 각 토큰이 받아들여질 확률을 α\alpha 라 하자. 앞에서부터 순서대로 보다가 첫 거절에서 잘라 내므로, 한 걸음에 확정되는 토큰 수의 기댓값은 이렇게 된다.

E[토큰]=1−αk+11−αE[\text{토큰}] = \frac{1 - \alpha^{k+1}}{1 - \alpha}

거절이 나도 그 자리는 타깃이 고쳐 넣으므로 최소 1개는 나온다는 점이 분자의 k+1k+1 에 들어 있다. 그리고 이 기댓값이 그대로 속도 배수는 아니다 — 초안 모델을 kk 번 돌리는 비용이 붙는다. 타깃 forward 한 번의 비용을 1로 두고 초안 한 번을 cc 라 하면, 실제 배수는 이렇게 된다.

속도 배수=E[토큰]1+kc\text{속도 배수} = \frac{E[\text{토큰}]}{1 + kc}

수용률에 따른 기대 토큰 수 곡선

곡선이 말하는 것은 하나다. 수용률이 낮으면 초안을 길게 뽑는 것이 무의미하다. α=0.5\alpha = 0.5 에서 초안을 2개로 하면 기대 토큰이 1.75, 7개로 늘려도 1.99다. 0.24개를 더 얻자고 초안 모델을 다섯 번 더 돌리는 셈이니 1+kc1 + kc 의 분모만 커지고 실제로는 느려진다.

반대로 α=0.9\alpha = 0.9 라면 초안 7개가 5.70토큰을 낸다. 같은 파이프라인인데 이득이 세 배 가까이 차이 난다. 그러니 초안 모델을 고르는 일은 사실상 수용률을 올리는 일이다.

cc 도 무시할 값이 아니다. 70B 타깃에 7B 초안을 쓰면 c≈0.1c \approx 0.1 이고 k=5k=5 면 분모가 1.5다. 기대 토큰이 3.0이어도 실제 배수는 2.0으로 깎인다. 초안 모델의 크기를 줄이면 cc 는 내려가지만 α\alpha 도 같이 내려가므로, 둘 사이 어딘가에 최적점이 있고 그 자리는 모델 조합마다 다르다.

초안을 만드는 네 가지 방식

방식 어떻게 초안을 내는가 수용률 초안 비용 cc 준비 부담
별도 소형 모델 같은 계열의 작은 체크포인트를 그대로 쓴다 중~높음 0.05~0.15 없음(있으면)
n-gram·프롬프트 룩업 앞 문맥에서 같은 접두사를 찾아 뒤를 복사한다 낮음(문맥 의존) 거의 0 없음
다중 헤드(Medusa 계열) 타깃 위에 헤드를 여러 개 붙여 앞 토큰들을 동시에 예측 중간 0.02~0.05 헤드 학습 필요
특징 예측(EAGLE 계열) 토큰이 아니라 타깃의 은닉 특징을 예측해 초안을 만든다 높음 0.05 안팎 별도 학습 필요

별도 소형 모델이 가장 손이 덜 간다. 같은 계열에 1B·3B 체크포인트가 이미 있으면 그것을 그대로 초안으로 붙이면 되고, 학습이 필요 없다. 다만 조건이 하나 있다 — 토크나이저가 같아야 한다. 토크나이저가 다르면 초안이 낸 토큰 id를 타깃의 어휘로 옮기는 작업이 매 스텝 끼고, 한쪽에만 있는 토큰은 아예 대응이 안 된다. 계열이 다른 모델을 초안으로 붙이는 것은 대개 여기서 막힌다.

n-gram 룩업은 모델을 아예 안 쓴다. 지금까지의 문맥에서 최근 몇 토큰과 같은 조각을 찾아 그 뒤를 그대로 초안으로 낸다. 비용이 사실상 0이라 실패해도 손해가 없고, 문서 요약이나 코드 편집처럼 입력에 있던 문자열이 출력에 다시 나오는 작업에서는 수용률이 놀랄 만큼 높다. 반대로 자유 생성에서는 거의 안 맞는다. 붙이는 비용이 없으니 다른 방식과 같이 써도 된다.

다중 헤드는 타깃 모델의 마지막 은닉 상태 위에 헤드를 kk 개 얹어, 각 헤드가 "ii 칸 뒤 토큰"을 맡게 한다. 초안 전용 모델을 따로 안 들고 다녀도 되고 cc 가 아주 작다. 대신 헤드를 학습시켜야 하고, 헤드마다 서로 독립적으로 예측하므로 뒤로 갈수록 정확도가 빠르게 떨어진다.

특징 예측은 초안 모델이 토큰 확률이 아니라 타깃의 은닉 특징을 예측하도록 학습한다. 예측 대상이 타깃 내부의 값이라 정보가 훨씬 진하고, 그래서 같은 크기의 소형 모델보다 수용률이 높게 나온다. 지금 수용률이 가장 잘 나오는 계열이지만 학습 파이프라인을 따로 갖춰야 한다.

배치를 키우면 이득이 사라진다

투기적 디코딩을 벤치마크에서 보고 도입했다가 프로덕션에서 효과가 없어 당황하는 일이 흔하다. 원인은 대개 하나다 — 벤치마크는 배치 1이고 프로덕션은 아니다.

이 기법이 성립하는 전제는 "GPU의 계산 유닛이 놀고 있다"였다. 동시 요청이 32개면 그 전제가 무너진다. 서버는 이미 32개 시퀀스의 토큰을 한 번의 forward로 처리하고 있어서 계산 유닛이 꽉 차 있고, 여기에 초안 검증까지 얹으면 그냥 계산이 더 늘어난다. 수용률이 아주 높지 않은 한 처리량은 오히려 떨어진다.

그래서 실전에서는 부하에 따라 켜고 끄는 것이 정석이다. 대기 큐가 짧고 배치가 작을 때만 투기적 경로를 쓰고, 배치가 임계값을 넘으면 평범한 디코딩으로 돌아간다. vLLM이나 TensorRT-LLM 계열은 이 전환을 설정으로 제공한다.

여기서 무엇을 최적화하는지도 분명히 해 둘 필요가 있다. 투기적 디코딩이 줄이는 것은 한 사용자가 느끼는 토큰 간 지연이지 서버 전체의 처리량이 아니다. 처리량이 목표라면 배치를 키우는 쪽이 언제나 낫고, 이 기법은 오히려 방해가 된다. 대화형 서비스라 첫 토큰 이후의 흐름이 끊기지 않는 것이 중요할 때, 그리고 그 시간대에 부하가 낮을 때가 제자리다.

도입할 때 순서대로 확인할 것

첫째, 자기 트래픽에서 수용률을 재 본다. 문서에 적힌 수치는 그 논문의 데이터셋에서 잰 값이다. 같은 초안 모델이라도 코드 생성에서 0.85가 나오고 한국어 자유 대화에서 0.55가 나오는 일이 얼마든지 있다. 실제 프롬프트 로그에서 몇백 건을 돌려 α\alpha 를 직접 재는 것이 첫걸음이다. 이 수치 하나만 있으면 위의 식으로 도입 여부가 거의 결정된다.

둘째, kk 를 고정하지 않는다. 프롬프트마다 예측 난이도가 다르므로, 잘 맞는 구간에서는 길게 뽑고 안 맞는 구간에서는 짧게 줄이는 것이 낫다. 직전 몇 걸음의 수용 결과를 보고 kk 를 올리고 내리는 간단한 규칙만으로도 고정값보다 나은 결과가 나온다. 거절이 연달아 나면 kk 를 1까지 내려 초안 비용을 줄이고, 연속 수용이 이어지면 다시 올린다.

셋째, KV 캐시가 두 벌이라는 것을 잊지 않는다. 초안 모델도 자기 KV 캐시를 들고 있어야 하고, 거절이 나면 그 뒤 자리를 되돌려야 한다. 소형 모델이라 크지는 않지만 동시 요청 수만큼 배수로 붙는 값이라 메모리 예산에서 빠뜨리면 안 된다. 그리고 되돌리기가 어긋나면 초안이 잘못된 문맥 위에서 계속 예측해 수용률이 조용히 무너지는데, 출력은 여전히 정확하므로 품질 지표로는 안 잡힌다. 수용률을 지표로 내보내 두어야 이런 버그가 보인다.

넷째, 이득의 기준선을 정직하게 잡는다. 비교 대상은 "아무 최적화도 안 한 디코딩"이 아니라 "지금 쓰고 있는 서빙 설정"이다. 연속 배칭과 PagedAttention이 이미 켜져 있는 서버 위에서 재야 실제로 얼마를 더 얻는지가 나온다.

정리

  • 투기적 디코딩은 근사가 아니다. 거절 샘플링의 보정 항이 최종 분포를 타깃 모델과 정확히 같게 유지하므로, 도입해도 품질 평가를 다시 돌릴 필요가 없다
  • 이득은 수용률 α\alpha 와 초안 비용 cc 두 숫자로 거의 결정된다. α\alpha 가 0.5 근처면 초안을 길게 뽑아도 소용이 없고, 0.8을 넘어야 초안 길이가 의미를 갖는다
  • 초안은 별도 소형 모델·n-gram 룩업·다중 헤드·특징 예측 중에서 고른다. 손이 가장 덜 가는 것은 같은 계열의 작은 체크포인트이고, 토크나이저가 같아야 한다는 조건이 붙는다
  • 배치가 커지면 전제가 무너져 오히려 느려진다. 부하에 따라 켜고 끄는 전환을 함께 넣는다
  • 줄이는 것은 한 사용자의 토큰 간 지연이지 서버 처리량이 아니다. 목표가 처리량이면 이 기법은 답이 아니다

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

LATEST

모델 운영의 최신 글

모델 운영2026.09.04

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

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

16 MIN
모델 운영2026.09.04

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

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

21 MIN
모델 운영2026.09.03

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

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

15 MIN