bench_report.rs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. //! Post-run report repair. tool-eval-bench's `--json-file` flag implies
  2. //! `--json`, and that dispatch path (v2.7.0) drops the llama-benchy perf
  3. //! section from the scenario report md (the samples are computed and printed
  4. //! to the bench log, but never passed to the report writer). The launcher
  5. //! re-attaches the table from the log so every campaign report carries its
  6. //! perf numbers like a `--perf-only` run would.
  7. use std::fs;
  8. use anyhow::Result;
  9. pub struct PerfRow {
  10. pub test: String,
  11. pub concurrency: u32,
  12. pub pp: String,
  13. pub tg: String,
  14. pub ttft: String,
  15. pub total: String,
  16. pub tokens: String,
  17. }
  18. /// Append a `## Throughput Metrics` section to `md` when it lacks one,
  19. /// taking the rows from the llama-benchy box in `log`. Returns true when
  20. /// the md was modified.
  21. pub fn ensure_throughput_section(md: &std::path::Path, log: &std::path::Path) -> Result<bool> {
  22. let body = fs::read_to_string(md)?;
  23. if body.contains("## Throughput Metrics") || body.contains("## Results") {
  24. return Ok(false);
  25. }
  26. let rows = parse_perf_rows(&fs::read_to_string(log)?)
  27. .ok_or_else(|| anyhow::anyhow!("no llama-benchy perf rows in {}", log.display()))?;
  28. let mut out = body;
  29. if !out.ends_with('\n') {
  30. out.push('\n');
  31. }
  32. out.push_str("\n## Throughput Metrics\n\n");
  33. out.push_str("> Appended by `sm121 bench`: the tool's `--json-file` path drops the\n");
  34. out.push_str("> perf section from this report; the rows come from the llama-benchy table\n");
  35. out.push_str("> in the bench log (same run).\n\n");
  36. out.push_str("| Test | pp t/s | tg t/s | TTFT (ms) | Total (ms) | Tokens |\n");
  37. out.push_str("|---|---:|---:|---:|---:|---:|\n");
  38. for r in &rows {
  39. let conc = if r.concurrency > 1 {
  40. format!(" c{}", r.concurrency)
  41. } else {
  42. String::new()
  43. };
  44. out.push_str(&format!(
  45. "| {}{} | {} | {} | {} | {} | {} |\n",
  46. r.test, conc, r.pp, r.tg, r.ttft, r.total, r.tokens
  47. ));
  48. }
  49. fs::write(md, out)?;
  50. Ok(true)
  51. }
  52. /// Extract the llama-benchy result rows from a bench log. Box rows look like
  53. /// `│ pp2048 tg128 @ d0 │ c1 │ 154 │ 19.7 │ 12,620 │ 18,810 │ 2048+128`.
  54. fn parse_perf_rows(log: &str) -> Option<Vec<PerfRow>> {
  55. let mut rows = Vec::new();
  56. for line in log.lines() {
  57. let line = line.trim();
  58. if !line.starts_with('│') {
  59. continue;
  60. }
  61. let parts: Vec<&str> = line
  62. .split('│')
  63. .map(|s| s.trim())
  64. .filter(|s| !s.is_empty())
  65. .collect();
  66. if parts.len() != 7 || !parts[0].starts_with("pp") {
  67. continue;
  68. }
  69. let concurrency = parts
  70. .get(1)
  71. .and_then(|c| c.strip_prefix('c'))
  72. .and_then(|n| n.parse().ok())
  73. .unwrap_or(1);
  74. rows.push(PerfRow {
  75. test: parts[0].to_string(),
  76. concurrency,
  77. pp: parts[2].to_string(),
  78. tg: parts[3].to_string(),
  79. ttft: parts[4].to_string(),
  80. total: parts[5].to_string(),
  81. tokens: parts[6].to_string(),
  82. });
  83. }
  84. (!rows.is_empty()).then_some(rows)
  85. }
  86. #[cfg(test)]
  87. mod tests {
  88. use super::*;
  89. const LOG: &str = "\
  90. ╭──────────────────── ⚡ llama-benchy Throughput Benchmark ────────────────────╮
  91. ┃ Test ┃ c ┃ pp t/s ┃ tg t/s ┃ TTFT ┃ Total ┃
  92. ┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━
  93. │ pp2048 tg128 @ d0 │ c1 │ 154 │ 19.7 │ 12,620 │ 18,810 │ 2048+128
  94. │ pp2048 tg128 @ d0 │ c2 │ 177 │ 21.8 │ 20,470 │ 30,788 │ 2048+128
  95. └──────────────────────┴────┴─────────┴─────────┴──────────┴──────────┴─────────
  96. ";
  97. #[test]
  98. fn appends_section_from_log_box() {
  99. let tmp =
  100. std::env::temp_dir().join(format!("sm121-bench-report-test-{}", std::process::id()));
  101. fs::create_dir_all(&tmp).unwrap();
  102. let md = tmp.join("r.md");
  103. let log = tmp.join("r.log");
  104. fs::write(&md, "# Report\n\n## Scenario Results\n\n| ok |\n").unwrap();
  105. fs::write(&log, LOG).unwrap();
  106. assert!(ensure_throughput_section(&md, &log).unwrap());
  107. let body = fs::read_to_string(&md).unwrap();
  108. assert!(body.contains("## Throughput Metrics"));
  109. assert!(body.contains("| pp2048 tg128 @ d0 | 154 | 19.7 | 12,620 | 18,810 | 2048+128 |"));
  110. assert!(
  111. body.contains("| pp2048 tg128 @ d0 c2 | 177 | 21.8 | 20,470 | 30,788 | 2048+128 |")
  112. );
  113. // idempotent: an existing section means no rewrite
  114. assert!(!ensure_throughput_section(&md, &log).unwrap());
  115. let _ = fs::remove_dir_all(&tmp);
  116. }
  117. #[test]
  118. fn no_rows_is_an_error() {
  119. let tmp =
  120. std::env::temp_dir().join(format!("sm121-bench-report-test2-{}", std::process::id()));
  121. fs::create_dir_all(&tmp).unwrap();
  122. let md = tmp.join("r.md");
  123. let log = tmp.join("r.log");
  124. fs::write(&md, "# Report\n").unwrap();
  125. fs::write(&log, "no box here\n").unwrap();
  126. assert!(ensure_throughput_section(&md, &log).is_err());
  127. let _ = fs::remove_dir_all(&tmp);
  128. }
  129. }