[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")?;