~things that did not work
/brag is the record of what shipped. This is the other half, and it is the half I actually learned from.
Everything here is specific, dated by the thing it happened to, and still true. None of it has been softened into a lesson.
I optimised the wrong function, four times
The performance table on the 1-bit llm page reads as a clean climb: 20 → 110 → 600 → 900 → 1,045 tokens a second. Presented like that it looks like a plan. It was not a plan. It was four rounds of being confidently wrong about where the time was going.
| I was sure it was | it was not |
|---|---|
| softmax | a rounding error in the profile |
| memory bandwidth | nowhere near the bound |
| the matmul, obviously | 94.7% of the forward pass, yes — but already running at 13–14 GMAC/s, which is close to the ceiling for four-wide SIMD |
| actually | the sampler was sorting all 4,096 logits to pick the top 100. About 49,000 comparator calls per token, in a function I had never once opened |
The tell was there the whole time and I did not look at it: forward() measured 0.870 ms — 1,149 tokens a second — while generation actually ran at 535. Half the time was outside the model entirely. I guessed twice before building a profiler. The profiler took an afternoon. The guessing took longer than that.
the transposition table nobody called
Sepentia's transposition table was not missing. It was written, it was correct, and it was sitting in the repository doing nothing. The game loop had two search functions — one that used the table, one that did not — and it always called the one that did not.
So the engine re-analysed identical positions thousands of times per move. It was not slow. It was wrong about what it had already seen, which is a different problem with a different fix, and I spent a while solving the first one. Before you optimise anything, check that the thing you built is the thing that is running.
two years of profiling the wrong language
The Python engine took five to ten seconds a move and froze its own window while it thought. The obvious response was to make the Python faster: profile the hot loops, reach for numpy, rewrite the inner search. I went a fair way down that road.
It was never a Python is slow problem. It was a runtime problem. The identical algorithm on V8 — which JITs a function called millions of times, as a chess search is — went from roughly 10–30K nodes a second to 100–500K. Not one line of the search got smarter. Those two diagnoses look the same from a distance and they send you to completely different places.
the king was worth nothing
For two years the evaluation function scored material, pawn structure and knight outposts, and assigned the king's position exactly zero. The most important piece on the board had no positional model at all. It took someone else reading the code to say so out loud.
Adding tapered king tables was one of the largest single jumps in the whole rebuild. That is the part that stings: it was not a subtle bug, it was a missing idea, and I had read that function more times than anyone.
an honest note about the web worker
Moving the search onto a worker thread is the single most visible improvement in sepentia, and it made the engine exactly zero percent stronger. Same depth, same nodes, same time. It made the board stop freezing, which is worth doing, but it is a fix to the experience and not to the engine — and one core does the work while the other seven sit idle. Real parallel search needs shared memory between workers, which is a project rather than an afternoon. I have not done it.
If you would rather read the version where everything went well, it is at /brag.