Skip to content

Commit 0819a56

Browse files
committed
feat(strategies): add AirMetapFL (v1.11.42)
1 parent 50a1bf2 commit 0819a56

4 files changed

Lines changed: 299 additions & 3 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ Get started with FedProC in just a few steps:
3232

3333
1. [Installation](docs/installation.md) - Set up your environment
3434
2. [Usage](docs/usage.md) - Run your first experiment
35-
3. [Strategies](docs/strategies.md) - Choose your federated learning strategy (53 strategies)
35+
3. [Strategies](docs/strategies.md) - Choose your federated learning strategy (54 strategies)
3636

3737
## Documentation Structure
3838

3939
- **[Installation](docs/installation.md)** - Installation guide and requirements
4040
- **[Usage](docs/usage.md)** - Basic usage and examples
41-
- **[Strategies](docs/strategies.md)** - Available federated learning strategies (53 strategies)
41+
- **[Strategies](docs/strategies.md)** - Available federated learning strategies (54 strategies)
4242
- **[Datasets](docs/datasets.md)** - Supported datasets and data preparation (31 datasets)
4343
- **[Models](docs/models.md)** - Available models and architectures (77 models)
4444
- **[Augmentations](docs/augs.md)** - Custom GPU-native PyTorch augmentations (8 augmentations)

docs/strategies.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
| pFedMe | NeurIPS | 2020 | Personalized Federated Learning with Moreau Envelopes | [Arxiv](https://arxiv.org/abs/2006.08848) - [REF](https://github.com/TsingZ0/PFLlib/blob/master/system/flcore/servers/serverpFedMe.py) |
4444
| APFL | arXiv | 2020 | Adaptive Personalized Federated Learning | [Arxiv](https://arxiv.org/abs/2003.13461) - [REF](https://github.com/TsingZ0/PFLlib/blob/master/system/flcore/servers/serverapfl.py) |
4545
| PerAvg | NeurIPS | 2020 | Personalized Federated Learning with Theoretical Guarantees: A Model-Agnostic Meta-Learning Approach | [Arxiv](https://arxiv.org/abs/2002.07948) - [REF](https://github.com/TsingZ0/PFLlib/blob/master/system/flcore/servers/serverperavg.py) |
46+
| AirMetapFL | arXiv | 2025 | Pre-Training and Personalized Fine-Tuning via Over-the-Air Federated Meta-Learning: Convergence-Generalization Trade-Offs | [Arxiv](https://arxiv.org/abs/2406.11569) |
4647
| FedAMP | AAAI | 2021 | Personalized Cross-Silo Federated Learning on Non-IID Data | [Arxiv](https://arxiv.org/abs/2007.03797) - [REF](https://github.com/TsingZ0/PFLlib/blob/master/system/flcore/servers/serveramp.py) |
4748
| FedBN | ICLR | 2021 | FedBN: Federated Learning on Non-IID Features via Local Batch Normalization | [Arxiv](https://arxiv.org/abs/2102.07623) - [REF](https://github.com/TsingZ0/PFLlib/blob/master/system/flcore/servers/serverbn.py) |
4849
| FML | NeurIPS | 2020 | Federated Mutual Learning | [Arxiv](https://arxiv.org/abs/2006.16765) - [REF](https://github.com/TsingZ0/PFLlib/blob/master/system/flcore/clients/clientfml.py) |

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "fedproc"
7-
version = "1.11.41"
7+
version = "1.11.42"
88
description = "A Benchmarking Framework for Federated Time-Series Forecasting"
99
readme = "README.md"
1010
requires-python = ">=3.10"

strategies/AirMetapFL.py

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
import copy
2+
import time
3+
from argparse import Namespace
4+
from typing import Any, Dict, List, Optional
5+
6+
import torch
7+
8+
from .pFL import pFL, pFL_Client
9+
10+
11+
class AirMetapFL(pFL):
12+
"""
13+
Air-meta-pFL: Over-the-Air Meta-Learning Based Personalized Federated Learning.
14+
15+
Implements Algorithm 1 from Wen et al. (2024), arXiv 2406.11569, adapted for
16+
digital channels (OTA wireless channel model omitted). Each client runs Q outer
17+
MAML gradient steps and applies top-k sparsification with error-feedback memory
18+
before sending model differences. The server performs uniform averaging.
19+
20+
Two MAML variants via --hf:
21+
- FO (default): first-order — 2 mini-batches per outer step.
22+
- HF (--hf): Hessian-free correction via finite differences — 3 mini-batches.
23+
g_hf = g - (α/2δ)·(∇f(θ+δg) - ∇f(θ-δg))
24+
25+
Reference: "Pre-Training and Personalized Fine-Tuning via Over-the-Air Federated
26+
Meta-Learning: Convergence-Generalization Trade-Offs", arXiv 2406.11569.
27+
"""
28+
29+
optional = {
30+
"alpha": 0.01,
31+
"sparsity": 1.0,
32+
"hf": False,
33+
"delta": 1e-3,
34+
}
35+
compulsory = {"return_diff": True}
36+
37+
@classmethod
38+
def args_update(cls, parser):
39+
parser.add_argument(
40+
"--alpha",
41+
type=float,
42+
default=None,
43+
help="Inner adaptation learning rate α (default: 0.01)",
44+
)
45+
parser.add_argument(
46+
"--sparsity",
47+
type=float,
48+
default=None,
49+
help="Fraction of entries to keep via top-k (1.0 = no sparsification)",
50+
)
51+
parser.add_argument(
52+
"--hf",
53+
action="store_true",
54+
default=None,
55+
help="Hessian-free second-order MAML correction (3 mini-batches/step)",
56+
)
57+
parser.add_argument(
58+
"--delta",
59+
type=float,
60+
default=None,
61+
help="Finite-difference perturbation for HF Hessian approximation",
62+
)
63+
return parser
64+
65+
def __init__(self, configs: Namespace, times: int) -> None:
66+
super().__init__(configs=configs, times=times)
67+
self.parallel = False
68+
69+
def calculate_aggregation_weights(self) -> None:
70+
pass # uniform 1/n weighting applied directly in aggregate_models
71+
72+
def aggregate_models(self) -> None:
73+
n = len(self.client_data)
74+
if n == 0:
75+
return
76+
for cd in self.client_data:
77+
for global_param, local_param in zip(
78+
self.model.parameters(), cd["model"].parameters()
79+
):
80+
global_param.data.sub_(
81+
local_param.data.to(global_param.device), alpha=1.0 / n
82+
)
83+
84+
85+
class AirMetapFL_Client(pFL_Client):
86+
"""
87+
Client for Air-meta-pFL.
88+
89+
Runs MAML outer gradient steps (FO or HF) and applies top-k sparsification
90+
with error-feedback memory before transmitting the model difference Δ_i.
91+
The memory vector m_i persists across rounds to accumulate sparsification error.
92+
"""
93+
94+
_memory: Optional[List[torch.Tensor]] = None
95+
96+
def __init__(self, configs: Namespace, id: int, times: int) -> None:
97+
super().__init__(configs=configs, id=id, times=times)
98+
if getattr(self, "hf", False):
99+
self._model_plus = copy.deepcopy(self.model)
100+
self._model_minus = copy.deepcopy(self.model)
101+
102+
@staticmethod
103+
def _top_k_sparsify(tensor: torch.Tensor, k_ratio: float) -> torch.Tensor:
104+
"""Keep top-k absolute-value entries, zero the rest."""
105+
flat = tensor.flatten()
106+
k = max(1, int(k_ratio * flat.numel()))
107+
_, idx = torch.topk(flat.abs(), k)
108+
mask = torch.zeros_like(flat)
109+
mask[idx] = 1.0
110+
return (flat * mask).reshape(tensor.shape)
111+
112+
def _next_batch(self, iterator, loader):
113+
try:
114+
return next(iterator)
115+
except StopIteration:
116+
return next(iter(loader))
117+
118+
def train(self) -> Optional[Dict[str, Any]]:
119+
train_loader = self.load_train_data()
120+
start_time = time.time()
121+
self.model.to(self.device)
122+
self.model.train()
123+
124+
if getattr(self, "hf", False):
125+
self._train_hf(train_loader)
126+
else:
127+
self._train_fo(train_loader)
128+
129+
self.scheduler.step()
130+
if self.efficiency != "high":
131+
self.model.to("cpu")
132+
self.metrics["train_time"].append(time.time() - start_time)
133+
return None
134+
135+
def _train_fo(self, train_loader) -> None:
136+
"""First-order MAML: 2 mini-batches per outer step."""
137+
alpha = getattr(self, "alpha", 0.01)
138+
lr = self.learning_rate
139+
140+
for _ in range(self.epochs):
141+
for batch_x, batch_y, x_mark, y_mark in train_loader:
142+
batch_x = batch_x.to(device=self.device, dtype=torch.float32)
143+
batch_y = batch_y.to(device=self.device, dtype=torch.float32)
144+
x_mark = x_mark.to(device=self.device, dtype=torch.float32)
145+
y_mark = y_mark.to(device=self.device, dtype=torch.float32)
146+
147+
half = batch_x.size(0) // 2 or batch_x.size(0)
148+
149+
# Save θ
150+
temp_params = [p.data.clone() for p in self.model.parameters()]
151+
152+
# Inner step: θ' = θ - α∇f(θ; D^tr)
153+
self.optimizer.zero_grad()
154+
out = self.model(
155+
batch_x[:half], x_mark=x_mark[:half], y_mark=y_mark[:half]
156+
)
157+
self.loss(out, batch_y[:half]).backward()
158+
with torch.no_grad():
159+
for p in self.model.parameters():
160+
if p.grad is not None:
161+
p.data.sub_(alpha * p.grad)
162+
163+
# Meta-gradient at θ' on query set: ∇f(θ'; D^va)
164+
self.optimizer.zero_grad()
165+
x_q = batch_x[half:] if half < batch_x.size(0) else batch_x
166+
y_q = batch_y[half:] if half < batch_x.size(0) else batch_y
167+
xm_q = x_mark[half:] if half < batch_x.size(0) else x_mark
168+
ym_q = y_mark[half:] if half < batch_x.size(0) else y_mark
169+
out2 = self.model(x_q, x_mark=xm_q, y_mark=ym_q)
170+
self.loss(out2, y_q).backward()
171+
172+
# Restore θ, apply outer step η
173+
with torch.no_grad():
174+
for p, tp in zip(self.model.parameters(), temp_params):
175+
p.data.copy_(tp)
176+
if p.grad is not None:
177+
p.data.sub_(lr * p.grad)
178+
179+
def _train_hf(self, train_loader) -> None:
180+
"""Hessian-free MAML: 3 mini-batches per outer step."""
181+
alpha = getattr(self, "alpha", 0.01)
182+
delta = getattr(self, "delta", 1e-3)
183+
lr = self.learning_rate
184+
185+
self._model_plus.to(self.device)
186+
self._model_minus.to(self.device)
187+
self._model_plus.train()
188+
self._model_minus.train()
189+
190+
steps_per_epoch = max(1, len(train_loader) // 3)
191+
192+
for _ in range(self.epochs):
193+
iterator = iter(train_loader)
194+
for _ in range(steps_per_epoch):
195+
# Batch 0: support set → inner step
196+
bx0, by0, bxm0, bym0 = self._next_batch(iterator, train_loader)
197+
bx0 = bx0.to(device=self.device, dtype=torch.float32)
198+
by0 = by0.to(device=self.device, dtype=torch.float32)
199+
bxm0 = bxm0.to(device=self.device, dtype=torch.float32)
200+
bym0 = bym0.to(device=self.device, dtype=torch.float32)
201+
202+
frz_params = [p.data.clone() for p in self.model.parameters()]
203+
self.optimizer.zero_grad()
204+
self.loss(self.model(bx0, x_mark=bxm0, y_mark=bym0), by0).backward()
205+
with torch.no_grad():
206+
for p in self.model.parameters():
207+
if p.grad is not None:
208+
p.data.sub_(alpha * p.grad)
209+
210+
# Batch 1: query set → meta-gradient g = ∇f(θ'; D^va)
211+
bx1, by1, bxm1, bym1 = self._next_batch(iterator, train_loader)
212+
bx1 = bx1.to(device=self.device, dtype=torch.float32)
213+
by1 = by1.to(device=self.device, dtype=torch.float32)
214+
bxm1 = bxm1.to(device=self.device, dtype=torch.float32)
215+
bym1 = bym1.to(device=self.device, dtype=torch.float32)
216+
217+
self.optimizer.zero_grad()
218+
self.loss(self.model(bx1, x_mark=bxm1, y_mark=bym1), by1).backward()
219+
meta_grads = [
220+
p.grad.clone() if p.grad is not None else torch.zeros_like(p)
221+
for p in self.model.parameters()
222+
]
223+
224+
# Batch 2: Hessian finite-difference at θ (frz_params)
225+
bx2, by2, bxm2, bym2 = self._next_batch(iterator, train_loader)
226+
bx2 = bx2.to(device=self.device, dtype=torch.float32)
227+
by2 = by2.to(device=self.device, dtype=torch.float32)
228+
bxm2 = bxm2.to(device=self.device, dtype=torch.float32)
229+
bym2 = bym2.to(device=self.device, dtype=torch.float32)
230+
231+
with torch.no_grad():
232+
for pp, pm, g, fp in zip(
233+
self._model_plus.parameters(),
234+
self._model_minus.parameters(),
235+
meta_grads,
236+
frz_params,
237+
):
238+
pp.data.copy_(fp + delta * g)
239+
pm.data.copy_(fp - delta * g)
240+
241+
self._model_plus.zero_grad()
242+
self._model_minus.zero_grad()
243+
self.loss(
244+
self._model_plus(bx2, x_mark=bxm2, y_mark=bym2), by2
245+
).backward()
246+
self.loss(
247+
self._model_minus(bx2, x_mark=bxm2, y_mark=bym2), by2
248+
).backward()
249+
250+
# g_hf = g - (α/2δ)·(∇f(θ+δg) - ∇f(θ-δg))
251+
hf_coef = alpha / (2.0 * delta)
252+
hf_grads = [
253+
g
254+
- hf_coef
255+
* (
256+
(pp.grad if pp.grad is not None else torch.zeros_like(g))
257+
- (pm.grad if pm.grad is not None else torch.zeros_like(g))
258+
)
259+
for g, pp, pm in zip(
260+
meta_grads,
261+
self._model_plus.parameters(),
262+
self._model_minus.parameters(),
263+
)
264+
]
265+
266+
# Restore θ, apply outer step η with HF gradient
267+
with torch.no_grad():
268+
for p, fp, g_hf in zip(
269+
self.model.parameters(), frz_params, hf_grads
270+
):
271+
p.data.copy_(fp - lr * g_hf)
272+
273+
self._model_plus.to("cpu")
274+
self._model_minus.to("cpu")
275+
276+
def variables_to_be_sent(self) -> Dict[str, Any]:
277+
data = super().variables_to_be_sent()
278+
279+
sparsity = getattr(self, "sparsity", 1.0)
280+
if sparsity < 1.0:
281+
if self._memory is None:
282+
self._memory = [
283+
torch.zeros_like(p.data.cpu()) for p in data["model"].parameters()
284+
]
285+
model = data["model"]
286+
with torch.no_grad():
287+
new_mem = []
288+
for mem, param in zip(self._memory, model.parameters()):
289+
combined = mem.to(param.device) + param.data
290+
sparsified = self._top_k_sparsify(combined, sparsity)
291+
new_mem.append((combined - sparsified).cpu())
292+
param.data.copy_(sparsified)
293+
self._memory = new_mem
294+
295+
return data

0 commit comments

Comments
 (0)