C.W.K.
Stream
Lesson 02 of 06 · published

Preparing Training Data — JSONL, Templates, and Cleaning

~16 min · data, jsonl, chat-template

Level 0Curious
0 XP0/51 lessons0/15 achievements
0/100 XP to next level100 XP to go0% complete

Bad data beats small models on the loss curve, every time

The single biggest predictor of fine-tuning quality is the training data, not the rank, not the optimizer, not the number of iterations. A 7B model with 200 cleaned, well-formatted examples will outperform the same model with 2000 messy examples on most downstream tasks. Spend more time on the data than on hyperparameter tuning.

The format mlx-lm expects

mlx-lm wants JSONL — one JSON object per line. Two file naming conventions live in the data directory you point --data at:

  • train.jsonl — required. The training examples.
  • valid.jsonl — required. Validation examples used to compute val loss every --steps-per-eval iters.
  • test.jsonl — optional. Used by --test to evaluate after training.

Inside each .jsonl line, mlx-lm accepts two shapes: chat format (a messages array, just like OpenAI's chat-completions) or completion format (a prompt + completion pair, for older non-chat training).

The chat format — the right default

Each line is a JSON object with a messages field. Each message has role (system / user / assistant) and content. mlx-lm will apply the model's chat template at training time, so the formatting matches what the model will see at inference. This is non-negotiable for chat-tuned base models — train on the chat-templated form or the resulting fine-tune behaves badly at inference.

The cleaning step everyone skips

Before training on any dataset (your own or one you pulled from HF), do these passes:

  1. Deduplicate. Identical or near-identical examples bias the model and inflate apparent training-set size.
  2. Length-filter. Drop examples that are absurdly short (no signal) or longer than your --max-seq-length (would be truncated, often mid-thought).
  3. Quality-filter. Even a quick eyeball of the bottom 5% of examples by your judgment catches a lot — typos, broken formatting, off-topic noise.
  4. Split without leakage. Hold out validation and test sets from the same dataset; make sure they're disjoint from training. Random split is fine for most cases; for very small datasets, stratify by your task's labels.

If you want a real-world dataset to play with

The mlx-community org on Hugging Face hosts a few small datasets that work out of the box with mlx-lm — mlx-community/wikisql (SQL generation, classic) is the canonical demo set. For your own task, even 100 well-curated examples are a respectable starting point.

Code

Chat-format JSONL — the canonical training data shape·python
# Each line is one example with a 'messages' array.
import json

examples = [
    {
        "messages": [
            {"role": "system",    "content": "You are a SQL expert."},
            {"role": "user",      "content": "How many users joined in 2024?"},
            {"role": "assistant", "content": "SELECT COUNT(*) FROM users WHERE YEAR(joined_at) = 2024;"},
        ]
    },
    {
        "messages": [
            {"role": "system",    "content": "You are a SQL expert."},
            {"role": "user",      "content": "List all products under $10."},
            {"role": "assistant", "content": "SELECT * FROM products WHERE price < 10;"},
        ]
    },
]

with open("./my-data/train.jsonl", "w") as f:
    for ex in examples:
        f.write(json.dumps(ex) + "\n")

# Then point mlx-lm at the directory:
#   python -m mlx_lm lora --model ... --train --data ./my-data
Quick deduplication + length filter·python
import json, hashlib

def line_hash(ex):
    # Hash the messages content for dedup
    return hashlib.md5(json.dumps(ex["messages"], sort_keys=True).encode()).hexdigest()

def total_chars(ex):
    return sum(len(m["content"]) for m in ex["messages"])

seen = set()
kept, dropped = [], 0
for line in open("./my-data/train.jsonl"):
    ex = json.loads(line)
    h = line_hash(ex)
    n = total_chars(ex)
    if h in seen or n < 50 or n > 8000:   # adjust thresholds for your task
        dropped += 1
        continue
    seen.add(h)
    kept.append(ex)

print(f"kept {len(kept)}, dropped {dropped}")
with open("./my-data/train.cleaned.jsonl", "w") as f:
    for ex in kept:
        f.write(json.dumps(ex) + "\n")

External links

Exercise

Pick a tiny domain you know well — could be SQL queries, regex patterns, witty haikus, summarizing tech RSS, anything narrow. Hand-write 30 chat-format JSONL examples (system + user + assistant). Save as train.jsonl, hold 5 out as valid.jsonl. Run the dedup + length filter from the second code block. The exercise is to feel the work — most of it is curation, not training.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.