Skip to content

Commit 73a9509

Browse files
mudlerclaude
andcommitted
feat(v1/engine): InputProcessor text path + SamplingParams PostInit close (M1.8 Task 2)
Port src/vllm/v1/engine/input_processor.{h,cpp} — the T0 text slice of vllm/v1/engine/input_processor.py::process_inputs @ e24d1b24: - _validate_params runs SamplingParams::PostInit()/Verify() (closes the M1.1 deferred-__post_init__ carry: the InputProcessor is the constructing unit). - tokenize the prompt via tok::Tokenizer::Encode -> prompt_token_ids. - default max_tokens = max_model_len - len(prompt) when unset. - update_from_generation_config: set sampling_params.eos_token_id + merge secondary eos ids into stop_token_ids (both gated on ignore_eos). - build EngineCoreRequest(request_id, prompt_token_ids, sampling_params, arrival_time). Deferred (marked stubs): dict/EngineInput/embeds/mm/pooling/LoRA/enc-dec, update_from_tokenizer bad_words, _validate_model_inputs, request-id randomization. Tests (tests/vllm/v1/test_input_processor.cpp, real qwen36 tokenizer fixture): token-ids match Encode, PostInit normalization (temp clamp + greedy sub-params), Verify throws on invalid params, max_tokens default, eos/stop wiring. 56/56 ctest green under warnings-as-errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 20ca8c2 commit 73a9509

5 files changed

Lines changed: 430 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ add_library(vllm STATIC
8686
src/vllm/v1/engine/types.cpp
8787
src/vllm/v1/engine/detokenizer.cpp
8888
src/vllm/v1/engine/core.cpp
89+
src/vllm/v1/engine/input_processor.cpp
8990
src/vllm/v1/executor/executor.cpp
9091
src/vt/dtype.cpp
9192
src/vt/backend.cpp
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Ported from: vllm/v1/engine/input_processor.py @ e24d1b24
2+
// (InputProcessor.__init__ + process_inputs, the T0 text path).
3+
//
4+
// Scope (M1.8 Task 2): turn a text prompt + SamplingParams into an
5+
// EngineCoreRequest the EngineCore (Task 1) can schedule. This is the T0 slice of
6+
// upstream InputProcessor.process_inputs (input_processor.py:242-385):
7+
// _validate_params -> RUN SamplingParams::PostInit()/Verify() (see below)
8+
// preprocess/tokenize -> tokenizer.Encode(prompt) -> prompt_token_ids
9+
// sampling_params.clone() (implicit: params passed by value)
10+
// default max_tokens = model_config.max_model_len - len(prompt) when unset
11+
// update_from_generation_config(generation_config, eos_token_id) (:323)
12+
// update_from_tokenizer(tokenizer) (:328)
13+
// -> EngineCoreRequest(request_id, prompt_token_ids, sampling_params, ...)
14+
//
15+
// THE M1.1 CARRY THIS CLOSES: our SamplingParams (M1.1) deferred __post_init__
16+
// to "the constructing unit — M1.8". The InputProcessor IS that unit:
17+
// ValidateParams runs PostInit() (which normalizes in place AND runs Verify()),
18+
// mirroring upstream, where __post_init__ ran at SamplingParams construction and
19+
// process_inputs then calls params.verify(model_config, ...).
20+
//
21+
// DEVIATIONS vs the pinned API (recorded, use OUR names):
22+
// - __init__ takes VllmConfig (from which it pulls model_config,
23+
// generation_config_fields and a renderer holding the tokenizer). We hold a
24+
// tokenizer + HfConfig reference directly (the T0 deps), deriving
25+
// max_model_len from HfConfig.max_position_embeddings and the primary eos +
26+
// secondary eos ids from HfConfig.raw["eos_token_id"] (int OR list) with a
27+
// Tokenizer::EosId() fallback. HfConfig has no max_model_len override
28+
// (rope-scaling etc.), so max_position_embeddings stands in for it at T0.
29+
// - process_inputs signature reordered to (request_id, prompt, params,
30+
// arrival_time): only the text prompt + SamplingParams path is kept.
31+
// - update_from_generation_config: our SamplingParams dropped the
32+
// _all_stop_token_ids field (M1.1 — the detokenizer computes its own stop
33+
// buffer), so the only observable T0 effect is setting eos_token_id and
34+
// merging the SECONDARY eos ids into stop_token_ids (both gated on
35+
// ignore_eos), matching sampling_params.py:627-655.
36+
// - update_from_tokenizer is a no-op stub: upstream only processes bad_words
37+
// there (sampling_params.py:657), and bad_words is a deferred SamplingParams
38+
// field (M1.1).
39+
//
40+
// DEFERRED (marked; matches upstream so re-adding is mechanical): dict/EngineInput
41+
// prompts, prompt_embeds, encoder/decoder split, multimodal (mm_features),
42+
// pooling (PoolingParams), LoRA, data_parallel_rank validation, request-id
43+
// randomization (assign_request_id), _validate_model_inputs (prompt-length /
44+
// out-of-vocab checks), current_platform.validate_request, trace_headers,
45+
// priority, resumable.
46+
#pragma once
47+
48+
#include <cstdint>
49+
#include <optional>
50+
#include <string>
51+
#include <vector>
52+
53+
#include "vllm/sampling_params.h"
54+
#include "vllm/transformers_utils/hf_config.h"
55+
#include "vllm/v1/engine/types.h"
56+
57+
namespace vllm::tok {
58+
class Tokenizer; // vllm/tokenizer/tokenizer.h
59+
}
60+
61+
namespace vllm::v1 {
62+
63+
class InputProcessor {
64+
public:
65+
// __init__ (T0 deps): the tokenizer + HfConfig the processor reads. Both must
66+
// outlive the InputProcessor. Derives max_model_len + eos ids up front.
67+
InputProcessor(const tok::Tokenizer& tokenizer, const HfConfig& config);
68+
69+
// process_inputs (text path): validate + tokenize + build the request.
70+
// `params` is taken BY VALUE (upstream clones it); PostInit()/eos-wiring
71+
// mutate the local copy, never the caller's. Throws std::runtime_error via
72+
// SamplingParams::Verify() on invalid params. arrival_time defaults to the
73+
// wall clock (upstream time.time()).
74+
EngineCoreRequest process_inputs(
75+
const std::string& request_id, const std::string& prompt,
76+
SamplingParams params,
77+
std::optional<double> arrival_time = std::nullopt) const;
78+
79+
private:
80+
// _validate_params: runs SamplingParams::PostInit() (normalize + Verify) —
81+
// this closes the M1.1 deferred-__post_init__ carry.
82+
void ValidateParams(SamplingParams& params) const;
83+
// update_from_generation_config (T0 subset: eos_token_id + secondary stop ids).
84+
void UpdateFromGenerationConfig(SamplingParams& params) const;
85+
// update_from_tokenizer (T0 no-op: bad_words is deferred).
86+
void UpdateFromTokenizer(SamplingParams& params) const;
87+
88+
const tok::Tokenizer& tokenizer_;
89+
const HfConfig& config_;
90+
// model_config.max_model_len (T0: HfConfig.max_position_embeddings).
91+
int64_t max_model_len_ = 0;
92+
// The primary eos id (renderer.get_eos_token_id()): the eos_ids list head, or
93+
// the tokenizer's eos, or unset.
94+
std::optional<int32_t> eos_token_id_;
95+
// generation_config["eos_token_id"] as a list (int is a 1-element list).
96+
std::vector<int32_t> generation_config_eos_ids_;
97+
};
98+
99+
} // namespace vllm::v1
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Ported from: vllm/v1/engine/input_processor.py @ e24d1b24
2+
// See include/vllm/v1/engine/input_processor.h for scope, deviations and
3+
// deferrals.
4+
#include "vllm/v1/engine/input_processor.h"
5+
6+
#include <chrono>
7+
#include <set>
8+
#include <utility>
9+
10+
#include "vllm/tokenizer/tokenizer.h"
11+
12+
namespace vllm::v1 {
13+
namespace {
14+
15+
// Wall-clock seconds since the epoch, mirroring upstream time.time().
16+
double NowSeconds() {
17+
const auto now = std::chrono::system_clock::now().time_since_epoch();
18+
return std::chrono::duration<double>(now).count();
19+
}
20+
21+
} // namespace
22+
23+
InputProcessor::InputProcessor(const tok::Tokenizer& tokenizer,
24+
const HfConfig& config)
25+
: tokenizer_(tokenizer), config_(config) {
26+
// model_config.max_model_len. HfConfig has no dedicated max_model_len (rope
27+
// scaling etc. are deferred), so max_position_embeddings stands in at T0.
28+
max_model_len_ = config.max_position_embeddings;
29+
30+
// renderer.get_eos_token_id() + generation_config["eos_token_id"]: derive the
31+
// primary eos id and the secondary eos-id list from config.json's
32+
// "eos_token_id" (int OR list), falling back to the tokenizer's own eos.
33+
bool found = false;
34+
if (config.raw.is_object()) {
35+
auto it = config.raw.find("eos_token_id");
36+
if (it != config.raw.end() && !it->is_null()) {
37+
if (it->is_number_integer()) {
38+
const auto id = it->get<int32_t>();
39+
eos_token_id_ = id;
40+
generation_config_eos_ids_.push_back(id);
41+
found = true;
42+
} else if (it->is_array()) {
43+
for (const auto& e : *it) {
44+
if (e.is_number_integer()) {
45+
generation_config_eos_ids_.push_back(e.get<int32_t>());
46+
}
47+
}
48+
if (!generation_config_eos_ids_.empty()) {
49+
eos_token_id_ = generation_config_eos_ids_.front();
50+
found = true;
51+
}
52+
}
53+
}
54+
}
55+
if (!found && tokenizer_.EosId() >= 0) {
56+
eos_token_id_ = tokenizer_.EosId();
57+
generation_config_eos_ids_.push_back(tokenizer_.EosId());
58+
}
59+
}
60+
61+
void InputProcessor::ValidateParams(SamplingParams& params) const {
62+
// Upstream _validate_params calls params.verify(model_config, ...) after
63+
// __post_init__ already ran at construction. Our SamplingParams deferred
64+
// __post_init__ to this constructing unit (M1.1), so PostInit() both
65+
// normalizes the params AND runs Verify() — closing that carry.
66+
params.PostInit();
67+
}
68+
69+
void InputProcessor::UpdateFromGenerationConfig(SamplingParams& params) const {
70+
// sampling_params.py:627-655 (T0 subset). _all_stop_token_ids is deferred
71+
// (M1.1), so its side of this is dropped; the observable effects are setting
72+
// eos_token_id and merging the SECONDARY eos ids into stop_token_ids.
73+
if (!params.ignore_eos) {
74+
params.eos_token_id = eos_token_id_;
75+
}
76+
77+
if (generation_config_eos_ids_.empty()) {
78+
return;
79+
}
80+
std::set<int32_t> eos_ids(generation_config_eos_ids_.begin(),
81+
generation_config_eos_ids_.end());
82+
// The primary eos id is handled separately for stopping; don't duplicate it.
83+
if (eos_token_id_.has_value()) {
84+
eos_ids.erase(*eos_token_id_);
85+
}
86+
if (!eos_ids.empty() && !params.ignore_eos) {
87+
for (int32_t id : params.stop_token_ids) {
88+
eos_ids.insert(id);
89+
}
90+
params.stop_token_ids.assign(eos_ids.begin(), eos_ids.end());
91+
}
92+
}
93+
94+
void InputProcessor::UpdateFromTokenizer(SamplingParams& params) const {
95+
// sampling_params.py:657 only processes bad_words, a deferred SamplingParams
96+
// field (M1.1) -> no-op at T0.
97+
(void)params;
98+
}
99+
100+
EngineCoreRequest InputProcessor::process_inputs(
101+
const std::string& request_id, const std::string& prompt,
102+
SamplingParams params, std::optional<double> arrival_time) const {
103+
// _validate_params: run PostInit()/Verify() on the (cloned) params.
104+
ValidateParams(params);
105+
106+
const double t = arrival_time.has_value() ? *arrival_time : NowSeconds();
107+
108+
// input_preprocessor.preprocess -> tokenize (text path only).
109+
std::vector<int32_t> prompt_token_ids = tokenizer_.Encode(prompt);
110+
111+
// params is already our clone (passed by value). If unset max_tokens, then
112+
// generate up to the max_model_len (input_processor.py:317-321).
113+
if (!params.max_tokens.has_value()) {
114+
const int64_t seq_len = static_cast<int64_t>(prompt_token_ids.size());
115+
params.max_tokens = static_cast<int>(max_model_len_ - seq_len);
116+
}
117+
118+
UpdateFromGenerationConfig(params);
119+
UpdateFromTokenizer(params);
120+
121+
EngineCoreRequest request;
122+
request.request_id = request_id;
123+
request.prompt_token_ids = std::move(prompt_token_ids);
124+
request.sampling_params = std::move(params);
125+
request.arrival_time = t;
126+
return request;
127+
}
128+
129+
} // namespace vllm::v1

tests/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ vllm_cpp_add_test(test_common_attn_metadata vllm/v1/attention/test_common_attn_m
4242
vllm_cpp_add_test(test_gdn_metadata_builder vllm/v1/attention/test_gdn_metadata_builder.cpp)
4343
vllm_cpp_add_test(test_engine_types vllm/test_engine_types.cpp)
4444
vllm_cpp_add_test(test_engine_core vllm/v1/test_engine_core.cpp)
45+
vllm_cpp_add_test(test_input_processor vllm/v1/test_input_processor.cpp)
46+
target_compile_definitions(test_input_processor PRIVATE
47+
PARITY_GOLDENS_DIR="${CMAKE_SOURCE_DIR}/tests/parity/goldens")
4548
vllm_cpp_add_test(test_outputs vllm/test_outputs.cpp)
4649
vllm_cpp_add_test(test_cuda_backend vt/test_cuda_backend.cpp)
4750
vllm_cpp_add_test(test_cuda_ops vt/test_cuda_ops.cpp)

0 commit comments

Comments
 (0)