Skip to content

Commit ce9d69a

Browse files
authored
benchmark: add response_provider benchmark for Agentic API vs vLLM (vllm-project#185)
## Summary Adds a new `response_provider` benchmark (`crates/agentic-server/benches/client/response_provider/`) that compares Agentic API (WebSocket, HTTP/SSE, HTTP/JSON) against direct vLLM across three workloads: transport (repeated tool-loop rounds), tool-call (BFCL function selection), and history-rehydration (multi-turn conversation state). Goal is to measure what actually differentiates the gateway from direct vLLM, not just raw inference latency, which is identical either way since both hit the same model. - **Wire-byte instrumentation**: `TurnResult`/`ProviderSummary` track `request_bytes` / `response_bytes` for every transport. This is the direct measurement of the state-scaling finding below (flat request size for the gateway vs growing for direct vLLM). - **`--depths 5,10,25` (history-rehydration)**: each depth runs as its own independent batch of sessions instead of bucketing one long run after the fact, so sample counts stay even across depths instead of thinning out at the tail. Produces `depth_summary.md`/`.json`. - **Reporting**: a standing correctness-check disclaimer on accuracy metrics (a gap is a bug signal, not a competition result), and a per-BFCL-case pass/fail matrix instead of a single aggregate tool-call accuracy number. - Defaults: 5 concurrent sessions, 50 requests/session. (A separate, non-repo orchestration script at the workspace level runs this benchmark across all providers/workloads with fresh vLLM/gateway restarts between each; not part of this PR's diff.) <details> <summary><h3>📊 Benchmark findings (click to expand)</h3></summary> Model: `Qwen/Qwen3.5-35B-A3B-FP8`. Full methodology and caveats in [`benches/client/response_provider/README.md`](crates/agentic-server/benches/client/response_provider/README.md). More benchmark detail and discussion: [design/results doc](https://docs.google.com/document/d/1zJUfxdloxu9_oBYfbfTtgbz5sSZxwAnqvlnt72T67oU/edit?tab=t.2qh4vijnc2n1#heading=h.u692ockfn047). **Headline: state-scaling.** At 25 turns of conversation, Agentic API sends a constant ~450-480 byte request regardless of history length. Direct vLLM sends ~84.7 KB for the same turn (99.4% more), because the client has to reconstruct and resend the whole transcript itself. Over the full 25-turn batch, cumulative client-sent bytes were 44.8 KB (Agentic API) vs 12.1 MB (direct vLLM), a 99.6% reduction. (Full byte-vs-depth table in the README; not charted here.) **Latency crossover with depth.** Direct vLLM is actually *faster* at shallow depth (5 turns: 9.6s vs 12.5-13.3s for Agentic API), since the extra gateway hop costs something before there's much history to save. By depth 25 that inverts: direct vLLM is *slowest* (35.7s) while Agentic API WS is fastest (23.5s). The resend penalty compounds with depth; the gateway-hop cost doesn't. ```mermaid --- config: theme: base themeVariables: xyChart: plotColorPalette: "#2a78d6, #eb6834, #1baf7a" xyChart: width: 700 height: 420 --- xychart-beta title "History-rehydration: p50 end-to-end latency (ms) vs turn depth" x-axis "Turn depth" [5, 10, 25] y-axis "p50 end-to-end latency (ms)" 0 --> 40000 line [13315, 13289, 23478] line [12507, 12605, 30768] line [9564, 14079, 35663] ``` Lines in plot order (blue, orange, green): Agentic API (WS), Agentic API (HTTP/SSE), direct vLLM. **Transport latency and tool-call accuracy are parity results, and that's the correct outcome.** Both arms hit the same underlying model, so raw inference latency and tool-selection accuracy should be identical, not a competition: ```mermaid --- config: theme: base themeVariables: xyChart: plotColorPalette: "#2a78d6, #eb6834, #1baf7a" xyChart: width: 700 height: 420 --- xychart-beta title "Transport workload: p50 end-to-end latency by provider (ms)" x-axis ["Agentic API (WS): 8500", "Agentic API (HTTP): 8513", "Direct vLLM: 8515.5"] y-axis "p50 end-to-end latency (ms)" 0 --> 10000 bar [8500, 0, 0] bar [0, 8513, 0] bar [0, 0, 8515.5] ``` ```mermaid --- config: theme: base themeVariables: xyChart: plotColorPalette: "#2a78d6, #eb6834, #1baf7a" xyChart: width: 700 height: 420 --- xychart-beta title "BFCL tool-call accuracy by provider (%)" x-axis ["Agentic API (WS): 81.6%", "Agentic API (HTTP): 82.0%", "Direct vLLM: 80.8%"] y-axis "Tool-call accuracy (%)" 0 --> 100 bar [81.6, 0, 0] bar [0, 82.0, 0] bar [0, 0, 80.8] ``` All three land within 16ms / a 1.2-point band of each other on both metrics: the gateway adds no measurable inference overhead and doesn't change what the model selects. Even so, a single transport turn already shows the byte gap that compounds into the state-scaling story above: direct vLLM resends the growing tool-call history on every round of the *same* 4-round turn, so its per-turn request payload (12,179 B p50) is already ~3.3x larger than either Agentic API transport (3,698 / 3,573 B), despite the identical latency (see README for the full bytes table). </details> ## Test Plan - `cargo fmt -- --check`, `cargo check --package agentic-server --bench response_provider`, `cargo clippy --package agentic-server --bench response_provider -- -D warnings` all pass. - `--print-prompts` smoke tests for `--depths` (valid list, `--depths` on a non-history-rehydration workload correctly rejected, `--depths 0,...` correctly rejected). - Full `./scripts/run_all_benchmarks.sh` executed end to end against all 3 providers x 3 workloads (9 provider/workload pairs, all completed successfully); results above are from that run. Signed-off-by: maral <maralbahari.98@gmail.com>
1 parent 04ec1a0 commit ce9d69a

9 files changed

Lines changed: 3283 additions & 1 deletion

File tree

crates/agentic-server/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ url.workspace = true
3232
bytes.workspace = true
3333
criterion.workspace = true
3434
futures.workspace = true
35-
reqwest = { workspace = true, features = ["json"] }
35+
reqwest = { workspace = true, features = ["json", "stream"] }
3636
rand = "0.8"
3737
rsa = "0.9"
3838
serde_json.workspace = true
@@ -46,6 +46,11 @@ uuid = { version = "1", features = ["v7"] }
4646
name = "benches"
4747
harness = false
4848

49+
[[bench]]
50+
name = "response_provider"
51+
path = "benches/client/response_provider/main.rs"
52+
harness = false
53+
4954
[lints]
5055
workspace = true
5156

crates/agentic-server/benches/client/response_provider/README.md

Lines changed: 302 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use std::path::PathBuf;
2+
3+
use clap::{Parser, ValueEnum};
4+
5+
use crate::types::Workload;
6+
7+
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
8+
pub enum ProviderSelection {
9+
/// Gateway WebSocket, HTTP/SSE, and JSON plus direct vLLM HTTP/SSE and JSON.
10+
All,
11+
/// Gateway WebSocket and direct vLLM HTTP/SSE (backwards-compatible pair).
12+
Both,
13+
AgenticApi,
14+
AgenticApiHttp,
15+
AgenticApiJson,
16+
Vllm,
17+
VllmJson,
18+
}
19+
20+
#[derive(Debug, Parser)]
21+
#[command(
22+
name = "agentic-responses-benchmark",
23+
about = "Run concurrent Responses workloads against Agentic API and direct vLLM"
24+
)]
25+
pub struct Cli {
26+
/// Model slug exposed by each selected provider.
27+
#[arg(long, env = "RESPONSE_PROVIDER_BENCH_MODEL")]
28+
pub model: String,
29+
30+
/// Capability measured by this run.
31+
#[arg(long, value_enum, default_value_t = Workload::HistoryRehydration)]
32+
pub workload: Workload,
33+
34+
/// Number of concurrent sessions per selected provider.
35+
#[arg(long, default_value_t = 5)]
36+
pub sessions: usize,
37+
38+
/// Sequential turns in each session. Defaults depend on `--workload`.
39+
#[arg(long, short = 'n')]
40+
pub requests_per_session: Option<usize>,
41+
42+
/// Fixed session depths (history-rehydration only), each run as its own independent batch of
43+
/// `--sessions` sessions instead of bucketing one long run after the fact. Overrides
44+
/// `--requests-per-session` when set. Example: --depths 1,5,10,25,50,100
45+
#[arg(long, value_delimiter = ',')]
46+
pub depths: Vec<usize>,
47+
48+
/// Responses created inside each transport workload turn.
49+
#[arg(long, default_value_t = 4)]
50+
pub transport_rounds: usize,
51+
52+
/// BFCL question JSONL for the tool-call workload.
53+
#[arg(long, requires = "dataset_answers")]
54+
pub dataset_questions: Option<PathBuf>,
55+
56+
/// BFCL possible-answer JSONL paired by case ID.
57+
#[arg(long, requires = "dataset_questions")]
58+
pub dataset_answers: Option<PathBuf>,
59+
60+
/// Zero-based BFCL case offset before selecting deterministic cases.
61+
#[arg(long, default_value_t = 0)]
62+
pub dataset_offset: usize,
63+
64+
/// Providers to benchmark. `all` includes both gateway transports.
65+
#[arg(long, value_enum, default_value_t = ProviderSelection::Both)]
66+
pub provider: ProviderSelection,
67+
68+
/// Agentic API OpenAI-compatible base URL.
69+
#[arg(long, default_value = "http://localhost:9000/v1")]
70+
pub agentic_url: String,
71+
72+
/// Direct vLLM OpenAI-compatible base URL.
73+
#[arg(long, default_value = "http://localhost:5050/v1")]
74+
pub vllm_url: String,
75+
76+
/// Timeout for one logical turn, including every model/tool round.
77+
#[arg(long, default_value_t = 300)]
78+
pub timeout_seconds: u64,
79+
80+
/// Seed for deterministic generated prompts and continuation secrets.
81+
#[arg(long, default_value_t = 2_026_081_7)]
82+
pub seed: u64,
83+
84+
/// Result directory. Defaults to target/response-provider-benchmark/<unix-milliseconds>.
85+
#[arg(long)]
86+
pub output_dir: Option<PathBuf>,
87+
88+
/// Stream timestamped, provider-tagged Responses events to stdout.
89+
#[arg(long)]
90+
pub live_jsonl: bool,
91+
92+
/// Validate inputs, print generated prompt specifications as JSONL, and exit.
93+
#[arg(long)]
94+
pub print_prompts: bool,
95+
96+
/// Automatically appended by `cargo bench` for custom harnesses.
97+
#[arg(long, hide = true)]
98+
pub bench: bool,
99+
}
100+
101+
impl Cli {
102+
#[must_use]
103+
pub fn requests_per_session(&self) -> usize {
104+
self.requests_per_session
105+
.unwrap_or_else(|| self.workload.default_requests())
106+
}
107+
}

0 commit comments

Comments
 (0)