Optimizing 400 tiny networks with agents

How small can a neural network be and still be exactly right? That was the question behind the 2026 NeuroGolf Championship on Kaggle. My team finished 4th, and much of our work was done not by us but by a pipeline of AI agents, each improving one task at a time and sharing what it learned with the others.

This post walks through how that pipeline worked, then looks closely at two of the networks it produced: one that solves a puzzle with two parameters, and one that solves a puzzle with none.

The game

NeuroGolf takes its 400 puzzles from the public training set of ARC-AGI. Each puzzle shows a handful of small colored grids before and after some transformation, and leaves you to work out the rule. Here’s the first one. Look at the three examples, then guess what the last grid becomes:

The puzzle task 001

Your turn

The rule: every colored cell becomes a copy of the whole grid, and every black cell becomes a black block.

What the network sees [1, 10, 30, 30]

Task 001, with three of its examples and a test input

click ? for the answer; click any input to see it as a tensor

Your job is to build a network, in the ONNX format, that performs the transformation. The network never sees a picture. Each grid arrives as a one-hot tensor of shape [1, 10, 30, 30]: ten 30×30 planes, one per color, with a 1 wherever that color appears. The network has to produce the output grid in the same form, exactly. Close doesn’t count. A network that gets a single cell wrong on the hidden test set scores zero for that task.

A correct network earns 25 − ln(cost) points, where the cost is its number of parameters plus the bytes of memory its intermediate tensors take up. The input and output tensors are free. Because the score is logarithmic, trimming a few bytes barely matters, but finding a different way to compute the same thing that is ten times smaller gains you more than two points. A network with a cost of 0 gets the maximum, 25.

Multiply that by 400 tasks, each with its own rule to understand, its own design to invent, and its own graph to debug, and it becomes a problem too big to solve by hand. So we built a system to do it.

The pipeline

The system had five parts: agents that improve one task at a time, a registry that remembers every model they produce, a shared catalogue of tricks, a way to check models against the real leaderboard, and a final step that assembles the submission.

Idiom system
Add / minenew mechanisms
Vote / comment
Auto-disablemisleading idioms
Summarize commentsfor prompt context
Agentic loop
Initial prompts and contexttask · graph · scorertargeted hint
Candidate propositionsnew or optimized graph
Persistent nudgingimplement · measure · revise
Task registry
Local evaluationvalidation · score · costlocal evaluation time · additional checks
Registry databasemodels by task and hash · score · timeleaderboard status: unknown · correct · broken
Verification
Probe selectionselect unknown modelsgroup and bisect failures
Leaderboard probessubmit selected groupsrecord correct/broken status
Submission
Model selectionselect eligible modelsmaximize score under local time
Final submissionone model per task
Optimization, task registry, and submission pipeline

hover a stage to follow its arrows

This is a simplified picture. Some tasks got extra instructions or approaches we developed by hand, and over the competition we kept experimenting with which prompts, context and tools to give the agents.

One agent, one task

Each agentic loop focuses on a single task, and many loops run in parallel on different tasks. When a loop starts, its prompt includes everything the agent needs so it doesn’t have to rediscover it:

  • a description of the task, with a reference implementation of the rule,
  • the current best ONNX graph for the task, with a breakdown of where its cost comes from,
  • a reference explaining exactly how the scorer counts cost,
  • the current catalogue of tricks, which keeps growing as agents add to it.

The agent can evaluate models with a tool, and vote on or add tricks to the catalogue. It also gets nudged depending on what’s holding it back. If its progress has plateaued, it’s told to rethink the algorithm from scratch. If a model fails local checks or the leaderboard, it’s asked to repair it. If evaluation is too slow, it’s asked to make the model faster.

Keeping every model

It’s tempting to keep only the best model for each task, but the best model locally can turn out to be broken. So for every task we kept many versions, each identified by a hash of the model, along with its score, how long it took to evaluate, and its leaderboard status: unknown, correct, or broken.

Every model was validated before it went into the registry. One of those checks looked for convolution bias vectors shorter than the number of output channels, which could make the grader read memory nondeterministically.

Sharing tricks between tasks

A trick an agent discovers on one task often applies to many others, but an agent working on one task has no way of knowing what an agent on another task figured out an hour ago. We fixed that with a catalogue of reusable tricks, which we called idioms.

Agents could add new idioms, vote on existing ones, and comment on how an idiom worked (or didn’t) on their task. Some idioms were also mined semi-automatically. Idioms that were repeatedly judged wrong or misleading got disabled. To keep prompts short, each agent saw only its own task’s comment on an idiom plus a summary of everyone else’s, not the full history.

Here’s one of them:

Idiom

ULP-collision finite-difference selector

Inside a terminal fp32 Einsum, use adjacent-magnitude taps [c_hi, -c_lo] and a coordinate code Q chosen so fl(c_hi*Q[r]*Q[h]) and fl(c_lo*Q[r]*Q[h]) are identical for mismatched classes but differ by a positive ULP for matched classes. A singleton dimension of a repeated runtime input can broadcast over the tap axis and keep it coupled to the source coordinates, preventing premature reduction. This realizes an exact thresholded equality selector with two tap parameters instead of a conventional multi-row coordinate table.

the fields an agent sees, and the raw entry

It’s a good example of how far the tricks went. It uses the rounding of 32-bit floats as a logic gate: two nearly opposite constants are chosen so that, inside a single Einsum, the products for the wrong cases round to exactly zero or below, while the right cases keep a tiny positive leftover. Across tasks it gathered 89 confirmations against 8 failures, and the failures mostly came from the order of operations changing how the numbers rounded.

Checking against the leaderboard

A model that passes every local example can still fail on the hidden test set, and the only way to find out is to submit it. Submissions are limited, so we checked unknown models in groups.

This works because of how the scoring behaves: a correct model scores exactly what it scored locally, and a broken one scores exactly zero. Submit a group of untested models, and if the total comes out lower than expected, at least one of them is broken. Split the group and try again, and you quickly narrow it down to the culprit.

Picking the final submission

The last step is choosing one model per task. You’d think you’d just take the highest-scoring one each time, but that can produce a submission too slow to finish. Some operations, Einsum especially, need only a few parameters but can take hours to run through all the examples on a single CPU.

So we treated runtime as a budget. We picked the set of models that roughly maximized the total score while keeping their combined local evaluation time within the limit.

What came out

Our final submission scored 8,142.02 locally. Nine tasks hit the perfect score of 25.

local score
8,142.02
nodes in total
5,747
one-node models
224
nodes in the largest model (task 023, 18.151)
491
  • One node, Einsum193
  • One node, another op31
  • Several nodes, at least one Einsum156
  • Several nodes, no Einsum20
Perfect score of 25
  • 053
  • 067
  • 087
  • 129
  • 135
  • 140
  • 179
  • 241
  • 326
The 400 final models, one square each, grouped by kind

hover a group of squares

The networks ended up remarkably simple. More than half of them, 224 of the 400, are a single ONNX node, and 193 of those single nodes are an Einsum. 349 networks use at least one Einsum. Across the whole submission there are 5,747 nodes. The biggest network, for task 023, has 491 nodes and still scores 18.151.

Two networks up close

Recoloring with two parameters

Task 267 shows a shape in one color and a single marker pixel in another. The output recolors the shape with the marker’s color and erases the marker. Our network does this with one Einsum node and a constant holding two numbers.

Task 267: recolor the figure using the color in the bottom-left corner, then set that corner pixel to 0.

Task examples
Input
Output
Final graph
input[1, 10, 30, 30]
Einsum
output[1, 10, 30, 30]
Einsum equationreab,qecd,refg,r,q,reij,rklm,reno,qkst,qkuv,qexy,qezA,qkBC,reDE,qeFG,pkhw → pehw

Hover an operand: 14 read the input and 2 read T.

cost
2
parameters
2
intermediate bytes
0
score
24.307

hover the equation; click T to see its bits

An Einsum multiplies tensors together and sums over the axes you choose, all described by a short string of letters. In this one, p is the batch axis, h and w are the rows and columns, k is the input color, and e is the output color. The last operand, pkhw, says which color each pixel has. Summing over k while keeping e decides each pixel’s new color independently, at every position.

Every other copy of the input in the equation uses its own spatial letters, so those get summed away entirely. What’s left of each copy is just how many pixels of color k or color e there are in the whole grid. That turns the problem into a question about color counts: pixels of the shape’s color should become the color that appears exactly once (the marker), the marker itself should become background, and the background should stay background.

The two numbers in T are almost exact opposites, stored as the float32 values 0xC5BCCB9A and 0x45BCCF43. Used along the q and r axes, they act as a selector. With the operands in exactly this order, float32 rounding makes the wrong color pairs cancel to zero or below, while every right one keeps a small positive leftover, which is enough for the output to pick the right color. Rearrange the equation into something mathematically identical and the rounding changes and the trick stops working. Here, the order of the operands is part of the program.

The cost is 2: the scorer counts the elements of the constant, not its bytes, so T costs exactly two parameters. Reusing the input or T inside the equation is free, and since the only node writes straight to the output, there’s no intermediate memory to charge. The score is 25 − ln(2) = 24.307.

Shifting with a pooling layer

Task 053 moves every colored pixel down by one row. Our network is a single LpPool node, with no parameters at all.

Task 053: move every colored pixel down by one row.

Task examples
Input
Output

Final graph
input[1, 10, 30, 30]
LpPoolp = 2
output[1, 10, 30, 30]
LpPool attributeskernel [2,1] · dilation [3,1] · stride [−2,1] · pads [−2,0,−82,0]
cost
0
parameters
0
intermediate bytes
0
score
25.000

hover an output row

LpPool normally summarizes each window of a tensor, the way max pooling does. Here its settings turn it into something else: a way to copy rows. A vertical kernel of size 2 with dilation 3, negative padding, and a row stride of −2 makes output rows 0, 1 and 2 read input rows 2, 0 and 1. In this task the bottom row of the input is always empty, so this is exactly a shift down by one. The horizontal kernel has size 1, so columns and colors are left alone, and every row past the 3×3 grid stays empty and is ignored when the output is read.

Each window contains at most one 1, and with p = 2 the pool just returns that value unchanged. There are no constants and the node writes straight to the output, so the cost is 0 and the score is the maximum, 25.

Looking back

Networks like these are hard to arrive at directly. They come from many rounds of trying to make a graph smaller and keeping what works. Running those rounds across 400 tasks at once depended less on any single agent than on the structure around them: context that told each agent what was already known, a registry that never threw a working model away, a shared catalogue so discoveries spread, a cheap way to catch broken models, and a final step that respected the runtime limit.

This post is adapted from our team’s writeup on Kaggle.