All checks were successful
CI / Format (push) Successful in 43s
CI / CUDA type-check (push) Successful in 2m35s
CI / Clippy (push) Successful in 2m46s
CI / Test (push) Successful in 5m54s
CI / Build cortex SRPM (push) Has been skipped
CI / Publish cortex to COPR (push) Has been skipped
CI / Build neuron SRPM (push) Has been skipped
CI / Publish neuron to COPR (push) Has been skipped
CI / Bump version in source (push) Has been skipped
CI / Format (pull_request) Successful in 43s
CI / CUDA type-check (pull_request) Successful in 1m53s
CI / Clippy (pull_request) Successful in 2m57s
CI / Test (pull_request) Successful in 7m42s
CI / Build cortex SRPM (pull_request) Has been skipped
CI / Build neuron SRPM (pull_request) Has been skipped
CI / Publish cortex to COPR (pull_request) Has been skipped
CI / Publish neuron to COPR (pull_request) Has been skipped
CI / Bump version in source (pull_request) Has been skipped
The six perf scenarios measure speed/resources; this measures the axis they miss — reasoning/planning quality — so the frontier A/B (F3) can pick on capability, not just throughput. Per the chosen approach: store the artifact always, with schema for BOTH a manual score and a future LLM-judge; start manual. - scenario: CapabilityScenario (capability:<name>) runs a fixed prompt and captures the full output text (stream_and_measure gains a capture_text path); opt-in via config.capability_probes (empty default — long outputs, deliberate). - store: three additive columns (artifact, quality_score, scorer); capability_runs(unscored_only) worklist + set_score(id, score, scorer). Drill-down RunRow omits the large artifact column. - cli: `helexa-bench score --id <n> --score <x> [--scorer ...]` (manual); `report --capability` (per-model median score + per-run artifact snippets); GET /api/capability. LLM-judge deferred (schema ready). - example config documents an implementation-planning probe. Tests: artifact storage + scoring lifecycle, capability scenario built from config, capability markdown (median + snippet). Part of the Performance observability epic (#83), O7 — completes the milestone. Feeds the F3 frontier A/B decision gate (#94). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VrJ4i3pfLRSTM76o3ofnVq
137 lines
4.8 KiB
Rust
137 lines
4.8 KiB
Rust
//! End-to-end sweep against a mock neuron: a sweep records samples, a
|
||
//! second sweep skips the satisfied cell, and bumping the reported build
|
||
//! SHA resumes fresh sampling.
|
||
|
||
use axum::Router;
|
||
use axum::extract::State;
|
||
use axum::http::header;
|
||
use axum::response::{IntoResponse, Json};
|
||
use axum::routing::{get, post};
|
||
use helexa_bench::config::{BenchConfig, BenchSettings, ScenarioConfig, TargetConfig, TargetKind};
|
||
use helexa_bench::sweep::Sweeper;
|
||
use serde_json::json;
|
||
use std::sync::{Arc, Mutex};
|
||
|
||
#[derive(Clone)]
|
||
struct MockState {
|
||
sha: Arc<Mutex<String>>,
|
||
}
|
||
|
||
async fn version(State(s): State<MockState>) -> Json<serde_json::Value> {
|
||
let sha = s.sha.lock().unwrap().clone();
|
||
Json(json!({
|
||
"package_version": "0.1.16",
|
||
"git_sha": sha,
|
||
"git_dirty": false,
|
||
"features": ["cuda", "cudnn"],
|
||
"candle_version": "0.10.2",
|
||
}))
|
||
}
|
||
|
||
async fn discovery() -> Json<serde_json::Value> {
|
||
Json(json!({
|
||
"hostname": "mock-beast",
|
||
"os": "Linux",
|
||
"kernel": "6.19.0",
|
||
"cuda_version": "13.0",
|
||
"driver_version": "580.159",
|
||
"devices": [{"index": 0, "name": "RTX 5090", "vram_total_mb": 32614, "compute_capability": "12.0"}],
|
||
"harnesses": ["candle"],
|
||
}))
|
||
}
|
||
|
||
async fn models() -> Json<serde_json::Value> {
|
||
Json(json!([
|
||
{"id": "Qwen/Qwen3.6-27B", "harness": "candle", "status": "loaded", "devices": [0], "capabilities": ["text"]},
|
||
// A non-warm model the bench must ignore.
|
||
{"id": "Qwen/cold", "harness": "candle", "status": "recovering", "devices": [0]},
|
||
]))
|
||
}
|
||
|
||
async fn chat() -> impl IntoResponse {
|
||
let body = concat!(
|
||
"data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\"},\"finish_reason\":null}]}\n\n",
|
||
"data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\" world\"},\"finish_reason\":null}]}\n\n",
|
||
"data: {\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":130,\"completion_tokens\":2,\"total_tokens\":132}}\n\n",
|
||
"data: [DONE]\n\n",
|
||
);
|
||
([(header::CONTENT_TYPE, "text/event-stream")], body)
|
||
}
|
||
|
||
async fn spawn_mock(sha: &str) -> (String, Arc<Mutex<String>>) {
|
||
let shared = Arc::new(Mutex::new(sha.to_string()));
|
||
let state = MockState {
|
||
sha: shared.clone(),
|
||
};
|
||
let app = Router::new()
|
||
.route("/version", get(version))
|
||
.route("/discovery", get(discovery))
|
||
.route("/models", get(models))
|
||
.route("/v1/chat/completions", post(chat))
|
||
.with_state(state);
|
||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||
let addr = listener.local_addr().unwrap();
|
||
tokio::spawn(async move {
|
||
axum::serve(listener, app).await.unwrap();
|
||
});
|
||
(format!("http://{addr}"), shared)
|
||
}
|
||
|
||
fn config_for(endpoint: String, db_path: String) -> BenchConfig {
|
||
BenchConfig {
|
||
bench: BenchSettings {
|
||
sweep_interval_secs: 1,
|
||
samples_per_version: 2,
|
||
iteration_pause_secs: 0,
|
||
request_timeout_secs: 30,
|
||
db_path,
|
||
},
|
||
scenarios: ScenarioConfig {
|
||
prompt_sizes: vec![128], // single scenario keeps assertions simple
|
||
max_tokens: 16,
|
||
concurrency_levels: Vec::new(),
|
||
concurrency_prompt_tokens: 512,
|
||
capability_probes: Vec::new(),
|
||
},
|
||
api: Default::default(),
|
||
targets: vec![TargetConfig {
|
||
name: "mock".into(),
|
||
kind: TargetKind::Neuron,
|
||
endpoint,
|
||
label: None,
|
||
}],
|
||
}
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn sweep_records_skips_and_resumes_on_new_sha() {
|
||
let (endpoint, sha_handle) = spawn_mock("aaaaaaa").await;
|
||
|
||
// Unique db path per run (bound port is unique).
|
||
let port = endpoint.rsplit(':').next().unwrap();
|
||
let db_path = std::env::temp_dir().join(format!("helexa-bench-it-{port}.sqlite"));
|
||
let _ = std::fs::remove_file(&db_path);
|
||
let db_str = db_path.to_string_lossy().to_string();
|
||
|
||
let sweeper = Sweeper::new(config_for(endpoint, db_str)).unwrap();
|
||
|
||
// First sweep: one warm model × one scenario × 2 samples.
|
||
let s1 = sweeper.run_once().await.unwrap();
|
||
assert_eq!(s1.measured, 2, "should record samples_per_version samples");
|
||
assert_eq!(s1.skipped, 0);
|
||
assert_eq!(s1.failed, 0);
|
||
|
||
// Second sweep at same SHA: cell satisfied, nothing measured.
|
||
let s2 = sweeper.run_once().await.unwrap();
|
||
assert_eq!(s2.measured, 0, "satisfied cell must be skipped");
|
||
assert_eq!(s2.skipped, 1);
|
||
|
||
// Bump the reported build SHA: a new cell → fresh sampling resumes.
|
||
*sha_handle.lock().unwrap() = "bbbbbbb".to_string();
|
||
let s3 = sweeper.run_once().await.unwrap();
|
||
assert_eq!(s3.measured, 2, "new SHA must resume sampling");
|
||
assert_eq!(s3.skipped, 0);
|
||
|
||
let _ = std::fs::remove_file(&db_path);
|
||
}
|