Skip to content

feat(sft): single-card equivalent of the 8-GPU H3 SFT recipe - #210

Draft
Rockdu wants to merge 9 commits into
mainfrom
sft/h3-sft-1gpu
Draft

feat(sft): single-card equivalent of the 8-GPU H3 SFT recipe#210
Rockdu wants to merge 9 commits into
mainfrom
sft/h3-sft-1gpu

Conversation

@Rockdu

@Rockdu Rockdu commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

What

scripts/run_diffusion_sft_h3_t2va_1gpu.pyrun_diffusion_sft_h3_t2va.py on one card, training the same thing rather than a scaled-down approximation. Same automation level as the 8-GPU recipe: prepare() fetches rockdu/WISA-80K-Practical-Dynamics-254, so a zero-argument launch is a complete run and --data-dir is the only data knob.

Below the docstring the two scripts differ in four lines: the run-name prefix, --fsdp-cpu-offload, --fsdp-master-dtype bf16, and 8 → 1 GPUs. Every *_args block — optimizer, LoRA, batch, sigma grid, reduce dtype — is byte-identical.

Why the two are equivalent

  • Batch schedule. global_batch_size is 8 in both. arguments.py derives it as rollout_batch_size × n_samples_per_prompt ÷ num_steps_per_rollout — no world-size term — so one card only changes the layout: 8 dp ranks × 1 micro-batch becomes 1 rank × 8 gradient-accumulation micro-batches at micro_batch_size=1, still 4 optimizer steps per rollout.
  • Gradient scale. actor.py backwards loss_sum / num_local_pairs: /8 on the single rank, /1 on each of eight before FSDP2's mesh-mean divides by 8 again.
  • Precision. --fsdp-master-dtype bf16 changes no math under --fsdp-cpu-offload. Only the LoRA adapters train and PEFT keeps those in fp32 whatever the base dtype (measured: base bf16 → lora_A/lora_B float32, requires_grad=True); the frozen base is gathered as bf16 for the forward at either setting, since MixedPrecisionPolicy applies param_dtype=bf16 to every wrap and H3 declares no param_dtype_patterns. Step 1 of an otherwise identical run reproduces the fp32 loss to every digit logged, 3.276656e-01. _keep_in_fp32_modules still holds proj_in/proj_out/audio_proj_*/time_embedder/rope in fp32; --fsdp-reduce-dtype stays fp32 because the LoRA gradients do train.

--fsdp-cpu-offload is the one flag that is a requirement rather than a free choice: H3 is 33.5B parameters, so even a bf16 master is ~66 GB and leaves no room for activations on a 139.8 GB H200.

What it costs

Against the 8-GPU reference run on the same dataset, warm cache. Per-step figures are perf/actor_train_time, so encoding is excluded.

8 GPUs 1 GPU
GPU peak (train step) ~57 GB/rank 39.5 GB — activations plus one block; no weights resident
host RAM 217 GB peak at init, 99 GB steady (~95 GB pinned)
init ~6 min
per optimizer step 30.5 s 278 s
per rollout (4 steps) 122 s 17.3 min

The 9× gap is 8× rank count plus ~14% for the PCIe round trip the offload adds to every block, on the forward and the gradient-checkpoint recompute alike. A cold .sft_cache costs more and only here — one encode worker instead of eight, ~480–590 s per rollout through the first epoch, during which the ~67 GB encoder rather than the train step owns the GPU peak.

Validation

A zero-argument run on a clean box, end to end:

  • prepare() downloaded the dataset (1.1 GB, 254 rows, clips/), and its relative media paths resolved — the path this recipe would otherwise never exercise, since every earlier run of mine used absolute paths.
  • FSDP init 6 min; encode 32/32 clips (583.7 s); 4 optimizer steps with per-sigma-bucket metrics; clean exit.
  • Separately, an 11-rollout run to iter_0000011scripts/export_lora.py → a 624-tensor rank-64/alpha-128 adapter structurally identical to the 8-GPU one, which then served under sglang serve --lora-path and reproduced the lora_sft_guide figure prompts.
  • --deterministic-mode was exercised and passes (no op lacks a deterministic implementation), but is not on by default: it costs ~25% per step and ~3.6 GB of GPU, enough to push the peak past a 40 GB card. Documented as --extra-args "--deterministic-mode".

Limits

The two curves are comparable on trend and on the per-sigma buckets, not point by point. prepare_sft_batch seeds the sigma and noise draw on (rollout_id, microbatch_id, dp_rank), so changing topology moves every sample to a different sigma by construction. A long-run trend comparison against the 8-GPU reference is not included here. The run that would have provided it reached 78 steps (19 rollouts) at fp32 master before I stopped it to switch the recipe to bf16; at that length both series still sit inside the 8-GPU run's own warm-up spread, so it would not have supported a conclusion either way.

Status is registered NV: no complete training curve has been run at this topology.

Checklist

  • pre-commit run --files <touched> passes
  • Added/updated tests — none; the script carries no library code, and its four differing lines are launch flags
  • python3 scripts/run_diffusion_sft_h3_t2va_1gpu.py --help parses
  • Zero-argument run validated end to end (see above)
  • New public flags — none; --fsdp-cpu-offload, --fsdp-master-dtype and --deterministic-mode all already exist and are documented
  • Docs updated (docs/models/h3/h3.md §4.1, recipe-verification.md)

Rockdu and others added 6 commits August 26, 2026 15:18
Same batch schedule as the 8-GPU recipe -- 32 samples per rollout,
num_steps_per_rollout=4, global_batch_size 8, micro_batch_size 1 -- with the
8 ranks' one micro-batch each folded into 8 gradient-accumulation micro-batches
on the single rank. Adds --fsdp-cpu-offload, without which H3's ~134 GB fp32
master does not fit beside the activations on one card.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The per-step numbers are perf/actor_train_time on a fully warm cache; the
single-worker encode cost lands in train_wait_time and was being read as if
it were included.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 35.3 GB figure came from one sample of an early rollout; the warm-cache
run peaks at 39.5 GB, which is what a card has to be provisioned for. Also
records that --fsdp-master-dtype moves host RAM, not GPU: under cpu-offload
the gathered copy is bf16 either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Only the LoRA adapters train and PEFT keeps them in fp32 whatever the base
dtype, while the frozen base is gathered as bf16 for the forward at either
setting -- MixedPrecisionPolicy uses param_dtype=bf16 on every wrap and H3
declares no param_dtype_patterns. So fp32 master changes no math here and
costs 66GB of host RAM. diffusers' _keep_in_fp32_modules still protects the
tensors MiniMax shipped as fp32.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Peak 217GB / steady 99GB at bf16 master, sampled through init. The steady
cost is CUDA pinned host memory for the offloaded shard, which is
shmem-accounted, does not touch /dev/shm, and cannot be paged out; the peak
is the mmap'd checkpoint briefly overlapping the state_dict copy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Rockdu
Rockdu changed the base branch from sft/relative-media-path to main August 26, 2026 22:19
Rockdu and others added 3 commits August 26, 2026 15:21
It was a 70-line section against 4.1's ten, repeating the whole memory
forensics the script docstring already carries. Both topologies are one
recipe with one schedule, so they belong in one section: a two-row recipe
table, both launch lines, what the extra two flags buy, and the cost table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One rank has no cross-rank reduction order to vary, so determinism is
reachable here and makes the recipe a reproducible reference. No attention
backend is pinned, so validate_attention_args takes the torch-native path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deterministic mode moves from a default to a documented opt-in: it buys
bit-reproducibility within a topology, which the recipe does not need to be
correct, and costs ~25% per step and ~3.6GB of GPU -- enough to push the
peak past a 40GB card.

The docstring and docs now lead with why the two recipes train the same
thing (global_batch_size has no world-size term; the gradient divisor works
out the same either way) rather than with the memory forensics.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Rockdu Rockdu changed the title feat(sft): 1-GPU H3 t2va LoRA SFT recipe feat(sft): single-card equivalent of the 8-GPU H3 SFT recipe Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant