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.
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.
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.
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.
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 2× 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.
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
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.
—
The trick is that a uniformly hashed id is a coin-flip sequence: seeing a hash with leading zeros is evidence you have seen about distinct things. One register would be far too noisy, so the first bits pick a bucket and the harmonic mean across buckets pulls the variance down — the estimate is , and the standard error it leaves you is , 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.
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.