[Model] Add InternVLA-A1 offline inference support (#2737)

Signed-off-by: zhuyangkun <zyk_120403228@163.com>
Signed-off-by: bl-wang1 <918395762@qq.com>
Co-authored-by: wangbolun <918395762@qq.com>
This commit is contained in:
yangkun.zhuandwangbolun authored and GitHub committed 2026-04-27 02:28:07 -07:00
1 parent 239abcb480
commit 0016006974
17 files changed
+3509

No files matched your search

+1
View File
@@ -22,6 +22,7 @@ nav:
- HunyuanImage-3.0 Image-to-Text Inference: user_guide/examples/offline_inference/hunyuan_image3.md
- Image-To-Image: user_guide/examples/offline_inference/image_to_image.md
- Image-To-Video: user_guide/examples/offline_inference/image_to_video.md
- InternVLA-A1: user_guide/examples/offline_inference/internvla_a1.md
- MammothModa2-Preview: user_guide/examples/offline_inference/mammothmodal2_preview.md
- MiMo-Audio Offline Inference: user_guide/examples/offline_inference/mimo_audio.md
- Qwen2.5-Omni: user_guide/examples/offline_inference/qwen2_5_omni.md
+1
View File
@@ -20,6 +20,7 @@ th {
| `Qwen3OmniMoeForConditionalGeneration` | Qwen3-Omni | `Qwen/Qwen3-Omni-30B-A3B-Instruct` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `Qwen2_5OmniForConditionalGeneration` | Qwen2.5-Omni | `Qwen/Qwen2.5-Omni-7B`, `Qwen/Qwen2.5-Omni-3B` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `BagelForConditionalGeneration` | BAGEL (DiT-only) | `ByteDance-Seed/BAGEL-7B-MoT` | ✅︎ | ✅︎ | | ✅︎ |
| `InternVLAA1Pipeline` | InternVLA-A1 | `InternRobotics/InternVLA-A1-3B` | ✅︎ | ✅︎ | | |
| `HunyuanImage3ForCausalMM` | HunyuanImage3.0 (DiT-only) | `tencent/HunyuanImage-3.0`, `tencent/HunyuanImage-3.0-Instruct` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `QwenImagePipeline` | Qwen-Image | `Qwen/Qwen-Image` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `QwenImagePipeline` | Qwen-Image-2512 | `Qwen/Qwen-Image-2512` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
@@ -0,0 +1,205 @@
# InternVLA-A1
Source <https://github.com/vllm-project/vllm-omni/tree/main/examples/offline_inference/internvla_a1>.
## Setup
This example is adapted from: https://github.com/InternRobotics/InternVLA-A1/blob/master/tests/policies/internvla_a1_3b/open_loop_genie1_real.ipynb
This example runs the single-path vLLM offline inference workflow for InternVLA-A1 open-loop action prediction.
Before running the script, export the required local paths:
```bash
# hf Jia-Zeng/InternVLA-A1-3B-FineTuned-Place_Markpen
export INTERNVLA_A1_MODEL_DIR=/path/to/InternVLA-A1-3B-ft-pen
# hf download InternRobotics/InternData-A1 real_lerobotv30/genie1/Genie1-Place_Markpen.tar.gz --repo-type dataset --local-dir /path/to/Genie1-Place_Markpen
export INTERNVLA_A1_DATASET_DIR=/path/to/Genie1-Place_Markpen
export INTERNVLA_A1_PROCESSOR_DIR=/path/to/Qwen3-VL-2B-Instruct
# hf tenstep/Cosmos-Tokenizer-CI8x8-SafeTensors
export INTERNVLA_A1_COSMOS_DIR=/path/to/Cosmos-Tokenizer-CI8x8-SafeTensor
```
The shell entrypoint also accepts these variables as defaults, so you can keep the command line itself short.
`INTERNVLA_A1_COSMOS_DIR` is expected to contain:
- `encoder.safetensors`
- `decoder.safetensors`
Reference Hugging Face repo: `tenstep/Cosmos-Tokenizer-CI8x8-SafeTensors`
## Run examples
Get into the example folder:
```bash
cd examples/offline_inference/internvla_a1
```
### Run one sample
This runs a single sample through the maintained vLLM registry path.
```bash
bash run.sh --num-samples 1 --num-episodes 0
```
### Run open-loop evaluation against GT
This runs one episode, writes evaluation metrics, and saves prediction-vs-GT plots when `matplotlib` is available.
```bash
bash run.sh --num-episodes 1
```
Outputs are written under:
```bash
outputs/internvla_a1/vllm_infer/
```
Typical files:
- `summary.json`: top-level run summary
- `registry/log.json`: per-episode GT comparison metrics
- `registry/plots/*.jpg`: prediction-vs-GT figures
### Eager float32 validation
Use eager attention and float32 when you want the most stable numerical comparison baseline.
```bash
bash run.sh \
--num-episodes 1 \
--attn-implementation eager \
--dtype float32
```
### Optional runtime switches
- `--dtype {bfloat16,float32}`: choose inference dtype
- `--attn-implementation {eager,sdpa}`: switch attention backend
- `--enable-regional-compile`: enable regional `torch.compile`
- `--enable-warmup`: run pipeline warmup in initialization
- `--skip-plots`: skip plot generation even if `matplotlib` is installed
### Collect results and performance logs
Use the helper below when you want to collect one-sample output, forward latency, GT comparison, plots, timing logs, and optional pytest output into a single result directory:
```bash
bash collect_results.sh
```
The script writes a timestamped directory under:
```bash
outputs/internvla_a1/collected_results/
```
Typical result files:
- `env_summary.txt`: environment and path summary
- `sample_run.log`: one-sample inference log
- `forward_benchmark/forward_latency.json`: isolated `pipeline.forward` latency summary
- `eval_run.log`: open-loop GT evaluation log
- `eval_outputs/summary.json`: top-level output summary
- `eval_outputs/registry/log.json`: GT comparison metrics
- `eval_outputs/registry/plots/*.jpg`: prediction-vs-GT figures
- `*_time.txt`: timing output
- `*_gpu.csv`: sampled GPU usage
### Benchmark forward latency
Use the dedicated mode below when you want the isolated `pipeline.forward` latency instead of end-to-end script time:
```bash
python end2end.py \
--model-dir "$INTERNVLA_A1_MODEL_DIR" \
--dataset-dir "$INTERNVLA_A1_DATASET_DIR" \
--benchmark-forward \
--dtype bfloat16 \
--attn-implementation eager \
--warmup-iters 3 \
--benchmark-iters 10 \
--output-dir outputs/internvla_a1/forward_benchmark
```
The output JSON is written to `outputs/internvla_a1/forward_benchmark/forward_latency.json` and contains:
- `cold_start_ms`
- `warmup_summary`
- `benchmark_summary`
- `benchmark_samples_ms`
### Reference results
Reference run collected on `1x NVIDIA H200`, `bfloat16`, `eager`:
- one-sample end-to-end run: `38s`
- one-episode GT evaluation run: `45s`
- `average_mse = 1.7173260857816786e-05`
- `average_mae = 0.0011860118247568607`
- `average_mse_joint = 7.42028441891307e-06`
- `average_mae_joint = 0.0010777723509818316`
- `average_mse_gripper = 8.544408774469048e-05`
- `average_mae_gripper = 0.0019436875591054559`
The reference plot filename is:
- `eval_outputs/registry/plots/vllm_registry_open_loop_ep0.jpg`
## Measure latency and VRAM
You can collect end-to-end runtime and peak host memory with:
```bash
/usr/bin/time -v bash run.sh --num-episodes 1
```
For GPU memory snapshots, use:
```bash
nvidia-smi --query-gpu=name,memory.total,memory.used --format=csv
```
Run `nvidia-smi` before and during inference to record the peak device memory for your PR report.
## FAQ
If `matplotlib` is missing, evaluation still runs and only plot generation is skipped. To enable plots:
```bash
pip install matplotlib
```
## Example materials
- `end2end.py`: main offline inference and GT evaluation entrypoint
- `run.sh`: shell wrapper with local path env vars
- `collect_results.sh`: helper to gather result summaries and performance logs into one directory
- `internvla_a1_common.py`: dataset, evaluation, and plotting helpers
## Embedded source listings
??? abstract "collect_results.sh"
``````sh
--8<-- "examples/offline_inference/internvla_a1/collect_results.sh"
``````
??? abstract "end2end.py"
``````py
--8<-- "examples/offline_inference/internvla_a1/end2end.py"
``````
??? abstract "internvla_a1_common.py"
``````py
--8<-- "examples/offline_inference/internvla_a1/internvla_a1_common.py"
``````
??? abstract "run.sh"
``````sh
--8<-- "examples/offline_inference/internvla_a1/run.sh"
``````
??? abstract "standalone/outputs/qwena1/standalone_infer/log.json"
``````json
--8<-- "examples/offline_inference/internvla_a1/standalone/outputs/qwena1/standalone_infer/log.json"
``````
@@ -0,0 +1,30 @@
# InternVLA-A1
Full usage and result-reporting guidance lives in [docs/user_guide/examples/offline_inference/internvla_a1.md](../../../docs/user_guide/examples/offline_inference/internvla_a1.md).
Quick start:
```bash
export INTERNVLA_A1_MODEL_DIR=/path/to/InternVLA-A1-3B-ft-pen
export INTERNVLA_A1_DATASET_DIR=/path/to/Genie1-Place_Markpen
export INTERNVLA_A1_PROCESSOR_DIR=/path/to/Qwen3-VL-2B-Instruct
# hf tenstep/Cosmos-Tokenizer-CI8x8-SafeTensors
export INTERNVLA_A1_COSMOS_DIR=/path/to/Cosmos-Tokenizer-CI8x8-SafeTensor
bash run.sh --num-samples 1 --num-episodes 0
bash run.sh --num-episodes 1
bash collect_results.sh
```
Expected files under `INTERNVLA_A1_COSMOS_DIR`:
- `encoder.safetensors`
- `decoder.safetensors`
Reference Hugging Face repo: `tenstep/Cosmos-Tokenizer-CI8x8-SafeTensors`
Key entrypoints:
- `run.sh`: wrapper for offline inference and GT evaluation
- `collect_results.sh`: collect sample output, latency, metrics, plots, and logs
- `end2end.py`: underlying Python entrypoint
@@ -0,0 +1,220 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$ROOT_DIR/../../.." && pwd)"
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
RESULT_ROOT="${INTERNVLA_A1_RESULT_DIR:-$REPO_ROOT/outputs/internvla_a1/collected_results/$TIMESTAMP}"
: "${INTERNVLA_A1_MODEL_DIR:?Please export INTERNVLA_A1_MODEL_DIR=/path/to/InternVLA-A1-3B-ft-pen}"
: "${INTERNVLA_A1_DATASET_DIR:?Please export INTERNVLA_A1_DATASET_DIR=/path/to/Genie1-Place_Markpen}"
: "${INTERNVLA_A1_PROCESSOR_DIR:?Please export INTERNVLA_A1_PROCESSOR_DIR=/path/to/Qwen3-VL-2B-Instruct}"
: "${INTERNVLA_A1_COSMOS_DIR:?Please export INTERNVLA_A1_COSMOS_DIR=/path/to/Cosmos-Tokenizer-CI8x8-SafeTensor}"
EVAL_OUTPUT_DIR="$RESULT_ROOT/eval_outputs"
GPU_MONITOR_INTERVAL="${GPU_MONITOR_INTERVAL:-1}"
mkdir -p "$RESULT_ROOT"
export INTERNVLA_A1_MODEL_DIR
export INTERNVLA_A1_DATASET_DIR
export INTERNVLA_A1_PROCESSOR_DIR
export INTERNVLA_A1_COSMOS_DIR
write_env_summary() {
cat >"$RESULT_ROOT/env_summary.txt" <<EOF
timestamp=$TIMESTAMP
repo_root=$REPO_ROOT
result_root=$RESULT_ROOT
python=$(command -v python || true)
python_version=$(python --version 2>&1 || true)
model_dir=$INTERNVLA_A1_MODEL_DIR
dataset_dir=$INTERNVLA_A1_DATASET_DIR
processor_dir=$INTERNVLA_A1_PROCESSOR_DIR
cosmos_dir=$INTERNVLA_A1_COSMOS_DIR
pwd=$(pwd)
EOF
}
capture_gpu_snapshot() {
local output_file="$1"
if command -v nvidia-smi >/dev/null 2>&1; then
nvidia-smi --query-gpu=index,name,memory.total,memory.used,utilization.gpu --format=csv >"$output_file" 2>&1 || true
else
echo "nvidia-smi not found" >"$output_file"
fi
}
start_gpu_monitor() {
local output_file="$1"
if ! command -v nvidia-smi >/dev/null 2>&1; then
echo "nvidia-smi not found" >"$output_file"
return 1
fi
(
echo "timestamp,index,name,memory.total [MiB],memory.used [MiB],utilization.gpu [%]"
while true; do
local now
now="$(date '+%Y-%m-%d %H:%M:%S')"
nvidia-smi --query-gpu=index,name,memory.total,memory.used,utilization.gpu --format=csv,noheader,nounits \
| awk -F',' -v ts="$now" '{gsub(/^[ \t]+|[ \t]+$/, "", $0); print ts "," $0}'
sleep "$GPU_MONITOR_INTERVAL"
done
) >"$output_file" 2>/dev/null &
echo $!
}
stop_gpu_monitor() {
local monitor_pid="${1:-}"
if [[ -n "$monitor_pid" ]] && kill -0 "$monitor_pid" 2>/dev/null; then
kill "$monitor_pid" 2>/dev/null || true
wait "$monitor_pid" 2>/dev/null || true
fi
}
copy_eval_outputs() {
local source_dir="$1"
local target_dir="$2"
local source_real=""
local target_real=""
source_real="$(realpath "$source_dir" 2>/dev/null || echo "$source_dir")"
target_real="$(realpath "$target_dir" 2>/dev/null || echo "$target_dir")"
if [[ "$source_real" == "$target_real" ]]; then
return 0
fi
mkdir -p "$target_dir"
if [[ -d "$source_dir" ]]; then
cp -r "$source_dir"/. "$target_dir"/
fi
}
run_with_artifacts() {
local name="$1"
shift
local log_file="$RESULT_ROOT/${name}.log"
local time_file="$RESULT_ROOT/${name}_time.txt"
local gpu_file="$RESULT_ROOT/${name}_gpu.csv"
local status_file="$RESULT_ROOT/${name}_status.txt"
local monitor_pid=""
monitor_pid="$(start_gpu_monitor "$gpu_file" || true)"
set +e
local exit_code=0
local start_ts=0
local end_ts=0
start_ts="$(date +%s)"
if [[ -x /usr/bin/time ]]; then
/usr/bin/time -v -o "$time_file" "$@" >"$log_file" 2>&1
exit_code=$?
if [[ ! -s "$time_file" ]]; then
end_ts="$(date +%s)"
{
echo "timing_mode=usr_bin_time_empty_fallback"
echo "start_ts=$start_ts"
echo "end_ts=$end_ts"
echo "elapsed_seconds=$((end_ts - start_ts))"
} >"$time_file"
fi
else
"$@" >"$log_file" 2>&1
exit_code=$?
end_ts="$(date +%s)"
{
echo "timing_mode=shell_date"
echo "start_ts=$start_ts"
echo "end_ts=$end_ts"
echo "elapsed_seconds=$((end_ts - start_ts))"
} >"$time_file"
fi
set -e
stop_gpu_monitor "$monitor_pid"
echo "exit_code=$exit_code" >"$status_file"
if [[ $exit_code -ne 0 ]]; then
echo "[error] command failed for $name, see $log_file"
return $exit_code
fi
}
write_manifest() {
cat >"$RESULT_ROOT/README.txt" <<EOF
InternVLA-A1 collected results
Key files:
- env_summary.txt: environment and path summary
- sample_run.log / sample_run_time.txt: one-sample functional run
- forward_benchmark.log / forward_benchmark_time.txt: pure pipeline.forward latency benchmark
- eval_run.log / eval_run_time.txt: GT evaluation run
- pytest_e2e.log / pytest_e2e_time.txt: offline e2e pytest result
- gpu_info_before.csv / gpu_info_after.csv: point-in-time GPU snapshots
- *_gpu.csv: sampled GPU usage during each run
- eval_outputs/: copied output directory from the GT evaluation run
Important outputs:
- forward_benchmark/forward_latency.json
- eval_outputs/summary.json
- eval_outputs/registry/log.json
- eval_outputs/registry/plots/
EOF
}
write_skip_artifact() {
local name="$1"
local reason="$2"
echo "$reason" >"$RESULT_ROOT/${name}.log"
echo "timing_mode=skipped" >"$RESULT_ROOT/${name}_time.txt"
echo "exit_code=0" >"$RESULT_ROOT/${name}_status.txt"
echo "skipped_reason=$reason" >>"$RESULT_ROOT/${name}_status.txt"
if [[ ! -f "$RESULT_ROOT/${name}_gpu.csv" ]]; then
echo "skipped,$reason" >"$RESULT_ROOT/${name}_gpu.csv"
fi
}
write_env_summary
write_manifest
capture_gpu_snapshot "$RESULT_ROOT/gpu_info_before.csv"
run_with_artifacts \
"sample_run" \
bash "$ROOT_DIR/run.sh" \
--output-dir "$RESULT_ROOT/sample_outputs" \
--num-samples 1 \
--num-episodes 0
run_with_artifacts \
"forward_benchmark" \
python "$ROOT_DIR/end2end.py" \
--model-dir "$INTERNVLA_A1_MODEL_DIR" \
--dataset-dir "$INTERNVLA_A1_DATASET_DIR" \
--benchmark-forward \
--dtype bfloat16 \
--attn-implementation eager \
--warmup-iters 3 \
--benchmark-iters 10 \
--output-dir "$RESULT_ROOT/forward_benchmark"
run_with_artifacts \
"eval_run" \
bash "$ROOT_DIR/run.sh" \
--output-dir "$EVAL_OUTPUT_DIR" \
--num-episodes 1
copy_eval_outputs "$EVAL_OUTPUT_DIR" "$RESULT_ROOT/eval_outputs"
if python - <<'PY' >/dev/null 2>&1
import importlib.util
raise SystemExit(0 if importlib.util.find_spec("pytest") else 1)
PY
then
run_with_artifacts \
"pytest_e2e" \
python -m pytest -sv tests/e2e/offline_inference/test_internvla_a1.py -m advanced_model
else
write_skip_artifact "pytest_e2e" "pytest is not installed in the current python environment"
fi
capture_gpu_snapshot "$RESULT_ROOT/gpu_info_after.csv"
echo "Results written to: $RESULT_ROOT"
+327
View File
@@ -0,0 +1,327 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import statistics
import sys
import time
from pathlib import Path
import torch
_WORKSPACE_ROOT = Path(__file__).resolve().parent
_REPO_ROOT = _WORKSPACE_ROOT.parents[2]
if str(_REPO_ROOT) not in sys.path:
# Keep the shared helper import clean by doing script-only path bootstrap here.
sys.path.insert(0, str(_REPO_ROOT))
from internvla_a1_common import ( # noqa: E402
A2DOpenLoopDataset,
collate_open_loop_samples,
make_shared_noise,
run_open_loop_evaluation,
select_indices,
tensor_dtype,
tensor_sha256,
)
from vllm_omni.diffusion.data import OmniDiffusionConfig # noqa: E402
from vllm_omni.diffusion.models.internvla_a1 import ( # noqa: E402
InternVLAA1Config,
InternVLAA1TrainMetadata,
)
from vllm_omni.diffusion.models.internvla_a1.config import OBS_STATE # noqa: E402
from vllm_omni.diffusion.registry import initialize_model # noqa: E402
from vllm_omni.diffusion.request import OmniDiffusionRequest # noqa: E402
from vllm_omni.inputs.data import OmniDiffusionSamplingParams # noqa: E402
def _required_path_arg(env_name: str, cli_value: str | None) -> str:
value = cli_value or os.getenv(env_name)
if not value:
raise ValueError(f"Missing required path: set --{env_name.lower().replace('_', '-')} or {env_name}.")
return value
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run vLLM InternVLA-A1 inference on a few samples.")
parser.add_argument("--model-dir")
parser.add_argument("--dataset-dir")
parser.add_argument("--num-samples", type=int, default=1)
parser.add_argument("--num-episodes", type=int, default=0)
parser.add_argument("--seed", type=int, default=1234)
parser.add_argument("--dtype", choices=["bfloat16", "float32"], default="bfloat16")
parser.add_argument("--device", default="cuda")
parser.add_argument("--compile-model", action="store_true")
parser.add_argument("--attn-implementation", default="eager")
parser.add_argument("--enable-regional-compile", action="store_true")
parser.add_argument("--enable-warmup", action="store_true")
parser.add_argument("--strict-load", action="store_true")
parser.add_argument("--output-dir", default="outputs/internvla_a1/vllm_infer")
parser.add_argument("--skip-plots", action="store_true")
parser.add_argument("--benchmark-forward", action="store_true")
parser.add_argument("--warmup-iters", type=int, default=3)
parser.add_argument("--benchmark-iters", type=int, default=10)
return parser.parse_args()
def build_od_config(args: argparse.Namespace, processor_model_name: str) -> OmniDiffusionConfig:
return OmniDiffusionConfig(
model=str(Path(args.model_dir).resolve()),
model_class_name="InternVLAA1Pipeline",
dtype=tensor_dtype(args.dtype),
custom_pipeline_args={
"device": args.device,
"dtype": args.dtype,
"compile_model": args.compile_model,
"attn_implementation": args.attn_implementation,
"enable_regional_compile": args.enable_regional_compile,
"enable_warmup": args.enable_warmup,
"strict_load": args.strict_load,
"processor_model_name": processor_model_name,
},
)
def build_dataset(args: argparse.Namespace) -> tuple[A2DOpenLoopDataset, InternVLAA1Config, InternVLAA1TrainMetadata]:
model_dir = Path(args.model_dir)
config = InternVLAA1Config.from_pretrained(model_dir)
config.device = args.device
config.dtype = args.dtype
config.compile_model = args.compile_model
config.attn_implementation = args.attn_implementation
config.enable_regional_compile = args.enable_regional_compile
train_meta = InternVLAA1TrainMetadata.from_pretrained(model_dir)
with open(model_dir / "stats.json", encoding="utf-8") as f:
train_stats = json.load(f)["a2d"]
dataset = A2DOpenLoopDataset(
args.dataset_dir,
config=config,
train_stats=train_stats,
)
return dataset, config, train_meta
def run_one_path(
pipeline,
dataset: A2DOpenLoopDataset,
config: InternVLAA1Config,
args: argparse.Namespace,
indices: list[int],
) -> list[dict[str, object]]:
results: list[dict[str, object]] = []
for index in indices:
sample = dataset.get_sample(index)
batch_inputs, _ = collate_open_loop_samples([sample], device=args.device, dtype=tensor_dtype(args.dtype))
noise = make_shared_noise(
args.seed,
index,
(
batch_inputs[OBS_STATE].shape[0],
config.chunk_size,
config.max_action_dim,
),
args.device,
)
pred = run_pipeline_forward(pipeline, batch_inputs, noise)
pred = pred[:, :, : dataset.physical_action_dim].to(torch.float32).cpu()
results.append(
{
"path": "registry",
"index": index,
"episode_index": sample.episode_index,
"task": sample.task,
"seed": args.seed,
"shape": list(pred.shape),
"mean": float(pred.mean().item()),
"std": float(pred.std().item()),
"action_sha256": tensor_sha256(pred),
"first_action_prefix": pred[0, 0, :8].tolist(),
}
)
return results
def run_pipeline_forward(
pipeline,
batch_inputs: dict[str, torch.Tensor],
noise: torch.Tensor,
) -> torch.Tensor:
output = pipeline.forward(
OmniDiffusionRequest(
prompts=[""],
sampling_params=OmniDiffusionSamplingParams(
extra_args={
"batch_inputs": batch_inputs,
"noise": noise,
"decode_image": False,
}
),
)
)
if output.error:
raise RuntimeError(output.error)
if output.output is None:
raise RuntimeError("InternVLAA1Pipeline.forward returned no output tensor.")
return output.output
def _synchronize(device: str) -> None:
if device.startswith("cuda") and torch.cuda.is_available():
torch.cuda.synchronize()
def _latency_summary(values_ms: list[float]) -> dict[str, float]:
sorted_values = sorted(values_ms)
p50_index = min(len(sorted_values) - 1, round(0.50 * (len(sorted_values) - 1)))
p90_index = min(len(sorted_values) - 1, round(0.90 * (len(sorted_values) - 1)))
return {
"mean_ms": float(statistics.mean(sorted_values)),
"stdev_ms": float(statistics.pstdev(sorted_values)) if len(sorted_values) > 1 else 0.0,
"min_ms": float(sorted_values[0]),
"max_ms": float(sorted_values[-1]),
"p50_ms": float(sorted_values[p50_index]),
"p90_ms": float(sorted_values[p90_index]),
}
def benchmark_forward(
pipeline,
dataset: A2DOpenLoopDataset,
config: InternVLAA1Config,
args: argparse.Namespace,
index: int,
output_dir: Path,
) -> dict[str, object]:
sample = dataset.get_sample(index)
batch_inputs, _ = collate_open_loop_samples([sample], device=args.device, dtype=tensor_dtype(args.dtype))
noise = make_shared_noise(
args.seed,
index,
(
batch_inputs[OBS_STATE].shape[0],
config.chunk_size,
config.max_action_dim,
),
args.device,
)
_synchronize(args.device)
cold_start_begin = time.perf_counter()
pred = run_pipeline_forward(pipeline, batch_inputs, noise)
_synchronize(args.device)
cold_start_ms = (time.perf_counter() - cold_start_begin) * 1000.0
warmup_ms: list[float] = []
for _ in range(args.warmup_iters):
_synchronize(args.device)
begin = time.perf_counter()
_ = run_pipeline_forward(pipeline, batch_inputs, noise)
_synchronize(args.device)
warmup_ms.append((time.perf_counter() - begin) * 1000.0)
benchmark_ms: list[float] = []
for _ in range(args.benchmark_iters):
_synchronize(args.device)
begin = time.perf_counter()
_ = run_pipeline_forward(pipeline, batch_inputs, noise)
_synchronize(args.device)
benchmark_ms.append((time.perf_counter() - begin) * 1000.0)
pred = pred[:, :, : dataset.physical_action_dim].to(torch.float32).cpu()
summary = {
"mode": "forward_latency",
"model_dir": str(Path(args.model_dir).resolve()),
"dataset_dir": str(Path(args.dataset_dir).resolve()),
"sample_index": index,
"episode_index": sample.episode_index,
"task": sample.task,
"device": args.device,
"dtype": args.dtype,
"attn_implementation": args.attn_implementation,
"enable_regional_compile": args.enable_regional_compile,
"warmup_iters": args.warmup_iters,
"benchmark_iters": args.benchmark_iters,
"output_shape": list(pred.shape),
"cold_start_ms": cold_start_ms,
"warmup_summary": _latency_summary(warmup_ms) if warmup_ms else {},
"benchmark_summary": _latency_summary(benchmark_ms) if benchmark_ms else {},
"benchmark_samples_ms": benchmark_ms,
}
with open(output_dir / "forward_latency.json", "w", encoding="utf-8") as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print(json.dumps(summary, ensure_ascii=False, indent=2))
return summary
def main() -> None:
args = parse_args()
args.model_dir = _required_path_arg("INTERNVLA_A1_MODEL_DIR", args.model_dir)
args.dataset_dir = _required_path_arg("INTERNVLA_A1_DATASET_DIR", args.dataset_dir)
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
dataset, config, train_meta = build_dataset(args)
indices = select_indices(dataset, args.num_samples)
od_config = build_od_config(args, train_meta.processor_model_name)
eval_summaries: dict[str, object] = {}
pipeline = initialize_model(od_config)
if args.benchmark_forward:
benchmark_forward(
pipeline=pipeline,
dataset=dataset,
config=config,
args=args,
index=indices[0],
output_dir=output_dir,
)
return
results = run_one_path(
pipeline=pipeline,
dataset=dataset,
config=config,
args=args,
indices=indices,
)
if args.num_episodes > 0:
eval_summaries["registry"] = run_open_loop_evaluation(
mode="vllm_registry",
policy=pipeline,
config=config,
dataset=dataset,
train_meta=train_meta,
collate_samples=collate_open_loop_samples,
run_sample_actions=lambda policy, batch_inputs, noise: run_pipeline_forward(policy, batch_inputs, noise),
num_episodes=args.num_episodes,
seed=args.seed,
device=args.device,
dtype=tensor_dtype(args.dtype),
output_dir=output_dir / "registry",
skip_plots=args.skip_plots,
)
summary = {
"mode": "registry",
"model_dir": str(Path(args.model_dir).resolve()),
"dataset_dir": str(Path(args.dataset_dir).resolve()),
"device": args.device,
"dtype": args.dtype,
"attn_implementation": args.attn_implementation,
"enable_regional_compile": args.enable_regional_compile,
"seed": args.seed,
"indices": indices,
"results": results,
"output_dir": str(output_dir.resolve()),
"eval_summaries": eval_summaries,
}
with open(output_dir / "summary.json", "w", encoding="utf-8") as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print(json.dumps(summary, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
@@ -0,0 +1,471 @@
from __future__ import annotations
import hashlib
import json
import math
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import numpy as np
import pyarrow.parquet as pq
import torch
import torch.nn.functional as F
import torchvision
from vllm_omni.diffusion.models.internvla_a1.config import ( # noqa: E402
OBS_IMAGES,
OBS_STATE,
OBS_TASK,
InternVLAA1Config,
)
def tensor_dtype(name: str) -> torch.dtype:
return torch.bfloat16 if name == "bfloat16" else torch.float32
def select_indices(dataset, num_samples: int) -> list[int]:
indices: list[int] = []
for _, episode_indices in dataset.episode_start_indices(max_episodes=num_samples):
if episode_indices:
indices.append(episode_indices[0])
return indices[:num_samples]
def make_shared_noise(seed: int, sample_index: int, shape: tuple[int, ...], device: str) -> torch.Tensor:
generator = torch.Generator(device="cpu")
generator.manual_seed(seed + sample_index)
noise = torch.randn(shape, generator=generator, dtype=torch.float32)
return noise.to(device=device)
def tensor_sha256(tensor: torch.Tensor) -> str:
array = tensor.detach().contiguous().cpu().numpy()
return hashlib.sha256(array.tobytes()).hexdigest()
def _clamp_index(index: int, start: int, end: int) -> int:
return max(start, min(end - 1, index))
def _load_parquet_rows(path: Path) -> list[dict[str, Any]]:
return pq.read_table(path).to_pylist()
def _load_json(path: Path) -> dict[str, Any]:
with open(path, encoding="utf-8") as f:
return json.load(f)
def _stack_stats(stats: dict[str, Any], keys: list[str]) -> dict[str, torch.Tensor]:
result = {}
for stat_name in ("mean", "std"):
values = []
for key in keys:
values.extend(stats[key][stat_name])
result[stat_name] = torch.tensor(values, dtype=torch.float32)
return result
def normalize_vector(values: torch.Tensor, stats: dict[str, torch.Tensor]) -> torch.Tensor:
denom = torch.where(stats["std"] == 0, torch.ones_like(stats["std"]), stats["std"])
return (values - stats["mean"]) / denom
def unnormalize_vector(values: torch.Tensor, stats: dict[str, torch.Tensor]) -> torch.Tensor:
return values * stats["std"] + stats["mean"]
class TorchvisionVideoReaderCache:
def __init__(self, backend: str = "pyav") -> None:
self.backend = backend
self._readers: dict[str, Any] = {}
torchvision.set_video_backend(backend)
def get(self, path: str) -> Any:
reader = self._readers.get(path)
if reader is None:
reader = torchvision.io.VideoReader(path, "video")
self._readers[path] = reader
return reader
def decode_frames(self, path: str, timestamps: list[float], tolerance_s: float = 1e-4) -> torch.Tensor:
reader = self.get(path)
first_ts = min(timestamps)
last_ts = max(timestamps)
reader.seek(first_ts, keyframes_only=self.backend == "pyav")
loaded_frames: list[torch.Tensor] = []
loaded_ts: list[float] = []
for frame in reader:
current_ts = float(frame["pts"])
loaded_frames.append(frame["data"])
loaded_ts.append(current_ts)
if current_ts >= last_ts:
break
query_ts = torch.tensor(timestamps, dtype=torch.float32)
loaded_ts_tensor = torch.tensor(loaded_ts, dtype=torch.float32)
distances = torch.cdist(query_ts[:, None], loaded_ts_tensor[:, None], p=1)
min_dist, argmin = distances.min(dim=1)
if not torch.all(min_dist < tolerance_s):
raise RuntimeError(
f"Video timestamps are outside tolerance: query={query_ts.tolist()} "
f"loaded={loaded_ts_tensor.tolist()} path={path}"
)
return torch.stack([loaded_frames[i] for i in argmin]).float() / 255.0
@dataclass
class A2DOpenLoopSample:
index: int
episode_index: int
task: str
state_raw: torch.Tensor
action_raw: torch.Tensor
inputs: dict[str, torch.Tensor]
class A2DOpenLoopDataset:
image_keys = [
"observation.images.head",
"observation.images.hand_left",
"observation.images.hand_right",
]
state_keys = [
"observation.states.joint.position",
"observation.states.effector.position",
]
action_keys = [
"actions.joint.position",
"actions.effector.position",
]
def __init__(
self,
dataset_root: str | Path,
*,
config: InternVLAA1Config,
train_stats: dict[str, Any],
image_offsets: tuple[int, int] = (-15, 0),
tolerance_s: float = 1e-4,
) -> None:
self.root = Path(dataset_root)
self.config = config
self.info = _load_json(self.root / "meta" / "info.json")
self.dataset_stats = _load_json(self.root / "meta" / "stats.json")
self.data_rows = _load_parquet_rows(self.root / "data" / "chunk-000" / "file-000.parquet")
self.episode_rows = _load_parquet_rows(self.root / "meta" / "episodes" / "chunk-000" / "file-000.parquet")
self.task_rows = _load_parquet_rows(self.root / "meta" / "tasks.parquet")
self.state_stats = _stack_stats(train_stats, self.state_keys)
self.action_stats = _stack_stats(train_stats, self.action_keys)
self.image_offsets = image_offsets
self.tolerance_s = tolerance_s
self.video_reader = TorchvisionVideoReaderCache(backend="pyav")
@property
def num_episodes(self) -> int:
return len(self.episode_rows)
@property
def physical_action_dim(self) -> int:
return 16
def episode_start_indices(self, max_episodes: int | None = None) -> list[tuple[int, list[int]]]:
rows = self.episode_rows if max_episodes is None else self.episode_rows[:max_episodes]
result = []
for ep in rows:
start = int(ep["dataset_from_index"])
end = int(ep["dataset_to_index"])
result.append((int(ep["episode_index"]), list(range(start, end, self.config.chunk_size))))
return result
def _task_text(self, task_index: int) -> str:
return self.task_rows[task_index]["__index_level_0__"]
def _episode_for_index(self, row: dict[str, Any]) -> dict[str, Any]:
return self.episode_rows[int(row["episode_index"])]
def _state_vector(self, row: dict[str, Any]) -> torch.Tensor:
return torch.tensor(row[self.state_keys[0]] + row[self.state_keys[1]], dtype=torch.float32)
def _action_vector(self, row: dict[str, Any]) -> torch.Tensor:
return torch.tensor(row[self.action_keys[0]] + row[self.action_keys[1]], dtype=torch.float32)
def _query_rows(self, idx: int, deltas: list[int]) -> list[dict[str, Any]]:
row = self.data_rows[idx]
episode = self._episode_for_index(row)
start = int(episode["dataset_from_index"])
end = int(episode["dataset_to_index"])
return [self.data_rows[_clamp_index(idx + delta, start, end)] for delta in deltas]
def _decode_camera_history(
self, episode: dict[str, Any], camera_key: str, rows: list[dict[str, Any]]
) -> torch.Tensor:
timestamps = [float(r["timestamp"]) for r in rows]
shifted = [float(episode[f"videos/{camera_key}/from_timestamp"]) + ts for ts in timestamps]
chunk_idx = int(episode[f"videos/{camera_key}/chunk_index"])
file_idx = int(episode[f"videos/{camera_key}/file_index"])
path = self.root / self.info["video_path"].format(
video_key=camera_key,
chunk_index=chunk_idx,
file_index=file_idx,
)
frames = self.video_reader.decode_frames(str(path), shifted, tolerance_s=self.tolerance_s)
return frames
def get_sample(self, idx: int) -> A2DOpenLoopSample:
row = self.data_rows[idx]
episode = self._episode_for_index(row)
image_rows = self._query_rows(idx, list((-15, 0)))
action_rows = self._query_rows(idx, list(range(self.config.chunk_size)))
camera_images = [self._decode_camera_history(episode, camera_key, image_rows) for camera_key in self.image_keys]
state_raw = self._state_vector(row)
state_norm = normalize_vector(state_raw, self.state_stats)
action_raw = torch.stack([self._action_vector(action_row) for action_row in action_rows], dim=0)
task = self._task_text(int(row["task_index"]))
inputs = {
OBS_STATE: state_norm,
OBS_TASK: task,
f"{OBS_IMAGES}.image0": camera_images[0],
f"{OBS_IMAGES}.image1": camera_images[1],
f"{OBS_IMAGES}.image2": camera_images[2],
f"{OBS_IMAGES}.image0_mask": torch.tensor(True),
f"{OBS_IMAGES}.image1_mask": torch.tensor(True),
f"{OBS_IMAGES}.image2_mask": torch.tensor(True),
}
return A2DOpenLoopSample(
index=idx,
episode_index=int(row["episode_index"]),
task=task,
state_raw=state_raw,
action_raw=action_raw,
inputs=inputs,
)
def collate_open_loop_samples(
samples: list[A2DOpenLoopSample],
*,
device: str,
dtype: torch.dtype,
) -> tuple[dict[str, torch.Tensor], dict[str, Any]]:
first = samples[0]
batch_inputs: dict[str, torch.Tensor] = {}
for key in first.inputs:
values = [sample.inputs[key] for sample in samples]
if isinstance(values[0], torch.Tensor):
tensor = torch.stack(values, dim=0)
if tensor.dtype in (torch.int64, torch.bool):
batch_inputs[key] = tensor.to(device=device)
else:
batch_inputs[key] = tensor.to(device=device, dtype=dtype)
else:
batch_inputs[key] = values
metadata = {
"indices": [sample.index for sample in samples],
"episode_indices": [sample.episode_index for sample in samples],
"tasks": [sample.task for sample in samples],
"state_raw": torch.stack([sample.state_raw for sample in samples], dim=0),
"action_raw": torch.stack([sample.action_raw for sample in samples], dim=0),
}
return batch_inputs, metadata
def plot_prediction_series(
*,
series: dict[str, np.ndarray],
output_path: Path,
title: str,
) -> None:
import matplotlib.pyplot as plt
first_series = next(iter(series.values()))
num_dims = int(first_series.shape[1])
num_cols = 2
num_rows = math.ceil(num_dims / num_cols)
fig, axs = plt.subplots(num_rows, num_cols, figsize=(16, max(6, 3 * num_rows)))
axs = np.array(axs).reshape(-1)
x_values = np.arange(first_series.shape[0])
styles = [
("Ground Truth", "blue", "-", 1.5),
("VLLM", "green", "-.", 1.4),
("Predicted", "red", "--", 1.4),
]
style_map = {name: (color, linestyle, linewidth) for name, color, linestyle, linewidth in styles}
for dim in range(num_dims):
ax = axs[dim]
for name, values in series.items():
color, linestyle, linewidth = style_map.get(name, (None, "-", 1.2))
ax.plot(
x_values,
values[:, dim],
label=name,
color=color,
linestyle=linestyle,
linewidth=linewidth,
)
ax.set_title(f"Dimension {dim + 1}")
ax.set_xlabel("Time Step / Sample Index")
ax.set_ylabel(f"Value Dim {dim + 1}")
ax.legend(loc="upper right")
ax.grid(True, linestyle="--", alpha=0.7)
for dim in range(num_dims, len(axs)):
axs[dim].axis("off")
fig.suptitle(title, fontsize=16)
plt.tight_layout(rect=(0, 0, 1, 0.97))
output_path.parent.mkdir(parents=True, exist_ok=True)
plt.savefig(output_path)
plt.close(fig)
def summarize_prediction_metrics(
*,
gt_tensor: torch.Tensor,
pred_tensor: torch.Tensor,
joint_dims: int = 14,
) -> dict[str, float]:
return {
"mse": float(F.mse_loss(gt_tensor, pred_tensor, reduction="mean").item()),
"mae": float(F.l1_loss(gt_tensor, pred_tensor, reduction="mean").item()),
"mse_joint": float(F.mse_loss(gt_tensor[:, :joint_dims], pred_tensor[:, :joint_dims], reduction="mean").item()),
"mae_joint": float(F.l1_loss(gt_tensor[:, :joint_dims], pred_tensor[:, :joint_dims], reduction="mean").item()),
"mse_gripper": float(
F.mse_loss(gt_tensor[:, joint_dims:], pred_tensor[:, joint_dims:], reduction="mean").item()
),
"mae_gripper": float(
F.l1_loss(gt_tensor[:, joint_dims:], pred_tensor[:, joint_dims:], reduction="mean").item()
),
}
def summarize_metric_list(values: list[float]) -> float | None:
return float(np.mean(values)) if values else None
def run_open_loop_evaluation(
*,
mode: str,
policy,
config,
dataset,
train_meta,
collate_samples: Callable[..., tuple[dict[str, torch.Tensor], dict[str, Any]]],
run_sample_actions: Callable[[Any, dict[str, torch.Tensor], torch.Tensor], torch.Tensor],
num_episodes: int,
seed: int,
device: str,
dtype: torch.dtype,
output_dir: str | Path,
skip_plots: bool,
) -> dict[str, Any]:
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
plots_dir = output_dir / "plots"
plots_dir.mkdir(parents=True, exist_ok=True)
mse_values: list[float] = []
mae_values: list[float] = []
mse_joint_values: list[float] = []
mae_joint_values: list[float] = []
mse_gripper_values: list[float] = []
mae_gripper_values: list[float] = []
per_episode: list[dict[str, Any]] = []
plotting_available = True
if not skip_plots:
try:
import matplotlib.pyplot as _plt # noqa: F401
except ModuleNotFoundError:
plotting_available = False
print("[warning] matplotlib is not installed; plots will be skipped.")
episode_specs = dataset.episode_start_indices(max_episodes=num_episodes)
for episode_index, indices in episode_specs:
print(f"[{mode}] episode: {episode_index}")
pred_chunks = []
gt_chunks = []
visited_indices = []
task = None
for index in indices:
sample = dataset.get_sample(index)
task = sample.task
batch_inputs, meta = collate_samples([sample], device=device, dtype=dtype)
noise = make_shared_noise(
seed,
index,
(batch_inputs[OBS_STATE].shape[0], config.chunk_size, config.max_action_dim),
device,
)
with torch.no_grad():
pred = run_sample_actions(policy, batch_inputs, noise)
pred = pred[:, :, : dataset.physical_action_dim].to(torch.float32).cpu()
pred_phys = unnormalize_vector(pred, dataset.action_stats)
if train_meta.action_mode == "delta":
pred_phys[:, :, :14] += meta["state_raw"][:, None, :14]
gt_phys = meta["action_raw"][:, :, : dataset.physical_action_dim].to(torch.float32).cpu()
pred_chunks.append(pred_phys[0])
gt_chunks.append(gt_phys[0])
visited_indices.append(index)
pred_tensor = torch.cat(pred_chunks, dim=0)
gt_tensor = torch.cat(gt_chunks, dim=0)
metrics = summarize_prediction_metrics(gt_tensor=gt_tensor, pred_tensor=pred_tensor)
mse_values.append(metrics["mse"])
mae_values.append(metrics["mae"])
mse_joint_values.append(metrics["mse_joint"])
mae_joint_values.append(metrics["mae_joint"])
mse_gripper_values.append(metrics["mse_gripper"])
mae_gripper_values.append(metrics["mae_gripper"])
if not skip_plots and plotting_available:
plot_prediction_series(
series={
"Ground Truth": gt_tensor.numpy(),
"VLLM": pred_tensor.numpy(),
},
output_path=plots_dir / f"{mode}_open_loop_ep{episode_index}.jpg",
title=f"{mode} Ground Truth vs VLLM",
)
episode_log = {
"episode_id": int(episode_index),
"task": task,
"visited_indices": visited_indices,
"num_pred_steps": int(pred_tensor.shape[0]),
**metrics,
}
per_episode.append(episode_log)
print(json.dumps({"mode": mode, **episode_log}, ensure_ascii=False))
summary = {
"mode": mode,
"num_episodes": len(per_episode),
"mse": mse_values,
"mae": mae_values,
"average_mse": summarize_metric_list(mse_values),
"average_mae": summarize_metric_list(mae_values),
"mse_joint": mse_joint_values,
"mae_joint": mae_joint_values,
"average_mse_joint": summarize_metric_list(mse_joint_values),
"average_mae_joint": summarize_metric_list(mae_joint_values),
"mse_gripper": mse_gripper_values,
"mae_gripper": mae_gripper_values,
"average_mse_gripper": summarize_metric_list(mse_gripper_values),
"average_mae_gripper": summarize_metric_list(mae_gripper_values),
"episodes": per_episode,
}
with open(output_dir / "log.json", "w", encoding="utf-8") as f:
json.dump(summary, f, indent=2, ensure_ascii=False)
return summary
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$ROOT_DIR/../../.." && pwd)"
: "${INTERNVLA_A1_MODEL_DIR:?Please export INTERNVLA_A1_MODEL_DIR=/path/to/InternVLA-A1-3B-ft-pen}"
: "${INTERNVLA_A1_DATASET_DIR:?Please export INTERNVLA_A1_DATASET_DIR=/path/to/Genie1-Place_Markpen}"
: "${INTERNVLA_A1_PROCESSOR_DIR:?Please export INTERNVLA_A1_PROCESSOR_DIR=/path/to/Qwen3-VL-2B-Instruct}"
: "${INTERNVLA_A1_COSMOS_DIR:?Please export INTERNVLA_A1_COSMOS_DIR=/path/to/Cosmos-Tokenizer-CI8x8-SafeTensor}"
INTERNVLA_A1_OUTPUT_DIR="${INTERNVLA_A1_OUTPUT_DIR:-$REPO_ROOT/outputs/internvla_a1/vllm_infer}"
export INTERNVLA_A1_MODEL_DIR
export INTERNVLA_A1_DATASET_DIR
export INTERNVLA_A1_PROCESSOR_DIR
export INTERNVLA_A1_COSMOS_DIR
python "$ROOT_DIR/end2end.py" \
--model-dir "$INTERNVLA_A1_MODEL_DIR" \
--dataset-dir "$INTERNVLA_A1_DATASET_DIR" \
--output-dir "$INTERNVLA_A1_OUTPUT_DIR" \
--num-episodes "${INTERNVLA_A1_NUM_EPISODES:-1}" \
--attn-implementation eager \
"$@"
@@ -0,0 +1,90 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Offline E2E test for InternVLA-A1 open-loop inference."""
from __future__ import annotations
import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path
import pytest
from tests.helpers.mark import hardware_test
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "1"
REPO_ROOT = Path(__file__).resolve().parents[3]
EXAMPLE_SCRIPT = REPO_ROOT / "examples" / "offline_inference" / "internvla_a1" / "end2end.py"
def _required_env(name: str) -> str:
value = os.getenv(name)
if not value:
pytest.skip(f"{name} is not set")
return value
@pytest.mark.core_model
@pytest.mark.advanced_model
@pytest.mark.diffusion
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
def test_internvla_a1_offline_open_loop(run_level: str) -> None:
if run_level != "advanced_model":
pytest.skip("InternVLA-A1 offline evaluation requires real local checkpoints and dataset.")
model_dir = _required_env("INTERNVLA_A1_MODEL_DIR")
dataset_dir = _required_env("INTERNVLA_A1_DATASET_DIR")
processor_dir = _required_env("INTERNVLA_A1_PROCESSOR_DIR")
cosmos_dir = _required_env("INTERNVLA_A1_COSMOS_DIR")
env = os.environ.copy()
env["INTERNVLA_A1_PROCESSOR_DIR"] = processor_dir
env["INTERNVLA_A1_COSMOS_DIR"] = cosmos_dir
with tempfile.TemporaryDirectory(prefix="internvla_a1_e2e_") as tmpdir:
output_dir = Path(tmpdir) / "outputs"
cmd = [
sys.executable,
str(EXAMPLE_SCRIPT),
"--model-dir",
model_dir,
"--dataset-dir",
dataset_dir,
"--output-dir",
str(output_dir),
"--num-samples",
"1",
"--num-episodes",
"1",
"--dtype",
"float32",
"--attn-implementation",
"eager",
"--skip-plots",
]
subprocess.run(cmd, check=True, env=env)
summary_path = output_dir / "summary.json"
assert summary_path.exists(), f"Missing summary file: {summary_path}"
with open(summary_path, encoding="utf-8") as f:
summary = json.load(f)
assert summary["mode"] == "registry"
assert summary["attn_implementation"] == "eager"
assert summary["dtype"] == "float32"
assert len(summary["results"]) == 1
assert "registry" in summary["eval_summaries"]
eval_summary = summary["eval_summaries"]["registry"]
assert eval_summary["num_episodes"] == 1
assert eval_summary["average_mse"] is not None
assert eval_summary["average_mae"] is not None
log_path = output_dir / "registry" / "log.json"
assert log_path.exists(), f"Missing evaluation log: {log_path}"
@@ -0,0 +1,25 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""InternVLA-A1 diffusion model components."""
from vllm_omni.diffusion.models.internvla_a1.config import (
InternVLAA1Config,
InternVLAA1TrainMetadata,
)
from vllm_omni.diffusion.models.internvla_a1.model_internvla_a1 import (
InternVLAA1,
InternVLAA1Policy,
)
from vllm_omni.diffusion.models.internvla_a1.pipeline_internvla_a1 import (
InternVLAA1Pipeline,
get_internvla_a1_post_process_func,
)
__all__ = [
"InternVLAA1",
"InternVLAA1Config",
"InternVLAA1Pipeline",
"InternVLAA1TrainMetadata",
"InternVLAA1Policy",
"get_internvla_a1_post_process_func",
]
@@ -0,0 +1,252 @@
from __future__ import annotations
import torch
import torch.nn as nn
from transformers.models.qwen3_vl.modeling_qwen3_vl import (
ALL_ATTENTION_FUNCTIONS,
BaseModelOutputWithPast,
Cache,
DynamicCache,
FlashAttentionKwargs,
Qwen3VLConfig,
Qwen3VLPreTrainedModel,
Qwen3VLTextConfig,
Qwen3VLTextRotaryEmbedding,
Qwen3VLVisionModel,
Unpack,
apply_rotary_pos_emb,
check_model_inputs,
create_causal_mask,
deprecate_kwarg,
eager_attention_forward,
)
from transformers.models.qwen3_vl.modeling_qwen3_vl import (
Qwen3VLForConditionalGeneration as HFQwen3VLForConditionalGeneration,
)
from transformers.models.qwen3_vl.modeling_qwen3_vl import (
Qwen3VLModel as HFQwen3VLModel,
)
from transformers.models.qwen3_vl.modeling_qwen3_vl import (
Qwen3VLTextAttention as HFQwen3VLTextAttention,
)
from transformers.models.qwen3_vl.modeling_qwen3_vl import (
Qwen3VLTextDecoderLayer as HFQwen3VLTextDecoderLayer,
)
from transformers.models.qwen3_vl.modeling_qwen3_vl import (
Qwen3VLTextModel as HFQwen3VLTextModel,
)
from transformers.models.qwen3_vl.modeling_qwen3_vl import (
Qwen3VLTextRMSNorm as HFQwen3VLTextRMSNorm,
)
class Qwen3VLTextRMSNorm(HFQwen3VLTextRMSNorm):
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
input_dtype = hidden_states.dtype
hidden_states = hidden_states.to(torch.float32)
variance = hidden_states.pow(2).mean(-1, keepdim=True)
hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
return (self.weight * hidden_states).to(input_dtype)
class Qwen3VLTextAttention(HFQwen3VLTextAttention):
def __init__(self, config: Qwen3VLTextConfig, layer_idx: int):
super().__init__(config, layer_idx)
self.q_norm = Qwen3VLTextRMSNorm(self.head_dim, eps=config.rms_norm_eps)
self.k_norm = Qwen3VLTextRMSNorm(self.head_dim, eps=config.rms_norm_eps)
@deprecate_kwarg("past_key_value", new_name="past_key_values", version="4.58")
def forward(
self,
hidden_states: torch.Tensor,
position_embeddings: tuple[torch.Tensor, torch.Tensor],
attention_mask: torch.Tensor | None,
past_key_values: Cache | None = None,
cache_position: torch.LongTensor | None = None,
use_cache: bool = False,
**kwargs: Unpack[FlashAttentionKwargs],
) -> tuple[torch.Tensor, torch.Tensor | None]:
input_shape = hidden_states.shape[:-1]
hidden_shape = (*input_shape, -1, self.head_dim)
query_states = self.q_norm(self.q_proj(hidden_states).view(hidden_shape)).transpose(1, 2)
key_states = self.k_norm(self.k_proj(hidden_states).view(hidden_shape)).transpose(1, 2)
value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)
cos, sin = position_embeddings
query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin)
if past_key_values is not None:
if use_cache:
cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position}
key_states, value_states = past_key_values.update(
key_states, value_states, self.layer_idx, cache_kwargs
)
else:
key_states = torch.cat([past_key_values[self.layer_idx][0], key_states], dim=2)
value_states = torch.cat([past_key_values[self.layer_idx][1], value_states], dim=2)
attention_interface = eager_attention_forward
if self.config._attn_implementation != "eager":
attention_interface = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation]
attn_output, attn_weights = attention_interface(
self,
query_states,
key_states,
value_states,
attention_mask,
dropout=0.0 if not self.training else self.attention_dropout,
scaling=self.scaling,
**kwargs,
)
attn_output = attn_output.reshape(*input_shape, -1).contiguous()
attn_output = self.o_proj(attn_output)
return attn_output, attn_weights
class Qwen3VLTextDecoderLayer(HFQwen3VLTextDecoderLayer):
def __init__(self, config: Qwen3VLTextConfig, layer_idx: int):
super().__init__(config, layer_idx)
self.self_attn = Qwen3VLTextAttention(config=config, layer_idx=layer_idx)
self.input_layernorm = Qwen3VLTextRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.post_attention_layernorm = Qwen3VLTextRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
class Qwen3VLTextModel(HFQwen3VLTextModel):
config: Qwen3VLTextConfig
_no_split_modules = ["Qwen3VLTextDecoderLayer"]
_repeated_blocks = ["Qwen3VLTextDecoderLayer"]
def __init__(self, config: Qwen3VLTextConfig):
Qwen3VLPreTrainedModel.__init__(self, config)
self.padding_idx = config.pad_token_id
self.vocab_size = config.vocab_size
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)
self.layers = nn.ModuleList(
[Qwen3VLTextDecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
)
self.norm = Qwen3VLTextRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.rotary_emb = Qwen3VLTextRotaryEmbedding(config=config)
self.gradient_checkpointing = False
self.post_init()
@check_model_inputs
def forward(
self,
input_ids: torch.LongTensor = None,
attention_mask: torch.Tensor | None = None,
position_ids: torch.LongTensor | None = None,
past_key_values: Cache | None = None,
inputs_embeds: torch.FloatTensor | None = None,
use_cache: bool | None = None,
cache_position: torch.LongTensor | None = None,
visual_pos_masks: torch.Tensor | None = None,
deepstack_visual_embeds: list[torch.Tensor] | None = None,
**kwargs: Unpack[FlashAttentionKwargs],
) -> tuple | BaseModelOutputWithPast:
if (input_ids is None) ^ (inputs_embeds is not None):
raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
if use_cache and past_key_values is None and not torch.jit.is_tracing():
past_key_values = DynamicCache(config=self.config)
if inputs_embeds is None:
inputs_embeds = self.embed_tokens(input_ids)
if cache_position is None:
past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
cache_position = torch.arange(
past_seen_tokens, past_seen_tokens + inputs_embeds.shape[1], device=inputs_embeds.device
)
if position_ids is None:
position_ids = cache_position.view(1, 1, -1).expand(3, inputs_embeds.shape[0], -1)
elif position_ids.ndim == 2:
position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1)
if position_ids.ndim == 3 and position_ids.shape[0] == 4:
text_position_ids = position_ids[0]
position_ids = position_ids[1:]
else:
text_position_ids = position_ids[0]
attention_mask = create_causal_mask(
config=self.config,
input_embeds=inputs_embeds,
attention_mask=attention_mask,
cache_position=cache_position,
past_key_values=past_key_values,
position_ids=text_position_ids,
)
hidden_states = inputs_embeds
position_embeddings = self.rotary_emb(hidden_states, position_ids)
for layer_idx, decoder_layer in enumerate(self.layers):
hidden_states = decoder_layer(
hidden_states,
attention_mask=attention_mask,
position_ids=text_position_ids,
past_key_values=past_key_values,
use_cache=use_cache,
cache_position=cache_position,
position_embeddings=position_embeddings,
**kwargs,
)
if deepstack_visual_embeds is not None and layer_idx in range(len(deepstack_visual_embeds)):
hidden_states = self._deepstack_process(
hidden_states,
visual_pos_masks,
deepstack_visual_embeds[layer_idx],
)
hidden_states = self.norm(hidden_states)
return BaseModelOutputWithPast(
last_hidden_state=hidden_states,
past_key_values=past_key_values,
)
class Qwen3VLModel(HFQwen3VLModel):
base_model_prefix = ""
_checkpoint_conversion_mapping = {}
accepts_loss_kwargs = False
config: Qwen3VLConfig
_no_split_modules = ["Qwen3VLTextDecoderLayer", "Qwen3VLVisionBlock"]
def __init__(self, config):
Qwen3VLPreTrainedModel.__init__(self, config)
self.visual = Qwen3VLVisionModel._from_config(config.vision_config)
self.language_model = Qwen3VLTextModel._from_config(config.text_config)
self.rope_deltas = None
self.post_init()
class Qwen3VLForConditionalGeneration(HFQwen3VLForConditionalGeneration):
_checkpoint_conversion_mapping = {}
_tied_weights_keys = ["lm_head.weight"]
accepts_loss_kwargs = False
config: Qwen3VLConfig
def __init__(self, config):
Qwen3VLPreTrainedModel.__init__(self, config)
self.model = Qwen3VLModel(config)
self.lm_head = nn.Linear(config.text_config.hidden_size, config.text_config.vocab_size, bias=False)
self.post_init()
__all__ = [
"Qwen3VLVisionModel",
"Qwen3VLForConditionalGeneration",
"Qwen3VLModel",
"Qwen3VLPreTrainedModel",
"Qwen3VLTextModel",
"apply_rotary_pos_emb",
"eager_attention_forward",
]
@@ -0,0 +1,126 @@
from __future__ import annotations
import json
import os
from dataclasses import dataclass, field
from dataclasses import fields as dataclass_fields
from pathlib import Path
from typing import Any
from huggingface_hub.constants import HF_HOME
ACTION = "action"
OBS_STR = "observation"
OBS_PREFIX = OBS_STR + "."
OBS_STATE = OBS_STR + ".state"
OBS_TASK = OBS_STR + ".task"
OBS_IMAGE = OBS_STR + ".image"
OBS_IMAGES = OBS_IMAGE + "s"
OPENPI_ATTENTION_MASK_VALUE = -2.3819763e38
DEFAULT_QWEN3_VL_MODEL = os.getenv("INTERNVLA_A1_PROCESSOR_DIR", "Qwen/Qwen3-VL-2B-Instruct")
DEFAULT_COSMOS_REPO = "nvidia/Cosmos-Tokenizer-CI8x8"
DEFAULT_COSMOS_DIR = Path(
os.getenv(
"INTERNVLA_A1_COSMOS_DIR",
str(Path(HF_HOME) / "hub" / "Cosmos-Tokenizer-CI8x8"),
)
)
@dataclass
class InternVLAA1Config:
"""Standalone-compatible InternVLA-A1 config with a few fake-smoke defaults."""
type: str = "internvla_a1"
qwen3_vl_variant: str = "qwen3_vl_28l"
action_expert_variant: str = "qwen3_28l"
dtype: str = "bfloat16"
device: str = "cuda"
chunk_size: int = 50
n_action_steps: int = 50
max_state_dim: int = 32
max_action_dim: int = 32
num_inference_steps: int = 10
time_sampling_beta_alpha: float = 1.5
time_sampling_beta_beta: float = 1.0
time_sampling_scale: float = 0.999
time_sampling_offset: float = 0.001
min_period: float = 4e-3
max_period: float = 4.0
image_resolution: tuple[int, int] = (224, 224)
empty_cameras: int = 0
gradient_checkpointing: bool = False
compile_model: bool = False
compile_mode: str = "max-autotune"
enable_regional_compile: bool = False
regional_compile_dynamic: bool = True
enable_suffix_static_context_optimization: bool = False
attn_implementation: str = "eager"
tokenizer_max_length: int = 48
freeze_vision_encoder: bool = False
train_expert_only: bool = False
train_vlm_only: bool = False
scale_factor: int = 8
lambda_gen: float = 0.01
input_features: dict[str, Any] = field(default_factory=dict)
output_features: dict[str, Any] = field(default_factory=dict)
# Fake/smoke-only knobs used by the lightweight pipeline path.
vocab_size: int = 256
hidden_size: int = 128
intermediate_size: int = 256
num_attention_heads: int = 4
num_hidden_layers: int = 2
num_cameras: int = 3
image_history: int = 2
pixel_feature_dim: int = 48
@classmethod
def from_pretrained(cls, checkpoint_dir: str | Path) -> InternVLAA1Config:
checkpoint_dir = Path(checkpoint_dir)
with open(checkpoint_dir / "config.json", encoding="utf-8") as f:
raw = json.load(f)
return cls.from_model_config(raw)
@classmethod
def from_model_config(cls, model_config: dict[str, Any] | None) -> InternVLAA1Config:
if not model_config:
return cls()
raw = dict(model_config)
raw["image_resolution"] = tuple(raw.get("image_resolution", [224, 224]))
allowed = {item.name for item in dataclass_fields(cls)}
filtered = {key: value for key, value in raw.items() if key in allowed}
return cls(**filtered)
@dataclass
class InternVLAA1TrainMetadata:
action_mode: str = "delta"
processor_model_name: str = DEFAULT_QWEN3_VL_MODEL
@classmethod
def from_pretrained(cls, checkpoint_dir: str | Path) -> InternVLAA1TrainMetadata:
checkpoint_dir = Path(checkpoint_dir)
path = checkpoint_dir / "train_config.json"
if not path.exists():
return cls()
with open(path, encoding="utf-8") as f:
raw = json.load(f)
dataset_cfg = raw.get("dataset", {})
processor_model_name = DEFAULT_QWEN3_VL_MODEL
for transform in dataset_cfg.get("data_transforms", {}).get("inputs", []):
if transform.get("type") == "internvla_a1_processor":
processor_model_name = transform.get(
"pretrained_model_name_or_path",
processor_model_name,
)
break
return cls(
action_mode=dataset_cfg.get("action_mode", "delta"),
processor_model_name=processor_model_name,
)
@@ -0,0 +1,416 @@
# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# This file vendors the minimal Continuous Image (CI) tokenizer implementation
# needed by InternVLA-A1 from NVIDIA/Cosmos-Tokenizer. The code is intentionally
# reduced to the CI image path only so the runtime does not depend on the
# external `cosmos_tokenizer` package.
from __future__ import annotations
import math
from collections import OrderedDict
import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange
_WAVELETS = {
"haar": torch.tensor([0.7071067811865476, 0.7071067811865476]),
"rearrange": torch.tensor([1.0, 1.0]),
}
_PERSISTENT = False
def nonlinearity(x: torch.Tensor) -> torch.Tensor:
return x * torch.sigmoid(x)
def normalize(in_channels: int, num_groups: int = 32) -> nn.GroupNorm:
return nn.GroupNorm(num_groups=num_groups, num_channels=in_channels, eps=1e-6, affine=True)
class IdentityDistribution(nn.Module):
def forward(self, parameters: torch.Tensor) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]:
zeros = torch.tensor([0.0], device=parameters.device, dtype=parameters.dtype)
return parameters, (zeros, zeros)
class Patcher(nn.Module):
def __init__(self, patch_size: int = 1, patch_method: str = "haar") -> None:
super().__init__()
self.patch_size = patch_size
self.patch_method = patch_method
self.register_buffer("wavelets", _WAVELETS[patch_method], persistent=_PERSISTENT)
self.range = range(int(torch.log2(torch.tensor(self.patch_size)).item()))
self.register_buffer(
"_arange",
torch.arange(_WAVELETS[patch_method].shape[0]),
persistent=_PERSISTENT,
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
if self.patch_method == "haar":
return self._haar(x)
if self.patch_method == "rearrange":
return self._arrange(x)
raise ValueError(f"Unknown patch method: {self.patch_method}")
def _dwt(self, x: torch.Tensor, mode: str = "reflect", *, rescale: bool = False) -> torch.Tensor:
dtype = x.dtype
h = self.wavelets
n = h.shape[0]
g = x.shape[1]
hl = h.flip(0).reshape(1, 1, -1).repeat(g, 1, 1).to(dtype=dtype)
hh = (h * ((-1) ** self._arange)).reshape(1, 1, -1).repeat(g, 1, 1).to(dtype=dtype)
x = F.pad(x, pad=(n - 2, n - 1, n - 2, n - 1), mode=mode).to(dtype)
xl = F.conv2d(x, hl.unsqueeze(2), groups=g, stride=(1, 2))
xh = F.conv2d(x, hh.unsqueeze(2), groups=g, stride=(1, 2))
xll = F.conv2d(xl, hl.unsqueeze(3), groups=g, stride=(2, 1))
xlh = F.conv2d(xl, hh.unsqueeze(3), groups=g, stride=(2, 1))
xhl = F.conv2d(xh, hl.unsqueeze(3), groups=g, stride=(2, 1))
xhh = F.conv2d(xh, hh.unsqueeze(3), groups=g, stride=(2, 1))
out = torch.cat([xll, xlh, xhl, xhh], dim=1)
if rescale:
out = out / 2
return out
def _haar(self, x: torch.Tensor) -> torch.Tensor:
for _ in self.range:
x = self._dwt(x, rescale=True)
return x
def _arrange(self, x: torch.Tensor) -> torch.Tensor:
return rearrange(
x,
"b c (h p1) (w p2) -> b (c p1 p2) h w",
p1=self.patch_size,
p2=self.patch_size,
).contiguous()
class UnPatcher(nn.Module):
def __init__(self, patch_size: int = 1, patch_method: str = "haar") -> None:
super().__init__()
self.patch_size = patch_size
self.patch_method = patch_method
self.register_buffer("wavelets", _WAVELETS[patch_method], persistent=_PERSISTENT)
self.range = range(int(torch.log2(torch.tensor(self.patch_size)).item()))
self.register_buffer(
"_arange",
torch.arange(_WAVELETS[patch_method].shape[0]),
persistent=_PERSISTENT,
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
if self.patch_method == "haar":
return self._ihaar(x)
if self.patch_method == "rearrange":
return self._iarrange(x)
raise ValueError(f"Unknown patch method: {self.patch_method}")
def _idwt(self, x: torch.Tensor, *, rescale: bool = False) -> torch.Tensor:
dtype = x.dtype
h = self.wavelets
n = h.shape[0]
g = x.shape[1] // 4
hl = h.flip([0]).reshape(1, 1, -1).repeat([g, 1, 1]).to(dtype=dtype)
hh = (h * ((-1) ** self._arange)).reshape(1, 1, -1).repeat(g, 1, 1).to(dtype=dtype)
xll, xlh, xhl, xhh = torch.chunk(x.to(dtype), 4, dim=1)
yl = F.conv_transpose2d(xll, hl.unsqueeze(3), groups=g, stride=(2, 1), padding=(n - 2, 0))
yl += F.conv_transpose2d(xlh, hh.unsqueeze(3), groups=g, stride=(2, 1), padding=(n - 2, 0))
yh = F.conv_transpose2d(xhl, hl.unsqueeze(3), groups=g, stride=(2, 1), padding=(n - 2, 0))
yh += F.conv_transpose2d(xhh, hh.unsqueeze(3), groups=g, stride=(2, 1), padding=(n - 2, 0))
y = F.conv_transpose2d(yl, hl.unsqueeze(2), groups=g, stride=(1, 2), padding=(0, n - 2))
y += F.conv_transpose2d(yh, hh.unsqueeze(2), groups=g, stride=(1, 2), padding=(0, n - 2))
if rescale:
y = y * 2
return y
def _ihaar(self, x: torch.Tensor) -> torch.Tensor:
for _ in self.range:
x = self._idwt(x, rescale=True)
return x
def _iarrange(self, x: torch.Tensor) -> torch.Tensor:
return rearrange(
x,
"b (c p1 p2) h w -> b c (h p1) (w p2)",
p1=self.patch_size,
p2=self.patch_size,
)
class Upsample(nn.Module):
def __init__(self, in_channels: int) -> None:
super().__init__()
self.conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=1, padding=1)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = x.repeat_interleave(2, dim=2).repeat_interleave(2, dim=3)
return self.conv(x)
class Downsample(nn.Module):
def __init__(self, in_channels: int) -> None:
super().__init__()
self.conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=2, padding=0)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.conv(F.pad(x, (0, 1, 0, 1), mode="constant", value=0))
class ResnetBlock(nn.Module):
def __init__(self, *, in_channels: int, out_channels: int | None = None, dropout: float, **_: object) -> None:
super().__init__()
out_channels = in_channels if out_channels is None else out_channels
self.norm1 = normalize(in_channels)
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1)
self.norm2 = normalize(out_channels)
self.dropout = nn.Dropout(dropout)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)
self.nin_shortcut = (
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0)
if in_channels != out_channels
else nn.Identity()
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
h = self.conv1(nonlinearity(self.norm1(x)))
h = self.conv2(self.dropout(nonlinearity(self.norm2(h))))
return self.nin_shortcut(x) + h
class AttnBlock(nn.Module):
def __init__(self, in_channels: int) -> None:
super().__init__()
self.norm = normalize(in_channels)
self.q = nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0)
self.k = nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0)
self.v = nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0)
self.proj_out = nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0)
def forward(self, x: torch.Tensor) -> torch.Tensor:
h = self.norm(x)
q = self.q(h)
k = self.k(h)
v = self.v(h)
b, c, height, width = q.shape
q = q.reshape(b, c, height * width).permute(0, 2, 1)
k = k.reshape(b, c, height * width)
attn = torch.bmm(q, k) * (int(c) ** -0.5)
attn = F.softmax(attn, dim=2)
v = v.reshape(b, c, height * width)
attn = attn.permute(0, 2, 1)
h = torch.bmm(v, attn).reshape(b, c, height, width)
return x + self.proj_out(h)
class Encoder(nn.Module):
def __init__(
self,
in_channels: int,
channels: int,
channels_mult: list[int],
num_res_blocks: int,
attn_resolutions: list[int],
dropout: float,
resolution: int,
z_channels: int,
spatial_compression: int,
**kwargs: object,
) -> None:
super().__init__()
self.num_resolutions = len(channels_mult)
self.num_res_blocks = num_res_blocks
patch_size = int(kwargs.get("patch_size", 1))
self.patcher = Patcher(patch_size, str(kwargs.get("patch_method", "rearrange")))
in_channels = in_channels * patch_size * patch_size
self.num_downsamples = int(math.log2(spatial_compression)) - int(math.log2(patch_size))
assert self.num_downsamples <= self.num_resolutions
self.conv_in = nn.Conv2d(in_channels, channels, kernel_size=3, stride=1, padding=1)
curr_res = resolution // patch_size
in_ch_mult = (1,) + tuple(channels_mult)
self.down = nn.ModuleList()
block_in = channels
for i_level in range(self.num_resolutions):
block = nn.ModuleList()
attn = nn.ModuleList()
block_in = channels * in_ch_mult[i_level]
block_out = channels * channels_mult[i_level]
for _ in range(self.num_res_blocks):
block.append(ResnetBlock(in_channels=block_in, out_channels=block_out, dropout=dropout))
block_in = block_out
if curr_res in attn_resolutions:
attn.append(AttnBlock(block_in))
down = nn.Module()
down.block = block
down.attn = attn
if i_level < self.num_downsamples:
down.downsample = Downsample(block_in)
curr_res //= 2
self.down.append(down)
self.mid = nn.Module()
self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in, dropout=dropout)
self.mid.attn_1 = AttnBlock(block_in)
self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in, dropout=dropout)
self.norm_out = normalize(block_in)
self.conv_out = nn.Conv2d(block_in, z_channels, kernel_size=3, stride=1, padding=1)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.patcher(x)
hs = [self.conv_in(x)]
for i_level in range(self.num_resolutions):
for i_block in range(self.num_res_blocks):
h = self.down[i_level].block[i_block](hs[-1])
if len(self.down[i_level].attn) > 0:
h = self.down[i_level].attn[i_block](h)
hs.append(h)
if i_level < self.num_downsamples:
hs.append(self.down[i_level].downsample(hs[-1]))
h = hs[-1]
h = self.mid.block_1(h)
h = self.mid.attn_1(h)
h = self.mid.block_2(h)
h = nonlinearity(self.norm_out(h))
return self.conv_out(h)
class Decoder(nn.Module):
def __init__(
self,
out_channels: int,
channels: int,
channels_mult: list[int],
num_res_blocks: int,
attn_resolutions: list[int],
dropout: float,
resolution: int,
z_channels: int,
spatial_compression: int,
**kwargs: object,
) -> None:
super().__init__()
self.num_resolutions = len(channels_mult)
self.num_res_blocks = num_res_blocks
patch_size = int(kwargs.get("patch_size", 1))
self.unpatcher = UnPatcher(patch_size, str(kwargs.get("patch_method", "rearrange")))
out_ch = out_channels * patch_size * patch_size
self.num_upsamples = int(math.log2(spatial_compression)) - int(math.log2(patch_size))
assert self.num_upsamples <= self.num_resolutions
block_in = channels * channels_mult[self.num_resolutions - 1]
curr_res = (resolution // patch_size) // 2 ** (self.num_resolutions - 1)
self.z_shape = (1, z_channels, curr_res, curr_res)
self.conv_in = nn.Conv2d(z_channels, block_in, kernel_size=3, stride=1, padding=1)
self.mid = nn.Module()
self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in, dropout=dropout)
self.mid.attn_1 = AttnBlock(block_in)
self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in, dropout=dropout)
self.up = nn.ModuleList()
for i_level in reversed(range(self.num_resolutions)):
block = nn.ModuleList()
attn = nn.ModuleList()
block_out = channels * channels_mult[i_level]
for _ in range(self.num_res_blocks + 1):
block.append(ResnetBlock(in_channels=block_in, out_channels=block_out, dropout=dropout))
block_in = block_out
if curr_res in attn_resolutions:
attn.append(AttnBlock(block_in))
up = nn.Module()
up.block = block
up.attn = attn
if i_level >= (self.num_resolutions - self.num_upsamples):
up.upsample = Upsample(block_in)
curr_res *= 2
self.up.insert(0, up)
self.norm_out = normalize(block_in)
self.conv_out = nn.Conv2d(block_in, out_ch, kernel_size=3, stride=1, padding=1)
def forward(self, z: torch.Tensor) -> torch.Tensor:
h = self.conv_in(z)
h = self.mid.block_1(h)
h = self.mid.attn_1(h)
h = self.mid.block_2(h)
for i_level in reversed(range(self.num_resolutions)):
for i_block in range(self.num_res_blocks + 1):
h = self.up[i_level].block[i_block](h)
if len(self.up[i_level].attn) > 0:
h = self.up[i_level].attn[i_block](h)
if i_level >= (self.num_resolutions - self.num_upsamples):
h = self.up[i_level].upsample(h)
h = self.conv_out(nonlinearity(self.norm_out(h)))
return self.unpatcher(h)
class ContinuousImageTokenizer(nn.Module):
def __init__(self, z_channels: int, z_factor: int, latent_channels: int, **kwargs: object) -> None:
super().__init__()
self.latent_channels = latent_channels
self.encoder = Encoder(z_channels=z_factor * z_channels, **kwargs)
self.decoder = Decoder(z_channels=z_channels, **kwargs)
self.quant_conv = nn.Conv2d(z_factor * z_channels, z_factor * latent_channels, 1)
self.post_quant_conv = nn.Conv2d(latent_channels, z_channels, 1)
self.distribution = IdentityDistribution()
def encoder_module(self) -> nn.Sequential:
return nn.Sequential(
OrderedDict(
[
("encoder", self.encoder),
("quant_conv", self.quant_conv),
("distribution", self.distribution),
]
)
)
def decoder_module(self) -> nn.Sequential:
return nn.Sequential(
OrderedDict(
[
("post_quant_conv", self.post_quant_conv),
("decoder", self.decoder),
]
)
)
def encode(self, x: torch.Tensor) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]:
h = self.encoder(x)
moments = self.quant_conv(h)
return self.distribution(moments)
def decode(self, z: torch.Tensor) -> torch.Tensor:
return self.decoder(self.post_quant_conv(z))
def build_cosmos_ci_torch_model(spatial_compression: int) -> ContinuousImageTokenizer:
return ContinuousImageTokenizer(
attn_resolutions=[32],
channels=128,
channels_mult=[2, 4, 4],
dropout=0.0,
in_channels=3,
spatial_compression=spatial_compression,
num_res_blocks=2,
out_channels=3,
resolution=1024,
patch_size=4,
patch_method="haar",
latent_channels=16,
z_channels=16,
z_factor=1,
name="CI",
)
@@ -0,0 +1,97 @@
from __future__ import annotations
from pathlib import Path
import torch
from safetensors.torch import load_file
from .cosmos_ci_torch import build_cosmos_ci_torch_model
_SAFETENSORS_EXTENSION = ".safetensors"
def is_safetensors_checkpoint(path: str | Path) -> bool:
return Path(path).suffix.lower() == _SAFETENSORS_EXTENSION
def infer_cosmos_ci_spatial_compression(path: str | Path) -> int:
normalized = str(path).upper()
if "CI8X8" in normalized or "8X8" in normalized:
return 8
if "CI16X16" in normalized or "16X16" in normalized:
return 16
raise ValueError(f"Unable to infer Cosmos CI spatial compression from checkpoint path: {path}")
def _load_safetensors_checkpoint(path: str | Path, device: str) -> dict[str, torch.Tensor]:
return load_file(str(path), device=device)
def _load_native_cosmos_component(
checkpoint_filepath: str | Path,
*,
component: str,
device: str,
) -> torch.nn.Module:
full_model = build_cosmos_ci_torch_model(
spatial_compression=infer_cosmos_ci_spatial_compression(checkpoint_filepath)
)
if component == "encoder":
model = full_model.encoder_module()
elif component == "decoder":
model = full_model.decoder_module()
else:
raise ValueError(f"Unsupported Cosmos component: {component}")
if not is_safetensors_checkpoint(checkpoint_filepath):
raise ValueError(f"Native Cosmos torch backend expects `.safetensors` checkpoints. Got: {checkpoint_filepath}")
state_dict = _load_safetensors_checkpoint(checkpoint_filepath, device)
model.load_state_dict(state_dict, strict=False)
return model.eval().to(device)
def load_cosmos_component(
checkpoint_filepath: str | Path,
*,
component: str,
device: str = "cuda",
) -> torch.nn.Module:
return _load_native_cosmos_component(
checkpoint_filepath,
component=component,
device=device,
)
class ImageTokenizer(torch.nn.Module):
def __init__(
self,
checkpoint_enc: str,
checkpoint_dec: str,
device: str = "cuda",
) -> None:
super().__init__()
self._enc_model = load_cosmos_component(
checkpoint_enc,
component="encoder",
device=device,
)
self._dec_model = load_cosmos_component(
checkpoint_dec,
component="decoder",
device=device,
)
def decode(self, input_latent: torch.Tensor) -> torch.Tensor:
original_dtype = input_latent.dtype
model_dtype = next(self.parameters()).dtype
output_tensor = self._dec_model(input_latent.to(model_dtype))
return output_tensor.to(original_dtype)
def encode(self, input_tensor: torch.Tensor) -> torch.Tensor:
original_dtype = input_tensor.dtype
model_dtype = next(self.parameters()).dtype
output_latent = self._enc_model(input_tensor.to(model_dtype))
if isinstance(output_latent, torch.Tensor):
return output_latent.to(original_dtype)
return output_latent[0].to(original_dtype)
@@ -0,0 +1,962 @@
from __future__ import annotations
import math
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal
import torch
import torch._dynamo as dynamo
import torch.nn.functional as F
from einops import rearrange
from safetensors import safe_open
from torch import Tensor, nn
from transformers import Qwen3VLProcessor
from transformers.models.auto import CONFIG_MAPPING
from vllm.model_executor.models.utils import AutoWeightsLoader
from .adapter_qwen3_vl import (
Qwen3VLForConditionalGeneration,
Qwen3VLTextModel,
apply_rotary_pos_emb,
eager_attention_forward,
)
from .config import (
DEFAULT_COSMOS_DIR,
DEFAULT_COSMOS_REPO,
DEFAULT_QWEN3_VL_MODEL,
OBS_IMAGES,
OBS_PREFIX,
OBS_STATE,
OBS_TASK,
OPENPI_ATTENTION_MASK_VALUE,
InternVLAA1Config,
)
from .model_cosmos import ImageTokenizer
def get_safe_dtype(target_dtype: torch.dtype, device_type: str) -> torch.dtype:
if device_type == "mps" and target_dtype == torch.float64:
return torch.float32
if device_type == "cpu" and target_dtype == torch.bfloat16:
return torch.float32
return target_dtype
def create_sinusoidal_pos_embedding(
time: torch.Tensor,
dimension: int,
min_period: float,
max_period: float,
device: torch.device,
) -> Tensor:
if dimension % 2 != 0:
raise ValueError(f"dimension ({dimension}) must be divisible by 2")
dtype = get_safe_dtype(torch.float64, device.type)
fraction = torch.linspace(0.0, 1.0, dimension // 2, dtype=dtype, device=device)
period = min_period * (max_period / min_period) ** fraction
scaling_factor = 1.0 / period * 2 * math.pi
sin_input = scaling_factor[None, :] * time[:, None]
return torch.cat([torch.sin(sin_input), torch.cos(sin_input)], dim=1)
def make_att_2d_masks(pad_masks: torch.Tensor, att_masks: torch.Tensor) -> torch.Tensor:
cumsum = torch.cumsum(att_masks, dim=1)
att_2d_masks = cumsum[:, None, :] <= cumsum[:, :, None]
pad_2d_masks = pad_masks[:, None, :] * pad_masks[:, :, None]
return att_2d_masks & pad_2d_masks
def pad_vector(vector: torch.Tensor, new_dim: int) -> torch.Tensor:
if vector.shape[-1] >= new_dim:
return vector
return F.pad(vector, (0, new_dim - vector.shape[-1]))
def resize_with_pad(images: torch.Tensor, size: tuple[int, int]) -> torch.Tensor:
target_h, target_w = size
if images.ndim != 4:
raise ValueError(f"Expected [T, C, H, W], got {tuple(images.shape)}")
_, _, src_h, src_w = images.shape
scale = min(target_h / src_h, target_w / src_w)
resized_h = max(1, int(round(src_h * scale)))
resized_w = max(1, int(round(src_w * scale)))
resized = F.interpolate(images, size=(resized_h, resized_w), mode="bilinear", align_corners=False)
pad_h = target_h - resized_h
pad_w = target_w - resized_w
pad_top = pad_h // 2
pad_bottom = pad_h - pad_top
pad_left = pad_w // 2
pad_right = pad_w - pad_left
return F.pad(resized, (pad_left, pad_right, pad_top, pad_bottom))
class Qwen3VLInputBuilder:
def __init__(
self,
processor_model_name: str = DEFAULT_QWEN3_VL_MODEL,
max_length: int = 48,
spatial_merge_size: int = 2,
) -> None:
processor_model_name = os.getenv("INTERNVLA_A1_PROCESSOR_DIR", processor_model_name)
local_path = Path(processor_model_name).expanduser()
if local_path.exists():
self.processor = Qwen3VLProcessor.from_pretrained(str(local_path), local_files_only=True)
else:
if os.getenv("INTERNVLA_A1_PROCESSOR_DIR"):
raise FileNotFoundError(f"INTERNVLA_A1_PROCESSOR_DIR points to a missing path: {local_path}")
self.processor = Qwen3VLProcessor.from_pretrained(processor_model_name)
self.max_length = max_length
self.spatial_merge_size = spatial_merge_size
def build(self, camera_images: list[torch.Tensor], task: str) -> dict[str, torch.Tensor]:
input_ids: list[int] = []
attention_mask: list[int] = []
pixel_values: list[torch.Tensor] = []
image_grid_thw: list[torch.Tensor] = []
for image_history in camera_images:
current_image = image_history[1]
image_inputs = self.processor.image_processor(current_image, do_rescale=False)
pixel_values.append(image_inputs.pixel_values)
image_grid_thw.append(image_inputs.image_grid_thw)
num_img_token = int(torch.prod(image_inputs.image_grid_thw[0]).item()) // (self.spatial_merge_size**2)
input_ids.extend(
[self.processor.vision_start_token_id]
+ [self.processor.image_token_id] * num_img_token
+ [self.processor.vision_end_token_id]
)
attention_mask.extend([1] * (num_img_token + 2))
text_inputs = self.processor.tokenizer(
task,
max_length=self.max_length,
padding="max_length",
padding_side="right",
truncation=True,
)
input_ids.extend(text_inputs.input_ids)
attention_mask.extend(text_inputs.attention_mask)
return {
f"{OBS_PREFIX}pixel_values": torch.cat(pixel_values, dim=0),
f"{OBS_PREFIX}image_grid_thw": torch.cat(image_grid_thw, dim=0),
f"{OBS_PREFIX}input_ids": torch.tensor(input_ids, dtype=torch.long),
f"{OBS_PREFIX}attention_mask": torch.tensor(attention_mask, dtype=torch.long),
}
@dataclass
class SuffixStaticContext:
state_emb: torch.Tensor
full_att_2d_masks_4d: torch.Tensor
position_ids: torch.Tensor
def compute_layer_complete(
layer_idx: int,
inputs_embeds: list[torch.Tensor],
attention_mask: torch.Tensor,
position_ids: torch.Tensor,
und_expert: nn.Module,
gen_expert: nn.Module,
act_expert: nn.Module,
) -> list[torch.Tensor]:
models = [und_expert.language_model, gen_expert, act_expert]
query_states = []
key_states = []
value_states = []
for i, hidden_states in enumerate(inputs_embeds):
layer = models[i].layers[layer_idx]
hidden_states = layer.input_layernorm(hidden_states)
input_shape = hidden_states.shape[:-1]
hidden_shape = (*input_shape, -1, layer.self_attn.head_dim)
if layer.self_attn.q_proj.weight.dtype == torch.bfloat16:
hidden_states = hidden_states.to(dtype=torch.bfloat16)
query_state = layer.self_attn.q_norm(layer.self_attn.q_proj(hidden_states).view(hidden_shape)).transpose(1, 2)
key_state = layer.self_attn.k_norm(layer.self_attn.k_proj(hidden_states).view(hidden_shape)).transpose(1, 2)
value_state = layer.self_attn.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)
query_states.append(query_state)
key_states.append(key_state)
value_states.append(value_state)
query_states = torch.cat(query_states, dim=2)
key_states = torch.cat(key_states, dim=2)
value_states = torch.cat(value_states, dim=2)
dummy_tensor = torch.zeros(
query_states.shape[0],
query_states.shape[2],
query_states.shape[-1],
device=query_states.device,
dtype=query_states.dtype,
)
cos, sin = und_expert.model.language_model.rotary_emb(dummy_tensor, position_ids)
query_states, key_states = apply_rotary_pos_emb(
query_states,
key_states,
cos,
sin,
unsqueeze_dim=1,
)
scaling = und_expert.language_model.layers[layer_idx].self_attn.scaling
att_output, _ = eager_attention_forward(
und_expert.language_model.layers[layer_idx].self_attn,
query_states,
key_states,
value_states,
attention_mask,
scaling,
)
head_dim = und_expert.language_model.layers[layer_idx].self_attn.head_dim
num_attention_heads = und_expert.language_model.layers[layer_idx].self_attn.config.num_attention_heads
batch_size = query_states.shape[0]
att_output = att_output.reshape(batch_size, -1, num_attention_heads * head_dim)
outputs_embeds = []
start_pos = 0
for i, hidden_states in enumerate(inputs_embeds):
layer = models[i].layers[layer_idx]
end_pos = start_pos + hidden_states.shape[1]
att_chunk = att_output[:, start_pos:end_pos]
if att_chunk.dtype != layer.self_attn.o_proj.weight.dtype:
att_chunk = att_chunk.to(layer.self_attn.o_proj.weight.dtype)
out_emb = layer.self_attn.o_proj(att_chunk)
out_emb = out_emb + hidden_states
residual = out_emb
out_emb = layer.post_attention_layernorm(out_emb)
if layer.mlp.up_proj.weight.dtype == torch.bfloat16:
out_emb = out_emb.to(dtype=torch.bfloat16)
out_emb = layer.mlp(out_emb)
out_emb = out_emb + residual
outputs_embeds.append(out_emb)
start_pos = end_pos
return outputs_embeds
class QwenConfig:
def __init__(
self,
head_dim: int,
hidden_size: int,
intermediate_size: int,
num_attention_heads: int,
num_hidden_layers: int,
num_key_value_heads: int,
) -> None:
self.head_dim = head_dim
self.hidden_size = hidden_size
self.intermediate_size = intermediate_size
self.num_attention_heads = num_attention_heads
self.num_hidden_layers = num_hidden_layers
self.num_key_value_heads = num_key_value_heads
def get_qwen_config(variant: str) -> QwenConfig:
num_hidden_layers = int(variant.split("_")[-1][:-1])
if variant.startswith("qwen3_vl"):
return QwenConfig(128, 2048, 6144, 16, num_hidden_layers, 8)
if variant.startswith("qwen3"):
return QwenConfig(128, 1024, 3072, 16, num_hidden_layers, 8)
raise ValueError(f"Unknown variant: {variant}")
def resolve_cosmos_checkpoint_paths(
*,
encoder_path: str | Path | None = None,
decoder_path: str | Path | None = None,
) -> tuple[Path, Path]:
encoder_override = encoder_path or os.getenv("INTERNVLA_A1_COSMOS_ENCODER_PATH")
decoder_override = decoder_path or os.getenv("INTERNVLA_A1_COSMOS_DECODER_PATH")
default_encoder_name = "encoder.safetensors"
default_decoder_name = "decoder.safetensors"
checkpoint_enc = (
Path(encoder_override).expanduser()
if encoder_override is not None
else DEFAULT_COSMOS_DIR / default_encoder_name
)
checkpoint_dec = (
Path(decoder_override).expanduser()
if decoder_override is not None
else DEFAULT_COSMOS_DIR / default_decoder_name
)
missing = [str(path) for path in (checkpoint_enc, checkpoint_dec) if not path.exists()]
if missing:
missing_display = ", ".join(missing)
raise FileNotFoundError(
"InternVLA-A1 requires local Cosmos tokenizer checkpoints. "
f"Missing: {missing_display}. "
f"Download {DEFAULT_COSMOS_REPO} and set INTERNVLA_A1_COSMOS_DIR to that directory, "
"or point INTERNVLA_A1_COSMOS_ENCODER_PATH / INTERNVLA_A1_COSMOS_DECODER_PATH "
"to explicit checkpoint files."
)
return checkpoint_enc, checkpoint_dec
class Qwen3VLWithExpertModel(nn.Module):
def __init__(
self,
vlm_config: QwenConfig,
action_expert_config: QwenConfig,
precision: Literal["bfloat16", "float32"] = "bfloat16",
) -> None:
super().__init__()
vlm_config_hf = CONFIG_MAPPING["qwen3_vl"]()
vlm_config_hf.text_config.hidden_size = vlm_config.hidden_size
vlm_config_hf.text_config.intermediate_size = vlm_config.intermediate_size
vlm_config_hf.text_config.num_attention_heads = vlm_config.num_attention_heads
vlm_config_hf.text_config.head_dim = vlm_config.head_dim
vlm_config_hf.text_config.num_hidden_layers = vlm_config.num_hidden_layers
vlm_config_hf.text_config.num_key_value_heads = vlm_config.num_key_value_heads
vlm_config_hf.text_config.max_position_embeddings = 262144
vlm_config_hf.text_config.rope_scaling = {
"mrope_interleaved": True,
"mrope_section": [24, 20, 20],
"rope_type": "default",
}
vlm_config_hf.text_config.tie_word_embeddings = True
vlm_config_hf.tie_word_embeddings = True
vlm_config_hf.vision_config.deepstack_visual_indexes = [5, 11, 17]
vlm_config_hf.vision_config.depth = 24
vlm_config_hf.vision_config.hidden_size = 1024
vlm_config_hf.vision_config.intermediate_size = 4096
vlm_config_hf.vision_config.out_hidden_size = 2048
self.und_expert = Qwen3VLForConditionalGeneration(config=vlm_config_hf)
gen_expert_config_hf = CONFIG_MAPPING["qwen3_vl_text"]()
gen_expert_config_hf.head_dim = action_expert_config.head_dim
gen_expert_config_hf.hidden_size = action_expert_config.hidden_size
gen_expert_config_hf.intermediate_size = action_expert_config.intermediate_size
gen_expert_config_hf.num_attention_heads = action_expert_config.num_attention_heads
gen_expert_config_hf.num_hidden_layers = action_expert_config.num_hidden_layers
gen_expert_config_hf.num_key_value_heads = action_expert_config.num_key_value_heads
gen_expert_config_hf.max_position_embeddings = self.und_expert.config.text_config.max_position_embeddings
gen_expert_config_hf.rope_scaling = self.und_expert.config.text_config.rope_scaling
self.gen_expert = Qwen3VLTextModel(config=gen_expert_config_hf)
self.gen_expert.embed_tokens = None
self.gen_expert.lm_head = None
act_expert_config_hf = CONFIG_MAPPING["qwen3_vl_text"]()
act_expert_config_hf.head_dim = action_expert_config.head_dim
act_expert_config_hf.hidden_size = action_expert_config.hidden_size
act_expert_config_hf.intermediate_size = action_expert_config.intermediate_size
act_expert_config_hf.num_attention_heads = action_expert_config.num_attention_heads
act_expert_config_hf.num_hidden_layers = action_expert_config.num_hidden_layers
act_expert_config_hf.num_key_value_heads = action_expert_config.num_key_value_heads
act_expert_config_hf.max_position_embeddings = self.und_expert.config.text_config.max_position_embeddings
act_expert_config_hf.rope_scaling = self.und_expert.config.text_config.rope_scaling
self.act_expert = Qwen3VLTextModel(config=act_expert_config_hf)
self.act_expert.embed_tokens = None
self.act_expert.lm_head = None
self.to_selected_precision(precision)
def to_selected_precision(self, precision: Literal["bfloat16", "float32"]) -> None:
if precision == "bfloat16":
self.to(dtype=torch.bfloat16)
elif precision == "float32":
# The fp32 path keeps the whole module in full precision, so no
# selective post-conversion fixups are required.
self.to(dtype=torch.float32)
return
else:
raise ValueError(f"Unsupported precision: {precision}")
# Match the original runtime numerics by keeping normalization layers
# in fp32 even when the rest of the module runs in bf16.
keep_fp32 = ["input_layernorm", "post_attention_layernorm", "model.norm"]
for name, param in self.named_parameters():
if any(key in name for key in keep_fp32):
param.data = param.data.to(dtype=torch.float32)
def forward(
self,
attention_mask: torch.Tensor | None,
position_ids: torch.LongTensor | None,
past_key_values: Any,
inputs_embeds: list[torch.Tensor | None],
use_cache: bool,
) -> tuple[list[torch.Tensor | None], Any]:
if inputs_embeds[1] is None and inputs_embeds[2] is None:
prefix_output = self.und_expert.language_model.forward(
inputs_embeds=inputs_embeds[0],
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
use_cache=use_cache,
)
return [prefix_output.last_hidden_state, None, None], prefix_output.past_key_values
if inputs_embeds[0] is None and inputs_embeds[2] is None:
middle_output = self.gen_expert.forward(
inputs_embeds=inputs_embeds[1],
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
use_cache=use_cache,
)
return [None, middle_output.last_hidden_state, None], middle_output.past_key_values
if inputs_embeds[0] is None and inputs_embeds[1] is None:
suffix_output = self.act_expert.forward(
inputs_embeds=inputs_embeds[2],
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
use_cache=use_cache,
)
return [None, None, suffix_output.last_hidden_state], None
models = [self.und_expert.language_model, self.gen_expert, self.act_expert]
stacked_inputs = [inputs_embeds[0], inputs_embeds[1], inputs_embeds[2]]
for layer_idx in range(self.und_expert.config.text_config.num_hidden_layers):
stacked_inputs = compute_layer_complete(
layer_idx,
stacked_inputs,
attention_mask,
position_ids,
und_expert=self.und_expert,
gen_expert=self.gen_expert,
act_expert=self.act_expert,
)
outputs = [models[i].norm(stacked_inputs[i]) for i in range(3)]
return outputs, None
class InternVLAA1(nn.Module):
def __init__(
self,
config: InternVLAA1Config,
*,
cosmos_encoder_path: str | Path | None = None,
cosmos_decoder_path: str | Path | None = None,
) -> None:
super().__init__()
self.config = config
vlm_config = get_qwen_config(config.qwen3_vl_variant)
action_expert_config = get_qwen_config(config.action_expert_variant)
self.qwen3_vl_with_expert = Qwen3VLWithExpertModel(vlm_config, action_expert_config, precision=config.dtype)
cosmos_encoder_path, cosmos_decoder_path = resolve_cosmos_checkpoint_paths(
encoder_path=cosmos_encoder_path,
decoder_path=cosmos_decoder_path,
)
self.cosmos = ImageTokenizer(
checkpoint_enc=str(cosmos_encoder_path),
checkpoint_dec=str(cosmos_decoder_path),
device=config.device,
)
hidden_size = action_expert_config.hidden_size
vae_dim = 16
ds = config.scale_factor
self.cosmos_in_proj = nn.Conv2d(vae_dim, hidden_size, kernel_size=1, stride=1, padding=0)
self.downsample_conv = nn.Conv2d(hidden_size, hidden_size, kernel_size=ds, stride=ds, padding=0)
self.upsample_conv = nn.ConvTranspose2d(hidden_size, hidden_size, kernel_size=ds, stride=ds, padding=0)
self.cosmos_out_proj = nn.Linear(hidden_size, vae_dim)
self.cosmos_out_layer_norm = nn.LayerNorm(hidden_size)
self.action_in_proj = nn.Linear(config.max_action_dim, hidden_size)
self.action_out_proj = nn.Linear(hidden_size, config.max_action_dim)
self.state_proj = nn.Linear(config.max_state_dim, hidden_size)
self.action_time_mlp_in = nn.Linear(2 * hidden_size, hidden_size)
self.action_time_mlp_out = nn.Linear(hidden_size, hidden_size)
if config.compile_model:
torch.set_float32_matmul_precision("high")
self.sample_actions = torch.compile(self.sample_actions, mode=config.compile_mode)
self.cosmos.eval()
for param in self.cosmos.parameters():
param.requires_grad = False
self.set_attention_implementation(config.attn_implementation)
def set_attention_implementation(self, attn_implementation: str) -> None:
self.config.attn_implementation = attn_implementation
self.qwen3_vl_with_expert.und_expert.config.text_config._attn_implementation = attn_implementation
self.qwen3_vl_with_expert.und_expert.language_model.config._attn_implementation = attn_implementation
self.qwen3_vl_with_expert.gen_expert.config._attn_implementation = attn_implementation
self.qwen3_vl_with_expert.act_expert.config._attn_implementation = attn_implementation
def _prepare_attention_masks_4d(
self,
att_2d_masks: torch.Tensor,
*,
dtype: torch.dtype,
) -> torch.Tensor:
att_2d_masks_4d = att_2d_masks[:, None, :, :]
bias_dtype = torch.float32 if self.config.attn_implementation == "eager" else dtype
valid_bias = torch.zeros((), dtype=bias_dtype, device=att_2d_masks.device)
invalid_bias = torch.tensor(
OPENPI_ATTENTION_MASK_VALUE,
dtype=bias_dtype,
device=att_2d_masks.device,
)
return torch.where(att_2d_masks_4d, valid_bias, invalid_bias)
def sample_noise(self, shape: tuple[int, ...], device: torch.device) -> torch.Tensor:
return torch.normal(0.0, 1.0, shape, dtype=torch.float32, device=device)
@dynamo.disable
def embed_prefix(
self,
pixel_values: torch.Tensor,
image_grid_thw: torch.Tensor,
lang_tokens: torch.Tensor,
lang_masks: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
image_token_id = self.qwen3_vl_with_expert.und_expert.config.image_token_id
pixel_values = pixel_values.view(-1, pixel_values.shape[-1])
image_grid_thw = image_grid_thw.view(-1, 3)
image_embs, _ = self.qwen3_vl_with_expert.und_expert.visual(pixel_values, image_grid_thw)
embs = self.qwen3_vl_with_expert.und_expert.get_input_embeddings()(lang_tokens)
batch_size, seq_len, hidden_dim = embs.shape
embs = embs.view(-1, hidden_dim)
lang_tokens_flat = lang_tokens.view(-1)
embs[lang_tokens_flat == image_token_id] = image_embs
embs = embs.view(batch_size, seq_len, hidden_dim)
pad_masks = lang_masks.to(torch.bool)
att_masks = torch.zeros_like(pad_masks, dtype=torch.bool, device=pad_masks.device)
return embs, pad_masks, att_masks
def get_cosmos_features(self, images: torch.Tensor) -> torch.Tensor:
shape = images.shape[:-3]
channels, height, width = images.shape[-3:]
images = images.reshape(-1, channels, height, width)
if (height, width) != (256, 256):
images = F.interpolate(images, size=(256, 256), mode="bilinear", align_corners=False)
images = images * 2 - 1
features = self.cosmos.encode(images)
channels, height, width = features.shape[-3:]
return features.view(*shape, channels, height, width)
def embed_middle(
self,
images: torch.Tensor,
img_masks: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
device = images.device
batch_size, n_view, timesteps = images.shape[:3]
features = self.get_cosmos_features(images)
features = rearrange(features, "b n t c h w -> (b n t) c h w")
features = self.cosmos_in_proj(features)
features = self.downsample_conv(features)
features = rearrange(features, "(b n t) c h w -> b n t c h w", b=batch_size, n=n_view, t=timesteps)
_, _, _, _, height, width = features.shape
embs = rearrange(features, "b n t c h w -> b (n t h w) c")
pad_masks = torch.zeros((batch_size, n_view, timesteps, height, width), dtype=torch.bool, device=device)
pad_masks[img_masks] = True
pad_masks = rearrange(pad_masks, "b n t h w -> b (n t h w)")
att_masks = torch.tensor([1] + [0] * (embs.shape[1] - 1), dtype=torch.bool, device=device)
att_masks = att_masks[None, :].expand(batch_size, att_masks.shape[0])
return embs, pad_masks, att_masks
def prepare_suffix_static_context(
self,
state: torch.Tensor,
prefix_pad_masks: torch.Tensor,
max_prefix_position_ids: torch.Tensor,
) -> SuffixStaticContext:
if self.state_proj.weight.dtype == torch.float32:
state = state.to(torch.float32)
state_emb = self.state_proj(state)
device = state_emb.device
batch_size = state_emb.shape[0]
suffix_len = self.config.chunk_size + 1
prefix_len = prefix_pad_masks.shape[1]
suffix_pad_masks = torch.ones(batch_size, suffix_len, dtype=torch.bool, device=device)
suffix_att_masks = torch.tensor([1] + [1] + [0] * (self.config.chunk_size - 1), dtype=torch.bool, device=device)
suffix_att_masks = suffix_att_masks[None, :].expand(batch_size, suffix_len)
prefix_pad_2d_masks = prefix_pad_masks[:, None, :].expand(batch_size, suffix_len, prefix_len)
suffix_att_2d_masks = make_att_2d_masks(suffix_pad_masks, suffix_att_masks)
full_att_2d_masks_4d = self._prepare_attention_masks_4d(
torch.cat([prefix_pad_2d_masks, suffix_att_2d_masks], dim=2),
dtype=state_emb.dtype,
)
position_ids = (
torch.arange(1, suffix_len + 1, device=max_prefix_position_ids.device)
.repeat(3, 1, 1)
.to(max_prefix_position_ids)
+ max_prefix_position_ids
)
return SuffixStaticContext(
state_emb=state_emb,
full_att_2d_masks_4d=full_att_2d_masks_4d,
position_ids=position_ids,
)
def embed_suffix(
self,
state: torch.Tensor,
noisy_actions: torch.Tensor,
timestep: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
if self.state_proj.weight.dtype == torch.float32:
state = state.to(torch.float32)
state_emb = self.state_proj(state)
batch_size = state_emb.shape[0]
device = state_emb.device
state_mask = torch.ones(batch_size, 1, dtype=torch.bool, device=device)
time_emb = create_sinusoidal_pos_embedding(
timestep,
self.action_in_proj.out_features,
min_period=self.config.min_period,
max_period=self.config.max_period,
device=timestep.device,
).to(dtype=timestep.dtype)
action_emb = self.action_in_proj(noisy_actions)
time_emb = time_emb[:, None, :].expand_as(action_emb)
action_time_emb = torch.cat([action_emb, time_emb], dim=2)
action_time_emb = self.action_time_mlp_out(F.silu(self.action_time_mlp_in(action_time_emb)))
action_mask = torch.ones(batch_size, action_time_emb.shape[1], dtype=torch.bool, device=device)
embs = torch.cat([state_emb[:, None, :], action_time_emb], dim=1)
pad_masks = torch.cat([state_mask, action_mask], dim=1)
att_masks = torch.tensor([1] + [1] + [0] * (self.config.chunk_size - 1), dtype=embs.dtype, device=device)
att_masks = att_masks[None, :].expand(batch_size, self.config.chunk_size + 1)
return embs, pad_masks, att_masks
def get_position_ids(
self,
lang_tokens: torch.Tensor,
image_grid_thw: torch.Tensor | None,
pad_masks: torch.Tensor,
) -> tuple[torch.Tensor, Any]:
seq_len = lang_tokens.shape[1]
padded_lang_tokens = torch.ones_like(pad_masks).to(lang_tokens) * 777
padded_lang_tokens[:, :seq_len] = lang_tokens
attention_mask = pad_masks.to(lang_tokens)
if image_grid_thw is not None:
image_grid_thw = image_grid_thw.view(-1, 3)
return self.qwen3_vl_with_expert.und_expert.model.get_rope_index(
padded_lang_tokens,
image_grid_thw,
attention_mask=attention_mask,
)
@torch.no_grad()
def sample_actions(
self,
images: torch.Tensor,
img_masks: torch.Tensor,
pixel_values: torch.Tensor,
image_grid_thw: torch.Tensor,
lang_tokens: torch.Tensor,
lang_masks: torch.Tensor,
state: torch.Tensor,
*,
noise: torch.Tensor | None = None,
num_steps: int | None = None,
decode_image: bool = False,
) -> tuple[torch.Tensor, torch.Tensor | None]:
if num_steps is None:
num_steps = self.config.num_inference_steps
batch_size = state.shape[0]
device = state.device
dtype = state.dtype
if noise is None:
noise = self.sample_noise((batch_size, self.config.chunk_size, self.config.max_action_dim), device)
prefix_embs, prefix_pad_masks, prefix_att_masks = self.embed_prefix(
pixel_values,
image_grid_thw,
lang_tokens,
lang_masks,
)
prefix_att_2d_masks = make_att_2d_masks(prefix_pad_masks, prefix_att_masks)
prefix_position_ids, _ = self.get_position_ids(lang_tokens, image_grid_thw, prefix_pad_masks)
prefix_att_2d_masks_4d = self._prepare_attention_masks_4d(
prefix_att_2d_masks,
dtype=prefix_embs.dtype,
)
_, past_key_values = self.qwen3_vl_with_expert.forward(
attention_mask=prefix_att_2d_masks_4d,
position_ids=prefix_position_ids,
past_key_values=None,
inputs_embeds=[prefix_embs, None, None],
use_cache=True,
)
max_prefix_position_ids = prefix_position_ids.max(dim=-1, keepdim=True).values
middle_embs, middle_pad_masks, middle_att_masks = self.embed_middle(images[:, :, :2], img_masks)
middle_len = middle_pad_masks.shape[1]
prefix_len = prefix_pad_masks.shape[1]
prefix_pad_2d_masks = prefix_pad_masks[:, None, :].expand(batch_size, middle_len, prefix_len)
middle_att_2d_masks = make_att_2d_masks(middle_pad_masks, middle_att_masks)
full_att_2d_masks = torch.cat([prefix_pad_2d_masks, middle_att_2d_masks], dim=2)
middle_position_ids = (
torch.arange(1, middle_len + 1, device=max_prefix_position_ids.device)
.repeat(3, 1, 1)
.to(max_prefix_position_ids)
+ max_prefix_position_ids
)
full_att_2d_masks_4d = self._prepare_attention_masks_4d(
full_att_2d_masks,
dtype=middle_embs.dtype,
)
(_, middle_out, _), past_key_values = self.qwen3_vl_with_expert.forward(
attention_mask=full_att_2d_masks_4d,
position_ids=middle_position_ids,
past_key_values=past_key_values,
inputs_embeds=[None, middle_embs, None],
use_cache=True,
)
max_position_ids = middle_position_ids.max(dim=-1, keepdim=True).values
curr_pad_masks = torch.cat([prefix_pad_masks, middle_pad_masks], dim=1)
dt = torch.tensor(-1.0 / num_steps, dtype=torch.float32, device=device)
x_t = noise
time = torch.tensor(1.0, dtype=torch.float32, device=device)
suffix_static = None
if self.config.enable_suffix_static_context_optimization:
suffix_static = self.prepare_suffix_static_context(state, curr_pad_masks, max_position_ids)
while time >= -dt / 2:
expanded_time = time.expand(batch_size)
if suffix_static is None:
v_t = self.denoise_step(
state,
curr_pad_masks,
past_key_values,
max_position_ids,
x_t.to(dtype),
expanded_time.to(dtype),
)
else:
v_t = self.denoise_step_optimized(
suffix_static, past_key_values, x_t.to(dtype), expanded_time.to(dtype)
)
x_t = x_t + dt * v_t
time += dt
return x_t, None if not decode_image else middle_out
def denoise_step(
self,
state: torch.Tensor,
prefix_pad_masks: torch.Tensor,
past_key_values: Any,
max_prefix_position_ids: torch.Tensor,
x_t: torch.Tensor,
timestep: torch.Tensor,
) -> torch.Tensor:
suffix_embs, suffix_pad_masks, suffix_att_masks = self.embed_suffix(state, x_t, timestep)
suffix_len = suffix_pad_masks.shape[1]
batch_size = prefix_pad_masks.shape[0]
prefix_len = prefix_pad_masks.shape[1]
prefix_pad_2d_masks = prefix_pad_masks[:, None, :].expand(batch_size, suffix_len, prefix_len)
suffix_att_2d_masks = make_att_2d_masks(suffix_pad_masks, suffix_att_masks)
full_att_2d_masks = torch.cat([prefix_pad_2d_masks, suffix_att_2d_masks], dim=2)
position_ids = (
torch.arange(1, suffix_len + 1, device=max_prefix_position_ids.device)
.repeat(3, 1, 1)
.to(max_prefix_position_ids)
+ max_prefix_position_ids
)
full_att_2d_masks_4d = self._prepare_attention_masks_4d(
full_att_2d_masks,
dtype=suffix_embs.dtype,
)
outputs_embeds, _ = self.qwen3_vl_with_expert.forward(
attention_mask=full_att_2d_masks_4d,
position_ids=position_ids,
past_key_values=past_key_values,
inputs_embeds=[None, None, suffix_embs],
use_cache=False,
)
suffix_out = outputs_embeds[2][:, -self.config.chunk_size :].to(dtype=torch.float32)
return self.action_out_proj(suffix_out)
def denoise_step_optimized(
self,
suffix_static: SuffixStaticContext,
past_key_values: Any,
x_t: torch.Tensor,
timestep: torch.Tensor,
) -> torch.Tensor:
state_emb = suffix_static.state_emb
time_emb = create_sinusoidal_pos_embedding(
timestep,
self.action_in_proj.out_features,
min_period=self.config.min_period,
max_period=self.config.max_period,
device=timestep.device,
).to(dtype=timestep.dtype)
action_emb = self.action_in_proj(x_t)
time_emb = time_emb[:, None, :].expand_as(action_emb)
action_time_emb = torch.cat([action_emb, time_emb], dim=2)
action_time_emb = self.action_time_mlp_out(F.silu(self.action_time_mlp_in(action_time_emb)))
suffix_embs = torch.cat([state_emb[:, None, :], action_time_emb], dim=1)
outputs_embeds, _ = self.qwen3_vl_with_expert.forward(
attention_mask=suffix_static.full_att_2d_masks_4d,
position_ids=suffix_static.position_ids,
past_key_values=past_key_values,
inputs_embeds=[None, None, suffix_embs],
use_cache=False,
)
suffix_out = outputs_embeds[2][:, -self.config.chunk_size :].to(dtype=torch.float32)
return self.action_out_proj(suffix_out)
class InternVLAA1Policy(nn.Module):
_AUTO_WEIGHTS_IGNORE_PREFIXES = ["model.cosmos."]
def __init__(
self,
config: InternVLAA1Config,
*,
processor_model_name: str = DEFAULT_QWEN3_VL_MODEL,
) -> None:
super().__init__()
self.config = config
self.input_builder = Qwen3VLInputBuilder(
processor_model_name=processor_model_name,
max_length=config.tokenizer_max_length,
)
cosmos_encoder_path, cosmos_decoder_path = resolve_cosmos_checkpoint_paths()
self.model = InternVLAA1(
config,
cosmos_encoder_path=cosmos_encoder_path,
cosmos_decoder_path=cosmos_decoder_path,
)
self.model.to(config.device)
@classmethod
def from_pretrained(
cls,
checkpoint_dir: str | Path,
*,
config: InternVLAA1Config | None = None,
processor_model_name: str = DEFAULT_QWEN3_VL_MODEL,
strict: bool = False,
) -> InternVLAA1Policy:
checkpoint_dir = Path(checkpoint_dir)
if config is None:
config = InternVLAA1Config.from_pretrained(checkpoint_dir)
instance = cls(config, processor_model_name=processor_model_name)
checkpoint_path = checkpoint_dir / "model.safetensors"
if not checkpoint_path.exists():
raise FileNotFoundError(f"InternVLAA1 checkpoint not found: {checkpoint_path}")
loader = AutoWeightsLoader(
instance,
ignore_unexpected_prefixes=cls._AUTO_WEIGHTS_IGNORE_PREFIXES,
)
with safe_open(str(checkpoint_path), framework="pt", device=str(config.device)) as f:
file_keys = list(f.keys())
loaded = loader.load_weights((name, f.get_tensor(name)) for name in file_keys)
if strict:
expected = {
name
for name, _ in instance.state_dict().items()
if not any(name.startswith(prefix) for prefix in cls._AUTO_WEIGHTS_IGNORE_PREFIXES)
}
missing = sorted(expected - loaded)
if missing:
preview = ", ".join(missing[:10])
suffix = " ..." if len(missing) > 10 else ""
raise RuntimeError(f"Missing weights after AutoWeightsLoader load: {preview}{suffix}")
instance.eval()
return instance
def to(self, *args, **kwargs): # type: ignore[override]
super().to(*args, **kwargs)
self.model.cosmos.to(torch.bfloat16)
self.model.action_out_proj.to(torch.float32)
return self
def _prepare_resized_histories(self, batch: dict[str, Any]) -> list[torch.Tensor]:
resized_images: list[torch.Tensor] = []
for i in range(3):
image_history = batch[f"{OBS_IMAGES}.image{i}"]
if image_history.shape[0] != 1:
raise ValueError(f"InternVLAA1Policy only supports bs=1, got image batch {tuple(image_history.shape)}")
resized_images.append(resize_with_pad(image_history[0], self.config.image_resolution))
return resized_images
def _preprocess_images(
self,
batch: dict[str, torch.Tensor],
*,
resized_histories: list[torch.Tensor] | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
if resized_histories is None:
resized_histories = self._prepare_resized_histories(batch)
images = torch.stack(resized_histories, dim=0).unsqueeze(0)
img_masks = torch.stack([batch[f"{OBS_IMAGES}.image{i}_mask"] for i in range(3)], dim=1)
return images, img_masks
def prepare_state(self, batch: dict[str, torch.Tensor]) -> torch.Tensor:
return pad_vector(batch[OBS_STATE], self.config.max_state_dim)
def _get_task(self, batch: dict[str, Any]) -> str:
task = batch[OBS_TASK]
if isinstance(task, str):
return task
if isinstance(task, (list, tuple)):
if len(task) != 1:
raise ValueError(f"InternVLAA1Policy only supports bs=1, got {len(task)} tasks")
item = task[0]
if not isinstance(item, str):
raise TypeError(f"Expected task string, got {type(item)!r}")
return item
raise TypeError(f"Unsupported task payload type: {type(task)!r}")
def _prepare_qwen_prefix_inputs(
self,
resized_histories: list[torch.Tensor],
task: str,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
qwen_inputs = self.input_builder.build(resized_histories, task)
device = self.model.action_in_proj.weight.device
return (
qwen_inputs[f"{OBS_PREFIX}pixel_values"]
.unsqueeze(0)
.to(device=device, dtype=self.model.action_in_proj.weight.dtype),
qwen_inputs[f"{OBS_PREFIX}image_grid_thw"].unsqueeze(0).to(device=device),
qwen_inputs[f"{OBS_PREFIX}input_ids"].unsqueeze(0).to(device=device),
qwen_inputs[f"{OBS_PREFIX}attention_mask"].unsqueeze(0).to(device=device),
)
@torch.inference_mode()
def forward(
self,
batch: dict[str, Any],
*,
noise: torch.Tensor | None = None,
decode_image: bool = False,
) -> tuple[torch.Tensor, torch.Tensor | None]:
task = self._get_task(batch)
resized_histories = self._prepare_resized_histories(batch)
pixel_values, image_grid_thw, lang_tokens, lang_masks = self._prepare_qwen_prefix_inputs(
resized_histories,
task,
)
state = self.prepare_state(batch)
images, img_masks = self._preprocess_images(batch, resized_histories=resized_histories)
return self.model.sample_actions(
images,
img_masks,
pixel_values,
image_grid_thw,
lang_tokens,
lang_masks,
state,
noise=noise,
decode_image=decode_image,
)
@@ -0,0 +1,257 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
import json
import os
from typing import Any
import torch
from torch import nn
from vllm.logger import init_logger
from vllm_omni.diffusion.compile import regionally_compile
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import (
DiffusionPipelineProfilerMixin,
wrap_methods_by_paths,
)
from vllm_omni.diffusion.request import OmniDiffusionRequest
from .config import (
DEFAULT_QWEN3_VL_MODEL,
OBS_IMAGES,
OBS_STATE,
OBS_TASK,
InternVLAA1Config,
)
from .model_internvla_a1 import InternVLAA1Policy, resolve_cosmos_checkpoint_paths
logger = init_logger(__name__)
def get_internvla_a1_post_process_func(od_config: OmniDiffusionConfig):
del od_config
def post_process_func(x):
return x
return post_process_func
class InternVLAA1Pipeline(nn.Module, DiffusionPipelineProfilerMixin):
"""InternVLA-A1 pipeline wrapper for the policy implementation."""
def __init__(self, *, od_config: OmniDiffusionConfig, prefix: str = ""):
super().__init__()
self.od_config = od_config
self.prefix = prefix
self.model_dir = od_config.model
self.config = self._build_config(od_config)
custom_args = od_config.custom_pipeline_args or {}
self.strict_load = bool(custom_args.get("strict_load", False))
self.processor_model_name = str(custom_args.get("processor_model_name", DEFAULT_QWEN3_VL_MODEL))
enable_warmup = custom_args.get("enable_warmup")
self.enable_warmup = bool(enable_warmup) if isinstance(enable_warmup, bool) else False
self.setup_diffusion_pipeline_profiler(
profiler_targets=["forward"],
enable_diffusion_pipeline_profiler=self.od_config.enable_diffusion_pipeline_profiler,
)
self.policy = self._initialize_policy()
self._setup_policy_profiler_targets()
if self.enable_warmup:
self._warmup()
def _build_config(self, od_config: OmniDiffusionConfig) -> InternVLAA1Config:
config_dict = self._load_config_dict(od_config)
config = InternVLAA1Config.from_model_config(config_dict)
custom_args = od_config.custom_pipeline_args or {}
device = custom_args.get("device")
if isinstance(device, str):
config.device = device
dtype = custom_args.get("dtype")
if isinstance(dtype, str):
config.dtype = dtype
elif od_config.dtype is not None:
config.dtype = str(od_config.dtype).split(".")[-1]
compile_model = custom_args.get("compile_model")
if isinstance(compile_model, bool):
config.compile_model = compile_model
attn_implementation = custom_args.get("attn_implementation")
if isinstance(attn_implementation, str):
config.attn_implementation = attn_implementation
enable_regional_compile = custom_args.get("enable_regional_compile")
if isinstance(enable_regional_compile, bool):
config.enable_regional_compile = enable_regional_compile
regional_compile_dynamic = custom_args.get("regional_compile_dynamic")
if isinstance(regional_compile_dynamic, bool):
config.regional_compile_dynamic = regional_compile_dynamic
return config
def _load_config_dict(self, od_config: OmniDiffusionConfig) -> dict[str, Any]:
if od_config.model_config:
return dict(od_config.model_config)
model_path = od_config.model
if not model_path:
return {}
config_path = os.path.join(model_path, "config.json")
if not os.path.exists(config_path):
logger.info("InternVLAA1Pipeline config.json not found under %s; using defaults.", model_path)
return {}
with open(config_path, encoding="utf-8") as f:
return json.load(f)
def has_real_checkpoint(self) -> bool:
return bool(self.model_dir) and os.path.exists(os.path.join(self.model_dir, "model.safetensors"))
def runtime_mode(self) -> str:
return "real_checkpoint_loaded" if self.has_real_checkpoint() else "no_checkpoint_policy"
def _setup_policy_profiler_targets(self) -> None:
if not self.od_config.enable_diffusion_pipeline_profiler:
return
wrap_methods_by_paths(
self,
[
"policy.model.sample_actions",
"policy.model.embed_prefix",
"policy.model.embed_middle",
"policy.model.denoise_step",
],
)
def _apply_policy_optimizations(self, policy: InternVLAA1Policy) -> None:
policy.model.set_attention_implementation(self.config.attn_implementation)
if not self.config.enable_regional_compile:
return
compile_targets = [
"qwen3_vl_with_expert.und_expert.visual",
"qwen3_vl_with_expert.und_expert.language_model",
"qwen3_vl_with_expert.gen_expert",
"qwen3_vl_with_expert.act_expert",
]
for path in compile_targets:
current = policy.model
for part in path.split("."):
current = getattr(current, part, None)
if current is None:
break
if current is None:
continue
try:
regionally_compile(current, dynamic=self.config.regional_compile_dynamic)
logger.info("InternVLAA1Pipeline regional compile applied to %s", path)
except Exception as exc:
logger.warning("InternVLAA1Pipeline regional compile failed for %s: %s", path, exc)
def _initialize_policy(self) -> InternVLAA1Policy:
resolve_cosmos_checkpoint_paths()
if self.has_real_checkpoint():
logger.info("Loading InternVLA-A1 weights from %s", self.model_dir)
policy = InternVLAA1Policy.from_pretrained(
self.model_dir,
config=self.config,
processor_model_name=self.processor_model_name,
strict=self.strict_load,
)
else:
logger.info("Initializing InternVLA-A1 policy without checkpoint weights.")
policy = InternVLAA1Policy(self.config, processor_model_name=self.processor_model_name)
policy.to(self.config.device)
policy.to(getattr(torch, self.config.dtype))
policy.eval()
self._apply_policy_optimizations(policy)
return policy
def _build_fake_batch_inputs(self) -> dict[str, torch.Tensor]:
device = torch.device(self.config.device)
dtype = getattr(torch, self.config.dtype)
history = 2
channels = 3
image_size = self.config.image_resolution
fake_image = torch.zeros((1, history, channels, image_size[0], image_size[1]), device=device, dtype=dtype)
fake_mask = torch.ones((1,), device=device, dtype=torch.bool)
return {
OBS_STATE: torch.zeros((1, self.config.max_state_dim), device=device, dtype=dtype),
OBS_TASK: [""],
f"{OBS_IMAGES}.image0": fake_image.clone(),
f"{OBS_IMAGES}.image1": fake_image.clone(),
f"{OBS_IMAGES}.image2": fake_image.clone(),
f"{OBS_IMAGES}.image0_mask": fake_mask.clone(),
f"{OBS_IMAGES}.image1_mask": fake_mask.clone(),
f"{OBS_IMAGES}.image2_mask": fake_mask.clone(),
}
def _warmup(self) -> None:
logger.info("InternVLAA1Pipeline warmup started")
try:
batch_inputs = self._build_fake_batch_inputs()
noise = torch.zeros(
(1, self.config.chunk_size, self.config.max_action_dim),
device=self.config.device,
dtype=torch.float32,
)
with torch.inference_mode():
self.policy.forward(batch_inputs, noise=noise, decode_image=False)
except Exception as exc:
logger.warning("InternVLAA1Pipeline warmup failed: %s", exc)
return
logger.info("InternVLAA1Pipeline warmup finished")
def _predict_actions(
self,
batch_inputs: dict[str, Any],
*,
noise: torch.Tensor | None = None,
decode_image: bool = False,
) -> tuple[torch.Tensor, torch.Tensor | None]:
logger.debug("InternVLAA1Pipeline forward mode=%s", self.runtime_mode())
return self.policy.forward(
batch_inputs,
noise=noise,
decode_image=decode_image,
)
@torch.inference_mode()
def forward(self, req: OmniDiffusionRequest) -> DiffusionOutput:
if len(req.prompts) > 1:
logger.warning("InternVLAA1Pipeline only supports a single prompt/request; taking the first sample.")
extra_args = getattr(req.sampling_params, "extra_args", {}) or {}
batch_inputs = extra_args.get("batch_inputs")
if batch_inputs is None:
return DiffusionOutput(
error=(
"InternVLAA1Pipeline.forward expects sampling_params.extra_args['batch_inputs'] "
"with pre-built repo-side inputs."
),
post_process_func=get_internvla_a1_post_process_func(self.od_config),
)
output, decoded = self._predict_actions(
batch_inputs,
noise=extra_args.get("noise"),
decode_image=bool(extra_args.get("decode_image", False)),
)
custom_output: dict[str, Any] = {}
if decoded is not None:
custom_output["decoded"] = decoded
return DiffusionOutput(
output=output,
custom_output=custom_output,
post_process_func=get_internvla_a1_post_process_func(self.od_config),
)
+6
View File
@@ -135,6 +135,11 @@ _DIFFUSION_MODELS = {
"pipeline_bagel",
"BagelPipeline",
),
"InternVLAA1Pipeline": (
"internvla_a1",
"pipeline_internvla_a1",
"InternVLAA1Pipeline",
),
"LongCatImageEditPipeline": (
"longcat_image",
"pipeline_longcat_image_edit",
@@ -421,6 +426,7 @@ _DIFFUSION_POST_PROCESS_FUNCS = {
"WanI2VDMD2Pipeline": "get_wan22_i2v_post_process_func",
"LongCatImagePipeline": "get_longcat_image_post_process_func",
"BagelPipeline": "get_bagel_post_process_func",
"InternVLAA1Pipeline": "get_internvla_a1_post_process_func",
"LongCatImageEditPipeline": "get_longcat_image_post_process_func",
"StableDiffusion3Pipeline": "get_sd3_image_post_process_func",
"FluxKontextPipeline": "get_flux_kontext_post_process_func",