The AY-Robots GPU cost table showing which card each trainable policy needs, typical run duration, price per run and minimum episodes
TrainingTroubleshootingGPUVLAIsaac-GR00TLeRobot

Fixing CUDA Out of Memory During VLA Training

AY-Robots ResearchAugust 23, 202619 min read

Every fix for CUDA out of memory in VLA fine-tuning, in the order to try them, with real Isaac-GR00T and lerobot flags and which levers quietly change the policy you get.

What you need to know

  • The error line names your fix. A large 'reserved by PyTorch but unallocated' figure means fragmentation, and an allocator flag solves that for free.
  • Mixed-precision AdamW costs about 18 bytes per parameter before any activation exists: 6 for the weight copies, 8 for the Adam moments, 4 for the gradient. Roughly 54 GB for a 3 B model.
  • Order: free the card, fix the dtype, cut the batch, accumulate to restore it, checkpoint activations, freeze modules, go LoRA, shrink the inputs, tune the allocator, rent a bigger card.
  • About half change the policy you get. Batch size, freezing, LoRA, resolution and chunk size do. Allocator flags, empty_cache and dataloader workers do not.
  • Isaac-GR00T's hardware guide (main, 2026-08-24) puts the default fine-tune under 35 GB peak per GPU, and says --tune-llm or --tune-visual pushes past 80 GB.
  • In lerobot main, --accelerator.activation_checkpointing.mode is a documented placeholder. The flag that works is --policy.gradient_checkpointing, and only Pi0.5 defines it.

Read the error line before you touch anything

CUDA out of memory is not one error. PyTorch's allocator prints a five-part sentence and each part points at a different fix, so reading it is the difference between one config change and an afternoon of bisection. The wording comes from CUDACachingAllocator.cpp. Here it is, from a fine-tuning run that died on an 80 GB card.

text
torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 2.34 GiB.
GPU 0 has a total capacity of 79.15 GiB of which 812.00 MiB is free.
Process 21874 has 78.35 GiB memory in use. Of the allocated memory
71.02 GiB is allocated by PyTorch, and 5.61 GiB is reserved by PyTorch
but unallocated. If reserved but unallocated memory is large try setting
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True to avoid fragmentation.
Illustrative numbers, real field layout. Your figures differ; the five fields do not.
FieldWhat it tells you
Tried to allocate XTens of GB means one tensor is wrong: resolution, chunk size, a stray concat. A small X means the card is full.
total capacity of YWhich card you actually got. Spot markets hand out 40 GB A100s when you assumed 80.
of which Z is freeZ near zero with a small X is a capacity problem. Several GB free with a smaller failing X is fragmentation.
allocated by PyTorchLive tensors: weights, gradients, optimizer state, activations. What batch size controls.
reserved but unallocatedCached blocks the allocator cannot serve. Large here means set the allocator flag first.
Reserved is not used

The PyTorch CUDA notes say the caching allocator keeps freed blocks so later allocations avoid a device synchronisation, and that this unused memory will still show as if used in nvidia-smi. Use memory_allocated() for live tensors, memory_reserved() for the allocator total. A few GB of gap is normal; tens of GB is fragmentation.

First check: is it the GPU at all?

Robot pipelines decode video on the CPU, in worker processes. A good share of reported out-of-memory failures on a LeRobot dataset never touched VRAM at all.

What you seeWhich memoryFirst change
torch.OutOfMemoryError with the five fieldsGPU VRAMEverything below
Process vanishes, exit code 137, no tracebackHost RAM, OS OOM killerFewer workers, smaller prefetch
DataLoader worker (pid N) killed by signalHost RAM in a workernum_workers and prefetch_factor down
Dies during loading, before step 1Host RAM during preloadGR00T: --num-shards-per-epoch. lerobot: --num_workers
Container restarts silentlyHost RAM under a container limitRaise the limit, not a trainer flag
The trap that eats a day: one flag, two contradictory docs

Isaac-GR00T's FinetuneConfig docstring for num_shards_per_epoch says reduce this number if vram is limited. The repo's hardware guide says the opposite: Reduce --num-shards-per-epoch if host memory (not VRAM) is limited. Both read from main on 2026-08-24, and the hardware guide matches the behaviour. Lower it hoping to fix a CUDA OOM and you change how much data the run sees per epoch, then hit the same wall.

Where the memory goes

Hugging Face's GPU memory anatomy page gives the per-parameter accounting for mixed-precision AdamW. It tells you which failures batch size can fix.

StoredBytes per parameterFor a 3 B VLAScales with batch?
Weights, bf16 copy26 GBNo
Weights, fp32 master copy412 GBNo
Adam momentum, fp32412 GBNo
Adam variance, fp32412 GBNo
Gradients, fp32412 GBNo
Subtotal, before any data18about 54 GBNo
Cached forward activationsvariesthe rest of the cardYes, near-linearly

Two consequences. A full fine-tune of a 3 B vision-language-action model does not fit on 24 GB at any batch size, which is why GR00T N1.7 and Pi0.5 are 80 GB-tier here while SmolVLA and ACT are not. And only the last row responds to batch size, so a run that dies at batch 1 needs a structural change.

Published figures agree. openpi states above 22.5 GB for a LoRA fine-tune on an RTX 4090 and above 70 GB for a full one on an A100 80 GB or H100. NVIDIA's guide states a 40 GB minimum and puts the default recipe, which tunes the projector and diffusion head rather than the language backbone, under about 35 GB. The gap between 35 and 70 is entirely which modules carry gradients.

The AY-Robots cost table listing which GPU each of the five trainable policies needs, typical run time, price per run and episodes required
The card each policy needs is a property of the model, not of your patience.

Pull the levers in this order

Order matters because the early levers are free and the late ones are not. Steps 1 to 5 cost time or nothing. From step 6 you are trading model quality, so do not start there just because the flag is easy to type.

  1. 1
    Free the card you already have

    A dead notebook kernel or a crashed run will sit on 20 GB. On a rented pod this is the commonest reason a run OOMs at step 0 after working yesterday.

    bash
    nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv
    kill -9 <pid>   # only processes you recognise as yours
  2. 2
    Check the dtype default

    lerobot's Pi0.5 config ships dtype: str = "float32". Training a 3 B model in fp32 doubles weights and activations for no benefit on Ampere or newer. Check the default before you set anything: lerobot's GR00T config already ships use_bf16 = True, so there is nothing to turn on there, while the accelerator's own mixed_precision still defaults to "no".

    bash
    lerobot-train --policy.type=pi05 --policy.dtype=bfloat16 ...   # default is "float32"
    lerobot-train --accelerator.mixed_precision=bf16 ...            # default is "no"
    
    # lerobot's groot policy is already use_bf16=True by default:
    # params stay FP32 and compute runs under BF16 autocast.
  3. 3
    Halve the batch size

    This tells you whether you are activation-bound or parameter-bound. If 32 dies and 16 runs, activations dominate. If batch 1 dies too, go to step 6.

    bash
    python gr00t/experiment/launch_finetune.py --global-batch-size 16 ...
    lerobot-train --batch_size=16 ...
  4. 4
    Accumulate to restore the effective batch

    Buys back the optimisation behaviour you gave away, at the cost of wall-clock time. GR00T's guide works an example: 4 GPUs, 8 accumulation steps, per-GPU batch 8 gives an effective global batch of 256.

    bash
    # GR00T: global_batch_size is PRE-accumulation, and the class warns you
    python gr00t/experiment/launch_finetune.py \
      --global-batch-size 16 --gradient-accumulation-steps 4 ...
    
    # lerobot main: it lives under the accelerator config
    lerobot-train --batch_size=16 --accelerator.gradient_accumulation.steps=4 ...
  5. 5
    Turn on gradient checkpointing

    Drop the cached activations and recompute them in the backward pass. Chen et al. 2016 took a 1000-layer residual network from 48 GB to 7 GB for about 30 percent extra running time; Hugging Face quotes about 20 percent for transformer fine-tunes.

    bash
    lerobot-train --policy.type=pi05 --policy.gradient_checkpointing=true ...
    
    # raw PyTorch: use_reentrant has no default any more
    from torch.utils.checkpoint import checkpoint
    out = checkpoint(block, x, use_reentrant=False)
  6. 6
    Freeze what you do not need to train

    Here the trade starts. Both stacks freeze most of the backbone already, and un-freezing pushes GR00T past 80 GB. lerobot names the price: the frozen-VLM variant is less memory, at some cost in success rate.

    bash
    # GR00T defaults: tune_llm=False, tune_visual=False,
    # tune_projector=True, tune_diffusion_model=True.
    # Adding --tune-llm or --tune-visual is what breaks a 40 GB card.
    
    lerobot-train --policy.type=pi05 \
      --policy.freeze_vision_encoder=true --policy.train_expert_only=true ...
  7. 7
    Switch to LoRA, if your version has it

    Adapters cut optimizer state and gradients to the adapter parameters. Two version traps: in lerobot main PEFT is a top-level config, not a policy flag, so it is --peft.* and it needs the peft extra installed; and on the Isaac-GR00T side the N1.5 script had --lora-rank while the N1.7 FinetuneConfig has no LoRA fields at all.

    bash
    # lerobot main: PeftConfig is top-level (method_type defaults to LORA, r to 16).
    # Passing any --peft.* field is what turns PEFT on; there is no --policy.use_peft.
    lerobot-train --peft.method_type=LORA --peft.r=16 --peft.lora_alpha=32 ...
    
    # Isaac-GR00T, N1.5 branch only
    python scripts/gr00t_finetune.py --lora-rank 64 --lora-alpha 128 ...
  8. 8
    Shrink the inputs, knowing the cost

    Fewer cameras, smaller images and shorter chunks cut activations hard and change the task the model solves. GR00T N1.7 gates resolution behind two flags set together.

    bash
    python gr00t/experiment/launch_finetune.py \
      --shortest-image-edge 224 --crop-fraction 0.9 ...
    
    lerobot-train --policy.chunk_size=25 --policy.n_action_steps=25 ...
  9. 9
    Tune the allocator, which costs nothing

    If the error showed several GB reserved-but-unallocated, this is your fix and none of the steps above were needed. In PyTorch 2.13 the variable is PYTORCH_ALLOC_CONF.

    bash
    export PYTORCH_ALLOC_CONF=expandable_segments:True
    
    # only if that is unavailable, and the docs call this a last resort:
    export PYTORCH_ALLOC_CONF=max_split_size_mb:512,garbage_collection_threshold:0.8
  10. 10
    Rent the right card

    Nine levers in, you have spent real time and given away accuracy. Price that against the right GPU for a few hours: 4 to 12 USD on the 80 GB tier here, 1 to 3 USD on 24 GB.

    bash
    uv run torchrun --nproc_per_node=8 --master_port=29500 \
      gr00t/experiment/launch_finetune.py \
      --base-model-path nvidia/GR00T-N1.7-3B \
      --num-gpus 8 --global-batch-size 64 ...

Which levers silently change the run

Some flags are pure infrastructure. Others alter what the model learns, and the effect surfaces weeks later as a policy that behaves differently from the one you validated. Comparing two checkpoints means holding every row marked yes constant.

LeverChanges the policy?Why
Allocator flags, empty_cache, fewer workersNoLayout and scheduling only; empty_cache costs about 10 percent
FSDP sharding or CPU offloadNoSame maths, distributed. Slower per step
Gradient checkpointingNo, in principleSame maths, recomputed. Determinism is not guaranteed if you move tensors across devices inside the function
bf16 instead of fp32SlightlyDifferent rounding; bf16 keeps fp32's range, so no loss scaling
8-bit optimizer statesSlightlybitsandbytes documents up to 75 percent less GPU memory with no accuracy loss
Smaller batch sizeYesNoisier gradients, and your learning rate was tuned for the old batch
Gradient accumulationYes, if the loss is normalised wronglySee the warning below
Freezing the vision encoder or the VLMYesFrozen modules stop adapting to your scene
LoRA adaptersYesLower-rank update, less capacity to move the backbone
Smaller images, fewer cameras, shorter chunksYes, a lotThe policy sees a different world and commits to a different horizon
Accumulation is not automatically equivalent to a bigger batch

In October 2024 Hugging Face published a fix for exactly this: the Trainer averaged per-micro-batch losses instead of summing over all micro-batches and dividing by the total item count. For token-weighted objectives the two differ, so batch 4 with 4 accumulation steps did not match batch 16. It landed through PR 34191. On an older pinned Transformers, or a hand-written loop, verify the equivalence yourself.

The BatchNorm folklore does not apply here

There is a folk rule that lowering batch size breaks BatchNorm. Check before acting on it. lerobot's ACT builds its ResNet-18 backbone with FrozenBatchNorm2d, so the statistics are baked in, and GR00T, Pi0.5 and SmolVLA build on transformer backbones that normalise within each sample rather than across the batch. Across all five trainable policies here the batch-size risk is gradient noise, not normalisation.

That risk is real enough. The default in FinetuneConfig on main is a global batch of 64 at a learning rate of 1e-4, and those numbers were chosen together. Cut to batch 8 and keep 1e-4 and you have changed the optimisation problem, not just the footprint.

Freezing and LoRA: the honest trade

Freezing the backbone or using LoRA instead of a full fine-tune
Advantages
  • Cuts optimizer state and gradients to the trainable subset, the largest fixed cost on the card
  • openpi puts LoRA above 22.5 GB against above 70 GB full, moving a 3 B model onto a 24 GB card
  • It is the default in both stacks, so leaving it on is no deviation from the tested recipe
  • Faster per step, and adapters are small enough that ten variants cost nothing to keep
Trade-offs
  • A frozen vision encoder cannot adapt to your lighting, table or camera
  • Low-rank updates run out of capacity first when the backbone must see something new
  • GR00T N1.7's FinetuneConfig exposes no LoRA fields, so there you get freezing but not adapters

Isaac-GR00T N1.7 specifics

Read from NVIDIA/Isaac-GR00T on main on 2026-08-24. The entry point moved: N1.5 used scripts/gr00t_finetune.py, N1.7 uses gr00t/experiment/launch_finetune.py, a tyro CLI over FinetuneConfig. Tutorials written for N1.5 have half the flag names wrong.

FinetuneConfig fieldDefault on mainMemory relevance
global_batch_size64Pre-accumulation, summed across all GPUs
gradient_accumulation_steps1Multiplies global_batch_size; the class warns when above 1
tune_llm / tune_visualFalse / FalseSetting either True is the fastest route to an 80 GB requirement
tune_projector / tune_diffusion_modelTrue / TrueThe default recipe
dataloader_num_workers2Host RAM, not VRAM
num_shards_per_epoch100000Host RAM preloading, despite the field docstring
learning_rate1e-4Pair any batch-size change with a decision here

Two details when hunting bytes. launch_finetune.py hard-codes load_bf16 = False and backbone_trainable_params_fp32 = True, so the profile is not simply everything in bf16. And the CLI exposes no seed: the Isaac-GR00T README says to expect 5 to 6 percent variance from non-deterministic augmentation, which is the floor for judging whether a memory change hurt your policy.

The numbered step list on the AY-Robots GR00T N1.7 on SO-100 training guide, walking a run from dataset selection through to checkpoint
The per-model guides carry the flag set the platform sends, worth diffing against your local command.

lerobot specifics: Pi0.5, SmolVLA, ACT

lerobot moved its runtime onto Hugging Face Accelerate, relocating several memory flags into an accelerator config that carries mixed precision, gradient accumulation, FSDP2 and DDP. Two sub-configs are unfinished, and that is the trap.

A flag that parses and does nothing

In src/lerobot/configs/accelerator.py on lerobot main, both ActivationCheckpointingConfig and CompileConfig carry the docstring a configured placeholder: wiring lands in a later round. So --accelerator.activation_checkpointing.mode=full is accepted, written into train_config.json, and changes nothing. The flag that works is --policy.gradient_checkpointing=true, and of the three lerobot-trained policies here (Pi0.5, SmolVLA, ACT) only Pi0.5 defines it. Read on 2026-08-24; check the file before relying on this.

bash
# Pi0.5 on lerobot, memory-conscious. The docs describe the un-frozen
# version of this command as sized for a single 80 GB GPU.
lerobot-train \
  --dataset.repo_id=$HF_USER/my_so100_dataset \
  --policy.type=pi05 \
  --policy.pretrained_path=lerobot/pi05_base \
  --policy.dtype=bfloat16 \
  --policy.gradient_checkpointing=true \
  --policy.freeze_vision_encoder=true \
  --policy.train_expert_only=true \
  --policy.device=cuda \
  --batch_size=16 \
  --accelerator.gradient_accumulation.steps=4 \
  --num_workers=4 --steps=30000 --seed=1000
Batch 16 with 4 accumulation steps reproduces batch 64 while holding a quarter of the activations.

SmolVLA is the easy case: it ships freeze_vision_encoder = True and train_expert_only = True, and uses only the first 16 layers of the SmolVLM2 backbone. The Hugging Face guide suggests batch 64. If it still will not fit on 24 GB, look at resize_imgs_with_padding, which defaults to 512 by 512.

ACT rarely runs out of memory: at roughly 80 M parameters the whole 18-bytes-per-parameter budget is about 1.4 GB. When it does, the cause is action chunking and camera count. lerobot's ACT defaults are chunk_size 100 and n_action_steps 100 over a 512-dimensional transformer, Chunk size sets the number of decoder queries, and each camera runs the shared ResNet-18 once and flattens its feature map into extra encoder tokens, so cameras and chunk length grow the activations independently.

Version pinning, stated plainly

These commands describe lerobot main as of 2026-08-24; the latest PyPI release is 0.6.1. Older pins differ. The accelerator sub-config arrived with the Accelerate migration, so on lerobot==0.5.1 there is no --accelerator.gradient_accumulation.steps at all. lerobot has also removed GR00T N1.5 support. Print lerobot-train --help on the version you run rather than trusting a blog post, this one included.

Stop guessing: take a memory snapshot

If you have pulled four levers and still do not know where the memory went, measure it. PyTorch records every allocation with its Python stack and renders it as a browser timeline.

python
import torch

torch.cuda.memory._record_memory_history(max_entries=100_000)
try:
    for step, batch in enumerate(loader):
        train_one_step(batch)
        if step == 5:      # peak is reached early
            break
finally:
    torch.cuda.memory._dump_snapshot('oom.pickle')
    torch.cuda.memory._record_memory_history(enabled=None)

print('allocated', torch.cuda.memory_allocated() / 2**30)
print('reserved ', torch.cuda.memory_reserved() / 2**30)
print('peak     ', torch.cuda.max_memory_allocated() / 2**30)
Drop oom.pickle onto https://pytorch.org/memory_viz for the timeline. The recorder is a private API, so the underscores are correct.

Read it for shape, not totals. A flat block that appears once and never moves is weights and optimizer state. A sawtooth rising through the forward and collapsing in the backward is activations. A staircase growing every step is a leak, usually a list of loss tensors that still carry their graph. No flag here fixes that; loss.detach() does.

Allocator settings that cost nothing

OptionDocumented defaultWhen to reach for it
expandable_segmentsFalse, experimentalFirst choice when allocation sizes change between iterations. The OOM message recommends it
max_split_size_mbunlimitedA last resort, per the docs, for a workload aborting with many inactive split blocks
garbage_collection_threshold1.0Set 0.8 to reclaim old blocks before the card is full
roundup_power2_divisionsnot setReduces churn when many nearby large sizes fragment the cache
backendnativecudaMallocAsync is an alternative; it ignores the three options above
The variable was renamed

PyTorch 2.13 documents the variable as PYTORCH_ALLOC_CONF and says PYTORCH_CUDA_ALLOC_CONF is its alias and is provided only for backward compatibility. Both work. Older guides, and the hint inside the OOM message, still use the CUDA-prefixed name.

Two ways to get a run that fits

You rent the GPU, install the trainer, convert the dataset and own every flag above. The right path if you are modifying the model or need a config nobody has packaged.

  1. 1
    Rent a card with headroom

    The stated minimum is 40 GB and the default recipe peaks near 35. Renting exactly 40 leaves nothing for a second camera.

    bash
    nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv
    python -c "import torch; print(torch.__version__, torch.cuda.get_device_name(0))"
  2. 2
    Install, then smoke-test for 50 steps

    The hardware guide pins Python 3.12, CUDA 12.6+, PyTorch 2.7+ and uv. A run that OOMs at step 4000 because one episode has an extra camera stream costs the whole rental.

    bash
    git clone https://github.com/NVIDIA/Isaac-GR00T && cd Isaac-GR00T && uv sync
    export PYTORCH_ALLOC_CONF=expandable_segments:True
    
    uv run python gr00t/experiment/launch_finetune.py \
      --base-model-path nvidia/GR00T-N1.7-3B \
      --dataset-path /data/my_so100 \
      --embodiment-tag NEW_EMBODIMENT \
      --modality-config-path examples/SO100/so100_config.py \
      --num-gpus 1 --max-steps 50 --global-batch-size 16 \
      --dataloader-num-workers 4 --output-dir /tmp/smoke
The dataset version bites before the memory does

A LeRobot v3.0 dataset crashes the GR00T loader and must be converted down to v2.1. That happens during loading and can look like a memory problem, because the process dies without a clean traceback. See the v3.0 rejection page before assuming VRAM.

When the answer is simply a bigger card

Engineers underrate this because it feels like giving up. Price it instead. Three hours fighting a 40 GB card, landing on a frozen vision encoder plus LoRA plus batch 4, gets you a policy worse than the reference recipe. The alternative was 8 more dollars. The levers above earn their keep when you are constrained by hardware you own. For a single policy on an SO-100, the cheapest fix is the right tier: 80 GB for GR00T or Pi0.5, or SmolVLA and ACT on 24 GB.

One caveat covers the subject. Fitting the run is not the same as getting a policy that works on the arm. A model that only fits because you froze the vision tower and halved the resolution can train to a low loss and still fail in your kitchen. That is low loss with no useful behaviour. Compare the constrained recipe against the reference one on rollouts, and use the benchmark arena for what the unconstrained versions reach.

The Find your combination matrix on the AY-Robots training page, five trainable policies as rows and four supported robot arms as columns, each cell linking to that guide
Each cell is a guide with the GPU tier, dataset format and defaults for that pairing.

Every failure mode, with the fix that actually works

Out of memory is one entry in a longer list: the dataset rejected as v3.0, the job stuck in the queue, the arm that twitches and sags, the policy that freezes mid-motion. A diagnosis each, not a guess.

Open the failure-mode index
Why does my run OOM at step 3000 after surviving the first 2000?

Three usual causes. A leak, where something accumulates tensors that still carry their autograd graph. Checkpoint saving, which briefly needs extra memory while state is gathered. Or a data outlier, where one episode has an extra camera stream. Take a snapshot for the first; check whether the failing step lands on your save_steps interval for the second.

Does lowering the batch size mean lowering the learning rate too?

If you want the same optimisation behaviour, yes, or use gradient accumulation to restore the effective batch instead. Isaac-GR00T ships a global batch of 64 with a learning rate of 1e-4, chosen together. Dropping to batch 8 while keeping 1e-4 gives noisier gradients at an unchanged step size. Accumulating is the lower-risk move because it leaves the recipe intact.

Is gradient checkpointing free?

Free in accuracy, not in time. Chen et al. 2016 reduced a 1000-layer residual network from 48 GB to 7 GB for about 30 percent extra runtime, and Hugging Face quotes 20 percent for transformer fine-tuning. One caveat from the PyTorch docs: the checkpointed function runs twice, and determinism is not guaranteed if you move tensors to another device inside it. Pass use_reentrant explicitly; it no longer has a default.

Can I train GR00T N1.7 on a 24 GB card at all?

Not with the supported recipe. NVIDIA's hardware guide states a 40 GB minimum and describes N1.7 as a 3 B parameter bfloat16 model whose default recipe peaks under about 35 GB per GPU. The arithmetic agrees: the fixed cost alone exceeds 24 GB before activations. On 24 GB, train SmolVLA or ACT instead.

Does expandable_segments have a downside?

The PyTorch docs mark it experimental and it defaults to False. The OOM message notes that with expandable segments enabled a process can instead exhaust virtual address space, because each segment reserves roughly nine eighths of device memory times the number of streams. That is rare with one or two streams, so for a single-process fine-tune it is the first thing to try.

Compact version: read the error, rule out host RAM, check your dtype, then work down the list and log every lever from the 'changes the policy' column. That log explains, weeks later, why the training run that fit on the card produced a different policy from the guide's. To skip the tuning, the GR00T N1.7 on SO-100 guide and the SmolVLA on SO-100 guide list what each run uses, and pricing shows what the right card costs. Still choosing a model? Start with the VLA overview and the SO-100 guide.

Sources

Sources

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started