Fine-Tuning & Training LLMsβοΈ
A task-oriented guide to training / fine-tuning large language models with
ezpz. If you know what you want to run (fine-tune a pretrained HF model,
or pretrain a model from scratch) but not which example or how to scale
it, start here β then follow the per-module example pages for the deep dive.
ezpz gives you three paths:
| Path | Module (example Β· API Β· source) | What it does | Larger-model knob |
|---|---|---|---|
1. HF fine-tune (Trainer) |
hf_trainer (example Β· API Β· src) |
Fine-tune a pretrained HF model via HuggingFace Trainer + FSDP |
--model_name_or_path + node count |
| 2. HF fine-tune (custom loop) | hf (example Β· API Β· src) |
Same dataset/model setup, hand-rolled training loop | --model_name_or_path + node count |
| 3. From-scratch (FSDP + TP) | fsdp_tp (example Β· API Β· src) |
Pretrain a Llama-arch model from scratch (FSDP2 + tensor/HSDP parallel) | --model {s,m,l,xl,xxl,xxxl} / agpt-2b / agpt-20b |
Which path?
- Fine-tuning an existing checkpoint (Llama, Qwen, Mistral, β¦)? β Path 1
(
hf_trainer) is the default; Path 2 (hf) is the same targets in an explicit loop, handy for A/B'ing the loop against theTrainer. - Pretraining from scratch, or you need tensor/sequence parallelism for a
model too big for FSDP alone? β Path 3 (
fsdp_tp). fsdp_tpalso accepts an HF repo id in--model(see below), so it can FSDP-fine-tune an HF model too (TP off).
Models / configurationsβοΈ
Paths 1 & 2 β HF fine-tuneβοΈ
Both HF paths take a real HuggingFace repo via --model_name_or_path (they do
not use the --model {s,m,l,β¦} size presets β those are Path 3 only).
Suggested targets:
| Role | --model_name_or_path |
~params | Notes |
|---|---|---|---|
| Small | meta-llama/Llama-3.2-1B |
1B | single-node |
| Default | meta-llama/Llama-3.1-8B |
8B | smallest target that meaningfully exercises multi-GPU FSDP for fine-tuning |
| Alt (Qwen) | Qwen/Qwen2.5-7B β or a Qwen3 8B/14B repo once confirmed |
7β14B | current-gen alternative arch |
| Alt (Mistral) | mistralai/Mistral-7B-v0.3 |
7B | GQA baseline |
| Larger | Qwen/Qwen2.5-32B |
32B | multi-node FSDP |
| Largest | meta-llama/Llama-3.3-70B |
70B | multi-node |
Confirm exact repo id + revision before running
Model repos are gated and get renamed/re-tagged; the Qwen3 line in
particular is evolving β pick the specific published repo (e.g. an
-Instruct vs base variant) and pin its revision when you record results.
The tokenizer usually matches the model repo (--tokenizer_name = same id).
Path 3 β from-scratch FSDP+TP, --model size ladderβοΈ
fsdp_tp exposes a --model size ladder:
--model |
dim Γ layers | ~params | default seq_len | notes |
|---|---|---|---|---|
s (small) |
768 Γ 12 | ~125M | 2048 | |
m (medium) |
1024 Γ 16 | ~246M | 2048 | |
l (large) |
1536 Γ 16 | ~495M | 2048 | |
xl (xlarge) |
2048 Γ 24 | ~1.5B | 2048 | Llama-1.5B-class |
xxl |
4096 Γ 32 | ~7B | 4096 | Llama-7B-class |
xxxl |
5120 Γ 40 | ~13B | 4096 | Llama-13B-class |
agpt-2b |
2048 Γ 12, vocab 256k | ~2B | 8192 | torchtitan AuroraGPT parity |
agpt-20b |
5120 Γ 64, vocab 256k | ~20B | 2048 | torchtitan AuroraGPT parity |
--model xxl(~7B) is the first rung that requires the FSDP2 + tensor-parallel machinery (and the memory-bounded loss impls) to fit, so it's a representative large-model benchmark.--model xxxl(~13B) andagpt-20b(~20B) are the largest configs;agpt-*match a real torchtitan production arch for A/B comparison.- Aliases all resolve:
smallβs,mediumβm,largeβl,xlarge/extra-largeβxl,xxlargeβxxl,xxxlargeβxxxl;agpt2b,agpt_2b,AGPT-2Bβagpt-2b. Use--model debugfor a laptop-friendly tiny model. - Arbitrary HF model.
--modelalso accepts a HuggingFace repo id (any value containing a/, e.g.--model meta-llama/Llama-3.1-8B).fsdp_tpthen loads real weights viaAutoModelForCausalLM.from_pretrainedinstead of building the from-scratch preset β i.e. it can fine-tune an HF model under FSDP2, not just pretrain the size ladder. Caveats: this path is FSDP2-only β--tp > 1is forced to1(ezpz's TP plan targets its own module names, not HFLlamaDecoderLayer/GemmaDecoderLayer/β¦); the tokenizer defaults to the same repo unless--tokenizer_nameis set; and gated repos honorHF_TOKEN.
Expected resource requirementsβοΈ
Rough guidance β validate against a real run before publishing (see Follow-up runs). "GPUs" assumes 80 GB-class accelerators (A100-80G / H100 / Aurora PVC tile).
Paths 1 & 2 β HF fine-tuneβοΈ
| Target | Precision / memory levers | Min GPUs (FSDP) | Notes |
|---|---|---|---|
| Llama-3.2-1B (baseline) | bf16 | 1β4 | single node |
| Llama-3.1-8B / Qwen2.5-7B (default) | bf16 + gradient_checkpointing + shard_grad_op |
~4β8 (1β2 nodes) | activation checkpointing needed at block_size=8192 |
| Qwen2.5-32B | bf16 + full_shard + grad-ckpt |
~8β16 (2β4 nodes) | multi-node FSDP |
| Llama-3.3-70B (largest) | bf16 + full_shard + grad-ckpt |
~16β32 (4β8 nodes) | tune block_size down if OOM |
Memory levers (both HF paths): --fsdp=full_shard (vs shard_grad_op),
--gradient_checkpointing=true, and lowering --block_size /
--per_device_train_batch_size.
Path 3 β from-scratch FSDP+TPβοΈ
--model |
~params | Min GPUs | Suggested parallelism | Memory levers |
|---|---|---|---|---|
xl |
~1.5B | 1β4 | --tp 1 |
--reshard-after-forward (default) |
xxl (default large) |
~7B | ~8 (1β2 nodes) | --tp 2 + HSDP |
--loss-impl=loss-parallel, --act-mem-budget<1.0, --compile |
xxxl |
~13B | ~16 (2β4 nodes) | --tp 2β4 |
same + smaller --seq-len |
agpt-20b |
~20B | ~24β48 (4β8 nodes) | --tp 4 + HSDP |
--loss-impl=loss-parallel (256k vocab β logits dominate) |
Key large-model levers (all fsdp_tp flags):
--tp Nβ tensor-parallel degree (shard within a node).--dp-replicate/--dp-shardβ HSDP (replicate Γ shard); constraintdp_replicate * dp_shard * tp == WORLD_SIZE.--dp-shard -1= "use all remaining ranks".--reshard-after-forward {always,never}(or--no-reshard-after-forward) βalways= ZeRO-3 (lowest memory, default),never= ZeRO-2 (more memory, skips the backward all-gather).--loss-impl {eager,chunked,chunked-backward,compiled,loss-parallel,fused-linear}β at large vocab (agpt's 256k) the(BΒ·T, vocab)logits tensor is the OOM driver;loss-parallel(needs tp>1) orfused-linearbound it.--act-mem-budget <float>β inductor recompute budget (needs--compile);<1.0recomputes activations in backward to cut peak memory.
Path 1: HF fine-tune with hf_trainer (HuggingFace Trainer)βοΈ
Full walkthrough:
ezpz.examples.hf_trainerΒ· Aurora vs Polaris comparison
If you already have a Python environment with torch + mpi4py, run without
installing anything via uv:
# Small: Llama-3.2-1B
TMPDIR=$(pwd) uv run \
--with "git+https://github.com/saforem2/ezpz" \
--python=$(which python3) \
ezpz launch python3 -m ezpz.examples.hf_trainer \
--streaming \
--dataset_name=eliplutchok/fineweb-small-sample \
--tokenizer_name meta-llama/Llama-3.2-1B \
--model_name_or_path meta-llama/Llama-3.2-1B \
--bf16=true \
--do_train=true \
--do_eval=true \
--report-to=wandb \
--logging-steps=1 \
--include-tokens-per-second=true \
--max-steps=50000 \
--include-num-input-tokens-seen=true \
--optim=adamw_torch \
--logging-first-step \
--include-for-metrics='inputs,loss' \
--max-eval-samples=50 \
--per_device_train_batch_size=1 \
--block_size=8192 \
--gradient_checkpointing=true \
--fsdp=shard_grad_op
Larger targets β swap the model + tokenizer id (and prefer full_shard
for 32B/70B; reduce --block_size if you OOM):
# Llama-3.1-8B
--tokenizer_name meta-llama/Llama-3.1-8B \
--model_name_or_path meta-llama/Llama-3.1-8B \
--fsdp=shard_grad_op \
--block_size=8192 \
# Qwen2.5-7B (or a Qwen3 8B/14B repo once confirmed)
--tokenizer_name Qwen/Qwen2.5-7B \
--model_name_or_path Qwen/Qwen2.5-7B \
--fsdp=shard_grad_op \
# Llama-3.3-70B (multi-node)
--tokenizer_name meta-llama/Llama-3.3-70B \
--model_name_or_path meta-llama/Llama-3.3-70B \
--fsdp=full_shard \
--block_size=4096 \
If you need torch and/or mpi4py, add the extras:
TMPDIR=$(pwd) uv run \
--with "git+https://github.com/saforem2/ezpz[torch,mpi,hf]" \
--python=$(which python3) \
# ...
For automatic module loading + virtual-environment setup:
before launching.
Path 2: HF fine-tune with hf (hand-rolled training loop)βοΈ
Full walkthrough:
ezpz.examples.hf
hf mirrors the dataset/model setup of hf_trainer but uses an explicit
training loop (like the other ezpz examples) instead of the HF Trainer.
Same --model_name_or_path scaling β useful for comparing the custom loop
against the Trainer path on identical targets.
# Llama-3.1-8B, custom loop.
# hf.py enables FSDP via ACCELERATE_USE_FSDP (it does NOT read --fsdp), and it
# reports tokens/sec on its own (train/tokens_per_sec) β no --include-* flags.
ACCELERATE_USE_FSDP=true TMPDIR=$(pwd) uv run \
--with "git+https://github.com/saforem2/ezpz" \
--python=$(which python3) \
ezpz launch python3 -m ezpz.examples.hf \
--streaming \
--dataset_name=eliplutchok/fineweb-small-sample \
--tokenizer_name meta-llama/Llama-3.1-8B \
--model_name_or_path meta-llama/Llama-3.1-8B \
--bf16=true \
--do_train=true \
--do_eval=true \
--report-to=wandb \
--logging-steps=1 \
--max-steps=100 \
--include-num-input-tokens-seen=true \
--optim=adamw_torch \
--logging-first-step \
--include-for-metrics='inputs,loss' \
--max-eval-samples=100 \
--per_device_train_batch_size=1 \
--per_device_eval_batch_size=1 \
--block_size=8192 \
--gradient_checkpointing=true
The same larger-target swaps from Path 1 apply here (Llama-3.1-8B / Qwen2.5-7B / Qwen2.5-32B / Llama-3.3-70B).
Note
The 8B+/70B targets are gated HF repos β configure HF_HOME and
huggingface-cli login (or HF_TOKEN) before the run.
Path 3: from-scratch FSDP+TP with --modelβοΈ
Full walkthrough:
ezpz.examples.fsdp_tp
A representative large config β xxl (~7B), tensor-parallel + HSDP, with the
memory-bounded loss and compile levers:
TMPDIR=$(pwd) uv run \
--with "git+https://github.com/saforem2/ezpz" \
--python=$(which python3) \
ezpz launch python3 -m ezpz.examples.fsdp_tp \
--model xxl \
--tp 2 \
--dp-shard -1 \
--dp-replicate 1 \
--reshard-after-forward always \
--loss-impl loss-parallel \
--act-mem-budget 0.5 \
--compile \
--seq-len 4096 \
--batch-size 1
fsdp_tpenables wandb automatically (viaezpz'sHistory/setup_wandbandWANDB_*env vars) β there is no--report-toflag here.
Other size-ladder targets (same flags, just change --model):
--model xl # ~1.5B, fits at tp=1 on a single node
--model xxxl # ~13B, prefer --tp 2..4 and a smaller --seq-len
Largest config β agpt-20b (~20B, 256k vocab), higher TP:
ezpz launch python3 -m ezpz.examples.fsdp_tp \
--model agpt-20b \
--tp 4 \
--dp-shard -1 \
--loss-impl loss-parallel \
--act-mem-budget 0.5 \
--compile \
--seq-len 2048 \
--batch-size 1
Note
--dp-shard -1 = "use all remaining ranks" =
WORLD_SIZE / (dp_replicate * tp). For pure HSDP, set --dp-replicate > 1
(e.g. one replica per node, sharded within).
Fine-tune an arbitrary HF model via fsdp_tp β pass a repo id (with a
/) as --model. Loads real HF weights under FSDP2 (TP is forced off, so
drop --tp); the same memory levers apply:
ezpz launch python3 -m ezpz.examples.fsdp_tp \
--model meta-llama/Llama-3.1-8B \
--dp-shard -1 \
--reshard-after-forward always \
--act-mem-budget 0.5 \
--compile \
--seq-len 4096 \
--batch-size 1
# --tokenizer_name defaults to --model; set HF_TOKEN for gated repos
# (wandb auto-enabled; no --report-to flag on fsdp_tp)
Any HF repo works here (e.g. Qwen/Qwen2.5-7B, mistralai/Mistral-7B-v0.3) β
this overlaps with Paths 1 & 2 but keeps you in the FSDP2 training loop rather
than the HF Trainer / custom-loop harnesses.
Follow-up runs to publish resultsβοΈ
Before the numbers are publishable, complete these on-node runs and record them (wandb + the resource tables above):
- Fit / OOM sweep per target. Confirm the default configs (Llama-3.1-8B
on Paths 1 & 2;
--model xxlon Path 3) actually fit at the stated GPU counts; record peak memory and the smallest node count that fits. Adjustblock_size/seq_len/act-mem-budgetand re-record on OOM. - Throughput / MFU. Capture tokens/sec-per-GPU and MFU at steady state
(past step ~20, after any
torch.compilewarmup/recompile). Path 1 (hf_trainer):--include-tokens-per-second. Path 2 (hf): reported automatically astrain/tokens_per_sec(no flag). Path 3 (fsdp_tp): thetps_per_gpu/mfuhistory metrics. Trainervs custom-loop A/B (Paths 1 vs 2). Run the same target on both HF paths and compare throughput/MFU + loss curves.- Scaling curve. Run each target at 2β3 node counts (1, 2, 4) for a strong/weak-scaling plot.
- Model-family sweep (HF). One run each of the alt targets (Qwen2.5-7B / Mistral-7B / Qwen2.5-32B, plus a Qwen3 repo once confirmed) to compare arch/tokenizer effects at fixed data.
- Largest-config validation. One end-to-end run each of Llama-3.3-70B
(HF) and
agpt-20b(Path 3) to prove they launch, train, and checkpoint at multi-node scale. - A/B vs torchtitan (Path 3). Compare
agpt-2b/agpt-20bMFU against a matched torchtitan run (the presets are byte-identical for this) and note any gap.
Running on Perlmutter @ NERSCβοΈ
-
Submit an interactive job:
NODES=2 ; HRS=02 ; QUEUE=interactive salloc --nodes $NODES --qos $QUEUE --time $HRS:30:00 \ -C 'gpu' --gpus=$(( 4 * NODES )) -A amsc013_gTip
For the 8B+/70B (HF) or
xxl/agpt-20b(Path 3) targets, bumpNODESper the resource tables above. -
Load modules:
-
Navigate to
$SCRATCHand set environment variables: -
Create and activate a virtual environment:
-
Install
ezpz(+mpi4py): -
Run:
# sanity check: MLP on MNIST ezpz launch python3 -m ezpz.examples.test # Path 1: HF Trainer (8B) ezpz launch python3 -m ezpz.examples.hf_trainer \ --dataset_name=eliplutchok/fineweb-small-sample --streaming \ --tokenizer_name meta-llama/Llama-3.1-8B \ --model_name_or_path meta-llama/Llama-3.1-8B \ --bf16=true --do_train=true --do_eval=true \ --report-to=wandb --logging-steps=1 --logging-first-step \ --include-tokens-per-second=true --include-num-input-tokens-seen=true \ --optim=adamw_torch --include-for-metrics='inputs,loss' \ --max-steps=100 --max-eval-samples=100 \ --per_device_train_batch_size=1 --per_device_eval_batch_size=1 \ --block_size=8192 --gradient_checkpointing=true \ --fsdp=shard_grad_op \ --output_dir=outputs/ezpz.hf_trainer/$(tstamp) # Path 2: HF custom loop (same target). FSDP via ACCELERATE_USE_FSDP; # no --fsdp / --include-tokens-per-second (see the Path 2 section above). ACCELERATE_USE_FSDP=true ezpz launch python3 -m ezpz.examples.hf \ --dataset_name=eliplutchok/fineweb-small-sample --streaming \ --tokenizer_name meta-llama/Llama-3.1-8B \ --model_name_or_path meta-llama/Llama-3.1-8B \ --bf16=true --do_train=true --do_eval=true \ --report-to=wandb --logging-steps=1 --logging-first-step \ --include-num-input-tokens-seen=true \ --optim=adamw_torch --include-for-metrics='inputs,loss' \ --max-steps=100 --max-eval-samples=100 \ --per_device_train_batch_size=1 --per_device_eval_batch_size=1 \ --block_size=8192 --gradient_checkpointing=true \ --output_dir=outputs/ezpz.hf/$(tstamp) # Path 3: from-scratch FSDP+TP (xxl / ~7B) [wandb auto-enabled] ezpz launch python3 -m ezpz.examples.fsdp_tp \ --model xxl --tp 2 --dp-shard -1 \ --loss-impl loss-parallel --act-mem-budget 0.5 --compile \ --seq-len 4096 --batch-size 1
See alsoβοΈ
- πΊοΈ Distributed Training β how
ezpzsets uptorch.distributed, scheduler detection, and DDP/FSDP mechanics. - Per-module deep dives:
hf_trainerΒ·hfΒ·fsdp_tp - π€ HF Trainer: Aurora vs Polaris comparison