Skip to content

Commit ff366c6

Browse files
mudlerclaude
andcommitted
feat(sampler): SamplingMetadata + LogprobsTensors + make_sampling_metadata (M1.7 Task 1)
Port the host-side sampling types the V1 Sampler consumes: - include/vllm/v1/sample/metadata.{h} + src/.../metadata.cpp: SamplingMetadata (T0 field subset, names 1:1 with vllm/v1/sample/metadata.py). The logitsprocs plugin graph is flattened to min_tokens / logit_bias / min_p; logprob_token_ids / spec_token_ids / thinking_budget stubbed with upstream cites. - include/vllm/v1/outputs.{h} + src/.../outputs.cpp: LogprobsTensors (logprob_token_ids [n,k+1] i32, logprobs [n,k+1] f32, selected_token_ranks [n] i32) + empty_cpu, ported from vllm/v1/outputs.py. - engine/types.h SamplerOutput: replace opaque optional<bool> logprobs_tensors with optional<LogprobsTensors> (field name kept). - InputBatch::make_sampling_metadata(): port gpu_input_batch.py _make_sampling_metadata field-fill order (temperature/top_p/top_k None-gates, penalty slices, prompt/output-token-ids penalty gates). generators / max_num_ logprobs / allowed-ids / bad-words / min-tokens / logit-bias / min-p left at faithful defaults with dependency cites (Task 2/3 / M1.8). Tests: 2-req mixed greedy+random predicates+fields, all-greedy None temperature, penalties enable prompt+output ids, remove+condense dense-prefix read, LogprobsTensors shapes, SamplerOutput payload. ctest 48/48 green, warnings-as-errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7ca1f79 commit ff366c6

10 files changed

Lines changed: 520 additions & 5 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ add_library(vllm STATIC
6161
src/vllm/tokenizer/bpe.cpp
6262
src/vllm/tokenizer/tokenizer.cpp
6363
src/vllm/v1/request.cpp
64+
src/vllm/v1/outputs.cpp
65+
src/vllm/v1/sample/metadata.cpp
6466
src/vllm/v1/kv_cache_interface.cpp
6567
src/vllm/v1/core/kv_cache_utils.cpp
6668
src/vllm/v1/core/block_pool.cpp

include/vllm/v1/engine/types.h

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919
// external_req_id, reasoning_ended / reasoning_parser_kwargs,
2020
// abort_immediately, and the params property. (eos_token_id is NOT a field
2121
// upstream either — it rides on sampling_params.eos_token_id.)
22-
// SamplerOutput: logprobs_tensors detail (kept as an opaque optional flag
23-
// here — the tensor payload lands with the sampler/logprobs unit).
22+
// SamplerOutput: logprobs_tensors now carries the real LogprobsTensors payload
23+
// (vllm/v1/outputs.py, ported at M1.7); the sampler's gather_logprobs fills
24+
// it. It stays std::optional (None => no logprobs requested this step).
2425
// ModelRunnerOutput: logprobs (LogprobsLists), prompt_logprobs_dict,
2526
// pooler_output, kv_connector_output / ec_connector_output (P/D KV
2627
// transfer), num_nans_in_logits, cudagraph_stats, routed_experts, and the
@@ -53,6 +54,7 @@
5354
#include <vector>
5455

5556
#include "vllm/sampling_params.h"
57+
#include "vllm/v1/outputs.h" // vllm::v1::LogprobsTensors (SamplerOutput payload)
5658
#include "vllm/v1/request.h" // vllm::v1::FinishReason (reused, not redefined)
5759

5860
namespace vllm::v1 {
@@ -78,9 +80,9 @@ struct EngineCoreRequest {
7880
struct SamplerOutput {
7981
// [num_reqs, max_num_generated_tokens]; T0 non-spec decode is [num_reqs, 1].
8082
std::vector<std::vector<int32_t>> sampled_token_ids;
81-
// logprobs_tensors upstream (LogprobsTensors | None). Payload deferred; the
82-
// engaged/disengaged state is preserved so downstream branching still ports.
83-
std::optional<bool> logprobs_tensors;
83+
// logprobs_tensors upstream (LogprobsTensors | None): the sampler's
84+
// gather_logprobs payload, None when no logprobs were requested this step.
85+
std::optional<LogprobsTensors> logprobs_tensors;
8486
};
8587

8688
// ModelRunnerOutput (vllm/v1/outputs.py): the runner -> scheduler result,

include/vllm/v1/outputs.h

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Ported from: vllm/v1/outputs.py @ e24d1b24
2+
//
3+
// The logprobs payload the V1 sampler produces. Upstream `LogprobsTensors` is a
4+
// NamedTuple of three torch tensors (+ an optional cumulative-count list); the
5+
// paired numpy form `LogprobsLists` is what crosses the process boundary. At T0
6+
// there is no torch: each "tensor" is a flat host vector with explicit dims, the
7+
// closest C++ analogue of the fixed [num_positions, k+1] / [num_positions] shapes.
8+
//
9+
// Field NAMES + shapes mirror upstream 1:1:
10+
// logprob_token_ids [num_positions, num_tokens_per_position] int32
11+
// logprobs [num_positions, num_tokens_per_position] float32
12+
// selected_token_ranks[num_positions] int32
13+
// where num_tokens_per_position == max_num_logprobs + 1 (the sampled token plus
14+
// the top-k). `num_positions == num_reqs x num_generated_tokens` (T0 non-spec
15+
// decode: one position per request).
16+
//
17+
// DEFERRED upstream members, intentionally omitted (marked): the
18+
// `cu_num_generated_tokens` slicing cursor (spec/jump decode — one position per
19+
// req at T0), and the torch/device helpers tolists / to_cpu_nonblocking /
20+
// filter / slice_request. The LogprobsLists numpy twin is represented by the
21+
// same flat-vector layout (the C++ side never re-serializes to numpy), so it is
22+
// not given a separate type here; add it when the OutputProcessor logprobs path
23+
// (M1.8) needs the distinct numpy-vs-tensor split.
24+
#ifndef VLLM_V1_OUTPUTS_H_
25+
#define VLLM_V1_OUTPUTS_H_
26+
27+
#include <cstdint>
28+
#include <vector>
29+
30+
namespace vllm::v1 {
31+
32+
// LogprobsTensors (vllm/v1/outputs.py): the (token_ids, logprobs, ranks) triple
33+
// the sampler's gather_logprobs (M1.7 Task 4) fills. Flat row-major storage;
34+
// element (pos, j) lives at index pos * num_tokens_per_position + j.
35+
struct LogprobsTensors {
36+
// Leading dim: num_reqs x num_generated_tokens (T0 non-spec: num_reqs).
37+
int num_positions = 0;
38+
// Trailing dim: max_num_logprobs + 1 (sampled token + top-k).
39+
int num_tokens_per_position = 0;
40+
41+
// [num_positions, num_tokens_per_position]
42+
std::vector<int32_t> logprob_token_ids;
43+
// [num_positions, num_tokens_per_position]
44+
std::vector<float> logprobs;
45+
// [num_positions]
46+
std::vector<int32_t> selected_token_ranks;
47+
48+
// Upstream LogprobsTensors.empty_cpu: allocate the (uninitialized) buffers at
49+
// the given shape.
50+
static LogprobsTensors empty_cpu(int num_positions,
51+
int num_tokens_per_position);
52+
};
53+
54+
} // namespace vllm::v1
55+
56+
#endif // VLLM_V1_OUTPUTS_H_

include/vllm/v1/sample/metadata.h

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Ported from: vllm/v1/sample/metadata.py @ e24d1b24
2+
//
3+
// SamplingMetadata — the per-slot sampling state the V1 Sampler (M1.7) consumes,
4+
// built once per step by InputBatch::make_sampling_metadata() (the port of
5+
// gpu_input_batch.py::_make_sampling_metadata). Field NAMES mirror upstream 1:1.
6+
//
7+
// ─── T0 field subset ────────────────────────────────────────────────────────
8+
// The scalar/vector fields are the num_reqs-dense slices of the InputBatch
9+
// per-slot arrays. Upstream keeps them as device tensors sliced `[:num_reqs]`;
10+
// here each is a host std::vector already truncated to num_reqs. The
11+
// None-when-not-needed optionals (temperature/top_p/top_k/prompt_token_ids/
12+
// allowed_token_ids_mask) preserve upstream's "skip the copy" semantics so the
13+
// sampler's branching ports unchanged.
14+
//
15+
// ─── logitsprocs → flat fields (recorded deviation) ─────────────────────────
16+
// Upstream carries a `logitsprocs: LogitsProcessors` plugin object graph plus
17+
// `logprob_token_ids`, `spec_token_ids`, `thinking_budget_state_holder`. We do
18+
// NOT port the plugin interface; instead the three T0 builtins
19+
// (vllm/v1/sample/logits_processor/builtin.py) are represented as flat inputs —
20+
// `min_tokens`, `logit_bias`, `min_p` — since Task 3 ports the three builtins
21+
// directly as functions rather than as a plugin dispatch. The remaining plugin
22+
// members are marked stubs below (defaulted empty/None) with their upstream cite.
23+
#ifndef VLLM_V1_SAMPLE_METADATA_H_
24+
#define VLLM_V1_SAMPLE_METADATA_H_
25+
26+
#include <cstdint>
27+
#include <map>
28+
#include <optional>
29+
#include <set>
30+
#include <vector>
31+
32+
namespace vllm::v1 {
33+
34+
// Per-request min-tokens state (the flattened MinTokensLogitsProcessor input,
35+
// vllm/v1/sample/logits_processor/builtin.py::MinTokensLogitsProcessor). While
36+
// `output_len < min_tokens`, the sampler masks every id in `stop_token_ids`
37+
// (eos + stop_token_ids, i.e. upstream `params.all_stop_token_ids`) to -inf.
38+
struct MinTokensState {
39+
int min_tokens = 0;
40+
std::set<int32_t> stop_token_ids;
41+
};
42+
43+
// SamplingMetadata (vllm/v1/sample/metadata.py::SamplingMetadata) — T0 subset.
44+
struct SamplingMetadata {
45+
// None when all_greedy (upstream skips the temperature copy). Else [num_reqs].
46+
std::optional<std::vector<float>> temperature;
47+
bool all_greedy = true;
48+
bool all_random = false;
49+
50+
// None when no_top_p / no_top_k. Else [num_reqs].
51+
std::optional<std::vector<float>> top_p;
52+
std::optional<std::vector<int32_t>> top_k;
53+
54+
// req_index -> per-request RNG seed. Upstream is `dict[int, torch.Generator]`;
55+
// we don't have torch.Generator, so we carry the seed (the actual seeded RNG
56+
// lands in Task 2's random_sample). Requests without their own seed are absent
57+
// from the map (upstream NOTE at gpu_input_batch.py:251-252).
58+
std::map<int, uint64_t> generators;
59+
60+
// None => no logprobs; 0 => sampled-token logprob only; k => top-k; -1 => all.
61+
std::optional<int> max_num_logprobs;
62+
63+
bool no_penalties = true;
64+
// None unless penalties (or a token-id-consuming proc) need it. Ragged per-req
65+
// prompt token ids (upstream: a padded [num_reqs, max_prompt_len] i32 tensor).
66+
std::optional<std::vector<std::vector<int32_t>>> prompt_token_ids;
67+
// [num_reqs] each (dense slices of the InputBatch penalty arrays).
68+
std::vector<float> frequency_penalties;
69+
std::vector<float> presence_penalties;
70+
std::vector<float> repetition_penalties;
71+
72+
// Per-request generated tokens so far (empty when no proc needs them, matching
73+
// upstream's needs_output_token_ids gate).
74+
std::vector<std::vector<int32_t>> output_token_ids;
75+
76+
// None unless a request restricts allowed ids. Upstream is a 2D bool tensor
77+
// [num_reqs, vocab]; represented here as row-major bool rows [num_reqs][vocab].
78+
std::optional<std::vector<std::vector<uint8_t>>> allowed_token_ids_mask;
79+
80+
// req_index -> list of bad-words token-id n-grams
81+
// (vllm/v1/sample/ops/bad_words.py::apply_bad_words input).
82+
std::map<int, std::vector<std::vector<int32_t>>> bad_words_token_ids;
83+
84+
// ─── T0 builtin logits-processor inputs (flat; see header deviation) ───────
85+
// req_index -> min-tokens state (MinTokensLogitsProcessor).
86+
std::map<int, MinTokensState> min_tokens;
87+
// req_index -> (token_id -> additive bias) (LogitBiasLogitsProcessor).
88+
std::map<int, std::map<int32_t, float>> logit_bias;
89+
// [num_reqs] min-p thresholds (MinPLogitsProcessor); 0 disables per row.
90+
std::vector<float> min_p;
91+
92+
// ─── STUBS (marked; defaulted empty/None at T0) ────────────────────────────
93+
// Upstream `logitsprocs: LogitsProcessors` plugin graph — NOT ported (the
94+
// three T0 builtins are the flat fields above). No field.
95+
//
96+
// Upstream `logprob_token_ids: dict[int, list[int]] | None` (generative-
97+
// scoring: gather logprobs for specific ids). Deferred behind this stub.
98+
std::optional<std::map<int, std::vector<int32_t>>> logprob_token_ids;
99+
// Upstream `spec_token_ids: list[list[int]] | None` (speculative decode).
100+
// Always empty lists at T0 (kept so the sampler's spec branch ports).
101+
std::optional<std::vector<std::vector<int32_t>>> spec_token_ids;
102+
// Upstream `thinking_budget_state_holder` — deferred; no field/flag ported.
103+
};
104+
105+
} // namespace vllm::v1
106+
107+
#endif // VLLM_V1_SAMPLE_METADATA_H_

include/vllm/v1/worker/gpu/input_batch.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777

7878
#include "vllm/sampling_params.h"
7979
#include "vllm/v1/core/sched/output.h" // NewRequestData (from_new_request)
80+
#include "vllm/v1/sample/metadata.h" // SamplingMetadata (make_sampling_metadata)
8081
#include "vllm/v1/worker/gpu/block_table.h"
8182

8283
namespace vllm::v1 {
@@ -150,6 +151,14 @@ class InputBatch {
150151
// num_reqs (property): len(req_id_to_index).
151152
int num_reqs() const { return static_cast<int>(req_id_to_index.size()); }
152153

154+
// make_sampling_metadata: build a SamplingMetadata for the current dense
155+
// [0, num_reqs) prefix from the per-slot arrays + predicates. Port of
156+
// gpu_input_batch.py::_make_sampling_metadata (M1.5-deferred, landed at M1.7).
157+
// Upstream caches this on self.sampling_metadata and rebuilds it whenever the
158+
// batch changes (add_request / condense); here it is recomputed on demand (the
159+
// caching + refresh_metadata scheduling is the model runner's concern, M1.8).
160+
SamplingMetadata make_sampling_metadata() const;
161+
153162
// Sampling predicates the M1.7 sampler keys on (upstream properties).
154163
bool all_greedy() const { return random_reqs.empty(); }
155164
bool all_random() const { return greedy_reqs.empty(); }

src/vllm/v1/outputs.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Ported from: vllm/v1/outputs.py @ e24d1b24
2+
// See include/vllm/v1/outputs.h for scope + the flat-vector layout.
3+
4+
#include "vllm/v1/outputs.h"
5+
6+
#include <cstddef>
7+
8+
namespace vllm::v1 {
9+
10+
LogprobsTensors LogprobsTensors::empty_cpu(int num_positions,
11+
int num_tokens_per_position) {
12+
LogprobsTensors out;
13+
out.num_positions = num_positions;
14+
out.num_tokens_per_position = num_tokens_per_position;
15+
const size_t area = static_cast<size_t>(num_positions) *
16+
static_cast<size_t>(num_tokens_per_position);
17+
out.logprob_token_ids.resize(area);
18+
out.logprobs.resize(area);
19+
out.selected_token_ranks.resize(static_cast<size_t>(num_positions));
20+
return out;
21+
}
22+
23+
} // namespace vllm::v1

src/vllm/v1/sample/metadata.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Ported from: vllm/v1/sample/metadata.py @ e24d1b24
2+
//
3+
// SamplingMetadata is a header-only value carrier (see
4+
// include/vllm/v1/sample/metadata.h). This translation unit exists so the header
5+
// is compiled standalone (self-containment check) and has a home in the build.
6+
// It is built by InputBatch::make_sampling_metadata()
7+
// (src/vllm/v1/worker/gpu/input_batch.cpp — the port of
8+
// gpu_input_batch.py::_make_sampling_metadata).
9+
#include "vllm/v1/sample/metadata.h"

src/vllm/v1/worker/gpu/input_batch.cpp

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,108 @@ int InputBatch::add_request(const CachedRequestState& request) {
219219
return req_index;
220220
}
221221

222+
SamplingMetadata InputBatch::make_sampling_metadata() const {
223+
// Port of gpu_input_batch.py::_make_sampling_metadata (@ e24d1b24). Fills the
224+
// dense [0, num_reqs) prefix, matching upstream's field-fill order + the
225+
// "skip the copy when not needed" None/[]-defaults.
226+
const int n = num_reqs();
227+
const size_t nn = static_cast<size_t>(n);
228+
SamplingMetadata md;
229+
230+
md.all_greedy = all_greedy();
231+
md.all_random = all_random();
232+
md.no_penalties = no_penalties();
233+
234+
// temperature: None when all_greedy, else the [:num_reqs] slice
235+
// (gpu_input_batch.py:834-839).
236+
if (!md.all_greedy) {
237+
md.temperature = std::vector<float>(temperature_cpu.begin(),
238+
temperature_cpu.begin() + nn);
239+
}
240+
// top_p / top_k: None when the corresponding predicate is empty
241+
// (gpu_input_batch.py:919-920).
242+
if (!no_top_p()) {
243+
md.top_p =
244+
std::vector<float>(top_p_cpu.begin(), top_p_cpu.begin() + nn);
245+
}
246+
if (!no_top_k()) {
247+
md.top_k =
248+
std::vector<int32_t>(top_k_cpu.begin(), top_k_cpu.begin() + nn);
249+
}
250+
251+
// Penalties are always sliced [:num_reqs] in the returned metadata
252+
// (gpu_input_batch.py:925-927); the device-copy is what upstream gates on
253+
// no_penalties, not the slice itself.
254+
md.frequency_penalties = std::vector<float>(
255+
frequency_penalties_cpu.begin(), frequency_penalties_cpu.begin() + nn);
256+
md.presence_penalties = std::vector<float>(
257+
presence_penalties_cpu.begin(), presence_penalties_cpu.begin() + nn);
258+
md.repetition_penalties = std::vector<float>(
259+
repetition_penalties_cpu.begin(), repetition_penalties_cpu.begin() + nn);
260+
261+
// prompt_token_ids: only when penalties (or a token-id-consuming proc, always
262+
// false at T0) need them (gpu_input_batch.py:861-876). Ragged per-req prompt
263+
// slice of token_ids_cpu[:, :num_prompt_tokens].
264+
const bool needs_prompt_token_ids = !md.no_penalties;
265+
if (needs_prompt_token_ids) {
266+
std::vector<std::vector<int32_t>> prompts(nn);
267+
for (int i = 0; i < n; ++i) {
268+
const int np = num_prompt_tokens[static_cast<size_t>(i)];
269+
const size_t row =
270+
static_cast<size_t>(i) * static_cast<size_t>(max_model_len);
271+
prompts[static_cast<size_t>(i)].assign(
272+
token_ids_cpu.begin() + static_cast<std::ptrdiff_t>(row),
273+
token_ids_cpu.begin() + static_cast<std::ptrdiff_t>(row) + np);
274+
}
275+
md.prompt_token_ids = std::move(prompts);
276+
}
277+
278+
// output_token_ids: only when a proc needs them (gpu_input_batch.py:884-894).
279+
// At T0 that is !no_penalties (bad_words / logitsprocs_need_output_token_ids
280+
// are always empty/false). Empty [] otherwise, matching upstream.
281+
const bool needs_output_token_ids = !md.no_penalties;
282+
if (needs_output_token_ids) {
283+
md.output_token_ids.resize(nn);
284+
for (int i = 0; i < n; ++i) {
285+
const auto& row = req_output_token_ids[static_cast<size_t>(i)];
286+
if (row.has_value()) {
287+
md.output_token_ids[static_cast<size_t>(i)] = *row;
288+
}
289+
}
290+
}
291+
292+
// spec_token_ids: pass the dense prefix (always empty lists at T0). Upstream
293+
// passes self.spec_token_ids directly (gpu_input_batch.py:929).
294+
md.spec_token_ids = std::vector<std::vector<int32_t>>(
295+
spec_token_ids.begin(), spec_token_ids.begin() + nn);
296+
297+
// ─── Fields whose InputBatch-side tracking is NOT yet landed (marked) ──────
298+
// Each is a faithful upstream default (empty/None) with its dependency cite;
299+
// wiring them requires per-slot state this InputBatch does not yet keep.
300+
//
301+
// * generators (gpu_input_batch.py:921, sourced :413-414 from
302+
// request.generator == sampling_params.seed): InputBatch keeps no per-req
303+
// generator/seed slot yet. Left empty; the seeded RNG + this map's
304+
// population is a Task-2 (random_sample) dependency. Type is present so
305+
// Task 2 can fill req_index -> seed.
306+
// * max_num_logprobs (gpu_input_batch.py:922 / :1122, from the num_logprobs
307+
// dict populated by sampling_params.logprobs): no num_logprobs tracking
308+
// here — left None (no logprobs). Logprobs wiring is an M1.8 dependency.
309+
// * allowed_token_ids_mask (gpu_input_batch.py:896-904) + bad_words_token_ids
310+
// (:932): no has_allowed_token_ids / bad_words slot tracking — left
311+
// None / empty. A Task-3 / M1.8 dependency (SamplingParams also defers
312+
// allowed_token_ids / bad_words_token_ids).
313+
// * min_tokens / logit_bias / min_p (the T0 builtins,
314+
// logits_processor/builtin.py): InputBatch keeps no per-slot min_p array /
315+
// min_tokens+stop set / logit_bias map yet — left empty. Populating them
316+
// (min_p is on SamplingParams; logit_bias / all_stop_token_ids are
317+
// deferred on SamplingParams) is a Task-3 dependency.
318+
// md.generators / max_num_logprobs / allowed_token_ids_mask /
319+
// bad_words_token_ids / min_tokens / logit_bias / min_p keep their defaults.
320+
321+
return md;
322+
}
323+
222324
std::optional<int> InputBatch::remove_request(const std::string& req_id) {
223325
const auto it = req_id_to_index.find(req_id);
224326
if (it == req_id_to_index.end()) {

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ vllm_cpp_add_test(test_sched_output vllm/v1/test_sched_output.cpp)
3232
vllm_cpp_add_test(test_scheduler vllm/v1/test_scheduler.cpp)
3333
vllm_cpp_add_test(test_block_table vllm/v1/worker/test_block_table.cpp)
3434
vllm_cpp_add_test(test_input_batch vllm/v1/worker/test_input_batch.cpp)
35+
vllm_cpp_add_test(test_sampling_metadata vllm/v1/sample/test_metadata.cpp)
3536
vllm_cpp_add_test(test_prepare_inputs vllm/v1/worker/test_prepare_inputs.cpp)
3637
vllm_cpp_add_test(test_common_attn_metadata vllm/v1/attention/test_common_attn_metadata.cpp)
3738
vllm_cpp_add_test(test_gdn_metadata_builder vllm/v1/attention/test_gdn_metadata_builder.cpp)

0 commit comments

Comments
 (0)