Skip to content

MAINT GCG: make optimization iteration state explicit and typed - #2467

Open
fei (feiiiiii5) wants to merge 7 commits into
microsoft:mainfrom
feiiiiii5:issue2416/typed-optimization-run-state
Open

MAINT GCG: make optimization iteration state explicit and typed#2467
fei (feiiiiii5) wants to merge 7 commits into
microsoft:mainfrom
feiiiiii5:issue2416/typed-optimization-run-state

Conversation

@feiiiiii5

Copy link
Copy Markdown
Contributor

Purpose

Implements the structural refactor requested in #2416: a typed optimization-iteration state for the GCG loop, with the iteration lifecycle made explicit — while keeping public attack behavior and extension protocols unchanged.

Closes #2416

What changed

Typed state (attack_manager.py)

  • StopReason enum: MAX_STEPS_REACHED, ALL_PROMPTS_JAILBROKEN
  • OptimizationRunState dataclass: current suffix, best suffix, current/best loss, step counter, step runtime, stop reason. Exposed as MultiPromptAttack.last_run_state after run(), so callers and tests can programmatically inspect why/when optimization stopped
  • ProgressiveScheduleState dataclass: goals/workers admitted so far, shared step counter, loss carry-over, inner stop flag. Exposed as ProgressiveMultiPromptAttack.last_schedule_state

Explicit phases with stable contracts

  • MultiPromptAttack.run: stopping phase extracted to _all_training_prompts_jailbroken(); periodic logging phase extracted to _log_best_checkpoint() (same best-suffix swap-and-restore semantics as before, now with a finally guarantee)
  • ProgressiveMultiPromptAttack.run: scheduling counters moved into ProgressiveScheduleState; final evaluation extracted to _finalize_progressive_run()
  • GCGMultiPromptAttack.step: selection phase (flat argmin → group/in-batch decomposition) extracted to _select_best_candidate(). Candidate batches intentionally remain step-local to preserve the existing VRAM-bounding behavior noted in the loop comment

Compatibility

  • All public signatures and return values are unchanged (run() still returns (control, loss, steps) / (control, steps)); the new attributes are additive
  • No algorithm changes: acceptance/annealing, admission schedule, control-weight bumping, and logging cadence are byte-for-byte the same decisions as before, just routed through typed state

Validation

  • New tests/unit/executor/promptgen/gcg/test_run_state.py (12 tests): stop-reason assignment (incl. that the final success check does not count as an executed step), best tracking under annealing rejection, periodic checkpoint restore contract, seeded identical trajectories, argmin decomposition across worker groups, progressive finalize path with call-through assertions on the inner attack log
  • Full GCG unit suite: 211 passed (199 pre-existing + 12 new)
  • ruff check + ruff format --check clean with the repo-pinned ruff v0.16.0

AI usage disclosure

  • This PR was developed with AI assistance (Claude code agent). All code was human-reviewed, and every behavioral claim above is backed by the tests in this PR.

Human verification: I reviewed the diff hunk-by-hunk against the pre-refactor control flow, confirmed the annealing acceptance rule, admission ordering, weight bump threshold (<= 0.09), and return semantics are preserved, and re-derived the argmin index decomposition used in the selection tests.

Implements the structural half of microsoft#2416 while keeping public attack
behavior and extension protocols unchanged.

- add StopReason enum plus typed OptimizationRunState and
  ProgressiveScheduleState dataclasses capturing suffix, losses, best
  result, counters, and stop reason
- MultiPromptAttack.run now tracks state through the typed object and
  exposes it as last_run_state; stopping and periodic logging phases are
  extracted into _all_training_prompts_jailbroken and
  _log_best_checkpoint with stable contracts
- ProgressiveMultiPromptAttack.run tracks admission scheduling through
  ProgressiveScheduleState (exposed as last_schedule_state) and moves
  final evaluation into _finalize_progressive_run
- GCGMultiPromptAttack extracts the candidate-selection phase into
  _select_best_candidate; candidate batches intentionally remain
  step-local to bound VRAM
- add deterministic seeded regression tests covering stop reasons,
  best tracking under annealing rejection, checkpoint restore, argmin
  decomposition across worker groups, and progressive finalize path
@feiiiiii5
fei (feiiiiii5) force-pushed the issue2416/typed-optimization-run-state branch from 181851a to b718775 Compare August 22, 2026 20:37
@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (now includes #2364 logging consolidation and #2434 ty adoption):

  • The extracted stopping/logging/finalize phases are adapted to the new module-level log helpers (_initialize_attack_log/_update_attack_log_params); no behavioral overlap remains with MAINT: Consolidate GCG attack manager logging #2364.
  • While rebasing I caught and fixed a bug in my own branch: my initial extraction had renamed the inner-run loss unpack target, which silently broke the progressive schedule's annealing continuity (prev_loss was pinned to inf across admissions instead of feeding the inner result back). The seeded regression test now asserts the correct feed-back semantics.
  • Validation on the rebased commit: GCG unit suite 217 passed (199 pre-existing incl. MAINT: Adopt ty 0.0.73 #2434's new tests + 12 new state tests), ty check clean under the repo's all = "error" config, ruff check + ruff format --check clean with the pinned ruff.

@romanlutz Roman Lutz (romanlutz) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the submission! A few things need adjusting, though.

Comment thread pyrit/executor/promptgen/gcg/attack/base/attack_manager.py Outdated
Comment thread pyrit/executor/promptgen/gcg/attack/base/attack_manager.py
Comment thread pyrit/executor/promptgen/gcg/attack/base/attack_manager.py
Comment thread pyrit/executor/promptgen/gcg/attack/base/attack_manager.py Outdated
…ring, candidate loss pairing

- Replace RST roles with double-backtick references (check-no-rest-roles)
- ProgressiveMultiPromptAttack carries the inner loss on
  ``ProgressiveScheduleState.loss`` instead of a loose local, so
  ``last_schedule_state.loss`` reflects the final inner run; added a
  post-loop assertion guarding silent carry-over regressions
- ``last_run_state`` / ``last_schedule_state`` default to ``None`` and are
  cleared at the start of each run, so failed runs expose no stale data
- Annealing rejection no longer pairs the accepted suffix with the rejected
  candidate's loss: ``OptimizationRunState.candidate_loss`` tracks what was
  just evaluated while ``loss`` stays paired with ``state.control``

Signed-off-by: fei <204683769+feiiiiii5@users.noreply.github.com>
Comment thread pyrit/executor/promptgen/gcg/attack/base/attack_manager.py Outdated
Comment thread pyrit/executor/promptgen/gcg/attack/base/attack_manager.py Outdated
Comment thread pyrit/executor/promptgen/gcg/attack/base/attack_manager.py
fei (feiiiiii5) and others added 3 commits August 24, 2026 13:21
Implements the structural half of microsoft#2416 while keeping public attack
behavior and extension protocols unchanged.

- add StopReason enum plus typed OptimizationRunState and
  ProgressiveScheduleState dataclasses capturing suffix, losses, best
  result, counters, and stop reason
- MultiPromptAttack.run now tracks state through the typed object and
  exposes it as last_run_state; stopping and periodic logging phases are
  extracted into _all_training_prompts_jailbroken and
  _log_best_checkpoint with stable contracts
- ProgressiveMultiPromptAttack.run tracks admission scheduling through
  ProgressiveScheduleState (exposed as last_schedule_state) and moves
  final evaluation into _finalize_progressive_run
- GCGMultiPromptAttack extracts the candidate-selection phase into
  _select_best_candidate; candidate batches intentionally remain
  step-local to bound VRAM
- add deterministic seeded regression tests covering stop reasons,
  best tracking under annealing rejection, checkpoint restore, argmin
  decomposition across worker groups, and progressive finalize path
…ring, candidate loss pairing

- Replace RST roles with double-backtick references (check-no-rest-roles)
- ProgressiveMultiPromptAttack carries the inner loss on
  ``ProgressiveScheduleState.loss`` instead of a loose local, so
  ``last_schedule_state.loss`` reflects the final inner run; added a
  post-loop assertion guarding silent carry-over regressions
- ``last_run_state`` / ``last_schedule_state`` default to ``None`` and are
  cleared at the start of each run, so failed runs expose no stale data
- Annealing rejection no longer pairs the accepted suffix with the rejected
  candidate's loss: ``OptimizationRunState.candidate_loss`` tracks what was
  just evaluated while ``loss`` stays paired with ``state.control``

Signed-off-by: fei <204683769+feiiiiii5@users.noreply.github.com>
…admission

The progressive-run guard asserted that a measurable loss existed whenever
any optimization step had completed. When an inner run consumes the entire
remaining step budget while a further goal/worker is still waiting to be
admitted, the admission transition resets schedule.loss to inf right
before the budget-exhausted loop exits — so the assertion crashed a valid
run instead of returning. (Two progressive goals at n_steps=3 with an
inner result of ("ctrl", 0.75, 3) reproduces it.)

The guard now only requires a measurable loss while budget remains; a
budget-exhausted exit legitimately carries an inf loss for the goal that
was admitted but never scored.

Regression test reproduces the exact review scenario: goals_admitted=2,
steps_completed=3, loss=inf — the run returns instead of crashing.
@feiiiiii5
fei (feiiiiii5) force-pushed the issue2416/typed-optimization-run-state branch from cbd449b to 0a68d59 Compare August 24, 2026 05:22
@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Addressed — thank you for the precise repro. The root cause is exactly as you described: when an inner run consumes the entire remaining step budget while a further goal/worker is still waiting to be admitted, the admission transition resets schedule.loss to inf right before the budget-exhausted loop exits, so the old guard ("loss was never updated") crashed a valid run instead of returning.

Fix: the guard now only requires a measurable loss while budget remains (steps_completed < n_steps). A budget-exhausted exit legitimately carries an inf loss for the goal that was admitted but never scored — returning is the correct outcome there.

Regression test reproduces the exact scenario (two progressive goals, n_steps=3, inner result ("ctrl", 0.75, 3)): goals_admitted=2, steps_completed=3, loss=inf, and the run returns ("ctrl", 3) instead of asserting. Pre-fix this test crashes; post-fix it passes. test_run_state.py + test_multi_prompt_attack.py: 27 passed; rebased onto current main.

…ate transitions on budget

Review round two on microsoft#2416:

- Seed run-state current/best loss from prev_loss instead of a 1e6 sentinel:
  a rejected first candidate could previously take over best-tracking and the
  starting suffix stayed paired with a fake loss. log() caps infinite seeds
  for readability only.
- Clear last_schedule_state before _update_attack_log_params so a rerun that
  fails during logfile setup no longer exposes the previous run's state.
- Gate phase admissions, sentinel resets, and the control-weight bump on
  remaining step budget: exact-budget exhaustion now returns instead of
  tripping the carried-loss assertion (e.g. two progressive goals with
  n_steps=3 and an inner result of ("ctrl", 0.75, 3)). Finalize-on-success
  semantics are unchanged.

Signed-off-by: fei <204683769+feiiiiii5@users.noreply.github.com>
Two sessions independently addressed romanlutz's round-two review on
microsoft#2416. This merge keeps the transition-gating direction the reviewer
suggested (skip admissions and sentinel resets once
steps_completed >= n_steps) over relaxing the carried-loss assertion:
gating keeps ``last_schedule_state`` honest (no admitted-but-never-
scored phase recorded with an inf loss) and preserves the assertion's
purpose of catching silent carry-over regressions. The duplicate
regression test is superseded by exact-budget tests covering goal,
worker, and control-weight transitions.

Signed-off-by: fei <204683769+feiiiiii5@users.noreply.github.com>
@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Follow-up to the comment above: while preparing these fixes we consolidated two parallel work attempts on this branch. The budget-exhaustion case is now handled by gating the phase admissions and their sentinel resets on remaining step budget (the direction suggested in review), rather than relaxing the carried-loss assertion — see c8fd9a4. Gating keeps last_schedule_state honest (no admitted-but-never-scored phase recorded with an inf loss) and preserves the assertion's value as a carry-over regression guard. Finalize-on-success behavior is unchanged, and exact-budget regression tests now cover goal, worker, and control-weight transitions (tests/unit/executor/promptgen/gcg/test_run_state.py). All 226 tests in the GCG unit suite pass.

# The inner run must have produced a measurable loss whenever any
# optimization happened; guards against silent carry-over regressions.
if schedule.steps_completed > 0:
assert not math.isinf(schedule.loss), "schedule.loss was never updated by the inner run"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion uses inf to mean "the inner run never updated the loss," but a completed inner run can also report inf because of a non-finite model loss or numeric overflow. That case previously returned normally and now raises AssertionError. Could we track whether an inner result was received explicitly, or validate non-finite losses with a deliberate error at the source, rather than infer update state from the numeric value?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MAINT Model GCG optimization iterations as explicit state

2 participants