use std::path::PathBuf;
use std::sync::Arc;
use henad_core::explore::plan::Plan;
use henad_core::explore::spec::SweepSpec;
use henad_core::params::ParamValue;
use henad_core::provenance::BuildInfo;
use henad_explore::exec::Concurrency;
use henad_explore::handle::{SweepEvent, SweepOutput, SweepPhase, SweepProgress, SweepRun, SweepRunOptions};
use henad_explore::output::manifest::ManifestExecution;
use henad_explore::search_run::{SearchPlan, SearchUpdate};
use henad_explore::sweep::{Provenance, SweepEnd, SweepOutline, SweepReport};
use crate::state::{AppState, lookup_message};
use crate::ui::results::ResultsPanel;
use crate::ui::sweep::builder::budget_text;
use crate::ui::sweep::draft::{SweepDraft, capitalize, describe_error};
use crate::ui::sweep::plan::PlanSummary;
pub struct KeptDraft {
pub draft: SweepDraft,
pub panel_values: Vec<ParamValue>,
pub plan: PlanSummary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionState {
Planning,
Running,
Paused,
Finished,
Aborted,
Stopped,
Failed,
}
impl SessionState {
pub fn is_running(self) -> bool {
matches!(self, Self::Planning | Self::Running | Self::Paused)
}
pub fn is_paused(self) -> bool {
self == Self::Paused
}
}
pub struct SweepSession {
run: SweepRun,
pub model_name: String,
pub output_dir: Option<PathBuf>,
pub warnings: Vec<String>,
pub report: Option<SweepReport>,
pub failure: Option<String>,
pub latest_search_update: Option<Arc<SearchUpdate>>,
pub latest_generation: u64,
finished_generations: u64,
pub outline: Option<Box<SweepOutline>>,
pub kept: Option<Box<KeptDraft>>,
pub execution: SessionExecution,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct SessionExecution {
pub concurrency: Concurrency,
pub memory_budget: Option<u64>,
pub gpu_memory_budget: Option<u64>,
}
impl SessionExecution {
pub fn recorded(recorded: &ManifestExecution) -> Self {
Self {
concurrency: Concurrency::Auto,
memory_budget: recorded.memory_budget,
gpu_memory_budget: recorded.gpu_memory_budget,
}
}
pub fn resume_text(&self, noun: &str) -> Option<String> {
let budgets = budget_text(self.memory_budget, self.gpu_memory_budget)?;
Some(format!(
"Missing runs will run with Memory budget {budgets}, as the {noun} recorded."
))
}
}
impl SweepSession {
pub fn start(
app: &mut AppState,
spec: SweepSpec,
execution: SessionExecution,
output_dir: Option<PathBuf>,
) -> Result<Self, String> {
let entry = app.lookup(&spec.model).map_err(|error| lookup_message(&error))?.clone();
let model_name = entry.name().to_owned();
let output = match &output_dir {
None => SweepOutput::Memory,
#[cfg(not(target_arch = "wasm32"))]
Some(dir) => SweepOutput::Directory(dir.clone()),
#[cfg(target_arch = "wasm32")]
Some(_) => return Err("Writing results to a folder is unavailable in a browser".to_owned()),
};
let mut options = SweepRunOptions::new(provenance(app.product.host));
options.concurrency = execution.concurrency;
options.memory_budget = execution.memory_budget;
options.gpu_memory_budget = execution.gpu_memory_budget;
options.wake = Some(app.repaint_waker());
let run = SweepRun::start(entry, None, spec, output, options).map_err(|error| describe_error(&error))?;
app.pause_simulation();
Ok(Self {
run,
model_name,
output_dir,
warnings: Vec::new(),
report: None,
failure: None,
latest_search_update: None,
latest_generation: 0,
finished_generations: 0,
outline: None,
kept: None,
execution,
})
}
#[cfg(not(target_arch = "wasm32"))]
pub fn resume_folder(
app: &mut AppState,
folder: PathBuf,
model_id: &str,
execution: SessionExecution,
) -> Result<Self, String> {
let entry = app.lookup(model_id).map_err(|error| lookup_message(&error))?.clone();
let model_name = entry.name().to_owned();
let mut options = SweepRunOptions::new(provenance(app.product.host));
options.concurrency = execution.concurrency;
options.memory_budget = execution.memory_budget;
options.gpu_memory_budget = execution.gpu_memory_budget;
options.wake = Some(app.repaint_waker());
let run = SweepRun::resume_directory(entry, None, &folder, options).map_err(|error| describe_error(&error))?;
app.pause_simulation();
Ok(Self {
run,
model_name,
output_dir: Some(folder),
warnings: Vec::new(),
report: None,
failure: None,
latest_search_update: None,
latest_generation: 0,
finished_generations: 0,
outline: None,
kept: None,
execution,
})
}
pub fn plan(&self) -> &Arc<Plan> {
self.run.plan()
}
pub fn search_plan(&self) -> Option<&Arc<SearchPlan>> {
self.run.search_plan()
}
pub fn noun(&self) -> &'static str {
if self.search_plan().is_some() {
"search"
} else {
"sweep"
}
}
pub fn update(&mut self, dt: f64, results: &mut ResultsPanel) {
self.run.update(dt);
let mut events = Vec::new();
while let Some(event) = self.run.try_recv() {
match &event {
SweepEvent::Warned(warning) => self.warnings.push(capitalize(&warning.to_string())),
SweepEvent::Finished(record) => self.report = Some(record.report.clone()),
SweepEvent::Failed(message) => self.failure = Some(message.clone()),
SweepEvent::SearchBatchTold(update) => {
self.latest_generation = self.finished_generations;
if let Some(last) = update.generations.last() {
self.finished_generations = self.finished_generations.max(last.generation + 1);
}
self.latest_search_update = Some(Arc::clone(update));
}
SweepEvent::Planned(outline) => self.outline = Some(outline.clone()),
SweepEvent::RunFinished { .. } => {}
}
events.push(event);
}
results.ingest(events);
}
pub fn progress(&self) -> SweepProgress {
self.run.progress()
}
pub fn state(&self, progress: &SweepProgress) -> SessionState {
if self.is_running() {
return match progress.phase {
SweepPhase::Planning => SessionState::Planning,
SweepPhase::Paused => SessionState::Paused,
SweepPhase::Running | SweepPhase::Ended(_) | SweepPhase::Failed => SessionState::Running,
};
}
match (&self.report, &self.failure) {
(_, Some(_)) => SessionState::Failed,
(Some(report), None) => match report.end {
SweepEnd::Complete | SweepEnd::Planned => SessionState::Finished,
SweepEnd::Aborted => SessionState::Aborted,
SweepEnd::DeviceLost => SessionState::Stopped,
},
(None, None) => SessionState::Finished,
}
}
pub fn is_resumed(&self) -> bool {
self.kept.is_none()
}
pub fn is_running(&self) -> bool {
!self.run.is_ended()
}
pub fn is_paused(&self) -> bool {
self.run.is_paused()
}
pub fn pause(&mut self) {
self.run.pause();
}
pub fn resume(&mut self) {
self.run.resume();
}
pub fn abort(&mut self) {
self.run.abort();
}
}
fn provenance(host: BuildInfo) -> Provenance {
let arguments = std::env::args_os()
.map(|argument| argument.to_string_lossy().into_owned())
.collect();
Provenance::new(host, arguments)
}
#[cfg(test)]
mod tests {
use std::time::{Duration, Instant};
use henad_compute::fault::Fault;
use henad_core::explore::spec::SweepSpec;
use henad_explore::sweep::SweepEnd;
use std::num::NonZeroUsize;
use henad_explore::exec::Concurrency;
use henad_explore::output::manifest::ManifestExecution;
use super::{SessionExecution, SessionState, SweepSession};
use crate::state::AppState;
use crate::ui::results::ResultsPanel;
fn headless_app() -> Option<AppState> {
AppState::headless(henad_models::example_models(), true)
}
#[test]
fn a_gpu_sweep_and_the_app_keep_their_faults_apart() {
let Some(mut app) = headless_app() else {
return;
};
app.render_ctx
.faults
.set_once(Fault::refused("drawing the viewport", "a fault of the app's"));
let mut spec = SweepSpec::new("gpu_sir");
spec.fixed = vec![
("grid_width".to_owned(), "32".to_owned()),
("grid_height".to_owned(), "32".to_owned()),
];
spec.run.steps = 20;
spec.run.replicates = 2;
spec.measure.stats_every = 4;
spec.measure.series_every = 4;
let mut session =
SweepSession::start(&mut app, spec, SessionExecution::default(), None).expect("the sweep starts");
let mut results = ResultsPanel::default();
let started = Instant::now();
while session.is_running() {
assert!(
started.elapsed() < Duration::from_secs(60),
"the sweep did not end in time"
);
session.update(0.0, &mut results);
std::thread::sleep(Duration::from_millis(2));
}
let state = session.state(&session.progress());
assert_eq!(state, SessionState::Finished, "the sweep failed: {:?}", session.failure);
let report = session.report.as_ref().expect("a finished sweep has a report");
assert_eq!((report.end, report.counts.ok), (SweepEnd::Complete, 2));
assert!(
app.render_ctx.faults.take().is_some(),
"the app's fault stays with the app"
);
}
fn finish(session: &mut SweepSession) -> ResultsPanel {
let mut results = ResultsPanel::default();
let started = Instant::now();
while session.is_running() {
assert!(
started.elapsed() < Duration::from_secs(60),
"the sweep did not end in time"
);
session.update(0.0, &mut results);
std::thread::sleep(Duration::from_millis(2));
}
results
}
#[test]
fn a_resume_names_the_budgets_the_sweep_recorded() {
let recorded = ManifestExecution {
backend: "cpu".to_owned(),
concurrency: "2".to_owned(),
cpu_lanes: 2,
threads_per_lane: 1,
gpu_tracks: 0,
projected_bytes: 0,
memory_budget: Some(4 << 30),
gpu_memory_budget: Some(2 << 30),
};
let execution = SessionExecution::recorded(&recorded);
assert_eq!(
execution,
SessionExecution {
concurrency: Concurrency::Auto,
memory_budget: Some(4 << 30),
gpu_memory_budget: Some(2 << 30),
},
"the recorded concurrency is left out"
);
assert_eq!(
execution.resume_text("sweep").as_deref(),
Some("Missing runs will run with Memory budget 4.0 GB, GPU 2.0 GB, as the sweep recorded.")
);
let automatic = ManifestExecution {
concurrency: "auto".to_owned(),
memory_budget: None,
gpu_memory_budget: None,
..recorded
};
assert_eq!(SessionExecution::recorded(&automatic), SessionExecution::default());
assert_eq!(SessionExecution::default().resume_text("search"), None);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn a_resume_runs_with_the_budgets_the_sweep_recorded() {
use henad_explore::result_set::ResultSet;
let Some(mut app) = headless_app() else {
return;
};
let folder = std::env::temp_dir().join(format!("henad-app-resume-execution-{}", std::process::id()));
drop(std::fs::remove_dir_all(&folder));
let mut spec = SweepSpec::new("sir");
spec.fixed = vec![
("grid_width".to_owned(), "8".to_owned()),
("grid_height".to_owned(), "8".to_owned()),
];
spec.run.steps = 3;
spec.run.replicates = 2;
let execution = SessionExecution {
concurrency: Concurrency::Fixed(NonZeroUsize::MIN),
memory_budget: Some(1 << 30),
gpu_memory_budget: None,
};
let mut session =
SweepSession::start(&mut app, spec, execution, Some(folder.clone())).expect("the sweep starts");
finish(&mut session);
let manifest_execution = |folder: &std::path::Path| {
let set = ResultSet::open_dir(folder, usize::MAX).expect("the folder reads");
set.manifest().execution.clone()
};
let recorded = manifest_execution(&folder);
assert_eq!(recorded.concurrency, "1");
let budgets = SessionExecution {
concurrency: Concurrency::Auto,
..execution
};
assert_eq!(SessionExecution::recorded(&recorded), budgets);
let mut session =
SweepSession::resume_folder(&mut app, folder.clone(), "sir", budgets).expect("the sweep resumes");
finish(&mut session);
assert_eq!(session.state(&session.progress()), SessionState::Finished);
let resumed = manifest_execution(&folder);
assert_eq!(resumed.concurrency, "auto", "the resume ran with automatic concurrency");
assert_eq!(
SessionExecution::recorded(&resumed),
budgets,
"the resume kept the budgets"
);
drop(std::fs::remove_dir_all(&folder));
}
}