launchbound_report/lib.rs
1//! The run report (`report.v1`). The section that prints every
2//! configuration that was **faster and refused** is the product: never
3//! soften, hide, or downrank it.
4
5#![warn(missing_docs)]
6
7mod build;
8mod render;
9
10pub use build::{RunDir, build_report};
11pub use render::render_text;
12
13// Re-exported, not merely used: `Summary` appears in the public fields of
14// `CandidateReport`, `ChosenInfo` and `RejectedFaster`, so a caller that reads
15// a report has to be able to name it. Without this the type was reachable and
16// unnameable unless you also depended on `launchbound-bench` directly.
17pub use launchbound_bench::Summary;
18
19use serde::{Deserialize, Serialize};
20
21/// What can go wrong assembling a report from a run directory.
22#[derive(Debug, thiserror::Error)]
23pub enum ReportError {
24 /// The run directory is missing a document, or one of them does not
25 /// declare the schema this build reads.
26 #[error("run dir: {0}")]
27 RunDir(String),
28 /// The report could not be written.
29 #[error("report io: {0}")]
30 Io(String),
31}
32
33/// A `report.v1` document: the whole run, as a reader should see it.
34///
35/// This is the surface every other consumer reads — the TUI, the text
36/// renderer, and anyone parsing the JSON. Its schema is validated against
37/// `schemas/report.v1.json` in CI.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Report {
40 /// Schema tag; always `report.v1`.
41 pub schema: String,
42 /// The kernel this run tuned.
43 pub kernel: String,
44 /// The compute capability the gate ran at. A verdict does not transfer
45 /// to another one (`docs/LIMITATIONS.md`).
46 pub gate_cc: String,
47 /// `measured` or `estimated` — stamped on the report and every number
48 /// in it. Reporting an estimate as a measurement is release-blocking
49 /// (docs/LIMITATIONS.md).
50 pub measurement_kind: String,
51 /// `full` when every candidate passed through the reconverge gate;
52 /// `none` on the Metal path (there is no MSL analyzer — §3.4). The
53 /// renderer prints the no-gate notice unconditionally when this is
54 /// `none`; a test asserts it cannot be omitted.
55 pub convergence_gate: String,
56 /// The part measured on. Absent when nothing was measured.
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub device: Option<DeviceInfo>,
59 /// The operator's written reason for measuring refused candidates.
60 /// Present exactly when `--allow-unsafe` was used, so a report
61 /// containing refused timings always says why they were taken.
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub allow_unsafe_reason: Option<String>,
64 /// The recommended configuration. Absent when nothing was both
65 /// admitted and measured — a gate-only run, or one whose whole
66 /// admitted set failed to measure.
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub chosen: Option<ChosenInfo>,
69 /// IDs whose intervals overlap the chosen one: reported as
70 /// indistinguishable, never ranked (docs/BENCHMARKING.md).
71 #[serde(default)]
72 pub indistinguishable_from_chosen: Vec<String>,
73 /// THE section: refused configurations that measurably beat the chosen
74 /// one (their CI is entirely below the chosen CI).
75 #[serde(default)]
76 pub rejected_faster: Vec<RejectedFaster>,
77 /// Every candidate, admitted or not, measured or not.
78 pub candidates: Vec<CandidateReport>,
79 /// Run-level tallies.
80 pub totals: Totals,
81}
82
83/// The part the measurements were taken on. Results are valid only for it.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct DeviceInfo {
86 /// Product name as the driver reports it, e.g. `NVIDIA A10G`.
87 pub name: String,
88 /// The device's actual compute capability, which need not equal
89 /// [`Report::gate_cc`] — the gate can be run for a different target
90 /// than the one measured on.
91 pub cc: String,
92 /// Driver version, part of what makes a timing reproducible.
93 pub driver_version: String,
94}
95
96/// The configuration this run recommends: fastest median among the
97/// candidates that were admitted by the gate *and* measured successfully.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct ChosenInfo {
100 /// Its canonical `config.v1` ID.
101 pub id: String,
102 /// Its dimension assignments, e.g. `block_x=128 tile=256`.
103 pub config: String,
104 /// Its timing summary. Present by construction — being measured is
105 /// what made it eligible.
106 pub summary: Summary,
107}
108
109/// A refused configuration that measurably beat the chosen one.
110///
111/// This is the section the whole tool exists to produce: it is the cost of
112/// the safety decision, stated in numbers rather than asserted. Populated
113/// only when refused candidates were measured under `--allow-unsafe`, and
114/// only when the candidate's whole confidence interval sits below the
115/// chosen one's — "faster" here means measurably, not on a point estimate.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct RejectedFaster {
118 /// Its canonical `config.v1` ID.
119 pub id: String,
120 /// Its dimension assignments.
121 pub config: String,
122 /// Its timing summary.
123 pub summary: Summary,
124 /// chosen_median / this_median: how much faster the refused one was.
125 pub speedup_vs_chosen: f64,
126 /// The rules that refused it — what the speed cost buys.
127 pub rules: Vec<RuleRef>,
128}
129
130/// One analyzer rule as the report carries it.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct RuleRef {
133 /// Rule ID, e.g. `RC001`.
134 pub rule: String,
135 /// `file:line:col` of the offending site, when the analyzer gave one.
136 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub span: Option<String>,
138 /// Why it applies at this configuration — the launch-shape reasoning,
139 /// not just the analyzer's generic message.
140 pub reason: String,
141}
142
143/// Every candidate the run considered, admitted or not, measured or not.
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct CandidateReport {
146 /// Its canonical `config.v1` ID.
147 pub id: String,
148 /// Its dimension assignments.
149 pub config: String,
150 /// clean | admitted_with_caveats | disqualified | tool_error
151 pub verdict: String,
152 /// Rules that fired: refusals when disqualified, caveats when
153 /// admitted with them, empty when clean.
154 #[serde(default)]
155 pub rules: Vec<RuleRef>,
156 /// ok | error | timeout | unmeasured
157 pub measurement_status: String,
158 /// Timing summary, present exactly when `measurement_status` is `ok`.
159 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub summary: Option<Summary>,
161 /// Why the measurement failed, when it did — including the timeout
162 /// message for a candidate that hung, which is the gate being right.
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub measurement_error: Option<String>,
165 /// Wall-clock GPU seconds this candidate consumed, spent or wasted.
166 pub gpu_seconds: f64,
167}
168
169/// Run-level tallies, so a reader can check the parts add up.
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct Totals {
172 /// Configurations enumerated, after constraints pruned the product.
173 pub candidates: usize,
174 /// Passed the gate — clean or admitted with caveats.
175 pub admitted: usize,
176 /// Refused by the gate.
177 pub refused: usize,
178 /// Measured successfully. At most `admitted`, and fewer when a budget
179 /// ran out or a measurement failed.
180 pub measured_ok: usize,
181 /// Total GPU seconds the run consumed.
182 pub gpu_seconds: f64,
183}