Same GRPO recipe on three from-scratch LLMs (353M/316M/672M) gave three different outcomes, with no clean relationship to scale [P]

Same GRPO recipe on three from-scratch LLMs (353M/316M/672M) gave three different outcomes, with no clean relationship to scale [P]

I trained three LLMs from scratch in raw PyTorch then post-trained each one with SFT and then GRPO. Same process every time: same synthetic arithmetic curriculum, same reward function, same hyperparameters, same KL coefficient.

Pre-training went as expected, the val loss went down as the model got more modern techniques (V1 to V2) and bigger (V3 being the biggest). However, GRPO hurt both V2 and V3 and I'm not sure why.

Setup

V1 V2 V3
Params 353M 316M 672M
d_model / layers 1024 / 24 1024 / 24 1536 / 24
Attention MHA Differential + GQA 4:1 XSA + GQA 4:1
Tokens 10B 10B 30B
Data FineWeb-Edu FineWeb-Edu FineWeb-Edu + code + math

Pre-training val loss went 2.8659 → 2.7844 → 2.5885.

Results

WikiText word perplexity across the three stages, all on lm-evaluation-harness with the same task versions and shot counts:

       base    SFT     GRPO     SFT→GRPO
V1     32.86   51.31   51.40    +0.2%
V2     31.28   46.81   71.06    +52%
V3     22.30   32.11   33.65    +5%

SFT hits all three on this eval, which I expected at this scale. Also interesting to see that the degradation gets smaller as the models get bigger (+56%, +50%, +44%).

GRPO is the weird one. V1 barely moved, V2 fell heavily, V3 degraded a bit. The smallest model was the least affected and the middle one was the worst, which isn't the pattern I'd have guessed. Downstream tasks moved the same way as perplexity in each case (arc_easy dropped about 6 points on V3 from SFT to GRPO).

The models did learn the thing GRPO trained them on. V3 mastered 4 of the 5 curriculum stages, the other two got 3. But it just didn't transfer: GSM8K stayed at basically 0, and the models got so committed to writing out long solutions that they often wouldn't stop generating (my fault when I did the training).

Caveats

This isn't a controlled experiment. Between V2 and V3 I changed the parameter count, the token count, the data mix and the attention mechanism at the same time (went from DiffAttn to XSA), so I can't attribute anything cleanly. KL coefficient was 0.02 for all three, with the SFT policy frozen as the reference and a k3 estimator. The whole series cost me about $750, which is why there are no ablations, I just couldn't afford them. Otherwise I would also have tried with different KL coeffs.

Someone raised two confounds after I published:

  1. GRPO trained on a bare solver template while SFT used a chat format. So part of what I'm calling degradation is me evaluating a policy outside its own training distribution. WikiText perplexity is format-independent and still moves a lot, but the downstream numbers are partly confounded.
  2. Nothing in my reward rewarded stopping. It just checks that a correct parseable number shows up somewhere, no length penalty.

Also something I only noticed afterwards: I never re-evaluated the earlier curriculum stages once the model advanced past them. So right now I can't tell the difference between "GRPO degraded general capability" and "sequential curriculum training made it forget the earlier stages." I will try to check that soon.

Inference

At the end, I wrote a KV cache from scratch (GQA-aware, per-request cache object rather than storing state on the module). To check it was right I ran a fixed sequence two ways, once as a single full forward pass and once as prefill-then-decode, and compared the logits: max difference 1.4e-06 against a 1e-4 tolerance.

Speedup generating 100 tokens: 3.7x from a 32-token prompt, 6.2x at 128, 10.1x at 512.

If you want to check

All nine checkpoints are on the Hugging Face, and there's a Space where you can send the same prompt to the base, SFT and GRPO versions of the same model and see the difference directly.

The GRPO variance is the bit I'd most like other people's take on. Happy to answer anything.

u/john_enev — 2 days ago

Nine from-scratch LLM checkpoints on the Hub, plus a Space to compare base vs SFT vs GRPO side by side

HF Space

I coded and trained from scratch 3 models (9 checkpoints total) in PyTorch. On top of that I built a HF Space where you can test the models and compare them.
The playground
The model weights

The Space: on the left you have a 3x3 grid for the models (one row per checkpoint: base, SFT, GRPO, one colum per model version: V1, V2, V3). You can easily select one and then ask a prompt in the chatbox.

The weights are safetensors. The Space downloads them lazily when they are first being used (then saved in memory).
I built a Docker Space running FastAPI (wrapped around my token generator). I had claude build a custom UI to interact with the models. It was super seamless (I was actually surprised by that).

I was very happy to see it was running pretty well (again, the models are small) on the CPU tier. No need for a GPU!

Some more details on the models (I tried to implement some interesting techniques. The val losses went down V1 > V2 > V3):
- V1 was a "basic" modern architecture using SwiGLU, MHA, RoPE, etc.
- V2 I tried to implement some modern techniques like GQA, Muon, DiffAttn. I also implemented mHC but it was too harsh on throughput so didn't use it in the full run.
- V3 I doubled the params of V2 and used XSA instead of DiffAttn. Finally wrote a KV cache for serving (generating 100 tokens from a 512-token prompt went from 278s to 28s): I quickly verified it was correct was running some short inference both with and without it (difference was within noise).

One of the biggest learnings was that at my scale, in my setup, GRPO did end up degrading the models general capabilities. I wish I could done more proper ablations to study the issue (and also to isolate the impacts of each technique) but I had a limited budget (renting GPUs is not cheap...).

I hope this is useful to people! There is a link to my github (everything is open) and my articles about the whole journey.

u/john_enev — 3 days ago

I trained three LLMs from scratch (353M–672M) and served them on HF Space for about $750. Some of the lessons I learned.

A couple of months ago I decided to follow Karpathy's nanochat and code/train my own models. Ended up training 3 versions for 9 total checkpoints (base, SFT, and GRPO for each). Wanted to challenge myself so went with raw PyTorch (with mentorship from claude!).

V1 was a "basic" modern architecture using SwiGLU, MHA, RoPE, etc.
V2 I tried to implement some modern techniques like GQA, Muon, DiffAttn. I also implemented mHC but it was too harsh on throughput so didn't use it in the full run.
V3 I doubled the params of V2 and used XSA instead of DiffAttn. Finally wrote a KV cache for serving (generating 100 tokens from a 512-token prompt went from 278s to 28s): I quickly verified it was correct was running some short inference both with and without it (difference was within noise).

The pre-training val losses went from 2.87 → 2.78 → 2.59. As planned, really, no surprises there.
I started encountering issues with post-training. SFT worked but with a hit to general knowledge. But GRPO was more of a miss at my scale: I tried to have the model learn some arithmetic (it did in a small way) but it degraded quite a bit when it came to general abilities.
The 3 models didn't survive GRPO. I'm not sure why (it wasn't a controlled experiment), but it was interesting.

some of the evals for V3

Some of the lessons:
- Proper eval measurement from day 1 with a held out set. Made it easier to compare V1/V2/V3 (at first I didn't have it, and I had to do it afterwards, cost me some time).
- If I could have, I would have changed one variable at a time to have some proper ablations (for example between V1 and V2 I added 5 new techniques). But I was budget constrained.
- The training loop was reasonably ok, it was the whole infrastructure/piping that was more complicated.
- I wrote some assertions about the model configs before my runs, I caught a couple of issues that way (ie. number of parameters in a model: I caught a wrong config because of that).
- Do lots of smoke tests, and short runs on cheaper GPUs before the big ones. And obviously track everything (I used wandb).

You can test the models on this playground.
The weights are also on HF.

And the code is on GitHub.

Hopefully that's interesting to some of you, if you want to learn more, I wrote quite a bit about the whole process and the learnings there, and happy to answer any questions.

reddit.com
u/john_enev — 4 days ago