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 matvecone optical transit, ~0.1 ns~N clock cycles, ~10–100 ns
throughputN^2 f_{\mathrm{mod}} MACs/sN^2 f_{\mathrm{clk}} MACs/s
energy per MAC\sim 2E_{\mathrm{edge}}/N — falls with Nfixed per-MAC switching energy
precisionanalog, ~6–8 effective bitsexact int8 / floating point
changing Mslow (thermal) or costly reprogramstream 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.