VLA (Vision-Language-Action) model hub for realtime inference on AWS ECS (EC2 GPU).
Six OSS VLA policies — GR00T N1.6, GR00T N1.7, π0.5, OpenVLA-7B, SmolVLA-450M, LAP-3B — packaged as independent gRPC endpoints in a single CDK project. Pick the model that fits your robot, deploy in minutes, swap models without re-architecting clients. Each stack ships its own VPC, internal NLB, ECS cluster, ASG, and ECR repo.
Adding a seventh model = one entry in vla-hub.json + a docker/<model>/ context
AzSelectorConstruct probes EC2 GPU capacity at deploy
time and pins the ASG to a confirmed AZ — no manual capacity hunting.The hub does not restrict itself to fully-generalist VLAs. In practice every public VLA sits somewhere on a generalist↔specialist spectrum, and the threshold is fuzzy. The policy is therefore:
A model is a candidate as long as it is OSS, subject to two guardrails:
- gRPC serving must be feasible. The model must either ship gRPC serving code, or be wrappable by a thin adapter under
docker/<model>/serve.py. Models that only ship custom training code are excluded.- Stack compatibility. The model must run on a standard PyTorch or JAX container. Specialized CUDA kernels or non-standard runtimes are excluded on operational-cost grounds.
Models passing both guardrails are included even if they lean toward the specialist end. The capability matrix below lets operators and clients pick a fit on a case-by-case basis.
| Model | License | Pretrain Scope | Embodiments (zero-shot) | Environments | New Embodiment Cost | Inference Hz | Stack | Best Fit |
|---|---|---|---|---|---|---|---|---|
| GR00T N1.6 | NVIDIA | OXE + NVIDIA fleet | gr1, behavior_r1_pro, robocasa_panda_omron | Sim+Real | MLP only (~$10, hours) | ~10 Hz (System2) / 120 Hz (System1) | PyTorch | New-robot PoC, MLP fine-tune demo |
| GR00T N1.7 | NVIDIA | N1.6 + additional robot data | (N1.6 superset) | Sim+Real | MLP only (~$10, hours) | ~10 / 120 Hz | PyTorch | Same as N1.6 + precision assembly, loco-manipulation |
| π0.5 | Apache-2.0 | Mobile robots + static arms + web data (~400h+) | Robots within co-training distribution | Sim+Real | Action Expert + LoRA(VLM) ($100s, days) | ~6 Hz | JAX | Mobile manipulation; in-distribution robots |
| OpenVLA-7B | MIT | OXE 970K (Bridge / Franka centric) | Bridge, Franka centric | Sim+Real (limited) | Effectively retrain ($1000s) | ~6 Hz on RTX 4090 | PyTorch | Broadest community baseline; LoRA fine-tune |
| SmolVLA-450M | Apache-2.0 | LeRobot community data | Lightweight robots | Sim+Real (limited) | Full-FT lightweight | Relatively fast | PyTorch | Edge / small robots; fast iteration |
| LAP-3B | Apache-2.0 | Language-Action Pre-Training (zero-shot cross-embodiment) | Cross-embodiment (zero-shot intent) | Sim+Real | Fine-tune per embodiment | ~25 Hz on RTX 4090 | JAX | Most generalist baseline; cross-embodiment transfer |
Note: Zero-shot feasibility depends on how close the customer robot is to the model's pretraining distribution. The table reflects embodiments that are explicitly part of pretraining; out-of-distribution embodiments require fine-tuning regardless of model.
| Scenario | Recommended (1st) | Alternatives |
|---|---|---|
| New customer robot, fast PoC | GR00T N1.7 (MLP fine-tune) | GR00T N1.6 |
| Precision assembly / loco-manipulation demo | GR00T N1.7 | GR00T N1.6 |
| Mobile manipulation (navigation + manipulation) | π0.5 | — |
| Cross-embodiment comparison demo | GR00T MLP vs π0.5 co-train vs OpenVLA full-FT in parallel | LAP-3B |
| Zero-shot cross-embodiment / most generalist baseline | LAP-3B | π0.5 |
| Edge / small robot | SmolVLA-450M | OpenVLA quantized |
| Most conservative baseline (community ground truth) | OpenVLA-7B | — |
Generalist ───────────────────────────────────── Specialist
LAP-3B π0.5 GR00T SmolVLA OpenVLA Fast-WAM
(co-train) (MLP) (Bridge/ (LIBERO/
Franka) RoboTwin sim)
❌ outside hub
Single-sim / single-embodiment specialists such as Fast-WAM are better operated ad-hoc outside the hub: they only run inside one sim, so they do not generalize across customer scenarios.

Robot / Sim Client
│
├─ gRPC (TCP:50050) → GR00T N1.6 NLB → ECS Task (g6/g5 GPU) — GR00TInference
├─ gRPC (TCP:50051) → GR00T N1.7 NLB → ECS Task (g6/g5 GPU) — GR00TInference
├─ gRPC (TCP:50052) → π0.5 NLB → ECS Task (g5/g6 GPU) — PIInference
├─ gRPC (TCP:50053) → OpenVLA NLB → ECS Task (g6/g5 GPU) — OpenVLAInference
├─ gRPC (TCP:50054) → SmolVLA NLB → ECS Task (g6/g5 GPU) — SmolVLAInference
└─ gRPC (TCP:50055) → LAP-3B NLB → ECS Task (g6/g5 GPU) — LAPInference
Each stack is isolated:
Each model id × version combination from vla-hub.json produces an independent
build stack and ECS stack. Adding a new model = entry in vla-hub.json +
docker/<model>/ context + matching stacks emitted by the CDK app.
Current model entries (see vla-hub.json):
| Model | Version | Port | Build Stack | ECS Stack |
|---|---|---|---|---|
| GR00T | N1.6 | 50050 | Gr00tN16BuildStack | Gr00tN16EcsStack |
| GR00T | N1.7 | 50051 | Gr00tN17BuildStack | Gr00tN17EcsStack |
| π | 0.5 | 50052 | PiBuildStack | PiEcsStack |
| OpenVLA | 7b | 50053 | OpenvlaBuildStack | OpenvlaEcsStack |
| SmolVLA | 450M | 50054 | Smolvla450mBuildStack | Smolvla450mEcsStack |
| LAP | 3B | 50055 | Lap3BBuildStack | (in VlaHubStack) |
npm install
npm run build
cdk bootstrap
Each model has its own build stack. Deploy the build stacks for the models you want, then trigger their CodeBuild projects:
# GR00T N1.6 / N1.7: require HuggingFace token in Secrets Manager
# (default secret name: gr00t/hf-token; override with -c hfTokenSecretName=<name>)
cdk deploy Gr00tN16BuildStack
cdk deploy Gr00tN17BuildStack
# π0.5: no token needed (public GCS checkpoint)
cdk deploy PiBuildStack
# OpenVLA-7B: HuggingFace public model — token only needed for rate-limit
cdk deploy OpenvlaBuildStack
# SmolVLA-450M: HuggingFace public model
cdk deploy Smolvla450mBuildStack
# LAP-3B: HuggingFace public model (checkpoint ~12.4 GB baked in at build; PaliGemma
# tokenizer from gs://big_vision pre-baked too). No token needed.
cdk deploy Lap3BBuildStack
After CodeBuild completes, note the ECR image URIs from each stack's outputs.
# GR00T N1.6
cdk deploy Gr00tN16EcsStack \
-c gr00tN16EcrImageUri=<account>.dkr.ecr.<region>.amazonaws.com/gr00t-n16-realtime:latest
# GR00T N1.7
cdk deploy Gr00tN17EcsStack \
-c gr00tN17EcrImageUri=<account>.dkr.ecr.<region>.amazonaws.com/gr00t-n17-realtime:latest
# π0.5
cdk deploy PiEcsStack \
-c piEcrImageUri=<account>.dkr.ecr.<region>.amazonaws.com/vla-pi-realtime:latest
# OpenVLA-7B
cdk deploy OpenvlaEcsStack \
-c openvlaEcrImageUri=<account>.dkr.ecr.<region>.amazonaws.com/openvla-realtime:latest
# SmolVLA-450M
cdk deploy Smolvla450mEcsStack \
-c smolvla450mEcrImageUri=<account>.dkr.ecr.<region>.amazonaws.com/smolvla-450m-realtime:latest
# LAP-3B (served from VlaHubStack — override the lap image URI via context)
cdk deploy VlaHubStack \
-c lapEcrImageUri=<account>.dkr.ecr.<region>.amazonaws.com/vla-lap-realtime:3B-latest
-c instanceTypes=g6.2xlarge,g5.2xlarge # comma-separated fallback list
-c desiredCount=1
| Output | Description |
|---|---|
GrpcEndpoint | NLB DNS + model-specific port — gRPC inference endpoint |
VpcId | Place gRPC client EC2 in this VPC |
PrivateSubnetIds | Place gRPC client EC2 in one of these subnets |
ClusterName / ServiceName | ECS identifiers |
SelectedInstanceType / SelectedAZ | AzSelector probe result |
Each model ships its own .proto. All five expose Infer + Health RPCs with
the per-model schema appropriate to that model's input/output conventions.
| Model | Proto | Service | Port |
|---|---|---|---|
| GR00T N1.6 | docker/gr00t-n16/gr00t.proto | GR00TInference | 50050 |
| GR00T N1.7 | docker/gr00t-n17/gr00t.proto | GR00TInference | 50051 |
| π0.5 | docker/pi/pi.proto | PIInference | 50052 |
| OpenVLA-7B | docker/openvla/openvla.proto | OpenVLAInference | 50053 |
| SmolVLA-450M | docker/smolvla/smolvla.proto | SmolVLAInference | 50054 |
| LAP-3B | docker/lap/lap.proto | LAPInference | 50055 |
Common shape for Infer:
image_data (JPEG bytes), instruction (string), optional joint statesaction_chunks — map<string, bytes> (float32 tobytes, reshape to
(H, DOF) per the model's action-chunk convention)Health(HealthRequest) → HealthResponse is provided on every service for NLB /
client probes.
AzSelectorConstruct probes EC2 capacity at deploy time and pins the ASG to a
confirmed AZ.
| Model | Priority order |
|---|---|
| GR00T N1.6 / N1.7 | g6.2xlarge → g5.2xlarge → g6.xlarge → g5.xlarge |
| π0.5 | g5.2xlarge → g5.xlarge → g6.2xlarge → g6.xlarge |
| OpenVLA-7B | g6.2xlarge → g5.2xlarge → g6.xlarge → g5.xlarge |
| SmolVLA-450M | g6.xlarge → g5.xlarge → g6.2xlarge → g5.2xlarge |
| LAP-3B | g6.xlarge → g5.xlarge → g6.2xlarge → g5.2xlarge |
GR00T (and OpenVLA's FlashAttention path) require Ampere GPU (SM80+). π0.5 and LAP-3B use JAX and do not require FlashAttention.
docker/
├── gr00t-n16/ # GR00T N1.6 container (PyTorch + Isaac Lab gRPC server)
├── gr00t-n17/ # GR00T N1.7 container (PyTorch + Isaac Lab gRPC server)
├── pi/ # π0.5 container (JAX + pi0.5 gRPC server)
├── openvla/ # OpenVLA-7B container (PyTorch + HuggingFace gRPC server)
├── smolvla/ # SmolVLA-450M container (PyTorch + LeRobot gRPC server)
└── lap/ # LAP-3B container (JAX + openpi/lap gRPC server)
Each context is packaged as an S3 Asset and passed to CodeBuild. When
docker/<model>/ changes, redeploy the corresponding BuildStack.
SageMaker supports Triton Inference Server internally, but it only exposes an
HTTP REST endpoint externally. The Triton gRPC port (:8001) is inaccessible
from outside the SageMaker container. This makes SageMaker unsuitable for two
requirements:
ECS + NLB (L4 passthrough) hands TLS termination to the ECS Task itself, enabling full gRPC + mTLS control.
For simple PoC/demo with REST, SageMaker is fine. For production-style robot connectivity patterns, ECS + NLB is correct.
| Factor | serve.py (current) | Triton Python backend |
|---|---|---|
Custom .proto | ✅ Per-model proto preserved | ❌ Must migrate to Triton standard proto → client SDK rewrite |
| GR00T monkey-patch | ✅ In serve.py init | Possible in model.py init, same complexity |
| π0.5 JAX runtime | ✅ Isolated ECS Task | JAX + PyTorch on the same GPU is risky (memory pre-allocation conflict) |
| Prometheus metrics | Manual | ✅ Built-in |
| Dynamic batching | Manual (ASG scale-out) | ✅ Built-in |
Current verdict: Triton adds operational complexity without reducing code
complexity. The blocking issue is the custom .proto requirement — migrating
to Triton's standard generate.proto would require rewriting all client SDKs
and the ZMQ-gRPC bridge. Additionally, π0.5 (JAX) and GR00T (PyTorch) cannot
safely share a GPU due to JAX's pre-emptive memory allocation
(XLA_PYTHON_CLIENT_MEM_FRACTION).
Re-evaluate when NVIDIA ships an official TensorRT-optimized GR00T package — that would eliminate the monkey-patch requirement and make adopting the standard Triton proto natural. See Triton Inference Server for upstream reference.
Key reasons for Physical AI inference workloads:
.proto auto-generates both server and client codeNLB sticky routing (TCP 5-tuple hash) works identically for both gRPC and WebSocket — it is not a differentiator. The advantages above are protocol-level, not routing-level.
NLB operates at L4 (TCP passthrough) — TLS is terminated inside the ECS Task container, enabling E2E mTLS. ALB terminates TLS at the load balancer layer, breaking the mTLS chain. NLB also handles long-lived gRPC streaming connections without HTTP-layer timeouts.
This repository is a sample for demonstration and PoC use. Before adapting it for production or customer-facing workloads, review the following — none are hard blockers, but each is a deliberate scoping choice in the sample:
AzSelectorConstruct pins the ASG to one confirmed-capacity AZ). For HA,
switch to multi-AZ ASG with capacity rebalancing, or run two stacks in
different AZs behind a regional endpoint.Operators are expected to evaluate the architecture against their own security, compliance, and availability requirements before any customer-facing use.
The cloud serving path above (x86 + NVIDIA g5/g6) is the supported, validated track and does not change. This section adds a separate Jetson (L4T) edge track: the same SmolVLA-450M policy, repackaged to run on-device (Orin/Thor). It is a roadmap — the code lands incrementally, and on-device validation depends on hardware we do not currently own (see the FAQ).
In
lerobot/smolvla_base on
HuggingFace) — no retraining, no new policy.Out
localhost:50054) that
exposes the same Infer / Health contract as the cloud — robot/sim
clients connect identically; only host:port changes."Runs" and "runs in realtime" are deliberately separate outcomes: the eager PyTorch path gets it running (functional, Hz not guaranteed); the TensorRT path (Phase 4) makes it run in realtime.
flowchart LR
W["SmolVLA-450M weights<br/>(same as cloud)"] --> BUILD
DEV(["Jetson<br/>Orin / Thor"]) --> PRE["preflight.sh<br/>detect → BASE_IMAGE"]
PRE --> BUILD["Dockerfile.jetson<br/>build L4T image"]
BUILD --> RUN["run.sh<br/>serve over gRPC"]
RUN --> OUT(["gRPC endpoint<br/>localhost:50054"])
RUN --> SMOKE["smoke_test.sh<br/>p50 / p99 + Hz"]
RUN -. "realtime (Phase 4)" .-> TRT["build-trt.sh<br/>TensorRT engine"]
TRT -. "BACKEND=tensorrt" .-> RUN
The L4T image must be built on the Jetson itself (aarch64 + Tegra) — x86 /
Graviton-sbsa cannot build a Tegra image. The full day-1 runbook is
edge/GETTING_STARTED_ON_HARDWARE.md; the
short version (CDK-free, lives in edge/):
cd edge
# 1. Detect the device and print the exact base image to use:
./preflight.sh # → Orin: …-py3-igpu | Thor: …-py3
# 2. Build the L4T image and serve, in one command:
export BASE_IMAGE=nvcr.io/nvidia/pytorch:25.08-py3-igpu # (Orin; preflight prints yours)
BUILD=1 BASE_IMAGE=$BASE_IMAGE ./run.sh smolvla
# → gRPC: localhost:50054 health: http://localhost:8080/health
# 3. Measure latency + Hz:
./smoke_test.sh smolvla
# Or deploy a prebuilt image from a registry (1-click):
./deploy-edge.sh smolvla <account>.dkr.ecr.<region>.amazonaws.com/vla-smolvla-realtime:edge-latest
No Jetson on hand? You can still exercise the run-target logic on a plain x86
GPU host — build the cloud SmolVLA image and drive it through the same run.sh
(see edge/README.md).
SmolVLA-450M is the first (and currently only) edge target — already tagged "edge / small robot" in the capability matrix above. The software is staged so the whole day-1 path is scripted, with no setup guesswork.
| Phase | What | State |
|---|---|---|
| 0 | This roadmap; cloud stack untouched | ✅ |
| 1 | serve.py / .proto / entrypoint shared across cloud + edge Dockerfiles | ✅ |
| 2 | docker/smolvla/Dockerfile.jetson on an L4T base (ARG BASE_IMAGE) | ✅ |
| 3 | edge/ run-target — docker-compose.yml / run.sh, no CDK | ✅ |
| 5 | edge/deploy-edge.sh 1-click deploy | ✅ |
| 6 | Test harness — bench_client.py + smoke_test.sh (p50/p99 + Hz) | ✅ |
| 7 | Day-1 enablement — preflight.sh + hardware runbook | ✅ |
| 4 | TensorRT realtime backend (BACKEND switch + build-trt.sh) | ◐ scaffold; fails loud, no fake engine — model-specific export finished on-device |
Because aarch64 is a false friend — chasing it makes the cloud harder without
helping the edge:
nvcr.io/nvidia/pytorch arm64/sbsa); Jetson Orin/Thor use the tegra/L4T
stack (nvcr.io/nvidia/l4t-pytorch). Both are aarch64 ISA, but the CUDA runtime,
driver model, and base image differ — an sbsa-ARM image does not run on a
Jetson. Moving the cloud to ARM buys nothing for the edge.g5g, is a Turing
T4G (SM75) — too old for GR00T and OpenVLA's FlashAttention path — and it splits
the fleet across two architectures for no benefit.serve.py (Python) and
*.proto (protobuf) are architecture-neutral; they move to a Jetson build for
free regardless of whether the cloud is x86 or ARM.So there is no multi-arch manifest to share. Cloud (x86) and edge (L4T) are two independent tracks with different base images; the shared asset is the source, expressed as two Dockerfiles over one source tree:
[same SmolVLA-450M weights] ──┬─→ Dockerfile → x86 image → ECS/NLB (cloud, validated, unchanged)
(HF: lerobot/smolvla_base) └─→ Dockerfile.jetson → L4T image → compose/run (edge, new)
↑ identical ↑ base differs only ↑ arch differs only
serve.py / .proto = shared (architecture-neutral)
| Layer | Location | Edge portability |
|---|---|---|
| Model adapter (preprocess · infer · action-chunk) | docker/<model>/serve.py | Reused as-is. Zero boto3/sagemaker/ecs/cloudwatch imports |
| Interface contract | docker/<model>/*.proto (Infer + Health) | Reused as-is. Same schema on cloud and edge |
| Runtime contract | entrypoint.sh (CUDA-compat branch) / check_health.sh (:8080) | Reused nearly as-is — device is env-driven (DEVICE) |
| Base image | Dockerfile FROM (pytorch/pytorch) | L4T variant added separately (Dockerfile.jetson) |
| Cloud orchestration | lib/vla-*.ts (VPC/NLB/ECS/ASG/AzSelector/CodeBuild) | Not used on edge — replaced by localhost gRPC / compose |
The cloud lock-in is confined to lib/*.ts; the serving layer is already
portable, so the edge track is a head-start on the existing code, not a rewrite.
nvcr.io/nvidia/l4t-pytorch / jetson-containers base, not the
standard sbsa arm64 one.lerobot / transformers / torch on L4T may
need source builds or a jetson-containers recipe for packages that have no
prebuilt wheel.5 commits
1 commits
Python
38.8%
TypeScript
32.8%
Shell
15.4%
Dockerfile
12.0%
VLA (Vision-Language-Action) model hub for realtime inference on AWS ECS (EC2 GPU).
Six OSS VLA policies — GR00T N1.6, GR00T N1.7, π0.5, OpenVLA-7B, SmolVLA-450M, LAP-3B — packaged as independent gRPC endpoints in a single CDK project. Pick the model that fits your robot, deploy in minutes, swap models without re-architecting clients. Each stack ships its own VPC, internal NLB, ECS cluster, ASG, and ECR repo.
Adding a seventh model = one entry in vla-hub.json + a docker/<model>/ context
AzSelectorConstruct probes EC2 GPU capacity at deploy
time and pins the ASG to a confirmed AZ — no manual capacity hunting.The hub does not restrict itself to fully-generalist VLAs. In practice every public VLA sits somewhere on a generalist↔specialist spectrum, and the threshold is fuzzy. The policy is therefore:
A model is a candidate as long as it is OSS, subject to two guardrails:
- gRPC serving must be feasible. The model must either ship gRPC serving code, or be wrappable by a thin adapter under
docker/<model>/serve.py. Models that only ship custom training code are excluded.- Stack compatibility. The model must run on a standard PyTorch or JAX container. Specialized CUDA kernels or non-standard runtimes are excluded on operational-cost grounds.
Models passing both guardrails are included even if they lean toward the specialist end. The capability matrix below lets operators and clients pick a fit on a case-by-case basis.
| Model | License | Pretrain Scope | Embodiments (zero-shot) | Environments | New Embodiment Cost | Inference Hz | Stack | Best Fit |
|---|---|---|---|---|---|---|---|---|
| GR00T N1.6 | NVIDIA | OXE + NVIDIA fleet | gr1, behavior_r1_pro, robocasa_panda_omron | Sim+Real | MLP only (~$10, hours) | ~10 Hz (System2) / 120 Hz (System1) | PyTorch | New-robot PoC, MLP fine-tune demo |
| GR00T N1.7 | NVIDIA | N1.6 + additional robot data | (N1.6 superset) | Sim+Real | MLP only (~$10, hours) | ~10 / 120 Hz | PyTorch | Same as N1.6 + precision assembly, loco-manipulation |
| π0.5 | Apache-2.0 | Mobile robots + static arms + web data (~400h+) | Robots within co-training distribution | Sim+Real | Action Expert + LoRA(VLM) ($100s, days) | ~6 Hz | JAX | Mobile manipulation; in-distribution robots |
| OpenVLA-7B | MIT | OXE 970K (Bridge / Franka centric) | Bridge, Franka centric | Sim+Real (limited) | Effectively retrain ($1000s) | ~6 Hz on RTX 4090 | PyTorch | Broadest community baseline; LoRA fine-tune |
| SmolVLA-450M | Apache-2.0 | LeRobot community data | Lightweight robots | Sim+Real (limited) | Full-FT lightweight | Relatively fast | PyTorch | Edge / small robots; fast iteration |
| LAP-3B | Apache-2.0 | Language-Action Pre-Training (zero-shot cross-embodiment) | Cross-embodiment (zero-shot intent) | Sim+Real | Fine-tune per embodiment | ~25 Hz on RTX 4090 | JAX | Most generalist baseline; cross-embodiment transfer |
Note: Zero-shot feasibility depends on how close the customer robot is to the model's pretraining distribution. The table reflects embodiments that are explicitly part of pretraining; out-of-distribution embodiments require fine-tuning regardless of model.
| Scenario | Recommended (1st) | Alternatives |
|---|---|---|
| New customer robot, fast PoC | GR00T N1.7 (MLP fine-tune) | GR00T N1.6 |
| Precision assembly / loco-manipulation demo | GR00T N1.7 | GR00T N1.6 |
| Mobile manipulation (navigation + manipulation) | π0.5 | — |
| Cross-embodiment comparison demo | GR00T MLP vs π0.5 co-train vs OpenVLA full-FT in parallel | LAP-3B |
| Zero-shot cross-embodiment / most generalist baseline | LAP-3B | π0.5 |
| Edge / small robot | SmolVLA-450M | OpenVLA quantized |
| Most conservative baseline (community ground truth) | OpenVLA-7B | — |
Generalist ───────────────────────────────────── Specialist
LAP-3B π0.5 GR00T SmolVLA OpenVLA Fast-WAM
(co-train) (MLP) (Bridge/ (LIBERO/
Franka) RoboTwin sim)
❌ outside hub
Single-sim / single-embodiment specialists such as Fast-WAM are better operated ad-hoc outside the hub: they only run inside one sim, so they do not generalize across customer scenarios.

Robot / Sim Client
│
├─ gRPC (TCP:50050) → GR00T N1.6 NLB → ECS Task (g6/g5 GPU) — GR00TInference
├─ gRPC (TCP:50051) → GR00T N1.7 NLB → ECS Task (g6/g5 GPU) — GR00TInference
├─ gRPC (TCP:50052) → π0.5 NLB → ECS Task (g5/g6 GPU) — PIInference
├─ gRPC (TCP:50053) → OpenVLA NLB → ECS Task (g6/g5 GPU) — OpenVLAInference
├─ gRPC (TCP:50054) → SmolVLA NLB → ECS Task (g6/g5 GPU) — SmolVLAInference
└─ gRPC (TCP:50055) → LAP-3B NLB → ECS Task (g6/g5 GPU) — LAPInference
Each stack is isolated:
Each model id × version combination from vla-hub.json produces an independent
build stack and ECS stack. Adding a new model = entry in vla-hub.json +
docker/<model>/ context + matching stacks emitted by the CDK app.
Current model entries (see vla-hub.json):
| Model | Version | Port | Build Stack | ECS Stack |
|---|---|---|---|---|
| GR00T | N1.6 | 50050 | Gr00tN16BuildStack | Gr00tN16EcsStack |
| GR00T | N1.7 | 50051 | Gr00tN17BuildStack | Gr00tN17EcsStack |
| π | 0.5 | 50052 | PiBuildStack | PiEcsStack |
| OpenVLA | 7b | 50053 | OpenvlaBuildStack | OpenvlaEcsStack |
| SmolVLA | 450M | 50054 | Smolvla450mBuildStack | Smolvla450mEcsStack |
| LAP | 3B | 50055 | Lap3BBuildStack | (in VlaHubStack) |
npm install
npm run build
cdk bootstrap
Each model has its own build stack. Deploy the build stacks for the models you want, then trigger their CodeBuild projects:
# GR00T N1.6 / N1.7: require HuggingFace token in Secrets Manager
# (default secret name: gr00t/hf-token; override with -c hfTokenSecretName=<name>)
cdk deploy Gr00tN16BuildStack
cdk deploy Gr00tN17BuildStack
# π0.5: no token needed (public GCS checkpoint)
cdk deploy PiBuildStack
# OpenVLA-7B: HuggingFace public model — token only needed for rate-limit
cdk deploy OpenvlaBuildStack
# SmolVLA-450M: HuggingFace public model
cdk deploy Smolvla450mBuildStack
# LAP-3B: HuggingFace public model (checkpoint ~12.4 GB baked in at build; PaliGemma
# tokenizer from gs://big_vision pre-baked too). No token needed.
cdk deploy Lap3BBuildStack
After CodeBuild completes, note the ECR image URIs from each stack's outputs.
# GR00T N1.6
cdk deploy Gr00tN16EcsStack \
-c gr00tN16EcrImageUri=<account>.dkr.ecr.<region>.amazonaws.com/gr00t-n16-realtime:latest
# GR00T N1.7
cdk deploy Gr00tN17EcsStack \
-c gr00tN17EcrImageUri=<account>.dkr.ecr.<region>.amazonaws.com/gr00t-n17-realtime:latest
# π0.5
cdk deploy PiEcsStack \
-c piEcrImageUri=<account>.dkr.ecr.<region>.amazonaws.com/vla-pi-realtime:latest
# OpenVLA-7B
cdk deploy OpenvlaEcsStack \
-c openvlaEcrImageUri=<account>.dkr.ecr.<region>.amazonaws.com/openvla-realtime:latest
# SmolVLA-450M
cdk deploy Smolvla450mEcsStack \
-c smolvla450mEcrImageUri=<account>.dkr.ecr.<region>.amazonaws.com/smolvla-450m-realtime:latest
# LAP-3B (served from VlaHubStack — override the lap image URI via context)
cdk deploy VlaHubStack \
-c lapEcrImageUri=<account>.dkr.ecr.<region>.amazonaws.com/vla-lap-realtime:3B-latest
-c instanceTypes=g6.2xlarge,g5.2xlarge # comma-separated fallback list
-c desiredCount=1
| Output | Description |
|---|---|
GrpcEndpoint | NLB DNS + model-specific port — gRPC inference endpoint |
VpcId | Place gRPC client EC2 in this VPC |
PrivateSubnetIds | Place gRPC client EC2 in one of these subnets |
ClusterName / ServiceName | ECS identifiers |
SelectedInstanceType / SelectedAZ | AzSelector probe result |
Each model ships its own .proto. All five expose Infer + Health RPCs with
the per-model schema appropriate to that model's input/output conventions.
| Model | Proto | Service | Port |
|---|---|---|---|
| GR00T N1.6 | docker/gr00t-n16/gr00t.proto | GR00TInference | 50050 |
| GR00T N1.7 | docker/gr00t-n17/gr00t.proto | GR00TInference | 50051 |
| π0.5 | docker/pi/pi.proto | PIInference | 50052 |
| OpenVLA-7B | docker/openvla/openvla.proto | OpenVLAInference | 50053 |
| SmolVLA-450M | docker/smolvla/smolvla.proto | SmolVLAInference | 50054 |
| LAP-3B | docker/lap/lap.proto | LAPInference | 50055 |
Common shape for Infer:
image_data (JPEG bytes), instruction (string), optional joint statesaction_chunks — map<string, bytes> (float32 tobytes, reshape to
(H, DOF) per the model's action-chunk convention)Health(HealthRequest) → HealthResponse is provided on every service for NLB /
client probes.
AzSelectorConstruct probes EC2 capacity at deploy time and pins the ASG to a
confirmed AZ.
| Model | Priority order |
|---|---|
| GR00T N1.6 / N1.7 | g6.2xlarge → g5.2xlarge → g6.xlarge → g5.xlarge |
| π0.5 | g5.2xlarge → g5.xlarge → g6.2xlarge → g6.xlarge |
| OpenVLA-7B | g6.2xlarge → g5.2xlarge → g6.xlarge → g5.xlarge |
| SmolVLA-450M | g6.xlarge → g5.xlarge → g6.2xlarge → g5.2xlarge |
| LAP-3B | g6.xlarge → g5.xlarge → g6.2xlarge → g5.2xlarge |
GR00T (and OpenVLA's FlashAttention path) require Ampere GPU (SM80+). π0.5 and LAP-3B use JAX and do not require FlashAttention.
docker/
├── gr00t-n16/ # GR00T N1.6 container (PyTorch + Isaac Lab gRPC server)
├── gr00t-n17/ # GR00T N1.7 container (PyTorch + Isaac Lab gRPC server)
├── pi/ # π0.5 container (JAX + pi0.5 gRPC server)
├── openvla/ # OpenVLA-7B container (PyTorch + HuggingFace gRPC server)
├── smolvla/ # SmolVLA-450M container (PyTorch + LeRobot gRPC server)
└── lap/ # LAP-3B container (JAX + openpi/lap gRPC server)
Each context is packaged as an S3 Asset and passed to CodeBuild. When
docker/<model>/ changes, redeploy the corresponding BuildStack.
SageMaker supports Triton Inference Server internally, but it only exposes an
HTTP REST endpoint externally. The Triton gRPC port (:8001) is inaccessible
from outside the SageMaker container. This makes SageMaker unsuitable for two
requirements:
ECS + NLB (L4 passthrough) hands TLS termination to the ECS Task itself, enabling full gRPC + mTLS control.
For simple PoC/demo with REST, SageMaker is fine. For production-style robot connectivity patterns, ECS + NLB is correct.
| Factor | serve.py (current) | Triton Python backend |
|---|---|---|
Custom .proto | ✅ Per-model proto preserved | ❌ Must migrate to Triton standard proto → client SDK rewrite |
| GR00T monkey-patch | ✅ In serve.py init | Possible in model.py init, same complexity |
| π0.5 JAX runtime | ✅ Isolated ECS Task | JAX + PyTorch on the same GPU is risky (memory pre-allocation conflict) |
| Prometheus metrics | Manual | ✅ Built-in |
| Dynamic batching | Manual (ASG scale-out) | ✅ Built-in |
Current verdict: Triton adds operational complexity without reducing code
complexity. The blocking issue is the custom .proto requirement — migrating
to Triton's standard generate.proto would require rewriting all client SDKs
and the ZMQ-gRPC bridge. Additionally, π0.5 (JAX) and GR00T (PyTorch) cannot
safely share a GPU due to JAX's pre-emptive memory allocation
(XLA_PYTHON_CLIENT_MEM_FRACTION).
Re-evaluate when NVIDIA ships an official TensorRT-optimized GR00T package — that would eliminate the monkey-patch requirement and make adopting the standard Triton proto natural. See Triton Inference Server for upstream reference.
Key reasons for Physical AI inference workloads:
.proto auto-generates both server and client codeNLB sticky routing (TCP 5-tuple hash) works identically for both gRPC and WebSocket — it is not a differentiator. The advantages above are protocol-level, not routing-level.
NLB operates at L4 (TCP passthrough) — TLS is terminated inside the ECS Task container, enabling E2E mTLS. ALB terminates TLS at the load balancer layer, breaking the mTLS chain. NLB also handles long-lived gRPC streaming connections without HTTP-layer timeouts.
This repository is a sample for demonstration and PoC use. Before adapting it for production or customer-facing workloads, review the following — none are hard blockers, but each is a deliberate scoping choice in the sample:
AzSelectorConstruct pins the ASG to one confirmed-capacity AZ). For HA,
switch to multi-AZ ASG with capacity rebalancing, or run two stacks in
different AZs behind a regional endpoint.Operators are expected to evaluate the architecture against their own security, compliance, and availability requirements before any customer-facing use.
The cloud serving path above (x86 + NVIDIA g5/g6) is the supported, validated track and does not change. This section adds a separate Jetson (L4T) edge track: the same SmolVLA-450M policy, repackaged to run on-device (Orin/Thor). It is a roadmap — the code lands incrementally, and on-device validation depends on hardware we do not currently own (see the FAQ).
In
lerobot/smolvla_base on
HuggingFace) — no retraining, no new policy.Out
localhost:50054) that
exposes the same Infer / Health contract as the cloud — robot/sim
clients connect identically; only host:port changes."Runs" and "runs in realtime" are deliberately separate outcomes: the eager PyTorch path gets it running (functional, Hz not guaranteed); the TensorRT path (Phase 4) makes it run in realtime.
flowchart LR
W["SmolVLA-450M weights<br/>(same as cloud)"] --> BUILD
DEV(["Jetson<br/>Orin / Thor"]) --> PRE["preflight.sh<br/>detect → BASE_IMAGE"]
PRE --> BUILD["Dockerfile.jetson<br/>build L4T image"]
BUILD --> RUN["run.sh<br/>serve over gRPC"]
RUN --> OUT(["gRPC endpoint<br/>localhost:50054"])
RUN --> SMOKE["smoke_test.sh<br/>p50 / p99 + Hz"]
RUN -. "realtime (Phase 4)" .-> TRT["build-trt.sh<br/>TensorRT engine"]
TRT -. "BACKEND=tensorrt" .-> RUN
The L4T image must be built on the Jetson itself (aarch64 + Tegra) — x86 /
Graviton-sbsa cannot build a Tegra image. The full day-1 runbook is
edge/GETTING_STARTED_ON_HARDWARE.md; the
short version (CDK-free, lives in edge/):
cd edge
# 1. Detect the device and print the exact base image to use:
./preflight.sh # → Orin: …-py3-igpu | Thor: …-py3
# 2. Build the L4T image and serve, in one command:
export BASE_IMAGE=nvcr.io/nvidia/pytorch:25.08-py3-igpu # (Orin; preflight prints yours)
BUILD=1 BASE_IMAGE=$BASE_IMAGE ./run.sh smolvla
# → gRPC: localhost:50054 health: http://localhost:8080/health
# 3. Measure latency + Hz:
./smoke_test.sh smolvla
# Or deploy a prebuilt image from a registry (1-click):
./deploy-edge.sh smolvla <account>.dkr.ecr.<region>.amazonaws.com/vla-smolvla-realtime:edge-latest
No Jetson on hand? You can still exercise the run-target logic on a plain x86
GPU host — build the cloud SmolVLA image and drive it through the same run.sh
(see edge/README.md).
SmolVLA-450M is the first (and currently only) edge target — already tagged "edge / small robot" in the capability matrix above. The software is staged so the whole day-1 path is scripted, with no setup guesswork.
| Phase | What | State |
|---|---|---|
| 0 | This roadmap; cloud stack untouched | ✅ |
| 1 | serve.py / .proto / entrypoint shared across cloud + edge Dockerfiles | ✅ |
| 2 | docker/smolvla/Dockerfile.jetson on an L4T base (ARG BASE_IMAGE) | ✅ |
| 3 | edge/ run-target — docker-compose.yml / run.sh, no CDK | ✅ |
| 5 | edge/deploy-edge.sh 1-click deploy | ✅ |
| 6 | Test harness — bench_client.py + smoke_test.sh (p50/p99 + Hz) | ✅ |
| 7 | Day-1 enablement — preflight.sh + hardware runbook | ✅ |
| 4 | TensorRT realtime backend (BACKEND switch + build-trt.sh) | ◐ scaffold; fails loud, no fake engine — model-specific export finished on-device |
Because aarch64 is a false friend — chasing it makes the cloud harder without
helping the edge:
nvcr.io/nvidia/pytorch arm64/sbsa); Jetson Orin/Thor use the tegra/L4T
stack (nvcr.io/nvidia/l4t-pytorch). Both are aarch64 ISA, but the CUDA runtime,
driver model, and base image differ — an sbsa-ARM image does not run on a
Jetson. Moving the cloud to ARM buys nothing for the edge.g5g, is a Turing
T4G (SM75) — too old for GR00T and OpenVLA's FlashAttention path — and it splits
the fleet across two architectures for no benefit.serve.py (Python) and
*.proto (protobuf) are architecture-neutral; they move to a Jetson build for
free regardless of whether the cloud is x86 or ARM.So there is no multi-arch manifest to share. Cloud (x86) and edge (L4T) are two independent tracks with different base images; the shared asset is the source, expressed as two Dockerfiles over one source tree:
[same SmolVLA-450M weights] ──┬─→ Dockerfile → x86 image → ECS/NLB (cloud, validated, unchanged)
(HF: lerobot/smolvla_base) └─→ Dockerfile.jetson → L4T image → compose/run (edge, new)
↑ identical ↑ base differs only ↑ arch differs only
serve.py / .proto = shared (architecture-neutral)
| Layer | Location | Edge portability |
|---|---|---|
| Model adapter (preprocess · infer · action-chunk) | docker/<model>/serve.py | Reused as-is. Zero boto3/sagemaker/ecs/cloudwatch imports |
| Interface contract | docker/<model>/*.proto (Infer + Health) | Reused as-is. Same schema on cloud and edge |
| Runtime contract | entrypoint.sh (CUDA-compat branch) / check_health.sh (:8080) | Reused nearly as-is — device is env-driven (DEVICE) |
| Base image | Dockerfile FROM (pytorch/pytorch) | L4T variant added separately (Dockerfile.jetson) |
| Cloud orchestration | lib/vla-*.ts (VPC/NLB/ECS/ASG/AzSelector/CodeBuild) | Not used on edge — replaced by localhost gRPC / compose |
The cloud lock-in is confined to lib/*.ts; the serving layer is already
portable, so the edge track is a head-start on the existing code, not a rewrite.
nvcr.io/nvidia/l4t-pytorch / jetson-containers base, not the
standard sbsa arm64 one.lerobot / transformers / torch on L4T may
need source builds or a jetson-containers recipe for packages that have no
prebuilt wheel.5 commits
1 commits
Python
38.8%
TypeScript
32.8%
Shell
15.4%
Dockerfile
12.0%