[Rust Frontend] Separate multimodal instrumentation from request timing (#58084)
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Bugen Zhao committed
Sep 22, 2026 at 14:57 UTC
a6c47fbbf4187e8ae5396ebdcf93234db617a4ad
9 files changed
+143
-52
rust/src/bench/src/mm_processor.rs
+4
-3
@@ -16,13 +16,14 @@ use tokio::sync::Semaphore;
16
17
use vllm_chat::{
18
ChatContent, ChatContentPart, ChatLlm, ChatMessage, ChatRequest, LoadModelBackendsOptions,
19
- MultiModalTimingStats, load_model_backends,
19
+ load_model_backends,
20
};
21
use vllm_engine_core_client::{EngineCoreClient, EngineCoreClientConfig, TransportMode};
22
use vllm_llm::Llm;
23
use vllm_managed_engine::cli::ManagedEngineArgs;
24
use vllm_managed_engine::{ManagedEngineConfig, ManagedEngineHandle};
25
use vllm_text::TextLlm;
26
+use vllm_tracing::timing::RequestTimingStats;
27
28
use crate::config::RangeRatio;
29
use crate::datasets::SampleRequest;
@@ -133,7 +134,7 @@ pub struct MmProcessorArgs {
134
}
135
136
/// Entry point for `vllm-bench mm-processor`.
136
-pub async fn run_mm_processor(args: MmProcessorArgs, timing: MultiModalTimingStats) -> Result<()> {
137
+pub async fn run_mm_processor(args: MmProcessorArgs, timing: RequestTimingStats) -> Result<()> {
138
if args.engine.data_parallel_size_local == Some(0) {
139
anyhow::bail!(
140
"`--data-parallel-size-local 0` is not supported; the benchmark requires \
@@ -179,7 +180,7 @@ pub async fn run_mm_processor(args: MmProcessorArgs, timing: MultiModalTimingSta
180
async fn run_mm_processor_with_engine(
181
args: MmProcessorArgs,
182
handshake_port: u16,
182
- timing: &MultiModalTimingStats,
183
+ timing: &RequestTimingStats,
184
) -> Result<()> {
185
let range_ratio =
186
RangeRatio::parse(&args.random_range_ratio).context("invalid --random-range-ratio")?;
rust/src/chat/src/lib.rs
+1
-3
@@ -22,6 +22,7 @@ pub use event::{
22
};
23
use futures::{StreamExt, TryStreamExt as _};
24
pub use llm_multimodal::MediaContentPart;
25
+pub use multimodal::{mm_request_span, mm_timing_layer};
26
use output::apply_output_grammar;
27
pub use output::{
28
ChatOutputProcessor, DefaultChatOutputProcessor, DynChatOutputProcessor,
@@ -47,9 +48,6 @@ pub use stream::{ChatEventStream, ChatEventStreamTrait, CollectedAssistantMessag
48
pub use vllm_engine_core_client::protocol::multimodal::MmFeatures;
49
pub use vllm_llm::FinishReason;
50
pub use vllm_text::GenerationConfigMode;
50
-pub use vllm_tracing::timing::{
51
- RequestTimingStats as MultiModalTimingStats, mm_request_span, mm_timing_layer,
52
-};
51
52
mod backend;
53
mod error;
rust/src/chat/src/multimodal.rs
+23
-23
@@ -43,11 +43,13 @@ mod input;
43
mod item;
44
mod preprocessed;
45
mod tensor;
46
+mod timing;
47
mod video;
48
49
use self::expand::expand_prompt_token_ids;
50
pub use self::input::MultimodalInput;
50
-use vllm_tracing::timing::{mm_request_span, mm_stage_span};
51
+use self::timing::MM_STAGE_TARGET;
52
+pub use self::timing::{mm_request_span, mm_timing_layer};
53
54
/// Resolved multimodal support for one loaded model.
55
#[derive(Clone)]
@@ -760,13 +762,17 @@ impl MultimodalModelInfo {
762
if media_parts.is_empty() {
763
return Ok(Vec::new());
764
}
763
- self.prepare_multimodal_timed(media_parts, prompt_token_ids, model_dtype)
764
- .instrument(mm_stage_span("preprocessor_total"))
765
- .await
765
+ self.prepare_multimodal_timed(media_parts, prompt_token_ids, model_dtype).await
766
}
767
768
/// Timed body of [`Self::prepare_multimodal`]; each stage is a `tracing`
769
/// span aggregated per request by the `vllm-tracing` timing layer.
770
+ #[tracing::instrument(
771
+ name = "mm_stage",
772
+ target = MM_STAGE_TARGET,
773
+ skip_all,
774
+ fields(stage = "preprocessor_total")
775
+ )]
776
async fn prepare_multimodal_timed(
777
&self,
778
media_parts: Vec<MediaContentPart>,
@@ -775,34 +781,22 @@ impl MultimodalModelInfo {
781
) -> Result<MmFeatures> {
782
let media_parts_len = media_parts.len();
783
self.validate_mm_limits(&media_parts)?;
778
- let fetched =
779
- self.fetch_media(media_parts).instrument(mm_stage_span("media_fetch")).await?;
784
+ let fetched = self.fetch_media(media_parts).await?;
785
786
let mut prepared = Vec::new();
787
if !fetched.images.is_empty() {
783
- prepared.push(
784
- self.prepare_images(fetched.images, fetched.image_uuids, model_dtype)
785
- .instrument(mm_stage_span("preprocess_image"))
786
- .await?,
787
- );
788
+ prepared
789
+ .push(self.prepare_images(fetched.images, fetched.image_uuids, model_dtype).await?);
790
}
791
if !fetched.videos.is_empty() {
790
- prepared.push(
791
- self.prepare_videos(fetched.videos, fetched.video_uuids, model_dtype)
792
- .instrument(mm_stage_span("preprocess_video"))
793
- .await?,
794
- );
792
+ prepared
793
+ .push(self.prepare_videos(fetched.videos, fetched.video_uuids, model_dtype).await?);
794
}
795
if !fetched.audios.is_empty() {
797
- prepared.push(
798
- self.prepare_audios(fetched.audios, fetched.audio_uuids)
799
- .instrument(mm_stage_span("preprocess_audio"))
800
- .await?,
801
- );
796
+ prepared.push(self.prepare_audios(fetched.audios, fetched.audio_uuids).await?);
797
}
798
804
- let mut ranges = mm_stage_span("prompt_expansion")
805
- .in_scope(|| expand_prompt_token_ids(prompt_token_ids, &prepared))?;
799
+ let mut ranges = expand_prompt_token_ids(prompt_token_ids, &prepared)?;
800
801
let mut features = Vec::with_capacity(media_parts_len);
802
for media in prepared {
@@ -845,6 +839,12 @@ impl MultimodalModelInfo {
839
840
/// Fetch all connector-backed media parts and split them per modality,
841
/// preserving their request-order UUID metadata.
842
+ #[tracing::instrument(
843
+ name = "mm_stage",
844
+ target = MM_STAGE_TARGET,
845
+ skip_all,
846
+ fields(stage = "media_fetch")
847
+ )]
848
async fn fetch_media(&self, media_parts: Vec<MediaContentPart>) -> Result<FetchedMedia> {
849
let mut tracker = AsyncMultiModalTracker::new(Arc::clone(&self.media_connector));
850
for part in media_parts {
rust/src/chat/src/multimodal/audio.rs
+46
@@ -8,6 +8,7 @@ use std::sync::Arc;
8
use llm_multimodal::{AudioClip, Modality, PreprocessedEncoderInputs};
9
use vllm_engine_core_client::protocol::dtype::ModelDtype;
10
11
+use super::timing::MM_STAGE_TARGET;
12
use super::{AudioModalitySupport, MultimodalModelInfo, PreparedMedia, item};
13
use crate::error::{Error, Result, bail_multimodal, multimodal};
14
@@ -16,6 +17,12 @@ pub(super) const AUDIO_PRIMARY_KEY: &str = "input_audio_features";
17
18
impl MultimodalModelInfo {
19
/// Preprocess fetched audio clips as one batch and build per-item features.
20
+ #[tracing::instrument(
21
+ name = "mm_stage",
22
+ target = MM_STAGE_TARGET,
23
+ skip_all,
24
+ fields(stage = "preprocess_audio")
25
+ )]
26
pub(super) async fn prepare_audios(
27
&self,
28
clips: Vec<Arc<AudioClip>>,
@@ -236,6 +243,45 @@ mod tests {
243
));
244
}
245
246
+ #[tokio::test]
247
+ async fn audio_pipeline_attributes_stage_timings_to_request() {
248
+ use tracing::Instrument as _;
249
+ use tracing::instrument::WithSubscriber as _;
250
+ use tracing_subscriber::layer::SubscriberExt as _;
251
+
252
+ use crate::{mm_request_span, mm_timing_layer};
253
+
254
+ let info = qwen3_asr_info();
255
+ let (layer, timings) = mm_timing_layer();
256
+ let parts = vec![MediaContentPart::AudioData {
257
+ data: wav_i16_mono(16_000, &[0; 1_600]),
258
+ mime_type: Some("audio/wav".to_string()),
259
+ uuid: Some("audio-1".to_string()),
260
+ }];
261
+ async {
262
+ info.prepare_multimodal(parts, &mut vec![AUDIO_PAD_ID], ModelDtype::Float32)
263
+ .instrument(mm_request_span("request-1"))
264
+ .await
265
+ .unwrap();
266
+ }
267
+ .with_subscriber(tracing_subscriber::registry().with(layer))
268
+ .await;
269
+
270
+ let records = timings.stat();
271
+ assert_eq!(records.len(), 1);
272
+ let mut stages = records["request-1"].keys().collect::<Vec<_>>();
273
+ stages.sort();
274
+ expect_test::expect![[r#"
275
+ [
276
+ "media_fetch_secs",
277
+ "preprocess_audio_secs",
278
+ "preprocessor_total_secs",
279
+ "prompt_expansion_secs",
280
+ ]
281
+ "#]]
282
+ .assert_debug_eq(&stages);
283
+ }
284
+
285
#[tokio::test]
286
async fn tracker_processor_and_lowering_preserve_audio_contract() {
287
let info = qwen3_asr_info();
rust/src/chat/src/multimodal/expand.rs
+7
@@ -10,6 +10,7 @@ use vllm_engine_core_client::protocol::multimodal::PlaceholderRange;
10
use vllm_engine_core_client::protocol::tensor::WireTensor;
11
12
use super::PreparedMedia;
13
+use super::timing::MM_STAGE_TARGET;
14
use crate::error::{Error, Result, bail_multimodal};
15
16
/// One modality's queue of pending placeholder replacements for prompt
@@ -55,6 +56,12 @@ impl<'a> ExpansionLane<'a> {
56
///
57
/// The returned ranges point into the already-expanded prompt, grouped per
58
/// modality in item order.
59
+#[tracing::instrument(
60
+ name = "mm_stage",
61
+ target = MM_STAGE_TARGET,
62
+ skip_all,
63
+ fields(stage = "prompt_expansion")
64
+)]
65
pub(super) fn expand_prompt_token_ids(
66
prompt_token_ids: &mut Vec<u32>,
67
prepared: &[PreparedMedia],
rust/src/chat/src/multimodal/image.rs
+7
@@ -9,6 +9,7 @@ use std::sync::Arc;
9
use llm_multimodal::{ImageFrame, Modality, PreprocessedEncoderInputs};
10
use vllm_engine_core_client::protocol::dtype::ModelDtype;
11
12
+use super::timing::MM_STAGE_TARGET;
13
use super::{ModalitySupport, MultimodalModelInfo, PreparedMedia, item};
14
use crate::error::{Error, Result, bail_multimodal, multimodal};
15
@@ -18,6 +19,12 @@ pub(super) const IMAGE_PRIMARY_KEY: &str = "pixel_values";
19
impl MultimodalModelInfo {
20
/// Preprocess all fetched image frames as one batch and build per-item
21
/// features.
22
+ #[tracing::instrument(
23
+ name = "mm_stage",
24
+ target = MM_STAGE_TARGET,
25
+ skip_all,
26
+ fields(stage = "preprocess_image")
27
+ )]
28
pub(super) async fn prepare_images(
29
&self,
30
frames: Vec<Arc<ImageFrame>>,
rust/src/chat/src/multimodal/timing.rs
new
+22
@@ -0,0 +1,22 @@
1
+// SPDX-License-Identifier: Apache-2.0
2
+// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+//! Multimodal preprocessing spans and their timing collector configuration.
5
+
6
+use tracing::{Span, info_span};
7
+use vllm_tracing::timing::{RequestTimingLayer, RequestTimingStats};
8
+
9
+/// Target of the multimodal preprocessing stage spans.
10
+pub(super) const MM_STAGE_TARGET: &str = "mm_processor_timing";
11
+
12
+/// Create the timing layer and its stats handle for multimodal preprocessing
13
+/// stage spans (`vllm-bench mm-processor`), mirroring the Python
14
+/// `TimingContext` / `MultiModalTimingRegistry`.
15
+pub fn mm_timing_layer() -> (RequestTimingLayer, RequestTimingStats) {
16
+ RequestTimingLayer::new(MM_STAGE_TARGET)
17
+}
18
+
19
+/// Span carrying the `request_id` used to attribute multimodal stage timings.
20
+pub fn mm_request_span(request_id: &str) -> Span {
21
+ info_span!("mm_request", request_id)
22
+}
rust/src/chat/src/multimodal/video.rs
+7
@@ -16,6 +16,7 @@ use vllm_engine_core_client::protocol::multimodal::{
16
SliceSpec,
17
};
18
19
+use super::timing::MM_STAGE_TARGET;
20
use super::{ModalitySupport, MultimodalModelInfo, PreparedItem, PreparedMedia, tensor};
21
use crate::error::{Error, Result, bail_multimodal, multimodal};
22
@@ -32,6 +33,12 @@ impl MultimodalModelInfo {
33
/// Unlike images, each clip runs through the preprocessor independently
34
/// (a batch of one), so its tensors are complete per item and need no
35
/// cross-item slicing.
36
+ #[tracing::instrument(
37
+ name = "mm_stage",
38
+ target = MM_STAGE_TARGET,
39
+ skip_all,
40
+ fields(stage = "preprocess_video")
41
+ )]
42
pub(super) async fn prepare_videos(
43
&self,
44
clips: Vec<Arc<VideoClip>>,
rust/src/tracing/src/timing.rs
+26
-23
@@ -16,8 +16,8 @@ use std::sync::{Arc, Mutex};
16
use std::time::Instant;
17
18
use tracing::field::{Field, Visit};
19
-use tracing::span::{Attributes, Span};
20
-use tracing::{Id, Subscriber, info_span};
19
+use tracing::span::Attributes;
20
+use tracing::{Id, Subscriber};
21
use tracing_subscriber::layer::{Context, Layer};
22
use tracing_subscriber::registry::LookupSpan;
23
@@ -60,6 +60,26 @@ impl Visit for SpanFields {
60
61
/// `tracing` layer that aggregates stage-span timings per request, keyed by
62
/// the nearest ancestor span with a `request_id` field.
63
+///
64
+/// # Span contract
65
+///
66
+/// Timed spans must have the configured target and a `stage` field. The
67
+/// `request_id` is taken from the timed span itself or its nearest ancestor
68
+/// carrying that field, regardless of the ancestor's target. Spans without
69
+/// a request ID are ignored. Span names are unrestricted.
70
+///
71
+/// Both fields are read at span creation; subsequent [`tracing::Span::record`]
72
+/// calls are ignored. String values are used verbatim; other values use their
73
+/// debug representation. Prefer string fields for stable keys.
74
+///
75
+/// # Timing semantics
76
+///
77
+/// Each sample measures wall-clock time from span creation to final close,
78
+/// including async waits and any time the span remains alive through cloned
79
+/// handles. Nested and concurrent spans are measured independently, so their
80
+/// durations can overlap. Repeated stages accumulate under `"{stage}_secs"`
81
+/// for each request. Closing spans are recorded regardless of the operation's
82
+/// success, failure, or cancellation; callers select the samples to report.
83
pub struct RequestTimingLayer {
84
target: &'static str,
85
stats: Arc<Mutex<HashMap<String, StageStats>>>,
@@ -87,6 +107,10 @@ impl RequestTimingLayer {
107
108
impl RequestTimingStats {
109
/// Drain and return `{request_id: {stage_secs}}` records.
110
+ ///
111
+ /// Returns accumulated timings since the previous drain. Spans still open
112
+ /// contribute to a later drain; finish the measured work before calling
113
+ /// this method to collect complete request timings.
114
pub fn stat(&self) -> HashMap<String, StageStats> {
115
std::mem::take(&mut *self.stats.lock().unwrap())
116
}
@@ -139,27 +163,6 @@ where
163
}
164
}
165
142
-/// Target of the multimodal preprocessing stage spans.
143
-const MM_STAGE_TARGET: &str = "mm_processor_timing";
144
-
145
-/// Create the timing layer and its stats handle for multimodal preprocessing
146
-/// stage spans (`vllm-bench mm-processor`), mirroring the Python
147
-/// `TimingContext` / `MultiModalTimingRegistry`.
148
-pub fn mm_timing_layer() -> (RequestTimingLayer, RequestTimingStats) {
149
- RequestTimingLayer::new(MM_STAGE_TARGET)
150
-}
151
-
152
-/// Span carrying the `request_id` used to attribute multimodal stage timings.
153
-pub fn mm_request_span(request_id: &str) -> Span {
154
- info_span!("mm_request", request_id)
155
-}
156
-
157
-/// Span for one multimodal preprocessing stage; the elapsed time is recorded
158
-/// on close.
159
-pub fn mm_stage_span(stage: &'static str) -> Span {
160
- info_span!(target: MM_STAGE_TARGET, "mm_stage", stage)
161
-}
162
-
166
#[cfg(test)]
167
mod tests {
168
use super::*;