Last updated: 2026-06-06
Best LB score: 6256.03 (submissions_6256_03)
Local reproduction: 6256.04 β (exact match with Kaggle ORT 1.24.4)
Code-golf for neural networks. For each of 400 ARC-AGI tasks (grid transformations β rotate, crop, magnify, fill, etc.) you submit one ONNX file that correctly computes the transformation. The objective is to make each network as small/cheap as possible.
Score per task = max(1, 25 - ln(cost))
where cost = total parameters + total memory footprint (bytes) of all intermediate tensors (input and output tensors are excluded from memory).
| Cost | Score | Notes |
|---|---|---|
| 0β1 | 25.0 | Maximum. Genuinely free tasks: 179, 241 |
| β10 | 22.7 | tasks 016, 276, 309, 337 |
| β30 | 21.6 | tasks 053, 113, 116, 164, 172, 210, 311, 385 |
| β900 | 18.2 | β |
| β12,000 | 15.6 | β |
| β163,000 | 13.0 | β |
400 tasks Γ 25 pts max = 10,000 theoretical maximum. Practical ceiling estimated at ~7,900β8,100 by top competitors.
Correctness: Kaggle only runs your ONNX against a private hidden test set (not the public train/test/arc-gen). You must implement the general rule, not just memorize the visible examples.
From discussion #693088 (Tony Li, 4th place, confirmed):
Python: 3.12
numpy: 2.4.4
onnx: 1.21.0
onnxruntime: 1.24.4
onnx-tool: 1.0.1 (debug/display only β NOT used in scoring since May 4)
Local setup β neurogolf conda env at /Users/yeyang/miniconda3/envs/neurogolf/:
# Create (one-time):
conda create -n neurogolf python=3.12 numpy=2.4.4 -y
/Users/yeyang/miniconda3/envs/neurogolf/bin/pip install -r requirements.txt
# Run scorer:
/Users/yeyang/miniconda3/envs/neurogolf/bin/python local_score.py \
--submission submissions/s_XXXX_XX/submission.zip --data data/
# Score a single task:
/Users/yeyang/miniconda3/envs/neurogolf/bin/python local_score.py \
--task 233 --onnx submissions/s_6256_03/onnx/task233.onnx --data data/All 4 packages now exactly match Kaggle. Local score = 6256.04 vs LB 6256.03 β.
401 files: task{001..400}.json + neurogolf_utils/neurogolf_utils.py (~97 MB total, Apache 2.0).
Each task JSON has three fields:
train: 2β10 input/output grid pairs (ARC-AGI original, human-crafted)test: 1β3 input/output pairs (ARC-AGI original)arc-gen: ~4β262 pairs (ARC-GEN-100K synthetic augmentations, for local validation)
A grid is a list-of-lists of integers 0β9 (colors). Maximum 30Γ30.
Network I/O is always [1, 10, 30, 30] β batch 1, one-hot 10 colors, 30Γ30 padded. Smaller grids embed in the top-left corner; the rest is zero-hot ("clear").
input[0][color][row][col] = 1.0 for each grid cell
output[0][color][row][col] = 1.0 for correct channel; 0 elsewhere
6 oversize tasks (inputs >30Γ30) β scorer silently skips those examples:
021, 055, 080, 184, 202, 366
Do not assume all arc-gen examples count for these tasks.
| Rule | Detail |
|---|---|
| Static shapes only | No symbolic dims, no missing dim_value after shape inference β score 0 if violated |
| Banned ops | Loop, Scan, NonZero, Unique, Script, Function, Compress, any Sequence* op |
| File size | β€ 1.44 MB per task |
| Opset domain | Only "" or "ai.onnx" β no custom domains |
| Graph I/O | Exactly 1 input (named "input") and 1 output (named "output") |
| Initializer names | Cannot be "input" or "output" (would intersect I/O names β rejected) |
| No subgraphs | No GRAPH/GRAPHS node attributes (no functions, no local subgraphs) |
| Tensor uniqueness | No duplicate value_info entries with the same name |
No kernel_time |
Node output names containing this string are rejected |
| Positive dims | All dim_value > 0 β zero or negative dims β score 0 |
| IR version | Supported up to IR 13 (requires ORT 1.24.4) |
# Score formula
cost = memory_bytes + params
score = max(1.0, 25.0 - math.log(max(1.0, cost)))
# What counts as params:
# - All initializer elements (dims=[] scalar β 1)
# - All Constant node attribute elements (t.dims, value_floats, value_ints, value_strings)
# - Sparse initializer nnz elements
# What counts as memory:
# - All intermediate tensors (excludes the graph input and output)
# - Memory = max runtime shape across all examples Γ dtype bytes
# - Determined by ORT profiling trace (JSON), NOT static shape alone
# - Reshape/Flatten/Identity/Squeeze/Unsqueeze all count (not zero-cost!)Key: memory is max across all examples for each tensor β optimize for the worst-case (largest) input example.
This is the most dangerous silent bug. If a Conv node's bias tensor has dims[0] < output_channels, ORT reads stale heap memory and corrupts the scoring of all subsequent tasks in the same bundle β they silently score 0, even though they're correct when submitted alone.
# WRONG: bias has fewer elements than output channels
Conv(input, W, bias) # where bias.shape[0] < W.shape[0]
# CORRECT: bias.shape[0] == W.shape[0]How to detect: local_score.py will print a WARNING if it finds this pattern. Validate the full bundle, not just single files. If a task scores correctly alone but contributes 0 in the bundle β check this.
Any tensor with a dim_param (symbolic, e.g., "N") or missing dim_value β calculate_memory() returns None β score 0.
# Check locally with onnx shape inference:
graph = onnx.shape_inference.infer_shapes(model, strict_mode=True).graph
for vi in list(graph.value_info) + list(graph.output):
for dim in vi.type.tensor_type.shape.dim:
assert dim.HasField("dim_value"), f"Symbolic dim in {vi.name}"β οΈ #3 β Hidden Test Scoring (not arc-gen!)
Kaggle runs your ONNX only on a private hidden test set, NOT the public train/test/arc-gen. The May 4 update notes: "scoring timeouts β now only hidden tests are executed."
Implications:
- A network that memorizes train/test/arc-gen may score 0 on hidden tests
- Spotting overfitting: look for N similar nodes where N = number of visible tests (= lookup table pattern β @jacekwl 3rd place)
- Solutions passing all arc-gen locally can still fail hidden tests if they overfit
- @AndreyYunoshev found several tasks where his solution scored 0 on Kaggle (he calls them "hidden zero" = HZ): tasks 076, 157, 209, 219, 255, 366 (for his solutions)
Overfit-risk tasks (community-reported, prone to hidden-test failures):
018, 048, 096, 118, 192, 219, 285, 319, 355, 359
math.prod([]) = 1, not 0. A scalar constant contributes 1 parameter.
onnx-tool is no longer used in scoring (eliminated May 4), but neurogolf_utils.verify_network() still calls onnx_tool.model_profile() for display. These ops cause onnx-tool to crash or return wrong results:
| Op | Issue | Workaround |
|---|---|---|
Min |
not numpy.minimum typo β crash |
Use Greater+Cast, or Neg+Max+Neg |
ArgMin |
No value_infer |
Use ArgMax(Neg(x)) |
Clip (opset 10 attr-form) |
Crashes | Upgrade to opset 11 with min/max as inputs |
ArgMax(keepdims=0) + Reshape to [1] |
Volume-of-scalar assert | Use keepdims=1 or add Unsqueeze |
Scalar-index Gather on rank-4 |
Phantom dim | Use rank-1 [1] index + Reshape |
BitShift |
NotImplementedError | Now safe (onnx-tool eliminated from scoring) |
ArgMin |
No profile override | Use ArgMax(Neg(x)) |
ORT profiling writes a timestamped JSON trace per session. These accumulate to >150 GB if not deleted. local_score.py cleans them automatically. If you write custom scoring scripts, always call os.remove(session.end_profiling()).
A malformed ONNX anywhere in your zip can corrupt later tasks' scores in the same run (same Python process, stale ORT heap state). Always validate the complete bundle with local_score.py --submission, not just individual files.
For some complex tasks, ARC-GEN generates inputs with ambiguous (multiple valid) outputs or same input β different outputs. These cause false failures locally. The host accepts solutions that pass the hidden private tests even if some arc-gen samples are "insoluble."
The Kaggle forum contains deliberate bait for AI agents β posts with sudo reboot, rm -rf *, fake submission.zip attachments. If you feed forum content to an agent, sanitize it first.
Same file scored 18.60 alone but only contributed 0.42 in the 400-task bundle. Root cause: a malformed earlier network (Conv bias bug) corrupted ORT state. Validate both ways.
Top solutions are deterministic ONNX programs (not trained neural nets). The task is program synthesis β encode the transformation rule as directly as possible in tensor ops.
Output tensor is free. Input and output tensors are excluded from memory cost. This means Pad-to-30Γ30 at the end is essentially free. Pattern:
crop to small intermediate R (e.g., 3Γ3 or object bounding box)
β process R cheaply β
Pad R back to 30Γ30 (free-ish)
Keep intermediate tensors as small as possible.
Dtype matters. bool = 1 byte, uint8 = 1 byte, int32 = 4 bytes, float32 = 4 bytes. Use smaller dtypes for intermediate tensors. (Early solvers found 4Γ improvement by switching output to bool then casting.)
Expand is zero-copy broadcast (like PyTorch's expand_as) β no new memory allocation. Prefer over Tile where possible.
Reshape/Flatten/Identity/Squeeze/Unsqueeze DO count toward memory β they're not free. Use them sparingly.
Dynamic cropping pattern (from hengck23 #695972): If the object size varies per example but is bounded, crop to a fixed maximum size using dynamic Slice, then statically annotate the value_info shape. Since memory is max-over-examples, this is valid β ORT verifies the actual runtime shape.
| Op | Use for |
|---|---|
Conv |
Spatial filtering, pattern detection, sliding-window logic |
MaxPool |
Max aggregation over regions, morphological ops |
Where |
Conditional masking (now correctly scored) |
Gather / GatherND |
Color lookup tables, index remapping |
ArgMax |
Find dominant color channel |
Pad |
Expand to 30Γ30 (excluded from memory cost if at output) |
Reshape, Transpose |
Rearrange tensor structure |
Slice |
Crop regions |
OneHot |
Encode class indices back to channels |
MatMul / Gemm |
Batch linear transforms |
BitShift, bitwise ops |
Bit-plane encoding (cuts memory ~8Γ); now fully safe |
Expand |
Zero-copy broadcast |
Tip from hengck23 (#694628): Don't just implement the transformation β reformat the problem so it becomes trivial. Example: reshape a flattened diagonal-pattern grid to (-1, 3) so diagonals become vertical lines, then max over axis.
- Understand the transformation rule (read arc-gen generator code if needed)
- Write a NumPy oracle first, validate on all train/test/arc-gen
- Translate to ONNX, then compress
- Iterate with LLM (~5 passes per task to approach the optimum)
- Agents rush to close too early β let them revisit with history
Model benchmarks (from Chet #703462, 10 task controlled experiment):
- GLM-4.7: 0/10 deploy-safe wins
- GLM-5.1: 6/10
- Opus: 9/10 (did operator-level reformulations, solved hardest task)
- Claude produces better/nicer solutions but drifts from harness instructions
- Codex is more stable and grinds non-stop
400/400 tasks scored. Local = 6256.04 = Kaggle LB 6256.03 β
Score distribution:
[10-12): 4 tasks β task018, task233, task255, task366 (hardest)
[12-14): 89 tasks β biggest improvement bucket
[14-16): 162 tasks β largest bucket overall
[16-18): 77 tasks
[18-20): 45 tasks
[20-22): 8 tasks β very efficient
[22-25): 6 tasks β near-optimal (cost β€ 10)
25.0 : 2 tasks β task179, task241 (zero-cost, legitimately free)
Zero-cost tasks (score = 25): task179, task241 β these are legitimately free (identity or constant output).
Near-perfect (score > 22): task016, task276, task309, task337 (score 22.70, cost β 10)
| Task | Score (local) | Cost (est.) | File Size | Notes |
|---|---|---|---|---|
| 255 | 10.66 | 1,685,209 | 297 KB | Hard, community also struggles |
| 233 | 10.68 | 1,659,621 | 321 KB | Hard, community also struggles |
| 366 | 11.36 | 836,850 | 84 KB | One of 6 oversize tasks |
| 018 | 11.93 | 476,203 | 29 KB | Overfit-risk! |
| 285 | 12.09 | 402,641 | 51 KB | Overfit-risk! |
| 096 | 12.83 | 193,837 | 59 KB | Overfit-risk! |
| 101 | 12.17 | 374,445 | 20 KB | β |
| 076 | 12.29 | 330,083 | 25 KB | Hidden-zero risk (Andrey) |
| 133 | 12.81 | 196,732 | 10 KB | Tony Li also struggles |
| 158 | 12.83 | 193,436 | 27 KB | Tony Li also struggles |
| 173 | 12.93 | 175,238 | 10 KB | β |
| 077 | 13.03 | 157,913 | 8 KB | β |
| 025 | 13.15 | 128,543 | 14 KB | Tony Li also struggles |
| 398 | 13.66 | 84,295 | 9 KB | β |
| 074 | 15.01 | 21,825 | 2 KB | Timeout-risk task |
| 286 | 13.00 | 162,183 | 20 KB | Tony Li also struggles |
| 054 | 13.08 | 130,781 | 6 KB | Tony Li also struggles |
Tasks marked "Tony Li also struggles" are validated by 4th-place competitor as genuinely hard.
Tasks marked "Overfit-risk" are prone to failing hidden tests β re-verify before optimizing cost.
Tasks marked "Hidden-zero risk" may already fail Kaggle's hidden tests (score 0 despite local pass).
009, 022, 070, 074, 212, 246, 335, 350, 358, 375
| Rank | Score | Key insight |
|---|---|---|
| #1 (CroDoc) | ~7,700 | ~19.25 avg/task (~665 bytes avg cost) |
| #4 (Tony Li) | ~7,500 | 101 tasks β₯20 pts; sets target at 7750 |
| #9 (yashbhaskar) | ~7,200 | "Token is all you need" |
| #46 (Andrey) | ~6,580 | Most detailed public methodology post |
| Us | 6,256 | 400 tasks scored |
| Public baseline | 6,029 | Chet's public notebook (kaggle.com/code/jsrdcht) |
Practical ceiling: ~7,900β8,100 (multiple competitor estimates). At rank 1 (~7,700), average cost β 665 bytes/task. We're at average β 8,950 bytes/task β ~13Γ more expensive than the leader.
Key insight (@jacekwl 3rd place): averages are misleading. His median cost is ~860 bytes but average ~7,200 β dominated by a few expensive tasks. Most tasks are already very small; the worst tasks dominate the gap.
From discussion and benchmark data:
-
Program synthesis, not trained nets. Top solutions are hard-coded ONNX transformations, often with zero learned parameters. @T.-C. Chang: big score boost from "bypassing CNNs entirely β shift vectors are constant, so compile pure functional tensor math (0 parameters)."
-
Read the ARC-GEN generator code (github.com/google/ARC-GEN). The generator
.pyfor each task tells you the exact rule, fixed colors, and invariants. This eliminates guessing and overfitting. -
The rule β compact expression loop beats everything else tried (training nets, "give me 10 ideas," auto-solving). Understand the rule exactly, then express it in as few ONNX nodes as possible.
-
Minimize intermediate tensor size. The scoring is: fewer bytes in intermediate tensors = better. Work with the smallest possible representation between input and output.
-
Per-task optimization depth. Each task needs ~5 agent passes to approach its optimum. Agents settle too early. Keep revisiting with history.
submissions/
s_6256_03/ β score 6256.03, attempt 3
submission.zip β always named exactly this (Kaggle requirement)
onnx/ β extracted for local inspection/scoring
task001.onnx
...
task400.onnx
s_<score>_<attempt>/ β new submissions follow this pattern
submission.zip
Never put the zip directly at the submissions root. Folder name encodes the score.
| Date | Change |
|---|---|
| Apr 15 | Initial: cost = params + memory + MACs (via onnx-tool) |
| Apr 21 | Ignore >30Γ30 inputs; negative memory patched; versions pinned |
| Apr 24 | Constant params to be counted; static shapes to be enforced |
| Apr 28 | "Metric Migration": Compress banned; static shapes strictly enforced; Constant params counted; memory = byte sum of static shapes; initializer names β I/O names |
| Apr 30 | Where and ConstantOfShape exploits patched; zero-cost β 25 pts |
| May 4 | MACs dropped entirely. Cost = params + memory only. ORT runtime shapes replace static analysis. onnx-tool eliminated from scoring. Hidden-only testing for speed. |
| May 6 | Scalar params count as 1. ORT trace prefix fixed (no more clobbering). Multi-input/output rejected. |
| May 14 | _EXCLUDED_OP_TYPES check restored; stronger name sanitization; duplicate value_info rejected. Final stable metric. |
No known remaining exploits that survive ORT 1.24.4 verification.
import math
# Score from cost
def score(cost):
return max(1.0, 25.0 - math.log(max(1.0, cost)))
# Cost needed for a target score
def cost_for(target_score):
return math.exp(25.0 - target_score)
# Examples:
# score(1) = 25.0
# score(10) = 22.70
# score(30) = 21.60
# score(900) = 18.21
# score(9000) = 15.89 β ~floor if you materialize the full 30x30 output
# score(12000) = 15.60
# Inspect an ONNX for cost components:
import onnx, math
model = onnx.load("task001.onnx")
params = sum(math.prod(i.dims) or 1 for i in model.graph.initializer)
print(f"Initializer params: {params}")
for node in model.graph.node:
print(f" {node.op_type}: inputs={list(node.input)} outputs={list(node.output)}")- task255 & task233 β worst tasks (score ~10.7, cost ~1.7M). Large files (300 KB). Rebuild from scratch after understanding the ARC-GEN generator.
- Bottom 20 tasks β improving these 20 tasks by +3 pts each = +60 total. Most tractable gains.
- Verify overfit-risk tasks (
018, 048, 096, 118, 192, 219, 285, 319, 355, 359) β if any are already failing hidden tests, they're contributing 0 pts despite looking fine locally. - Read ARC-GEN generators for bottom tasks to understand the exact rule.
- LLM optimization loop β for each bottom-30 task: describe the rule, show current ONNX + cost breakdown, ask for a smaller reimplementation. Repeat ~5Γ.
- Deadline: July 15, 2026 (~5.5 weeks).