| name | functional-testing |
| description | Validate the Physical AI RL training stack end-to-end from infrastructure smoke tests to training step verification |
Skill 13: Functional Testing
Purpose
Validate the RL training stack end-to-end before launching production training runs. This includes smoke tests for individual components, integration tests for the full pipeline, and regression checks after infrastructure or code changes.
RLinf Example: See the rlinf-functional-testing skill for RLinf-specific L5 test configurations and expected outputs.
Testing Levels
Level 0: Infrastructure --> GPU, EFA, storage accessible
Level 1: Runtime --> PyTorch, CUDA, libraries working
Level 2: Single-GPU --> Model loads, forward pass succeeds
Level 3: Multi-GPU --> FSDP wrapping, NCCL allreduce
Level 3.5: EFA Validation --> NCCL over EFA (Phase 0, cheap instances)
Level 4: Rollout --> Simulation env renders, actions generated
Level 5: End-to-End --> Full training step completes (rollout --> update)
Level 6: Validation --> Evaluation produces success rate metrics
Phase 0: EFA Validation Sprint (MUST DO FIRST)
CRITICAL: Always validate EFA/NCCL on cheap instances (g6.8xlarge, ~$1/hr) before purchasing expensive capacity blocks (p5.48xlarge, ~$725/hr). A failed $1,450 capacity block taught this lesson.
Prerequisites
- EKS cluster with GPU nodes (2x g6.8xlarge for cheapest test)
- NVIDIA GPU Operator running
- EFA device plugin running
- Kubeflow MPI Operator installed (
enable_mpi_operator = true in terraform.tfvars)
Build NCCL Test Image
A pre-built image is available on public ECR — no need to build your own:
export NCCL_TEST_IMAGE=public.ecr.aws/hpc-cloud/nccl-tests:cuda12.8.1-efa1.43.2-ofiv1.16.3-ncclv2.27.7-1-testsv2.16.9
Run NCCL Test (MPIJob)
envsubst '${NCCL_TEST_IMAGE}' < infrastructure/manifests/nccl-tests-mpijob.yaml | kubectl apply -f -
watch kubectl get pods -o wide
kubectl logs -f $(kubectl get pods | grep launcher | cut -d ' ' -f 1)
What to Check
- EFA provider selected: Look for
NET/OFI Selected Provider is efa (NOT socket)
- No crashes: No
cm_req_handle_error_entry or Caught signal 11
- Reasonable bandwidth: g6.8xlarge should show ~2-3 GB/s BusBW at 1GB
- All GPUs participating: Verify
-np matches expected GPU count
Cleanup
kubectl delete mpijob nccl-tests
Only after Phase 0 passes, proceed to purchase capacity blocks for production training.
Level 0: Infrastructure Smoke Tests
GPU Availability
kubectl run gpu-test --rm -it \
--image=nvcr.io/nvidia/pytorch:24.07-py3 \
--overrides='{
"spec": {
"nodeSelector": {"role": "gpu-training"},
"tolerations": [{"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}],
"containers": [{
"name": "gpu-test",
"image": "nvcr.io/nvidia/pytorch:24.07-py3",
"command": ["nvidia-smi"],
"resources": {"requests": {"nvidia.com/gpu": "8"}, "limits": {"nvidia.com/gpu": "8"}}
}]
}
}'
Expected: 8 GPUs visible with correct type (A100 80GB or H100).
EFA Availability
kubectl exec <gpu-pod> -- ls /sys/class/infiniband/
Storage Availability
kubectl exec <gpu-pod> -- bash -c "
echo 'Write test...' && echo 'test' > /fsx/test_write && \
echo 'Read test...' && cat /fsx/test_write && \
echo 'Delete test...' && rm /fsx/test_write && \
echo 'Storage OK'
"
Level 1: Runtime Validation
kubectl exec <gpu-pod> -- python -c "
import torch
print(f'PyTorch: {torch.__version__}')
print(f'CUDA: {torch.version.cuda}')
print(f'cuDNN: {torch.backends.cudnn.version()}')
print(f'GPUs: {torch.cuda.device_count()}')
assert torch.cuda.is_available(), 'CUDA not available!'
assert torch.cuda.device_count() == 8, f'Expected 8 GPUs, got {torch.cuda.device_count()}'
# Test key imports
import verl # Replace with your RL framework
from flash_attn import flash_attn_func
import transformers
import ray
print('All imports OK')
"
Level 2: Single-GPU Model Test
kubectl exec <gpu-pod> -- python -c "
import torch
from transformers import AutoModelForVision2Seq, AutoTokenizer
model_path = '/fsx/models/openvla-7b' # Replace with your model path
print(f'Loading model from {model_path}...')
tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoModelForVision2Seq.from_pretrained(
model_path,
torch_dtype=torch.bfloat16,
device_map='cuda:0'
)
params = sum(p.numel() for p in model.parameters())
print(f'Model loaded: {params / 1e9:.1f}B parameters')
print(f'Tokenizer vocab: {tokenizer.vocab_size}')
assert params > 6e9, f'Model too small: {params}'
print('Single-GPU model test PASSED')
"
Level 3: Multi-GPU FSDP Test
kubectl exec <gpu-pod> -- python -c "
import torch
import torch.distributed as dist
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
import os
os.environ['MASTER_ADDR'] = 'localhost'
os.environ['MASTER_PORT'] = '29500'
# Init process group for single-node multi-GPU
dist.init_process_group('nccl', rank=0, world_size=1)
# Test NCCL allreduce
tensor = torch.ones(1024, device='cuda:0')
dist.all_reduce(tensor)
print(f'AllReduce result: {tensor[0].item()} (expected 1.0)')
dist.destroy_process_group()
print('Multi-GPU NCCL test PASSED')
"
NCCL Performance Test (Multi-Node via MPIJob)
Discovery #46: The canonical NCCL test on EKS uses an MPIJob (Kubeflow MPI Operator), NOT a StatefulSet with torchrun.
See infrastructure/manifests/nccl-tests-mpijob.yaml. Adjust for your instance type:
| Setting | g6.8xlarge | p5.48xlarge |
|---|
slotsPerWorker | 1 | 8 |
-np | 2 | 16 |
nvidia.com/gpu | 1 | 8 |
vpc.amazonaws.com/efa | 1 | 32 |
Expected results (2x p5.48xlarge):
- 1 GB message: ~437 GB/s BusBW
- 16 GB message: ~487 GB/s BusBW
Level 4: Rollout Test
Test that the simulation environment initializes and the model generates actions:
kubectl exec <gpu-pod> -- python -c "
import os, sys
# CRITICAL: Many robotics simulation libraries call input() at module import
# time if config directories aren't set. This will HANG in non-interactive
# containers. Always set env vars AND redirect stdin before importing.
os.environ['ROBOT_PLATFORM'] = 'LIBERO' # Replace with your simulation platform
os.environ['LIBERO_DATASET_DIR'] = '/fsx/datasets/libero' # Replace with your dataset env var and path
sys.stdin = open('/dev/null') # Prevent input() hang
# Test simulation environment
import libero.libero # Replace with your simulation library
# ... load benchmark/tasks and verify
print('Rollout environment test PASSED')
"
Example (RLinf): The RLinf reference tests LIBERO with os.environ['ROBOT_PLATFORM'] = 'LIBERO', os.environ['LIBERO_DATASET_DIR'] = '/fsx/datasets/libero', then imports libero.libero and verifies 10 tasks via get_benchmark('libero_10'). See rlinf-functional-testing skill for details.
General pattern: Always test simulation library imports in a non-interactive container (Kubernetes pod, CI) before assuming they work. Many robotics libraries assume interactive terminals.
Level 5: End-to-End Training Step
Run a minimal training step (1 epoch, small batch):
apiVersion: batch/v1
kind: Job
metadata:
name: test-training-e2e
spec:
backoffLimit: 0
activeDeadlineSeconds: 3600
template:
spec:
restartPolicy: Never
nodeSelector:
role: gpu-training
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
containers:
- name: test
image: <ECR_URI>:latest
command:
- bash
- -c
- |
# Run framework-specific pre-training setup
# Then launch a single training step with minimal data
bash /workspace/eks/scripts/run_training_eks.sh --max-steps 1 # Replace with your launch script
resources:
requests:
nvidia.com/gpu: 8
vpc.amazonaws.com/efa: 4
limits:
Example (RLinf): The RLinf L5 test runs verl.trainer.main_ppo with Hydra overrides for minimal data (n_samples=2, train_batch_size=4, total_epochs=1) and uses overwrite_vla_ckpt_utils.sh for model preparation. The full veRL command includes data.task_suite_name=libero_10, actor_rollout_ref.model.path=/fsx/models/sft-base/openvla-oft-libero10-traj1, and algorithm.adv_estimator=grpo. See rlinf-functional-testing skill for complete configurations.
kubectl apply -f test-training-job.yaml
kubectl logs -f job/test-training-e2e
Expected: Training completes 1 epoch without errors. Validation success rate is reported.
Level 6: Validation-Only Run
Run evaluation to ensure the full inference pipeline works:
Automated Test Suite
The repo includes validate.sh, a full validation harness that runs all levels:
./validate.sh --mode local
./validate.sh --mode cluster --level 6
./validate.sh --mode cluster --level 3
./validate.sh --mode cluster --skip-to 2
./validate.sh --mode cluster --level 5 --example maniskill-openvlaoft
./validate.sh --mode cluster --continue-on-error
ECR_URI=123456789.dkr.ecr.us-east-2.amazonaws.com/rlinf-on-eks/rlinf \
./validate.sh --mode cluster --skip-to 2 --level 3
Validation Output
========================================
VALIDATION RESULTS
========================================
Level Name Status Time
----- ------------------------ ------ ----
L0 Infrastructure PASS 12s
L1 Container Build PASS 847s
L2 Container Test PASS 95s
L3 NCCL/EFA PASS 180s
L4 Model Download PASS 320s
L5 <example-name> PASS 540s
L5 <example-name> PASS 510s
L5 <example-name> PASS 620s
L6 Multi-Node PASS 480s
========================================
9/9 passed in 3604s
Implementation Details
| File | Purpose |
|---|
validate.sh | Main orchestrator (arg parsing, level dispatch, summary) |
tests/lib/common.sh | Shared utilities (logging, timers, K8s helpers) |
tests/lib/local.sh | Local mode checks (terraform, kubeconform, shellcheck) |
tests/lib/cluster.sh | Cluster levels L0-L6 |
tests/manifests/training-step-test.yaml | L5 single-step training Job template |
infrastructure/manifests/container-test.yaml | L2 container validation (multi-venv, EFA, CUDA checks) |
examples/<example>/manifests/model-download.yaml | L4 model download Jobs (per-example) |
L5 Training Step Tests
Uses runner.max_steps=1 (or equivalent) override to limit the framework to exactly 1 training step per example. The key insight: find the single override that limits the training loop to one step.
Example (RLinf): RLinf uses runner.max_steps=1 Hydra override to limit to exactly 1 PPO step. RLinf's num_steps_per_epoch is hardcoded to 1.
The training launch script (examples/scripts/<launch-script>) supports HYDRA_OVERRIDES env var for injecting additional overrides without modifying the script:
Common Testing Pitfalls for Physical AI Stacks
These issues affect any RL training deployment, not just specific frameworks:
1. Interactive Prompts in Simulation Libraries
Many robotics simulation libraries (LIBERO, RoboSuite, Habitat, etc.) call input() at module import time to configure dataset directories. In non-interactive containers, this causes the pod to hang indefinitely with no error message.
Fix: Set all required environment variables (e.g., LIBERO_DATASET_DIR) via the pod spec env block AND redirect stdin in the test script: sys.stdin = open('/dev/null').
2. Model/Config Version Skew
Model checkpoints are saved with a config.json that reflects the code version at save time. When the modeling code evolves and adds new config attributes (e.g., use_proprio, num_action_chunks), older checkpoints lack these attributes and from_pretrained() fails with AttributeError.
Fix: Apply patches that use getattr(config, 'attr', default) instead of direct attribute access. Store patches in examples/patches/ for traceability.
3. Container Image Pull Timeouts
Physical AI RL images are 15-20 GB. First pull takes 5-10 minutes. If activeDeadlineSeconds is set too low, test jobs timeout during image pull -- before any code runs.
Fix: Set activeDeadlineSeconds to at least 600s (10 min) for any job using a large image. Use imagePullPolicy: IfNotPresent (not Always) to avoid re-pulling on every run. Tag images with specific versions.
4. YAML/Bash Script Interactions
Complex bash scripts embedded in YAML | block scalars fail in subtle ways. Semicolons, curly braces, and &&/|| chains can be misinterpreted by the YAML parser.
Fix: Keep inline bash simple. For complex test logic, write a Python script and mount it via ConfigMap.
5. GPU Memory Not Freed Between Tests
If running multiple test levels in a single pod, GPU memory from one test may not be freed for the next. Model loading followed by FSDP wrapping can OOM.
Fix: Run each test level in a separate pod, or explicitly del model; torch.cuda.empty_cache(); gc.collect() between levels.
Validation Checklist
Related Skills