Jeremy A. Collins, Seungjae Lee, Rachanon Wachakorn, Krishnan Srinivasan, Animesh Garg, Vitor Campagnolo Guizilini, Paarth Shah
This repo contains the official implementation of TAPESTRY: A Single Backbone for Video Generation and Robot Control (CoRL 2026).
TAPESTRY turns a pretrained video diffusion transformer (CogVideoX-2b) into a robot policy without generating video at inference time. Each action timestep becomes one patch in the video VAE's latent space through a single linear map (1,280 parameters for 20-d actions), and passes through the same patchifying convolution and transformer backbone as the video frames. The backbone is fine-tuned with three diffusion objectives:
- Video diffusion on unlabeled robot and human video
- Joint video and action diffusion with a coupled timestep and block-causal attention
- Action-only diffusion, the mode the policy runs in
The policy generates each action chunk from pure noise in a single DDIM step, which takes about 84 ms on an RTX 5090.
Paste one of these prompts into a coding agent such as Claude Code, Codex or Cursor on the machine with your GPU. The agent follows AGENTS.md: it checks your GPUs and disk, proposes a plan and waits for your go-ahead before it downloads or trains anything. It needs network access and permission to run shell commands.
Fine-tune the pretrained checkpoint on your robot demos (one GPU with 32 GB):
Clone https://github.com/pairlab/TAPESTRY.git, read the AGENTS.md at its root and follow it to
fine-tune the pretrained TAPESTRY checkpoint on my robot demos.
Run the policy on your robot (about 10 GB of GPU memory):
Clone https://github.com/pairlab/TAPESTRY.git, read the AGENTS.md at its root and follow it to
run the pretrained TAPESTRY policy on my robot.
Train and evaluate the LIBERO recipe (four 141 GB GPUs, or a one-task overfit check on one 80 GB GPU):
Clone https://github.com/pairlab/TAPESTRY.git, read the AGENTS.md at its root and follow it to
set up TAPESTRY, train the paper's LIBERO recipe and evaluate the trained policy on LIBERO.
Training and evaluation run as background jobs that outlive the agent session. To check on one later, open an agent in the clone and paste:
Follow AGENTS.md to check on the TAPESTRY run and report its progress or results.
conda create -n tapestry python=3.10 -y
conda activate tapestry
git clone https://github.com/pairlab/TAPESTRY.git
cd TAPESTRY
pip install -e .pip install -e . installs PyTorch 2.13 for CUDA 13, which needs NVIDIA driver 580 or newer. With an older driver, first install a PyTorch between 2.6 and 2.13 for your CUDA version from pytorch.org, which pip install -e . then keeps. We tested:
pip install torch==2.7.0 torchvision==0.22.0 --index-url https://download.pytorch.org/whl/cu128The frozen base models download from HuggingFace on first use: the CogVideoX-2b transformer and VAE (about 4 GB) and the T5-XXL text encoder (about 45 GB, or 9.5 GB with the fp16 copy).
| Checkpoint | Training data | Cameras and actions | Inference GPU memory |
|---|---|---|---|
tapestry.pt |
robot and human video plus bimanual YAM demos (cfg/train_tapestry.yaml) |
front and two wrist cameras; 20-d bimanual eepose_6drot |
about 10 GB, or 28 GB without caching text prompts |
hf download jeremy-collins/TAPESTRY tapestry.pt --local-dir .The 7.6 GB file bundles the policy weights, the training config and the action normalizer, so it needs no other file besides the base models. It drives our bimanual YAM robot and is the starting point for fine-tuning on yours.
from tapestry.models.policy import TapestryPolicy
policy = TapestryPolicy("tapestry.pt", device="cuda", num_inference_steps=1, t5_override=None, text_cache_dir="text_cache")
print(policy.cameras, policy.image_size, policy.action_horizon, policy.action_dim)
# ['front_img_1', 'left_wrist_img', 'right_wrist_img'] 128 16 20
actions = policy.act([front_rgb, left_wrist_rgb, right_wrist_rgb], "Pick up the corn and put it in the bowl",
ref_action=current_pose) # [16, 20] numpyacttakes one[image_size, image_size, 3]uint8 RGB array per camera, inpolicy.camerasorder. Resize each full camera frame to that size without cropping (bilinear), as the data loader does.ref_actionis the robot's current pose in the action layout of fine-tuning step 1. The model predicts poses relative to it, andactreturns absolute poses ready to execute. Delta action spaces such as LIBERO's need noref_action.- Execute the first few actions of each chunk and re-plan (our LIBERO evaluation executes 8 of 16), or blend overlapping chunks with temporal aggregation as in
tapestry/eval_libero.py. text_cache_dirkeeps T5 off the GPU, which brings inference to about 10 GB. Each new instruction is encoded once with T5 on the CPU (about 20 GB of RAM) and saved in that directory for later runs, so callpolicy.text_embedding(text)for your instructions before the control loop, or fill the directory on another machine and copy it over.text_cache_dir=Nonekeeps T5 on the GPU (about 28 GB).t5_override="THUDM/CogVideoX-2b"loads the smaller fp16 T5 in place of the one the checkpoint was trained with.
scripts/policy_example.py runs this loop in LIBERO with a checkpoint exported as in step 4 of fine-tuning. Copy it for your robot.
LoRA fine-tuning from the pretrained checkpoint runs on one 32 GB GPU.
| Dataset | Shape | Content |
|---|---|---|
actions/<action_space> |
(T, D) float |
D = ACTION_SPACE_CONFIGS[action_space]["total_dim"] (tapestry/utils/action_normalizer.py) |
observations/images/<camera> |
(T, H, W, 3) uint8 |
one dataset per camera, any names |
text |
scalar string | task description; add it with python scripts/add_text_to_hdf5.py --demo-dir data/my_robot --text "pick up the cup" |
- Built-in action spaces are
eepose_6drot(20-d, the paper's),eepose_ypr(14-d),joints(14-d) andlibero(7-d deltas). A new one is an entry inACTION_SPACE_CONFIGSthat gives its total dim, the dims to normalize and whether actions are deltas. eepose_6drotstores each arm as[x, y, z, R00, R01, R10, R11, R20, R21, gripper](dims 0-9 and 10-19), with the first two columns of the rotation matrix interleaved row-major as inmatrix_to_rot6d_np(tapestry/utils/action_preprocess.py).- All scripts read every
*.hdf5directly inside the demo directory, in filename order.
python scripts/compute_normalizer_stats.py --demo-dir data/my_robot --action-space eepose_6drot \
--horizon 16 --output data/my_robot/normalizer_stats.json --relative-rotation-actions--horizon is the action-chunk length (training.true_horizon). --relative-rotation-actions makes rotations relative to the chunk start (eepose_6drot only) and must match training.dataset.relative_rotation_actions.
accelerate launch train.py --config-name finetune init_from=tapestry.pt \
training.dataset.demo_root_dir=data training.dataset.demo_dir=my_robot \
training.dataset.normalizer_stats_path=data/my_robot/normalizer_stats.json \
'cond_cameraviews=[front_img_1,left_wrist_img,right_wrist_img]'cfg/finetune.yaml keeps the paper's architecture and objectives, trains LoRA adapters on the frozen backbone and holds out the last 10% of demos for validation, so the directory needs at least 6 demos. Checkpoints land in checkpoints/<experiment_name>/, or checkpoints/<experiment_name>_1/ when that exists: latest.pt every epoch, epoch_N.pt every 10 epochs and best_checkpoint.pt at the lowest validation action loss, 7.7 GB each.
init_fromloads weights only, so a checkpoint trained on another action space still works: the action projection is re-initialized and everything else is inherited. Setmodel.action_dimandtraining.dataset.action_spacetogether, for exampletraining.dataset.action_space=joints model.action_dim=14 training.dataset.relative_rotation_actions=false, and compute the statistics in step 2 with the same--action-space.cond_cameraviewslists the cameras the policy sees, in order, up to three. Deployment must use the same cameras in the same order.- Full fine-tuning needs both
decoders.cogvideox.use_lora=falseanddecoders.cogvideox.freeze_pretrained_weights=false. resume=truecontinues the latest run of thisexperiment_namewith its saved config plus any overrides. Optimizer settings cannot change during a resume, so useinit_fromfor a fresh optimizer.accelerate launchtrains on every visible GPU withtraining.batch_sizesamples on each. Add--num_processes 1for one GPU.
python scripts/prepare_release_checkpoint.py --checkpoint checkpoints/tapestry_finetune/epoch_50.pt --output my_robot.pt --verifymy_robot.pt loads in TapestryPolicy as in Run a policy.
The paper's real-world recipe fully fine-tunes the backbone:
pip install -e ".[decord]" # video decoding for the video-only datasets
accelerate launch train.py --config-name train_tapestryIts data mixture (cfg/data/tapestry_paper.yaml):
| Share | Data | Actions |
|---|---|---|
| 44.5% | cross-embodiment robot and human video from the Large Video Planner mixture | no |
| 50% | robot demos, 12.5 points of which are play data | yes |
| 5.5% | task-aligned human video | no |
- Our robot datasets are template entries in the config, so point them at your HDF5 demo directories (layout).
- Each video-only dataset reads a metadata CSV with the columns documented in
tapestry/loaders/video_only_dataset.py.tapestry/loaders/path_translator.pydocuments the on-disk layout of droid, ego4d, epic_kitchens, ssv2, pandas (Panda-70M), bridge and agibot. - For droid, ego4d, epic_kitchens and ssv2, place the
cleaned_metadata.csvfiles of the LVP release at each entry'smetadata_pathundertraining.dataset.root_dir. Our Panda-70M, Bridge and AgiBot CSVs (re-cut clips and multi-view entries) are not released, so build these with the same columns. translate_paths: falseresolvesvideo_pathrelative toroot_dir, which only works for single-camera entries (cond_cameraviews: null).
The LIBERO recipe is under Train on LIBERO.
Any config value can be overridden from the command line, such as training.batch_size=8 evaluation.eval_every=1.
| Override | Effect |
|---|---|
decoders.cogvideox.joint_diffusion_mix_prob |
fraction of samples with both targets that train with the joint objective |
decoders.cogvideox.use_lora, decoders.cogvideox.freeze_pretrained_weights |
LoRA or full fine-tuning |
training.dataset.datasets.<name>.batch_weight |
batch composition across datasets |
evaluation.num_inference_steps |
DDIM steps during in-training evaluation (1 in the paper) |
quick=true |
10-sample datasets for smoke tests |
The loss weights live under training.objectives (real-world: action 1.0 and video 0.1; LIBERO: action 0.03 and video 1.0). Their names contain a slash, so pass them as a quoted dict:
accelerate launch train.py --config-name train_libero \
'training.objectives={action/future_frames:{weight:0.1},video/future_frames:{weight:1.0}}'The paper also evaluates on an action-free variant of the LIBERO simulation benchmark, where only libero_90 has action labels and libero_10, libero_object, libero_spatial and libero_goal contribute video alone and are evaluated zero-shot.
| Run | Hardware we ran it on | Disk | Time |
|---|---|---|---|
| Recipe for 7 epochs, then evaluation on the four held-out suites | four H200 GPUs with 141 GB each | about 370 GB | about 3 days |
| Overfit check for 20 epochs on one task, then evaluation on that task | one H200 GPU, of which it used 47 GB | about 210 GB | 6 to 11 hours |
LIBERO requires CMake. See Notes for issues with egl_probe or a stale LIBERO config.
git clone https://github.com/Lifelong-Robot-Learning/LIBERO.git
cd LIBERO
pip install -r requirements.txt
pip install -e . --config-settings editable_mode=compat # a plain "pip install -e ." leaves libero unimportable
pip install gymnasium robosuite==1.4.1 mujoco==3.2.2 # the simulator versions we tested
mkdir -p libero/datasets # LIBERO prints a warning on every call while this folder is missing
echo N | python -c "import libero.libero" # writes ~/.libero/config.yaml with the default paths inside LIBERO
cd ..
pip install -e . # LIBERO pins old versions of transformers and numpy, and this restores oursEvaluation only needs the simulator. Training also needs the demonstrations of all five suites (about 100 GB) from the LIBERO HuggingFace dataset, and the overfit check only reads libero_object (about 7 GB, add --include "libero_object/*"):
hf download yifengzhu-hf/LIBERO-datasets --repo-type dataset --local-dir LIBERO/libero/datasets
mkdir -p preprocessed_data && ln -s "$(realpath LIBERO/libero/datasets)" preprocessed_data/libero_demosaccelerate launch --multi_gpu --num_processes 4 train.py --config-name train_liberotraining.batch_size is per GPU, and its default of 32 on 4 GPUs gives the paper's batch of 128 but needs more than 80 GB per GPU. Other GPU counts work with a per-GPU batch that keeps the product at 128, such as --num_processes 8 training.batch_size=16, although we have only run this code on four. In-training evaluation runs every evaluation.eval_every epochs, and each epoch keeps a 21 GB checkpoint.
python eval_libero_parallel.py checkpoint=checkpoints/tapestry_libero_taskgen/latest.pt \
'suites=[libero_10,libero_object,libero_spatial,libero_goal]' n_rollouts_per_task=50The evaluator starts one worker per visible GPU and reads the cameras, image size, action horizon and action dimension from the checkpoint. On a 32 GB GPU set n_envs=5. With the defaults, the four suites take about an hour at 10 rollouts per task on one 80 GB GPU. The other options (task_ids, seed, temporal_agg, execution_horizon, num_inference_steps, results_file, ...) are in cfg/eval_libero_parallel.yaml.
cfg/overfit_libero.yaml trains on a single task with no video-only data, which checks training and inference end to end on one GPU:
mkdir -p preprocessed_data/overfit/libero_demos/libero_object_t0
ln -s "$(realpath preprocessed_data/libero_demos/libero_object/pick_up_the_alphabet_soup_and_place_it_in_the_basket_demo.hdf5)" \
preprocessed_data/overfit/libero_demos/libero_object_t0/
accelerate launch --num_processes 1 train.py --config-name overfit_libero \
training.dataset.root_dir=preprocessed_data/overfit
python eval_libero_parallel.py checkpoint=checkpoints/libero_overfit/epoch_20.pt \
'suites=[libero_object]' 'task_ids=[0]' n_rollouts_per_task=50 n_envs=5The policy only starts to succeed late in training, so evaluate epoch_15.pt and epoch_20.pt. On a 32 GB GPU, decoders.cogvideox.use_lora=true decoders.cogvideox.freeze_pretrained_weights=true training.batch_size=2 runs the same path as a smoke test, although we only validated task success with full fine-tuning. scripts/verify_libero_data.py replays the recorded actions of a task, which checks the simulator independently of any model.
Multi-step DDIM adds high-frequency jitter to action chunks, while one step from pure noise gives smooth chunks. The analysis scripts:
scripts/eval_teacher_forced.py: teacher-forced jitter sweep on HDF5 demosscripts/eval_determinism_at_t.py: determinism at a given timestepscripts/benchmark_tapestry_latency.py,scripts/benchmark_tapestry_fullpath.py: latency without T5 encoding
The original T5-XXL downloads as a 45 GB fp32 file that also holds the unused decoder. The CogVideoX-2b repository ships the same encoder in fp16 (9.5 GB), whose embeddings differ only by rounding. Pass t5_override=THUDM/CogVideoX-2b to eval_libero_parallel.py, t5_override="THUDM/CogVideoX-2b" to TapestryPolicy or --t5-override THUDM/CogVideoX-2b to the scripts that take a --checkpoint, and set encoders.text.model_name_or_path=THUDM/CogVideoX-2b when training.
Logging to Weights & Biases is off by default. Log in with wandb login and add training.wandb.use_wandb=true for training or use_wandb=true for eval_libero_parallel.py, which also saves the first LIBERO rollout of each task as a video. training.wandb.project and wandb_project choose the project.
pip install -e ".[test]"
python -m pytest tests/Checkpoints are memory-mapped at load time, which can drop to a few MB/s on Lustre or NFS. Copy the checkpoint to local disk first.
LIBERO depends on egl_probe, which needs CMake and a C++ compiler to build. Install the build tools, then retry:
conda install -c conda-forge cmake cxx-compiler
python -m pip install egl-probeLIBERO caches its paths in ~/.libero/config.yaml on first import, and a stale config from an older checkout breaks path resolution. Delete it (rm ~/.libero/config.yaml) and regenerate it with the echo N | python -c "import libero.libero" line from Install LIBERO. Set LIBERO_CONFIG_PATH=<dir> to keep a separate config per checkout.
TAPESTRY builds on CogVideoX and its implementation in diffusers. Simulation experiments use LIBERO.
If you find this work useful, please use the following citation:
@inproceedings{collins2026tapestry,
title={TAPESTRY: A Single Backbone for Video Generation and Robot Control},
author={Jeremy A. Collins and Seungjae Lee and Rachanon Wachakorn and Krishnan Srinivasan and Animesh Garg and Vitor Campagnolo Guizilini and Paarth Shah},
booktitle={Conference on Robot Learning (CoRL)},
year={2026}}
TAPESTRY is released under the MIT License. tapestry/video_diffusion/cogvideox_transformer_3d.py is adapted from diffusers and keeps its Apache 2.0 license, and tapestry/utils/libero_utils/custom_venv.py is adapted from LIBERO and keeps its MIT license. The pretrained checkpoint is fine-tuned from CogVideoX-2b and uses the T5-XXL encoder, both released under the Apache 2.0 license.
