@@ -19,6 +19,18 @@ use crate::models::{
1919const SUBMISSION_POLL_INTERVAL_SECONDS : u64 = 5 ;
2020const 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
2335pub 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+
706745fn 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
733778async 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