Conversation
`num-examples` was set from `len(trainloader)` / `len(testloader)` at all four call sites. `len()` on a DataLoader counts batches, so 33 samples and 64 samples both reported 2 at batch_size=32. flwr's FedAvg defaults to `weighted_by_key="num-examples"`, so that batch count weighted the adapter aggregate and the reported loss/accuracy. The `ceil(n/32)` quantisation over-weights the smallest partitions, which is worst under the Dirichlet skew this testbed studies. Use `len(loader.dataset)`. Add `fl.round.ess`: effective sample size, `1/Σwᵢ²` over those same weights. Equal to the client count on even shares, falling toward 1.0 as one client dominates, NaN when nothing aggregated. Participation counts cannot show concentration; this can, and would have made the batch-count weighting visible.
ty rejects float() on a MetricRecord value: the union includes Array. Same situation _round_summary already handles, so use the same idiom.
Four defects from review, each with a regression test. _num_examples indexed msg.content["metrics"] by literal record name. flwr addresses the record by type, so a reply naming its MetricRecord anything else raised KeyError inside aggregate_train — a telemetry read aborting the round it only meant to observe. Read metric_records by type; return None, never raise. ESS as 1/Σ(wᵢ/Σw)² is not float-exact: an even five-way split read 4.999999999999999, ten-way 9.999999999999996. Kish's (Σwᵢ)²/Σwᵢ² over the raw weights with math.fsum is exact for every n in 2..32. The four changed client_app lines ran in no test: nothing imports the module, the CI job named smoke-test only runs `flwr build`, and ty sees torch.** as Any. Extract _sample_count and cover it. Add fl.ess to the docs' round-attribute and metric enumerations, which were also already missing fl.failures.
…have flwr.supercore.task_identity arrives in 1.38; the lock pins 1.36, where Message constructs with no process identity. Verified the test_server assertions against 1.36.0.
Review notesFour defects in the first cut of this branch, each now fixed and covered by a test. 1. 2. 3. 4. Docs omitted the new metric. RefutedSuspected that run manifests written before this change recorded the batch-count convention under the same Verified clean
Not exercised end to end: no live |
num-exampleswas set fromlen(trainloader)/len(testloader)at all four call sites inclient_app.py.len()on a DataLoader counts batches, not examples.Confirmed against torch at
batch_size=32:len(dataloader)len(dataloader.dataset)flwr 1.38's
FedAvgtakesweighted_by_key="num-examples"by default, so that batch count was weighting both the LoRA adapter aggregate and the reported loss/accuracy. Since batches areceil(n/32), the error is a quantisation that systematically over-weights the smallest partitions — largest exactly under the Dirichlet skew the testbed exists to study. Nowlen(loader.dataset).fl.round.essEffective sample size,
1/Σwᵢ²over the samenum-examplesweights FedAvg aggregates by. Equals the client count when shares are even, falls toward 1.0 as one client dominates, NaN when nothing aggregated. Emitted as a round metric and anfl.essspan attribute.Participation counts report how many clients replied, not how much each one moved the aggregate. On the four partition sizes above, ESS reads 2.38 over sample counts against 4 clients — this metric is what makes that class of weighting bug visible instead of silent.
Verification
ruff format --checkandruff checkpass locally.ty checkandpytestneed the app env, andrayships no macOS x86_64 wheel, souv synccannot build on an Intel Mac — those ride on CI here.Follow-up, not in this PR
Each client evaluates on a 20% holdout carved from its own partition, so the round figure is a weighted mean over local shards rather than a global test set. Under Dirichlet the holdout inherits the partition's label skew. Filed as a ROADMAP v2 item, since fixing it changes the experiment rather than the plumbing.