Reinforcement learning · p5.brush · Qwen3-8B

An 8B model learns to paint watercolours by writing code.

The model writes JavaScript, a sandboxed browser paints it, a score says how good it is, and the model learns from its better attempts. Three versions, three lessons, every painting rated by me.

v1 · flowers
22/24

held-out wins after 20 RL updates

v2 · cats from scratch
1/169

paintings I loved, rated blind

v3 · the cat kit
33/40

held-out paintings I loved, rated blind

01

Flowers, from scratch

A small reproduction of Hugging Face's train to paint with code. At first the model could not call the library correctly; after a prompt fix and 20 GRPO-style updates on 2× H100, scored by HPSv3, it beat its untrained self on 22 of 24 held-out pairs.

Held-out, same seed: untrained → trained

8 flowers × 3 seeds; half were never seen in training. Click a painting for its code.

UntrainedAfter 20 updates
Mean reward0.0600.424
Valid code10 / 2423 / 24
Paired wins–22 / 24

Caveats: the evaluation score is the training reward, and the paintings converged on one composition.

02

Cute cats, from scratch: an honest failure

My theme, my 30 training and 10 secret subjects, my ratings. The untrained model only copied the example cat, so I taught it first (SFT on 84 sketches I approved), then ran 35 RL steps on a reward checked against my taste. The numbers rose; my blind ratings did not. Across 169 paintings from five variants I loved one, and my own ratings of the same paintings agreed only half the time. Dictating thousands of coordinates without seeing the picture hit a ceiling.

Held-out subjects, same seed

Blind, strict mean (0–2)Round 1Round 2
Original Qwen3-8B0.470.30
v2 SFT + RL0.470.30
v2 SFT0.07–
RL only, no SFT–0.07
v2.1 (no example, KL to SFT)–0.10

The sketches it learned from (ones I loved)

03

The cat kit: compose, don't dictate

I wrote a small watercolour library that draws cats, props and scenes well every time. The model now writes about ten lines that compose the picture. I checked the ceiling first (I loved 30 of 50 kit paintings), taught it 255 compositions I rated, then ran 31 RL steps rewarding image quality and "shows this exact subject".

// what the model writes
paper(); weather('snow');
cat(300, 500, 1, 'sit', 'tabby', 'up');
wear('scarf', '#d95f6a');

Held-out subjects: v2's best against v3

Blind round 3 · 127 paintingsLoveOkayNoStrict mean
v2 best110270.30
Untrained + kit13100.68*
v3 SFT28711.57
v3 SFT + RL33421.75

* Only 14 of 40 rendered: one kit effect crashed the headless renderer (fixed since). I loved 13 of the 14.

The kit's ceiling: compositions I wrote and loved

04

How one training step works

  1. Promptsubject and rules, plus the kit's functions in v3
  2. Qwen3-8B + LoRAeight programs per subject, sampled at temperature 1
  3. Safety gatean allowlist of drawing calls, numbers and strings; anything else scores 0
  4. Sandboxed Chromiump5.js and p5.brush render a seeded 600 × 600 painting
  5. RewardHPSv3 image quality, plus a VLM "shows the subject" check in v2 and v3
  6. Updateadvantage = reward − group mean; nudge the LoRA toward the better ones

Spend on GPUs from SF Compute: v1 $24.50 · v2 $42.54 · v2.1 and control $28.77 · v3 $23.33.

the code that painted it