DM05 Tutorial

September 16, 2026 ยท View on GitHub

DM0.5 is Dexmal's open-world VLA: a 4B VLM plus a 680M Action Expert that emits continuous actions with Flow Matching. It is larger than DM0, and history, embodied reasoning, action supervision, and data quality are stronger, so the model can track task progress, follow open instructions, and stay stable under camera change and human interference.

Main gains (pretrained):

  • Zero-shot: follow language in unseen open scenes (8 primitives, 7 condition types).
  • Fine-tune: a stronger base gives cheaper, better specialists.
  • Long memory: history up to 60 seconds.
  • Robust actions: lighting, camera motion, human perturbation.
  • Multi-embodiment: multi-robot pretrain, then transfer.

This repo implements Gemma3 VLM + Gemma3 Action Expert.

DM0.5 model overview

More narrative and official eval videos: DM0.5 blog.

Pretrained Model

Download the Hugging Face base weights to checkpoints/DM05. Playground training defaults resolve to that path.

ModelDescriptionInput ImagesAction DimBackboneLink
DM05Base Gemma3 VLM + Flow Matching Action Expertset by the playground (LIBERO: agentview + wrist)padded to 32D (LIBERO valid dim 7)4B VLM๐Ÿค— Hugging Face
huggingface-cli download Dexmal/DM05 \
  --local-dir checkpoints/DM05 \
  --local-dir-use-symlinks False

Entrypoints default to ./checkpoints/DM05 for training. The official LIBERO eval checkpoint is Dexmal/DM05-libero; see Benchmark Results.

Installation and Environment Setup

The DM05 image is dexmal/dexbotic:dm05. Clone the repo on the host, mount it into the container, then install the mounted source.

git clone https://github.com/Dexmal/dexbotic.git

docker run -it --rm --gpus all --network host \
  -v /path/to/dexbotic:/dexbotic \
  dexmal/dexbotic:dm05 \
  bash

cd /dexbotic
conda activate dexbotic
pip install -e .

Local conda install if you are not using Docker:

git clone https://github.com/Dexmal/dexbotic.git
conda create -n dexbotic python=3.10 -y
conda activate dexbotic

pip install torch torchvision \
  --index-url https://download.pytorch.org/whl/cu128

pip install ninja packaging
MAX_JOBS=2 pip install flash-attn --no-build-isolation

cd dexbotic
pip install -e .
pip install transformers==5.3.0

The default train recipes below need 8 GPUs (H20 / A100 / H100 for full SFT; 4090 works for LoRA). One GPU is enough for inference. FSDP2 notes: FSDP2.md.

LIBERO Data

LIBERO is the shipped example. The in-repo recipes train on libero_pi0_all from Dexmal/libero. After download, that split should sit under data/libero. Full SFT vs LoRA differences are in the Training table.

huggingface-cli download Dexmal/libero \
  --repo-type dataset \
  --local-dir data/.hf_downloads/libero \
  --local-dir-use-symlinks False

mkdir -p data/libero
cp -a data/.hf_downloads/libero/libero_pi0_all data/libero/
test -d data/libero/libero_pi0_all/jsonl
test -d data/libero/libero_pi0_all/image

If the download is archives instead of a folder, extract into data/libero so those two paths exist. Dataset catalog: Data.md.

Training

Start from ./checkpoints/DM05 and data/libero/libero_pi0_all above. Full SFT and LoRA are two playground files.

Training a Model with Provided Data

ItemFull SFTLoRA
Entrypointlibero_dm05.pylibero_dm05_lora.py
use_loraFalseTrue
BackendFSDP2DDP only (deepspeed=None)
GPUs8 ร— H20 / A100 / H1008 ร— 4090 or H20; on multi-GPU 4090 set NCCL_P2P_DISABLE=1 NCCL_IB_DISABLE=1
Extra installโ€”pip install "peft>=0.13.0"
LLM attentionflex_attentioneager
Vision / action attentionflash_attention_2 / sdpasdpa / sdpa
VLM / AE gradient checkpointingoffon
LR / warmup2.5e-5 / 10005e-4 / 500
Per-device batch84
model_max_length7681024
Steps / save50000 / 1000050000 / 10000
LoRAโ€”r=32, alpha=16, dropout=0, all-linear (no lm_head)
Output./user_checkpoints/dexbotic/libero_dm05/libero-sft./user_checkpoints/dexbotic/libero_dm05_lora/libero-lora-MMDD
Infer default path./checkpoints/DM05 unless you pass --model_name_or_path./user_checkpoints/dexbotic/libero_dm05_lora/libero-lora-MMDD

Shared: model_name_or_path=./checkpoints/DM05, dataset_name=libero_pi0_all, 2 views, chunk_size=10, diffusion_steps=10, pad-to-square then 448ร—448, bf16, save_hf_sidecar=True.

# full SFT
torchrun --nproc_per_node=8 playground/benchmarks/libero/libero_dm05.py --task train --train-backend fsdp2

# LoRA
pip install "peft>=0.13.0"
export NCCL_P2P_DISABLE=1   # multi-GPU 4090 only
export NCCL_IB_DISABLE=1
torchrun --nproc_per_node=8 playground/benchmarks/libero/libero_dm05_lora.py --task train --train-backend ddp

If norm_stats.json is missing at train start, rank 0 computes it and the other ranks wait. Training raises if that file never appears. Inference does not compute it; the file must already sit next to --model_name_or_path.

python playground/benchmarks/libero/libero_dm05.py --task compute_norm_stats

LoRA writes adapter_model.safetensors + adapter_config.json and checkpoint-{step}-hf. Infer merges the adapter; continuing from an adapter keeps PeftModel. More LoRA recipe notes: LiberoLora.md.

Training a Model with Your Own Data

Write Dexdata JSONL, register it under dexbotic/data/data_source, then copy a LIBERO playground file and edit it for your data. Each frame needs images, state, and prompt. The example below is three-view:

dexdata_frame = {
    "images_1": {"type": "image", "url": frame["image_path_1"]},
    "images_2": {"type": "image", "url": frame["image_path_2"]},
    "images_3": {"type": "image", "url": frame["image_path_3"]},
    "state": frame["robot_state"],
    "prompt": frame["instruction"],
    "is_robot": True,
}

Register under dexbotic/data/data_source/:

from dexbotic.data.data_source.register import register_dataset
import math

MY_CUSTOM_DATASET = {
    "my_robot_data": {
        "data_path_prefix": "",
        "annotations": "/path/to/your/custom_dataset/",
        "frequency": 1,
    },
}
meta_data = {
    "non_delta_mask": [6],
    "periodic_mask": [3, 4, 5],
    "periodic_range": 2 * math.pi,
}
register_dataset(MY_CUSTOM_DATASET, meta_data=meta_data, prefix="my_custom")

The registered name is my_custom_my_robot_data. Copy libero_dm05.py or libero_dm05_lora.py and change the fields below to match this three-view example (keep two-view defaults if your data is two-view):

# DM05TrainerConfig
output_dir = [checkpoint dir]
num_train_steps = [steps]

# DM05DataConfig
dataset_name = "my_custom_my_robot_data"
num_images = 3
images_keys = ["images_1", "images_2", "images_3"]
valid_action_dim = 7

# DM05ModelConfig / DM05InferenceConfig
model_name_or_path = "./checkpoints/DM05"
action_dim = 7
camera_order = ["agentview", "wrist", "right_wrist"]

# DM05DataCollator (class attr; default is 2 views)
image_prompts = ("Head", "Left wrist", "Right wrist")

num_images must equal len(DM05DataCollator.image_prompts) (default ("Head", "Left wrist")). A mismatch raises.

torchrun --nproc_per_node=8 path/to/your_dm05_exp.py --task train

More Dexdata fields and multi-dataset + mixing: Data.md.

Inference Backends

DM05InferenceConfig.backend selects one of two inference paths:

BackendVision pathPrefix and action pathHistory frames
defaultPyTorchDynamic KV-cache inferenceSupported
fastTensorRTDM05 Triton kernels with fixed prefix bucketsSupported; requests containing history bypass CUDA Graph capture

Both backends expose the same HTTP service on 0.0.0.0:7891 by default, including /health, /v1/capabilities, /v1/infer, /v1/reset, and the legacy /process_frame route. The backend only changes how the model executes; clients do not need to use a different request format.

Run the commands below from the repository root. Replace /path/to/dm05-checkpoint with a full Hugging Face checkpoint directory or a checkpoint-*-hf directory. The checkpoint directory must contain norm_stats.json.

Start the LIBERO default backend

The default backend uses PyTorch and does not require the optional fast plugin. Start it with the LIBERO entrypoint:

CUDA_VISIBLE_DEVICES=0 python playground/benchmarks/libero/libero_dm05.py \
  --task inference \
  --backend default \
  --port 7891 \
  --model-name-or-path /path/to/dm05-checkpoint

--backend default is optional because it is the configured default. The command blocks while the server is running.

Start the LIBERO fast backend

The fast backend requires the optional DM05 plugin. Install it in the active Dexbotic environment:

bash plugins/dm05-fast/install.sh

Start the same LIBERO entrypoint with --backend fast:

CUDA_VISIBLE_DEVICES=0 python playground/benchmarks/libero/libero_dm05.py \
  --task inference \
  --backend fast \
  --port 7891 \
  --model-name-or-path /path/to/dm05-checkpoint \
  --vision-trt-engine-path checkpoints/trt_engines/dm05-checkpoint.engine

The CLI leaves build_vision_engine_if_missing=True by default, so the first startup builds a fixed-shape TensorRT vision engine if a matching engine does not already exist. Later startups reuse that engine. Use a separate engine path for each checkpoint because the engine contains checkpoint-specific vision weights. Alternatively, pass --force-rebuild-vision-engine after changing the checkpoint. The fast backend supports batch size 1.

Other fast options are available through --prefix-seq-len-buckets, --fast-overflow-policy, --fast-prefix-qkv-mode, and --no-build-vision-engine-if-missing. Run the entrypoint with --help for the complete CLI.

To build the TensorRT engine separately instead, pass the total number of image slots accepted by the engine. The standard history-disabled setup uses two current camera views, so --num-images is 2:

CUDA_VISIBLE_DEVICES=0 python -m dexbotic.model.dm05.infer.fast.build_vision_trt \
  --checkpoint /path/to/dm05-checkpoint \
  --onnx-path checkpoints/trt_engines/dm05_vision.onnx \
  --engine-path checkpoints/trt_engines/dm05_vision.engine \
  --num-images 2

Verify and call the service

After either backend finishes loading, verify that the service is ready:

curl http://localhost:7891/health
curl http://localhost:7891/v1/capabilities

Send two images in camera_order order (agentview, then wrist) to the legacy route:

curl -X POST \
  -F "text=What action should the robot take to put the bowl on the plate?" \
  -F "image=@/path/to/agentview.png" \
  -F "image=@/path/to/wrist.png" \
  http://localhost:7891/process_frame

See InferenceAPI.md for the /v1/infer JSON schema and DexClient example.

Start a RoboTwin2 service

RoboTwin2 uses a dedicated entrypoint because its inference contract differs from LIBERO: three RGB views (Head, Left wrist, Right wrist), a required 14-dimensional Aloha state, 14-dimensional actions, and a 50-step action chunk. Do not use libero_dm05.py for a RoboTwin2 checkpoint.

Start the default backend:

CUDA_VISIBLE_DEVICES=0 python \
  playground/benchmarks/robotwin2/robotwin2_dm05.py \
  --backend default \
  --port 7891 \
  --model-name-or-path /path/to/robotwin2-dm05-checkpoint

Start the fast backend after installing plugins/dm05-fast:

bash plugins/dm05-fast/install.sh

CUDA_VISIBLE_DEVICES=0 python \
  playground/benchmarks/robotwin2/robotwin2_dm05.py \
  --backend fast \
  --port 7891 \
  --model-name-or-path /path/to/robotwin2-dm05-checkpoint \
  --vision-trt-engine-path \
    checkpoints/trt_engines/robotwin2-dm05-checkpoint.engine

The checkpoint directory must contain norm_stats.json. If the statistics live elsewhere, pass --norm-stats /path/to/norm_stats.json. The fast command builds a fixed-shape three-image TensorRT engine on first startup and remains single-request. Use a checkpoint-specific engine path, as in the example.

Both backends accept the same legacy request and return an action array with shape 50 x 14:

curl -X POST \
  -F "text=Pick up the block" \
  -F "image=@/path/to/head.png" \
  -F "image=@/path/to/left-wrist.png" \
  -F "image=@/path/to/right-wrist.png" \
  -F 'states=[0,0,0,0,0,0,0,0,0,0,0,0,0,0]' \
  http://localhost:7891/process_frame

RoboTwin2 supports the same opt-in, explicit history-image contract as LIBERO. Start the service with --history-enabled --max-history-images 5, then repeat the multipart history_images field in oldest-to-newest order on each request. The service does not retain frames between requests.

Backend configuration reference

Relevant DM05InferenceConfig settings:

SettingDefaultMeaning
backend"default"Select default or fast inference
model_max_lengthNoneOptional preprocessing prefix-length limit
vision_trt_engine_pathcheckpoints/trt_engines/dm05_vision.engineTensorRT engine used by the fast backend
build_vision_engine_if_missingTrueEnsure that a matching engine exists during startup
force_rebuild_vision_engineFalseRebuild the engine even if its input shape already matches
prefix_seq_len_buckets[576, 704, 768, 896, 1024]Fixed prefix lengths available to the fast runtime
fast_overflow_policy"fallback"Use dynamic execution or raise when no bucket fits
fast_prefix_qkv_mode"packed"Select packed or separate prefix QKV projection
history_enabledFalseAccept explicit history frames
max_history_images5Maximum history frames accepted by the policy

The fast backend supports batch size 1. Without history frames, the first request assigned to a prefix bucket lazily captures prefix prefill and fixed-step action denoising in one CUDA Graph. Later requests using that bucket stage their inputs into stable buffers and replay the graph. No graph profile is captured during service startup, although CUDA Graph availability is validated when the fast runtime is initialized. A prefix longer than the largest configured bucket either uses the dynamic fallback or raises, according to fast_overflow_policy.

Inference Performance

GPUDefault latencyFast latencyApprox. speedup
RTX 4090 D523.7 ms103.4 ms5.1x
H20610.0 ms129.1 ms4.7x

Measured with a real DM05 checkpoint, BF16 loading, batch size 1, chunk size 10, and 10 diffusion steps. Results use warmed-up CUDA Graph replay with two current images and no history.

These are approximate model-inference speedups, excluding HTTP, preprocessing, and first-request capture. History and overflow fallback performance may differ.

History-frame inference

History is opt-in and explicit: the server does not collect frames between requests. The caller supplies an ordered history_images list with every request. Frames must be ordered from oldest to newest.

Enable history on either backend by adding these CLI options to its startup command:

--history-enabled --max-history-images 5

Both backends accept history frames. The default maximum is five images. The fast backend has a fixed maximum of five and reserves five history slots in its TensorRT vision engine. Therefore, a history-enabled engine for the standard two-camera setup needs seven image slots. Give the fast history service a different engine path from the history-disabled service:

CUDA_VISIBLE_DEVICES=0 python playground/benchmarks/libero/libero_dm05.py \
  --task inference \
  --backend fast \
  --model-name-or-path /path/to/dm05-checkpoint \
  --vision-trt-engine-path checkpoints/trt_engines/dm05-checkpoint-history.engine \
  --history-enabled \
  --max-history-images 5

For this LIBERO configuration, automatic engine building calculates the seven-slot shape. To build the same engine separately, use --num-images 7:

CUDA_VISIBLE_DEVICES=0 python -m dexbotic.model.dm05.infer.fast.build_vision_trt \
  --checkpoint /path/to/dm05-checkpoint \
  --onnx-path checkpoints/trt_engines/dm05_vision_history.onnx \
  --engine-path checkpoints/trt_engines/dm05_vision_history.engine \
  --num-images 7

The RoboTwin2 entrypoint accepts the same two flags. Its fast backend uses three current-view slots plus five fixed history slots, so use a separate eight-slot TensorRT engine path when history is enabled. Automatic engine building selects the eight-slot shape.

When automatic engine building is enabled, the fast backend calculates this slot count automatically. A request containing history still uses TensorRT for vision and the optimized DM05 kernels, but it runs prefix prefill and denoising without a request-level CUDA Graph. Requests without history can continue to use the lazily captured graph profiles. Because the TensorRT input shape is fixed, use different engine files for history-enabled and history-disabled configurations.

The v1 API accepts history as an array of base64-encoded images. The legacy /process_frame route accepts repeated multipart fields named history_images:

curl -X POST \
  -F "text=Put the bowl on the plate" \
  -F "image=@/path/to/agentview.png" \
  -F "image=@/path/to/wrist.png" \
  -F "history_images=@/path/to/history-oldest.png" \
  -F "history_images=@/path/to/history-newest.png" \
  http://localhost:7891/process_frame

See InferenceAPI.md for the v1 JSON schema and DexClient example.

Evaluation

Start an inference server and POST to /process_frame, or point dexbotic-benchmark at that server.

Pass --model_name_or_path at a finished run dir or a checkpoint-*-hf. norm_stats.json must sit next to that path. Full SFT without the flag falls back to ./checkpoints/DM05 (the pretrained base, not your SFT run). Server port 7891. Two images: camera_order=["agentview", "wrist"].

CUDA_VISIBLE_DEVICES=0 python playground/benchmarks/libero/libero_dm05.py \
  --task inference --model_name_or_path ./user_checkpoints/dexbotic/libero_dm05/libero-sft
# or playground/benchmarks/libero/libero_dm05_lora.py --task inference \
#   --model_name_or_path ./user_checkpoints/dexbotic/libero_dm05_lora/libero-lora-MMDD
curl -X POST \
  -F "text=What action should the robot take to put the bowl on the plate?" \
  -F "image=@/path/to/agentview.png" \
  -F "image=@/path/to/wrist.png" \
  http://localhost:7891/process_frame
cd dexbotic-benchmark
docker run --gpus all --network host -v $(pwd):/workspace \
  dexmal/dexbotic_benchmark \
  bash /workspace/scripts/env_sh/libero.sh /workspace/evaluation/configs/libero/example_dm0_libero.yaml

The client must send those two cameras. Docker-free setup: dexbotic-benchmark README. To reproduce the official LIBERO numbers below, serve Dexmal/DM05-libero as in Benchmark Results.

Benchmark Results

Official DM0.5 scores on LIBERO and RoboChallenge Table30 V2.

LIBERO

Four-suite protocol; last column is the average.

MethodSpatialObjectGoalLongAverage
ฯ€096.898.895.885.294.2
ฯ€0.598.898.298.092.496.9
OpenVLA-OFT97.698.497.994.597.1
GR00T N1.797.798.597.594.497.0
StarVLA99.099.898.594.197.9
ABot-M098.899.899.096.698.6
Being-H0.599.299.699.497.498.9
Cosmos Policy98.1100.098.297.698.5
DM0.599.099.899.697.499.0

The DM0.5 row is the official Dexmal/DM05-libero checkpoint. Download it and serve (norm_stats.json is in the repo):

huggingface-cli download Dexmal/DM05-libero \
  --local-dir checkpoints/DM05-libero \
  --local-dir-use-symlinks False

CUDA_VISIBLE_DEVICES=0 python playground/benchmarks/libero/libero_dm05.py \
  --task inference --model_name_or_path ./checkpoints/DM05-libero

RoboChallenge Table30 V2

Real-robot Table30 V2 score and success rate.

MetricDM0.5ฯ€0ฯ€0.5GR00T N1.7
Score54.42-31.48-
SR43.0%-14.3%-