Skip to content

Commit b4ee740

Browse files
authored
feat: redesign the pwm-tui dashboard (#202)
A horizontal pipeline flow strip replaces the vertical rail, and a dominant always-visible overview pane carries the run: live download gauges with a throughput sparkline during FETCH, substage progress and a quantize bar during EXPORT, then a KPI grid (checkpoint, params, graph, latency, witness, Freivalds, float gate, z_next) that fills in live from exporter events, op-mix bars, the commitment table, and the verdict banner. A secondary per-stage detail pane (arrow keys) keeps the advanced view. Charm-style truecolor palette, rounded padded panels, NO_COLOR intact, narrow terminals drop the side pane. Header and labels are factual; promotional phrasing removed.
1 parent eba6572 commit b4ee740

5 files changed

Lines changed: 878 additions & 385 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ entry.
1515

1616
### Added
1717

18+
- The `pwm-tui` dashboard layout: a horizontal pipeline flow strip, a dominant
19+
always-visible overview pane (live download gauges with a throughput
20+
sparkline, exporter substage progress, a KPI grid that fills in as the
21+
exporter reports facts, op-mix bars, the commitment table, and the verdict
22+
banner), and a secondary per-stage detail pane with ←/→ navigation.
1823
- **`pwm-tui`: a ratatui demo that proves the REAL pretrained checkpoint end to
1924
end locally, with no Docker** (#200): downloads and sha256-pin-verifies the
2025
`quentinll/lewm-pusht` checkpoint and a real `lerobot/pusht` episode into a

crates/pwm-tui/src/app.rs

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! TUI and `--headless` can never disagree about what happened.
55
66
use std::collections::VecDeque;
7+
use std::time::Instant;
78

89
use pwm_testkit::report::PredictorReport;
910

@@ -32,6 +33,53 @@ pub(crate) struct ExportSubRow {
3233
pub(crate) ms: Option<f64>,
3334
}
3435

36+
/// Typed key facts parsed out of the exporter's kv stream, so the dashboard
37+
/// can show live numbers before the stage-2 report exists.
38+
#[derive(Default)]
39+
pub(crate) struct Dash {
40+
/// Checkpoint size in bytes (load stage).
41+
pub(crate) checkpoint_bytes: Option<u64>,
42+
/// Checkpoint tensor count (load stage).
43+
pub(crate) tensors: Option<u64>,
44+
/// Checkpoint float parameter count (load stage).
45+
pub(crate) float_params: Option<u64>,
46+
/// Quantized matrices in the exported manifest (quantize stage).
47+
pub(crate) matrices: Option<u64>,
48+
/// Exported int8 parameter count (quantize stage; wider scope than the
49+
/// proven relation, which the report narrows to its own count).
50+
pub(crate) int8_params: Option<u64>,
51+
/// Measured max |int - float| (calibrate stage).
52+
pub(crate) error: Option<f64>,
53+
/// Bundle-carried float tolerance (calibrate stage).
54+
pub(crate) tolerance: Option<f64>,
55+
/// Predictor-scoped weights root carried in the bundle (bundle stage).
56+
pub(crate) weights_root: Option<String>,
57+
/// Predictor bundle size in bytes (bundle stage).
58+
pub(crate) bundle_bytes: Option<u64>,
59+
/// Input provenance string (encode stage).
60+
pub(crate) source: Option<String>,
61+
}
62+
63+
impl Dash {
64+
fn absorb(&mut self, stage: &str, key: &str, value: &str) {
65+
let u = || value.parse::<u64>().ok();
66+
let f = || value.parse::<f64>().ok();
67+
match (stage, key) {
68+
("load", "checkpoint_bytes") => self.checkpoint_bytes = u(),
69+
("load", "tensors") => self.tensors = u(),
70+
("load", "float_params") => self.float_params = u(),
71+
("quantize", "matrices") => self.matrices = u(),
72+
("quantize", "int8_params") => self.int8_params = u(),
73+
("calibrate", "error") => self.error = f(),
74+
("calibrate", "tolerance") => self.tolerance = f(),
75+
("bundle", "weights_root") => self.weights_root = Some(value.to_string()),
76+
("bundle", "predictor_bundle_bytes") => self.bundle_bytes = u(),
77+
("encode", "source") => self.source = Some(value.to_string()),
78+
_ => {}
79+
}
80+
}
81+
}
82+
3583
/// Everything the front ends render.
3684
pub(crate) struct App {
3785
/// Per-stage lifecycle, indexed by [`Stage::idx`].
@@ -58,6 +106,16 @@ pub(crate) struct App {
58106
pub(crate) cache_root: String,
59107
/// Spinner frame counter (advanced by the draw loop).
60108
pub(crate) spin: usize,
109+
/// Typed dashboard facts parsed from the exporter kv stream.
110+
pub(crate) dash: Dash,
111+
/// Wall-clock start of the run.
112+
pub(crate) started: Instant,
113+
/// Wall-clock end of the run (freezes the elapsed display).
114+
pub(crate) finished_at: Option<Instant>,
115+
/// Per-tick downloaded-bytes deltas, newest last (throughput sparkline).
116+
pub(crate) net_history: Vec<u64>,
117+
/// Total fetched bytes at the previous tick.
118+
last_net: u64,
61119
}
62120

63121
impl App {
@@ -84,7 +142,33 @@ impl App {
84142
show_logs: false,
85143
cache_root,
86144
spin: 0,
145+
dash: Dash::default(),
146+
started: Instant::now(),
147+
finished_at: None,
148+
net_history: Vec::new(),
149+
last_net: 0,
150+
}
151+
}
152+
153+
/// Advance animation state and sample the download throughput. Called once
154+
/// per draw tick.
155+
pub(crate) fn on_tick(&mut self) {
156+
self.spin = self.spin.wrapping_add(1);
157+
let total: u64 = self.fetch.iter().map(|r| r.done).sum();
158+
let fetching = matches!(self.stages[Stage::Fetch.idx()], StageState::Running);
159+
if fetching {
160+
self.net_history.push(total.saturating_sub(self.last_net));
161+
if self.net_history.len() > 72 {
162+
self.net_history.remove(0);
163+
}
87164
}
165+
self.last_net = total;
166+
}
167+
168+
/// Elapsed wall time, frozen once the pipeline finishes.
169+
pub(crate) fn elapsed_secs(&self) -> f64 {
170+
let end = self.finished_at.unwrap_or_else(Instant::now);
171+
end.duration_since(self.started).as_secs_f64()
88172
}
89173

90174
/// Fold one pipeline event into the state.
@@ -119,15 +203,21 @@ impl App {
119203
}
120204
}
121205
Event::ExportProgress { done, total } => self.export_progress = Some((done, total)),
122-
Event::Kv { stage, key, value } => self.kvs.push((stage, key, value)),
206+
Event::Kv { stage, key, value } => {
207+
self.dash.absorb(&stage, &key, &value);
208+
self.kvs.push((stage, key, value));
209+
}
123210
Event::Log(line) => {
124211
self.logs.push_back(line);
125212
while self.logs.len() > 500 {
126213
self.logs.pop_front();
127214
}
128215
}
129216
Event::Report(r) => self.report = Some(*r),
130-
Event::Finished { ok } => self.finished = Some(ok),
217+
Event::Finished { ok } => {
218+
self.finished = Some(ok);
219+
self.finished_at = Some(Instant::now());
220+
}
131221
}
132222
}
133223

crates/pwm-tui/src/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ fn run_tui(opts: &Options, json: bool) -> ExitCode {
232232
while let Ok(ev) = rx.try_recv() {
233233
app.handle(ev);
234234
}
235-
app.spin = app.spin.wrapping_add(1);
235+
app.on_tick();
236236
if terminal.draw(|f| ui::render(f, &app)).is_err() {
237237
break;
238238
}
@@ -247,8 +247,8 @@ fn run_tui(opts: &Options, json: bool) -> ExitCode {
247247
break 'outer;
248248
}
249249
(KeyCode::Char('l'), _) => app.show_logs = !app.show_logs,
250-
(KeyCode::Up, _) => app.select(-1),
251-
(KeyCode::Down, _) => app.select(1),
250+
(KeyCode::Up | KeyCode::Left | KeyCode::Char('k'), _) => app.select(-1),
251+
(KeyCode::Down | KeyCode::Right | KeyCode::Char('j'), _) => app.select(1),
252252
(KeyCode::Esc, _) => app.selected = None,
253253
_ => {}
254254
}

crates/pwm-tui/src/pipeline.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,13 @@ impl Stage {
6767
/// One-line description for the detail panel header.
6868
pub(crate) fn blurb(self) -> &'static str {
6969
match self {
70-
Stage::Fetch => "pin-verified checkpoint + real episode files (cache-first)",
71-
Stage::Export => {
72-
"PyTorch exporter: extract V0, quantize int8, commit, encode, calibrate"
73-
}
74-
Stage::Load => "parse the bundle, bind the export weights root, build the circuit",
75-
Stage::Infer => "exact integer forward pass (the world model runs; no float, no GPU)",
76-
Stage::Commit => "bind execution to the Fiat-Shamir transcript and commitments",
77-
Stage::Verify => "no_std float-free audit: commitments, Freivalds, exact recompute",
78-
Stage::Tamper => "forge one matmul output; the audit must reject it",
70+
Stage::Fetch => "fetch the checkpoint and episode assets, sha256-pinned, cache-first",
71+
Stage::Export => "extract the V0 subgraph, quantize to int8, commit, encode, calibrate",
72+
Stage::Load => "parse the bundle, bind the weights root, build the circuit",
73+
Stage::Infer => "exact integer forward pass, no float, no GPU",
74+
Stage::Commit => "bind the execution to the Fiat-Shamir transcript",
75+
Stage::Verify => "audit: commitments, Freivalds checks, exact recompute",
76+
Stage::Tamper => "forge one matmul output, the audit must reject it",
7977
}
8078
}
8179

0 commit comments

Comments
 (0)