CU4 — Fans in both directions
Graph.somatize(Left().named("left") | Right().named("right")) >> Mean()# `Mean` receives {"left": …, "right": …}Status: closed. 46 tests in Rust, 52 in Python.
The question: where does aggregation live?
Section titled “The question: where does aggregation live?”The original answers it twice, and both answers teach something:
- On the edge (forward): it joins what arrives into a
serde_json::Mapkeyed by the source node, and the aggregator is an ordinary node —MajorityVoteis aFilter. - In training (federated):
FederatedAggregation::{FedAvg, FedProx, FedYogi}andGradientAggregation::{AllReduce, ParameterServer, …}— enums of algorithms, wrapped inStateAggregator/GradientAggregatortraits with exactly one implementor each: the enum itself. Both are on the orphan-trait list. When they enumerated FL’s real algorithms they got an enum; the trait on top bought nothing.
And the trap to spot: federated aggregation is not fan-in. In FedAvg what is
averaged are the states of N workers when a round closes — there is no edge and
no predecessors there. It is an operation inside fit, and it will arrive with
it.
Decisions taken
Section titled “Decisions taken”- There is no
Aggregatortrait. An aggregator is a filter that reads a map.Mean,MajorityVote,Concat,WeightedMeanare library. Value::Map, ordered. AHashMapiterates differently in each process, so flattening it to a list would give a different order every time and the content hash — once the cache arrives — would be useless. The pairs follow the edges’ declaration order, which is also what mirrors a Pythondict: the round trip gives the same dict.- Both directions have the same shape. Several inputs → a map keyed by each source. Several leaves → a map keyed by each leaf. A diamond comes back round.
- The weight travels with the value. FedAvg weights by each client’s
samples; neither a list nor a map of raw outputs gives that weight. Each
branch produces something like
{"update": …, "n": 128}— another independent reason to haveValue::Map.
What was removed, and the lesson in it
Section titled “What was removed, and the lesson in it”A Plan::Parallel variant that meant these branches do not depend on each
other broke on the diamond: both branches claimed the join node and it executed
twice. The right shape is for every step to carry where its input comes from
(Execute { node, from }). With that the plan stays self-contained — the engine
does not look at the graph again — and the fans fall out with no special variant
at all.
So the variant went, and with it the CompileError::Fanin and ManyLeaves
errors, leaving CompileError with one. A variant that only describes
structure buys nothing; parallelism comes back in CU9 meaning something it did
not mean here — running at the same time.