DeepSpeed is Microsoft’s distributed training library. ZeRO partitions optimizer state, gradients, and (at stage 3) parameters so a full fine-tune can span more than one GPU. This guide launches a Massed Compute multi-GPU VM, installs DeepSpeed in a venv, runs a PyTorch / Hugging Face Trainer baseline, then the same job under ZeRO-2 and ZeRO-3, and writes a checkpoint you can convert with zero_to_fp32.py.
The walkthrough full-fine-tunes ungated Qwen/Qwen2.5-0.5B-Instruct (20 steps) and Qwen/Qwen2.5-7B-Instruct (8 steps) on the first 1,024 rows of yahma/alpaca-cleaned. That is a smoke you can finish in minutes, not a 70B overnight job. Swap the model name once the ZeRO checkpoint path is green. This was validated on two NVIDIA H100 PCIe (80 GB) on September 1, 2026 (repeat of the August 31, 2026 stack) — first with CUDA_VISIBLE_DEVICES=0, then torchrun DDP, then deepspeed --num_gpus=2. Topology is PHB (PCIe through the host bridge), not NVLink. That is DeepSpeed multi-GPU training on a Massed Compute VM, not a single-GPU notebook.
If you already follow Fine-Tune LLMs with Axolotl on Multi-GPU VMs, that post is YAML QLoRA + torchrun DDP. RL Post-Training with verl on Multi-GPU Infrastructure is FSDP + vLLM GRPO. Fine-Tune LLMs with LLaMA-Factory on GPU Cloud is another YAML trainer. This post is deepspeed --num_gpus=2 train_ds.py --deepspeed ds_z3.json. Do not copy those install commands. Axolotl can point deepspeed: at the same JSON — named here, not re-run.
| Component | Version | Purpose |
|---|---|---|
| Ubuntu Server | 24.04 LTS | Image 184: NVIDIA driver 580.126.16, CUDA 13.0 |
| DeepSpeed | 0.19.6 | ZeRO (deepspeed --num_gpus=2) |
| PyTorch | 2.12.1+cu130 | CUDA 13.0 wheels (UV_TORCH_BACKEND=cu130) |
| Transformers | 5.16.1 | Qwen2 tokenizer + causal LM, HF Trainer |
| Accelerate | 1.14.0 | DeepSpeed plugin inside Trainer |
| datasets | 5.0.1 | yahma/alpaca-cleaned |
| Models (validated) | Qwen2.5-0.5B-Instruct, Qwen2.5-7B-Instruct | Ungated instruct, full FT (not LoRA) |
| Dataset | yahma/alpaca-cleaned | 51,760 rows; this run used 1,024 (train[:1024]) |
| Resource | This walkthrough | Notes |
|---|---|---|
| GPU | 2× H100 PCIe 80 GB (gpu_2x_h100) |
Validated. 0.5B ZeRO is a launcher smoke. 7B full FT is why you want 80 GB × 2 |
| System RAM | 256 GB | SKU ships 256 GiB |
| vCPU | 40 | SKU ships 40 vCPU |
| Storage | 2500 GB | Hugging Face cache plus 7B ZeRO checkpoints (~15 GB gathered bf16) |
| Network | 1 Gbps | First uv pip install torch pulls CUDA 13 wheels |
ZeRO stages vs FSDP
ZeRO does not make a 0.5B job faster. It changes what is replicated on each GPU.
| Stage | What is partitioned | When to use it (this VM) |
|---|---|---|
| DDP / ZeRO-0 | Nothing. Full replica per GPU | Model + Adam fit one card. This 0.5B job. Fastest samples/s here |
| ZeRO-2 | Optimizer + gradients. Params still replicated | Mid-size full FT when DDP OOMs on optimizer. 7B ZeRO-2 OOM’d on this pair (see troubleshooting) |
| ZeRO-3 | Params + optimizer + gradients | When even ZeRO-2 OOMs, or you need more than two cards. 7B ZeRO-3 completed |
| CPU offload | Optimizer (and optionally params) on host RAM | Next step after GPU ZeRO-3 still OOMs. Not run here |
| FSDP | PyTorch-native shard | verl post — different launcher |
On this 2× H100 PHB pair, 0.5B DDP was 8.848 samples/s. ZeRO-2 was 4.546. ZeRO-3 was 3.201. Use ZeRO when the 7B / 70B job does not fit, not to speed up a tiny smoke.
Axolotl hook. The Axolotl DDP post left deepspeed: unset. For a later full FT, add deepspeed: ds_z3.json to that YAML and keep the same deepspeed --num_gpus=2 JSON as below. Do not mix that with fsdp_config:.
Massed Compute VM Pricing
Lead with the 2× H100 this guide was tested on. 2× A100 and 8× SXM are listed for jobs that do not fit two PCIe cards.
Pricing fetched from the Massed Compute inventory API on August 31, 2026.
| SKU | Description | vCPU | RAM | Storage | Price | Capacity |
|---|---|---|---|---|---|---|
gpu_2x_l40 |
2x L40 (48GB) | 26 | 144 GiB | 1250 GB | $1.72/hr | 15 |
gpu_2x_a100 |
2x A100 (80GB) | 30 | 150 GiB | 1024 GB | $2.70/hr | 3 |
gpu_1x_h100 |
1x H100 (80GB) | 20 | 128 GiB | 1250 GB | $2.73/hr | 6 |
gpu_2x_h100_spot |
2x H100 (80GB) [Spot] | 40 | 256 GiB | 2500 GB | $4.90/hr | 2 |
gpu_2x_h100 |
2x H100 (80GB) | 40 | 256 GiB | 2500 GB | $5.46/hr | 2 |
gpu_8x_A100_SXM4 |
8x A100 SXM4 (80GB) | 126 | 800 GiB | 5000 GB | $11.04/hr | 1 |
gpu_8x_H100_SXM5 |
8x H100 SXM5 (80GB) | 126 | 976 GiB | 2800 GB | $25.12/hr | 13 |
gpu_2x_h100. Use on-demand if the job cannot restart. Hopper / H100 context: Llama 3.1 Benchmark by GPU Type and Why NVIDIA Leads in AI. A100 context: NVIDIA A100 GPU Best Use Cases. Training vs inference card sizing: The Best GPU for LLM Inference Without Overpaying. After you have a merged checkpoint, serve it with SGLang — that is a different post.Step-by-Step Deployment
Image 184 logs in as Ubuntu (capital U). If SSH offers extra keys and then fails, add -o IdentitiesOnly=yes. Image 184 ships NVIDIA drivers, not a ready Python venv. Install python3.12-venv before uv venv.
Launch GPU VM
# Launch via Massed Compute dashboard or API
# Product: gpu_2x_h100
# Image: 184 (Ubuntu Server 24.04 w/ Drivers)
# SSH Key: attach your public key
# Instance name: deepspeed-multigpu
Wait until the VM is running and copy the SSH details.
Verify GPU Access
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP \
'nvidia-smi -L; nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader; nvidia-smi topo -m'
This run printed two lines: NVIDIA H100 PCIe, 81559 MiB, 580.126.16. nvidia-smi -L must show 2 GPUs. topo -m printed PHB between GPU0 and GPU1 — PCIe through the CPU host bridge, not NVLink (NV#). Training still used both cards.
Lock the firewall to SSH only. No public training UI is required.
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP 'bash -s' <<'EOF'
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
echo y | sudo ufw enable
sudo ufw status
EOF
Install DeepSpeed
Pin whatever uv pip actually installs. This VM printed DeepSpeed 0.19.6, torch 2.12.1+cu130, Transformers 5.16.1, Accelerate 1.14.0. DS_BUILD_OPS=0 skips JIT CUDA ops (FusedAdam, transformer kernels). ZeRO does not need those ops. import deepspeed then warns CUDA_HOME does not exist — expected on image 184 without a CUDA toolkit.
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP 'bash -s' <<'EOF'
set -euxo pipefail
sudo apt-get update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y python3.12-venv curl ninja-build
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
export UV_TORCH_BACKEND=cu130
uv venv --python 3.12 ~/ds_env
source ~/ds_env/bin/activate
uv pip install -U pip packaging setuptools wheel ninja py-cpuinfo
uv pip install torch==2.12.1 torchvision
DS_BUILD_OPS=0 uv pip install transformers datasets accelerate deepspeed
python -c "import torch,deepspeed,transformers; print(torch.__version__, deepspeed.__version__, transformers.__version__, torch.cuda.device_count())"
EOF
Stop if device_count is not 2.
Train script and ZeRO JSON
Copy train_ds.py, ds_z2.json, and ds_z3.json to the VM. The script accepts --local_rank because the DeepSpeed launcher injects it.
train_ds.py (HF Trainer, causal LM, optional --deepspeed):
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM, AutoTokenizer, DataCollatorForLanguageModeling,
Trainer, TrainingArguments,
)
def main() -> None:
p = argparse.ArgumentParser()
p.add_argument("--model", default="Qwen/Qwen2.5-0.5B-Instruct")
p.add_argument("--output_dir", required=True)
p.add_argument("--deepspeed")
p.add_argument("--max_steps", type=int, default=20)
p.add_argument("--seq_len", type=int, default=256)
p.add_argument("--per_device_train_batch_size", type=int, default=1)
p.add_argument("--gradient_accumulation_steps", type=int, default=4)
p.add_argument("--rows", type=int, default=1024)
p.add_argument("--lr", type=float, default=2e-5)
p.add_argument("--gradient_checkpointing", action="store_true")
p.add_argument("--local_rank", type=int, default=-1)
args, _unknown = p.parse_known_args()
tok = AutoTokenizer.from_pretrained(args.model, trust_remote_code=True)
if tok.pad_token is None:
tok.pad_token = tok.eos_token
raw = load_dataset("yahma/alpaca-cleaned", split=f"train[:{args.rows}]")
def to_text(ex):
instr, inp, out = ex["instruction"], ex.get("input") or "", ex["output"]
if inp:
text = f"### Instruction:\n{instr}\n\n### Input:\n{inp}\n\n### Response:\n{out}"
else:
text = f"### Instruction:\n{instr}\n\n### Response:\n{out}"
return {"text": text}
ds = raw.map(to_text, remove_columns=raw.column_names)
ds = ds.map(lambda b: tok(b["text"], truncation=True, max_length=args.seq_len, padding=False),
batched=True, remove_columns=["text"])
model = AutoModelForCausalLM.from_pretrained(
args.model, dtype=torch.bfloat16, trust_remote_code=True, attn_implementation="sdpa",
)
if args.gradient_checkpointing:
model.gradient_checkpointing_enable()
model.config.use_cache = False
targs = TrainingArguments(
output_dir=args.output_dir, max_steps=args.max_steps,
per_device_train_batch_size=args.per_device_train_batch_size,
gradient_accumulation_steps=args.gradient_accumulation_steps,
learning_rate=args.lr, bf16=True, logging_steps=1,
save_strategy="steps", save_steps=args.max_steps, save_total_limit=1,
report_to=[], dataloader_num_workers=0, deepspeed=args.deepspeed,
gradient_checkpointing=args.gradient_checkpointing,
remove_unused_columns=False, ddp_find_unused_parameters=False,
logging_nan_inf_filter=False,
)
trainer = Trainer(model=model, args=targs, train_dataset=ds,
data_collator=DataCollatorForLanguageModeling(tok, mlm=False),
processing_class=tok)
trainer.train()
trainer.save_model(args.output_dir)
tok.save_pretrained(args.output_dir)
if __name__ == "__main__":
main()
ds_z2.json:
{
"train_batch_size": "auto",
"train_micro_batch_size_per_gpu": "auto",
"gradient_accumulation_steps": "auto",
"bf16": { "enabled": true },
"zero_optimization": {
"stage": 2,
"allgather_partitions": true,
"reduce_scatter": true,
"overlap_comm": false,
"contiguous_gradients": true
},
"gradient_clipping": 1.0,
"steps_per_print": 1,
"wall_clock_breakdown": false
}
ds_z3.json — same auto batch keys, "stage": 3, plus "stage3_gather_16bit_weights_on_model_save": true so save_model writes a single model.safetensors.
Do not add "optimizer": { "type": "AdamW" } unless you have CUDA_HOME or set "torch_adam": true. On this image, that JSON made DeepSpeed JIT FusedAdam and exited with CUDA_HOME environment variable is not set. Let HF Trainer own Adam.
0.5B one-GPU baseline
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP 'bash -s' <<'EOF'
source ~/ds_env/bin/activate
CUDA_VISIBLE_DEVICES=0 python -u ~/train_ds.py \
--output_dir ~/ds-out/0p5-1gpu --max_steps 20 --seq_len 256
EOF
This run: train_runtime 15.76 s, loss 1.368, peak 5985 MiB on GPU 0, 0 MiB on GPU 1. Wrote ~/ds-out/0p5-1gpu/model.safetensors (943 MB / 494M params). Full fine-tune, not a LoRA adapter.
0.5B two-GPU DDP (no DeepSpeed)
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP \
'source ~/ds_env/bin/activate && torchrun --nproc_per_node=2 --standalone ~/train_ds.py --output_dir ~/ds-out/0p5-ddp --max_steps 20 --seq_len 256'
This run: train_runtime 18.08 s, 8.848 samples/s (about 1.7× the 1-GPU 5.077 samples/s), peak 7257 / 7033 MiB, 100% util on both. GPU 1 is no longer idle. Same peak as the August 31 capture.
0.5B ZeRO-2 and ZeRO-3
The DeepSpeed launcher warns Unable to find hostfile, will proceed with training with local resources only on a single node. That is expected. Do not invent a hostfile for one VM.
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP 'bash -s' <<'EOF'
source ~/ds_env/bin/activate
deepspeed --num_gpus=2 ~/train_ds.py --deepspeed ~/ds_z2.json \
--output_dir ~/ds-out/0p5-z2 --max_steps 20 --seq_len 256
deepspeed --num_gpus=2 ~/train_ds.py --deepspeed ~/ds_z3.json \
--output_dir ~/ds-out/0p5-z3 --max_steps 20 --seq_len 256
EOF
| Job | train_runtime | samples/s | Peak GPU0 / GPU1 | Checkpoint |
|---|---|---|---|---|
| ZeRO-2 | 35.19 s | 4.546 | 8977 / 8099 MiB | model.safetensors 1.2 GB + zero_to_fp32.py |
| ZeRO-3 | 49.99 s | 3.201 | 8529 / 8433 MiB | model.safetensors 943 MB + checkpoint-20/zero_to_fp32.py |
On 0.5B, ZeRO uses more VRAM and fewer samples/s than DDP. The overhead is real. Size the SKU for the 7B job.
7B full FT — why ZeRO exists
Same script, --gradient_checkpointing, 8 steps, Qwen/Qwen2.5-7B-Instruct.
One GPU (CUDA_VISIBLE_DEVICES=0, default GAS 4):
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP 'bash -s' <<'EOF'
source ~/ds_env/bin/activate
CUDA_VISIBLE_DEVICES=0 python -u ~/train_ds.py \
--model Qwen/Qwen2.5-7B-Instruct \
--output_dir ~/ds-out/7b-1gpu \
--max_steps 8 --seq_len 256 --gradient_checkpointing
EOF
This run fit: train_runtime 55.43 s, loss 1.179, peak 61601 MiB on GPU 0, GPU 1 idle. Gradient checkpointing is why 7B full FT landed under 80 GB on one H100. train_runtime includes the 15 GB shard write, so wall time moves with disk; VRAM matched the August 31 capture exactly.
ZeRO-2 on both cards OOM’d. First step tried to allocate another 14.19 GiB (torch.zeros_like for Adam exp_avg_sq) with ~65 GB already in use on each GPU. ZeRO-2 still replicates parameters. Do not treat ZeRO-2 as a free 2× memory cut on this HF Trainer + torch Adam path.
ZeRO-3 completed:
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP 'bash -s' <<'EOF'
source ~/ds_env/bin/activate
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
deepspeed --num_gpus=2 ~/train_ds.py \
--model Qwen/Qwen2.5-7B-Instruct \
--deepspeed ~/ds_z3.json \
--output_dir ~/ds-out/7b-z3 \
--max_steps 8 --seq_len 256 --gradient_checkpointing \
--gradient_accumulation_steps 1
EOF
This run: train_runtime 125.2 s, then a second independent launch 134.8 s, loss 1.157 both times, peak 68855 / 68851 MiB both times, 100% util on both. Wrote ~/ds-out/7b-z3/model.safetensors (15 GB / 7.616B params) and checkpoint-8/zero_to_fp32.py. nvidia-smi sat at ~68 GB for the whole train, not only at save. VRAM matched the August 31 capture exactly on both repeats.
ZeRO-3 did not beat the 1-GPU 61.6 GB number on this 7B+checkpointing job. It used both cards. Use it when one 80 GB card is not enough (longer seq, larger batch, 13B / 32B / 70B), or when you already know DDP / ZeRO-2 OOM.
Confirm the checkpoint
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP \
'ls -lh ~/ds-out/0p5-z3/model.safetensors ~/ds-out/7b-z3/model.safetensors ~/ds-out/7b-z3/checkpoint-8/zero_to_fp32.py'
zero_to_fp32.py is the DeepSpeed helper that gathers ZeRO shards into one fp32 folder. This smoke already gathered 16-bit weights via stage3_gather_16bit_weights_on_model_save.
DeepSpeed vs Axolotl vs FSDP
| Path | What it is | Use this post? |
|---|---|---|
| DeepSpeed ZeRO | JSON + deepspeed --num_gpus=N, HF Trainer |
Yes — this guide |
| Axolotl DDP | YAML QLoRA, axolotl train + torchrun |
Axolotl multi-GPU |
| verl FSDP | HybridFlow GRPO, FSDP actor | verl multi-GPU |
| Unsloth | FastLanguageModel SFT |
Unsloth GPU cloud |
| LLaMA-Factory | YAML + WebUI | LLaMA-Factory GPU cloud |
| PEFT + bitsandbytes | Generic Hugging Face QLoRA | Existing QLoRA guide |
This VM did not bake off against Axolotl or FSDP. Do not quote a speedup from this article.
VRAM / GPU tier
| Job | This article | SKU |
|---|---|---|
| 0.5B full FT, 1 GPU | Measured: 5985 MiB, GPU 1 idle | 24 GB is enough; we used 2× 80 GB to show ZeRO |
| 0.5B DDP / ZeRO-2 / ZeRO-3 | Measured: ~7 / ~9 / ~8.5 GB both cards | Launcher smoke |
| 7B full FT, 1 GPU, checkpointing | Measured: 61601 MiB | One H100 80 GB |
| 7B ZeRO-2, 2 GPU | OOM (~72 GB then 14.19 GiB Adam) | Skip; use ZeRO-3 |
| 7B ZeRO-3, 2 GPU | Measured: ~68855 MiB both | This SKU |
| 70B full FT | Not run | 8× H100 SXM / NVLink — not this PCIe pair |
CTA: launch gpu_2x_h100 when you need two 80 GB cards and a DeepSpeed launcher. 7B QLoRA still belongs on Axolotl or Unsloth — this post is full FT + ZeRO.
Measured result
Same 2× H100, DeepSpeed 0.19.6, torch 2.12.1+cu130, yahma/alpaca-cleaned (1,024 rows), September 1, 2026 retest.
0.5B Instruct, 20 steps, seq 256
| Metric | 1 GPU | DDP 2 GPU | ZeRO-2 | ZeRO-3 |
|---|---|---|---|---|
train_runtime |
15.76 s | 18.08 s | 35.19 s | 49.99 s |
| Samples/s | 5.077 | 8.848 | 4.546 | 3.201 |
| Train loss | 1.368 | 1.418 | 1.433 | 1.432 |
Peak nvidia-smi |
5985 / 0 MiB | 7257 / 7033 | 8977 / 8099 | 8529 / 8433 |
7B Instruct, 8 steps, seq 256, gradient checkpointing
| Metric | 1 GPU (GAS 4) | ZeRO-2 | ZeRO-3 (GAS 1) |
|---|---|---|---|
train_runtime |
55.43 s | OOM | 125.2 s then 134.8 s |
| Train loss | 1.179 | — | 1.157 |
Peak nvidia-smi |
61601 / 0 MiB | 73693 then OOM | 68855 / 68851 (both repeats) |
| Checkpoint | 7B bf16 folder | — | 15 GB model.safetensors + zero_to_fp32.py |
This is a smoke, not a 70B run and not a vs-FSDP bake-off.
Troubleshooting
SSH fails or keeps asking for a password. Image 184’s user is Ubuntu, not ubuntu. Use -o IdentitiesOnly=yes. Refresh host keys with ssh-keygen -R YOUR_VM_IP if the instance was relaunched on a reused IP.
unrecognized arguments: --local_rank=0. The DeepSpeed launcher injects --local_rank. Add that flag (or parse_known_args()). This was the first ZeRO fail on this VM.
CUDA_HOME does not exist on import. Expected with DS_BUILD_OPS=0. ZeRO still trains. Do not add an "optimizer" block that loads FusedAdam unless a CUDA toolkit is installed.
Unable to find hostfile. Single-node default. Ignore. Multi-node needs a hostfile; this post is one VM.
P2P support not detected / PHB in nvidia-smi topo. This SKU is PCIe through the host bridge, not NVLink. Training still used both cards. Do not force P2P.
Only GPU 0 is busy. You launched with CUDA_VISIBLE_DEVICES=0 (the baseline) or without --num_gpus=2. The 1-GPU baseline is supposed to idle GPU 1.
ZeRO-2 OOM on 7B. Seen here: ~72 GB used, then Adam exp_avg_sq asked for 14.19 GiB. Switch to ZeRO-3, shorten seq_len, or QLoRA instead of full FT. overlap_comm: false did not save this job.
Gradient accumulation mismatch. Trainer may log GradientAccumulationPlugin has 1, DeepSpeed config has 4. Keep "gradient_accumulation_steps": "auto" and pass the same --gradient_accumulation_steps you want.
Gated Llama 401. This guide uses ungated Qwen2.5 Instruct. Llama 3.x needs HF_TOKEN.
Out of memory on 70B. Not measured. Use 8× SXM / NVLink SKUs in the pricing table, ZeRO-3 + CPU offload, or QLoRA. This 7B smoke is not a 70B test.
Train Large Models with DeepSpeed on Multi-GPU VMs
Launch NVIDIA H100 instances for DeepSpeed ZeRO full fine-tunes. Get two 80GB cards, ZeRO-2 / ZeRO-3, and per-second billing.
Think it. Build it. Scale it.
Quick Setup Reference
# 1. Launch gpu_2x_h100, image 184
# 2. Verify two GPUs + PHB topo
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP 'nvidia-smi -L; nvidia-smi topo -m'
# 3. venv + DeepSpeed (uv, CUDA 13, no fused ops)
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP 'bash -s' <<'EOF'
sudo apt-get update -qq && sudo apt-get install -y python3.12-venv curl ninja-build
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH" UV_TORCH_BACKEND=cu130
uv venv --python 3.12 ~/ds_env && source ~/ds_env/bin/activate
uv pip install torch==2.12.1 torchvision
DS_BUILD_OPS=0 uv pip install transformers datasets accelerate deepspeed
EOF
# 4. 0.5B 1-GPU then DDP then ZeRO-3
# CUDA_VISIBLE_DEVICES=0 python ~/train_ds.py --output_dir ~/ds-out/0p5-1gpu --max_steps 20
# torchrun --nproc_per_node=2 --standalone ~/train_ds.py --output_dir ~/ds-out/0p5-ddp --max_steps 20
# deepspeed --num_gpus=2 ~/train_ds.py --deepspeed ~/ds_z3.json --output_dir ~/ds-out/0p5-z3 --max_steps 20
# 5. 7B ZeRO-3
# deepspeed --num_gpus=2 ~/train_ds.py --model Qwen/Qwen2.5-7B-Instruct --deepspeed ~/ds_z3.json \
# --output_dir ~/ds-out/7b-z3 --max_steps 8 --seq_len 256 --gradient_checkpointing --gradient_accumulation_steps 1
# 6. Confirm gathered weights
ssh -o IdentitiesOnly=yes Ubuntu@YOUR_VM_IP \
'ls -lh ~/ds-out/7b-z3/model.safetensors ~/ds-out/7b-z3/checkpoint-8/zero_to_fp32.py'
Frequently Asked Questions
01What did this guide actually run?
gpu_2x_h100, image 184, DeepSpeed 0.19.6, torch 2.12.1+cu130, yahma/alpaca-cleaned (1,024 of 51,760 rows). 0.5B 20-step full FT: 1-GPU train 15.76 s, peak 5985 MiB; DDP 8.848 samples/s; ZeRO-2 35.19 s / ~9 GB; ZeRO-3 49.99 s / ~8.5 GB. 7B 8-step full FT with gradient checkpointing: 1-GPU 55.43 s / 61601 MiB; ZeRO-2 OOM (14.19 GiB Adam); ZeRO-3 125.2 s then 134.8 s / 68855 / 68851 MiB, 15 GB gathered checkpoint (7.616B params).
02Is ZeRO faster than DDP?
Not on this 0.5B smoke. DDP had the highest samples/s. ZeRO is for memory, not for speeding up a model that already fits.
03Should I start with ZeRO-3?
Start with one GPU if nvidia-smi stays under the card. Use DDP when you want two replicas. Use ZeRO-3 when 7B+ full FT OOMs (as ZeRO-2 did here). 70B-class: 8× SXM.
04DeepSpeed or FSDP?
This post is DeepSpeed JSON + deepspeed. FSDP is the PyTorch-native shard used in the verl GRPO guide. Axolotl can attach either via YAML. This VM did not run FSDP.
05Can Axolotl use this JSON?
Yes. Set deepspeed: ds_z3.json in the Axolotl YAML from the Axolotl multi-GPU post. That post’s smoke was DDP QLoRA, not ZeRO.
06Do I need a Hugging Face token?
Not for Qwen2.5-0.5B-Instruct, Qwen2.5-7B-Instruct, or yahma/alpaca-cleaned. Gated Llama weights need HF_TOKEN.
07What’s the difference between spot and on-demand?
Spot is cheaper and can be interrupted. This run used on-demand gpu_2x_h100. Use on-demand if a long epoch cannot restart.
Recipe tested on September 1, 2026 (repeat of the August 31, 2026 stack).











