What an adapter update's direction reveals: initialization first, training length second, source barely¶
datasets/results/adapter-geometry-s1.json, scripts/adapter_geometry.py. RQ2's per-client
question in its simplest form: is a LoRA update's direction in weight space closer to updates from
its own organization? The cosine between two updates dW = (alpha / r) B A, for every pair of the 35
saved adapters and all 196 adapted modules, computed through r x r traces without forming dW. Run
from a worktree pinned at e23b905 so the queued jobs' checkout was untouched; CPU only, three
minutes. Weight-space provenance is established for the training objective (Paul,
arXiv:2604.08844, AUC 1.00 from spectral features), which names "organic drift from distributional
shift", the same objective on different data, as untested. This is that case.
| pair | cosine |
|---|---|
| identical data and seed, two runs (marker-0 vs sym-0, halves a and b) | 0.964, 0.894 |
| identical data, different seed (sym-0 a, seeds 1 to 3) | 0.128 to 0.137 |
| same seed, disjoint OpenStack halves (sym-0 a vs b, seeds 1 to 3) | 0.132 to 0.142 |
| same seed, disjoint Qt projects, 1,517 each (qt-creator vs qtbase) | 0.135 |
| same seed, OpenStack half vs qt-creator or qtbase | 0.106 to 0.116 |
| same seed, OpenStack half vs qtdeclarative (788) | 0.133 to 0.140 |
| same seed, Qt projects at 788 (qtdeclarative vs either) | 0.212 |
| same seed, OpenStack vs Qt train windows, 4,327 each | 0.055, 0.056 |
| different seed and different data, same organization | 0.031, 0.032 |
| different seed, different organization (OpenStack s2 vs Qt s1) | 0.016 |
Initialization decides most of it. Retraining on byte-identical data at the same seed returns nearly the same update (0.96), with training's own nondeterminism visible (0.89 on the other half); a different seed on the same data returns 0.13. An observer comparing updates across seeds sees almost nothing of the data. Federated rounds share an initialization, so the same-seed rows are the realistic ones.
Training length decides most of the rest. Short runs stay closer together (0.21 at 788 examples) and long ones drift apart (0.055 at 4,327), whatever the source. The 0.055 between the OpenStack and Qt windows is mostly length: at matched size the cross-organization cosine is 0.11.
Source is a small term. At matched size, same-organization pairs sit at 0.13 and cross-organization at 0.11, a 0.02 margin smaller than the effect of training size, and an OpenStack update is closer to a small Qt project's update than to its own sibling half. Two Qt projects align exactly as much as two OpenStack halves, so even that margin cannot be separated from language: OpenStack is Python, Qt C++.
What it means for RQ2. An update's raw direction is a weak witness to its source: a nearest-neighbour attack on cosine would mostly recover seed and training length. RQ2's per-client attack therefore has to be learned against reference updates at matched initialization and size, which is FedAttr's design (paired subsets with and without the target), not a similarity lookup. The null here is informative in its own right: the privacy question is not answered by the geometry alone, and a defence argued from "updates look alike" would be arguing from the wrong quantity.
Caveats: one seed of the organization-window pair beyond the first; halves and projects overlap the train windows they were drawn from, so window-versus-subset rows share data; cosine over all modules pools layers whose roles differ, and a per-layer or spectral reading (Paul's features) could separate what the pooled cosine does not.