class_docstr: ``torch.optim.Optimizer`` wrapper that adds additional functionality to clip per
  sample gradients and add Gaussian noise.
  
  Can be used with any ``torch.optim.Optimizer`` subclass as an underlying optimizer.
  ``DPOptimzer`` assumes that parameters over which it performs optimization belong
  to GradSampleModule and therefore have the ``grad_sample`` attribute.
  
  On a high level ``DPOptimizer``'s step looks like this:
  1) Aggregate ``p.grad_sample`` over all parameters to calculate per sample norms
  2) Clip ``p.grad_sample`` so that per sample norm is not above threshold
  3) Aggregate clipped per sample gradients into ``p.grad``
  4) Add Gaussian noise to ``p.grad`` calibrated to a given noise multiplier and
  max grad norm limit (``std = noise_multiplier * max_grad_norm``).
  5) Call underlying optimizer to perform optimization step
  
  Examples:
      >>> module = MyCustomModel()
      >>> optimizer = torch.optim.SGD(module.parameters(), lr=0.1)
      >>> dp_optimizer = DPOptimizer(
      ...     optimizer=optimizer,
      ...     noise_multiplier=1.0,
      ...     max_grad_norm=1.0,
      ...     expected_batch_size=4,
      ... )
docstr: Performs gradient clipping.
  Stores clipped and aggregated gradients into `p.summed_grad```
docstr_tok: ["Performs","gradient","clipping",".","Stores","clipped","and","aggregated","gradients","into","`","p.summed_grad","``","`"]
code: def clip_and_accumulate(self):
      """
      Performs gradient clipping.
      Stores clipped and aggregated gradients into `p.summed_grad```
      """
  
      if len(self.grad_samples[0]) == 0:
          # Empty batch
          per_sample_clip_factor = torch.zeros((0,))
      else:
          per_param_norms = [
              g.reshape(len(g), -1).norm(2, dim=-1)
              for g in self.grad_samples
          ]
          per_sample_norms = torch.stack(per_param_norms, dim=1).norm(
              2, dim=1
          )
          per_sample_clip_factor = (
              self.max_grad_norm / (per_sample_norms + 1e-6)
          ).clamp(max=1.0)
  
      for p in self.params:
          _check_processed_flag(p.grad_sample)
          grad_sample = self._get_flat_grad_sample(p)
          grad = contract("i,i...", per_sample_clip_factor, grad_sample)
  
          if p.summed_grad is not None:
              p.summed_grad += grad
          else:
              p.summed_grad = grad
  
          _mark_as_processed(p.grad_sample)
  
imports: from __future__ import annotations
  import logging
  from typing import Callable, List, Optional, Union
  import torch
  from opacus.optimizers.utils import params
  from opt_einsum.contract import contract
  from torch import nn
  from torch.optim import Optimizer
label_desc: Use image node_id 9 for calling the DPOptimizer obj's underlying member method code with example usage: obj.clip_and_accumulate() without return types