Optical Matrix–Vector Multiplication
Ask a modern AI accelerator what it does all day and the honest answer is one operation:
y = Mx, the matrix–vector multiply, repeated until the power bill
arrives. The TPU's
systolic array is a city block of multiply–accumulate units marching data through
silicon at gigahertz rates precisely because Mx is where the work lives.
This lesson cashes in everything Module 4 has built and does that same operation with
interference: encode x as optical amplitudes, let the light
fall through an SVD-programmed
mesh, and photodetect y — all
N^2 multiply–accumulates performed simultaneously, passively, in a
few picoseconds, by physics that was going to happen anyway. Then, because this is an
engineering course and not a press release, we do the honest accounting: what the transit time
does and does not buy, where the energy actually goes, and why the edges of the chip — not the
glorious mesh in the middle — decide whether the whole idea wins.
The pipeline: encode, propagate, detect
Encode. A row of
modulators,
one per waveguide, sets the amplitude (and, in coherent schemes, phase) of light in mode
k to x_k — a DAC and a modulator per element,
refreshed once per vector. Propagate. The mesh does nothing, magnificently:
programmed once with the phases of M's decomposition, it is a fixed
piece of glass through which the field distribution rearranges itself into
Mx because that is what Maxwell's equations do to a linear system. No
clock, no instruction fetch, no data movement in the electronic sense. Detect.
Photodiodes
read each output port. One subtlety earns its keep here: a photodiode measures
|y_k|^2 — power, not amplitude — so the sign and phase of
y_k are invisible to naive direct detection. Real systems recover them
with coherent (homodyne) detection against a reference beam, or sidestep them with differential
encodings using pairs of ports. It is a solvable tax, but it is hardware at every output, and it
belongs in the bill of materials.
The numbers: latency, throughput, energy
Latency is the transit time of light. A 64-mode Clements mesh is 64 columns deep;
at roughly 100 µm of waveguide per column and group index
n_g \approx 4, that is 6.4 mm of path:
t \;=\; \frac{L\, n_g}{c} \;=\;
\frac{6.4\times10^{-3} \times 4}{3\times10^{8}\ \mathrm{m/s}} \;\approx\; 85\ \mathrm{ps}.
Eighty-five picoseconds for a complete 64×64 matrix–vector product — 4,096 MACs — versus the ~64
clock cycles a systolic array needs to stream the same product through: tens of nanoseconds. A
three-orders-of-magnitude latency win, genuinely attributable to computing in flight.
Throughput is a different quantity with a different owner: you get one matvec per
modulator refresh, so at modulation rate f the chip delivers
N^2 f MACs per second — a 64-mode mesh at 10 GHz is ~41 tera-MACs/s,
before wavelength
parallelism multiplies it further. Energy is where the story
sharpens. The mesh itself charges nothing per operation — interference is not a billable event —
so the cost concentrates entirely at the edges: one DAC + modulator per input, one
detector + ADC per output, roughly 2N conversions costing
E_{\mathrm{edge}} each (laser power included), amortised over
N^2 MACs:
\frac{E}{\mathrm{MAC}} \;\approx\; \frac{2N\,E_{\mathrm{edge}}}{N^2}
\;=\; \frac{2\,E_{\mathrm{edge}}}{N}.
This little formula is the economic thesis of photonic computing: the energy per MAC falls
as the mesh grows, because the free part scales as N^2 while
the paid part scales as N. With picojoule-class conversions and
N in the hundreds, sub-femtojoule MACs come into view — territory
electronic MACs, each a billable switching event, cannot follow. The catch, of course, is that
the formula rewards exactly the large-N regime where loss, phase error
and calibration bite hardest; the whole tension of
Module
5's energy accounting lives inside that fraction.
| Photonic mesh (N modes) | Systolic array (N×N PEs) |
| latency per matvec | one optical transit, ~0.1 ns | ~N clock cycles, ~10–100 ns |
| throughput | N^2 f_{\mathrm{mod}} MACs/s | N^2 f_{\mathrm{clk}} MACs/s |
| energy per MAC | \sim 2E_{\mathrm{edge}}/N — falls with N | fixed per-MAC switching energy |
| precision | analog, ~6–8 effective bits | exact int8 / floating point |
| changing M | slow (thermal) or costly reprogram | stream new weights next cycle |
Flagship simulation: a mesh, end to end
Time to build the whole machine in software. The program below constructs 2×2 MZI unitaries from
their knobs (\theta, \varphi), embeds them into
N = 4 modes at the Clements positions, and then computes
y = Mx two ways: once as light would — the vector passing
through the mesh gate by gate — and once as a textbook would, by assembling the full matrix
product first and applying it. If Module 4 has told the truth, the two answers must agree to
machine precision, and the mesh must conserve power:
type C = { re: number; im: number };
const c = (re: number, im = 0): C => ({ re, im });
const add = (a: C, b: C): C => c(a.re + b.re, a.im + b.im);
const mul = (a: C, b: C): C => c(a.re * b.re - a.im * b.im, a.re * b.im + a.im * b.re);
const expi = (x: number): C => c(Math.cos(x), Math.sin(x));
type M = C[][];
const identity = (n: number): M =>
Array.from({ length: n }, (_, i) => Array.from({ length: n }, (_, j) => c(i === j ? 1 : 0)));
const matvec = (m: M, v: C[]): C[] =>
m.map((row) => row.reduce((acc, mij, j) => add(acc, mul(mij, v[j])), c(0)));
const matmul = (p: M, q: M): M =>
p.map((row, i) => q[0].map((_, j) =>
row.reduce((acc, pik, k) => add(acc, mul(pik, q[k][j])), c(0))));
// One MZI as a 2×2 unitary: U = i·e^{iθ/2} [[e^{iφ}sin(θ/2), cos(θ/2)], [e^{iφ}cos(θ/2), −sin(θ/2)]]
function mziBlock(theta: number, phi: number): M {
const s = Math.sin(theta / 2), co = Math.cos(theta / 2);
const g = mul(c(0, 1), expi(theta / 2)); // global phase i·e^{iθ/2}
const ph = expi(phi);
return [
[mul(g, mul(ph, c(s))), mul(g, c(co))],
[mul(g, mul(ph, c(co))), mul(g, c(-s))],
];
}
// Embed a 2×2 block onto adjacent modes (m, m+1) of an n-mode identity.
function embed(n: number, m: number, blk: M): M {
const t = identity(n);
t[m][m] = blk[0][0]; t[m][m + 1] = blk[0][1];
t[m + 1][m] = blk[1][0]; t[m + 1][m + 1] = blk[1][1];
return t;
}
// Clements layout for N = 4: columns couple modes (0,1),(2,3) then (1,2), repeated. 6 MZIs.
const N = 4;
const layers = [ [0, 2], [1], [0, 2], [1] ];
// Deterministic "random" phases so every run agrees.
let seed = 42;
const rand = (): number => ((seed = (seed * 16807) % 2147483647) / 2147483647) * 2 * Math.PI;
const gates: M[] = [];
for (const layer of layers)
for (const m of layer) gates.push(embed(N, m, mziBlock(rand(), rand())));
// Path A: compose the full mesh matrix M = T_k ··· T_1.
let mesh = identity(N);
for (const t of gates) mesh = matmul(t, mesh);
// Path B: propagate the vector through the mesh, gate by gate — what the light does.
const x: C[] = [c(0.6), c(-0.4), c(0.5, 0.2), c(0.3)];
let v = x;
for (const t of gates) v = matvec(t, v);
const direct = matvec(mesh, x);
const fmt = (z: C): string =>
z.re.toFixed(4) + (z.im >= 0 ? "+" : "-") + Math.abs(z.im).toFixed(4) + "i";
console.log("light through the mesh: [" + v.map(fmt).join(", ") + "]");
console.log("matrix times vector: [" + direct.map(fmt).join(", ") + "]");
let worst = 0;
for (let i = 0; i < N; i++)
worst = Math.max(worst, Math.hypot(v[i].re - direct[i].re, v[i].im - direct[i].im));
console.log("max disagreement: " + worst.toExponential(2));
const power = (w: C[]): number => w.reduce((p, z) => p + z.re * z.re + z.im * z.im, 0);
console.log("power in: " + power(x).toFixed(6));
console.log("power out: " + power(v).toFixed(6) + " (unitary mesh: conserved)");
console.log("MACs performed by one transit: " + N * N);
Two completely different computations — a gate-by-gate cascade and a single dense matrix — agree to
fifteen decimal places, and the output power matches the input exactly: the mesh is unitary because
every factor was. Scale the idea, not the code: at N = 256 the same
transit performs 65,536 MACs, and the physics does not charge per MAC. Notice also what the
simulation quietly demonstrates about where the answer lives: path B never materialises
the matrix at all. The mesh is the matrix; light evaluates it by falling through.
"The mesh multiplies in 85 picoseconds, therefore it performs
10^{10}-plus matvecs per second" — this sentence, or its marketing
cousin, confuses two clocks that have nothing to do with each other. The transit
time says how long one vector takes to fall through: it sets latency, and it is
gloriously small. The refresh rate says how often you can present a new
vector, and it is owned entirely by the electronic edges — DAC settling, modulator bandwidth,
detector and ADC speed. A mesh fed by 10 GHz modulators does 10^{10}
matvecs per second whether its transit is 85 ps or 85 ns; a mesh fed by kilohertz thermal tuners
does a thousand, full stop. The two numbers even decouple in the other direction: since transit
(~85 ps) is shorter than the refresh period (100 ps at 10 GHz), successive vectors barely overlap
in the mesh — the "pipeline" is usually almost empty. Photonics' real throughput story is not the
picoseconds; it is N^2 MACs per refresh and the parallelism multipliers
(more wavelengths, more meshes) stacked on top. Keep latency and throughput in separate mental
columns and most photonic-computing hype resolves into checkable claims.
Yes and no, and the boundary is exactly the chip's edge. Inside the mesh, genuinely yes:
the interference performing the N^2 multiply–accumulates costs no
switching energy, emits no heat per operation, and takes no longer for
N = 512 than the light's slightly longer stroll. Physics runs the inner
loop gratis. At the edges, never: every element of x must be
converted digital→analog→optical, every element of y optical→analog→
digital, and each conversion costs picojoules and — worse — precision: cascaded DAC,
modulator, detector and ADC noise pins effective accuracy near 6–8 bits, a budget
Module
5 audits line by line. The strategic consequence follows immediately: photonics wins
when the ratio of computation to conversion is high — big N, weights
that stay put while millions of vectors stream through (inference, not weight updates), workloads
tolerant of analog precision. It loses when matrices are small, change constantly, or demand
exactness. This one paragraph is, compressed, the market analysis of every photonic-AI company in
existence.
Where this goes next
The machine works — on paper. The
final
lesson of this module confronts the part the simulation idealised away: real phase
shifters come from a fab with errors baked in, heaters whisper heat into their neighbours'
waveguides, and a thousand-MZI mesh must somehow be set to the phases the decomposition
demands and kept there. Programming, calibration, self-configuration — the difference
between a beautiful theorem and a shippable processor.