3232from ax .core .runner import Runner
3333from ax .core .trial import Trial
3434from ax .core .trial_status import TrialStatus
35+ from ax .core .utils import compute_metric_availability , MetricAvailability
3536from ax .exceptions .core import (
3637 AxError ,
3738 DataRequiredError ,
7172 trials attached to the underlying Ax experiment '{experiment_name}'.
7273"""
7374FAILURE_EXCEEDED_MSG = (
74- "NOTE: This error is usually not caused by Ax. Please please check any trial "
75- "evaluation processes/jobs to see why they are failing, and ensure that they "
76- "succeed over the entire range of the parameters defined in this optimization .\n \n "
77- "Trials are failing or being abandoned at a rate {observed_rate} that exceeds the "
78- "tolerated trial failure rate of {f_rate} (at least {n_failed} out of first "
79- "{n_ran} trials failed or were abandoned). Checks are triggered both at the end "
80- "of an optimization and if at least {min_failed } trials have been "
81- "failed/abandoned, potentially automatically due to issues with the trial ."
75+ "NOTE: This error is usually not caused by Ax. Please check any trial evaluation "
76+ "processes/jobs and metric fetching infrastructure to determine why trials are "
77+ "failing or required data is missing .\n \n "
78+ "Trials are FAILED or ABANDONED, or have incomplete data for metrics in the "
79+ "optimization config, at a rate {observed_rate} that exceeds the tolerated trial "
80+ "failure rate of {f_rate} "
81+ "(at least {n_failed} out of the first {n_ran } trials). Checks are triggered both "
82+ "at the end of an optimization and after at least {min_failed} affected trials ."
8283)
8384METRIC_FETCH_ERR_MESSAGE = (
84- "A majority of the trial failures encountered are due to metric fetching errors. "
85- "This could mean the metrics are flaky, broken, or misconfigured. Please check "
86- "that the trial processes/jobs are successfully producing the expected metrics and "
87- "that the metric is correctly configured."
85+ "One or more completed trials have incomplete data for metrics in the optimization "
86+ "config. This could mean the metrics are flaky, broken, or misconfigured. Please "
87+ "check that the trial processes/jobs are producing the expected metrics and that "
88+ "the metrics are correctly configured."
8889)
8990
9091EXPECTED_STAGED_MSG = (
@@ -191,17 +192,20 @@ class Orchestrator(WithDBSettingsBase, BestPointMixin):
191192 # Saved as a property so that it can be accessed after optimization is complex (ex.
192193 # for global stopping saving calculation).
193194 _num_remaining_requested_trials : int = 0
194- # Total number of MetricFetchEs encountered during the course of optimization. Note
195- # this is different from and may be greater than the number of trials that have
196- # been marked either FAILED or ABANDONED due to metric fetching errors.
195+ # Total number of MetricFetchEs encountered for non-running trials during the
196+ # course of optimization.
197197 _num_metric_fetch_e_encountered : int = 0
198- # Number of trials that have been marked either FAILED or ABANDONED due to
199- # MetricFetchE being encountered during _fetch_and_process_trials_data_results
198+ # Number of completed trials with incomplete data for metrics in the optimization
199+ # config.
200+ # Retained under its existing name for telemetry compatibility.
200201 _num_trials_bad_due_to_err : int = 0
201202 # Keeps track of whether the allowed failure rate has been exceeded during
202203 # the optimization. If true, allows any pending trials to finish and raises
203204 # an error through self._complete_optimization.
204205 _failure_rate_has_been_exceeded : bool = False
206+ # Counts captured when the failure rate first exceeds the tolerance. Pending
207+ # trials may finish afterward, so the final error must use this snapshot.
208+ _failure_rate_exceeded_counts : tuple [int , int ] | None = None
205209 # Timestamp of last optimization start time (milliseconds since Unix epoch);
206210 # recorded in each `run_n_trials`.
207211 _latest_optimization_start_timestamp : int | None = None
@@ -1079,14 +1083,15 @@ def _check_if_failure_rate_exceeded(self, force_check: bool = False) -> bool:
10791083 """Checks if the failure rate (set in Orchestrator options) has been exceeded at
10801084 any point during the optimization.
10811085
1082- NOTE: Both FAILED and ABANDONED trial statuses count towards the failure rate.
1086+ FAILED and ABANDONED statuses count towards the failure rate, as do completed
1087+ trials with incomplete data for metrics in the optimization config.
10831088
10841089 Args:
10851090 force_check: Indicates whether to force a failure-rate check
10861091 regardless of the number of trials that have been executed. If False
1087- (default), the check will be skipped if the optimization has fewer than
1088- five failed trials. If True, the check will be performed unless there
1089- are 0 failures .
1092+ (default), the check will be skipped if the optimization has fewer
1093+ affected trials than ``min_failed_trials_for_failure_rate_check``. If
1094+ True, the check will be performed unless there are 0 affected trials .
10901095
10911096 Effect on state:
10921097 If the failure rate has been exceeded, a warning is logged and the private
@@ -1100,7 +1105,11 @@ def _check_if_failure_rate_exceeded(self, force_check: bool = False) -> bool:
11001105 if self ._failure_rate_has_been_exceeded :
11011106 return True
11021107
1103- num_bad_in_orchestrator = self ._num_bad_in_orchestrator ()
1108+ num_failed_or_abandoned = self ._num_bad_in_orchestrator ()
1109+ self ._num_trials_bad_due_to_err = self ._num_metric_incomplete_in_orchestrator ()
1110+ num_bad_in_orchestrator = (
1111+ num_failed_or_abandoned + self ._num_trials_bad_due_to_err
1112+ )
11041113 # skip check if 0 failures
11051114 if num_bad_in_orchestrator == 0 :
11061115 return False
@@ -1120,15 +1129,18 @@ def _check_if_failure_rate_exceeded(self, force_check: bool = False) -> bool:
11201129 ) > self .options .tolerated_trial_failure_rate
11211130
11221131 if failure_rate_exceeded :
1123- if self ._num_trials_bad_due_to_err > num_bad_in_orchestrator / 2 :
1132+ if self ._num_trials_bad_due_to_err > 0 :
11241133 self .logger .warning (
1125- "MetricFetchE INFO: Sweep aborted due to an exceeded error rate, "
1126- "which was primarily caused by failure to fetch metrics. Please "
1127- "check if anything could cause your metrics to be flaky or "
1128- "broken."
1134+ "MetricFetchE INFO: Sweep aborted due to an exceeded error rate "
1135+ "that includes incomplete data for metrics in the optimization "
1136+ "config."
11291137 )
11301138 # NOTE: this private attribute causes `_get_max_pending_trials` to
11311139 # return zero, which causes no further trials to be scheduled.
1140+ self ._failure_rate_exceeded_counts = (
1141+ num_bad_in_orchestrator ,
1142+ num_ran_in_orchestrator ,
1143+ )
11321144 self ._failure_rate_has_been_exceeded = True
11331145 return True
11341146
@@ -1138,19 +1150,23 @@ def error_if_failure_rate_exceeded(self, force_check: bool = False) -> None:
11381150 """Raises an exception if the failure rate (set in Orchestrator options) has
11391151 been exceeded at any point during the optimization.
11401152
1141- NOTE: Both FAILED and ABANDONED trial statuses count towards the failure rate.
1153+ FAILED and ABANDONED statuses count towards the failure rate, as do completed
1154+ trials with incomplete data for metrics in the optimization config.
11421155
11431156 Args:
11441157 force_check: Indicates whether to force a failure-rate check
11451158 regardless of the number of trials that have been executed. If False
1146- (default), the check will be skipped if the optimization has fewer than
1147- five failed trials. If True, the check will be performed unless there
1148- are 0 failures .
1159+ (default), the check will be skipped if the optimization has fewer
1160+ affected trials than ``min_failed_trials_for_failure_rate_check``. If
1161+ True, the check will be performed unless there are 0 affected trials .
11491162 """
11501163 if self ._check_if_failure_rate_exceeded (force_check = force_check ):
1164+ num_bad_in_orchestrator , num_ran_in_orchestrator = none_throws (
1165+ self ._failure_rate_exceeded_counts
1166+ )
11511167 raise self ._get_failure_rate_exceeded_error (
1152- num_bad_in_orchestrator = self . _num_bad_in_orchestrator () ,
1153- num_ran_in_orchestrator = self . _num_ran_in_orchestrator () ,
1168+ num_bad_in_orchestrator = num_bad_in_orchestrator ,
1169+ num_ran_in_orchestrator = num_ran_in_orchestrator ,
11541170 )
11551171
11561172 def _error_if_status_quo_infeasible (self ) -> None :
@@ -1403,8 +1419,8 @@ def _fetch_data_and_return_trial_indices_with_new_data(
14031419 )
14041420 return {
14051421 i
1406- for i , results_by_metric_name in results .items ()
1407- for r in results_by_metric_name .values ()
1422+ for i , results_by_metric_signature in results .items ()
1423+ for r in results_by_metric_signature .values ()
14081424 if r .is_ok ()
14091425 }
14101426 return set ()
@@ -1425,6 +1441,26 @@ def _num_ran_in_orchestrator(self) -> int:
14251441 """Returns the number of trials that have been run by the orchestrator."""
14261442 return len (self .experiment .trials ) - self ._num_preexisting_trials
14271443
1444+ def _num_metric_incomplete_in_orchestrator (self ) -> int :
1445+ """Count completed trials with incomplete optimization config data."""
1446+ if self .experiment .optimization_config is None :
1447+ return 0
1448+
1449+ completed_trial_indices = [
1450+ trial .index
1451+ for trial in self .trials
1452+ if trial .status == TrialStatus .COMPLETED
1453+ and trial .index >= self ._num_preexisting_trials
1454+ ]
1455+ metric_availability = compute_metric_availability (
1456+ experiment = self .experiment ,
1457+ trial_indices = completed_trial_indices ,
1458+ )
1459+ return sum (
1460+ availability != MetricAvailability .COMPLETE
1461+ for availability in metric_availability .values ()
1462+ )
1463+
14281464 def _apply_trial_statuses (
14291465 self , polled_status_to_trial_idcs : dict [TrialStatus , set [int ]]
14301466 ) -> set [int ]:
@@ -2071,8 +2107,7 @@ def _fetch_and_process_trials_data_results(
20712107 trial_indices : Iterable [int ],
20722108 ) -> dict [int , dict [str , MetricFetchResult ]]:
20732109 """
2074- Fetches results from experiment and modifies trial statuses depending on
2075- success or failure.
2110+ Fetch results and report errors without changing trial statuses.
20762111 """
20772112
20782113 try :
@@ -2092,8 +2127,8 @@ def _fetch_and_process_trials_data_results(
20922127 )
20932128 return {}
20942129
2095- for trial_index , results_by_metric_name in results .items ():
2096- for metric_name , result in results_by_metric_name .items ():
2130+ for trial_index , results_by_metric_signature in results .items ():
2131+ for metric_signature , result in results_by_metric_signature .items ():
20972132 # If the fetch call succeeded, continue.
20982133 if result .is_ok ():
20992134 continue
@@ -2102,59 +2137,34 @@ def _fetch_and_process_trials_data_results(
21022137 # we do not do anything
21032138 metric_fetch_e = result .unwrap_err ()
21042139
2105- # If the metric is available while running just continue (we can try
2106- # again later).
2107- # NOTE: We don't need to report fetching errors in this case either
2108- metric = self .experiment .metrics [metric_name ]
21092140 status = self .experiment .trials [trial_index ].status
2141+ metric = self .experiment .signature_to_metric .get (metric_signature )
21102142 if (
2111- metric .is_available_while_running ()
2112- and status == TrialStatus .RUNNING
2143+ status == TrialStatus .RUNNING
2144+ and metric is not None
2145+ and metric .is_available_while_running ()
21132146 ):
21142147 self .logger .info (
2115- f"MetricFetchE INFO: Because { metric_name } is "
2116- f"available_while_running and trial { trial_index } is still "
2117- "RUNNING continuing the experiment and retrying on next "
2118- "poll..."
2148+ f"MetricFetchE INFO: Because trial { trial_index } is still "
2149+ "RUNNING, continuing after failing to fetch "
2150+ f"{ metric_signature } and retrying on the next poll."
21192151 )
21202152 continue
21212153
21222154 self .logger .error (
2123- f"Failed to fetch { metric_name } for trial { trial_index } with "
2155+ f"Failed to fetch { metric_signature } for trial { trial_index } with "
21242156 f"status { status } , found { metric_fetch_e } ."
21252157 )
21262158 self ._num_metric_fetch_e_encountered += 1
21272159 self ._report_metric_fetch_e (
21282160 trial = self .experiment .trials [trial_index ],
2129- metric_name = metric_name ,
2161+ metric_signature = metric_signature ,
21302162 metric_fetch_e = metric_fetch_e ,
21312163 )
21322164
2133- # If the fetch failure was for a metric in the optimization config (an
2134- # objective or constraint) mark the trial as failed
2135- optimization_config = self .experiment .optimization_config
2136- if (
2137- optimization_config is not None
2138- and metric_name in optimization_config .metric_names
2139- and not self .experiment .metrics [metric_name ].is_recoverable_fetch_e (
2140- metric_fetch_e = metric_fetch_e
2141- )
2142- ):
2143- status = self ._mark_err_trial_status (
2144- trial = self .experiment .trials [trial_index ],
2145- metric_name = metric_name ,
2146- metric_fetch_e = metric_fetch_e ,
2147- )
2148- self .logger .warning (
2149- f"MetricFetchE INFO: Because { metric_name } is an objective, "
2150- f"marking trial { trial_index } as { status } ."
2151- )
2152- self ._num_trials_bad_due_to_err += 1
2153- continue
2154-
21552165 self .logger .info (
2156- "MetricFetchE INFO: Continuing optimization even though "
2157- "MetricFetchE encountered ."
2166+ "MetricFetchE INFO: Trial status remains unchanged; metric data "
2167+ "availability is tracked separately ."
21582168 )
21592169 continue
21602170
@@ -2163,22 +2173,11 @@ def _fetch_and_process_trials_data_results(
21632173 def _report_metric_fetch_e (
21642174 self ,
21652175 trial : BaseTrial ,
2166- metric_name : str ,
2176+ metric_signature : str ,
21672177 metric_fetch_e : MetricFetchE ,
21682178 ) -> None :
21692179 pass
21702180
2171- def _mark_err_trial_status (
2172- self ,
2173- trial : BaseTrial ,
2174- metric_name : str | None = None ,
2175- metric_fetch_e : MetricFetchE | None = None ,
2176- ) -> TrialStatus :
2177- trial .mark_abandoned (
2178- reason = metric_fetch_e .message if metric_fetch_e else None , unsafe = True
2179- )
2180- return TrialStatus .ABANDONED
2181-
21822181 def _get_failure_rate_exceeded_error (
21832182 self ,
21842183 num_bad_in_orchestrator : int ,
@@ -2187,7 +2186,7 @@ def _get_failure_rate_exceeded_error(
21872186 return FailureRateExceededError (
21882187 (
21892188 f"{ METRIC_FETCH_ERR_MESSAGE } \n "
2190- if self ._num_trials_bad_due_to_err > num_bad_in_orchestrator / 2
2189+ if self ._num_trials_bad_due_to_err > 0
21912190 else ""
21922191 )
21932192 + " Original error message: "
0 commit comments