[Rust][Benchmark] Use `tracing` for logs (#48937)

Signed-off-by: Bugen Zhao <i@bugenzhao.com>

Bugen Zhao committed Jul 21, 2026 at 13:18 UTC 8688a06d676bda6e9e3ec3a87233dce91c40bb4f
24 files changed +498 -249
rust/Cargo.lock
+3
@@ -5498,10 +5498,13 @@ dependencies = [
5498 "serde",
5499 "serde_json",
5500 "thiserror 2.0.18",
5501 + "thiserror-ext",
5502 "tiktoken-rs 0.9.1",
5503 "tokenizers",
5504 "tokio",
5505 "tokio-stream",
5506 + "tracing",
5507 + "tracing-subscriber",
5508 "url",
5509 "uuid",
5510 ]
rust/src/bench/Cargo.toml
+3
@@ -26,10 +26,13 @@ rustc-hash.workspace = true
26 serde = { workspace = true, features = ["rc"] }
27 serde_json = { workspace = true, features = ["raw_value"] }
28 thiserror.workspace = true
29 +thiserror-ext.workspace = true
30 tiktoken-rs.workspace = true
31 tokenizers.workspace = true
32 tokio.workspace = true
33 tokio-stream.workspace = true
34 +tracing.workspace = true
35 +tracing-subscriber.workspace = true
36 url.workspace = true
37 uuid.workspace = true
38
rust/src/bench/src/backends/pooling.rs
+8 -7
@@ -158,9 +158,10 @@ impl PoolingBackend {
158 // (mirrors Python async_request_vllm_rerank).
159 if let Some(ref list) = input.prompt_list {
160 if list.len() < 2 {
161 - eprintln!(
162 - "WARNING: vllm-rerank request has no documents \
163 - (prompt_list needs [query, doc, ...])"
161 + tracing::warn!(
162 + backend = "vllm-rerank",
163 + inputs = list.len(),
164 + "rerank request has no documents"
165 );
166 }
167 let query = list.first().map(|s| s.as_ref()).unwrap_or("");
@@ -175,10 +176,10 @@ impl PoolingBackend {
176 // Legacy path: text prompt as query, documents via --extra-body.
177 let query = input.prompt.as_ref();
178 if query.is_empty() && input.prompt_token_ids.is_some() {
178 - eprintln!(
179 - "WARNING: vllm-rerank received empty query (random dataset uses \
180 - token IDs only). Use --dataset-name random-rerank for meaningful \
181 - rerank benchmarks."
179 + tracing::warn!(
180 + backend = "vllm-rerank",
181 + dataset = "random",
182 + "rerank request has an empty query; use the random-rerank dataset"
183 );
184 }
185 serde_json::json!({
rust/src/bench/src/benchmark.rs
+107 -80
@@ -6,6 +6,7 @@ use std::sync::Arc;
6 use std::time::Instant;
7
8 use indicatif::{ProgressBar, ProgressStyle};
9 +use thiserror_ext::AsReport as _;
10 use tokio::sync::Semaphore;
11
12 use crate::backends::{RequestFuncInput, RequestFuncOutput, get_backend};
@@ -72,12 +73,12 @@ pub fn pre_resolve_dns(
73 v4.extend(v6);
74 if !v4.is_empty() {
75 let ips: Vec<_> = v4.iter().map(|a| a.ip()).collect();
75 - println!("Pre-resolved {host} -> {ips:?}");
76 + tracing::info!(host, addresses = ?ips, "pre-resolved benchmark endpoint DNS");
77 builder = builder.resolve_to_addrs(host, &v4);
78 }
79 }
80 Err(e) => {
80 - eprintln!("Warning: DNS pre-resolution for '{host}' failed: {e}");
81 + tracing::warn!(host, error = %e.as_report(), "failed to pre-resolve benchmark endpoint DNS");
82 }
83 }
84
@@ -346,10 +347,14 @@ pub async fn run_benchmark(config: &BenchConfig) -> Result<serde_json::Value> {
347 let (model_id, model_name) = if let Some(ref m) = config.model {
348 (m.clone(), config.model_name.clone())
349 } else {
349 - println!("Model not specified, fetching first model from server...");
350 + tracing::info!(base_url = %config.base_url, "fetching first model from server");
351 let (name, id) =
352 get_first_model_from_server(&config.base_url, &client, &config.extra_headers).await?;
352 - println!("First model name: {name}, first model id: {id}");
353 + tracing::info!(
354 + model_name = name,
355 + model_id = id,
356 + "selected first model from server"
357 + );
358 (id, Some(name))
359 };
360
@@ -358,10 +363,9 @@ pub async fn run_benchmark(config: &BenchConfig) -> Result<serde_json::Value> {
363 None
364 } else {
365 let tid = config.tokenizer_id.as_deref().unwrap_or(&model_id);
361 - println!("Loading tokenizer: {tid}");
366 + tracing::info!(tokenizer = tid, "loading tokenizer");
367 let server_info = Some((config.base_url.as_str(), model_id.as_str()));
368 let t = crate::tokenizer::load_tokenizer(tid, config.trust_remote_code, server_info)?;
364 - println!("Tokenizer loaded successfully.");
369 Some(t)
370 };
371 let has_tokenizer = tokenizer.is_some();
@@ -421,7 +425,12 @@ pub async fn run_benchmark(config: &BenchConfig) -> Result<serde_json::Value> {
425 config.num_prompts, config.random_batch_size, config.is_reranker,
426 ),
427 };
424 - println!("Generating {dataset_label}...");
428 + tracing::info!(
429 + dataset = ?config.dataset_name,
430 + prompts = config.num_prompts,
431 + description = %dataset_label,
432 + "generating benchmark dataset"
433 + );
434 let gen_start = Instant::now();
435
436 let mut input_requests = match config.dataset_name {
@@ -608,18 +617,19 @@ pub async fn run_benchmark(config: &BenchConfig) -> Result<serde_json::Value> {
617 };
618
619 let gen_elapsed = gen_start.elapsed();
611 - println!(
612 - "Generated {} prompts in {:.2}s",
613 - input_requests.len(),
614 - gen_elapsed.as_secs_f64()
620 + tracing::info!(
621 + prompts = input_requests.len(),
622 + elapsed_seconds = gen_elapsed.as_secs_f64(),
623 + "generated benchmark dataset"
624 );
625
626 let filtered_count =
627 filter_requests_by_max_model_len(&mut input_requests, config.max_model_len);
628 if filtered_count > 0 {
620 - println!(
621 - "Filtered {filtered_count} prompt(s) above --max-model-len {}.",
622 - config.max_model_len.unwrap()
629 + tracing::info!(
630 + filtered_prompts = filtered_count,
631 + max_model_len = config.max_model_len.unwrap(),
632 + "filtered prompts above maximum model length"
633 );
634 }
635 if input_requests.is_empty() {
@@ -670,7 +680,7 @@ pub async fn run_benchmark(config: &BenchConfig) -> Result<serde_json::Value> {
680
681 // Ready check
682 if config.ready_check_timeout_sec > 0 {
673 - println!("Starting initial single prompt test run...");
683 + tracing::info!("starting initial single-prompt test run");
684 let test_output = wait_for_endpoint(
685 config.backend,
686 &client,
@@ -685,7 +695,7 @@ pub async fn run_benchmark(config: &BenchConfig) -> Result<serde_json::Value> {
695 test_output.error
696 )));
697 }
688 - println!("Initial test run completed.");
698 + tracing::info!("initial single-prompt test run completed");
699 }
700
701 // Verify and fix prompt token lengths against the server's /tokenize endpoint.
@@ -703,12 +713,15 @@ pub async fn run_benchmark(config: &BenchConfig) -> Result<serde_json::Value> {
713 DatasetName::Random | DatasetName::PrefixRepetition
714 );
715 if verifiable_dataset && has_token_ids && !config.backend.is_pooling() {
706 - println!("Using prompt_token_ids, skipping server-side tokenizer verification.");
716 + tracing::info!(
717 + reason = "prompt_token_ids",
718 + "skipping server tokenizer verification"
719 + );
720 }
721 if verifiable_dataset && !has_token_ids && !config.backend.is_pooling() {
722 let cache_key = tokenizer_verify_cache_key(&config.base_url, &model_id);
723 if is_tokenizer_verified(&cache_key) {
711 - println!("Tokenizer verified in previous run (cached), skipping verification.");
724 + tracing::info!(reason = "cached", "skipping server tokenizer verification");
725 } else {
726 let num_special =
727 tokenizer.as_ref().map(|t| t.num_special_tokens_to_add()).unwrap_or(0);
@@ -723,14 +736,17 @@ pub async fn run_benchmark(config: &BenchConfig) -> Result<serde_json::Value> {
736 .await?
737 {
738 SampleVerifyOutcome::Passed => {
726 - println!("Sample verification passed, skipping full verification.");
739 + tracing::info!("tokenizer sample verification passed");
740 mark_tokenizer_verified(&cache_key);
741 }
742 SampleVerifyOutcome::Skipped(reason) => {
730 - println!("Server /tokenize unavailable ({reason}), skipping verification.");
743 + tracing::warn!(
744 + reason = %reason,
745 + "server tokenizer unavailable; skipping prompt verification"
746 + );
747 }
748 SampleVerifyOutcome::Mismatch => {
733 - println!("Sample verification found mismatch, running full verify+fix...");
749 + tracing::warn!("tokenizer sample mismatch; verifying and fixing all prompts");
750 match verify_and_fix_prompt_lengths(
751 &client,
752 &config.base_url,
@@ -742,16 +758,16 @@ pub async fn run_benchmark(config: &BenchConfig) -> Result<serde_json::Value> {
758 .await
759 {
760 Ok(()) => {
745 - println!(
746 - "All {} prompts verified: exact token length match.",
747 - input_requests.len()
761 + tracing::info!(
762 + prompts = input_requests.len(),
763 + "verified exact prompt token lengths"
764 );
765 mark_tokenizer_verified(&cache_key);
766 }
767 Err(BenchError::TokenizeUnavailable(reason)) => {
752 - println!(
753 - "Server /tokenize became unavailable during verification \
754 - ({reason}); proceeding with client-side token counts."
768 + tracing::warn!(
769 + reason = %reason,
770 + "server tokenizer became unavailable; using client token counts"
771 );
772 }
773 Err(e) => return Err(e),
@@ -763,7 +779,7 @@ pub async fn run_benchmark(config: &BenchConfig) -> Result<serde_json::Value> {
779
780 // Warmup
781 if config.num_warmups > 0 {
766 - println!("Warming up with {} requests...", config.num_warmups);
782 + tracing::info!(requests = config.num_warmups, "starting benchmark warmup");
783 run_warmup(
784 config.backend,
785 &client,
@@ -776,7 +792,7 @@ pub async fn run_benchmark(config: &BenchConfig) -> Result<serde_json::Value> {
792 config.disable_tqdm,
793 )
794 .await;
779 - println!("Warmup run completed.");
795 + tracing::info!(requests = config.num_warmups, "benchmark warmup completed");
796 }
797
798 // Start profiler if requested (immediate mode — no batch threshold)
@@ -814,28 +830,22 @@ pub async fn run_benchmark(config: &BenchConfig) -> Result<serde_json::Value> {
830 let spec_decode_before =
831 fetch_spec_decode_metrics(&config.base_url, &client, &config.extra_headers).await;
832 if spec_decode_before.is_some() {
817 - println!("Speculative decoding detected, will collect metrics.");
833 + tracing::info!("detected speculative decoding; collecting metrics");
834 }
835
836 // Main benchmark
821 - println!("Starting main benchmark run...");
837 let distribution = if config.burstiness == 1.0 {
838 "Poisson process"
839 } else {
840 "Gamma distribution"
841 };
827 - println!(
828 - "Traffic request rate: {}",
829 - if config.request_rate.is_infinite() {
830 - "inf".to_string()
831 - } else {
832 - format!("{}", config.request_rate)
833 - }
834 - );
835 - println!("Burstiness factor: {} ({distribution})", config.burstiness);
836 - println!(
837 - "Maximum request concurrency: {}",
838 - config.max_concurrency.unwrap_or(config.num_prompts)
842 + tracing::info!(
843 + request_rate = config.request_rate,
844 + burstiness = config.burstiness,
845 + distribution,
846 + max_concurrency = config.max_concurrency.unwrap_or(config.num_prompts),
847 + prompts = config.num_prompts,
848 + "starting main benchmark run"
849 );
850
851 // Pre-assign LoRA adapters to each request (None when --lora-modules not set).
@@ -847,11 +857,11 @@ pub async fn run_benchmark(config: &BenchConfig) -> Result<serde_json::Value> {
857 );
858 if let (Some(modules), Some(_)) = (config.lora_modules.as_ref(), lora_assignments.as_ref()) {
859 let names: Vec<&str> = modules.iter().map(|s| s.as_ref()).collect();
850 - println!(
851 - "LoRA adapters ({}): {:?} [assignment={:?}]",
852 - modules.len(),
853 - names,
854 - config.lora_assignment
860 + tracing::info!(
861 + adapters = modules.len(),
862 + names = ?names,
863 + assignment = ?config.lora_assignment,
864 + "assigned LoRA adapters"
865 );
866 }
867
@@ -1125,7 +1135,7 @@ pub async fn run_benchmark(config: &BenchConfig) -> Result<serde_json::Value> {
1135 if let Some((cancel_tx, task)) = profile_task {
1136 let _ = cancel_tx.send(());
1137 if let Err(e) = task.await {
1128 - eprintln!("WARNING: Profile background task failed: {e}");
1138 + tracing::error!(error = %e.as_report(), "profiler background task failed");
1139 }
1140 }
1141
@@ -1289,12 +1299,14 @@ pub(crate) async fn start_profiler_immediate(
1299 base_url: &str,
1300 extra_headers: &Option<std::collections::HashMap<String, String>>,
1301 ) {
1292 - println!("Starting profiler...");
1302 let profile_url = format!("{base_url}/start_profile");
1303 + tracing::info!(url = %profile_url, "starting profiler");
1304 match send_profile_request(client, &profile_url, extra_headers).await {
1295 - Ok(true) => println!("Profiler started"),
1296 - Ok(false) => eprintln!("WARNING: Profiler start request returned non-success"),
1297 - Err(e) => eprintln!("WARNING: Failed to start profiler: {e}"),
1305 + Ok(true) => tracing::info!(url = %profile_url, "profiler started"),
1306 + Ok(false) => tracing::warn!(url = %profile_url, "profiler start request was unsuccessful"),
1307 + Err(e) => {
1308 + tracing::warn!(url = %profile_url, error = %e.as_report(), "failed to start profiler")
1309 + }
1310 }
1311 }
1312
@@ -1304,12 +1316,14 @@ pub(crate) async fn stop_profiler_immediate(
1316 base_url: &str,
1317 extra_headers: &Option<std::collections::HashMap<String, String>>,
1318 ) {
1307 - println!("Stopping profiler...");
1319 let profile_url = format!("{base_url}/stop_profile");
1320 + tracing::info!(url = %profile_url, "stopping profiler");
1321 match send_profile_request(client, &profile_url, extra_headers).await {
1310 - Ok(true) => println!("Profiler stopped"),
1311 - Ok(false) => eprintln!("WARNING: Profiler stop request returned non-success"),
1312 - Err(e) => eprintln!("WARNING: Failed to stop profiler: {e}"),
1322 + Ok(true) => tracing::info!(url = %profile_url, "profiler stopped"),
1323 + Ok(false) => tracing::warn!(url = %profile_url, "profiler stop request was unsuccessful"),
1324 + Err(e) => {
1325 + tracing::warn!(url = %profile_url, error = %e.as_report(), "failed to stop profiler")
1326 + }
1327 }
1328 }
1329
@@ -1371,25 +1385,30 @@ pub(crate) async fn profile_on_batch_threshold(
1385 duration_secs: f64,
1386 mut cancel_rx: tokio::sync::oneshot::Receiver<()>,
1387 ) {
1374 - println!(
1375 - "Waiting for batch size >= {threshold} before starting profiler \
1376 - (will capture {duration_secs}s)..."
1388 + tracing::info!(
1389 + threshold,
1390 + duration_seconds = duration_secs,
1391 + "waiting for profiler batch threshold"
1392 );
1393
1394 loop {
1395 if let Some(running) = fetch_num_requests_running(client, base_url).await
1396 && running >= threshold
1397 {
1383 - println!("Batch size {running} >= {threshold}, starting profiler...");
1398 + tracing::info!(
1399 + running_requests = running,
1400 + threshold,
1401 + "profiler batch threshold reached"
1402 + );
1403 break;
1404 }
1405 // Wait 500ms or until the benchmark signals cancellation
1406 tokio::select! {
1407 _ = tokio::time::sleep(std::time::Duration::from_millis(500)) => {}
1408 _ = &mut cancel_rx => {
1390 - eprintln!(
1391 - "NOTE: Benchmark finished before batch threshold {threshold} was reached; \
1392 - profiling skipped."
1409 + tracing::warn!(
1410 + threshold,
1411 + "benchmark finished before profiler batch threshold; skipping profiling"
1412 );
1413 return;
1414 }
@@ -1398,13 +1417,13 @@ pub(crate) async fn profile_on_batch_threshold(
1417
1418 let start_url = format!("{base_url}/start_profile");
1419 match send_profile_request(client, &start_url, extra_headers).await {
1401 - Ok(true) => println!("Profiler started"),
1420 + Ok(true) => tracing::info!(url = %start_url, "profiler started"),
1421 Ok(false) => {
1403 - eprintln!("WARNING: Profiler start request returned non-success");
1422 + tracing::warn!(url = %start_url, "profiler start request was unsuccessful");
1423 return;
1424 }
1425 Err(e) => {
1407 - eprintln!("WARNING: Failed to start profiler: {e}");
1426 + tracing::warn!(url = %start_url, error = %e.as_report(), "failed to start profiler");
1427 return;
1428 }
1429 }
@@ -1413,15 +1432,17 @@ pub(crate) async fn profile_on_batch_threshold(
1432 tokio::select! {
1433 _ = tokio::time::sleep(std::time::Duration::from_secs_f64(duration_secs)) => {}
1434 _ = &mut cancel_rx => {
1416 - println!("Benchmark finished, stopping profiler early...");
1435 + tracing::info!("benchmark finished; stopping profiler early");
1436 }
1437 }
1438
1439 let stop_url = format!("{base_url}/stop_profile");
1440 match send_profile_request(client, &stop_url, extra_headers).await {
1422 - Ok(true) => println!("Profiler stopped after capturing"),
1423 - Ok(false) => eprintln!("WARNING: Profiler stop request returned non-success"),
1424 - Err(e) => eprintln!("WARNING: Failed to stop profiler: {e}"),
1441 + Ok(true) => tracing::info!(url = %stop_url, "profiler stopped after capture"),
1442 + Ok(false) => tracing::warn!(url = %stop_url, "profiler stop request was unsuccessful"),
1443 + Err(e) => {
1444 + tracing::warn!(url = %stop_url, error = %e.as_report(), "failed to stop profiler")
1445 + }
1446 }
1447 }
1448
@@ -1502,10 +1523,11 @@ async fn verify_and_fix_prompt_lengths(
1523 let excess = tokens.len().saturating_sub(expected_input_len);
1524 let compensate = if excess > 0 && last_excess == Some(excess) {
1525 if _iter == 1 {
1505 - eprintln!(
1506 - "Prompt {i}: server consistently adds {excess} extra token(s) \
1507 - (likely BOS), compensating target to {}.",
1508 - expected_input_len.saturating_sub(excess),
1526 + tracing::warn!(
1527 + prompt_index = i,
1528 + extra_tokens = excess,
1529 + adjusted_target = expected_input_len.saturating_sub(excess),
1530 + "server consistently adds prompt tokens; compensating verification target"
1531 );
1532 }
1533 excess
@@ -1563,7 +1585,10 @@ async fn verify_and_fix_prompt_lengths(
1585
1586 let fc = fixed_count.load(std::sync::atomic::Ordering::Relaxed);
1587 if fc > 0 {
1566 - println!("Fixed {fc} prompt(s) via server tokenize/detokenize convergence.");
1588 + tracing::info!(
1589 + fixed_prompts = fc,
1590 + "fixed prompt lengths using server tokenizer"
1591 + );
1592 }
1593
1594 Ok(())
@@ -1818,7 +1843,7 @@ async fn sample_verify_prompts(
1843 let tokenize_url = format!("{base_url}/tokenize");
1844 let api_key = std::env::var("OPENAI_API_KEY").ok();
1845
1821 - println!("Sampling {sample_size} prompts for verification...");
1846 + tracing::info!(sample_size, "sampling prompts for tokenizer verification");
1847
1848 for (i, request) in requests.iter().enumerate().take(sample_size) {
1849 let tokens = match server_tokenize(
@@ -1841,9 +1866,11 @@ async fn sample_verify_prompts(
1866
1867 let expected = request.prompt_len + num_special;
1868 if tokens.len() != expected {
1844 - println!(
1845 - "Prompt {i}: expected {expected} tokens, server returned {}",
1846 - tokens.len()
1869 + tracing::warn!(
1870 + prompt_index = i,
1871 + expected_tokens = expected,
1872 + actual_tokens = tokens.len(),
1873 + "tokenizer verification sample mismatch"
1874 );
1875 return Ok(SampleVerifyOutcome::Mismatch);
1876 }
rust/src/bench/src/config.rs
+18 -9
@@ -288,10 +288,18 @@ impl BenchConfig {
288 }
289 Some(other) => {
290 // extra_body was not an object — just use sampling params
291 - eprintln!(
292 - "Warning: --extra-body is not a JSON object, sampling params may be lost"
291 + let value_type = match &other {
292 + serde_json::Value::Null => "null",
293 + serde_json::Value::Bool(_) => "boolean",
294 + serde_json::Value::Number(_) => "number",
295 + serde_json::Value::String(_) => "string",
296 + serde_json::Value::Array(_) => "array",
297 + serde_json::Value::Object(_) => unreachable!(),
298 + };
299 + tracing::warn!(
300 + value_type,
301 + "sampling parameters may be lost because --extra-body is not a JSON object"
302 );
294 - let _ = other;
303 sampling_params
304 }
305 None => sampling_params,
@@ -489,9 +497,9 @@ impl BenchConfig {
497 _ => {}
498 }
499 if !args.skip_chat_template {
492 - eprintln!(
493 - "NOTE: client-side chat template rendering is not supported; custom \
494 - dataset prompts are sent raw (equivalent to --skip-chat-template)."
500 + tracing::warn!(
501 + dataset = "custom",
502 + "client-side chat template rendering is unsupported; sending prompts raw"
503 );
504 }
505 }
@@ -570,9 +578,10 @@ impl BenchConfig {
578 }
579
580 if ignore_eos {
573 - eprintln!(
574 - "WARNING: --ignore-eos is set with --multi-turn. The server may not \
575 - respect output length limits, causing unbounded context growth."
581 + tracing::warn!(
582 + ignore_eos,
583 + multi_turn = true,
584 + "output length limits may be ignored, causing unbounded context growth"
585 );
586 }
587
rust/src/bench/src/datasets/hf_dataset.rs
+59 -26
@@ -8,6 +8,7 @@ use rand::seq::SliceRandom;
8 use rand::{Rng, SeedableRng};
9
10 use super::SampleRequest;
11 +use super::progress::RowDownloadReporter;
12 use crate::error::{BenchError, Result};
13 use crate::tokenizer::TokenizerKind;
14
@@ -201,7 +202,12 @@ pub fn download_hf_dataset(
202 (resolved_config, resolved_split)
203 };
204
204 - println!("HF dataset: {dataset} (config={resolved_config}, split={resolved_split})");
205 + tracing::info!(
206 + dataset,
207 + config = resolved_config,
208 + split = resolved_split,
209 + "resolved Hugging Face dataset"
210 + );
211
212 // Check cache
213 let dir = cache_dir();
@@ -215,11 +221,16 @@ pub fn download_hf_dataset(
221
222 if cache_path.exists() {
223 let path_str = cache_path.to_string_lossy().to_string();
218 - println!("HF dataset cached: {path_str}");
224 + tracing::info!(dataset, path = %path_str, "using cached Hugging Face dataset");
225 return Ok((path_str, resolved_config, resolved_split));
226 }
227
222 - println!("Downloading HF dataset '{dataset}' from datasets-server...");
228 + tracing::info!(
229 + dataset,
230 + config = resolved_config,
231 + split = resolved_split,
232 + "downloading Hugging Face dataset"
233 + );
234
235 let encoded_config: String =
236 url::form_urlencoded::byte_serialize(resolved_config.as_bytes()).collect();
@@ -229,6 +240,7 @@ pub fn download_hf_dataset(
240 let mut all_rows: Vec<serde_json::Value> = Vec::new();
241 let mut offset = 0usize;
242 let page_size = 100usize;
243 + let mut progress = RowDownloadReporter::new();
244
245 loop {
246 let url = format!(
@@ -260,14 +272,14 @@ pub fn download_hf_dataset(
272 offset += fetched;
273
274 let total = data["num_rows_total"].as_u64().unwrap_or(0);
263 - eprint!("\r Fetched {offset}/{total} rows...");
275 + progress.update(offset, total);
276
277 // Stop if we have enough rows or reached end of dataset
278 if all_rows.len() >= num_rows_needed || fetched < page_size {
279 break;
280 }
281 }
270 - eprintln!(); // newline after progress
282 + progress.finish();
283
284 if all_rows.is_empty() {
285 return Err(BenchError::Config(format!(
@@ -280,7 +292,12 @@ pub fn download_hf_dataset(
292 std::fs::write(&cache_path, &json_str)?;
293
294 let path_str = cache_path.to_string_lossy().to_string();
283 - println!("HF dataset: {} rows saved to {path_str}", all_rows.len());
295 + tracing::info!(
296 + dataset,
297 + rows = all_rows.len(),
298 + path = %path_str,
299 + "saved Hugging Face dataset"
300 + );
301 Ok((path_str, resolved_config, resolved_split))
302 }
303
@@ -483,21 +500,31 @@ pub fn load_hf_dataset(
500 // Detect column format from first row
501 let format = detect_column_format(&entries[0], text_column_override)?;
502
486 - // Print detected format
503 match &format {
488 - ColumnFormat::Chat(col) => println!("HF dataset: detected chat column '{col}'"),
504 + ColumnFormat::Chat(col) => {
505 + tracing::info!(
506 + format = "chat",
507 + column = col,
508 + "detected Hugging Face dataset format"
509 + );
510 + }
511 ColumnFormat::Text {
512 prompt_col,
513 output_col,
514 } => {
493 - let out_msg = output_col.as_deref().unwrap_or("none");
494 - println!("HF dataset: detected text column '{prompt_col}', output column: {out_msg}");
515 + tracing::info!(
516 + format = "text",
517 + prompt_column = prompt_col,
518 + output_column = output_col.as_deref().unwrap_or("none"),
519 + "detected Hugging Face dataset format"
520 + );
521 }
522 ColumnFormat::Combined { cols, output_col } => {
497 - let out_msg = output_col.as_deref().unwrap_or("none");
498 - println!(
499 - "HF dataset: detected combined columns {:?}, output column: {out_msg}",
500 - cols
523 + tracing::info!(
524 + format = "combined",
525 + prompt_columns = ?cols,
526 + output_column = output_col.as_deref().unwrap_or("none"),
527 + "detected Hugging Face dataset format"
528 );
529 }
530 }
@@ -608,9 +635,10 @@ pub fn load_hf_dataset(
635 if len == 0 { 128 } else { len }
636 } else {
637 if !warned_no_output {
611 - eprintln!(
612 - "WARNING: No output column detected and --hf-output-len not set. \
613 - Using default output length of 128 tokens."
638 + tracing::warn!(
639 + path = dataset_path,
640 + default_output_tokens = 128,
641 + "no dataset output column or --hf-output-len; using default output length"
642 );
643 warned_no_output = true;
644 }
@@ -618,9 +646,10 @@ pub fn load_hf_dataset(
646 }
647 } else {
648 if !warned_no_output {
621 - eprintln!(
622 - "WARNING: No output column detected and --hf-output-len not set. \
623 - Using default output length of 128 tokens."
649 + tracing::warn!(
650 + path = dataset_path,
651 + default_output_tokens = 128,
652 + "no dataset output column or --hf-output-len; using default output length"
653 );
654 warned_no_output = true;
655 }
@@ -640,9 +669,11 @@ pub fn load_hf_dataset(
669 // Oversample if needed
670 if samples.len() < num_requests {
671 if no_oversample {
643 - println!(
644 - "Skipping oversampling. Total samples: {} (requested: {num_requests})",
645 - samples.len()
672 + tracing::info!(
673 + dataset = "hf",
674 + samples = samples.len(),
675 + requested = num_requests,
676 + "skipping dataset oversampling"
677 );
678 } else if !samples.is_empty() {
679 let original_len = samples.len();
@@ -652,9 +683,11 @@ pub fn load_hf_dataset(
683 req.request_id = Some(format!("{request_id_prefix}{}", original_len + i));
684 samples.push(req);
685 }
655 - println!(
656 - "Oversampled HF dataset from {original_len} to {} total samples.",
657 - samples.len()
686 + tracing::info!(
687 + dataset = "hf",
688 + original_samples = original_len,
689 + samples = samples.len(),
690 + "oversampled dataset"
691 );
692 }
693 }
rust/src/bench/src/datasets/mod.rs
+9 -6
@@ -5,6 +5,7 @@ pub mod custom;
5 pub mod hf_dataset;
6 pub mod multi_turn;
7 pub mod prefix_repetition;
8 +mod progress;
9 pub mod random;
10 pub mod random_mm;
11 pub mod random_rerank;
@@ -90,9 +91,10 @@ pub fn oversample_requests(
91 return;
92 }
93 if no_oversample {
93 - println!(
94 - "Skipping oversampling. Total samples: {} (requested: {num_requests})",
95 - requests.len()
94 + tracing::info!(
95 + samples = requests.len(),
96 + requested = num_requests,
97 + "skipping dataset oversampling"
98 );
99 return;
100 }
@@ -103,9 +105,10 @@ pub fn oversample_requests(
105 req.request_id = Some(format!("{request_id_prefix}{}", original_len + i));
106 requests.push(req);
107 }
106 - println!(
107 - "Oversampled requests from {original_len} to {} total samples.",
108 - requests.len()
108 + tracing::info!(
109 + original_samples = original_len,
110 + samples = requests.len(),
111 + "oversampled dataset"
112 );
113 }
114
rust/src/bench/src/datasets/multi_turn.rs
+4 -3
@@ -445,9 +445,10 @@ pub fn load_sharegpt_multi_turn(
445 conv.conversation_id = format!("{request_id_prefix}conv-{}", original_len + i);
446 conversations.push(conv);
447 }
448 - println!(
449 - "Oversampled multi-turn conversations from {original_len} to {} total.",
450 - conversations.len()
448 + tracing::info!(
449 + original_conversations = original_len,
450 + conversations = conversations.len(),
451 + "oversampled multi-turn conversations"
452 );
453 }
454
rust/src/bench/src/datasets/prefix_repetition.rs
+7 -5
@@ -41,11 +41,13 @@ pub fn generate_prefix_repetition_dataset(
41 }
42 let total = prompts_per_prefix * num_prefixes;
43 if total != num_requests {
44 - println!(
45 - "prefix_repetition: generating {total} requests \
46 - ({num_prefixes} prefixes x {prompts_per_prefix} prompts each; \
47 - {} dropped to divide evenly)",
48 - num_requests - total
44 + tracing::info!(
45 + requested = num_requests,
46 + generated = total,
47 + prefixes = num_prefixes,
48 + prompts_per_prefix,
49 + dropped = num_requests - total,
50 + "adjusted prefix-repetition request count"
51 );
52 }
53
rust/src/bench/src/datasets/progress.rs new
+77
@@ -0,0 +1,77 @@
1 +// SPDX-License-Identifier: Apache-2.0
2 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +use std::time::{Duration, Instant};
5 +
6 +use indicatif::{ProgressBar, ProgressStyle};
7 +
8 +const REPORT_INTERVAL: Duration = Duration::from_secs(10);
9 +
10 +/// Reports row download progress to an interactive progress bar, or through
11 +/// periodic tracing events when the progress bar is hidden on a non-TTY.
12 +pub(super) struct RowDownloadReporter {
13 + progress: ProgressBar,
14 + next_report: Instant,
15 +}
16 +
17 +impl RowDownloadReporter {
18 + /// Creates a reporter that emits non-TTY updates every 10 seconds.
19 + pub fn new() -> Self {
20 + let progress = ProgressBar::new(0);
21 + progress.set_style(
22 + ProgressStyle::with_template(
23 + "{spinner:.green} Fetching rows [{bar:30.cyan/blue}] {pos}/{len}",
24 + )
25 + .unwrap()
26 + .progress_chars("#>-"),
27 + );
28 + Self {
29 + progress,
30 + next_report: Instant::now() + REPORT_INTERVAL,
31 + }
32 + }
33 +
34 + /// Updates the current row count and reports progress when due.
35 + pub fn update(&mut self, rows: usize, total: u64) {
36 + let rows = rows as u64;
37 + let total = total.max(rows);
38 + self.progress.set_length(total);
39 + self.progress.set_position(rows);
40 +
41 + if self.should_report(Instant::now()) {
42 + tracing::info!(rows, total, "fetching dataset rows");
43 + }
44 + }
45 +
46 + /// Clears the interactive progress bar after the download completes.
47 + pub fn finish(self) {
48 + self.progress.finish_and_clear();
49 + }
50 +
51 + fn should_report(&mut self, now: Instant) -> bool {
52 + if !self.progress.is_hidden() || now < self.next_report {
53 + return false;
54 + }
55 + self.next_report = now + REPORT_INTERVAL;
56 + true
57 + }
58 +}
59 +
60 +#[cfg(test)]
61 +mod tests {
62 + use super::*;
63 +
64 + #[test]
65 + fn hidden_reporter_uses_ten_second_deadline() {
66 + let start = Instant::now();
67 + let mut reporter = RowDownloadReporter {
68 + progress: ProgressBar::hidden(),
69 + next_report: start + REPORT_INTERVAL,
70 + };
71 +
72 + assert!(!reporter.should_report(start + Duration::from_secs(9)));
73 + assert!(reporter.should_report(start + Duration::from_secs(10)));
74 + assert!(!reporter.should_report(start + Duration::from_secs(19)));
75 + assert!(reporter.should_report(start + Duration::from_secs(20)));
76 + }
77 +}
rust/src/bench/src/datasets/random.rs
+6 -3
@@ -49,9 +49,12 @@ pub fn generate_random_dataset(
49 let (input_low, input_high) = range_ratio.input_bounds(real_input_len);
50 let (output_low, output_high) = range_ratio.output_bounds(output_len);
51 if !range_ratio.is_fixed() {
52 - println!(
53 - "Sampling input_len from [{input_low}, {input_high}] and \
54 - output_len from [{output_low}, {output_high}]"
52 + tracing::info!(
53 + input_low,
54 + input_high,
55 + output_low,
56 + output_high,
57 + "sampling random request lengths"
58 );
59 }
60
rust/src/bench/src/datasets/sharegpt.rs
+15 -9
@@ -23,8 +23,10 @@ const DEFAULT_SHAREGPT_FILE: &str = "ShareGPT_V3_unfiltered_cleaned_split.json";
23 /// Download the default ShareGPT dataset from HuggingFace Hub.
24 /// Uses hf-hub's built-in cache — subsequent calls return the cached path instantly.
25 pub fn download_sharegpt_dataset() -> Result<String> {
26 - println!(
27 - "Downloading ShareGPT dataset from {DEFAULT_SHAREGPT_REPO}/{DEFAULT_SHAREGPT_FILE} ..."
26 + tracing::info!(
27 + repository = DEFAULT_SHAREGPT_REPO,
28 + file = DEFAULT_SHAREGPT_FILE,
29 + "downloading ShareGPT dataset"
30 );
31 let repo = crate::hub::HubRepo::dataset(DEFAULT_SHAREGPT_REPO.to_string());
32 let path = repo.get(DEFAULT_SHAREGPT_FILE).map_err(|e| {
@@ -33,7 +35,7 @@ pub fn download_sharegpt_dataset() -> Result<String> {
35 ))
36 })?;
37 let path_str = path.to_string_lossy().to_string();
36 - println!("ShareGPT dataset ready: {path_str}");
38 + tracing::info!(dataset = "sharegpt", path = %path_str, "dataset is ready");
39 Ok(path_str)
40 }
41
@@ -135,9 +137,11 @@ pub fn load_sharegpt_dataset(
137 // Oversample if dataset is smaller than requested
138 if samples.len() < num_requests {
139 if no_oversample {
138 - println!(
139 - "Skipping oversampling. Total samples: {} (requested: {num_requests})",
140 - samples.len()
140 + tracing::info!(
141 + dataset = "sharegpt",
142 + samples = samples.len(),
143 + requested = num_requests,
144 + "skipping dataset oversampling"
145 );
146 } else if !samples.is_empty() {
147 let needed = num_requests - samples.len();
@@ -147,9 +151,11 @@ pub fn load_sharegpt_dataset(
151 req.request_id = Some(format!("{request_id_prefix}{}", original_len + i));
152 samples.push(req);
153 }
150 - println!(
151 - "Oversampled requests from {original_len} to {} total samples.",
152 - samples.len()
154 + tracing::info!(
155 + dataset = "sharegpt",
156 + original_samples = original_len,
157 + samples = samples.len(),
158 + "oversampled dataset"
159 );
160 }
161 }
rust/src/bench/src/datasets/speed_bench.rs
+22 -16
@@ -8,6 +8,7 @@ use rand::seq::SliceRandom;
8 use rand::{Rng, SeedableRng};
9
10 use super::SampleRequest;
11 +use super::progress::RowDownloadReporter;
12 use crate::cli::SpeedBenchConfig;
13 use crate::error::{BenchError, Result};
14 use crate::tokenizer::TokenizerKind;
@@ -35,11 +36,11 @@ pub fn download_speed_bench(config: SpeedBenchConfig) -> Result<String> {
36 // Return cached file if it exists
37 if cache_path.exists() {
38 let path_str = cache_path.to_string_lossy().to_string();
38 - println!("SPEED-Bench ({config_name}) cached: {path_str}");
39 + tracing::info!(config = config_name, path = %path_str, "using cached SPEED-Bench dataset");
40 return Ok(path_str);
41 }
42
42 - println!("Downloading SPEED-Bench ({config_name}) from HuggingFace datasets-server...");
43 + tracing::info!(config = config_name, "downloading SPEED-Bench dataset");
44
45 let client = reqwest::blocking::Client::builder()
46 .timeout(std::time::Duration::from_secs(120))
@@ -49,6 +50,7 @@ pub fn download_speed_bench(config: SpeedBenchConfig) -> Result<String> {
50 let mut all_rows: Vec<serde_json::Value> = Vec::new();
51 let mut offset = 0usize;
52 let page_size = 100usize;
53 + let mut progress = RowDownloadReporter::new();
54
55 loop {
56 let url = format!(
@@ -116,15 +118,14 @@ pub fn download_speed_bench(config: SpeedBenchConfig) -> Result<String> {
118 let fetched = rows.len();
119 offset += fetched;
120
119 - // Print progress
121 let total = data["num_rows_total"].as_u64().unwrap_or(0);
121 - eprint!("\r Fetched {offset}/{total} rows...");
122 + progress.update(offset, total);
123
124 if fetched < page_size {
125 break;
126 }
127 }
127 - eprintln!(); // newline after progress
128 + progress.finish();
129
130 if all_rows.is_empty() {
131 return Err(BenchError::Config(
@@ -137,9 +138,11 @@ pub fn download_speed_bench(config: SpeedBenchConfig) -> Result<String> {
138 std::fs::write(&cache_path, &json_str)?;
139
140 let path_str = cache_path.to_string_lossy().to_string();
140 - println!(
141 - "SPEED-Bench ({config_name}): {} rows saved to {path_str}",
142 - all_rows.len()
141 + tracing::info!(
142 + config = config_name,
143 + rows = all_rows.len(),
144 + path = %path_str,
145 + "saved SPEED-Bench dataset"
146 );
147 Ok(path_str)
148 }
@@ -263,9 +266,11 @@ pub fn load_speed_bench_dataset(
266 // Oversample if needed
267 if samples.len() < num_requests {
268 if no_oversample {
266 - println!(
267 - "Skipping oversampling. Total samples: {} (requested: {num_requests})",
268 - samples.len()
269 + tracing::info!(
270 + dataset = "speed-bench",
271 + samples = samples.len(),
272 + requested = num_requests,
273 + "skipping dataset oversampling"
274 );
275 } else if !samples.is_empty() {
276 let original_len = samples.len();
@@ -275,9 +280,11 @@ pub fn load_speed_bench_dataset(
280 req.request_id = Some(format!("{request_id_prefix}{}", original_len + i));
281 samples.push(req);
282 }
278 - println!(
279 - "Oversampled SPEED-Bench from {original_len} to {} total samples.",
280 - samples.len()
283 + tracing::info!(
284 + dataset = "speed-bench",
285 + original_samples = original_len,
286 + samples = samples.len(),
287 + "oversampled dataset"
288 );
289 }
290 }
@@ -288,7 +295,6 @@ pub fn load_speed_bench_dataset(
295 ));
296 }
297
291 - // Print category distribution
298 let mut cat_counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
299 for entry in &filtered[..filtered.len().min(samples.len())] {
300 let cat = entry.get("category").and_then(|c| c.as_str()).unwrap_or("unknown");
@@ -297,7 +303,7 @@ pub fn load_speed_bench_dataset(
303 let mut cats: Vec<_> = cat_counts.into_iter().collect();
304 cats.sort_by_key(|b| std::cmp::Reverse(b.1));
305 let cat_str: Vec<String> = cats.iter().map(|(k, v)| format!("{k}:{v}")).collect();
300 - println!("SPEED-Bench categories: {}", cat_str.join(", "));
306 + tracing::info!(categories = %cat_str.join(", "), "computed SPEED-Bench category distribution");
307
308 Ok(samples)
309 }
rust/src/bench/src/lib.rs
+1 -1
@@ -33,7 +33,7 @@ pub fn prepare_process() {
33 if let Ok(new) = rlimit::increase_nofile_limit(u64::MAX)
34 && new > 1024
35 {
36 - eprintln!("Open-file limit: {new}");
36 + tracing::info!(soft_limit = new, "raised open-file limit");
37 }
38 }
39
rust/src/bench/src/main.rs
+12
@@ -19,7 +19,19 @@ struct Cli {
19 args: vllm_bench::BenchServeArgs,
20 }
21
22 +// TODO: unify the tracing subscriber used by different binaries.
23 +fn init_tracing() {
24 + let filter = tracing_subscriber::EnvFilter::try_from_default_env()
25 + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
26 + let _ = tracing_subscriber::fmt()
27 + .with_env_filter(filter)
28 + .with_writer(std::io::stderr)
29 + .try_init();
30 +}
31 +
32 fn main() -> anyhow::Result<()> {
33 + init_tracing();
34 +
35 let cli = Cli::parse();
36 vllm_bench::prepare_process();
37
rust/src/bench/src/metrics/calculator.rs
+18 -16
@@ -7,6 +7,22 @@ use crate::datasets::SampleRequest;
7 use crate::metrics::{BenchmarkMetrics, MultiTurnMetrics};
8 use crate::multi_turn::ConversationOutput;
9
10 +fn log_failed_requests(outputs: &[RequestFuncOutput]) {
11 + let failed_outputs: Vec<_> = outputs.iter().filter(|output| !output.success).collect();
12 + if failed_outputs.is_empty() {
13 + return;
14 + }
15 +
16 + tracing::warn!(
17 + failed_requests = failed_outputs.len(),
18 + displayed_errors = failed_outputs.len().min(10),
19 + "benchmark requests failed"
20 + );
21 + for (index, output) in failed_outputs.into_iter().take(10).enumerate() {
22 + tracing::warn!(index, error = %output.error, "benchmark request failed");
23 + }
24 +}
25 +
26 /// Calculate benchmark metrics from request outputs.
27 ///
28 /// Mirrors Python's `calculate_metrics()` from serve.py:392-599.
@@ -63,14 +79,7 @@ pub fn calculate_metrics(
79
80 let failed = outputs.len() - completed;
81
66 - // Print failed request errors (capped to 10)
67 - let failed_outputs: Vec<&RequestFuncOutput> = outputs.iter().filter(|o| !o.success).collect();
68 - if !failed_outputs.is_empty() {
69 - eprintln!("Failed requests during benchmark run detected (capping to 10):");
70 - for (i, err) in failed_outputs.iter().take(10).enumerate() {
71 - eprintln!("Error {i}: {}", err.error);
72 - }
73 - }
82 + log_failed_requests(outputs);
83
84 // Calculate max output tokens per second and max concurrent requests
85 let mut max_output_tokens_per_s = 0.0_f64;
@@ -295,14 +304,7 @@ pub fn calculate_embedding_metrics(
304
305 let failed = outputs.len() - completed;
306
298 - // Print failed request errors (capped to 10)
299 - let failed_outputs: Vec<&RequestFuncOutput> = outputs.iter().filter(|o| !o.success).collect();
300 - if !failed_outputs.is_empty() {
301 - eprintln!("Failed requests during benchmark run detected (capping to 10):");
302 - for (i, err) in failed_outputs.iter().take(10).enumerate() {
303 - eprintln!("Error {i}: {}", err.error);
304 - }
305 - }
307 + log_failed_requests(outputs);
308
309 // Compute peak concurrent requests from start_time + latency windows
310 let successful_outputs: Vec<&RequestFuncOutput> =
rust/src/bench/src/multi_turn.rs
+50 -37
@@ -6,6 +6,7 @@ use std::sync::Arc;
6 use std::time::Instant;
7
8 use indicatif::{ProgressBar, ProgressStyle};
9 +use thiserror_ext::AsReport as _;
10 use tokio::sync::Semaphore;
11
12 use crate::backends::{Backend, RequestFuncInput, RequestFuncOutput, get_backend};
@@ -72,9 +73,13 @@ pub async fn run_multi_turn_benchmark(config: &BenchConfig) -> Result<serde_json
73 let (model_id, model_name) = if let Some(ref m) = config.model {
74 (m.clone(), config.model_name.clone())
75 } else {
75 - println!("Model not specified, fetching first model from server...");
76 + tracing::info!(base_url = %config.base_url, "fetching first model from server");
77 let (name, id) = get_first_model(&config.base_url, &client, &config.extra_headers).await?;
77 - println!("First model name: {name}, first model id: {id}");
78 + tracing::info!(
79 + model_name = name,
80 + model_id = id,
81 + "selected first model from server"
82 + );
83 (id, Some(name))
84 };
85
@@ -83,15 +88,18 @@ pub async fn run_multi_turn_benchmark(config: &BenchConfig) -> Result<serde_json
88 None
89 } else {
90 let tid = config.tokenizer_id.as_deref().unwrap_or(&model_id);
86 - println!("Loading tokenizer: {tid}");
91 + tracing::info!(tokenizer = tid, "loading tokenizer");
92 let server_info = Some((config.base_url.as_str(), model_id.as_str()));
93 let t = crate::tokenizer::load_tokenizer(tid, config.trust_remote_code, server_info)?;
89 - println!("Tokenizer loaded successfully.");
94 Some(t)
95 };
96
97 // Generate/load conversations
94 - println!("Generating multi-turn conversations...");
98 + tracing::info!(
99 + dataset = ?config.dataset_name,
100 + conversations = config.num_prompts,
101 + "generating multi-turn conversations"
102 + );
103 let gen_start = Instant::now();
104
105 let mut conversations = match config.dataset_name {
@@ -179,8 +187,11 @@ pub async fn run_multi_turn_benchmark(config: &BenchConfig) -> Result<serde_json
187 let (filtered_conversations, filtered_turns) =
188 filter_turns_by_max_model_len(&mut conversations, max_model_len, no_history);
189 if filtered_turns > 0 || filtered_conversations > 0 {
182 - println!(
183 - "Filtered {filtered_turns} turn(s) and {filtered_conversations} conversation(s) above --max-model-len {max_model_len}."
190 + tracing::info!(
191 + filtered_turns,
192 + filtered_conversations,
193 + max_model_len,
194 + "filtered conversations above maximum model length"
195 );
196 }
197 if conversations.is_empty() {
@@ -192,11 +203,11 @@ pub async fn run_multi_turn_benchmark(config: &BenchConfig) -> Result<serde_json
203
204 let gen_elapsed = gen_start.elapsed();
205 let total_turns: usize = conversations.iter().map(|c| c.turns.len()).sum();
195 - println!(
196 - "Generated {} conversations ({} total turns) in {:.2}s",
197 - conversations.len(),
206 + tracing::info!(
207 + conversations = conversations.len(),
208 total_turns,
199 - gen_elapsed.as_secs_f64()
209 + elapsed_seconds = gen_elapsed.as_secs_f64(),
210 + "generated multi-turn conversations"
211 );
212
213 // Log prefix sharing info
@@ -208,18 +219,15 @@ pub async fn run_multi_turn_benchmark(config: &BenchConfig) -> Result<serde_json
219 let conv_tokens =
220 (real_input_len as f64 * config.multi_turn_prefix_conversation_ratio).floor() as usize;
221 let unique_tokens = real_input_len.saturating_sub(global_tokens + conv_tokens);
211 - println!(
212 - "User message prefix sharing: {:.0}% global ({} tokens), {:.0}% per-conversation ({} tokens), {:.0}% unique ({} tokens)",
213 - config.multi_turn_prefix_global_ratio * 100.0,
222 + tracing::info!(
223 + global_ratio = config.multi_turn_prefix_global_ratio,
224 global_tokens,
215 - config.multi_turn_prefix_conversation_ratio * 100.0,
216 - conv_tokens,
217 - (1.0 - config.multi_turn_prefix_global_ratio
218 - - config.multi_turn_prefix_conversation_ratio)
219 - * 100.0,
225 + conversation_ratio = config.multi_turn_prefix_conversation_ratio,
226 + conversation_tokens = conv_tokens,
227 unique_tokens,
228 + history_accumulation = false,
229 + "configured multi-turn prefix sharing"
230 );
222 - println!("No history accumulation: each turn sends fixed-length prompt only.");
231 }
232
233 if config.dry_run {
@@ -253,7 +261,7 @@ pub async fn run_multi_turn_benchmark(config: &BenchConfig) -> Result<serde_json
261 ..Default::default()
262 };
263
256 - println!("Starting initial single prompt test run...");
264 + tracing::info!("starting initial single-prompt test run");
265 let test_output = crate::ready_checker::wait_for_endpoint(
266 config.backend,
267 &client,
@@ -268,7 +276,7 @@ pub async fn run_multi_turn_benchmark(config: &BenchConfig) -> Result<serde_json
276 test_output.error
277 )));
278 }
271 - println!("Initial test run completed.");
279 + tracing::info!("initial single-prompt test run completed");
280 }
281
282 // For random datasets in multi-turn mode, auto-set min_tokens to enforce
@@ -283,9 +291,10 @@ pub async fn run_multi_turn_benchmark(config: &BenchConfig) -> Result<serde_json
291 "min_tokens".to_string(),
292 serde_json::json!(config.random_output_len),
293 );
286 - println!(
287 - "Auto-setting min_tokens={} for multi-turn random dataset (use --extra-body to override)",
288 - config.random_output_len
294 + tracing::info!(
295 + min_tokens = config.random_output_len,
296 + dataset = "random",
297 + "set minimum output tokens for multi-turn dataset"
298 );
299 }
300 Some(body)
@@ -297,7 +306,7 @@ pub async fn run_multi_turn_benchmark(config: &BenchConfig) -> Result<serde_json
306 let spec_decode_before =
307 fetch_spec_decode_metrics(&config.base_url, &client, &config.extra_headers).await;
308 if spec_decode_before.is_some() {
300 - println!("Speculative decoding detected, will collect metrics.");
309 + tracing::info!("detected speculative decoding; collecting metrics");
310 }
311
312 // Start profiler if requested (immediate mode — no batch threshold)
@@ -330,10 +339,13 @@ pub async fn run_multi_turn_benchmark(config: &BenchConfig) -> Result<serde_json
339 };
340
341 // Main benchmark
333 - println!("Starting multi-turn benchmark...");
334 - println!("Conversations: {}", conversations.len());
335 - println!("Concurrency: {concurrency}");
336 - println!("Inter-turn delay: {} ms", config.multi_turn_delay_ms);
342 + tracing::info!(
343 + conversations = conversations.len(),
344 + total_turns,
345 + concurrency,
346 + inter_turn_delay_ms = config.multi_turn_delay_ms,
347 + "starting multi-turn benchmark"
348 + );
349
350 let max_turn_count = conversations.iter().map(|c| c.turns.len()).max().unwrap_or(0);
351
@@ -364,11 +376,12 @@ pub async fn run_multi_turn_benchmark(config: &BenchConfig) -> Result<serde_json
376 );
377 if let Some(modules) = config.lora_modules.as_ref() {
378 let names: Vec<&str> = modules.iter().map(|s| s.as_ref()).collect();
367 - println!(
368 - "LoRA adapters ({}): {:?} [assignment={:?}, scope=conversation]",
369 - modules.len(),
370 - names,
371 - config.lora_assignment
379 + tracing::info!(
380 + adapters = modules.len(),
381 + names = ?names,
382 + assignment = ?config.lora_assignment,
383 + scope = "conversation",
384 + "assigned LoRA adapters"
385 );
386 }
387
@@ -433,7 +446,7 @@ pub async fn run_multi_turn_benchmark(config: &BenchConfig) -> Result<serde_json
446 match handle.await {
447 Ok(output) => all_outputs.push(output),
448 Err(e) => {
436 - eprintln!("Conversation task panicked: {e}");
449 + tracing::error!(error = %e.as_report(), "conversation task panicked");
450 }
451 }
452 }
@@ -453,7 +466,7 @@ pub async fn run_multi_turn_benchmark(config: &BenchConfig) -> Result<serde_json
466 if let Some((cancel_tx, task)) = profile_task {
467 let _ = cancel_tx.send(());
468 if let Err(e) = task.await {
456 - eprintln!("WARNING: Profile background task failed: {e}");
469 + tracing::error!(error = %e.as_report(), "profiler background task failed");
470 }
471 }
472
rust/src/bench/src/output/json.rs
+2 -2
@@ -603,7 +603,7 @@ fn add_metric_stats(
603 pub fn save_result(json: &Value, file_path: &str) -> Result<()> {
604 let content = serde_json::to_string(json)?;
605 std::fs::write(file_path, content)?;
606 - println!("Results saved to {file_path}");
606 + tracing::info!(path = file_path, "saved benchmark results");
607 Ok(())
608 }
609
@@ -618,7 +618,7 @@ pub fn append_result(json: &Value, file_path: &str) -> Result<()> {
618 file.write_all(b"\n")?;
619 }
620 file.write_all(content.as_bytes())?;
621 - println!("Results appended to {file_path}");
621 + tracing::info!(path = file_path, "appended benchmark results");
622 Ok(())
623 }
624
rust/src/bench/src/ready_checker.rs
+8 -2
@@ -23,7 +23,11 @@ pub async fn wait_for_endpoint(
23 let backend = get_backend(backend)?;
24 let deadline = Instant::now() + std::time::Duration::from_secs(timeout_seconds);
25
26 - println!("Waiting for endpoint to become up in {timeout_seconds}s");
26 + tracing::info!(
27 + timeout_seconds,
28 + retry_interval,
29 + "waiting for endpoint readiness"
30 + );
31
32 let pb = ProgressBar::new(timeout_seconds);
33 pb.set_style(
@@ -53,7 +57,9 @@ pub async fn wait_for_endpoint(
57 Ok(output) => {
58 let err = output.error.clone();
59 let err_last_line = err.lines().last().unwrap_or(&err);
56 - eprintln!("Endpoint is not ready. Error='{err_last_line}'");
60 + pb.suspend(|| {
61 + tracing::warn!(error = err_last_line, "endpoint is not ready");
62 + });
63 last_error = err;
64 }
65 Err(e) => {
rust/src/bench/src/sweep.rs
+1 -1
@@ -16,7 +16,7 @@ async fn reset_prefix_cache(base_url: &str) -> Result<()> {
16 .await
17 .map_err(|e| BenchError::Backend(format!("Failed to reset prefix cache: {e}")))?;
18 if resp.status().is_success() {
19 - println!("Prefix cache reset successfully.");
19 + tracing::info!(url = %url, "reset prefix cache");
20 } else {
21 let status = resp.status();
22 let body = resp.text().await.unwrap_or_default();
rust/src/bench/src/tiktoken.rs
+15 -9
@@ -199,7 +199,12 @@ pub fn load_builtin_tiktoken(encoding: &str) -> Result<TiktokenTokenizer> {
199 }
200 };
201 let bpe = bpe.map_err(|e| BenchError::Tokenizer(format!("Failed to load {encoding}: {e}")))?;
202 - println!("Tokenizer: Built-in tiktoken {encoding} (vocab_size={vocab_size})");
202 + tracing::info!(
203 + encoding,
204 + kind = "built-in-tiktoken",
205 + vocab_size,
206 + "loaded tokenizer"
207 + );
208 Ok(TiktokenTokenizer::from_builtin_bpe(bpe, vocab_size))
209 }
210
@@ -309,15 +314,16 @@ fn build_tiktoken(
314 }
315 }
316
312 - println!(
313 - "Loading tiktoken model for '{model_id}' (base={}, special={}, pat={})...",
314 - num_base_tokens,
315 - all_special_tokens.len(),
316 - if pattern.is_some() {
317 + tracing::info!(
318 + model = model_id,
319 + base_tokens = num_base_tokens,
320 + special_tokens = all_special_tokens.len(),
321 + pattern = if pattern.is_some() {
322 "custom"
323 } else {
324 "default"
325 },
326 + "loading tiktoken model"
327 );
328
329 TiktokenTokenizer::from_file(
@@ -438,9 +444,9 @@ fn extract_pat_str_from_source(source: &str) -> Option<String> {
444
445 if !fragments.is_empty() {
446 let pattern = fragments.join("|");
441 - println!(
442 - "Extracted pat_str from Python source: {} fragments",
443 - fragments.len()
447 + tracing::debug!(
448 + fragments = fragments.len(),
449 + "extracted tiktoken pattern from Python source"
450 );
451 return Some(pattern);
452 }
rust/src/bench/src/tokenizer.rs
+27 -9
@@ -4,6 +4,7 @@
4 use std::collections::HashSet;
5 use std::path::Path;
6
7 +use thiserror_ext::AsReport as _;
8 use tokenizers::Tokenizer;
9
10 use crate::error::{BenchError, Result};
@@ -214,29 +215,46 @@ pub fn load_tokenizer(
215 // 1. Try local HuggingFace tokenizer (tokenizer.json)
216 match try_load_local(model_id) {
217 Ok(tok) => {
217 - println!("Tokenizer: Local (vocab_size={})", tok.get_vocab_size(true));
218 + tracing::info!(
219 + model = model_id,
220 + kind = "local",
221 + vocab_size = tok.get_vocab_size(true),
222 + "loaded tokenizer"
223 + );
224 Ok(TokenizerKind::Local(Box::new(tok)))
225 }
226 Err(local_err) => {
227 // 2. Try tiktoken format
222 - println!("No tokenizer.json for '{model_id}', trying tiktoken format...");
228 + tracing::info!(
229 + model = model_id,
230 + error = %local_err.as_report(),
231 + "local tokenizer unavailable; trying tiktoken"
232 + );
233 match crate::tiktoken::try_load_tiktoken(model_id) {
234 Ok(tok) => {
225 - println!("Tokenizer: Tiktoken (vocab_size={})", tok.vocab_size());
235 + tracing::info!(
236 + model = model_id,
237 + kind = "tiktoken",
238 + vocab_size = tok.vocab_size(),
239 + "loaded tokenizer"
240 + );
241 Ok(TokenizerKind::Tiktoken(tok))
242 }
243 Err(tiktoken_err) => {
244 // 3. Try server-side fallback
245 if let Some((base_url, model)) = server_info {
231 - println!(
232 - "Tiktoken also not available ({tiktoken_err}), \
233 - trying server-side tokenization..."
246 + tracing::info!(
247 + model = model_id,
248 + error = %tiktoken_err.as_report(),
249 + "tiktoken unavailable; trying server-side tokenization"
250 );
251 match ServerTokenizer::new(base_url, model) {
252 Ok(srv) => {
237 - println!(
238 - "Tokenizer: Server (vocab_size≈{})",
239 - srv.cached_vocab_size
253 + tracing::info!(
254 + model = model_id,
255 + kind = "server",
256 + vocab_size = srv.cached_vocab_size,
257 + "loaded tokenizer"
258 );
259 return Ok(TokenizerKind::Server(srv));
260 }
rust/src/cmd/src/logging.rs
+17 -7
@@ -26,18 +26,21 @@ const RESET: &str = "\x1b[0m";
26 const VLLM_TIME_FORMAT: &[time::format_description::FormatItem<'static>] =
27 format_description!("[month]-[day] [hour]:[minute]:[second]");
28
29 -const PROCESS_LABEL: &str = "RustFrontend";
30 -
29 /// Install the process-wide vLLM-style tracing subscriber for the CLI binary.
32 -pub(crate) fn init_tracing() {
30 +pub(crate) fn init_tracing(process_label: &str) {
31 let filter = build_targets_filter(
32 env::var("VLLM_LOGGING_LEVEL").ok().as_deref(),
33 env::var("RUST_LOG").ok().as_deref(),
34 );
37 - let formatter = VllmEventFormatter::new();
35 + let formatter = VllmEventFormatter::new(process_label);
36
37 let _ = tracing_subscriber::registry()
40 - .with(tracing_subscriber::fmt::layer().event_format(formatter).with_filter(filter))
38 + .with(
39 + tracing_subscriber::fmt::layer()
40 + .event_format(formatter)
41 + .with_writer(std::io::stderr)
42 + .with_filter(filter),
43 + )
44 .try_init();
45 }
46
@@ -94,9 +97,9 @@ struct VllmEventFormatter {
97 }
98
99 impl VllmEventFormatter {
97 - fn new() -> Self {
100 + fn new(process_label: &str) -> Self {
101 Self {
99 - prefix: format!("({} pid={})", PROCESS_LABEL, process::id()),
102 + prefix: format!("({process_label} pid={})", process::id()),
103 timer: VllmLocalTimer::default(),
104 }
105 }
@@ -291,6 +294,13 @@ fn map_python_log_level(level: &str) -> LevelFilter {
294 mod tests {
295 use super::*;
296
297 + #[test]
298 + fn formatter_prefix_uses_process_label() {
299 + let formatter = VllmEventFormatter::new("Bench");
300 +
301 + assert_eq!(formatter.prefix, format!("(Bench pid={})", process::id()));
302 + }
303 +
304 #[test]
305 fn rust_log_target_overrides_are_merged_with_vllm_default_level() {
306 let filter = build_targets_filter(Some("DEBUG"), Some("hyper=warn,tower=error"));
rust/src/cmd/src/main.rs
+9 -1
@@ -5,6 +5,7 @@ mod cli;
5 mod logging;
6
7 use std::env;
8 +use std::ffi::OsStr;
9 use std::process::ExitStatus;
10
11 use anyhow::{Context, Result, anyhow, bail};
@@ -82,7 +83,14 @@ fn shutdown_signal() -> CancellationToken {
83 }
84
85 fn main() -> Result<()> {
85 - logging::init_tracing();
86 + let process_label =
87 + match env::args_os().nth(1).as_deref().and_then(OsStr::to_str).unwrap_or_default() {
88 + "bench" => "Bench",
89 + "serve" | "frontend" => "RustFrontend",
90 + _ => "Rust",
91 + };
92 + logging::init_tracing(process_label);
93 +
94 let cli = Cli::parse();
95
96 let mut runtime = tokio::runtime::Builder::new_multi_thread();