Skip to content

Commit 1f4cd4e

Browse files
authored
Surface the leaderboard score: fix empty score in list/show + print geomean on submit (#62)
1 parent f7150d4 commit 1f4cd4e

1 file changed

Lines changed: 128 additions & 4 deletions

File tree

src/service/mod.rs

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ use crate::models::{
1919
const SUBMISSION_POLL_INTERVAL_SECONDS: u64 = 5;
2020
const SUBMISSION_POLL_TIMEOUT_SECONDS: u64 = 60 * 60;
2121

22+
/// Parse a run's `score` field, which the server may send either as a JSON
23+
/// number or as a JSON string (e.g. `0.0033` or `"0.0033"`). A plain
24+
/// `Value::as_f64()` returns `None` for the string form, which is why scores
25+
/// rendered as `-` in the submissions list/show views. Accept both forms.
26+
fn parse_score(value: &Value) -> Option<f64> {
27+
match value {
28+
Value::Number(n) => n.as_f64(),
29+
Value::String(s) => s.parse::<f64>().ok(),
30+
_ => None,
31+
}
32+
}
33+
2234
// Helper function to create a reusable reqwest client
2335
pub fn create_client(cli_id: Option<String>) -> Result<Client> {
2436
let mut default_headers = HeaderMap::new();
@@ -408,7 +420,7 @@ pub async fn get_user_submissions(
408420
arr.iter()
409421
.map(|r| UserSubmissionRun {
410422
gpu_type: r["gpu_type"].as_str().unwrap_or("").to_string(),
411-
score: r["score"].as_f64(),
423+
score: parse_score(&r["score"]),
412424
})
413425
.collect()
414426
})
@@ -463,7 +475,7 @@ pub async fn get_user_submission(client: &Client, submission_id: i64) -> Result<
463475
mode: r["mode"].as_str().unwrap_or("").to_string(),
464476
secret: r["secret"].as_bool().unwrap_or(false),
465477
runner: r["runner"].as_str().unwrap_or("").to_string(),
466-
score: r["score"].as_f64(),
478+
score: parse_score(&r["score"]),
467479
passed: r["passed"].as_bool().unwrap_or(false),
468480
})
469481
.collect()
@@ -703,6 +715,33 @@ async fn submit_solution_background<P: AsRef<Path>>(
703715
}
704716
}
705717

718+
/// Build a human-readable summary of the geomean leaderboard score(s) for a
719+
/// finished submission, or `None` if it has no scored `leaderboard` run.
720+
///
721+
/// The score the server reports for a `leaderboard`-mode run is the geometric
722+
/// mean (in seconds) of the per-shape benchmark means — i.e. the number the
723+
/// leaderboard ranks on. It is otherwise only visible buried in the runs JSON,
724+
/// so surface it on its own line(s).
725+
fn leaderboard_score_summary(details: &SubmissionDetails) -> Option<String> {
726+
let lines: Vec<String> = details
727+
.runs
728+
.iter()
729+
.filter(|run| run.mode == "leaderboard")
730+
.filter_map(|run| {
731+
run.score.map(|score| {
732+
let scope = if run.secret { " (secret)" } else { " (public)" };
733+
format!("Geomean score{} on {}: {} s", scope, run.runner, score)
734+
})
735+
})
736+
.collect();
737+
738+
if lines.is_empty() {
739+
None
740+
} else {
741+
Some(lines.join("\n"))
742+
}
743+
}
744+
706745
fn format_submission_details(details: &SubmissionDetails) -> Result<String> {
707746
let runs: Vec<Value> = details
708747
.runs
@@ -720,14 +759,20 @@ fn format_submission_details(details: &SubmissionDetails) -> Result<String> {
720759
})
721760
.collect();
722761

723-
serde_json::to_string_pretty(&serde_json::json!({
762+
let json = serde_json::to_string_pretty(&serde_json::json!({
724763
"submission_id": details.id,
725764
"leaderboard": details.leaderboard_name,
726765
"file_name": details.file_name,
727766
"done": details.done,
728767
"runs": runs,
729768
}))
730-
.map_err(|e| anyhow!("Failed to format submission result: {}", e))
769+
.map_err(|e| anyhow!("Failed to format submission result: {}", e))?;
770+
771+
// Lead with the geomean score so it is not lost in the runs JSON.
772+
match leaderboard_score_summary(details) {
773+
Some(summary) => Ok(format!("{}\n\n{}", summary, json)),
774+
None => Ok(json),
775+
}
731776
}
732777

733778
async fn submit_solution_streaming<P: AsRef<Path>>(
@@ -1232,4 +1277,83 @@ mod tests {
12321277

12331278
std::env::set_current_dir(original_dir).unwrap();
12341279
}
1280+
1281+
#[test]
1282+
fn test_parse_score_accepts_number_and_string() {
1283+
use serde_json::json;
1284+
1285+
// The server sends score as a JSON string; older code only handled
1286+
// numbers, so string scores rendered as `-`. Both must parse now.
1287+
assert_eq!(parse_score(&json!("0.0033")), Some(0.0033));
1288+
assert_eq!(parse_score(&json!(0.0033)), Some(0.0033));
1289+
1290+
// Absent / null / non-numeric scores stay None.
1291+
assert_eq!(parse_score(&json!(null)), None);
1292+
assert_eq!(parse_score(&json!("")), None);
1293+
assert_eq!(parse_score(&json!("not-a-number")), None);
1294+
assert_eq!(parse_score(&Value::Null), None);
1295+
}
1296+
1297+
fn run(mode: &str, secret: bool, score: Option<f64>) -> SubmissionRun {
1298+
SubmissionRun {
1299+
start_time: None,
1300+
end_time: None,
1301+
mode: mode.to_string(),
1302+
secret,
1303+
runner: "B200".to_string(),
1304+
score,
1305+
passed: true,
1306+
}
1307+
}
1308+
1309+
fn details(runs: Vec<SubmissionRun>) -> SubmissionDetails {
1310+
SubmissionDetails {
1311+
id: 1,
1312+
leaderboard_id: 1,
1313+
leaderboard_name: "qr_v2".to_string(),
1314+
file_name: "submission.py".to_string(),
1315+
user_id: "u".to_string(),
1316+
submission_time: String::new(),
1317+
done: true,
1318+
code: String::new(),
1319+
runs,
1320+
job: None,
1321+
}
1322+
}
1323+
1324+
#[test]
1325+
fn test_leaderboard_score_summary_reports_geomean_scores() {
1326+
// Only the scored `leaderboard` runs are reported; test/benchmark and
1327+
// null-score runs are skipped.
1328+
let d = details(vec![
1329+
run("test", false, None),
1330+
run("benchmark", false, None),
1331+
run("leaderboard", false, Some(0.0066)),
1332+
run("leaderboard", true, Some(0.0018)),
1333+
]);
1334+
let summary = leaderboard_score_summary(&d).expect("expected a score summary");
1335+
assert_eq!(
1336+
summary,
1337+
"Geomean score (public) on B200: 0.0066 s\n\
1338+
Geomean score (secret) on B200: 0.0018 s"
1339+
);
1340+
1341+
// And it is prepended to the formatted submission details.
1342+
let formatted = format_submission_details(&d).unwrap();
1343+
assert!(formatted.starts_with("Geomean score (public) on B200: 0.0066 s"));
1344+
}
1345+
1346+
#[test]
1347+
fn test_leaderboard_score_summary_none_without_scored_leaderboard_run() {
1348+
// A submission with no scored leaderboard run (e.g. test/benchmark
1349+
// mode, or scores not yet populated) yields no summary.
1350+
let d = details(vec![
1351+
run("test", false, None),
1352+
run("benchmark", false, Some(0.5)),
1353+
run("leaderboard", false, None),
1354+
]);
1355+
assert!(leaderboard_score_summary(&d).is_none());
1356+
// format_submission_details still works, just without a summary header.
1357+
assert!(format_submission_details(&d).unwrap().starts_with('{'));
1358+
}
12351359
}

0 commit comments

Comments
 (0)