logprobs=-1 ("give me every vocab entry") is accepted by SamplingParams and
reaches the sampler, which takes a different tensor shape for it than for any
logprobs=k — and the engine-side consumer reads that shape out of bounds.
Where it breaks
src/vllm/v1/sample/sampler.cpp:344-352 — for num_logprobs == -1 the sampler
builds a LogprobsTensors that carries ONLY logprobs (the raw [n, vocab]
block), leaving logprob_token_ids and selected_token_ranks empty, with
num_tokens_per_position = vocab:
LogprobsTensors lt;
lt.num_positions = static_cast<int>(n);
lt.num_tokens_per_position = static_cast<int>(vocab);
lt.logprobs = raw_logprobs; // ids + ranks deliberately left empty
logprobs_tensors = std::move(lt);
src/vllm/v1/engine/logprobs.cpp:51-73 (UpdateSampleLogprobs) then indexes all
three arrays for every position:
const int width = lists.num_tokens_per_position;
if (width <= 0) return; // the only guard -- vocab is > 0, so it passes
const int32_t* id_row = &lists.logprob_token_ids[base]; // EMPTY -> OOB
const int rank = lists.selected_token_ranks[pos]; // EMPTY -> OOB
The guard at :55 says "the num_logprobs==-1 raw-vocab shape is not wired", but
it only catches width <= 0, which this shape never is.
Repro
Any request through the engine with SamplingParams::logprobs = -1:
SamplingParams sp; sp.temperature = 0.0; sp.max_tokens = 1;
sp.logprobs = -1;
engine.generate(prompt_token_ids, sp, "req"); // SIGSEGV
Observed as SIGSEGV in test_llm_engine while writing the gate for
#223; the -1 prompt-logprobs
path is unaffected (the runner widens -1 to vocab_size before gathering, so it
produces the ordinary k+1 shape).
Expected
vLLM's sampling_params.py treats logprobs=-1 as "all logprobs" and
gpu_input_batch.py:435-440 stores vocab_size, so the sampler produces the
same [n, vocab+1] gathered shape as any other k and nothing downstream needs a
special case. Either mirror that (widen at admission, drop the special shape) or
teach UpdateSampleLogprobs the raw shape. The first is what upstream does.
Row: SAMPLE-LOGPROBS / SAMPLE-LOGPROB-TOKEN-IDS (.agents/engine-matrix.md:131,133).
logprobs=-1("give me every vocab entry") is accepted bySamplingParamsandreaches the sampler, which takes a different tensor shape for it than for any
logprobs=k— and the engine-side consumer reads that shape out of bounds.Where it breaks
src/vllm/v1/sample/sampler.cpp:344-352— fornum_logprobs == -1the samplerbuilds a
LogprobsTensorsthat carries ONLYlogprobs(the raw[n, vocab]block), leaving
logprob_token_idsandselected_token_ranksempty, withnum_tokens_per_position = vocab:src/vllm/v1/engine/logprobs.cpp:51-73(UpdateSampleLogprobs) then indexes allthree arrays for every position:
The guard at
:55says "the num_logprobs==-1 raw-vocab shape is not wired", butit only catches
width <= 0, which this shape never is.Repro
Any request through the engine with
SamplingParams::logprobs = -1:Observed as
SIGSEGVintest_llm_enginewhile writing the gate for#223; the
-1prompt-logprobspath is unaffected (the runner widens
-1tovocab_sizebefore gathering, so itproduces the ordinary
k+1shape).Expected
vLLM's
sampling_params.pytreatslogprobs=-1as "all logprobs" andgpu_input_batch.py:435-440storesvocab_size, so the sampler produces thesame
[n, vocab+1]gathered shape as any other k and nothing downstream needs aspecial case. Either mirror that (widen at admission, drop the special shape) or
teach
UpdateSampleLogprobsthe raw shape. The first is what upstream does.Row:
SAMPLE-LOGPROBS/SAMPLE-LOGPROB-TOKEN-IDS(.agents/engine-matrix.md:131,133).