# LingBot-VLA 2.0

LingBot-VLA 2.0 est le modèle vision-langage-action de Robbyant, successeur de LingBot-VLA 1.0. Il associe un backbone Qwen3-VL-4B à un expert d'action Qwen2 en Mixture-of-Experts creux (32 experts, routage top-4) et génère des blocs d'actions par flow matching dans un espace canonique d'état/action de 55 dimensions partagé entre les incarnations.

Le port LeRobot est la politique `lingbot_vla_v2`. Il utilise les jeux de données, les processeurs, `lerobot-train`, `lerobot-eval` et `lerobot-rollout` standards de LeRobot, y compris la distillation de profondeur et de vidéo à double requête de l'amont.

Pour plus de détails sur le modèle, consultez le [dépôt amont](https://github.com/Robbyant/lingbot-vla-v2) et le [rapport technique](https://arxiv.org/abs/2607.06403).

## Installation

```bash
pip install -e ".[training,lingbot_vla_v2]"
```

Le tokenizer et le processeur d'images de Qwen3-VL sont chargés depuis `Qwen/Qwen3-VL-4B-Instruct` (`--policy.tokenizer_path`) lors de la construction du processeur. `flash-attn` n'est pas nécessaire ; l'attention utilise PyTorch SDPA.

## Points de contrôle

| Point de contrôle                        | Source                                               | Utiliser                                                                                                                                       |
| --------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `lerobot/lingbot_vla_v2_base`     | `robbyant/lingbot-vla-v2-6b`, conversion au format uniquement | Point de départ pour le fine-tuning. Les caractéristiques correspondent à la disposition canonique complète de 55 dimensions ; passez les correspondances de slots pour votre robot.                           |
| `lerobot/lingbot_vla_v2_robotwin` | `robbyant/lingbot-vla-v2-6b-robotwin`, étape 50000    | Évaluation RoboTwin 2.0. État/action 14-D, caméras `cam_high`, `cam_left_wrist`, `cam_right_wrist`, normalisation `QUANTILES` (q01/q99). |

Chargez l’un ou l’autre avec `--policy.path` ; le type de politique est lu depuis le `config.json` du point de contrôle.

## Cartographier un robot sur la disposition canonique

Le modèle reçoit un état de 55 dimensions et prédit des actions de 55 dimensions. La disposition est définie par `canonical_joints` dans la configuration :

| Slot                | Dimensions | Contenu               |
| ------------------- | ---- | --------------------- |
| `arm.position`      | 14   | positions des articulations du bras   |
| `end.position`      | 14   | poses de l'effecteur terminal    |
| `effector.position` | 2    | positions du gripper     |
| `hand.position`     | 12   | articulations de la main habile |
| `waist.position`    | 4    | articulations de la taille          |
| `head.position`     | 2    | articulations de tête           |
| `base.velocity`     | 3    | vitesse de base mobile  |
| `reserved.slots`    | 4    | réservé              |

`--policy.state_slots` et `--policy.action_slots` associent les plages de `observation.state` et `action` de votre jeu de données à ces slots, au format JSON :

```json
{
  "observation.state.arm.position": {
    "origin_keys": [{ "observation.state": { "start": 0, "end": 5 } }]
  }
}
```

- Les noms de slots sont les noms canoniques ci-dessus, éventuellement préfixés par `observation.state.` ou `action.`. Les noms inconnus déclenchent une erreur.
- Chaque slot lit une ou plusieurs plages `{start, end}` depuis `observation.state` (slots d'état) ou `action` (slots d'action). Les plages sont concaténées et empilées depuis le début du slot ; le reste du slot est complété par des zéros et exclu de la perte.
- En l'absence de correspondance de slots, les caractéristiques brutes sont interprétées selon la disposition canonique de 55 dimensions, comme dans le point de contrôle de base.
- Les actions relatives sont désactivées par défaut. `--policy.use_relative_actions=true` soustrait l'état actuel de l'action, à l'exception des dimensions dont le nom contient une entrée de `--policy.relative_exclude_joints` (par défaut `["gripper"]`).

Le postprocesseur inverse le mappage, de sorte que `select_action` renvoie des actions dans l'espace d'action brut de votre robot. Les points de contrôle affinés enregistrent les mappages de slots et les statistiques du jeu de données avec leurs processeurs.

### Caméras

L'étape d'image lit les caméras listées dans `input_features` du checkpoint, dans cet ordre. Pour le checkpoint de base, il s'agit de `camera_top`, `camera_wrist_left` et `camera_wrist_right`. Associez les caméras de votre jeu de données à celles-ci à l'aide de `--rename_map` ; les vues dont votre robot ne dispose pas sont masquées.

## Ajustement fin

Exemple sur [`maximellerbach/omx_multicubes`](https://huggingface.co/datasets/maximellerbach/omx_multicubes), un bras unique avec 5 articulations et une pince ainsi que deux caméras (`top`, `wrist`) :

```bash
lerobot-train \
  --dataset.repo_id=maximellerbach/omx_multicubes \
  --policy.path=lerobot/lingbot_vla_v2_base \
  --policy.state_slots='{"observation.state.arm.position": {"origin_keys": [{"observation.state": {"start": 0, "end": 5}}]}, "observation.state.effector.position": {"origin_keys": [{"observation.state": {"start": 5, "end": 6}}]}}' \
  --policy.action_slots='{"action.arm.position": {"origin_keys": [{"action": {"start": 0, "end": 5}}]}, "action.effector.position": {"origin_keys": [{"action": {"start": 5, "end": 6}}]}}' \
  --rename_map='{"observation.images.top": "observation.images.camera_top", "observation.images.wrist": "observation.images.camera_wrist_left"}' \
  --policy.dtype=float32 \
  --accelerator.mixed_precision=bf16 \
  --policy.repo_id=your_repo_id \
  --batch_size=1 \
  --steps=30000 \
  --save_freq=5000 \
  --output_dir=outputs/train/lingbot_vla_v2_omx
```

Conservez les poids en fp32 et effectuez les calculs en bf16 (`--policy.dtype=float32 --accelerator.mixed_precision=bf16`), comme le fait l’implémentation amont. Avec `--policy.dtype=bfloat16`, l’optimiseur met à jour directement les poids en bf16, et la plupart des mises à jour sont trop faibles pour les modifier : sur ce jeu de données, 66 % des poids sont restés identiques au bit près par rapport au point de contrôle de base, et la politique affinée était beaucoup plus saccadée. Effectuez également l’inférence en fp32 (la valeur par défaut d’un point de contrôle entraîné de cette manière) ; l’inférence en bf16 rend les segments d’actions nettement moins fluides.

Les statistiques de normalisation proviennent du jeu de données. L’entraînement multi-GPU utilise le lanceur standard `torchrun` avec les drapeaux `--parallelism.*` et `--accelerator.*`. Les poids, gradients et état de l’optimiseur en fp32 occupent environ 96 Go, il est donc nécessaire de les fractionner avec `--parallelism.dp_shard=<num_gpus>` si une seule GPU est trop petite.

### Distillation à double requête

LingBot-VLA 2.0 ajoute des jetons de requête appris (profondeur et vidéo actuelles et futures) après les jetons de langage, et l'expert d'action leur accorde de l'attention. Les deux points de contrôle contiennent la recette amont dans `align_params`, de sorte que les jetons de requête sont toujours utilisés lors de l'inférence. Pendant l'entraînement, de petites têtes lisent ces jetons et sont distillées à partir d'enseignants figés (LingBot-Depth avec MoGe-2, et DINO-Video) avec les poids de perte amont ; les pertes supplémentaires sont journalisées sous la forme de `depth_loss`, `future_depth_loss`, `current_video_loss` et `future_video_loss`.

Les modèles enseignants sont téléchargés lors de la première étape d’entraînement (environ 3 Go depuis `robbyant/lingbot-vla-v2-6b` et `Ruicheng/moge-2-vitb-normal`), occupent environ 2 Go de mémoire GPU et ajoutent approximativement 130 ms par échantillon à chaque étape. Chaque échantillon charge également le cadre `chunk_size - 1` étapes en avance, ce dont ont besoin les cibles futures. Rien d’autre ne s’exécute lors de l’inférence.

### Paramètres d'entraînement clés

| Paramètre                               | Par défaut                     | Description                                                                |
| --------------------------------------- | --------------------------- | -------------------------------------------------------------------------- |
| `state_slots` / `action_slots`          | `null`                      | Spans bruts vers slots canoniques (voir ci-dessus).                                  |
| `use_relative_actions`                  | `false`                     | Entraînez sur des actions relatives à l'état actuel.                            |
| `loss_type`                             | `L1_fm`                     | Perte de flow-matching sur la vitesse.                                        |
| `normalization_mapping`                 | État et action `MEAN_STD` | Le checkpoint RoboTwin utilise `QUANTILES`.                                  |
| `train_expert_only`                     | `false`                     | Geler le backbone de Qwen3-VL et entraîner uniquement l'expert d'action.             |
| `freeze_vision_encoder`                 | `false`                     | Geler la tour de vision.                                                   |
| `gradient_checkpointing`                | `false`                     | Recalculer les activations lors de la rétropropagation pour économiser de la mémoire au détriment du temps d'étape. |
| `use_moe_expert_lr`                     | `true`                      | Les experts routés s'entraînent à `lr * sqrt(num_experts / top_k)`, comme en amont.     |
| `optimizer_lr` / `scheduler_decay_lr`   | `1e-5` / `1e-5`             | Taux d'apprentissage maximal et final d'AdamW.                                        |
| `resize_imgs_with_padding`              | `[256, 256]`                | Taille du cadre avant le processeur d'images Qwen3-VL, comme en amont.               |
| `image_max_pixels` / `image_min_pixels` | `65536` / `65536`           | Budget de pixels de Qwen3-VL : 64 jetons par caméra, comme en amont.                  |
| `chunk_size` / `n_action_steps`         | `50` / `50`                 | Horizon prédit et nombre d'actions exécutées par bloc.                |
| `num_steps`                             | `10`                        | Étapes de débruitage lors de l'inférence.                                              |
| `tokenizer_path`                        | `Qwen/Qwen3-VL-4B-Instruct` | Tokeniseur et processeur d'images de Qwen3-VL. Utilisez un chemin local hors ligne.          |

Pour utiliser un GPU plus petit, combinez `--policy.train_expert_only=true` et `--policy.gradient_checkpointing=true` avec une petite valeur de `--batch_size`.

## Évaluation

Évaluez le point de contrôle RoboTwin dans l'environnement RoboTwin de LeRobot (voir [RoboTwin](./robotwin) pour la configuration). La carte de renommage aligne les noms des caméras de l'environnement avec ceux du point de contrôle :

```bash
lerobot-eval \
  --policy.path=lerobot/lingbot_vla_v2_robotwin \
  --env.type=robotwin \
  --env.task=beat_block_hammer \
  --eval.batch_size=1 \
  --eval.n_episodes=100 \
  --rename_map='{"observation.images.head_camera": "observation.images.cam_high", "observation.images.left_camera": "observation.images.cam_left_wrist", "observation.images.right_camera": "observation.images.cam_right_wrist"}'
```

Taux de réussite moyen sur les 50 tâches de RoboTwin 2.0 (100 épisodes chacune), tel que rapporté dans le [README amont](https://github.com/Robbyant/lingbot-vla-v2) pour le point de contrôle RoboTwin publié (post-entraînement Muon sur des données propres et aléatoires, inférence en FP32 avec le lanceur RoboTwin amont). Il s’agit de chiffres amont, non mesurés via ce portage.

| Configuration    | π0.5   | LingBot-VLA 1.0 | LingBot-VLA 2.0 |
| ---------- | ------ | --------------- | --------------- |
| Propre      | 82.74% | 88.56%          | **93.52%**      |
| Aléatoire | 76.76% | 86.68%          | **92.80%**      |

## Accélération de l'inférence

Quatre drapeaux, tous désactivés par défaut, accélèrent `select_action`. Ils nécessitent CUDA et des formes d’entrée fixes, sans dépendances supplémentaires :

- `--policy.use_cudagraph_denoise=true` : capture la boucle de débruitage en tant que graphique CUDA unique.
- `--policy.use_cudagraph_prefix=true` : capture le remplissage KV du préfixe vision-langage.
- `--policy.use_cudagraph_prefix_full=true` : capture également la tour de vision dans le graphe préfixe.
- `--policy.compile_predict_velocity=true` : compile la prédiction de vitesse par étape avec `torch.compile`. Le premier appel prend environ une minute pour la compilation.

Il est temps pour un chunk d'action de 50 étapes avec 10 étapes de débruitage, checkpoint RoboTwin, une RTX PRO 6000 :

| Drapeaux                                | bf16    | fp32    |
| ------------------------------------ | ------- | ------- |
| none                                 | ~756 ms | ~614 ms |
| les trois drapeaux de graphes CUDA           | ~180 ms | ~203 ms |
| les trois drapeaux CUDA graph + compile | ~120 ms | ~156 ms |

En bf16, les graphes CUDA produisent les mêmes résultats que le mode eager ; `torch.compile` modifie légèrement les valeurs numériques.

## Déploiement

Exécuter un point de contrôle affiné sur un robot avec `lerobot-rollout`, en transmettant le même `--rename_map` que lors de l’entraînement :

```bash
lerobot-rollout \
  --strategy.type=base \
  --policy.path=outputs/train/lingbot_vla_v2_omx/checkpoints/last/pretrained_model \
  --robot.type=<your_robot_type> \
  --robot.port=<port> \
  --task="pick up the cube" \
  --fps=30 \
  --device=cuda
```

Le découpage en temps réel est disponible avec `--inference.type=rtc`, comme pour les autres politiques de flow-matching.

## Citation

```bibtex
@article{lingbotvla2,
      title={From Foundation to Application: Improving VLA Models in Practice},
      author={Wei Wu and Fangjing Wang and Fan Lu and He Sun and Shi Liu and Yunnan Wang and Yibin Yan and Yong Wang and Shuailei Ma and Xinyang Wang and Yibin Liu and Shuai Yang and Tianxiang Zhou and Kejia Zhang and Lei Zhou and Cheng Su and Nan Xue and Bin Tan and Han Zhang and Youchao Zhang and Fei Liao and Xing Zhu and Yujun Shen and Kecheng Zheng},
      journal={arXiv preprint arXiv:2607.06403},
      year={2026}
}
```

## Licence

Le code et les points de contrôle convertis sont sous licence Apache-2.0, tout comme les versions amont de LingBot-VLA 2.0 et Qwen3-VL.

L’entraînement par distillation télécharge trois enseignants figés sous leurs propres licences (l’inférence ne les charge jamais) :

| Enseignant       | Poids                                      | Licence                                                                           |
| ------------- | -------------------------------------------- | --------------------------------------------------------------------------------- |
| MoGe-2        | `Ruicheng/moge-2-vitb-normal`                | MIT                                                                               |
| LingBot-Depth | `robbyant/lingbot-vla-v2-6b` (`depth/`)      | Apache-2.0                                                                        |
| DINO-Video    | `robbyant/lingbot-vla-v2-6b` (`dino_video/`) | [License de DINOv3](https://github.com/facebookresearch/dinov3/blob/main/LICENSE.md) |

Les poids de DINO-Video sont dérivés de DINOv3 de Meta, de sorte que l’entraînement par distillation implique de les utiliser sous la licence DINOv3, qui, entre autres conditions, vous demande de citer DINOv3 dans vos publications et restreint certains usages finaux.

