Merge pull request #13 from xmarre/codex/align-forecaster-with-sigma-axis

Fix forecaster to use sigma axis for runtime validation
This commit is contained in:
xmarre
2026-03-20 15:36:12 +01:00
committed by GitHub
2 changed files with 143 additions and 20 deletions
+27 -14
View File
@@ -172,21 +172,21 @@ class SpectrumSDXLRuntime:
return None
return float(min(values)), float(max(values))
def _model_time_coord_bounds(
def _observed_coord_bounds(
self,
state: _StreamState,
current_time_coord: float,
) -> Optional[Tuple[float, float]]:
"""Return bounds for the active model-time axis within this stream."""
values = [float(current_time_coord)]
for decision in state.decisions_by_solver_step.values():
coord = decision.get("time_coord", None)
if coord is None:
continue
"""Return bounds for the active forecast axis within this stream."""
del current_time_coord
values = []
for coord, _ in state.forecaster.history:
try:
values.append(float(coord))
coord = float(coord)
except (TypeError, ValueError):
continue
if math.isfinite(coord):
values.append(coord)
if not values:
return None
return float(min(values)), float(max(values))
@@ -196,7 +196,8 @@ class SpectrumSDXLRuntime:
run_id = transformer_options.get(_RUN_ID_KEY, None)
solver_step_id = transformer_options.get(_SOLVER_STEP_ID_KEY, None)
sigma_coord = transformer_options.get(_TIME_COORD_KEY, None)
time_coord = transformer_options.get(_MODEL_TIME_COORD_KEY, sigma_coord)
model_time_coord = transformer_options.get(_MODEL_TIME_COORD_KEY, None)
time_coord = sigma_coord
actual_forward = transformer_options.get(_ACTUAL_FORWARD_KEY, None)
if run_id is None and solver_step_id is None and time_coord is None and sigma_coord is None and actual_forward is None:
@@ -219,6 +220,15 @@ class SpectrumSDXLRuntime:
if not math.isfinite(time_coord) or not math.isfinite(sigma_coord):
return None, "invalid_solver_step_context"
if model_time_coord is not None:
try:
model_time_coord = float(model_time_coord)
except (TypeError, ValueError):
model_time_coord = None
else:
if not math.isfinite(model_time_coord):
model_time_coord = None
if actual_forward is not None and not isinstance(actual_forward, bool):
return None, "invalid_solver_step_context"
@@ -226,6 +236,7 @@ class SpectrumSDXLRuntime:
"run_id": run_id,
"solver_step_id": solver_step_id,
"time_coord": time_coord,
"model_time_coord": model_time_coord,
"sigma_coord": sigma_coord,
"actual_forward": actual_forward,
"total_steps": self._extract_total_steps(transformer_options),
@@ -324,6 +335,7 @@ class SpectrumSDXLRuntime:
"forecast_safe": False,
"finalized": False,
"time_coord": None,
"model_time_coord": None,
"sigma_coord": None,
"total_steps": self.num_steps(),
}
@@ -354,15 +366,15 @@ class SpectrumSDXLRuntime:
"forecast_safe": False,
"finalized": False,
"time_coord": ctx["time_coord"],
"model_time_coord": ctx.get("model_time_coord", None),
"sigma_coord": ctx["sigma_coord"],
"total_steps": ctx["total_steps"],
}
state = self._ensure_stream_state(stream_key, ctx["run_id"])
if _MODEL_TIME_COORD_KEY in transformer_options:
schedule_coord_bounds = self._model_time_coord_bounds(state, ctx["time_coord"])
else:
schedule_coord_bounds = self._schedule_coord_bounds(transformer_options)
schedule_coord_bounds = self._schedule_coord_bounds(transformer_options)
if schedule_coord_bounds is None:
schedule_coord_bounds = self._observed_coord_bounds(state, ctx["time_coord"])
if schedule_coord_bounds is not None:
state.forecaster.set_coord_bounds(*schedule_coord_bounds)
existing = state.decisions_by_solver_step.get(ctx["solver_step_id"])
@@ -410,6 +422,7 @@ class SpectrumSDXLRuntime:
"forecast_safe": forecast_safe,
"finalized": False,
"time_coord": ctx["time_coord"],
"model_time_coord": ctx.get("model_time_coord", None),
"recent_validation_rel_l2": recent_validation_rel_l2,
"sigma_coord": ctx["sigma_coord"],
"total_steps": ctx["total_steps"],
@@ -420,7 +433,7 @@ class SpectrumSDXLRuntime:
print(
f"[Spectrum SDXL] begin "
f"run={ctx['run_id']} step={ctx['solver_step_id']} "
f"sigma={ctx['sigma_coord']} model_time={ctx['time_coord']} "
f"sigma={ctx['sigma_coord']} model_time={ctx.get('model_time_coord', None)} "
f"val_rel_l2={recent_validation_rel_l2} "
f"local={local_step_count} actual={actual_forward} "
f"forecast_safe={forecast_safe} ready={state.forecaster.ready()} "
+116 -6
View File
@@ -161,8 +161,8 @@ def test_explicit_solver_step_context_allows_forecast() -> None:
assert runtime.last_info["forecasted_passes"] == 1
def test_runtime_prefers_model_time_coord_for_forecasting() -> None:
"""The forecast axis should follow model-time while sigma still validates the schedule."""
def test_runtime_prefers_sigma_coord_for_forecasting_even_when_model_time_is_present() -> None:
"""The forecast axis should stay on raw sigma even when model-time metadata is present."""
runtime = SpectrumSDXLRuntime(_make_relaxed_cfg())
sample_sigmas = torch.tensor([14.0, 7.0, 3.0, 1.5, 0.75, 0.25, 0.0], dtype=torch.float32)
@@ -237,7 +237,8 @@ def test_runtime_prefers_model_time_coord_for_forecasting() -> None:
(2, 8, 4, 4),
)
assert forecast["time_coord"] == 400.0
assert forecast["time_coord"] == 1.5
assert forecast["model_time_coord"] == 400.0
assert forecast["sigma_coord"] == 1.5
assert forecast["actual_forward"] is False
assert forecast["forecast_safe"] is True
@@ -249,8 +250,96 @@ def test_runtime_prefers_model_time_coord_for_forecasting() -> None:
state = runtime.stream_states[forecast["stream_key"]]
fit_cache = state.forecaster._fit_cache
assert fit_cache is not None
assert torch.allclose(torch.tensor(fit_cache.coord_min), torch.tensor(100.0), atol=1e-6)
assert torch.allclose(torch.tensor(fit_cache.coord_max), torch.tensor(400.0), atol=1e-6)
assert torch.allclose(torch.tensor(fit_cache.coord_min), torch.tensor(0.25), atol=1e-6)
assert torch.allclose(torch.tensor(fit_cache.coord_max), torch.tensor(14.0), atol=1e-6)
def test_runtime_prefers_sigma_coord_for_forecasting_without_sample_sigmas() -> None:
"""Fallback bounds should stay on observed sigma history when schedule bounds are absent."""
runtime = SpectrumSDXLRuntime(_make_relaxed_cfg())
first = runtime.begin_step(
_step_options(
"run-a",
0,
14.0,
True,
total_steps=8,
model_time_coord=100.0,
),
torch.tensor([100.0]),
(2, 8, 4, 4),
)
runtime.observe_actual_feature(
first["stream_key"],
first["solver_step_id"],
torch.full((2, 8, 4, 4), 1.0, dtype=torch.float16),
)
second = runtime.begin_step(
_step_options(
"run-a",
1,
7.0,
True,
total_steps=8,
model_time_coord=200.0,
),
torch.tensor([200.0]),
(2, 8, 4, 4),
)
runtime.observe_actual_feature(
second["stream_key"],
second["solver_step_id"],
torch.full((2, 8, 4, 4), 2.0, dtype=torch.float16),
)
third = runtime.begin_step(
_step_options(
"run-a",
2,
3.0,
True,
total_steps=8,
model_time_coord=300.0,
),
torch.tensor([300.0]),
(2, 8, 4, 4),
)
runtime.observe_actual_feature(
third["stream_key"],
third["solver_step_id"],
torch.full((2, 8, 4, 4), 3.0, dtype=torch.float16),
)
forecast = runtime.begin_step(
_step_options(
"run-a",
3,
1.5,
False,
total_steps=8,
model_time_coord=400.0,
),
torch.tensor([400.0]),
(2, 8, 4, 4),
)
assert forecast["time_coord"] == 1.5
assert forecast["model_time_coord"] == 400.0
assert forecast["sigma_coord"] == 1.5
assert forecast["actual_forward"] is False
assert forecast["forecast_safe"] is True
pred = runtime.predict_feature(forecast["stream_key"], forecast["solver_step_id"])
assert pred.shape == (2, 8, 4, 4)
assert torch.isfinite(pred).all()
state = runtime.stream_states[forecast["stream_key"]]
fit_cache = state.forecaster._fit_cache
assert fit_cache is not None
assert torch.allclose(torch.tensor(fit_cache.coord_min), torch.tensor(3.0), atol=1e-6)
assert torch.allclose(torch.tensor(fit_cache.coord_max), torch.tensor(14.0), atol=1e-6)
def test_explicit_solver_step_context_without_decision_still_schedules() -> None:
@@ -706,6 +795,25 @@ def test_forecaster_chebyshev_fit_falls_back_to_observed_sigma_range() -> None:
assert torch.isfinite(pred).all()
def test_runtime_fallback_coord_bounds_ignore_unobserved_candidate_step() -> None:
"""Fallback bounds must come only from observed feature history, not pending steps."""
runtime = SpectrumSDXLRuntime(_make_relaxed_cfg())
first = runtime.begin_step(_step_options("run-a", 0, 10.0, True), torch.tensor([10.0]), (1, 1, 1, 1))
runtime.observe_actual_feature(
first["stream_key"],
first["solver_step_id"],
torch.full((1, 1, 1, 1), 10.0, dtype=torch.float16),
)
second = runtime.begin_step(_step_options("run-a", 1, 1.0, True), torch.tensor([1.0]), (1, 1, 1, 1))
state = runtime.stream_states[second["stream_key"]]
assert state.forecaster._coord_bounds is not None
assert torch.allclose(torch.tensor(state.forecaster._coord_bounds[0]), torch.tensor(10.0), atol=1e-6)
assert torch.allclose(torch.tensor(state.forecaster._coord_bounds[1]), torch.tensor(10.0), atol=1e-6)
def test_runtime_uses_schedule_wide_sigma_bounds_for_forecast_fit() -> None:
"""When sample_sigmas are available, the fit must use the full active schedule span."""
cfg = _make_relaxed_cfg()
@@ -1274,7 +1382,8 @@ def main() -> None:
test_invalid_solver_step_context_fails_open()
test_missing_stream_identity_fails_open()
test_explicit_solver_step_context_allows_forecast()
test_runtime_prefers_model_time_coord_for_forecasting()
test_runtime_prefers_sigma_coord_for_forecasting_even_when_model_time_is_present()
test_runtime_prefers_sigma_coord_for_forecasting_without_sample_sigmas()
test_explicit_solver_step_context_without_decision_still_schedules()
test_outer_step_controller_injects_context_and_resets_runs()
test_outer_step_controller_same_object_restart_resets_run_state()
@@ -1291,6 +1400,7 @@ def main() -> None:
test_forecast_fallback_commits_actual_bookkeeping()
test_run_id_switch_resets_stream_state()
test_forecaster_chebyshev_fit_falls_back_to_observed_sigma_range()
test_runtime_fallback_coord_bounds_ignore_unobserved_candidate_step()
test_runtime_uses_schedule_wide_sigma_bounds_for_forecast_fit()
test_runtime_holds_forecasting_until_conservative_min_fit_points()
test_runtime_blocks_forecast_when_recent_validation_error_is_bad()