Repository navigation
server: Support logprobs with speculative decoding - #27196
Conversation
|
/bot review |
Automated code reviewReviewed VerdictThe change is small, correct, and reuses existing infrastructure rather than inventing new machinery. I traced the indexing, the off-by-one risk, the move semantics, and the partial-accept/replay path; all check out. No blocking findings. A couple of non-blocking suggestions below. Correctness notes (verified, no action needed)
Will slow the review (non-blocking)(point 1) Missing regression test. (point 2) Nits(point 3) The two added comments are concise and explain why, which is good. The one at (point 4) Pre-existing TODO at This review was generated automatically by pi coding agent using |
|
The only thing the bot suggests that seems potentially interesting to me is a regression test. Although it is of course a tradeoff for increasing CI time, since this would probably not be a very fast test to run, so I am slightly inclined not to include it. But happy to if others think so. |
|
I ran into the same problem while using gemma 4 e4b with MTP as a transcription model. I did an independent fix with Fable 5, then discovered your PR. Here is the regression test the bot asked for, cherry-pickable, on this branch: https://github.com/sigjhl/llama.cpp/tree/pr27196-regression-test. The results: My independent fix has post_sampling_probs via a sampler callback and happy to open it as a follow-up after this PR merges. It will probably settle the bot's point 2. |
Asserts accepted draft tokens report the target model's logprobs, comparing a draft-enabled run against a no-draft baseline (ggml-org#24271). Fails on master (top_logprobs empty, logprob 0.0), passes with the fix from ggml-org#27196. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KARBWpPcijcBXzJEvttuwk
297 upstream commits. Conflicts and how they were resolved: - ggml ops: upstream added GGML_OP_TOP_K_MASK next to the fork's GGML_OP_TURBO_WHT; both are kept, upstream's first, GGML_OP_COUNT 103. - MoE down projection: upstream replaced the MISTRAL4 F32 src1 precision hint (its own, ggml-org#29029) with a per-row amax rescale (ggml-org#30861) for MISTRAL4 and MELLUM. It now sits in build_moe_ffn_exps, so every tiered call (hot, cold, each arena window) rescales its own rows; rows skipped by a partial mul_mat_id are zero, so their scale is 1. - MoE post_exp_norm (new upstream): applied in build_moe_ffn after the hot/cold merge and before the router weights; the two halves fill disjoint expert slots, so the norm sees whole rows. - build_lora_mm_id: upstream's Hadamard input transform (prism) after the fork's partial/slots assert. Expert tensors split by LLAMA_MOE_HOT are not in the Hadamard map, so prism and tiering do not stack on one layer. - kv-cache attention rotation: the fork's default (off unless LLAMA_ATTN_ROT_DISABLE=0) is kept; upstream's llama_model_set_attn_rot can still turn it off per model. - model loader: hugetlb per-tensor populate kept before upstream's multi-buffer lookup of the mmap buffer. - llama-model: MoE arena setup and upstream's load_tensors_hadamard both run at the end of load_tensors. - qwen4exp: turbo Q pre-rotation kept; the selection mask is now `sel`. - server: upstream added draft-token logprobs (ggml-org#27196) read back from the batch logits, without post_sampling_probs. The fork's on_sample path, which records them during verification and covers post_sampling_probs, is kept; upstream's test_draft_token_probs passes with it. Upstream's local spec_i_batch is taken. - mmq: fork's IQ1_M MMQ gains upstream's prec_src1 parameter. - fattn-tile: the fork's D=640 case and HIP guard kept, upstream's default-case style. - argsort.cu: upstream's CCCL guard; the fork's hipcub arm was dead since HIP does not define GGML_CUDA_USE_CUB. - tests/CMakeLists.txt: fork tests kept, upstream's LINK and consolidated test-llama-context taken. Tested on gfx1100: ctest 78/80 (the two failures are environmental: the ggml-vocabs clone has git-lfs pointers, and system python has no jinja2). All fork tests pass (turbo-kv x8, moe-tier, fattn-turbo4, fattn-long-kv, mmvq-src1-cache, fusions, mul-mat-f16-range, top-k-graph-capture, op-offload-devices). test-llama-archs 390/390. Server test_speculative.py 10/10, completion/probs pytests 14/14. Needle at 20.5k tokens, Qwen3.8-27B IQ4_NL, turbo4 K/V: correct, prefill 925 tok/s vs 909 before the merge (single run).
…feature list The header now explains why the fork's logprobs for draft-accepted tokens differ from upstream's ggml-org#27196 (post_sampling_probs, alignment check, replay, per-token cost, streamed tools, row text, per-request type), each point checked against upstream/master 7775ffb, and lists what the fork has that upstream does not. FORK.md: MoE tiering has been in master since 2026-10-01 (5d2e5db), not only on the moe-tier branch; section 4 points to the logprobs work.
Overview
I use logprobs with
/completion. It is very useful. Recently, it seemed to stop working, but eventually I realized that was because I finally started using speculative decoding.There was a feature request about it (#24271) from @drazdra, auto-closed due to inactivity. I also found this comment from @ServeurpersoCom that made it seem probably feasible to add. With the help of Qwen 3.8-27B and llama.cpp itself, it was.
Example:
On master, this will produce correct output for the first token only:
After that first token, logprobs are missing. Like:
{ "id": 13, "token": ".", "bytes": [ 46 ], "logprob": 0.0, "top_logprobs": [] },Whereas on this PR, they are correctly returned for all tokens. From my tests, the logprobs match the same values given by master branch without speculative decoding.
Additional information
Fixes #24271. Also there was a TODO in the source code about this issue.
Note that it still does not work for
post_sampling_probsbecause that is a whole more complicated beast.Requirements