Skip to content

Conversation

@evgri243
Copy link
Contributor

@evgri243 evgri243 commented Oct 20, 2025

// I'm not sure how to make changes of that scale or whether they are desired, but I'd like to highlight them for discussion. Probably, we should keep it out for a while until the implementation is feature complete and well-tested; meanwhile we can use the PR to discuss the changes.//

Summary

This PR introduces PrivacyEngineGradSampleController, an alternative implementation of Opacus's PrivacyEngine that attaches hooks directly to models without wrapping them in GradSampleModule. This solves compatibility issues with transformers and other models that have complex attribute access patterns.

Motivation

The current PrivacyEngine wraps models in a GradSampleModule, which creates several issues:

  1. Type checking breaks: isinstance(model, BertModel) returns False after wrapping
  2. State dict complexity: Wrapped models have _module. prefixes in state dicts
  3. Attribute access issues: Complex __getattr__ behavior in transformers can break
  4. Debugging difficulty: Model structure is hidden behind wrapper

These issues are particularly problematic with HuggingFace transformers and other libraries that perform introspection on model objects.

Solution

Instead of wrapping the model, we:

  1. Attach hooks directly to model submodules via register_forward_hook() and register_full_backward_hook()
  2. Manage hooks externally through a GradSampleController class
  3. Add attributes directly to parameters using setattr() (e.g., param.grad_sample)

The model remains unchanged - no wrapper, no indirection, no type issues.

Implementation

Files Added

  1. opacus/grad_sample_controller.py (~480 lines)

    • GradSampleController class that manages hook lifecycle
    • Captures activations in forward pass
    • Computes per-sample gradients in backward pass
    • Cleanup methods to remove hooks and attributes
  2. opacus/privacy_engine_gsc.py (~530 lines)

    • PrivacyEngineGradSampleController class with same API as PrivacyEngine
    • Creates GradSampleController instead of wrapping model
    • All other functionality identical (accounting, clipping, noise)
  3. opacus/tests/privacy_engine_gsc_test.py (~260 lines)

    • Comprehensive test suite
    • Tests initialization, hook attachment, grad computation
    • Tests state dict compatibility, checkpoints, cleanup

Key Differences from Current Approach

Feature PrivacyEngine (Current) PrivacyEngineHookBased (New)
Model wrapping Yes (GradSampleModule) No
Type preservation ❌ No ✅ Yes
State dict Has _module. prefix Clean, no prefix
Direct attribute access No Direct
Transformer compatibility Can break Better
Privacy guarantees Same Same
Performance Baseline Similar

Usage

from opacus.privacy_engine_hook_based import PrivacyEngineHookBased

model = BertModel(...)
optimizer = torch.optim.Adam(model.parameters())
dataloader = ...

privacy_engine = PrivacyEngineHookBased()

# Model is NOT wrapped!
model, optimizer, dataloader = privacy_engine.make_private(
    module=model,
    optimizer=optimizer,
    data_loader=dataloader,
    noise_multiplier=1.0,
    max_grad_norm=1.0,
)

# Train normally
for data, target in dataloader:
    output = model(data)
    loss = criterion(output, target)
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()

# Clean up when done
privacy_engine.cleanup()

Validation

The code was used to do Zetta 7B transformer LoRA alignment using DPOTrainer from TRL library.

Correctness

  1. Hook implementation: Mirrors GradSampleModule.add_hooks() exactly
  2. Grad computation: Uses same create_or_accumulate_grad_sample() and promote_current_grad_sample() functions
  3. DPOptimizer compatibility: Only requires param.grad_sample attribute, which we provide
  4. Privacy accounting: Uses same accountant classes and mechanisms

Key Implementation Details

  1. Grad samplers: Automatically imported from GradSampleModule.GRAD_SAMPLERS (registered via decorators)
  2. Hook lifecycle: Proper enable/disable/remove/cleanup
  3. Validation: Includes same buffer checking as GradSampleModule
  4. Attribute cleanup: Removes all Opacus-added attributes on cleanup

API Compatibility

The new class maintains full API compatibility with PrivacyEngine:

  • Same make_private() signature
  • Same make_private_with_epsilon() signature
  • Same save_checkpoint() and load_checkpoint() methods
  • Same get_epsilon() method

Migration is trivial: Just change import statement.

Testing

Comprehensive test suite covers:

  • ✅ Initialization
  • ✅ Hook attachment
  • ✅ Per-sample gradient computation
  • ✅ Optimizer step functionality
  • ✅ State dict preservation (no _module. prefix)
  • ✅ Direct attribute access
  • ✅ Checkpoint save/load
  • ✅ Cleanup (hooks and attributes removed)

Benefits

  1. Better transformer compatibility: No wrapper means no __getattr__ issues
  2. Simpler state management: Direct model access, no delegation
  3. Cleaner checkpoints: No _module. prefix to handle
  4. Type checking works: isinstance(model, MyModel) returns True
  5. Easier debugging: Model structure unchanged

Trade-offs

  1. Explicit cleanup needed: Must call privacy_engine.cleanup() to remove hooks
  2. Parameter attributes: Adds attributes directly to parameters (cleaned up on cleanup())
  3. Less battle-tested: New implementation, though logic is identical to existing code

Backward Compatibility

  • ✅ Does not modify existing PrivacyEngine
  • ✅ Can be used alongside existing code
  • ✅ Same privacy guarantees
  • ✅ Compatible with same DPOptimizer classes
  • ✅ Works with existing accountants

Future Work

  • Support for ghost clipping mode (currently only supports hooks/functorch)
  • Support for ghost clipping model + PrivacyEngineAdapriveClipping
  • FSDP support

Checklist

  • Implementation complete
  • Tests written and passing (locally, pending CI)
  • Documentation written
  • Examples provided
  • Code follows Opacus style (Meta copyright, type hints, docstrings)
  • No breaking changes to existing code
  • CI tests pass

@meta-cla meta-cla bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 20, 2025
@meta-codesync
Copy link

meta-codesync bot commented Oct 20, 2025

@facebook-github-bot has imported this pull request. If you are a Meta employee, you can view this in D85086663. (Because this pull request was imported automatically, there will not be any future comments.)

@iden-kalemaj
Copy link
Contributor

iden-kalemaj commented Oct 24, 2025

@evgri243 thank you for this heavy-lifting change. I will take some time to digest and also discuss internally with the team. In the meanwhile I have some questions for you:

  1. Can you provide some examples of failure modes for the current PrivacyEngine approach with HuggingFace transformers. In your tutorial you use BertForSequenceClassification, but this should work fine with the current approach? I'd like to better understand the extent to which the current PrivacyEngine fails to handle certain models, i.e., which model types have you encountered that do not work with PrivacyEngine, and can we handle the failure modes with smaller changes within the current approach?

  2. A main consideration is to avoid code duplication and maintaining several approaches in parallel. With your method, can we avoid some duplication by having PrivacyEngineGradSampleController inherit from PrivacyEngine?

  3. What would the changes needed for extending your methodology to ghost clipping and FSDP?

As an intermediate step, we could place your approach in the "research" folder, which we do not actively maintain, but are happy to support you in maintaining it. This would allow some time for the method to be digested before moving it into the main opacus folder.

@iden-kalemaj iden-kalemaj self-assigned this Oct 24, 2025
@evgri243
Copy link
Contributor Author

evgri243 commented Oct 24, 2025

Thanks for consideration. It is still work in progress, but I'd love to know your opinion. Let me answer your questions in words, then I'll come with examples if needed:

  1. Yeah. The example is more Opacus oriented. We don't experience issues with the models as we mostly use LoRA that limits us to linear layers only anyway. The problem is the trainer. We have a custom loop to replace a standard trainers, but trainers from try library (DPO, KTO) are much harder to substitute due to complicated data preparation and loss implementations. Those trainers are really into isinstance(model, PeftModel) checks, calling direct methods model.from_pretrained(...) on checkpointing, or accessing properties directly model.loss_function. We implemented originally getattr but checkpoint restoration started failing as the model started saving as _module.<tensor_name>, but recovering through forwarding with getattr without it. We overriden state_dict as well, but it all started falling apart. And then we realized that we can avoid wrapping in the first place... and here we are.
  2. We can look into privacy engine to wrap it deeper into the existing one. Yeah, duplication is major and existence of two parallel modes is even worse, but keeping it completely to ourselves was unfair as the wrap-less mode just works.
  3. We are on ghost clipping now, but thanks to the same KTO and DPO Trainers integrating the loss wrapper is another headache which we so far fail to implement. We have standard ghost clipping wired, but haven't had a proper chance to test it. FSDP is the final goal and we are slowly going there...

I thought about "research" or "contrib". My major problem is to make it package able, but I guess it is not a major implementation issue to add yet another package.

@iden-kalemaj
Copy link
Contributor

Thank you for these explanations. I like your solution and have also experienced some annoyances with accessing attributes of the model post-wrapping. I also understand the use case better now.

I believe we can minimize code duplication which would make it more reasonable to introduce this into Opacus.

  1. having GradSampleController extend GradSampleModule to avoid duplicate code. It's okay if this requires some factorization on the side of GradSampleModule.
  2. having PrivacyEngineGradSampleController extend PrivacyEngine to avoid the duplicated functions. The more we can minimize new code in PrivacyEngineGradSampleController the better. Likewise we can factorize things in PrivacyEngine as needed.

Do these make sense?

Regarding ghost clipping and FSDP. You mention that you mostly use LoRA. Ghost clipping does not give any memory advantage with LoRA fine-tunining since the effective linear layer width is small, so just wanted to give a heads up that ghost clipping might not be needed for your use case.

We did not implement FSDP with vanilla (non-ghost) clipping since this required more significant effort, though we did put some work into this and if you're interested in using extending FSDP + vanilla, then we'd welcome PRs here.

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

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants