|
| 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