Repository files navigation

APT

APT: Action Expert Pretraining
Improves Instruction Generalization of Vision-Language-Action Policies

Kechun Xu · Zhenjie Zhu · Anzhe Chen · Rong Xiong · Yue Wang

Paper PDFProject Page

TL; DR: APT factorizes the VLA policy into a Vision-Action (VA) prior and a language-conditioned VLA likelihood, and pretrains the action expert as the VA prior on vision-action pairs from a frozen VLM. A layer-wise gated fusion mechanism then injects language tokens into the pretrained action expert, preserving the visuomotor prior while enabling instruction following. APT delivers consistent gains on OOD language instructions and compositional tasks.

🏆 Highlights

🔍 Key Findings: continuous-action VLA policies start from a randomly initialized action expert and learn from imbalanced VLA data, producing noisy gradients that corrupt the VLM backbone and collapse to visual shortcuts.

Key Insights:

  • Bayesian factorization of the VLA policy:

$$ \pi(\mathbf{a}\mid\mathbf{v},\ell)\ \propto\ \pi^{p}(\mathbf{a}\mid\mathbf{v})\cdot L(\ell\mid\mathbf{v},\mathbf{a}) $$

  • VA prior$\pi^p(\mathbf{a}\mid\mathbf{v})$ is trained on balanced vision-action pairs alone, so the action expert builds coherent visuomotor priors without any language shortcut.

  • VLA likelihood$L(\ell\mid\mathbf{v},\mathbf{a})$ then aligns the prior to language instructions, a much easier sub-problem than learning action generation and language grounding jointly.

  • Layer-wise gated fusion injects each Qwen3-VL intermediate feature into the corresponding action-expert self-attention layer through a learnable sigmoid gate, letting the action expert inherit VLM semantics without overwriting the pretrained visuomotor pathway.

  • Two-stage realization inside one network. Stage 1 activates only half of the action-expert attention layers and masks language tokens, training a pure VA prior. Stage 2 inserts an interleaved attention layer after each Stage-1 layer, unmasks language, and jointly trains the prior and likelihood under large-scale data.

  • Architecture-agnostic: the two-stage recipe also boosts $\pi$-style and GR00T-style architectures on OOD language generalization.

🧩 Overview

Given VLA datasets with modality imbalance, APT trains the policy in two stages:

  • Stage 1 - VA Prior Pretraining: the action expert is conditioned solely on visual tokens from a frozen Qwen3-VL backbone and learns $\pi^p(\mathbf{a}\mid\mathbf{v})$.
  • Stage 2 - VLA Likelihood Alignment: the Stage-1 layers are duplicated with interleaved language-injection layers; the full policy is jointly trained on the same data.

APT method overview

📁 Project Structure

APT/
├── apt/ # core model + unified trainer (this is the package)
│ ├── vla.py # VLA wrapper (VLM + ActionExpert)
│ ├── vlm.py # Qwen3-VL encoder bridge
│ ├── action_expert.py # diffusion-based action expert with gated fusion
│ ├── action_transform.py # SE(3) ↔ 10-dim action conversions
│ ├── configs.py # TrainConfig + CONFIGS registry
│ ├── train.py # unified DDP + DeepSpeed trainer
│ ├── ds_config_zero2.json # DeepSpeed ZeRO-2 config
│ ├── ds_config_zero3.json # DeepSpeed ZeRO-3 config
│ ├── encoders/ # Qwen3-VL (LoRA-capable) wrapper
│ ├── layers/ # attention / RoPE / norms / 6D-rotation utils
│ └── infer/ # planner + remote inference service
├── data_utils/ # HDF5 IO, datasets, video decoding, distributed samplers
├── train_utils/ # EMA implementation
├── infer_utils/ # trajectory ensembler and visualizer
├── shm_transport/ # Pyro4 + shared-memory RPC for remote inference
├── scripts/
│ ├── train.sh # two-stage pretraining (DDP or DeepSpeed)
│ └── finetune.sh # task-specific fine-tuning (DDP or DeepSpeed)
├── examples/
│ ├── libero/ # LIBERO / LIBERO-PRO / LIBERO-plus evaluator
│ └── PickPlace/ # Isaac Sim pick-and-place benchmark (UR5 + Robotiq 85)
├── assets/ # logo / method (PDF source + PNG for README), paper.pdf
├── requirements.txt
├── .gitignore
└── README.md

📘 Usage

Environment

conda create -n apt python=3.10 -y
conda activate apt
# Install a PyTorch build that matches your CUDA. The pinned xformers requires# torch 2.4.1; relax it if you use a different torch version.
pip install torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cu126
pip install -r requirements.txt

Note on xformers / deepspeed: both are version-sensitive. If you do not need DeepSpeed, you can skip installing it - --backend ddp works without it. Likewise drop xformers if you do not need its kernels.

Data Preparation

APT consumes trajectories stored as per-episode HDF5 files. Each sample yielded by the dataloader looks like:

{
"obs_rgbs": (To, ncam, 3, H, W), # observation frames"prompt_text": str, # task description"current_ee_pose": (nee, 4, 4), # current EE pose in world frame"gt_future_ee_states": (Ta, nee, 17), # ground truth pose + gripper"history_ee_states": (Th, nee, 17),
"obs_norm_xys": (...), # per-pixel normalized 2D coords"obs_extrinsics": (To, ncam, 4, 4),
"valid_ee_mask": (nee,),
... # see data_utils/dataset_base.py
}

Adding a new dataset:

  1. Subclass H5DatasetMapBase in data_utils/datasets.py.
  2. Register the dataset entry in data_utils/data_loc.py. The file looks up the host IP (get_ipv4_address) and selects the matching dictionary - edit it to point at your local paths before launching training.
  3. Reference the new class from a TrainConfig entry in apt/configs.py.

We expose a number of preset configs (see apt/configs.py for the full list). Examples:

ConfigPurpose
pretrainPretrain on Droid + AgiBotWorld + InternA1 + InternM1
finetune_aloha_pp_storageReal-world ALOHA pick-place + table-storage fine-tuning
debugTiny single-batch config used for smoke tests

Two-stage Pretraining

scripts/train.sh drives both stages and accepts either back-end. Common arguments:

FlagMeaning
--backendddp (torchrun) or deepspeed
--gpusComma-separated GPU IDs, e.g. 0,1,2,3
--stage0 (VA only), 1 (VLA only), or both
--configConfig name from apt/configs.py
--va-nameSave name for the Stage-0 checkpoint
--vla-nameSave name for the Stage-1 checkpoint
--va-contiResume Stage-0 from an existing checkpoint
--vla-contiResume Stage-1 from an existing checkpoint
--bs / --max-iterPer-GPU batch size / max iterations

DeepSpeed-only flags (ignored when --backend ddp):

FlagMeaning
--ds-zero 2|3ZeRO stage (selects ds_config_zero{2,3}.json)
--accum NGradient accumulation steps
--vlm-modefrozen / lora / full VLM finetune mode
--vlm-lrSeparate learning rate for VLM parameters
--gcEnable gradient checkpointing

DDP (torchrun) + Fix VLM, Stage-0 only:

bash scripts/train.sh --backend ddp --gpus 0,1,2,3 --stage 0 \
--config pretrain \
--va-name apt_va --vla-name apt_vla

DeepSpeed ZeRO-2 + Full VLM, Stage-1 only:

bash scripts/train.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config pretrain \
--va-name apt_va --vla-name apt_vla_vlmft \
--ds-zero 2 --vlm-mode full --gc --vlm-lr 1e-5

Resume Stage-1 after preemption:

bash scripts/train.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config pretrain --vla-conti apt_vla_vlmft \
--ds-zero 2 --vlm-mode full --gc --vlm-lr 1e-5

Stage-1 internally calls VLA.load_from_pretrain(..., load_from_va=True), which doubles the Stage-1 attention layers and copies the Stage-0 weights into the odd indices while leaving the inserted (even-index) language-injection layers randomly initialized.

Task-specific Fine-tuning

scripts/finetune.sh mirrors train.sh but additionally exposes --pretrained-ckpt so you can bootstrap from any pretraining checkpoint. The Stage-1 launch automatically reuses the Stage-0 name (if any) or the pretraining checkpoint as the upstream.

# Stage-1 only, DeepSpeed ZeRO-2 + Full VLM on Pick-Place from a pretrained VLA checkpoint
bash scripts/finetune.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config finetune_pp --pretrained-ckpt apt_vla \
--vla-name ft_apt_pp \
--ds-zero 3 --vlm-mode lora --gc --accum 4 --bs 64 --vlm-lr 1e-5
# Stage-1 only, Fix VLM on real ALOHA data from a pretrained VLA checkpoint
bash scripts/finetune.sh --backend ddp --gpus 0,1,2,3 --stage 1 \
--config finetune_aloha_pp_storage --pretrained-ckpt apt_vla_vlmft \
--vla-name ft_apt_aloha

Checkpoints are written under ./checkpoints/APT/<name>/ and TensorBoard logs under ./logs/APT/<name>/. Both roots are configurable per run via the optional flags --ckpt_dir /path/to/ckpts and --log_dir /path/to/logs, e.g. to keep separate experiments on different volumes. Likewise --dataloader_timeout <seconds> (default 300) lets you raise the DataLoader worker timeout for slow shared storage.

Loading existing APT checkpoints

The merged trainer is backwards-compatible with checkpoints produced by the pre-refactor training scripts. A checkpoint is recognised by its file layout:

Saved byTop-level keys in ckpt_latest.pt
DDP (train_dist.py)weights, optimizer, scheduler, scaler, current_iters, ...
DeepSpeed (train_deepspeed.py)weights, vlm_weights (if VLM fine-tuned), no embedded optimizer

The merged apt.train accepts both, and you can also switch back-ends across resumes (e.g. resume a DeepSpeed-trained run under DDP). When a checkpoint's optimizer state cannot be re-loaded (e.g. param groups differ because --vlm-mode changed), the trainer logs a warning and continues with a fresh optimizer.

Pre-flight check - validate any existing checkpoint before launching training:

# Stage-0 VA checkpoint
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 0
# Same VA checkpoint used to bootstrap Stage-1
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 1 --load-from-va
# Stage-1 VLA checkpoint (already-trained policy)
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 1

Bootstrap a new fine-tuning run from an existing VLA checkpoint (most common):

bash scripts/finetune.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config finetune_pp \
--pretrained-ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt \
--vla-name ft_apt_pp --vlm-mode full

--pretrained-ckpt accepts either a checkpoint subdir name under ./checkpoints/APT/or an absolute path ending in .pt. The trainer starts with current_iters=0 so the new run gets a clean iteration counter, and saves under ./checkpoints/APT/<vla-name>/.

Inference

For local inference, instantiate the planner directly:

fromapt.infer.plannerimportTrajPlannerplanner=TrajPlanner(
ckpt_path="checkpoints/APT/ft_vla/ckpt_latest.pt",
device="cuda:0",
ensemble=4,
use_ema=False,
)
planner.set_prompt("Pick up the grape and place it on the pink box.")
planner.add_obs_frame(obs_frame)
actions=planner.get_action()

To serve the policy as a remote service (e.g. for hardware control), first launch a Pyro4 naming server, then start the service:

# 1. Naming server (defaults to localhost:9091)
pyro4-ns -p 9091
# 2. Inference service
python -m apt.infer.remote_service \
--ckpt checkpoints/APT/ft_vla/ckpt_latest.pt \
--uri apt_control \
--host localhost --port 0 \
--ensemble 4

The client side uses shm_transport (zero-copy shared memory + Pyro4) to call add_obs_frame, set_prompt, get_action, etc.

🧪 Evaluation on LIBERO benchmarks

A single evaluator under examples/libero/ drives all three LIBERO benchmark families against an APT policy server:

BenchmarkSimulatorSuitesDefault trials/task
LIBEROLifelong-Robot-Learning/LIBEROlibero_{object,spatial,goal,10} — 4 suites50
LIBERO-PROZxy-MLlab/LIBERO-PROthe 4 above × {_swap, _task} — 8 suites50
LIBERO-plussylvestf/LIBERO-pluslibero_{object,spatial,goal,10} — 4 suites1

Quick start (LIBERO conda env, after launching the APT policy server in a separate terminal):

bash examples/libero/test_libero.sh \
--benchmark libero \
--gpu 0 \
--model_name apt_vla \
--controller_name control --controller_port 9091

See examples/libero/README.md for the per-benchmark conda setup, the policy-server launch command, the full list of test_libero.sh flags, and the on-disk output layout.

🤖 Evaluation on Pick-and-Place in Isaac Sim

An Isaac-Sim-based pick-and-place benchmark lives under examples/PickPlace/. A UR5 + Robotiq 85 arm executes language-conditioned pick-and-place on a tabletop scene; the APT policy server is queried for actions each control step.

SettingDescription
soSeen object set, default lighting / ground
uoHeld-out object set, default lighting / ground
ucSame objects as so, but the target container is a held-out mug
uoueHeld-out objects + novel HDR background + randomized ground (seed sweep)

Two separate Python environments are involved: the APT env (running the policy server with shm_transport) and the Isaac Sim bundled Python (running the benchmark). shm_transport lives at the APT root and is picked up automatically by the driver via PYTHONPATH. Quick start:

# 1. APT env (separate terminal) — launch the policy server, see "Inference" above.# 2. Isaac Sim env — drive all 4 settings:cd examples/PickPlace
GPU_ID=0 URI=control PORT=9091 SAVE_DIR=./data/exp_results/myrun \
bash eval_all.sh

See examples/PickPlace/README.md for the asset preparation, full environment requirements, per-setting flags, and the on-disk output format (<save_dir>/<setting>/videos/*.mp4 + metrics/*.json).

📥 Pretrained Checkpoints

All checkpoints live under a single Hugging Face repo: KechunXu1/apt_models.

StageConfigDatasetsHugging Face
Pretrained VLA policypretrain (--load_from_va)Droid + AgiBotWorld + InternA1 + InternM1apt_vla
LIBERO fine-tunedfinetune_liberoLIBERO Spatial / Object / Goal / 10apt_vla_ftlibero
Pick-Place fine-tunedfinetune_ppPickPlaceCanapt_vla_ftpp

Download a checkpoint (e.g. the pretrained VLA policy):

hf download KechunXu1/apt_models --include "apt_vla/*" --local-dir ./checkpoints/APT

Then point the inference script at the downloaded checkpoint via --ckpt ./checkpoints/APT/apt_vla/ckpt_latest.pt (see the Inference section).

🤝 Acknowledgements

This project builds upon BayesVLA, and E2VLA. We thank these teams for their open-source contributions.

📚 Citation

If you find this work useful, please consider citing:

@article{xu2026apt,
title={APT: Action Expert Pretraining Improves Instruction Generalization of Vision-Language-Action Policies},
author={Xu, Kechun and Zhu, Zhenjie and Chen, Anzhe and Xiong, Rong and Wang, Yue},
journal={arXiv preprint arXiv:2606.12366},
year={2026}
}

About

[arXiv 2026] APT: Action Expert Pretraining Improves Instruction Generalization of Vision-Language-Action Policies

Resources

Stars

39 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

APT

APT: Action Expert Pretraining
Improves Instruction Generalization of Vision-Language-Action Policies

Kechun Xu · Zhenjie Zhu · Anzhe Chen · Rong Xiong · Yue Wang

Paper PDFProject Page

TL; DR: APT factorizes the VLA policy into a Vision-Action (VA) prior and a language-conditioned VLA likelihood, and pretrains the action expert as the VA prior on vision-action pairs from a frozen VLM. A layer-wise gated fusion mechanism then injects language tokens into the pretrained action expert, preserving the visuomotor prior while enabling instruction following. APT delivers consistent gains on OOD language instructions and compositional tasks.

🏆 Highlights

🔍 Key Findings: continuous-action VLA policies start from a randomly initialized action expert and learn from imbalanced VLA data, producing noisy gradients that corrupt the VLM backbone and collapse to visual shortcuts.

Key Insights:

  • Bayesian factorization of the VLA policy:

$$ \pi(\mathbf{a}\mid\mathbf{v},\ell)\ \propto\ \pi^{p}(\mathbf{a}\mid\mathbf{v})\cdot L(\ell\mid\mathbf{v},\mathbf{a}) $$

  • VA prior$\pi^p(\mathbf{a}\mid\mathbf{v})$ is trained on balanced vision-action pairs alone, so the action expert builds coherent visuomotor priors without any language shortcut.

  • VLA likelihood$L(\ell\mid\mathbf{v},\mathbf{a})$ then aligns the prior to language instructions, a much easier sub-problem than learning action generation and language grounding jointly.

  • Layer-wise gated fusion injects each Qwen3-VL intermediate feature into the corresponding action-expert self-attention layer through a learnable sigmoid gate, letting the action expert inherit VLM semantics without overwriting the pretrained visuomotor pathway.

  • Two-stage realization inside one network. Stage 1 activates only half of the action-expert attention layers and masks language tokens, training a pure VA prior. Stage 2 inserts an interleaved attention layer after each Stage-1 layer, unmasks language, and jointly trains the prior and likelihood under large-scale data.

  • Architecture-agnostic: the two-stage recipe also boosts $\pi$-style and GR00T-style architectures on OOD language generalization.

🧩 Overview

Given VLA datasets with modality imbalance, APT trains the policy in two stages:

  • Stage 1 - VA Prior Pretraining: the action expert is conditioned solely on visual tokens from a frozen Qwen3-VL backbone and learns $\pi^p(\mathbf{a}\mid\mathbf{v})$.
  • Stage 2 - VLA Likelihood Alignment: the Stage-1 layers are duplicated with interleaved language-injection layers; the full policy is jointly trained on the same data.

APT method overview

📁 Project Structure

APT/
├── apt/ # core model + unified trainer (this is the package)
│ ├── vla.py # VLA wrapper (VLM + ActionExpert)
│ ├── vlm.py # Qwen3-VL encoder bridge
│ ├── action_expert.py # diffusion-based action expert with gated fusion
│ ├── action_transform.py # SE(3) ↔ 10-dim action conversions
│ ├── configs.py # TrainConfig + CONFIGS registry
│ ├── train.py # unified DDP + DeepSpeed trainer
│ ├── ds_config_zero2.json # DeepSpeed ZeRO-2 config
│ ├── ds_config_zero3.json # DeepSpeed ZeRO-3 config
│ ├── encoders/ # Qwen3-VL (LoRA-capable) wrapper
│ ├── layers/ # attention / RoPE / norms / 6D-rotation utils
│ └── infer/ # planner + remote inference service
├── data_utils/ # HDF5 IO, datasets, video decoding, distributed samplers
├── train_utils/ # EMA implementation
├── infer_utils/ # trajectory ensembler and visualizer
├── shm_transport/ # Pyro4 + shared-memory RPC for remote inference
├── scripts/
│ ├── train.sh # two-stage pretraining (DDP or DeepSpeed)
│ └── finetune.sh # task-specific fine-tuning (DDP or DeepSpeed)
├── examples/
│ ├── libero/ # LIBERO / LIBERO-PRO / LIBERO-plus evaluator
│ └── PickPlace/ # Isaac Sim pick-and-place benchmark (UR5 + Robotiq 85)
├── assets/ # logo / method (PDF source + PNG for README), paper.pdf
├── requirements.txt
├── .gitignore
└── README.md

📘 Usage

Environment

conda create -n apt python=3.10 -y
conda activate apt
# Install a PyTorch build that matches your CUDA. The pinned xformers requires# torch 2.4.1; relax it if you use a different torch version.
pip install torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cu126
pip install -r requirements.txt

Note on xformers / deepspeed: both are version-sensitive. If you do not need DeepSpeed, you can skip installing it - --backend ddp works without it. Likewise drop xformers if you do not need its kernels.

Data Preparation

APT consumes trajectories stored as per-episode HDF5 files. Each sample yielded by the dataloader looks like:

{
"obs_rgbs": (To, ncam, 3, H, W), # observation frames"prompt_text": str, # task description"current_ee_pose": (nee, 4, 4), # current EE pose in world frame"gt_future_ee_states": (Ta, nee, 17), # ground truth pose + gripper"history_ee_states": (Th, nee, 17),
"obs_norm_xys": (...), # per-pixel normalized 2D coords"obs_extrinsics": (To, ncam, 4, 4),
"valid_ee_mask": (nee,),
... # see data_utils/dataset_base.py
}

Adding a new dataset:

  1. Subclass H5DatasetMapBase in data_utils/datasets.py.
  2. Register the dataset entry in data_utils/data_loc.py. The file looks up the host IP (get_ipv4_address) and selects the matching dictionary - edit it to point at your local paths before launching training.
  3. Reference the new class from a TrainConfig entry in apt/configs.py.

We expose a number of preset configs (see apt/configs.py for the full list). Examples:

ConfigPurpose
pretrainPretrain on Droid + AgiBotWorld + InternA1 + InternM1
finetune_aloha_pp_storageReal-world ALOHA pick-place + table-storage fine-tuning
debugTiny single-batch config used for smoke tests

Two-stage Pretraining

scripts/train.sh drives both stages and accepts either back-end. Common arguments:

FlagMeaning
--backendddp (torchrun) or deepspeed
--gpusComma-separated GPU IDs, e.g. 0,1,2,3
--stage0 (VA only), 1 (VLA only), or both
--configConfig name from apt/configs.py
--va-nameSave name for the Stage-0 checkpoint
--vla-nameSave name for the Stage-1 checkpoint
--va-contiResume Stage-0 from an existing checkpoint
--vla-contiResume Stage-1 from an existing checkpoint
--bs / --max-iterPer-GPU batch size / max iterations

DeepSpeed-only flags (ignored when --backend ddp):

FlagMeaning
--ds-zero 2|3ZeRO stage (selects ds_config_zero{2,3}.json)
--accum NGradient accumulation steps
--vlm-modefrozen / lora / full VLM finetune mode
--vlm-lrSeparate learning rate for VLM parameters
--gcEnable gradient checkpointing

DDP (torchrun) + Fix VLM, Stage-0 only:

bash scripts/train.sh --backend ddp --gpus 0,1,2,3 --stage 0 \
--config pretrain \
--va-name apt_va --vla-name apt_vla

DeepSpeed ZeRO-2 + Full VLM, Stage-1 only:

bash scripts/train.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config pretrain \
--va-name apt_va --vla-name apt_vla_vlmft \
--ds-zero 2 --vlm-mode full --gc --vlm-lr 1e-5

Resume Stage-1 after preemption:

bash scripts/train.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config pretrain --vla-conti apt_vla_vlmft \
--ds-zero 2 --vlm-mode full --gc --vlm-lr 1e-5

Stage-1 internally calls VLA.load_from_pretrain(..., load_from_va=True), which doubles the Stage-1 attention layers and copies the Stage-0 weights into the odd indices while leaving the inserted (even-index) language-injection layers randomly initialized.

Task-specific Fine-tuning

scripts/finetune.sh mirrors train.sh but additionally exposes --pretrained-ckpt so you can bootstrap from any pretraining checkpoint. The Stage-1 launch automatically reuses the Stage-0 name (if any) or the pretraining checkpoint as the upstream.

# Stage-1 only, DeepSpeed ZeRO-2 + Full VLM on Pick-Place from a pretrained VLA checkpoint
bash scripts/finetune.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config finetune_pp --pretrained-ckpt apt_vla \
--vla-name ft_apt_pp \
--ds-zero 3 --vlm-mode lora --gc --accum 4 --bs 64 --vlm-lr 1e-5
# Stage-1 only, Fix VLM on real ALOHA data from a pretrained VLA checkpoint
bash scripts/finetune.sh --backend ddp --gpus 0,1,2,3 --stage 1 \
--config finetune_aloha_pp_storage --pretrained-ckpt apt_vla_vlmft \
--vla-name ft_apt_aloha

Checkpoints are written under ./checkpoints/APT/<name>/ and TensorBoard logs under ./logs/APT/<name>/. Both roots are configurable per run via the optional flags --ckpt_dir /path/to/ckpts and --log_dir /path/to/logs, e.g. to keep separate experiments on different volumes. Likewise --dataloader_timeout <seconds> (default 300) lets you raise the DataLoader worker timeout for slow shared storage.

Loading existing APT checkpoints

The merged trainer is backwards-compatible with checkpoints produced by the pre-refactor training scripts. A checkpoint is recognised by its file layout:

Saved byTop-level keys in ckpt_latest.pt
DDP (train_dist.py)weights, optimizer, scheduler, scaler, current_iters, ...
DeepSpeed (train_deepspeed.py)weights, vlm_weights (if VLM fine-tuned), no embedded optimizer

The merged apt.train accepts both, and you can also switch back-ends across resumes (e.g. resume a DeepSpeed-trained run under DDP). When a checkpoint's optimizer state cannot be re-loaded (e.g. param groups differ because --vlm-mode changed), the trainer logs a warning and continues with a fresh optimizer.

Pre-flight check - validate any existing checkpoint before launching training:

# Stage-0 VA checkpoint
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 0
# Same VA checkpoint used to bootstrap Stage-1
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 1 --load-from-va
# Stage-1 VLA checkpoint (already-trained policy)
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 1

Bootstrap a new fine-tuning run from an existing VLA checkpoint (most common):

bash scripts/finetune.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config finetune_pp \
--pretrained-ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt \
--vla-name ft_apt_pp --vlm-mode full

--pretrained-ckpt accepts either a checkpoint subdir name under ./checkpoints/APT/or an absolute path ending in .pt. The trainer starts with current_iters=0 so the new run gets a clean iteration counter, and saves under ./checkpoints/APT/<vla-name>/.

Inference

For local inference, instantiate the planner directly:

fromapt.infer.plannerimportTrajPlannerplanner=TrajPlanner(
ckpt_path="checkpoints/APT/ft_vla/ckpt_latest.pt",
device="cuda:0",
ensemble=4,
use_ema=False,
)
planner.set_prompt("Pick up the grape and place it on the pink box.")
planner.add_obs_frame(obs_frame)
actions=planner.get_action()

To serve the policy as a remote service (e.g. for hardware control), first launch a Pyro4 naming server, then start the service:

# 1. Naming server (defaults to localhost:9091)
pyro4-ns -p 9091
# 2. Inference service
python -m apt.infer.remote_service \
--ckpt checkpoints/APT/ft_vla/ckpt_latest.pt \
--uri apt_control \
--host localhost --port 0 \
--ensemble 4

The client side uses shm_transport (zero-copy shared memory + Pyro4) to call add_obs_frame, set_prompt, get_action, etc.

🧪 Evaluation on LIBERO benchmarks

A single evaluator under examples/libero/ drives all three LIBERO benchmark families against an APT policy server:

BenchmarkSimulatorSuitesDefault trials/task
LIBEROLifelong-Robot-Learning/LIBEROlibero_{object,spatial,goal,10} — 4 suites50
LIBERO-PROZxy-MLlab/LIBERO-PROthe 4 above × {_swap, _task} — 8 suites50
LIBERO-plussylvestf/LIBERO-pluslibero_{object,spatial,goal,10} — 4 suites1

Quick start (LIBERO conda env, after launching the APT policy server in a separate terminal):

bash examples/libero/test_libero.sh \
--benchmark libero \
--gpu 0 \
--model_name apt_vla \
--controller_name control --controller_port 9091

See examples/libero/README.md for the per-benchmark conda setup, the policy-server launch command, the full list of test_libero.sh flags, and the on-disk output layout.

🤖 Evaluation on Pick-and-Place in Isaac Sim

An Isaac-Sim-based pick-and-place benchmark lives under examples/PickPlace/. A UR5 + Robotiq 85 arm executes language-conditioned pick-and-place on a tabletop scene; the APT policy server is queried for actions each control step.

SettingDescription
soSeen object set, default lighting / ground
uoHeld-out object set, default lighting / ground
ucSame objects as so, but the target container is a held-out mug
uoueHeld-out objects + novel HDR background + randomized ground (seed sweep)

Two separate Python environments are involved: the APT env (running the policy server with shm_transport) and the Isaac Sim bundled Python (running the benchmark). shm_transport lives at the APT root and is picked up automatically by the driver via PYTHONPATH. Quick start:

# 1. APT env (separate terminal) — launch the policy server, see "Inference" above.# 2. Isaac Sim env — drive all 4 settings:cd examples/PickPlace
GPU_ID=0 URI=control PORT=9091 SAVE_DIR=./data/exp_results/myrun \
bash eval_all.sh

See examples/PickPlace/README.md for the asset preparation, full environment requirements, per-setting flags, and the on-disk output format (<save_dir>/<setting>/videos/*.mp4 + metrics/*.json).

📥 Pretrained Checkpoints

All checkpoints live under a single Hugging Face repo: KechunXu1/apt_models.

StageConfigDatasetsHugging Face
Pretrained VLA policypretrain (--load_from_va)Droid + AgiBotWorld + InternA1 + InternM1apt_vla
LIBERO fine-tunedfinetune_liberoLIBERO Spatial / Object / Goal / 10apt_vla_ftlibero
Pick-Place fine-tunedfinetune_ppPickPlaceCanapt_vla_ftpp

Download a checkpoint (e.g. the pretrained VLA policy):

hf download KechunXu1/apt_models --include "apt_vla/*" --local-dir ./checkpoints/APT

Then point the inference script at the downloaded checkpoint via --ckpt ./checkpoints/APT/apt_vla/ckpt_latest.pt (see the Inference section).

🤝 Acknowledgements

This project builds upon BayesVLA, and E2VLA. We thank these teams for their open-source contributions.

📚 Citation

If you find this work useful, please consider citing:

@article{xu2026apt,
title={APT: Action Expert Pretraining Improves Instruction Generalization of Vision-Language-Action Policies},
author={Xu, Kechun and Zhu, Zhenjie and Chen, Anzhe and Xiong, Rong and Wang, Yue},
journal={arXiv preprint arXiv:2606.12366},
year={2026}
}

About

[arXiv 2026] APT: Action Expert Pretraining Improves Instruction Generalization of Vision-Language-Action Policies

Resources

Stars

39 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

APT

APT: Action Expert Pretraining
Improves Instruction Generalization of Vision-Language-Action Policies

Kechun Xu · Zhenjie Zhu · Anzhe Chen · Rong Xiong · Yue Wang

Paper PDFProject Page

TL; DR: APT factorizes the VLA policy into a Vision-Action (VA) prior and a language-conditioned VLA likelihood, and pretrains the action expert as the VA prior on vision-action pairs from a frozen VLM. A layer-wise gated fusion mechanism then injects language tokens into the pretrained action expert, preserving the visuomotor prior while enabling instruction following. APT delivers consistent gains on OOD language instructions and compositional tasks.

🏆 Highlights

🔍 Key Findings: continuous-action VLA policies start from a randomly initialized action expert and learn from imbalanced VLA data, producing noisy gradients that corrupt the VLM backbone and collapse to visual shortcuts.

Key Insights:

  • Bayesian factorization of the VLA policy:

$$ \pi(\mathbf{a}\mid\mathbf{v},\ell)\ \propto\ \pi^{p}(\mathbf{a}\mid\mathbf{v})\cdot L(\ell\mid\mathbf{v},\mathbf{a}) $$

  • VA prior$\pi^p(\mathbf{a}\mid\mathbf{v})$ is trained on balanced vision-action pairs alone, so the action expert builds coherent visuomotor priors without any language shortcut.

  • VLA likelihood$L(\ell\mid\mathbf{v},\mathbf{a})$ then aligns the prior to language instructions, a much easier sub-problem than learning action generation and language grounding jointly.

  • Layer-wise gated fusion injects each Qwen3-VL intermediate feature into the corresponding action-expert self-attention layer through a learnable sigmoid gate, letting the action expert inherit VLM semantics without overwriting the pretrained visuomotor pathway.

  • Two-stage realization inside one network. Stage 1 activates only half of the action-expert attention layers and masks language tokens, training a pure VA prior. Stage 2 inserts an interleaved attention layer after each Stage-1 layer, unmasks language, and jointly trains the prior and likelihood under large-scale data.

  • Architecture-agnostic: the two-stage recipe also boosts $\pi$-style and GR00T-style architectures on OOD language generalization.

🧩 Overview

Given VLA datasets with modality imbalance, APT trains the policy in two stages:

  • Stage 1 - VA Prior Pretraining: the action expert is conditioned solely on visual tokens from a frozen Qwen3-VL backbone and learns $\pi^p(\mathbf{a}\mid\mathbf{v})$.
  • Stage 2 - VLA Likelihood Alignment: the Stage-1 layers are duplicated with interleaved language-injection layers; the full policy is jointly trained on the same data.

APT method overview

📁 Project Structure

APT/
├── apt/ # core model + unified trainer (this is the package)
│ ├── vla.py # VLA wrapper (VLM + ActionExpert)
│ ├── vlm.py # Qwen3-VL encoder bridge
│ ├── action_expert.py # diffusion-based action expert with gated fusion
│ ├── action_transform.py # SE(3) ↔ 10-dim action conversions
│ ├── configs.py # TrainConfig + CONFIGS registry
│ ├── train.py # unified DDP + DeepSpeed trainer
│ ├── ds_config_zero2.json # DeepSpeed ZeRO-2 config
│ ├── ds_config_zero3.json # DeepSpeed ZeRO-3 config
│ ├── encoders/ # Qwen3-VL (LoRA-capable) wrapper
│ ├── layers/ # attention / RoPE / norms / 6D-rotation utils
│ └── infer/ # planner + remote inference service
├── data_utils/ # HDF5 IO, datasets, video decoding, distributed samplers
├── train_utils/ # EMA implementation
├── infer_utils/ # trajectory ensembler and visualizer
├── shm_transport/ # Pyro4 + shared-memory RPC for remote inference
├── scripts/
│ ├── train.sh # two-stage pretraining (DDP or DeepSpeed)
│ └── finetune.sh # task-specific fine-tuning (DDP or DeepSpeed)
├── examples/
│ ├── libero/ # LIBERO / LIBERO-PRO / LIBERO-plus evaluator
│ └── PickPlace/ # Isaac Sim pick-and-place benchmark (UR5 + Robotiq 85)
├── assets/ # logo / method (PDF source + PNG for README), paper.pdf
├── requirements.txt
├── .gitignore
└── README.md

📘 Usage

Environment

conda create -n apt python=3.10 -y
conda activate apt
# Install a PyTorch build that matches your CUDA. The pinned xformers requires# torch 2.4.1; relax it if you use a different torch version.
pip install torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cu126
pip install -r requirements.txt

Note on xformers / deepspeed: both are version-sensitive. If you do not need DeepSpeed, you can skip installing it - --backend ddp works without it. Likewise drop xformers if you do not need its kernels.

Data Preparation

APT consumes trajectories stored as per-episode HDF5 files. Each sample yielded by the dataloader looks like:

{
"obs_rgbs": (To, ncam, 3, H, W), # observation frames"prompt_text": str, # task description"current_ee_pose": (nee, 4, 4), # current EE pose in world frame"gt_future_ee_states": (Ta, nee, 17), # ground truth pose + gripper"history_ee_states": (Th, nee, 17),
"obs_norm_xys": (...), # per-pixel normalized 2D coords"obs_extrinsics": (To, ncam, 4, 4),
"valid_ee_mask": (nee,),
... # see data_utils/dataset_base.py
}

Adding a new dataset:

  1. Subclass H5DatasetMapBase in data_utils/datasets.py.
  2. Register the dataset entry in data_utils/data_loc.py. The file looks up the host IP (get_ipv4_address) and selects the matching dictionary - edit it to point at your local paths before launching training.
  3. Reference the new class from a TrainConfig entry in apt/configs.py.

We expose a number of preset configs (see apt/configs.py for the full list). Examples:

ConfigPurpose
pretrainPretrain on Droid + AgiBotWorld + InternA1 + InternM1
finetune_aloha_pp_storageReal-world ALOHA pick-place + table-storage fine-tuning
debugTiny single-batch config used for smoke tests

Two-stage Pretraining

scripts/train.sh drives both stages and accepts either back-end. Common arguments:

FlagMeaning
--backendddp (torchrun) or deepspeed
--gpusComma-separated GPU IDs, e.g. 0,1,2,3
--stage0 (VA only), 1 (VLA only), or both
--configConfig name from apt/configs.py
--va-nameSave name for the Stage-0 checkpoint
--vla-nameSave name for the Stage-1 checkpoint
--va-contiResume Stage-0 from an existing checkpoint
--vla-contiResume Stage-1 from an existing checkpoint
--bs / --max-iterPer-GPU batch size / max iterations

DeepSpeed-only flags (ignored when --backend ddp):

FlagMeaning
--ds-zero 2|3ZeRO stage (selects ds_config_zero{2,3}.json)
--accum NGradient accumulation steps
--vlm-modefrozen / lora / full VLM finetune mode
--vlm-lrSeparate learning rate for VLM parameters
--gcEnable gradient checkpointing

DDP (torchrun) + Fix VLM, Stage-0 only:

bash scripts/train.sh --backend ddp --gpus 0,1,2,3 --stage 0 \
--config pretrain \
--va-name apt_va --vla-name apt_vla

DeepSpeed ZeRO-2 + Full VLM, Stage-1 only:

bash scripts/train.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config pretrain \
--va-name apt_va --vla-name apt_vla_vlmft \
--ds-zero 2 --vlm-mode full --gc --vlm-lr 1e-5

Resume Stage-1 after preemption:

bash scripts/train.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config pretrain --vla-conti apt_vla_vlmft \
--ds-zero 2 --vlm-mode full --gc --vlm-lr 1e-5

Stage-1 internally calls VLA.load_from_pretrain(..., load_from_va=True), which doubles the Stage-1 attention layers and copies the Stage-0 weights into the odd indices while leaving the inserted (even-index) language-injection layers randomly initialized.

Task-specific Fine-tuning

scripts/finetune.sh mirrors train.sh but additionally exposes --pretrained-ckpt so you can bootstrap from any pretraining checkpoint. The Stage-1 launch automatically reuses the Stage-0 name (if any) or the pretraining checkpoint as the upstream.

# Stage-1 only, DeepSpeed ZeRO-2 + Full VLM on Pick-Place from a pretrained VLA checkpoint
bash scripts/finetune.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config finetune_pp --pretrained-ckpt apt_vla \
--vla-name ft_apt_pp \
--ds-zero 3 --vlm-mode lora --gc --accum 4 --bs 64 --vlm-lr 1e-5
# Stage-1 only, Fix VLM on real ALOHA data from a pretrained VLA checkpoint
bash scripts/finetune.sh --backend ddp --gpus 0,1,2,3 --stage 1 \
--config finetune_aloha_pp_storage --pretrained-ckpt apt_vla_vlmft \
--vla-name ft_apt_aloha

Checkpoints are written under ./checkpoints/APT/<name>/ and TensorBoard logs under ./logs/APT/<name>/. Both roots are configurable per run via the optional flags --ckpt_dir /path/to/ckpts and --log_dir /path/to/logs, e.g. to keep separate experiments on different volumes. Likewise --dataloader_timeout <seconds> (default 300) lets you raise the DataLoader worker timeout for slow shared storage.

Loading existing APT checkpoints

The merged trainer is backwards-compatible with checkpoints produced by the pre-refactor training scripts. A checkpoint is recognised by its file layout:

Saved byTop-level keys in ckpt_latest.pt
DDP (train_dist.py)weights, optimizer, scheduler, scaler, current_iters, ...
DeepSpeed (train_deepspeed.py)weights, vlm_weights (if VLM fine-tuned), no embedded optimizer

The merged apt.train accepts both, and you can also switch back-ends across resumes (e.g. resume a DeepSpeed-trained run under DDP). When a checkpoint's optimizer state cannot be re-loaded (e.g. param groups differ because --vlm-mode changed), the trainer logs a warning and continues with a fresh optimizer.

Pre-flight check - validate any existing checkpoint before launching training:

# Stage-0 VA checkpoint
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 0
# Same VA checkpoint used to bootstrap Stage-1
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 1 --load-from-va
# Stage-1 VLA checkpoint (already-trained policy)
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 1

Bootstrap a new fine-tuning run from an existing VLA checkpoint (most common):

bash scripts/finetune.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config finetune_pp \
--pretrained-ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt \
--vla-name ft_apt_pp --vlm-mode full

--pretrained-ckpt accepts either a checkpoint subdir name under ./checkpoints/APT/or an absolute path ending in .pt. The trainer starts with current_iters=0 so the new run gets a clean iteration counter, and saves under ./checkpoints/APT/<vla-name>/.

Inference

For local inference, instantiate the planner directly:

fromapt.infer.plannerimportTrajPlannerplanner=TrajPlanner(
ckpt_path="checkpoints/APT/ft_vla/ckpt_latest.pt",
device="cuda:0",
ensemble=4,
use_ema=False,
)
planner.set_prompt("Pick up the grape and place it on the pink box.")
planner.add_obs_frame(obs_frame)
actions=planner.get_action()

To serve the policy as a remote service (e.g. for hardware control), first launch a Pyro4 naming server, then start the service:

# 1. Naming server (defaults to localhost:9091)
pyro4-ns -p 9091
# 2. Inference service
python -m apt.infer.remote_service \
--ckpt checkpoints/APT/ft_vla/ckpt_latest.pt \
--uri apt_control \
--host localhost --port 0 \
--ensemble 4

The client side uses shm_transport (zero-copy shared memory + Pyro4) to call add_obs_frame, set_prompt, get_action, etc.

🧪 Evaluation on LIBERO benchmarks

A single evaluator under examples/libero/ drives all three LIBERO benchmark families against an APT policy server:

BenchmarkSimulatorSuitesDefault trials/task
LIBEROLifelong-Robot-Learning/LIBEROlibero_{object,spatial,goal,10} — 4 suites50
LIBERO-PROZxy-MLlab/LIBERO-PROthe 4 above × {_swap, _task} — 8 suites50
LIBERO-plussylvestf/LIBERO-pluslibero_{object,spatial,goal,10} — 4 suites1

Quick start (LIBERO conda env, after launching the APT policy server in a separate terminal):

bash examples/libero/test_libero.sh \
--benchmark libero \
--gpu 0 \
--model_name apt_vla \
--controller_name control --controller_port 9091

See examples/libero/README.md for the per-benchmark conda setup, the policy-server launch command, the full list of test_libero.sh flags, and the on-disk output layout.

🤖 Evaluation on Pick-and-Place in Isaac Sim

An Isaac-Sim-based pick-and-place benchmark lives under examples/PickPlace/. A UR5 + Robotiq 85 arm executes language-conditioned pick-and-place on a tabletop scene; the APT policy server is queried for actions each control step.

SettingDescription
soSeen object set, default lighting / ground
uoHeld-out object set, default lighting / ground
ucSame objects as so, but the target container is a held-out mug
uoueHeld-out objects + novel HDR background + randomized ground (seed sweep)

Two separate Python environments are involved: the APT env (running the policy server with shm_transport) and the Isaac Sim bundled Python (running the benchmark). shm_transport lives at the APT root and is picked up automatically by the driver via PYTHONPATH. Quick start:

# 1. APT env (separate terminal) — launch the policy server, see "Inference" above.# 2. Isaac Sim env — drive all 4 settings:cd examples/PickPlace
GPU_ID=0 URI=control PORT=9091 SAVE_DIR=./data/exp_results/myrun \
bash eval_all.sh

See examples/PickPlace/README.md for the asset preparation, full environment requirements, per-setting flags, and the on-disk output format (<save_dir>/<setting>/videos/*.mp4 + metrics/*.json).

📥 Pretrained Checkpoints

All checkpoints live under a single Hugging Face repo: KechunXu1/apt_models.

StageConfigDatasetsHugging Face
Pretrained VLA policypretrain (--load_from_va)Droid + AgiBotWorld + InternA1 + InternM1apt_vla
LIBERO fine-tunedfinetune_liberoLIBERO Spatial / Object / Goal / 10apt_vla_ftlibero
Pick-Place fine-tunedfinetune_ppPickPlaceCanapt_vla_ftpp

Download a checkpoint (e.g. the pretrained VLA policy):

hf download KechunXu1/apt_models --include "apt_vla/*" --local-dir ./checkpoints/APT

Then point the inference script at the downloaded checkpoint via --ckpt ./checkpoints/APT/apt_vla/ckpt_latest.pt (see the Inference section).

🤝 Acknowledgements

This project builds upon BayesVLA, and E2VLA. We thank these teams for their open-source contributions.

📚 Citation

If you find this work useful, please consider citing:

@article{xu2026apt,
title={APT: Action Expert Pretraining Improves Instruction Generalization of Vision-Language-Action Policies},
author={Xu, Kechun and Zhu, Zhenjie and Chen, Anzhe and Xiong, Rong and Wang, Yue},
journal={arXiv preprint arXiv:2606.12366},
year={2026}
}

About

[arXiv 2026] APT: Action Expert Pretraining Improves Instruction Generalization of Vision-Language-Action Policies

Resources

Stars

39 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

APT

APT: Action Expert Pretraining
Improves Instruction Generalization of Vision-Language-Action Policies

Kechun Xu · Zhenjie Zhu · Anzhe Chen · Rong Xiong · Yue Wang

Paper PDFProject Page

TL; DR: APT factorizes the VLA policy into a Vision-Action (VA) prior and a language-conditioned VLA likelihood, and pretrains the action expert as the VA prior on vision-action pairs from a frozen VLM. A layer-wise gated fusion mechanism then injects language tokens into the pretrained action expert, preserving the visuomotor prior while enabling instruction following. APT delivers consistent gains on OOD language instructions and compositional tasks.

🏆 Highlights

🔍 Key Findings: continuous-action VLA policies start from a randomly initialized action expert and learn from imbalanced VLA data, producing noisy gradients that corrupt the VLM backbone and collapse to visual shortcuts.

Key Insights:

  • Bayesian factorization of the VLA policy:

$$ \pi(\mathbf{a}\mid\mathbf{v},\ell)\ \propto\ \pi^{p}(\mathbf{a}\mid\mathbf{v})\cdot L(\ell\mid\mathbf{v},\mathbf{a}) $$

  • VA prior$\pi^p(\mathbf{a}\mid\mathbf{v})$ is trained on balanced vision-action pairs alone, so the action expert builds coherent visuomotor priors without any language shortcut.

  • VLA likelihood$L(\ell\mid\mathbf{v},\mathbf{a})$ then aligns the prior to language instructions, a much easier sub-problem than learning action generation and language grounding jointly.

  • Layer-wise gated fusion injects each Qwen3-VL intermediate feature into the corresponding action-expert self-attention layer through a learnable sigmoid gate, letting the action expert inherit VLM semantics without overwriting the pretrained visuomotor pathway.

  • Two-stage realization inside one network. Stage 1 activates only half of the action-expert attention layers and masks language tokens, training a pure VA prior. Stage 2 inserts an interleaved attention layer after each Stage-1 layer, unmasks language, and jointly trains the prior and likelihood under large-scale data.

  • Architecture-agnostic: the two-stage recipe also boosts $\pi$-style and GR00T-style architectures on OOD language generalization.

🧩 Overview

Given VLA datasets with modality imbalance, APT trains the policy in two stages:

  • Stage 1 - VA Prior Pretraining: the action expert is conditioned solely on visual tokens from a frozen Qwen3-VL backbone and learns $\pi^p(\mathbf{a}\mid\mathbf{v})$.
  • Stage 2 - VLA Likelihood Alignment: the Stage-1 layers are duplicated with interleaved language-injection layers; the full policy is jointly trained on the same data.

APT method overview

📁 Project Structure

APT/
├── apt/ # core model + unified trainer (this is the package)
│ ├── vla.py # VLA wrapper (VLM + ActionExpert)
│ ├── vlm.py # Qwen3-VL encoder bridge
│ ├── action_expert.py # diffusion-based action expert with gated fusion
│ ├── action_transform.py # SE(3) ↔ 10-dim action conversions
│ ├── configs.py # TrainConfig + CONFIGS registry
│ ├── train.py # unified DDP + DeepSpeed trainer
│ ├── ds_config_zero2.json # DeepSpeed ZeRO-2 config
│ ├── ds_config_zero3.json # DeepSpeed ZeRO-3 config
│ ├── encoders/ # Qwen3-VL (LoRA-capable) wrapper
│ ├── layers/ # attention / RoPE / norms / 6D-rotation utils
│ └── infer/ # planner + remote inference service
├── data_utils/ # HDF5 IO, datasets, video decoding, distributed samplers
├── train_utils/ # EMA implementation
├── infer_utils/ # trajectory ensembler and visualizer
├── shm_transport/ # Pyro4 + shared-memory RPC for remote inference
├── scripts/
│ ├── train.sh # two-stage pretraining (DDP or DeepSpeed)
│ └── finetune.sh # task-specific fine-tuning (DDP or DeepSpeed)
├── examples/
│ ├── libero/ # LIBERO / LIBERO-PRO / LIBERO-plus evaluator
│ └── PickPlace/ # Isaac Sim pick-and-place benchmark (UR5 + Robotiq 85)
├── assets/ # logo / method (PDF source + PNG for README), paper.pdf
├── requirements.txt
├── .gitignore
└── README.md

📘 Usage

Environment

conda create -n apt python=3.10 -y
conda activate apt
# Install a PyTorch build that matches your CUDA. The pinned xformers requires# torch 2.4.1; relax it if you use a different torch version.
pip install torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cu126
pip install -r requirements.txt

Note on xformers / deepspeed: both are version-sensitive. If you do not need DeepSpeed, you can skip installing it - --backend ddp works without it. Likewise drop xformers if you do not need its kernels.

Data Preparation

APT consumes trajectories stored as per-episode HDF5 files. Each sample yielded by the dataloader looks like:

{
"obs_rgbs": (To, ncam, 3, H, W), # observation frames"prompt_text": str, # task description"current_ee_pose": (nee, 4, 4), # current EE pose in world frame"gt_future_ee_states": (Ta, nee, 17), # ground truth pose + gripper"history_ee_states": (Th, nee, 17),
"obs_norm_xys": (...), # per-pixel normalized 2D coords"obs_extrinsics": (To, ncam, 4, 4),
"valid_ee_mask": (nee,),
... # see data_utils/dataset_base.py
}

Adding a new dataset:

  1. Subclass H5DatasetMapBase in data_utils/datasets.py.
  2. Register the dataset entry in data_utils/data_loc.py. The file looks up the host IP (get_ipv4_address) and selects the matching dictionary - edit it to point at your local paths before launching training.
  3. Reference the new class from a TrainConfig entry in apt/configs.py.

We expose a number of preset configs (see apt/configs.py for the full list). Examples:

ConfigPurpose
pretrainPretrain on Droid + AgiBotWorld + InternA1 + InternM1
finetune_aloha_pp_storageReal-world ALOHA pick-place + table-storage fine-tuning
debugTiny single-batch config used for smoke tests

Two-stage Pretraining

scripts/train.sh drives both stages and accepts either back-end. Common arguments:

FlagMeaning
--backendddp (torchrun) or deepspeed
--gpusComma-separated GPU IDs, e.g. 0,1,2,3
--stage0 (VA only), 1 (VLA only), or both
--configConfig name from apt/configs.py
--va-nameSave name for the Stage-0 checkpoint
--vla-nameSave name for the Stage-1 checkpoint
--va-contiResume Stage-0 from an existing checkpoint
--vla-contiResume Stage-1 from an existing checkpoint
--bs / --max-iterPer-GPU batch size / max iterations

DeepSpeed-only flags (ignored when --backend ddp):

FlagMeaning
--ds-zero 2|3ZeRO stage (selects ds_config_zero{2,3}.json)
--accum NGradient accumulation steps
--vlm-modefrozen / lora / full VLM finetune mode
--vlm-lrSeparate learning rate for VLM parameters
--gcEnable gradient checkpointing

DDP (torchrun) + Fix VLM, Stage-0 only:

bash scripts/train.sh --backend ddp --gpus 0,1,2,3 --stage 0 \
--config pretrain \
--va-name apt_va --vla-name apt_vla

DeepSpeed ZeRO-2 + Full VLM, Stage-1 only:

bash scripts/train.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config pretrain \
--va-name apt_va --vla-name apt_vla_vlmft \
--ds-zero 2 --vlm-mode full --gc --vlm-lr 1e-5

Resume Stage-1 after preemption:

bash scripts/train.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config pretrain --vla-conti apt_vla_vlmft \
--ds-zero 2 --vlm-mode full --gc --vlm-lr 1e-5

Stage-1 internally calls VLA.load_from_pretrain(..., load_from_va=True), which doubles the Stage-1 attention layers and copies the Stage-0 weights into the odd indices while leaving the inserted (even-index) language-injection layers randomly initialized.

Task-specific Fine-tuning

scripts/finetune.sh mirrors train.sh but additionally exposes --pretrained-ckpt so you can bootstrap from any pretraining checkpoint. The Stage-1 launch automatically reuses the Stage-0 name (if any) or the pretraining checkpoint as the upstream.

# Stage-1 only, DeepSpeed ZeRO-2 + Full VLM on Pick-Place from a pretrained VLA checkpoint
bash scripts/finetune.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config finetune_pp --pretrained-ckpt apt_vla \
--vla-name ft_apt_pp \
--ds-zero 3 --vlm-mode lora --gc --accum 4 --bs 64 --vlm-lr 1e-5
# Stage-1 only, Fix VLM on real ALOHA data from a pretrained VLA checkpoint
bash scripts/finetune.sh --backend ddp --gpus 0,1,2,3 --stage 1 \
--config finetune_aloha_pp_storage --pretrained-ckpt apt_vla_vlmft \
--vla-name ft_apt_aloha

Checkpoints are written under ./checkpoints/APT/<name>/ and TensorBoard logs under ./logs/APT/<name>/. Both roots are configurable per run via the optional flags --ckpt_dir /path/to/ckpts and --log_dir /path/to/logs, e.g. to keep separate experiments on different volumes. Likewise --dataloader_timeout <seconds> (default 300) lets you raise the DataLoader worker timeout for slow shared storage.

Loading existing APT checkpoints

The merged trainer is backwards-compatible with checkpoints produced by the pre-refactor training scripts. A checkpoint is recognised by its file layout:

Saved byTop-level keys in ckpt_latest.pt
DDP (train_dist.py)weights, optimizer, scheduler, scaler, current_iters, ...
DeepSpeed (train_deepspeed.py)weights, vlm_weights (if VLM fine-tuned), no embedded optimizer

The merged apt.train accepts both, and you can also switch back-ends across resumes (e.g. resume a DeepSpeed-trained run under DDP). When a checkpoint's optimizer state cannot be re-loaded (e.g. param groups differ because --vlm-mode changed), the trainer logs a warning and continues with a fresh optimizer.

Pre-flight check - validate any existing checkpoint before launching training:

# Stage-0 VA checkpoint
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 0
# Same VA checkpoint used to bootstrap Stage-1
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 1 --load-from-va
# Stage-1 VLA checkpoint (already-trained policy)
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 1

Bootstrap a new fine-tuning run from an existing VLA checkpoint (most common):

bash scripts/finetune.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config finetune_pp \
--pretrained-ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt \
--vla-name ft_apt_pp --vlm-mode full

--pretrained-ckpt accepts either a checkpoint subdir name under ./checkpoints/APT/or an absolute path ending in .pt. The trainer starts with current_iters=0 so the new run gets a clean iteration counter, and saves under ./checkpoints/APT/<vla-name>/.

Inference

For local inference, instantiate the planner directly:

fromapt.infer.plannerimportTrajPlannerplanner=TrajPlanner(
ckpt_path="checkpoints/APT/ft_vla/ckpt_latest.pt",
device="cuda:0",
ensemble=4,
use_ema=False,
)
planner.set_prompt("Pick up the grape and place it on the pink box.")
planner.add_obs_frame(obs_frame)
actions=planner.get_action()

To serve the policy as a remote service (e.g. for hardware control), first launch a Pyro4 naming server, then start the service:

# 1. Naming server (defaults to localhost:9091)
pyro4-ns -p 9091
# 2. Inference service
python -m apt.infer.remote_service \
--ckpt checkpoints/APT/ft_vla/ckpt_latest.pt \
--uri apt_control \
--host localhost --port 0 \
--ensemble 4

The client side uses shm_transport (zero-copy shared memory + Pyro4) to call add_obs_frame, set_prompt, get_action, etc.

🧪 Evaluation on LIBERO benchmarks

A single evaluator under examples/libero/ drives all three LIBERO benchmark families against an APT policy server:

BenchmarkSimulatorSuitesDefault trials/task
LIBEROLifelong-Robot-Learning/LIBEROlibero_{object,spatial,goal,10} — 4 suites50
LIBERO-PROZxy-MLlab/LIBERO-PROthe 4 above × {_swap, _task} — 8 suites50
LIBERO-plussylvestf/LIBERO-pluslibero_{object,spatial,goal,10} — 4 suites1

Quick start (LIBERO conda env, after launching the APT policy server in a separate terminal):

bash examples/libero/test_libero.sh \
--benchmark libero \
--gpu 0 \
--model_name apt_vla \
--controller_name control --controller_port 9091

See examples/libero/README.md for the per-benchmark conda setup, the policy-server launch command, the full list of test_libero.sh flags, and the on-disk output layout.

🤖 Evaluation on Pick-and-Place in Isaac Sim

An Isaac-Sim-based pick-and-place benchmark lives under examples/PickPlace/. A UR5 + Robotiq 85 arm executes language-conditioned pick-and-place on a tabletop scene; the APT policy server is queried for actions each control step.

SettingDescription
soSeen object set, default lighting / ground
uoHeld-out object set, default lighting / ground
ucSame objects as so, but the target container is a held-out mug
uoueHeld-out objects + novel HDR background + randomized ground (seed sweep)

Two separate Python environments are involved: the APT env (running the policy server with shm_transport) and the Isaac Sim bundled Python (running the benchmark). shm_transport lives at the APT root and is picked up automatically by the driver via PYTHONPATH. Quick start:

# 1. APT env (separate terminal) — launch the policy server, see "Inference" above.# 2. Isaac Sim env — drive all 4 settings:cd examples/PickPlace
GPU_ID=0 URI=control PORT=9091 SAVE_DIR=./data/exp_results/myrun \
bash eval_all.sh

See examples/PickPlace/README.md for the asset preparation, full environment requirements, per-setting flags, and the on-disk output format (<save_dir>/<setting>/videos/*.mp4 + metrics/*.json).

📥 Pretrained Checkpoints

All checkpoints live under a single Hugging Face repo: KechunXu1/apt_models.

StageConfigDatasetsHugging Face
Pretrained VLA policypretrain (--load_from_va)Droid + AgiBotWorld + InternA1 + InternM1apt_vla
LIBERO fine-tunedfinetune_liberoLIBERO Spatial / Object / Goal / 10apt_vla_ftlibero
Pick-Place fine-tunedfinetune_ppPickPlaceCanapt_vla_ftpp

Download a checkpoint (e.g. the pretrained VLA policy):

hf download KechunXu1/apt_models --include "apt_vla/*" --local-dir ./checkpoints/APT

Then point the inference script at the downloaded checkpoint via --ckpt ./checkpoints/APT/apt_vla/ckpt_latest.pt (see the Inference section).

🤝 Acknowledgements

This project builds upon BayesVLA, and E2VLA. We thank these teams for their open-source contributions.

📚 Citation

If you find this work useful, please consider citing:

@article{xu2026apt,
title={APT: Action Expert Pretraining Improves Instruction Generalization of Vision-Language-Action Policies},
author={Xu, Kechun and Zhu, Zhenjie and Chen, Anzhe and Xiong, Rong and Wang, Yue},
journal={arXiv preprint arXiv:2606.12366},
year={2026}
}

About

[arXiv 2026] APT: Action Expert Pretraining Improves Instruction Generalization of Vision-Language-Action Policies

Resources

Stars

39 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

APT

APT: Action Expert Pretraining
Improves Instruction Generalization of Vision-Language-Action Policies

Kechun Xu · Zhenjie Zhu · Anzhe Chen · Rong Xiong · Yue Wang

Paper PDFProject Page

TL; DR: APT factorizes the VLA policy into a Vision-Action (VA) prior and a language-conditioned VLA likelihood, and pretrains the action expert as the VA prior on vision-action pairs from a frozen VLM. A layer-wise gated fusion mechanism then injects language tokens into the pretrained action expert, preserving the visuomotor prior while enabling instruction following. APT delivers consistent gains on OOD language instructions and compositional tasks.

🏆 Highlights

🔍 Key Findings: continuous-action VLA policies start from a randomly initialized action expert and learn from imbalanced VLA data, producing noisy gradients that corrupt the VLM backbone and collapse to visual shortcuts.

Key Insights:

  • Bayesian factorization of the VLA policy:

$$ \pi(\mathbf{a}\mid\mathbf{v},\ell)\ \propto\ \pi^{p}(\mathbf{a}\mid\mathbf{v})\cdot L(\ell\mid\mathbf{v},\mathbf{a}) $$

  • VA prior$\pi^p(\mathbf{a}\mid\mathbf{v})$ is trained on balanced vision-action pairs alone, so the action expert builds coherent visuomotor priors without any language shortcut.

  • VLA likelihood$L(\ell\mid\mathbf{v},\mathbf{a})$ then aligns the prior to language instructions, a much easier sub-problem than learning action generation and language grounding jointly.

  • Layer-wise gated fusion injects each Qwen3-VL intermediate feature into the corresponding action-expert self-attention layer through a learnable sigmoid gate, letting the action expert inherit VLM semantics without overwriting the pretrained visuomotor pathway.

  • Two-stage realization inside one network. Stage 1 activates only half of the action-expert attention layers and masks language tokens, training a pure VA prior. Stage 2 inserts an interleaved attention layer after each Stage-1 layer, unmasks language, and jointly trains the prior and likelihood under large-scale data.

  • Architecture-agnostic: the two-stage recipe also boosts $\pi$-style and GR00T-style architectures on OOD language generalization.

🧩 Overview

Given VLA datasets with modality imbalance, APT trains the policy in two stages:

  • Stage 1 - VA Prior Pretraining: the action expert is conditioned solely on visual tokens from a frozen Qwen3-VL backbone and learns $\pi^p(\mathbf{a}\mid\mathbf{v})$.
  • Stage 2 - VLA Likelihood Alignment: the Stage-1 layers are duplicated with interleaved language-injection layers; the full policy is jointly trained on the same data.

APT method overview

📁 Project Structure

APT/
├── apt/ # core model + unified trainer (this is the package)
│ ├── vla.py # VLA wrapper (VLM + ActionExpert)
│ ├── vlm.py # Qwen3-VL encoder bridge
│ ├── action_expert.py # diffusion-based action expert with gated fusion
│ ├── action_transform.py # SE(3) ↔ 10-dim action conversions
│ ├── configs.py # TrainConfig + CONFIGS registry
│ ├── train.py # unified DDP + DeepSpeed trainer
│ ├── ds_config_zero2.json # DeepSpeed ZeRO-2 config
│ ├── ds_config_zero3.json # DeepSpeed ZeRO-3 config
│ ├── encoders/ # Qwen3-VL (LoRA-capable) wrapper
│ ├── layers/ # attention / RoPE / norms / 6D-rotation utils
│ └── infer/ # planner + remote inference service
├── data_utils/ # HDF5 IO, datasets, video decoding, distributed samplers
├── train_utils/ # EMA implementation
├── infer_utils/ # trajectory ensembler and visualizer
├── shm_transport/ # Pyro4 + shared-memory RPC for remote inference
├── scripts/
│ ├── train.sh # two-stage pretraining (DDP or DeepSpeed)
│ └── finetune.sh # task-specific fine-tuning (DDP or DeepSpeed)
├── examples/
│ ├── libero/ # LIBERO / LIBERO-PRO / LIBERO-plus evaluator
│ └── PickPlace/ # Isaac Sim pick-and-place benchmark (UR5 + Robotiq 85)
├── assets/ # logo / method (PDF source + PNG for README), paper.pdf
├── requirements.txt
├── .gitignore
└── README.md

📘 Usage

Environment

conda create -n apt python=3.10 -y
conda activate apt
# Install a PyTorch build that matches your CUDA. The pinned xformers requires# torch 2.4.1; relax it if you use a different torch version.
pip install torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cu126
pip install -r requirements.txt

Note on xformers / deepspeed: both are version-sensitive. If you do not need DeepSpeed, you can skip installing it - --backend ddp works without it. Likewise drop xformers if you do not need its kernels.

Data Preparation

APT consumes trajectories stored as per-episode HDF5 files. Each sample yielded by the dataloader looks like:

{
"obs_rgbs": (To, ncam, 3, H, W), # observation frames"prompt_text": str, # task description"current_ee_pose": (nee, 4, 4), # current EE pose in world frame"gt_future_ee_states": (Ta, nee, 17), # ground truth pose + gripper"history_ee_states": (Th, nee, 17),
"obs_norm_xys": (...), # per-pixel normalized 2D coords"obs_extrinsics": (To, ncam, 4, 4),
"valid_ee_mask": (nee,),
... # see data_utils/dataset_base.py
}

Adding a new dataset:

  1. Subclass H5DatasetMapBase in data_utils/datasets.py.
  2. Register the dataset entry in data_utils/data_loc.py. The file looks up the host IP (get_ipv4_address) and selects the matching dictionary - edit it to point at your local paths before launching training.
  3. Reference the new class from a TrainConfig entry in apt/configs.py.

We expose a number of preset configs (see apt/configs.py for the full list). Examples:

ConfigPurpose
pretrainPretrain on Droid + AgiBotWorld + InternA1 + InternM1
finetune_aloha_pp_storageReal-world ALOHA pick-place + table-storage fine-tuning
debugTiny single-batch config used for smoke tests

Two-stage Pretraining

scripts/train.sh drives both stages and accepts either back-end. Common arguments:

FlagMeaning
--backendddp (torchrun) or deepspeed
--gpusComma-separated GPU IDs, e.g. 0,1,2,3
--stage0 (VA only), 1 (VLA only), or both
--configConfig name from apt/configs.py
--va-nameSave name for the Stage-0 checkpoint
--vla-nameSave name for the Stage-1 checkpoint
--va-contiResume Stage-0 from an existing checkpoint
--vla-contiResume Stage-1 from an existing checkpoint
--bs / --max-iterPer-GPU batch size / max iterations

DeepSpeed-only flags (ignored when --backend ddp):

FlagMeaning
--ds-zero 2|3ZeRO stage (selects ds_config_zero{2,3}.json)
--accum NGradient accumulation steps
--vlm-modefrozen / lora / full VLM finetune mode
--vlm-lrSeparate learning rate for VLM parameters
--gcEnable gradient checkpointing

DDP (torchrun) + Fix VLM, Stage-0 only:

bash scripts/train.sh --backend ddp --gpus 0,1,2,3 --stage 0 \
--config pretrain \
--va-name apt_va --vla-name apt_vla

DeepSpeed ZeRO-2 + Full VLM, Stage-1 only:

bash scripts/train.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config pretrain \
--va-name apt_va --vla-name apt_vla_vlmft \
--ds-zero 2 --vlm-mode full --gc --vlm-lr 1e-5

Resume Stage-1 after preemption:

bash scripts/train.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config pretrain --vla-conti apt_vla_vlmft \
--ds-zero 2 --vlm-mode full --gc --vlm-lr 1e-5

Stage-1 internally calls VLA.load_from_pretrain(..., load_from_va=True), which doubles the Stage-1 attention layers and copies the Stage-0 weights into the odd indices while leaving the inserted (even-index) language-injection layers randomly initialized.

Task-specific Fine-tuning

scripts/finetune.sh mirrors train.sh but additionally exposes --pretrained-ckpt so you can bootstrap from any pretraining checkpoint. The Stage-1 launch automatically reuses the Stage-0 name (if any) or the pretraining checkpoint as the upstream.

# Stage-1 only, DeepSpeed ZeRO-2 + Full VLM on Pick-Place from a pretrained VLA checkpoint
bash scripts/finetune.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config finetune_pp --pretrained-ckpt apt_vla \
--vla-name ft_apt_pp \
--ds-zero 3 --vlm-mode lora --gc --accum 4 --bs 64 --vlm-lr 1e-5
# Stage-1 only, Fix VLM on real ALOHA data from a pretrained VLA checkpoint
bash scripts/finetune.sh --backend ddp --gpus 0,1,2,3 --stage 1 \
--config finetune_aloha_pp_storage --pretrained-ckpt apt_vla_vlmft \
--vla-name ft_apt_aloha

Checkpoints are written under ./checkpoints/APT/<name>/ and TensorBoard logs under ./logs/APT/<name>/. Both roots are configurable per run via the optional flags --ckpt_dir /path/to/ckpts and --log_dir /path/to/logs, e.g. to keep separate experiments on different volumes. Likewise --dataloader_timeout <seconds> (default 300) lets you raise the DataLoader worker timeout for slow shared storage.

Loading existing APT checkpoints

The merged trainer is backwards-compatible with checkpoints produced by the pre-refactor training scripts. A checkpoint is recognised by its file layout:

Saved byTop-level keys in ckpt_latest.pt
DDP (train_dist.py)weights, optimizer, scheduler, scaler, current_iters, ...
DeepSpeed (train_deepspeed.py)weights, vlm_weights (if VLM fine-tuned), no embedded optimizer

The merged apt.train accepts both, and you can also switch back-ends across resumes (e.g. resume a DeepSpeed-trained run under DDP). When a checkpoint's optimizer state cannot be re-loaded (e.g. param groups differ because --vlm-mode changed), the trainer logs a warning and continues with a fresh optimizer.

Pre-flight check - validate any existing checkpoint before launching training:

# Stage-0 VA checkpoint
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 0
# Same VA checkpoint used to bootstrap Stage-1
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 1 --load-from-va
# Stage-1 VLA checkpoint (already-trained policy)
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 1

Bootstrap a new fine-tuning run from an existing VLA checkpoint (most common):

bash scripts/finetune.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config finetune_pp \
--pretrained-ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt \
--vla-name ft_apt_pp --vlm-mode full

--pretrained-ckpt accepts either a checkpoint subdir name under ./checkpoints/APT/or an absolute path ending in .pt. The trainer starts with current_iters=0 so the new run gets a clean iteration counter, and saves under ./checkpoints/APT/<vla-name>/.

Inference

For local inference, instantiate the planner directly:

fromapt.infer.plannerimportTrajPlannerplanner=TrajPlanner(
ckpt_path="checkpoints/APT/ft_vla/ckpt_latest.pt",
device="cuda:0",
ensemble=4,
use_ema=False,
)
planner.set_prompt("Pick up the grape and place it on the pink box.")
planner.add_obs_frame(obs_frame)
actions=planner.get_action()

To serve the policy as a remote service (e.g. for hardware control), first launch a Pyro4 naming server, then start the service:

# 1. Naming server (defaults to localhost:9091)
pyro4-ns -p 9091
# 2. Inference service
python -m apt.infer.remote_service \
--ckpt checkpoints/APT/ft_vla/ckpt_latest.pt \
--uri apt_control \
--host localhost --port 0 \
--ensemble 4

The client side uses shm_transport (zero-copy shared memory + Pyro4) to call add_obs_frame, set_prompt, get_action, etc.

🧪 Evaluation on LIBERO benchmarks

A single evaluator under examples/libero/ drives all three LIBERO benchmark families against an APT policy server:

BenchmarkSimulatorSuitesDefault trials/task
LIBEROLifelong-Robot-Learning/LIBEROlibero_{object,spatial,goal,10} — 4 suites50
LIBERO-PROZxy-MLlab/LIBERO-PROthe 4 above × {_swap, _task} — 8 suites50
LIBERO-plussylvestf/LIBERO-pluslibero_{object,spatial,goal,10} — 4 suites1

Quick start (LIBERO conda env, after launching the APT policy server in a separate terminal):

bash examples/libero/test_libero.sh \
--benchmark libero \
--gpu 0 \
--model_name apt_vla \
--controller_name control --controller_port 9091

See examples/libero/README.md for the per-benchmark conda setup, the policy-server launch command, the full list of test_libero.sh flags, and the on-disk output layout.

🤖 Evaluation on Pick-and-Place in Isaac Sim

An Isaac-Sim-based pick-and-place benchmark lives under examples/PickPlace/. A UR5 + Robotiq 85 arm executes language-conditioned pick-and-place on a tabletop scene; the APT policy server is queried for actions each control step.

SettingDescription
soSeen object set, default lighting / ground
uoHeld-out object set, default lighting / ground
ucSame objects as so, but the target container is a held-out mug
uoueHeld-out objects + novel HDR background + randomized ground (seed sweep)

Two separate Python environments are involved: the APT env (running the policy server with shm_transport) and the Isaac Sim bundled Python (running the benchmark). shm_transport lives at the APT root and is picked up automatically by the driver via PYTHONPATH. Quick start:

# 1. APT env (separate terminal) — launch the policy server, see "Inference" above.# 2. Isaac Sim env — drive all 4 settings:cd examples/PickPlace
GPU_ID=0 URI=control PORT=9091 SAVE_DIR=./data/exp_results/myrun \
bash eval_all.sh

See examples/PickPlace/README.md for the asset preparation, full environment requirements, per-setting flags, and the on-disk output format (<save_dir>/<setting>/videos/*.mp4 + metrics/*.json).

📥 Pretrained Checkpoints

All checkpoints live under a single Hugging Face repo: KechunXu1/apt_models.

StageConfigDatasetsHugging Face
Pretrained VLA policypretrain (--load_from_va)Droid + AgiBotWorld + InternA1 + InternM1apt_vla
LIBERO fine-tunedfinetune_liberoLIBERO Spatial / Object / Goal / 10apt_vla_ftlibero
Pick-Place fine-tunedfinetune_ppPickPlaceCanapt_vla_ftpp

Download a checkpoint (e.g. the pretrained VLA policy):

hf download KechunXu1/apt_models --include "apt_vla/*" --local-dir ./checkpoints/APT

Then point the inference script at the downloaded checkpoint via --ckpt ./checkpoints/APT/apt_vla/ckpt_latest.pt (see the Inference section).

🤝 Acknowledgements

This project builds upon BayesVLA, and E2VLA. We thank these teams for their open-source contributions.

📚 Citation

If you find this work useful, please consider citing:

@article{xu2026apt,
title={APT: Action Expert Pretraining Improves Instruction Generalization of Vision-Language-Action Policies},
author={Xu, Kechun and Zhu, Zhenjie and Chen, Anzhe and Xiong, Rong and Wang, Yue},
journal={arXiv preprint arXiv:2606.12366},
year={2026}
}

About

[arXiv 2026] APT: Action Expert Pretraining Improves Instruction Generalization of Vision-Language-Action Policies

Resources

Stars

39 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

APT

APT: Action Expert Pretraining
Improves Instruction Generalization of Vision-Language-Action Policies

Kechun Xu · Zhenjie Zhu · Anzhe Chen · Rong Xiong · Yue Wang

Paper PDFProject Page

TL; DR: APT factorizes the VLA policy into a Vision-Action (VA) prior and a language-conditioned VLA likelihood, and pretrains the action expert as the VA prior on vision-action pairs from a frozen VLM. A layer-wise gated fusion mechanism then injects language tokens into the pretrained action expert, preserving the visuomotor prior while enabling instruction following. APT delivers consistent gains on OOD language instructions and compositional tasks.

🏆 Highlights

🔍 Key Findings: continuous-action VLA policies start from a randomly initialized action expert and learn from imbalanced VLA data, producing noisy gradients that corrupt the VLM backbone and collapse to visual shortcuts.

Key Insights:

  • Bayesian factorization of the VLA policy:

$$ \pi(\mathbf{a}\mid\mathbf{v},\ell)\ \propto\ \pi^{p}(\mathbf{a}\mid\mathbf{v})\cdot L(\ell\mid\mathbf{v},\mathbf{a}) $$

  • VA prior$\pi^p(\mathbf{a}\mid\mathbf{v})$ is trained on balanced vision-action pairs alone, so the action expert builds coherent visuomotor priors without any language shortcut.

  • VLA likelihood$L(\ell\mid\mathbf{v},\mathbf{a})$ then aligns the prior to language instructions, a much easier sub-problem than learning action generation and language grounding jointly.

  • Layer-wise gated fusion injects each Qwen3-VL intermediate feature into the corresponding action-expert self-attention layer through a learnable sigmoid gate, letting the action expert inherit VLM semantics without overwriting the pretrained visuomotor pathway.

  • Two-stage realization inside one network. Stage 1 activates only half of the action-expert attention layers and masks language tokens, training a pure VA prior. Stage 2 inserts an interleaved attention layer after each Stage-1 layer, unmasks language, and jointly trains the prior and likelihood under large-scale data.

  • Architecture-agnostic: the two-stage recipe also boosts $\pi$-style and GR00T-style architectures on OOD language generalization.

🧩 Overview

Given VLA datasets with modality imbalance, APT trains the policy in two stages:

  • Stage 1 - VA Prior Pretraining: the action expert is conditioned solely on visual tokens from a frozen Qwen3-VL backbone and learns $\pi^p(\mathbf{a}\mid\mathbf{v})$.
  • Stage 2 - VLA Likelihood Alignment: the Stage-1 layers are duplicated with interleaved language-injection layers; the full policy is jointly trained on the same data.

APT method overview

📁 Project Structure

APT/
├── apt/ # core model + unified trainer (this is the package)
│ ├── vla.py # VLA wrapper (VLM + ActionExpert)
│ ├── vlm.py # Qwen3-VL encoder bridge
│ ├── action_expert.py # diffusion-based action expert with gated fusion
│ ├── action_transform.py # SE(3) ↔ 10-dim action conversions
│ ├── configs.py # TrainConfig + CONFIGS registry
│ ├── train.py # unified DDP + DeepSpeed trainer
│ ├── ds_config_zero2.json # DeepSpeed ZeRO-2 config
│ ├── ds_config_zero3.json # DeepSpeed ZeRO-3 config
│ ├── encoders/ # Qwen3-VL (LoRA-capable) wrapper
│ ├── layers/ # attention / RoPE / norms / 6D-rotation utils
│ └── infer/ # planner + remote inference service
├── data_utils/ # HDF5 IO, datasets, video decoding, distributed samplers
├── train_utils/ # EMA implementation
├── infer_utils/ # trajectory ensembler and visualizer
├── shm_transport/ # Pyro4 + shared-memory RPC for remote inference
├── scripts/
│ ├── train.sh # two-stage pretraining (DDP or DeepSpeed)
│ └── finetune.sh # task-specific fine-tuning (DDP or DeepSpeed)
├── examples/
│ ├── libero/ # LIBERO / LIBERO-PRO / LIBERO-plus evaluator
│ └── PickPlace/ # Isaac Sim pick-and-place benchmark (UR5 + Robotiq 85)
├── assets/ # logo / method (PDF source + PNG for README), paper.pdf
├── requirements.txt
├── .gitignore
└── README.md

📘 Usage

Environment

conda create -n apt python=3.10 -y
conda activate apt
# Install a PyTorch build that matches your CUDA. The pinned xformers requires# torch 2.4.1; relax it if you use a different torch version.
pip install torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cu126
pip install -r requirements.txt

Note on xformers / deepspeed: both are version-sensitive. If you do not need DeepSpeed, you can skip installing it - --backend ddp works without it. Likewise drop xformers if you do not need its kernels.

Data Preparation

APT consumes trajectories stored as per-episode HDF5 files. Each sample yielded by the dataloader looks like:

{
"obs_rgbs": (To, ncam, 3, H, W), # observation frames"prompt_text": str, # task description"current_ee_pose": (nee, 4, 4), # current EE pose in world frame"gt_future_ee_states": (Ta, nee, 17), # ground truth pose + gripper"history_ee_states": (Th, nee, 17),
"obs_norm_xys": (...), # per-pixel normalized 2D coords"obs_extrinsics": (To, ncam, 4, 4),
"valid_ee_mask": (nee,),
... # see data_utils/dataset_base.py
}

Adding a new dataset:

  1. Subclass H5DatasetMapBase in data_utils/datasets.py.
  2. Register the dataset entry in data_utils/data_loc.py. The file looks up the host IP (get_ipv4_address) and selects the matching dictionary - edit it to point at your local paths before launching training.
  3. Reference the new class from a TrainConfig entry in apt/configs.py.

We expose a number of preset configs (see apt/configs.py for the full list). Examples:

ConfigPurpose
pretrainPretrain on Droid + AgiBotWorld + InternA1 + InternM1
finetune_aloha_pp_storageReal-world ALOHA pick-place + table-storage fine-tuning
debugTiny single-batch config used for smoke tests

Two-stage Pretraining

scripts/train.sh drives both stages and accepts either back-end. Common arguments:

FlagMeaning
--backendddp (torchrun) or deepspeed
--gpusComma-separated GPU IDs, e.g. 0,1,2,3
--stage0 (VA only), 1 (VLA only), or both
--configConfig name from apt/configs.py
--va-nameSave name for the Stage-0 checkpoint
--vla-nameSave name for the Stage-1 checkpoint
--va-contiResume Stage-0 from an existing checkpoint
--vla-contiResume Stage-1 from an existing checkpoint
--bs / --max-iterPer-GPU batch size / max iterations

DeepSpeed-only flags (ignored when --backend ddp):

FlagMeaning
--ds-zero 2|3ZeRO stage (selects ds_config_zero{2,3}.json)
--accum NGradient accumulation steps
--vlm-modefrozen / lora / full VLM finetune mode
--vlm-lrSeparate learning rate for VLM parameters
--gcEnable gradient checkpointing

DDP (torchrun) + Fix VLM, Stage-0 only:

bash scripts/train.sh --backend ddp --gpus 0,1,2,3 --stage 0 \
--config pretrain \
--va-name apt_va --vla-name apt_vla

DeepSpeed ZeRO-2 + Full VLM, Stage-1 only:

bash scripts/train.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config pretrain \
--va-name apt_va --vla-name apt_vla_vlmft \
--ds-zero 2 --vlm-mode full --gc --vlm-lr 1e-5

Resume Stage-1 after preemption:

bash scripts/train.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config pretrain --vla-conti apt_vla_vlmft \
--ds-zero 2 --vlm-mode full --gc --vlm-lr 1e-5

Stage-1 internally calls VLA.load_from_pretrain(..., load_from_va=True), which doubles the Stage-1 attention layers and copies the Stage-0 weights into the odd indices while leaving the inserted (even-index) language-injection layers randomly initialized.

Task-specific Fine-tuning

scripts/finetune.sh mirrors train.sh but additionally exposes --pretrained-ckpt so you can bootstrap from any pretraining checkpoint. The Stage-1 launch automatically reuses the Stage-0 name (if any) or the pretraining checkpoint as the upstream.

# Stage-1 only, DeepSpeed ZeRO-2 + Full VLM on Pick-Place from a pretrained VLA checkpoint
bash scripts/finetune.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config finetune_pp --pretrained-ckpt apt_vla \
--vla-name ft_apt_pp \
--ds-zero 3 --vlm-mode lora --gc --accum 4 --bs 64 --vlm-lr 1e-5
# Stage-1 only, Fix VLM on real ALOHA data from a pretrained VLA checkpoint
bash scripts/finetune.sh --backend ddp --gpus 0,1,2,3 --stage 1 \
--config finetune_aloha_pp_storage --pretrained-ckpt apt_vla_vlmft \
--vla-name ft_apt_aloha

Checkpoints are written under ./checkpoints/APT/<name>/ and TensorBoard logs under ./logs/APT/<name>/. Both roots are configurable per run via the optional flags --ckpt_dir /path/to/ckpts and --log_dir /path/to/logs, e.g. to keep separate experiments on different volumes. Likewise --dataloader_timeout <seconds> (default 300) lets you raise the DataLoader worker timeout for slow shared storage.

Loading existing APT checkpoints

The merged trainer is backwards-compatible with checkpoints produced by the pre-refactor training scripts. A checkpoint is recognised by its file layout:

Saved byTop-level keys in ckpt_latest.pt
DDP (train_dist.py)weights, optimizer, scheduler, scaler, current_iters, ...
DeepSpeed (train_deepspeed.py)weights, vlm_weights (if VLM fine-tuned), no embedded optimizer

The merged apt.train accepts both, and you can also switch back-ends across resumes (e.g. resume a DeepSpeed-trained run under DDP). When a checkpoint's optimizer state cannot be re-loaded (e.g. param groups differ because --vlm-mode changed), the trainer logs a warning and continues with a fresh optimizer.

Pre-flight check - validate any existing checkpoint before launching training:

# Stage-0 VA checkpoint
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 0
# Same VA checkpoint used to bootstrap Stage-1
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 1 --load-from-va
# Stage-1 VLA checkpoint (already-trained policy)
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 1

Bootstrap a new fine-tuning run from an existing VLA checkpoint (most common):

bash scripts/finetune.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config finetune_pp \
--pretrained-ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt \
--vla-name ft_apt_pp --vlm-mode full

--pretrained-ckpt accepts either a checkpoint subdir name under ./checkpoints/APT/or an absolute path ending in .pt. The trainer starts with current_iters=0 so the new run gets a clean iteration counter, and saves under ./checkpoints/APT/<vla-name>/.

Inference

For local inference, instantiate the planner directly:

fromapt.infer.plannerimportTrajPlannerplanner=TrajPlanner(
ckpt_path="checkpoints/APT/ft_vla/ckpt_latest.pt",
device="cuda:0",
ensemble=4,
use_ema=False,
)
planner.set_prompt("Pick up the grape and place it on the pink box.")
planner.add_obs_frame(obs_frame)
actions=planner.get_action()

To serve the policy as a remote service (e.g. for hardware control), first launch a Pyro4 naming server, then start the service:

# 1. Naming server (defaults to localhost:9091)
pyro4-ns -p 9091
# 2. Inference service
python -m apt.infer.remote_service \
--ckpt checkpoints/APT/ft_vla/ckpt_latest.pt \
--uri apt_control \
--host localhost --port 0 \
--ensemble 4

The client side uses shm_transport (zero-copy shared memory + Pyro4) to call add_obs_frame, set_prompt, get_action, etc.

🧪 Evaluation on LIBERO benchmarks

A single evaluator under examples/libero/ drives all three LIBERO benchmark families against an APT policy server:

BenchmarkSimulatorSuitesDefault trials/task
LIBEROLifelong-Robot-Learning/LIBEROlibero_{object,spatial,goal,10} — 4 suites50
LIBERO-PROZxy-MLlab/LIBERO-PROthe 4 above × {_swap, _task} — 8 suites50
LIBERO-plussylvestf/LIBERO-pluslibero_{object,spatial,goal,10} — 4 suites1

Quick start (LIBERO conda env, after launching the APT policy server in a separate terminal):

bash examples/libero/test_libero.sh \
--benchmark libero \
--gpu 0 \
--model_name apt_vla \
--controller_name control --controller_port 9091

See examples/libero/README.md for the per-benchmark conda setup, the policy-server launch command, the full list of test_libero.sh flags, and the on-disk output layout.

🤖 Evaluation on Pick-and-Place in Isaac Sim

An Isaac-Sim-based pick-and-place benchmark lives under examples/PickPlace/. A UR5 + Robotiq 85 arm executes language-conditioned pick-and-place on a tabletop scene; the APT policy server is queried for actions each control step.

SettingDescription
soSeen object set, default lighting / ground
uoHeld-out object set, default lighting / ground
ucSame objects as so, but the target container is a held-out mug
uoueHeld-out objects + novel HDR background + randomized ground (seed sweep)

Two separate Python environments are involved: the APT env (running the policy server with shm_transport) and the Isaac Sim bundled Python (running the benchmark). shm_transport lives at the APT root and is picked up automatically by the driver via PYTHONPATH. Quick start:

# 1. APT env (separate terminal) — launch the policy server, see "Inference" above.# 2. Isaac Sim env — drive all 4 settings:cd examples/PickPlace
GPU_ID=0 URI=control PORT=9091 SAVE_DIR=./data/exp_results/myrun \
bash eval_all.sh

See examples/PickPlace/README.md for the asset preparation, full environment requirements, per-setting flags, and the on-disk output format (<save_dir>/<setting>/videos/*.mp4 + metrics/*.json).

📥 Pretrained Checkpoints

All checkpoints live under a single Hugging Face repo: KechunXu1/apt_models.

StageConfigDatasetsHugging Face
Pretrained VLA policypretrain (--load_from_va)Droid + AgiBotWorld + InternA1 + InternM1apt_vla
LIBERO fine-tunedfinetune_liberoLIBERO Spatial / Object / Goal / 10apt_vla_ftlibero
Pick-Place fine-tunedfinetune_ppPickPlaceCanapt_vla_ftpp

Download a checkpoint (e.g. the pretrained VLA policy):

hf download KechunXu1/apt_models --include "apt_vla/*" --local-dir ./checkpoints/APT

Then point the inference script at the downloaded checkpoint via --ckpt ./checkpoints/APT/apt_vla/ckpt_latest.pt (see the Inference section).

🤝 Acknowledgements

This project builds upon BayesVLA, and E2VLA. We thank these teams for their open-source contributions.

📚 Citation

If you find this work useful, please consider citing:

@article{xu2026apt,
title={APT: Action Expert Pretraining Improves Instruction Generalization of Vision-Language-Action Policies},
author={Xu, Kechun and Zhu, Zhenjie and Chen, Anzhe and Xiong, Rong and Wang, Yue},
journal={arXiv preprint arXiv:2606.12366},
year={2026}
}

About

[arXiv 2026] APT: Action Expert Pretraining Improves Instruction Generalization of Vision-Language-Action Policies

Resources

Stars

39 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

APT

APT: Action Expert Pretraining
Improves Instruction Generalization of Vision-Language-Action Policies

Kechun Xu · Zhenjie Zhu · Anzhe Chen · Rong Xiong · Yue Wang

Paper PDFProject Page

TL; DR: APT factorizes the VLA policy into a Vision-Action (VA) prior and a language-conditioned VLA likelihood, and pretrains the action expert as the VA prior on vision-action pairs from a frozen VLM. A layer-wise gated fusion mechanism then injects language tokens into the pretrained action expert, preserving the visuomotor prior while enabling instruction following. APT delivers consistent gains on OOD language instructions and compositional tasks.

🏆 Highlights

🔍 Key Findings: continuous-action VLA policies start from a randomly initialized action expert and learn from imbalanced VLA data, producing noisy gradients that corrupt the VLM backbone and collapse to visual shortcuts.

Key Insights:

  • Bayesian factorization of the VLA policy:

$$ \pi(\mathbf{a}\mid\mathbf{v},\ell)\ \propto\ \pi^{p}(\mathbf{a}\mid\mathbf{v})\cdot L(\ell\mid\mathbf{v},\mathbf{a}) $$

  • VA prior$\pi^p(\mathbf{a}\mid\mathbf{v})$ is trained on balanced vision-action pairs alone, so the action expert builds coherent visuomotor priors without any language shortcut.

  • VLA likelihood$L(\ell\mid\mathbf{v},\mathbf{a})$ then aligns the prior to language instructions, a much easier sub-problem than learning action generation and language grounding jointly.

  • Layer-wise gated fusion injects each Qwen3-VL intermediate feature into the corresponding action-expert self-attention layer through a learnable sigmoid gate, letting the action expert inherit VLM semantics without overwriting the pretrained visuomotor pathway.

  • Two-stage realization inside one network. Stage 1 activates only half of the action-expert attention layers and masks language tokens, training a pure VA prior. Stage 2 inserts an interleaved attention layer after each Stage-1 layer, unmasks language, and jointly trains the prior and likelihood under large-scale data.

  • Architecture-agnostic: the two-stage recipe also boosts $\pi$-style and GR00T-style architectures on OOD language generalization.

🧩 Overview

Given VLA datasets with modality imbalance, APT trains the policy in two stages:

  • Stage 1 - VA Prior Pretraining: the action expert is conditioned solely on visual tokens from a frozen Qwen3-VL backbone and learns $\pi^p(\mathbf{a}\mid\mathbf{v})$.
  • Stage 2 - VLA Likelihood Alignment: the Stage-1 layers are duplicated with interleaved language-injection layers; the full policy is jointly trained on the same data.

APT method overview

📁 Project Structure

APT/
├── apt/ # core model + unified trainer (this is the package)
│ ├── vla.py # VLA wrapper (VLM + ActionExpert)
│ ├── vlm.py # Qwen3-VL encoder bridge
│ ├── action_expert.py # diffusion-based action expert with gated fusion
│ ├── action_transform.py # SE(3) ↔ 10-dim action conversions
│ ├── configs.py # TrainConfig + CONFIGS registry
│ ├── train.py # unified DDP + DeepSpeed trainer
│ ├── ds_config_zero2.json # DeepSpeed ZeRO-2 config
│ ├── ds_config_zero3.json # DeepSpeed ZeRO-3 config
│ ├── encoders/ # Qwen3-VL (LoRA-capable) wrapper
│ ├── layers/ # attention / RoPE / norms / 6D-rotation utils
│ └── infer/ # planner + remote inference service
├── data_utils/ # HDF5 IO, datasets, video decoding, distributed samplers
├── train_utils/ # EMA implementation
├── infer_utils/ # trajectory ensembler and visualizer
├── shm_transport/ # Pyro4 + shared-memory RPC for remote inference
├── scripts/
│ ├── train.sh # two-stage pretraining (DDP or DeepSpeed)
│ └── finetune.sh # task-specific fine-tuning (DDP or DeepSpeed)
├── examples/
│ ├── libero/ # LIBERO / LIBERO-PRO / LIBERO-plus evaluator
│ └── PickPlace/ # Isaac Sim pick-and-place benchmark (UR5 + Robotiq 85)
├── assets/ # logo / method (PDF source + PNG for README), paper.pdf
├── requirements.txt
├── .gitignore
└── README.md

📘 Usage

Environment

conda create -n apt python=3.10 -y
conda activate apt
# Install a PyTorch build that matches your CUDA. The pinned xformers requires# torch 2.4.1; relax it if you use a different torch version.
pip install torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cu126
pip install -r requirements.txt

Note on xformers / deepspeed: both are version-sensitive. If you do not need DeepSpeed, you can skip installing it - --backend ddp works without it. Likewise drop xformers if you do not need its kernels.

Data Preparation

APT consumes trajectories stored as per-episode HDF5 files. Each sample yielded by the dataloader looks like:

{
"obs_rgbs": (To, ncam, 3, H, W), # observation frames"prompt_text": str, # task description"current_ee_pose": (nee, 4, 4), # current EE pose in world frame"gt_future_ee_states": (Ta, nee, 17), # ground truth pose + gripper"history_ee_states": (Th, nee, 17),
"obs_norm_xys": (...), # per-pixel normalized 2D coords"obs_extrinsics": (To, ncam, 4, 4),
"valid_ee_mask": (nee,),
... # see data_utils/dataset_base.py
}

Adding a new dataset:

  1. Subclass H5DatasetMapBase in data_utils/datasets.py.
  2. Register the dataset entry in data_utils/data_loc.py. The file looks up the host IP (get_ipv4_address) and selects the matching dictionary - edit it to point at your local paths before launching training.
  3. Reference the new class from a TrainConfig entry in apt/configs.py.

We expose a number of preset configs (see apt/configs.py for the full list). Examples:

ConfigPurpose
pretrainPretrain on Droid + AgiBotWorld + InternA1 + InternM1
finetune_aloha_pp_storageReal-world ALOHA pick-place + table-storage fine-tuning
debugTiny single-batch config used for smoke tests

Two-stage Pretraining

scripts/train.sh drives both stages and accepts either back-end. Common arguments:

FlagMeaning
--backendddp (torchrun) or deepspeed
--gpusComma-separated GPU IDs, e.g. 0,1,2,3
--stage0 (VA only), 1 (VLA only), or both
--configConfig name from apt/configs.py
--va-nameSave name for the Stage-0 checkpoint
--vla-nameSave name for the Stage-1 checkpoint
--va-contiResume Stage-0 from an existing checkpoint
--vla-contiResume Stage-1 from an existing checkpoint
--bs / --max-iterPer-GPU batch size / max iterations

DeepSpeed-only flags (ignored when --backend ddp):

FlagMeaning
--ds-zero 2|3ZeRO stage (selects ds_config_zero{2,3}.json)
--accum NGradient accumulation steps
--vlm-modefrozen / lora / full VLM finetune mode
--vlm-lrSeparate learning rate for VLM parameters
--gcEnable gradient checkpointing

DDP (torchrun) + Fix VLM, Stage-0 only:

bash scripts/train.sh --backend ddp --gpus 0,1,2,3 --stage 0 \
--config pretrain \
--va-name apt_va --vla-name apt_vla

DeepSpeed ZeRO-2 + Full VLM, Stage-1 only:

bash scripts/train.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config pretrain \
--va-name apt_va --vla-name apt_vla_vlmft \
--ds-zero 2 --vlm-mode full --gc --vlm-lr 1e-5

Resume Stage-1 after preemption:

bash scripts/train.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config pretrain --vla-conti apt_vla_vlmft \
--ds-zero 2 --vlm-mode full --gc --vlm-lr 1e-5

Stage-1 internally calls VLA.load_from_pretrain(..., load_from_va=True), which doubles the Stage-1 attention layers and copies the Stage-0 weights into the odd indices while leaving the inserted (even-index) language-injection layers randomly initialized.

Task-specific Fine-tuning

scripts/finetune.sh mirrors train.sh but additionally exposes --pretrained-ckpt so you can bootstrap from any pretraining checkpoint. The Stage-1 launch automatically reuses the Stage-0 name (if any) or the pretraining checkpoint as the upstream.

# Stage-1 only, DeepSpeed ZeRO-2 + Full VLM on Pick-Place from a pretrained VLA checkpoint
bash scripts/finetune.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config finetune_pp --pretrained-ckpt apt_vla \
--vla-name ft_apt_pp \
--ds-zero 3 --vlm-mode lora --gc --accum 4 --bs 64 --vlm-lr 1e-5
# Stage-1 only, Fix VLM on real ALOHA data from a pretrained VLA checkpoint
bash scripts/finetune.sh --backend ddp --gpus 0,1,2,3 --stage 1 \
--config finetune_aloha_pp_storage --pretrained-ckpt apt_vla_vlmft \
--vla-name ft_apt_aloha

Checkpoints are written under ./checkpoints/APT/<name>/ and TensorBoard logs under ./logs/APT/<name>/. Both roots are configurable per run via the optional flags --ckpt_dir /path/to/ckpts and --log_dir /path/to/logs, e.g. to keep separate experiments on different volumes. Likewise --dataloader_timeout <seconds> (default 300) lets you raise the DataLoader worker timeout for slow shared storage.

Loading existing APT checkpoints

The merged trainer is backwards-compatible with checkpoints produced by the pre-refactor training scripts. A checkpoint is recognised by its file layout:

Saved byTop-level keys in ckpt_latest.pt
DDP (train_dist.py)weights, optimizer, scheduler, scaler, current_iters, ...
DeepSpeed (train_deepspeed.py)weights, vlm_weights (if VLM fine-tuned), no embedded optimizer

The merged apt.train accepts both, and you can also switch back-ends across resumes (e.g. resume a DeepSpeed-trained run under DDP). When a checkpoint's optimizer state cannot be re-loaded (e.g. param groups differ because --vlm-mode changed), the trainer logs a warning and continues with a fresh optimizer.

Pre-flight check - validate any existing checkpoint before launching training:

# Stage-0 VA checkpoint
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 0
# Same VA checkpoint used to bootstrap Stage-1
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 1 --load-from-va
# Stage-1 VLA checkpoint (already-trained policy)
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 1

Bootstrap a new fine-tuning run from an existing VLA checkpoint (most common):

bash scripts/finetune.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config finetune_pp \
--pretrained-ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt \
--vla-name ft_apt_pp --vlm-mode full

--pretrained-ckpt accepts either a checkpoint subdir name under ./checkpoints/APT/or an absolute path ending in .pt. The trainer starts with current_iters=0 so the new run gets a clean iteration counter, and saves under ./checkpoints/APT/<vla-name>/.

Inference

For local inference, instantiate the planner directly:

fromapt.infer.plannerimportTrajPlannerplanner=TrajPlanner(
ckpt_path="checkpoints/APT/ft_vla/ckpt_latest.pt",
device="cuda:0",
ensemble=4,
use_ema=False,
)
planner.set_prompt("Pick up the grape and place it on the pink box.")
planner.add_obs_frame(obs_frame)
actions=planner.get_action()

To serve the policy as a remote service (e.g. for hardware control), first launch a Pyro4 naming server, then start the service:

# 1. Naming server (defaults to localhost:9091)
pyro4-ns -p 9091
# 2. Inference service
python -m apt.infer.remote_service \
--ckpt checkpoints/APT/ft_vla/ckpt_latest.pt \
--uri apt_control \
--host localhost --port 0 \
--ensemble 4

The client side uses shm_transport (zero-copy shared memory + Pyro4) to call add_obs_frame, set_prompt, get_action, etc.

🧪 Evaluation on LIBERO benchmarks

A single evaluator under examples/libero/ drives all three LIBERO benchmark families against an APT policy server:

BenchmarkSimulatorSuitesDefault trials/task
LIBEROLifelong-Robot-Learning/LIBEROlibero_{object,spatial,goal,10} — 4 suites50
LIBERO-PROZxy-MLlab/LIBERO-PROthe 4 above × {_swap, _task} — 8 suites50
LIBERO-plussylvestf/LIBERO-pluslibero_{object,spatial,goal,10} — 4 suites1

Quick start (LIBERO conda env, after launching the APT policy server in a separate terminal):

bash examples/libero/test_libero.sh \
--benchmark libero \
--gpu 0 \
--model_name apt_vla \
--controller_name control --controller_port 9091

See examples/libero/README.md for the per-benchmark conda setup, the policy-server launch command, the full list of test_libero.sh flags, and the on-disk output layout.

🤖 Evaluation on Pick-and-Place in Isaac Sim

An Isaac-Sim-based pick-and-place benchmark lives under examples/PickPlace/. A UR5 + Robotiq 85 arm executes language-conditioned pick-and-place on a tabletop scene; the APT policy server is queried for actions each control step.

SettingDescription
soSeen object set, default lighting / ground
uoHeld-out object set, default lighting / ground
ucSame objects as so, but the target container is a held-out mug
uoueHeld-out objects + novel HDR background + randomized ground (seed sweep)

Two separate Python environments are involved: the APT env (running the policy server with shm_transport) and the Isaac Sim bundled Python (running the benchmark). shm_transport lives at the APT root and is picked up automatically by the driver via PYTHONPATH. Quick start:

# 1. APT env (separate terminal) — launch the policy server, see "Inference" above.# 2. Isaac Sim env — drive all 4 settings:cd examples/PickPlace
GPU_ID=0 URI=control PORT=9091 SAVE_DIR=./data/exp_results/myrun \
bash eval_all.sh

See examples/PickPlace/README.md for the asset preparation, full environment requirements, per-setting flags, and the on-disk output format (<save_dir>/<setting>/videos/*.mp4 + metrics/*.json).

📥 Pretrained Checkpoints

All checkpoints live under a single Hugging Face repo: KechunXu1/apt_models.

StageConfigDatasetsHugging Face
Pretrained VLA policypretrain (--load_from_va)Droid + AgiBotWorld + InternA1 + InternM1apt_vla
LIBERO fine-tunedfinetune_liberoLIBERO Spatial / Object / Goal / 10apt_vla_ftlibero
Pick-Place fine-tunedfinetune_ppPickPlaceCanapt_vla_ftpp

Download a checkpoint (e.g. the pretrained VLA policy):

hf download KechunXu1/apt_models --include "apt_vla/*" --local-dir ./checkpoints/APT

Then point the inference script at the downloaded checkpoint via --ckpt ./checkpoints/APT/apt_vla/ckpt_latest.pt (see the Inference section).

🤝 Acknowledgements

This project builds upon BayesVLA, and E2VLA. We thank these teams for their open-source contributions.

📚 Citation

If you find this work useful, please consider citing:

@article{xu2026apt,
title={APT: Action Expert Pretraining Improves Instruction Generalization of Vision-Language-Action Policies},
author={Xu, Kechun and Zhu, Zhenjie and Chen, Anzhe and Xiong, Rong and Wang, Yue},
journal={arXiv preprint arXiv:2606.12366},
year={2026}
}

About

[arXiv 2026] APT: Action Expert Pretraining Improves Instruction Generalization of Vision-Language-Action Policies

Resources

Stars

39 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

APT

APT: Action Expert Pretraining
Improves Instruction Generalization of Vision-Language-Action Policies

Kechun Xu · Zhenjie Zhu · Anzhe Chen · Rong Xiong · Yue Wang

Paper PDFProject Page

TL; DR: APT factorizes the VLA policy into a Vision-Action (VA) prior and a language-conditioned VLA likelihood, and pretrains the action expert as the VA prior on vision-action pairs from a frozen VLM. A layer-wise gated fusion mechanism then injects language tokens into the pretrained action expert, preserving the visuomotor prior while enabling instruction following. APT delivers consistent gains on OOD language instructions and compositional tasks.

🏆 Highlights

🔍 Key Findings: continuous-action VLA policies start from a randomly initialized action expert and learn from imbalanced VLA data, producing noisy gradients that corrupt the VLM backbone and collapse to visual shortcuts.

Key Insights:

  • Bayesian factorization of the VLA policy:

$$ \pi(\mathbf{a}\mid\mathbf{v},\ell)\ \propto\ \pi^{p}(\mathbf{a}\mid\mathbf{v})\cdot L(\ell\mid\mathbf{v},\mathbf{a}) $$

  • VA prior$\pi^p(\mathbf{a}\mid\mathbf{v})$ is trained on balanced vision-action pairs alone, so the action expert builds coherent visuomotor priors without any language shortcut.

  • VLA likelihood$L(\ell\mid\mathbf{v},\mathbf{a})$ then aligns the prior to language instructions, a much easier sub-problem than learning action generation and language grounding jointly.

  • Layer-wise gated fusion injects each Qwen3-VL intermediate feature into the corresponding action-expert self-attention layer through a learnable sigmoid gate, letting the action expert inherit VLM semantics without overwriting the pretrained visuomotor pathway.

  • Two-stage realization inside one network. Stage 1 activates only half of the action-expert attention layers and masks language tokens, training a pure VA prior. Stage 2 inserts an interleaved attention layer after each Stage-1 layer, unmasks language, and jointly trains the prior and likelihood under large-scale data.

  • Architecture-agnostic: the two-stage recipe also boosts $\pi$-style and GR00T-style architectures on OOD language generalization.

🧩 Overview

Given VLA datasets with modality imbalance, APT trains the policy in two stages:

  • Stage 1 - VA Prior Pretraining: the action expert is conditioned solely on visual tokens from a frozen Qwen3-VL backbone and learns $\pi^p(\mathbf{a}\mid\mathbf{v})$.
  • Stage 2 - VLA Likelihood Alignment: the Stage-1 layers are duplicated with interleaved language-injection layers; the full policy is jointly trained on the same data.

APT method overview

📁 Project Structure

APT/
├── apt/ # core model + unified trainer (this is the package)
│ ├── vla.py # VLA wrapper (VLM + ActionExpert)
│ ├── vlm.py # Qwen3-VL encoder bridge
│ ├── action_expert.py # diffusion-based action expert with gated fusion
│ ├── action_transform.py # SE(3) ↔ 10-dim action conversions
│ ├── configs.py # TrainConfig + CONFIGS registry
│ ├── train.py # unified DDP + DeepSpeed trainer
│ ├── ds_config_zero2.json # DeepSpeed ZeRO-2 config
│ ├── ds_config_zero3.json # DeepSpeed ZeRO-3 config
│ ├── encoders/ # Qwen3-VL (LoRA-capable) wrapper
│ ├── layers/ # attention / RoPE / norms / 6D-rotation utils
│ └── infer/ # planner + remote inference service
├── data_utils/ # HDF5 IO, datasets, video decoding, distributed samplers
├── train_utils/ # EMA implementation
├── infer_utils/ # trajectory ensembler and visualizer
├── shm_transport/ # Pyro4 + shared-memory RPC for remote inference
├── scripts/
│ ├── train.sh # two-stage pretraining (DDP or DeepSpeed)
│ └── finetune.sh # task-specific fine-tuning (DDP or DeepSpeed)
├── examples/
│ ├── libero/ # LIBERO / LIBERO-PRO / LIBERO-plus evaluator
│ └── PickPlace/ # Isaac Sim pick-and-place benchmark (UR5 + Robotiq 85)
├── assets/ # logo / method (PDF source + PNG for README), paper.pdf
├── requirements.txt
├── .gitignore
└── README.md

📘 Usage

Environment

conda create -n apt python=3.10 -y
conda activate apt
# Install a PyTorch build that matches your CUDA. The pinned xformers requires# torch 2.4.1; relax it if you use a different torch version.
pip install torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cu126
pip install -r requirements.txt

Note on xformers / deepspeed: both are version-sensitive. If you do not need DeepSpeed, you can skip installing it - --backend ddp works without it. Likewise drop xformers if you do not need its kernels.

Data Preparation

APT consumes trajectories stored as per-episode HDF5 files. Each sample yielded by the dataloader looks like:

{
"obs_rgbs": (To, ncam, 3, H, W), # observation frames"prompt_text": str, # task description"current_ee_pose": (nee, 4, 4), # current EE pose in world frame"gt_future_ee_states": (Ta, nee, 17), # ground truth pose + gripper"history_ee_states": (Th, nee, 17),
"obs_norm_xys": (...), # per-pixel normalized 2D coords"obs_extrinsics": (To, ncam, 4, 4),
"valid_ee_mask": (nee,),
... # see data_utils/dataset_base.py
}

Adding a new dataset:

  1. Subclass H5DatasetMapBase in data_utils/datasets.py.
  2. Register the dataset entry in data_utils/data_loc.py. The file looks up the host IP (get_ipv4_address) and selects the matching dictionary - edit it to point at your local paths before launching training.
  3. Reference the new class from a TrainConfig entry in apt/configs.py.

We expose a number of preset configs (see apt/configs.py for the full list). Examples:

ConfigPurpose
pretrainPretrain on Droid + AgiBotWorld + InternA1 + InternM1
finetune_aloha_pp_storageReal-world ALOHA pick-place + table-storage fine-tuning
debugTiny single-batch config used for smoke tests

Two-stage Pretraining

scripts/train.sh drives both stages and accepts either back-end. Common arguments:

FlagMeaning
--backendddp (torchrun) or deepspeed
--gpusComma-separated GPU IDs, e.g. 0,1,2,3
--stage0 (VA only), 1 (VLA only), or both
--configConfig name from apt/configs.py
--va-nameSave name for the Stage-0 checkpoint
--vla-nameSave name for the Stage-1 checkpoint
--va-contiResume Stage-0 from an existing checkpoint
--vla-contiResume Stage-1 from an existing checkpoint
--bs / --max-iterPer-GPU batch size / max iterations

DeepSpeed-only flags (ignored when --backend ddp):

FlagMeaning
--ds-zero 2|3ZeRO stage (selects ds_config_zero{2,3}.json)
--accum NGradient accumulation steps
--vlm-modefrozen / lora / full VLM finetune mode
--vlm-lrSeparate learning rate for VLM parameters
--gcEnable gradient checkpointing

DDP (torchrun) + Fix VLM, Stage-0 only:

bash scripts/train.sh --backend ddp --gpus 0,1,2,3 --stage 0 \
--config pretrain \
--va-name apt_va --vla-name apt_vla

DeepSpeed ZeRO-2 + Full VLM, Stage-1 only:

bash scripts/train.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config pretrain \
--va-name apt_va --vla-name apt_vla_vlmft \
--ds-zero 2 --vlm-mode full --gc --vlm-lr 1e-5

Resume Stage-1 after preemption:

bash scripts/train.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config pretrain --vla-conti apt_vla_vlmft \
--ds-zero 2 --vlm-mode full --gc --vlm-lr 1e-5

Stage-1 internally calls VLA.load_from_pretrain(..., load_from_va=True), which doubles the Stage-1 attention layers and copies the Stage-0 weights into the odd indices while leaving the inserted (even-index) language-injection layers randomly initialized.

Task-specific Fine-tuning

scripts/finetune.sh mirrors train.sh but additionally exposes --pretrained-ckpt so you can bootstrap from any pretraining checkpoint. The Stage-1 launch automatically reuses the Stage-0 name (if any) or the pretraining checkpoint as the upstream.

# Stage-1 only, DeepSpeed ZeRO-2 + Full VLM on Pick-Place from a pretrained VLA checkpoint
bash scripts/finetune.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config finetune_pp --pretrained-ckpt apt_vla \
--vla-name ft_apt_pp \
--ds-zero 3 --vlm-mode lora --gc --accum 4 --bs 64 --vlm-lr 1e-5
# Stage-1 only, Fix VLM on real ALOHA data from a pretrained VLA checkpoint
bash scripts/finetune.sh --backend ddp --gpus 0,1,2,3 --stage 1 \
--config finetune_aloha_pp_storage --pretrained-ckpt apt_vla_vlmft \
--vla-name ft_apt_aloha

Checkpoints are written under ./checkpoints/APT/<name>/ and TensorBoard logs under ./logs/APT/<name>/. Both roots are configurable per run via the optional flags --ckpt_dir /path/to/ckpts and --log_dir /path/to/logs, e.g. to keep separate experiments on different volumes. Likewise --dataloader_timeout <seconds> (default 300) lets you raise the DataLoader worker timeout for slow shared storage.

Loading existing APT checkpoints

The merged trainer is backwards-compatible with checkpoints produced by the pre-refactor training scripts. A checkpoint is recognised by its file layout:

Saved byTop-level keys in ckpt_latest.pt
DDP (train_dist.py)weights, optimizer, scheduler, scaler, current_iters, ...
DeepSpeed (train_deepspeed.py)weights, vlm_weights (if VLM fine-tuned), no embedded optimizer

The merged apt.train accepts both, and you can also switch back-ends across resumes (e.g. resume a DeepSpeed-trained run under DDP). When a checkpoint's optimizer state cannot be re-loaded (e.g. param groups differ because --vlm-mode changed), the trainer logs a warning and continues with a fresh optimizer.

Pre-flight check - validate any existing checkpoint before launching training:

# Stage-0 VA checkpoint
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 0
# Same VA checkpoint used to bootstrap Stage-1
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 1 --load-from-va
# Stage-1 VLA checkpoint (already-trained policy)
python scripts/test_ckpt.py \
--ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt --train-stage 1

Bootstrap a new fine-tuning run from an existing VLA checkpoint (most common):

bash scripts/finetune.sh --backend deepspeed --gpus 0,1,2,3 --stage 1 \
--config finetune_pp \
--pretrained-ckpt /path/to/apt_ckpt_dir/ckpt_latest.pt \
--vla-name ft_apt_pp --vlm-mode full

--pretrained-ckpt accepts either a checkpoint subdir name under ./checkpoints/APT/or an absolute path ending in .pt. The trainer starts with current_iters=0 so the new run gets a clean iteration counter, and saves under ./checkpoints/APT/<vla-name>/.

Inference

For local inference, instantiate the planner directly:

fromapt.infer.plannerimportTrajPlannerplanner=TrajPlanner(
ckpt_path="checkpoints/APT/ft_vla/ckpt_latest.pt",
device="cuda:0",
ensemble=4,
use_ema=False,
)
planner.set_prompt("Pick up the grape and place it on the pink box.")
planner.add_obs_frame(obs_frame)
actions=planner.get_action()

To serve the policy as a remote service (e.g. for hardware control), first launch a Pyro4 naming server, then start the service:

# 1. Naming server (defaults to localhost:9091)
pyro4-ns -p 9091
# 2. Inference service
python -m apt.infer.remote_service \
--ckpt checkpoints/APT/ft_vla/ckpt_latest.pt \
--uri apt_control \
--host localhost --port 0 \
--ensemble 4

The client side uses shm_transport (zero-copy shared memory + Pyro4) to call add_obs_frame, set_prompt, get_action, etc.

🧪 Evaluation on LIBERO benchmarks

A single evaluator under examples/libero/ drives all three LIBERO benchmark families against an APT policy server:

BenchmarkSimulatorSuitesDefault trials/task
LIBEROLifelong-Robot-Learning/LIBEROlibero_{object,spatial,goal,10} — 4 suites50
LIBERO-PROZxy-MLlab/LIBERO-PROthe 4 above × {_swap, _task} — 8 suites50
LIBERO-plussylvestf/LIBERO-pluslibero_{object,spatial,goal,10} — 4 suites1

Quick start (LIBERO conda env, after launching the APT policy server in a separate terminal):

bash examples/libero/test_libero.sh \
--benchmark libero \
--gpu 0 \
--model_name apt_vla \
--controller_name control --controller_port 9091

See examples/libero/README.md for the per-benchmark conda setup, the policy-server launch command, the full list of test_libero.sh flags, and the on-disk output layout.

🤖 Evaluation on Pick-and-Place in Isaac Sim

An Isaac-Sim-based pick-and-place benchmark lives under examples/PickPlace/. A UR5 + Robotiq 85 arm executes language-conditioned pick-and-place on a tabletop scene; the APT policy server is queried for actions each control step.

SettingDescription
soSeen object set, default lighting / ground
uoHeld-out object set, default lighting / ground
ucSame objects as so, but the target container is a held-out mug
uoueHeld-out objects + novel HDR background + randomized ground (seed sweep)

Two separate Python environments are involved: the APT env (running the policy server with shm_transport) and the Isaac Sim bundled Python (running the benchmark). shm_transport lives at the APT root and is picked up automatically by the driver via PYTHONPATH. Quick start:

# 1. APT env (separate terminal) — launch the policy server, see "Inference" above.# 2. Isaac Sim env — drive all 4 settings:cd examples/PickPlace
GPU_ID=0 URI=control PORT=9091 SAVE_DIR=./data/exp_results/myrun \
bash eval_all.sh

See examples/PickPlace/README.md for the asset preparation, full environment requirements, per-setting flags, and the on-disk output format (<save_dir>/<setting>/videos/*.mp4 + metrics/*.json).

📥 Pretrained Checkpoints

All checkpoints live under a single Hugging Face repo: KechunXu1/apt_models.

StageConfigDatasetsHugging Face
Pretrained VLA policypretrain (--load_from_va)Droid + AgiBotWorld + InternA1 + InternM1apt_vla
LIBERO fine-tunedfinetune_liberoLIBERO Spatial / Object / Goal / 10apt_vla_ftlibero
Pick-Place fine-tunedfinetune_ppPickPlaceCanapt_vla_ftpp

Download a checkpoint (e.g. the pretrained VLA policy):

hf download KechunXu1/apt_models --include "apt_vla/*" --local-dir ./checkpoints/APT

Then point the inference script at the downloaded checkpoint via --ckpt ./checkpoints/APT/apt_vla/ckpt_latest.pt (see the Inference section).

🤝 Acknowledgements

This project builds upon BayesVLA, and E2VLA. We thank these teams for their open-source contributions.

📚 Citation

If you find this work useful, please consider citing:

@article{xu2026apt,
title={APT: Action Expert Pretraining Improves Instruction Generalization of Vision-Language-Action Policies},
author={Xu, Kechun and Zhu, Zhenjie and Chen, Anzhe and Xiong, Rong and Wang, Yue},
journal={arXiv preprint arXiv:2606.12366},
year={2026}
}

About

[arXiv 2026] APT: Action Expert Pretraining Improves Instruction Generalization of Vision-Language-Action Policies

Resources

Stars

39 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages