llama-server에서 추측 디코딩(speculative decoding) 사용 시 logprobs가 플레이스홀더인 문제
요약
llama-server에서 추측 디코딩(speculative decoding)을 사용할 경우, 생성된 토큰의 logprobs가 0.0으로 잘못 전송되는 문제가 발생합니다. 이는 서버 측 구현 문제로 보이며, 특히 첫 번째 토큰 이후 모든 토큰에 대해 확률 정보가 누락됩니다. 이 문제는 API 호출 시 로그 확률 집계 및 분석에 심각한 오류를 초래할 수 있습니다.
핵심 포인트
- 추측 디코딩 사용 시 logprobs가 0으로 잘못 전송됨.
- 첫 번째 토큰 이후 모든 토큰의 확률 정보가 누락되는 현상 발생.
- 로그 확률(logprobs) 집계 및 분석에 심각한 오류를 초래함.
- 문제 해결을 위해 서버 측 구현 수정이 필요함.
요약: llama-server를 추측 디코딩(speculative decoding, -md, MTP 또는 n-gram 유형 중 하나를 사용하는 드래프트 모델과 함께)으로 실행할 때, 추측 루프를 통해 나오는 모든 토큰은 logprob: 0.0 및 빈 top_logprobs 리스트와 함께 전송됩니다. 코드는 주석 // set later로 확률을 1.0으로 설정하지만 실제로 설정하지 않습니다. 드래프트 모델이 첫 번째 토큰 이후의 모든 토큰인 경우에도 마찬가지입니다. 현재 버전인 b11430에서, 온도 1로 64개 토큰 샘플링했을 때, 추측 없이 평균 logprob는 -0.48이었고, 추측을 사용했을 때는 -0.0011이었습니다. 생성된 텍스트는 정상적으로 보였으며, 응답이나 로그 어디에도 이 숫자들이 채워진 값(fill-ins)이라는 내용은 없습니다. 요청별로 설정할 수 있는 스위치는 없는데, 추측을 조정하는 데 사용되던 요청 필드들이 컴파일되어 무시됩니다. logprob 요청은 추측 없이 시작된 인스턴스로 보내야 합니다. 토큰 자체는 문제가 없습니다: 작은 테스트 환경에서 서버의 추측 출력은 일반 샘플링과 동일한 다음 토큰 분포를 가졌습니다. 하지만 llama-speculative 예제 프로그램은 그렇지 않았습니다. 툴킷의 check-spec-logprobs.sh 스크립트는 실행 중인 서버를 확인합니다. 상위 이슈: ggml-org/llama.cpp#27972 및 #29975.**
증상
Debian 13 LXC, CPU 전용, release tarball에서 가져온 llama.cpp b11430 (현재 버전, 릴리스 다이제스트와 일치하는 SHA-256), gemma-3-1b-it-Q4_K_M. 드래프트 모델은 일반적으로 시간을 절약하기 위해 타겟보다 작아야 합니다. 같은 파일을 자체 드래프트로 사용해도 모든 토큰이 추측 경로를 거치도록 충분하며 추가 다운로드가 필요하지 않습니다. 요청은 보고서의 요청입니다:
curl -s http://127.0.0.1:8080/v1/chat/completions -H 'Content-Type: application/json' -d '{
'Okay' logprob=-0.00813608 top_logprobs=3
',' logprob=0 top_logprobs=3
' let' logprob=-2.38419e-07 top_logprobs=3
...
`Same server started with -md gemma-3-1b-it-Q4_K_M.gguf --spec-type draft-simple`:
'Okay' logprob=-0.00813608 top_logprobs=3
',' logprob=0 top_logprobs=0
' let' logprob=0 top_logprobs=0
...
첫 번째 토큰 이후의 23개 모든 토큰이 온도(temperature) 0 및 1에서 `/v1/chat/completions`와 네이티브한 `/completion` 엔드포인트 모두에서 `n_probs`로 인해 0으로 반환되었으며, 대안도 없었습니다. `post_sampling_probs: true`를 사용했을 때 네이티브 엔드포인트는 첫 번째 토큰 이후 모든 토큰에 대해 `prob: 1.0`과 비어 있는 `top_probs`를 보고했고, 예측하지 않은(unspeculated) 실행은 예를 들어 `What` 0.048을 반환했습니다.
로그 확률(logprobs)을 집계하는 모든 것에 미치는 영향은 미묘하지 않습니다. 온도 1에서 각각 64개의 토큰으로
텍스트는 정상적으로 보이기 때문에 출력물을 읽는 누구에게도 아무 문제가 없어 보입니다. 그리고 서버 자체의 설명도 도움이 되지 않습니다: `-md`와 `--spec-type ngram-mod`로 시작된 서버의 기본 생성 설정에서 `/props`는 `
만약 logprobs가 잘못되었다면, 당연히 다음 질문은 출력 또한 그러한지 여부입니다. Speculative decoding은 손실이 없어야 합니다: 타겟의 검증 과정은 마치 draft가 존재하지 않았던 것처럼 샘플링된 토큰들의 분포를 정확하게 유지해야 합니다. [#27694](https://github.com/ggml-org/llama.cpp/pull/27694)는 2026-10-02에 병합되었고 b11430 버전에서, 서버가 temperature가 0보다 큰 상태에서 draft-simple 및 MTP draft를 거부 샘플링(rejection sampling)으로 검증하도록 만들었으며, 그 설명에는 출력 분포가 정확하게 보존된다고 명시되어 있습니다.
[#29975](https://github.com/ggml-org/llama.cpp/issues/29975)에서는 `llama-speculative` 예제 프로그램이 이를 보존하지 못한다고 보고하며, 이를 보여주기 위해 다음을 포함하는 픽스처(fixture)를 제공합니다: 8글자 어휘 사전을 가진 두 개의 24 KB GGUF 파일로 구성된 타겟과 draft가 있는 노이즈 복사본입니다. 프롬프트 `bcdbc`와 첫 번째 생성 토큰 `f` 이후, 타겟의 다음 토큰에 대한 자체 top-3 분포는 `e` 0.345, `g` 0.452, `h` 0.203입니다. 아카이브의 SHA-256은 이슈에 있는 것과 일치했습니다. b11430에서 소스 코드를 빌드하고 보고서의 스크립트를 사용하여 시드(seed) 1부터 1000까지 실행한 결과는 다음과 같습니다:
sseeds 1..1000; runs with first token 'f' (5): 820
next token 4 ('e'): 1.000 target: 0.345
next token 6 ('g'): 0.000 target: 0.452
...
이 보고서는 서버를 테스트하지 않습니다. 동일한 픽스처를 같은 빌드, 같은 샘플러 설정(top-k 3, temperature 1)으로 `llama-server`에서, 그리고 `-md draft.gguf` 유무에 따라 1000개의 시드를 사용하여 실행한 결과는 다음과 같습니다:
no speculation first token 'f': 840 next e 0.330 g 0.452 h 0.218
draft model first token 'f': 840 next e 0.330 g 0.452 h 0.218 (2470 drafted, 884 accepted)
둘 다 샘플링 오차 범위 내에서 목표 분포와 일치합니다 (표준 편차가 840개 샘플에서 약 0.016입니다). 따라서 서버의 추측 경로(speculative path)는 #27694에서 말하는 대로, 확률을 보고하는 토큰 자체를 바꾸지만 선택하는 토큰은 바꾸지 않습니다. 추측 디코딩이 어떻게 작동하는지 학습하거나 이를 벤치마킹할 때 자연스러운 장소인 예제 프로그램은 항상 `e`를 선택합니다. 이 리포트는 두 가지 라인을 지적합니다. 첫째, 예제는 샘플링한 토큰 대신 초안(draft)의 최고 후보를 사용하며, 둘째, 거부(rejection) 시에는 두 분포를 모두 정렬하여 위치별로 빼기 때문에 서로 다른 토큰을 쌍으로 묶게 됩니다. 이러한 이유들은 리포트의 것이며, 위 출력에서 측정된 것은 오직 결과값뿐입니다.
## 해결 방법 (The fix)
하나의 요청에 대해서만 추측(speculation) 기능을 끌 방법은 없습니다. 서버는 이전에는 요청당 `speculative.n_max` 및 관련 필드를 수락했지만, b11430 버전에서는 `tools/server/server-schema.cpp` 파일 내 `#if 0` 블록 안에 포함되어 있으며 (
측정 결과가 너무 좋게 나올 때는 측정하는 대상 장치보다 그 앞의 기기를 점검해야 합니다. 보고서에 따르면 MTP는 온도 1에서 모델이 거의 완벽하게 확신한다고 했지만, 이는 물리적으로 불가능한 수치였고, 이것이 바로 그들이 모델 대신 logprobs를 살펴보게 만든 원인이었습니다. 이를 위한 저렴한 방법은 반드시 달라야 하는 조건 하에서 동일한 측정을 수행해보고, 장비가 이 둘을 구별할 수 있는지 확인하는 것입니다. 여기서는 그것이 가능했고, 전체 차이는 오직 장비 자체의 것이었습니다.
두 번째 점검 사항은 사람들이 건너뛰는 부분입니다. 최적화(optimisation)가 눈에 띄지 않아야 한다면, 셀 수 있을 만큼 작은 것으로 테스트하여 실제로 눈에 띄지 않는지 확인해야 합니다. 24 KB 모델 두 개와 1000개의 시드만으로도 서버의 추측(speculation)이 분포를 그대로 유지하는지, 그리고 예제는 그렇지 않은지를 보여주기에 충분했습니다.
툴킷의 `check-spec-logprobs.sh` 스크립트는 `/slots`를 읽고 하나의 logprob 요청을 보내어 첫 번째 토큰 이후의 토큰들이 대체(alternatives)와 함께 돌아오는지 여부를 보고합니다.
## 툴킷
본 게시물의 수정 사항은 툴킷에서 테스트가 완료되고 바로 실행할 수 있는 스크립트로 제공됩니다.
**[툴킷 보기 →](https://homelabpostmortem.com/toolkit/)**
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기