Centralised simulations#
Parameter-server distributed SGD simulation confronting Byzantine workers.
Each synchronous round follows the same pattern:
Honest workers compute a gradient on their local data shard.
Byzantine workers craft adversarial gradients.
The aggregator combines all \(n\) gradients into a single update.
The aggregated update is applied via an SGD step.
The CentralisedSimulation implements the full lifecycle (model
initialisation, per-worker data loading, training loop). Evaluation is
defined by subclasses overriding evaluate() — each protocol reports
its own set of metrics.
Available simulations#
Base simulation class#
- class krum.simulations.centralised.CentralisedSimulation(*, model_cls: type[~torch.nn.modules.module.Module], train_datasets: ~collections.abc.Sequence[~torch.utils.data.dataset.Dataset[~typing.Any]], test_set: ~torch.utils.data.dataset.Dataset[~typing.Any], aggregator: type[~krum.primitives.aggregators.Aggregator] | None = None, aggregator_kwargs: dict[str, ~typing.Any] | None = None, attack: type[~krum.primitives.attacks.Attack] | None = None, attack_kwargs: dict[str, ~typing.Any] | None = None, n: int, f: int, rounds: int, batch_size: int, lr: float, lr_schedule: ~typing.Literal['exponential', 'robbins_monro', 'none'] = 'exponential', lr_decay: float | None = 0.99, r_eta: float | None = None, weight_decay: float = 0.0, xavier_init: bool = False, stop_attack_at: int | None = None, loss_fn: ~typing.Callable[[...], ~torch.Tensor] = <function cross_entropy>, device: ~torch.device | None = None, seed: int = 42, eval_every: int = 10)[source]#
Bases:
objectParameter-server distributed SGD simulation with Byzantine workers.
One instance = one (aggregator, attack, dataset, model) configuration run over
roundssynchronous rounds. Each of the \(n\) workers — of which \(f\) are Byzantine (up to the tolerance of the chosen aggregator) — brings its own training dataset, IID or not.Evaluation is defined by subclasses overriding
evaluate(). Each protocol reports its own set of metrics — e.g.KrumSimulationreturns(test_loss, test_error)andHiddenVulnerabilitySimulationreturns(test_loss, test_error, test_accuracy).- Parameters:
model_cls –
nn.Modulesubclass to instantiate for training.train_datasets – One dataset per worker (honest and Byzantine);
len(train_datasets)must equaln. Only the firstn - f(the honest workers) are ever wrapped into aDataLoaderand trained on — Byzantine workers craft their gradients from the honest ones viaattack, not from local data — but the fulln-length sequence is required so a future data-consuming attack (e.g. label-flipping) has something to read. Splitting a full dataset into per-worker datasets (IID or not) is the caller’s responsibility, via aDataPartitioner.test_set – Test dataset (evaluated via full-batch loader).
aggregator – Gradient aggregation rule class (e.g.
Average,Krum). Pass the class itself —step()callsaggregator.aggregate(gradients, n=n, f=f, **aggregator_kwargs).aggregator_kwargs – Extra keyword arguments forwarded to
aggregator.aggregate(e.g.{"m": 12}forMultiKrum).nandfare automatically injected by the simulation. To override thefinjected fromf(the “real” Byzantine count), pass"f"explicitly here — for example, to run withf=0honest workers but configure Bulyan as if it had to defend against 9 Byzantins, passaggregator_kwargs={"f": 9}.attack – Byzantine attack strategy class (e.g.
GaussianAttack). Pass the class itself —step()callsattack.generate(honest_gradients, f=f, **attack_kwargs).attack_kwargs – Extra keyword arguments forwarded to
attack.generate(e.g.{"std": 200.0}forGaussianAttack).n – Total number of workers.
f – Number of Byzantine workers (the real Byzantine count). Controls how many gradients the simulation produces per round:
n - fhonest workers always run, and the remainingfslots are filled with Byzantine gradients only whenattackis provided. Must be within the aggregator’s Byzantine tolerance. To configure the aggregator with a different (declared) Byzantine budget, seeaggregator_kwargsabove.rounds – Number of synchronous training rounds.
batch_size – Mini-batch size per honest worker.
lr – Initial learning rate for SGD. Used as \(η_0\) by every supported scheduler.
lr_schedule – Learning-rate schedule applied after each round.
"exponential"uses multiplicative decaylr_decayper round (the default for the ICML 2018 protocol)."robbins_monro"uses the \(η(t) = r_η · η_0 / (t + r_η)\) schedule of El Mhamdi et al. (ICML 2018), with fading rater_eta."none"keeps a constant learning rate (the default for the NIPS 2017 protocol).lr_decay – Multiplicative learning-rate decay per round, used when
lr_schedule == "exponential".Nonedisables the scheduler in that mode. Default:0.99.r_eta – Fading rate for the Robbins-Monro schedule \(η(t) = r_η · η_0 / (t + r_η)\). Required when
lr_schedule == "robbins_monro".weight_decay – \(ℓ_2\) regularization coefficient applied directly on the flat parameter tensor. Set to
0.0(default) to disable. Section 5.1 of El Mhamdi et al. (ICML 2018) recommends1e-4.xavier_init – When
True, apply Glorot/Xavier uniform initialization to every weight tensor of the model after construction, and zero-initialize biases. Matches Section 5.1 of El Mhamdi et al. (ICML 2018).stop_attack_at – If set, the Byzantine attack is disabled at round
t = stop_attack_at: thefByzantine workers then send zero gradients. Used by Experiment 1 / Figure 2 of El Mhamdi et al. (ICML 2018), where the attack is maintained only up to round 50.loss_fn – Per-sample loss function. Default:
cross_entropy.device – Device for training and evaluation. Auto-detected if
None(CUDA → MPS → CPU).seed – Random seed for reproducibility.
eval_every – Hint for how often evaluation should occur (used by experiment scripts, not enforced by the simulation).
- Raises:
ValueError – If
lr_scheduleis invalid, required hyperparameters (\(r_eta\)) are missing, the number of train datasets does not equaln, or an honest worker’s dataset is empty.
- property model: Model#
The encapsulated
Model, available aftersetup()orrun().- Returns:
The wrapped ``nn.Module`` with zero-copy flat parameter/gradient views.
- Raises:
RuntimeError – If the simulation has not been set up yet.
- setup() None[source]#
Initialise the model, learning rate, and per-worker dataloaders.
Each honest worker’s
train_datasetsentry is wrapped into a dedicatedDataLoaderwith its own RNG generator, so mini-batch sampling is reproducible across runs. The datasets are used as-is — splitting a full dataset into per-worker datasets (IID or not) is the caller’s responsibility, via aDataPartitioner.The learning rate is initialised to
self.lr. The Robbins-Monro schedule updates it each round insidestep(); the exponential schedule decays it after every round viaself._current_lr *= lr_decay.When
xavier_initis enabled, every weight tensor of the instantiated model is re-initialized with the Glorot/Xavier uniform rule, and every bias is reset to zero. The Xavier draws use a per-parameter localtorch.Generator(matched to the parameter device), so they do not perturb the global PyTorch RNG and work correctly on MPS/CUDA.Determinism:
setup()is fully deterministic for a givenseed. Localtorch.Generatorinstances are used for the per-worker dataloaders and (when enabled) the Xavier re-initialization, so the global RNG state is left untouched and re-runningsetup()reproduces the exact same model, weights, and dataloaders.Safe to call multiple times — each call resets all internal state.
- step() None[source]#
Advance the simulation by one synchronous round.
The learning rate is updated to the value of the current round (only for the Robbins-Monro schedule; the exponential schedule updates the rate after the optimizer step instead).
Each of the \(n - f\) honest workers computes a gradient on its local data shard via
_train_one_worker().If \(f > 0\) and the attack has not been stopped, Byzantine workers generate attack gradients. For
FullGradientNegationAttack, the full-dataset honest gradient is computed first.The aggregator combines all \(n\) gradients into a single update via
self.aggregator.aggregate(...).The aggregated gradient is written to
self._model.gradientsand applied via an in-place SGD update on the flat parameter tensor.The learning rate (if scheduled) is decayed.
- Raises:
RuntimeError – If
setup()has not been called.