[Rust Frontend] Simplify data-parallel size ownership (#52575)

Signed-off-by: Bugen Zhao <i@bugenzhao.com> Co-authored-by: Nick Hill <nickhill123@gmail.com>

Bugen Zhao committed Aug 18, 2026 at 19:06 UTC 41f179b57aa8ab6f634f508128ce1f1efadd0eb1
19 files changed +188 -134
.buildkite/test-amd.yaml
+3
@@ -2417,6 +2417,7 @@ steps:
2417 - vllm/v1/engine/
2418 - vllm/v1/worker/
2419 - tests/utils.py
2420 + - tests/v1/distributed/test_dense_dp_world_size.py
2421 - tests/v1/distributed/test_external_lb_dp.py
2422 - tests/v1/distributed/test_hybrid_lb_dp.py
2423 - tests/v1/distributed/test_internal_lb_dp.py
@@ -2424,6 +2425,7 @@ steps:
2425 commands:
2426 - export VLLM_USE_RUST_FRONTEND=1
2427 - export VLLM_WORKER_MULTIPROC_METHOD=spawn
2428 + - TP_SIZE=1 DP_SIZE=4 pytest -v -s v1/distributed/test_dense_dp_world_size.py
2429 - TP_SIZE=1 DP_SIZE=4 pytest -v -s v1/distributed/test_internal_lb_dp.py -k "not 4 and not server_info"
2430 - TP_SIZE=1 DP_SIZE=2 pytest -v -s v1/distributed/test_external_lb_dp.py -k "not 4 and not server_info"
2431 - TP_SIZE=1 DP_SIZE=4 pytest -v -s v1/distributed/test_hybrid_lb_dp.py -k "not 4 and not server_info"
@@ -2882,6 +2884,7 @@ steps:
2884 - TP_SIZE=2 DP_SIZE=2 pytest -v -s v1/distributed/test_async_llm_dp.py
2885 - TP_SIZE=2 DP_SIZE=2 pytest -v -s v1/distributed/test_eagle_dp.py
2886 - TP_SIZE=2 DP_SIZE=2 pytest -v -s v1/distributed/test_external_lb_dp.py
2887 + - TP_SIZE=1 DP_SIZE=4 pytest -v -s v1/distributed/test_dense_dp_world_size.py
2888 - TP_SIZE=1 DP_SIZE=4 pytest -v -s v1/distributed/test_internal_lb_dp.py
2889 - TP_SIZE=1 DP_SIZE=4 pytest -v -s v1/distributed/test_hybrid_lb_dp.py
2890 - pytest -v -s v1/engine/test_engine_core_client.py::test_kv_cache_events_dp
.buildkite/test_areas/distributed.yaml
+1
@@ -154,6 +154,7 @@ steps:
154 - TP_SIZE=2 DP_SIZE=2 pytest -v -s v1/distributed/test_async_llm_dp.py
155 - TP_SIZE=2 DP_SIZE=2 pytest -v -s v1/distributed/test_eagle_dp.py
156 - TP_SIZE=2 DP_SIZE=2 pytest -v -s v1/distributed/test_external_lb_dp.py
157 + - TP_SIZE=1 DP_SIZE=4 pytest -v -s v1/distributed/test_dense_dp_world_size.py
158 - TP_SIZE=1 DP_SIZE=4 pytest -v -s v1/distributed/test_internal_lb_dp.py
159 - TP_SIZE=1 DP_SIZE=4 pytest -v -s v1/distributed/test_hybrid_lb_dp.py
160 - pytest -v -s v1/engine/test_engine_core_client.py::test_kv_cache_events_dp
.buildkite/test_areas/rust_frontend.yaml
+2
@@ -108,6 +108,7 @@ steps:
108 - vllm/v1/engine/
109 - vllm/v1/worker/
110 - tests/utils.py
111 + - tests/v1/distributed/test_dense_dp_world_size.py
112 - tests/v1/distributed/test_external_lb_dp.py
113 - tests/v1/distributed/test_hybrid_lb_dp.py
114 - tests/v1/distributed/test_internal_lb_dp.py
@@ -115,6 +116,7 @@ steps:
116 - export VLLM_USE_RUST_FRONTEND=1
117 - export VLLM_WORKER_MULTIPROC_METHOD=spawn
118 - export NCCL_CUMEM_HOST_ENABLE=0
119 + - TP_SIZE=1 DP_SIZE=4 pytest -v -s v1/distributed/test_dense_dp_world_size.py
120 - TP_SIZE=1 DP_SIZE=4 pytest -v -s v1/distributed/test_internal_lb_dp.py -k "not 4 and not server_info"
121 - TP_SIZE=1 DP_SIZE=2 pytest -v -s v1/distributed/test_external_lb_dp.py -k "not 4 and not server_info"
122 - TP_SIZE=1 DP_SIZE=4 pytest -v -s v1/distributed/test_hybrid_lb_dp.py -k "not 4 and not server_info"
rust/src/cmd/src/cli.rs
+1 -2
@@ -474,9 +474,9 @@ impl SharedRuntimeArgs {
474 output_address,
475 engine_start_index,
476 engine_count,
477 + data_parallel_size,
478 ready_timeout,
479 },
479 - data_parallel_size,
480 coordinator_mode: match coordinator_address {
481 Some(address) => CoordinatorMode::External { address },
482 None => CoordinatorMode::None,
@@ -533,7 +533,6 @@ impl SharedRuntimeArgs {
533 local_input_address,
534 local_output_address,
535 },
536 - data_parallel_size: engine_count,
536 coordinator_mode: CoordinatorMode::MaybeInProc,
537 model: self.model,
538 served_model_name: self.served_model_name,
rust/src/cmd/src/cli/tests.rs
+2 -4
@@ -864,7 +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);
867 + assert_eq!(args.into_config().transport_mode.data_parallel_size(), 4);
868 }
869
870 #[test]
@@ -1521,7 +1521,6 @@ fn serve_frontend_config_uses_dp_address_as_advertised_host() {
1521 "<ipc output>",
1522 ),
1523 },
1524 - data_parallel_size: 4,
1524 coordinator_mode: MaybeInProc,
1525 model: "Qwen/Qwen3-0.6B",
1526 served_model_name: [],
@@ -1607,7 +1606,6 @@ fn serve_frontend_config_keeps_tcp_transport_for_non_local_only_topology() {
1606 local_input_address: None,
1607 local_output_address: None,
1608 },
1610 - data_parallel_size: 4,
1609 coordinator_mode: MaybeInProc,
1610 model: "Qwen/Qwen3-0.6B",
1611 served_model_name: [],
@@ -1710,9 +1708,9 @@ fn frontend_config_uses_external_coordinator_when_coordinator_address_is_present
1708 output_address: "ipc:///tmp/output.sock",
1709 engine_start_index: 3,
1710 engine_count: 1,
1711 + data_parallel_size: 4,
1712 ready_timeout: 600s,
1713 },
1715 - data_parallel_size: 4,
1714 coordinator_mode: External {
1715 address: "tcp://127.0.0.1:7000",
1716 },
rust/src/engine-core-client/src/client.rs
+73 -1
@@ -15,7 +15,7 @@ use tracing::{debug, info, trace};
15
16 use crate::client::imp::{ClientInner, run_abort_loop, run_output_dispatcher_loop};
17 use crate::coordinator::CoordinatorHandle;
18 -use crate::error::{Error, Result};
18 +use crate::error::{Error, Result, bail_invalid_client_config};
19 use crate::protocol::dtype::ModelDtype;
20 use crate::protocol::handshake::EngineCoreReadyResponse;
21 use crate::protocol::lora::LoraRequest;
@@ -68,11 +68,70 @@ pub enum TransportMode {
68 engine_start_index: u32,
69 /// Total number of engines expected to register on this transport.
70 engine_count: usize,
71 + /// Deployment-wide data-parallel size. This may be larger than
72 + /// `engine_count` when a supervisor partitions ranks across frontends.
73 + data_parallel_size: usize,
74 /// Maximum time to wait for all expected engines to register.
75 ready_timeout: Duration,
76 },
77 }
78
79 +impl TransportMode {
80 + /// Return the deployment-wide data-parallel size for this transport.
81 + pub fn data_parallel_size(&self) -> usize {
82 + match self {
83 + Self::HandshakeOwner { engine_count, .. } => *engine_count,
84 + Self::Bootstrapped {
85 + data_parallel_size, ..
86 + } => *data_parallel_size,
87 + }
88 + }
89 +
90 + /// Validate the transport topology before opening any sockets.
91 + pub fn validate(&self) -> Result<()> {
92 + let data_parallel_size = self.data_parallel_size();
93 + if data_parallel_size == 0 {
94 + bail_invalid_client_config!("data parallel size must be at least 1");
95 + }
96 + if data_parallel_size > usize::from(u16::MAX) + 1 {
97 + bail_invalid_client_config!(
98 + "data parallel size ({data_parallel_size}) exceeds the two-byte engine identity limit"
99 + );
100 + }
101 +
102 + match self {
103 + Self::HandshakeOwner { .. } => {}
104 + Self::Bootstrapped {
105 + engine_start_index,
106 + engine_count,
107 + ..
108 + } => {
109 + if *engine_count == 0 {
110 + bail_invalid_client_config!("engine count must be at least 1");
111 + }
112 + let engine_start_index = usize::try_from(*engine_start_index).map_err(|_| {
113 + Error::InvalidClientConfig {
114 + message: "engine start index does not fit usize".to_string(),
115 + }
116 + })?;
117 + let engine_end_index =
118 + engine_start_index.checked_add(*engine_count).ok_or_else(|| {
119 + Error::InvalidClientConfig {
120 + message: "engine start index + engine count overflows".to_string(),
121 + }
122 + })?;
123 + if engine_end_index > data_parallel_size {
124 + bail_invalid_client_config!(
125 + "connected engine range [{engine_start_index}, {engine_end_index}) exceeds data parallel size ({data_parallel_size})"
126 + );
127 + }
128 + }
129 + }
130 +
131 + Ok(())
132 + }
133 +}
134 +
135 /// Which coordinator implementation should be active when one is present for a
136 /// frontend client.
137 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -117,6 +176,11 @@ impl EngineCoreClientConfig {
176 }
177 }
178
179 + /// Validate the client topology before opening any transport sockets.
180 + pub fn validate(&self) -> Result<()> {
181 + self.transport_mode.validate()
182 + }
183 +
184 /// Set the model name used by frontend-side metrics and diagnostics.
185 pub fn with_model_name(mut self, model_name: impl Into<String>) -> Self {
186 self.model_name = model_name.into();
@@ -226,6 +290,7 @@ impl EngineCoreClient {
290 /// handshake. In bootstrapped mode it binds the provided frontend
291 /// sockets and waits for the expected engine registration frames.
292 pub async fn connect(config: EngineCoreClientConfig) -> Result<Self> {
293 + config.validate()?;
294 let connected = match &config.transport_mode {
295 TransportMode::HandshakeOwner {
296 handshake_address,
@@ -261,6 +326,7 @@ impl EngineCoreClient {
326 engine_start_index,
327 engine_count,
328 ready_timeout,
329 + ..
330 } => {
331 if let Some(CoordinatorMode::InProc) = config.coordinator_mode {
332 panic!("cannot use in-process coordinator with bootstrapped transport mode")
@@ -378,6 +444,12 @@ impl EngineCoreClient {
444 self.engines.len()
445 }
446
447 + /// Return the deployment-wide data-parallel size configured for this
448 + /// client.
449 + pub fn data_parallel_size(&self) -> usize {
450 + self.config.transport_mode.data_parallel_size()
451 + }
452 +
453 /// Return the engine-side indices connected to this client.
454 pub fn engine_indices(&self) -> Vec<u32> {
455 self.engines
rust/src/engine-core-client/src/error.rs
+2
@@ -49,6 +49,8 @@ pub enum Error {
49 UnexpectedHandshakeIdentity { expected: Vec<u8>, actual: Vec<u8> },
50 #[error("unexpected startup handshake message: {message}")]
51 UnexpectedHandshakeMessage { message: String },
52 + #[error("invalid engine-core client configuration: {message}")]
53 + InvalidClientConfig { message: String },
54 #[error("unexpected non-control output on coordinator path: {message}")]
55 UnexpectedCoordinatorOutput { message: String },
56 #[error("unexpected output on main dispatcher path: {message}")]
rust/src/engine-core-client/src/mock_engine.rs
+1 -1
@@ -56,7 +56,7 @@ pub fn default_ready_response() -> EngineCoreReadyResponse {
56 dtype: ModelDtype::Float32,
57 vllm_version: "test-vllm-version".to_string(),
58 world_size: 1,
59 - data_parallel_size: 1,
59 + effective_data_parallel_size: 1,
60 tensor_parallel_size: 1,
61 pipeline_parallel_size: 1,
62 decode_context_parallel_size: 1,
rust/src/engine-core-client/src/protocol/handshake.rs
+5 -2
@@ -63,8 +63,11 @@ pub struct EngineCoreReadyResponse {
63 pub vllm_version: String,
64 /// World size (TP * PP) from the parallel config.
65 pub world_size: u64,
66 - /// Data parallelism size from the parallel config.
67 - pub data_parallel_size: u64,
66 + /// Data-parallel size from this EngineCore's effective parallel config.
67 + /// Dense independent-DP ranks are reconfigured to report `1`; the client
68 + /// transport owns the deployment-wide data-parallel size.
69 + #[serde(rename = "data_parallel_size")]
70 + pub effective_data_parallel_size: u64,
71 // Required discovery metadata; EngineCore and client versions must match.
72 /// Tensor-parallel size of this engine.
73 pub tensor_parallel_size: u32,
rust/src/engine-core-client/src/tests/client.rs
+33
@@ -303,6 +303,7 @@ fn bootstrapped_test_config(
303 output_address,
304 engine_start_index: 0,
305 engine_count,
306 + data_parallel_size: engine_count,
307 ready_timeout,
308 },
309 coordinator_mode,
@@ -330,15 +331,47 @@ fn bootstrapped_test_config_with_start_index(
331 );
332 let TransportMode::Bootstrapped {
333 engine_start_index: start,
334 + data_parallel_size,
335 ..
336 } = &mut config.transport_mode
337 else {
338 unreachable!("bootstrapped_test_config returns bootstrapped transport")
339 };
340 *start = engine_start_index;
341 + *data_parallel_size = usize::try_from(engine_start_index)
342 + .expect("test start index fits usize")
343 + .checked_add(engine_count)
344 + .expect("test engine range fits usize");
345 config
346 }
347
348 +#[test]
349 +fn client_config_validates_bootstrapped_dp_range() {
350 + let mut config = bootstrapped_test_config_with_start_index(
351 + "ipc://unused-input".to_string(),
352 + "ipc://unused-output".to_string(),
353 + 1,
354 + 2,
355 + Duration::from_secs(1),
356 + 0,
357 + None,
358 + );
359 + config.validate().expect("frontend may own a subset of global DP ranks");
360 +
361 + let TransportMode::Bootstrapped {
362 + data_parallel_size, ..
363 + } = &mut config.transport_mode
364 + else {
365 + unreachable!("expected bootstrapped transport")
366 + };
367 + *data_parallel_size = 2;
368 + let error = config.validate().expect_err("engine range above DP size must fail");
369 + expect_test::expect![[
370 + "invalid engine-core client configuration: connected engine range [1, 3) exceeds data parallel size (2)"
371 + ]]
372 + .assert_eq(&error.to_string());
373 +}
374 +
375 async fn recv_xpub_message(xpub: &mut XPubSocket) -> Vec<bytes::Bytes> {
376 xpub.recv().await.unwrap().into_vec()
377 }
rust/src/server/examples/external_engine_openai_qwen.rs
-1
@@ -58,7 +58,6 @@ async fn main() -> Result<()> {
58 local_input_address: None,
59 local_output_address: None,
60 },
61 - data_parallel_size: args.engine_count,
61 coordinator_mode: CoordinatorMode::MaybeInProc,
62 model: args.model,
63 served_model_name: vec![],
rust/src/server/src/config.rs
+1 -42
@@ -161,8 +161,6 @@ 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,
164 /// Requested frontend-side coordinator behavior.
165 pub coordinator_mode: CoordinatorMode,
166 /// Backend model identifier used for engine-core loading.
@@ -239,46 +237,7 @@ impl Config {
237 max_logprobs
238 );
239 }
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 - }
240 + self.transport_mode.validate()?;
241
242 Ok(())
243 }
rust/src/server/src/grpc/control.rs
+1 -1
@@ -43,7 +43,7 @@ impl ControlServiceImpl {
43 pb::ParallelismInfo {
44 tensor_parallel_size: ready.tensor_parallel_size,
45 pipeline_parallel_size: ready.pipeline_parallel_size,
46 - data_parallel_size: self.state.data_parallel_size().min(u32::MAX as usize) as u32,
46 + data_parallel_size: self.client().data_parallel_size().min(u32::MAX as usize) as u32,
47 data_parallel_rank: ready.data_parallel_rank,
48 decode_context_parallel_size: ready.decode_context_parallel_size,
49 }
rust/src/server/src/grpc/tests.rs
+4 -4
@@ -1625,7 +1625,7 @@ async fn control_aggregates_multi_engine_capacity() {
1625 let mut ready_0 = default_ready_response();
1626 ready_0.max_model_len = 8_192;
1627 ready_0.num_gpu_blocks = 10;
1628 - ready_0.data_parallel_size = 2;
1628 + ready_0.effective_data_parallel_size = 2;
1629 ready_0.weight_transfer_backend = Some("nccl".to_string());
1630 ready_0.enable_sleep_mode = true;
1631 ready_0.supports_draft_weight_updates = true;
@@ -1633,7 +1633,7 @@ async fn control_aggregates_multi_engine_capacity() {
1633 let mut ready_1 = default_ready_response();
1634 ready_1.max_model_len = 4_096;
1635 ready_1.num_gpu_blocks = 20;
1636 - ready_1.data_parallel_size = 2;
1636 + ready_1.effective_data_parallel_size = 2;
1637 ready_1.data_parallel_rank = 1;
1638
1639 let engine_tasks = [ready_0, ready_1].map(|ready| {
@@ -1667,7 +1667,7 @@ async fn control_aggregates_multi_engine_capacity() {
1667 Llm::new(client),
1668 Arc::new(FakeTextBackend) as Arc<dyn ChatTextBackend>,
1669 );
1670 - let state = AppState::new(vec!["test-model".to_string()], chat).with_data_parallel_size(4);
1670 + let state = AppState::new(vec!["test-model".to_string()], chat);
1671 let service = ControlServiceImpl::new(Arc::new(state));
1672
1673 let server = pb::control_server::Control::get_server_info(
@@ -1684,7 +1684,7 @@ async fn control_aggregates_multi_engine_capacity() {
1684 assert!(rl.weight_transfer_backend.is_empty());
1685 assert!(!rl.sleep_mode_enabled);
1686 assert!(!rl.draft_weight_updates_enabled);
1687 - assert_eq!(server.parallelism.unwrap().data_parallel_size, 4);
1687 + assert_eq!(server.parallelism.unwrap().data_parallel_size, 2);
1688
1689 drop(engine_tasks);
1690 }
rust/src/server/src/lib.rs
-1
@@ -141,7 +141,6 @@ 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)
144 .with_api_keys(config.api_keys.clone())
145 .with_cors(config.cors.clone())
146 .with_profiler(config.profiler.clone()),
rust/src/server/src/routes/tests.rs
+6 -59
@@ -704,14 +704,13 @@ async fn test_app_with_dev_mode(dev_mode_enabled: bool) -> axum::Router {
704 /// stays alive for the duration of the test.
705 async fn test_dev_mode_app_with_ready(
706 ready_response: vllm_engine_core_client::protocol::handshake::EngineCoreReadyResponse,
707 - data_parallel_size: usize,
707 ) -> (axum::Router, MockEngineTask) {
708 let ipc = IpcNamespace::new().expect("create ipc namespace");
709 let handshake_address = ipc.handshake_endpoint();
710 let engine_id = b"engine-world-size".to_vec();
711 let engine_task = MockEngineTask::new(spawn_mock_engine_task_with_ready(
712 handshake_address.clone(),
714 - engine_id.clone(),
713 + engine_id,
714 ready_response,
715 |_dealer, _push| boxed_test_future(async {}),
716 ));
@@ -729,10 +728,10 @@ async fn test_dev_mode_app_with_ready(
728
729 let chat = ChatLlm::from_shared_backend(test_llm(client), Arc::new(FakeChatBackend::new()));
730 let app = build_router_with_dev_mode(
732 - Arc::new(
733 - AppState::new(vec!["Qwen/Qwen1.5-0.5B-Chat".to_string()], chat)
734 - .with_data_parallel_size(data_parallel_size),
735 - ),
731 + Arc::new(AppState::new(
732 + vec!["Qwen/Qwen1.5-0.5B-Chat".to_string()],
733 + chat,
734 + )),
735 true,
736 );
737 (app, engine_task)
@@ -6292,7 +6291,7 @@ async fn tokenize_allows_prompts_at_or_above_max_model_len() {
6291 max_model_len: 4,
6292 ..default_ready_response()
6293 };
6295 - let (mut app, _engine_task) = test_dev_mode_app_with_ready(ready, 1).await;
6294 + let (mut app, _engine_task) = test_dev_mode_app_with_ready(ready).await;
6295
6296 let (completion_status, completion_json) = post_json(
6297 &mut app,
@@ -6543,58 +6542,6 @@ async fn world_size_endpoint_is_dev_mode_only() {
6542 assert_eq!(response.status(), StatusCode::NOT_FOUND);
6543 }
6544
6546 -#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6547 -#[serial]
6548 -async fn world_size_includes_data_parallelism_by_default() {
6549 - let ready = vllm_engine_core_client::protocol::handshake::EngineCoreReadyResponse {
6550 - world_size: 2,
6551 - data_parallel_size: 1,
6552 - ..default_ready_response()
6553 - };
6554 - let (mut app, _engine_task) = test_dev_mode_app_with_ready(ready, 4).await;
6555 -
6556 - let response = app
6557 - .call(
6558 - Request::builder()
6559 - .uri("/get_world_size")
6560 - .body(Body::empty())
6561 - .expect("build request"),
6562 - )
6563 - .await
6564 - .expect("call app");
6565 -
6566 - assert_eq!(response.status(), StatusCode::OK);
6567 - let body = to_bytes(response.into_body(), usize::MAX).await.expect("read body");
6568 - let json: serde_json::Value = serde_json::from_slice(&body).expect("decode json");
6569 - assert_eq!(json, json!({"world_size": 8}));
6570 -}
6571 -
6572 -#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6573 -#[serial]
6574 -async fn world_size_excludes_data_parallelism_when_include_dp_false() {
6575 - let ready = vllm_engine_core_client::protocol::handshake::EngineCoreReadyResponse {
6576 - world_size: 2,
6577 - data_parallel_size: 4,
6578 - ..default_ready_response()
6579 - };
6580 - let (mut app, _engine_task) = test_dev_mode_app_with_ready(ready, 4).await;
6581 -
6582 - let response = app
6583 - .call(
6584 - Request::builder()
6585 - .uri("/get_world_size?include_dp=false")
6586 - .body(Body::empty())
6587 - .expect("build request"),
6588 - )
6589 - .await
6590 - .expect("call app");
6591 -
6592 - assert_eq!(response.status(), StatusCode::OK);
6593 - let body = to_bytes(response.into_body(), usize::MAX).await.expect("read body");
6594 - let json: serde_json::Value = serde_json::from_slice(&body).expect("decode json");
6595 - assert_eq!(json, json!({"world_size": 2}));
6596 -}
6597 -
6545 // ========================= Profiler route tests =========================
6546
6547 async fn test_profiling_app_with_engine_script<F>(script: F) -> (axum::Router, MockEngineTask)
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 = state.data_parallel_size() as u64;
50 + let dp = client.data_parallel_size() as u64;
51 ws * dp
52 } else {
53 ws
rust/src/server/src/state.rs
-15
@@ -40,8 +40,6 @@ 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,
43 /// SHA-256 hashes of API keys accepted as bearer tokens for guarded routes.
44 api_key_hashes: Vec<ApiKeyHash>,
45 /// Number of in-flight inference requests currently owned by this frontend.
@@ -71,14 +69,12 @@ impl AppState {
69 !served_model_names.is_empty(),
70 "served_model_names must not be empty"
71 );
74 - let data_parallel_size = chat.engine_core_client().engine_count();
72 Self {
73 served_model_names,
74 chat,
75 api_server_options: ApiServerOptions::default(),
76 cors: CorsConfig::default(),
77 server_info: None,
81 - data_parallel_size,
78 api_key_hashes: Vec::new(),
79 server_load: AtomicU64::new(0),
80 lora_manager: LoraManager::new(),
@@ -118,17 +114,6 @@ impl AppState {
114 self
115 }
116
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 -
117 /// Build a `/server_info` response payload.
118 pub(crate) fn server_info_response(
119 &self,
tests/v1/distributed/test_dense_dp_world_size.py new
+52
@@ -0,0 +1,52 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +import os
4 +
5 +import requests
6 +
7 +from tests.utils import RemoteOpenAIServer
8 +from vllm.platforms import current_platform
9 +
10 +MODEL_NAME = "Qwen/Qwen3-0.6B"
11 +DP_SIZE = int(os.getenv("DP_SIZE", "2"))
12 +TP_SIZE = int(os.getenv("TP_SIZE", "1"))
13 +
14 +
15 +def test_dense_dp_world_size():
16 + server_args = [
17 + "--dtype",
18 + "bfloat16",
19 + "--max-model-len",
20 + "2048",
21 + "--max-num-seqs",
22 + "128",
23 + "--enforce-eager",
24 + "--data-parallel-size",
25 + str(DP_SIZE),
26 + "--data-parallel-size-local",
27 + str(DP_SIZE),
28 + "--tensor-parallel-size",
29 + str(TP_SIZE),
30 + ]
31 + env_dict = {
32 + "VLLM_SERVER_DEV_MODE": "1",
33 + current_platform.device_control_env_var: ",".join(
34 + str(current_platform.device_id_to_physical_device_id(i))
35 + for i in range(DP_SIZE * TP_SIZE)
36 + ),
37 + }
38 +
39 + with RemoteOpenAIServer(
40 + MODEL_NAME,
41 + server_args,
42 + env_dict=env_dict,
43 + ) as server:
44 + response = requests.get(server.url_for("get_world_size"))
45 + response.raise_for_status()
46 + assert response.json() == {"world_size": TP_SIZE * DP_SIZE}
47 +
48 + response = requests.get(
49 + server.url_for("get_world_size"), params={"include_dp": "false"}
50 + )
51 + response.raise_for_status()
52 + assert response.json() == {"world_size": TP_SIZE}