Jonathan Xu SWE @ Google

Lab

Things I built to understand something

Not projects — there is no product at the end of any of these, and I did not invent the ideas they demonstrate. I built them because reading a paper and believing you understand it are different states, and writing the thing is how I tell them apart. They live here rather than on the front page because that page is about work I did, and this is not that.

  1. 01 Why attention divides by √dtransformers
  2. 02 Why loop order is worth 185×matrix multiply
  3. 03 The lock that made things slowerconcurrency
  4. 04 Reed–Solomon, solved by handerasure coding
  5. 05 Counting the fleet in kilobytesHyperLogLog
  6. 06 More memory, more page faultsBélády’s anomaly

Why attention divides by √d

Attention is one line: softmax(QK / √d) V. The division looks like a detail and is not. Dot products of d-dimensional vectors grow like √d, so without it the logits spread out as the model gets wider, the softmax saturates, and attention collapses onto a single token. Turn the scaling off and drag d upward to watch it happen.

d = 16
Mean row entropy0 bits
Largest single weight0%
Uniform would be2.58 bits

Each row is one token deciding where to look; the row sums to 1. Entropy measures how spread out that decision is — 2.58 bits is perfectly uniform over six tokens, and 0 bits is all the weight on one. With scaling on, entropy barely moves as d grows, which is the whole point. Turn it off and push d to 256: entropy collapses and one token takes nearly everything, which in a real model means vanishing gradients through the softmax. The causal mask is the other half of an autoregressive model — token i may not look at anything after it, so the matrix goes triangular. The query and key vectors here are arbitrary; the mechanism around them is not.

Why loop order is worth 185×

This is matrix multiplication — the inner loop of a GEMM, and the same arithmetic a convolution unrolls into, which is why it decides how fast a neural network trains. Three nested loops, one multiply-accumulate: C[i][j] += A[i][k] · B[k][j]. Reordering them changes nothing about the arithmetic and everything about the speed: on my own GEMM the worst order ran at 0.62 GFLOP/s and a blocked one at 115. I could recite the reason without really seeing it, so here it is, with every access to B drawn as it happens.

32×32 · 8-element lines · 32-line LRU
Accesses0
Cache misses0
Miss rate, all three0%
Miss rate on B (drawn above)0%

Orange is a hit, grey is a miss. The picture is B alone, which is why its rate differs from the total across all three matrices: under i-j-k every single access to B misses, and the grid goes solid grey. Counts are for the full 32×32 run; the animation replays the first passes on a loop, since the pattern repeats. i-j-k walks B down a column: every element sits on a different cache line, so each one drags in eight values and uses exactly one before the line is evicted. i-k-j walks the same matrix along a row and gets all eight. Blocking wins differently — it shrinks the working set until A, B and C all fit at once, which is what buys you reuse across the whole tile rather than just within a line. Nothing about the arithmetic changed.

The lock that made things slower

I once wrote two thread-safe versions of the same hash table. The obvious one — take a mutex at the top of the insert, release it at the bottom — came out slower than doing no synchronisation at all. Not slower than the clever version: slower than the broken one. Here is where the time goes.

6 threads · 8 buckets
Making progress0 / 6
Blocked on a lock0 / 6
Lost updates0

No locks keeps every thread busy and silently drops writes when two land on the same bucket — the red flashes. One mutex never loses a write and never lets more than one thread work: it is the unsynchronised version plus overhead, which is exactly why it measured ~10% slower than the baseline when I measured it, and 30–40% slower once I pushed it to 4,000 threads. Per bucket only makes threads wait when they actually collide — about on the dual-core machine I wrote it on, and ~8× on an eight-core box. The animation shows the mechanism; those numbers are measured, not modelled.

Reed–Solomon, solved by hand

Replication survives failure by keeping whole copies. Reed–Solomon survives it by keeping arithmetic. Every shard is one linear equation in six unknowns over GF(28); collect any six of the nine and you can solve for the original. Destroy whichever you like and watch which rows the decoder reaches for.

Reed–Solomon · GF(2⁸) · 6 data + 3 parity

The encoding matrix · shard = row × data

  • Rows D0–D5 are the identity — a 1 on the diagonal, so each data shard is stored as itself. That is what systematic means.
  • Rows P0–P2 are Vandermonde. Any six rows of the nine are linearly independent, so any six shards suffice.
  • Highlighted rows are the six the decoder picked. Destroy a data shard and watch it reach down for a parity row instead.
  • Struck rows are gone. Lose four and there is no 6×6 left to invert.

decoded

Shards destroyed0 / 3
Storage overhead50%
Same durability by copying300%

Read the matrix as nine equations. The top six rows are the identity, which is why the first six shards are just the data itself — that is what "systematic" means, and it is why reading undamaged data costs nothing. The bottom three are Vandermonde rows, so any six of the nine are linearly independent and therefore invertible. Decoding is just that: drop the rows you lost, invert the 6×6 that remains, multiply it by the shards you still have. Pressing reconstruct really runs that — I checked it against all 220 combinations of up to three losses before putting it here. And the reason storage systems bother: tolerating three failures by replication means four whole copies and 300% overhead. This does it for 50%.

Counting the fleet in kilobytes, not megabytes

Storage telemetry asks awkward questions — how many distinct disks touched this pool today? — and the honest answer costs megabytes of RAM per query. HyperLogLog answers it in a fixed few kilobytes, with error you can state in advance. Here it is, running against ground truth.

p = 12
last id
Register bank max leading-zero run, summarised across m buckets
Relative error over the stream bands = ±1σ and ±2σ  ·  1σ = ±1.63%
Ids streamed 0
Distinct (exact) 0
HyperLogLog estimate 0
Error 0.00%
Exact set memory 0 B
Sketch memory 4.0 KB

The trick is that a uniformly hashed id is a coin-flip sequence: seeing a hash with k leading zeros is evidence you have seen about 2k distinct things. One register would be far too noisy, so the first p bits pick a bucket and the harmonic mean across m=2p buckets pulls the variance down — the estimate is E=αmm2/j2M[j], and the standard error it leaves you is 1.04/m, chosen at design time by choosing how much memory to spend. Drag the slider and watch the band tighten as the register bank grows.

More memory, more page faults

Adding RAM should never make a program slower. For FIFO page replacement it can, and Bélády found the reference string that proves it in 1969. Give FIFO a fourth frame here and it faults more often than it did with three. LRU and OPT cannot do this, and the reason they cannot is a property worth knowing about.

3 frames

FIFO faults0
LRU faults0
OPT faults (clairvoyant)0

LRU is a stack algorithm: the pages held with n frames are always a subset of those held with n+1, so more memory can only ever help. FIFO has no such property — evicting by arrival order rather than by use means a bigger cache can hold on to exactly the wrong pages for longer. OPT is the offline optimum, which needs to know the future and so exists only as a lower bound to measure real policies against.