Skip to content

Commit c65e650

Browse files
mudlerclaude
andcommitted
feat(v1/sched): SchedulerOutput + NewRequestData/CachedRequestData
Ported from vllm/v1/core/sched/output.py @ e24d1b24 (M1.4 Task 2). Value types the scheduler produces + the model runner (M1.5) consumes: NewRequestData (full new-req payload + from_request), CachedRequestData (diff-only running-req payload + make_empty/is_context_phase/num_reqs), SchedulerOutput (T0 field set + make_empty). Behavioral only, all CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fc81a43 commit c65e650

5 files changed

Lines changed: 378 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ add_library(vllm STATIC
6868
src/vllm/v1/core/kv_cache_coordinator.cpp
6969
src/vllm/v1/core/kv_cache_manager.cpp
7070
src/vllm/v1/core/sched/request_queue.cpp
71+
src/vllm/v1/core/sched/output.cpp
7172
src/vllm/v1/engine/types.cpp
7273
src/vllm/v1/engine/detokenizer.cpp
7374
src/vt/dtype.cpp
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Ported from: vllm/v1/core/sched/output.py @ e24d1b24
2+
//
3+
// Scope (M1.4 Task 2): the value types the V1 Scheduler PRODUCES each step and
4+
// the model runner (M1.5) CONSUMES — the new-request full payload, the
5+
// running-request diff payload, and the SchedulerOutput envelope that carries
6+
// them across the (upstream: process) boundary. Behavioral only: plain value
7+
// carriers, no CUDA / model. Field names + the new-vs-cached diff semantics are
8+
// mirrored 1:1 with upstream.
9+
//
10+
// THE new-vs-cached DIFF PROTOCOL (mirrored 1:1):
11+
// * scheduled_new_reqs: requests scheduled for the FIRST time carry their
12+
// FULL data (NewRequestData) — prompt, sampling params, the complete
13+
// per-group block_ids, num_computed_tokens. The worker caches this so it is
14+
// never re-sent.
15+
// * scheduled_cached_reqs: requests scheduled BEFORE carry only the DIFF
16+
// (CachedRequestData) — parallel arrays over req_ids: the NEWLY allocated
17+
// block_ids per request per group (appended to the cached block table,
18+
// unless the req_id is in resumed_req_ids in which case they REPLACE it),
19+
// the updated num_computed_tokens, num_output_tokens, and (PP-only)
20+
// new_token_ids. The heavy per-request state already lives in the worker.
21+
//
22+
// DEFERRED upstream state (marked; T0 never populates these):
23+
// NewRequestData: mm_features (multimodal), pooling_params, lora_request,
24+
// prompt_embeds, prompt_is_token_ids, prefill_token_ids (v2 model runner).
25+
// SchedulerOutput trailing optionals, OMITTED here (a later unit slots them
26+
// back in without reshaping the struct): preempted_req_ids /
27+
// new_block_ids_to_zero (v2 model runner), has_structured_output_requests /
28+
// pending_structured_output_tokens (grammar), num_invalid_spec_tokens /
29+
// num_spec_tokens_to_schedule (spec decode), kv_connector_metadata /
30+
// ec_connector_metadata (KV/EC transfer). GrammarOutput (structured output)
31+
// is likewise omitted.
32+
//
33+
// DEVIATIONS, recorded:
34+
// - Upstream NewRequestData.prompt_token_ids is `list[int] | None` and
35+
// .sampling_params is `SamplingParams | None`; represented here as
36+
// std::optional<...>. from_request always populates both in the T0
37+
// pure-token / generation path (pooling is deferred), but the optional
38+
// preserves the upstream nullability.
39+
// - block_ids is `tuple[list[int], ...]` upstream (one list per KV cache
40+
// group); here std::vector<std::vector<int>>, exactly what
41+
// KVCacheBlocks::get_block_ids() / KVCacheManager::get_block_ids() return.
42+
// - CachedRequestData.new_block_ids entries are
43+
// `tuple[list[int], ...] | None` upstream (None => no new blocks this step,
44+
// from get_block_ids(allow_none=True)); here
45+
// std::optional<std::vector<std::vector<int>>>.
46+
// - Upstream caches _req_id_to_num_output_tokens (a cached_property) for O(1)
47+
// is_context_phase lookups; here is_context_phase does a linear scan over
48+
// req_ids (T0 batches are small; the value types are rebuilt fresh each
49+
// step so no cache-invalidation concern). Semantics are identical.
50+
// - all_token_ids is the MRV1-only connector-propagation map (dict[str,
51+
// list[int]] upstream); carried for shape fidelity, left empty in T0.
52+
// - The scheduler assembles CachedRequestData field-by-field via
53+
// scheduler.py::_make_cached_request_data (lands in Task 3); output.py
54+
// itself exposes ONLY make_empty (no from_request/append constructor). The
55+
// public aggregate here matches that — the diff-building loop is Task 3.
56+
#pragma once
57+
58+
#include <map>
59+
#include <optional>
60+
#include <set>
61+
#include <string>
62+
#include <vector>
63+
64+
#include "vllm/sampling_params.h"
65+
66+
namespace vllm::v1 {
67+
68+
struct Request; // vllm/v1/request.h (from_request source; cpp includes it).
69+
70+
// NewRequestData: the FULL payload for a request scheduled for the first time.
71+
// (Upstream NewRequestData dataclass.)
72+
struct NewRequestData {
73+
std::string req_id;
74+
// Upstream list[int] | None — always populated in the T0 token path.
75+
std::optional<std::vector<int32_t>> prompt_token_ids;
76+
// Upstream SamplingParams | None — always populated for generation requests.
77+
std::optional<SamplingParams> sampling_params;
78+
// Per-KV-cache-group block ids (upstream tuple[list[int], ...]).
79+
std::vector<std::vector<int>> block_ids;
80+
int num_computed_tokens = 0;
81+
82+
// from_request: build the full payload from the Request + its allocated
83+
// per-group block ids. Copies req_id, prompt_token_ids, sampling_params,
84+
// block_ids, num_computed_tokens. (Upstream NewRequestData.from_request;
85+
// mm/pooling/lora/prompt_embeds/prefill_token_ids DEFERRED.)
86+
static NewRequestData from_request(const Request& request,
87+
std::vector<std::vector<int>> block_ids);
88+
};
89+
90+
// CachedRequestData: the DIFF-only payload for requests scheduled before.
91+
// Parallel arrays indexed by position over req_ids. (Upstream CachedRequestData
92+
// dataclass.)
93+
struct CachedRequestData {
94+
std::vector<std::string> req_ids;
95+
// req_ids in this set have their block table REPLACED by new_block_ids
96+
// (resumed from preemption); those not in it have new_block_ids APPENDED.
97+
std::set<std::string> resumed_req_ids;
98+
// PP-only: the sampled token ids to forward. Empty when PP is not used.
99+
std::vector<std::vector<int32_t>> new_token_ids;
100+
// MRV1-only connector propagation (req_id -> full token ids). Empty in T0.
101+
std::map<std::string, std::vector<int32_t>> all_token_ids;
102+
// Per request: the NEWLY allocated per-group block ids, or nullopt when no
103+
// new blocks were allocated this step (upstream get_block_ids(allow_none)).
104+
std::vector<std::optional<std::vector<std::vector<int>>>> new_block_ids;
105+
std::vector<int> num_computed_tokens;
106+
std::vector<int> num_output_tokens;
107+
108+
// num_reqs (property): len(req_ids).
109+
int num_reqs() const { return static_cast<int>(req_ids.size()); }
110+
111+
// is_context_phase: true iff the request is present and still in its prefill
112+
// (context) phase, i.e. num_output_tokens == 0 for that req_id. (Upstream
113+
// is_context_phase via the _req_id_to_num_output_tokens cache; linear scan
114+
// here — see the header DEVIATIONS note.)
115+
bool is_context_phase(const std::string& req_id) const;
116+
117+
// make_empty: the no-cached-requests diff.
118+
static CachedRequestData make_empty();
119+
};
120+
121+
// SchedulerOutput: the per-step envelope the model runner consumes. (Upstream
122+
// SchedulerOutput dataclass — T0 field subset; trailing optionals OMITTED, see
123+
// the header DEFERRED note.)
124+
struct SchedulerOutput {
125+
// Requests scheduled for the first time (full data, worker-cached).
126+
std::vector<NewRequestData> scheduled_new_reqs;
127+
// Requests scheduled before (diff only).
128+
CachedRequestData scheduled_cached_reqs;
129+
130+
// req_id -> number of tokens scheduled for it this step.
131+
std::map<std::string, int> num_scheduled_tokens;
132+
// sum(num_scheduled_tokens.values()).
133+
int total_num_scheduled_tokens = 0;
134+
135+
// req_id -> spec decode token ids. DEFERRED semantics: always empty in T0.
136+
std::map<std::string, std::vector<int32_t>> scheduled_spec_decode_tokens;
137+
// req_id -> encoder input indices to process. DEFERRED semantics: empty in T0.
138+
std::map<std::string, std::vector<int>> scheduled_encoder_inputs;
139+
// Number of common prefix blocks per KV cache group (for cascade attention).
140+
std::vector<int> num_common_prefix_blocks;
141+
142+
// Request ids finished between the previous and current step (free cached
143+
// worker state for these).
144+
std::set<std::string> finished_req_ids;
145+
// mm_hash strings whose encoder outputs can be freed. DEFERRED: empty in T0.
146+
std::vector<std::string> free_encoder_mm_hashes;
147+
148+
// make_empty: an empty step output.
149+
static SchedulerOutput make_empty();
150+
};
151+
152+
} // namespace vllm::v1

src/vllm/v1/core/sched/output.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Ported from: vllm/v1/core/sched/output.py @ e24d1b24
2+
// See include/vllm/v1/core/sched/output.h for the new-vs-cached diff protocol,
3+
// the deferred-field list, and the recorded deviations.
4+
#include "vllm/v1/core/sched/output.h"
5+
6+
#include <utility>
7+
8+
#include "vllm/v1/request.h"
9+
10+
namespace vllm::v1 {
11+
12+
NewRequestData NewRequestData::from_request(
13+
const Request& request, std::vector<std::vector<int>> block_ids) {
14+
NewRequestData data;
15+
data.req_id = request.request_id;
16+
data.prompt_token_ids = request.prompt_token_ids;
17+
data.sampling_params = request.sampling_params;
18+
data.block_ids = std::move(block_ids);
19+
data.num_computed_tokens = request.num_computed_tokens;
20+
return data;
21+
}
22+
23+
bool CachedRequestData::is_context_phase(const std::string& req_id) const {
24+
for (std::size_t i = 0; i < req_ids.size(); ++i) {
25+
if (req_ids[i] == req_id) {
26+
return i < num_output_tokens.size() && num_output_tokens[i] == 0;
27+
}
28+
}
29+
return false;
30+
}
31+
32+
CachedRequestData CachedRequestData::make_empty() {
33+
return CachedRequestData{};
34+
}
35+
36+
SchedulerOutput SchedulerOutput::make_empty() {
37+
SchedulerOutput output;
38+
output.scheduled_cached_reqs = CachedRequestData::make_empty();
39+
return output;
40+
}
41+
42+
} // namespace vllm::v1

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ vllm_cpp_add_test(test_kv_cache_coordinator vllm/v1/test_kv_cache_coordinator.cp
2828
vllm_cpp_add_test(test_kv_cache_manager vllm/v1/test_kv_cache_manager.cpp)
2929
vllm_cpp_add_test(test_scheduler_config vllm/test_scheduler_config.cpp)
3030
vllm_cpp_add_test(test_request_queue vllm/v1/test_request_queue.cpp)
31+
vllm_cpp_add_test(test_sched_output vllm/v1/test_sched_output.cpp)
3132
vllm_cpp_add_test(test_engine_types vllm/test_engine_types.cpp)
3233
vllm_cpp_add_test(test_outputs vllm/test_outputs.cpp)
3334
vllm_cpp_add_test(test_cuda_backend vt/test_cuda_backend.cpp)
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// Tests for the SchedulerOutput / NewRequestData / CachedRequestData port
2+
// (vllm/v1/core/sched/output.py @ e24d1b24).
3+
//
4+
// Upstream output.py has no dedicated unit test; these types are exercised via
5+
// tests/v1/core/test_scheduler.py (the new-vs-cached diff shape the model runner
6+
// consumes). Ported here as direct construct-and-check value-carrier tests: the
7+
// diff protocol (full NewRequestData vs diff-only CachedRequestData), the
8+
// from_request field copy, make_empty, and the SchedulerOutput envelope.
9+
#include <doctest/doctest.h>
10+
11+
#include <cstdint>
12+
#include <optional>
13+
#include <string>
14+
#include <vector>
15+
16+
#include "vllm/sampling_params.h"
17+
#include "vllm/v1/core/sched/output.h"
18+
#include "vllm/v1/request.h"
19+
20+
using vllm::SamplingParams;
21+
using vllm::v1::CachedRequestData;
22+
using vllm::v1::NewRequestData;
23+
using vllm::v1::Request;
24+
using vllm::v1::SchedulerOutput;
25+
26+
namespace {
27+
28+
Request MakeRequest(const std::string& id,
29+
std::vector<int32_t> prompt = {1, 2, 3, 4}) {
30+
SamplingParams params;
31+
params.max_tokens = 16;
32+
Request req(id, std::move(prompt), params, /*arrival_time=*/0.0);
33+
req.num_computed_tokens = 2;
34+
return req;
35+
}
36+
37+
} // namespace
38+
39+
TEST_CASE("NewRequestData::from_request copies the T0 fields + block_ids") {
40+
Request req = MakeRequest("req-0", {10, 11, 12});
41+
// Per-group block ids (one group here).
42+
std::vector<std::vector<int>> block_ids = {{7, 8, 9}};
43+
44+
NewRequestData data = NewRequestData::from_request(req, block_ids);
45+
46+
CHECK(data.req_id == "req-0");
47+
REQUIRE(data.prompt_token_ids.has_value());
48+
CHECK(data.prompt_token_ids.value() == std::vector<int32_t>{10, 11, 12});
49+
REQUIRE(data.sampling_params.has_value());
50+
CHECK(data.sampling_params->max_tokens == 16);
51+
CHECK(data.num_computed_tokens == 2);
52+
REQUIRE(data.block_ids.size() == 1);
53+
CHECK(data.block_ids[0] == std::vector<int>{7, 8, 9});
54+
}
55+
56+
TEST_CASE("NewRequestData carries multi-group block_ids by group") {
57+
Request req = MakeRequest("req-mg");
58+
std::vector<std::vector<int>> block_ids = {{1, 2}, {3, 4, 5}};
59+
60+
NewRequestData data = NewRequestData::from_request(req, block_ids);
61+
62+
REQUIRE(data.block_ids.size() == 2);
63+
CHECK(data.block_ids[0] == std::vector<int>{1, 2});
64+
CHECK(data.block_ids[1] == std::vector<int>{3, 4, 5});
65+
}
66+
67+
TEST_CASE("CachedRequestData::make_empty is the empty diff") {
68+
CachedRequestData cached = CachedRequestData::make_empty();
69+
70+
CHECK(cached.num_reqs() == 0);
71+
CHECK(cached.req_ids.empty());
72+
CHECK(cached.resumed_req_ids.empty());
73+
CHECK(cached.new_token_ids.empty());
74+
CHECK(cached.all_token_ids.empty());
75+
CHECK(cached.new_block_ids.empty());
76+
CHECK(cached.num_computed_tokens.empty());
77+
CHECK(cached.num_output_tokens.empty());
78+
}
79+
80+
TEST_CASE("CachedRequestData diff shape: parallel arrays over req_ids") {
81+
CachedRequestData cached;
82+
cached.req_ids = {"a", "b"};
83+
// "b" is resumed from preemption -> its block table is REPLACED.
84+
cached.resumed_req_ids = {"b"};
85+
// Per request: newly allocated per-group block ids; nullopt = none this step.
86+
cached.new_block_ids = {
87+
std::optional<std::vector<std::vector<int>>>{{{20, 21}}}, // a: append
88+
std::optional<std::vector<std::vector<int>>>{{{30}}}, // b: replace
89+
};
90+
cached.num_computed_tokens = {5, 8};
91+
cached.num_output_tokens = {1, 0};
92+
93+
CHECK(cached.num_reqs() == 2);
94+
// "a" is appended (not resumed); "b" is resumed (replace).
95+
CHECK(cached.resumed_req_ids.count("a") == 0);
96+
CHECK(cached.resumed_req_ids.count("b") == 1);
97+
98+
REQUIRE(cached.new_block_ids.size() == 2);
99+
REQUIRE(cached.new_block_ids[0].has_value());
100+
CHECK(cached.new_block_ids[0].value()[0] == std::vector<int>{20, 21});
101+
REQUIRE(cached.new_block_ids[1].has_value());
102+
CHECK(cached.new_block_ids[1].value()[0] == std::vector<int>{30});
103+
104+
CHECK(cached.num_computed_tokens == std::vector<int>{5, 8});
105+
CHECK(cached.num_output_tokens == std::vector<int>{1, 0});
106+
}
107+
108+
TEST_CASE("CachedRequestData new_block_ids nullopt = no new blocks this step") {
109+
CachedRequestData cached;
110+
cached.req_ids = {"only"};
111+
cached.new_block_ids = {std::nullopt};
112+
cached.num_computed_tokens = {3};
113+
cached.num_output_tokens = {2};
114+
115+
REQUIRE(cached.new_block_ids.size() == 1);
116+
CHECK_FALSE(cached.new_block_ids[0].has_value());
117+
}
118+
119+
TEST_CASE("CachedRequestData::is_context_phase reflects num_output_tokens") {
120+
CachedRequestData cached;
121+
cached.req_ids = {"prefill", "decode"};
122+
cached.num_output_tokens = {0, 4};
123+
124+
// prefill: still 0 output tokens -> context (prefill) phase.
125+
CHECK(cached.is_context_phase("prefill"));
126+
// decode: has output tokens -> not context phase.
127+
CHECK_FALSE(cached.is_context_phase("decode"));
128+
// unknown req_id -> false.
129+
CHECK_FALSE(cached.is_context_phase("missing"));
130+
}
131+
132+
TEST_CASE("SchedulerOutput::make_empty is an empty step") {
133+
SchedulerOutput out = SchedulerOutput::make_empty();
134+
135+
CHECK(out.scheduled_new_reqs.empty());
136+
CHECK(out.scheduled_cached_reqs.num_reqs() == 0);
137+
CHECK(out.num_scheduled_tokens.empty());
138+
CHECK(out.total_num_scheduled_tokens == 0);
139+
CHECK(out.scheduled_spec_decode_tokens.empty());
140+
CHECK(out.scheduled_encoder_inputs.empty());
141+
CHECK(out.num_common_prefix_blocks.empty());
142+
CHECK(out.finished_req_ids.empty());
143+
CHECK(out.free_encoder_mm_hashes.empty());
144+
}
145+
146+
TEST_CASE("SchedulerOutput carries new + cached reqs, token map, finished ids") {
147+
Request new_req = MakeRequest("new-1", {100, 101});
148+
149+
SchedulerOutput out;
150+
out.scheduled_new_reqs.push_back(
151+
NewRequestData::from_request(new_req, {{42}}));
152+
153+
CachedRequestData cached;
154+
cached.req_ids = {"cached-1"};
155+
cached.new_block_ids = {std::optional<std::vector<std::vector<int>>>{{{43}}}};
156+
cached.num_computed_tokens = {6};
157+
cached.num_output_tokens = {3};
158+
out.scheduled_cached_reqs = cached;
159+
160+
out.num_scheduled_tokens = {{"new-1", 2}, {"cached-1", 1}};
161+
out.total_num_scheduled_tokens = 3;
162+
out.num_common_prefix_blocks = {0};
163+
out.finished_req_ids = {"done-1"};
164+
165+
// New reqs carry FULL data.
166+
REQUIRE(out.scheduled_new_reqs.size() == 1);
167+
CHECK(out.scheduled_new_reqs[0].req_id == "new-1");
168+
CHECK(out.scheduled_new_reqs[0].block_ids[0] == std::vector<int>{42});
169+
170+
// Cached reqs carry only the DIFF.
171+
CHECK(out.scheduled_cached_reqs.num_reqs() == 1);
172+
CHECK(out.scheduled_cached_reqs.req_ids[0] == "cached-1");
173+
174+
// Token accounting: total == sum of the per-request map.
175+
int sum = 0;
176+
for (const auto& [id, n] : out.num_scheduled_tokens) sum += n;
177+
CHECK(sum == out.total_num_scheduled_tokens);
178+
CHECK(out.num_scheduled_tokens.at("new-1") == 2);
179+
CHECK(out.num_scheduled_tokens.at("cached-1") == 1);
180+
181+
CHECK(out.finished_req_ids.count("done-1") == 1);
182+
}

0 commit comments

Comments
 (0)