1use super::*;
3
4#[derive(Clone, Debug, Default)]
5pub struct SequentialScheduler;
6
7#[derive(Clone, Debug)]
8pub struct ParallelScheduler {
9 max_workers: usize,
10}
11
12impl ParallelScheduler {
13 pub fn new(max_workers: usize) -> Result<Self> {
14 if max_workers == 0 {
15 return Err(DagMlError::RuntimeValidation(
16 "parallel scheduler max_workers must be at least 1".to_string(),
17 ));
18 }
19 Ok(Self { max_workers })
20 }
21
22 pub fn max_workers(&self) -> usize {
23 self.max_workers
24 }
25}
26
27#[derive(Clone, Debug)]
28pub(crate) struct PhaseScope {
29 pub(crate) phase: Phase,
30 pub(crate) variant_id: Option<VariantId>,
31 pub(crate) variant: Option<VariantExecutionSpec>,
32 pub(crate) fold_id: Option<FoldId>,
33 pub(crate) seed_root: Option<u64>,
34}
35
36#[derive(Clone, Debug)]
37pub(crate) struct ReplayPredictionCacheContract {
38 pub(crate) requirement: BundlePredictionRequirement,
39 pub(crate) cache: BundlePredictionCacheRecord,
40}
41
42pub(crate) struct MaterializedReplayArtifacts {
43 pub(crate) handles: BTreeMap<NodeId, BTreeMap<String, HandleRef>>,
44 pub(crate) inputs: BTreeMap<NodeId, BTreeMap<String, ArtifactInputSpec>>,
45}
46
47fn prediction_output_ports_for_node(plan: &ExecutionPlan, node_id: &NodeId) -> Result<Vec<String>> {
48 let node = plan
49 .graph_plan
50 .graph
51 .nodes
52 .iter()
53 .find(|node| node.id == *node_id)
54 .ok_or_else(|| {
55 DagMlError::RuntimeValidation(format!(
56 "node `{node_id}` is absent from the execution graph"
57 ))
58 })?;
59 let mut ports = node
60 .ports
61 .outputs
62 .iter()
63 .filter(|port| port.kind == PortKind::Prediction)
64 .map(|port| port.name.clone())
65 .collect::<Vec<_>>();
66 ports.sort();
67 Ok(ports)
68}
69
70fn normalize_prediction_result_port(
71 node_id: &NodeId,
72 block_kind: &str,
73 producer_port: &mut Option<String>,
74 prediction_ports: &[String],
75) -> Result<()> {
76 if let Some(port) = producer_port.as_ref() {
77 if port.trim().is_empty() {
78 return Err(DagMlError::RuntimeValidation(format!(
79 "node `{node_id}` emitted {block_kind} with blank producer_port"
80 )));
81 }
82 if !prediction_ports.iter().any(|candidate| candidate == port) {
83 return Err(DagMlError::RuntimeValidation(format!(
84 "node `{node_id}` emitted {block_kind} for undeclared or non-prediction output port `{port}`; declared prediction ports are {:?}",
85 prediction_ports
86 )));
87 }
88 return Ok(());
89 }
90 match prediction_ports {
91 [only] => {
92 *producer_port = Some(only.clone());
93 Ok(())
94 }
95 [] => Err(DagMlError::RuntimeValidation(format!(
96 "node `{node_id}` emitted {block_kind} without producer_port but declares no prediction output port"
97 ))),
98 _ => Err(DagMlError::RuntimeValidation(format!(
99 "node `{node_id}` emitted {block_kind} without producer_port but declares {} prediction output ports {:?}; multi-output controllers must emit producer_port explicitly",
100 prediction_ports.len(),
101 prediction_ports
102 ))),
103 }
104}
105
106pub(crate) fn normalize_result_prediction_ports(
107 plan: &ExecutionPlan,
108 task: &NodeTask,
109 result: &mut NodeResult,
110) -> Result<()> {
111 if result.predictions.is_empty()
112 && result.observation_predictions.is_empty()
113 && result.aggregated_predictions.is_empty()
114 && result.explanations.is_empty()
115 {
116 return Ok(());
117 }
118 let prediction_ports = prediction_output_ports_for_node(plan, &task.node_plan.node_id)?;
119 for block in &mut result.predictions {
120 normalize_prediction_result_port(
121 &task.node_plan.node_id,
122 "prediction block",
123 &mut block.producer_port,
124 &prediction_ports,
125 )?;
126 }
127 for block in &mut result.observation_predictions {
128 normalize_prediction_result_port(
129 &task.node_plan.node_id,
130 "observation prediction block",
131 &mut block.producer_port,
132 &prediction_ports,
133 )?;
134 }
135 for block in &mut result.aggregated_predictions {
136 normalize_prediction_result_port(
137 &task.node_plan.node_id,
138 "aggregated prediction block",
139 &mut block.producer_port,
140 &prediction_ports,
141 )?;
142 }
143 for block in &mut result.explanations {
144 normalize_prediction_result_port(
145 &task.node_plan.node_id,
146 "explanation block",
147 &mut block.producer_port,
148 &prediction_ports,
149 )?;
150 }
151 Ok(())
152}
153
154fn validate_direct_sample_prediction_result(task: &NodeTask, result: &NodeResult) -> Result<()> {
159 if !result.observation_predictions.is_empty() {
160 return Err(DagMlError::RuntimeValidation(format!(
161 "direct terminal PREDICT node `{}` emitted observation-level predictions; relation aggregation is not permitted",
162 task.node_plan.node_id
163 )));
164 }
165 if !result.aggregated_predictions.is_empty() {
166 return Err(DagMlError::RuntimeValidation(format!(
167 "direct terminal PREDICT node `{}` emitted aggregated predictions; relation aggregation is not permitted",
168 task.node_plan.node_id
169 )));
170 }
171 Ok(())
172}
173
174#[derive(Default)]
175pub(crate) struct PhaseScopeResources<'a> {
176 pub(crate) data_provider: Option<&'a dyn RuntimeDataProvider>,
177 pub(crate) fold_set_override: Option<&'a FoldSet>,
181 pub(crate) node_filter: Option<&'a BTreeSet<NodeId>>,
185 pub(crate) suppress_inner_cv: bool,
188 pub(crate) nested_stacking: Option<NestedStackingInput<'a>>,
192 pub(crate) replay_artifact_handles: Option<&'a BTreeMap<NodeId, BTreeMap<String, HandleRef>>>,
193 pub(crate) replay_artifact_inputs:
194 Option<&'a BTreeMap<NodeId, BTreeMap<String, ArtifactInputSpec>>>,
195 pub(crate) replay_bundle_id: Option<&'a BundleId>,
196 pub(crate) data_envelopes: Option<&'a BTreeMap<String, ExternalDataPlanEnvelope>>,
197 pub(crate) prediction_cache_store: Option<&'a dyn RuntimePredictionCacheStore>,
198 pub(crate) prediction_cache_contracts:
199 Option<&'a BTreeMap<String, ReplayPredictionCacheContract>>,
200 pub(crate) direct_sample_prediction_only: bool,
205 pub(crate) artifact_store: Option<&'a mut InMemoryArtifactStore>,
206}
207
208#[derive(Clone, Copy, Debug, Eq, PartialEq)]
209enum HpoCandidateFitCvOutcome {
210 Completed,
211 Pruned,
212}
213
214struct HpoFoldFeedback<'a> {
215 trial_id: i64,
216 selection: &'a RuntimeHpoSelectionTarget,
217 session: &'a mut dyn RuntimeTunerSession,
218}
219
220fn validate_hpo_progressive_fold_topology(plan: &ExecutionPlan) -> Result<()> {
221 if nested_stacking_campaign_plan(plan)?.is_some() {
222 return Err(DagMlError::RuntimeValidation(
223 "runtime HPO progressive pruning does not support nested-stacking FIT_CV; the scheduler cannot attest one report-grade intermediate per outer fold"
224 .to_string(),
225 ));
226 }
227 let fold_set = plan.fold_set.as_ref().ok_or_else(|| {
228 DagMlError::RuntimeValidation(
229 "runtime HPO progressive pruning requires an explicit validated fold set".to_string(),
230 )
231 })?;
232 if fold_set.partition_mode != FoldPartitionMode::Partition {
233 return Err(DagMlError::RuntimeValidation(
234 "runtime HPO progressive pruning requires FoldPartitionMode::Partition; resampled folds cannot attest one stable validation resource step per sample"
235 .to_string(),
236 ));
237 }
238 i32::try_from(fold_set.folds.len()).map_err(|_| {
239 DagMlError::RuntimeValidation(
240 "runtime HPO fold count exceeds the native intermediate step range".to_string(),
241 )
242 })?;
243 Ok(())
244}
245
246impl SequentialScheduler {
247 pub fn execute_hpo_campaign(
254 &self,
255 plan: &ExecutionPlan,
256 controllers: &RuntimeControllerRegistry,
257 data_provider: &dyn RuntimeDataProvider,
258 ctx: &RunContext,
259 hpo: &RuntimeHpoExecutionContext,
260 ) -> Result<RuntimeHpoCampaignResult> {
261 plan.validate()?;
262 hpo.validate_for_plan(plan)?;
263 validate_hpo_progressive_fold_topology(plan)?;
264 let controller = controllers.get(&hpo.controller_id).ok_or_else(|| {
265 DagMlError::RuntimeValidation(format!(
266 "runtime HPO campaign controller `{}` is not registered",
267 hpo.controller_id
268 ))
269 })?;
270 let task = RuntimeHpoCampaignTask {
271 run_id: ctx.run_id.clone(),
272 operation_id: hpo.operation_id.clone(),
273 controller_id: hpo.controller_id.clone(),
274 target_node_id: hpo.target_node_id.clone(),
275 seed: ctx.root_seed,
276 };
277 let mut session = controller.create_tuner_session(&task, hpo)?;
278 let history_at_start = session.trial_history_len()?;
279 if history_at_start > hpo.trial_budget_total {
280 return Err(DagMlError::RuntimeValidation(format!(
281 "runtime HPO restored native history ({history_at_start}) exceeds total trial budget ({})",
282 hpo.trial_budget_total
283 )));
284 }
285 let remaining_trials = hpo.trial_budget_total - history_at_start;
286 let mut candidates = Vec::new();
287 let mut proposed_variant_ids = BTreeSet::new();
288 let mut trial_variants = BTreeMap::new();
293 let mut incumbent_variants = hpo.resume_variants.clone();
294 let mut terminal_trials = BTreeMap::new();
295 let mut completed_proposals = Vec::new();
296 let mut completed_reports = Vec::new();
297
298 for _ in 0..remaining_trials {
299 let Some(proposal) = session.ask()? else {
300 break;
301 };
302 if trial_variants
303 .insert(proposal.trial_id, proposal.variant.variant_id.clone())
304 .is_some()
305 {
306 return Err(DagMlError::RuntimeValidation(format!(
307 "runtime HPO session proposed duplicate trial `{}`",
308 proposal.trial_id
309 )));
310 }
311 if incumbent_variants
312 .insert(proposal.trial_id, proposal.variant.variant_id.clone())
313 .is_some()
314 {
315 return Err(DagMlError::RuntimeValidation(format!(
316 "runtime HPO session reused restored trial `{}`",
317 proposal.trial_id
318 )));
319 }
320 if !proposed_variant_ids.insert(proposal.variant.variant_id.clone()) {
321 return Err(DagMlError::RuntimeValidation(format!(
322 "runtime HPO session proposed duplicate variant `{}`",
323 proposal.variant.variant_id
324 )));
325 }
326 let mut candidate_plan = plan.clone();
327 candidate_plan.variants = vec![proposal.variant.clone()];
328 candidate_plan.validate()?;
329 let mut candidate_ctx =
330 RunContext::new(ctx.run_id.clone(), proposal.variant.seed.or(ctx.root_seed));
331 candidate_ctx.variant_id = Some(proposal.variant.variant_id.clone());
332
333 let evaluation = {
334 let mut feedback = HpoFoldFeedback {
335 trial_id: proposal.trial_id,
336 selection: &hpo.selection,
337 session: session.as_mut(),
338 };
339 self.execute_hpo_candidate_fit_cv(
340 &candidate_plan,
341 controllers,
342 data_provider,
343 &mut candidate_ctx,
344 &mut feedback,
345 )
346 };
347 let evaluation = match evaluation {
348 Ok(evaluation) => evaluation,
349 Err(error) => {
350 session.tell(
351 proposal.trial_id,
352 RuntimeHpoTerminal::Failed {
353 failure: RuntimeHpoFailure {
354 code: "DAGML_CV_ERROR".to_string(),
355 message: error.to_string(),
356 retryable: false,
357 },
358 },
359 )?;
360 terminal_trials.insert(proposal.trial_id, HpoTrialTerminalState::Failed);
361 continue;
362 }
363 };
364 if evaluation == HpoCandidateFitCvOutcome::Pruned {
365 terminal_trials.insert(proposal.trial_id, HpoTrialTerminalState::Pruned);
366 continue;
367 }
368 if let Err(error) = candidate_ctx
369 .collect_cross_fold_validation_scores(plan_oof_partition_mode(&candidate_plan))
370 {
371 session.tell(
372 proposal.trial_id,
373 RuntimeHpoTerminal::Failed {
374 failure: RuntimeHpoFailure {
375 code: "DAGML_SCORE_ERROR".to_string(),
376 message: error.to_string(),
377 retryable: false,
378 },
379 },
380 )?;
381 terminal_trials.insert(proposal.trial_id, HpoTrialTerminalState::Failed);
382 continue;
383 }
384 let report = candidate_ctx
385 .score_collector
386 .iter()
387 .find(|report| {
388 report.producer_node == hpo.selection.producer_node
389 && report.producer_port.as_deref()
390 == Some(hpo.selection.producer_port.as_str())
391 && report.partition == PredictionPartition::Validation
392 && report
393 .fold_id
394 .as_ref()
395 .is_some_and(|fold| fold.as_str() == "avg")
396 })
397 .cloned();
398 let Some(mut report) = report else {
399 session.tell(
400 proposal.trial_id,
401 RuntimeHpoTerminal::Failed {
402 failure: RuntimeHpoFailure {
403 code: "DAGML_SCORE_MISSING".to_string(),
404 message: format!(
405 "runtime HPO trial `{}` emitted no target OOF average",
406 proposal.trial_id
407 ),
408 retryable: false,
409 },
410 },
411 )?;
412 terminal_trials.insert(proposal.trial_id, HpoTrialTerminalState::Failed);
413 continue;
414 };
415 report.variant_id = Some(proposal.variant.variant_id.clone());
416 let score = report
417 .metrics
418 .get(hpo.selection.metric.name())
419 .copied()
420 .filter(|score| score.is_finite());
421 let Some(score) = score else {
422 session.tell(
423 proposal.trial_id,
424 RuntimeHpoTerminal::Failed {
425 failure: RuntimeHpoFailure {
426 code: "DAGML_SCORE_NONFINITE".to_string(),
427 message: format!(
428 "runtime HPO trial `{}` emitted no finite `{}` score",
429 proposal.trial_id,
430 hpo.selection.metric.name()
431 ),
432 retryable: false,
433 },
434 },
435 )?;
436 terminal_trials.insert(proposal.trial_id, HpoTrialTerminalState::Failed);
437 continue;
438 };
439 session.tell(proposal.trial_id, RuntimeHpoTerminal::Completed { score })?;
440 terminal_trials.insert(proposal.trial_id, HpoTrialTerminalState::Completed);
441 completed_proposals.push(proposal.clone());
442 completed_reports.push(RuntimeHpoCompletedReport {
443 trial_id: proposal.trial_id,
444 variant_id: proposal.variant.variant_id.clone(),
445 report: report.clone(),
446 });
447
448 let mut validation_reports = candidate_ctx
449 .score_collector
450 .iter()
451 .filter(|item| item.partition == PredictionPartition::Validation)
452 .cloned()
453 .collect::<Vec<_>>();
454 for item in &mut validation_reports {
455 item.variant_id = Some(proposal.variant.variant_id.clone());
456 }
457 candidates.push(RuntimeHpoCandidateEvaluation {
458 validation_predictions: capture_variant_validation_predictions(
459 &proposal.variant.variant_id,
460 None,
461 &candidate_ctx,
462 ),
463 lineage: candidate_ctx.lineage.records().cloned().collect(),
464 proposal,
465 score,
466 validation_reports,
467 });
468 }
469
470 let history_at_checkpoint = session.trial_history_len()?;
471 if history_at_checkpoint != hpo.trial_budget_total {
472 return Err(DagMlError::RuntimeValidation(format!(
473 "runtime HPO native history ended at {history_at_checkpoint}, expected total trial budget {}",
474 hpo.trial_budget_total
475 )));
476 }
477
478 let checkpoint = RuntimeHpoCheckpointResult {
479 artifact: session.checkpoint()?,
480 provenance: hpo.provenance.clone(),
481 operation_id: hpo.operation_id.clone(),
482 controller_id: hpo.controller_id.clone(),
483 target_node_id: hpo.target_node_id.clone(),
484 completed_proposals,
485 completed_reports,
486 trial_history_len: history_at_checkpoint,
487 };
488 validate_hpo_checkpoint_result(
489 &checkpoint,
490 hpo,
491 &trial_variants,
492 &terminal_trials,
493 history_at_start,
494 )?;
495 let incumbent = session.incumbent(&incumbent_variants)?.ok_or_else(|| {
496 DagMlError::RuntimeValidation(
497 "native HPO campaign has no completed native incumbent after terminalization"
498 .to_string(),
499 )
500 })?;
501 if incumbent.metric != hpo.selection.metric.name()
502 || incumbent.direction != hpo.selection.direction
503 || incumbent_variants.get(&incumbent.trial_id) != Some(&incumbent.variant_id)
504 || !incumbent.score.is_finite()
505 {
506 return Err(DagMlError::RuntimeValidation(
507 "native HPO incumbent is not bound to this scheduler campaign's metric, direction, trial, and variant"
508 .to_string(),
509 ));
510 }
511 let terminal_trials = session.terminal_trial_snapshots(&incumbent_variants)?;
512 if terminal_trials.len() != history_at_checkpoint as usize
513 || terminal_trials
514 .windows(2)
515 .any(|pair| pair[0].trial.id >= pair[1].trial.id)
516 {
517 return Err(DagMlError::RuntimeValidation(
518 "native HPO terminal ledger is not a complete strictly ordered history".to_string(),
519 ));
520 }
521 Ok(RuntimeHpoCampaignResult {
522 operation_id: hpo.operation_id.clone(),
523 controller_id: hpo.controller_id.clone(),
524 target_node_id: hpo.target_node_id.clone(),
525 candidates,
526 checkpoint,
527 incumbent,
528 terminal_trials,
529 })
530 }
531
532 fn execute_hpo_candidate_fit_cv(
533 &self,
534 plan: &ExecutionPlan,
535 controllers: &RuntimeControllerRegistry,
536 data_provider: &dyn RuntimeDataProvider,
537 ctx: &mut RunContext,
538 feedback: &mut HpoFoldFeedback<'_>,
539 ) -> Result<HpoCandidateFitCvOutcome> {
540 let candidate_plan = plan;
541 ctx.configure_global_oof_aggregation(candidate_plan, data_provider)?;
542 let fold_ids = candidate_plan
543 .fold_set
544 .as_ref()
545 .expect("progressive HPO topology was preflighted")
546 .folds
547 .iter()
548 .map(|fold| Some(fold.fold_id.clone()))
549 .collect::<Vec<_>>();
550 let variant = candidate_plan
551 .variants
552 .first()
553 .expect("candidate plan has exactly one variant");
554 for (step, fold_id) in fold_ids.into_iter().enumerate() {
555 let score_start = ctx.score_collector.len();
556 self.execute_phase_scope(
557 candidate_plan,
558 controllers,
559 ctx,
560 PhaseScope {
561 phase: Phase::FitCv,
562 variant_id: Some(variant.variant_id.clone()),
563 variant: Some(VariantExecutionSpec::from_plan(variant)),
564 fold_id: fold_id.clone(),
565 seed_root: variant.seed.or(ctx.root_seed),
566 },
567 PhaseScopeResources {
568 data_provider: Some(data_provider),
569 ..Default::default()
570 },
571 )?;
572 let fold_reports = ctx.score_collector[score_start..]
573 .iter()
574 .filter(|report| {
575 report.producer_node == feedback.selection.producer_node
576 && report.producer_port.as_deref()
577 == Some(feedback.selection.producer_port.as_str())
578 && report.partition == PredictionPartition::Validation
579 && report.fold_id == fold_id
580 })
581 .collect::<Vec<_>>();
582 let [fold_report] = fold_reports.as_slice() else {
583 return Err(DagMlError::RuntimeValidation(format!(
584 "runtime HPO trial `{}` must emit exactly one target validation report per fold; fold {fold_id:?} emitted {}",
585 feedback.trial_id,
586 fold_reports.len()
587 )));
588 };
589 let score = fold_report
590 .metrics
591 .get(feedback.selection.metric.name())
592 .copied()
593 .filter(|score| score.is_finite())
594 .ok_or_else(|| {
595 DagMlError::RuntimeValidation(format!(
596 "runtime HPO trial `{}` fold {fold_id:?} emitted no finite `{}` intermediate score",
597 feedback.trial_id,
598 feedback.selection.metric.name()
599 ))
600 })?;
601 let step = i32::try_from(step).map_err(|_| {
602 DagMlError::RuntimeValidation(
603 "runtime HPO fold intermediate count exceeds i32".to_string(),
604 )
605 })?;
606 if feedback
607 .session
608 .report_intermediate(RuntimeHpoIntermediate {
609 trial_id: feedback.trial_id,
610 step,
611 score,
612 })?
613 == RuntimeHpoIntermediateOutcome::Pruned
614 {
615 return Ok(HpoCandidateFitCvOutcome::Pruned);
616 }
617 }
618 Ok(HpoCandidateFitCvOutcome::Completed)
619 }
620
621 pub fn execute_phase(
622 &self,
623 plan: &ExecutionPlan,
624 controllers: &RuntimeControllerRegistry,
625 ctx: &mut RunContext,
626 phase: Phase,
627 ) -> Result<Vec<NodeResult>> {
628 plan.validate()?;
629 let variant_id = ctx.variant_id.clone();
630 let seed_root = ctx.root_seed;
631 self.execute_phase_scope(
632 plan,
633 controllers,
634 ctx,
635 PhaseScope {
636 phase,
637 variant_id,
638 variant: None,
639 fold_id: None,
640 seed_root,
641 },
642 PhaseScopeResources::default(),
643 )
644 }
645
646 pub fn execute_phase_with_data_provider(
647 &self,
648 plan: &ExecutionPlan,
649 controllers: &RuntimeControllerRegistry,
650 data_provider: &dyn RuntimeDataProvider,
651 ctx: &mut RunContext,
652 phase: Phase,
653 ) -> Result<Vec<NodeResult>> {
654 plan.validate()?;
655 let variant_id = ctx.variant_id.clone();
656 let seed_root = ctx.root_seed;
657 self.execute_phase_scope(
658 plan,
659 controllers,
660 ctx,
661 PhaseScope {
662 phase,
663 variant_id,
664 variant: None,
665 fold_id: None,
666 seed_root,
667 },
668 PhaseScopeResources {
669 data_provider: Some(data_provider),
670 ..Default::default()
671 },
672 )
673 }
674
675 pub fn execute_campaign_phase(
676 &self,
677 plan: &ExecutionPlan,
678 controllers: &RuntimeControllerRegistry,
679 ctx: &mut RunContext,
680 phase: Phase,
681 ) -> Result<Vec<NodeResult>> {
682 plan.validate()?;
683 if phase == Phase::FitCv && nested_stacking_campaign_plan(plan)?.is_some() {
684 return Err(DagMlError::RuntimeValidation(
685 "nested stacking FIT_CV requires execute_campaign_phase_with_data_provider so the scheduler can materialize parent-bound inner folds"
686 .to_string(),
687 ));
688 }
689 let mut results = Vec::new();
690 let fold_ids = if phase == Phase::FitCv {
691 plan.fold_set
692 .as_ref()
693 .map(|fold_set| {
694 fold_set
695 .folds
696 .iter()
697 .map(|fold| Some(fold.fold_id.clone()))
698 .collect::<Vec<_>>()
699 })
700 .unwrap_or_else(|| vec![None])
701 } else {
702 vec![None]
703 };
704 for variant in &plan.variants {
705 if ctx
706 .variant_id
707 .as_ref()
708 .is_some_and(|requested| requested != &variant.variant_id)
709 {
710 continue;
711 }
712 for fold_id in &fold_ids {
713 let seed_root = variant.seed.or(ctx.root_seed);
714 results.extend(self.execute_phase_scope(
715 plan,
716 controllers,
717 ctx,
718 PhaseScope {
719 phase,
720 variant_id: Some(variant.variant_id.clone()),
721 variant: Some(VariantExecutionSpec::from_plan(variant)),
722 fold_id: fold_id.clone(),
723 seed_root,
724 },
725 PhaseScopeResources::default(),
726 )?);
727 }
728 }
729 Ok(results)
730 }
731
732 pub fn execute_campaign_phase_with_data_provider(
733 &self,
734 plan: &ExecutionPlan,
735 controllers: &RuntimeControllerRegistry,
736 data_provider: &dyn RuntimeDataProvider,
737 ctx: &mut RunContext,
738 phase: Phase,
739 ) -> Result<Vec<NodeResult>> {
740 plan.validate()?;
741 if phase == Phase::FitCv {
742 ctx.configure_global_oof_aggregation(plan, data_provider)?;
743 if let Some(nested) = nested_stacking_campaign_plan(plan)? {
744 return self.execute_nested_stacking_fit_cv(
745 plan,
746 controllers,
747 data_provider,
748 ctx,
749 &nested,
750 );
751 }
752 }
753 let mut results = Vec::new();
754 if phase == Phase::Refit {
755 results.extend(self.execute_stacking_refit_oof(
756 plan,
757 controllers,
758 data_provider,
759 ctx,
760 )?);
761 }
762 let fold_ids = if phase == Phase::FitCv {
763 plan.fold_set
764 .as_ref()
765 .map(|fold_set| {
766 fold_set
767 .folds
768 .iter()
769 .map(|fold| Some(fold.fold_id.clone()))
770 .collect::<Vec<_>>()
771 })
772 .unwrap_or_else(|| vec![None])
773 } else {
774 vec![None]
775 };
776 for variant in &plan.variants {
777 if ctx
778 .variant_id
779 .as_ref()
780 .is_some_and(|requested| requested != &variant.variant_id)
781 {
782 continue;
783 }
784 for fold_id in &fold_ids {
785 let seed_root = variant.seed.or(ctx.root_seed);
786 results.extend(self.execute_phase_scope(
787 plan,
788 controllers,
789 ctx,
790 PhaseScope {
791 phase,
792 variant_id: Some(variant.variant_id.clone()),
793 variant: Some(VariantExecutionSpec::from_plan(variant)),
794 fold_id: fold_id.clone(),
795 seed_root,
796 },
797 PhaseScopeResources {
798 data_provider: Some(data_provider),
799 ..Default::default()
800 },
801 )?);
802 }
803 }
804 Ok(results)
805 }
806
807 pub fn execute_campaign_phase_with_data_provider_and_artifact_store(
808 &self,
809 plan: &ExecutionPlan,
810 controllers: &RuntimeControllerRegistry,
811 data_provider: &dyn RuntimeDataProvider,
812 artifact_store: &mut InMemoryArtifactStore,
813 ctx: &mut RunContext,
814 phase: Phase,
815 ) -> Result<Vec<NodeResult>> {
816 plan.validate()?;
817 if phase == Phase::FitCv {
818 ctx.configure_global_oof_aggregation(plan, data_provider)?;
819 if let Some(nested) = nested_stacking_campaign_plan(plan)? {
820 return self.execute_nested_stacking_fit_cv(
824 plan,
825 controllers,
826 data_provider,
827 ctx,
828 &nested,
829 );
830 }
831 }
832 let mut results = Vec::new();
833 if phase == Phase::Refit {
834 results.extend(self.execute_stacking_refit_oof(
835 plan,
836 controllers,
837 data_provider,
838 ctx,
839 )?);
840 }
841 let fold_ids = if phase == Phase::FitCv {
842 plan.fold_set
843 .as_ref()
844 .map(|fold_set| {
845 fold_set
846 .folds
847 .iter()
848 .map(|fold| Some(fold.fold_id.clone()))
849 .collect::<Vec<_>>()
850 })
851 .unwrap_or_else(|| vec![None])
852 } else {
853 vec![None]
854 };
855 for variant in &plan.variants {
856 if ctx
857 .variant_id
858 .as_ref()
859 .is_some_and(|requested| requested != &variant.variant_id)
860 {
861 continue;
862 }
863 for fold_id in &fold_ids {
864 let seed_root = variant.seed.or(ctx.root_seed);
865 results.extend(self.execute_phase_scope(
866 plan,
867 controllers,
868 ctx,
869 PhaseScope {
870 phase,
871 variant_id: Some(variant.variant_id.clone()),
872 variant: Some(VariantExecutionSpec::from_plan(variant)),
873 fold_id: fold_id.clone(),
874 seed_root,
875 },
876 PhaseScopeResources {
877 data_provider: Some(data_provider),
878 artifact_store: Some(&mut *artifact_store),
879 ..Default::default()
880 },
881 )?);
882 }
883 }
884 Ok(results)
885 }
886
887 fn execute_stacking_refit_oof(
894 &self,
895 plan: &ExecutionPlan,
896 controllers: &RuntimeControllerRegistry,
897 data_provider: &dyn RuntimeDataProvider,
898 ctx: &mut RunContext,
899 ) -> Result<Vec<NodeResult>> {
900 let Some(nested) = nested_stacking_campaign_plan(plan)? else {
901 return Ok(Vec::new());
902 };
903 let Some(folds) = &nested.refit_fold_set else {
904 return Ok(Vec::new());
905 };
906 let outer_fold_ids = nested
907 .outer_scopes
908 .iter()
909 .map(|scope| scope.outer_fold_id.clone())
910 .collect::<BTreeSet<_>>();
911 if let Some(existing) = &ctx.validation_scoring_fold_ids {
912 if existing != &outer_fold_ids {
913 return Err(DagMlError::RuntimeValidation(
914 "stacking REFIT OOF cannot reuse a different outer evaluation scope"
915 .to_string(),
916 ));
917 }
918 } else {
919 ctx.validation_scoring_fold_ids = Some(outer_fold_ids);
920 }
921 let mut results = Vec::new();
922 for variant in &plan.variants {
923 if ctx
924 .variant_id
925 .as_ref()
926 .is_some_and(|id| id != &variant.variant_id)
927 {
928 continue;
929 }
930 for fold in &folds.folds {
931 results.extend(self.execute_phase_scope(
932 plan,
933 controllers,
934 ctx,
935 PhaseScope {
936 phase: Phase::FitCv,
937 variant_id: Some(variant.variant_id.clone()),
938 variant: Some(VariantExecutionSpec::from_plan(variant)),
939 fold_id: Some(fold.fold_id.clone()),
940 seed_root: variant.seed.or(ctx.root_seed),
941 },
942 PhaseScopeResources {
943 data_provider: Some(data_provider),
944 fold_set_override: Some(folds),
945 node_filter: Some(&nested.base_node_ids),
946 suppress_inner_cv: true,
947 ..Default::default()
948 },
949 )?);
950 }
951 }
952 Ok(results)
953 }
954
955 fn execute_nested_stacking_fit_cv(
964 &self,
965 plan: &ExecutionPlan,
966 controllers: &RuntimeControllerRegistry,
967 data_provider: &dyn RuntimeDataProvider,
968 ctx: &mut RunContext,
969 nested: &NestedStackingCampaignPlan,
970 ) -> Result<Vec<NodeResult>> {
971 let parent_fold_ids = nested
972 .outer_scopes
973 .iter()
974 .map(|outer| outer.outer_fold_id.clone())
975 .collect::<BTreeSet<_>>();
976 if let Some(existing) = &ctx.validation_scoring_fold_ids {
977 if existing != &parent_fold_ids {
978 return Err(DagMlError::RuntimeValidation(
979 "nested stacking cannot reuse a run context with a different report-grade outer fold set"
980 .to_string(),
981 ));
982 }
983 } else {
984 ctx.validation_scoring_fold_ids = Some(parent_fold_ids);
985 }
986 let mut results = Vec::new();
987 for variant in &plan.variants {
988 if ctx
989 .variant_id
990 .as_ref()
991 .is_some_and(|requested| requested != &variant.variant_id)
992 {
993 continue;
994 }
995 let seed_root = variant.seed.or(ctx.root_seed);
996 let variant_id = Some(variant.variant_id.clone());
997 let variant_spec = Some(VariantExecutionSpec::from_plan(variant));
998 for outer in &nested.outer_scopes {
999 for inner_fold in &outer.inner.inner_fold_set.folds {
1000 results.extend(self.execute_phase_scope(
1001 plan,
1002 controllers,
1003 ctx,
1004 PhaseScope {
1005 phase: Phase::FitCv,
1006 variant_id: variant_id.clone(),
1007 variant: variant_spec.clone(),
1008 fold_id: Some(inner_fold.fold_id.clone()),
1009 seed_root,
1010 },
1011 PhaseScopeResources {
1012 data_provider: Some(data_provider),
1013 fold_set_override: Some(&outer.inner.inner_fold_set),
1014 node_filter: Some(&nested.base_node_ids),
1015 suppress_inner_cv: true,
1016 ..Default::default()
1017 },
1018 )?);
1019 }
1020
1021 results.extend(self.execute_phase_scope(
1024 plan,
1025 controllers,
1026 ctx,
1027 PhaseScope {
1028 phase: Phase::FitCv,
1029 variant_id: variant_id.clone(),
1030 variant: variant_spec.clone(),
1031 fold_id: Some(outer.outer_fold_id.clone()),
1032 seed_root,
1033 },
1034 PhaseScopeResources {
1035 data_provider: Some(data_provider),
1036 node_filter: Some(&nested.base_node_ids),
1037 suppress_inner_cv: true,
1038 ..Default::default()
1039 },
1040 )?);
1041
1042 let meta_only = BTreeSet::from([nested.meta_node_id.clone()]);
1043 results.extend(self.execute_phase_scope(
1044 plan,
1045 controllers,
1046 ctx,
1047 PhaseScope {
1048 phase: Phase::FitCv,
1049 variant_id: variant_id.clone(),
1050 variant: variant_spec.clone(),
1051 fold_id: Some(outer.outer_fold_id.clone()),
1052 seed_root,
1053 },
1054 PhaseScopeResources {
1055 data_provider: Some(data_provider),
1056 node_filter: Some(&meta_only),
1057 suppress_inner_cv: true,
1058 nested_stacking: Some(NestedStackingInput {
1059 meta_node_id: &nested.meta_node_id,
1060 inner: &outer.inner,
1061 }),
1062 ..Default::default()
1063 },
1064 )?);
1065 }
1066 }
1067 Ok(results)
1068 }
1069
1070 pub fn execute_bundle_replay(
1071 &self,
1072 replay: BundleReplayExecution<'_>,
1073 ctx: &mut RunContext,
1074 ) -> Result<Vec<NodeResult>> {
1075 self.execute_bundle_replay_with_prediction_mode(replay, ctx, false)
1076 }
1077
1078 pub(crate) fn execute_direct_sample_bundle_replay(
1082 &self,
1083 replay: BundleReplayExecution<'_>,
1084 ctx: &mut RunContext,
1085 ) -> Result<Vec<NodeResult>> {
1086 if replay.replay_request.phase != Phase::Predict {
1087 return Err(DagMlError::RuntimeValidation(
1088 "direct sample bundle replay is valid only for PREDICT".to_string(),
1089 ));
1090 }
1091 self.execute_bundle_replay_with_prediction_mode(replay, ctx, true)
1092 }
1093
1094 fn execute_bundle_replay_with_prediction_mode(
1095 &self,
1096 replay: BundleReplayExecution<'_>,
1097 ctx: &mut RunContext,
1098 direct_sample_prediction_only: bool,
1099 ) -> Result<Vec<NodeResult>> {
1100 replay.bundle.validate_against_plan(replay.plan)?;
1101 replay
1102 .replay_request
1103 .validate_for_bundle_with_prediction_cache_store(
1104 replay.bundle,
1105 replay.prediction_cache_store.is_some(),
1106 )?;
1107 replay
1108 .bundle
1109 .validate_replay_envelopes(replay.data_envelopes)?;
1110 let prediction_cache_contracts = if replay.replay_request.phase == Phase::Refit {
1111 Some(replay_prediction_cache_contracts(replay.bundle)?)
1112 } else {
1113 None
1114 };
1115 if replay.replay_request.phase == Phase::Refit {
1116 preload_replay_prediction_cache_store(
1117 replay.bundle,
1118 replay.prediction_cache_store,
1119 ctx,
1120 )?;
1121 }
1122 let replay_artifacts = materialize_replay_artifact_handles(
1123 replay.plan,
1124 replay.bundle,
1125 replay.replay_request,
1126 replay.artifact_store,
1127 ctx,
1128 )?;
1129 let selected_variant = replay
1130 .bundle
1131 .selected_variant_id
1132 .as_ref()
1133 .map(|selected| {
1134 replay
1135 .plan
1136 .variants
1137 .iter()
1138 .find(|variant| &variant.variant_id == selected)
1139 .map(VariantExecutionSpec::from_plan)
1140 .ok_or_else(|| {
1141 DagMlError::RuntimeValidation(format!(
1142 "bundle `{}` selected unknown variant `{selected}`",
1143 replay.bundle.bundle_id
1144 ))
1145 })
1146 })
1147 .transpose()?;
1148 let seed_root = selected_variant
1149 .as_ref()
1150 .and_then(|variant| variant.seed)
1151 .or(ctx.root_seed);
1152
1153 self.execute_phase_scope(
1154 replay.plan,
1155 replay.controllers,
1156 ctx,
1157 PhaseScope {
1158 phase: replay.replay_request.phase,
1159 variant_id: replay.bundle.selected_variant_id.clone(),
1160 variant: selected_variant,
1161 fold_id: None,
1162 seed_root,
1163 },
1164 PhaseScopeResources {
1165 data_provider: Some(replay.data_provider),
1166 replay_artifact_handles: Some(&replay_artifacts.handles),
1167 replay_artifact_inputs: Some(&replay_artifacts.inputs),
1168 replay_bundle_id: Some(&replay.bundle.bundle_id),
1169 data_envelopes: Some(replay.data_envelopes),
1170 prediction_cache_store: replay.prediction_cache_store,
1171 prediction_cache_contracts: prediction_cache_contracts.as_ref(),
1172 direct_sample_prediction_only,
1173 ..Default::default()
1174 },
1175 )
1176 }
1177
1178 fn execute_phase_scope(
1179 &self,
1180 plan: &ExecutionPlan,
1181 controllers: &RuntimeControllerRegistry,
1182 ctx: &mut RunContext,
1183 scope: PhaseScope,
1184 mut resources: PhaseScopeResources<'_>,
1185 ) -> Result<Vec<NodeResult>> {
1186 let _phase_span = crate::observability::phase_span(
1187 ctx.run_id.as_str(),
1188 plan.id.as_str(),
1189 scope.phase.as_str(),
1190 scope.variant_id.as_ref().map(VariantId::as_str),
1191 scope.fold_id.as_ref().map(FoldId::as_str),
1192 )
1193 .entered();
1194 let mut results = Vec::new();
1195 let mut output_handles = BTreeMap::<NodeId, BTreeMap<String, HandleRef>>::new();
1196 let mut output_data_views =
1197 BTreeMap::<NodeId, BTreeMap<String, DataProviderViewSpec>>::new();
1198 let mut input_lineage = BTreeMap::<NodeId, LineageId>::new();
1199
1200 for level in plan.node_parallel_levels_for_phase(scope.phase)? {
1201 for node_id in &level {
1202 if resources
1203 .node_filter
1204 .is_some_and(|allowed| !allowed.contains(node_id))
1205 {
1206 continue;
1207 }
1208 let node_plan = plan
1209 .node_plans
1210 .get(node_id)
1211 .expect("execution plan was validated");
1212 if let Some(reduction) = merge_reduction_mode(plan, node_plan) {
1219 if let Some(mut result) =
1220 reassemble_branch_merge(plan, node_plan, ctx, &scope, reduction)?
1221 {
1222 let task_node_plan = effective_node_plan_for_scope(node_plan, &scope)?;
1223 let task = NodeTask {
1224 inner_fold_set: None,
1225 run_id: ctx.run_id.clone(),
1226 node_plan: task_node_plan.clone(),
1227 phase: scope.phase,
1228 variant_id: scope.variant_id.clone(),
1229 variant: scope.variant.clone(),
1230 fold_id: scope.fold_id.clone(),
1231 branch_path: Vec::new(),
1232 input_handles: BTreeMap::new(),
1233 data_views: BTreeMap::new(),
1234 prediction_inputs: BTreeMap::new(),
1235 artifact_inputs: BTreeMap::new(),
1236 required_loss_attestations: NodeTask::required_loss_attestations_for(
1237 &task_node_plan,
1238 scope.phase,
1239 )?,
1240 fit_influence: FitInfluenceTask::default(),
1241 seed: None,
1242 };
1243 normalize_result_prediction_ports(plan, &task, &mut result)?;
1244 result.validate_for_task(&task)?;
1245 for prediction in &result.predictions {
1246 ctx.prediction_store.append(prediction.clone())?;
1247 }
1248 apply_result_scoring(
1249 &result,
1250 &mut ctx.score_collector,
1251 &mut ctx.regression_target_records,
1252 )?;
1253 ctx.lineage.record(result.lineage.clone())?;
1254 output_handles.insert(node_id.clone(), result.outputs.clone());
1255 input_lineage.insert(node_id.clone(), result.lineage.record_id.clone());
1256 results.push(result);
1257 }
1258 continue;
1259 }
1260 let controller = controllers.get(&node_plan.controller_id).ok_or_else(|| {
1261 DagMlError::RuntimeValidation(format!(
1262 "runtime controller `{}` is not registered",
1263 node_plan.controller_id
1264 ))
1265 })?;
1266 let collected_inputs = collect_input_handles(
1267 plan,
1268 node_plan,
1269 &output_handles,
1270 &output_data_views,
1271 &resources,
1272 ctx,
1273 &scope,
1274 )?;
1275 if collected_inputs.skip_node {
1276 continue;
1277 }
1278 let mut input_handles = collected_inputs.handles;
1279 let mut prediction_inputs = collected_inputs.prediction_inputs;
1280 if let Some(nested) = resources.nested_stacking.as_ref() {
1281 replace_nested_stacking_fit_cv_inputs(
1282 plan,
1283 node_plan,
1284 ctx,
1285 &scope,
1286 nested,
1287 &mut input_handles,
1288 &mut prediction_inputs,
1289 )?;
1290 }
1291 let mut artifact_inputs = BTreeMap::new();
1292 if let Some(node_artifact_handles) = resources
1293 .replay_artifact_handles
1294 .and_then(|handles| handles.get(node_id))
1295 {
1296 for (key, handle) in node_artifact_handles {
1297 if input_handles.insert(key.clone(), handle.clone()).is_some() {
1298 return Err(DagMlError::RuntimeValidation(format!(
1299 "node `{node_id}` received duplicate replay artifact input `{key}`"
1300 )));
1301 }
1302 }
1303 }
1304 if let Some(node_artifact_inputs) = resources
1305 .replay_artifact_inputs
1306 .and_then(|inputs| inputs.get(node_id))
1307 {
1308 for (key, spec) in node_artifact_inputs {
1309 if artifact_inputs.insert(key.clone(), spec.clone()).is_some() {
1310 return Err(DagMlError::RuntimeValidation(format!(
1311 "node `{node_id}` received duplicate replay artifact metadata `{key}`"
1312 )));
1313 }
1314 }
1315 }
1316 let task_node_plan = effective_node_plan_for_scope(node_plan, &scope)?;
1317 let inner_fold_set = (!resources.suppress_inner_cv)
1318 .then(|| {
1319 inner_fold_set_for_scope(
1320 &plan.campaign,
1321 plan.fold_set.as_ref(),
1322 node_plan,
1323 &scope,
1324 )
1325 })
1326 .transpose()?
1327 .flatten();
1328 let fit_influence = fit_influence_task_for_node(
1329 plan,
1330 &task_node_plan,
1331 &collected_inputs.data_views,
1332 )?;
1333 let task = NodeTask {
1334 inner_fold_set,
1335 run_id: ctx.run_id.clone(),
1336 node_plan: task_node_plan.clone(),
1337 phase: scope.phase,
1338 variant_id: scope.variant_id.clone(),
1339 variant: scope.variant.clone(),
1340 fold_id: scope.fold_id.clone(),
1341 branch_path: Vec::new(),
1342 input_handles,
1343 data_views: collected_inputs.data_views,
1344 prediction_inputs,
1345 artifact_inputs,
1346 required_loss_attestations: NodeTask::required_loss_attestations_for(
1347 &task_node_plan,
1348 scope.phase,
1349 )?,
1350 fit_influence,
1351 seed: derive_task_seed(
1352 scope.seed_root,
1353 scope.variant_id.as_ref(),
1354 scope.fold_id.as_ref(),
1355 &task_node_plan,
1356 scope.phase,
1357 ),
1358 };
1359 let _node_span = crate::observability::node_span(
1360 task.run_id.as_str(),
1361 plan.id.as_str(),
1362 task.phase.as_str(),
1363 task.node_plan.node_id.as_str(),
1364 task.node_plan.controller_id.as_str(),
1365 )
1366 .entered();
1367 let mut result = if task.node_plan.kind == NodeKind::Tuner {
1368 return Err(DagMlError::RuntimeValidation(format!(
1369 "tuner node `{}` requires execute_hpo_campaign with an explicit RuntimeHpoExecutionContext",
1370 task.node_plan.node_id
1371 )));
1372 } else {
1373 match resources.data_provider {
1374 Some(data_provider) => {
1375 controller.invoke_with_data_provider(&task, data_provider)?
1376 }
1377 None => controller.invoke(&task)?,
1378 }
1379 };
1380 record_fit_influence_diagnostic(&task, &mut result);
1381 normalize_result_prediction_ports(plan, &task, &mut result)?;
1382 result.validate_for_task(&task)?;
1383 if resources.direct_sample_prediction_only {
1384 validate_direct_sample_prediction_result(&task, &result)?;
1385 } else {
1386 apply_result_prediction_aggregation(
1387 plan,
1388 controllers,
1389 &task,
1390 &mut result,
1391 &resources,
1392 )?;
1393 }
1394 if let Some(nested) = resources.nested_stacking.as_ref() {
1395 attach_nested_stacking_input_lineage(&mut result, plan, &task, ctx, nested)?;
1396 } else {
1397 attach_coordinator_input_lineage(
1398 &mut result,
1399 plan,
1400 &task.node_plan.node_id,
1401 &input_lineage,
1402 )?;
1403 }
1404 if let Some(store) = resources.artifact_store.as_deref_mut() {
1405 if scope.phase == Phase::Refit {
1406 store.capture_refit_artifacts(&task, &result)?;
1407 }
1408 }
1409 for prediction in &result.predictions {
1410 ctx.prediction_store.append(prediction.clone())?;
1411 }
1412 for prediction in &result.aggregated_predictions {
1413 ctx.aggregated_prediction_store.append(prediction.clone())?;
1414 }
1415 apply_result_scoring(
1416 &result,
1417 &mut ctx.score_collector,
1418 &mut ctx.regression_target_records,
1419 )?;
1420 ctx.lineage.record(result.lineage.clone())?;
1421 let data_views = derive_output_data_views(plan, &task, &result)?;
1422 output_handles.insert(node_id.clone(), result.outputs.clone());
1423 output_data_views.insert(node_id.clone(), data_views);
1424 input_lineage.insert(node_id.clone(), result.lineage.record_id.clone());
1425 results.push(result);
1426 }
1427 }
1428
1429 Ok(results)
1430 }
1431}
1432
1433impl ParallelScheduler {
1434 pub fn execute_phase(
1435 &self,
1436 plan: &ExecutionPlan,
1437 controllers: &RuntimeControllerRegistry,
1438 ctx: &mut RunContext,
1439 phase: Phase,
1440 ) -> Result<Vec<NodeResult>> {
1441 plan.validate()?;
1442 let variant_id = ctx.variant_id.clone();
1443 let seed_root = ctx.root_seed;
1444 self.execute_phase_scope(
1445 plan,
1446 controllers,
1447 ctx,
1448 PhaseScope {
1449 phase,
1450 variant_id,
1451 variant: None,
1452 fold_id: None,
1453 seed_root,
1454 },
1455 PhaseScopeResources::default(),
1456 )
1457 }
1458
1459 pub fn execute_phase_with_data_provider(
1460 &self,
1461 plan: &ExecutionPlan,
1462 controllers: &RuntimeControllerRegistry,
1463 data_provider: &dyn RuntimeDataProvider,
1464 ctx: &mut RunContext,
1465 phase: Phase,
1466 ) -> Result<Vec<NodeResult>> {
1467 plan.validate()?;
1468 let variant_id = ctx.variant_id.clone();
1469 let seed_root = ctx.root_seed;
1470 self.execute_phase_scope(
1471 plan,
1472 controllers,
1473 ctx,
1474 PhaseScope {
1475 phase,
1476 variant_id,
1477 variant: None,
1478 fold_id: None,
1479 seed_root,
1480 },
1481 PhaseScopeResources {
1482 data_provider: Some(data_provider),
1483 ..Default::default()
1484 },
1485 )
1486 }
1487
1488 pub fn execute_campaign_phase(
1489 &self,
1490 plan: &ExecutionPlan,
1491 controllers: &RuntimeControllerRegistry,
1492 ctx: &mut RunContext,
1493 phase: Phase,
1494 ) -> Result<Vec<NodeResult>> {
1495 plan.validate()?;
1496 if phase == Phase::FitCv && nested_stacking_campaign_plan(plan)?.is_some() {
1497 return Err(DagMlError::RuntimeValidation(
1498 "nested stacking FIT_CV is scheduler-serial by construction; use SequentialScheduler so inner OOF evidence is retained before outer evaluation"
1499 .to_string(),
1500 ));
1501 }
1502 let mut results = Vec::new();
1503 let fold_ids = if phase == Phase::FitCv {
1504 plan.fold_set
1505 .as_ref()
1506 .map(|fold_set| {
1507 fold_set
1508 .folds
1509 .iter()
1510 .map(|fold| Some(fold.fold_id.clone()))
1511 .collect::<Vec<_>>()
1512 })
1513 .unwrap_or_else(|| vec![None])
1514 } else {
1515 vec![None]
1516 };
1517 for variant in &plan.variants {
1518 if ctx
1519 .variant_id
1520 .as_ref()
1521 .is_some_and(|requested| requested != &variant.variant_id)
1522 {
1523 continue;
1524 }
1525 for fold_id in &fold_ids {
1526 let seed_root = variant.seed.or(ctx.root_seed);
1527 results.extend(self.execute_phase_scope(
1528 plan,
1529 controllers,
1530 ctx,
1531 PhaseScope {
1532 phase,
1533 variant_id: Some(variant.variant_id.clone()),
1534 variant: Some(VariantExecutionSpec::from_plan(variant)),
1535 fold_id: fold_id.clone(),
1536 seed_root,
1537 },
1538 PhaseScopeResources::default(),
1539 )?);
1540 }
1541 }
1542 Ok(results)
1543 }
1544
1545 pub fn execute_campaign_phase_with_data_provider(
1546 &self,
1547 plan: &ExecutionPlan,
1548 controllers: &RuntimeControllerRegistry,
1549 data_provider: &dyn RuntimeDataProvider,
1550 ctx: &mut RunContext,
1551 phase: Phase,
1552 ) -> Result<Vec<NodeResult>> {
1553 plan.validate()?;
1554 if phase == Phase::FitCv {
1555 ctx.configure_global_oof_aggregation(plan, data_provider)?;
1556 }
1557 if phase == Phase::FitCv && nested_stacking_campaign_plan(plan)?.is_some() {
1558 return Err(DagMlError::RuntimeValidation(
1559 "nested stacking FIT_CV is scheduler-serial by construction; use SequentialScheduler so inner OOF evidence is retained before outer evaluation"
1560 .to_string(),
1561 ));
1562 }
1563 let mut results = Vec::new();
1564 let fold_ids = if phase == Phase::FitCv {
1565 plan.fold_set
1566 .as_ref()
1567 .map(|fold_set| {
1568 fold_set
1569 .folds
1570 .iter()
1571 .map(|fold| Some(fold.fold_id.clone()))
1572 .collect::<Vec<_>>()
1573 })
1574 .unwrap_or_else(|| vec![None])
1575 } else {
1576 vec![None]
1577 };
1578 for variant in &plan.variants {
1579 if ctx
1580 .variant_id
1581 .as_ref()
1582 .is_some_and(|requested| requested != &variant.variant_id)
1583 {
1584 continue;
1585 }
1586 for fold_id in &fold_ids {
1587 let seed_root = variant.seed.or(ctx.root_seed);
1588 results.extend(self.execute_phase_scope(
1589 plan,
1590 controllers,
1591 ctx,
1592 PhaseScope {
1593 phase,
1594 variant_id: Some(variant.variant_id.clone()),
1595 variant: Some(VariantExecutionSpec::from_plan(variant)),
1596 fold_id: fold_id.clone(),
1597 seed_root,
1598 },
1599 PhaseScopeResources {
1600 data_provider: Some(data_provider),
1601 ..Default::default()
1602 },
1603 )?);
1604 }
1605 }
1606 Ok(results)
1607 }
1608
1609 pub fn execute_campaign_phase_with_data_provider_and_artifact_store(
1610 &self,
1611 plan: &ExecutionPlan,
1612 controllers: &RuntimeControllerRegistry,
1613 data_provider: &dyn RuntimeDataProvider,
1614 artifact_store: &mut InMemoryArtifactStore,
1615 ctx: &mut RunContext,
1616 phase: Phase,
1617 ) -> Result<Vec<NodeResult>> {
1618 plan.validate()?;
1619 if phase == Phase::FitCv && nested_stacking_campaign_plan(plan)?.is_some() {
1620 return Err(DagMlError::RuntimeValidation(
1621 "nested stacking FIT_CV is scheduler-serial by construction; use SequentialScheduler so inner OOF evidence is retained before outer evaluation"
1622 .to_string(),
1623 ));
1624 }
1625 let mut results = Vec::new();
1626 let fold_ids = if phase == Phase::FitCv {
1627 plan.fold_set
1628 .as_ref()
1629 .map(|fold_set| {
1630 fold_set
1631 .folds
1632 .iter()
1633 .map(|fold| Some(fold.fold_id.clone()))
1634 .collect::<Vec<_>>()
1635 })
1636 .unwrap_or_else(|| vec![None])
1637 } else {
1638 vec![None]
1639 };
1640 for variant in &plan.variants {
1641 if ctx
1642 .variant_id
1643 .as_ref()
1644 .is_some_and(|requested| requested != &variant.variant_id)
1645 {
1646 continue;
1647 }
1648 for fold_id in &fold_ids {
1649 let seed_root = variant.seed.or(ctx.root_seed);
1650 results.extend(self.execute_phase_scope(
1651 plan,
1652 controllers,
1653 ctx,
1654 PhaseScope {
1655 phase,
1656 variant_id: Some(variant.variant_id.clone()),
1657 variant: Some(VariantExecutionSpec::from_plan(variant)),
1658 fold_id: fold_id.clone(),
1659 seed_root,
1660 },
1661 PhaseScopeResources {
1662 data_provider: Some(data_provider),
1663 artifact_store: Some(&mut *artifact_store),
1664 ..Default::default()
1665 },
1666 )?);
1667 }
1668 }
1669 Ok(results)
1670 }
1671
1672 pub fn execute_bundle_replay(
1673 &self,
1674 replay: BundleReplayExecution<'_>,
1675 ctx: &mut RunContext,
1676 ) -> Result<Vec<NodeResult>> {
1677 replay.bundle.validate_against_plan(replay.plan)?;
1678 replay
1679 .replay_request
1680 .validate_for_bundle_with_prediction_cache_store(
1681 replay.bundle,
1682 replay.prediction_cache_store.is_some(),
1683 )?;
1684 replay
1685 .bundle
1686 .validate_replay_envelopes(replay.data_envelopes)?;
1687 let prediction_cache_contracts = if replay.replay_request.phase == Phase::Refit {
1688 Some(replay_prediction_cache_contracts(replay.bundle)?)
1689 } else {
1690 None
1691 };
1692 if replay.replay_request.phase == Phase::Refit {
1693 preload_replay_prediction_cache_store(
1694 replay.bundle,
1695 replay.prediction_cache_store,
1696 ctx,
1697 )?;
1698 }
1699 let replay_artifacts = materialize_replay_artifact_handles(
1700 replay.plan,
1701 replay.bundle,
1702 replay.replay_request,
1703 replay.artifact_store,
1704 ctx,
1705 )?;
1706 let selected_variant = replay
1707 .bundle
1708 .selected_variant_id
1709 .as_ref()
1710 .map(|selected| {
1711 replay
1712 .plan
1713 .variants
1714 .iter()
1715 .find(|variant| &variant.variant_id == selected)
1716 .map(VariantExecutionSpec::from_plan)
1717 .ok_or_else(|| {
1718 DagMlError::RuntimeValidation(format!(
1719 "bundle `{}` selected unknown variant `{selected}`",
1720 replay.bundle.bundle_id
1721 ))
1722 })
1723 })
1724 .transpose()?;
1725 let seed_root = selected_variant
1726 .as_ref()
1727 .and_then(|variant| variant.seed)
1728 .or(ctx.root_seed);
1729
1730 self.execute_phase_scope(
1731 replay.plan,
1732 replay.controllers,
1733 ctx,
1734 PhaseScope {
1735 phase: replay.replay_request.phase,
1736 variant_id: replay.bundle.selected_variant_id.clone(),
1737 variant: selected_variant,
1738 fold_id: None,
1739 seed_root,
1740 },
1741 PhaseScopeResources {
1742 data_provider: Some(replay.data_provider),
1743 replay_artifact_handles: Some(&replay_artifacts.handles),
1744 replay_artifact_inputs: Some(&replay_artifacts.inputs),
1745 replay_bundle_id: Some(&replay.bundle.bundle_id),
1746 data_envelopes: Some(replay.data_envelopes),
1747 prediction_cache_store: replay.prediction_cache_store,
1748 prediction_cache_contracts: prediction_cache_contracts.as_ref(),
1749 ..Default::default()
1750 },
1751 )
1752 }
1753
1754 fn execute_phase_scope(
1755 &self,
1756 plan: &ExecutionPlan,
1757 controllers: &RuntimeControllerRegistry,
1758 ctx: &mut RunContext,
1759 scope: PhaseScope,
1760 mut resources: PhaseScopeResources<'_>,
1761 ) -> Result<Vec<NodeResult>> {
1762 let phase_span = crate::observability::phase_span(
1766 ctx.run_id.as_str(),
1767 plan.id.as_str(),
1768 scope.phase.as_str(),
1769 scope.variant_id.as_ref().map(VariantId::as_str),
1770 scope.fold_id.as_ref().map(FoldId::as_str),
1771 );
1772 let _phase_entered = phase_span.clone().entered();
1773 let plan_id = plan.id.as_str();
1775 plan.validate_parallel_controller_capabilities(self.max_workers, scope.phase)?;
1776 let mut results = Vec::new();
1777 let mut output_handles = BTreeMap::<NodeId, BTreeMap<String, HandleRef>>::new();
1778 let mut output_data_views =
1779 BTreeMap::<NodeId, BTreeMap<String, DataProviderViewSpec>>::new();
1780 let mut input_lineage = BTreeMap::<NodeId, LineageId>::new();
1781
1782 for level in plan.node_parallel_levels_for_phase(scope.phase)? {
1783 let mut prepared = Vec::<PreparedNodeTask>::new();
1784 let mut merge_nodes = Vec::<(NodeId, MergeReduction)>::new();
1791 for node_id in &level {
1792 let node_plan = plan
1793 .node_plans
1794 .get(node_id)
1795 .expect("execution plan was validated");
1796 if let Some(reduction) = merge_reduction_mode(plan, node_plan) {
1797 merge_nodes.push((node_id.clone(), reduction));
1798 continue;
1799 }
1800 let collected_inputs = collect_input_handles(
1801 plan,
1802 node_plan,
1803 &output_handles,
1804 &output_data_views,
1805 &resources,
1806 ctx,
1807 &scope,
1808 )?;
1809 if collected_inputs.skip_node {
1810 continue;
1811 }
1812 let mut input_handles = collected_inputs.handles;
1813 let mut artifact_inputs = BTreeMap::new();
1814 if let Some(node_artifact_handles) = resources
1815 .replay_artifact_handles
1816 .and_then(|handles| handles.get(node_id))
1817 {
1818 for (key, handle) in node_artifact_handles {
1819 if input_handles.insert(key.clone(), handle.clone()).is_some() {
1820 return Err(DagMlError::RuntimeValidation(format!(
1821 "node `{node_id}` received duplicate replay artifact input `{key}`"
1822 )));
1823 }
1824 }
1825 }
1826 if let Some(node_artifact_inputs) = resources
1827 .replay_artifact_inputs
1828 .and_then(|inputs| inputs.get(node_id))
1829 {
1830 for (key, spec) in node_artifact_inputs {
1831 if artifact_inputs.insert(key.clone(), spec.clone()).is_some() {
1832 return Err(DagMlError::RuntimeValidation(format!(
1833 "node `{node_id}` received duplicate replay artifact metadata `{key}`"
1834 )));
1835 }
1836 }
1837 }
1838 let task_node_plan = effective_node_plan_for_scope(node_plan, &scope)?;
1839 let inner_fold_set = inner_fold_set_for_scope(
1840 &plan.campaign,
1841 plan.fold_set.as_ref(),
1842 node_plan,
1843 &scope,
1844 )?;
1845 let fit_influence = fit_influence_task_for_node(
1846 plan,
1847 &task_node_plan,
1848 &collected_inputs.data_views,
1849 )?;
1850 prepared.push(PreparedNodeTask {
1851 node_id: node_id.clone(),
1852 task: NodeTask {
1853 inner_fold_set,
1854 run_id: ctx.run_id.clone(),
1855 node_plan: task_node_plan.clone(),
1856 phase: scope.phase,
1857 variant_id: scope.variant_id.clone(),
1858 variant: scope.variant.clone(),
1859 fold_id: scope.fold_id.clone(),
1860 branch_path: Vec::new(),
1861 input_handles,
1862 data_views: collected_inputs.data_views,
1863 prediction_inputs: collected_inputs.prediction_inputs,
1864 artifact_inputs,
1865 required_loss_attestations: NodeTask::required_loss_attestations_for(
1866 &task_node_plan,
1867 scope.phase,
1868 )?,
1869 fit_influence,
1870 seed: derive_task_seed(
1871 scope.seed_root,
1872 scope.variant_id.as_ref(),
1873 scope.fold_id.as_ref(),
1874 &task_node_plan,
1875 scope.phase,
1876 ),
1877 },
1878 });
1879 }
1880
1881 for chunk in prepared.chunks(self.max_workers) {
1882 let chunk_results = std::thread::scope(
1883 |thread_scope| -> Result<Vec<NodeResult>> {
1884 let mut handles = Vec::with_capacity(chunk.len());
1885 for prepared_task in chunk {
1886 let controller = controllers
1887 .get(&prepared_task.task.node_plan.controller_id)
1888 .ok_or_else(|| {
1889 DagMlError::RuntimeValidation(format!(
1890 "runtime controller `{}` is not registered",
1891 prepared_task.task.node_plan.controller_id
1892 ))
1893 })?;
1894 let worker_span = phase_span.clone();
1895 handles.push(thread_scope.spawn(move || {
1896 let _worker_span = worker_span.entered();
1897 let _node_span = crate::observability::node_span(
1898 prepared_task.task.run_id.as_str(),
1899 plan_id,
1900 prepared_task.task.phase.as_str(),
1901 prepared_task.task.node_plan.node_id.as_str(),
1902 prepared_task.task.node_plan.controller_id.as_str(),
1903 )
1904 .entered();
1905 let mut result =
1906 if prepared_task.task.node_plan.kind == NodeKind::Tuner {
1907 return Err(DagMlError::RuntimeValidation(format!(
1908 "tuner node `{}` requires execute_hpo_campaign with an explicit RuntimeHpoExecutionContext",
1909 prepared_task.task.node_plan.node_id
1910 )));
1911 } else {
1912 controller.invoke(&prepared_task.task)?
1918 };
1919 record_fit_influence_diagnostic(&prepared_task.task, &mut result);
1920 normalize_result_prediction_ports(
1921 plan,
1922 &prepared_task.task,
1923 &mut result,
1924 )?;
1925 result.validate_for_task(&prepared_task.task)?;
1926 Ok(result)
1927 }));
1928 }
1929 handles
1930 .into_iter()
1931 .map(|handle| {
1932 handle.join().map_err(|_| {
1933 DagMlError::RuntimeValidation(
1934 "parallel scheduler worker panicked".to_string(),
1935 )
1936 })?
1937 })
1938 .collect()
1939 },
1940 )?;
1941
1942 for (prepared_task, mut result) in chunk.iter().zip(chunk_results) {
1943 apply_result_prediction_aggregation(
1944 plan,
1945 controllers,
1946 &prepared_task.task,
1947 &mut result,
1948 &resources,
1949 )?;
1950 if let Some(nested) = resources.nested_stacking.as_ref() {
1951 attach_nested_stacking_input_lineage(
1952 &mut result,
1953 plan,
1954 &prepared_task.task,
1955 ctx,
1956 nested,
1957 )?;
1958 } else {
1959 attach_coordinator_input_lineage(
1960 &mut result,
1961 plan,
1962 &prepared_task.task.node_plan.node_id,
1963 &input_lineage,
1964 )?;
1965 }
1966 if let Some(store) = resources.artifact_store.as_deref_mut() {
1967 if scope.phase == Phase::Refit {
1968 store.capture_refit_artifacts(&prepared_task.task, &result)?;
1969 }
1970 }
1971 for prediction in &result.predictions {
1972 ctx.prediction_store.append(prediction.clone())?;
1973 }
1974 for prediction in &result.aggregated_predictions {
1975 ctx.aggregated_prediction_store.append(prediction.clone())?;
1976 }
1977 apply_result_scoring(
1978 &result,
1979 &mut ctx.score_collector,
1980 &mut ctx.regression_target_records,
1981 )?;
1982 ctx.lineage.record(result.lineage.clone())?;
1983 let data_views = derive_output_data_views(plan, &prepared_task.task, &result)?;
1984 output_handles.insert(prepared_task.node_id.clone(), result.outputs.clone());
1985 output_data_views.insert(prepared_task.node_id.clone(), data_views);
1986 input_lineage.insert(
1987 prepared_task.node_id.clone(),
1988 result.lineage.record_id.clone(),
1989 );
1990 results.push(result);
1991 }
1992 }
1993
1994 for (node_id, reduction) in &merge_nodes {
1999 let node_plan = plan
2000 .node_plans
2001 .get(node_id)
2002 .expect("execution plan was validated");
2003 if let Some(mut result) =
2004 reassemble_branch_merge(plan, node_plan, ctx, &scope, *reduction)?
2005 {
2006 let task_node_plan = effective_node_plan_for_scope(node_plan, &scope)?;
2007 let task = NodeTask {
2008 inner_fold_set: None,
2009 run_id: ctx.run_id.clone(),
2010 node_plan: task_node_plan.clone(),
2011 phase: scope.phase,
2012 variant_id: scope.variant_id.clone(),
2013 variant: scope.variant.clone(),
2014 fold_id: scope.fold_id.clone(),
2015 branch_path: Vec::new(),
2016 input_handles: BTreeMap::new(),
2017 data_views: BTreeMap::new(),
2018 prediction_inputs: BTreeMap::new(),
2019 artifact_inputs: BTreeMap::new(),
2020 required_loss_attestations: NodeTask::required_loss_attestations_for(
2021 &task_node_plan,
2022 scope.phase,
2023 )?,
2024 fit_influence: FitInfluenceTask::default(),
2025 seed: None,
2026 };
2027 normalize_result_prediction_ports(plan, &task, &mut result)?;
2028 result.validate_for_task(&task)?;
2029 for prediction in &result.predictions {
2030 ctx.prediction_store.append(prediction.clone())?;
2031 }
2032 apply_result_scoring(
2033 &result,
2034 &mut ctx.score_collector,
2035 &mut ctx.regression_target_records,
2036 )?;
2037 ctx.lineage.record(result.lineage.clone())?;
2038 output_handles.insert(node_id.clone(), result.outputs.clone());
2039 input_lineage.insert(node_id.clone(), result.lineage.record_id.clone());
2040 results.push(result);
2041 }
2042 }
2043 }
2044
2045 Ok(results)
2046 }
2047}
2048
2049#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2050enum HpoTrialTerminalState {
2051 Completed,
2052 Pruned,
2053 Failed,
2054}
2055
2056fn validate_hpo_checkpoint_result(
2057 checkpoint: &RuntimeHpoCheckpointResult,
2058 hpo: &RuntimeHpoExecutionContext,
2059 trial_variants: &BTreeMap<i64, VariantId>,
2060 terminal_trials: &BTreeMap<i64, HpoTrialTerminalState>,
2061 history_at_start: u32,
2062) -> Result<()> {
2063 checkpoint.artifact.validate().map_err(|error| {
2064 DagMlError::RuntimeValidation(format!(
2065 "runtime HPO checkpoint artifact is invalid: {error}"
2066 ))
2067 })?;
2068 if checkpoint.operation_id != hpo.operation_id
2069 || checkpoint.controller_id != hpo.controller_id
2070 || checkpoint.target_node_id != hpo.target_node_id
2071 || checkpoint.provenance != hpo.provenance
2072 {
2073 return Err(DagMlError::RuntimeValidation(
2074 "runtime HPO checkpoint provenance does not exactly match its execution context"
2075 .to_string(),
2076 ));
2077 }
2078 let proposed_count = u32::try_from(trial_variants.len()).map_err(|_| {
2079 DagMlError::RuntimeValidation(
2080 "runtime HPO scheduler proposal count does not fit u32".to_string(),
2081 )
2082 })?;
2083 if checkpoint.trial_history_len != hpo.trial_budget_total
2084 || checkpoint.trial_history_len < history_at_start
2085 || checkpoint.trial_history_len - history_at_start != proposed_count
2086 {
2087 return Err(DagMlError::RuntimeValidation(
2088 "runtime HPO checkpoint native history is inconsistent with scheduler-observed trials"
2089 .to_string(),
2090 ));
2091 }
2092 if checkpoint.artifact.binding.controller_id != hpo.controller_id.as_str()
2093 || checkpoint.artifact.binding.controller_id != hpo.study.controller_id
2094 || checkpoint.artifact.binding.study_id != hpo.study.study_id
2095 || checkpoint.artifact.methods_abi != hpo.study.methods_abi
2096 {
2097 return Err(DagMlError::RuntimeValidation(
2098 "runtime HPO checkpoint binding/controller/study does not match the active tuner"
2099 .to_string(),
2100 ));
2101 }
2102 let expected_search_space = hpo.study.search_space.fingerprint().map_err(|error| {
2103 DagMlError::RuntimeValidation(format!(
2104 "runtime HPO cannot fingerprint the configured search space: {error}"
2105 ))
2106 })?;
2107 if checkpoint.artifact.binding.search_space_fingerprint != expected_search_space {
2108 return Err(DagMlError::RuntimeValidation(
2109 "runtime HPO checkpoint search-space binding does not match the active study"
2110 .to_string(),
2111 ));
2112 }
2113
2114 let completed_trial_ids = terminal_trials
2115 .iter()
2116 .filter_map(|(trial_id, state)| {
2117 (*state == HpoTrialTerminalState::Completed).then_some(*trial_id)
2118 })
2119 .collect::<BTreeSet<_>>();
2120 let mut proposal_trial_ids = BTreeSet::new();
2121 for proposal in &checkpoint.completed_proposals {
2122 if !proposal_trial_ids.insert(proposal.trial_id) {
2123 return Err(DagMlError::RuntimeValidation(format!(
2124 "runtime HPO checkpoint has duplicate completed proposal for trial `{}`",
2125 proposal.trial_id
2126 )));
2127 }
2128 if trial_variants.get(&proposal.trial_id) != Some(&proposal.variant.variant_id) {
2129 return Err(DagMlError::RuntimeValidation(format!(
2130 "runtime HPO checkpoint proposal for trial `{}` does not exactly match its scheduler proposal",
2131 proposal.trial_id
2132 )));
2133 }
2134 }
2135 if proposal_trial_ids != completed_trial_ids {
2136 return Err(DagMlError::RuntimeValidation(
2137 "runtime HPO checkpoint proposals must cover exactly the completed trials".to_string(),
2138 ));
2139 }
2140
2141 let mut report_trial_ids = BTreeSet::new();
2142 for completed in &checkpoint.completed_reports {
2143 if !report_trial_ids.insert(completed.trial_id) {
2144 return Err(DagMlError::RuntimeValidation(format!(
2145 "runtime HPO checkpoint has duplicate completed report for trial `{}`",
2146 completed.trial_id
2147 )));
2148 }
2149 if trial_variants.get(&completed.trial_id) != Some(&completed.variant_id)
2150 || !proposal_trial_ids.contains(&completed.trial_id)
2151 {
2152 return Err(DagMlError::RuntimeValidation(format!(
2153 "runtime HPO checkpoint report for trial `{}` does not match a completed proposal",
2154 completed.trial_id
2155 )));
2156 }
2157 let report = &completed.report;
2158 if report.producer_node != hpo.selection.producer_node
2159 || report.producer_port.as_deref() != Some(hpo.selection.producer_port.as_str())
2160 || report.partition != PredictionPartition::Validation
2161 || report
2162 .fold_id
2163 .as_ref()
2164 .is_none_or(|fold| fold.as_str() != "avg")
2165 || report.variant_id.as_ref() != Some(&completed.variant_id)
2166 || !report
2167 .metrics
2168 .get(hpo.selection.metric.name())
2169 .is_some_and(|score| score.is_finite())
2170 {
2171 return Err(DagMlError::RuntimeValidation(format!(
2172 "runtime HPO checkpoint report for trial `{}` is not its one finite target OOF average",
2173 completed.trial_id
2174 )));
2175 }
2176 }
2177 if report_trial_ids != completed_trial_ids {
2178 return Err(DagMlError::RuntimeValidation(
2179 "runtime HPO checkpoint reports must cover exactly one OOF average per completed trial"
2180 .to_string(),
2181 ));
2182 }
2183 Ok(())
2184}
2185
2186pub(crate) struct PreparedNodeTask {
2187 pub(crate) node_id: NodeId,
2188 pub(crate) task: NodeTask,
2189}
2190
2191#[cfg(test)]
2194#[allow(clippy::items_after_test_module)]
2195mod hpo_scheduler_tests {
2196 use std::collections::{BTreeMap, BTreeSet};
2197 use std::sync::{Arc, Mutex};
2198
2199 use sha2::{Digest, Sha256};
2200
2201 use super::*;
2202 use crate::controller::{
2203 ArtifactPolicy, ControllerCapability, ControllerFitScope, ControllerManifest,
2204 ControllerRegistry, RngPolicy,
2205 };
2206 use crate::data::InMemoryDataProvider;
2207 use crate::fold::{FoldAssignment, FoldPartitionMode, KFoldSpec, NestedCvSpec};
2208 use crate::graph::{
2209 EdgeContract, EdgeSpec, GraphInterface, GraphSpec, NodeSpec, PortRef, PortSchema, PortSpec,
2210 };
2211 use crate::hpo::{
2212 HpoDirection, HpoMetric, HpoOptimizerConfig, HpoParameter, HpoPruner, HpoSampler,
2213 HpoSearchSpace, HpoStudyBinding, MethodsHpoStudyConfig, N4moptCheckpointArtifact,
2214 N4MOPT_ARTIFACT_KIND, N4MOPT_CHECKPOINT_SCHEMA_VERSION, N4MOPT_FORMAT,
2215 };
2216 #[cfg(feature = "methods-optimizer-local")]
2217 use crate::hpo::{MethodsHpoController, MethodsRuntime};
2218 use crate::metrics::RegressionTargetBlock;
2219 use crate::oof::PredictionBlock;
2220 use crate::plan::{build_execution_plan, SplitInvocation};
2221
2222 struct HpoTestModel {
2223 id: ControllerId,
2224 trace: Arc<Mutex<Vec<String>>>,
2225 fail_variant: Option<VariantId>,
2226 score_by_trial: bool,
2227 }
2228
2229 impl RuntimeController for HpoTestModel {
2230 fn controller_id(&self) -> &ControllerId {
2231 &self.id
2232 }
2233
2234 fn invoke(&self, task: &NodeTask) -> Result<NodeResult> {
2235 self.trace.lock().unwrap().push("model_cv".to_string());
2236 if task.variant_id.as_ref() == self.fail_variant.as_ref() {
2237 return Err(DagMlError::RuntimeValidation(
2238 "controlled HPO fold failure".to_string(),
2239 ));
2240 }
2241 let sample_id = match task.fold_id.as_ref().map(FoldId::as_str) {
2242 Some("fold:0") => SampleId::new("sample:one").unwrap(),
2243 Some("fold:1") => SampleId::new("sample:two").unwrap(),
2244 other => {
2245 return Err(DagMlError::RuntimeValidation(format!(
2246 "HPO test model received unexpected fold {other:?}"
2247 )));
2248 }
2249 };
2250 let prediction = if self.score_by_trial {
2251 let trial_id = task
2252 .variant_id
2253 .as_ref()
2254 .and_then(|variant| variant.as_str().strip_prefix("hpo:trial:"))
2255 .and_then(|value| value.parse::<i64>().ok())
2256 .ok_or_else(|| {
2257 DagMlError::RuntimeValidation(
2258 "native HPO test model received no trial variant".to_string(),
2259 )
2260 })?;
2261 let magnitude = (trial_id + 1) as f64;
2262 1.0 + magnitude * magnitude
2263 } else {
2264 1.0
2265 };
2266 Ok(NodeResult {
2267 schema_version: None,
2268 node_id: task.node_plan.node_id.clone(),
2269 outputs: BTreeMap::from([(
2270 "prediction".to_string(),
2271 HandleRef {
2272 handle: 2,
2273 kind: HandleKind::Prediction,
2274 owner_controller: self.id.clone(),
2275 },
2276 )]),
2277 predictions: vec![PredictionBlock {
2278 prediction_id: Some(format!("prediction:{}", task.fold_id.as_ref().unwrap())),
2279 producer_node: task.node_plan.node_id.clone(),
2280 producer_port: None,
2281 partition: PredictionPartition::Validation,
2282 fold_id: task.fold_id.clone(),
2283 sample_ids: vec![sample_id.clone()],
2284 values: vec![vec![prediction]],
2285 target_names: vec!["target".to_string()],
2286 }],
2287 observation_predictions: Vec::new(),
2288 aggregated_predictions: Vec::new(),
2289 explanations: Vec::new(),
2290 shape_deltas: Vec::new(),
2291 artifacts: Vec::new(),
2292 artifact_handles: BTreeMap::new(),
2293 fit_influence_diagnostics: Vec::new(),
2294 regression_targets: vec![RegressionTargetBlock {
2295 level: PredictionLevel::Sample,
2296 unit_ids: vec![PredictionUnitId::Sample(sample_id)],
2297 values: vec![vec![1.0]],
2298 target_names: vec!["target".to_string()],
2299 }],
2300 lineage: LineageRecord {
2301 record_id: LineageId::new(format!(
2302 "lineage:hpo-model:{}",
2303 task.fold_id.as_ref().unwrap()
2304 ))
2305 .unwrap(),
2306 run_id: task.run_id.clone(),
2307 node_id: task.node_plan.node_id.clone(),
2308 phase: task.phase,
2309 controller_id: self.id.clone(),
2310 controller_version: task.node_plan.controller_version.clone(),
2311 variant_id: task.variant_id.clone(),
2312 fold_id: task.fold_id.clone(),
2313 branch_path: Vec::new(),
2314 input_lineage: Vec::new(),
2315 artifact_refs: Vec::new(),
2316 params_fingerprint: task.node_plan.params_fingerprint.clone(),
2317 data_model_shape_fingerprint: None,
2318 aggregation_policy_fingerprint: None,
2319 seed: task.seed,
2320 unsafe_flags: BTreeSet::new(),
2321 metrics: BTreeMap::new(),
2322 loss_attestations: Vec::new(),
2323 early_stopping_records: Vec::new(),
2324 },
2325 })
2326 }
2327 }
2328
2329 struct HpoTestTuner {
2330 id: ControllerId,
2331 trace: Arc<Mutex<Vec<String>>>,
2332 history_len: u32,
2333 proposal_count: u32,
2334 prune_at: Option<(i64, i32)>,
2335 }
2336
2337 struct HpoTestSession {
2338 proposals: Vec<RuntimeHpoProposal>,
2339 trace: Arc<Mutex<Vec<String>>>,
2340 checkpoint: N4moptCheckpointArtifact,
2341 history_len: u32,
2342 completed: BTreeMap<i64, f64>,
2343 failed: BTreeMap<i64, RuntimeHpoFailure>,
2344 prune_at: Option<(i64, i32)>,
2345 pruned: BTreeSet<i64>,
2346 intermediates: BTreeMap<i64, Vec<crate::hpo::HpoIntermediate>>,
2347 }
2348
2349 impl RuntimeController for HpoTestTuner {
2350 fn controller_id(&self) -> &ControllerId {
2351 &self.id
2352 }
2353
2354 fn invoke(&self, task: &NodeTask) -> Result<NodeResult> {
2355 Err(DagMlError::RuntimeValidation(format!(
2356 "HPO test tuner `{}` was dispatched through generic invoke",
2357 task.node_plan.node_id
2358 )))
2359 }
2360
2361 fn create_tuner_session(
2362 &self,
2363 task: &RuntimeHpoCampaignTask,
2364 context: &RuntimeHpoExecutionContext,
2365 ) -> Result<Box<dyn RuntimeTunerSession>> {
2366 assert_eq!(task.operation_id, context.operation_id);
2367 self.trace
2368 .lock()
2369 .unwrap()
2370 .push("session_factory".to_string());
2371 let payload = vec![7_u8];
2372 let proposals = (0..self.proposal_count)
2373 .map(|offset| {
2374 let trial_id = i64::from(self.history_len + offset + 1);
2375 let mut variant = context.base_variant.clone();
2376 if self.history_len != 0 || self.proposal_count != 1 {
2377 variant.variant_id = VariantId::new(format!("hpo:trial:{trial_id}"))
2378 .map_err(|error| DagMlError::RuntimeValidation(error.to_string()))?;
2379 variant.fingerprint = format!("hpo-test-{trial_id}");
2380 }
2381 Ok(RuntimeHpoProposal { trial_id, variant })
2382 })
2383 .collect::<Result<Vec<_>>>()?;
2384 Ok(Box::new(HpoTestSession {
2385 proposals: proposals.into_iter().rev().collect(),
2386 trace: Arc::clone(&self.trace),
2387 history_len: self.history_len,
2388 completed: BTreeMap::new(),
2389 failed: BTreeMap::new(),
2390 prune_at: self.prune_at,
2391 pruned: BTreeSet::new(),
2392 intermediates: BTreeMap::new(),
2393 checkpoint: N4moptCheckpointArtifact {
2394 schema_version: N4MOPT_CHECKPOINT_SCHEMA_VERSION,
2395 artifact_kind: N4MOPT_ARTIFACT_KIND.to_string(),
2396 format: N4MOPT_FORMAT.to_string(),
2397 abi_major: crate::hpo::METHODS_ABI_MAJOR,
2398 abi_min_minor: crate::hpo::METHODS_N4MOPT_MIN_ABI_MINOR,
2399 binding: HpoStudyBinding {
2400 controller_id: context.study.controller_id.clone(),
2401 study_id: context.study.study_id.clone(),
2402 search_space_fingerprint: context
2403 .study
2404 .search_space
2405 .fingerprint()
2406 .map_err(|error| DagMlError::RuntimeValidation(error.to_string()))?,
2407 optimizer_fingerprint: "optimizer:test".to_string(),
2408 },
2409 methods_abi: context.study.methods_abi.clone(),
2410 payload_sha256: format!("{:x}", Sha256::digest(&payload)),
2411 opaque_payload: payload,
2412 },
2413 }))
2414 }
2415 }
2416
2417 impl RuntimeTunerSession for HpoTestSession {
2418 fn trial_history_len(&self) -> Result<u32> {
2419 Ok(self.history_len)
2420 }
2421
2422 fn ask(&mut self) -> Result<Option<RuntimeHpoProposal>> {
2423 self.trace.lock().unwrap().push("ask".to_string());
2424 let proposal = self.proposals.pop();
2425 if proposal.is_some() {
2426 self.history_len += 1;
2427 }
2428 Ok(proposal)
2429 }
2430
2431 fn report_intermediate(
2432 &mut self,
2433 intermediate: RuntimeHpoIntermediate,
2434 ) -> Result<RuntimeHpoIntermediateOutcome> {
2435 assert!(intermediate.score.is_finite());
2436 let should_prune = self.prune_at == Some((intermediate.trial_id, intermediate.step));
2437 self.trace.lock().unwrap().push(format!(
2438 "intermediate:{}:{}",
2439 intermediate.trial_id, intermediate.step
2440 ));
2441 self.intermediates
2442 .entry(intermediate.trial_id)
2443 .or_default()
2444 .push(crate::hpo::HpoIntermediate {
2445 sequence: i64::from(intermediate.step) + 1,
2446 step: intermediate.step,
2447 score: intermediate.score,
2448 should_prune,
2449 });
2450 if should_prune {
2451 self.pruned.insert(intermediate.trial_id);
2452 Ok(RuntimeHpoIntermediateOutcome::Pruned)
2453 } else {
2454 Ok(RuntimeHpoIntermediateOutcome::Continue)
2455 }
2456 }
2457
2458 fn tell(&mut self, trial_id: i64, terminal: RuntimeHpoTerminal) -> Result<()> {
2459 assert!(trial_id > 0);
2460 match terminal {
2461 RuntimeHpoTerminal::Completed { score } if score.is_finite() => {
2462 self.trace.lock().unwrap().push("tell".to_string());
2463 self.completed.insert(trial_id, score);
2464 }
2465 RuntimeHpoTerminal::Failed { failure } => {
2466 self.trace.lock().unwrap().push("tell_failed".to_string());
2467 self.failed.insert(trial_id, failure);
2468 }
2469 other => {
2470 return Err(DagMlError::RuntimeValidation(format!(
2471 "test HPO session received unsupported terminal state {other:?}"
2472 )));
2473 }
2474 }
2475 Ok(())
2476 }
2477
2478 fn checkpoint(&self) -> Result<N4moptCheckpointArtifact> {
2479 self.trace.lock().unwrap().push("checkpoint".to_string());
2480 Ok(self.checkpoint.clone())
2481 }
2482
2483 fn incumbent(
2484 &self,
2485 variants: &BTreeMap<i64, VariantId>,
2486 ) -> Result<Option<RuntimeHpoIncumbent>> {
2487 let Some((&trial_id, &score)) = self
2488 .completed
2489 .iter()
2490 .min_by(|left, right| left.1.total_cmp(right.1).then_with(|| left.0.cmp(right.0)))
2491 else {
2492 return Ok(None);
2493 };
2494 Ok(Some(RuntimeHpoIncumbent {
2495 trial_id,
2496 score,
2497 metric: "rmse".to_string(),
2498 direction: HpoDirection::Minimize,
2499 variant_id: variants.get(&trial_id).cloned().unwrap(),
2500 }))
2501 }
2502
2503 fn terminal_trial_snapshots(
2504 &self,
2505 variants: &BTreeMap<i64, VariantId>,
2506 ) -> Result<Vec<RuntimeHpoTerminalSnapshot>> {
2507 if self.completed.is_empty() {
2508 return Err(DagMlError::RuntimeValidation(
2509 "test HPO session has no completed trial".to_string(),
2510 ));
2511 }
2512 let completed = &self.completed;
2513 let failed = &self.failed;
2514 Ok((1..=i64::from(self.history_len))
2515 .map(|id| {
2516 let score = completed.get(&id).copied();
2517 let is_completed = score.is_some();
2518 let pruned = self.pruned.contains(&id);
2519 let failure = failed
2520 .get(&id)
2521 .map(|failure| crate::hpo::HpoFailure {
2522 code: failure.code.clone(),
2523 message: failure.message.clone(),
2524 retryable: failure.retryable,
2525 })
2526 .or_else(|| {
2527 (!is_completed && !pruned).then(|| crate::hpo::HpoFailure {
2528 code: "RESTORED_TEST_FAILURE".to_string(),
2529 message: "synthetic restored terminal".to_string(),
2530 retryable: false,
2531 })
2532 });
2533 RuntimeHpoTerminalSnapshot {
2534 trial: crate::hpo::HpoTrial {
2535 id,
2536 ask_sequence: id,
2537 terminal_sequence: Some(id),
2538 parameters: BTreeMap::new(),
2539 parameter_order: Vec::new(),
2540 status: if is_completed {
2541 crate::hpo::HpoTrialStatus::Completed
2542 } else if pruned {
2543 crate::hpo::HpoTrialStatus::Pruned
2544 } else {
2545 crate::hpo::HpoTrialStatus::Failed
2546 },
2547 score,
2548 rung: 0,
2549 duration: 0.0,
2550 intermediates: self.intermediates.get(&id).cloned().unwrap_or_default(),
2551 failure,
2552 },
2553 variant_id: variants.get(&id).cloned(),
2554 }
2555 })
2556 .collect())
2557 }
2558 }
2559
2560 fn node(id: &str, kind: NodeKind, outputs: Vec<PortSpec>) -> NodeSpec {
2561 NodeSpec {
2562 id: NodeId::new(id).unwrap(),
2563 kind,
2564 operator: None,
2565 params: BTreeMap::new(),
2566 ports: PortSchema {
2567 inputs: Vec::new(),
2568 outputs,
2569 },
2570 metadata: BTreeMap::new(),
2571 seed_label: None,
2572 }
2573 }
2574
2575 fn prediction_port(name: &str) -> PortSpec {
2576 PortSpec {
2577 name: name.to_string(),
2578 kind: PortKind::Prediction,
2579 representation: None,
2580 cardinality: crate::graph::PortCardinality::One,
2581 unit_level: None,
2582 alignment_key: None,
2583 target_level: None,
2584 description: String::new(),
2585 }
2586 }
2587
2588 fn manifest(id: &str, kind: NodeKind) -> ControllerManifest {
2589 ControllerManifest {
2590 controller_id: ControllerId::new(id).unwrap(),
2591 controller_version: "test".to_string(),
2592 operator_kind: kind,
2593 priority: 0,
2594 supported_phases: BTreeSet::from([Phase::FitCv]),
2595 input_ports: Vec::new(),
2596 output_ports: Vec::new(),
2597 data_requirements: None,
2598 capabilities: BTreeSet::from([
2599 ControllerCapability::Deterministic,
2600 ControllerCapability::EmitsPredictions,
2601 ]),
2602 operator_selectors: Vec::new(),
2603 fit_scope: ControllerFitScope::FoldTrain,
2604 rng_policy: RngPolicy::UsesCoreSeed,
2605 artifact_policy: ArtifactPolicy::Serializable,
2606 }
2607 }
2608
2609 #[test]
2610 fn hpo_campaign_invokes_registered_session_and_routes_oof_feedback() {
2611 let target = NodeId::new("model:score").unwrap();
2612 let graph = GraphSpec {
2613 id: "graph:hpo.scheduler".to_string(),
2614 interface: GraphInterface::default(),
2615 nodes: vec![node(
2616 "model:score",
2617 NodeKind::Model,
2618 vec![PortSpec {
2619 name: "prediction".to_string(),
2620 kind: PortKind::Prediction,
2621 representation: None,
2622 cardinality: crate::graph::PortCardinality::One,
2623 unit_level: None,
2624 alignment_key: None,
2625 target_level: None,
2626 description: String::new(),
2627 }],
2628 )],
2629 edges: Vec::new(),
2630 search_space_fingerprint: None,
2631 metadata: BTreeMap::new(),
2632 };
2633 let fold_set = FoldSet {
2634 id: "folds:hpo".to_string(),
2635 sample_ids: vec![
2636 SampleId::new("sample:one").unwrap(),
2637 SampleId::new("sample:two").unwrap(),
2638 ],
2639 folds: vec![
2640 FoldAssignment {
2641 fold_id: FoldId::new("fold:0").unwrap(),
2642 train_sample_ids: vec![SampleId::new("sample:two").unwrap()],
2643 validation_sample_ids: vec![SampleId::new("sample:one").unwrap()],
2644 metadata: BTreeMap::new(),
2645 },
2646 FoldAssignment {
2647 fold_id: FoldId::new("fold:1").unwrap(),
2648 train_sample_ids: vec![SampleId::new("sample:one").unwrap()],
2649 validation_sample_ids: vec![SampleId::new("sample:two").unwrap()],
2650 metadata: BTreeMap::new(),
2651 },
2652 ],
2653 sample_groups: BTreeMap::new(),
2654 partition_mode: FoldPartitionMode::Partition,
2655 };
2656 let mut registry = ControllerRegistry::new();
2657 registry
2658 .register(manifest("controller:model", NodeKind::Model))
2659 .unwrap();
2660 let plan = build_execution_plan(
2661 "plan:hpo.scheduler",
2662 graph,
2663 CampaignSpec {
2664 inner_cv: None,
2665 id: "campaign:hpo.scheduler".to_string(),
2666 root_seed: Some(13),
2667 leakage_policy: Default::default(),
2668 aggregation_policy: Default::default(),
2669 split_invocation: Some(SplitInvocation {
2670 id: "split:hpo".to_string(),
2671 controller_id: None,
2672 leakage_policy: Default::default(),
2673 params: BTreeMap::new(),
2674 fold_set: Some(fold_set),
2675 }),
2676 generation: Default::default(),
2677 shape_plans: BTreeMap::new(),
2678 data_bindings: BTreeMap::new(),
2679 branch_view_plans: Vec::new(),
2680 metadata: BTreeMap::new(),
2681 },
2682 ®istry,
2683 )
2684 .unwrap();
2685 let trace = Arc::new(Mutex::new(Vec::new()));
2686 let mut controllers = RuntimeControllerRegistry::new();
2687 controllers
2688 .register(Box::new(HpoTestTuner {
2689 id: ControllerId::new("controller:tuner").unwrap(),
2690 trace: Arc::clone(&trace),
2691 history_len: 0,
2692 proposal_count: 1,
2693 prune_at: None,
2694 }))
2695 .unwrap();
2696 controllers
2697 .register(Box::new(HpoTestModel {
2698 id: ControllerId::new("controller:model").unwrap(),
2699 trace: Arc::clone(&trace),
2700 fail_variant: None,
2701 score_by_trial: false,
2702 }))
2703 .unwrap();
2704 let hpo = RuntimeHpoExecutionContext {
2705 operation_id: "hpo:test".to_string(),
2706 controller_id: ControllerId::new("controller:tuner").unwrap(),
2707 target_node_id: target.clone(),
2708 base_variant: plan.variants[0].clone(),
2709 trial_budget_total: 1,
2710 study: MethodsHpoStudyConfig {
2711 controller_id: "controller:tuner".to_string(),
2712 study_id: "study:hpo.scheduler".to_string(),
2713 methods_abi: "test-abi".to_string(),
2714 search_space: HpoSearchSpace {
2715 parameters: vec![HpoParameter::Int {
2716 name: "n_components".to_string(),
2717 low: 1,
2718 high: 1,
2719 step: 1,
2720 log: false,
2721 }],
2722 },
2723 optimizer: HpoOptimizerConfig {
2724 sampler: HpoSampler::Random,
2725 pruner: HpoPruner::None,
2726 direction: HpoDirection::Minimize,
2727 metric: HpoMetric::Rmse,
2728 seed: 13,
2729 n_startup_trials: 1,
2730 max_resource: 0,
2731 reduction_factor: 1,
2732 },
2733 },
2734 parameter_paths: BTreeMap::from([(
2735 "n_components".to_string(),
2736 "n_components".to_string(),
2737 )]),
2738 resume_checkpoint: None,
2739 resume_variants: BTreeMap::new(),
2740 resume_terminal_trials: Vec::new(),
2741 selection: RuntimeHpoSelectionTarget {
2742 producer_node: target,
2743 producer_port: "prediction".to_string(),
2744 metric: RegressionMetricKind::Rmse,
2745 direction: HpoDirection::Minimize,
2746 },
2747 provenance: RuntimeHpoProvenance {
2748 graph_fingerprint: plan.graph_fingerprint.clone(),
2749 campaign_fingerprint: plan.campaign_fingerprint.clone(),
2750 controller_fingerprint: plan.controller_fingerprint.clone(),
2751 data_identities_fingerprint: "identity:test".to_string(),
2752 fold_set_fingerprint: plan
2753 .fold_set
2754 .as_ref()
2755 .map(stable_json_fingerprint)
2756 .transpose()
2757 .unwrap(),
2758 training_influence_fingerprint: "influence:test".to_string(),
2759 relation_fingerprint: "relation:test".to_string(),
2760 },
2761 };
2762 let provider = InMemoryDataProvider::new(ControllerId::new("controller:data").unwrap());
2763 let ctx = RunContext::new(RunId::new("run:hpo.scheduler").unwrap(), Some(13));
2764
2765 let result = SequentialScheduler
2766 .execute_hpo_campaign(&plan, &controllers, &provider, &ctx, &hpo)
2767 .unwrap();
2768
2769 assert_eq!(result.operation_id, "hpo:test");
2770 assert_eq!(result.candidates.len(), 1);
2771 assert_eq!(result.checkpoint.completed_proposals.len(), 1);
2772 assert_eq!(result.checkpoint.completed_reports.len(), 1);
2773 assert_eq!(result.candidates[0].lineage.len(), 2);
2774 assert_eq!(result.incumbent.variant_id, plan.variants[0].variant_id);
2775 let mut selected_ctx = RunContext::new(RunId::new("run:hpo.scheduler").unwrap(), Some(13));
2776 selected_ctx.variant_id = Some(plan.variants[0].variant_id.clone());
2777 let selected_results = SequentialScheduler
2778 .execute_campaign_phase_with_data_provider(
2779 &plan,
2780 &controllers,
2781 &provider,
2782 &mut selected_ctx,
2783 Phase::FitCv,
2784 )
2785 .unwrap();
2786 assert_eq!(selected_results.len(), 2);
2787 assert_eq!(selected_ctx.lineage.len(), 2);
2788 assert_eq!(
2789 trace.lock().unwrap().as_slice(),
2790 [
2791 "session_factory",
2792 "ask",
2793 "model_cv",
2794 "intermediate:1:0",
2795 "model_cv",
2796 "intermediate:1:1",
2797 "tell",
2798 "checkpoint",
2799 "model_cv",
2800 "model_cv"
2801 ]
2802 );
2803
2804 let prune_trace = Arc::new(Mutex::new(Vec::new()));
2809 let mut prune_controllers = RuntimeControllerRegistry::new();
2810 prune_controllers
2811 .register(Box::new(HpoTestTuner {
2812 id: ControllerId::new("controller:tuner").unwrap(),
2813 trace: Arc::clone(&prune_trace),
2814 history_len: 0,
2815 proposal_count: 2,
2816 prune_at: Some((2, 0)),
2817 }))
2818 .unwrap();
2819 prune_controllers
2820 .register(Box::new(HpoTestModel {
2821 id: ControllerId::new("controller:model").unwrap(),
2822 trace: Arc::clone(&prune_trace),
2823 fail_variant: None,
2824 score_by_trial: false,
2825 }))
2826 .unwrap();
2827 let mut pruning_hpo = hpo.clone();
2828 pruning_hpo.trial_budget_total = 2;
2829 let pruned = SequentialScheduler
2830 .execute_hpo_campaign(
2831 &plan,
2832 &prune_controllers,
2833 &provider,
2834 &RunContext::new(RunId::new("run:hpo.pruned").unwrap(), Some(13)),
2835 &pruning_hpo,
2836 )
2837 .unwrap();
2838 assert_eq!(pruned.candidates.len(), 1);
2839 assert_eq!(pruned.checkpoint.completed_proposals.len(), 1);
2840 assert_eq!(pruned.checkpoint.completed_reports.len(), 1);
2841 assert_eq!(pruned.incumbent.trial_id, 1);
2842 assert_eq!(
2843 pruned
2844 .terminal_trials
2845 .iter()
2846 .map(|entry| (entry.trial.id, entry.trial.status))
2847 .collect::<Vec<_>>(),
2848 vec![
2849 (1, crate::hpo::HpoTrialStatus::Completed),
2850 (2, crate::hpo::HpoTrialStatus::Pruned),
2851 ]
2852 );
2853 assert_eq!(
2854 prune_trace
2855 .lock()
2856 .unwrap()
2857 .iter()
2858 .filter(|event| event.as_str() == "model_cv")
2859 .count(),
2860 3,
2861 "the pruned second trial must execute one fold, not both"
2862 );
2863
2864 let failed_trace = Arc::new(Mutex::new(Vec::new()));
2869 let mut failed_controllers = RuntimeControllerRegistry::new();
2870 failed_controllers
2871 .register(Box::new(HpoTestTuner {
2872 id: ControllerId::new("controller:tuner").unwrap(),
2873 trace: Arc::clone(&failed_trace),
2874 history_len: 0,
2875 proposal_count: 2,
2876 prune_at: None,
2877 }))
2878 .unwrap();
2879 failed_controllers
2880 .register(Box::new(HpoTestModel {
2881 id: ControllerId::new("controller:model").unwrap(),
2882 trace: Arc::clone(&failed_trace),
2883 fail_variant: Some(VariantId::new("hpo:trial:2").unwrap()),
2884 score_by_trial: false,
2885 }))
2886 .unwrap();
2887 let failed = SequentialScheduler
2888 .execute_hpo_campaign(
2889 &plan,
2890 &failed_controllers,
2891 &provider,
2892 &RunContext::new(RunId::new("run:hpo.failed").unwrap(), Some(13)),
2893 &pruning_hpo,
2894 )
2895 .unwrap();
2896 assert_eq!(failed.candidates.len(), 1);
2897 assert_eq!(
2898 failed.candidates[0]
2899 .validation_predictions
2900 .predictions
2901 .len(),
2902 2
2903 );
2904 assert_eq!(failed.checkpoint.completed_proposals.len(), 1);
2905 assert_eq!(failed.checkpoint.completed_reports.len(), 1);
2906 assert_eq!(
2907 failed
2908 .terminal_trials
2909 .iter()
2910 .map(|entry| (entry.trial.id, entry.trial.status))
2911 .collect::<Vec<_>>(),
2912 vec![
2913 (1, crate::hpo::HpoTrialStatus::Completed),
2914 (2, crate::hpo::HpoTrialStatus::Failed),
2915 ]
2916 );
2917 let failed_trial = &failed.terminal_trials[1].trial;
2918 assert_eq!(
2919 failed_trial.failure.as_ref().unwrap().code,
2920 "DAGML_CV_ERROR"
2921 );
2922 assert!(failed_trial.intermediates.is_empty());
2923 assert_eq!(
2924 failed_trace
2925 .lock()
2926 .unwrap()
2927 .iter()
2928 .filter(|event| event.as_str() == "model_cv")
2929 .count(),
2930 3,
2931 "the failed second trial must not schedule another fold or output handle"
2932 );
2933 assert!(failed_trace
2934 .lock()
2935 .unwrap()
2936 .iter()
2937 .any(|event| event == "tell_failed"));
2938
2939 let mut resampled_plan = plan.clone();
2942 resampled_plan.fold_set.as_mut().unwrap().partition_mode = FoldPartitionMode::Resampled;
2943 let mut resampled_hpo = hpo.clone();
2944 resampled_hpo.provenance.fold_set_fingerprint =
2945 Some(stable_json_fingerprint(resampled_plan.fold_set.as_ref().unwrap()).unwrap());
2946 let trace_len_before = trace.lock().unwrap().len();
2947 let error = SequentialScheduler
2948 .execute_hpo_campaign(
2949 &resampled_plan,
2950 &controllers,
2951 &provider,
2952 &ctx,
2953 &resampled_hpo,
2954 )
2955 .unwrap_err();
2956 assert!(error
2957 .to_string()
2958 .contains("requires FoldPartitionMode::Partition"));
2959 assert_eq!(trace.lock().unwrap().len(), trace_len_before);
2960
2961 let mut empty_plan = plan.clone();
2964 empty_plan.fold_set.as_mut().unwrap().folds.clear();
2965 let trace_len_before = trace.lock().unwrap().len();
2966 let error = SequentialScheduler
2967 .execute_hpo_campaign(&empty_plan, &controllers, &provider, &ctx, &hpo)
2968 .unwrap_err();
2969 assert!(error.to_string().contains("fold set contains no folds"));
2970 assert_eq!(trace.lock().unwrap().len(), trace_len_before);
2971
2972 let mut no_fold_plan = plan.clone();
2975 no_fold_plan.fold_set = None;
2976 let mut no_fold_hpo = hpo.clone();
2977 no_fold_hpo.provenance.fold_set_fingerprint = None;
2978 let trace_len_before = trace.lock().unwrap().len();
2979 let error = SequentialScheduler
2980 .execute_hpo_campaign(&no_fold_plan, &controllers, &provider, &ctx, &no_fold_hpo)
2981 .unwrap_err();
2982 assert!(error
2983 .to_string()
2984 .contains("requires an explicit validated fold set"));
2985 assert_eq!(trace.lock().unwrap().len(), trace_len_before);
2986
2987 let samples = (1..=6)
2991 .map(|index| SampleId::new(format!("nested:{index}")).unwrap())
2992 .collect::<Vec<_>>();
2993 let outer_folds = KFoldSpec {
2994 n_splits: 3,
2995 shuffle: false,
2996 seed: Some(7),
2997 }
2998 .split("folds:hpo.nested", &samples)
2999 .unwrap();
3000 let base_a = NodeId::new("model:nested.base.a").unwrap();
3001 let base_b = NodeId::new("model:nested.base.b").unwrap();
3002 let meta = NodeId::new("model:nested.meta").unwrap();
3003 let mut meta_node = NodeSpec {
3004 id: meta.clone(),
3005 kind: NodeKind::Model,
3006 operator: None,
3007 params: BTreeMap::new(),
3008 ports: PortSchema {
3009 inputs: vec![prediction_port("a"), prediction_port("b")],
3010 outputs: vec![prediction_port("prediction")],
3011 },
3012 metadata: BTreeMap::new(),
3013 seed_label: None,
3014 };
3015 meta_node.metadata.insert(
3016 NESTED_STACKING_EXECUTION_METADATA_KEY.to_string(),
3017 serde_json::json!(NESTED_STACKING_EXECUTION_V1),
3018 );
3019 let nested_graph = GraphSpec {
3020 id: "graph:hpo.nested".to_string(),
3021 interface: GraphInterface::default(),
3022 nodes: vec![
3023 node(
3024 base_a.as_str(),
3025 NodeKind::Model,
3026 vec![prediction_port("prediction")],
3027 ),
3028 node(
3029 base_b.as_str(),
3030 NodeKind::Model,
3031 vec![prediction_port("prediction")],
3032 ),
3033 meta_node,
3034 ],
3035 edges: vec![
3036 EdgeSpec {
3037 source: PortRef {
3038 node_id: base_a,
3039 port_name: "prediction".to_string(),
3040 },
3041 target: PortRef {
3042 node_id: meta.clone(),
3043 port_name: "a".to_string(),
3044 },
3045 contract: EdgeContract {
3046 requires_oof: true,
3047 requires_fold_alignment: true,
3048 ..EdgeContract::new(PortKind::Prediction, None)
3049 },
3050 },
3051 EdgeSpec {
3052 source: PortRef {
3053 node_id: base_b,
3054 port_name: "prediction".to_string(),
3055 },
3056 target: PortRef {
3057 node_id: meta.clone(),
3058 port_name: "b".to_string(),
3059 },
3060 contract: EdgeContract {
3061 requires_oof: true,
3062 requires_fold_alignment: true,
3063 ..EdgeContract::new(PortKind::Prediction, None)
3064 },
3065 },
3066 ],
3067 search_space_fingerprint: None,
3068 metadata: BTreeMap::new(),
3069 };
3070 let mut nested_campaign = plan.campaign.clone();
3071 nested_campaign.id = "campaign:hpo.nested".to_string();
3072 nested_campaign.inner_cv = Some(NestedCvSpec::KFold(KFoldSpec {
3073 n_splits: 2,
3074 shuffle: false,
3075 seed: Some(11),
3076 }));
3077 nested_campaign.split_invocation.as_mut().unwrap().fold_set = Some(outer_folds);
3078 let mut nested_registry = ControllerRegistry::new();
3079 let mut nested_model_manifest = manifest("controller:model", NodeKind::Model);
3080 nested_model_manifest
3081 .capabilities
3082 .insert(ControllerCapability::ConsumesOofPredictions);
3083 nested_registry.register(nested_model_manifest).unwrap();
3084 let nested_plan = build_execution_plan(
3085 "plan:hpo.nested",
3086 nested_graph,
3087 nested_campaign,
3088 &nested_registry,
3089 )
3090 .unwrap();
3091 let mut nested_hpo = hpo.clone();
3092 nested_hpo.target_node_id = meta.clone();
3093 nested_hpo.base_variant = nested_plan.variants[0].clone();
3094 nested_hpo.selection.producer_node = meta;
3095 nested_hpo.provenance.graph_fingerprint = nested_plan.graph_fingerprint.clone();
3096 nested_hpo.provenance.campaign_fingerprint = nested_plan.campaign_fingerprint.clone();
3097 nested_hpo.provenance.controller_fingerprint = nested_plan.controller_fingerprint.clone();
3098 nested_hpo.provenance.fold_set_fingerprint = nested_plan
3099 .fold_set
3100 .as_ref()
3101 .map(stable_json_fingerprint)
3102 .transpose()
3103 .unwrap();
3104 let trace_len_before = trace.lock().unwrap().len();
3105 let error = SequentialScheduler
3106 .execute_hpo_campaign(&nested_plan, &controllers, &provider, &ctx, &nested_hpo)
3107 .unwrap_err();
3108 assert!(error
3109 .to_string()
3110 .contains("does not support nested-stacking FIT_CV"));
3111 assert_eq!(trace.lock().unwrap().len(), trace_len_before);
3112
3113 let resumed_trace = Arc::new(Mutex::new(Vec::new()));
3117 let mut resumed_controllers = RuntimeControllerRegistry::new();
3118 resumed_controllers
3119 .register(Box::new(HpoTestTuner {
3120 id: ControllerId::new("controller:tuner").unwrap(),
3121 trace: Arc::clone(&resumed_trace),
3122 history_len: 2,
3123 proposal_count: 2,
3124 prune_at: None,
3125 }))
3126 .unwrap();
3127 resumed_controllers
3128 .register(Box::new(HpoTestModel {
3129 id: ControllerId::new("controller:model").unwrap(),
3130 trace: Arc::clone(&resumed_trace),
3131 fail_variant: None,
3132 score_by_trial: false,
3133 }))
3134 .unwrap();
3135 let mut resumed_hpo = hpo.clone();
3136 resumed_hpo.trial_budget_total = 4;
3137 let resumed_ctx = RunContext::new(RunId::new("run:hpo.resumed").unwrap(), Some(13));
3138 let resumed = SequentialScheduler
3139 .execute_hpo_campaign(
3140 &plan,
3141 &resumed_controllers,
3142 &provider,
3143 &resumed_ctx,
3144 &resumed_hpo,
3145 )
3146 .unwrap();
3147 assert_eq!(resumed.candidates.len(), 2);
3148 assert_eq!(resumed.checkpoint.trial_history_len, 4);
3149 assert_eq!(
3150 resumed_trace
3151 .lock()
3152 .unwrap()
3153 .iter()
3154 .filter(|event| event.as_str() == "ask")
3155 .count(),
3156 2
3157 );
3158
3159 let mut over_budget_controllers = RuntimeControllerRegistry::new();
3160 over_budget_controllers
3161 .register(Box::new(HpoTestTuner {
3162 id: ControllerId::new("controller:tuner").unwrap(),
3163 trace: Arc::new(Mutex::new(Vec::new())),
3164 history_len: 5,
3165 proposal_count: 0,
3166 prune_at: None,
3167 }))
3168 .unwrap();
3169 let error = SequentialScheduler
3170 .execute_hpo_campaign(
3171 &plan,
3172 &over_budget_controllers,
3173 &provider,
3174 &resumed_ctx,
3175 &resumed_hpo,
3176 )
3177 .unwrap_err();
3178 assert!(error.to_string().contains("exceeds total trial budget"));
3179
3180 #[cfg(feature = "methods-optimizer-local")]
3181 {
3182 let library_path = std::env::var_os("N4M_LIBRARY_PATH")
3186 .expect("native HPO scheduler test requires N4M_LIBRARY_PATH");
3187 let runtime = MethodsRuntime::configure(library_path).unwrap();
3188 let native_trace = Arc::new(Mutex::new(Vec::new()));
3189 let mut native_controllers = RuntimeControllerRegistry::new();
3190 native_controllers
3191 .register(Box::new(MethodsHpoController::new(
3192 ControllerId::new("controller:tuner").unwrap(),
3193 runtime,
3194 )))
3195 .unwrap();
3196 native_controllers
3197 .register(Box::new(HpoTestModel {
3198 id: ControllerId::new("controller:model").unwrap(),
3199 trace: Arc::clone(&native_trace),
3200 fail_variant: None,
3201 score_by_trial: true,
3202 }))
3203 .unwrap();
3204 let mut native_hpo = hpo.clone();
3205 native_hpo.trial_budget_total = 3;
3206 native_hpo.study.study_id = "study:hpo.scheduler.native".to_string();
3207 native_hpo.study.methods_abi = "n4m-abi-2.2".to_string();
3208 native_hpo.study.optimizer.sampler = HpoSampler::Tpe;
3209 native_hpo.study.optimizer.pruner = HpoPruner::Median;
3210 native_hpo.study.optimizer.n_startup_trials = 2;
3211 native_hpo.study.optimizer.seed = 51;
3212 native_hpo.study.optimizer.reduction_factor = 0;
3213 native_hpo.study.search_space.parameters = vec![HpoParameter::Int {
3214 name: "n_components".to_string(),
3215 low: 1,
3216 high: 3,
3217 step: 1,
3218 log: false,
3219 }];
3220 let native = SequentialScheduler
3221 .execute_hpo_campaign(
3222 &plan,
3223 &native_controllers,
3224 &provider,
3225 &RunContext::new(RunId::new("run:hpo.native").unwrap(), Some(13)),
3226 &native_hpo,
3227 )
3228 .unwrap();
3229 assert_eq!(native.checkpoint.trial_history_len, 3);
3230 assert_eq!(native.checkpoint.artifact.format, N4MOPT_FORMAT);
3231 assert_eq!(native.candidates.len(), 2);
3232 assert_eq!(
3233 native
3234 .terminal_trials
3235 .iter()
3236 .map(|entry| entry.trial.status)
3237 .collect::<Vec<_>>(),
3238 vec![
3239 crate::hpo::HpoTrialStatus::Completed,
3240 crate::hpo::HpoTrialStatus::Completed,
3241 crate::hpo::HpoTrialStatus::Pruned,
3242 ]
3243 );
3244 assert!(native.terminal_trials[2]
3245 .trial
3246 .intermediates
3247 .iter()
3248 .any(|item| item.step == 0 && item.should_prune));
3249 assert_eq!(
3250 native_trace
3251 .lock()
3252 .unwrap()
3253 .iter()
3254 .filter(|event| event.as_str() == "model_cv")
3255 .count(),
3256 5,
3257 "native pruning must stop the third trial after its first fold"
3258 );
3259
3260 let prior_checkpoint = native.checkpoint.artifact.clone();
3261 native_hpo.resume_checkpoint = Some(prior_checkpoint.clone());
3262 native_hpo.resume_variants = native
3263 .terminal_trials
3264 .iter()
3265 .filter_map(|snapshot| {
3266 snapshot
3267 .variant_id
3268 .clone()
3269 .map(|variant| (snapshot.trial.id, variant))
3270 })
3271 .collect();
3272 native_hpo.resume_terminal_trials = native.terminal_trials.clone();
3273 native_hpo.trial_budget_total = 4;
3274 let resumed = SequentialScheduler
3275 .execute_hpo_campaign(
3276 &plan,
3277 &native_controllers,
3278 &provider,
3279 &RunContext::new(RunId::new("run:hpo.native.resume").unwrap(), Some(13)),
3280 &native_hpo,
3281 )
3282 .unwrap();
3283 assert_eq!(resumed.checkpoint.trial_history_len, 4);
3284 assert_eq!(resumed.terminal_trials.len(), 4);
3285 assert_eq!(
3286 &resumed.terminal_trials[..3],
3287 native.terminal_trials.as_slice()
3288 );
3289 assert_ne!(
3290 resumed.checkpoint.artifact.opaque_payload,
3291 prior_checkpoint.opaque_payload
3292 );
3293 }
3294 }
3295}
3296
3297pub(crate) fn attach_coordinator_input_lineage(
3298 result: &mut NodeResult,
3299 plan: &ExecutionPlan,
3300 node_id: &NodeId,
3301 upstream_lineage: &BTreeMap<NodeId, LineageId>,
3302) -> Result<()> {
3303 let inferred = inferred_input_lineage_for_node(plan, node_id, upstream_lineage);
3304 if result.lineage.input_lineage.is_empty() {
3305 result.lineage.input_lineage = inferred;
3306 return Ok(());
3307 }
3308
3309 let declared = result
3310 .lineage
3311 .input_lineage
3312 .iter()
3313 .cloned()
3314 .collect::<BTreeSet<_>>()
3315 .into_iter()
3316 .collect::<Vec<_>>();
3317 if declared != inferred {
3318 return Err(DagMlError::RuntimeValidation(format!(
3319 "lineage for node `{}` declared input lineage {:?}, expected {:?}",
3320 result.node_id, declared, inferred
3321 )));
3322 }
3323 result.lineage.input_lineage = declared;
3324 Ok(())
3325}
3326
3327fn attach_nested_stacking_input_lineage(
3334 result: &mut NodeResult,
3335 plan: &ExecutionPlan,
3336 task: &NodeTask,
3337 ctx: &RunContext,
3338 nested: &NestedStackingInput<'_>,
3339) -> Result<()> {
3340 if task.phase != Phase::FitCv || task.node_plan.node_id != *nested.meta_node_id {
3341 return Ok(());
3342 }
3343 let inner_fold_ids = nested
3344 .inner
3345 .inner_fold_set
3346 .folds
3347 .iter()
3348 .map(|fold| fold.fold_id.clone())
3349 .collect::<BTreeSet<_>>();
3350 let source_nodes = incoming_oof_edges(plan, &task.node_plan)?
3351 .into_iter()
3352 .map(|edge| edge.source.node_id.clone())
3353 .collect::<BTreeSet<_>>();
3354 let expected = source_nodes
3355 .iter()
3356 .flat_map(|node_id| {
3357 inner_fold_ids
3358 .iter()
3359 .cloned()
3360 .map(move |fold_id| (node_id.clone(), fold_id))
3361 })
3362 .collect::<BTreeSet<_>>();
3363 let mut actual = BTreeMap::new();
3364 for record in ctx.lineage.records().filter(|record| {
3365 record.phase == Phase::FitCv
3366 && record.variant_id == task.variant_id
3367 && source_nodes.contains(&record.node_id)
3368 && record
3369 .fold_id
3370 .as_ref()
3371 .is_some_and(|fold_id| inner_fold_ids.contains(fold_id))
3372 }) {
3373 let fold_id = record
3374 .fold_id
3375 .clone()
3376 .expect("inner-fold predicate requires a fold id");
3377 if actual
3378 .insert((record.node_id.clone(), fold_id), record.record_id.clone())
3379 .is_some()
3380 {
3381 return Err(DagMlError::RuntimeValidation(
3382 "nested stacking meta input lineage contains duplicate inner-fold evidence"
3383 .to_string(),
3384 ));
3385 }
3386 }
3387 if actual.keys().cloned().collect::<BTreeSet<_>>() != expected {
3388 return Err(DagMlError::RuntimeValidation(
3389 "nested stacking meta input lineage does not exactly cover inner OOF evidence"
3390 .to_string(),
3391 ));
3392 }
3393 let inferred = actual.into_values().collect::<Vec<_>>();
3394 if result.lineage.input_lineage.is_empty() {
3395 result.lineage.input_lineage = inferred;
3396 return Ok(());
3397 }
3398 let declared = result
3399 .lineage
3400 .input_lineage
3401 .iter()
3402 .cloned()
3403 .collect::<BTreeSet<_>>();
3404 if declared.into_iter().collect::<Vec<_>>() != inferred {
3405 return Err(DagMlError::RuntimeValidation(format!(
3406 "nested stacking meta lineage for node `{}` does not match inner OOF evidence",
3407 task.node_plan.node_id
3408 )));
3409 }
3410 result.lineage.input_lineage = inferred;
3411 Ok(())
3412}
3413
3414pub(crate) fn inferred_input_lineage_for_node(
3415 plan: &ExecutionPlan,
3416 node_id: &NodeId,
3417 upstream_lineage: &BTreeMap<NodeId, LineageId>,
3418) -> Vec<LineageId> {
3419 plan.graph_plan
3420 .graph
3421 .edges
3422 .iter()
3423 .filter(|edge| &edge.target.node_id == node_id && edge.contract.propagates_lineage)
3424 .filter_map(|edge| upstream_lineage.get(&edge.source.node_id).cloned())
3425 .collect::<BTreeSet<_>>()
3426 .into_iter()
3427 .collect()
3428}
3429pub(crate) fn collect_input_handles(
3430 plan: &ExecutionPlan,
3431 node_plan: &NodePlan,
3432 output_handles: &BTreeMap<NodeId, BTreeMap<String, HandleRef>>,
3433 output_data_views: &BTreeMap<NodeId, BTreeMap<String, DataProviderViewSpec>>,
3434 resources: &PhaseScopeResources<'_>,
3435 ctx: &RunContext,
3436 scope: &PhaseScope,
3437) -> Result<CollectedInputs> {
3438 let mut inputs = BTreeMap::new();
3439 let mut data_views = BTreeMap::new();
3440 let mut prediction_inputs = BTreeMap::new();
3441 let training_oof_edges = incoming_training_oof_edges(plan, node_plan, scope)?;
3442 let masked_oof_source_ports = if scope.phase == Phase::Predict {
3448 incoming_oof_edges(plan, node_plan)?
3449 } else {
3450 training_oof_edges.clone()
3451 }
3452 .into_iter()
3453 .map(|edge| (edge.source.node_id.clone(), edge.source.port_name.clone()))
3454 .collect::<BTreeSet<_>>();
3455 let bound_data_inputs = node_plan
3456 .data_bindings
3457 .iter()
3458 .map(|binding| binding.input_name.clone())
3459 .collect::<BTreeSet<_>>();
3460 let declared_source_ports = plan
3464 .graph_plan
3465 .graph
3466 .edges
3467 .iter()
3468 .filter(|edge| edge.target.node_id == node_plan.node_id)
3469 .map(|edge| (edge.source.node_id.clone(), edge.source.port_name.clone()))
3470 .collect::<BTreeSet<_>>();
3471 for upstream in &node_plan.input_nodes {
3472 if let Some(handles) = output_handles.get(upstream) {
3473 for (port, handle) in handles {
3474 if !declared_source_ports.contains(&(upstream.clone(), port.clone())) {
3475 continue;
3476 }
3477 if masked_oof_source_ports.contains(&(upstream.clone(), port.clone())) {
3478 continue;
3479 }
3480 inputs.insert(format!("{upstream}.{port}"), handle.clone());
3481 }
3482 }
3483 }
3484 for edge in plan
3485 .graph_plan
3486 .graph
3487 .edges
3488 .iter()
3489 .filter(|edge| edge.target.node_id == node_plan.node_id)
3490 .filter(|edge| edge.contract.kind == PortKind::Data && !edge.contract.requires_oof)
3491 {
3492 if bound_data_inputs.contains(&edge.target.port_name) {
3493 continue;
3494 }
3495 let Some(handles) = output_handles.get(&edge.source.node_id) else {
3496 continue;
3497 };
3498 let Some(handle) = handles.get(&edge.source.port_name) else {
3499 continue;
3500 };
3501 let key = data_view_key(&edge.target.port_name);
3502 if inputs.insert(key.clone(), handle.clone()).is_some() {
3503 return Err(DagMlError::RuntimeValidation(format!(
3504 "node `{}` received duplicate data edge input `{key}`",
3505 node_plan.node_id
3506 )));
3507 }
3508 if let Some(source_views) = output_data_views.get(&edge.source.node_id) {
3509 if let Some(view) = source_views.get(&edge.source.port_name) {
3510 if data_views.insert(key.clone(), view.clone()).is_some() {
3511 return Err(DagMlError::RuntimeValidation(format!(
3512 "node `{}` received duplicate data edge view `{key}`",
3513 node_plan.node_id
3514 )));
3515 }
3516 }
3517 let source_validation_key = validation_data_view_key(&edge.source.port_name);
3518 if let Some(view) = source_views.get(&source_validation_key) {
3519 let validation_key = format!("{key}:validation");
3520 if data_views
3521 .insert(validation_key.clone(), view.clone())
3522 .is_some()
3523 {
3524 return Err(DagMlError::RuntimeValidation(format!(
3525 "node `{}` received duplicate data edge validation view `{validation_key}`",
3526 node_plan.node_id
3527 )));
3528 }
3529 }
3530 }
3531 }
3532 for edge in training_oof_edges {
3533 let key = format!("{}.{}", edge.source.node_id, edge.source.port_name);
3534 let Some(input) = collect_oof_prediction_input(plan, edge, ctx, scope, resources)? else {
3535 return Ok(CollectedInputs {
3536 handles: BTreeMap::new(),
3537 data_views: BTreeMap::new(),
3538 prediction_inputs: BTreeMap::new(),
3539 skip_node: true,
3540 });
3541 };
3542 if inputs.insert(key.clone(), input.handle).is_some() {
3543 return Err(DagMlError::RuntimeValidation(format!(
3544 "node `{}` received duplicate OOF prediction input `{key}`",
3545 node_plan.node_id
3546 )));
3547 }
3548 if prediction_inputs.insert(key.clone(), input.spec).is_some() {
3549 return Err(DagMlError::RuntimeValidation(format!(
3550 "node `{}` received duplicate OOF prediction spec `{key}`",
3551 node_plan.node_id
3552 )));
3553 }
3554 }
3555 if matches!(scope.phase, Phase::Refit | Phase::Predict) {
3562 let off_fold_suffix = scope.phase.as_str().to_ascii_lowercase();
3563 for edge in incoming_oof_edges(plan, node_plan)? {
3564 let Some(input) = collect_off_fold_prediction_input(plan, edge, ctx, scope)? else {
3565 continue;
3566 };
3567 let key = format!(
3568 "{}.{}:{off_fold_suffix}",
3569 edge.source.node_id, edge.source.port_name
3570 );
3571 if inputs.insert(key.clone(), input.handle).is_some() {
3572 return Err(DagMlError::RuntimeValidation(format!(
3573 "node `{}` received duplicate off-fold prediction input `{key}`",
3574 node_plan.node_id
3575 )));
3576 }
3577 if prediction_inputs.insert(key.clone(), input.spec).is_some() {
3578 return Err(DagMlError::RuntimeValidation(format!(
3579 "node `{}` received duplicate off-fold prediction spec `{key}`",
3580 node_plan.node_id
3581 )));
3582 }
3583 }
3584 }
3585 if !node_plan.data_bindings.is_empty() && resources.data_provider.is_none() {
3586 return Err(DagMlError::RuntimeValidation(format!(
3587 "node `{}` requires {} data binding(s) but no runtime data provider is registered",
3588 node_plan.node_id,
3589 node_plan.data_bindings.len()
3590 )));
3591 }
3592 if let Some(data_provider) = resources.data_provider {
3593 let excluded_samples = if scope.phase == Phase::Predict {
3598 BTreeSet::new()
3599 } else {
3600 coordinator_relations_for_node(node_plan, resources)?
3601 .map(|relations| relations.excluded_sample_ids())
3602 .unwrap_or_default()
3603 };
3604 let scope_fold_set = resources.fold_set_override.or(plan.fold_set.as_ref());
3605 for binding in &node_plan.data_bindings {
3606 let predict_cohort = if scope.phase == Phase::Predict {
3607 data_provider.predict_cohort(binding, scope.phase)?
3608 } else {
3609 None
3610 };
3611 let materialized = data_provider.materialize(&DataMaterializationRequest {
3612 run_id: ctx.run_id.clone(),
3613 node_id: node_plan.node_id.clone(),
3614 input_name: binding.input_name.clone(),
3615 phase: scope.phase,
3616 variant_id: scope.variant_id.clone(),
3617 fold_id: scope.fold_id.clone(),
3618 binding: binding.clone(),
3619 predict_cohort: predict_cohort.clone(),
3620 })?;
3621 let branch_view_for_node = branch_view_from_node_metadata(plan, &node_plan.node_id)?;
3622 let mut view = data_view_for_scope(
3623 binding,
3624 scope_fold_set,
3625 scope,
3626 branch_view_for_node.as_ref(),
3627 &excluded_samples,
3628 )?;
3629 if scope.phase == Phase::Refit
3630 && scope_fold_set.is_none()
3631 && view.partition == DataRequestPartition::FullTrain
3632 && view.sample_ids.is_none()
3633 {
3634 view.sample_ids = data_provider.refit_sample_ids(binding)?;
3635 if !view.include_excluded {
3636 if let Some(sample_ids) = view.sample_ids.as_mut() {
3637 sample_ids.retain(|sample_id| !excluded_samples.contains(sample_id));
3638 }
3639 }
3640 view.validate()?;
3641 }
3642 if let Some(cohort) = predict_cohort.as_ref() {
3643 bind_predict_cohort_to_view(&mut view, cohort)?;
3644 }
3645 let key = data_view_key(&binding.input_name);
3646 let view_handle = make_data_view_handle(
3647 data_provider,
3648 ctx,
3649 node_plan,
3650 scope,
3651 binding,
3652 DataViewHandleInput {
3653 data_handle: &materialized,
3654 view: &view,
3655 predict_cohort: predict_cohort.as_ref(),
3656 },
3657 )?;
3658 if data_views.insert(key.clone(), view).is_some() {
3659 return Err(DagMlError::RuntimeValidation(format!(
3660 "node `{}` received duplicate data view `{key}`",
3661 node_plan.node_id
3662 )));
3663 }
3664 if inputs.insert(key.clone(), view_handle).is_some() {
3665 return Err(DagMlError::RuntimeValidation(format!(
3666 "node `{}` received duplicate data input `{key}`",
3667 node_plan.node_id
3668 )));
3669 }
3670
3671 if let Some(validation_view) = validation_data_view_for_scope(
3672 binding,
3673 scope_fold_set,
3674 scope,
3675 branch_view_for_node.as_ref(),
3676 &excluded_samples,
3677 )? {
3678 let validation_key = format!("{key}:validation");
3679 let validation_handle = make_data_view_handle(
3680 data_provider,
3681 ctx,
3682 node_plan,
3683 scope,
3684 binding,
3685 DataViewHandleInput {
3686 data_handle: &materialized,
3687 view: &validation_view,
3688 predict_cohort: None,
3689 },
3690 )?;
3691 if data_views
3692 .insert(validation_key.clone(), validation_view)
3693 .is_some()
3694 {
3695 return Err(DagMlError::RuntimeValidation(format!(
3696 "node `{}` received duplicate validation data view `{validation_key}`",
3697 node_plan.node_id
3698 )));
3699 }
3700 if inputs
3701 .insert(validation_key.clone(), validation_handle)
3702 .is_some()
3703 {
3704 return Err(DagMlError::RuntimeValidation(format!(
3705 "node `{}` received duplicate validation data input `{validation_key}`",
3706 node_plan.node_id
3707 )));
3708 }
3709 }
3710 }
3711 }
3712 Ok(CollectedInputs {
3713 handles: inputs,
3714 data_views,
3715 prediction_inputs,
3716 skip_node: false,
3717 })
3718}
3719pub(crate) fn preload_replay_prediction_cache_store(
3720 bundle: &ExecutionBundle,
3721 prediction_cache_store: Option<&dyn RuntimePredictionCacheStore>,
3722 ctx: &mut RunContext,
3723) -> Result<()> {
3724 if bundle.prediction_requirements.is_empty() {
3725 return Ok(());
3726 }
3727 let store = prediction_cache_store.ok_or_else(|| {
3728 DagMlError::RuntimeValidation(format!(
3729 "bundle `{}` cannot preload OOF prediction caches without a prediction cache store",
3730 bundle.bundle_id
3731 ))
3732 })?;
3733 if !ctx.prediction_store.blocks().is_empty() {
3734 return Err(DagMlError::RuntimeValidation(format!(
3735 "bundle `{}` cannot preload OOF prediction caches into a non-empty prediction store",
3736 bundle.bundle_id
3737 )));
3738 }
3739 let contracts = replay_prediction_cache_contracts(bundle)?;
3740 for contract in contracts.values() {
3741 if contract.requirement.prediction_level == PredictionLevel::Sample {
3742 let blocks = store.load_blocks(&contract.cache.requirement_key)?;
3743 if blocks.iter().any(|block| {
3744 block.producer_node != contract.requirement.producer_node
3745 || block.partition != contract.requirement.partition
3746 }) {
3747 return Err(DagMlError::RuntimeValidation(format!(
3748 "prediction cache store returned blocks outside requirement `{}`",
3749 contract.cache.requirement_key
3750 )));
3751 }
3752 let mut payload = build_prediction_cache_payload(&contract.requirement, &blocks)?;
3753 payload.cache_namespace_fingerprints =
3754 contract.cache.cache_namespace_fingerprints.clone();
3755 validate_prediction_cache_payload_matches_record(&payload, &contract.cache)?;
3756 for block in &payload.blocks {
3757 ctx.prediction_store.append(block.clone())?;
3758 }
3759 } else {
3760 let blocks = store.load_aggregated_blocks(&contract.cache.requirement_key)?;
3761 if blocks.iter().any(|block| {
3762 block.producer_node != contract.requirement.producer_node
3763 || block.partition != contract.requirement.partition
3764 || block.level != contract.requirement.prediction_level
3765 }) {
3766 return Err(DagMlError::RuntimeValidation(format!(
3767 "prediction cache store returned aggregated blocks outside requirement `{}`",
3768 contract.cache.requirement_key
3769 )));
3770 }
3771 let mut payload =
3772 build_aggregated_prediction_cache_payload(&contract.requirement, &blocks)?;
3773 payload.cache_namespace_fingerprints =
3774 contract.cache.cache_namespace_fingerprints.clone();
3775 validate_prediction_cache_payload_matches_record(&payload, &contract.cache)?;
3776 }
3777 }
3778 Ok(())
3779}
3780
3781pub(crate) fn replay_prediction_cache_contracts(
3782 bundle: &ExecutionBundle,
3783) -> Result<BTreeMap<String, ReplayPredictionCacheContract>> {
3784 bundle.validate()?;
3785 let requirements = bundle
3786 .prediction_requirements
3787 .iter()
3788 .map(|requirement| (requirement.key(), requirement))
3789 .collect::<BTreeMap<_, _>>();
3790 let mut contracts = BTreeMap::new();
3791 for cache in &bundle.prediction_caches {
3792 let requirement = requirements.get(&cache.requirement_key).ok_or_else(|| {
3793 DagMlError::RuntimeValidation(format!(
3794 "prediction cache `{}` references unknown prediction requirement `{}`",
3795 cache.cache_id, cache.requirement_key
3796 ))
3797 })?;
3798 contracts.insert(
3799 cache.requirement_key.clone(),
3800 ReplayPredictionCacheContract {
3801 requirement: (*requirement).clone(),
3802 cache: cache.clone(),
3803 },
3804 );
3805 }
3806 Ok(contracts)
3807}
3808
3809pub(crate) fn materialize_replay_artifact_handles(
3810 plan: &ExecutionPlan,
3811 bundle: &ExecutionBundle,
3812 replay_request: &ReplayPhaseRequest,
3813 artifact_store: &dyn RuntimeArtifactStore,
3814 ctx: &RunContext,
3815) -> Result<MaterializedReplayArtifacts> {
3816 let mut handles = BTreeMap::<NodeId, BTreeMap<String, HandleRef>>::new();
3817 let mut inputs = BTreeMap::<NodeId, BTreeMap<String, ArtifactInputSpec>>::new();
3818 for artifact in &bundle.refit_artifacts {
3819 artifact.validate()?;
3820 let node_plan = plan.node_plans.get(&artifact.node_id).ok_or_else(|| {
3821 DagMlError::RuntimeValidation(format!(
3822 "bundle `{}` artifact references unknown node `{}`",
3823 bundle.bundle_id, artifact.node_id
3824 ))
3825 })?;
3826 if !node_plan.supported_phases.contains(&replay_request.phase) {
3827 return Err(DagMlError::RuntimeValidation(format!(
3828 "bundle `{}` artifact node `{}` does not support replay phase {:?}",
3829 bundle.bundle_id, artifact.node_id, replay_request.phase
3830 )));
3831 }
3832 let handle = artifact_store.materialize(&ArtifactMaterializationRequest {
3833 run_id: ctx.run_id.clone(),
3834 bundle_id: bundle.bundle_id.clone(),
3835 node_id: artifact.node_id.clone(),
3836 phase: replay_request.phase,
3837 variant_id: bundle.selected_variant_id.clone(),
3838 controller_id: artifact.controller_id.clone(),
3839 artifact: artifact.artifact.clone(),
3840 params_fingerprint: artifact.params_fingerprint.clone(),
3841 training_loss_fingerprint: artifact.training_loss_fingerprint.clone(),
3842 })?;
3843 if !matches!(handle.kind, HandleKind::Model | HandleKind::Artifact) {
3844 return Err(DagMlError::RuntimeValidation(format!(
3845 "artifact `{}` materialized as unsupported handle kind {:?}",
3846 artifact.artifact.id, handle.kind
3847 )));
3848 }
3849 if handle.owner_controller != artifact.controller_id {
3850 return Err(DagMlError::RuntimeValidation(format!(
3851 "artifact `{}` handle owner `{}` does not match controller `{}`",
3852 artifact.artifact.id, handle.owner_controller, artifact.controller_id
3853 )));
3854 }
3855 let key = refit_artifact_input_key(&artifact.artifact.id);
3856 if handles
3857 .entry(artifact.node_id.clone())
3858 .or_default()
3859 .insert(key.clone(), handle)
3860 .is_some()
3861 {
3862 return Err(DagMlError::RuntimeValidation(format!(
3863 "duplicate replay artifact input `{key}` for node `{}`",
3864 artifact.node_id
3865 )));
3866 }
3867 if inputs
3868 .entry(artifact.node_id.clone())
3869 .or_default()
3870 .insert(key.clone(), ArtifactInputSpec::from_refit_record(artifact)?)
3871 .is_some()
3872 {
3873 return Err(DagMlError::RuntimeValidation(format!(
3874 "duplicate replay artifact metadata `{key}` for node `{}`",
3875 artifact.node_id
3876 )));
3877 }
3878 }
3879 Ok(MaterializedReplayArtifacts { handles, inputs })
3880}
3881
3882pub(crate) fn derive_task_seed(
3883 root_seed: Option<u64>,
3884 variant_id: Option<&VariantId>,
3885 fold_id: Option<&FoldId>,
3886 node_plan: &NodePlan,
3887 phase: Phase,
3888) -> Option<u64> {
3889 root_seed.map(|root| {
3890 let mut context = SeedContext::root(root);
3891 if let Some(variant_id) = variant_id {
3892 context = context.child(format!("variant:{variant_id}"));
3893 }
3894 if let Some(fold_id) = fold_id {
3895 context = context.child(format!("fold:{fold_id}"));
3896 }
3897 context
3898 .child(format!("node:{}", node_plan.node_id))
3899 .child(format!("phase:{phase:?}"))
3900 .derive_u64("task")
3901 })
3902}