Blog · Kaggle

Solving ARC Prize 2025 With No Pretraining: A Per-Puzzle CompressARC Solver Built for 12 Hours on Four GPUs

ARC Prize 2025 · 9 min read

The bet: no pretraining, one network per puzzle

Most competitive ARC entries lean on scale — pretrain a big model on synthetic ARC-like data, or prompt a frontier LLM. For ARC Prize 2025 I deliberately took the opposite bet. My solution is in the CompressARC lineage: it uses no pretraining, no external data, and no search. Instead, for every single puzzle it initializes a small neural network from random weights and trains it, at test time, on only that puzzle's own train pairs. Then it uses the fitted network to predict that puzzle's test output. Hence the notebook's name: "ARC-AGI Without Pretraining."

CompressARC (Isaac Liao and Jyo Pari, 2025) frames each puzzle as a minimum-description-length compression problem: a tiny (~76K-parameter) network is fit per task using a VAE-style loss so that gradient descent — not combinatorial program search — does the "reasoning." It's an intriguing idea because it treats generalization as compression: the only thing the model ever sees is the handful of demonstration pairs in front of it. My work here builds directly on that method; the interesting part I want to be honest about is that the hard problem for me wasn't the model, it was throughput.

Why the constraints make this hard

ARC Prize 2025 runs on the harder ARC-AGI-2 benchmark. Submissions execute as a Kaggle notebook, offline (no internet, no external APIs), inside a fixed hardware sandbox, and must finish within a 12-hour wall-clock window. Scoring is pass@2: for each test input you submit exactly two guesses, and you get credit if either is an exact grid match. Winning open-source solutions must also be CC0-licensed to be prize-eligible.

Now put those two things together. CompressARC's original blog setup solved puzzles in series, badly underutilized a single RTX 4070, and — as the notebook markdown candidly admits — "blew past the time budget." A method that fits a fresh network per puzzle is embarrassingly parallel, but only if you actually schedule it that way. So the whole engineering story of my notebook is turning a serial, GPU-starved research prototype into something that saturates all the CPUs and GPUs Kaggle gives you and lands inside 12 hours.

What the notebook actually is

To be precise about the source: the network internals — the CompressARC model, the per-puzzle training loop, grid encoding, and the VAE/MDL loss — live in imported modules (solve_task.py, layers.py, preprocessing.py) that ship in the /kaggle/input/compressarc dataset. My notebook is the orchestration and scheduling layer that sits on top of them. It imports preprocessing, calls solve_task.solve_task(...) once per puzzle, and is responsible for memory probing, parallelization, timing, and writing submission.json. One concrete model-side change I made was rewriting layers.direction_share() to run faster, which bought roughly a 5–10% speedup.

The per-puzzle call signature the scheduler drives is essentially:

args = (task_name, "test", end_time, n_iterations,
        gpu_id, memory_dict, solutions_dict, error_queue)
p = multiprocessing.Process(target=solve_task.solve_task, args=args)
p.start()

Each spawned process trains one puzzle's network for n_iterations gradient steps on a chosen gpu_id, and writes its predicted grids back into a shared solutions_dict — which becomes the pass@2 submission. Global setup pins torch.float32, enables cudnn.benchmark and TF32 matmuls, and uses the spawn start method so each CUDA process is clean.

The scheduling strategy

The plan described in the notebook is a three-phase probe-then-pack:

The scheduler itself (parallelize_runs) reads each GPU's free memory with torch.cuda.mem_get_info, subtracts a 6 GB safety buffer, and then greedily launches puzzle processes while two constraints hold: the GPU still has enough memory quota for that puzzle's measured usage, and the number of live processes stays under the CPU count. When a process finishes it returns its memory quota to the pool so the next puzzle can start. There's a hard timeout guard (MAX_TASK_RUNTIME = 600s) that terminates any puzzle that runs away, plus a global end-time check so the run stops cleanly before the deadline rather than being killed mid-write.

flowchart TD
  A["Load ARC-AGI-2 test tasks"] --> B["Read free GPU memory, minus 6GB buffer"]
  B --> C["2-step memory probe per puzzle"]
  C --> D["10-step timing probe at chosen parallelization"]
  D --> E["Compute max steps that fit 12h budget"]
  E --> F["Pack puzzles across GPUs under memory + CPU limits"]
  F --> G["Per puzzle: train tiny net from scratch, ~2300 steps"]
  G --> H["Form 2 predictions per test input"]
  H --> I["Write submission.json, pass@2"]

Augmentation and pass@2 formation

CompressARC's fitting relies on equivariance and augmentation of the puzzle's own examples (transposes, color/geometry symmetries) so that a network fit to a few pairs isn't just memorizing pixels. The two required pass@2 attempts per test input are produced by solve_task from the fitted per-puzzle model and collected into solutions_dict; my orchestration layer's job is to make sure every task actually gets its two attempts written before the clock runs out. I want to be transparent that these prediction/augmentation details are implemented inside the imported solve_task module rather than in the notebook cells I authored, so I'm characterizing them at the method level rather than quoting exact transforms.

Honest results and limitations

A few things I won't dress up:

What I like about this entry is that it's an honest, from-scratch bet: no giant pretrained prior, just gradient descent compressing each puzzle on its own terms. It's not the highest-scoring recipe on ARC-AGI-2, but it's a clean demonstration that "learning at test time, per instance" is a real and schedulable strategy — and that on constrained hardware, the systems engineering around a method matters as much as the method itself.

← Back to all posts