Agent Conversation: openai/gpt-5.4-pro
Task: optimization-dp-sgd
Differentially Private SGD: Privacy-Utility Optimization
Research Question
Design an improved DP-SGD variant that achieves better privacy-utility tradeoff — higher test accuracy under the same (epsilon, delta)-differential privacy budget.
Background
Differentially Private Stochastic Gradient Descent (DP-SGD) [Abadi et al., 2016] enables training deep learning models with formal privacy guarantees. The core mechanism has two steps: (1) clip each per-sample gradient to a fixed norm C, and (2) add calibrated Gaussian noise proportional to C. The noise level is determined by the desired privacy budget (epsilon, delta).
The standard approach uses a fixed clipping threshold and constant noise throughout training, which is suboptimal: gradient magnitudes change during training, the fixed threshold either over-clips (losing signal) or under-clips (adding excess noise), and the uniform noise allocation ignores the varying informativeness of gradients across training stages.
Task
Modify the DPMechanism class in custom_dpsgd.py. Your mechanism receives per-sample gradients and must return aggregated noised gradients. You control the gradient clipping strategy, noise calibration, and any per-step adaptations.
Interface
class DPMechanism:
def __init__(self, max_grad_norm, noise_multiplier, n_params,
dataset_size, batch_size, epochs, target_epsilon, target_delta):
...
def clip_and_noise(self, per_sample_grads, step, epoch) -> list[Tensor]:
# per_sample_grads: list of tensors [B, *param_shape]
# Returns: list of noised gradients [*param_shape]
...
def get_effective_sigma(self, step, epoch) -> float:
# Returns current noise multiplier for privacy accounting
...
Constraints
- The total privacy budget (target_epsilon, target_delta) is FIXED and checked externally.
- The model architecture, data pipeline, optimizer, and training loop are FIXED.
- Focus on algorithmic innovation in the DP mechanism: clipping strategies, noise schedules, gradient processing.
- Available imports:
torch,math,numpy(via the FIXED section),scipy.optimize.
Evaluation
Trained and evaluated on three datasets at epsilon=3.0, delta=1e-5:
- MNIST (28x28 grayscale digits, 10 classes)
- Fashion-MNIST (28x28 grayscale clothing, 10 classes)
- CIFAR-10 (32x32 color images, 10 classes)
Metric: test accuracy (higher is better) under the same privacy budget.
Hints
- Adaptive clipping: adjust the clipping threshold based on observed gradient norms (e.g., track quantiles).
- Noise scheduling: allocate more noise to early training (large gradients) and less to later training (refined gradients).
- Gradient normalization: normalize per-sample gradients instead of clipping to a fixed threshold.
- Per-layer strategies: clip/noise at the layer level rather than globally.
- Momentum-based variance reduction: use gradient moment
Tool Input
1{
2 "op": "replace",
3 "filename": "opacus/custom_dpsgd.py",
4 "content": "class DPMechanism:\n \"\"\"Selective layerwise clipping with cosine privacy-budget reallocation.\n\n Two changes over standard DP-SGD:\n 1) Per-sample *water-filling* clipping across parameter tensors: for each\n sample, only layers with unusually large norms are clipped, while small\n layers are kept intact. This preserves weak but useful gradients that\n global clipping would unnecessarily shrink.\n 2) A data-independent cosine noise schedule with exact harmonic-mean\n accounting, so later optimization steps receive slightly less noise\n without exceeding the same total privacy budget.\n \"\"\"\n\n def __init__(self, max_grad_norm, noise_multiplier, n_params,\n dataset_size, batch_size, epochs, target_epsilon, target_delta):\n self.max_grad_norm = max_grad_norm\n self.noise_multiplier = noise_multiplier\n self.n_params = n_params\n self.dataset_size = dataset_size\n self.batch_size = batch_size\n self.epochs = epochs\n self.target_epsilon = target_epsilon\n self.target_delta = target_delta\n\n self.total_steps = max(1, (dataset_size // batch_size) * epochs)\n self._bisection_steps = 14\n\n # Smoothly shift privacy budget toward later, more refined updates.\n amp = 0.25\n idx = np.arange(self.total_steps, dtype=np.float64)\n progress = (idx + 0.5) / self.total_steps\n rel_sigma = 1.0 + amp * np.cos(np.pi * progress) # early > 1, late < 1\n scale = math.sqrt(np.mean(1.0 / (rel_sigma ** 2)))\n self.sigma_schedule = [float(noise_multiplier * scale * r) for r in rel_sigma]\n\n self.prefix_inv_sq = [0.0]\n acc = 0.0\n for sigma in self.sigma_schedule:\n acc += 1.0 / (sigma * sigma)\n self.prefix_inv_sq.append(acc)\n\n def _waterfill_scales(self, layer_norms):\n \"\"\"Return per-sample, per-layer scales with total norm <= max_grad_norm.\n\n For each sample i, choose a cap tau_i so clipped layer norms become\n min(||g_ij||, tau_i) and the concatenated clipped gradient has norm C.\n This clips only the dominant layers instead of shrinking all layers.\n \"\"\"\n scales = torch.ones_like(layer_norms)\n target_sq = self.max_grad_norm * self.max_grad_norm\n total_sq = (layer_norms * layer_norms).sum(dim=1)\n active = total_sq > target_sq\n if not active.any():\n return scales\n\n ln = layer_norms[active]\n low = torch.zeros(ln.size(0), device=ln.device, dtype=ln.dtype)\n high = ln.max(dim=1).values\n for _ in range(self._bisection_steps):\n mid = 0.5 * (low + high)\n capped_sq = torch.minimum(ln, mid.unsqueeze(1)).pow(2).sum(dim=1)\n too_big = capped_sq > target_sq\n high = torch.where(too_big, mid, high)\n low = torch.where(too_big, low, mid)\n\n tau = 0.5 * (low + high)\n scales[active] = torch.minimum(\n torch.ones_like(ln), tau.unsqueeze(1) / ln.clamp(min=1e-12)\n )\n return scales\n\n def clip_and_noise(self, per_sample_grads, step, epoch):\n batch_size = per_sample_grads[0].shape[0]\n layer_norms = torch.stack(\n [g.reshape(batch_size, -1).norm(2, dim=1) for g in per_sample_grads], dim=1\n )\n scales = self._waterfill_scales(layer_norms)\n\n sigma = self.sigma_schedule[min(step, self.total_steps - 1)]\n noise_std = sigma * self.max_grad_norm / batch_size\n\n noised_grads = []\n for j, g in enumerate(per_sample_grads):\n shape = [batch_size] + [1] * (g.dim() - 1)\n clipped = g * scales[:, j].reshape(shape)\n avg = clipped.mean(dim=0)\n noised_grads.append(avg + torch.randn_like(avg) * noise_std)\n return noised_grads\n\n def get_effective_sigma(self, step, epoch):\n if step <= 0:\n return self.sigma_schedule[0]\n step = min(step, self.total_steps)\n return math.sqrt(step / self.prefix_inv_sq[step])",
5 "after_line": 171,
6 "start_line": 172,
7 "end_line": 229
8}OK: Replaced lines 172..229 with 94 line(s) in opacus/custom_dpsgd.py. Editable range: 152–269. [Current file: opacus/custom_dpsgd.py | editable: 152–269 | total: 581 lines] 149| return (sigma_low + sigma_high) / 2 150| 151| 152 # ===================================================================== 153 # EDITABLE SECTION START (lines 152-233) 154 # ===================================================================== 155 # DPMechanism: Controls how per-sample gradients are clipped and noised. 156 # 157 # Interface contract: 158 # __init__(self, max_grad_norm, noise_multiplier, n_params, dataset_size, 159 # batch_size, epochs, target_epsilon, target_delta) 160 # clip_and_noise(self, per_sample_grads, step, epoch) -> noised_gradient 161 # get_effective_sigma(self, step, epoch) -> float 162 # 163 # The mechanism receives per-sample gradients (list of tensors, each [B, *param_shape]) 164 # and must return
[Test #1] (2 tests remaining; call submit(n=1) to submit this result) ## Seed 42 ### mnist (scripts/mnist.sh) [output file not found: /srv/home/bohanlyu/MLS-Bench/logs/optimization-dp-sgd/openai_gpt-5.4-pro_20260405_233917/20260406_000504/group_1_0/mnist_s42.out] ### fmnist (scripts/fmnist.sh) Training progress (fmnist): TRAIN_METRICS epoch=4 step=902 loss=0.972050 accuracy=79.12 TRAIN_METRICS epoch=5 step=986 loss=0.962547 accuracy=78.87 TRAIN_METRICS epoch=5 step=1036 loss=0.951078 accuracy=79.22 TRAIN_METRICS epoch=5 step=1086 loss=0.940222 accuracy=79.59 TRAIN_METRICS epoch=5 step=1136 loss=0.943287 accuracy=79.69 Epoch summaries (fmnist): Epoch 3/5: train_loss=0.9978 train_acc=78.12% test_loss=1.0113 test_acc=77.61% epsilon_spent=1.95 sigma=0.2782 Epoch 4/5: train_loss=0.9653 train_acc=79.19% test_loss=0.9650 test_acc=78.75% epsilon_spent=2.47 sigma=0.2567 Epoch 5/5: train_loss=0.9448 train_acc=79.66% test_loss=0.9987 test_acc=78.67% epsilon_spent=3.00 sigma=0.2385 accuracy_fm
Tool Input
1{
2 "op": "replace",
3 "filename": "opacus/custom_dpsgd.py",
4 "content": "class DPMechanism:\n \"\"\"Progressive radial clipping with exact privacy-budget reallocation.\n\n We interpolate between two bounded transforms of each per-sample gradient:\n - standard DP clipping, which preserves magnitude for small gradients;\n - AUTO-S-style radial normalization, which boosts weak late-stage gradients.\n\n The AUTO-S component is gradually increased during training, while the\n Gaussian noise follows a mild cosine schedule with exact harmonic-mean\n accounting so the overall privacy budget remains unchanged.\n \"\"\"\n\n def __init__(self, max_grad_norm, noise_multiplier, n_params,\n dataset_size, batch_size, epochs, target_epsilon, target_delta):\n self.max_grad_norm = max_grad_norm\n self.noise_multiplier = noise_multiplier\n self.n_params = n_params\n self.dataset_size = dataset_size\n self.batch_size = batch_size\n self.epochs = epochs\n self.target_epsilon = target_epsilon\n self.target_delta = target_delta\n\n self.total_steps = max(1, (dataset_size // batch_size) * epochs)\n self.gamma = 0.02\n self.mix_start = 0.15\n self.mix_end = 0.80\n\n # Gentle schedules for short runs, stronger reallocation for long runs.\n amp = 0.10 + 0.15 * min(1.0, epochs / 30.0)\n idx = np.arange(self.total_steps, dtype=np.float64)\n progress = (idx + 0.5) / self.total_steps\n rel_sigma = 1.0 + amp * np.cos(np.pi * progress) # early > 1, late < 1\n scale = math.sqrt(np.mean(1.0 / (rel_sigma ** 2)))\n self.sigma_schedule = [float(noise_multiplier * scale * r) for r in rel_sigma]\n\n self.prefix_inv_sq = [0.0]\n acc = 0.0\n for sigma in self.sigma_schedule:\n acc += 1.0 / (sigma * sigma)\n self.prefix_inv_sq.append(acc)\n\n def _mix_weight(self, step):\n if self.total_steps <= 1:\n return self.mix_end\n progress = min(1.0, max(0.0, step / (self.total_steps - 1)))\n smooth = 0.5 - 0.5 * math.cos(math.pi * progress)\n return self.mix_start + (self.mix_end - self.mix_start) * smooth\n\n def clip_and_noise(self, per_sample_grads, step, epoch):\n batch_size = per_sample_grads[0].shape[0]\n flat = torch.cat([g.reshape(batch_size, -1) for g in per_sample_grads], dim=1)\n norms = flat.norm(2, dim=1).clamp(min=1e-12)\n\n c = self.max_grad_norm\n std_scale = (c / norms).clamp(max=1.0)\n auto_scale = c / (norms + self.gamma * c)\n mix = self._mix_weight(step)\n scale = (1.0 - mix) * std_scale + mix * auto_scale\n\n sigma = self.sigma_schedule[min(step, self.total_steps - 1)]\n noise_std = sigma * c / batch_size\n\n noised_grads = []\n for g in per_sample_grads:\n shape = [batch_size] + [1] * (g.dim() - 1)\n transformed = g * scale.reshape(shape)\n avg = transformed.mean(dim=0)\n noised_grads.append(avg + torch.randn_like(avg) * noise_std)\n return noised_grads\n\n def get_effective_sigma(self, step, epoch):\n if step <= 0:\n return self.sigma_schedule[0]\n step = min(step, self.total_steps)\n return math.sqrt(step / self.prefix_inv_sq[step])",
5 "after_line": 171,
6 "start_line": 172,
7 "end_line": 265
8}OK: Replaced lines 172..265 with 76 line(s) in opacus/custom_dpsgd.py. Editable range: 152–251. [Current file: opacus/custom_dpsgd.py | editable: 152–251 | total: 563 lines] 149| return (sigma_low + sigma_high) / 2 150| 151| 152 # ===================================================================== 153 # EDITABLE SECTION START (lines 152-233) 154 # ===================================================================== 155 # DPMechanism: Controls how per-sample gradients are clipped and noised. 156 # 157 # Interface contract: 158 # __init__(self, max_grad_norm, noise_multiplier, n_params, dataset_size, 159 # batch_size, epochs, target_epsilon, target_delta) 160 # clip_and_noise(self, per_sample_grads, step, epoch) -> noised_gradient 161 # get_effective_sigma(self, step, epoch) -> float 162 # 163 # The mechanism receives per-sample gradients (list of tensors, each [B, *param_shape]) 164 # and must return
[Test #2] (1 test remaining; call submit(n=2) to submit this result) ### mnist (scripts/mnist.sh) Calibrated noise_multiplier sigma=0.2385 for epsilon=3.0, delta=1e-05, steps=1170, q=0.0043 Model: MNISTNet, Parameters: 26010 ### fmnist (scripts/fmnist.sh) Training progress (fmnist): TRAIN_METRICS epoch=1 step=50 loss=1.493073 accuracy=46.79 ### cifar10 (scripts/cifar10.sh) Training progress (cifar10): TRAIN_METRICS epoch=29 step=5560 loss=1.514568 accuracy=55.23 TRAIN_METRICS epoch=29 step=5610 loss=1.514746 accuracy=55.24 TRAIN_METRICS epoch=30 step=5705 loss=1.516016 accuracy=55.60 TRAIN_METRICS epoch=30 step=5755 loss=1.522833 accuracy=55.19 TRAIN_METRICS epoch=30 step=5805 loss=1.522484 accuracy=55.21 Epoch summaries (cifar10): Epoch 28/30: train_loss=1.5188 train_acc=55.20% test_loss=1.5479 test_acc=54.55% epsilon_spent=2.82 sigma=0.6548 Epoch 29/30: train_loss=1.5195 train_acc=55.22% test_loss=1.5527 test_acc=54.48% epsilon_spent=2.91 sigma=0.6474 Epoch 30/30: train_loss=1.51
Tool Input
1{
2 "op": "replace",
3 "filename": "opacus/custom_dpsgd.py",
4 "content": " self.total_steps = max(1, (dataset_size // batch_size) * epochs)\n run_frac = min(1.0, epochs / 30.0)\n self.gamma = 0.03 - 0.01 * run_frac\n self.mix_start = 0.05 + 0.10 * run_frac\n self.mix_end = 0.55 + 0.25 * run_frac",
5 "after_line": 194,
6 "start_line": 195,
7 "end_line": 198
8}OK: Replaced lines 195..198 with 5 line(s) in opacus/custom_dpsgd.py. Editable range: 152–252. [Current file: opacus/custom_dpsgd.py | editable: 152–252 | total: 564 lines] 149| return (sigma_low + sigma_high) / 2 150| 151| 152 # ===================================================================== 153 # EDITABLE SECTION START (lines 152-233) 154 # ===================================================================== 155 # DPMechanism: Controls how per-sample gradients are clipped and noised. 156 # 157 # Interface contract: 158 # __init__(self, max_grad_norm, noise_multiplier, n_params, dataset_size, 159 # batch_size, epochs, target_epsilon, target_delta) 160 # clip_and_noise(self, per_sample_grads, step, epoch) -> noised_gradient 161 # get_effective_sigma(self, step, epoch) -> float 162 # 163 # The mechanism receives per-sample gradients (list of tensors, each [B, *param_shape]) 164 # and must return
[Test #3] (0 tests remaining) ## Seed 42 ### mnist (scripts/mnist.sh) Training progress (mnist): TRAIN_METRICS epoch=4 step=902 loss=0.247150 accuracy=95.79 TRAIN_METRICS epoch=5 step=986 loss=0.258026 accuracy=95.69 TRAIN_METRICS epoch=5 step=1036 loss=0.246426 accuracy=95.76 TRAIN_METRICS epoch=5 step=1086 loss=0.250779 accuracy=95.78 TRAIN_METRICS epoch=5 step=1136 loss=0.252588 accuracy=95.80 Epoch summaries (mnist): Epoch 3/5: train_loss=0.2726 train_acc=95.20% test_loss=0.2404 test_acc=95.79% epsilon_spent=2.13 sigma=0.2556 Epoch 4/5: train_loss=0.2573 train_acc=95.71% test_loss=0.2282 test_acc=96.08% epsilon_spent=2.58 sigma=0.2464 Epoch 5/5: train_loss=0.2512 train_acc=95.86% test_loss=0.2257 test_acc=96.15% epsilon_spent=3.00 sigma=0.2385 accuracy_mnist: 96.1500 epsilon_mnist: 3.0016 best_accuracy_mnist: 96.1500 ### fmnist (scripts/fmnist.sh) Training progress (fmnist): TRAIN_METRICS epoch=4 step=902 loss=1.001485 accuracy=80.70 TRAIN_METRICS epoch=5 step=986 loss=1.008733