Skip to content

Commit 5292c35

Browse files
committed
Implemented the FedDF server aggregation algorithm (#411).
* Implemented the FedDF server-aggregation example. Add a new FedDF server-aggregation example that transports proxy-set logits from clients and distills the next global model on the server. - add the FedDF client, server, aggregation strategy, algorithm, and shared proxy helpers - add a runnable MNIST/LeNet-5 example config and documentation mapping the code to the paper - cover the logits transport and server-side distillation paths with targeted tests Validation: - uv run pytest -- tests/clients/test_feddf_strategy.py tests/servers/test_feddf_server_strategy.py - uv run pytest -- tests/clients/test_feddf_strategy.py tests/servers/test_feddf_server_strategy.py tests/clients/test_fednova_strategy.py tests/servers/test_fedavg_strategy.py - uv run ruff check examples/server_aggregation/feddf tests/clients/test_feddf_strategy.py tests/servers/test_feddf_server_strategy.py - git diff --check - uv run python import/config smoke check for the FedDF example * Shared the FedDF proxy payload between server and clients. Address the DT-335 review findings by making the proxy protocol explicit instead of assuming that every client can reconstruct the same proxy subset locally. - send the server-selected proxy inputs together with the global weights in the FedDF server payload - load and reuse the shared proxy payload on the client before emitting logits - update the tests to exercise the real proxy-resolution path and document the repaired protocol Validation: - uv run pytest -- tests/clients/test_feddf_strategy.py tests/servers/test_feddf_server_strategy.py - uv run pytest -- tests/clients/test_feddf_strategy.py tests/servers/test_feddf_server_strategy.py tests/clients/test_fednova_strategy.py tests/servers/test_fedavg_strategy.py - uv run ruff check examples/server_aggregation/feddf tests/clients/test_feddf_strategy.py tests/servers/test_feddf_server_strategy.py - git diff --check - uv run python import/config smoke check for the FedDF example * Reserved deterministic FedDF proxy holdouts. Prefer the datasource unlabeled split for FedDF proxy selection before falling back to the test split. Added deterministic Torchvision subset slicing so CIFAR-10 can reserve a non-overlapping training holdout for proxy distillation while preserving classes and targets metadata. Expanded datasource and FedDF strategy coverage for the new subset semantics and unlabeled-first proxy path. * Aligned FedDF distillation with uniform AVGLOGITS and Adam. Match the FedDF server distillation path more closely to the paper/reference recipe by defaulting teacher fusion to uniform AVGLOGITS, switching the student distillation loop to Adam with optional cosine annealing, and shuffling proxy batches during server training. Keep sample-weighted fusion and SGD available as explicit opt-in settings for experiments that need them. Add a focused server-side regression test to lock the uniform teacher-logit averaging default and keep the existing FedDF distillation test green under the new optimizer/scheduler path. Validation: uv run pytest tests/servers/test_feddf_server_strategy.py tests/clients/test_feddf_strategy.py -q; uv run ruff check examples/server_aggregation/feddf tests/servers/test_feddf_server_strategy.py tests/clients/test_feddf_strategy.py * Clarified MOON documentation taxonomy. Summary: - moved the detailed MOON walkthrough under customized client training loops - left the server-aggregation page with a pointer that explains MOON keeps FedAvg aggregation unchanged - aligned the example docs with the manuscript taxonomy used for the TKDE revision Validation: - documentation-only change; no runtime behavior changed * Clarified MOON taxonomy heading. Summary: - marked the MOON pointer in the server-aggregation docs as a client-training customization - avoided leaving MOON visually classified as a server aggregation rule in the packaged docs Validation: - documentation-only change; no runtime behavior changed * Timed FedDF client and server overhead. Include the proxy-logit generation pass in the client-reported training time and record server distillation time on the FedDF server so logged round_time and elapsed_time reflect FedDF-specific work.\n\nAlso cache the stacked proxy inputs on the server and add focused tests for the FedDF timing path.
1 parent 5781421 commit 5292c35

13 files changed

Lines changed: 1208 additions & 29 deletions

docs/docs/examples/algorithms/1. Server Aggregation Algorithms.md

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -56,40 +56,42 @@ Key configuration parameters:
5656

5757
---
5858

59-
### MOON
59+
### FedDF
6060

61-
MOON (Model-Contrastive Federated Learning) enhances standard FedAvg by adding a model-level contrastive regularizer. Each client augments the shared model with a projection head, clones the incoming global model as a positive anchor, and reuses a small buffer of its historical checkpoints as negatives. The server still performs sample-weighted averaging but records a short history of global states for downstream analysis or warm restarts.
61+
FedDF (Federated Distillation and Fusion) replaces direct parameter averaging with server-side distillation on a proxy set. The server selects a deterministic unlabeled proxy subset, ships those proxy inputs alongside the current global weights, and each client returns teacher logits on that shared payload instead of ordinary weight deltas. The server then aggregates those logits and distills their ensemble into the next global model using temperature-scaled soft targets.
6262

6363
```bash
64-
cd examples/server_aggregation/moon/
65-
uv run moon.py -c moon_MNIST_lenet5.toml
64+
cd examples/server_aggregation/feddf/
65+
uv run feddf.py -c feddf_MNIST_lenet5.toml
6666
```
6767

6868
Key configuration parameters:
6969

70-
- `algorithm.mu`: Weight assigned to the contrastive term (default: 5.0).
71-
- `algorithm.temperature`: Softmax temperature applied to cosine similarities (default: 0.5).
72-
- `algorithm.history_size`: Number of historical local models cached per client as negatives (default: 2).
73-
- `trainer.model_name`: Name used for checkpointing the projection-ready backbone (default: `moon_lenet5`).
70+
- `algorithm.proxy_set_size`: Number of unlabeled proxy samples used on the server for distillation.
71+
- `algorithm.proxy_batch_size`: Batch size for iterating through the proxy set.
72+
- `algorithm.proxy_seed`: Seed used to select the deterministic proxy subset shared by clients and server.
73+
- `algorithm.temperature`: Softmax temperature used to smooth teacher logits before distillation.
74+
- `algorithm.distillation_epochs`: Number of server-side distillation passes per round.
75+
- `algorithm.distillation_batch_size`: Batch size for the distillation optimizer.
76+
- `algorithm.learning_rate`: Learning rate for the server-side distillation optimizer.
7477

75-
**Reference:** Qinbin Li, Bingsheng He, Dawn Song. “[Model-Contrastive Federated Learning](https://openaccess.thecvf.com/content/CVPR2021/papers/Li_Model-Contrastive_Federated_Learning_CVPR_2021_paper.pdf),” in Proc. CVPR, 2021.
78+
**Reference:** Tao Lin, Lingjing Kong, Sebastian U. Stich, Martin Jaggi. "[Ensemble Distillation for Robust Model Fusion in Federated Learning](https://arxiv.org/abs/2006.07242)," arXiv:2006.07242, 2020.
7679

7780
!!! note "Alignment with the paper"
78-
Here’s how Plato's implementation lines up with Li et al. (CVPR 2021) and the authors’ [reference implementation](https://github.com/Xtra-Computing/MOON):
81+
The module split follows the FedDF workflow directly: `feddf.py` stays as a thin launcher, `feddf_server.py` packages the current global weights together with the shared proxy inputs, `feddf_client.py` performs the standard local update and then emits logits on that server-supplied proxy payload, `feddf_server_strategy.py` resolves the deterministic proxy subset and routes the logits payload through direct weight aggregation, and `feddf_algorithm.py` encapsulates the weighted-logit ensemble plus the temperature-scaled KL distillation step.
7982

80-
- Projection head & representations – `moon_model.py:31-79` implements the LeNet-style backbone plus a two-layer projection head, returning both logits and L2-normalised embeddings. The paper’s Eq. (3) (and typical contrastive- learning practice) calls for that projection step; the public repo’s simple CNN head even hints at it (they keep the projection MLP commented out). So keeping the projection in our model is faithful—and helps the cosine similarities stay well behaved.
83+
The configuration surface above mirrors the paper’s core knobs. `proxy_set_size`, `proxy_batch_size`, and `proxy_seed` control the deterministic unlabeled proxy data used for ensemble distillation, `temperature` shapes the softened teacher distribution, and `distillation_epochs`, `distillation_batch_size`, and `learning_rate` control the server-side student optimization that replaces direct averaging.
8184

82-
- Local training objective – `moon_trainer.py:26-152` combines the supervised cross-entropy with the temperature-scaled contrastive loss exactly like Eq. (1): positives come from the frozen global model, negatives from the stored local-history models, using the same \\(\\mu\\) and \\(\\tau\\) hyper-parameters exposed in the config (`moon_MNIST_lenet5.toml:41-45`). This mirrors `train_net_fedcon` in the reference implementation, which also weights the contrastive term by \\(\\mu\\) and uses CrossEntropy on logits built from cosine similarities.
83-
84-
- Historical model buffer – the client keeps a FIFO queue of past local checkpoints (`moon_client.py:21-64`), equivalent to `model_buffer_size` in the paper and the author's reference implementation; that buffer is fed into the trainer through the strategy context so MOON always has negatives available.
85+
---
8586

86-
- Server aggregation – the server still performs sample-weighted FedAvg (`moon_server.py:12-35`, `moon_server_strategy.py:19-63`), matching the MOON design which leaves the aggregation rule unchanged. The extra global-history deque is bookkeeping-only.
87+
### MOON (client-training customization)
8788

88-
- Shared architecture – `moon.py:8-15` now instantiates `MoonModel` once and passes it into both the client and server `(model=model)`. That guarantees the projection-enabled architecture is shared exactly, as required for the contrastive comparisons.
89+
MOON is included in Plato as a **client-training customization** rather than as a server-aggregation
90+
rule. The server still performs sample-weighted FedAvg; the distinguishing mechanism is the
91+
contrastive local objective together with the historical-model buffer maintained by each client.
8992

90-
The only intentional deviation is that we L2-normalise the projection outputs before computing cosine similarities
91-
(`moon_model.py:76-79`), which the paper assumes implicitly and improves stability. Aside from that, the workflow, hyper-
92-
parameters, and loss all line up with the CVPR paper and the publicly released PyTorch reference.
93+
See **5. Algorithms with Customized Client Training Loops** for the runnable example, key
94+
configuration parameters, and the implementation alignment notes for MOON.
9395

9496
---
9597

docs/docs/examples/algorithms/5. Algorithms with Customized Client Training Loops.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,45 @@ uv run feddyn/feddyn.py -c feddyn/feddyn_MNIST_lenet5.toml
5858

5959
---
6060

61+
### MOON
62+
63+
MOON (Model-Contrastive Federated Learning) enhances standard FedAvg by adding a model-level
64+
contrastive regularizer. Each client augments the shared model with a projection head, clones the
65+
incoming global model as a positive anchor, and reuses a small buffer of its historical checkpoints
66+
as negatives. The server still performs sample-weighted averaging but records a short history of
67+
global states for downstream analysis or warm restarts.
68+
69+
```bash
70+
cd examples/server_aggregation/moon/
71+
uv run moon.py -c moon_MNIST_lenet5.toml
72+
```
73+
74+
Key configuration parameters:
75+
76+
- `algorithm.mu`: Weight assigned to the contrastive term (default: 5.0).
77+
- `algorithm.temperature`: Softmax temperature applied to cosine similarities (default: 0.5).
78+
- `algorithm.history_size`: Number of historical local models cached per client as negatives (default: 2).
79+
- `trainer.model_name`: Name used for checkpointing the projection-ready backbone (default: `moon_lenet5`).
80+
81+
**Reference:** Qinbin Li, Bingsheng He, Dawn Song. “[Model-Contrastive Federated Learning](https://openaccess.thecvf.com/content/CVPR2021/papers/Li_Model-Contrastive_Federated_Learning_CVPR_2021_paper.pdf),” in Proc. CVPR, 2021.
82+
83+
!!! note "Alignment with the paper"
84+
Here’s how Plato's implementation lines up with Li et al. (CVPR 2021) and the authors’ [reference implementation](https://github.com/Xtra-Computing/MOON):
85+
86+
- Projection head & representations – `moon_model.py:31-79` implements the LeNet-style backbone plus a two-layer projection head, returning both logits and L2-normalised embeddings. The paper’s Eq. (3) (and typical contrastive-learning practice) calls for that projection step; the public repo’s simple CNN head even hints at it (they keep the projection MLP commented out). So keeping the projection in our model is faithful and helps the cosine similarities stay well behaved.
87+
88+
- Local training objective – `moon_trainer.py:26-152` combines the supervised cross-entropy with the temperature-scaled contrastive loss exactly like Eq. (1): positives come from the frozen global model, negatives from the stored local-history models, using the same \\(\\mu\\) and \\(\\tau\\) hyper-parameters exposed in the config (`moon_MNIST_lenet5.toml:41-45`). This mirrors `train_net_fedcon` in the reference implementation, which also weights the contrastive term by \\(\\mu\\) and uses CrossEntropy on logits built from cosine similarities.
89+
90+
- Historical model buffer – the client keeps a FIFO queue of past local checkpoints (`moon_client.py:21-64`), equivalent to `model_buffer_size` in the paper and the author's reference implementation; that buffer is fed into the trainer through the strategy context so MOON always has negatives available.
91+
92+
- Server aggregation – the server still performs sample-weighted FedAvg (`moon_server.py:12-35`, `moon_server_strategy.py:19-63`), matching the MOON design which leaves the aggregation rule unchanged. The extra global-history deque is bookkeeping-only.
93+
94+
- Shared architecture – `moon.py:8-15` now instantiates `MoonModel` once and passes it into both the client and server `(model=model)`. That guarantees the projection-enabled architecture is shared exactly, as required for the contrastive comparisons.
95+
96+
The only intentional deviation is that we L2-normalise the projection outputs before computing cosine similarities (`moon_model.py:76-79`), which the paper assumes implicitly and improves stability. Aside from that, the workflow, hyper-parameters, and loss all line up with the CVPR paper and the publicly released PyTorch reference.
97+
98+
---
99+
61100
### FedMoS
62101

63102
FedMoS is a communication-efficient FL framework with coupled double momentum-based update and adaptive client selection, to jointly mitigate the intrinsic variance.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""
2+
Entry point for running the FedDF server aggregation example.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
import feddf_client
8+
import feddf_server
9+
10+
11+
def main():
12+
"""Launch a Plato training session with the FedDF algorithm."""
13+
client = feddf_client.create_client()
14+
server = feddf_server.Server()
15+
server.run(client)
16+
17+
18+
if __name__ == "__main__":
19+
main()
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
[clients]
2+
type = "simple"
3+
total_clients = 100
4+
per_round = 10
5+
random_seed = 1
6+
do_test = false
7+
speed_simulation = true
8+
sleep_simulation = true
9+
avg_training_time = 20
10+
11+
[clients.simulation_distribution]
12+
distribution = "normal"
13+
mean = 10
14+
sd = 3
15+
16+
[server]
17+
address = "127.0.0.1"
18+
port = 8000
19+
synchronous = false
20+
simulate_wall_time = true
21+
minimum_clients_aggregated = 6
22+
staleness_bound = 10
23+
random_seed = 1
24+
25+
[data]
26+
datasource = "Torchvision"
27+
dataset_name = "MNIST"
28+
download = true
29+
data_path = "data"
30+
partition_size = 200
31+
sampler = "noniid"
32+
concentration = 0.5
33+
random_seed = 1
34+
35+
[trainer]
36+
rounds = 20
37+
type = "basic"
38+
max_concurrency = 4
39+
target_accuracy = 1.0
40+
epochs = 5
41+
batch_size = 64
42+
optimizer = "SGD"
43+
model_name = "lenet5"
44+
45+
[algorithm]
46+
type = "fedavg"
47+
proxy_set_size = 2048
48+
proxy_batch_size = 128
49+
proxy_seed = 1
50+
temperature = 2.0
51+
distillation_epochs = 5
52+
distillation_batch_size = 64
53+
learning_rate = 0.001
54+
55+
[parameters]
56+
57+
[parameters.optimizer]
58+
lr = 0.01
59+
momentum = 0.9
60+
weight_decay = 0.0
61+
62+
[results]
63+
result_path = "results/MNIST_lenet5/FedDF"
64+
types = "round, accuracy, elapsed_time, comm_time, round_time"
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""FedDF-specific helpers for ensemble distillation on the server."""
2+
3+
from __future__ import annotations
4+
5+
from collections import OrderedDict
6+
from collections.abc import Mapping, Sequence
7+
8+
import torch
9+
import torch.nn.functional as F
10+
from feddf_utils import extract_batch_inputs, unwrap_model_outputs
11+
from torch.utils.data import DataLoader, Dataset, TensorDataset
12+
13+
from plato.algorithms import fedavg
14+
15+
16+
class Algorithm(fedavg.Algorithm):
17+
"""Algorithm helpers for aggregating logits and distilling the student."""
18+
19+
@staticmethod
20+
def aggregate_teacher_logits(
21+
updates,
22+
payloads: Sequence[Mapping[str, torch.Tensor]],
23+
*,
24+
weighting: str = "uniform",
25+
) -> torch.Tensor:
26+
"""Compute the ensembled teacher logits for AVGLOGITS distillation."""
27+
if not payloads:
28+
raise ValueError("FedDF requires at least one logits payload.")
29+
30+
first_logits = payloads[0].get("logits")
31+
if not isinstance(first_logits, torch.Tensor):
32+
raise TypeError("FedDF payloads must include a 'logits' tensor.")
33+
34+
weighting_name = weighting.strip().lower()
35+
if weighting_name not in {"uniform", "samples"}:
36+
raise ValueError(
37+
"FedDF teacher weighting must be either 'uniform' or 'samples'."
38+
)
39+
40+
total_samples = sum(getattr(update.report, "num_samples", 0) for update in updates)
41+
use_uniform_average = weighting_name == "uniform" or total_samples <= 0
42+
43+
aggregated = torch.zeros_like(first_logits, dtype=torch.float32)
44+
45+
for update, payload in zip(updates, payloads):
46+
logits = payload.get("logits")
47+
if not isinstance(logits, torch.Tensor):
48+
raise TypeError("FedDF payloads must include a 'logits' tensor.")
49+
if logits.shape != first_logits.shape:
50+
raise ValueError(
51+
"FedDF client logits must share the same proxy-set shape."
52+
)
53+
54+
if use_uniform_average:
55+
weight = 1 / len(payloads)
56+
else:
57+
weight = getattr(update.report, "num_samples", 0) / total_samples
58+
59+
aggregated += logits.detach().float() * weight
60+
61+
return aggregated
62+
63+
def distill_weights(
64+
self,
65+
baseline_weights: Mapping[str, torch.Tensor],
66+
teacher_logits: torch.Tensor,
67+
proxy_dataset: Dataset,
68+
*,
69+
temperature: float,
70+
distillation_epochs: int,
71+
distillation_batch_size: int,
72+
distillation_learning_rate: float,
73+
distillation_optimizer_name: str,
74+
use_cosine_annealing: bool,
75+
shuffle_batches: bool,
76+
) -> OrderedDict[str, torch.Tensor]:
77+
"""Distill the server model on proxy inputs using ensemble logits."""
78+
if len(proxy_dataset) != len(teacher_logits):
79+
raise ValueError(
80+
"FedDF proxy samples and teacher logits must have matching lengths."
81+
)
82+
83+
trainer = self.require_trainer()
84+
model = self.require_model()
85+
device = torch.device(getattr(trainer, "device", "cpu"))
86+
87+
self.load_weights(baseline_weights)
88+
89+
inputs = []
90+
for example in proxy_dataset:
91+
inputs.append(extract_batch_inputs(example))
92+
93+
proxy_inputs = torch.stack(inputs)
94+
distillation_dataset = TensorDataset(proxy_inputs, teacher_logits.detach().cpu())
95+
dataloader = DataLoader(
96+
distillation_dataset,
97+
batch_size=distillation_batch_size,
98+
shuffle=shuffle_batches,
99+
)
100+
101+
was_training = model.training
102+
model.to(device)
103+
model.train()
104+
105+
optimizer_name = distillation_optimizer_name.strip().lower()
106+
if optimizer_name == "adam":
107+
optimizer = torch.optim.Adam(
108+
model.parameters(),
109+
lr=distillation_learning_rate,
110+
)
111+
elif optimizer_name == "sgd":
112+
optimizer = torch.optim.SGD(
113+
model.parameters(),
114+
lr=distillation_learning_rate,
115+
)
116+
else:
117+
raise ValueError(
118+
"FedDF distillation optimizer must be either 'adam' or 'sgd'."
119+
)
120+
121+
total_steps = max(distillation_epochs * len(dataloader), 1)
122+
scheduler = None
123+
if use_cosine_annealing:
124+
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
125+
optimizer,
126+
T_max=total_steps,
127+
)
128+
129+
for _ in range(distillation_epochs):
130+
for batch_inputs, batch_logits in dataloader:
131+
batch_inputs = batch_inputs.to(device)
132+
batch_logits = batch_logits.to(device)
133+
teacher_probs = torch.softmax(batch_logits / temperature, dim=1)
134+
135+
optimizer.zero_grad()
136+
student_logits = unwrap_model_outputs(model(batch_inputs))
137+
student_log_probs = F.log_softmax(student_logits / temperature, dim=1)
138+
loss = (
139+
F.kl_div(student_log_probs, teacher_probs, reduction="batchmean")
140+
* temperature
141+
* temperature
142+
)
143+
loss.backward()
144+
optimizer.step()
145+
if scheduler is not None:
146+
scheduler.step()
147+
148+
if not was_training:
149+
model.eval()
150+
151+
return OrderedDict(
152+
(name, tensor.detach().cpu().clone())
153+
for name, tensor in model.state_dict().items()
154+
)

0 commit comments

Comments
 (0)