[Rust Frontend][gRPC] Add explicit data-parallel rank routing (#51178)
Signed-off-by: Connor Carpenter <connorc@nvidia.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Connor Carpenter committed
Aug 10, 2026 at 15:05 UTC
fa722b9f0188179a66728f2f882e9d5bc92c9b0c
19 files changed
+215
-60
rust/src/cmd/src/cli.rs
+8
@@ -457,6 +457,7 @@ impl SharedRuntimeArgs {
457
coordinator_address: Option<String>,
458
engine_start_index: u32,
459
engine_count: usize,
460
+ data_parallel_size: usize,
461
) -> Config {
462
let ready_timeout = self.ready_timeout();
463
let shutdown_timeout = self.shutdown_timeout();
@@ -474,6 +475,7 @@ impl SharedRuntimeArgs {
475
engine_count,
476
ready_timeout,
477
},
478
+ data_parallel_size,
479
coordinator_mode: match coordinator_address {
480
Some(address) => CoordinatorMode::External { address },
481
None => CoordinatorMode::None,
@@ -530,6 +532,7 @@ impl SharedRuntimeArgs {
532
local_input_address,
533
local_output_address,
534
},
535
+ data_parallel_size: engine_count,
536
coordinator_mode: CoordinatorMode::MaybeInProc,
537
model: self.model,
538
served_model_name: self.served_model_name,
@@ -661,6 +664,9 @@ pub struct FrontendArgs {
664
/// Total number of data-parallel engines expected for this frontend.
665
#[arg(long, default_value_t = 1)]
666
pub engine_count: usize,
667
+ /// Deployment-wide configured data-parallel size. Defaults to engine count.
668
+ #[arg(long)]
669
+ pub data_parallel_size: Option<usize>,
670
671
/// Shared frontend arguments as one JSON object.
672
#[arg(long = "args-json", value_parser = parse_runtime_args_json, value_name = "JSON")]
@@ -670,6 +676,7 @@ pub struct FrontendArgs {
676
impl FrontendArgs {
677
/// Convert the CLI arguments into the OpenAI server's runtime config.
678
pub fn into_config(self) -> Config {
679
+ let data_parallel_size = self.data_parallel_size.unwrap_or(self.engine_count);
680
self.runtime.into_bootstrapped_config(
681
self.listen_fd,
682
self.input_address,
@@ -677,6 +684,7 @@ impl FrontendArgs {
684
self.coordinator_address,
685
self.engine_start_index,
686
self.engine_count,
687
+ data_parallel_size,
688
)
689
}
690
}
rust/src/cmd/src/cli/tests.rs
+9
@@ -785,6 +785,7 @@ fn frontend_args_accept_json() {
785
),
786
engine_start_index: 0,
787
engine_count: 1,
788
+ data_parallel_size: None,
789
runtime: SharedRuntimeArgs {
790
model: "Qwen/Qwen3-0.6B",
791
engine_ready_timeout_secs: 600,
@@ -846,6 +847,8 @@ fn frontend_args_json_applies_defaults() {
847
"ipc:///tmp/input.sock",
848
"--output-address",
849
"ipc:///tmp/output.sock",
850
+ "--engine-count",
851
+ "4",
852
"--args-json",
853
r#"{"model_tag":"Qwen/Qwen3-0.6B"}"#,
854
])
@@ -861,6 +864,7 @@ fn frontend_args_json_applies_defaults() {
864
assert_eq!(args.runtime.renderer, RendererSelection::Auto);
865
assert_eq!(args.runtime.max_logprobs, None);
866
assert_eq!(args.runtime.shutdown_timeout, 0);
867
+ assert_eq!(args.into_config().data_parallel_size, 4);
868
}
869
870
#[test]
@@ -1517,6 +1521,7 @@ fn serve_frontend_config_uses_dp_address_as_advertised_host() {
1521
"<ipc output>",
1522
),
1523
},
1524
+ data_parallel_size: 4,
1525
coordinator_mode: MaybeInProc,
1526
model: "Qwen/Qwen3-0.6B",
1527
served_model_name: [],
@@ -1602,6 +1607,7 @@ fn serve_frontend_config_keeps_tcp_transport_for_non_local_only_topology() {
1607
local_input_address: None,
1608
local_output_address: None,
1609
},
1610
+ data_parallel_size: 4,
1611
coordinator_mode: MaybeInProc,
1612
model: "Qwen/Qwen3-0.6B",
1613
served_model_name: [],
@@ -1685,6 +1691,8 @@ fn frontend_config_uses_external_coordinator_when_coordinator_address_is_present
1691
"3",
1692
"--engine-count",
1693
"1",
1694
+ "--data-parallel-size",
1695
+ "4",
1696
"--args-json",
1697
r#"{"model_tag":"Qwen/Qwen3-0.6B"}"#,
1698
])
@@ -1704,6 +1712,7 @@ fn frontend_config_uses_external_coordinator_when_coordinator_address_is_present
1712
engine_count: 1,
1713
ready_timeout: 600s,
1714
},
1715
+ data_parallel_size: 4,
1716
coordinator_mode: External {
1717
address: "tcp://127.0.0.1:7000",
1718
},
rust/src/engine-core-client/src/client.rs
-9
@@ -451,15 +451,6 @@ impl EngineCoreClient {
451
.world_size
452
}
453
454
- /// Return the data parallel size from the parallel config, if available.
455
- pub fn data_parallel_size(&self) -> u64 {
456
- self.engines
457
- .first()
458
- .expect("engine core client requires at least one engine")
459
- .ready_response
460
- .data_parallel_size
461
- }
462
-
454
/// Get the model name associated with this client used for metrics
455
/// labeling.
456
pub fn model_name(&self) -> &str {
rust/src/engine-core-client/src/client/state.rs
+22
-18
@@ -161,15 +161,16 @@ impl RequestRegistry {
161
162
fn choose_engine_for_request(&mut self, data_parallel_rank: Option<u32>) -> Result<EngineId> {
163
if let Some(rank) = data_parallel_rank {
164
- // Route to the engine at the specified rank index.
165
- let engine_id = EngineId::from_engine_index(rank);
166
- return self
167
- .routing_per_engine
168
- .contains_key(&engine_id)
169
- .then_some(engine_id)
164
+ let engine_id = u16::try_from(rank).ok().map(EngineId::from_engine_index);
165
+ return engine_id
166
+ .filter(|engine_id| self.routing_per_engine.contains_key(engine_id))
167
.ok_or_else(|| Error::InvalidDataParallelRank {
168
rank,
172
- num_engines: self.routing_per_engine.len() as u32,
169
+ connected_ranks: self
170
+ .routing_per_engine
171
+ .keys()
172
+ .filter_map(EngineId::engine_index)
173
+ .collect(),
174
});
175
}
176
@@ -343,6 +344,9 @@ impl RequestRegistry {
344
}
345
346
fn apply_scheduler_counts(&mut self, engine_index: u32, next: EngineLoadSnapshot) -> bool {
347
+ let Ok(engine_index) = u16::try_from(engine_index) else {
348
+ return false;
349
+ };
350
let engine_id = EngineId::from_engine_index(engine_index);
351
let Some(state) = self.routing_per_engine.get_mut(&engine_id) else {
352
return false;
@@ -840,26 +844,26 @@ mod tests {
844
error,
845
crate::error::Error::InvalidDataParallelRank {
846
rank: 2,
843
- num_engines: 2,
844
- }
847
+ connected_ranks,
848
+ } if connected_ranks == vec![0, 1]
849
));
850
}
851
852
#[test]
849
- fn register_with_rank_on_single_engine_only_accepts_zero() {
850
- let engine_0 = EngineId::from_engine_index(0);
851
- let mut registry = RequestRegistry::new(&[connected_engine(engine_0.clone())]);
853
+ fn register_with_rank_uses_global_engine_identity() {
854
+ let engine_3 = EngineId::from_engine_index(3);
855
+ let mut registry = RequestRegistry::new(&[connected_engine(engine_3.clone())]);
856
853
- let (chosen, _) = registry.register("req-ok".to_string(), None, Some(0)).unwrap();
854
- assert_eq!(chosen, engine_0);
857
+ let (chosen, _) = registry.register("req-ok".to_string(), None, Some(3)).unwrap();
858
+ assert_eq!(chosen, engine_3);
859
856
- let error = registry.register("req-bad".to_string(), None, Some(1)).unwrap_err();
860
+ let error = registry.register("req-bad".to_string(), None, Some(0)).unwrap_err();
861
assert!(matches!(
862
error,
863
crate::error::Error::InvalidDataParallelRank {
860
- rank: 1,
861
- num_engines: 1,
862
- }
864
+ rank: 0,
865
+ connected_ranks,
866
+ } if connected_ranks == vec![3]
867
));
868
}
869
rust/src/engine-core-client/src/error.rs
+7
-2
@@ -68,8 +68,13 @@ pub enum Error {
68
ControlClosed { message: String },
69
#[error("request `{request_id}` is already in flight")]
70
DuplicateRequestId { request_id: String },
71
- #[error("data parallel rank {rank} is out of range for {num_engines} engine(s)")]
72
- InvalidDataParallelRank { rank: u32, num_engines: u32 },
71
+ #[error(
72
+ "data parallel rank {rank} is not connected to this frontend; connected ranks: {connected_ranks:?}"
73
+ )]
74
+ InvalidDataParallelRank {
75
+ rank: u32,
76
+ connected_ranks: Vec<u32>,
77
+ },
78
#[error("engine-core output dispatcher closed: {message}")]
79
DispatcherClosed { message: String },
80
#[error("engine-core client is closed: {message}")]
rust/src/engine-core-client/src/transport.rs
+22
-4
@@ -66,8 +66,8 @@ impl EngineId {
66
67
/// Construct an engine id from the Python-compatible engine index encoding
68
/// (two-byte little-endian).
69
- pub fn from_engine_index(value: u32) -> Self {
70
- Self(Bytes::copy_from_slice(&(value as u16).to_le_bytes()))
69
+ pub fn from_engine_index(value: u16) -> Self {
70
+ Self(Bytes::copy_from_slice(&value.to_le_bytes()))
71
}
72
}
73
@@ -336,6 +336,22 @@ pub async fn connect_bootstrapped(
336
if engine_count == 0 {
337
bail_unexpected_handshake_message!("expected engine_count >= 1");
338
}
339
+ let engine_start_index =
340
+ u16::try_from(engine_start_index).map_err(|_| Error::UnexpectedHandshakeMessage {
341
+ message: "engine_start_index exceeds the two-byte engine identity limit".to_string(),
342
+ })?;
343
+ let engine_end_index =
344
+ usize::from(engine_start_index).checked_add(engine_count).ok_or_else(|| {
345
+ Error::UnexpectedHandshakeMessage {
346
+ message: "engine_start_index + engine_count overflows".to_string(),
347
+ }
348
+ })?;
349
+ if engine_end_index > usize::from(u16::MAX) + 1 {
350
+ return Err(Error::UnexpectedHandshakeMessage {
351
+ message: "engine_start_index + engine_count exceeds the two-byte engine identity limit"
352
+ .to_string(),
353
+ });
354
+ }
355
356
let mut input_socket = RouterSocket::new();
357
let input_address = input_socket.bind(input_address).await?.to_string();
@@ -345,8 +361,10 @@ pub async fn connect_bootstrapped(
361
362
let engines = wait_for_input_registrations(
363
&mut input_socket,
348
- (0..engine_count)
349
- .map(|offset| EngineId::from_engine_index(engine_start_index + offset as u32)),
364
+ (0..engine_count).map(|offset| {
365
+ let offset = u16::try_from(offset).expect("validated engine offset fits u16");
366
+ EngineId::from_engine_index(engine_start_index + offset)
367
+ }),
368
ready_timeout,
369
)
370
.await?;
rust/src/mock-engine/src/lib.rs
+7
-8
@@ -48,10 +48,13 @@ pub struct Opt {
48
}
49
50
/// Run one mock engine until shutdown or transport failure.
51
-async fn run_engine(engine_index: u32, opt: Opt, shutdown: CancellationToken) -> Result<()> {
51
+async fn run_engine(engine_index: usize, opt: Opt, shutdown: CancellationToken) -> Result<()> {
52
+ let engine_index =
53
+ u16::try_from(engine_index).context("mock engine index exceeds two-byte identity limit")?;
54
+ let engine_id = EngineId::from_engine_index(engine_index);
55
let MockEngineSockets { data_sockets, .. } = connect_to_frontend(
56
&opt.handshake_address,
54
- EngineId::from_engine_index(engine_index),
57
+ engine_id,
58
MockEngineConfig::default(),
59
)
60
.await
@@ -71,7 +74,7 @@ async fn run_engine(engine_index: u32, opt: Opt, shutdown: CancellationToken) ->
74
));
75
// Engine loop: input_rx -> engine logic -> output_tx
76
let mut engine_loop = tokio::spawn(engine::run_engine_loop(
74
- engine_index,
77
+ u32::from(engine_index),
78
opt,
79
input_rx,
80
output_tx,
@@ -111,11 +114,7 @@ pub async fn run(opt: Opt, shutdown: CancellationToken) -> Result<()> {
114
115
let mut engines = JoinSet::new();
116
for engine_index in 0..opt.engine_count {
114
- engines.spawn(run_engine(
115
- engine_index as u32,
116
- opt.clone(),
117
- shutdown.clone(),
118
- ));
117
+ engines.spawn(run_engine(engine_index, opt.clone(), shutdown.clone()));
118
}
119
120
tokio::select! {
rust/src/server/examples/external_engine_openai_qwen.rs
+1
@@ -58,6 +58,7 @@ async fn main() -> Result<()> {
58
local_input_address: None,
59
local_output_address: None,
60
},
61
+ data_parallel_size: args.engine_count,
62
coordinator_mode: CoordinatorMode::MaybeInProc,
63
model: args.model,
64
served_model_name: vec![],
rust/src/server/src/config.rs
+42
@@ -161,6 +161,8 @@ impl TlsConfig {
161
pub struct Config {
162
/// Frontend-to-engine transport setup.
163
pub transport_mode: TransportMode,
164
+ /// Deployment-wide data-parallel size retained by the frontend.
165
+ pub data_parallel_size: usize,
166
/// Requested frontend-side coordinator behavior.
167
pub coordinator_mode: CoordinatorMode,
168
/// Backend model identifier used for engine-core loading.
@@ -237,6 +239,46 @@ impl Config {
239
max_logprobs
240
);
241
}
242
+ if self.data_parallel_size == 0 {
243
+ bail!("data parallel size must be at least 1");
244
+ }
245
+ if self.data_parallel_size > usize::from(u16::MAX) + 1 {
246
+ bail!(
247
+ "data parallel size ({}) exceeds the two-byte engine identity limit",
248
+ self.data_parallel_size
249
+ );
250
+ }
251
+ match &self.transport_mode {
252
+ TransportMode::HandshakeOwner { engine_count, .. } => {
253
+ if *engine_count != self.data_parallel_size {
254
+ bail!(
255
+ "managed frontend engine count ({engine_count}) must equal data parallel size ({})",
256
+ self.data_parallel_size
257
+ );
258
+ }
259
+ }
260
+ TransportMode::Bootstrapped {
261
+ engine_start_index,
262
+ engine_count,
263
+ ..
264
+ } => {
265
+ if *engine_count == 0 {
266
+ bail!("engine count must be at least 1");
267
+ }
268
+ let engine_start_index = usize::try_from(*engine_start_index)
269
+ .map_err(|_| anyhow::anyhow!("engine start index does not fit usize"))?;
270
+ let engine_end_index =
271
+ engine_start_index.checked_add(*engine_count).ok_or_else(|| {
272
+ anyhow::anyhow!("engine start index + engine count overflows")
273
+ })?;
274
+ if engine_end_index > self.data_parallel_size {
275
+ bail!(
276
+ "connected engine range [{engine_start_index}, {engine_end_index}) exceeds data parallel size {}",
277
+ self.data_parallel_size
278
+ );
279
+ }
280
+ }
281
+ }
282
283
Ok(())
284
}
rust/src/server/src/grpc/control.rs
+1
-1
@@ -31,7 +31,7 @@ impl ControlServiceImpl {
31
pb::ParallelismInfo {
32
tensor_parallel_size: ready.tensor_parallel_size,
33
pipeline_parallel_size: ready.pipeline_parallel_size,
34
- data_parallel_size: ready.data_parallel_size.min(u64::from(u32::MAX)) as u32,
34
+ data_parallel_size: self.state.data_parallel_size().min(u32::MAX as usize) as u32,
35
data_parallel_rank: ready.data_parallel_rank,
36
decode_context_parallel_size: ready.decode_context_parallel_size,
37
}
rust/src/server/src/grpc/inference.rs
+25
-2
@@ -22,6 +22,8 @@ use crate::state::AppState;
22
23
pub(crate) type InferenceGrpcService = InferenceServer<InferenceServiceImpl>;
24
25
+const DATA_PARALLEL_RANK_METADATA_KEY: &str = "x-data-parallel-rank";
26
+
27
/// gRPC inference service backed by the shared application state.
28
pub struct InferenceServiceImpl {
29
state: Arc<AppState>,
@@ -41,6 +43,7 @@ impl InferenceServiceImpl {
43
async fn prepare_request(
44
&self,
45
mut proto_request: pb::GenerateRequest,
46
+ data_parallel_rank: Option<u32>,
47
stream: bool,
48
rpc: &'static str,
49
) -> Result<PreparedGrpcRequest, Status> {
@@ -62,6 +65,7 @@ impl InferenceServiceImpl {
65
rpc,
66
%model,
67
media_count,
68
+ ?data_parallel_rank,
69
);
70
info!(parent: &request_span, "gRPC inference request received");
71
@@ -70,6 +74,7 @@ impl InferenceServiceImpl {
74
let mut text_request =
75
convert::to_text_request(proto_request, stream, self.state.served_model_names())?;
76
text_request.arrival_time = Some(arrival_time);
77
+ text_request.data_parallel_rank = data_parallel_rank;
78
79
let media = convert::media_parts_from_request(media)?;
80
if !media.is_empty() {
@@ -115,6 +120,20 @@ impl InferenceServiceImpl {
120
}
121
}
122
123
+fn data_parallel_rank_from_metadata(
124
+ request: &Request<pb::GenerateRequest>,
125
+) -> Result<Option<u32>, Status> {
126
+ let Some(value) = request.metadata().get(DATA_PARALLEL_RANK_METADATA_KEY) else {
127
+ return Ok(None);
128
+ };
129
+ let value = value.to_str().map_err(|_| {
130
+ Status::invalid_argument("x-data-parallel-rank metadata must be an unsigned 32-bit integer")
131
+ })?;
132
+ value.trim().parse::<u32>().map(Some).map_err(|_| {
133
+ Status::invalid_argument("x-data-parallel-rank metadata must be an unsigned 32-bit integer")
134
+ })
135
+}
136
+
137
#[tonic::async_trait]
138
impl pb::inference_server::Inference for InferenceServiceImpl {
139
type GenerateStreamStream =
@@ -125,13 +144,14 @@ impl pb::inference_server::Inference for InferenceServiceImpl {
144
&self,
145
request: Request<pb::GenerateRequest>,
146
) -> Result<Response<pb::GenerateResponse>, Status> {
147
+ let data_parallel_rank = data_parallel_rank_from_metadata(&request)?;
148
let proto_req = request.into_inner();
149
let response_opts = ResponseOpts::from_proto(proto_req.response.as_ref());
150
let PreparedGrpcRequest {
151
text_request,
152
request_span,
153
started_at,
134
- } = self.prepare_request(proto_req, false, "Generate").await?;
154
+ } = self.prepare_request(proto_req, data_parallel_rank, false, "Generate").await?;
155
156
let stream = self
157
.state
@@ -186,13 +206,16 @@ impl pb::inference_server::Inference for InferenceServiceImpl {
206
&self,
207
request: Request<pb::GenerateRequest>,
208
) -> Result<Response<Self::GenerateStreamStream>, Status> {
209
+ let data_parallel_rank = data_parallel_rank_from_metadata(&request)?;
210
let proto_req = request.into_inner();
211
let response_opts = ResponseOpts::from_proto(proto_req.response.as_ref());
212
let PreparedGrpcRequest {
213
text_request,
214
request_span,
215
started_at,
195
- } = self.prepare_request(proto_req, true, "GenerateStream").await?;
216
+ } = self
217
+ .prepare_request(proto_req, data_parallel_rank, true, "GenerateStream")
218
+ .await?;
219
220
let stream = self
221
.state
rust/src/server/src/grpc/tests.rs
+37
-5
@@ -843,6 +843,37 @@ async fn unary_generate_missing_prompt_returns_invalid_argument() {
843
server_task.abort();
844
}
845
846
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
847
+#[serial]
848
+async fn unary_generate_unconnected_data_parallel_rank_returns_invalid_argument() {
849
+ let (mut client, server_task, _engine_task) = grpc_test_server(
850
+ EngineId::from_engine_index(3),
851
+ default_stream_output_specs(),
852
+ )
853
+ .await;
854
+
855
+ let mut request = tonic::Request::new(pb::GenerateRequest {
856
+ request_id: "test-unconnected-dp-rank".to_string(),
857
+ model: "test-model".to_string(),
858
+ prompt: Some(pb::generate_request::Prompt::Text("hello".to_string())),
859
+ ..Default::default()
860
+ });
861
+ request.metadata_mut().insert(
862
+ "x-data-parallel-rank",
863
+ "0".parse().expect("valid metadata value"),
864
+ );
865
+
866
+ let status = client
867
+ .generate(request)
868
+ .await
869
+ .expect_err("rank 0 should not select globally ranked engine 3");
870
+
871
+ assert_eq!(status.code(), tonic::Code::InvalidArgument);
872
+ assert!(status.message().contains("connected ranks: [3]"));
873
+
874
+ server_task.abort();
875
+}
876
+
877
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
878
#[serial]
879
async fn unary_generate_min_tokens_above_max_tokens_returns_invalid_argument() {
@@ -1523,7 +1554,9 @@ async fn control_aggregates_multi_engine_capacity() {
1554
ready_1.data_parallel_rank = 1;
1555
1556
let engine_tasks = [ready_0, ready_1].map(|ready| {
1526
- let engine_id = EngineId::from_engine_index(ready.data_parallel_rank);
1557
+ let engine_id = EngineId::from_engine_index(
1558
+ ready.data_parallel_rank.try_into().expect("test rank fits engine identity"),
1559
+ );
1560
MockEngineTask::new(spawn_mock_engine_task_with_ready(
1561
handshake_address.clone(),
1562
engine_id,
@@ -1551,10 +1584,8 @@ async fn control_aggregates_multi_engine_capacity() {
1584
Llm::new(client),
1585
Arc::new(FakeTextBackend) as Arc<dyn ChatTextBackend>,
1586
);
1554
- let service = ControlServiceImpl::new(Arc::new(AppState::new(
1555
- vec!["test-model".to_string()],
1556
- chat,
1557
- )));
1587
+ let state = AppState::new(vec!["test-model".to_string()], chat).with_data_parallel_size(4);
1588
+ let service = ControlServiceImpl::new(Arc::new(state));
1589
1590
let server = pb::control_server::Control::get_server_info(
1591
&service,
@@ -1565,6 +1596,7 @@ async fn control_aggregates_multi_engine_capacity() {
1596
.into_inner();
1597
assert_eq!(server.max_model_len, 4_096);
1598
assert_eq!(server.total_kv_blocks, 30);
1599
+ assert_eq!(server.parallelism.unwrap().data_parallel_size, 4);
1600
1601
drop(engine_tasks);
1602
}
rust/src/server/src/lib.rs
+1
@@ -141,6 +141,7 @@ async fn build_state(config: &Config) -> Result<Arc<AppState>> {
141
.with_model_path(config.model.clone())
142
.with_api_server_options(config.api_server_options)
143
.with_server_info(ServerInfoSnapshot::from_config(config))
144
+ .with_data_parallel_size(config.data_parallel_size)
145
.with_api_keys(config.api_keys.clone())
146
.with_cors(config.cors.clone())
147
.with_profiler(config.profiler.clone()),
rust/src/server/src/routes/tests.rs
+9
-9
@@ -725,11 +725,11 @@ async fn test_app_with_dev_mode(dev_mode_enabled: bool) -> axum::Router {
725
/// stays alive for the duration of the test.
726
async fn test_dev_mode_app_with_ready(
727
ready_response: vllm_engine_core_client::protocol::handshake::EngineCoreReadyResponse,
728
+ data_parallel_size: usize,
729
) -> (axum::Router, MockEngineTask) {
730
let ipc = IpcNamespace::new().expect("create ipc namespace");
731
let handshake_address = ipc.handshake_endpoint();
732
let engine_id = b"engine-world-size".to_vec();
732
-
733
let engine_task = MockEngineTask::new(spawn_mock_engine_task_with_ready(
734
handshake_address.clone(),
735
engine_id.clone(),
@@ -750,10 +750,10 @@ async fn test_dev_mode_app_with_ready(
750
751
let chat = ChatLlm::from_shared_backend(test_llm(client), Arc::new(FakeChatBackend::new()));
752
let app = build_router_with_dev_mode(
753
- Arc::new(AppState::new(
754
- vec!["Qwen/Qwen1.5-0.5B-Chat".to_string()],
755
- chat,
756
- )),
753
+ Arc::new(
754
+ AppState::new(vec!["Qwen/Qwen1.5-0.5B-Chat".to_string()], chat)
755
+ .with_data_parallel_size(data_parallel_size),
756
+ ),
757
true,
758
);
759
(app, engine_task)
@@ -6313,7 +6313,7 @@ async fn tokenize_allows_prompts_at_or_above_max_model_len() {
6313
max_model_len: 4,
6314
..default_ready_response()
6315
};
6316
- let (mut app, _engine_task) = test_dev_mode_app_with_ready(ready).await;
6316
+ let (mut app, _engine_task) = test_dev_mode_app_with_ready(ready, 1).await;
6317
6318
let (completion_status, completion_json) = post_json(
6319
&mut app,
@@ -6569,10 +6569,10 @@ async fn world_size_endpoint_is_dev_mode_only() {
6569
async fn world_size_includes_data_parallelism_by_default() {
6570
let ready = vllm_engine_core_client::protocol::handshake::EngineCoreReadyResponse {
6571
world_size: 2,
6572
- data_parallel_size: 4,
6572
+ data_parallel_size: 1,
6573
..default_ready_response()
6574
};
6575
- let (mut app, _engine_task) = test_dev_mode_app_with_ready(ready).await;
6575
+ let (mut app, _engine_task) = test_dev_mode_app_with_ready(ready, 4).await;
6576
6577
let response = app
6578
.call(
@@ -6598,7 +6598,7 @@ async fn world_size_excludes_data_parallelism_when_include_dp_false() {
6598
data_parallel_size: 4,
6599
..default_ready_response()
6600
};
6601
- let (mut app, _engine_task) = test_dev_mode_app_with_ready(ready).await;
6601
+ let (mut app, _engine_task) = test_dev_mode_app_with_ready(ready, 4).await;
6602
6603
let response = app
6604
.call(
rust/src/server/src/routes/world_size.rs
+1
-1
@@ -47,7 +47,7 @@ pub async fn get_world_size(
47
let ws = client.world_size();
48
49
let world_size = if params.include_dp {
50
- let dp = client.data_parallel_size();
50
+ let dp = state.data_parallel_size() as u64;
51
ws * dp
52
} else {
53
ws
rust/src/server/src/state.rs
+15
@@ -40,6 +40,8 @@ pub struct AppState {
40
pub cors: CorsConfig,
41
/// Runtime server information returned by `/server_info`, when available.
42
server_info: Option<ServerInfoSnapshot>,
43
+ /// Deployment-wide data-parallel size retained by the frontend.
44
+ data_parallel_size: usize,
45
/// SHA-256 hashes of API keys accepted as bearer tokens for guarded routes.
46
api_key_hashes: Vec<ApiKeyHash>,
47
/// Number of in-flight inference requests currently owned by this frontend.
@@ -69,12 +71,14 @@ impl AppState {
71
!served_model_names.is_empty(),
72
"served_model_names must not be empty"
73
);
74
+ let data_parallel_size = chat.engine_core_client().engine_count();
75
Self {
76
served_model_names,
77
chat,
78
api_server_options: ApiServerOptions::default(),
79
cors: CorsConfig::default(),
80
server_info: None,
81
+ data_parallel_size,
82
api_key_hashes: Vec::new(),
83
server_load: AtomicU64::new(0),
84
lora_manager: LoraManager::new(),
@@ -114,6 +118,17 @@ impl AppState {
118
self
119
}
120
121
+ /// Set the deployment-wide data-parallel size reported by frontend APIs.
122
+ pub(crate) fn with_data_parallel_size(mut self, size: usize) -> Self {
123
+ self.data_parallel_size = size;
124
+ self
125
+ }
126
+
127
+ /// Return the deployment-wide data-parallel size.
128
+ pub(crate) fn data_parallel_size(&self) -> usize {
129
+ self.data_parallel_size
130
+ }
131
+
132
/// Build a `/server_info` response payload.
133
pub(crate) fn server_info_response(
134
&self,
rust/src/text/src/error.rs
+4
-1
@@ -62,7 +62,10 @@ impl Error {
62
| Self::InvalidRepetitionDetection { .. }
63
// An empty tokenized prompt detected later, at request prepare
64
// time, surfaces through the transparent Llm wrapper.
65
- | Self::Llm(LlmError::EmptyPromptTokenIds { .. }) => true,
65
+ | Self::Llm(LlmError::EmptyPromptTokenIds { .. })
66
+ | Self::Llm(LlmError::EngineCoreClient(
67
+ EngineCoreError::InvalidDataParallelRank { .. },
68
+ )) => true,
69
_ => false,
70
}
71
}
vllm/entrypoints/cli/serve.py
+1
@@ -352,6 +352,7 @@ def run_multi_api_server(args: argparse.Namespace):
352
output_address=addresses.outputs[0],
353
engine_start_index=expected_engine_start_index,
354
engine_count=expected_engine_count,
355
+ data_parallel_size=parallel_config.data_parallel_size,
356
stats_update_address=stats_update_address,
357
)
358
else:
vllm/v1/utils.py
+3
@@ -339,6 +339,7 @@ class RustFrontendProcessManager:
339
output_address: str,
340
engine_start_index: int,
341
engine_count: int,
342
+ data_parallel_size: int,
343
stats_update_address: str | None = None,
344
):
345
import os
@@ -360,6 +361,8 @@ class RustFrontendProcessManager:
361
str(engine_start_index),
362
"--engine-count",
363
str(engine_count),
364
+ "--data-parallel-size",
365
+ str(data_parallel_size),
366
]
367
if stats_update_address is not None:
368
cmd.extend(["--coordinator-address", stats_update_address])