45 minutes · The substrate layer of Pillar 1 (Data)
Three formats. One template. The inspection loop that catches every silent bug.
Pillar 1 — Data · FT04
The model trains. The loss drops. The model is subtly wrong.
You blame data quality, LoRA rank, learning rate — never the one-character whitespace bug.
| Format | Shape | Origin |
|---|---|---|
| Raw messages | [{role, content}] | The lingua franca |
| Instruction / response | instruction + output | Alpaca / Vicuna |
| ShareGPT | {conversations: [{from, value}]} | Community chat |
Valid roles: system · user · assistant · tool. Anything else is a bug.
# ShareGPT -> messages (the conversion you'll run most)
ROLE_MAP = {"human": "user", "gpt": "assistant",
"system": "system", "tool": "tool"}
def sharegpt_to_messages(conv):
return [{"role": ROLE_MAP[t["from"]], "content": t["value"]}
for t in conv["conversations"]]
Result is identical to the raw-messages shape — proof all three formats are import conventions for one underlying conversation.
text = tokenizer.apply_chat_template(
messages,
tokenize=False, # string back, not token IDs
add_generation_prompt=False # full conversation (training)
)
The template is a Jinja file shipped with the tokenizer — the exact bytes the model was instruction-tuned against.
| Family | Delimiters |
|---|---|
| ChatML (Qwen, Yi) | <|im_start|> ... <|im_end|> |
| Llama-3 | <|start_header_id|> ... <|eot_id|> |
| MiniCPM | ChatML variant, different EOS |
Three failure modes — each invisible until you decode the tokenized output.
| Bug | What happens |
|---|---|
| Token-boundary errors | Off-by-one whitespace → different BPE merges → model trains on a sequence it never saw |
| Special-token merging | <|im_start|> as raw text → 6 ordinary tokens, not 1 special ID |
| Wrong role tokens | ChatML delimiters on a Llama-3 model → unknown text, garbage boundaries |
# 1. Render to string
text = tokenizer.apply_chat_template(messages, tokenize=False)
# 2. Tokenize
inputs = tokenizer(text, return_tensors="pt")
# 3. THE INSPECTION — read this
print(tokenizer.decode(inputs["input_ids"][0]))
10 seconds of reading saves 10 hours of "why is the model broken." Once per dataset. Non-negotiable.
for tok in ["<|im_start|>", "<|im_end|>", "<|eot_id|>"]:
if tok in tokenizer.added_tokens_encoder:
print(f"{tok!r:18} -> single token ID "
f"{tokenizer.added_tokens_encoder[tok]} (CORRECT)")
else:
print(f"{tok!r:18} -> NOT a special token (BUG)")
added_tokens_encoder, your tokenizer and model are mismatched. Stop. Reconcile before any training.
SFT — one correct response
{"messages": [
{"role": "user", "content": q},
{"role": "assistant", "content": a}
]}
Cross-entropy on the assistant turn. → SFTTrainer (FT12)
Preference — chosen vs rejected
{"prompt": [{user...}],
"chosen": [{assistant good}],
"rejected": [{assistant bad}]}
Contrastive: chosen > rejected. → DPOTrainer (FT13)
| Bug | Symptom | Root cause |
|---|---|---|
| Missing EOS | Model never stops | No closing im_end / eot_id |
| Wrong role tokens | Can't follow instructions | ChatML template on Llama-3 |
| Specials as text | Trains on literal text | Hand-concat, not template |
| Trailing spaces | Stopping drifts | Space before closing token |
| Loss on user turns | Model echoes prompt | labels not -100-masked |
| Mixed formats | Half dataset malformed | Didn't normalize before map() |
tokenizer.decode(input_ids[0]) · None caught by: loss curves.
decode on your input_ids, you're flying blind. Inspect once per dataset.
FT04 is the wheel itself. Format errors corrupt it at the byte level:
apply_chat_template + the inspection loop. Keep the substrate clean so PEFT, SFT, DPO can trust their inputs.
Next: FT05 — Synthetic Data Generation