The AY-Robots training matrix showing five VLA policies against four robot arms, each cell a guide with GPU tier and default hyperparameters
VLA trainingGPU memoryfine-tuningPyTorchcost

GPU Memory Math for VLA Training

AY-Robots ResearchAugust 23, 202625 min read

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.

What this page is not

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

BucketWhat it holdsScales withCan you predict it?
ParametersThe weights themselves, frozen and trainable alikeTotal parameter countYes, exactly
GradientsOne gradient per trainable parameterTrainable parameter countYes, exactly
Optimizer stateAdamW keeps two running moments per trainable parameter, plus an fp32 master copy under mixed precisionTrainable parameter countYes, exactly
ActivationsEvery intermediate tensor the backward pass needsBatch size, token count, image resolution, architectureOnly roughly, and only if you know the architecture
Everything elseCUDA context, cuBLAS and cuDNN workspaces, allocator fragmentation, pinned dataloader buffers, the checkpoint save spikeImplementation detailsNo. 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.

RegimeWeightsGradientsOptimizer stateTotal per trainable parameter
fp32 AdamW, no mixed precision4 B4 B8 B (two fp32 moments)16 B
bf16 or fp16 mixed precision, AdamW2 B2 B12 B (fp32 master + two fp32 moments)16 B
bf16 mixed precision, 8-bit AdamW2 B2 B6 B (fp32 master + two 8-bit moments)10 B
Frozen parameter, bf162 Bnonenone2 B
Frozen parameter, fp324 Bnonenone4 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.

Mixed precision is an activation optimization

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.

PolicyTotal paramsTrainable by defaultFrozen bytesTrainable bytesModel-state floor
GR00T N1.7, defaults3.0 B (bf16, per NVIDIA)~40 M (projector + diffusion action head)2.96 B x 2 B = 5.9 GB40 M x 16 B = 0.64 GB~6.6 GB
GR00T N1.7 with --tune-llm3.0 B~3.0 B03.0 B x 16 B = 48 GB~48 GB
GR00T N1.5~3 Bprojector + 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 float3203.0 B x 16 B = 48 GB~48 GB
SmolVLA450 M (paper)~100 M action expert (paper)350 M x 4 B = 1.4 GB100 M x 16 B = 1.6 GB~3.0 GB
ACT~80 M (paper)all of it, trained from scratch080 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.

text
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.
Korthikanti et al., equation 1: activation bytes per transformer layer

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 sequenceWhat that looks likeActivation memory, 16 layers, batch 2
1282 cameras, no language0.16 GiB
2002 cameras plus a short instruction0.28 GiB
3284 cameras plus the same instruction0.56 GiB
5844 cameras at double token density1.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.

Adding a third camera is not a 50 percent increase

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.

  1. 1
    Start 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.

    python
    total_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
  2. 2
    Subtract 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.

    python
    peak_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
  3. 3
    Turn 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.

    python
    def 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
  4. 4
    Check 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.

    bash
    CUDA_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 numbered step list on the AY-Robots GR00T N1.7 on SO-100 training guide, showing the sequence of a fine-tuning run
The GR00T N1.7 guide on /train/groot-n1-7-on-so-100 walks the same run described above. The batch size in the form is the number this article is trying to help you choose.
GR00T N1.5 takes the other road

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.

SmolVLAACT
Total parameters450 M~80 M
Trainable by default~100 M action expertall of it
Model-state floor~3.0 GB~1.3 GB
Platform batch size28
Left over on a 24 GB card~21 GB~22.7 GB
Dominant cost512x512 image tokens through 16 VLM layersResNet18 feature maps, 512-dim transformer
Inference latency245 ms per action step20 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.

bash
# 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=bf16
lerobot main (0.6.x). On the pinned 0.5.1 line neither --accelerator.mixed_precision nor a gradient accumulation flag exists.

The 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.

The AY-Robots cost table on the try page, listing which GPU each policy needs, typical run duration and price, and how many episodes are needed before a policy is useful
The card each policy needs is not a preference, it is the output of the arithmetic above. The 24 GB tier runs 2 to 5 hours at 0.30 to 0.60 USD per hour; the 80 GB tier runs 3 to 6 hours at 1.20 to 2.00 USD per hour.

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.

python
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))
Measure the peak before committing to a batch size

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.

  1. 1
    Sweep 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")
  2. 2
    Set 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.

    bash
    export 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
  3. 3
    If 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.

    python
    torch.cuda.memory._record_memory_history()
    # ... run a few training steps ...
    torch.cuda.memory._dump_snapshot("my_snapshot.pickle")
lerobot already logs this for you

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.

KnobWhat it removesPublished costWorth it for a VLA?
Freeze the backbone14 bytes per frozen parameter of model stateLess adaptation of the vision and language towerYes. Already the default for GR00T and SmolVLA, and the reason they fit at all.
Lower the batch sizeLinear in activationsNoisier gradients, slower convergence per stepYes, and it is the first thing to try.
Gradient accumulationNothing directly; lets you keep a small batch while raising the effective batchOne forward and backward per accumulation stepYes, when a small batch hurts convergence. Absent from lerobot 0.5.1, present in GR00T's trainer.
Mixed precision (bf16)Roughly half the activation memoryNeeds an Ampere or newer card; unlike fp16 it needs no loss scalingYes. Free on the cards these models run on, but you have to ask for it.
Gradient checkpointingCollapses per-layer activations from the 34-plus-attention term to 2sbhHugging Face documents about 20 percent slower training; the original paper measured 30 percent extra runtime, and NVIDIA reports 30 to 40 percent for transformersYes when activations dominate, but see the trap below: lerobot's flag is not wired up yet.
8-bit AdamW6 of the 16 bytes per trainable parameterbitsandbytes documents finetuning with 75 percent less GPU memory and no accuracy lossOnly when the trainable set is large. bitsandbytes warns that models dominated by activation memory do not really benefit.
LoRAAlmost all gradient and optimizer stateThe paper reports 10,000x fewer trainable parameters and 3x less GPU memory on GPT-3 175BSometimes. It does nothing for activations, which is the bucket that usually binds here.
Fewer or smaller camera streamsQuadratic in the attention termLess visual context for the policyEffective, but it changes the task rather than the run. Decide it on task grounds.
Doing the memory math up front
Advantages
  • 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.
Trade-offs
  • 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

GB, GiB, and the number nvidia-smi shows you

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_checkpointing exists on main, but the config validation raises an error calling it a placeholder that is not wired yet. --accelerator.compile is 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.

  1. 1
    Read 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
  2. 2
    Count 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.

    python
    trainable = 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")
  3. 3
    Probe 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.

    bash
    export PYTORCH_ALLOC_CONF=expandable_segments:True
    python probe.py --batch 2 --steps 5
    python probe.py --batch 8 --steps 5
  4. 4
    Rent the card and launch

    You now know the batch size, so the only remaining unknown is whether the policy learns anything.

    bash
    lerobot-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 AY-Robots policies comparison table listing the five trainable policies with parameter counts, required GPU, inference latency and minimum episode counts
The comparison table on /policies carries the same GPU tier column that the arithmetic above derives: parameter count and trainable fraction decide the card.

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 guides

Sources

Sources

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started