A 360M-parameter code model trained from random initialization on two workstation GPUs, and what decontaminating its instruction data did to its benchmark scores.
We trained a 360.8M-parameter decoder-only language model from random initialization on 20.0 billion tokens of Python and educational English, on two NVIDIA RTX A4500 cards in 5 days and 23 hours. The tokenizer, model, training loop, sampler, fine-tuning and evaluation harness were all written by hand in PyTorch. The base model scores 13.4% pass@1 on HumanEval, level with OpenAI's Codex-300M. Supervised fine-tuning on 82,464 instruction pairs raised the published score to 26.8%. After an outside reader questioned that number, we found that one of the two public instruction sets contains the problem text of 60 of HumanEval's 164 problems, and that Magicoder's decontaminated redistribution of it still contains 52. Removing every training example that matched a HumanEval or MBPP problem and retraining lowered HumanEval to 22.6%, while a control run on the original data with a different seed scored 26.2%. None of the model's solutions is a copy of a benchmark solution: the contamination worked as practice on benchmark-style problems, it was worth about four points, and it spread to problems that were never in the data. We report the decontaminated figure as the model's capability, measure run-to-run variance that the original report omitted, and describe five measurement errors that were caught only by reading logs.
Base-model HumanEval pass@1 after 20B tokens on two workstation GPUs, level with Codex-300M (13.2%). About 143 hours and an estimated 79 kWh.
HumanEval problems whose docstring appears in evol-codealpaca-v1. Magicoder's decontaminated copy removed 89 rows and still contains 52.
Chat-model HumanEval before and after removing benchmark-matching training data. A same-data rerun with a new seed scored 26.2%.
Chat-model passes within 0.9 similarity of a canonical solution. The contamination taught the benchmark's style, not its answers.
Vendors describe language models with adjectives. We wanted to describe one with measurements: what it costs to build a small model end to end, what the public training data actually contains, how capability appears during training, and how far a published benchmark number can be trusted, including our own. The model was scoped to one job, short Python programs and beginner explanations, so that every measurement could be checked against a clear standard.
The project was also built to be reproduced. Every number in this report comes from code and logs in a public repository, organized as nine chapters that a reader with one or two GPUs can follow.
All training ran on one machine: two RTX A4500 cards (20 GB each, Ampere, NVLink), an AMD Ryzen 9 7900, 32 GB of RAM and a 1 TB NVMe drive, under PyTorch 2.11 with CUDA 12.8. Plans were made from measured figures, which diverged from the specification in both directions.
| Quantity | Specification or plan | Measured |
|---|---|---|
| Dense bf16 throughput per card | 94 TFLOPS | 78 TFLOPS |
| NVLink bandwidth | n/a | 41 GiB/s |
| Model FLOPs utilization, full run | 35% assumed | 69.1% |
| Throughput, median | n/a | 38,900 tokens/s |
| Time per 524,288-token step, mean | n/a | 13.46 s |
| Run length | 9 days | 5 days 23 hours |
| Energy | n/a | about 79 kWh, estimated |
The energy figure is estimated from rated draw at the 100% utilization both cards reported for the whole run; no meter was attached. At $0.27/kWh it is about $21.
Four public sources, tokenized into 27.5B tokens of shards from which 20.0B were sampled by mix weight: Stack-Edu Python (45%, classifier-filtered educational code), StarCoderData Python (20%), FineWeb-Edu (25%) and Cosmopedia v2 (10%). Stack Overflow was excluded because its CC-BY-SA license would impose share-alike terms on the weights. The final 10% of training used an anneal mix of 60% Stack-Edu, 25% Cosmopedia and 15% FineWeb-Edu.
Two properties of the data were found by inspection. 48% of StarCoderData files begin with a <reponame>/<filename>/<gh_stars> metadata line that is not Python; left in, it would have taught the model to open every file with noise. After those lines were stripped, about 6% of code files still failed Python 3's parser, mostly Python 2, and were dropped.
A byte-level BPE with 32,768 entries (256 bytes, 32,496 merges and 16 reserved special tokens), trained on an 800 MB sample in the training-mix proportions. Digits are split one per token and runs of whitespace are kept whole, so a Python indent is a single token. On held-out Python it encodes 3.69 bytes per token, against 2.16 for GPT-2's 50k vocabulary and 3.80 for GPT-4's 100k vocabulary: 41% fewer tokens than GPT-2 for the same code. On English it is 8% less efficient than GPT-2, the trade the plan asked for.
A Llama-style decoder with RMSNorm, rotary position embeddings, SwiGLU feed-forward layers, no biases and tied embeddings: 24 layers, width 1,024, 16 heads, feed-forward width 3,072 and a 2,048-token context, 360.8M parameters. Next-token prediction, with half of all code documents rearranged into fill-in-the-middle form. AdamW (betas 0.9 and 0.95, weight decay 0.1), gradient clipping at 1.0, bf16 autocast, 1,000 warmup steps to a peak learning rate of 4e-4, cosine decay to 4e-5, then linear decay to zero over the final 10% as the mix switched to the anneal mix. Data-parallel across both GPUs with torch.compile, 38,146 optimizer steps of 524,288 tokens.
Two epochs at a learning rate of 5e-5 on one GPU (93 minutes), with the loss masked to the assistant reply. Data: Magicoder OSS-Instruct and evol-codealpaca-v1, filtered to replies whose code parses as Python 3 and fits in 2,048 tokens (36,964 and 47,002 examples), plus 45 handwritten examples repeated four times covering the model's identity, its limits and beginner concepts. 82,464 training examples and 25.2M supervised tokens.
The run completed without a loss spike or divergence. After warmup the gradient norm exceeded the clipping threshold on 6 of 37,146 steps, with a median of 0.16. A restart at step 13,001 reproduced the pre-restart loss to four decimal places, because the data loader position and every random-number state are part of the checkpoint.
The base model completes a function from its signature and docstring. Its competence has a sharp edge: all 22 of its HumanEval passes fall in the first 61 problems, which are roughly the easier ones. Asked a question in prose, it continues the text instead of answering it; asked to continue a sentence about Providence, Rhode Island, it called the city the largest in the United States and repeated variations of that claim for 200 tokens.
Instruction tuning taught three things: the chat template means a question is being asked, the reply should be an answer, and the end-of-turn token means stop. Held-out reply loss fell from 1.33 to 1.03 in the first 200 steps, which is the template, and to 0.94 by the end of the second epoch. Most of the benchmark gain is format: the base model's HumanEval failures include 15 completions that ran past the function and hit the token limit, and the chat model has 4.
| Metric | n | Base | Chat, as published |
|---|---|---|---|
| Pass rate | |||
| HumanEval | 164 | 13.4%[8.2, 18.6] | 26.8%[20.0, 33.6] |
| HumanEval+ | 164 | 11.0%[6.2, 15.8] | 25.6%[18.9, 32.3] |
| MBPP (sanitized) | 257 | 20.2%[15.3, 25.1] | 38.9%[32.9, 44.9] |
| HumanEval failures by type | |||
| Runs, returns the wrong answer | 105 of 142 | 102 of 120 | |
| Syntax error, mostly truncation | 15 | 4 | |
| Name, type and recursion errors | 22 | 14 | |
What fine-tuning did not change is the shape of the failures. Most are code that runs, looks idiomatic and returns the wrong answer. The model's best-known example writes a correct is_prime and then, in the usage examples beneath it, states that is_prime(2) and is_prime(5) return False. The conversational layer is thin in the same way: 45 handwritten examples gave the model a stable identity, and three greeting examples against 82,000 programming questions left a lowercase "hi" with an empty reply.
Two days after release, a reader pointed out that evol-codealpaca-v1 is the dataset Magicoder redistributes as a decontaminated version, and that the project had never checked its instruction data for benchmark overlap. The concern was specific. A doubling from 25M supervised tokens is where leaked solutions tend to surface, and the small HumanEval to HumanEval+ drop we had read as evidence of fully correct solutions is also what memorized canonical solutions would produce. Three outcomes were possible: the score was earned, the model was reciting answers, or the data was teaching the benchmark some other way. We separated them in three steps.
None of the chat model's 44 HumanEval passes reaches 0.9 similarity to its canonical solution. The highest is 0.73, on a problem whose canonical solution is one line, and most are below 0.3. By this test the model is not reciting answers.
| Dataset | Examples | Docstrings found | Canonical solutions found |
|---|---|---|---|
| evol-codealpaca-v1 (used) | 111,272 | 60 problems, 156 examples | 8 problems, 67 examples |
| Magicoder-Evol-Instruct-110K (filtered copy) | 111,183 | 52 problems, 116 examples | 8 problems, 66 examples |
| Magicoder-OSS-Instruct-75K (used) | 75,197 | 0 | 1 problem, 1 example |
The matches are benchmark problems in practice-exam form: reworded, extended with an extra requirement or ported to Java, with full solutions. HumanEval/43, pairs_sum_to_zero, appears with its original docstring, its doctests and a working solution under an added instruction to make it linear time. Evol-Instruct grows a dataset by repeatedly rewriting seed problems, and some of the seeds were benchmark problems. Switching to the filtered copy would not have fixed it.
We wrote a filter that drops any instruction example containing a HumanEval or MBPP problem statement or half of a reference solution, using the same tests and covering all 427 sanitized MBPP problems rather than the test split alone. It removed 452 source examples touching 63 HumanEval and 64 MBPP problems; 271 of them had survived the existing Python and length filters, so the training split went from 82,464 to 82,198 examples. The decontaminated model was fine-tuned from the same base checkpoint with every other setting unchanged.
On HumanEval the two runs on the original data agree to within one problem (44 and 43 passes, 40 in common), while the decontaminated run is six to seven problems lower. Against the published run it lost 8 problems and gained 1 (p = 0.039); against the seed-2 run the difference is similar and not significant on its own (p = 0.11). On MBPP the picture reverses: the two original-data runs differ by 11 problems, and the decontaminated run sits on the lower one. Only 10 MBPP problems matched removed data, and the published 38.9% is best read as a favorable draw from run-to-run variance.
| Subset | Problems | Base | Published | Seed 2 | Decontaminated |
|---|---|---|---|---|---|
| In the removed data | 63 | 5 | 14 | 17 | 13 |
| Not in the removed data | 101 | 17 | 30 | 26 | 24 |
If the contamination worked by memorization, the lost passes would sit on the problems that appeared in the data. They do not. Against the mean of the two original-data runs, the decontaminated model is about four points lower on both subsets: 2.5 of 63 problems and 4 of 101. Practice on HumanEval-style questions helped on HumanEval-style questions in general.
We report the decontaminated checkpoint as the model's capability: 22.6% on HumanEval, 21.3% on HumanEval+ and 35.0% on MBPP, with run-to-run variance of up to about four points on MBPP.
| Model | Parameters | Year | HumanEval |
|---|---|---|---|
| GPT-3 | n/a | 2020 | 0.0% |
| GPT-Neo | 1.3B | 2021 | 4.8% |
| GPT-J | 6B | 2021 | 11.6% |
| Codex-300M | 300M | 2021 | 13.2% |
| Elroy base | 360M | 2026 | 13.4% |
| Elroy chat, decontaminated | 360M | 2026 | 22.6% |
| Elroy chat, as first published | 360M | 2026 | 26.8% |
| Codex-12B | 12B | 2021 | 28.8% |
Five errors in this project would have produced wrong published numbers or a failed run. None raised an exception that stopped anything; each was found by reading a log, a sample or a failure record instead of a summary.
MemoryError, because the expanded test files exceed a 1 GB subprocess limit. At 4 GB the score is 11.0%.NameError. Showing the test assertions, as the MBPP paper does, gave 38.9% for that run.With current open data and published architecture choices, a model at the 2021 frontier for its size trains on hardware a small firm can buy, in under a week, for about $21 of electricity. The data work dominated both the effort and the outcome: the run was uneventful because the corpus had been read before it was tokenized.
A published score is a property of a model, its training data, its evaluation harness and a random seed, and a single run reports all four as one number. In this project the harness moved one score from 3.0% to 11.0% and another from 2.5% to 38.9%, the training data moved HumanEval by about four points without any detectable copying, and the seed moved MBPP by four points. None of this is visible in a headline figure. Any model provider, including us, should be asked for the decontamination method, the harness and the number of runs behind a score.
The redistribution offered as the clean option removed 89 rows and kept 52 of 60 contaminated HumanEval problems. Exact-match filters miss rewritten problems, and rewriting is how Evol-Instruct data is made. A filter that also matches solution n-grams and problem-text prefixes is cheap; ours runs in minutes and is in the repository.
The model's typical error is code that runs, looks right and is wrong, delivered after fine-tuning in a confident, helpful register. Larger models make this error less often, not differently, and it becomes harder to notice.
Pretraining ran from 14 to 20 September 2026 and fine-tuning, decontamination and the seed control from 20 to 22 September, on the two RTX A4500 cards described above with PyTorch 2.11, CUDA 12.8 and Python 3.12. The code, the nine-chapter write-up, every benchmark completion and failure record and the training logs are at github.com/strifero/elroy under Apache-2.0, including the contamination check (scripts/check_contamination.py) and the filter (elroy/decontam.py). The base and chat weights are at huggingface.co/strifero/elroy-350m-base and huggingface.co/strifero/elroy-350m-chat. The chat weights released there are the original run that scored 26.8%; the decontaminated checkpoint this report measures is not yet published.
We thank the reader whose review prompted section 5. They identified the problem, proposed the first test, and were right.