Add DpsgdConfig execution plan for Poisson and fixed-size DP-SGD - #337
Add DpsgdConfig execution plan for Poisson and fixed-size DP-SGD#337Ayush7614 wants to merge 8 commits into
Conversation
Provide Tier-2 DP-SGD configs that couple Poisson or fixed-size batch selection, clipping, Gaussian noise, and matching accounting events so users can run vanilla DP-SGD without hand-wiring Tier-3 components.
There was a problem hiding this comment.
👋 Welcome, and thanks for opening your first PR!
We're excited to have you contribute. A few things to help you get started:
- Join the community! Say hello on the Google Differential Privacy Slack — we'd love to hear who you are, what you're working on, and what brought you here.
- A maintainer will review your PR shortly. If you have questions in the meantime, don't hesitate to ask here or on Slack.
Thanks for contributing to privacy-preserving machine learning! 🔐
ryan112358
left a comment
There was a problem hiding this comment.
Nice addition, thanks! Please address the comments and I'll take another look
|
|
||
|
|
||
| @dataclasses.dataclass(frozen=True, kw_only=True) | ||
| class DpsgdConfig: |
There was a problem hiding this comment.
Let's try to unify DpsgdConfig and FixedSizeDpsgdConfig under a single dataclass, where the batch selection / accounting is routed based on one of the dataclass fields
There was a problem hiding this comment.
Done — merged into a single DpsgdConfig. Routing is now via an explicit batch_selection: DpsgdBatchSelection field (POISSON / FIXED), so additional strategies can be added later without a second config class.
|
|
||
| Attributes: | ||
| iterations: Number of training iterations / batch draws. | ||
| expected_batch_size: Expected Poisson batch size. Sampling probability is |
There was a problem hiding this comment.
Being able to specify this implies you know the number of examples, which rules out ADD_OR_REMOVE neighboring relation. Let's parameterize this in terms of "expected_participations" like BandMFConfig
There was a problem hiding this comment.
Done — Poisson path now takes expected_participations (same idea as BandMFConfig). num_examples is only required for fixed-size (and truncated Poisson) where the dataset size is needed to derive the batch size / sampling probability.
| ``expected_batch_size`` when ``None``. | ||
| truncated_batch_size: Optional truncation cap for Poisson sampling. When | ||
| set, accounting uses ``truncated_dpsgd_event``. | ||
| use_zcdp: If True, account with a ``ZCDpEvent`` (e.g. for discrete |
There was a problem hiding this comment.
Discrete gaussian is currently not fully compatable with this API or the noise_addition API, so let's drop that reference and this flag here.
There was a problem hiding this comment.
Done — removed use_zcdp and the discrete-Gaussian references from the config and docs.
Merge FixedSizeDpsgdConfig into DpsgdConfig, route Poisson vs fixed-size via batch_size, parameterize Poisson like BandMF with expected_participations, and drop the use_zcdp / discrete-Gaussian references.
46c3978 to
30f7a49
Compare
|
Thanks for the review @ryan112358 — addressed in the latest commit:
Happy to adjust further if needed. |
ryan112358
left a comment
There was a problem hiding this comment.
Thx for the quick turnaround, please respond to each comment individually or mark it as resolved. (from prior review & this one)
| batch_size: int | None = None | ||
| num_examples: int | None = None | ||
| truncated_batch_size: int | None = None | ||
| replace: bool = False |
There was a problem hiding this comment.
Done — dropped the replace kwarg.
| Poisson sampling. | ||
| """ | ||
|
|
||
| iterations: int |
There was a problem hiding this comment.
In the future, we will want to support more than just Poisson / Fixed Batch. There's also Random allocation - support is being added for that in dp-accounting now. Whatever design we have here needs to be done with that future in mind
There was a problem hiding this comment.
Agreed — switched the routing field to an explicit DpsgdBatchSelection enum (POISSON, FIXED) so we can extend with RANDOM_ALLOCATION (or others) once dp-accounting support lands, without another config class or a boolean/batch_size-as-sentinel design.
| l2_clip_norm: float = 1.0 | ||
| rescale_to_unit_norm: bool = True | ||
| normalize_by: float = 1.0 | ||
| batch_size: int | None = None |
There was a problem hiding this comment.
This can be inferred from expected_participations and num_examples so is probably not needed
There was a problem hiding this comment.
Done — removed batch_size as a constructor argument. For FIXED, it is now derived as expected_participations * num_examples / iterations (exposed as a read-only property for callers that still need it).
Introduce DpsgdBatchSelection so more strategies (e.g. random allocation) can be added later, infer fixed batch size from expected_participations and num_examples, and remove the replace kwarg.
|
Thanks @ryan112358 — replied on each thread. Summary of the latest push:
Happy to adjust further if the enum / inference shape isn’t quite right. |
Keep both Unreleased entries: DpsgdConfig (this PR) and the clipped_fun Formal Guarantees fix from google-deepmind#338.
ryan112358
left a comment
There was a problem hiding this comment.
Please update the PR description to disclose how you used AI for this PR. Things like how did you guide it, how did you check it's work, how much iteration did you do before you opened the PR, etc. AI usage is generally fine, but reviewing has a cost so it's helpful to know
| return clipped_grad_transform | ||
|
|
||
|
|
||
| def _check_noise_multiplier_set(noise_multiplier: float | None) -> None: |
There was a problem hiding this comment.
Maybe add this to _validate.py as say "def notnull(**kwargs)" or similar
There was a problem hiding this comment.
Done — added _validate.not_none(**kwargs) and use it from DpsgdConfig.make() (and BandMF’s calibrated check) instead of a one-off helper.
| f' {type(self.batch_selection)!r}.' | ||
| ) | ||
|
|
||
| if ( |
There was a problem hiding this comment.
This is adding a significant amount of bloat. Please think carefully about if it can be reduced or otherwise factored into _validate.py
There was a problem hiding this comment.
Done — pulled common checks into _validate.not_none / instance_of / at_most and slimmed __post_init__ accordingly (net ~150 lines removed in this push with the example deletion).
| ) | ||
|
|
||
| @property | ||
| def batch_size(self) -> int: |
There was a problem hiding this comment.
For poisson we have variable batch sizes, so this would have to be 'expected_batch_size'
There was a problem hiding this comment.
Done — renamed the property to expected_batch_size (float). Fixed-size sampling still validates/uses an integer via a private _fixed_batch_size() helper.
| @@ -0,0 +1,138 @@ | |||
| # coding=utf-8 | |||
There was a problem hiding this comment.
Generally we prefer smaller focused PRs and we pay close attention to "diff hygiene" and whether a change warrants the maintenance burden. Given that users can (or should be able to) just take an existing example and swap out the execution plan, I think a totally separate example is unnecessary here, so I'd remove this here align with the change to examples_guide.md
There was a problem hiding this comment.
Done — removed examples/dp_sgd_execution_plan.py and the corresponding examples_guide.md / CHANGELOG references. Existing examples can swap in DpsgdConfig as needed.
Move not_none/instance_of/at_most into _validate, rename batch_size to expected_batch_size, and remove the standalone execution-plan example.
|
Thanks @ryan112358 — addressed the latest review in
Happy to adjust further if anything still feels heavy. |
ryan112358
left a comment
There was a problem hiding this comment.
Mostly looks good, will send for an internal review next. Thanks!
| ) | ||
| if self.noise_multiplier is not None: | ||
| _validate.non_negative(noise_multiplier=self.noise_multiplier) | ||
| _validate.instance_of( |
There was a problem hiding this comment.
The type checker should catch this at "compile" time, no need for runtime validation
There was a problem hiding this comment.
Done — removed the runtime instance_of check (and the unused _validate.instance_of helper).
| ValueError: If ``num_examples`` is unset. | ||
| """ | ||
| _validate.not_none(num_examples=self.num_examples) | ||
| return self.expected_participations * self.num_examples / self.iterations |
There was a problem hiding this comment.
This might not be an integer under FIXED
There was a problem hiding this comment.
Done — clarified that expected_batch_size is always a float and need not be integral under FIXED. The integer passed to FixedBatchSampling is derived separately via _fixed_batch_size(), which still requires an exact positive integer for accounting consistency.
There was a problem hiding this comment.
Probably for fixed_batch_size this should still be an integer (rounded) since by definition it has to be fixed across all steps. I think you should fold in the logic from the _fixed_batch_size helper here, and delete that method.
| f'Unsupported batch_selection={self.batch_selection!r}. Additional' | ||
| ' strategies will be wired as dp-accounting support lands.' | ||
| ) | ||
| query_sensitivity = clipped_grad_transform(lambda: None).sensitivity( |
There was a problem hiding this comment.
So I think dp_accounting treats "noise_multiplier" as relative to "l2_norm_bound" which is 2x smaller than "sensitivity" under REPLACE_ONE DP. It's worth changign this to l2_norm_bound and adding an inline comment explaining why, along with a test that noise get's calibrated correctly in this setting.
There was a problem hiding this comment.
Done — make() now scales noise by l2_norm_bound (with an inline comment explaining why sensitivity() would over-noise under REPLACE_ONE). Added test_fixed_size_noise_matches_l2_norm_bound_not_sensitivity to lock this in.
Remove instance_of validation, clarify expected_batch_size may be non-integral under FIXED, and calibrate Gaussian noise using l2_norm_bound so REPLACE_ONE does not over-noise vs dp_accounting.
|
Thanks @ryan112358 — addressed the latest review:
|
| return self.expected_participations * self.num_examples / self.iterations | ||
|
|
||
| def _fixed_batch_size(self) -> int: | ||
| """Integer batch size required by ``DpsgdBatchSelection.FIXED``. |
There was a problem hiding this comment.
Our linter is blocking approval since this multi-line docstring doesn't have a "Returns:"
| batch_size=self._fixed_batch_size(), | ||
| replace=False, | ||
| ) | ||
| if self.batch_selection is not DpsgdBatchSelection.POISSON: |
There was a problem hiding this comment.
Style nitpick: For readability/consistency with similar statement in other parts of this library/related libraries could you reformat this as:
if self.batch_selection is DpsgdBatchSelection.FIXED:
...
elif self.batch_selection is DpsgdBatchSelection.POISSON:
...
else:
raise NotImplementedError(...)
| ValueError: If ``num_examples`` is unset. | ||
| """ | ||
| _validate.not_none(num_examples=self.num_examples) | ||
| return self.expected_participations * self.num_examples / self.iterations |
There was a problem hiding this comment.
Probably for fixed_batch_size this should still be an integer (rounded) since by definition it has to be fixed across all steps. I think you should fold in the logic from the _fixed_batch_size helper here, and delete that method.
| batch_size=self._fixed_batch_size(), | ||
| replace=False, | ||
| ) | ||
| if self.batch_selection is not DpsgdBatchSelection.POISSON: |
| if accountant_fn is None: | ||
| if self.batch_selection is DpsgdBatchSelection.FIXED: | ||
|
|
||
| def _rdp_accountant( |
There was a problem hiding this comment.
Not sure what this does, can it be deleted?
| iterations=self.iterations, | ||
| cycle_length=1, | ||
| truncated_batch_size=self.truncated_batch_size, | ||
| partition_type=( |
There was a problem hiding this comment.
Since we have cycle_length=1, partition_type is a no-op and can be removed.
|
Hi @Ayush7614 , I have mulled over this change a bit and have a few thoughts. DpSGDConfig is a special case of BandMFConfig in most cases, with the exception of fixed batch sampling (which is not SOTA). For that reason, the maintenance burden of this extra code is likely not worth the delta in capabilities it brings to Jax privacy currently. With that said, if you are able to land a couple of other PRs before this one, then this class can feature some nice additional capabilities. Specifically, if you can generalize DpSgd accounting to support a list of sampling probabilities and a list of batch sizes respectively, along with a list of noise multipliers, this would provide enough of a difference to potentially warrant bringing this in. If you want to work on this, please open an issue first with your proposed change including what files it affects and how. |
Summary
DpsgdConfigTier-2DPExecutionPlanconfig for vanilla DP-SGD, withDpsgdBatchSelection(POISSON/FIXED) routing batch selection and matching accounting events.BandMFConfigcalibrate()/make()API, includingexpected_participations(no publicnum_examplesrequired for add/remove Poisson) and privacy-correctness tests._validate(not_none,instance_of,at_most); expected batch size is exposed asexpected_batch_size.AI usage disclosure
AI (Cursor) was used to draft and iterate on this PR under my direction. I specified the API shape (unify configs,
expected_participations, enum routing, dropreplace/use_zcdp, lean validation, remove the extra example), ran and checked the test suite locally (pytest tests/execution_plan_test.py), and reviewed the resulting diffs before each push. Several review rounds with @ryan112358 drove further edits; I verified each change against the comments and re-ran tests after updates.Test plan
pytest tests/execution_plan_test.py(21 passed)