Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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.

132 changes: 132 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,137 @@ 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,
}

/// 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 goes to a self-labeled file
/// in `~/.rocm/bench/` 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: distinct self-labeled file, not the shared bench_results_dir.
let csv_path = if let Some(p) = out {
p
} else {
let paths = AppPaths::discover()?;
let bench_dir = paths.data_dir.join("bench");
std::fs::create_dir_all(&bench_dir)
.with_context(|| format!("creating bench output dir {}", bench_dir.display()))?;
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
bench_dir.join(format!("rocm-bench-{ts}.csv"))
};

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
92 changes: 92 additions & 0 deletions apps/rocm/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,12 @@ rocm logs --search error timeout")]
#[arg(long)]
chat_mock: bool,
},
/// Saturate a local OpenAI-compatible endpoint and report rough client-side
/// throughput (local smoke-test, NOT an official ROCm/AMD benchmark).
Bench {
#[command(subcommand)]
command: BenchCommand,
},
/// Remove ROCm CLI-managed files from this computer.
Uninstall {
/// Do not ask for interactive confirmation.
Expand All @@ -421,6 +427,46 @@ rocm logs --search error timeout")]
},
}

#[derive(Subcommand, Debug)]
enum BenchCommand {
/// Run a concurrency sweep (RAW serving throughput, synthetic single-shot
/// requests — not agent-shaped, not comparable to *-agent-bench harnesses).
///
/// Measures RAW serving throughput (synthetic single-shot requests, vLLM
/// benchmark_serving shape). It does NOT reproduce agent-shaped, multi-turn,
/// long-context tool traffic and is not comparable to the *-agent-bench quality
/// harnesses.
Load {
/// Base URL of the OpenAI-compatible endpoint, e.g. http://127.0.0.1:8000
#[arg(long, value_name = "URL")]
endpoint: String,
/// Model name (defaults to the first model returned by GET {endpoint}/v1/models)
#[arg(long)]
model: Option<String>,
/// Concurrency levels to sweep, comma-separated
#[arg(long, value_delimiter = ',', default_value = "1,8,32,64")]
concurrency: Vec<u32>,
/// Input sequence length in tokens
#[arg(long, default_value_t = 1024)]
isl: u32,
/// Output sequence length in tokens
#[arg(long, default_value_t = 1024)]
osl: u32,
/// Requests per concurrency cell
#[arg(long, default_value_t = 128)]
requests: u32,
/// Output CSV file (default: ~/.rocm/bench/rocm-bench-<timestamp>.csv)
#[arg(long, value_name = "FILE")]
out: Option<PathBuf>,
/// Ramp concurrency automatically (1,2,4,8,16,32,64,128), stopping at saturation.
///
/// Ignores --concurrency when set. Stops early when gen_tps plateaus
/// or the request queue backs up.
#[arg(long)]
auto_ramp: bool,
},
}

#[derive(Subcommand, Debug)]
enum InstallTarget {
/// Install TheRock ROCm wheels into a Python folder managed by ROCm CLI.
Expand Down Expand Up @@ -1562,6 +1608,27 @@ fn dispatch(cli: Cli) -> Result<()> {
demo,
chat_mock,
}) => dash::run(replay, demo, chat_mock),
Some(Command::Bench { command }) => match command {
BenchCommand::Load {
endpoint,
model,
concurrency,
isl,
osl,
requests,
out,
auto_ramp,
} => dash::run_bench(dash::BenchLoadArgs {
endpoint,
model,
concurrency,
isl,
osl,
requests,
out,
auto_ramp,
}),
},
Some(Command::Uninstall {
yes,
dry_run,
Expand Down Expand Up @@ -15228,6 +15295,7 @@ fn treat_as_natural_language(args: &[String]) -> bool {
"logs",
"daemon",
"dash",
"bench",
"uninstall",
"help",
"--help",
Expand Down Expand Up @@ -19166,6 +19234,7 @@ install therock";
"logs",
"daemon",
"dash",
"bench",
"uninstall",
"completions",
"help",
Expand All @@ -19188,6 +19257,29 @@ install therock";
Cli::try_parse_from(["rocm", "comfy", "logs"]).expect("comfy alias should parse");
}

#[test]
fn t5_bench_load_clap_parse_smoke() {
// T5: verify BenchCommand::Load parses correctly including comma-separated concurrency.
let cli = Cli::try_parse_from([
"rocm",
"bench",
"load",
"--endpoint",
"http://x",
"--concurrency",
"1,8,32,64",
])
.expect("rocm bench load should parse");
match cli.command {
Some(Command::Bench {
command: BenchCommand::Load { concurrency, .. },
}) => {
assert_eq!(concurrency, vec![1u32, 8, 32, 64]);
}
other => panic!("expected Bench/Load, got {other:?}"),
}
}

#[test]
fn setup_reset_cli_output_is_plain_and_persists_first_time_prompt() -> Result<()> {
let (_root, paths) = test_paths("setup-reset-cli");
Expand Down
2 changes: 1 addition & 1 deletion crates/rocm-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3942,7 +3942,7 @@ pub struct DashboardDaemonConfig {
pub discovery_tick_secs: f64,
#[serde(default = "default_instance_tick_secs")]
pub instance_tick_secs: f64,
/// Watch this directory for new normalized benchmark CSVs.
/// Watch this file for new normalized benchmark CSV rows.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bench_results_dir: Option<PathBuf>,
}
Expand Down
1 change: 1 addition & 0 deletions crates/rocm-dash-collectors/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ reqwest = { version = "0.12", default-features = false }

[dev-dependencies]
tokio = { version = "1", features = ["full"] }
wiremock = "0.6"
Loading
Loading