Core orchestration: measurements and rationale¶
This page holds the measurements behind small implementation choices in the core
orchestration (noodl._broadcast, noodl.layers.kirchhoff, noodl.solvers.anderson). The
code keeps the rule and the reason; the numbers live here, with how to reproduce them. All
figures are CPU measurements (torch CPU, Windows 11, Python 3.12, float64) on the reference
composed model of benchmarks/composed_model.py unless stated; they move with hardware and
only their ordering is a claim.
Shape broadcasting¶
noodl._broadcast.broadcast_shapes replaces torch.broadcast_shapes for plain integer
shapes. The torch function goes through torch._refs (symbolic-shape checks) and costs about
150-190 us a call on CPU, more than most of the small tensor operations it sizes. The airflow
and transport steps call it about a hundred times per step; in the Modelica parity runs this
was about 12 % of a step (profiled).
Reproduce: profile a building-physics Modelica parity run (tests/verification/
test_modelica_parity.py, -m slow) with cProfile and look at the cumulative time of
torch.broadcast_shapes.
Dense incidence matrix¶
KirchhoffLayer.A, the dense (n, b_layer) incidence matrix, is built on first access only
(functools.cached_property). At the composed model's reference size it is 18.1 MB per layer
and grows 4x per node doubling. Held eagerly, it broke the memory-scaling budget of the
composed model (measured growth 3.16x against a 2.5x budget).
Reproduce: python -m benchmarks.report_composed_scaling (memory scaling of the composed
model).
Residual incidence product¶
KirchhoffLayer.residual forms the interior row sums A_I q by scatter-add over the layer's
edges, not by an einsum against the dense (n_I, b) slice of the incidence. At ensemble 100
the einsum form alone cost 14.2 ms of a 41.8 ms residual evaluation (profiled on the composed
model).
Reproduce: python -m benchmarks.profile_forward and compare the residual's share of the
profile.
Backend of the classic solve¶
For the sparse-direct versus PCG ratios (about 4.6x slower PCG on the single-instance
forward solve) and the ensemble crossover at select._SPARSE_DIRECT_MAX_BATCH, see
Solver selection.
Anderson mixing¶
noodl.solvers.anderson writes Gram-Schmidt and the back substitution out in elementwise
torch over the (at most M, in practice a handful of) history columns. A LAPACK QR or
triangular solve there costs far more than its arithmetic: after one, the OpenMP worker
threads it wakes slowed every later small elementwise op of the pass about threefold
(measured on the sewer dynamic wave).
Reproduce: python -m benchmarks.sewer_dynwave_speed, swapping the hand-written
factorisation for torch.linalg.qr.