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

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.
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) Enter fullscreen mode Exit fullscreen mode 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.
"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.
The rules disqualify any solution code committed before kickoff, so the two days before it went into everything else. Three things paid off.
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.
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.
A wrong answer on the hidden final seed zeroes a track, so correctness came before speed. Before kickoff there were:
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.
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.
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.
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.
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.
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.
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.
This is the part I'd most want to read in someone else's write-up.
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:
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.
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.
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 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.
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 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.
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.
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.
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.
Most of what I learned that isn't in a PERF.md is about orchestration.
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.
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.
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 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.
Every board run of the exact source we froze, not just the lucky ones:
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.
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.
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.