Skip to content

Commit cc804d7

Browse files
committed
add dp with options
1 parent 69ee808 commit cc804d7

File tree

8 files changed

+649
-19
lines changed

8 files changed

+649
-19
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from .dp_mechanisms import DPMechanism, DPAccountant
2+
from .server_dp import Server_DP
3+
from .trainer_dp import Trainer_General_DP
4+
5+
__version__ = "1.0.0"
6+
__author__ = "FedGraph Team"
7+
8+
__all__ = [
9+
"DPMechanism",
10+
"DPAccountant",
11+
"Server_DP",
12+
"Trainer_General_DP",
13+
]
14+
15+
# Module-level configuration
16+
DEFAULT_DP_CONFIG = {
17+
"epsilon": 1.0,
18+
"delta": 1e-5,
19+
"mechanism": "gaussian",
20+
"sensitivity": 1.0,
21+
"clip_norm": 1.0,
22+
}
23+
24+
def get_default_config():
25+
"""Get default DP configuration."""
26+
return DEFAULT_DP_CONFIG.copy()
27+
28+
def validate_dp_config(config):
29+
"""Validate DP configuration parameters."""
30+
required_keys = ["epsilon", "delta", "mechanism"]
31+
for key in required_keys:
32+
if key not in config:
33+
raise ValueError(f"Missing required DP parameter: {key}")
34+
35+
if config["epsilon"] <= 0:
36+
raise ValueError("epsilon must be positive")
37+
if config["delta"] <= 0 or config["delta"] >= 1:
38+
raise ValueError("delta must be in (0, 1)")
39+
40+
valid_mechanisms = ["gaussian", "laplace", "local"]
41+
if config["mechanism"] not in valid_mechanisms:
42+
raise ValueError(f"mechanism must be one of {valid_mechanisms}")
43+
44+
return True
45+
46+
print(f"FedGraph Differential Privacy module loaded (v{__version__})")
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import torch
2+
import numpy as np
3+
import random
4+
import time
5+
from typing import Dict, List, Tuple, Optional, Any
6+
7+
class DPMechanism:
8+
"""
9+
Differential Privacy mechanisms for federated learning.
10+
11+
Supports multiple DP mechanisms:
12+
- Gaussian mechanism
13+
- Laplace mechanism
14+
- Local DP with randomized response
15+
"""
16+
17+
def __init__(self, epsilon: float = 1.0, delta: float = 1e-5,
18+
sensitivity: float = 1.0, mechanism: str = "gaussian"):
19+
"""
20+
Initialize DP mechanism.
21+
22+
Parameters
23+
----------
24+
epsilon : float
25+
Privacy budget (smaller = more private)
26+
delta : float
27+
Failure probability for (ε,δ)-DP
28+
sensitivity : float
29+
L2 sensitivity of the function
30+
mechanism : str
31+
DP mechanism ("gaussian", "laplace", "local")
32+
"""
33+
self.epsilon = epsilon
34+
self.delta = delta
35+
self.sensitivity = sensitivity
36+
self.mechanism = mechanism
37+
38+
# Calculate noise parameters
39+
if mechanism == "gaussian":
40+
# For (ε,δ)-DP: σ ≥ sqrt(2ln(1.25/δ)) * Δ / ε
41+
self.sigma = np.sqrt(2 * np.log(1.25 / delta)) * sensitivity / epsilon
42+
elif mechanism == "laplace":
43+
# For ε-DP: b = Δ / ε
44+
self.scale = sensitivity / epsilon
45+
elif mechanism == "local":
46+
# For local DP
47+
self.p = np.exp(epsilon) / (np.exp(epsilon) + 1)
48+
49+
print(f"Initialized {mechanism} DP mechanism:")
50+
print(f" ε={epsilon}, δ={delta}, sensitivity={sensitivity}")
51+
if mechanism == "gaussian":
52+
print(f" Gaussian noise σ={self.sigma:.4f}")
53+
elif mechanism == "laplace":
54+
print(f" Laplace scale={self.scale:.4f}")
55+
56+
def add_noise(self, tensor: torch.Tensor) -> torch.Tensor:
57+
"""
58+
Add differential privacy noise to tensor.
59+
60+
Parameters
61+
----------
62+
tensor : torch.Tensor
63+
Input tensor to add noise to
64+
65+
Returns
66+
-------
67+
torch.Tensor
68+
Tensor with DP noise added
69+
"""
70+
if self.mechanism == "gaussian":
71+
noise = torch.normal(0, self.sigma, size=tensor.shape, device=tensor.device)
72+
return tensor + noise
73+
74+
elif self.mechanism == "laplace":
75+
# Laplace noise using exponential distribution
76+
uniform = torch.rand(tensor.shape, device=tensor.device)
77+
sign = torch.sign(uniform - 0.5)
78+
noise = -sign * self.scale * torch.log(1 - 2 * torch.abs(uniform - 0.5))
79+
return tensor + noise
80+
81+
elif self.mechanism == "local":
82+
# Local DP with randomized response
83+
prob_matrix = torch.rand(tensor.shape, device=tensor.device)
84+
mask = prob_matrix < self.p
85+
# Flip with probability (1-p)
86+
noisy_tensor = tensor.clone()
87+
noisy_tensor[~mask] = -noisy_tensor[~mask] # Simple bit flip for demonstration
88+
return noisy_tensor
89+
90+
else:
91+
raise ValueError(f"Unknown mechanism: {self.mechanism}")
92+
93+
def clip_gradients(self, tensor: torch.Tensor, max_norm: float) -> torch.Tensor:
94+
"""
95+
Clip tensor to bound sensitivity.
96+
97+
Parameters
98+
----------
99+
tensor : torch.Tensor
100+
Input tensor to clip
101+
max_norm : float
102+
Maximum L2 norm
103+
104+
Returns
105+
-------
106+
torch.Tensor
107+
Clipped tensor
108+
"""
109+
current_norm = torch.norm(tensor)
110+
if current_norm > max_norm:
111+
return tensor * (max_norm / current_norm)
112+
return tensor
113+
114+
def get_privacy_spent(self) -> Tuple[float, float]:
115+
"""Get privacy budget spent."""
116+
return self.epsilon, self.delta
117+
118+
119+
class DPAccountant:
120+
"""
121+
Privacy accountant for tracking cumulative privacy loss.
122+
"""
123+
124+
def __init__(self):
125+
self.total_epsilon = 0.0
126+
self.total_delta = 0.0
127+
self.rounds = 0
128+
129+
def add_step(self, epsilon: float, delta: float):
130+
"""Add privacy cost of one step."""
131+
# Simple composition (can be improved with advanced composition)
132+
self.total_epsilon += epsilon
133+
self.total_delta += delta
134+
self.rounds += 1
135+
136+
def get_total_privacy_spent(self) -> Tuple[float, float]:
137+
"""Get total privacy spent."""
138+
return self.total_epsilon, self.total_delta
139+
140+
def print_privacy_budget(self):
141+
"""Print current privacy budget."""
142+
print(f"Privacy Budget Used: ε={self.total_epsilon:.4f}, δ={self.total_delta:.8f}")
143+
print(f"Rounds completed: {self.rounds}")
144+
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import torch
2+
import time
3+
from typing import Dict, List, Tuple, Optional, Any
4+
5+
from ..server_class import Server
6+
from .dp_mechanisms import DPMechanism, DPAccountant
7+
8+
9+
class Server_DP(Server):
10+
"""
11+
Enhanced server class with Differential Privacy support for FedGCN.
12+
Extends the original Server class to support DP in pre-training aggregation.
13+
"""
14+
15+
def __init__(self, feature_dim: int, args_hidden: int, class_num: int,
16+
device: torch.device, trainers: list, args: Any):
17+
super().__init__(feature_dim, args_hidden, class_num, device, trainers, args)
18+
19+
# DP configuration
20+
self.use_dp = getattr(args, 'use_dp', False)
21+
22+
if self.use_dp:
23+
self.dp_epsilon = getattr(args, 'dp_epsilon', 1.0)
24+
self.dp_delta = getattr(args, 'dp_delta', 1e-5)
25+
self.dp_sensitivity = getattr(args, 'dp_sensitivity', 1.0)
26+
self.dp_mechanism = getattr(args, 'dp_mechanism', 'gaussian')
27+
self.dp_clip_norm = getattr(args, 'dp_clip_norm', 1.0)
28+
29+
# Initialize DP mechanism
30+
self.dp_mechanism_obj = DPMechanism(
31+
epsilon=self.dp_epsilon,
32+
delta=self.dp_delta,
33+
sensitivity=self.dp_sensitivity,
34+
mechanism=self.dp_mechanism
35+
)
36+
37+
# Privacy accountant
38+
self.privacy_accountant = DPAccountant()
39+
40+
print(f"Server initialized with Differential Privacy:")
41+
print(f" Mechanism: {self.dp_mechanism}")
42+
print(f" Privacy parameters: ε={self.dp_epsilon}, δ={self.dp_delta}")
43+
print(f" Sensitivity: {self.dp_sensitivity}")
44+
print(f" Clipping norm: {self.dp_clip_norm}")
45+
46+
def aggregate_dp_feature_sums(self, local_feature_sums: List[torch.Tensor]) -> Tuple[torch.Tensor, Dict]:
47+
"""
48+
Aggregate feature sums with differential privacy.
49+
50+
Parameters
51+
----------
52+
local_feature_sums : List[torch.Tensor]
53+
List of local feature sums from trainers
54+
55+
Returns
56+
-------
57+
Tuple[torch.Tensor, Dict]
58+
Aggregated feature sum with DP noise and statistics
59+
"""
60+
aggregation_start = time.time()
61+
62+
# Step 1: Clip individual contributions
63+
clipped_sums = []
64+
clipping_stats = []
65+
66+
for i, local_sum in enumerate(local_feature_sums):
67+
original_norm = torch.norm(local_sum).item()
68+
clipped_sum = self.dp_mechanism_obj.clip_gradients(local_sum, self.dp_clip_norm)
69+
clipped_norm = torch.norm(clipped_sum).item()
70+
71+
clipped_sums.append(clipped_sum)
72+
clipping_stats.append({
73+
'trainer_id': i,
74+
'original_norm': original_norm,
75+
'clipped_norm': clipped_norm,
76+
'was_clipped': original_norm > self.dp_clip_norm
77+
})
78+
79+
# Step 2: Aggregate clipped sums
80+
aggregated_sum = torch.stack(clipped_sums).sum(dim=0)
81+
82+
# Step 3: Add DP noise
83+
noisy_aggregated_sum = self.dp_mechanism_obj.add_noise(aggregated_sum)
84+
85+
aggregation_time = time.time() - aggregation_start
86+
87+
# Step 4: Update privacy accountant
88+
self.privacy_accountant.add_step(self.dp_epsilon, self.dp_delta)
89+
90+
# Statistics
91+
dp_stats = {
92+
'aggregation_time': aggregation_time,
93+
'clipping_stats': clipping_stats,
94+
'num_clipped': sum(1 for stat in clipping_stats if stat['was_clipped']),
95+
'pre_noise_norm': torch.norm(aggregated_sum).item(),
96+
'post_noise_norm': torch.norm(noisy_aggregated_sum).item(),
97+
'noise_magnitude': torch.norm(noisy_aggregated_sum - aggregated_sum).item(),
98+
'privacy_spent': self.privacy_accountant.get_total_privacy_spent()
99+
}
100+
101+
return noisy_aggregated_sum, dp_stats
102+
103+
def print_dp_stats(self, dp_stats: Dict):
104+
"""Print differential privacy statistics."""
105+
print("\n=== Differential Privacy Statistics ===")
106+
print(f"Aggregation time: {dp_stats['aggregation_time']:.4f}s")
107+
print(f"Trainers clipped: {dp_stats['num_clipped']}/{len(dp_stats['clipping_stats'])}")
108+
print(f"Pre-noise norm: {dp_stats['pre_noise_norm']:.4f}")
109+
print(f"Post-noise norm: {dp_stats['post_noise_norm']:.4f}")
110+
print(f"Noise magnitude: {dp_stats['noise_magnitude']:.4f}")
111+
112+
total_eps, total_delta = dp_stats['privacy_spent']
113+
print(f"Total privacy spent: ε={total_eps:.4f}, δ={total_delta:.8f}")
114+
115+
# Per-trainer clipping details
116+
clipped_trainers = [stat for stat in dp_stats['clipping_stats'] if stat['was_clipped']]
117+
if clipped_trainers:
118+
print("Clipped trainers:")
119+
for stat in clipped_trainers:
120+
print(f" Trainer {stat['trainer_id']}: {stat['original_norm']:.4f} -> {stat['clipped_norm']:.4f}")
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import torch
2+
import time
3+
from typing import Dict, List, Tuple, Optional, Any
4+
5+
from ..trainer_class import Trainer_General
6+
from ..utils_nc import get_1hop_feature_sum
7+
8+
class Trainer_General_DP(Trainer_General):
9+
"""
10+
Enhanced trainer class with Differential Privacy support.
11+
"""
12+
13+
def __init__(self, *args, **kwargs):
14+
super().__init__(*args, **kwargs)
15+
self.use_dp = getattr(self.args, 'use_dp', False)
16+
17+
if self.use_dp:
18+
print(f"Trainer {self.rank} initialized with DP support")
19+
20+
def get_dp_local_feature_sum(self) -> Tuple[torch.Tensor, Dict]:
21+
"""
22+
Get local feature sum with optional client-side DP preprocessing.
23+
24+
Returns
25+
-------
26+
Tuple[torch.Tensor, Dict]
27+
Local feature sum and computation statistics
28+
"""
29+
computation_start = time.time()
30+
31+
# Compute feature sum (same as original)
32+
new_feature_for_trainer = torch.zeros(
33+
self.global_node_num, self.features.shape[1]
34+
).to(self.device)
35+
new_feature_for_trainer[self.local_node_index] = self.features
36+
37+
one_hop_neighbor_feature_sum = get_1hop_feature_sum(
38+
new_feature_for_trainer, self.adj, self.device
39+
)
40+
41+
computation_time = time.time() - computation_start
42+
43+
# Compute statistics for DP
44+
feature_sum_norm = torch.norm(one_hop_neighbor_feature_sum).item()
45+
data_size = one_hop_neighbor_feature_sum.element_size() * one_hop_neighbor_feature_sum.nelement()
46+
47+
stats = {
48+
'trainer_id': self.rank,
49+
'computation_time': computation_time,
50+
'feature_sum_norm': feature_sum_norm,
51+
'data_size': data_size,
52+
'shape': one_hop_neighbor_feature_sum.shape
53+
}
54+
55+
print(f"Trainer {self.rank} - DP feature sum computed:")
56+
print(f" Norm: {feature_sum_norm:.4f}")
57+
print(f" Shape: {one_hop_neighbor_feature_sum.shape}")
58+
print(f" Computation time: {computation_time:.4f}s")
59+
60+
return one_hop_neighbor_feature_sum, stats
61+

0 commit comments

Comments
 (0)