Serblo-125M

This is the first open Bosnian-Croatian-Serbian model where the entire stack is from scratch and public: corpus pipeline, tokenizer, architecture, training loop, SFT and preference datasets. All 70.8B training tokens are BCS. Open Serbian models do exist (jerteh/gpt2-orao, YugoGPT) but the claim here is the fully open stack.

I built this over one summer. The final training run cost ~$230 of Spot H100 time in cloud credits. Out of pocket the whole project was about ~$110.

The model is here to demonstrate the released data pipeline and to be a native-BCS base for finetunes. It writes fluent, native Serbian (Latin/gajica) across registers: news, forums, recipes, casual chat, literary prose. But it's (obviously) not a knowledge base or a competitive assistant.

Specs

Parameters 125,064,960
Architecture Llama-style decoder, from scratch in PyTorch: RoPE, GQA (12 query / 4 KV heads), RMSNorm, SwiGLU, tied embeddings
Layers / d_model / ctx 14 / 768 / 2048
Tokenizer 32k byte-level BPE, trained on the corpus (1.63 tokens/word on BCS, 43% leaner than GPT-2's, 9% leaner than XLM-R's on Serbian)
Training 135,000 steps × 524,288 tokens = 70.8B tokens seen ≈ 3 epochs over 23.3B unique
Recipe AdamW (0.9/0.95, wd 0.1), peak LR 4e-4, cosine → 4e-5, warmup 2k, bf16, torch.compile
Final validation loss 2.7478, perplexity 15.61, ≈0.96 bits/byte (own tokenizer, not comparable across tokenizers/languages)
Hardware 1× Spot H100 (Vertex AI), ~57 GPU-hours at ~28% MFU (small models can't feed big tensor cores)

Corpus (~23.3B unique tokens)

The corpus is a 96% FineWeb-2 backbone plus deliberate register injection. The pipeline (released alongside) does LID → Cyrillic→Latin transliteration (diacritics preserved; č ć š ž đ are load-bearing) → decluttering → dedup → tokenization.

slice tokens notes
FineWeb-2 BCS (bos/hrv/srp ×2 scripts) 22.5B 28.2M documents, cleaned + transliterated
srWaC 0.64B
Synthetic conversational pillar 0.14B (×3 upsampled) multi-model generated casual register the web lacks; diversity-gated (self-BLEU vs real)
OpenSubtitles (repaired) 0.02B (×3) double-mojibake repair (CP1250-as-CP1251 and CP1250-as-Latin1)

Use

import torch, json
from safetensors.torch import load_file
from tokenizers import Tokenizer
from model import GPT, GPTConfig   # model.py ships in this repo

cfg = GPTConfig(**json.load(open("config.json"))["config"])
sd = load_file("model.safetensors")
sd["lm_head.weight"] = sd["tok_emb.weight"]     # embeddings are tied, stored once
model = GPT(cfg); model.load_state_dict(sd); model.eval()
tok = Tokenizer.from_file("tokenizer.json")
# sample with temperature 0.7, top-k 200, repetition penalty 1.3 (see sample.py)

This is a base model: it continues text. For chat, use Serblo-125M-Instruct or fine-tune with the released SFT stack.

Limitations

  • It makes up specifics. Names, numbers, institutions etc. are fluent inventions ("Beograd, grad sa preko 400 miliona stanovnika").
  • It can't faithfully rewrite long passages. Giving it source text to transform; it riffs on the theme instead. Found this by accident while building the eval set and documented it there.
  • One rare prompt context (em-dash dialogue openers) makes ~1 in 5 generations start with a stray wrong-script byte before recovering. A repetition penalty of 1.3 and the instruct-tune both do suppress it, but no promises.
  • English does suprisingly show up, but often times as authentic BCS code-switching ("Ok so anyway, šta ti se dešava..."), which is corpus-faithful. Standalone English prompts produce word salad.
  • Trained on web text with the usual biases, no alignment stage beyond data curation.

Training story

Pretraining ran on Spot H100, checkpointing to GCS every 1,000 steps with an auto-resubmitting supervisor. The run survived three incidents: a plateau false-alarm (actually LR starvation; cutting the cosine schedule 200k→135k gave an annealed finish and val dropped 2.85→2.75), a multi-hour Cloud Logging blackout (checkpoint timestamps were used to verify it was alive) and the cloud project getting billing-suspended hours after the final checkpoint saved. The model was rescued at step 135,000. The details are in the pipeline repo.

Lineage & credits

The synthetic conversational pillar was written by:

model docs share of pillar
Gemini 2.5 Flash-Lite 499,804 98.93%
Llama 3.3 70B (Groq + SambaNova) 3,504 0.69%
MiMo (mimo-auto + V2.5) 1,041 0.21%
DeepSeek (V4 Flash + V3.2 + V4 Pro) 593 0.12%
GLM 5.2 175 0.03%
Claude Opus (4.6 + 4.8) 86 0.02%
GPT-5.5, Kimi K2.6 7 trace

Shares are by document count (505,210 total). Gemini carried the volume, the rest is deliberate diversity seasoning: pooling lineages cut phrase repetition 41% below single-model output in balanced tests (self-BLEU 0.331→0.195; the measurement tools ship in the pipeline).

Built by Stefan Selakov (@sterlixlol)

Downloads last month
400
Safetensors
Model size
0.1B params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for sterlixlol/serblo-125m

Finetunes
1 model