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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ current-user-asks.md
__pycache__/
/.hawkeye-bin/
/.claude
/plans/
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ git diff origin/main..HEAD \
&& echo "REVIEW each hit" || echo "diff clean"
```

Planning and scratch artifacts are not repository documentation. Keep `plans/`,
implementation logs, and agent working notes out of commits; durable design
documentation belongs under `docs/`.

## 5) Investigate rocm-cli Before Editing

Understand existing patterns first:
Expand Down
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
180 changes: 179 additions & 1 deletion apps/rocm/src/dash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@
) -> RunnerOptions {
let d = &config.dashboard.daemon;
RunnerOptions {
bench_csv: d.bench_results_dir.clone(),
bench_csv: Some(
d.bench_results_dir
.clone()
.unwrap_or_else(|| default_bench_csv_path(paths)),
),
enable_docker,
image_patterns: None,
gpu_tick: Duration::from_secs_f64(d.gpu_tick_secs),
Expand Down Expand Up @@ -201,6 +205,7 @@
// 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 +324,149 @@
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")
}

fn ensure_bench_csv_parent(csv_path: &std::path::Path) -> Result<()> {
if let Some(parent) = csv_path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
Comment thread
michaelroy-amd marked this conversation as resolved.
Dismissed
.with_context(|| format!("creating bench output dir {}", parent.display()))?;
}
Ok(())
}

/// 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. Rows are appended, matching the `CsvBenchTailer`
// semantics. Create parents for explicit and default paths alike.
let csv_path = match out {
Some(path) => path,
None => default_bench_csv_path(&AppPaths::discover()?),
};
ensure_bench_csv_parent(&csv_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 +605,36 @@
assert!(!opts.enable_docker);
}

#[test]
fn runner_options_derives_default_bench_csv_from_current_paths() {
let p = paths();
let opts = runner_options(&cfg(), &p, false);
assert_eq!(opts.bench_csv, Some(default_bench_csv_path(&p)));
}

#[test]
fn runner_options_preserves_explicit_bench_csv_override() {
let p = paths();
let mut config = cfg();
let explicit = PathBuf::from("/var/rocm/custom-bench.csv");
config.dashboard.daemon.bench_results_dir = Some(explicit.clone());

let opts = runner_options(&config, &p, false);
assert_eq!(opts.bench_csv, Some(explicit));
}

#[test]
fn ensure_bench_csv_parent_creates_explicit_output_directory() {
let root =
std::env::temp_dir().join(format!("rocm-cli-bench-parent-{}", std::process::id()));
let csv = root.join("nested").join("results.csv");

ensure_bench_csv_parent(&csv).unwrap();
assert!(csv.parent().unwrap().is_dir());

let _ = std::fs::remove_dir_all(root);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
Comment thread
michaelroy-amd marked this conversation as resolved.
Dismissed
}

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