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
154#[derive(Default)]
155pub(crate) struct PhaseScopeResources<'a> {
156 pub(crate) data_provider: Option<&'a dyn RuntimeDataProvider>,
157 pub(crate) fold_set_override: Option<&'a FoldSet>,
161 pub(crate) node_filter: Option<&'a BTreeSet<NodeId>>,
165 pub(crate) suppress_inner_cv: bool,
168 pub(crate) nested_stacking: Option<NestedStackingInput<'a>>,
172 pub(crate) replay_artifact_handles: Option<&'a BTreeMap<NodeId, BTreeMap<String, HandleRef>>>,
173 pub(crate) replay_artifact_inputs:
174 Option<&'a BTreeMap<NodeId, BTreeMap<String, ArtifactInputSpec>>>,
175 pub(crate) replay_bundle_id: Option<&'a BundleId>,
176 pub(crate) data_envelopes: Option<&'a BTreeMap<String, ExternalDataPlanEnvelope>>,
177 pub(crate) prediction_cache_store: Option<&'a dyn RuntimePredictionCacheStore>,
178 pub(crate) prediction_cache_contracts:
179 Option<&'a BTreeMap<String, ReplayPredictionCacheContract>>,
180 pub(crate) artifact_store: Option<&'a mut InMemoryArtifactStore>,
181}
182
183impl SequentialScheduler {
184 pub fn execute_hpo_campaign(
191 &self,
192 plan: &ExecutionPlan,
193 controllers: &RuntimeControllerRegistry,
194 data_provider: &dyn RuntimeDataProvider,
195 ctx: &RunContext,
196 hpo: &RuntimeHpoExecutionContext,
197 ) -> Result<RuntimeHpoCampaignResult> {
198 plan.validate()?;
199 hpo.validate_for_plan(plan)?;
200 let controller = controllers.get(&hpo.controller_id).ok_or_else(|| {
201 DagMlError::RuntimeValidation(format!(
202 "runtime HPO campaign controller `{}` is not registered",
203 hpo.controller_id
204 ))
205 })?;
206 let task = RuntimeHpoCampaignTask {
207 run_id: ctx.run_id.clone(),
208 operation_id: hpo.operation_id.clone(),
209 controller_id: hpo.controller_id.clone(),
210 target_node_id: hpo.target_node_id.clone(),
211 seed: ctx.root_seed,
212 };
213 let mut session = controller.create_tuner_session(&task, hpo)?;
214 let history_at_start = session.trial_history_len()?;
215 if history_at_start > hpo.trial_budget_total {
216 return Err(DagMlError::RuntimeValidation(format!(
217 "runtime HPO restored native history ({history_at_start}) exceeds total trial budget ({})",
218 hpo.trial_budget_total
219 )));
220 }
221 let remaining_trials = hpo.trial_budget_total - history_at_start;
222 let mut candidates = Vec::new();
223 let mut proposed_variant_ids = BTreeSet::new();
224 let mut trial_variants = BTreeMap::new();
229 let mut incumbent_variants = hpo.resume_variants.clone();
230 let mut terminal_trials = BTreeMap::new();
231 let mut completed_proposals = Vec::new();
232 let mut completed_reports = Vec::new();
233
234 for _ in 0..remaining_trials {
235 let Some(proposal) = session.ask()? else {
236 break;
237 };
238 if trial_variants
239 .insert(proposal.trial_id, proposal.variant.variant_id.clone())
240 .is_some()
241 {
242 return Err(DagMlError::RuntimeValidation(format!(
243 "runtime HPO session proposed duplicate trial `{}`",
244 proposal.trial_id
245 )));
246 }
247 if incumbent_variants
248 .insert(proposal.trial_id, proposal.variant.variant_id.clone())
249 .is_some()
250 {
251 return Err(DagMlError::RuntimeValidation(format!(
252 "runtime HPO session reused restored trial `{}`",
253 proposal.trial_id
254 )));
255 }
256 if !proposed_variant_ids.insert(proposal.variant.variant_id.clone()) {
257 return Err(DagMlError::RuntimeValidation(format!(
258 "runtime HPO session proposed duplicate variant `{}`",
259 proposal.variant.variant_id
260 )));
261 }
262 let mut candidate_plan = plan.clone();
263 candidate_plan.variants = vec![proposal.variant.clone()];
264 candidate_plan.validate()?;
265 let mut candidate_ctx =
266 RunContext::new(ctx.run_id.clone(), proposal.variant.seed.or(ctx.root_seed));
267 candidate_ctx.variant_id = Some(proposal.variant.variant_id.clone());
268
269 let evaluation = self.execute_hpo_candidate_fit_cv(
270 &candidate_plan,
271 controllers,
272 data_provider,
273 &mut candidate_ctx,
274 );
275 if let Err(error) = evaluation {
276 session.tell(
277 proposal.trial_id,
278 RuntimeHpoTerminal::Failed {
279 failure: RuntimeHpoFailure {
280 code: "DAGML_CV_ERROR".to_string(),
281 message: error.to_string(),
282 retryable: false,
283 },
284 },
285 )?;
286 terminal_trials.insert(proposal.trial_id, HpoTrialTerminalState::Failed);
287 continue;
288 }
289 if let Err(error) = candidate_ctx
290 .collect_cross_fold_validation_scores(plan_oof_partition_mode(&candidate_plan))
291 {
292 session.tell(
293 proposal.trial_id,
294 RuntimeHpoTerminal::Failed {
295 failure: RuntimeHpoFailure {
296 code: "DAGML_SCORE_ERROR".to_string(),
297 message: error.to_string(),
298 retryable: false,
299 },
300 },
301 )?;
302 terminal_trials.insert(proposal.trial_id, HpoTrialTerminalState::Failed);
303 continue;
304 }
305 let report = candidate_ctx
306 .score_collector
307 .iter()
308 .find(|report| {
309 report.producer_node == hpo.selection.producer_node
310 && report.producer_port.as_deref()
311 == Some(hpo.selection.producer_port.as_str())
312 && report.partition == PredictionPartition::Validation
313 && report
314 .fold_id
315 .as_ref()
316 .is_some_and(|fold| fold.as_str() == "avg")
317 })
318 .cloned();
319 let Some(mut report) = report else {
320 session.tell(
321 proposal.trial_id,
322 RuntimeHpoTerminal::Failed {
323 failure: RuntimeHpoFailure {
324 code: "DAGML_SCORE_MISSING".to_string(),
325 message: format!(
326 "runtime HPO trial `{}` emitted no target OOF average",
327 proposal.trial_id
328 ),
329 retryable: false,
330 },
331 },
332 )?;
333 terminal_trials.insert(proposal.trial_id, HpoTrialTerminalState::Failed);
334 continue;
335 };
336 report.variant_id = Some(proposal.variant.variant_id.clone());
337 let score = report
338 .metrics
339 .get(hpo.selection.metric.name())
340 .copied()
341 .filter(|score| score.is_finite());
342 let Some(score) = score else {
343 session.tell(
344 proposal.trial_id,
345 RuntimeHpoTerminal::Failed {
346 failure: RuntimeHpoFailure {
347 code: "DAGML_SCORE_NONFINITE".to_string(),
348 message: format!(
349 "runtime HPO trial `{}` emitted no finite `{}` score",
350 proposal.trial_id,
351 hpo.selection.metric.name()
352 ),
353 retryable: false,
354 },
355 },
356 )?;
357 terminal_trials.insert(proposal.trial_id, HpoTrialTerminalState::Failed);
358 continue;
359 };
360 let intermediate = RuntimeHpoIntermediate {
361 trial_id: proposal.trial_id,
362 step: 0,
363 score,
364 };
365 if session.report_intermediate(intermediate)? == RuntimeHpoIntermediateOutcome::Pruned {
366 terminal_trials.insert(proposal.trial_id, HpoTrialTerminalState::Pruned);
367 continue;
368 }
369 session.tell(proposal.trial_id, RuntimeHpoTerminal::Completed { score })?;
370 terminal_trials.insert(proposal.trial_id, HpoTrialTerminalState::Completed);
371 completed_proposals.push(proposal.clone());
372 completed_reports.push(RuntimeHpoCompletedReport {
373 trial_id: proposal.trial_id,
374 variant_id: proposal.variant.variant_id.clone(),
375 report: report.clone(),
376 });
377
378 let mut validation_reports = candidate_ctx
379 .score_collector
380 .iter()
381 .filter(|item| item.partition == PredictionPartition::Validation)
382 .cloned()
383 .collect::<Vec<_>>();
384 for item in &mut validation_reports {
385 item.variant_id = Some(proposal.variant.variant_id.clone());
386 }
387 candidates.push(RuntimeHpoCandidateEvaluation {
388 validation_predictions: capture_variant_validation_predictions(
389 &proposal.variant.variant_id,
390 None,
391 &candidate_ctx,
392 ),
393 lineage: candidate_ctx.lineage.records().cloned().collect(),
394 proposal,
395 score,
396 validation_reports,
397 });
398 }
399
400 let history_at_checkpoint = session.trial_history_len()?;
401 if history_at_checkpoint != hpo.trial_budget_total {
402 return Err(DagMlError::RuntimeValidation(format!(
403 "runtime HPO native history ended at {history_at_checkpoint}, expected total trial budget {}",
404 hpo.trial_budget_total
405 )));
406 }
407
408 let checkpoint = RuntimeHpoCheckpointResult {
409 artifact: session.checkpoint()?,
410 provenance: hpo.provenance.clone(),
411 operation_id: hpo.operation_id.clone(),
412 controller_id: hpo.controller_id.clone(),
413 target_node_id: hpo.target_node_id.clone(),
414 completed_proposals,
415 completed_reports,
416 trial_history_len: history_at_checkpoint,
417 };
418 validate_hpo_checkpoint_result(
419 &checkpoint,
420 hpo,
421 &trial_variants,
422 &terminal_trials,
423 history_at_start,
424 )?;
425 let incumbent = session.incumbent(&incumbent_variants)?.ok_or_else(|| {
426 DagMlError::RuntimeValidation(
427 "native HPO campaign has no completed native incumbent after terminalization"
428 .to_string(),
429 )
430 })?;
431 if incumbent.metric != hpo.selection.metric.name()
432 || incumbent.direction != hpo.selection.direction
433 || incumbent_variants.get(&incumbent.trial_id) != Some(&incumbent.variant_id)
434 || !incumbent.score.is_finite()
435 {
436 return Err(DagMlError::RuntimeValidation(
437 "native HPO incumbent is not bound to this scheduler campaign's metric, direction, trial, and variant"
438 .to_string(),
439 ));
440 }
441 let terminal_trials = session.terminal_trial_snapshots(&incumbent_variants)?;
442 if terminal_trials.len() != history_at_checkpoint as usize
443 || terminal_trials
444 .windows(2)
445 .any(|pair| pair[0].trial.id >= pair[1].trial.id)
446 {
447 return Err(DagMlError::RuntimeValidation(
448 "native HPO terminal ledger is not a complete strictly ordered history".to_string(),
449 ));
450 }
451 Ok(RuntimeHpoCampaignResult {
452 operation_id: hpo.operation_id.clone(),
453 controller_id: hpo.controller_id.clone(),
454 target_node_id: hpo.target_node_id.clone(),
455 candidates,
456 checkpoint,
457 incumbent,
458 terminal_trials,
459 })
460 }
461
462 fn execute_hpo_candidate_fit_cv(
463 &self,
464 plan: &ExecutionPlan,
465 controllers: &RuntimeControllerRegistry,
466 data_provider: &dyn RuntimeDataProvider,
467 ctx: &mut RunContext,
468 ) -> Result<Vec<NodeResult>> {
469 if let Some(nested) = nested_stacking_campaign_plan(plan)? {
470 return self.execute_nested_stacking_fit_cv(
471 plan,
472 controllers,
473 data_provider,
474 ctx,
475 &nested,
476 );
477 }
478 let candidate_plan = plan;
479 ctx.configure_global_oof_aggregation(candidate_plan, data_provider)?;
480 let fold_ids = candidate_plan
481 .fold_set
482 .as_ref()
483 .map(|fold_set| {
484 fold_set
485 .folds
486 .iter()
487 .map(|fold| Some(fold.fold_id.clone()))
488 .collect::<Vec<_>>()
489 })
490 .unwrap_or_else(|| vec![None]);
491 let variant = candidate_plan
492 .variants
493 .first()
494 .expect("candidate plan has exactly one variant");
495 let mut results = Vec::new();
496 for fold_id in fold_ids {
497 results.extend(self.execute_phase_scope(
498 candidate_plan,
499 controllers,
500 ctx,
501 PhaseScope {
502 phase: Phase::FitCv,
503 variant_id: Some(variant.variant_id.clone()),
504 variant: Some(VariantExecutionSpec::from_plan(variant)),
505 fold_id,
506 seed_root: variant.seed.or(ctx.root_seed),
507 },
508 PhaseScopeResources {
509 data_provider: Some(data_provider),
510 ..Default::default()
511 },
512 )?);
513 }
514 Ok(results)
515 }
516
517 pub fn execute_phase(
518 &self,
519 plan: &ExecutionPlan,
520 controllers: &RuntimeControllerRegistry,
521 ctx: &mut RunContext,
522 phase: Phase,
523 ) -> Result<Vec<NodeResult>> {
524 plan.validate()?;
525 let variant_id = ctx.variant_id.clone();
526 let seed_root = ctx.root_seed;
527 self.execute_phase_scope(
528 plan,
529 controllers,
530 ctx,
531 PhaseScope {
532 phase,
533 variant_id,
534 variant: None,
535 fold_id: None,
536 seed_root,
537 },
538 PhaseScopeResources::default(),
539 )
540 }
541
542 pub fn execute_phase_with_data_provider(
543 &self,
544 plan: &ExecutionPlan,
545 controllers: &RuntimeControllerRegistry,
546 data_provider: &dyn RuntimeDataProvider,
547 ctx: &mut RunContext,
548 phase: Phase,
549 ) -> Result<Vec<NodeResult>> {
550 plan.validate()?;
551 let variant_id = ctx.variant_id.clone();
552 let seed_root = ctx.root_seed;
553 self.execute_phase_scope(
554 plan,
555 controllers,
556 ctx,
557 PhaseScope {
558 phase,
559 variant_id,
560 variant: None,
561 fold_id: None,
562 seed_root,
563 },
564 PhaseScopeResources {
565 data_provider: Some(data_provider),
566 ..Default::default()
567 },
568 )
569 }
570
571 pub fn execute_campaign_phase(
572 &self,
573 plan: &ExecutionPlan,
574 controllers: &RuntimeControllerRegistry,
575 ctx: &mut RunContext,
576 phase: Phase,
577 ) -> Result<Vec<NodeResult>> {
578 plan.validate()?;
579 if phase == Phase::FitCv && nested_stacking_campaign_plan(plan)?.is_some() {
580 return Err(DagMlError::RuntimeValidation(
581 "nested stacking FIT_CV requires execute_campaign_phase_with_data_provider so the scheduler can materialize parent-bound inner folds"
582 .to_string(),
583 ));
584 }
585 let mut results = Vec::new();
586 let fold_ids = if phase == Phase::FitCv {
587 plan.fold_set
588 .as_ref()
589 .map(|fold_set| {
590 fold_set
591 .folds
592 .iter()
593 .map(|fold| Some(fold.fold_id.clone()))
594 .collect::<Vec<_>>()
595 })
596 .unwrap_or_else(|| vec![None])
597 } else {
598 vec![None]
599 };
600 for variant in &plan.variants {
601 if ctx
602 .variant_id
603 .as_ref()
604 .is_some_and(|requested| requested != &variant.variant_id)
605 {
606 continue;
607 }
608 for fold_id in &fold_ids {
609 let seed_root = variant.seed.or(ctx.root_seed);
610 results.extend(self.execute_phase_scope(
611 plan,
612 controllers,
613 ctx,
614 PhaseScope {
615 phase,
616 variant_id: Some(variant.variant_id.clone()),
617 variant: Some(VariantExecutionSpec::from_plan(variant)),
618 fold_id: fold_id.clone(),
619 seed_root,
620 },
621 PhaseScopeResources::default(),
622 )?);
623 }
624 }
625 Ok(results)
626 }
627
628 pub fn execute_campaign_phase_with_data_provider(
629 &self,
630 plan: &ExecutionPlan,
631 controllers: &RuntimeControllerRegistry,
632 data_provider: &dyn RuntimeDataProvider,
633 ctx: &mut RunContext,
634 phase: Phase,
635 ) -> Result<Vec<NodeResult>> {
636 plan.validate()?;
637 if phase == Phase::FitCv {
638 ctx.configure_global_oof_aggregation(plan, data_provider)?;
639 if let Some(nested) = nested_stacking_campaign_plan(plan)? {
640 return self.execute_nested_stacking_fit_cv(
641 plan,
642 controllers,
643 data_provider,
644 ctx,
645 &nested,
646 );
647 }
648 }
649 let mut results = Vec::new();
650 let fold_ids = if phase == Phase::FitCv {
651 plan.fold_set
652 .as_ref()
653 .map(|fold_set| {
654 fold_set
655 .folds
656 .iter()
657 .map(|fold| Some(fold.fold_id.clone()))
658 .collect::<Vec<_>>()
659 })
660 .unwrap_or_else(|| vec![None])
661 } else {
662 vec![None]
663 };
664 for variant in &plan.variants {
665 if ctx
666 .variant_id
667 .as_ref()
668 .is_some_and(|requested| requested != &variant.variant_id)
669 {
670 continue;
671 }
672 for fold_id in &fold_ids {
673 let seed_root = variant.seed.or(ctx.root_seed);
674 results.extend(self.execute_phase_scope(
675 plan,
676 controllers,
677 ctx,
678 PhaseScope {
679 phase,
680 variant_id: Some(variant.variant_id.clone()),
681 variant: Some(VariantExecutionSpec::from_plan(variant)),
682 fold_id: fold_id.clone(),
683 seed_root,
684 },
685 PhaseScopeResources {
686 data_provider: Some(data_provider),
687 ..Default::default()
688 },
689 )?);
690 }
691 }
692 Ok(results)
693 }
694
695 pub fn execute_campaign_phase_with_data_provider_and_artifact_store(
696 &self,
697 plan: &ExecutionPlan,
698 controllers: &RuntimeControllerRegistry,
699 data_provider: &dyn RuntimeDataProvider,
700 artifact_store: &mut InMemoryArtifactStore,
701 ctx: &mut RunContext,
702 phase: Phase,
703 ) -> Result<Vec<NodeResult>> {
704 plan.validate()?;
705 if phase == Phase::FitCv {
706 ctx.configure_global_oof_aggregation(plan, data_provider)?;
707 if let Some(nested) = nested_stacking_campaign_plan(plan)? {
708 return self.execute_nested_stacking_fit_cv(
712 plan,
713 controllers,
714 data_provider,
715 ctx,
716 &nested,
717 );
718 }
719 }
720 let mut results = Vec::new();
721 let fold_ids = if phase == Phase::FitCv {
722 plan.fold_set
723 .as_ref()
724 .map(|fold_set| {
725 fold_set
726 .folds
727 .iter()
728 .map(|fold| Some(fold.fold_id.clone()))
729 .collect::<Vec<_>>()
730 })
731 .unwrap_or_else(|| vec![None])
732 } else {
733 vec![None]
734 };
735 for variant in &plan.variants {
736 if ctx
737 .variant_id
738 .as_ref()
739 .is_some_and(|requested| requested != &variant.variant_id)
740 {
741 continue;
742 }
743 for fold_id in &fold_ids {
744 let seed_root = variant.seed.or(ctx.root_seed);
745 results.extend(self.execute_phase_scope(
746 plan,
747 controllers,
748 ctx,
749 PhaseScope {
750 phase,
751 variant_id: Some(variant.variant_id.clone()),
752 variant: Some(VariantExecutionSpec::from_plan(variant)),
753 fold_id: fold_id.clone(),
754 seed_root,
755 },
756 PhaseScopeResources {
757 data_provider: Some(data_provider),
758 artifact_store: Some(&mut *artifact_store),
759 ..Default::default()
760 },
761 )?);
762 }
763 }
764 Ok(results)
765 }
766
767 fn execute_nested_stacking_fit_cv(
776 &self,
777 plan: &ExecutionPlan,
778 controllers: &RuntimeControllerRegistry,
779 data_provider: &dyn RuntimeDataProvider,
780 ctx: &mut RunContext,
781 nested: &NestedStackingCampaignPlan,
782 ) -> Result<Vec<NodeResult>> {
783 let parent_fold_ids = nested
784 .outer_scopes
785 .iter()
786 .map(|outer| outer.outer_fold_id.clone())
787 .collect::<BTreeSet<_>>();
788 if let Some(existing) = &ctx.validation_scoring_fold_ids {
789 if existing != &parent_fold_ids {
790 return Err(DagMlError::RuntimeValidation(
791 "nested stacking cannot reuse a run context with a different report-grade outer fold set"
792 .to_string(),
793 ));
794 }
795 } else {
796 ctx.validation_scoring_fold_ids = Some(parent_fold_ids);
797 }
798 let mut results = Vec::new();
799 for variant in &plan.variants {
800 if ctx
801 .variant_id
802 .as_ref()
803 .is_some_and(|requested| requested != &variant.variant_id)
804 {
805 continue;
806 }
807 let seed_root = variant.seed.or(ctx.root_seed);
808 let variant_id = Some(variant.variant_id.clone());
809 let variant_spec = Some(VariantExecutionSpec::from_plan(variant));
810 for outer in &nested.outer_scopes {
811 for inner_fold in &outer.inner.inner_fold_set.folds {
812 results.extend(self.execute_phase_scope(
813 plan,
814 controllers,
815 ctx,
816 PhaseScope {
817 phase: Phase::FitCv,
818 variant_id: variant_id.clone(),
819 variant: variant_spec.clone(),
820 fold_id: Some(inner_fold.fold_id.clone()),
821 seed_root,
822 },
823 PhaseScopeResources {
824 data_provider: Some(data_provider),
825 fold_set_override: Some(&outer.inner.inner_fold_set),
826 node_filter: Some(&nested.base_node_ids),
827 suppress_inner_cv: true,
828 ..Default::default()
829 },
830 )?);
831 }
832
833 results.extend(self.execute_phase_scope(
836 plan,
837 controllers,
838 ctx,
839 PhaseScope {
840 phase: Phase::FitCv,
841 variant_id: variant_id.clone(),
842 variant: variant_spec.clone(),
843 fold_id: Some(outer.outer_fold_id.clone()),
844 seed_root,
845 },
846 PhaseScopeResources {
847 data_provider: Some(data_provider),
848 node_filter: Some(&nested.base_node_ids),
849 suppress_inner_cv: true,
850 ..Default::default()
851 },
852 )?);
853
854 let meta_only = BTreeSet::from([nested.meta_node_id.clone()]);
855 results.extend(self.execute_phase_scope(
856 plan,
857 controllers,
858 ctx,
859 PhaseScope {
860 phase: Phase::FitCv,
861 variant_id: variant_id.clone(),
862 variant: variant_spec.clone(),
863 fold_id: Some(outer.outer_fold_id.clone()),
864 seed_root,
865 },
866 PhaseScopeResources {
867 data_provider: Some(data_provider),
868 node_filter: Some(&meta_only),
869 suppress_inner_cv: true,
870 nested_stacking: Some(NestedStackingInput {
871 meta_node_id: &nested.meta_node_id,
872 inner: &outer.inner,
873 }),
874 ..Default::default()
875 },
876 )?);
877 }
878 }
879 Ok(results)
880 }
881
882 pub fn execute_bundle_replay(
883 &self,
884 replay: BundleReplayExecution<'_>,
885 ctx: &mut RunContext,
886 ) -> Result<Vec<NodeResult>> {
887 replay.bundle.validate_against_plan(replay.plan)?;
888 replay
889 .replay_request
890 .validate_for_bundle_with_prediction_cache_store(
891 replay.bundle,
892 replay.prediction_cache_store.is_some(),
893 )?;
894 replay
895 .bundle
896 .validate_replay_envelopes(replay.data_envelopes)?;
897 let prediction_cache_contracts = if replay.replay_request.phase == Phase::Refit {
898 Some(replay_prediction_cache_contracts(replay.bundle)?)
899 } else {
900 None
901 };
902 if replay.replay_request.phase == Phase::Refit {
903 preload_replay_prediction_cache_store(
904 replay.bundle,
905 replay.prediction_cache_store,
906 ctx,
907 )?;
908 }
909 let replay_artifacts = materialize_replay_artifact_handles(
910 replay.plan,
911 replay.bundle,
912 replay.replay_request,
913 replay.artifact_store,
914 ctx,
915 )?;
916 let selected_variant = replay
917 .bundle
918 .selected_variant_id
919 .as_ref()
920 .map(|selected| {
921 replay
922 .plan
923 .variants
924 .iter()
925 .find(|variant| &variant.variant_id == selected)
926 .map(VariantExecutionSpec::from_plan)
927 .ok_or_else(|| {
928 DagMlError::RuntimeValidation(format!(
929 "bundle `{}` selected unknown variant `{selected}`",
930 replay.bundle.bundle_id
931 ))
932 })
933 })
934 .transpose()?;
935 let seed_root = selected_variant
936 .as_ref()
937 .and_then(|variant| variant.seed)
938 .or(ctx.root_seed);
939
940 self.execute_phase_scope(
941 replay.plan,
942 replay.controllers,
943 ctx,
944 PhaseScope {
945 phase: replay.replay_request.phase,
946 variant_id: replay.bundle.selected_variant_id.clone(),
947 variant: selected_variant,
948 fold_id: None,
949 seed_root,
950 },
951 PhaseScopeResources {
952 data_provider: Some(replay.data_provider),
953 replay_artifact_handles: Some(&replay_artifacts.handles),
954 replay_artifact_inputs: Some(&replay_artifacts.inputs),
955 replay_bundle_id: Some(&replay.bundle.bundle_id),
956 data_envelopes: Some(replay.data_envelopes),
957 prediction_cache_store: replay.prediction_cache_store,
958 prediction_cache_contracts: prediction_cache_contracts.as_ref(),
959 ..Default::default()
960 },
961 )
962 }
963
964 fn execute_phase_scope(
965 &self,
966 plan: &ExecutionPlan,
967 controllers: &RuntimeControllerRegistry,
968 ctx: &mut RunContext,
969 scope: PhaseScope,
970 mut resources: PhaseScopeResources<'_>,
971 ) -> Result<Vec<NodeResult>> {
972 let _phase_span = crate::observability::phase_span(
973 ctx.run_id.as_str(),
974 plan.id.as_str(),
975 scope.phase.as_str(),
976 scope.variant_id.as_ref().map(VariantId::as_str),
977 scope.fold_id.as_ref().map(FoldId::as_str),
978 )
979 .entered();
980 let mut results = Vec::new();
981 let mut output_handles = BTreeMap::<NodeId, BTreeMap<String, HandleRef>>::new();
982 let mut output_data_views =
983 BTreeMap::<NodeId, BTreeMap<String, DataProviderViewSpec>>::new();
984 let mut input_lineage = BTreeMap::<NodeId, LineageId>::new();
985
986 for level in plan.node_parallel_levels_for_phase(scope.phase)? {
987 for node_id in &level {
988 if resources
989 .node_filter
990 .is_some_and(|allowed| !allowed.contains(node_id))
991 {
992 continue;
993 }
994 let node_plan = plan
995 .node_plans
996 .get(node_id)
997 .expect("execution plan was validated");
998 if let Some(reduction) = merge_reduction_mode(plan, node_plan) {
1005 if let Some(mut result) =
1006 reassemble_branch_merge(plan, node_plan, ctx, &scope, reduction)?
1007 {
1008 let task_node_plan = effective_node_plan_for_scope(node_plan, &scope)?;
1009 let task = NodeTask {
1010 inner_fold_set: None,
1011 run_id: ctx.run_id.clone(),
1012 node_plan: task_node_plan.clone(),
1013 phase: scope.phase,
1014 variant_id: scope.variant_id.clone(),
1015 variant: scope.variant.clone(),
1016 fold_id: scope.fold_id.clone(),
1017 branch_path: Vec::new(),
1018 input_handles: BTreeMap::new(),
1019 data_views: BTreeMap::new(),
1020 prediction_inputs: BTreeMap::new(),
1021 artifact_inputs: BTreeMap::new(),
1022 required_loss_attestations: NodeTask::required_loss_attestations_for(
1023 &task_node_plan,
1024 scope.phase,
1025 )?,
1026 fit_influence: FitInfluenceTask::default(),
1027 seed: None,
1028 };
1029 normalize_result_prediction_ports(plan, &task, &mut result)?;
1030 result.validate_for_task(&task)?;
1031 for prediction in &result.predictions {
1032 ctx.prediction_store.append(prediction.clone())?;
1033 }
1034 apply_result_scoring(
1035 &result,
1036 &mut ctx.score_collector,
1037 &mut ctx.regression_target_records,
1038 )?;
1039 ctx.lineage.record(result.lineage.clone())?;
1040 output_handles.insert(node_id.clone(), result.outputs.clone());
1041 input_lineage.insert(node_id.clone(), result.lineage.record_id.clone());
1042 results.push(result);
1043 }
1044 continue;
1045 }
1046 let controller = controllers.get(&node_plan.controller_id).ok_or_else(|| {
1047 DagMlError::RuntimeValidation(format!(
1048 "runtime controller `{}` is not registered",
1049 node_plan.controller_id
1050 ))
1051 })?;
1052 let collected_inputs = collect_input_handles(
1053 plan,
1054 node_plan,
1055 &output_handles,
1056 &output_data_views,
1057 &resources,
1058 ctx,
1059 &scope,
1060 )?;
1061 if collected_inputs.skip_node {
1062 continue;
1063 }
1064 let mut input_handles = collected_inputs.handles;
1065 let mut prediction_inputs = collected_inputs.prediction_inputs;
1066 if let Some(nested) = resources.nested_stacking.as_ref() {
1067 replace_nested_stacking_fit_cv_inputs(
1068 plan,
1069 node_plan,
1070 ctx,
1071 &scope,
1072 nested,
1073 &mut input_handles,
1074 &mut prediction_inputs,
1075 )?;
1076 }
1077 let mut artifact_inputs = BTreeMap::new();
1078 if let Some(node_artifact_handles) = resources
1079 .replay_artifact_handles
1080 .and_then(|handles| handles.get(node_id))
1081 {
1082 for (key, handle) in node_artifact_handles {
1083 if input_handles.insert(key.clone(), handle.clone()).is_some() {
1084 return Err(DagMlError::RuntimeValidation(format!(
1085 "node `{node_id}` received duplicate replay artifact input `{key}`"
1086 )));
1087 }
1088 }
1089 }
1090 if let Some(node_artifact_inputs) = resources
1091 .replay_artifact_inputs
1092 .and_then(|inputs| inputs.get(node_id))
1093 {
1094 for (key, spec) in node_artifact_inputs {
1095 if artifact_inputs.insert(key.clone(), spec.clone()).is_some() {
1096 return Err(DagMlError::RuntimeValidation(format!(
1097 "node `{node_id}` received duplicate replay artifact metadata `{key}`"
1098 )));
1099 }
1100 }
1101 }
1102 let task_node_plan = effective_node_plan_for_scope(node_plan, &scope)?;
1103 let inner_fold_set = (!resources.suppress_inner_cv)
1104 .then(|| {
1105 inner_fold_set_for_scope(
1106 &plan.campaign,
1107 plan.fold_set.as_ref(),
1108 node_plan,
1109 &scope,
1110 )
1111 })
1112 .transpose()?
1113 .flatten();
1114 let fit_influence = fit_influence_task_for_node(
1115 plan,
1116 &task_node_plan,
1117 &collected_inputs.data_views,
1118 )?;
1119 let task = NodeTask {
1120 inner_fold_set,
1121 run_id: ctx.run_id.clone(),
1122 node_plan: task_node_plan.clone(),
1123 phase: scope.phase,
1124 variant_id: scope.variant_id.clone(),
1125 variant: scope.variant.clone(),
1126 fold_id: scope.fold_id.clone(),
1127 branch_path: Vec::new(),
1128 input_handles,
1129 data_views: collected_inputs.data_views,
1130 prediction_inputs,
1131 artifact_inputs,
1132 required_loss_attestations: NodeTask::required_loss_attestations_for(
1133 &task_node_plan,
1134 scope.phase,
1135 )?,
1136 fit_influence,
1137 seed: derive_task_seed(
1138 scope.seed_root,
1139 scope.variant_id.as_ref(),
1140 scope.fold_id.as_ref(),
1141 &task_node_plan,
1142 scope.phase,
1143 ),
1144 };
1145 let _node_span = crate::observability::node_span(
1146 task.run_id.as_str(),
1147 plan.id.as_str(),
1148 task.phase.as_str(),
1149 task.node_plan.node_id.as_str(),
1150 task.node_plan.controller_id.as_str(),
1151 )
1152 .entered();
1153 let mut result = if task.node_plan.kind == NodeKind::Tuner {
1154 return Err(DagMlError::RuntimeValidation(format!(
1155 "tuner node `{}` requires execute_hpo_campaign with an explicit RuntimeHpoExecutionContext",
1156 task.node_plan.node_id
1157 )));
1158 } else {
1159 match resources.data_provider {
1160 Some(data_provider) => {
1161 controller.invoke_with_data_provider(&task, data_provider)?
1162 }
1163 None => controller.invoke(&task)?,
1164 }
1165 };
1166 record_fit_influence_diagnostic(&task, &mut result);
1167 normalize_result_prediction_ports(plan, &task, &mut result)?;
1168 result.validate_for_task(&task)?;
1169 apply_result_prediction_aggregation(
1170 plan,
1171 controllers,
1172 &task,
1173 &mut result,
1174 &resources,
1175 )?;
1176 attach_coordinator_input_lineage(
1177 &mut result,
1178 plan,
1179 &task.node_plan.node_id,
1180 &input_lineage,
1181 )?;
1182 if let Some(store) = resources.artifact_store.as_deref_mut() {
1183 if scope.phase == Phase::Refit {
1184 store.capture_refit_artifacts(&task, &result)?;
1185 }
1186 }
1187 for prediction in &result.predictions {
1188 ctx.prediction_store.append(prediction.clone())?;
1189 }
1190 for prediction in &result.aggregated_predictions {
1191 ctx.aggregated_prediction_store.append(prediction.clone())?;
1192 }
1193 apply_result_scoring(
1194 &result,
1195 &mut ctx.score_collector,
1196 &mut ctx.regression_target_records,
1197 )?;
1198 ctx.lineage.record(result.lineage.clone())?;
1199 let data_views = derive_output_data_views(plan, &task, &result)?;
1200 output_handles.insert(node_id.clone(), result.outputs.clone());
1201 output_data_views.insert(node_id.clone(), data_views);
1202 input_lineage.insert(node_id.clone(), result.lineage.record_id.clone());
1203 results.push(result);
1204 }
1205 }
1206
1207 Ok(results)
1208 }
1209}
1210
1211impl ParallelScheduler {
1212 pub fn execute_phase(
1213 &self,
1214 plan: &ExecutionPlan,
1215 controllers: &RuntimeControllerRegistry,
1216 ctx: &mut RunContext,
1217 phase: Phase,
1218 ) -> Result<Vec<NodeResult>> {
1219 plan.validate()?;
1220 let variant_id = ctx.variant_id.clone();
1221 let seed_root = ctx.root_seed;
1222 self.execute_phase_scope(
1223 plan,
1224 controllers,
1225 ctx,
1226 PhaseScope {
1227 phase,
1228 variant_id,
1229 variant: None,
1230 fold_id: None,
1231 seed_root,
1232 },
1233 PhaseScopeResources::default(),
1234 )
1235 }
1236
1237 pub fn execute_phase_with_data_provider(
1238 &self,
1239 plan: &ExecutionPlan,
1240 controllers: &RuntimeControllerRegistry,
1241 data_provider: &dyn RuntimeDataProvider,
1242 ctx: &mut RunContext,
1243 phase: Phase,
1244 ) -> Result<Vec<NodeResult>> {
1245 plan.validate()?;
1246 let variant_id = ctx.variant_id.clone();
1247 let seed_root = ctx.root_seed;
1248 self.execute_phase_scope(
1249 plan,
1250 controllers,
1251 ctx,
1252 PhaseScope {
1253 phase,
1254 variant_id,
1255 variant: None,
1256 fold_id: None,
1257 seed_root,
1258 },
1259 PhaseScopeResources {
1260 data_provider: Some(data_provider),
1261 ..Default::default()
1262 },
1263 )
1264 }
1265
1266 pub fn execute_campaign_phase(
1267 &self,
1268 plan: &ExecutionPlan,
1269 controllers: &RuntimeControllerRegistry,
1270 ctx: &mut RunContext,
1271 phase: Phase,
1272 ) -> Result<Vec<NodeResult>> {
1273 plan.validate()?;
1274 if phase == Phase::FitCv && nested_stacking_campaign_plan(plan)?.is_some() {
1275 return Err(DagMlError::RuntimeValidation(
1276 "nested stacking FIT_CV is scheduler-serial by construction; use SequentialScheduler so inner OOF evidence is retained before outer evaluation"
1277 .to_string(),
1278 ));
1279 }
1280 let mut results = Vec::new();
1281 let fold_ids = if phase == Phase::FitCv {
1282 plan.fold_set
1283 .as_ref()
1284 .map(|fold_set| {
1285 fold_set
1286 .folds
1287 .iter()
1288 .map(|fold| Some(fold.fold_id.clone()))
1289 .collect::<Vec<_>>()
1290 })
1291 .unwrap_or_else(|| vec![None])
1292 } else {
1293 vec![None]
1294 };
1295 for variant in &plan.variants {
1296 if ctx
1297 .variant_id
1298 .as_ref()
1299 .is_some_and(|requested| requested != &variant.variant_id)
1300 {
1301 continue;
1302 }
1303 for fold_id in &fold_ids {
1304 let seed_root = variant.seed.or(ctx.root_seed);
1305 results.extend(self.execute_phase_scope(
1306 plan,
1307 controllers,
1308 ctx,
1309 PhaseScope {
1310 phase,
1311 variant_id: Some(variant.variant_id.clone()),
1312 variant: Some(VariantExecutionSpec::from_plan(variant)),
1313 fold_id: fold_id.clone(),
1314 seed_root,
1315 },
1316 PhaseScopeResources::default(),
1317 )?);
1318 }
1319 }
1320 Ok(results)
1321 }
1322
1323 pub fn execute_campaign_phase_with_data_provider(
1324 &self,
1325 plan: &ExecutionPlan,
1326 controllers: &RuntimeControllerRegistry,
1327 data_provider: &dyn RuntimeDataProvider,
1328 ctx: &mut RunContext,
1329 phase: Phase,
1330 ) -> Result<Vec<NodeResult>> {
1331 plan.validate()?;
1332 if phase == Phase::FitCv {
1333 ctx.configure_global_oof_aggregation(plan, data_provider)?;
1334 }
1335 if phase == Phase::FitCv && nested_stacking_campaign_plan(plan)?.is_some() {
1336 return Err(DagMlError::RuntimeValidation(
1337 "nested stacking FIT_CV is scheduler-serial by construction; use SequentialScheduler so inner OOF evidence is retained before outer evaluation"
1338 .to_string(),
1339 ));
1340 }
1341 let mut results = Vec::new();
1342 let fold_ids = if phase == Phase::FitCv {
1343 plan.fold_set
1344 .as_ref()
1345 .map(|fold_set| {
1346 fold_set
1347 .folds
1348 .iter()
1349 .map(|fold| Some(fold.fold_id.clone()))
1350 .collect::<Vec<_>>()
1351 })
1352 .unwrap_or_else(|| vec![None])
1353 } else {
1354 vec![None]
1355 };
1356 for variant in &plan.variants {
1357 if ctx
1358 .variant_id
1359 .as_ref()
1360 .is_some_and(|requested| requested != &variant.variant_id)
1361 {
1362 continue;
1363 }
1364 for fold_id in &fold_ids {
1365 let seed_root = variant.seed.or(ctx.root_seed);
1366 results.extend(self.execute_phase_scope(
1367 plan,
1368 controllers,
1369 ctx,
1370 PhaseScope {
1371 phase,
1372 variant_id: Some(variant.variant_id.clone()),
1373 variant: Some(VariantExecutionSpec::from_plan(variant)),
1374 fold_id: fold_id.clone(),
1375 seed_root,
1376 },
1377 PhaseScopeResources {
1378 data_provider: Some(data_provider),
1379 ..Default::default()
1380 },
1381 )?);
1382 }
1383 }
1384 Ok(results)
1385 }
1386
1387 pub fn execute_campaign_phase_with_data_provider_and_artifact_store(
1388 &self,
1389 plan: &ExecutionPlan,
1390 controllers: &RuntimeControllerRegistry,
1391 data_provider: &dyn RuntimeDataProvider,
1392 artifact_store: &mut InMemoryArtifactStore,
1393 ctx: &mut RunContext,
1394 phase: Phase,
1395 ) -> Result<Vec<NodeResult>> {
1396 plan.validate()?;
1397 if phase == Phase::FitCv && nested_stacking_campaign_plan(plan)?.is_some() {
1398 return Err(DagMlError::RuntimeValidation(
1399 "nested stacking FIT_CV is scheduler-serial by construction; use SequentialScheduler so inner OOF evidence is retained before outer evaluation"
1400 .to_string(),
1401 ));
1402 }
1403 let mut results = Vec::new();
1404 let fold_ids = if phase == Phase::FitCv {
1405 plan.fold_set
1406 .as_ref()
1407 .map(|fold_set| {
1408 fold_set
1409 .folds
1410 .iter()
1411 .map(|fold| Some(fold.fold_id.clone()))
1412 .collect::<Vec<_>>()
1413 })
1414 .unwrap_or_else(|| vec![None])
1415 } else {
1416 vec![None]
1417 };
1418 for variant in &plan.variants {
1419 if ctx
1420 .variant_id
1421 .as_ref()
1422 .is_some_and(|requested| requested != &variant.variant_id)
1423 {
1424 continue;
1425 }
1426 for fold_id in &fold_ids {
1427 let seed_root = variant.seed.or(ctx.root_seed);
1428 results.extend(self.execute_phase_scope(
1429 plan,
1430 controllers,
1431 ctx,
1432 PhaseScope {
1433 phase,
1434 variant_id: Some(variant.variant_id.clone()),
1435 variant: Some(VariantExecutionSpec::from_plan(variant)),
1436 fold_id: fold_id.clone(),
1437 seed_root,
1438 },
1439 PhaseScopeResources {
1440 data_provider: Some(data_provider),
1441 artifact_store: Some(&mut *artifact_store),
1442 ..Default::default()
1443 },
1444 )?);
1445 }
1446 }
1447 Ok(results)
1448 }
1449
1450 pub fn execute_bundle_replay(
1451 &self,
1452 replay: BundleReplayExecution<'_>,
1453 ctx: &mut RunContext,
1454 ) -> Result<Vec<NodeResult>> {
1455 replay.bundle.validate_against_plan(replay.plan)?;
1456 replay
1457 .replay_request
1458 .validate_for_bundle_with_prediction_cache_store(
1459 replay.bundle,
1460 replay.prediction_cache_store.is_some(),
1461 )?;
1462 replay
1463 .bundle
1464 .validate_replay_envelopes(replay.data_envelopes)?;
1465 let prediction_cache_contracts = if replay.replay_request.phase == Phase::Refit {
1466 Some(replay_prediction_cache_contracts(replay.bundle)?)
1467 } else {
1468 None
1469 };
1470 if replay.replay_request.phase == Phase::Refit {
1471 preload_replay_prediction_cache_store(
1472 replay.bundle,
1473 replay.prediction_cache_store,
1474 ctx,
1475 )?;
1476 }
1477 let replay_artifacts = materialize_replay_artifact_handles(
1478 replay.plan,
1479 replay.bundle,
1480 replay.replay_request,
1481 replay.artifact_store,
1482 ctx,
1483 )?;
1484 let selected_variant = replay
1485 .bundle
1486 .selected_variant_id
1487 .as_ref()
1488 .map(|selected| {
1489 replay
1490 .plan
1491 .variants
1492 .iter()
1493 .find(|variant| &variant.variant_id == selected)
1494 .map(VariantExecutionSpec::from_plan)
1495 .ok_or_else(|| {
1496 DagMlError::RuntimeValidation(format!(
1497 "bundle `{}` selected unknown variant `{selected}`",
1498 replay.bundle.bundle_id
1499 ))
1500 })
1501 })
1502 .transpose()?;
1503 let seed_root = selected_variant
1504 .as_ref()
1505 .and_then(|variant| variant.seed)
1506 .or(ctx.root_seed);
1507
1508 self.execute_phase_scope(
1509 replay.plan,
1510 replay.controllers,
1511 ctx,
1512 PhaseScope {
1513 phase: replay.replay_request.phase,
1514 variant_id: replay.bundle.selected_variant_id.clone(),
1515 variant: selected_variant,
1516 fold_id: None,
1517 seed_root,
1518 },
1519 PhaseScopeResources {
1520 data_provider: Some(replay.data_provider),
1521 replay_artifact_handles: Some(&replay_artifacts.handles),
1522 replay_artifact_inputs: Some(&replay_artifacts.inputs),
1523 replay_bundle_id: Some(&replay.bundle.bundle_id),
1524 data_envelopes: Some(replay.data_envelopes),
1525 prediction_cache_store: replay.prediction_cache_store,
1526 prediction_cache_contracts: prediction_cache_contracts.as_ref(),
1527 ..Default::default()
1528 },
1529 )
1530 }
1531
1532 fn execute_phase_scope(
1533 &self,
1534 plan: &ExecutionPlan,
1535 controllers: &RuntimeControllerRegistry,
1536 ctx: &mut RunContext,
1537 scope: PhaseScope,
1538 mut resources: PhaseScopeResources<'_>,
1539 ) -> Result<Vec<NodeResult>> {
1540 let phase_span = crate::observability::phase_span(
1544 ctx.run_id.as_str(),
1545 plan.id.as_str(),
1546 scope.phase.as_str(),
1547 scope.variant_id.as_ref().map(VariantId::as_str),
1548 scope.fold_id.as_ref().map(FoldId::as_str),
1549 );
1550 let _phase_entered = phase_span.clone().entered();
1551 let plan_id = plan.id.as_str();
1553 plan.validate_parallel_controller_capabilities(self.max_workers, scope.phase)?;
1554 let mut results = Vec::new();
1555 let mut output_handles = BTreeMap::<NodeId, BTreeMap<String, HandleRef>>::new();
1556 let mut output_data_views =
1557 BTreeMap::<NodeId, BTreeMap<String, DataProviderViewSpec>>::new();
1558 let mut input_lineage = BTreeMap::<NodeId, LineageId>::new();
1559
1560 for level in plan.node_parallel_levels_for_phase(scope.phase)? {
1561 let mut prepared = Vec::<PreparedNodeTask>::new();
1562 let mut merge_nodes = Vec::<(NodeId, MergeReduction)>::new();
1569 for node_id in &level {
1570 let node_plan = plan
1571 .node_plans
1572 .get(node_id)
1573 .expect("execution plan was validated");
1574 if let Some(reduction) = merge_reduction_mode(plan, node_plan) {
1575 merge_nodes.push((node_id.clone(), reduction));
1576 continue;
1577 }
1578 let collected_inputs = collect_input_handles(
1579 plan,
1580 node_plan,
1581 &output_handles,
1582 &output_data_views,
1583 &resources,
1584 ctx,
1585 &scope,
1586 )?;
1587 if collected_inputs.skip_node {
1588 continue;
1589 }
1590 let mut input_handles = collected_inputs.handles;
1591 let mut artifact_inputs = BTreeMap::new();
1592 if let Some(node_artifact_handles) = resources
1593 .replay_artifact_handles
1594 .and_then(|handles| handles.get(node_id))
1595 {
1596 for (key, handle) in node_artifact_handles {
1597 if input_handles.insert(key.clone(), handle.clone()).is_some() {
1598 return Err(DagMlError::RuntimeValidation(format!(
1599 "node `{node_id}` received duplicate replay artifact input `{key}`"
1600 )));
1601 }
1602 }
1603 }
1604 if let Some(node_artifact_inputs) = resources
1605 .replay_artifact_inputs
1606 .and_then(|inputs| inputs.get(node_id))
1607 {
1608 for (key, spec) in node_artifact_inputs {
1609 if artifact_inputs.insert(key.clone(), spec.clone()).is_some() {
1610 return Err(DagMlError::RuntimeValidation(format!(
1611 "node `{node_id}` received duplicate replay artifact metadata `{key}`"
1612 )));
1613 }
1614 }
1615 }
1616 let task_node_plan = effective_node_plan_for_scope(node_plan, &scope)?;
1617 let inner_fold_set = inner_fold_set_for_scope(
1618 &plan.campaign,
1619 plan.fold_set.as_ref(),
1620 node_plan,
1621 &scope,
1622 )?;
1623 let fit_influence = fit_influence_task_for_node(
1624 plan,
1625 &task_node_plan,
1626 &collected_inputs.data_views,
1627 )?;
1628 prepared.push(PreparedNodeTask {
1629 node_id: node_id.clone(),
1630 task: NodeTask {
1631 inner_fold_set,
1632 run_id: ctx.run_id.clone(),
1633 node_plan: task_node_plan.clone(),
1634 phase: scope.phase,
1635 variant_id: scope.variant_id.clone(),
1636 variant: scope.variant.clone(),
1637 fold_id: scope.fold_id.clone(),
1638 branch_path: Vec::new(),
1639 input_handles,
1640 data_views: collected_inputs.data_views,
1641 prediction_inputs: collected_inputs.prediction_inputs,
1642 artifact_inputs,
1643 required_loss_attestations: NodeTask::required_loss_attestations_for(
1644 &task_node_plan,
1645 scope.phase,
1646 )?,
1647 fit_influence,
1648 seed: derive_task_seed(
1649 scope.seed_root,
1650 scope.variant_id.as_ref(),
1651 scope.fold_id.as_ref(),
1652 &task_node_plan,
1653 scope.phase,
1654 ),
1655 },
1656 });
1657 }
1658
1659 for chunk in prepared.chunks(self.max_workers) {
1660 let chunk_results = std::thread::scope(
1661 |thread_scope| -> Result<Vec<NodeResult>> {
1662 let mut handles = Vec::with_capacity(chunk.len());
1663 for prepared_task in chunk {
1664 let controller = controllers
1665 .get(&prepared_task.task.node_plan.controller_id)
1666 .ok_or_else(|| {
1667 DagMlError::RuntimeValidation(format!(
1668 "runtime controller `{}` is not registered",
1669 prepared_task.task.node_plan.controller_id
1670 ))
1671 })?;
1672 let worker_span = phase_span.clone();
1673 handles.push(thread_scope.spawn(move || {
1674 let _worker_span = worker_span.entered();
1675 let _node_span = crate::observability::node_span(
1676 prepared_task.task.run_id.as_str(),
1677 plan_id,
1678 prepared_task.task.phase.as_str(),
1679 prepared_task.task.node_plan.node_id.as_str(),
1680 prepared_task.task.node_plan.controller_id.as_str(),
1681 )
1682 .entered();
1683 let mut result =
1684 if prepared_task.task.node_plan.kind == NodeKind::Tuner {
1685 return Err(DagMlError::RuntimeValidation(format!(
1686 "tuner node `{}` requires execute_hpo_campaign with an explicit RuntimeHpoExecutionContext",
1687 prepared_task.task.node_plan.node_id
1688 )));
1689 } else {
1690 controller.invoke(&prepared_task.task)?
1696 };
1697 record_fit_influence_diagnostic(&prepared_task.task, &mut result);
1698 normalize_result_prediction_ports(
1699 plan,
1700 &prepared_task.task,
1701 &mut result,
1702 )?;
1703 result.validate_for_task(&prepared_task.task)?;
1704 Ok(result)
1705 }));
1706 }
1707 handles
1708 .into_iter()
1709 .map(|handle| {
1710 handle.join().map_err(|_| {
1711 DagMlError::RuntimeValidation(
1712 "parallel scheduler worker panicked".to_string(),
1713 )
1714 })?
1715 })
1716 .collect()
1717 },
1718 )?;
1719
1720 for (prepared_task, mut result) in chunk.iter().zip(chunk_results) {
1721 apply_result_prediction_aggregation(
1722 plan,
1723 controllers,
1724 &prepared_task.task,
1725 &mut result,
1726 &resources,
1727 )?;
1728 attach_coordinator_input_lineage(
1729 &mut result,
1730 plan,
1731 &prepared_task.task.node_plan.node_id,
1732 &input_lineage,
1733 )?;
1734 if let Some(store) = resources.artifact_store.as_deref_mut() {
1735 if scope.phase == Phase::Refit {
1736 store.capture_refit_artifacts(&prepared_task.task, &result)?;
1737 }
1738 }
1739 for prediction in &result.predictions {
1740 ctx.prediction_store.append(prediction.clone())?;
1741 }
1742 for prediction in &result.aggregated_predictions {
1743 ctx.aggregated_prediction_store.append(prediction.clone())?;
1744 }
1745 apply_result_scoring(
1746 &result,
1747 &mut ctx.score_collector,
1748 &mut ctx.regression_target_records,
1749 )?;
1750 ctx.lineage.record(result.lineage.clone())?;
1751 let data_views = derive_output_data_views(plan, &prepared_task.task, &result)?;
1752 output_handles.insert(prepared_task.node_id.clone(), result.outputs.clone());
1753 output_data_views.insert(prepared_task.node_id.clone(), data_views);
1754 input_lineage.insert(
1755 prepared_task.node_id.clone(),
1756 result.lineage.record_id.clone(),
1757 );
1758 results.push(result);
1759 }
1760 }
1761
1762 for (node_id, reduction) in &merge_nodes {
1767 let node_plan = plan
1768 .node_plans
1769 .get(node_id)
1770 .expect("execution plan was validated");
1771 if let Some(mut result) =
1772 reassemble_branch_merge(plan, node_plan, ctx, &scope, *reduction)?
1773 {
1774 let task_node_plan = effective_node_plan_for_scope(node_plan, &scope)?;
1775 let task = NodeTask {
1776 inner_fold_set: None,
1777 run_id: ctx.run_id.clone(),
1778 node_plan: task_node_plan.clone(),
1779 phase: scope.phase,
1780 variant_id: scope.variant_id.clone(),
1781 variant: scope.variant.clone(),
1782 fold_id: scope.fold_id.clone(),
1783 branch_path: Vec::new(),
1784 input_handles: BTreeMap::new(),
1785 data_views: BTreeMap::new(),
1786 prediction_inputs: BTreeMap::new(),
1787 artifact_inputs: BTreeMap::new(),
1788 required_loss_attestations: NodeTask::required_loss_attestations_for(
1789 &task_node_plan,
1790 scope.phase,
1791 )?,
1792 fit_influence: FitInfluenceTask::default(),
1793 seed: None,
1794 };
1795 normalize_result_prediction_ports(plan, &task, &mut result)?;
1796 result.validate_for_task(&task)?;
1797 for prediction in &result.predictions {
1798 ctx.prediction_store.append(prediction.clone())?;
1799 }
1800 apply_result_scoring(
1801 &result,
1802 &mut ctx.score_collector,
1803 &mut ctx.regression_target_records,
1804 )?;
1805 ctx.lineage.record(result.lineage.clone())?;
1806 output_handles.insert(node_id.clone(), result.outputs.clone());
1807 input_lineage.insert(node_id.clone(), result.lineage.record_id.clone());
1808 results.push(result);
1809 }
1810 }
1811 }
1812
1813 Ok(results)
1814 }
1815}
1816
1817#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1818enum HpoTrialTerminalState {
1819 Completed,
1820 Pruned,
1821 Failed,
1822}
1823
1824fn validate_hpo_checkpoint_result(
1825 checkpoint: &RuntimeHpoCheckpointResult,
1826 hpo: &RuntimeHpoExecutionContext,
1827 trial_variants: &BTreeMap<i64, VariantId>,
1828 terminal_trials: &BTreeMap<i64, HpoTrialTerminalState>,
1829 history_at_start: u32,
1830) -> Result<()> {
1831 checkpoint.artifact.validate().map_err(|error| {
1832 DagMlError::RuntimeValidation(format!(
1833 "runtime HPO checkpoint artifact is invalid: {error}"
1834 ))
1835 })?;
1836 if checkpoint.operation_id != hpo.operation_id
1837 || checkpoint.controller_id != hpo.controller_id
1838 || checkpoint.target_node_id != hpo.target_node_id
1839 || checkpoint.provenance != hpo.provenance
1840 {
1841 return Err(DagMlError::RuntimeValidation(
1842 "runtime HPO checkpoint provenance does not exactly match its execution context"
1843 .to_string(),
1844 ));
1845 }
1846 let proposed_count = u32::try_from(trial_variants.len()).map_err(|_| {
1847 DagMlError::RuntimeValidation(
1848 "runtime HPO scheduler proposal count does not fit u32".to_string(),
1849 )
1850 })?;
1851 if checkpoint.trial_history_len != hpo.trial_budget_total
1852 || checkpoint.trial_history_len < history_at_start
1853 || checkpoint.trial_history_len - history_at_start != proposed_count
1854 {
1855 return Err(DagMlError::RuntimeValidation(
1856 "runtime HPO checkpoint native history is inconsistent with scheduler-observed trials"
1857 .to_string(),
1858 ));
1859 }
1860 if checkpoint.artifact.binding.controller_id != hpo.controller_id.as_str()
1861 || checkpoint.artifact.binding.controller_id != hpo.study.controller_id
1862 || checkpoint.artifact.binding.study_id != hpo.study.study_id
1863 || checkpoint.artifact.methods_abi != hpo.study.methods_abi
1864 {
1865 return Err(DagMlError::RuntimeValidation(
1866 "runtime HPO checkpoint binding/controller/study does not match the active tuner"
1867 .to_string(),
1868 ));
1869 }
1870 let expected_search_space = hpo.study.search_space.fingerprint().map_err(|error| {
1871 DagMlError::RuntimeValidation(format!(
1872 "runtime HPO cannot fingerprint the configured search space: {error}"
1873 ))
1874 })?;
1875 if checkpoint.artifact.binding.search_space_fingerprint != expected_search_space {
1876 return Err(DagMlError::RuntimeValidation(
1877 "runtime HPO checkpoint search-space binding does not match the active study"
1878 .to_string(),
1879 ));
1880 }
1881
1882 let completed_trial_ids = terminal_trials
1883 .iter()
1884 .filter_map(|(trial_id, state)| {
1885 (*state == HpoTrialTerminalState::Completed).then_some(*trial_id)
1886 })
1887 .collect::<BTreeSet<_>>();
1888 let mut proposal_trial_ids = BTreeSet::new();
1889 for proposal in &checkpoint.completed_proposals {
1890 if !proposal_trial_ids.insert(proposal.trial_id) {
1891 return Err(DagMlError::RuntimeValidation(format!(
1892 "runtime HPO checkpoint has duplicate completed proposal for trial `{}`",
1893 proposal.trial_id
1894 )));
1895 }
1896 if trial_variants.get(&proposal.trial_id) != Some(&proposal.variant.variant_id) {
1897 return Err(DagMlError::RuntimeValidation(format!(
1898 "runtime HPO checkpoint proposal for trial `{}` does not exactly match its scheduler proposal",
1899 proposal.trial_id
1900 )));
1901 }
1902 }
1903 if proposal_trial_ids != completed_trial_ids {
1904 return Err(DagMlError::RuntimeValidation(
1905 "runtime HPO checkpoint proposals must cover exactly the completed trials".to_string(),
1906 ));
1907 }
1908
1909 let mut report_trial_ids = BTreeSet::new();
1910 for completed in &checkpoint.completed_reports {
1911 if !report_trial_ids.insert(completed.trial_id) {
1912 return Err(DagMlError::RuntimeValidation(format!(
1913 "runtime HPO checkpoint has duplicate completed report for trial `{}`",
1914 completed.trial_id
1915 )));
1916 }
1917 if trial_variants.get(&completed.trial_id) != Some(&completed.variant_id)
1918 || !proposal_trial_ids.contains(&completed.trial_id)
1919 {
1920 return Err(DagMlError::RuntimeValidation(format!(
1921 "runtime HPO checkpoint report for trial `{}` does not match a completed proposal",
1922 completed.trial_id
1923 )));
1924 }
1925 let report = &completed.report;
1926 if report.producer_node != hpo.selection.producer_node
1927 || report.producer_port.as_deref() != Some(hpo.selection.producer_port.as_str())
1928 || report.partition != PredictionPartition::Validation
1929 || report
1930 .fold_id
1931 .as_ref()
1932 .is_none_or(|fold| fold.as_str() != "avg")
1933 || report.variant_id.as_ref() != Some(&completed.variant_id)
1934 || !report
1935 .metrics
1936 .get(hpo.selection.metric.name())
1937 .is_some_and(|score| score.is_finite())
1938 {
1939 return Err(DagMlError::RuntimeValidation(format!(
1940 "runtime HPO checkpoint report for trial `{}` is not its one finite target OOF average",
1941 completed.trial_id
1942 )));
1943 }
1944 }
1945 if report_trial_ids != completed_trial_ids {
1946 return Err(DagMlError::RuntimeValidation(
1947 "runtime HPO checkpoint reports must cover exactly one OOF average per completed trial"
1948 .to_string(),
1949 ));
1950 }
1951 Ok(())
1952}
1953
1954pub(crate) struct PreparedNodeTask {
1955 pub(crate) node_id: NodeId,
1956 pub(crate) task: NodeTask,
1957}
1958
1959#[cfg(test)]
1962#[allow(clippy::items_after_test_module)]
1963mod hpo_scheduler_tests {
1964 use std::collections::{BTreeMap, BTreeSet};
1965 use std::sync::{Arc, Mutex};
1966
1967 use sha2::{Digest, Sha256};
1968
1969 use super::*;
1970 use crate::controller::{
1971 ArtifactPolicy, ControllerCapability, ControllerFitScope, ControllerManifest,
1972 ControllerRegistry, RngPolicy,
1973 };
1974 use crate::data::InMemoryDataProvider;
1975 use crate::fold::{FoldAssignment, FoldPartitionMode};
1976 use crate::graph::{GraphInterface, GraphSpec, NodeSpec, PortSchema, PortSpec};
1977 use crate::hpo::{
1978 HpoDirection, HpoMetric, HpoOptimizerConfig, HpoParameter, HpoPruner, HpoSampler,
1979 HpoSearchSpace, HpoStudyBinding, MethodsHpoStudyConfig, N4moptCheckpointArtifact,
1980 N4MOPT_ARTIFACT_KIND, N4MOPT_CHECKPOINT_SCHEMA_VERSION, N4MOPT_FORMAT,
1981 };
1982 use crate::metrics::RegressionTargetBlock;
1983 use crate::oof::PredictionBlock;
1984 use crate::plan::{build_execution_plan, SplitInvocation};
1985
1986 struct HpoTestModel {
1987 id: ControllerId,
1988 trace: Arc<Mutex<Vec<String>>>,
1989 }
1990
1991 impl RuntimeController for HpoTestModel {
1992 fn controller_id(&self) -> &ControllerId {
1993 &self.id
1994 }
1995
1996 fn invoke(&self, task: &NodeTask) -> Result<NodeResult> {
1997 self.trace.lock().unwrap().push("model_cv".to_string());
1998 let sample_id = match task.fold_id.as_ref().map(FoldId::as_str) {
1999 Some("fold:0") => SampleId::new("sample:one").unwrap(),
2000 Some("fold:1") => SampleId::new("sample:two").unwrap(),
2001 other => {
2002 return Err(DagMlError::RuntimeValidation(format!(
2003 "HPO test model received unexpected fold {other:?}"
2004 )));
2005 }
2006 };
2007 Ok(NodeResult {
2008 schema_version: None,
2009 node_id: task.node_plan.node_id.clone(),
2010 outputs: BTreeMap::from([(
2011 "prediction".to_string(),
2012 HandleRef {
2013 handle: 2,
2014 kind: HandleKind::Prediction,
2015 owner_controller: self.id.clone(),
2016 },
2017 )]),
2018 predictions: vec![PredictionBlock {
2019 prediction_id: Some(format!("prediction:{}", task.fold_id.as_ref().unwrap())),
2020 producer_node: task.node_plan.node_id.clone(),
2021 producer_port: None,
2022 partition: PredictionPartition::Validation,
2023 fold_id: task.fold_id.clone(),
2024 sample_ids: vec![sample_id.clone()],
2025 values: vec![vec![1.0]],
2026 target_names: vec!["target".to_string()],
2027 }],
2028 observation_predictions: Vec::new(),
2029 aggregated_predictions: Vec::new(),
2030 explanations: Vec::new(),
2031 shape_deltas: Vec::new(),
2032 artifacts: Vec::new(),
2033 artifact_handles: BTreeMap::new(),
2034 fit_influence_diagnostics: Vec::new(),
2035 regression_targets: vec![RegressionTargetBlock {
2036 level: PredictionLevel::Sample,
2037 unit_ids: vec![PredictionUnitId::Sample(sample_id)],
2038 values: vec![vec![1.0]],
2039 target_names: vec!["target".to_string()],
2040 }],
2041 lineage: LineageRecord {
2042 record_id: LineageId::new(format!(
2043 "lineage:hpo-model:{}",
2044 task.fold_id.as_ref().unwrap()
2045 ))
2046 .unwrap(),
2047 run_id: task.run_id.clone(),
2048 node_id: task.node_plan.node_id.clone(),
2049 phase: task.phase,
2050 controller_id: self.id.clone(),
2051 controller_version: task.node_plan.controller_version.clone(),
2052 variant_id: task.variant_id.clone(),
2053 fold_id: task.fold_id.clone(),
2054 branch_path: Vec::new(),
2055 input_lineage: Vec::new(),
2056 artifact_refs: Vec::new(),
2057 params_fingerprint: task.node_plan.params_fingerprint.clone(),
2058 data_model_shape_fingerprint: None,
2059 aggregation_policy_fingerprint: None,
2060 seed: task.seed,
2061 unsafe_flags: BTreeSet::new(),
2062 metrics: BTreeMap::new(),
2063 loss_attestations: Vec::new(),
2064 early_stopping_records: Vec::new(),
2065 },
2066 })
2067 }
2068 }
2069
2070 struct HpoTestTuner {
2071 id: ControllerId,
2072 trace: Arc<Mutex<Vec<String>>>,
2073 history_len: u32,
2074 proposal_count: u32,
2075 }
2076
2077 struct HpoTestSession {
2078 proposals: Vec<RuntimeHpoProposal>,
2079 trace: Arc<Mutex<Vec<String>>>,
2080 checkpoint: N4moptCheckpointArtifact,
2081 history_len: u32,
2082 completed: Option<(i64, f64)>,
2083 }
2084
2085 impl RuntimeController for HpoTestTuner {
2086 fn controller_id(&self) -> &ControllerId {
2087 &self.id
2088 }
2089
2090 fn invoke(&self, task: &NodeTask) -> Result<NodeResult> {
2091 Err(DagMlError::RuntimeValidation(format!(
2092 "HPO test tuner `{}` was dispatched through generic invoke",
2093 task.node_plan.node_id
2094 )))
2095 }
2096
2097 fn create_tuner_session(
2098 &self,
2099 task: &RuntimeHpoCampaignTask,
2100 context: &RuntimeHpoExecutionContext,
2101 ) -> Result<Box<dyn RuntimeTunerSession>> {
2102 assert_eq!(task.operation_id, context.operation_id);
2103 self.trace
2104 .lock()
2105 .unwrap()
2106 .push("session_factory".to_string());
2107 let payload = vec![7_u8];
2108 let proposals = (0..self.proposal_count)
2109 .map(|offset| {
2110 let trial_id = i64::from(self.history_len + offset + 1);
2111 let mut variant = context.base_variant.clone();
2112 if self.history_len != 0 || self.proposal_count != 1 {
2113 variant.variant_id = VariantId::new(format!("hpo:trial:{trial_id}"))
2114 .map_err(|error| DagMlError::RuntimeValidation(error.to_string()))?;
2115 variant.fingerprint = format!("hpo-test-{trial_id}");
2116 }
2117 Ok(RuntimeHpoProposal { trial_id, variant })
2118 })
2119 .collect::<Result<Vec<_>>>()?;
2120 Ok(Box::new(HpoTestSession {
2121 proposals: proposals.into_iter().rev().collect(),
2122 trace: Arc::clone(&self.trace),
2123 history_len: self.history_len,
2124 completed: None,
2125 checkpoint: N4moptCheckpointArtifact {
2126 schema_version: N4MOPT_CHECKPOINT_SCHEMA_VERSION,
2127 artifact_kind: N4MOPT_ARTIFACT_KIND.to_string(),
2128 format: N4MOPT_FORMAT.to_string(),
2129 binding: HpoStudyBinding {
2130 controller_id: context.study.controller_id.clone(),
2131 study_id: context.study.study_id.clone(),
2132 search_space_fingerprint: context
2133 .study
2134 .search_space
2135 .fingerprint()
2136 .map_err(|error| DagMlError::RuntimeValidation(error.to_string()))?,
2137 optimizer_fingerprint: "optimizer:test".to_string(),
2138 },
2139 methods_abi: context.study.methods_abi.clone(),
2140 payload_sha256: format!("{:x}", Sha256::digest(&payload)),
2141 opaque_payload: payload,
2142 },
2143 }))
2144 }
2145 }
2146
2147 impl RuntimeTunerSession for HpoTestSession {
2148 fn trial_history_len(&self) -> Result<u32> {
2149 Ok(self.history_len)
2150 }
2151
2152 fn ask(&mut self) -> Result<Option<RuntimeHpoProposal>> {
2153 self.trace.lock().unwrap().push("ask".to_string());
2154 let proposal = self.proposals.pop();
2155 if proposal.is_some() {
2156 self.history_len += 1;
2157 }
2158 Ok(proposal)
2159 }
2160
2161 fn report_intermediate(
2162 &mut self,
2163 intermediate: RuntimeHpoIntermediate,
2164 ) -> Result<RuntimeHpoIntermediateOutcome> {
2165 assert_eq!(intermediate.step, 0);
2166 assert!(intermediate.score.is_finite());
2167 self.trace.lock().unwrap().push("intermediate".to_string());
2168 Ok(RuntimeHpoIntermediateOutcome::Continue)
2169 }
2170
2171 fn tell(&mut self, trial_id: i64, terminal: RuntimeHpoTerminal) -> Result<()> {
2172 assert!(trial_id > 0);
2173 assert!(
2174 matches!(terminal, RuntimeHpoTerminal::Completed { score } if score.is_finite())
2175 );
2176 self.trace.lock().unwrap().push("tell".to_string());
2177 if let RuntimeHpoTerminal::Completed { score } = terminal {
2178 self.completed = Some((trial_id, score));
2179 }
2180 Ok(())
2181 }
2182
2183 fn checkpoint(&self) -> Result<N4moptCheckpointArtifact> {
2184 self.trace.lock().unwrap().push("checkpoint".to_string());
2185 Ok(self.checkpoint.clone())
2186 }
2187
2188 fn incumbent(
2189 &self,
2190 variants: &BTreeMap<i64, VariantId>,
2191 ) -> Result<Option<RuntimeHpoIncumbent>> {
2192 let Some((trial_id, score)) = self.completed else {
2193 return Ok(None);
2194 };
2195 Ok(Some(RuntimeHpoIncumbent {
2196 trial_id,
2197 score,
2198 metric: "rmse".to_string(),
2199 direction: HpoDirection::Minimize,
2200 variant_id: variants.get(&trial_id).cloned().unwrap(),
2201 }))
2202 }
2203
2204 fn terminal_trial_snapshots(
2205 &self,
2206 variants: &BTreeMap<i64, VariantId>,
2207 ) -> Result<Vec<RuntimeHpoTerminalSnapshot>> {
2208 let (trial_id, score) = self.completed.ok_or_else(|| {
2209 DagMlError::RuntimeValidation("test HPO session has no completed trial".to_string())
2210 })?;
2211 Ok((1..=i64::from(self.history_len))
2212 .map(|id| {
2213 let completed = id == trial_id;
2214 RuntimeHpoTerminalSnapshot {
2215 trial: crate::hpo::HpoTrial {
2216 id,
2217 ask_sequence: id,
2218 terminal_sequence: Some(id),
2219 parameters: BTreeMap::new(),
2220 parameter_order: Vec::new(),
2221 status: if completed {
2222 crate::hpo::HpoTrialStatus::Completed
2223 } else {
2224 crate::hpo::HpoTrialStatus::Failed
2225 },
2226 score: completed.then_some(score),
2227 rung: 0,
2228 duration: 0.0,
2229 intermediates: Vec::new(),
2230 failure: (!completed).then(|| crate::hpo::HpoFailure {
2231 code: "RESTORED_TEST_FAILURE".to_string(),
2232 message: "synthetic restored terminal".to_string(),
2233 retryable: false,
2234 }),
2235 },
2236 variant_id: variants.get(&id).cloned(),
2237 }
2238 })
2239 .collect())
2240 }
2241 }
2242
2243 fn node(id: &str, kind: NodeKind, outputs: Vec<PortSpec>) -> NodeSpec {
2244 NodeSpec {
2245 id: NodeId::new(id).unwrap(),
2246 kind,
2247 operator: None,
2248 params: BTreeMap::new(),
2249 ports: PortSchema {
2250 inputs: Vec::new(),
2251 outputs,
2252 },
2253 metadata: BTreeMap::new(),
2254 seed_label: None,
2255 }
2256 }
2257
2258 fn manifest(id: &str, kind: NodeKind) -> ControllerManifest {
2259 ControllerManifest {
2260 controller_id: ControllerId::new(id).unwrap(),
2261 controller_version: "test".to_string(),
2262 operator_kind: kind,
2263 priority: 0,
2264 supported_phases: BTreeSet::from([Phase::FitCv]),
2265 input_ports: Vec::new(),
2266 output_ports: Vec::new(),
2267 data_requirements: None,
2268 capabilities: BTreeSet::from([
2269 ControllerCapability::Deterministic,
2270 ControllerCapability::EmitsPredictions,
2271 ]),
2272 operator_selectors: Vec::new(),
2273 fit_scope: ControllerFitScope::FoldTrain,
2274 rng_policy: RngPolicy::UsesCoreSeed,
2275 artifact_policy: ArtifactPolicy::Serializable,
2276 }
2277 }
2278
2279 #[test]
2280 fn hpo_campaign_invokes_registered_session_and_routes_oof_feedback() {
2281 let target = NodeId::new("model:score").unwrap();
2282 let graph = GraphSpec {
2283 id: "graph:hpo.scheduler".to_string(),
2284 interface: GraphInterface::default(),
2285 nodes: vec![node(
2286 "model:score",
2287 NodeKind::Model,
2288 vec![PortSpec {
2289 name: "prediction".to_string(),
2290 kind: PortKind::Prediction,
2291 representation: None,
2292 cardinality: crate::graph::PortCardinality::One,
2293 unit_level: None,
2294 alignment_key: None,
2295 target_level: None,
2296 description: String::new(),
2297 }],
2298 )],
2299 edges: Vec::new(),
2300 search_space_fingerprint: None,
2301 metadata: BTreeMap::new(),
2302 };
2303 let fold_set = FoldSet {
2304 id: "folds:hpo".to_string(),
2305 sample_ids: vec![
2306 SampleId::new("sample:one").unwrap(),
2307 SampleId::new("sample:two").unwrap(),
2308 ],
2309 folds: vec![
2310 FoldAssignment {
2311 fold_id: FoldId::new("fold:0").unwrap(),
2312 train_sample_ids: vec![SampleId::new("sample:two").unwrap()],
2313 validation_sample_ids: vec![SampleId::new("sample:one").unwrap()],
2314 metadata: BTreeMap::new(),
2315 },
2316 FoldAssignment {
2317 fold_id: FoldId::new("fold:1").unwrap(),
2318 train_sample_ids: vec![SampleId::new("sample:one").unwrap()],
2319 validation_sample_ids: vec![SampleId::new("sample:two").unwrap()],
2320 metadata: BTreeMap::new(),
2321 },
2322 ],
2323 sample_groups: BTreeMap::new(),
2324 partition_mode: FoldPartitionMode::Partition,
2325 };
2326 let mut registry = ControllerRegistry::new();
2327 registry
2328 .register(manifest("controller:model", NodeKind::Model))
2329 .unwrap();
2330 let plan = build_execution_plan(
2331 "plan:hpo.scheduler",
2332 graph,
2333 CampaignSpec {
2334 inner_cv: None,
2335 id: "campaign:hpo.scheduler".to_string(),
2336 root_seed: Some(13),
2337 leakage_policy: Default::default(),
2338 aggregation_policy: Default::default(),
2339 split_invocation: Some(SplitInvocation {
2340 id: "split:hpo".to_string(),
2341 controller_id: None,
2342 leakage_policy: Default::default(),
2343 params: BTreeMap::new(),
2344 fold_set: Some(fold_set),
2345 }),
2346 generation: Default::default(),
2347 shape_plans: BTreeMap::new(),
2348 data_bindings: BTreeMap::new(),
2349 branch_view_plans: Vec::new(),
2350 metadata: BTreeMap::new(),
2351 },
2352 ®istry,
2353 )
2354 .unwrap();
2355 let trace = Arc::new(Mutex::new(Vec::new()));
2356 let mut controllers = RuntimeControllerRegistry::new();
2357 controllers
2358 .register(Box::new(HpoTestTuner {
2359 id: ControllerId::new("controller:tuner").unwrap(),
2360 trace: Arc::clone(&trace),
2361 history_len: 0,
2362 proposal_count: 1,
2363 }))
2364 .unwrap();
2365 controllers
2366 .register(Box::new(HpoTestModel {
2367 id: ControllerId::new("controller:model").unwrap(),
2368 trace: Arc::clone(&trace),
2369 }))
2370 .unwrap();
2371 let hpo = RuntimeHpoExecutionContext {
2372 operation_id: "hpo:test".to_string(),
2373 controller_id: ControllerId::new("controller:tuner").unwrap(),
2374 target_node_id: target.clone(),
2375 base_variant: plan.variants[0].clone(),
2376 trial_budget_total: 1,
2377 study: MethodsHpoStudyConfig {
2378 controller_id: "controller:tuner".to_string(),
2379 study_id: "study:hpo.scheduler".to_string(),
2380 methods_abi: "test-abi".to_string(),
2381 search_space: HpoSearchSpace {
2382 parameters: vec![HpoParameter::Int {
2383 name: "n_components".to_string(),
2384 low: 1,
2385 high: 1,
2386 step: 1,
2387 log: false,
2388 }],
2389 },
2390 optimizer: HpoOptimizerConfig {
2391 sampler: HpoSampler::Random,
2392 pruner: HpoPruner::None,
2393 direction: HpoDirection::Minimize,
2394 metric: HpoMetric::Rmse,
2395 seed: 13,
2396 n_startup_trials: 1,
2397 max_resource: 0,
2398 reduction_factor: 1,
2399 },
2400 },
2401 parameter_paths: BTreeMap::from([(
2402 "n_components".to_string(),
2403 "n_components".to_string(),
2404 )]),
2405 resume_checkpoint: None,
2406 resume_variants: BTreeMap::new(),
2407 resume_terminal_trials: Vec::new(),
2408 selection: RuntimeHpoSelectionTarget {
2409 producer_node: target,
2410 producer_port: "prediction".to_string(),
2411 metric: RegressionMetricKind::Rmse,
2412 direction: HpoDirection::Minimize,
2413 },
2414 provenance: RuntimeHpoProvenance {
2415 graph_fingerprint: plan.graph_fingerprint.clone(),
2416 campaign_fingerprint: plan.campaign_fingerprint.clone(),
2417 controller_fingerprint: plan.controller_fingerprint.clone(),
2418 data_identities_fingerprint: "identity:test".to_string(),
2419 fold_set_fingerprint: plan
2420 .fold_set
2421 .as_ref()
2422 .map(stable_json_fingerprint)
2423 .transpose()
2424 .unwrap(),
2425 training_influence_fingerprint: "influence:test".to_string(),
2426 relation_fingerprint: "relation:test".to_string(),
2427 },
2428 };
2429 let provider = InMemoryDataProvider::new(ControllerId::new("controller:data").unwrap());
2430 let ctx = RunContext::new(RunId::new("run:hpo.scheduler").unwrap(), Some(13));
2431
2432 let result = SequentialScheduler
2433 .execute_hpo_campaign(&plan, &controllers, &provider, &ctx, &hpo)
2434 .unwrap();
2435
2436 assert_eq!(result.operation_id, "hpo:test");
2437 assert_eq!(result.candidates.len(), 1);
2438 assert_eq!(result.checkpoint.completed_proposals.len(), 1);
2439 assert_eq!(result.checkpoint.completed_reports.len(), 1);
2440 assert_eq!(result.candidates[0].lineage.len(), 2);
2441 assert_eq!(result.incumbent.variant_id, plan.variants[0].variant_id);
2442 let mut selected_ctx = RunContext::new(RunId::new("run:hpo.scheduler").unwrap(), Some(13));
2443 selected_ctx.variant_id = Some(plan.variants[0].variant_id.clone());
2444 let selected_results = SequentialScheduler
2445 .execute_campaign_phase_with_data_provider(
2446 &plan,
2447 &controllers,
2448 &provider,
2449 &mut selected_ctx,
2450 Phase::FitCv,
2451 )
2452 .unwrap();
2453 assert_eq!(selected_results.len(), 2);
2454 assert_eq!(selected_ctx.lineage.len(), 2);
2455 assert_eq!(
2456 trace.lock().unwrap().as_slice(),
2457 [
2458 "session_factory",
2459 "ask",
2460 "model_cv",
2461 "model_cv",
2462 "intermediate",
2463 "tell",
2464 "checkpoint",
2465 "model_cv",
2466 "model_cv"
2467 ]
2468 );
2469
2470 let resumed_trace = Arc::new(Mutex::new(Vec::new()));
2474 let mut resumed_controllers = RuntimeControllerRegistry::new();
2475 resumed_controllers
2476 .register(Box::new(HpoTestTuner {
2477 id: ControllerId::new("controller:tuner").unwrap(),
2478 trace: Arc::clone(&resumed_trace),
2479 history_len: 2,
2480 proposal_count: 2,
2481 }))
2482 .unwrap();
2483 resumed_controllers
2484 .register(Box::new(HpoTestModel {
2485 id: ControllerId::new("controller:model").unwrap(),
2486 trace: Arc::clone(&resumed_trace),
2487 }))
2488 .unwrap();
2489 let mut resumed_hpo = hpo.clone();
2490 resumed_hpo.trial_budget_total = 4;
2491 let resumed_ctx = RunContext::new(RunId::new("run:hpo.resumed").unwrap(), Some(13));
2492 let resumed = SequentialScheduler
2493 .execute_hpo_campaign(
2494 &plan,
2495 &resumed_controllers,
2496 &provider,
2497 &resumed_ctx,
2498 &resumed_hpo,
2499 )
2500 .unwrap();
2501 assert_eq!(resumed.candidates.len(), 2);
2502 assert_eq!(resumed.checkpoint.trial_history_len, 4);
2503 assert_eq!(
2504 resumed_trace
2505 .lock()
2506 .unwrap()
2507 .iter()
2508 .filter(|event| event.as_str() == "ask")
2509 .count(),
2510 2
2511 );
2512
2513 let mut over_budget_controllers = RuntimeControllerRegistry::new();
2514 over_budget_controllers
2515 .register(Box::new(HpoTestTuner {
2516 id: ControllerId::new("controller:tuner").unwrap(),
2517 trace: Arc::new(Mutex::new(Vec::new())),
2518 history_len: 5,
2519 proposal_count: 0,
2520 }))
2521 .unwrap();
2522 let error = SequentialScheduler
2523 .execute_hpo_campaign(
2524 &plan,
2525 &over_budget_controllers,
2526 &provider,
2527 &resumed_ctx,
2528 &resumed_hpo,
2529 )
2530 .unwrap_err();
2531 assert!(error.to_string().contains("exceeds total trial budget"));
2532 }
2533}
2534
2535pub(crate) fn attach_coordinator_input_lineage(
2536 result: &mut NodeResult,
2537 plan: &ExecutionPlan,
2538 node_id: &NodeId,
2539 upstream_lineage: &BTreeMap<NodeId, LineageId>,
2540) -> Result<()> {
2541 let inferred = inferred_input_lineage_for_node(plan, node_id, upstream_lineage);
2542 if result.lineage.input_lineage.is_empty() {
2543 result.lineage.input_lineage = inferred;
2544 return Ok(());
2545 }
2546
2547 let declared = result
2548 .lineage
2549 .input_lineage
2550 .iter()
2551 .cloned()
2552 .collect::<BTreeSet<_>>()
2553 .into_iter()
2554 .collect::<Vec<_>>();
2555 if declared != inferred {
2556 return Err(DagMlError::RuntimeValidation(format!(
2557 "lineage for node `{}` declared input lineage {:?}, expected {:?}",
2558 result.node_id, declared, inferred
2559 )));
2560 }
2561 result.lineage.input_lineage = declared;
2562 Ok(())
2563}
2564
2565pub(crate) fn inferred_input_lineage_for_node(
2566 plan: &ExecutionPlan,
2567 node_id: &NodeId,
2568 upstream_lineage: &BTreeMap<NodeId, LineageId>,
2569) -> Vec<LineageId> {
2570 plan.graph_plan
2571 .graph
2572 .edges
2573 .iter()
2574 .filter(|edge| &edge.target.node_id == node_id && edge.contract.propagates_lineage)
2575 .filter_map(|edge| upstream_lineage.get(&edge.source.node_id).cloned())
2576 .collect::<BTreeSet<_>>()
2577 .into_iter()
2578 .collect()
2579}
2580pub(crate) fn collect_input_handles(
2581 plan: &ExecutionPlan,
2582 node_plan: &NodePlan,
2583 output_handles: &BTreeMap<NodeId, BTreeMap<String, HandleRef>>,
2584 output_data_views: &BTreeMap<NodeId, BTreeMap<String, DataProviderViewSpec>>,
2585 resources: &PhaseScopeResources<'_>,
2586 ctx: &RunContext,
2587 scope: &PhaseScope,
2588) -> Result<CollectedInputs> {
2589 let mut inputs = BTreeMap::new();
2590 let mut data_views = BTreeMap::new();
2591 let mut prediction_inputs = BTreeMap::new();
2592 let training_oof_edges = incoming_training_oof_edges(plan, node_plan, scope)?;
2593 let masked_oof_source_ports = if scope.phase == Phase::Predict {
2599 incoming_oof_edges(plan, node_plan)?
2600 } else {
2601 training_oof_edges.clone()
2602 }
2603 .into_iter()
2604 .map(|edge| (edge.source.node_id.clone(), edge.source.port_name.clone()))
2605 .collect::<BTreeSet<_>>();
2606 let bound_data_inputs = node_plan
2607 .data_bindings
2608 .iter()
2609 .map(|binding| binding.input_name.clone())
2610 .collect::<BTreeSet<_>>();
2611 let declared_source_ports = plan
2615 .graph_plan
2616 .graph
2617 .edges
2618 .iter()
2619 .filter(|edge| edge.target.node_id == node_plan.node_id)
2620 .map(|edge| (edge.source.node_id.clone(), edge.source.port_name.clone()))
2621 .collect::<BTreeSet<_>>();
2622 for upstream in &node_plan.input_nodes {
2623 if let Some(handles) = output_handles.get(upstream) {
2624 for (port, handle) in handles {
2625 if !declared_source_ports.contains(&(upstream.clone(), port.clone())) {
2626 continue;
2627 }
2628 if masked_oof_source_ports.contains(&(upstream.clone(), port.clone())) {
2629 continue;
2630 }
2631 inputs.insert(format!("{upstream}.{port}"), handle.clone());
2632 }
2633 }
2634 }
2635 for edge in plan
2636 .graph_plan
2637 .graph
2638 .edges
2639 .iter()
2640 .filter(|edge| edge.target.node_id == node_plan.node_id)
2641 .filter(|edge| edge.contract.kind == PortKind::Data && !edge.contract.requires_oof)
2642 {
2643 if bound_data_inputs.contains(&edge.target.port_name) {
2644 continue;
2645 }
2646 let Some(handles) = output_handles.get(&edge.source.node_id) else {
2647 continue;
2648 };
2649 let Some(handle) = handles.get(&edge.source.port_name) else {
2650 continue;
2651 };
2652 let key = data_view_key(&edge.target.port_name);
2653 if inputs.insert(key.clone(), handle.clone()).is_some() {
2654 return Err(DagMlError::RuntimeValidation(format!(
2655 "node `{}` received duplicate data edge input `{key}`",
2656 node_plan.node_id
2657 )));
2658 }
2659 if let Some(source_views) = output_data_views.get(&edge.source.node_id) {
2660 if let Some(view) = source_views.get(&edge.source.port_name) {
2661 if data_views.insert(key.clone(), view.clone()).is_some() {
2662 return Err(DagMlError::RuntimeValidation(format!(
2663 "node `{}` received duplicate data edge view `{key}`",
2664 node_plan.node_id
2665 )));
2666 }
2667 }
2668 let source_validation_key = validation_data_view_key(&edge.source.port_name);
2669 if let Some(view) = source_views.get(&source_validation_key) {
2670 let validation_key = format!("{key}:validation");
2671 if data_views
2672 .insert(validation_key.clone(), view.clone())
2673 .is_some()
2674 {
2675 return Err(DagMlError::RuntimeValidation(format!(
2676 "node `{}` received duplicate data edge validation view `{validation_key}`",
2677 node_plan.node_id
2678 )));
2679 }
2680 }
2681 }
2682 }
2683 for edge in training_oof_edges {
2684 let key = format!("{}.{}", edge.source.node_id, edge.source.port_name);
2685 let Some(input) = collect_oof_prediction_input(plan, edge, ctx, scope, resources)? else {
2686 return Ok(CollectedInputs {
2687 handles: BTreeMap::new(),
2688 data_views: BTreeMap::new(),
2689 prediction_inputs: BTreeMap::new(),
2690 skip_node: true,
2691 });
2692 };
2693 if inputs.insert(key.clone(), input.handle).is_some() {
2694 return Err(DagMlError::RuntimeValidation(format!(
2695 "node `{}` received duplicate OOF prediction input `{key}`",
2696 node_plan.node_id
2697 )));
2698 }
2699 if prediction_inputs.insert(key.clone(), input.spec).is_some() {
2700 return Err(DagMlError::RuntimeValidation(format!(
2701 "node `{}` received duplicate OOF prediction spec `{key}`",
2702 node_plan.node_id
2703 )));
2704 }
2705 }
2706 if matches!(scope.phase, Phase::Refit | Phase::Predict) {
2713 let off_fold_suffix = scope.phase.as_str().to_ascii_lowercase();
2714 for edge in incoming_oof_edges(plan, node_plan)? {
2715 let Some(input) = collect_off_fold_prediction_input(plan, edge, ctx, scope)? else {
2716 continue;
2717 };
2718 let key = format!(
2719 "{}.{}:{off_fold_suffix}",
2720 edge.source.node_id, edge.source.port_name
2721 );
2722 if inputs.insert(key.clone(), input.handle).is_some() {
2723 return Err(DagMlError::RuntimeValidation(format!(
2724 "node `{}` received duplicate off-fold prediction input `{key}`",
2725 node_plan.node_id
2726 )));
2727 }
2728 if prediction_inputs.insert(key.clone(), input.spec).is_some() {
2729 return Err(DagMlError::RuntimeValidation(format!(
2730 "node `{}` received duplicate off-fold prediction spec `{key}`",
2731 node_plan.node_id
2732 )));
2733 }
2734 }
2735 }
2736 if !node_plan.data_bindings.is_empty() && resources.data_provider.is_none() {
2737 return Err(DagMlError::RuntimeValidation(format!(
2738 "node `{}` requires {} data binding(s) but no runtime data provider is registered",
2739 node_plan.node_id,
2740 node_plan.data_bindings.len()
2741 )));
2742 }
2743 if let Some(data_provider) = resources.data_provider {
2744 let excluded_samples = if scope.phase == Phase::Predict {
2749 BTreeSet::new()
2750 } else {
2751 coordinator_relations_for_node(node_plan, resources)?
2752 .map(|relations| relations.excluded_sample_ids())
2753 .unwrap_or_default()
2754 };
2755 let scope_fold_set = resources.fold_set_override.or(plan.fold_set.as_ref());
2756 for binding in &node_plan.data_bindings {
2757 let predict_cohort = if scope.phase == Phase::Predict {
2758 data_provider.predict_cohort(binding, scope.phase)?
2759 } else {
2760 None
2761 };
2762 let materialized = data_provider.materialize(&DataMaterializationRequest {
2763 run_id: ctx.run_id.clone(),
2764 node_id: node_plan.node_id.clone(),
2765 input_name: binding.input_name.clone(),
2766 phase: scope.phase,
2767 variant_id: scope.variant_id.clone(),
2768 fold_id: scope.fold_id.clone(),
2769 binding: binding.clone(),
2770 predict_cohort: predict_cohort.clone(),
2771 })?;
2772 let branch_view_for_node = branch_view_from_node_metadata(plan, &node_plan.node_id)?;
2773 let mut view = data_view_for_scope(
2774 binding,
2775 scope_fold_set,
2776 scope,
2777 branch_view_for_node.as_ref(),
2778 &excluded_samples,
2779 )?;
2780 if let Some(cohort) = predict_cohort.as_ref() {
2781 bind_predict_cohort_to_view(&mut view, cohort)?;
2782 }
2783 let key = data_view_key(&binding.input_name);
2784 let view_handle = make_data_view_handle(
2785 data_provider,
2786 ctx,
2787 node_plan,
2788 scope,
2789 binding,
2790 DataViewHandleInput {
2791 data_handle: &materialized,
2792 view: &view,
2793 predict_cohort: predict_cohort.as_ref(),
2794 },
2795 )?;
2796 if data_views.insert(key.clone(), view).is_some() {
2797 return Err(DagMlError::RuntimeValidation(format!(
2798 "node `{}` received duplicate data view `{key}`",
2799 node_plan.node_id
2800 )));
2801 }
2802 if inputs.insert(key.clone(), view_handle).is_some() {
2803 return Err(DagMlError::RuntimeValidation(format!(
2804 "node `{}` received duplicate data input `{key}`",
2805 node_plan.node_id
2806 )));
2807 }
2808
2809 if let Some(validation_view) = validation_data_view_for_scope(
2810 binding,
2811 scope_fold_set,
2812 scope,
2813 branch_view_for_node.as_ref(),
2814 &excluded_samples,
2815 )? {
2816 let validation_key = format!("{key}:validation");
2817 let validation_handle = make_data_view_handle(
2818 data_provider,
2819 ctx,
2820 node_plan,
2821 scope,
2822 binding,
2823 DataViewHandleInput {
2824 data_handle: &materialized,
2825 view: &validation_view,
2826 predict_cohort: None,
2827 },
2828 )?;
2829 if data_views
2830 .insert(validation_key.clone(), validation_view)
2831 .is_some()
2832 {
2833 return Err(DagMlError::RuntimeValidation(format!(
2834 "node `{}` received duplicate validation data view `{validation_key}`",
2835 node_plan.node_id
2836 )));
2837 }
2838 if inputs
2839 .insert(validation_key.clone(), validation_handle)
2840 .is_some()
2841 {
2842 return Err(DagMlError::RuntimeValidation(format!(
2843 "node `{}` received duplicate validation data input `{validation_key}`",
2844 node_plan.node_id
2845 )));
2846 }
2847 }
2848 }
2849 }
2850 Ok(CollectedInputs {
2851 handles: inputs,
2852 data_views,
2853 prediction_inputs,
2854 skip_node: false,
2855 })
2856}
2857pub(crate) fn preload_replay_prediction_cache_store(
2858 bundle: &ExecutionBundle,
2859 prediction_cache_store: Option<&dyn RuntimePredictionCacheStore>,
2860 ctx: &mut RunContext,
2861) -> Result<()> {
2862 if bundle.prediction_requirements.is_empty() {
2863 return Ok(());
2864 }
2865 let store = prediction_cache_store.ok_or_else(|| {
2866 DagMlError::RuntimeValidation(format!(
2867 "bundle `{}` cannot preload OOF prediction caches without a prediction cache store",
2868 bundle.bundle_id
2869 ))
2870 })?;
2871 if !ctx.prediction_store.blocks().is_empty() {
2872 return Err(DagMlError::RuntimeValidation(format!(
2873 "bundle `{}` cannot preload OOF prediction caches into a non-empty prediction store",
2874 bundle.bundle_id
2875 )));
2876 }
2877 let contracts = replay_prediction_cache_contracts(bundle)?;
2878 for contract in contracts.values() {
2879 if contract.requirement.prediction_level == PredictionLevel::Sample {
2880 let blocks = store.load_blocks(&contract.cache.requirement_key)?;
2881 if blocks.iter().any(|block| {
2882 block.producer_node != contract.requirement.producer_node
2883 || block.partition != contract.requirement.partition
2884 }) {
2885 return Err(DagMlError::RuntimeValidation(format!(
2886 "prediction cache store returned blocks outside requirement `{}`",
2887 contract.cache.requirement_key
2888 )));
2889 }
2890 let mut payload = build_prediction_cache_payload(&contract.requirement, &blocks)?;
2891 payload.cache_namespace_fingerprints =
2892 contract.cache.cache_namespace_fingerprints.clone();
2893 validate_prediction_cache_payload_matches_record(&payload, &contract.cache)?;
2894 for block in &payload.blocks {
2895 ctx.prediction_store.append(block.clone())?;
2896 }
2897 } else {
2898 let blocks = store.load_aggregated_blocks(&contract.cache.requirement_key)?;
2899 if blocks.iter().any(|block| {
2900 block.producer_node != contract.requirement.producer_node
2901 || block.partition != contract.requirement.partition
2902 || block.level != contract.requirement.prediction_level
2903 }) {
2904 return Err(DagMlError::RuntimeValidation(format!(
2905 "prediction cache store returned aggregated blocks outside requirement `{}`",
2906 contract.cache.requirement_key
2907 )));
2908 }
2909 let mut payload =
2910 build_aggregated_prediction_cache_payload(&contract.requirement, &blocks)?;
2911 payload.cache_namespace_fingerprints =
2912 contract.cache.cache_namespace_fingerprints.clone();
2913 validate_prediction_cache_payload_matches_record(&payload, &contract.cache)?;
2914 }
2915 }
2916 Ok(())
2917}
2918
2919pub(crate) fn replay_prediction_cache_contracts(
2920 bundle: &ExecutionBundle,
2921) -> Result<BTreeMap<String, ReplayPredictionCacheContract>> {
2922 bundle.validate()?;
2923 let requirements = bundle
2924 .prediction_requirements
2925 .iter()
2926 .map(|requirement| (requirement.key(), requirement))
2927 .collect::<BTreeMap<_, _>>();
2928 let mut contracts = BTreeMap::new();
2929 for cache in &bundle.prediction_caches {
2930 let requirement = requirements.get(&cache.requirement_key).ok_or_else(|| {
2931 DagMlError::RuntimeValidation(format!(
2932 "prediction cache `{}` references unknown prediction requirement `{}`",
2933 cache.cache_id, cache.requirement_key
2934 ))
2935 })?;
2936 contracts.insert(
2937 cache.requirement_key.clone(),
2938 ReplayPredictionCacheContract {
2939 requirement: (*requirement).clone(),
2940 cache: cache.clone(),
2941 },
2942 );
2943 }
2944 Ok(contracts)
2945}
2946
2947pub(crate) fn materialize_replay_artifact_handles(
2948 plan: &ExecutionPlan,
2949 bundle: &ExecutionBundle,
2950 replay_request: &ReplayPhaseRequest,
2951 artifact_store: &dyn RuntimeArtifactStore,
2952 ctx: &RunContext,
2953) -> Result<MaterializedReplayArtifacts> {
2954 let mut handles = BTreeMap::<NodeId, BTreeMap<String, HandleRef>>::new();
2955 let mut inputs = BTreeMap::<NodeId, BTreeMap<String, ArtifactInputSpec>>::new();
2956 for artifact in &bundle.refit_artifacts {
2957 artifact.validate()?;
2958 let node_plan = plan.node_plans.get(&artifact.node_id).ok_or_else(|| {
2959 DagMlError::RuntimeValidation(format!(
2960 "bundle `{}` artifact references unknown node `{}`",
2961 bundle.bundle_id, artifact.node_id
2962 ))
2963 })?;
2964 if !node_plan.supported_phases.contains(&replay_request.phase) {
2965 return Err(DagMlError::RuntimeValidation(format!(
2966 "bundle `{}` artifact node `{}` does not support replay phase {:?}",
2967 bundle.bundle_id, artifact.node_id, replay_request.phase
2968 )));
2969 }
2970 let handle = artifact_store.materialize(&ArtifactMaterializationRequest {
2971 run_id: ctx.run_id.clone(),
2972 bundle_id: bundle.bundle_id.clone(),
2973 node_id: artifact.node_id.clone(),
2974 phase: replay_request.phase,
2975 variant_id: bundle.selected_variant_id.clone(),
2976 controller_id: artifact.controller_id.clone(),
2977 artifact: artifact.artifact.clone(),
2978 params_fingerprint: artifact.params_fingerprint.clone(),
2979 training_loss_fingerprint: artifact.training_loss_fingerprint.clone(),
2980 })?;
2981 if !matches!(handle.kind, HandleKind::Model | HandleKind::Artifact) {
2982 return Err(DagMlError::RuntimeValidation(format!(
2983 "artifact `{}` materialized as unsupported handle kind {:?}",
2984 artifact.artifact.id, handle.kind
2985 )));
2986 }
2987 if handle.owner_controller != artifact.controller_id {
2988 return Err(DagMlError::RuntimeValidation(format!(
2989 "artifact `{}` handle owner `{}` does not match controller `{}`",
2990 artifact.artifact.id, handle.owner_controller, artifact.controller_id
2991 )));
2992 }
2993 let key = refit_artifact_input_key(&artifact.artifact.id);
2994 if handles
2995 .entry(artifact.node_id.clone())
2996 .or_default()
2997 .insert(key.clone(), handle)
2998 .is_some()
2999 {
3000 return Err(DagMlError::RuntimeValidation(format!(
3001 "duplicate replay artifact input `{key}` for node `{}`",
3002 artifact.node_id
3003 )));
3004 }
3005 if inputs
3006 .entry(artifact.node_id.clone())
3007 .or_default()
3008 .insert(key.clone(), ArtifactInputSpec::from_refit_record(artifact)?)
3009 .is_some()
3010 {
3011 return Err(DagMlError::RuntimeValidation(format!(
3012 "duplicate replay artifact metadata `{key}` for node `{}`",
3013 artifact.node_id
3014 )));
3015 }
3016 }
3017 Ok(MaterializedReplayArtifacts { handles, inputs })
3018}
3019
3020pub(crate) fn derive_task_seed(
3021 root_seed: Option<u64>,
3022 variant_id: Option<&VariantId>,
3023 fold_id: Option<&FoldId>,
3024 node_plan: &NodePlan,
3025 phase: Phase,
3026) -> Option<u64> {
3027 root_seed.map(|root| {
3028 let mut context = SeedContext::root(root);
3029 if let Some(variant_id) = variant_id {
3030 context = context.child(format!("variant:{variant_id}"));
3031 }
3032 if let Some(fold_id) = fold_id {
3033 context = context.child(format!("fold:{fold_id}"));
3034 }
3035 context
3036 .child(format!("node:{}", node_plan.node_id))
3037 .child(format!("phase:{phase:?}"))
3038 .derive_u64("task")
3039 })
3040}