Skip to content

PETRecipe: one shared DP-SGD training path - #447

Open
fazelehh wants to merge 2 commits into
feature/pet-optimizationfrom
feature/pet-recipe
Open

fazelehh wants to merge 2 commits into
feature/pet-optimizationfrom
feature/pet-recipe

Conversation

@fazelehh

@fazelehh fazelehh commented Aug 18, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #438 (base is feature/pet-optimization; GitHub retargets this to main automatically once #438 merges).

What

Adds leakpro/optimization/training.py: the structured training contract (PETRecipe) and the single shared Opacus path (train_with_dpsgd, build_campaign_fns) that replaces the DP-SGD loop previously duplicated in every campaign example. All three examples (LOS-LR, CIFAR, LOS GRU-D) are ported onto it and shrink to declaring a recipe; the standalone GRU-D script is superseded by the recipe-ported one.

A PETRecipe is the plan's structured contract: instead of a monolithic train(data) -> model, the user supplies factories the optimizer can intervene in (make_model, make_optimizer, make_loader, criterion, epochs). Each factory receives the sampled knob config and reads what it needs, which is what makes the next PET cheap — a regularization bundle is just make_model reading dropout and make_optimizer reading weight_decay, with no interface change. Exotic loops that do not fit keep the escape hatch of writing the three campaign callables directly.

Hardening beyond the refactor

  • make_opacus_compatible() — ModuleValidator pass (BatchNorm → GroupNorm), disables in-place activations, and rewrites torchvision residual blocks whose in-place out += identity ModuleValidator cannot see because it lives in forward rather than a submodule. Without this, any BatchNorm architecture — including both webapp image presets — fails at make_private().
  • The privacy accountant is a parameter, defaulting to "prv" (was hard-coded "rdp"). RDP is the looser bound, so identical noise reported a larger ε here than via the webapp's DP-SGD path. This changes reported ε for existing campaigns (downward, for identical noise); empirical attack numbers are unaffected.
  • The accuracy metric thresholds single-logit binary heads at 0 instead of argmax — argmax over one column is always 0, silently yielding 0% or 100%.

Review fixes applied

An adversarial review of this PR produced 10 findings; all are addressed in 18db5a2a. The theme was silent behaviour changes versus the per-example code this PR replaced.

  • train_with_dpsgd fails closed. A missing or misspelled noise_multiplier silently trained a fully non-private model, from a function whose only purpose is DP-SGD. The key is now required; non-private runs pass an explicit 0.0.
  • make_opacus_compatible runs on both branches. Rewriting BatchNorm → GroupNorm only for private configs left the noise=0 anchor a structurally different model, so its utility gap mixed the cost of DP noise with the cost of an architecture change — the frontier's utility axis was not comparable end to end. Documented consequence: the submitted architecture is not necessarily what trains.
  • Full mimicry is enforced, not silently downgraded. n_ref_train used min(target, ref_pool), so a short reference pool quietly trained weaker references and biased the audit optimistic. A short pool is now an error — which is what the old per-example code did loudly.
  • _patch_residual_blocks only rewrites blocks still using the stock forward. Subclasses with their own forward (SE, attention) were being silently replaced by the vanilla residual path, changing the model rather than making it Opacus-safe. Such blocks are now left alone with a warning.
  • The auc utility metric refuses multiclass output instead of reshape(-1)-ing it into a meaningless number.
  • The accountant travels with the epsilon in campaign_extras. The prv default is deliberate, but PRV and RDP epsilons are not comparable — measured 4.22 vs 4.87 for identical noise — so a record that does not name its accountant cannot safely be compared with another run's.

Examples: GRU-D regains the max_physical_batch=128 cap the pre-PR code set deliberately (it is the memory-heaviest target in the repo; the shared 256 default would OOM mid-campaign) and gains the small-dataset split guard its LR sibling already had. The LOS campaign imports target_models.LR instead of redeclaring an identical LRSigmoid, so it trains the same class the audited target used.

One earlier test asserted the now-incorrect behaviour (BatchNorm surviving the non-private path) and has been inverted to assert architecture consistency instead.

Verification

  • pytest leakpro/tests/test_optimization/ — 47 tests pass.
  • ruff check . clean.
  • The DP-SGD compatibility tests fail with the fix disabled (checked), so they are real regression tests rather than vacuous ones.

The webapp's near-verbatim copy of the Opacus-compat block is removed in #448, which now calls this shared helper.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QBPG7MprEqhqG95fFURk8h

@fazelehh
fazelehh force-pushed the feature/pet-recipe branch from 172f691 to 692a9bd Compare August 19, 2026 06:14
fazelehh added a commit that referenced this pull request Aug 19, 2026
…ull mimicry

Findings from an adversarial review of this PR. The theme is silent behaviour
changes versus the code this PR replaced.

train_with_dpsgd now fails closed: a missing or misspelled "noise_multiplier"
raised nothing and trained a fully non-private model from a function whose only
purpose is DP-SGD. The key is required; non-private runs pass an explicit 0.

make_opacus_compatible runs on both branches, not just the private one.
Rewriting BatchNorm to GroupNorm only for private configs left the noise=0
anchor a structurally different model, so its utility gap mixed the cost of DP
noise with the cost of an architecture change — the frontier's utility axis was
not comparable end to end. Documented that the submitted architecture is
therefore not necessarily what trains.

Full mimicry is enforced instead of silently downgraded: n_ref_train used
min(target, ref_pool), so a short reference pool quietly trained weaker
references and biased the audit optimistic. A short pool is now an error, which
is what the old per-example code did loudly.

_patch_residual_blocks only rewrites blocks that still use the stock forward.
Subclasses with their own forward (SE, attention) were silently replaced by the
vanilla residual path — changing the model rather than making it Opacus-safe.
Such blocks are now left alone with a warning.

The "auc" utility metric refuses multiclass output instead of reshape(-1)-ing
it into a meaningless number.

The accountant travels with the epsilon in campaign_extras. The prv default is
deliberate (it matches the webapp's DP-SGD path), but PRV and RDP epsilons are
not comparable — measured 4.22 vs 4.87 for identical noise here — so a record
that does not name its accountant cannot safely be compared with another run's.

Examples: GRU-D regains the max_physical_batch=128 cap the pre-PR code set
deliberately (it is the memory-heaviest target in the repo, and the shared 256
default would OOM mid-campaign), and gains the small-dataset split guard its LR
sibling already had. The LOS campaign imports target_models.LR instead of
redeclaring an identical LRSigmoid, so the campaign trains the same class the
audited target used.

One earlier test asserted the now-incorrect behaviour (BatchNorm surviving the
non-private path) and has been inverted to assert architecture consistency.
47 tests pass, ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBPG7MprEqhqG95fFURk8h
fazelehh added a commit that referenced this pull request Aug 19, 2026
The webapp's DP-SGD loop carried a near-verbatim copy of the BatchNorm fix,
in-place-activation pass and residual-forward patch that now live in
leakpro.optimization.make_opacus_compatible. Sharing one implementation means a
model trained through the wizard and the same model trained by a PET campaign
are the same architecture, and the subclass-safety fix from the #447 review
applies to both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBPG7MprEqhqG95fFURk8h
@fazelehh

Copy link
Copy Markdown
Collaborator Author

Addressed the code-review findings on this PR. The theme was silent behaviour changes versus the per-example code this PR replaced.

Correctness

  • train_with_dpsgd fails closed: a missing or misspelled noise_multiplier silently trained a fully non-private model. The key is now required; non-private runs pass an explicit 0.0.
  • make_opacus_compatible runs on both branches. Rewriting BatchNorm→GroupNorm only for private configs left the noise=0 anchor a structurally different model, so its utility gap mixed the cost of DP noise with the cost of an architecture change — the frontier's utility axis was not comparable end to end.
  • Full mimicry is enforced, not silently downgraded: n_ref_train used min(target, ref_pool), so a short reference pool quietly trained weaker references and biased the audit optimistic. Now an error, as the old per-example code was.
  • _patch_residual_blocks only rewrites blocks still using the stock forward; subclasses with a custom forward (SE/attention) were being silently replaced by the vanilla residual path.
  • The auc utility metric refuses multiclass output instead of reshape(-1)-ing it into a meaningless number.
  • The accountant travels with the epsilon in campaign_extras. The prv default is deliberate (it matches the webapp's DP-SGD path), but PRV and RDP epsilons are not comparable — measured 4.22 vs 4.87 for identical noise — so a record that does not name its accountant cannot safely be compared with another run's.

Examples

  • GRU-D regains max_physical_batch=128 (it is the memory-heaviest target here; the shared 256 default would OOM mid-campaign) and gains the small-dataset split guard its LR sibling already had.
  • The LOS campaign imports target_models.LR instead of redeclaring an identical LRSigmoid, so it trains the same class the audited target used.

Also: one of my earlier tests asserted the now-incorrect behaviour (BatchNorm surviving the non-private path) and has been inverted to assert architecture consistency. The webapp's duplicate Opacus-compat block is removed in #448, which now calls this shared helper.

47 tests pass on this branch, ruff clean.

fazelehh added a commit that referenced this pull request Aug 20, 2026
…ull mimicry

Findings from an adversarial review of this PR. The theme is silent behaviour
changes versus the code this PR replaced.

train_with_dpsgd now fails closed: a missing or misspelled "noise_multiplier"
raised nothing and trained a fully non-private model from a function whose only
purpose is DP-SGD. The key is required; non-private runs pass an explicit 0.

make_opacus_compatible runs on both branches, not just the private one.
Rewriting BatchNorm to GroupNorm only for private configs left the noise=0
anchor a structurally different model, so its utility gap mixed the cost of DP
noise with the cost of an architecture change — the frontier's utility axis was
not comparable end to end. Documented that the submitted architecture is
therefore not necessarily what trains.

Full mimicry is enforced instead of silently downgraded: n_ref_train used
min(target, ref_pool), so a short reference pool quietly trained weaker
references and biased the audit optimistic. A short pool is now an error, which
is what the old per-example code did loudly.

_patch_residual_blocks only rewrites blocks that still use the stock forward.
Subclasses with their own forward (SE, attention) were silently replaced by the
vanilla residual path — changing the model rather than making it Opacus-safe.
Such blocks are now left alone with a warning.

The "auc" utility metric refuses multiclass output instead of reshape(-1)-ing
it into a meaningless number.

The accountant travels with the epsilon in campaign_extras. The prv default is
deliberate (it matches the webapp's DP-SGD path), but PRV and RDP epsilons are
not comparable — measured 4.22 vs 4.87 for identical noise here — so a record
that does not name its accountant cannot safely be compared with another run's.

Examples: GRU-D regains the max_physical_batch=128 cap the pre-PR code set
deliberately (it is the memory-heaviest target in the repo, and the shared 256
default would OOM mid-campaign), and gains the small-dataset split guard its LR
sibling already had. The LOS campaign imports target_models.LR instead of
redeclaring an identical LRSigmoid, so the campaign trains the same class the
audited target used.

One earlier test asserted the now-incorrect behaviour (BatchNorm surviving the
non-private path) and has been inverted to assert architecture consistency.
47 tests pass, ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBPG7MprEqhqG95fFURk8h
@fazelehh
fazelehh force-pushed the feature/pet-recipe branch from 18db5a2 to 6d20b70 Compare August 20, 2026 07:20
fazelehh added a commit that referenced this pull request Aug 20, 2026
The webapp's DP-SGD loop carried a near-verbatim copy of the BatchNorm fix,
in-place-activation pass and residual-forward patch that now live in
leakpro.optimization.make_opacus_compatible. Sharing one implementation means a
model trained through the wizard and the same model trained by a PET campaign
are the same architecture, and the subclass-safety fix from the #447 review
applies to both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBPG7MprEqhqG95fFURk8h
fazelehh added a commit that referenced this pull request Aug 20, 2026
…ull mimicry

Findings from an adversarial review of this PR. The theme is silent behaviour
changes versus the code this PR replaced.

train_with_dpsgd now fails closed: a missing or misspelled "noise_multiplier"
raised nothing and trained a fully non-private model from a function whose only
purpose is DP-SGD. The key is required; non-private runs pass an explicit 0.

make_opacus_compatible runs on both branches, not just the private one.
Rewriting BatchNorm to GroupNorm only for private configs left the noise=0
anchor a structurally different model, so its utility gap mixed the cost of DP
noise with the cost of an architecture change — the frontier's utility axis was
not comparable end to end. Documented that the submitted architecture is
therefore not necessarily what trains.

Full mimicry is enforced instead of silently downgraded: n_ref_train used
min(target, ref_pool), so a short reference pool quietly trained weaker
references and biased the audit optimistic. A short pool is now an error, which
is what the old per-example code did loudly.

_patch_residual_blocks only rewrites blocks that still use the stock forward.
Subclasses with their own forward (SE, attention) were silently replaced by the
vanilla residual path — changing the model rather than making it Opacus-safe.
Such blocks are now left alone with a warning.

The "auc" utility metric refuses multiclass output instead of reshape(-1)-ing
it into a meaningless number.

The accountant travels with the epsilon in campaign_extras. The prv default is
deliberate (it matches the webapp's DP-SGD path), but PRV and RDP epsilons are
not comparable — measured 4.22 vs 4.87 for identical noise here — so a record
that does not name its accountant cannot safely be compared with another run's.

Examples: GRU-D regains the max_physical_batch=128 cap the pre-PR code set
deliberately (it is the memory-heaviest target in the repo, and the shared 256
default would OOM mid-campaign), and gains the small-dataset split guard its LR
sibling already had. The LOS campaign imports target_models.LR instead of
redeclaring an identical LRSigmoid, so the campaign trains the same class the
audited target used.

One earlier test asserted the now-incorrect behaviour (BatchNorm surviving the
non-private path) and has been inverted to assert architecture consistency.
47 tests pass, ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBPG7MprEqhqG95fFURk8h
@fazelehh
fazelehh force-pushed the feature/pet-recipe branch from 6d20b70 to c72393a Compare August 20, 2026 07:58
fazelehh added a commit that referenced this pull request Aug 20, 2026
The webapp's DP-SGD loop carried a near-verbatim copy of the BatchNorm fix,
in-place-activation pass and residual-forward patch that now live in
leakpro.optimization.make_opacus_compatible. Sharing one implementation means a
model trained through the wizard and the same model trained by a PET campaign
are the same architecture, and the subclass-safety fix from the #447 review
applies to both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBPG7MprEqhqG95fFURk8h
@fazelehh
fazelehh requested a review from TheColdIce September 18, 2026 13:34
fazelehh and others added 2 commits September 21, 2026 11:34
Adds leakpro/optimization/training.py with two entry points over a single
Opacus loop, so every PET optimization run — library, example, webapp —
trains through the same code:

- fit_dpsgd(model, loader, criterion, optimizer, epochs, noise_multiplier,
  max_grad_norm, ...) has the shape of AbstractInputHandler.train on purpose.
  A handler forwards its arguments here and the RMIA shadow models are then
  trained by exactly the code that trained the target, which is what makes
  them mimic it.
- train_with_dpsgd(recipe, config, indices, ...) builds model, loader and
  optimizer from a PETRecipe (factories) and calls fit_dpsgd. It fails closed:
  a missing noise_multiplier or max_grad_norm raises instead of silently
  training a non-private model.

Every model, private or not, first goes through make_opacus_compatible
(BatchNorm -> GroupNorm, in-place activations off, torchvision residual adds
out of place) so all points on one frontier share an architecture. Because
ModuleValidator.fix swaps children in place, the root module keeps its
identity while its parameter set changes; fit_dpsgd therefore compares
parameter identity, not object identity, and rebuilds the optimizer when
they differ — an optimizer built before the rewrite would otherwise keep
stepping detached BatchNorm parameters and never touch the GroupNorm ones.

The accountant defaults to PRV and travels with epsilon as model.dp_accounting:
PRV and RDP epsilons for the same noise are not comparable, so a number that
does not name its accountant cannot be compared across runs.

Supersedes the Campaign-based recipe branch: build_campaign_fns, the
confidence-signal attack and the campaign example ports are gone, since the
optimization layer now audits with the real RMIA attack via run_rmia_audit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RivRrjHEc3puv5EYAHLkAs
…t_dpsgd

dp_handler.dp_train no longer carries its own PrivacyEngine/BatchMemoryManager
loop; it reads the DP knobs from dpsgd_dic.pkl and forwards to
leakpro.optimization.fit_dpsgd, so target and shadow models in the CIFAR
example are trained by the same code as every other PET run. Training-set
accuracy/loss are measured after the last epoch on the logical loader via a
shared evaluate(), since the Poisson-sampled private loader does not visit
every example exactly once.

PrivacyUtilityConfig gains `accountant` (prv | rdp | gdp, default prv). The
run writes it into each trial's dpsgd_dic.pkl and records it next to epsilon
in the trial extras, so a frontier never reports an epsilon without naming
the accountant that produced it. Default moves from the handler's hard-coded
"rdp" to "prv" — the same noise now reads as a smaller epsilon, and the
recorded accountant makes that visible rather than silent.

Verified: `run_optimization.py --smoke` completes end to end (target, 3 RMIA
shadow models, 2 Optuna trials) through the shared loop.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RivRrjHEc3puv5EYAHLkAs
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.

1 participant