1,740 board runs, one human, a crew of AI agents: making Python up to 2,000 faster for Speed Demon 2026
My write-up for Speed Demon 2026 by Hackathon Raptors. The code is public: github.com/SaudSatopay/speed-demon-2026. Every number below comes from the board's run log or a track's PERF.md, where you can check it.
Speed Demon hands you four slow, correct Python programs and one rule: make them fast without changing a single byte of output. You never submit a timing. The organisers build your Docker image and run it on their own machine, an i5-13500 with turbo off, 12 threads and 8 GB, and they hash every output. One wrong byte and the track scores zero. It's a very fair system, in the way a guillotine is fair.
I entered solo, in Rust with only the standard library, and I'll be upfront: I wrote very little of the code myself. AI agents did most of the typing, mainly Claude Code, with Codex for a stretch. My job was to write the rules they worked under, decide what to keep, read the board at unreasonable hours, and stop the whole operation from quietly setting itself on fire. So this post is about two things: what made the code fast, and what I learned running a crew of AI agents against a stopwatch for 72 hours.
The short version: our final builds ran about 500× to 2,050× faster than the references, every output byte-identical. On the live final run, on a dataset nobody had seen, we came 3rd on the machine score with 84.68, a whole 0.11 points behind 2nd. I've made my peace with that 0.11. Mostly. The judges' 35% isn't in yet.
The scoring, in one paragraph
Each track gets an efficiency number:
K = (T_ref/T)^0.50 × min(M_ref/M, 50)^0.20 × min(S_ref/S, 1)^0.15 × (D_ref/D)^0.15 speed points = 50 × log10(K) T is wall time, M is peak memory, S is lines of code after a formatter, D is dependencies. Time is half the weight and has no cap, so every 2× speedup is worth the same 7.5 points, whether you go from 10× to 20× or from 1,000× to 2,000×. Memory stops paying at 50× better than the reference. Lines and dependencies can only cost you, which is the formula's way of saying "we know about npm". That shaped the whole plan: get all four tracks correct on the board first, then always take the cheapest 2× on offer, on whichever track it is.
The overall title is the geometric mean of your best three tracks, and the judges score 35% of each track on code quality, technique and the optimisation log.
The four tracks
| Track | What it does | Reference on the box | Our best board run | Speedup |
|---|---|---|---|---|
| A · Ingest | Hourly latency percentiles from a 72-hour access log | 32.05 s, 228 MB | 0.0156 s, 16.1 MB | ~2,050× |
| B · Index | 50,000 boolean and phrase queries over a 254 MB corpus | 56.35 s, 2,200 MB | 0.0643 s, 29.2 MB | ~880× |
| C · Join | A deliberately hostile CSV joined to a region table | 14.44 s, 367 MB | 0.0189 s, 10.0 MB | ~760× |
| D · Traverse | Shortest paths on a 5-million-edge road graph | 72.69 s, 893 MB | 0.1444 s, 42.2 MB | ~500× |
"Deliberately hostile" is the spec's own spirit, not my editorialising: quoted fields, doubled quotes, commas and line feeds inside quotes, byte-order marks. Someone enjoyed writing that generator.
Before the first line of code
The rules disqualify any solution code committed before kickoff, so the two days before it went into everything else. Three things paid off.
Reading the referee's code
I read all four specs and the scoring harness source, which is not how most people spend a Wednesday. Two places disagreed with the website: which dataset the board used for Track B (the spec said the 10% sample; the board's code ran the full set), and the final-scoring timeout (3× in the script, 10× on the site). I asked on Discord, and the organisers fixed both in the repo the same day. If a contest publishes its harness, read it like a contract. It's the only document that can't be wrong about what the harness does.
A local scorer that measures like their box
I rebuilt the measurement from the organisers' own harness: their timer, their runner, their line counter, and Docker's cgroup memory peak. The source-size baselines matched the published ones exactly (56, 91, 108 and 43 lines), and Track C's memory came within 0.4% of the box.
One finding mattered more than the rest. The box's kernel sets transparent huge pages to always, while my WSL2 kernel uses madvise. On the box, touching one byte of a sparse table can be billed as a whole 2 MB page. My scorer switches the setting while it measures, because finding that out from the live board would have cost a day.
Then, on day two, the untouched reference got 16% slower. I ruled out the harness update, the CPU clocks, the power plan, Windows updates and the RAM. The culprit was the Realtek audio driver, burning a full CPU thread all day with nothing playing. WSL2's virtual CPUs are ordinary Windows threads, so the driver's hobby landed on my cores. After a reboot, the reference ran 8.084 s against an 8.083 s baseline. A local benchmark is only as good as the machine's quietest moment, and apparently my machine's quietest moment requires turning it off and on again.
191 answer sets
A wrong answer on the hidden final seed zeroes a track, so correctness came before speed. Before kickoff there were:
- 15 generator seeds, with the expected outputs computed by the official reference.
- 32 answer-preserving rewrites of those datasets: line endings flipped (one variant has exactly two CRs in a 20 MB file), a BOM before every record or none at all, empty fields swapped between quoted and unquoted, lines and edges shuffled or reversed.
- 19 synthetic limit files, checked against each spec's grammar: an empty log, every endpoint length from 1 to 125 bytes, sums past 2^32, every kind of unreachable query.
A thread sweep re-runs everything at 1, 2, 3, 7, 12 and 64 threads, which moves every chunk boundary; I tested it with a bug that only appears at 3 threads, and it caught it by name. A pre-push hook built the exact commit offline in the official image and ran the grid before anything reached the board.
The rules shaped the AI side too. Every agent worked under the same written rules: Rust standard library only, no precomputed answers, nothing tuned to the public seed, one idea per push, and every key compared in full. That last rule cost us speed, and I'd keep it. More on that below.
The first push landed 4 minutes 50 seconds after kickoff. An agent wrote Track A's first version from the design notes, which is the most satisfying 4 minutes 50 seconds of preparation I've ever cashed in.
What made it fast
The first versions were already 19× to 160× faster than the references, because the references are naive on purpose: Track A's spends about 77% of its time in strptime, and Track D's is 89% Dijkstra. Turning on twelve threads was the next big jump: Track A went from 0.443 s to 0.062 s, and Track C from 0.770 s to 0.086 s. After that, every gain had to be earned.
Track A: one integer per line
The log is mapped, not read, and parsed in place at fixed offsets. The hour comes from one multiply on an 8-byte load, and each line becomes one 32-bit integer whose plain integer order is the sort order, so percentiles fall out of a radix sort with no comparisons. Hours are finished and written while the parse is still running, because the log arrives nearly in order: that took memory from 18.8 to 16.4 MB and time from 0.0245 to 0.0221 s.
"Nearly in order" is doing a lot of work there, and the spec promises no bound on lateness. So the early finishing is checked, not assumed: a line that arrives more than 1/48 of the log late restarts the run with early finishing switched off, which about doubles the time. It never fires on generated data, and it's the kind of insurance you buy precisely so you never get to use it.
Track B: never store a posting
The reference builds a set for each of 609,833 distinct terms, which is thorough and exactly what you'd do if time were free. We index only the roughly 75,000 terms the queries actually use, and answer every query in one scan of the corpus. A rare term is an "anchor": documents are checked only around its occurrences. Boolean queries become small truth tables, so checking a document is a table lookup, not a formula. That chain took T from 0.308 s to 0.211, then 0.100, then 0.078, and later choosing anchors over the whole query got it to about 0.068.
Track C: break the record-to-record chain
A normal CSV parser finds each field from the end of the last one, so every record waits on the previous one. That's about 75 cycles of waiting per record, multiplied by a lot of records. We first list every separator in each 64-byte block, with quote parity from a carry-less multiply (so doubled quotes and commas inside quotes cost no branches), and only then read records from the list. Counting distinct names uses one shared lock-free table with one cache line per name.
Track D: put neighbours next to each other
The input's node IDs are random, so every edge is a cache miss with a long commute. Renumbering the graph so neighbours sit together took the queries from about 190 ms to about 105 ms. Removing low-degree nodes with shortcuts took T from 0.166 s to 0.152 s and memory from 67.9 to 46.0 MB. The search is a two-ended Dijkstra that alternates sides by work done, not by which side looks closer, because a query towards an unreachable island otherwise wastes millions of settles proving a negative.
The biggest single Track D win was embarrassing in hindsight: 1.74 million locked increments on one shared counter. Removing them took T from 0.389 s to 0.272 s. Contention on a single cache line is the kind of bug that doesn't show up in a profile so much as in a sigh.
The two smallest changes with the best return per line
Some of the cheapest wins weren't in the algorithms at all, but in the few milliseconds around them. The clock starts at exec, so everything before your first instruction counts.
Static linking: one line of config, zero lines of code. A normal Rust binary loads glibc through the dynamic loader at startup, and on the box that loading happens through the container's overlay filesystem, inside the timed run. Adding +crt-static to the target features in .cargo/config.toml removes it. An empty main measured 1.25 ms dynamic against 0.58 ms static under the harness's Docker flags. On Track A that was a paired 315 µs locally, about 0.5 ms of the box's 25 ms at the time; on Track C, 235 µs. Two percent for one line is a deal you don't get often.
Skipping Rust's own startup. Before your main runs, Rust's standard runtime reads /proc/self/maps, installs signal handlers and gives each thread a signal stack: about 15 system calls the timer counts, plus about 5 more per thread, some of them taking the address space's lock for writing. System calls cost far more on the box than on my PC. So three of our tracks declare main as the C entry point and skip all of it. It's a few lines, it's documented in the code, and the reason it works is the reason it's slightly rude.
The measurements that lied
This is the part I'd most want to read in someone else's write-up.
My PC and the box disagreed, and only one of them counts
My machine is a Zen 4. The box is a Golden Cove with a smaller shared L1 and about half the clock (2.5 GHz with turbo off). So:
- On Track C, cutting branch mispredicts from 1.42 to 0.56 per record changed nothing on the board. Very satisfying locally. Completely ignored by the scoreboard.
- Aligning Track C's input mapping cut local page faults from 3,094 to 651. The box didn't move either.
- On Track B, three block-size settings ranked one way by local cycles (2.10G, 2.13G, 2.16G) and a different way on the board (0.0996, 0.0909, 0.0942 s).
- On Track A, a "shorter dependency chain" cut single-thread cycles from 225.7M to 199.5M locally, and the board read 0.0220 s against 0.0193. Slower.
- On Track D, our own scorer predicted 0.253 s for one change; the board read 0.389.
We ended up trusting local numbers only to rank instruction-count cuts, and settling everything else on the box. The box, it turns out, has opinions, and it doesn't share them in advance.
Exit is part of the timed run
The clock runs until the process exits, and exit tears down the mapped input page by page, on one thread, while the clock watches. On Track A, mapping the log lost on the board three times, until the threads shared releasing its pages before exit. On Track C the same teardown cost about 3.6 ms, roughly 15% of the run; without the shared release, K fell from 46.91 to 42.74. The fastest code in the world still has to clean up after itself, and it does so on company time.
Some savings were worth exactly nothing
On Track B, memory stops paying at 50× better than the reference, which works out to about 44 MB, because the board measured the reference at 2,199,961,600 bytes. Once under that, cutting memory from 42 MB to 27 MB earned zero points. A genuinely impressive optimisation with a genuinely impressive return of nothing. We stopped there and spent the spare memory buying time instead.
The memory peak counts more than your data
The cgroup's peak includes each CPU's unused charge batch, up to 252 KB per CPU, and threads racing to touch the same huge page each get billed for clearing their own copy. On Track C the main thread now faults in, up front, the pages a worker would otherwise be charged for. Being charged for memory you didn't use is a very modern experience.
Noise was bigger than most of our ideas
Fifteen runs of one Track B build spanned T 0.0681 to 0.0726 s, so a 1% gain was invisible in a single run. 234 runs of the final Track D source spread from 0.143 to 0.190 s. One afternoon, a bit-identical Track A build read 0.0290 s against 0.0255 s earlier: the box had moved, and the code had not. Track A also has a slow mode we never fully explained: identical code sometimes reads 0.018 s instead of 0.0158.
So the rule became: never decide between two builds on one timing, and report medians. Our best board numbers are the best of many runs. The full spread is in the numbers section below. Anyone quoting only their best run is quoting their luckiest one.
The changes that should have helped
- Track C: batching lock writes, to do fewer locked operations, made T worse: 0.0244 against 0.0199, because the batch cost a second cache-line fill per record, about 22%.
- Track D: capping the search at six threads, one per physical core, made it slower too, 0.196 s. Hyper-threads hide memory latency, and a graph search is almost all memory latency.
- Track D again: looking nodes up by hash instead of renumbering them was about 5× slower. Hashing is wonderful until it's the bottleneck.
- Track B: a filter to skip tokens that appear in no query added 13% more instructions and was reverted. The cheapest work is the work you don't add.
Correctness: the bugs the grid didn't catch
The multi-seed grid caught plenty, usually within seconds. A backwards digit-parsing trick on Track A failed 43 of 44 cells at once. A radix-queue experiment on Track D gave wrong answers immediately, and a too-large candidate silently dropped by a compare-before-check was caught by the answer bank the same way. Those are the friendly bugs. The scary ones were the bugs it couldn't see.
- Track A: a race every check passed. The lock-free name table had a window where two threads inserting the same new key at the same instant could clash. Nothing caught it: it needs two threads, a new key, and the same nanosecond. After the fix, 300 repeated runs became a standing check, and the final build passed 130,233 runs with no bad output.
- Track B: a hang at one thread. An adversarial review found a helper that could park twice, while a thread keeps only one wake-up. Pinned to one thread, it hung 10 runs out of 10. The thread sweep would have caught it. Normal runs never would.
- Track C: 2 crashes in 1,900 runs. A review agent found a pointer and length read before the atomic that makes them valid. It only showed up on a spec-legal stress input, which is the worst kind: legal, rare, and fatal.
- Track B: a late start. Threads sometimes started late because the threads pre-faulting the corpus held the kernel's memory-map lock. Not a wrong answer, just a mystery that looked like noise until someone read the kernel side.
We also tested the tests: 56 bugs were planted one at a time on Track B, and the fuzzing caught 49. Of the seven it missed, three couldn't change the output, two were races that need a particular input, one was caught by AddressSanitizer, and one was a coincidence the tests never produced.
Check it yourself, with one command
All of that testing ships with the repo, outside the scored track folders. python3 tools/verify.py needs only Docker, git and Python. It builds every track offline, the way the board does, runs it in the board's sandbox (no network, read-only root, 8 GB, an unprivileged user), and checks it against 171 datasets: 15 seeds at two scales, the answer-preserving rewrites, the limit files, and five repeated runs for determinism. Every expected answer comes from the organisers' own reference, run on their own generators' data. A cold start took 16 minutes on my PC. If you'd like to check a claim in this post, that's the fastest way to catch me out.
Exact keys, on purpose
Several of the fastest entries match names or tokens by hash alone and accept a small chance of a wrong answer; one README puts it at about 1 dataset in 3,000 for its search track. That's a reasonable engineering bet, right up until it isn't. Our rule was the opposite: every key compared in full, plus a test build where every key gets the same hash, which must still produce the right output. It cost some speed. On the final run, on a seed nobody had seen, all four of our tracks passed, and I slept better for it, which at that point in the week was saying something.
Running a crew of AI agents
Most of what I learned that isn't in a PERF.md is about orchestration.
Context is the real cost
By Sunday, the two main sessions (one owning tracks A and B, one C and D) were carrying 833K and 685K tokens of history, and every turn paid for all of it. Imagine re-reading a 600-page diary before every sentence you write. We switched to fresh sessions that start from a short written handover in a shared PROGRESS.md. In the next 1 hour 45 minutes, the overall score rose from 82.21 to 82.97 and took 1st on the live board, while usage fell from about 3.1% to 1.3% of the weekly limit per hour. A fresh session with a good handover beats a long one that remembers everything, which is also true of meetings.
More agents is not more progress
Early on, a 13-agent prototyping workflow at maximum effort burned the whole 5-hour usage window in about 50 minutes, and every agent was cut off before reporting. Zero results. A dozen brilliant colleagues who all leave before saying anything is, it turns out, the same as no colleagues. After that: two or three agents at a time, each with a narrow brief, and the deterministic test rig as the judge instead of more agents.
The handover file is the team
Any agent could be cut off at any moment, by a usage limit, a crash or me closing the wrong window. So PROGRESS.md had a "now" block, a table per track, and the reason for every decision. A new session could pick up a half-finished experiment from the file alone. Without it, every handover would have restarted the work from scratch.
Agents need guard rails, not trust
A few that earned their keep:
- A pre-push gate no agent could skip, so nothing broken reached the board. Skipping it with
--no-verifywas in the rules as something you confess to the human first. - No experiment as the last commit at freeze, and a scheduled check 45 minutes before the freeze that compared each track against its chosen final source.
- An audit for line-count tricks, after the organisers warned that judges check for them. Comments that game the line counter had already got one team caught.
- One shared checkout, carefully. At one point I found it detached on an old commit, probably by another agent: the software equivalent of a colleague leaving the office printer on "envelope". After that, risky jobs ran in their own git worktrees.
- Interrupted is not stopped. A command into WSL can keep running inside Linux after the tool reports it as rejected. One push landed that way. After that, the first thing after any interruption was to check the processes and
git log. - Check the boring parts. The scheduled task that made the repo public at the deadline used a command-line flag our version of
ghdidn't have. It would have failed, quietly, ten minutes before the cutoff. A last-hour check caught it with about 25 minutes to spare.
Decide on the box, not on the story
Agents are very good at explaining why a change should be faster. Fluently, confidently, with diagrams if you let them. The board was the only thing that settled it, and it overruled the explanation often enough that "one idea per push, median of several runs" became the whole method. Our repo has 583 commits, 344 of them marked exp:: experiments sent to the box to answer one question each. The agents wrote most of the code. The box decided what stayed.
What I'd do differently
- I'd stop neglecting Track D sooner. Its code hasn't changed since Oct 31, because it was outside our best three tracks, and the overall title counts only those. That was correct for points, but D had the most headroom: the leader on that track ran in 0.085 s against our 0.144.
- I'd treat lines of code as a real budget from day one. Track B ended at 1,077 lines against the reference's 91, and against a rival with about 800 lines, the line factor alone cost us about 4.5% of K. Another team moved two tracks from Rust to C++ for 22–28% fewer scored lines at about the same speed. Rust-only was our own rule, not the contest's. I chose it; I'd probably choose it again; I'd just stop pretending it was free.
- I'd take back the late memory redesigns on Track C. On the last day, one session spent hours on designs that cut memory to about 8 MB but lost time, and every one was rejected on the board. A smaller budget of experiments, decided earlier, would have bought more.
- I'd write the "a human understood this" part as I went. The rules say
PERF.mdmust show a human understood the work. With agents doing the typing, that means reading every decision as it lands, not speed-reading 300 log rows the night before judging.
The numbers
Every board run of the exact source we froze, not just the lucky ones:
| Track | Runs of the final source | K: worst / median / best | T in s: best / median / worst | Best run overall | Rank by K on the board |
|---|---|---|---|---|---|
| A | 65 | 47.70 / 56.72 / 58.58 | 0.0156 / 0.0164 / 0.0228 | run-a3d80d9d43be, K 58.58 | 3 |
| B | 26 | 42.47 / 44.03 / 44.60 | 0.0645 / 0.0662 / 0.0712 | run-4d59d4dfba18, K 44.70 | 2 |
| C | 26 | 43.84 / 46.75 / 47.26 | 0.0193 / 0.0195 / 0.0222 | run-d8ffdb6e4d14, K 47.28 | 6 |
| D | 234 | 24.02 / 27.10 / 27.68 | 0.1432 / 0.1488 / 0.1896 | run-ba733dba1abf, K 27.68 | 15 |
The best runs on B and C came from earlier, slightly different sources, which is why they sit just above the final source's own best. Track A's worst run, K 47.70, is the slow mode in its natural habitat.
Over the 72 hours the board ran our code 1,740 times, from 435 pushes. The PERF.md files log 342 steps across the four tracks, failed experiments included, each with its board run ID.
On the final run, on the hidden dataset, our tracks scored 88.3, 82.3, 83.5 and 72.2 speed points, for a machine score of 84.68: 3rd, behind LippInc (86.49) and another ello (84.79). Track A's 88.3 tied the best on that row. The judges' scores land by Nov 13, and I'll be refreshing the page with the composure of someone who definitely isn't refreshing the page.
One more run, after the freeze
Curiosity being what it is, I ran the top four teams' frozen code (LippInc, another ello, SALAAR and ours) through the organisers' harness on my own PC, on a freshly generated seed, 576548, that no one had ever seen. Five runs per track, sixteen tracks in all. Every single one produced the right answer, including the hash-only entries, which is a reminder that a 1-in-3,000 risk usually doesn't fire.
My PC put us first on speed. I'd love to report that as a prediction, but this whole post is about why I can't: on Track C my PC ranked one rival's design below ours, while the real box has it about 20% ahead. Same code, opposite verdict. The box is the referee, and the referee was in Falkenstein.
Links
- The repo, with every step and failed experiment in each track's
PERF.md: github.com/SaudSatopay/speed-demon-2026 - The event, by Hackathon Raptors: speeddemon.dev
Thanks to the Hackathon Raptors organisers for publishing the harness, answering questions within hours, and running a contest where the only thing you can't argue with is a SHA-256.