@@ -68,14 +68,17 @@ class OptimizationRunState:
6868 Captures the current suffix, losses, best result, counters, and stop reason
6969 explicitly instead of leaving them as loose loop locals, so each phase of
7070 the optimization loop has a stable contract that can be asserted under
71- seeded tests. Exposed as ``MultiPromptAttack.last_run_state`` after a call
72- to :meth:`MultiPromptAttack.run`.
71+ seeded tests. ``loss`` is the loss of the *active* control suffix;
72+ ``candidate_loss`` is the loss of the most recently evaluated candidate,
73+ which annealing may have rejected. Exposed as ``MultiPromptAttack.last_run_state`` after a call
74+ to ``MultiPromptAttack.run``.
7375 """
7476
7577 control : str
7678 best_control : str
7779 loss : float
7880 best_loss : float
81+ candidate_loss : float | None = None
7982 steps_completed : int = 0
8083 runtime : float = 0.0
8184 stop_reason : StopReason | None = None
@@ -84,12 +87,12 @@ class OptimizationRunState:
8487@dataclass
8588class ProgressiveScheduleState :
8689 """
87- Typed schedule state for :class:` ProgressiveMultiPromptAttack`.
90+ Typed schedule state for `` ProgressiveMultiPromptAttack` `.
8891
8992 Tracks how many goals and workers have been admitted so far, together with
9093 the shared step counter and the loss carried between progressive rounds.
9194 Exposed as ``ProgressiveMultiPromptAttack.last_schedule_state`` after a call
92- to :meth:` ProgressiveMultiPromptAttack.run`.
95+ to `` ProgressiveMultiPromptAttack.run` `.
9396 """
9497
9598 goals_admitted : int
@@ -814,6 +817,10 @@ def disallowed_toks(self) -> torch.Tensor:
814817class MultiPromptAttack :
815818 """A class used to manage multiple prompt-based attacks."""
816819
820+ #: State of the most recent `run` call; ``None`` until one completes
821+ #: and cleared at the start of each run so failed runs expose no stale data.
822+ last_run_state : OptimizationRunState | None = None
823+
817824 def __init__ (
818825 self ,
819826 goals : list [str ],
@@ -1041,6 +1048,10 @@ def control_weight_fn(_: int) -> float:
10411048 def control_weight_fn (_ : int ) -> float :
10421049 return control_weight
10431050
1051+ # Clear eagerly: a run that raises mid-loop must not leave the previous
1052+ # run's state looking current.
1053+ self .last_run_state = None
1054+
10441055 state = OptimizationRunState (
10451056 control = self .control_str ,
10461057 best_control = self .control_str ,
@@ -1082,9 +1093,13 @@ def control_weight_fn(_: int) -> float:
10821093 if keep_control :
10831094 self .control_str = control
10841095 state .control = control
1096+ state .loss = loss
10851097
1098+ # ``candidate_loss`` tracks what was just evaluated even when
1099+ # annealing rejects it, so ``state.loss`` always describes the
1100+ # suffix in ``state.control``.
1101+ state .candidate_loss = loss
10861102 prev_loss = loss
1087- state .loss = loss
10881103 if loss < state .best_loss :
10891104 state .best_loss = loss
10901105 state .best_control = control
@@ -1251,6 +1266,10 @@ def log(
12511266class ProgressiveMultiPromptAttack :
12521267 """A class used to manage multiple progressive prompt-based attacks."""
12531268
1269+ #: State of the most recent `run` call; ``None`` until one completes
1270+ #: and cleared at the start of each run so failed runs expose no stale data.
1271+ last_schedule_state : ProgressiveScheduleState | None = None
1272+
12541273 def __init__ (
12551274 self ,
12561275 goals : list [str ],
@@ -1429,12 +1448,15 @@ def run(
14291448 },
14301449 )
14311450
1451+ # Clear eagerly: a run that raises mid-loop must not leave the previous
1452+ # run's state looking current.
1453+ self .last_schedule_state = None
1454+
14321455 schedule = ProgressiveScheduleState (
14331456 goals_admitted = 1 if self .progressive_goals else len (self .goals ),
14341457 workers_admitted = 1 if self .progressive_models else len (self .workers ),
14351458 stop_inner_on_success = self .progressive_goals ,
14361459 )
1437- loss = np .inf
14381460
14391461 while schedule .steps_completed < n_steps :
14401462 attack = self .managers ["MPA" ](
@@ -1461,39 +1483,49 @@ def run(
14611483 control_weight = control_weight ,
14621484 anneal = anneal ,
14631485 anneal_from = schedule .steps_completed ,
1464- prev_loss = loss ,
1486+ prev_loss = schedule . loss ,
14651487 stop_on_success = schedule .stop_inner_on_success ,
14661488 test_steps = test_steps ,
14671489 filter_cand = filter_cand ,
14681490 verbose = verbose ,
14691491 )
1470- control , loss , inner_steps = inner_result
1492+ control , inner_loss , inner_steps = inner_result
1493+ schedule .loss = inner_loss
14711494
14721495 schedule .steps_completed += inner_steps
14731496 self .control = control
14741497
14751498 if schedule .goals_admitted < len (self .goals ):
14761499 schedule .goals_admitted += 1
1477- loss = np .inf
1500+ schedule . loss = np .inf
14781501 elif schedule .goals_admitted == len (self .goals ):
14791502 if schedule .workers_admitted < len (self .workers ):
14801503 schedule .workers_admitted += 1
1481- loss = np .inf
1504+ schedule . loss = np .inf
14821505 elif schedule .workers_admitted == len (self .workers ) and stop_on_success :
14831506 self ._finalize_progressive_run (
1484- attack = attack , step = schedule .steps_completed , n_steps = n_steps , loss = loss , verbose = verbose
1507+ attack = attack ,
1508+ step = schedule .steps_completed ,
1509+ n_steps = n_steps ,
1510+ loss = schedule .loss ,
1511+ verbose = verbose ,
14851512 )
14861513 break
14871514 else :
14881515 if isinstance (control_weight , (int , float )) and incr_control :
14891516 if control_weight <= 0.09 :
14901517 control_weight += 0.01
1491- loss = np .inf
1518+ schedule . loss = np .inf
14921519 if verbose :
14931520 logger .info (f"Control weight increased to { control_weight :.5} " )
14941521 else :
14951522 schedule .stop_inner_on_success = False
14961523
1524+ # The inner run must have produced a measurable loss whenever any
1525+ # optimization happened; guards against silent carry-over regressions.
1526+ if schedule .steps_completed > 0 :
1527+ assert not math .isinf (schedule .loss ), "schedule.loss was never updated by the inner run"
1528+
14971529 self .last_schedule_state = schedule
14981530
14991531 return self .control , schedule .steps_completed
0 commit comments