trifero.ai Technical report
Language modelsOctober 2026

Elroy, from scratch

A 360M-parameter code model trained from random initialization on two workstation GPUs, and what decontaminating its instruction data did to its benchmark scores.

Abstract

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.

13.4% from scratch

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.

60 of 164

HumanEval problems whose docstring appears in evol-codealpaca-v1. Magicoder's decontaminated copy removed 89 rows and still contains 52.

26.8% to 22.6%

Chat-model HumanEval before and after removing benchmark-matching training data. A same-data rerun with a new seed scored 26.2%.

0 near-copies

Chat-model passes within 0.9 similarity of a canonical solution. The contamination taught the benchmark's style, not its answers.

1The question

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.

2Method

Hardware, measured rather than assumed

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.

Table 1. Specification against measurement. Utilization is model FLOPs utilization against the measured throughput.
QuantitySpecification or planMeasured
Dense bf16 throughput per card94 TFLOPS78 TFLOPS
NVLink bandwidthn/a41 GiB/s
Model FLOPs utilization, full run35% assumed69.1%
Throughput, mediann/a38,900 tokens/s
Time per 524,288-token step, meann/a13.46 s
Run length9 days5 days 23 hours
Energyn/aabout 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.

Data

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.

Tokenizer

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.

Model and training

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.

Instruction tuning

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.

Evaluation

3Pretraining

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.

1.5 2 2.5 3 0B 5B 10B 15B 20B training tokens anneal mix Training loss: 1.58 at 20B tokens Training loss 1.58
Figure 1. Training loss, exponential moving average, against tokens seen. Loss starts at 10.58, close to ln(32,768), the loss of a uniform guess over the vocabulary. The step at 18B tokens is the switch to the easier anneal mix, not a jump in learning. Hover the line for values.
Stack-Edu (Python)StarCoderData (Python)FineWeb-Edu (English)Cosmopedia (synthetic)
1 2 3 4 0B 5B 10B 15B 20B training tokens Stack-Edu: 1.26 at 20B tokens Stack-Edu 1.26 StarCoderData: 1.22 at 20B tokens StarCoderData 1.22 FineWeb-Edu: 2.85 at 20B tokens FineWeb-Edu 2.85 Cosmopedia: 1.69 at 20B tokens Cosmopedia 1.69
Figure 2. Validation loss on held-out data from each source. Synthetic textbook text is easier to predict than real English. No curve is flat at 20B tokens; the anneal lowered the final losses by 0.013, 0.002, 0.008 and 0.041 in legend order.

4What the base and chat models can do

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.

Table 2. pass@1 for the base model and the chat model as published on September 20, with failure types. Brackets give 95% intervals.
MetricnBaseChat, as published
Pass rate
HumanEval16413.4%[8.2, 18.6]26.8%[20.0, 33.6]
HumanEval+16411.0%[6.2, 15.8]25.6%[18.9, 32.3]
MBPP (sanitized)25720.2%[15.3, 25.1]38.9%[32.9, 44.9]
HumanEval failures by type
Runs, returns the wrong answer105 of 142102 of 120
Syntax error, mostly truncation154
Name, type and recursion errors2214

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.

5Was the chat score earned?

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.

The outputs are not copies

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.

The data contains the benchmark

Table 3. HumanEval overlap by instruction set. The filtered copy differs from the original by 89 rows.
DatasetExamplesDocstrings foundCanonical solutions found
evol-codealpaca-v1 (used)111,27260 problems, 156 examples8 problems, 67 examples
Magicoder-Evol-Instruct-110K (filtered copy)111,18352 problems, 116 examples8 problems, 66 examples
Magicoder-OSS-Instruct-75K (used)75,19701 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.

Removing 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.

BasePublished chatSame data, seed 2Decontaminated
0% 10% 20% 30% 40% 50% Base, HumanEval: 13.4% (22 of 164; 95% CI 8.2 to 18.6) Published chat, HumanEval: 26.8% (44 of 164; 95% CI 20.0 to 33.6) Same data, seed 2, HumanEval: 26.2% (43 of 164; 95% CI 19.5 to 33.0) Decontaminated, HumanEval: 22.6% (37 of 164; 95% CI 16.2 to 29.0) HumanEval n = 164 Base, HumanEval+: 11.0% (18 of 164; 95% CI 6.2 to 15.8) Published chat, HumanEval+: 25.6% (42 of 164; 95% CI 18.9 to 32.3) Same data, seed 2, HumanEval+: 25.6% (42 of 164; 95% CI 18.9 to 32.3) Decontaminated, HumanEval+: 21.3% (35 of 164; 95% CI 15.1 to 27.6) HumanEval+ n = 164 Base, MBPP: 20.2% (52 of 257; 95% CI 15.3 to 25.1) Published chat, MBPP: 38.9% (100 of 257; 95% CI 32.9 to 44.9) Same data, seed 2, MBPP: 34.6% (89 of 257; 95% CI 28.8 to 40.4) Decontaminated, MBPP: 35.0% (90 of 257; 95% CI 29.2 to 40.9) MBPP n = 257
Figure 3. pass@1 by benchmark and checkpoint, with 95% intervals. Single-run intervals are about seven points wide on HumanEval, so the paired comparison in Figure 4 is the sharper test. Hover a bar for exact values.
−10 pp −5 pp 0 pp +5 pp first worse first better diff, 95% CI, p HumanEval Decontaminated vs published Decontaminated vs published, HumanEval: −4.3 points (95% CI −7.8 to −0.7); 8 problems lost, 1 gained; McNemar p = 0.039 −4.3 [−7.8, −0.7] p = 0.039 Decontaminated vs seed 2 Decontaminated vs seed 2, HumanEval: −3.7 points (95% CI −7.4 to +0.1); 8 problems lost, 2 gained; McNemar p = 0.11 −3.7 [−7.4, +0.1] p = 0.11 Seed 2 vs published Seed 2 vs published, HumanEval: −0.6 points (95% CI −3.8 to +2.6); 4 problems lost, 3 gained; McNemar p = 1.0 −0.6 [−3.8, +2.6] p = 1.0 MBPP Decontaminated vs published Decontaminated vs published, MBPP: −3.9 points (95% CI −8.4 to +0.7); 23 problems lost, 13 gained; McNemar p = 0.13 −3.9 [−8.4, +0.7] p = 0.13 Decontaminated vs seed 2 Decontaminated vs seed 2, MBPP: +0.4 points (95% CI −4.2 to +5.0); 18 problems lost, 19 gained; McNemar p = 1.0 +0.4 [−4.2, +5.0] p = 1.0 Seed 2 vs published Seed 2 vs published, MBPP: −4.3 points (95% CI −8.9 to +0.3); 24 problems lost, 13 gained; McNemar p = 0.099 −4.3 [−8.9, +0.3] p = 0.099
Figure 4. Paired change in pass@1, in percentage points, with 95% intervals and exact McNemar p-values. Each row compares two checkpoints problem by problem; points left of zero mean the first checkpoint lost problems the second solved.

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.

Table 4. HumanEval passes split by whether the problem matched any removed training example.
SubsetProblemsBasePublishedSeed 2Decontaminated
In the removed data635141713
Not in the removed data10117302624

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.

The contamination was real and it was not memorization. Practice on the benchmark's style was worth about four points, and we had reported it as ability.

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.

Table 5. HumanEval pass@1 against reference models. Reference figures are from Chen et al. (2021).
ModelParametersYearHumanEval
GPT-3n/a20200.0%
GPT-Neo1.3B20214.8%
GPT-J6B202111.6%
Codex-300M300M202113.2%
Elroy base360M202613.4%
Elroy chat, decontaminated360M202622.6%
Elroy chat, as first published360M202626.8%
Codex-12B12B202128.8%

6Errors found by reading

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.

7Discussion

A workstation is enough for 2021

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 benchmark score has four inputs

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.

Decontamination by name is not decontamination

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 failure that matters in practice

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.

8Limitations

9Reproducibility

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.

10References

  1. Chen, M., et al. Evaluating large language models trained on code. 2021. arxiv.org/abs/2107.03374
  2. Austin, J., et al. Program synthesis with large language models. 2021. arxiv.org/abs/2108.07732
  3. Liu, J., Xia, C. S., Wang, Y., and Zhang, L. Is your code generated by ChatGPT really correct? Rigorous evaluation of large language models for code generation. NeurIPS 2023. arxiv.org/abs/2305.01210
  4. Wei, Y., Wang, Z., Liu, J., Ding, Y., and Zhang, L. Magicoder: empowering code generation with OSS-Instruct. 2023. arxiv.org/abs/2312.02120
  5. Luo, Z., et al. WizardCoder: empowering code large language models with Evol-Instruct. 2023. arxiv.org/abs/2306.08568
  6. Allal, L. B., et al. SmolLM2: when smol goes big, data-centric training of a small language model. 2025. arxiv.org/abs/2502.02737
  7. Li, R., et al. StarCoder: may the source be with you! 2023. arxiv.org/abs/2305.06161
  8. Penedo, G., et al. The FineWeb datasets: decanting the web for the finest text data at scale. 2024. arxiv.org/abs/2406.17557
  9. Touvron, H., et al. LLaMA: open and efficient foundation language models. 2023. arxiv.org/abs/2302.13971
  10. Su, J., et al. RoFormer: enhanced transformer with rotary position embedding. 2021. arxiv.org/abs/2104.09864
  11. Shazeer, N. GLU variants improve transformer. 2020. arxiv.org/abs/2002.05202
  12. Zhang, B., and Sennrich, R. Root mean square layer normalization. NeurIPS 2019. arxiv.org/abs/1910.07467
  13. Bavarian, M., et al. Efficient training of language models to fill in the middle. 2022. arxiv.org/abs/2207.14255
  14. Sennrich, R., Haddow, B., and Birch, A. Neural machine translation of rare words with subword units. ACL 2016. arxiv.org/abs/1508.07909