[Bugfix][Rust Frontend] Skip engine-derived metrics under `--disable-log-stats` (#59205)
Signed-off-by: Bugen Zhao <i@bugenzhao.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Bugen Zhao committed
Sep 30, 2026 at 02:01 UTC
afac509a333db6d4ffa1f35b13c27c9e0c1f33aa
16 files changed
+248
-34
rust/src/bench/src/mm_processor.rs
+1
@@ -230,6 +230,7 @@ async fn run_mm_processor_with_engine(
230
coordinator_mode: None,
231
model_name: args.model.clone(),
232
client_index: 0,
233
+ engine_stats_enabled: true,
234
})
235
.await
236
.context("failed to connect to engine core")?;
rust/src/chat/examples/external_engine_chat_qwen.rs
+1
@@ -70,6 +70,7 @@ async fn main() -> Result<()> {
70
coordinator_mode: None,
71
model_name: args.model.clone(),
72
client_index: CLIENT_INDEX,
73
+ engine_stats_enabled: true,
74
})
75
.await
76
.context("failed to connect to external vLLM engine")?;
rust/src/cmd/src/cli.rs
+3
-1
@@ -380,7 +380,9 @@ pub struct SharedRuntimeArgs {
380
pub api_key: Vec<String>,
381
382
/// Disable periodic logging of engine statistics (throughput, queue depth,
383
- /// cache usage).
383
+ /// cache usage). Engines also stop recording stats, so metrics derived from
384
+ /// engine-reported scheduler stats and request lifecycle events are not
385
+ /// exported.
386
#[arg(long)]
387
#[serde(default)]
388
pub disable_log_stats: bool,
rust/src/engine-core-client/examples/external_engine_logprobs.rs
+1
@@ -118,6 +118,7 @@ async fn main() -> Result<()> {
118
coordinator_mode: None,
119
model_name: args.model.clone(),
120
client_index: args.client_index,
121
+ engine_stats_enabled: true,
122
})
123
.await
124
.context("failed to connect to external vLLM engine")?;
rust/src/engine-core-client/examples/external_engine_utility_call.rs
+1
@@ -68,6 +68,7 @@ async fn main() -> Result<()> {
68
coordinator_mode: None,
69
model_name: args.model.clone(),
70
client_index: args.client_index,
71
+ engine_stats_enabled: true,
72
})
73
.await
74
.context("failed to connect to external vLLM engine")?;
rust/src/engine-core-client/src/client.rs
+18
@@ -155,6 +155,11 @@ pub struct EngineCoreClientConfig {
155
pub model_name: String,
156
/// Frontend client index stamped onto every request.
157
pub client_index: u32,
158
+ /// Whether the connected engines record stats, i.e. were not started with
159
+ /// `--disable-log-stats`. When `false`, engines emit neither
160
+ /// `SchedulerStats` nor request lifecycle events, so frontend metrics
161
+ /// derived from them are not exported.
162
+ pub engine_stats_enabled: bool,
163
}
164
165
impl EngineCoreClientConfig {
@@ -173,6 +178,7 @@ impl EngineCoreClientConfig {
178
coordinator_mode: None,
179
model_name: String::new(),
180
client_index: 0,
181
+ engine_stats_enabled: true,
182
}
183
}
184
@@ -193,6 +199,12 @@ impl EngineCoreClientConfig {
199
self
200
}
201
202
+ /// Set whether the connected engines record stats.
203
+ pub fn with_engine_stats_enabled(mut self, engine_stats_enabled: bool) -> Self {
204
+ self.engine_stats_enabled = engine_stats_enabled;
205
+ self
206
+ }
207
+
208
/// Override the optional coordinator mode for this client config.
209
pub fn with_coordinator_mode(mut self, coordinator_mode: Option<CoordinatorMode>) -> Self {
210
self.coordinator_mode = coordinator_mode;
@@ -361,6 +373,7 @@ impl EngineCoreClient {
373
connected.input_send,
374
runtime.handle().clone(),
375
config.model_name.clone(),
376
+ config.engine_stats_enabled,
377
&engines,
378
));
379
let output_task = AbortOnDropHandle::new(runtime.spawn(transport::run_output_loop(
@@ -546,6 +559,11 @@ impl EngineCoreClient {
559
self.inner.model_name()
560
}
561
562
+ /// Return whether the connected engines record stats.
563
+ pub fn engine_stats_enabled(&self) -> bool {
564
+ self.config.engine_stats_enabled
565
+ }
566
+
567
/// Return whether the client still considers the engine healthy.
568
pub fn is_healthy(&self) -> bool {
569
self.inner.is_healthy()
rust/src/engine-core-client/src/client/imp.rs
+21
-7
@@ -46,7 +46,12 @@ pub(crate) struct ClientInner {
46
/// Per-tensor byte threshold loaded from env variable
47
/// `VLLM_MSGPACK_ZERO_COPY_THRESHOLD` when this inner client is created.
48
msgpack_zero_copy_threshold: usize,
49
- scheduler_stats_recorder: SchedulerStatsRecorder,
49
+ /// Whether the connected engines record stats. See
50
+ /// [`crate::EngineCoreClientConfig::engine_stats_enabled`].
51
+ engine_stats_enabled: bool,
52
+ /// Scheduler-stats metric handles, present only when engines record stats
53
+ /// so that gauges are not exported with values no engine ever reports.
54
+ scheduler_stats_recorder: Option<SchedulerStatsRecorder>,
55
iteration_metrics: BTreeMap<u32, IterationMetricHandles>,
56
request_reg: Mutex<RequestRegistry>,
57
utility_reg: Mutex<UtilityRegistry>,
@@ -61,10 +66,11 @@ impl ClientInner {
66
input_send: RouterSendHalf,
67
handle: Handle,
68
model_name: String,
69
+ engine_stats_enabled: bool,
70
engines: &[ConnectedEngine],
71
) -> Self {
66
- let scheduler_stats_recorder =
67
- SchedulerStatsRecorder::new(&METRICS.scheduler, &model_name, engines);
72
+ let scheduler_stats_recorder = engine_stats_enabled
73
+ .then(|| SchedulerStatsRecorder::new(&METRICS.scheduler, &model_name, engines));
74
let iteration_metrics = engines
75
.iter()
76
.filter_map(|engine| {
@@ -78,6 +84,7 @@ impl ClientInner {
84
handle,
85
model_name,
86
msgpack_zero_copy_threshold: msgpack_zero_copy_threshold(),
87
+ engine_stats_enabled,
88
scheduler_stats_recorder,
89
iteration_metrics,
90
request_reg: Mutex::new(RequestRegistry::new(engines)),
@@ -412,7 +419,9 @@ pub(crate) async fn run_output_dispatcher_loop(
419
inner: Arc<ClientInner>,
420
mut output_rx: mpsc::Receiver<Result<EngineCoreOutputs>>,
421
) {
415
- let mut lora_info = LoraInfoExporter::default();
422
+ // LoRA phases come from request lifecycle events, which engines only emit
423
+ // when they record stats.
424
+ let mut lora_info = inner.engine_stats_enabled.then(LoraInfoExporter::default);
425
426
let result: Result<()> = async {
427
loop {
@@ -475,14 +484,18 @@ pub(crate) async fn run_output_dispatcher_loop(
484
"dropping scheduler stats for unknown engine"
485
);
486
}
478
- inner.scheduler_stats_recorder.record(batch.engine_index, scheduler_stats);
487
+ if let Some(recorder) = &inner.scheduler_stats_recorder {
488
+ recorder.record(batch.engine_index, scheduler_stats);
489
+ }
490
}
491
492
// The engine's scheduler stats never carry adapter names;
493
// the gauge is derived from the registry's frontend-side
494
// request tracking instead.
484
- let (running, waiting) = inner.lora_adapter_states();
485
- lora_info.update(&METRICS.scheduler, running, waiting);
495
+ if let Some(lora_info) = &mut lora_info {
496
+ let (running, waiting) = inner.lora_adapter_states();
497
+ lora_info.update(&METRICS.scheduler, running, waiting);
498
+ }
499
}
500
EngineCoreOutputs::Utility(utility) => {
501
let call_id = utility.output.call_id;
@@ -530,6 +543,7 @@ mod tests {
543
send,
544
Handle::current(),
545
"test-model".to_string(),
546
+ true,
547
&[ConnectedEngine {
548
engine_id: EngineId::from(b"engine-0"),
549
ready_response: default_ready_response(),
rust/src/engine-core-client/src/tests/client.rs
+2
@@ -299,6 +299,7 @@ fn handshake_test_config(
299
coordinator_mode,
300
model_name: model_name.to_string(),
301
client_index,
302
+ engine_stats_enabled: true,
303
}
304
}
305
@@ -322,6 +323,7 @@ fn bootstrapped_test_config(
323
coordinator_mode,
324
model_name: "test-model".to_string(),
325
client_index,
326
+ engine_stats_enabled: true,
327
}
328
}
329
rust/src/llm/examples/external_engine_smoke.rs
+1
@@ -122,6 +122,7 @@ async fn main() -> Result<()> {
122
coordinator_mode: None,
123
model_name: args.model.clone(),
124
client_index: args.client_index,
125
+ engine_stats_enabled: true,
126
})
127
.await
128
.context("failed to connect to external vLLM engine")?;
rust/src/llm/src/lib.rs
+1
@@ -106,6 +106,7 @@ impl Llm {
106
prompt_len,
107
max_tokens_param,
108
1,
109
+ self.client.engine_stats_enabled(),
110
);
111
let guard = self.inflight.track(external_request_id, internal_request_id);
112
rust/src/llm/src/request_metrics.rs
+53
-24
@@ -55,7 +55,6 @@ struct RequestMetricHandles {
55
labels: EngineLabels,
56
57
// Request-derived counters.
58
- num_preemptions: U64Counter,
58
prompt_tokens: U64Counter,
59
prompt_tokens_local_compute: U64Counter,
60
prompt_tokens_local_cache_hit: U64Counter,
@@ -70,16 +69,27 @@ struct RequestMetricHandles {
69
request_max_num_generation_tokens: HistogramMetric,
70
request_params_max_tokens: HistogramMetric,
71
request_params_n: HistogramMetric,
73
- request_num_preemptions: HistogramMetric,
72
request_prefill_kv_computed_tokens: HistogramMetric,
73
time_to_first_token_seconds: HistogramMetric,
74
inter_token_latency_seconds: HistogramMetric,
75
e2e_request_latency_seconds: HistogramMetric,
76
+ request_decode_time_seconds: HistogramMetric,
77
+ request_time_per_output_token_seconds: HistogramMetric,
78
+
79
+ /// Present only when engines record stats.
80
+ events: Option<EventMetricHandles>,
81
+}
82
+
83
+/// Cached handles for request metrics derived from engine-core lifecycle
84
+/// events (`QUEUED`, `SCHEDULED`, `PREEMPTED`), which engines only emit when
85
+/// they record stats.
86
+#[derive(Clone)]
87
+struct EventMetricHandles {
88
+ num_preemptions: U64Counter,
89
+ request_num_preemptions: HistogramMetric,
90
request_queue_time_seconds: HistogramMetric,
91
request_prefill_time_seconds: HistogramMetric,
80
- request_decode_time_seconds: HistogramMetric,
92
request_inference_time_seconds: HistogramMetric,
82
- request_time_per_output_token_seconds: HistogramMetric,
93
}
94
95
impl RequestMetricsTracker {
@@ -92,9 +102,14 @@ impl RequestMetricsTracker {
102
prompt_len: u32,
103
max_tokens_param: Option<u32>,
104
n_param: u32,
105
+ engine_stats_enabled: bool,
106
) -> Self {
107
Self {
97
- handles: resolve_request_metric_handles(&model_name, engine_index),
108
+ handles: resolve_request_metric_handles(
109
+ &model_name,
110
+ engine_index,
111
+ engine_stats_enabled,
112
+ ),
113
arrival_time,
114
prompt_len,
115
max_tokens_param,
@@ -163,10 +178,7 @@ impl RequestMetricsTracker {
178
let prefill_kv_computed_tokens =
179
self.prompt_len.saturating_sub(self.latest_num_cached_tokens);
180
let e2e_latency_seconds = received_at - self.arrival_time;
166
- let queue_time_seconds = diff_or_zero(self.scheduled_ts, self.queued_ts);
167
- let prefill_time_seconds = diff_or_zero(self.first_token_ts, self.scheduled_ts);
181
let decode_time_seconds = diff_or_zero(self.last_token_ts, self.first_token_ts);
169
- let inference_time_seconds = diff_or_zero(self.last_token_ts, self.scheduled_ts);
182
let time_per_output_token_seconds = if self.num_generation_tokens > 1 {
183
diff_or_zero(self.last_token_ts, self.first_token_ts)
184
/ (self.num_generation_tokens - 1) as f64
@@ -187,18 +199,25 @@ impl RequestMetricsTracker {
199
self.handles.request_params_max_tokens.observe(max_tokens_param as f64);
200
}
201
self.handles.request_params_n.observe(self.n_param as f64);
190
- self.handles.request_num_preemptions.observe(self.num_preemptions as f64);
202
self.handles
203
.request_prefill_kv_computed_tokens
204
.observe(prefill_kv_computed_tokens as f64);
205
self.handles.e2e_request_latency_seconds.observe(e2e_latency_seconds);
195
- self.handles.request_queue_time_seconds.observe(queue_time_seconds);
196
- self.handles.request_prefill_time_seconds.observe(prefill_time_seconds);
206
self.handles.request_decode_time_seconds.observe(decode_time_seconds);
198
- self.handles.request_inference_time_seconds.observe(inference_time_seconds);
207
self.handles
208
.request_time_per_output_token_seconds
209
.observe(time_per_output_token_seconds);
210
+
211
+ if let Some(events) = &self.handles.events {
212
+ let queue_time_seconds = diff_or_zero(self.scheduled_ts, self.queued_ts);
213
+ let prefill_time_seconds = diff_or_zero(self.first_token_ts, self.scheduled_ts);
214
+ let inference_time_seconds = diff_or_zero(self.last_token_ts, self.scheduled_ts);
215
+
216
+ events.request_num_preemptions.observe(self.num_preemptions as f64);
217
+ events.request_queue_time_seconds.observe(queue_time_seconds);
218
+ events.request_prefill_time_seconds.observe(prefill_time_seconds);
219
+ events.request_inference_time_seconds.observe(inference_time_seconds);
220
+ }
221
}
222
223
/// Record prompt token counters through cached metric handles.
@@ -228,7 +247,9 @@ impl RequestMetricsTracker {
247
}
248
EngineCoreEventType::Preempted => {
249
self.num_preemptions += 1;
231
- self.handles.num_preemptions.inc();
250
+ if let Some(events) = &self.handles.events {
251
+ events.num_preemptions.inc();
252
+ }
253
}
254
}
255
}
@@ -248,7 +269,11 @@ impl RequestMetricsTracker {
269
}
270
271
/// Resolve fixed request metric handles for one model and engine index.
251
-fn resolve_request_metric_handles(model_name: &str, engine: u32) -> RequestMetricHandles {
272
+fn resolve_request_metric_handles(
273
+ model_name: &str,
274
+ engine: u32,
275
+ engine_stats_enabled: bool,
276
+) -> RequestMetricHandles {
277
let metrics = &METRICS.request;
278
let labels = EngineLabels {
279
model_name: model_name.to_string(),
@@ -256,7 +281,6 @@ fn resolve_request_metric_handles(model_name: &str, engine: u32) -> RequestMetri
281
};
282
283
RequestMetricHandles {
259
- num_preemptions: metrics.num_preemptions.get_or_create_owned(&labels),
284
prompt_tokens: metrics.prompt_tokens.get_or_create_owned(&labels),
285
prompt_tokens_local_compute: metrics.prompt_tokens_by_source.get_or_create_owned(
286
&prompt_token_source_labels(model_name, engine, PROMPT_TOKEN_SOURCE_LOCAL_COMPUTE),
@@ -281,7 +305,6 @@ fn resolve_request_metric_handles(model_name: &str, engine: u32) -> RequestMetri
305
.get_or_create_owned(&labels),
306
request_params_max_tokens: metrics.request_params_max_tokens.get_or_create_owned(&labels),
307
request_params_n: metrics.request_params_n.get_or_create_owned(&labels),
284
- request_num_preemptions: metrics.request_num_preemptions.get_or_create_owned(&labels),
308
request_prefill_kv_computed_tokens: metrics
309
.request_prefill_kv_computed_tokens
310
.get_or_create_owned(&labels),
@@ -294,19 +317,25 @@ fn resolve_request_metric_handles(model_name: &str, engine: u32) -> RequestMetri
317
e2e_request_latency_seconds: metrics
318
.e2e_request_latency_seconds
319
.get_or_create_owned(&labels),
297
- request_queue_time_seconds: metrics.request_queue_time_seconds.get_or_create_owned(&labels),
298
- request_prefill_time_seconds: metrics
299
- .request_prefill_time_seconds
300
- .get_or_create_owned(&labels),
320
request_decode_time_seconds: metrics
321
.request_decode_time_seconds
322
.get_or_create_owned(&labels),
304
- request_inference_time_seconds: metrics
305
- .request_inference_time_seconds
306
- .get_or_create_owned(&labels),
323
request_time_per_output_token_seconds: metrics
324
.request_time_per_output_token_seconds
325
.get_or_create_owned(&labels),
326
+ events: engine_stats_enabled.then(|| EventMetricHandles {
327
+ num_preemptions: metrics.num_preemptions.get_or_create_owned(&labels),
328
+ request_num_preemptions: metrics.request_num_preemptions.get_or_create_owned(&labels),
329
+ request_queue_time_seconds: metrics
330
+ .request_queue_time_seconds
331
+ .get_or_create_owned(&labels),
332
+ request_prefill_time_seconds: metrics
333
+ .request_prefill_time_seconds
334
+ .get_or_create_owned(&labels),
335
+ request_inference_time_seconds: metrics
336
+ .request_inference_time_seconds
337
+ .get_or_create_owned(&labels),
338
+ }),
339
labels,
340
}
341
}
@@ -356,7 +385,7 @@ mod tests {
385
#[test]
386
fn tracker_updates_timing_state_across_prefill_decode_and_finish() {
387
let mut tracker =
359
- RequestMetricsTracker::new("model".to_string(), 2, 100.0, 64, Some(128), 1);
388
+ RequestMetricsTracker::new("model".to_string(), 2, 100.0, 64, Some(128), 1, true);
389
390
tracker.observe_output(
391
10.0,
rust/src/llm/tests/generate.rs
+139
-1
@@ -6,12 +6,16 @@ use std::sync::{Arc, Once};
6
use std::time::Duration;
7
8
use futures::StreamExt as _;
9
+use tokio::sync::oneshot;
10
use tokio::time::timeout;
11
use tracing_subscriber::EnvFilter;
12
use uuid::Uuid;
13
+use vllm_engine_core_client::mock_engine::default_ready_response;
14
+use vllm_engine_core_client::protocol::handshake::EngineCoreReadyResponse;
15
use vllm_engine_core_client::protocol::logprobs::{
16
Logprobs, MaybeWireLogprobs, PositionLogprobs, TokenLogprob,
17
};
18
+use vllm_engine_core_client::protocol::lora::LoraRequest;
19
use vllm_engine_core_client::protocol::output::{
20
EngineCoreEvent, EngineCoreEventType, EngineCoreFinishReason, EngineCoreOutput,
21
EngineCoreOutputs, RequestBatchOutputs,
@@ -20,7 +24,9 @@ use vllm_engine_core_client::protocol::request::EngineCoreRequest;
24
use vllm_engine_core_client::protocol::sampling::EngineCoreSamplingParams;
25
use vllm_engine_core_client::protocol::sampling_mask::{MaybeWireSamplingMask, SamplingMask};
26
use vllm_engine_core_client::protocol::stats::PrefillStats;
23
-use vllm_engine_core_client::test_utils::{IpcNamespace, spawn_mock_engine_task};
27
+use vllm_engine_core_client::test_utils::{
28
+ IpcNamespace, spawn_mock_engine_task, spawn_mock_engine_task_with_ready,
29
+};
30
use vllm_engine_core_client::{EngineCoreClient, EngineCoreClientConfig, EngineId};
31
use vllm_llm::{
32
Error, FinishReason, GenerateOutputStreamExt as _, GeneratePromptInfo, GenerateRequest, Llm,
@@ -868,6 +874,138 @@ async fn generate_records_request_metrics_in_prometheus_output() {
874
assert!(rendered.contains(&format!(
875
"vllm:request_prefill_kv_computed_tokens_count{{model_name=\"{model_name}\",engine=\"4\"}} 1"
876
)));
877
+ assert!(rendered.contains(&format!(
878
+ "vllm:request_queue_time_seconds_sum{{model_name=\"{model_name}\",engine=\"4\"}} 1.0"
879
+ )));
880
+
881
+ let _ = shutdown_tx.send(());
882
+ engine_task.await.unwrap();
883
+ llm.shutdown().await.unwrap();
884
+}
885
+
886
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
887
+async fn generate_without_engine_stats_exports_only_frontend_observed_metrics() {
888
+ let ipc = IpcNamespace::new().unwrap();
889
+ let handshake_address = ipc.handshake_endpoint();
890
+ let engine_id = EngineId::from_engine_index(6);
891
+ let model_name = request_metrics_model_name("metrics-no-log-stats-model");
892
+ let lora_name = request_metrics_model_name("metrics-no-log-stats-lora");
893
+ let (finish_tx, finish_rx) = oneshot::channel::<()>();
894
+
895
+ let (shutdown_tx, engine_task) = spawn_mock_engine_task_with_ready(
896
+ handshake_address.clone(),
897
+ engine_id,
898
+ EngineCoreReadyResponse {
899
+ supports_lora: true,
900
+ max_loras: 1,
901
+ ..default_ready_response()
902
+ },
903
+ move |dealer, push| {
904
+ Box::pin(async move {
905
+ let add = recv_engine_message(dealer).await;
906
+ assert_eq!(add[0].as_ref(), &[0x00]);
907
+ let request: EngineCoreRequest = rmp_serde::from_slice(&add[1]).unwrap();
908
+
909
+ // Engines started with `--disable-log-stats` attach neither
910
+ // lifecycle events nor scheduler stats to their outputs.
911
+ for token in [1, 2] {
912
+ send_outputs(
913
+ push,
914
+ RequestBatchOutputs {
915
+ engine_index: 6,
916
+ timestamp: 10.0 + f64::from(token),
917
+ outputs: vec![request_output(&request.request_id, vec![token], None)],
918
+ ..Default::default()
919
+ }
920
+ .into(),
921
+ )
922
+ .await;
923
+ }
924
+
925
+ finish_rx.await.unwrap();
926
+ send_outputs(
927
+ push,
928
+ RequestBatchOutputs {
929
+ engine_index: 6,
930
+ timestamp: 13.0,
931
+ outputs: vec![request_output(
932
+ &request.request_id,
933
+ vec![3],
934
+ Some(EngineCoreFinishReason::Length),
935
+ )],
936
+ finished_requests: Some(BTreeSet::from([request.request_id])),
937
+ ..Default::default()
938
+ }
939
+ .into(),
940
+ )
941
+ .await;
942
+ })
943
+ },
944
+ );
945
+
946
+ let client = EngineCoreClient::connect(
947
+ EngineCoreClientConfig::new_single(handshake_address)
948
+ .with_model_name(&model_name)
949
+ .with_engine_stats_enabled(false)
950
+ .with_local_input_output_addresses(
951
+ Some(ipc.input_endpoint()),
952
+ Some(ipc.output_endpoint()),
953
+ ),
954
+ )
955
+ .await
956
+ .unwrap();
957
+ let llm = Llm::new(client);
958
+ let mut request = sample_generate_request("req-no-log-stats", 8);
959
+ request.arrival_time = None;
960
+ request.lora_request = Some(LoraRequest {
961
+ lora_name: lora_name.clone(),
962
+ lora_int_id: 1,
963
+ lora_path: "/unused".to_string(),
964
+ base_model_name: None,
965
+ tensorizer_config_dict: None,
966
+ load_inplace: false,
967
+ is_3d_lora_weight: false,
968
+ });
969
+ let mut stream = llm.generate(request).await.unwrap();
970
+
971
+ assert_eq!(stream.next().await.unwrap().unwrap().token_ids, vec![1]);
972
+ // The dispatcher handles the second batch only after finishing the first
973
+ // one, including its LoRA info update.
974
+ assert_eq!(stream.next().await.unwrap().unwrap().token_ids, vec![2]);
975
+ assert!(!METRICS.render().unwrap().contains(&lora_name));
976
+
977
+ finish_tx.send(()).unwrap();
978
+ let final_output = stream.next().await.unwrap().unwrap();
979
+ assert_eq!(final_output.finish_reason, Some(FinishReason::Length));
980
+ assert!(stream.next().await.is_none());
981
+
982
+ let rendered = METRICS.render().unwrap();
983
+ for metric in [
984
+ "vllm:time_to_first_token_seconds_count",
985
+ "vllm:e2e_request_latency_seconds_count",
986
+ "vllm:request_decode_time_seconds_count",
987
+ ] {
988
+ assert!(
989
+ rendered.contains(&format!(
990
+ "{metric}{{model_name=\"{model_name}\",engine=\"6\"}} 1"
991
+ )),
992
+ "{metric} should be exported"
993
+ );
994
+ }
995
+ for metric in [
996
+ "vllm:num_requests_running",
997
+ "vllm:kv_cache_usage_perc",
998
+ "vllm:num_preemptions_total",
999
+ "vllm:request_num_preemptions_count",
1000
+ "vllm:request_queue_time_seconds_count",
1001
+ "vllm:request_prefill_time_seconds_count",
1002
+ "vllm:request_inference_time_seconds_count",
1003
+ ] {
1004
+ assert!(
1005
+ !rendered.contains(&format!("{metric}{{model_name=\"{model_name}\"")),
1006
+ "{metric} should not be exported"
1007
+ );
1008
+ }
1009
1010
let _ = shutdown_tx.send(());
1011
engine_task.await.unwrap();
rust/src/mock-engine/src/tests.rs
+1
@@ -29,6 +29,7 @@ fn client_config(handshake_address: String, engine_count: usize) -> EngineCoreCl
29
coordinator_mode: None,
30
model_name: "mock-model".to_string(),
31
client_index: 0,
32
+ engine_stats_enabled: true,
33
}
34
}
35
rust/src/server/src/config.rs
+3
-1
@@ -268,7 +268,9 @@ pub struct Config {
268
#[educe(Debug(method(fmt_redacted_api_keys)))]
269
pub api_keys: Vec<String>,
270
/// When `true`, suppress periodic stats logging (throughput, queue depth,
271
- /// cache usage).
271
+ /// cache usage). Engines also stop recording stats, so metrics derived from
272
+ /// engine-reported scheduler stats and request lifecycle events are not
273
+ /// exported.
274
pub disable_log_stats: bool,
275
/// TCP port for the gRPC Inference service. When `None`, no gRPC server is
276
/// started.
rust/src/server/src/grpc/tests.rs
+1
@@ -2234,6 +2234,7 @@ async fn control_aggregates_multi_engine_capacity() {
2234
coordinator_mode: None,
2235
model_name: "test-model".to_string(),
2236
client_index: 0,
2237
+ engine_stats_enabled: true,
2238
};
2239
let client_task = tokio::spawn(EngineCoreClient::connect(client_config));
2240
let mut engine_sockets = Vec::new();
rust/src/server/src/lib.rs
+1
@@ -130,6 +130,7 @@ async fn build_state(config: &Config) -> Result<Arc<AppState>> {
130
coordinator_mode,
131
model_name: metrics_model_name,
132
client_index: 0,
133
+ engine_stats_enabled: !config.disable_log_stats,
134
})
135
.await
136
.context("failed to connect to engine core")?;