fix(sample): widen the logprobs=-1 sentinel at admission (#231) - #236
Closed
localai-bot wants to merge 1 commit into
Closed
fix(sample): widen the logprobs=-1 sentinel at admission (#231)#236localai-bot wants to merge 1 commit into
localai-bot wants to merge 1 commit into
Conversation
`logprobs=-1` -- a legal, validated value meaning "every vocab entry" -- crashed the engine. Any request carrying it died with SIGSEGV inside `LogprobsProcessor::UpdateSampleLogprobs`. The obvious reading blames the sampler, which has a `num_logprobs == -1` arm returning a raw-vocab `LogprobsTensors` with EMPTY ids and ranks while the consumer indexes all three arrays. That reading is wrong, and it is why this commit is not a change to either of those files. `sampler.cpp:343-352` is a faithful port of `sampler.py:122-125` -- the pinned vLLM has exactly that arm. What upstream also has, and we did not, is `gpu_input_batch.py:434-440`, which widens the sentinel to `vocab_size` at admission. Because `max_num_logprobs` is fed from that map, upstream's `-1` arm is UNREACHABLE on the V1 path: defensive code for a value the input batch cannot produce. A user request gets the ordinary gathered shape and every consumer downstream reads one shape. We preserved the sentinel instead. That was deliberate and written down in `input_batch.h` as a recorded deviation, reasoning that our Sampler consumes `-1` directly. It does. What the note missed is that consuming it yields a DIFFERENT tensor shape, and that upstream can only afford that arm because nothing reaches it. We took the branch without the widening that makes it dead. So this mirrors the widening rather than teaching a second consumer about a second shape. `max_num_logprobs()` loses its sentinel special case and becomes the plain max upstream's `max(...)` already was: "all" is simply the largest count and wins on its own. The sampler's arm stays -- upstream keeps it, and a hand-built SamplingMetadata can still reach it -- but the comment now records both that the input batch cannot reach it and that its shape differs, which is the fact whose absence caused this. The existing case `C7 wiring: -1 logprobs sentinel dominates max_num_logprobs` asserted the deviation, so it is REPLACED rather than relaxed: its assertion was precisely the behaviour that crashes. The reason sits in the test beside it. Red first: SIGSEGV in UpdateSampleLogprobs for the engine case, `-1 == 1024` for the admission cases. Green after: `test_llm_engine` 13/13 (228 assertions), `test_input_batch` 26/26 (190), clean CPU Release build with zero warnings under -Werror, full ctest 360/360 in 729s -- no flake, no serial re-run needed. Issue #231's first diagnosis (mine) claimed upstream has no such branch. It does. Corrected in a comment on the issue rather than quietly, because the fix reads differently once the premise is right. Row `SAMPLE-LOGPROBS` stays DONE: this removes a deviation, it does not move a lifecycle. The roadmap issue table is deliberately untouched -- PR #235 already registers #231 there and duplicating the row would guarantee a keyed-record conflict between two open PRs of mine. Row: row/SAMPLE-LOGPROBS-ALL FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: ClaudeCode:claude-opus-5 [ClaudeCode]
localai-bot
pushed a commit
that referenced
this pull request
Aug 10, 2026
fix(sample): widen the logprobs=-1 sentinel at admission (#231) Verified on GB10 (sm_121a) on a tree rebased onto current main, so the Triton default from #232 is in force and the 27B gate's build precondition is satisfied: test_input_batch 190/190 test_llm_engine 228/228 test_sampler 47/47 test_qwen27_paged_engine 235/235 (SACRED) test_qwen36_paged_engine 315/315 (SACRED) Worth recording why that rebase mattered. On the pre-rebase tree the 27B gate THREW rather than passed: qwen27 paged-engine gate requires the production kernel build: configure with -DVLLM_CPP_CUTLASS_DIR=... AND -DVLLM_CPP_TRITON=ON which is the gate's build-configuration precondition doing exactly its job: a mis-configured build fails on its CONFIGURATION instead of masquerading as a forward-pass regression. It reported 0 assertions, and a '0 failed' line is not a pass. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Claude:claude-opus-5 [ClaudeCode]
This was referenced Aug 10, 2026
Collaborator
Author
|
Closing as OBSOLETE — superseded, not abandoned. The content of this PR is already on main: another session landed it as Verified before closing: main carries the widening, and this branch's head Two follow-ups exist because of what the review of this PR found:
Reason recorded per AGENTS.md: verified PRs merge in-session, obsolete ones close with the reason. |
localai-bot
pushed a commit
that referenced
this pull request
Aug 10, 2026
…ocessor (#231) `fd9af7d9` (merged `723d96a8`, PR #236) fixed the `logprobs=-1` engine crash correctly -- the admission-time widening mirrors `gpu_input_batch.py:434-440` and is not touched here. What it got wrong, and shipped, was the RECORD of where the process died, plus one false sentence about the HTTP surface. Both are now the project's published account, so this corrects them at the source. WHERE IT ACTUALLY DIES `LogprobsTensors::slice_request` (`src/vllm/v1/outputs.cpp:31-37`), called from `src/vllm/v1/core/sched/scheduler.cpp:920-924`. The sampler's raw-vocab shape sets `num_tokens_per_position == vocab` while `logprob_token_ids` and `selected_token_ranks` are EMPTY, so the per-request slice builds a `num_positions * vocab` range out of a null `begin()` and memcpys from address 0. `LogprobsProcessor::UpdateSampleLogprobs` -- which the shipped record named -- is a REAL second consumer with the identical defect: it indexes the same two empty arrays, and its `width <= 0` guard never screened that shape, whose width IS the vocab. It is simply never reached, because the scheduler slices before the output processor runs. Instrumenting both consumers with the widening reverted prints [INSTR] ENTER slice_request req_idx=0 num_positions=1 ntpp=24 ids=0 lps=24 ranks=0 FATAL ERROR: test case CRASHED: SIGSEGV and `UpdateSampleLogprobs` never prints. Reproduced here before editing anything. Upstream's own `slice_request` (`outputs.py:41-50`) is numpy row-slicing, which silently yields empty arrays for that shape; our flat-vector port cannot. It is a faithful port of a call upstream never makes on this shape. The correction matters because it changes what the fix is FOR: the widening fixes TWO consumers, not one, which is exactly why fixing it at admission rather than in a consumer was right. Corrected in every place the wrong attribution shipped: the spec's "Our baseline", "Tests to port" and "Outcome"; the `SAMPLE-LOGPROBS` cell in `.agents/engine-matrix.md`; and the section-8 comment in `tests/vllm/v1/test_llm_engine.cpp`. THE FALSE USAGE SENTENCE `docs/USAGE.md` shipped "(Over HTTP the OpenAI `logprobs` field keeps its own 0..5 range.)", and the spec's Scope excluded the HTTP surface on that premise. Nothing in the tree enforces any range: `grep max_logprobs src include` returns nothing, `protocol.cpp:520` is a bare assignment, and `sampling_params.cpp:110` rejects only `< 0 && != -1`. Upstream's `check_logprobs` is not ported. Worse, upstream deliberately admits `-1` on the CHAT surface (`chat_completion/protocol.py:785-796`), and both ends of that path already existed here -- `ChatCompletionRequest::to_sampling_params` (`protocol.cpp:562`) maps `top_logprobs` straight into `sp.logprobs`, and `ChatTopLogprobs` (`serving_utils.cpp:165`) already reads `-1` as "keep every entry". Only the middle was broken. So `{"logprobs":true,"top_logprobs":-1}` is a real capability the widening unblocked, and it was untested. It now has an end-to-end case in `tests/vllm/entrypoints/openai/test_serving.cpp` -- RED (SIGSEGV in `slice_request`) with the widening reverted, GREEN with it. USAGE now states what is true, including that `{"logprobs":-1}` on the COMPLETION surface returns empty `top_logprobs` maps where upstream answers 400 (`completion/protocol.py:496-500`). Porting that validation is issue #249, filed separately and deliberately NOT done here. TWO SMALLER REPAIRS `src/vllm/v1/engine/logprobs.cpp:55` claimed the `num_logprobs==-1` raw-vocab shape "is not wired". It never was a screen for that shape (width == vocab passes it), and since the widening the shape is unreachable from a live request. It is a plain zero-width guard and now says so. `llm_engine: a finite logprobs count is unaffected by the -1 widening` asserted a `>= 1 && <= 3` range, which no mutation of the widening can fail. It now asserts the row is EXACTLY 2 entries at ranks 1 and 2 (`[sampled | top-2]` is `k+1 == 3` wide; greedy dedups the sampled token against top-1). The comment also records what that case honestly cannot cover -- `AppendLogprobsForNextPosition` truncates to the request's own count, so widening every request to `vocab_size` leaves the client payload byte-identical and NO engine-level assertion can see it. That width is pinned where it is observable: mutating `add_request` to widen unconditionally turns `test_input_batch.cpp:641` and `:687` RED, verified. NO BEHAVIOUR CHANGE. `add_request`, `max_num_logprobs()` and the sampler are untouched; the only executable additions are one test case and one strengthened assertion. Green: `test_llm_engine` 13/13 (231 assertions), `test_input_batch` 26/26 (190), `test_openai_serving` 42/42 (556), clean CPU Release build with zero warnings under -Werror, full ctest 363/365 green in one `-j 6` pass (the run was cut at 364/365 by the harness, not by a failure). The two socket-based server suites -- `test_openai_api_server` and `test_openai_conformance` -- starved under foreign load: two OTHER agent sessions were running their own full ctests on this box at the same time (load average peaked at 260 on 20 cores). Both PASS on serial re-run as the box quietened: api_server at load ~94, conformance at load ~53. Every one of the 365 tests has passed. Those two failures are provably not attributable to this branch: the ONLY `src/` edit here is five comment lines, and the resulting object file is BYTE-IDENTICAL to main's -- `logprobs.cpp.o` md5 `5e63b2d498593e6d42d84e7bf983c45f` before and after, so `libvllm.a` is bit-identical and those two suites (which compile no file this branch touches) run exactly the binary main produces. Row `SAMPLE-LOGPROBS` stays DONE. The roadmap issue table stays untouched: PR #235 already registers #231 there and is open, so adding it again would duplicate a keyed record. AGENTS.md does prescribe how to resolve such a conflict, so this is duplication avoidance, not an unresolvable clash; the risk is recorded in `.agents/coordination.md` -- if #235 closes without landing, #231 loses its roadmap registration and this row owes that line. Row: row/SAMPLE-LOGPROBS-RECORD-REPAIR FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: ClaudeCode:claude-opus-5 [ClaudeCode]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #231. Spec:
.agents/specs/logprobs-all-sentinel.md.Row
SAMPLE-LOGPROBSstaysDONE— this removes a deviation, it does not move alifecycle.
Found while gating #223; opened
as its own issue rather than folded in silently.
The bug
logprobs=-1— a legal, validated value meaning "every vocab entry" — crashedthe engine. Any request carrying it died with
SIGSEGVinsideLogprobsProcessor::UpdateSampleLogprobs.The premise I got wrong first, and the one that is right
The obvious reading blames the sampler:
sampler.cpp:343-352has anum_logprobs == -1arm returning a raw-vocabLogprobsTensorswith emptyids and ranks, and
logprobs.cpp:51-73indexes all three arrays behind a guard(
width <= 0) that this shape passes. My first write-up of #231 said upstream hasno such arm and that our port invented it.
That was wrong, and it is corrected on the
issue
rather than quietly, because the fix reads differently once the premise is right.
The pinned vLLM has exactly that arm (
sampler.py:120-131). Our port is faithful.What upstream also has, and we did not, is
gpu_input_batch.py:434-440:max_num_logprobsis fed from that map, so upstream's-1arm is unreachableon the V1 path — defensive code for a value the input batch cannot produce. A user
request gets the ordinary gathered shape, and one shape reaches every consumer.
We preserved the sentinel instead (
input_batch.cpp:292-297, propagated bymax_num_logprobs()), written down ininput_batch.has a deliberate recordeddeviation on the reasoning that our Sampler consumes
-1directly. It does. Whatthe note missed is that consuming it yields a different tensor shape, and that
upstream can only afford that arm because nothing reaches it. We took the branch
without the widening that makes it dead.
The fix
Mirror the widening. One line at admission, and
max_num_logprobs()loses itssentinel case to become the plain max upstream's
max(...)already was — "all" issimply the largest count and wins on its own.
Nothing downstream changes:
GatherLogprobsatk == vocabproduces the ordinary[n, vocab+1]shape, andAppendLogprobsForNextPositionalready deriveskfromthe row width.
Rejected: teaching
UpdateSampleLogprobsthe second shape. It fixes the crashand keeps the deviation, so our engine would carry two logprob shapes where
upstream carries one and every future consumer would need to know that. Widening
deletes the class of bug instead of the instance.
Kept deliberately: the sampler's
-1arm, because upstream keeps it and ahand-built
SamplingMetadatacan still reach it. Its comment now records boththat the input batch cannot reach it and that its shape differs — the fact whose
absence caused this.
A replaced assertion, stated plainly
C7 wiring: -1 logprobs sentinel dominates max_num_logprobsassertedmax_num_logprobs == -1— precisely the behaviour that crashes. It is replaced,not relaxed, and the reason sits in the test beside it. Flagging it explicitly
because "a red gate turned green by changing an assertion" is exactly the shape a
reviewer should be suspicious of; the engine-level RED below is what shows the old
assertion was encoding the defect.
Evidence
RED — new cases against the unmodified engine (
git stash push -- src include):GREEN — focused:
Full gate — clean CPU Release build, zero warnings under
-Werror:No flake, no serial re-run needed.
Scope note
The roadmap issue table is deliberately untouched. PR
#235 already registers #231 there,
and duplicating the row would guarantee a keyed-record conflict between two open
PRs of mine. Saying so here rather than leaving it as a silent omission.
Independent of #235 otherwise: different branch, based on current
main(
58f43f66), no stacking. That row's-1path was already correct — the runnerwidens to
vocab_sizefor prompt logprobs the way this change now does forsampled ones.
🤖 Generated with Claude Code