Skip to content

Commit c7ba3a5

Browse files
mudlerclaude
andcommitted
feat(v1/engine): OutputProcessor — detokenize + string-stop + RequestOutput (M1.8 Task 5)
Port src/vllm/v1/engine/output_processor.{h,cpp} from vllm/v1/engine/output_processor.py @ e24d1b24 — the T0 synchronous text path: OutputProcessor + RequestState + OutputProcessorOutput. - add_request builds a RequestState with our IncrementalDetokenizer (RequestState::FromNewRequest, the detokenizer branch), keyed by request_id. - process_outputs loops the EngineCoreOutputs: drives detokenizer.Update() (incremental text + STRING-level stop match), assembles the streaming-delta vs cumulative CompletionOutput/RequestOutput per RequestOutputKind, removes finished req states, and returns reqs_to_abort for requests the detokenizer stopped but EngineCore did not (output_processor.py:678). - Deferred (marked 1:1 stubs): LogprobsProcessor, pooling, routed_experts, parallel sampling, async RequestOutputCollector queue, streaming-input chunk queue, iteration/per-request stats, tracing, num_cached_tokens. Tests (real IncrementalDetokenizer + tiny BPE fixture): streaming DELTA deltas concatenate to the cumulative full text; stop-string terminate + reqs_to_abort; finish-reason mapping (LENGTH/STOP); EngineCore-signaled EOS finish with no abort; unknown-request output ignored. 5 cases / 37 assertions green; full ctest 57/57 under warnings-as-errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 73a9509 commit c7ba3a5

5 files changed

Lines changed: 679 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ add_library(vllm STATIC
8787
src/vllm/v1/engine/detokenizer.cpp
8888
src/vllm/v1/engine/core.cpp
8989
src/vllm/v1/engine/input_processor.cpp
90+
src/vllm/v1/engine/output_processor.cpp
9091
src/vllm/v1/executor/executor.cpp
9192
src/vt/dtype.cpp
9293
src/vt/backend.cpp
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Ported from: vllm/v1/engine/output_processor.py @ e24d1b24
2+
// (OutputProcessor + RequestState + OutputProcessorOutput — the T0 synchronous
3+
// text path: incremental detokenize + string-level stop + RequestOutput
4+
// assembly.)
5+
//
6+
// Scope (M1.8 Task 5): turn the per-step EngineCoreOutputs into RequestOutputs.
7+
// This is the SYNCHRONOUS LLMEngine path (no queue): process_outputs loops the
8+
// EngineCoreOutputs, drives our IncrementalDetokenizer.Update() (which does the
9+
// STRING-level stop match the scheduler's token-level check_stop cannot), builds
10+
// the streaming-delta vs full CompletionOutput/RequestOutput per RequestOutputKind,
11+
// removes finished req states, and returns the reqs_to_abort feedback for
12+
// requests the detokenizer stopped but EngineCore did not (output_processor.py
13+
// :678). Mirrors OutputProcessor.process_outputs (:576-693),
14+
// RequestState.from_new_request (:210-270), make_request_output (:272-331),
15+
// _new_completion_output (:376-411), _new_request_output (:333-374),
16+
// _finish_request (:695-707).
17+
//
18+
// DEVIATIONS vs the pinned API (recorded, use OUR names):
19+
// - __init__ takes a nullable tokenizer pointer + stream_interval (T0). The
20+
// upstream log_stats / tracing_enabled knobs are deferred (stats/tracing are
21+
// deferred below), so they are dropped.
22+
// - RequestState has no external_req_id field upstream-separate at T0: our
23+
// EngineCoreRequest deferred external_req_id (see v1/engine/types.h), so the
24+
// external id == request_id here (no parallel-sampling / streaming-input
25+
// remap). The external_req_ids map is kept for structural parity with
26+
// _finish_request even though it degenerates to a 1:1 mapping at T0.
27+
// - kv_transfer_params is dropped from make_request_output (our EngineCoreOutput
28+
// has no kv_transfer_params field — deferred in v1/engine/types.h).
29+
// - make_request_output returns std::optional<RequestOutput> (pooling deferred,
30+
// so PoolingRequestOutput never occurs; the None return still models
31+
// FINAL_ONLY / stream_interval hold-back).
32+
// - The upstream private _new_* helpers are PascalCase here (NewCompletionOutput
33+
// / NewRequestOutput); _finish_request -> FinishRequest.
34+
//
35+
// DEFERRED (marked; matches upstream so re-adding is mechanical):
36+
// LogprobsProcessor (sample + prompt logprobs), pooling outputs
37+
// (PoolingOutput / PoolingRequestOutput branch), routed_experts accumulation,
38+
// parallel sampling (ParentRequest / parent_requests / get_outputs),
39+
// the async RequestOutputCollector queue (AsyncLLM only), streaming-input
40+
// chunk queue (StreamingUpdate / apply_streaming_update / resumable),
41+
// iteration + per-request stats (RequestStateStats / IterationStats /
42+
// LoRARequestStates), tracing (do_tracing), num_cached_tokens / prefill_stats,
43+
// LoRA, prompt_embeds, and the client-initiated abort_requests() path (needs
44+
// the queue/pooling/parent machinery).
45+
#pragma once
46+
47+
#include <cstdint>
48+
#include <map>
49+
#include <memory>
50+
#include <optional>
51+
#include <string>
52+
#include <vector>
53+
54+
#include "vllm/outputs.h"
55+
#include "vllm/sampling_params.h"
56+
#include "vllm/v1/engine/detokenizer.h"
57+
#include "vllm/v1/engine/types.h"
58+
#include "vllm/v1/request.h"
59+
60+
namespace vllm::tok {
61+
class Tokenizer; // vllm/tokenizer/tokenizer.h
62+
}
63+
64+
namespace vllm::v1 {
65+
66+
// OutputProcessorOutput (@dataclass, output_processor.py:109-113). The
67+
// synchronous return of process_outputs: the RequestOutputs to hand back to the
68+
// caller + the request ids EngineCore must abort (detokenizer-detected stop that
69+
// EngineCore did not itself signal). PoolingRequestOutput deferred.
70+
struct OutputProcessorOutput {
71+
std::vector<RequestOutput> request_outputs;
72+
std::vector<std::string> reqs_to_abort;
73+
};
74+
75+
// RequestState (output_processor.py:129): per-request accumulation state held by
76+
// the OutputProcessor for the life of a request. Members are public, mirroring
77+
// the upstream dataclass-like attributes; the deferred members are omitted (see
78+
// the file header).
79+
class RequestState {
80+
public:
81+
// from_new_request (:210): build a RequestState with our IncrementalDetokenizer
82+
// (the detokenizer branch). `tokenizer` may be nullptr (=> no detokenization);
83+
// if sampling_params.detokenize is false the tokenizer is dropped as upstream
84+
// (:223). LogprobsProcessor / pooling / parent_req / queue are deferred.
85+
static RequestState FromNewRequest(const tok::Tokenizer* tokenizer,
86+
const EngineCoreRequest& request,
87+
std::optional<std::string> prompt,
88+
int request_index, int stream_interval);
89+
90+
// make_request_output (:272): assemble the streaming-delta vs full
91+
// CompletionOutput/RequestOutput honoring output_kind. Returns nullopt when
92+
// FINAL_ONLY-and-not-finished or a stream_interval hold-back suppresses this
93+
// step's output. kv_transfer_params deferred (see header).
94+
std::optional<RequestOutput> make_request_output(
95+
const std::vector<int32_t>& new_token_ids,
96+
std::optional<FinishReason> finish_reason,
97+
std::optional<std::string> stop_reason);
98+
99+
std::string request_id;
100+
std::string external_req_id; // == request_id at T0 (see header).
101+
int request_index = 0;
102+
RequestOutputKind output_kind = RequestOutputKind::kCumulative;
103+
std::optional<std::string> prompt;
104+
std::vector<int32_t> prompt_token_ids;
105+
size_t prompt_len = 0;
106+
std::unique_ptr<IncrementalDetokenizer> detokenizer;
107+
std::optional<int> max_tokens_param;
108+
bool is_prefilling = true;
109+
int num_cached_tokens = 0; // deferred (no prefill_stats at T0); stays 0.
110+
int stream_interval = 1;
111+
size_t sent_tokens_offset = 0;
112+
113+
private:
114+
// _new_completion_output (:376): text/token_ids in delta vs cumulative mode.
115+
CompletionOutput NewCompletionOutput(std::vector<int32_t> token_ids,
116+
std::optional<FinishReason> finish_reason,
117+
std::optional<std::string> stop_reason);
118+
// _new_request_output (:333): wrap the CompletionOutput(s) in a RequestOutput.
119+
RequestOutput NewRequestOutput(const std::string& external_req_id,
120+
std::vector<CompletionOutput> outputs,
121+
bool finished);
122+
};
123+
124+
// OutputProcessor (output_processor.py:417): process EngineCoreOutputs into
125+
// RequestOutputs. T0 synchronous path only.
126+
class OutputProcessor {
127+
public:
128+
// __init__ (:420). `tokenizer` may be nullptr (=> no detokenization). It must
129+
// outlive the OutputProcessor. log_stats / tracing_enabled deferred.
130+
explicit OutputProcessor(const tok::Tokenizer* tokenizer,
131+
int stream_interval = 1);
132+
133+
int get_num_unfinished_requests() const {
134+
return static_cast<int>(request_states_.size());
135+
}
136+
bool has_unfinished_requests() const { return !request_states_.empty(); }
137+
138+
// add_request (:512): build + register a RequestState. parent_req / queue /
139+
// the streaming-update re-entry are deferred (T0: a request_id appears once).
140+
void add_request(const EngineCoreRequest& request,
141+
std::optional<std::string> prompt, int request_index = 0);
142+
143+
// process_outputs (:576): the per-EngineCoreOutput loop — detokenize + stop +
144+
// RequestOutput assembly + reqs_to_abort feedback. Stats/tracing/timestamp
145+
// args deferred.
146+
OutputProcessorOutput process_outputs(
147+
const EngineCoreOutputs& engine_core_outputs);
148+
149+
private:
150+
// _finish_request (:695): remove the finished req state from the maps.
151+
void FinishRequest(RequestState& req_state);
152+
153+
const tok::Tokenizer* tokenizer_;
154+
int stream_interval_;
155+
std::map<std::string, std::unique_ptr<RequestState>> request_states_;
156+
// external_req_id -> [internal request_id, ...] (1:1 at T0, see header).
157+
std::map<std::string, std::vector<std::string>> external_req_ids_;
158+
};
159+
160+
} // namespace vllm::v1

0 commit comments

Comments
 (0)