Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions MANIFEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ repository.
| apple-native-keyring-store | 1.0.0 | MIT OR Apache-2.0 |
| approx | 0.5.1 | Apache-2.0 |
| as-any | 0.3.2 | MIT OR Apache-2.0 |
| assert-json-diff | 2.0.2 | MIT |
| async-broadcast | 0.7.2 | MIT OR Apache-2.0 |
| async-channel | 2.5.0 | Apache-2.0 OR MIT |
| async-executor | 1.14.0 | Apache-2.0 OR MIT |
Expand Down Expand Up @@ -126,6 +127,8 @@ repository.
| darling_core | 0.23.0 | MIT |
| darling_macro | 0.23.0 | MIT |
| data-encoding | 2.11.0 | MIT |
| deadpool | 0.12.3 | MIT OR Apache-2.0 |
| deadpool-runtime | 0.1.4 | MIT OR Apache-2.0 |
| deltae | 0.3.2 | MIT |
| der | 0.7.10 | Apache-2.0 OR MIT |
| deranged | 0.5.8 | MIT OR Apache-2.0 |
Expand Down Expand Up @@ -274,6 +277,7 @@ repository.
| num-iter | 0.1.45 | MIT OR Apache-2.0 |
| num-rational | 0.4.2 | MIT OR Apache-2.0 |
| num-traits | 0.2.19 | MIT OR Apache-2.0 |
| num_cpus | 1.17.0 | MIT OR Apache-2.0 |
| num_threads | 0.1.7 | MIT OR Apache-2.0 |
| objc2-core-foundation | 0.3.2 | Zlib OR Apache-2.0 OR MIT |
| object | 0.37.3 | Apache-2.0 OR MIT |
Expand Down Expand Up @@ -543,6 +547,7 @@ repository.
| windows_x86_64_msvc | 0.53.1 | MIT OR Apache-2.0 |
| winnow | 0.7.15 | MIT |
| winnow | 1.0.3 | MIT |
| wiremock | 0.6.5 | MIT/Apache-2.0 |
| wit-bindgen | 0.57.1 | Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT |
| writeable | 0.6.3 | Unicode-3.0 |
| xattr | 1.6.1 | MIT OR Apache-2.0 |
Expand Down
176 changes: 176 additions & 0 deletions apps/rocm/src/dash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ pub fn resolved_args(
// The real executor is injected in `run_async` for a live dash; None
// here keeps demo/replay/mock behaving exactly as today.
tool_executor: None,
bench_results_dir: config.dashboard.daemon.bench_results_dir.clone(),
}
}

Expand Down Expand Up @@ -319,6 +320,148 @@ pub fn run_chat(chat_mock: bool) -> Result<()> {
rt.block_on(run_async(config, paths, args, None, chat_mock))
}

/// CLI arguments for `rocm bench load`.
pub struct BenchLoadArgs {
pub endpoint: String,
pub model: Option<String>,
pub concurrency: Vec<u32>,
pub isl: u32,
pub osl: u32,
pub requests: u32,
pub out: Option<std::path::PathBuf>,
pub auto_ramp: bool,
}

/// Default `--out` path for `rocm bench load`: the same file the telemetry
/// daemon tails by default (`DashboardDaemonConfig::bench_results_dir`), so a
/// plain CLI run populates the dashboard's Bench panel without any config
/// edits. Extracted as a pure function of `AppPaths` for testability.
fn default_bench_csv_path(paths: &AppPaths) -> std::path::PathBuf {
paths.data_dir.join("bench").join("results.csv")
}

/// Entry point for `rocm bench load`.
///
/// Runs a concurrency sweep against a local http:// endpoint and appends one
/// aggregate CSV row per concurrency level. Output defaults to the daemon's
/// tailed `<data_dir>/bench/results.csv` unless `--out` is specified explicitly.
pub fn run_bench(a: BenchLoadArgs) -> Result<()> {
use rocm_dash_daemon::bench_load::{LoadSpec, run_and_append_csv, run_auto_ramp};

let BenchLoadArgs {
endpoint,
model,
concurrency,
isl,
osl,
requests,
out,
auto_ramp,
} = a;

// Reject https:// — no TLS backend compiled in for the load generator.
// Compare on the lowercased scheme so HTTPS:// is also caught.
if endpoint.to_lowercase().starts_with("https://") {
anyhow::bail!(
"rocm bench load supports http:// endpoints only (no TLS backend compiled in)"
);
}

// Resolve the model: use the provided value or probe GET {endpoint}/v1/models.
let model = if let Some(m) = model {
m
} else {
let models_url = format!("{}/v1/models", endpoint.trim_end_matches('/'));
let resp = ureq::get(&models_url)
.timeout(std::time::Duration::from_secs(5))
.call()
.context("fetching /v1/models to detect the default model")?;
let body: serde_json::Value = resp.into_json().context("parsing /v1/models response")?;
rocm_dash_tui::llm::pick_first_model(&body).with_context(|| {
format!("no model found at {endpoint}/v1/models — pass --model explicitly")
})?
};

// Resolve the output path: default to the same `<data_dir>/bench/results.csv`
// file the daemon tails by default (`DashboardDaemonConfig::bench_results_dir`,
// rocm-core/src/lib.rs), so a plain `rocm bench load` run — no config edits,
// no explicit --out — shows up in the dashboard's Bench panel. Rows are
// appended (see `run_and_append_csv` / `run_auto_ramp` below), matching the
// "tail" semantics the daemon's `CsvBenchTailer` expects.
let csv_path = if let Some(p) = out {
p
} else {
let paths = AppPaths::discover()?;
let default_path = default_bench_csv_path(&paths);
if let Some(parent) = default_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating bench output dir {}", parent.display()))?;
}
default_path
};

let spec = LoadSpec {
endpoint: endpoint.clone(),
model: model.clone(),
input_len: isl,
output_len: osl,
requests,
};

println!("mode: raw serving throughput (synthetic prompts) — not agent-workload.");
if auto_ramp {
println!(
"endpoint={endpoint} model={model} mode=auto-ramp isl={isl} osl={osl} requests={requests}"
);
} else {
println!(
"endpoint={endpoint} model={model} concurrency={} isl={isl} osl={osl} requests={requests}",
concurrency
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",")
);
}

let rt = build_dashboard_runtime()?;
let rows = if auto_ramp {
rt.block_on(run_auto_ramp(&spec, &csv_path))
.context("running bench auto-ramp")?
} else {
rt.block_on(run_and_append_csv(&spec, &concurrency, &csv_path))
.context("running bench load sweep")?
};

for row in &rows {
let conc = row
.concurrency
.map_or_else(|| "-".to_string(), |v| v.to_string());
let gen_tps = row
.gen_tps
.map_or_else(|| "-".to_string(), |v| format!("{v:.1}"));
let prompt_tps = row
.prompt_tps
.map_or_else(|| "-".to_string(), |v| format!("{v:.1}"));
let wall = row
.wall_s
.map_or_else(|| "-".to_string(), |v| format!("{v:.2}"));
let n = row
.n_requests
.map_or_else(|| "-".to_string(), |v| v.to_string());
println!(
"cell={} concurrency={conc} gen_tps={gen_tps} prompt_tps={prompt_tps} wall={wall}s n={n}",
row.cell
);
}
println!(
"note: local saturation smoke-test — client-measured throughput, not an official ROCm/AMD benchmark."
);
println!("wrote {} row(s) to {}", rows.len(), csv_path.display());

Ok(())
}

/// Entry point for `rocm bootstrap setup`. Routes to the same focused Setup host
/// as the launcher's "Set up this system" row — the first-run onboarding wizard
/// (install ROCm SDK / adopt an existing folder), with no daemon or tab shell.
Expand Down Expand Up @@ -457,6 +600,39 @@ mod tests {
assert!(!opts.enable_docker);
}

#[test]
fn default_bench_csv_path_matches_the_daemon_tailed_default() {
// EAI-7361: `rocm bench load` with no `--out` must land in the exact
// same file the daemon tails by default
// (`DashboardDaemonConfig::bench_results_dir`, rocm-core/src/lib.rs),
// or a plain CLI run never shows up in the dashboard's Bench panel.
let p = paths();
assert_eq!(
default_bench_csv_path(&p),
p.data_dir.join("bench").join("results.csv")
);
}

#[test]
fn producer_and_daemon_bench_defaults_resolve_to_the_same_path() {
// EAI-7361 cross-binary invariant: the producer default (`rocm bench
// load`'s no-`--out` path, resolved from `AppPaths::discover`) and the
// daemon-tailed default (`DashboardDaemonConfig::bench_results_dir`)
// MUST resolve to the same absolute path — both use the same
// `AppPaths::discover` data-dir resolution (honoring
// `ROCM_CLI_DATA_DIR` and host normalization), so a plain CLI run is
// always what the daemon tails. Guard on `discover()` succeeding
// ($HOME is set in CI/dev); skip rather than assert an uncontrolled
// value otherwise.
if let Ok(p) = AppPaths::discover() {
assert_eq!(
Some(default_bench_csv_path(&p)),
rocm_core::DashboardDaemonConfig::default().bench_results_dir,
"producer default and daemon-tailed default must be identical"
);
}
}

#[test]
fn bootstrap_routes_to_focused_setup() {
// `rocm bootstrap setup` must route to the same focused Setup host as the
Expand Down
Loading
Loading