
Work out whether a VLA policy fits on a GPU before you rent it: bytes per parameter, optimizer state, activation memory, and the batch size that falls out of the arithmetic.
The short version
- •Model state is easy to predict: trainable parameters in billions times 16 equals gigabytes of weights, gradients and AdamW state. Frozen parameters cost only 2 bytes each in bfloat16.
- •Mixed precision does not shrink that number. Under the ZeRO paper's accounting, mixed-precision Adam is 16 bytes per trainable parameter, exactly what plain fp32 Adam costs. What mixed precision shrinks is activations.
- •Activations are the bucket you cannot read off a model card. They grow linearly with batch size and worse than linearly with token count, which is why camera count and resolution are the expensive knobs.
- •GR00T N1.7 fine-tuning fits in 40 GB because the LLM backbone stays frozen. Turn on --tune-llm and the model state alone becomes about 48 GB, which is why NVIDIA's own guide then asks for 80 GB.
- •The arithmetic gives you a floor, never the peak. Measure with torch.cuda.max_memory_allocated before you commit to a card, and on AY-Robots let the backend pick the card from required VRAM.
The ten minutes that save you a wasted rental
There are two ways to find out whether a vision-language-action model fits on the GPU you are about to rent. One is to rent it, launch the run, and read the CUDA out-of-memory traceback four minutes later. The other is arithmetic, which takes about ten minutes and works offline.
This page is that arithmetic: the four things that occupy VRAM during fine-tuning, the bytes-per-parameter constants from the published literature, the one bucket you genuinely cannot predict, and how to measure the rest in six lines of Python. The worked examples use the five policies you can actually train on this platform: GR00T N1.7, GR00T N1.5, Pi0.5, SmolVLA and ACT.
This is not a benchmark. Every VRAM figure below is either quoted from an upstream document that we link, or derived from published constants with the arithmetic shown. Nothing here was measured on our hardware. Where a derived number disagrees with an upstream measurement, we say so instead of picking the flattering one.
Four buckets, and only three of them are predictable
| Bucket | What it holds | Scales with | Can you predict it? |
|---|---|---|---|
| Parameters | The weights themselves, frozen and trainable alike | Total parameter count | Yes, exactly |
| Gradients | One gradient per trainable parameter | Trainable parameter count | Yes, exactly |
| Optimizer state | AdamW keeps two running moments per trainable parameter, plus an fp32 master copy under mixed precision | Trainable parameter count | Yes, exactly |
| Activations | Every intermediate tensor the backward pass needs | Batch size, token count, image resolution, architecture | Only roughly, and only if you know the architecture |
| Everything else | CUDA context, cuBLAS and cuDNN workspaces, allocator fragmentation, pinned dataloader buffers, the checkpoint save spike | Implementation details | No. Leave headroom. |
The first three buckets are the ones people quote, and together they are usually called the model state. For most VLA fine-tuning runs they are the smaller half of the problem. The fourth bucket is what decides your batch size, and the fifth is why a careful calculation still comes out optimistic.
Bytes per parameter, by training regime
The canonical accounting is in section 3.1 of the ZeRO paper. For a model of size psi, mixed-precision Adam holds an fp16 copy of the parameters and of the gradients at 2 bytes each, plus optimizer state made of an fp32 copy of the parameters, the momentum and the variance at 4 bytes each. The paper calls the optimizer multiplier K, states that mixed-precision Adam has K equal to 12, and totals it as 16 bytes per parameter. Plain fp32 Adam lands on the same 16 by a different route.
| Regime | Weights | Gradients | Optimizer state | Total per trainable parameter |
|---|---|---|---|---|
| fp32 AdamW, no mixed precision | 4 B | 4 B | 8 B (two fp32 moments) | 16 B |
| bf16 or fp16 mixed precision, AdamW | 2 B | 2 B | 12 B (fp32 master + two fp32 moments) | 16 B |
| bf16 mixed precision, 8-bit AdamW | 2 B | 2 B | 6 B (fp32 master + two 8-bit moments) | 10 B |
| Frozen parameter, bf16 | 2 B | none | none | 2 B |
| Frozen parameter, fp32 | 4 B | none | none | 4 B |
Read the first two rows again, because that pair is the most common misconception in this subject. Moving from fp32 to bf16 mixed precision does not reduce model state at all; it shuffles bytes around inside the 16. The Hugging Face GPU training guide is blunt about the fp16 case: because an fp16 and an fp32 copy of the model are both resident, a small-batch run can end up using more memory rather than less.
The Mixed Precision Training paper (Micikevicius et al., arXiv 2017, ICLR 2018) stores weights, activations and gradients in IEEE half precision and reports that this can "reduce the memory consumption of deep learning models by nearly 2x". Two details get lost in retellings. The paper is about fp16; bf16 training came later and inherited the same accounting. And the saving lands on those three tensors, not on the fp32 master copy the same paper recommends keeping. If your run is dominated by optimizer state, mixed precision buys speed and nothing else. If it is dominated by activations, which is the normal case for a VLA with cameras attached, it roughly halves the dominant term.
Frozen parameters cost 2 bytes, trainable ones cost 16
This is the lever that makes 3B-parameter VLAs trainable on one card at all. Both GR00T and SmolVLA freeze most of themselves by default. NVIDIA's fine-tuning config sets tune_llm and tune_visual to False while tune_projector and tune_diffusion_model default to True, so a default run trains the projector and the diffusion action head and leaves the Cosmos-Reason2-2B backbone alone. The SmolVLA config in lerobot does the same with different names: freeze_vision_encoder and train_expert_only are both True, and the SmolVLA paper puts roughly 100 million of its 450 million parameters in the action expert.
So the model-state floor for a fine-tune is not 16 bytes times the total. It is 2 or 4 bytes times the frozen part plus 16 bytes times the trainable part. Here is that sum for the five trainable policies. Parameter counts come from the model cards and papers; the trainable fractions come from each project's own defaults, except the GR00T trainable count, which NVIDIA does not publish and which is taken from this platform's catalog.
| Policy | Total params | Trainable by default | Frozen bytes | Trainable bytes | Model-state floor |
|---|---|---|---|---|---|
| GR00T N1.7, defaults | 3.0 B (bf16, per NVIDIA) | ~40 M (projector + diffusion action head) | 2.96 B x 2 B = 5.9 GB | 40 M x 16 B = 0.64 GB | ~6.6 GB |
| GR00T N1.7 with --tune-llm | 3.0 B | ~3.0 B | 0 | 3.0 B x 16 B = 48 GB | ~48 GB |
| GR00T N1.5 | ~3 B | projector + action head by default | ~5.9 GB with the backbone frozen | ~0.6 GB frozen-backbone, ~48 GB fully unfrozen | ~6.6 GB, or ~48 GB unfrozen |
| Pi0.5, lerobot defaults | ~3 B (PaliGemma backbone) | all of it: train_expert_only is False, dtype is float32 | 0 | 3.0 B x 16 B = 48 GB | ~48 GB |
| SmolVLA | 450 M (paper) | ~100 M action expert (paper) | 350 M x 4 B = 1.4 GB | 100 M x 16 B = 1.6 GB | ~3.0 GB |
| ACT | ~80 M (paper) | all of it, trained from scratch | 0 | 80 M x 16 B = 1.28 GB | ~1.3 GB |
That table already explains the GPU tiers on this platform. ACT and SmolVLA have floors of one to three gigabytes and sit comfortably on a 24 GB card. GR00T N1.7 and Pi0.5 are on the A100 80 GB or H100 80 GB tier, and the Pi0.5 row shows why: lerobot's pi05 configuration defaults to dtype float32 with nothing frozen, so the model state alone is around 48 GB before a single image is loaded.
Activations: the bucket that sets your batch size
Activations are every intermediate tensor the backward pass will need. Unlike model state they depend on the shape of the data, and for a transformer there is a published closed form. Equation 1 of NVIDIA's activation-recomputation paper gives the per-layer cost with no parallelism, in bytes, assuming 16-bit storage.
activations_per_layer = s * b * h * (34 + 5 * a * s / h)
s = sequence length (tokens)
b = microbatch size
h = hidden dimension
a = number of attention heads
total = activations_per_layer * L # L = number of layers
# The 34*s*b*h term is linear in tokens.
# The 5*a*s^2*b term is quadratic in tokens: that is attention.
# With full activation recomputation the whole thing collapses to 2*s*b*h*L.For a VLA the sequence is not a sentence. It is image tokens plus language tokens plus a state token, and the image tokens dominate. Take SmolVLA's backbone as a concrete case. Its lerobot config pads images to 512x512 and its SmolVLM2 backbone uses a patch size of 16, which is 1024 patches per image, folded down by the pixel-shuffle scale_factor of 4 to 64 tokens per camera. Text hidden size is 960 with 15 attention heads, and the policy uses only the first 16 of the backbone's 32 language-model layers. Plugging those into equation 1 at the platform default batch size of 2 gives the following.
| Tokens in the sequence | What that looks like | Activation memory, 16 layers, batch 2 |
|---|---|---|
| 128 | 2 cameras, no language | 0.16 GiB |
| 200 | 2 cameras plus a short instruction | 0.28 GiB |
| 328 | 4 cameras plus the same instruction | 0.56 GiB |
| 584 | 4 cameras at double token density | 1.33 GiB |
Raising the token count by 64 percent, from 200 to 328, does not raise the memory by 64 percent: it doubles it. Another 78 percent on top, to 584 tokens, multiplies it by 2.4 again. That is the quadratic attention term taking over, and it is why resolution and camera count are more expensive than they look. Batch size, by contrast, is honestly linear: the same 200-token configuration costs 0.28 GiB at batch 2, 1.14 GiB at batch 8 and 4.54 GiB at batch 32.
Going from two cameras to three raises the token count by half, which raises the linear activation term by half and the attention term by about 2.25x. If you are already close to the ceiling, a third camera is more likely to trigger an out-of-memory than a batch size increase of the same nominal size. Decide your camera set before you tune the batch size, not after. The data collection guide covers how to choose that set on task grounds rather than memory grounds.
Worked example: GR00T N1.7, and why the guide asks for 80 GB
NVIDIA's hardware recommendation guide states two numbers that together give you a usable model of the whole run. It sets the fine-tuning minimum at one GPU with 40 GB or more of VRAM, and it says that default fine-tuning, which tunes the projector and diffusion action head rather than the backbone, keeps peak VRAM under about 35 GB per GPU. The README's single-GPU example runs at --global-batch-size 32.
- 1Start from the model-state floor
3.0 billion parameters in bfloat16 is 6.0 GB. About 40 million of them are trainable at 16 bytes each rather than 2, which adds another 0.56 GB. Call the floor 6.6 GB.
pythontotal_params = 3.0e9 trainable = 40e6 frozen_bytes = (total_params - trainable) * 2 # bf16 weights trainable_bytes = trainable * 16 # bf16 + fp32 master + 2 fp32 moments floor_gb = (frozen_bytes + trainable_bytes) / 1e9 print(round(floor_gb, 2)) # 6.56 - 2Subtract the floor from the reported peak
Upstream reports a peak under about 35 GB at a global batch size of 32. Everything above the floor is activations plus workspaces plus fragmentation, so that is roughly 28 GB spread across 32 samples.
pythonpeak_gb = 35.0 # upstream figure, batch 32 floor_gb = 6.56 per_sample_gb = (peak_gb - floor_gb) / 32 print(round(per_sample_gb, 2)) # 0.89 GB per sample - 3Turn it into a batch-size rule
Peak is roughly 6.6 GB plus 0.9 GB per sample. Reserve 10 percent of the card and solve for the batch size. This is an estimate built from two published numbers, not a measurement, so treat the answer as an upper bound to test rather than a promise.
pythondef max_batch(card_gb, floor=6.6, per_sample=0.9, headroom=0.10): usable = card_gb * (1 - headroom) return int((usable - floor) / per_sample) for card in (24, 40, 48, 80, 96): print(card, max_batch(card)) # 24 -> 16 40 -> 32 48 -> 40 80 -> 72 96 -> 88 - 4Check the answer against reality, and believe reality
The rule says batch 16 would fit on a 24 GB card. Upstream says the minimum is 40 GB. The gap is bucket five: CUDA context, cuDNN workspaces, pinned dataloader buffers and the transient spike when a checkpoint is written. When a derived rule and a vendor minimum disagree, the vendor minimum is the one that has actually been run.
bashCUDA_VISIBLE_DEVICES=0 uv run python \ gr00t/experiment/launch_finetune.py \ --base-model-path nvidia/GR00T-N1.7-3B \ --dataset-path demo_data/cube_to_bowl_5 \ --embodiment-tag NEW_EMBODIMENT \ --modality-config-path examples/SO100/so100_config.py \ --num-gpus 1 \ --output-dir /tmp/test_finetune \ --max-steps 2000 \ --global-batch-size 32 \ --dataloader-num-workers 4
The same arithmetic explains the flag that everyone eventually reaches for and regrets. Enabling --tune-llm makes essentially all 3 billion parameters trainable, which takes the model state from 6.6 GB to 3.0 times 16, or 48 GB. That leaves about 24 GB of an 80 GB card for activations, and it is exactly why the upstream guide recommends 80 GB or more per GPU once --tune-llm or --tune-visual is on. The line between the 40 GB tier and the 80 GB tier is one boolean.

The platform default for GR00T N1.5 is batch size 1 with gradient accumulation 16, against batch size 32 with no accumulation for N1.7. Gradient accumulation does apply for both, because GR00T's own trainer implements --gradient-accumulation-steps and multiplies it into the global batch size. That gets N1.5 to an effective batch of 16 against N1.7's 32, and it gets there at a much lower activation peak, at the cost of 16 forward and backward passes per optimizer step. Gradient accumulation buys memory with wall-clock time, and buys nothing at all on the model-state side.
Worked example: the 24 GB tier
SmolVLA and ACT are the two policies you can train on an RTX 4090 or any other 24 GB card, and the arithmetic shows real headroom rather than a squeeze. The ACT paper is the sanity check here: its authors trained the model in about five hours on a single 11 GB RTX 2080 Ti. Both policies are trained through lerobot, whose training step defaults live in one dataclass: batch_size 8, steps 100000, num_workers 4, save_freq 20000 and seed 1000.
| SmolVLA | ACT | |
|---|---|---|
| Total parameters | 450 M | ~80 M |
| Trainable by default | ~100 M action expert | all of it |
| Model-state floor | ~3.0 GB | ~1.3 GB |
| Platform batch size | 2 | 8 |
| Left over on a 24 GB card | ~21 GB | ~22.7 GB |
| Dominant cost | 512x512 image tokens through 16 VLM layers | ResNet18 feature maps, 512-dim transformer |
| Inference latency | 245 ms per action step | 20 ms per action step |
There is a version detail here that matters more than it should, and it is worth dating because upstream moves. On the lerobot 0.5.1 line that this platform pins, the policy config carries a use_amp field, but the training script never reads it: it builds its Accelerator without a mixed_precision setting, so training runs in fp32 and stores activations at roughly twice the bf16 cost. On lerobot main, which carries the version string 0.6.2 while 0.6.1 is the newest release on PyPI, mixed precision is a first-class accelerator setting that defaults to "no" and has to be asked for.
# single GPU, bf16 mixed precision, small batch
lerobot-train \
--policy.type=smolvla \
--dataset.repo_id=<user>/<dataset> \
--batch_size=2 \
--steps=20000 \
--policy.device=cuda \
--accelerator.mixed_precision=bf16
# eight GPUs, sharded
torchrun --nproc-per-node=8 $(which lerobot-train) \
--dataset.repo_id=... --policy.type=act \
--parallelism.dp_shard=8 --accelerator.mixed_precision=bf16The same pinning explains the training form. The gradient accumulation field does not take effect for Pi0.5 or SmolVLA, because lerobot 0.5.1 has no such flag at all; accumulation only arrived with the accelerator config on the 0.6 line. It does apply for both GR00T variants, which use NVIDIA's trainer rather than lerobot's. The training documentation and the out-of-memory failure mode page both record which knobs are live for which trainer.

Stop guessing: measure the peak
Every number above is a floor or an estimate. The peak is a measurement, and PyTorch gives it to you for free. torch.cuda.max_memory_allocated returns the high-water mark of tensor memory since the last reset, and torch.cuda.reset_peak_memory_stats clears it. Wrap a handful of training steps and you have the real answer in under a minute of GPU time, which on the 24 GB tier costs about a cent.
import torch
GB = 1024 ** 3
def probe(batch_size, steps=5):
torch.cuda.empty_cache()
torch.cuda.reset_peak_memory_stats()
for _ in range(steps):
batch = make_batch(batch_size) # your dataloader
loss = policy(batch)[0]
loss.backward()
optimizer.step()
optimizer.zero_grad(set_to_none=True)
return {
"peak_alloc_gb": torch.cuda.max_memory_allocated() / GB,
"peak_reserved_gb": torch.cuda.max_memory_reserved() / GB,
}
for b in (1, 2, 4, 8, 16):
print(b, probe(b))Watch the gap between allocated and reserved. Allocated is what your tensors hold; reserved is what the caching allocator has taken from the driver and not returned. A large and growing gap is fragmentation, and the PyTorch CUDA notes describe the two options for it: expandable_segments, which is marked experimental and defaults to False, and max_split_size_mb, which stops the native allocator from splitting blocks larger than the given size. In PyTorch 2.13, the current stable release, the variable is named PYTORCH_ALLOC_CONF and PYTORCH_CUDA_ALLOC_CONF is kept as a backward-compatible alias.
- 1Sweep the batch size on the smallest card you own
Run the probe above at batch 1, 2, 4, 8 and 16. Two of those points give you the slope and intercept of a straight line, and the line predicts the rest.
python# peak(b) ~= floor + per_sample * b # solve from two measured points b1, p1 = 2, 9.4 b2, p2 = 8, 14.8 per_sample = (p2 - p1) / (b2 - b1) floor = p1 - per_sample * b1 print(f"peak(b) = {floor:.1f} + {per_sample:.2f} * b") - 2Set the allocator variable if reserved runs far ahead of allocated
Fragmentation is the failure mode where the run that worked yesterday dies at step 4000 today. Both options are documented in the PyTorch CUDA memory-management notes.
bashexport PYTORCH_ALLOC_CONF=expandable_segments:True # older PyTorch, or if you prefer the alias: export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True,max_split_size_mb:128 - 3If you still need the shape of the problem, take a snapshot
The snapshot records stack traces for every allocation. Drop the pickle into the visualiser at pytorch.org/memory_viz, which the docs describe as a JavaScript application that runs locally and uploads nothing.
pythontorch.cuda.memory._record_memory_history() # ... run a few training steps ... torch.cuda.memory._dump_snapshot("my_snapshot.pickle")
The lerobot training loop calls torch.cuda.reset_peak_memory_stats() at the start of every update and records torch.cuda.max_memory_allocated() / (1024**3) as the metric gpu_mem_gb, reduced with max across ranks because headroom is gated by the worst-case rank. If you are training ACT or SmolVLA through lerobot, the number you need is already in the log line. Read it at step 100 rather than waiting for step 4000.
The knobs, ranked by what they actually buy
Not all memory savings are equal, and several popular ones do very little for a vision policy. This ordering is by how much VRAM each option removes per unit of pain, with the sourced cost of each.
| Knob | What it removes | Published cost | Worth it for a VLA? |
|---|---|---|---|
| Freeze the backbone | 14 bytes per frozen parameter of model state | Less adaptation of the vision and language tower | Yes. Already the default for GR00T and SmolVLA, and the reason they fit at all. |
| Lower the batch size | Linear in activations | Noisier gradients, slower convergence per step | Yes, and it is the first thing to try. |
| Gradient accumulation | Nothing directly; lets you keep a small batch while raising the effective batch | One forward and backward per accumulation step | Yes, when a small batch hurts convergence. Absent from lerobot 0.5.1, present in GR00T's trainer. |
| Mixed precision (bf16) | Roughly half the activation memory | Needs an Ampere or newer card; unlike fp16 it needs no loss scaling | Yes. Free on the cards these models run on, but you have to ask for it. |
| Gradient checkpointing | Collapses per-layer activations from the 34-plus-attention term to 2sbh | Hugging Face documents about 20 percent slower training; the original paper measured 30 percent extra runtime, and NVIDIA reports 30 to 40 percent for transformers | Yes when activations dominate, but see the trap below: lerobot's flag is not wired up yet. |
| 8-bit AdamW | 6 of the 16 bytes per trainable parameter | bitsandbytes documents finetuning with 75 percent less GPU memory and no accuracy loss | Only when the trainable set is large. bitsandbytes warns that models dominated by activation memory do not really benefit. |
| LoRA | Almost all gradient and optimizer state | The paper reports 10,000x fewer trainable parameters and 3x less GPU memory on GPT-3 175B | Sometimes. It does nothing for activations, which is the bucket that usually binds here. |
| Fewer or smaller camera streams | Quadratic in the attention term | Less visual context for the policy | Effective, but it changes the task rather than the run. Decide it on task grounds. |
- You know before you rent whether the run can start, and roughly what batch size to ask for.
- You can tell a model that does not fit from a dataloader that is leaking, which an OOM traceback renders identically.
- The trainable-versus-frozen split becomes visible, and with it the real reason a 3B model fits on one card.
- Cost follows directly: about 4 to 12 USD on the 80 GB tier, about 1 to 3 USD on the 24 GB tier.
- It gives you a floor, never a peak. The size of the gap is not predictable from first principles.
- Activation estimates need hidden size, head count, layer count and token count. Model cards often omit at least one.
- It says nothing about whether the resulting policy is any good. A run that fits is not a run that works.
- Host RAM has its own ceiling. GR00T's num_shards_per_epoch controls dataset preloading, and the hardware recommendation guide attributes it to host memory while the config docstring attributes it to VRAM.
Traps that eat a day
3 billion parameters times 16 bytes is 48 GB in decimal and 44.7 GiB in binary, and nvidia-smi reports MiB. A card advertised as 80 GB does not give you 80 GiB, and the CUDA context takes its cut before your first tensor. If your calculation lands within 10 percent of the card's nameplate capacity, it does not fit. Treat anything above 90 percent utilisation as a failed calculation, not a tight one.
- Gradient checkpointing is not currently reachable from lerobot's CLI.
--accelerator.activation_checkpointingexists on main, but the config validation raises an error calling it a placeholder that is not wired yet.--accelerator.compileis in the same state. Plan around the batch size instead. - Reserved memory climbing while allocated stays flat is fragmentation, not a leak. Reach for expandable_segments before you reach for a bigger card.
- The checkpoint write is a spike. GR00T's save_only_model skips optimizer, scheduler and RNG state, at the price of being unable to resume from that checkpoint.
- torch.cuda.empty_cache() releases only unused cached blocks and cannot free memory your tensors still hold. Hugging Face puts the cost of using it during training at roughly 10 percent slower.
- A LeRobot v3.0 dataset crashes the GR00T loader outright and has to be converted down to v2.1 first. That traceback has nothing to do with memory; see the dataset rejected page.
- GR00T's launch_finetune.py is a tyro CLI that exposes no seed, and NVIDIA notes 5 to 6 percent variance between runs from non-deterministic image augmentations. Two runs at the same batch size can peak slightly differently. lerobot's default seed is 1000.
Two ways to reach the same answer
Clone the trainer, read the config dataclass for the trainable-parameter split, do the bytes-per-parameter sum by hand, then rent a card and probe the peak empirically before you commit to a long run.
- 1Read the freeze flags out of the source
This is the number that decides everything else. For GR00T it is four booleans in one file; for lerobot policies it is two, plus a dtype.
bash# GR00T grep -n 'tune_llm\|tune_visual\|tune_projector\|tune_diffusion' \ gr00t/configs/finetune_config.py # lerobot policies grep -n 'freeze_vision_encoder\|train_expert_only\|dtype' \ src/lerobot/policies/smolvla/configuration_smolvla.py \ src/lerobot/policies/pi05/configuration_pi05.py - 2Count the trainable parameters for real
Do not trust the round number on the model card. Load the model with the flags you intend to use and count.
pythontrainable = sum(p.numel() for p in model.parameters() if p.requires_grad) frozen = sum(p.numel() for p in model.parameters() if not p.requires_grad) model_state_gb = (frozen * 2 + trainable * 16) / 1e9 print(f"{trainable/1e6:.1f} M trainable, floor {model_state_gb:.2f} GB") - 3Probe the peak at two batch sizes and fit the line
Two points give you slope and intercept. Then pick the largest batch that leaves 10 percent of the card free.
bashexport PYTORCH_ALLOC_CONF=expandable_segments:True python probe.py --batch 2 --steps 5 python probe.py --batch 8 --steps 5 - 4Rent the card and launch
You now know the batch size, so the only remaining unknown is whether the policy learns anything.
bashlerobot-train \ --policy.type=act \ --dataset.repo_id=<user>/<dataset> \ --batch_size=8 \ --steps=100000 \ --policy.device=cuda \ --accelerator.mixed_precision=bf16
The advantage of this route is that you own every number and can change any of them. The cost is that you are debugging CUDA, driver versions, dataset format conversion and the trainer at the same time, on a rented clock. The SO-100 end-to-end guide covers what surrounds this step.
The training form takes a model, a LeRobot dataset and hyperparameters. The backend reads the required VRAM for that policy, rents a matching GPU on the spot market, runs the trainer and writes checkpoints to object storage. You do not choose the card, which means the arithmetic in this article tells you what the run will cost rather than whether it will start.
| What you set | GR00T N1.7 | Pi0.5 | SmolVLA | ACT |
|---|---|---|---|---|
| Batch size default | 32 | 1 | 2 | 8 |
| Learning rate default | 1e-4 | 5e-5 | 1e-4 | 1e-5 |
| Max steps default | 20000 | 30000 | 20000 | 100000 |
| Gradient accumulation | 1, and it applies | 16, but inert on lerobot 0.5.1 | 8, but inert | 1, and it does not apply |
| Extra fields in the form | saveSteps | seed, logFreq | seed, logFreq | chunkSize, nActionSteps, seed, logFreq |
| GPU tier the backend picks | A100 or H100 80 GB | A100 or H100 80 GB | RTX 4090 or any 24 GB | RTX 4090 or any 24 GB |
Two honest limits. First, there is no gradient checkpointing switch, no 8-bit optimizer choice and no LoRA toggle in the form, so the memory-reduction techniques in the table above are not available to you here; a tight fit is solved by renting a larger card instead. Second, the gradient accumulation field really is inert for Pi0.5 and SmolVLA because of the pinned lerobot version, and setting it will change your effective batch size not at all.
You do not debug CUDA versions, you do not pay for the hour you spent finding the right batch size, and the pod carries an idle watchdog that destroys it after an idle period so nothing bills silently. A run on the 80 GB tier takes 3 to 6 hours and costs about 4 to 12 USD; the 24 GB tier is 2 to 5 hours and about 1 to 3 USD. The pricing page has the current numbers and the training page has a guide for every model and arm combination.

Where the arithmetic stops helping
Fitting is a low bar. The memory math tells you nothing about whether 50 episodes are enough, whether your normalisation statistics are right, or whether the policy will generalise past the exact table height you recorded at. It is a gate, not a predictor.
- It says nothing about throughput. A run that fits at batch 1 with 16 accumulation steps can be far slower in wall-clock than one that fits at batch 32, at identical effective batch size.
- It says nothing about inference. Serving needs the weights plus a working set, and NVIDIA quotes a 16 GB minimum for GR00T N1.7 inference against 40 GB for fine-tuning. A policy that took an 80 GB card to train may serve on a 4090.
- It does not predict data-loading stalls. If your GPU sits at 30 percent utilisation, memory is not your problem and a bigger batch will not fix it.
- It cannot see inference latency, which is what actually decides whether the trained policy is usable on the arm.
That last point is the limit this platform does not engineer away. The control loop runs at 20 to 485 ms per action step depending on the model, and adding public-internet round trips turns a working policy into a hesitant one. Remote inference is fine for slow pick-and-place and is not fine for fast reactive motion, no matter how the training run went. Compare the numbers yourself on the GR00T versus Pi0.5 comparison or across all 85 models on the arena leaderboard, and read the VLA overview if you are still choosing an architecture.
How much VRAM do I need to fine-tune a 3B VLA?▾
It depends entirely on how much of it you unfreeze. With the backbone frozen, as GR00T N1.7 does by default, the model state is about 6.6 GB and NVIDIA's guide reports peak VRAM under about 35 GB at global batch size 32, with a stated 40 GB minimum. Unfreeze the language model with --tune-llm and the model state alone becomes about 48 GB, which is why the same guide then recommends 80 GB or more per GPU.
Does bfloat16 halve my memory?▾
It roughly halves activation memory, which for a camera-driven policy is usually the dominant term. It does not halve model state. Under the ZeRO accounting, mixed-precision Adam is 16 bytes per trainable parameter, the same total as plain fp32 Adam, because the fp32 master copy and the two fp32 moments are still there.
What is the fastest way to find my maximum batch size?▾
Measure it. Wrap five training steps in torch.cuda.reset_peak_memory_stats() and torch.cuda.max_memory_allocated(), run at two batch sizes, fit a straight line, and pick the largest batch that leaves 10 percent of the card free. On the 24 GB tier that experiment costs about a cent. If you train through lerobot the number is already logged as gpu_mem_gb every step.
Will gradient checkpointing let me train GR00T on a 4090?▾
No. Gradient checkpointing attacks activations, and it cannot remove weights. Even with the backbone frozen the model state is 6.6 GB, and NVIDIA states a 40 GB minimum for fine-tuning. On AY-Robots GR00T and Pi0.5 are cloud-only for this reason; SmolVLA and ACT also run locally on a 24 GB card.
Why is the Pi0.5 default batch size 1?▾
Because lerobot's pi05 configuration defaults to dtype float32 with train_expert_only set to False, so all roughly 3 billion parameters are trainable in fp32. At 16 bytes per trainable parameter that is about 48 GB of model state on an 80 GB card before any image is loaded, which leaves room for very few samples. Batch size 1 is not conservatism, it is arithmetic.
Does LoRA solve this?▾
Partly, and less than people expect. The LoRA paper reports 10,000 times fewer trainable parameters and 3 times less GPU memory on GPT-3 175B, and those savings are real for gradients and optimizer state. But it does nothing for activations, which is usually the term that binds for a VLA with two or three camera streams. bitsandbytes makes the same point about 8-bit optimizers.
Skip the card-shopping step
Pick a model and a dataset, and the backend rents a GPU that matches the required VRAM, runs the trainer and stores the checkpoints. Guides for every model and arm combination, with the defaults already filled in.
Open the training guidesSources
- ZeRO: Memory Optimizations Toward Training Trillion Parameter Models (Rajbhandari et al., 2019) - section 3.1, K=12 and the 16 bytes per parameter accounting
- Reducing Activation Recomputation in Large Transformer Models (Korthikanti et al., 2022) - equation 1 and the 30-40 percent full-recompute overhead
- Mixed Precision Training (Micikevicius et al., arXiv 2017, ICLR 2018) - fp16 storage, fp32 master weights, loss scaling
- Training Deep Nets with Sublinear Memory Cost (Chen et al., 2016) - gradient checkpointing at 30 percent extra runtime
- 8-bit Optimizers via Block-wise Quantization (Dettmers et al., ICLR 2022)
- LoRA: Low-Rank Adaptation of Large Language Models (Hu et al., 2021)
- SmolVLA: A Vision-Language-Action Model for Affordable and Efficient Robotics - 450M parameters, ~100M action expert, first 16 LLM layers
- Learning Fine-Grained Bimanual Manipulation with Low-Cost Hardware (ACT / ALOHA, Zhao et al., 2023) - ~80M parameters, trained on one 11 GB 2080 Ti
- PyTorch 2.13 CUDA semantics: caching allocator, PYTORCH_ALLOC_CONF, expandable_segments, max_split_size_mb
- PyTorch 2.13: understanding CUDA memory usage, _record_memory_history and _dump_snapshot
- torch.utils.checkpoint: activation checkpointing trades compute for memory
- Hugging Face Transformers, GPU training: gradient checkpointing ~20 percent slower, empty_cache ~10 percent slower, the fp16 two-copies warning
- bitsandbytes 8-bit optimizers: 75 percent less GPU memory, and the activation-memory caveat
- Isaac-GR00T hardware recommendations: 16 GB inference minimum, 40 GB fine-tuning minimum, ~35 GB peak with the backbone frozen, 80 GB with --tune-llm
- lerobot training loop (main): reset_peak_memory_stats, the gpu_mem_gb metric, --accelerator and --parallelism flags
Sources
- ZeRO: Memory Optimizations Toward Training Trillion Parameter Models (Rajbhandari et al., 2019) - section 3.1, K=12 and the 16 bytes per parameter accounting
- Reducing Activation Recomputation in Large Transformer Models (Korthikanti et al., 2022) - equation 1 and the 30-40 percent full-recompute overhead
- Mixed Precision Training (Micikevicius et al., arXiv 2017, ICLR 2018) - fp16 storage, fp32 master weights, loss scaling
- Training Deep Nets with Sublinear Memory Cost (Chen et al., 2016) - gradient checkpointing at 30 percent extra runtime
- 8-bit Optimizers via Block-wise Quantization (Dettmers et al., ICLR 2022)
- LoRA: Low-Rank Adaptation of Large Language Models (Hu et al., 2021)
- SmolVLA: A Vision-Language-Action Model for Affordable and Efficient Robotics - 450M parameters, ~100M action expert, first 16 LLM layers
- Learning Fine-Grained Bimanual Manipulation with Low-Cost Hardware (ACT / ALOHA, Zhao et al., 2023) - ~80M parameters, trained on one 11 GB 2080 Ti
- PyTorch 2.13 CUDA semantics: caching allocator, PYTORCH_ALLOC_CONF, expandable_segments, max_split_size_mb
- PyTorch 2.13: understanding CUDA memory usage, _record_memory_history and _dump_snapshot
- torch.utils.checkpoint: activation checkpointing trades compute for memory
- Hugging Face Transformers, GPU training: gradient checkpointing ~20 percent slower, empty_cache ~10 percent slower, the fp16 two-copies warning
- bitsandbytes 8-bit optimizers: 75 percent less GPU memory, and the activation-memory caveat
- Isaac-GR00T hardware recommendations: 16 GB inference minimum, 40 GB fine-tuning minimum, ~35 GB peak with the backbone frozen, 80 GB with --tune-llm
- lerobot training loop (main): reset_peak_memory_stats, the gpu_mem_gb metric, --accelerator and --parallelism flags
Ready for high-quality robotics data?
AY-Robots connects your robots to skilled operators worldwide.
Get Started