Network Flow Code Generation Speedup¶
This tutorial benchmarks a small capacitated network-flow problem. It compares a direct NumPy implementation with a verified generated C solver, then reports the speedup from code generation and the subsequent C optimization pass.
Open the companion marimo notebook, or edit the source locally from the repository root:
uv run marimo edit docs/tutorials/codegen/network.py
Problem¶
The problem is a capacitated min-cost network-flow linear program:
Here \(z\) is the edge-flow vector, \(c\) contains edge costs, \(A_s\) maps flows to supply-node usage, \(b_s\) contains supply capacities, \(A_d\) maps flows to demand-node balance, \(x\) is the demand parameter, and \(u\) contains edge capacities. The notebook uses a complete bipartite graph from supply nodes to demand nodes.
Method¶
The solver uses a primal-dual projected update. With step size \(\eta\), supply dual variable \(v\), and demand-balance dual variable \(w\), each iteration is
Both the NumPy reference and the generated C solver run the same fixed iteration map.
Speedup¶
The recorded benchmark ran 200 primal-dual projected iterations per solve.
| implementation | time per solve |
|---|---|
| direct NumPy Python | 2358.812 us |
| generated C after code generation | 10.613 us |
| generated C after C optimization | 2.593 us |
The generated C solver is 222.3x faster than direct Python. The C
optimization pass adds another 4.09x speedup over the initial generated C
solver, for a total speedup of 909.8x from direct Python to optimized C. The
optimized solver was kept after verification with worst max-absolute error
1.443e-14.
Optimization Trick¶
The main trick for network flow is scalarizing the whole 11-state recurrence. The optimized solver replaces generic clipping and nonnegative-projection helpers with direct scalar clamp and ReLU code, splits the final residual-producing step from the non-final loop to remove a branch from most iterations, and fuses final state and solution stores where that was neutral or slightly faster.
In this example, fewer arithmetic operations were not always faster; register pressure and compiler scheduling mattered more than some algebraic reductions.