Spaces:
Running
Running
richard.lin commited on
Commit ·
ecfbb2c
1
Parent(s): 620f331
add: unidrive_vla_nusc_base_evaluation
Browse files- PLANS/agile-discovering-sunbeam.md +51 -0
- README.md +182 -135
- unidrive_vla_nusc_base_evaluation.py +915 -0
- unidrive_vla_nusc_base_quantize_all.py +873 -0
PLANS/agile-discovering-sunbeam.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Plan: unidrive_vla_nusc_base_quantize_all.py
|
| 2 |
+
|
| 3 |
+
## Goal
|
| 4 |
+
量化 `owl10/UniDriveVLA_Nusc_Base_Stage1` (Qwen3-VL-2B, bf16) 到 INT8 和 NVFP4,使用 Nvidia ModelOpt PyTorch 原生 API,并在 DriveLM 上评测。
|
| 5 |
+
|
| 6 |
+
## Architecture
|
| 7 |
+
单文件脚本,分为 3 个阶段:
|
| 8 |
+
1. **量化阶段** — 使用 `modelopt.torch.quantization` 对 PyTorch 模型做 PTQ
|
| 9 |
+
2. **评测阶段** — 复用 `unidrive_vla_nusc_base_evaluation.py` 的评测逻辑
|
| 10 |
+
3. **汇总阶段** — 打印 bf16/INT8/NVFP4 的对比结果
|
| 11 |
+
|
| 12 |
+
## Key Decisions
|
| 13 |
+
|
| 14 |
+
### ModelOpt API
|
| 15 |
+
- `modelopt.torch.quantization.quantize()` + `QConfig`
|
| 16 |
+
- INT8: `percentile` calibration (对 VLM 比 max 更稳健)
|
| 17 |
+
- NVFP4: `awq` (weight-only, 适合大模型)
|
| 18 |
+
|
| 19 |
+
### Calibration Data
|
| 20 |
+
- 来源: DriveLM `v1_1_train_nus.json` 前 128 个样本
|
| 21 |
+
- 每个样本生成 multi-view 输入 (6 相机 + 文本) 送入模型 forward pass
|
| 22 |
+
- 用 `forward_loop` 参数传给 `mtq.quantize()`
|
| 23 |
+
|
| 24 |
+
### Model Loading
|
| 25 |
+
- `Qwen3VLForConditionalGeneration.from_pretrained()` bf16
|
| 26 |
+
- 量化在 bf16 基础上做 (ModelOpt 0.44+ 支持 bf16 基模型)
|
| 27 |
+
|
| 28 |
+
### Evaluation
|
| 29 |
+
- 完全复用 `unidrive_vla_nusc_base_evaluation.py` 的:
|
| 30 |
+
- `load_drivelm_dataset()` — 加载数据
|
| 31 |
+
- `run_inference()` — 模型推理
|
| 32 |
+
- `compute_accuracy()` / `compute_language_metrics()` / `compute_gpt_score()` / `compute_match_score()` / `compute_final_score()` — 指标
|
| 33 |
+
- 在 val split 上评测,subset=200 加速
|
| 34 |
+
|
| 35 |
+
## Script Structure
|
| 36 |
+
|
| 37 |
+
```
|
| 38 |
+
unidrive_vla_nusc_base_quantize_all.py
|
| 39 |
+
├── Imports (复用 evaluation.py 的 metric 函数和 dataset 加载)
|
| 40 |
+
├── Config constants
|
| 41 |
+
├── load_calibration_data() — 从 DriveLM 取 128 样本构造 forward_loop
|
| 42 |
+
├── quantize_int8() — mtq.quantize(model, INT8_CFG, forward_loop)
|
| 43 |
+
├── quantize_nvfp4() — mtq.quantize(model, NVFP4_CFG, forward_loop)
|
| 44 |
+
├── evaluate_quantized() — 在 DriveLM val 上跑全套指标
|
| 45 |
+
├── main() — 依次: bf16 baseline → INT8 → NVFP4, 打印汇总
|
| 46 |
+
└── CLI args: --mode {all,int8,nvfp4} --subset --split --skip_baseline_eval
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## Files
|
| 50 |
+
- **Create**: `unidrive_vla_nusc_base_quantize_all.py` (~500 lines)
|
| 51 |
+
- **Reference** (不修改): `unidrive_vla_nusc_base_evaluation.py`
|
README.md
CHANGED
|
@@ -8,7 +8,7 @@ pinned: false
|
|
| 8 |
license: apache-2.0
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
| 12 |
|
| 13 |
脚本 hgnetv2_b2_qat.py 已完成并验证。以下是摘要:
|
| 14 |
|
|
@@ -31,25 +31,16 @@ license: apache-2.0
|
|
| 31 |
|
| 32 |
CLI 标志
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 45 |
-
│ --train-samples │ 0 │ 限制 QAT 训练样本(0=全部) │
|
| 46 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 47 |
-
│ --device │ cpu │ 训练设备(如可用则使用 cuda) │
|
| 48 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 49 |
-
│ --subset │ 0 │ 评估前 N 张图像(0=所有 5万张) │
|
| 50 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 51 |
-
│ --skip-eval │ - │ 跳过中间 PTQ/QAT PyTorch 评估 │
|
| 52 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 53 |
|
| 54 |
- 默认使用 CPU,因为 modelopt 的 CUDA 假量化扩展在此系统上无法编译 (nvcc 不支持 c++20)。请使用 --device cuda,如果您的 CUDA 扩展正常工作。
|
| 55 |
- 使用 modelopt PyTorch QAT (modelopt.torch.quantization) 而非已弃用的 torch.ao.quantization,因为 modelopt 已安装并支持 INT8_DEFAULT_CFG
|
|
@@ -58,98 +49,60 @@ license: apache-2.0
|
|
| 58 |
|
| 59 |
CLI 标志
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 68 |
-
│ --batch-size │ 32 │ 训练/评估的批处理大小 │
|
| 69 |
-
├─────────────────┼────────┼───��─────────────────────────────┤
|
| 70 |
-
│ --calib-samples │ 1000 │ PTQ 校准图像数量 │
|
| 71 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 72 |
-
│ --train-samples │ 0 │ 限制 QAT 训练样本(0=全部) │
|
| 73 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 74 |
下的 CNN 模型。
|
| 75 |
- ONNX 导出使用 dynamo=False,因为 modelopt 中的数据相关控制流(if min_amax < 0)与 torch.export 不兼容。
|
| 76 |
|
| 77 |
CLI 标志
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
│ --subset │ 0 │ 评估前 N 张图像(0=所有 5万张) │
|
| 106 |
-
│ --batch-size │ 32 │ 训练/评估的批处理大小 │
|
| 107 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 108 |
-
│ --calib-samples │ 1000 │ PTQ 校准图像数量 │
|
| 109 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 110 |
-
│ --train-samples │ 0 │ 限制 QAT 训练样本(0=全部) │
|
| 111 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 112 |
-
│ --device │ cpu │ 训练设备(如可用则使用 cuda) │
|
| 113 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 114 |
-
│ --subset │ 0 │ 评估前 N 张图像(0=所有 5万张) │
|
| 115 |
-
│ --train-samples │ 0 │ 限制 QAT 训练样本(0=全部) │
|
| 116 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 117 |
-
│ --device │ cpu │ 训练设备(如可用则使用 cuda) │
|
| 118 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 119 |
-
│ --subset │ 0 │ 评估前 N 张图像(0=所有 5万张) │
|
| 120 |
-
│ --device │ cpu │ 训练设备(如可用则使用 cuda) │
|
| 121 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 122 |
-
│ --subset │ 0 │ 评估前 N 张图像(0=所有 5万张) │
|
| 123 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 124 |
-
│ --subset │ 0 │ 评估前 N 张图像(0=所有 5万张) │
|
| 125 |
下的 CNN 模型。
|
| 126 |
- ONNX 导出使用 dynamo=False,因为 modelopt 中的数据相关控制流(if min_amax < 0)与 torch.export 不兼容。
|
| 127 |
|
| 128 |
CLI 标志
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 143 |
-
│ --device │ cpu │ 训练设备(如可用则使用 cuda) │
|
| 144 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 145 |
-
│ --subset │ 0 │ 评估前 N 张图像(0=所有 5万张) │
|
| 146 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 147 |
-
│ --skip-eval │ - │ 跳过中间 PTQ/QAT PyTorch 评估 │
|
| 148 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 149 |
-
│ --calib-only │ - │ 在 PTQ 校准后停止 │
|
| 150 |
-
├─────────────────┼────────┼─────────────────────────────────┤
|
| 151 |
-
│ --eval-only │ - │ 只评估现有 ONNX 模型 │
|
| 152 |
-
└─────────────────┴────────┴─────────────────────────────────┘
|
| 153 |
|
| 154 |
输出文件
|
| 155 |
|
|
@@ -159,46 +112,33 @@ license: apache-2.0
|
|
| 159 |
└── hgnetv2_b2_int8_qat_calib.pth 45.3 MB # PTQ 校准检查点
|
| 160 |
|
| 161 |
|
| 162 |
-
|
| 163 |
-
|
|
|
|
| 164 |
|
| 165 |
✅ 全部完成
|
| 166 |
|
| 167 |
1. 项目传输
|
| 168 |
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
│ ImageNet arrow shards (14个) │ 6.3 GB │ ~/.cache/huggingface/datasets/Tsomaros___imagenet-1k_validation/... │ ✅ │
|
| 175 |
-
├────────────────────────────────┼────────┼─────��───────────────────────────────────────────────────────────────┼──────┤
|
| 176 |
-
│ HF model cache (ViT + hgnetv2) │ 2.4 GB │ ~/.cache/huggingface/hub/ │ ✅ │
|
| 177 |
-
└────────────────────────────────┴────────┴─────────────────────────────────────────────────────────────────────┴──────┘
|
| 178 |
|
| 179 |
2. 远程环境配置
|
| 180 |
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
│ timm │ 1.0.27 (升级自0.9.2) │ ✅ │
|
| 193 |
-
├──────────────────┼────────────────────────┼──────┤
|
| 194 |
-
│ transformers │ 4.57.6 │ ✅ │
|
| 195 |
-
├──────────────────┼────────────────────────┼──────┤
|
| 196 |
-
│ nvidia-modelopt │ 0.43.0.dev99 │ ✅ │
|
| 197 |
-
├──────────────────┼────────────────────────┼──────┤
|
| 198 |
-
│ numpy │ 1.26.4 (升级自1.23.0) │ ✅ │
|
| 199 |
-
├──────────────────┼────────────────────────┼──────┤
|
| 200 |
-
│ HF_HUB_OFFLINE │ 1 (已写入.bashrc) │ ✅ │
|
| 201 |
-
└──────────────────┴────────────────────────┴──────┘
|
| 202 |
|
| 203 |
3. prepare_env.sh
|
| 204 |
|
|
@@ -218,4 +158,111 @@ license: apache-2.0
|
|
| 218 |
|
| 219 |
5. 代码修复
|
| 220 |
|
| 221 |
-
- hgnetv2_b2_eval_quantized.py: pretrained=True → pretrained=False(获取 transform 不需要下载权重)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
license: apache-2.0
|
| 9 |
---
|
| 10 |
|
| 11 |
+
QAT
|
| 12 |
|
| 13 |
脚本 hgnetv2_b2_qat.py 已完成并验证。以下是摘要:
|
| 14 |
|
|
|
|
| 31 |
|
| 32 |
CLI 标志
|
| 33 |
|
| 34 |
+
| 标志 | 默认值 | 描述 |
|
| 35 |
+
| --- | ---- | ------------------------------ |
|
| 36 |
+
| --epochs | 3 | QAT 微调周期数 |
|
| 37 |
+
| --lr | 1e-5 | 学习率 |
|
| 38 |
+
| --batch-size | 32 | 训练/评估的批处理大小 |
|
| 39 |
+
| --calib-samples | 1000 | PTQ 校准图像数量 |
|
| 40 |
+
| --train-samples | 0 | 限制 QAT 训练样本(0=全部) |
|
| 41 |
+
| --device | cpu | 训练设备(如可用则使用 cuda) |
|
| 42 |
+
| --subset | 0 | 评估前 N 张图像(0=所有 5万张) |
|
| 43 |
+
| --skip-eval | - | 跳过中间 PTQ/QAT PyTorch 评估 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
- 默认使用 CPU,因为 modelopt 的 CUDA 假量化扩展在此系统上无法编译 (nvcc 不支持 c++20)。请使用 --device cuda,如果您的 CUDA 扩展正常工作。
|
| 46 |
- 使用 modelopt PyTorch QAT (modelopt.torch.quantization) 而非已弃用的 torch.ao.quantization,因为 modelopt 已安装并支持 INT8_DEFAULT_CFG
|
|
|
|
| 49 |
|
| 50 |
CLI 标志
|
| 51 |
|
| 52 |
+
| 标志 | 默认值 | 描述 |
|
| 53 |
+
| --- | ---- | ------------------------------ |
|
| 54 |
+
| --epochs | 3 | QAT 微调周期数 |
|
| 55 |
+
| --lr | 1e-5 | 学习率 |
|
| 56 |
+
| --calib-samples | 1000 | PTQ 校准图像数量 |
|
| 57 |
+
| --train-samples | 0 | 限制 QAT 训练样本(0=全部) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
下的 CNN 模型。
|
| 59 |
- ONNX 导出使用 dynamo=False,因为 modelopt 中的数据相关控制流(if min_amax < 0)与 torch.export 不兼容。
|
| 60 |
|
| 61 |
CLI 标志
|
| 62 |
|
| 63 |
+
| 标志 | 默认值 | 描述 |
|
| 64 |
+
| --- | ---- | ------------------------------ |
|
| 65 |
+
| --epochs | 3 | QAT 微调周期数 |
|
| 66 |
+
| --lr | 1e-5 | 学习率 |
|
| 67 |
+
| --batch-size | 32 | 训练/评估的批处理大小 |
|
| 68 |
+
| --calib-samples | 1000 | PTQ 校准图像数量 |
|
| 69 |
+
| --train-samples | 0 | 限制 QAT 训练样本(0=全部) |
|
| 70 |
+
| --device | cpu | 训练设备(如可用则使用 cuda) |
|
| 71 |
+
| --subset | 0 | 评估前 N 张图像(0=所有 5万张) |
|
| 72 |
+
| --lr | 1e-5 | 学习率 |
|
| 73 |
+
| --batch-size | 32 | 训练/评估的批处理大小 |
|
| 74 |
+
| --calib-samples | 1000 | PTQ 校准图像数量 |
|
| 75 |
+
| --train-samples | 0 | 限制 QAT 训练样本(0=全部) |
|
| 76 |
+
| --device | cpu | 训练设备(如可用则使用 cuda) |
|
| 77 |
+
| --subset | 0 | 评估前 N 张图像(0=所有 5万张) |
|
| 78 |
+
| --batch-size | 32 | 训练/评估的批处理大小 |
|
| 79 |
+
| --calib-samples | 1000 | PTQ 校准图像数量 |
|
| 80 |
+
| --train-samples | 0 | 限制 QAT 训练样本(0=全部) |
|
| 81 |
+
| --device | cpu | 训练设备(如可用则使用 cuda) |
|
| 82 |
+
| --subset | 0 | 评估前 N 张图像(0=所有 5万张) |
|
| 83 |
+
| --train-samples | 0 | 限制 QAT 训练样本(0=全部) |
|
| 84 |
+
| --device | cpu | 训练设备(如可用则使用 cuda) |
|
| 85 |
+
| --subset | 0 | 评估前 N 张图像(0=所有 5万张) |
|
| 86 |
+
| --device | cpu | 训练设备(如可用则使用 cuda) |
|
| 87 |
+
| --subset | 0 | 评估前 N 张图像(0=所有 5万张) |
|
| 88 |
+
| --subset | 0 | 评估前 N 张图像(0=所有 5万张) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
下的 CNN 模型。
|
| 90 |
- ONNX 导出使用 dynamo=False,因为 modelopt 中的数据相关控制流(if min_amax < 0)与 torch.export 不兼容。
|
| 91 |
|
| 92 |
CLI 标志
|
| 93 |
|
| 94 |
+
| 标志 | 默认值 | 描述 |
|
| 95 |
+
| --- | ---- | ------------------------------ |
|
| 96 |
+
| --epochs | 3 | QAT 微调周期数 |
|
| 97 |
+
| --lr | 1e-5 | 学习率 |
|
| 98 |
+
| --batch-size | 32 | 训练/评估的批处理大小 |
|
| 99 |
+
| --calib-samples | 1000 | PTQ 校准图像数量 |
|
| 100 |
+
| --train-samples | 0 | 限制 QAT 训练样本(0=全部) |
|
| 101 |
+
| --device | cpu | 训练设备(如可用则使用 cuda) |
|
| 102 |
+
| --subset | 0 | 评估前 N 张图像(0=所有 5万张) |
|
| 103 |
+
| --skip-eval | - | 跳过中间 PTQ/QAT PyTorch 评估 |
|
| 104 |
+
| --calib-only | - | 在 PTQ 校准后停止 |
|
| 105 |
+
| --eval-only | - | 只评估现有 ONNX 模型 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
输出文件
|
| 108 |
|
|
|
|
| 112 |
└── hgnetv2_b2_int8_qat_calib.pth 45.3 MB # PTQ 校准检查点
|
| 113 |
|
| 114 |
|
| 115 |
+
---
|
| 116 |
+
|
| 117 |
+
脚本已创建完成:prepare_env.sh
|
| 118 |
|
| 119 |
✅ 全部完成
|
| 120 |
|
| 121 |
1. 项目传输
|
| 122 |
|
| 123 |
+
| 项目 | 大小 | 目标路径 | 状态 |
|
| 124 |
+
| --- | ------ | ---------------------------------------------------------- | ---- |
|
| 125 |
+
| MODULES_PLAY | 7.8 GB | /mnt/vepfs/share/GW00387266/MODULES_PLAY/ | ✅ |
|
| 126 |
+
| ImageNet arrow shards (14个) | 6.3 GB | ~/.cache/huggingface/datasets/Tsomaros___imagenet-1k_validation/... | ✅ |
|
| 127 |
+
| HF model cache (ViT + hgnetv2) | 2.4 GB | ~/.cache/huggingface/hub/ | ✅ |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
2. 远程环境配置
|
| 130 |
|
| 131 |
+
| 组件 | 版本 | 状态 |
|
| 132 |
+
| --- | ---------------------- | ---- |
|
| 133 |
+
| GPU | NVIDIA H20 48GB | ✅ |
|
| 134 |
+
| CUDA | 12.6 | ✅ |
|
| 135 |
+
| PyTorch | 2.6.0+cu126 | ✅ |
|
| 136 |
+
| ONNX Runtime GPU | 1.21.0 (CUDA+TensorRT) | ✅ |
|
| 137 |
+
| timm | 1.0.27 (升级自0.9.2) | ✅ |
|
| 138 |
+
| transformers | 4.57.6 | ✅ |
|
| 139 |
+
| nvidia-modelopt | 0.43.0.dev99 | ✅ |
|
| 140 |
+
| numpy | 1.26.4 (升级自1.23.0) | ✅ |
|
| 141 |
+
| HF_HUB_OFFLINE | 1 (已写入.bashrc) | ✅ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
3. prepare_env.sh
|
| 144 |
|
|
|
|
| 158 |
|
| 159 |
5. 代码修复
|
| 160 |
|
| 161 |
+
- hgnetv2_b2_eval_quantized.py: pretrained=True → pretrained=False(获取 transform 不需要下载权重)
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
---
|
| 165 |
+
|
| 166 |
+
脚本已创建完成:unidrive_vla_nusc_base_evaluation.py
|
| 167 |
+
|
| 168 |
+
功能概览
|
| 169 |
+
|
| 170 |
+
脚本参考 vit_large_patch16_224_evaluate.py 的结构,实现了以下功能:
|
| 171 |
+
|
| 172 |
+
模型加载
|
| 173 |
+
|
| 174 |
+
- 下载并加载 owl10/UniDriveVLA_Nusc_Base_Stage1(Qwen3VLForConditionalGeneration,~2.1B 参数,BF16 精度)
|
| 175 |
+
- 使用 AutoProcessor + qwen_vl_utils.process_vision_info 处理多视角图像输入
|
| 176 |
+
- 6 个 nuScenes 摄像头视角通过特殊 token 映射:<FRONT_VIEW>, <FRONT_LEFT_VIEW> 等
|
| 177 |
+
|
| 178 |
+
数据集
|
| 179 |
+
|
| 180 |
+
- 从 HuggingFace 下载 OpenDriveLab/DriveLM 的 v1.1 nuScenes JSON( gated,需先申请访问权限)
|
| 181 |
+
- 自动解析 scene → key_frame → QA 的层级结构
|
| 182 |
+
- 支持 4 类任务:perception / prediction / planning / behavior
|
| 183 |
+
- 图像路径自动解析到 nuScenes samples/ 目录
|
| 184 |
+
|
| 185 |
+
评测指标(遵循 DriveLM Challenge 规范)
|
| 186 |
+
|
| 187 |
+
| Tag | 指标 | 适用问题类型 | 实现 |
|
| 188 |
+
| --- | ---- | ---------- | ---- |
|
| 189 |
+
| 0 | Accuracy | 多选/是否/behavior | 精确匹配 |
|
| 190 |
+
| 1 | GPT-Score | 开放式 planning | GPT-3.5 打分 (0-100) |
|
| 191 |
+
| 2 | Language | 描述性 perception | BLEU-1/2/3/4, ROUGE-L, CIDEr |
|
| 192 |
+
| 3 | Match | 坐标引用 prediction | F1(16px L1阈值) + GPT打分 |
|
| 193 |
+
|
| 194 |
+
Final Score = 0.4×GPT + 0.2×Language + 0.2×Match + 0.2×Accuracy
|
| 195 |
+
|
| 196 |
+
使用方法
|
| 197 |
+
|
| 198 |
+
# 基本评测(不含 GPT-Score)
|
| 199 |
+
python unidrive_vla_nusc_base_evaluation.py \
|
| 200 |
+
--data_dir /path/to/DriveLM \
|
| 201 |
+
--nuscenes_dir /path/to/nuscenes
|
| 202 |
+
|
| 203 |
+
# 完整评测(含 GPT-Score,需 OpenAI API Key)
|
| 204 |
+
python unidrive_vla_nusc_base_evaluation.py \
|
| 205 |
+
--data_dir /path/to/DriveLM \
|
| 206 |
+
--nuscenes_dir /path/to/nuscenes \
|
| 207 |
+
--gpt_eval --openai_api_key sk-xxx
|
| 208 |
+
|
| 209 |
+
# 快速测试(子集)
|
| 210 |
+
python unidrive_vla_nusc_base_evaluation.py \
|
| 211 |
+
--subset 50 --data_dir /path/to/DriveLM
|
| 212 |
+
|
| 213 |
+
# 保存/加载预测结果(避免重复推理)
|
| 214 |
+
python unidrive_vla_nusc_base_evaluation.py \
|
| 215 |
+
--save_predictions results.json ...
|
| 216 |
+
python unidrive_vla_nusc_base_evaluation.py \
|
| 217 |
+
--load_predictions results.json --gpt_eval
|
| 218 |
+
|
| 219 |
+
依赖
|
| 220 |
+
|
| 221 |
+
- transformers (需 Qwen3-VL 支持)
|
| 222 |
+
- qwen-vl-utils
|
| 223 |
+
- nltk, rouge-score, pycocoevalcap(Language 指标)
|
| 224 |
+
- openai(GPT-Score,可选)
|
| 225 |
+
- huggingface_hub(数据集下载)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
unidrive_vla_nusc_base_quantize_all.py 已创建完成 (586 行)。
|
| 229 |
+
|
| 230 |
+
脚本结构
|
| 231 |
+
|
| 232 |
+
量化 (ModelOpt PyTorch 原生 API)
|
| 233 |
+
|
| 234 |
+
┌───────┬────────────────────┬────────────────────────────────────────────┐
|
| 235 |
+
│ 模式 │ 方法 │ 说明 │
|
| 236 |
+
├───────┼────────────────────┼────────────────────────────────────────────┤
|
| 237 |
+
│ INT8 │ percentile │ 激活+权重均量化,percentile 对 VLM 比 max │
|
| 238 |
+
│ │ calibration │ 更稳健 │
|
| 239 |
+
├───────┼────────────────────┼────────────────────────────────────────────┤
|
| 240 |
+
│ NVFP4 │ awq_lite │ NVIDIA FP4 格式,仅量化权重,AWQ │
|
| 241 |
+
│ │ weight-only │ 保留重要权重 │
|
| 242 |
+
└───────┴────────────────────┴────────────────────────────────────────────┘
|
| 243 |
+
|
| 244 |
+
校准数据
|
| 245 |
+
|
| 246 |
+
- 从 DriveLM 训练集取 128 个样本的多视角图像+问题
|
| 247 |
+
- 通过 forward_loop 传入 mtq.quantize() 收集激活统计
|
| 248 |
+
|
| 249 |
+
评测
|
| 250 |
+
|
| 251 |
+
- 完全复用 unidrive_vla_nusc_base_evaluation.py 的全套指标:
|
| 252 |
+
- Accuracy (tag 0)、GPT-Score (tag 1)、Language Score (tag 2)、Match Score (tag 3)
|
| 253 |
+
- Final Score = 0.4×GPT + 0.2×Language + 0.2×Match + 0.2×Accuracy
|
| 254 |
+
|
| 255 |
+
执行流程
|
| 256 |
+
|
| 257 |
+
bf16 baseline → INT8 量化+评测 → NVFP4 量化+评测 → 汇总对比表 → JSON 保存
|
| 258 |
+
|
| 259 |
+
CLI 用法
|
| 260 |
+
|
| 261 |
+
python unidrive_vla_nusc_base_quantize_all.py # 全部模式
|
| 262 |
+
python unidrive_vla_nusc_base_quantize_all.py --mode int8 # 仅 INT8
|
| 263 |
+
python unidrive_vla_nusc_base_quantize_all.py --mode nvfp4 # 仅 NVFP4
|
| 264 |
+
python unidrive_vla_nusc_base_quantize_all.py --skip_baseline_eval # 跳过 bf16
|
| 265 |
+
python unidrive_vla_nusc_base_quantize_all.py --gpt_eval --openai_api_key KEY # GPT评分
|
| 266 |
+
python unidrive_vla_nusc_base_quantize_all.py --subset 100 # 评测前100样本
|
| 267 |
+
|
| 268 |
+
▎ 注意: 实际运行时 ModelOpt 的 NVFP4 配置可能需要根据安装版本微调 (nf4 vs fp4 格式名、block_size 参数等),建议先跑 --mode int8 验证基础流程,再调试 NVFP4。
|
unidrive_vla_nusc_base_evaluation.py
ADDED
|
@@ -0,0 +1,915 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Evaluate owl10/UniDriveVLA_Nusc_Base_Stage1 on the DriveLM benchmark.
|
| 3 |
+
|
| 4 |
+
UniDriveVLA is a Vision-Language-Action (VLA) model for autonomous driving built on
|
| 5 |
+
Qwen3-VL-2B-Instruct. The Stage-1 checkpoint is the VLM pretraining stage that
|
| 6 |
+
produces a driving-aware VLM backbone.
|
| 7 |
+
|
| 8 |
+
We evaluate on the DriveLM (OpenDriveLab/DriveLM) Graph-VQA benchmark, which
|
| 9 |
+
provides perception / prediction / planning / behavior QA pairs over nuScenes
|
| 10 |
+
multi-view camera frames.
|
| 11 |
+
|
| 12 |
+
Evaluation metrics (following the DriveLM challenge):
|
| 13 |
+
- Accuracy (tag 0): exact match for multi-choice / yes-no questions
|
| 14 |
+
- GPT-Score (tag 1): GPT-rated similarity for open-ended planning Qs
|
| 15 |
+
- Language Score (tag 2): BLEU-1/2/3/4, ROUGE-L, CIDEr for descriptive Qs
|
| 16 |
+
- Match Score (tag 3): F1 coordinate matching + GPT score for object-ref Qs
|
| 17 |
+
- Final Score = 0.4*GPT + 0.2*Language + 0.2*Match + 0.2*Accuracy
|
| 18 |
+
|
| 19 |
+
Dataset: OpenDriveLab/DriveLM (v1.1 nuScenes, gated — request access first)
|
| 20 |
+
Model: owl10/UniDriveVLA_Nusc_Base_Stage1 (Qwen3VLForConditionalGeneration)
|
| 21 |
+
|
| 22 |
+
Usage:
|
| 23 |
+
python unidrive_vla_nusc_base_evaluation.py \
|
| 24 |
+
[--batch_size 1] [--num_workers 4] [--subset 0] \
|
| 25 |
+
[--split train] [--gpt_eval] [--openai_api_key YOUR_KEY]
|
| 26 |
+
|
| 27 |
+
Notes:
|
| 28 |
+
- This script uses the Stage-1 VLM checkpoint only (no perception/planning
|
| 29 |
+
experts from Stages 2-3). Results will be VQA-only, not end-to-end driving.
|
| 30 |
+
- DriveLM is gated; run `huggingface-cli login` and request access at
|
| 31 |
+
https://huggingface.co/datasets/OpenDriveLab/DriveLM before running.
|
| 32 |
+
- nuScenes images must be downloaded separately or via the DriveLM image zip.
|
| 33 |
+
- GPT-Score requires an OpenAI API key (--openai_api_key or OPENAI_API_KEY env).
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
import argparse
|
| 37 |
+
import json
|
| 38 |
+
import os
|
| 39 |
+
import re
|
| 40 |
+
import time
|
| 41 |
+
from collections import defaultdict
|
| 42 |
+
|
| 43 |
+
# ---------------------------------------------------------------------------
|
| 44 |
+
# Patch: disable MLX on non-Apple platforms
|
| 45 |
+
# ---------------------------------------------------------------------------
|
| 46 |
+
# The `mlx` pip package may be installed on Linux but its native shared
|
| 47 |
+
# library (libmlx.so) is Apple-Silicon-only. Transformers checks
|
| 48 |
+
# `_is_mlx_available` at import time and later calls `import mlx.core`
|
| 49 |
+
# inside `is_mlx_array()` during ModelOutput construction, which crashes
|
| 50 |
+
# with "libmlx.so: cannot open shared object file". We pre-emptively
|
| 51 |
+
# set the flag to False so that code path is never taken.
|
| 52 |
+
# ---------------------------------------------------------------------------
|
| 53 |
+
import transformers.utils.generic as _tug
|
| 54 |
+
_tug._is_mlx_available = False
|
| 55 |
+
|
| 56 |
+
import numpy as np
|
| 57 |
+
import torch
|
| 58 |
+
from datasets import load_dataset
|
| 59 |
+
from huggingface_hub import hf_hub_download
|
| 60 |
+
from PIL import Image
|
| 61 |
+
from tqdm import tqdm
|
| 62 |
+
|
| 63 |
+
# ---------------------------------------------------------------------------
|
| 64 |
+
# Metric helpers
|
| 65 |
+
# ---------------------------------------------------------------------------
|
| 66 |
+
|
| 67 |
+
# View token mapping: nuScenes camera name -> UniDriveVLA special token
|
| 68 |
+
CAMERA_VIEW_TOKENS = {
|
| 69 |
+
"CAM_FRONT": "<FRONT_VIEW>",
|
| 70 |
+
"CAM_FRONT_LEFT": "<FRONT_LEFT_VIEW>",
|
| 71 |
+
"CAM_FRONT_RIGHT": "<FRONT_RIGHT_VIEW>",
|
| 72 |
+
"CAM_BACK": "<BACK_VIEW>",
|
| 73 |
+
"CAM_BACK_LEFT": "<BACK_LEFT_VIEW>",
|
| 74 |
+
"CAM_BACK_RIGHT": "<BACK_RIGHT_VIEW>",
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
CAMERAS = list(CAMERA_VIEW_TOKENS.keys())
|
| 78 |
+
|
| 79 |
+
# QA type -> evaluation tag
|
| 80 |
+
# [0] accuracy — multi-choice, yes/no, behavior
|
| 81 |
+
# [1] gpt-score — open-ended planning questions
|
| 82 |
+
# [2] language — descriptive / free-text answers
|
| 83 |
+
# [3] match — coordinate-referencing prediction questions
|
| 84 |
+
QA_TAG_MAP = {
|
| 85 |
+
"perception": 2, # descriptive ("important objects")
|
| 86 |
+
"prediction": 3, # coordinate-matching
|
| 87 |
+
"planning": 1, # open-ended action reasoning
|
| 88 |
+
"behavior": 0, # multi-choice
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def classify_question_tag(task_type: str, question: str, answer: str) -> int:
|
| 93 |
+
"""Assign evaluation tag based on task type and question/answer content.
|
| 94 |
+
|
| 95 |
+
Heuristics follow the DriveLM extract_data.py tagging logic:
|
| 96 |
+
- Behavior questions are always tag 0 (multi-choice)
|
| 97 |
+
- Perception: tag 2 (language) for descriptive, tag 0 if multi-choice
|
| 98 |
+
- Prediction: tag 3 (match) if has c-tags, tag 0 if yes/no, tag 2 otherwise
|
| 99 |
+
- Planning: tag 1 (gpt-score) for action questions, tag 0 if multi-choice
|
| 100 |
+
"""
|
| 101 |
+
has_options = "select the correct answer" in question.lower()
|
| 102 |
+
is_yes_no = answer.strip().lower() in ("yes.", "no.", "yes", "no")
|
| 103 |
+
has_ctag = bool(re.search(r"<c\d+,", question)) or bool(re.search(r"<c\d+,", answer))
|
| 104 |
+
|
| 105 |
+
if task_type == "behavior":
|
| 106 |
+
return 0
|
| 107 |
+
elif task_type == "perception":
|
| 108 |
+
if has_options:
|
| 109 |
+
return 0
|
| 110 |
+
return 2
|
| 111 |
+
elif task_type == "prediction":
|
| 112 |
+
if has_options:
|
| 113 |
+
return 0
|
| 114 |
+
if is_yes_no:
|
| 115 |
+
return 0
|
| 116 |
+
if has_ctag:
|
| 117 |
+
return 3
|
| 118 |
+
return 2
|
| 119 |
+
elif task_type == "planning":
|
| 120 |
+
if has_options:
|
| 121 |
+
return 0
|
| 122 |
+
return 1
|
| 123 |
+
else:
|
| 124 |
+
return 2
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
# ---------------------------------------------------------------------------
|
| 128 |
+
# Accuracy metric (tag 0)
|
| 129 |
+
# ---------------------------------------------------------------------------
|
| 130 |
+
|
| 131 |
+
def compute_accuracy(predictions: list[str], references: list[str]) -> dict:
|
| 132 |
+
"""Exact-match accuracy for multi-choice / yes-no questions."""
|
| 133 |
+
correct = 0
|
| 134 |
+
total = len(predictions)
|
| 135 |
+
for pred, ref in zip(predictions, references):
|
| 136 |
+
pred_clean = pred.strip().lower()
|
| 137 |
+
ref_clean = ref.strip().lower()
|
| 138 |
+
if pred_clean == ref_clean:
|
| 139 |
+
correct += 1
|
| 140 |
+
# Also check first character / word for option-style answers
|
| 141 |
+
elif ref_clean in ("a", "b", "c", "d") and pred_clean.startswith(ref_clean):
|
| 142 |
+
correct += 1
|
| 143 |
+
acc = correct / total if total > 0 else 0.0
|
| 144 |
+
return {"accuracy": acc, "correct": correct, "total": total}
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# ---------------------------------------------------------------------------
|
| 148 |
+
# Language metric (tag 2): BLEU, ROUGE-L, CIDEr
|
| 149 |
+
# ---------------------------------------------------------------------------
|
| 150 |
+
|
| 151 |
+
def compute_language_metrics(predictions: list[str], references: list[str]) -> dict:
|
| 152 |
+
"""Compute BLEU-1/2/3/4, ROUGE-L, and CIDEr using standard NLP libraries."""
|
| 153 |
+
try:
|
| 154 |
+
from nltk.translate.bleu_score import corpus_bleu, SmoothingFunction
|
| 155 |
+
except ImportError:
|
| 156 |
+
print(" [WARN] nltk not installed; BLEU scores will be 0. Install: pip install nltk")
|
| 157 |
+
corpus_bleu = None
|
| 158 |
+
|
| 159 |
+
try:
|
| 160 |
+
from rouge_score import rouge_scorer as _rouge_scorer
|
| 161 |
+
except ImportError:
|
| 162 |
+
print(" [WARN] rouge_score not installed; ROUGE-L will be 0. Install: pip install rouge-score")
|
| 163 |
+
_rouge_scorer = None
|
| 164 |
+
|
| 165 |
+
try:
|
| 166 |
+
from pycocoevalcap.cider.cider import Cider
|
| 167 |
+
except ImportError:
|
| 168 |
+
print(" [WARN] pycocoevalcap not installed; CIDEr will be 0. Install: pip install pycocoevalcap")
|
| 169 |
+
Cider = None
|
| 170 |
+
|
| 171 |
+
results = {
|
| 172 |
+
"Bleu_1": 0.0, "Bleu_2": 0.0, "Bleu_3": 0.0, "Bleu_4": 0.0,
|
| 173 |
+
"ROUGE_L": 0.0, "CIDEr": 0.0,
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
if not predictions:
|
| 177 |
+
return results
|
| 178 |
+
|
| 179 |
+
# Tokenize
|
| 180 |
+
def _tokenize(s):
|
| 181 |
+
return s.lower().strip().split()
|
| 182 |
+
|
| 183 |
+
ref_tokens = [[_tokenize(r)] for r in references] # list of list of token lists
|
| 184 |
+
pred_tokens = [_tokenize(p) for p in predictions]
|
| 185 |
+
|
| 186 |
+
# BLEU
|
| 187 |
+
if corpus_bleu is not None:
|
| 188 |
+
smooth = SmoothingFunction().method1
|
| 189 |
+
for n in range(1, 5):
|
| 190 |
+
weights = [1.0 / n] * n + [0.0] * (4 - n)
|
| 191 |
+
score = corpus_bleu(ref_tokens, pred_tokens, weights=weights, smoothing_function=smooth)
|
| 192 |
+
results[f"Bleu_{n}"] = score
|
| 193 |
+
|
| 194 |
+
# ROUGE-L
|
| 195 |
+
if _rouge_scorer is not None:
|
| 196 |
+
scorer = _rouge_scorer.RougeScorer(["rougeL"], use_stemmer=True)
|
| 197 |
+
rouge_scores = []
|
| 198 |
+
for pred, ref in zip(predictions, references):
|
| 199 |
+
s = scorer.score(ref, pred)
|
| 200 |
+
rouge_scores.append(s["rougeL"].fmeasure)
|
| 201 |
+
results["ROUGE_L"] = np.mean(rouge_scores)
|
| 202 |
+
|
| 203 |
+
# CIDEr
|
| 204 |
+
if Cider is not None:
|
| 205 |
+
cider = Cider()
|
| 206 |
+
# CIDEr expects dict format: {id: [reference], id: [hypothesis]}
|
| 207 |
+
gts = {i: [r] for i, r in enumerate(references)}
|
| 208 |
+
res = {i: [p] for i, p in enumerate(predictions)}
|
| 209 |
+
try:
|
| 210 |
+
score, _ = cider.compute_score(gts, res)
|
| 211 |
+
results["CIDEr"] = score
|
| 212 |
+
except Exception as e:
|
| 213 |
+
print(f" [WARN] CIDEr computation failed: {e}")
|
| 214 |
+
|
| 215 |
+
# Normalized language score (following DriveLM challenge)
|
| 216 |
+
# (Bleu_1 + Bleu_4) / 3 + ROUGE_L / 3 + CIDEr / 10 / 3
|
| 217 |
+
norm = (
|
| 218 |
+
(results["Bleu_1"] + results["Bleu_4"]) / 3.0
|
| 219 |
+
+ results["ROUGE_L"] / 3.0
|
| 220 |
+
+ results["CIDEr"] / 10.0 / 3.0
|
| 221 |
+
)
|
| 222 |
+
results["language_score_normalized"] = norm
|
| 223 |
+
|
| 224 |
+
return results
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
# ---------------------------------------------------------------------------
|
| 228 |
+
# GPT-Score metric (tag 1)
|
| 229 |
+
# ---------------------------------------------------------------------------
|
| 230 |
+
|
| 231 |
+
def gpt_score_single(question: str, prediction: str, reference: str, client) -> float:
|
| 232 |
+
"""Score a single prediction against reference using GPT-3.5/4."""
|
| 233 |
+
prompt = (
|
| 234 |
+
"Rate my answer based on the correct answer out of 100, with higher "
|
| 235 |
+
"scores indicating that the answer is closer to the correct answer, "
|
| 236 |
+
"and you should be accurate to single digits like 62, 78, 41, etc. "
|
| 237 |
+
"Output the number only.\n\n"
|
| 238 |
+
f"Question: {question}\n"
|
| 239 |
+
f"Correct answer: {reference}\n"
|
| 240 |
+
f"My answer: {prediction}"
|
| 241 |
+
)
|
| 242 |
+
try:
|
| 243 |
+
resp = client.chat.completions.create(
|
| 244 |
+
model="gpt-3.5-turbo",
|
| 245 |
+
messages=[{"role": "user", "content": prompt}],
|
| 246 |
+
max_tokens=10,
|
| 247 |
+
temperature=0.0,
|
| 248 |
+
)
|
| 249 |
+
text = resp.choices[0].message.content.strip()
|
| 250 |
+
score = float(re.search(r"\d+", text).group())
|
| 251 |
+
return min(max(score, 0.0), 100.0)
|
| 252 |
+
except Exception as e:
|
| 253 |
+
print(f" [WARN] GPT scoring failed: {e}")
|
| 254 |
+
return 0.0
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def compute_gpt_score(
|
| 258 |
+
questions: list[str],
|
| 259 |
+
predictions: list[str],
|
| 260 |
+
references: list[str],
|
| 261 |
+
api_key: str | None = None,
|
| 262 |
+
) -> dict:
|
| 263 |
+
"""Compute GPT-score for open-ended QA pairs."""
|
| 264 |
+
key = api_key or os.environ.get("OPENAI_API_KEY")
|
| 265 |
+
if not key:
|
| 266 |
+
print(" [WARN] No OpenAI API key; GPT-Score will be 0. "
|
| 267 |
+
"Set --openai_api_key or OPENAI_API_KEY env var.")
|
| 268 |
+
return {"gpt_score": 0.0, "total": len(predictions), "scored": 0}
|
| 269 |
+
|
| 270 |
+
from openai import OpenAI
|
| 271 |
+
client = OpenAI(api_key=key)
|
| 272 |
+
|
| 273 |
+
scores = []
|
| 274 |
+
for q, pred, ref in tqdm(
|
| 275 |
+
zip(questions, predictions, references),
|
| 276 |
+
total=len(questions),
|
| 277 |
+
desc=" GPT scoring",
|
| 278 |
+
disable=None,
|
| 279 |
+
):
|
| 280 |
+
s = gpt_score_single(q, pred, ref, client)
|
| 281 |
+
scores.append(s)
|
| 282 |
+
# Small delay to avoid rate limits
|
| 283 |
+
time.sleep(0.1)
|
| 284 |
+
|
| 285 |
+
avg = np.mean(scores) if scores else 0.0
|
| 286 |
+
return {"gpt_score": avg, "total": len(scores), "scored": len(scores)}
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
# ---------------------------------------------------------------------------
|
| 290 |
+
# Match metric (tag 3): coordinate F1 + GPT score
|
| 291 |
+
# ---------------------------------------------------------------------------
|
| 292 |
+
|
| 293 |
+
def extract_coordinates(text: str) -> list[tuple[str, float, float]]:
|
| 294 |
+
"""Extract <cN,CAM,x,y> coordinate references from text."""
|
| 295 |
+
pattern = r"<c(\d+),([^,]+),([\d.]+),([\d.]+)>"
|
| 296 |
+
matches = re.findall(pattern, text)
|
| 297 |
+
return [(f"c{m[0]}", float(m[2]), float(m[3])) for m in matches]
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def compute_match_score(
|
| 301 |
+
questions: list[str],
|
| 302 |
+
predictions: list[str],
|
| 303 |
+
references: list[str],
|
| 304 |
+
api_key: str | None = None,
|
| 305 |
+
coord_threshold: float = 16.0,
|
| 306 |
+
) -> dict:
|
| 307 |
+
"""Compute match score = (F1 * 100 + GPT_score) / 2.
|
| 308 |
+
|
| 309 |
+
F1 is based on coordinate matching: predicted 2D coords matched to GT
|
| 310 |
+
coords if L1 distance < threshold pixels.
|
| 311 |
+
"""
|
| 312 |
+
all_prec, all_rec, all_f1 = [], [], []
|
| 313 |
+
|
| 314 |
+
for pred, ref in zip(predictions, references):
|
| 315 |
+
pred_coords = extract_coordinates(pred)
|
| 316 |
+
ref_coords = extract_coordinates(ref)
|
| 317 |
+
|
| 318 |
+
if not ref_coords and not pred_coords:
|
| 319 |
+
all_f1.append(1.0)
|
| 320 |
+
all_prec.append(1.0)
|
| 321 |
+
all_rec.append(1.0)
|
| 322 |
+
continue
|
| 323 |
+
if not ref_coords:
|
| 324 |
+
all_f1.append(0.0)
|
| 325 |
+
all_prec.append(0.0)
|
| 326 |
+
all_rec.append(0.0)
|
| 327 |
+
continue
|
| 328 |
+
if not pred_coords:
|
| 329 |
+
all_f1.append(0.0)
|
| 330 |
+
all_prec.append(0.0)
|
| 331 |
+
all_rec.append(0.0)
|
| 332 |
+
continue
|
| 333 |
+
|
| 334 |
+
# Match predicted coords to reference coords by L1 distance
|
| 335 |
+
matched_pred = set()
|
| 336 |
+
matched_ref = set()
|
| 337 |
+
|
| 338 |
+
for ri, (_, rx, ry) in enumerate(ref_coords):
|
| 339 |
+
best_dist = float("inf")
|
| 340 |
+
best_pi = -1
|
| 341 |
+
for pi, (_, px, py) in enumerate(pred_coords):
|
| 342 |
+
if pi in matched_pred:
|
| 343 |
+
continue
|
| 344 |
+
dist = abs(px - rx) + abs(py - ry)
|
| 345 |
+
if dist < best_dist:
|
| 346 |
+
best_dist = dist
|
| 347 |
+
best_pi = pi
|
| 348 |
+
if best_dist < coord_threshold and best_pi >= 0:
|
| 349 |
+
matched_pred.add(best_pi)
|
| 350 |
+
matched_ref.add(ri)
|
| 351 |
+
|
| 352 |
+
tp = len(matched_ref)
|
| 353 |
+
fp = len(pred_coords) - tp
|
| 354 |
+
fn = len(ref_coords) - tp
|
| 355 |
+
|
| 356 |
+
prec = tp / (tp + fp) if (tp + fp) > 0 else 0.0
|
| 357 |
+
rec = tp / (tp + fn) if (tp + fn) > 0 else 0.0
|
| 358 |
+
f1 = 2 * prec * rec / (prec + rec) if (prec + rec) > 0 else 0.0
|
| 359 |
+
|
| 360 |
+
all_prec.append(prec)
|
| 361 |
+
all_rec.append(rec)
|
| 362 |
+
all_f1.append(f1)
|
| 363 |
+
|
| 364 |
+
avg_f1 = np.mean(all_f1) if all_f1 else 0.0
|
| 365 |
+
|
| 366 |
+
# GPT component of match score
|
| 367 |
+
gpt_result = compute_gpt_score(questions, predictions, references, api_key)
|
| 368 |
+
gpt_avg = gpt_result["gpt_score"]
|
| 369 |
+
|
| 370 |
+
match_score = (avg_f1 * 100 + gpt_avg) / 2.0
|
| 371 |
+
|
| 372 |
+
return {
|
| 373 |
+
"match_score": match_score,
|
| 374 |
+
"coord_f1": avg_f1,
|
| 375 |
+
"coord_precision": np.mean(all_prec) if all_prec else 0.0,
|
| 376 |
+
"coord_recall": np.mean(all_rec) if all_rec else 0.0,
|
| 377 |
+
"gpt_component": gpt_avg,
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
# ---------------------------------------------------------------------------
|
| 382 |
+
# Final weighted score
|
| 383 |
+
# ---------------------------------------------------------------------------
|
| 384 |
+
|
| 385 |
+
def compute_final_score(
|
| 386 |
+
accuracy: float,
|
| 387 |
+
gpt_score: float,
|
| 388 |
+
language_score: float,
|
| 389 |
+
match_score: float,
|
| 390 |
+
) -> float:
|
| 391 |
+
"""DriveLM weighted final score.
|
| 392 |
+
|
| 393 |
+
Weights: GPT=0.4, Language=0.2, Match=0.2, Accuracy=0.2.
|
| 394 |
+
All scores are in [0, 1] (GPT/match/language normalized).
|
| 395 |
+
"""
|
| 396 |
+
weights = [0.4, 0.2, 0.2, 0.2]
|
| 397 |
+
components = [gpt_score / 100.0, language_score, match_score / 100.0, accuracy]
|
| 398 |
+
return sum(w * c for w, c in zip(weights, components))
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
# ---------------------------------------------------------------------------
|
| 402 |
+
# DriveLM dataset loader
|
| 403 |
+
# ---------------------------------------------------------------------------
|
| 404 |
+
|
| 405 |
+
def load_drivelm_dataset(
|
| 406 |
+
split: str = "train",
|
| 407 |
+
data_dir: str | None = None,
|
| 408 |
+
nuscenes_dir: str | None = None,
|
| 409 |
+
) -> list[dict]:
|
| 410 |
+
"""Load DriveLM QA data and return a list of evaluation samples.
|
| 411 |
+
|
| 412 |
+
Each sample dict has:
|
| 413 |
+
- scene_token, frame_token
|
| 414 |
+
- scene_description
|
| 415 |
+
- image_paths: dict of {CAM_NAME: absolute_path}
|
| 416 |
+
- task_type: perception | prediction | planning | behavior
|
| 417 |
+
- question: str
|
| 418 |
+
- answer: str (ground truth)
|
| 419 |
+
- tag: int (0-3 evaluation tag)
|
| 420 |
+
|
| 421 |
+
Args:
|
| 422 |
+
split: 'train' or 'val'
|
| 423 |
+
data_dir: directory containing the DriveLM JSON and nuscenes images.
|
| 424 |
+
If None, attempts to download from HuggingFace.
|
| 425 |
+
nuscenes_dir: directory containing nuScenes 'samples/' folder.
|
| 426 |
+
"""
|
| 427 |
+
filename = f"v1_1_{split}_nus.json"
|
| 428 |
+
if split == "val":
|
| 429 |
+
filename = "v1_1_val_nus_q_only.json"
|
| 430 |
+
|
| 431 |
+
json_path = None
|
| 432 |
+
|
| 433 |
+
if data_dir:
|
| 434 |
+
candidate = os.path.join(data_dir, filename)
|
| 435 |
+
if os.path.exists(candidate):
|
| 436 |
+
json_path = candidate
|
| 437 |
+
|
| 438 |
+
if json_path is None:
|
| 439 |
+
print(f" Downloading {filename} from HuggingFace ...")
|
| 440 |
+
try:
|
| 441 |
+
json_path = hf_hub_download(
|
| 442 |
+
repo_id="OpenDriveLab/DriveLM",
|
| 443 |
+
filename=filename,
|
| 444 |
+
repo_type="dataset",
|
| 445 |
+
)
|
| 446 |
+
except Exception as e:
|
| 447 |
+
print(f" ERROR: Failed to download {filename}: {e}")
|
| 448 |
+
print(" Please request access at https://huggingface.co/datasets/OpenDriveLab/DriveLM")
|
| 449 |
+
return []
|
| 450 |
+
|
| 451 |
+
print(f" Loading {json_path} ...")
|
| 452 |
+
with open(json_path, "r") as f:
|
| 453 |
+
raw_data = json.load(f)
|
| 454 |
+
|
| 455 |
+
# Determine image base directory
|
| 456 |
+
img_base = nuscenes_dir or data_dir or os.path.dirname(json_path)
|
| 457 |
+
|
| 458 |
+
samples = []
|
| 459 |
+
for scene_token, scene_data in raw_data.items():
|
| 460 |
+
scene_desc = scene_data.get("scene_description", "")
|
| 461 |
+
key_frames = scene_data.get("key_frames", {})
|
| 462 |
+
|
| 463 |
+
for frame_token, frame_data in key_frames.items():
|
| 464 |
+
image_paths_raw = frame_data.get("image_paths", {})
|
| 465 |
+
key_objects = frame_data.get("key_object_infos", {})
|
| 466 |
+
qa = frame_data.get("QA", {})
|
| 467 |
+
|
| 468 |
+
# Resolve absolute image paths
|
| 469 |
+
abs_image_paths = {}
|
| 470 |
+
for cam, rel_path in image_paths_raw.items():
|
| 471 |
+
# Try multiple possible base directories
|
| 472 |
+
candidates = [
|
| 473 |
+
os.path.join(img_base, rel_path),
|
| 474 |
+
os.path.join(img_base, "nuscenes", rel_path),
|
| 475 |
+
os.path.join(img_base, "samples", os.path.basename(rel_path)),
|
| 476 |
+
os.path.join(img_base, "nuscenes", "samples",
|
| 477 |
+
cam, os.path.basename(rel_path)),
|
| 478 |
+
]
|
| 479 |
+
for c in candidates:
|
| 480 |
+
if os.path.exists(c):
|
| 481 |
+
abs_image_paths[cam] = c
|
| 482 |
+
break
|
| 483 |
+
else:
|
| 484 |
+
# Keep relative path; will be checked at inference time
|
| 485 |
+
abs_image_paths[cam] = rel_path
|
| 486 |
+
|
| 487 |
+
for task_type in ["perception", "prediction", "planning", "behavior"]:
|
| 488 |
+
qa_list = qa.get(task_type, [])
|
| 489 |
+
for idx, qa_item in enumerate(qa_list):
|
| 490 |
+
question = qa_item.get("Q", "")
|
| 491 |
+
answer = qa_item.get("A", "")
|
| 492 |
+
if not question:
|
| 493 |
+
continue
|
| 494 |
+
|
| 495 |
+
tag = classify_question_tag(task_type, question, answer)
|
| 496 |
+
|
| 497 |
+
samples.append({
|
| 498 |
+
"id": f"{scene_token}_{frame_token}_{task_type}_{idx}",
|
| 499 |
+
"scene_token": scene_token,
|
| 500 |
+
"frame_token": frame_token,
|
| 501 |
+
"scene_description": scene_desc,
|
| 502 |
+
"image_paths": abs_image_paths,
|
| 503 |
+
"key_objects": key_objects,
|
| 504 |
+
"task_type": task_type,
|
| 505 |
+
"question": question,
|
| 506 |
+
"answer": answer,
|
| 507 |
+
"tag": tag,
|
| 508 |
+
})
|
| 509 |
+
|
| 510 |
+
return samples
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
# ---------------------------------------------------------------------------
|
| 514 |
+
# Model inference
|
| 515 |
+
# ---------------------------------------------------------------------------
|
| 516 |
+
|
| 517 |
+
SYSTEM_PROMPT = (
|
| 518 |
+
"You are an advanced autonomous driving assistant. You analyze multi-view "
|
| 519 |
+
"camera images from a vehicle and answer questions about the driving scene, "
|
| 520 |
+
"including perception, prediction, planning, and behavior. "
|
| 521 |
+
"Provide concise and accurate answers."
|
| 522 |
+
)
|
| 523 |
+
|
| 524 |
+
|
| 525 |
+
@torch.no_grad()
|
| 526 |
+
def run_inference(
|
| 527 |
+
model,
|
| 528 |
+
processor,
|
| 529 |
+
sample: dict,
|
| 530 |
+
device: torch.device,
|
| 531 |
+
max_new_tokens: int = 256,
|
| 532 |
+
) -> str:
|
| 533 |
+
"""Run inference on a single DriveLM sample.
|
| 534 |
+
|
| 535 |
+
Constructs a multi-view chat message using UniDriveVLA's special view tokens,
|
| 536 |
+
processes the inputs, and generates a text response.
|
| 537 |
+
"""
|
| 538 |
+
# Build multi-view image content
|
| 539 |
+
image_content = []
|
| 540 |
+
image_files = []
|
| 541 |
+
for cam in CAMERAS:
|
| 542 |
+
cam_path = sample["image_paths"].get(cam)
|
| 543 |
+
if cam_path and os.path.exists(cam_path):
|
| 544 |
+
view_token = CAMERA_VIEW_TOKENS[cam]
|
| 545 |
+
image_content.append({"type": "text", "text": f"{view_token}"})
|
| 546 |
+
image_content.append({"type": "image", "image": f"file://{cam_path}"})
|
| 547 |
+
image_files.append(cam_path)
|
| 548 |
+
|
| 549 |
+
# Build messages in Qwen3-VL chat format
|
| 550 |
+
user_content = image_content + [
|
| 551 |
+
{"type": "text", "text": sample["question"]},
|
| 552 |
+
]
|
| 553 |
+
|
| 554 |
+
messages = [
|
| 555 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 556 |
+
{"role": "user", "content": user_content},
|
| 557 |
+
]
|
| 558 |
+
|
| 559 |
+
# Process with Qwen3-VL processor
|
| 560 |
+
try:
|
| 561 |
+
from qwen_vl_utils import process_vision_info
|
| 562 |
+
except ImportError:
|
| 563 |
+
print(" [ERROR] qwen_vl_utils not installed. Install: pip install qwen-vl-utils")
|
| 564 |
+
return ""
|
| 565 |
+
|
| 566 |
+
text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 567 |
+
image_inputs, video_inputs = process_vision_info(messages)
|
| 568 |
+
inputs = processor(
|
| 569 |
+
text=[text],
|
| 570 |
+
images=image_inputs,
|
| 571 |
+
videos=video_inputs,
|
| 572 |
+
padding=True,
|
| 573 |
+
return_tensors="pt",
|
| 574 |
+
).to(device)
|
| 575 |
+
|
| 576 |
+
output_ids = model.generate(
|
| 577 |
+
**inputs,
|
| 578 |
+
max_new_tokens=max_new_tokens,
|
| 579 |
+
do_sample=False,
|
| 580 |
+
temperature=1.0, # greedy when do_sample=False
|
| 581 |
+
)
|
| 582 |
+
|
| 583 |
+
# Decode only the generated tokens (skip input)
|
| 584 |
+
generated_ids = output_ids[:, inputs.input_ids.shape[1]:]
|
| 585 |
+
response = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
|
| 586 |
+
return response.strip()
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
# ---------------------------------------------------------------------------
|
| 590 |
+
# Main evaluation loop
|
| 591 |
+
# ---------------------------------------------------------------------------
|
| 592 |
+
|
| 593 |
+
def evaluate_model(
|
| 594 |
+
model,
|
| 595 |
+
processor,
|
| 596 |
+
samples: list[dict],
|
| 597 |
+
device: torch.device,
|
| 598 |
+
max_new_tokens: int = 256,
|
| 599 |
+
print_every: int = 100,
|
| 600 |
+
) -> dict:
|
| 601 |
+
"""Run evaluation on all samples and collect predictions."""
|
| 602 |
+
model.eval()
|
| 603 |
+
predictions = {} # tag -> list of (question, prediction, reference)
|
| 604 |
+
total = len(samples)
|
| 605 |
+
total_inference_time = 0.0
|
| 606 |
+
errors = 0
|
| 607 |
+
|
| 608 |
+
start = time.time()
|
| 609 |
+
for i, sample in enumerate(tqdm(samples, desc="Evaluating", disable=None)):
|
| 610 |
+
t0 = time.perf_counter()
|
| 611 |
+
try:
|
| 612 |
+
pred = run_inference(model, processor, sample, device, max_new_tokens)
|
| 613 |
+
except Exception as e:
|
| 614 |
+
print(f" [ERROR] Sample {sample['id']}: {e}")
|
| 615 |
+
pred = ""
|
| 616 |
+
errors += 1
|
| 617 |
+
t1 = time.perf_counter()
|
| 618 |
+
total_inference_time += (t1 - t0)
|
| 619 |
+
|
| 620 |
+
tag = sample["tag"]
|
| 621 |
+
if tag not in predictions:
|
| 622 |
+
predictions[tag] = {"questions": [], "predictions": [], "references": []}
|
| 623 |
+
predictions[tag]["questions"].append(sample["question"])
|
| 624 |
+
predictions[tag]["predictions"].append(pred)
|
| 625 |
+
predictions[tag]["references"].append(sample["answer"])
|
| 626 |
+
|
| 627 |
+
if print_every and (i + 1) % print_every == 0:
|
| 628 |
+
elapsed = time.time() - start
|
| 629 |
+
speed = (i + 1) / elapsed
|
| 630 |
+
print(f" [{i + 1:>6d}/{total}] {speed:.2f} samples/s "
|
| 631 |
+
f"avg_inference={(t1-t0)*1000:.0f}ms")
|
| 632 |
+
|
| 633 |
+
elapsed = time.time() - start
|
| 634 |
+
return {
|
| 635 |
+
"predictions": predictions,
|
| 636 |
+
"total_samples": total,
|
| 637 |
+
"total_errors": errors,
|
| 638 |
+
"elapsed": elapsed,
|
| 639 |
+
"total_inference_time": total_inference_time,
|
| 640 |
+
"avg_process_ms": elapsed / total * 1000 if total > 0 else 0.0,
|
| 641 |
+
"avg_inference_ms": total_inference_time / total * 1000 if total > 0 else 0.0,
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
|
| 645 |
+
# ---------------------------------------------------------------------------
|
| 646 |
+
# Main
|
| 647 |
+
# ---------------------------------------------------------------------------
|
| 648 |
+
|
| 649 |
+
def main():
|
| 650 |
+
parser = argparse.ArgumentParser(
|
| 651 |
+
description="Evaluate owl10/UniDriveVLA_Nusc_Base_Stage1 on DriveLM"
|
| 652 |
+
)
|
| 653 |
+
parser.add_argument("--batch_size", type=int, default=1,
|
| 654 |
+
help="Batch size (VLM inference is typically 1)")
|
| 655 |
+
parser.add_argument("--num_workers", type=int, default=4,
|
| 656 |
+
help="DataLoader workers (unused for VLM, kept for compat)")
|
| 657 |
+
parser.add_argument("--subset", type=int, default=0,
|
| 658 |
+
help="Evaluate on first N samples only (0 = all)")
|
| 659 |
+
parser.add_argument("--split", type=str, default="train",
|
| 660 |
+
choices=["train", "val"],
|
| 661 |
+
help="DriveLM split to evaluate on")
|
| 662 |
+
parser.add_argument("--data_dir", type=str, default=None,
|
| 663 |
+
help="Local directory with DriveLM JSON + nuScenes images")
|
| 664 |
+
parser.add_argument("--nuscenes_dir", type=str, default=None,
|
| 665 |
+
help="Directory containing nuScenes samples/ folder")
|
| 666 |
+
parser.add_argument("--max_new_tokens", type=int, default=256,
|
| 667 |
+
help="Max tokens to generate per sample")
|
| 668 |
+
parser.add_argument("--gpt_eval", action="store_true",
|
| 669 |
+
help="Enable GPT-Score evaluation (requires OpenAI API key)")
|
| 670 |
+
parser.add_argument("--openai_api_key", type=str, default=None,
|
| 671 |
+
help="OpenAI API key for GPT-Score (or set OPENAI_API_KEY)")
|
| 672 |
+
parser.add_argument("--save_predictions", type=str, default=None,
|
| 673 |
+
help="Save predictions to this JSON file")
|
| 674 |
+
parser.add_argument("--load_predictions", type=str, default=None,
|
| 675 |
+
help="Load predictions from JSON file (skip inference)")
|
| 676 |
+
args = parser.parse_args()
|
| 677 |
+
|
| 678 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 679 |
+
print(f"Device: {device}")
|
| 680 |
+
if torch.cuda.is_available():
|
| 681 |
+
print(f" GPU: {torch.cuda.get_device_name(0)}")
|
| 682 |
+
print(f" VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB")
|
| 683 |
+
|
| 684 |
+
# ------------------------------------------------------------------
|
| 685 |
+
# Load model & processor
|
| 686 |
+
# ------------------------------------------------------------------
|
| 687 |
+
model_name = "owl10/UniDriveVLA_Nusc_Base_Stage1"
|
| 688 |
+
print(f"\nLoading {model_name} ...")
|
| 689 |
+
|
| 690 |
+
from transformers import Qwen3VLForConditionalGeneration, AutoProcessor
|
| 691 |
+
|
| 692 |
+
processor = AutoProcessor.from_pretrained(model_name)
|
| 693 |
+
model = Qwen3VLForConditionalGeneration.from_pretrained(
|
| 694 |
+
model_name,
|
| 695 |
+
torch_dtype=torch.bfloat16,
|
| 696 |
+
device_map="auto",
|
| 697 |
+
)
|
| 698 |
+
model.eval()
|
| 699 |
+
|
| 700 |
+
# Print model info
|
| 701 |
+
param_count = sum(p.numel() for p in model.parameters()) / 1e9
|
| 702 |
+
print(f" Architecture: Qwen3VLForConditionalGeneration")
|
| 703 |
+
print(f" Parameters: {param_count:.2f}B")
|
| 704 |
+
print(f" Dtype: bfloat16")
|
| 705 |
+
|
| 706 |
+
# ------------------------------------------------------------------
|
| 707 |
+
# Load dataset
|
| 708 |
+
# ------------------------------------------------------------------
|
| 709 |
+
print(f"\nLoading DriveLM dataset (split={args.split}) ...")
|
| 710 |
+
samples = load_drivelm_dataset(
|
| 711 |
+
split=args.split,
|
| 712 |
+
data_dir=args.data_dir,
|
| 713 |
+
nuscenes_dir=args.nuscenes_dir,
|
| 714 |
+
)
|
| 715 |
+
|
| 716 |
+
if not samples:
|
| 717 |
+
print("ERROR: No samples loaded. Check dataset access and paths.")
|
| 718 |
+
return
|
| 719 |
+
|
| 720 |
+
# Tag distribution
|
| 721 |
+
tag_counts = defaultdict(int)
|
| 722 |
+
task_counts = defaultdict(int)
|
| 723 |
+
for s in samples:
|
| 724 |
+
tag_counts[s["tag"]] += 1
|
| 725 |
+
task_counts[s["task_type"]] += 1
|
| 726 |
+
|
| 727 |
+
print(f" Total QA pairs: {len(samples)}")
|
| 728 |
+
print(f" Task distribution: {dict(task_counts)}")
|
| 729 |
+
print(f" Tag distribution: {dict(tag_counts)}")
|
| 730 |
+
|
| 731 |
+
if args.subset > 0:
|
| 732 |
+
samples = samples[:args.subset]
|
| 733 |
+
print(f" Using subset: {len(samples)} samples")
|
| 734 |
+
|
| 735 |
+
# ------------------------------------------------------------------
|
| 736 |
+
# Run evaluation (or load cached predictions)
|
| 737 |
+
# ------------------------------------------------------------------
|
| 738 |
+
if args.load_predictions:
|
| 739 |
+
print(f"\nLoading predictions from {args.load_predictions} ...")
|
| 740 |
+
with open(args.load_predictions, "r") as f:
|
| 741 |
+
cached = json.load(f)
|
| 742 |
+
eval_result = cached.get("eval_result", {})
|
| 743 |
+
predictions = {}
|
| 744 |
+
for tag_str, data in cached.get("predictions", {}).items():
|
| 745 |
+
predictions[int(tag_str)] = data
|
| 746 |
+
else:
|
| 747 |
+
print(f"\n{'='*60}")
|
| 748 |
+
print(f"Running DriveLM evaluation on {model_name}")
|
| 749 |
+
print(f"{'='*60}\n")
|
| 750 |
+
|
| 751 |
+
eval_result = evaluate_model(
|
| 752 |
+
model, processor, samples, device,
|
| 753 |
+
max_new_tokens=args.max_new_tokens,
|
| 754 |
+
)
|
| 755 |
+
predictions = eval_result["predictions"]
|
| 756 |
+
|
| 757 |
+
# ------------------------------------------------------------------
|
| 758 |
+
# Compute metrics per tag
|
| 759 |
+
# ------------------------------------------------------------------
|
| 760 |
+
print(f"\n{'='*60}")
|
| 761 |
+
print(f"Evaluation Results — {model_name}")
|
| 762 |
+
print(f"{'='*60}")
|
| 763 |
+
|
| 764 |
+
tag_metrics = {}
|
| 765 |
+
|
| 766 |
+
# --- Tag 0: Accuracy ---
|
| 767 |
+
if 0 in predictions and predictions[0]["predictions"]:
|
| 768 |
+
m = compute_accuracy(predictions[0]["predictions"], predictions[0]["references"])
|
| 769 |
+
tag_metrics[0] = m
|
| 770 |
+
print(f"\n [Tag 0] Accuracy (multi-choice / yes-no):")
|
| 771 |
+
print(f" Accuracy: {m['accuracy']*100:.2f}%")
|
| 772 |
+
print(f" Correct: {m['correct']}/{m['total']}")
|
| 773 |
+
else:
|
| 774 |
+
tag_metrics[0] = {"accuracy": 0.0, "correct": 0, "total": 0}
|
| 775 |
+
|
| 776 |
+
# --- Tag 2: Language Score ---
|
| 777 |
+
if 2 in predictions and predictions[2]["predictions"]:
|
| 778 |
+
m = compute_language_metrics(predictions[2]["predictions"], predictions[2]["references"])
|
| 779 |
+
tag_metrics[2] = m
|
| 780 |
+
print(f"\n [Tag 2] Language Score (descriptive answers):")
|
| 781 |
+
print(f" BLEU-1: {m['Bleu_1']:.4f}")
|
| 782 |
+
print(f" BLEU-2: {m['Bleu_2']:.4f}")
|
| 783 |
+
print(f" BLEU-3: {m['Bleu_3']:.4f}")
|
| 784 |
+
print(f" BLEU-4: {m['Bleu_4']:.4f}")
|
| 785 |
+
print(f" ROUGE-L: {m['ROUGE_L']:.4f}")
|
| 786 |
+
print(f" CIDEr: {m['CIDEr']:.4f}")
|
| 787 |
+
print(f" Norm Lang: {m['language_score_normalized']:.4f}")
|
| 788 |
+
else:
|
| 789 |
+
tag_metrics[2] = {
|
| 790 |
+
"Bleu_1": 0.0, "Bleu_2": 0.0, "Bleu_3": 0.0, "Bleu_4": 0.0,
|
| 791 |
+
"ROUGE_L": 0.0, "CIDEr": 0.0, "language_score_normalized": 0.0,
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
# --- Tag 1: GPT Score ---
|
| 795 |
+
if 1 in predictions and predictions[1]["predictions"]:
|
| 796 |
+
if args.gpt_eval:
|
| 797 |
+
m = compute_gpt_score(
|
| 798 |
+
predictions[1]["questions"],
|
| 799 |
+
predictions[1]["predictions"],
|
| 800 |
+
predictions[1]["references"],
|
| 801 |
+
args.openai_api_key,
|
| 802 |
+
)
|
| 803 |
+
tag_metrics[1] = m
|
| 804 |
+
print(f"\n [Tag 1] GPT-Score (open-ended planning):")
|
| 805 |
+
print(f" GPT-Score: {m['gpt_score']:.2f}/100")
|
| 806 |
+
print(f" Scored: {m['scored']}/{m['total']}")
|
| 807 |
+
else:
|
| 808 |
+
tag_metrics[1] = {"gpt_score": 0.0, "total": len(predictions[1]["predictions"]), "scored": 0}
|
| 809 |
+
print(f"\n [Tag 1] GPT-Score: SKIPPED (use --gpt_eval to enable)")
|
| 810 |
+
else:
|
| 811 |
+
tag_metrics[1] = {"gpt_score": 0.0, "total": 0, "scored": 0}
|
| 812 |
+
|
| 813 |
+
# --- Tag 3: Match Score ---
|
| 814 |
+
if 3 in predictions and predictions[3]["predictions"]:
|
| 815 |
+
if args.gpt_eval:
|
| 816 |
+
m = compute_match_score(
|
| 817 |
+
predictions[3]["questions"],
|
| 818 |
+
predictions[3]["predictions"],
|
| 819 |
+
predictions[3]["references"],
|
| 820 |
+
args.openai_api_key,
|
| 821 |
+
)
|
| 822 |
+
tag_metrics[3] = m
|
| 823 |
+
print(f"\n [Tag 3] Match Score (coordinate + GPT):")
|
| 824 |
+
print(f" Match Score: {m['match_score']:.2f}")
|
| 825 |
+
print(f" Coord F1: {m['coord_f1']:.4f}")
|
| 826 |
+
print(f" Coord Prec: {m['coord_precision']:.4f}")
|
| 827 |
+
print(f" Coord Rec: {m['coord_recall']:.4f}")
|
| 828 |
+
print(f" GPT Component: {m['gpt_component']:.2f}")
|
| 829 |
+
else:
|
| 830 |
+
# Compute coordinate F1 without GPT component
|
| 831 |
+
m_no_gpt = compute_match_score(
|
| 832 |
+
predictions[3]["questions"],
|
| 833 |
+
predictions[3]["predictions"],
|
| 834 |
+
predictions[3]["references"],
|
| 835 |
+
api_key=None, # skip GPT
|
| 836 |
+
)
|
| 837 |
+
tag_metrics[3] = m_no_gpt
|
| 838 |
+
print(f"\n [Tag 3] Match Score (coordinate F1 only, GPT skipped):")
|
| 839 |
+
print(f" Coord F1: {m_no_gpt['coord_f1']:.4f}")
|
| 840 |
+
print(f" Coord Prec: {m_no_gpt['coord_precision']:.4f}")
|
| 841 |
+
print(f" Coord Rec: {m_no_gpt['coord_recall']:.4f}")
|
| 842 |
+
else:
|
| 843 |
+
tag_metrics[3] = {"match_score": 0.0, "coord_f1": 0.0}
|
| 844 |
+
|
| 845 |
+
# ------------------------------------------------------------------
|
| 846 |
+
# Final weighted score
|
| 847 |
+
# ------------------------------------------------------------------
|
| 848 |
+
accuracy = tag_metrics[0].get("accuracy", 0.0)
|
| 849 |
+
gpt_score = tag_metrics[1].get("gpt_score", 0.0)
|
| 850 |
+
language_score = tag_metrics[2].get("language_score_normalized", 0.0)
|
| 851 |
+
match_score = tag_metrics[3].get("match_score", 0.0)
|
| 852 |
+
|
| 853 |
+
final = compute_final_score(accuracy, gpt_score, language_score, match_score)
|
| 854 |
+
|
| 855 |
+
print(f"\n{'='*60}")
|
| 856 |
+
print(f" Final Score (DriveLM weighting)")
|
| 857 |
+
print(f" Accuracy: {accuracy*100:.2f}% (weight=0.2)")
|
| 858 |
+
print(f" GPT-Score: {gpt_score:.2f}/100 (weight=0.4)")
|
| 859 |
+
print(f" Language: {language_score:.4f} (weight=0.2)")
|
| 860 |
+
print(f" Match: {match_score:.2f} (weight=0.2)")
|
| 861 |
+
print(f" ------------------------------------------")
|
| 862 |
+
print(f" FINAL SCORE: {final:.4f}")
|
| 863 |
+
print(f"{'='*60}")
|
| 864 |
+
|
| 865 |
+
# Timing info
|
| 866 |
+
if "elapsed" in eval_result:
|
| 867 |
+
print(f"\n Total Samples: {eval_result.get('total_samples', 'N/A')}")
|
| 868 |
+
print(f" Errors: {eval_result.get('total_errors', 0)}")
|
| 869 |
+
print(f" Total Time: {eval_result.get('elapsed', 0):.1f}s")
|
| 870 |
+
print(f" Avg Process Time: {eval_result.get('avg_process_ms', 0):.0f}ms/sample")
|
| 871 |
+
print(f" Avg Inference Time: {eval_result.get('avg_inference_ms', 0):.0f}ms/sample")
|
| 872 |
+
|
| 873 |
+
# Reference baseline
|
| 874 |
+
print(f"\n{'='*60}")
|
| 875 |
+
print("Reference (DriveLM zero-shot baseline from challenge):")
|
| 876 |
+
print(" Accuracy: 0.00% | GPT-Score: 67.75 | Match: 18.83")
|
| 877 |
+
print(" Language: BLEU-1=0.238, BLEU-4=0.011, ROUGE-L=0.199, CIDEr=0.007")
|
| 878 |
+
print(" Final Score: 0.328")
|
| 879 |
+
print(f"{'='*60}")
|
| 880 |
+
|
| 881 |
+
# ------------------------------------------------------------------
|
| 882 |
+
# Save predictions
|
| 883 |
+
# ------------------------------------------------------------------
|
| 884 |
+
if args.save_predictions:
|
| 885 |
+
# Convert predictions to serializable format
|
| 886 |
+
serializable_predictions = {}
|
| 887 |
+
for tag, data in predictions.items():
|
| 888 |
+
serializable_predictions[str(tag)] = data
|
| 889 |
+
|
| 890 |
+
output = {
|
| 891 |
+
"model": model_name,
|
| 892 |
+
"dataset": "OpenDriveLab/DriveLM",
|
| 893 |
+
"split": args.split,
|
| 894 |
+
"subset": args.subset,
|
| 895 |
+
"predictions": serializable_predictions,
|
| 896 |
+
"metrics": {
|
| 897 |
+
"accuracy": accuracy,
|
| 898 |
+
"gpt_score": gpt_score,
|
| 899 |
+
"language_score": language_score,
|
| 900 |
+
"match_score": match_score,
|
| 901 |
+
"final_score": final,
|
| 902 |
+
},
|
| 903 |
+
"eval_result": {
|
| 904 |
+
k: v for k, v in eval_result.items()
|
| 905 |
+
if k != "predictions" and not isinstance(v, torch.Tensor)
|
| 906 |
+
},
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
with open(args.save_predictions, "w") as f:
|
| 910 |
+
json.dump(output, f, indent=2, ensure_ascii=False)
|
| 911 |
+
print(f"\n Predictions saved to {args.save_predictions}")
|
| 912 |
+
|
| 913 |
+
|
| 914 |
+
if __name__ == "__main__":
|
| 915 |
+
main()
|
unidrive_vla_nusc_base_quantize_all.py
ADDED
|
@@ -0,0 +1,873 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Quantize owl10/UniDriveVLA_Nusc_Base_Stage1 using Nvidia ModelOpt.
|
| 3 |
+
|
| 4 |
+
Quantization modes:
|
| 5 |
+
- INT8: percentile calibration (activation + weight, PyTorch native)
|
| 6 |
+
- NVFP4: AWQ weight-only quantization (4-bit floating point, PyTorch native)
|
| 7 |
+
- ONNX_NVFP4: PyTorch NVFP4 quantization → ONNX export → NVFP4QuantExporter
|
| 8 |
+
post-processing (FLOAT4E2M1 weights + 2×DQ dequant chains)
|
| 9 |
+
|
| 10 |
+
Calibration data: First 128 samples from DriveLM training set (multi-view images + QA)
|
| 11 |
+
Evaluation: DriveLM benchmark metrics (Accuracy, GPT-Score, Language, Match, Final Score)
|
| 12 |
+
|
| 13 |
+
Usage:
|
| 14 |
+
python unidrive_vla_nusc_base_quantize_all.py # run all modes
|
| 15 |
+
python unidrive_vla_nusc_base_quantize_all.py --mode int8 # INT8 only
|
| 16 |
+
python unidrive_vla_nusc_base_quantize_all.py --mode nvfp4 # NVFP4 only
|
| 17 |
+
python unidrive_vla_nusc_base_quantize_all.py --mode onnx_nvfp4 # ONNX NVFP4 only
|
| 18 |
+
python unidrive_vla_nusc_base_quantize_all.py --skip_baseline_eval # skip bf16 eval
|
| 19 |
+
|
| 20 |
+
Note on onnx_nvfp4:
|
| 21 |
+
modelopt.onnx.quantization.quantize() does NOT support quantize_mode="nvfp4"
|
| 22 |
+
(only int8/fp8/int4 are wired in the dispatch). The ONNX NVFP4 pipeline is:
|
| 23 |
+
1. PyTorch-level: mtq.quantize(model, NVFP4_AWQ_LITE_CFG, forward_loop)
|
| 24 |
+
2. ONNX export: torch.onnx.export() with QuantizedLinear ops
|
| 25 |
+
3. Post-process: NVFP4QuantExporter replaces TRT_FP4QDQ custom ops
|
| 26 |
+
with chained DequantizeLinear (FP4→FP8→FP32)
|
| 27 |
+
This is the canonical ModelOpt 0.44.0 ONNX NVFP4 path.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
import argparse
|
| 31 |
+
import gc
|
| 32 |
+
import json
|
| 33 |
+
import os
|
| 34 |
+
import re
|
| 35 |
+
import time
|
| 36 |
+
from collections import defaultdict
|
| 37 |
+
|
| 38 |
+
# ---------------------------------------------------------------------------
|
| 39 |
+
# Patch: disable MLX on non-Apple platforms (same as evaluation script)
|
| 40 |
+
# ---------------------------------------------------------------------------
|
| 41 |
+
import transformers.utils.generic as _tug
|
| 42 |
+
_tug._is_mlx_available = False
|
| 43 |
+
|
| 44 |
+
import numpy as np
|
| 45 |
+
import torch
|
| 46 |
+
from huggingface_hub import hf_hub_download
|
| 47 |
+
from tqdm import tqdm
|
| 48 |
+
|
| 49 |
+
# Import evaluation utilities from the evaluation script
|
| 50 |
+
from unidrive_vla_nusc_base_evaluation import (
|
| 51 |
+
CAMERA_VIEW_TOKENS,
|
| 52 |
+
CAMERAS,
|
| 53 |
+
QA_TAG_MAP,
|
| 54 |
+
SYSTEM_PROMPT,
|
| 55 |
+
classify_question_tag,
|
| 56 |
+
compute_accuracy,
|
| 57 |
+
compute_final_score,
|
| 58 |
+
compute_gpt_score,
|
| 59 |
+
compute_language_metrics,
|
| 60 |
+
compute_match_score,
|
| 61 |
+
evaluate_model,
|
| 62 |
+
load_drivelm_dataset,
|
| 63 |
+
run_inference,
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# ---------------------------------------------------------------------------
|
| 68 |
+
# Configuration
|
| 69 |
+
# ---------------------------------------------------------------------------
|
| 70 |
+
MODEL_NAME = "owl10/UniDriveVLA_Nusc_Base_Stage1"
|
| 71 |
+
NUM_CALIBRATION_SAMPLES = 128
|
| 72 |
+
ALL_MODES = ["int8", "nvfp4", "onnx_nvfp4"]
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# ---------------------------------------------------------------------------
|
| 76 |
+
# Calibration data preparation
|
| 77 |
+
# ---------------------------------------------------------------------------
|
| 78 |
+
|
| 79 |
+
def prepare_calibration_data(
|
| 80 |
+
samples: list[dict],
|
| 81 |
+
processor,
|
| 82 |
+
device: torch.device,
|
| 83 |
+
num_samples: int = NUM_CALIBRATION_SAMPLES,
|
| 84 |
+
) -> list:
|
| 85 |
+
"""Prepare calibration inputs from DriveLM samples.
|
| 86 |
+
|
| 87 |
+
Each calibration sample is a dict of model inputs ready for forward().
|
| 88 |
+
We use multi-view images + question text from the first N samples.
|
| 89 |
+
"""
|
| 90 |
+
from qwen_vl_utils import process_vision_info
|
| 91 |
+
|
| 92 |
+
calibration_inputs = []
|
| 93 |
+
for i, sample in enumerate(samples[:num_samples]):
|
| 94 |
+
# Build multi-view image content
|
| 95 |
+
image_content = []
|
| 96 |
+
for cam in CAMERAS:
|
| 97 |
+
cam_path = sample["image_paths"].get(cam)
|
| 98 |
+
if cam_path and os.path.exists(cam_path):
|
| 99 |
+
view_token = CAMERA_VIEW_TOKENS[cam]
|
| 100 |
+
image_content.append({"type": "text", "text": f"{view_token}"})
|
| 101 |
+
image_content.append({"type": "image", "image": f"file://{cam_path}"})
|
| 102 |
+
|
| 103 |
+
# Build messages
|
| 104 |
+
user_content = image_content + [
|
| 105 |
+
{"type": "text", "text": sample["question"]},
|
| 106 |
+
]
|
| 107 |
+
messages = [
|
| 108 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 109 |
+
{"role": "user", "content": user_content},
|
| 110 |
+
]
|
| 111 |
+
|
| 112 |
+
# Process with Qwen3-VL processor
|
| 113 |
+
try:
|
| 114 |
+
text = processor.apply_chat_template(
|
| 115 |
+
messages, tokenize=False, add_generation_prompt=True
|
| 116 |
+
)
|
| 117 |
+
image_inputs, video_inputs = process_vision_info(messages)
|
| 118 |
+
inputs = processor(
|
| 119 |
+
text=[text],
|
| 120 |
+
images=image_inputs,
|
| 121 |
+
videos=video_inputs,
|
| 122 |
+
padding=True,
|
| 123 |
+
return_tensors="pt",
|
| 124 |
+
).to(device)
|
| 125 |
+
|
| 126 |
+
calibration_inputs.append(inputs)
|
| 127 |
+
except Exception as e:
|
| 128 |
+
print(f" [WARN] Failed to prepare sample {i}: {e}")
|
| 129 |
+
continue
|
| 130 |
+
|
| 131 |
+
print(f" Prepared {len(calibration_inputs)}/{num_samples} calibration samples")
|
| 132 |
+
return calibration_inputs
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def make_forward_loop(calibration_inputs: list):
|
| 136 |
+
"""Create a forward_loop function for ModelOpt calibration.
|
| 137 |
+
|
| 138 |
+
ModelOpt calls forward_loop(model) and expects the function to run
|
| 139 |
+
forward passes on calibration data to collect activation statistics.
|
| 140 |
+
"""
|
| 141 |
+
def forward_loop(model):
|
| 142 |
+
with torch.no_grad():
|
| 143 |
+
for inputs in tqdm(
|
| 144 |
+
calibration_inputs,
|
| 145 |
+
desc="Calibration forward",
|
| 146 |
+
disable=None,
|
| 147 |
+
):
|
| 148 |
+
try:
|
| 149 |
+
model(**inputs)
|
| 150 |
+
except Exception as e:
|
| 151 |
+
print(f" [WARN] Forward pass failed: {e}")
|
| 152 |
+
return forward_loop
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
# ---------------------------------------------------------------------------
|
| 156 |
+
# INT8 Quantization
|
| 157 |
+
# ---------------------------------------------------------------------------
|
| 158 |
+
|
| 159 |
+
def quantize_int8(model, calibration_inputs, output_dir="int8"):
|
| 160 |
+
"""Quantize model to INT8 using percentile calibration.
|
| 161 |
+
|
| 162 |
+
Uses ModelOpt's PyTorch-native quantization API.
|
| 163 |
+
"""
|
| 164 |
+
import modelopt.torch.quantization as mtq
|
| 165 |
+
|
| 166 |
+
print("\n" + "=" * 70)
|
| 167 |
+
print("INT8 Quantization (percentile calibration)")
|
| 168 |
+
print("=" * 70)
|
| 169 |
+
|
| 170 |
+
# INT8 config: activation + weight quantization
|
| 171 |
+
# Percentile calibration is more robust for VLMs than max calibration
|
| 172 |
+
INT8_CFG = {
|
| 173 |
+
"quant_cfg": {
|
| 174 |
+
"*weight": {"num_bits": 8, "axis": 0},
|
| 175 |
+
"*input_quantizer": {"num_bits": 8, "axis": None, "calib_method": "percentile"},
|
| 176 |
+
},
|
| 177 |
+
"algorithm": "percentile",
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
forward_loop = make_forward_loop(calibration_inputs)
|
| 181 |
+
|
| 182 |
+
t0 = time.time()
|
| 183 |
+
quantized_model = mtq.quantize(
|
| 184 |
+
model,
|
| 185 |
+
quant_cfg=INT8_CFG,
|
| 186 |
+
forward_loop=forward_loop,
|
| 187 |
+
)
|
| 188 |
+
elapsed = time.time() - t0
|
| 189 |
+
|
| 190 |
+
# Save quantized model
|
| 191 |
+
if not os.path.exists(output_dir):
|
| 192 |
+
os.makedirs(output_dir)
|
| 193 |
+
output_path = os.path.join(output_dir, "unidrive_vla_int8")
|
| 194 |
+
quantized_model.save_pretrained(output_path)
|
| 195 |
+
print(f" Saved: {output_path} in {elapsed:.1f}s")
|
| 196 |
+
|
| 197 |
+
return quantized_model, output_path
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
# ---------------------------------------------------------------------------
|
| 201 |
+
# NVFP4 Quantization
|
| 202 |
+
# ---------------------------------------------------------------------------
|
| 203 |
+
|
| 204 |
+
def quantize_nvfp4(model, calibration_inputs, output_dir="nvfp4"):
|
| 205 |
+
"""Quantize model to NVFP4 using AWQ weight-only quantization.
|
| 206 |
+
|
| 207 |
+
NVFP4 is NVIDIA's 4-bit floating point format optimized for inference
|
| 208 |
+
on Blackwell and newer architectures.
|
| 209 |
+
"""
|
| 210 |
+
import modelopt.torch.quantization as mtq
|
| 211 |
+
|
| 212 |
+
print("\n" + "=" * 70)
|
| 213 |
+
print("NVFP4 Quantization (AWQ weight-only)")
|
| 214 |
+
print("=" * 70)
|
| 215 |
+
|
| 216 |
+
# NVFP4 config: weight-only quantization with AWQ
|
| 217 |
+
# AWQ (Activation-aware Weight Quantization) preserves important weights
|
| 218 |
+
NVFP4_CFG = {
|
| 219 |
+
"quant_cfg": {
|
| 220 |
+
"*weight": {"num_bits": "nf4", "axis": 0, "block_size": 128},
|
| 221 |
+
"*input_quantizer": {"enable": False}, # weight-only
|
| 222 |
+
},
|
| 223 |
+
"algorithm": "awq_lite",
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
forward_loop = make_forward_loop(calibration_inputs)
|
| 227 |
+
|
| 228 |
+
t0 = time.time()
|
| 229 |
+
quantized_model = mtq.quantize(
|
| 230 |
+
model,
|
| 231 |
+
quant_cfg=NVFP4_CFG,
|
| 232 |
+
forward_loop=forward_loop,
|
| 233 |
+
)
|
| 234 |
+
elapsed = time.time() - t0
|
| 235 |
+
|
| 236 |
+
# Save quantized model
|
| 237 |
+
if not os.path.exists(output_dir):
|
| 238 |
+
os.makedirs(output_dir)
|
| 239 |
+
output_path = os.path.join(output_dir, "unidrive_vla_nvfp4")
|
| 240 |
+
quantized_model.save_pretrained(output_path)
|
| 241 |
+
print(f" Saved: {output_path} in {elapsed:.1f}s")
|
| 242 |
+
|
| 243 |
+
return quantized_model, output_path
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
# ---------------------------------------------------------------------------
|
| 247 |
+
# ONNX NVFP4 Quantization
|
| 248 |
+
# ---------------------------------------------------------------------------
|
| 249 |
+
|
| 250 |
+
def _export_quantized_model_to_onnx(
|
| 251 |
+
model,
|
| 252 |
+
calibration_inputs: list,
|
| 253 |
+
output_path: str,
|
| 254 |
+
opset: int = 23,
|
| 255 |
+
):
|
| 256 |
+
"""Export a quantized PyTorch model to ONNX.
|
| 257 |
+
|
| 258 |
+
Uses the first calibration sample as the dummy input for torch.onnx.export().
|
| 259 |
+
Opset 23+ is required for FLOAT4E2M1 support.
|
| 260 |
+
"""
|
| 261 |
+
import torch.onnx
|
| 262 |
+
|
| 263 |
+
# Use the first calibration sample as dummy input
|
| 264 |
+
sample_input = calibration_inputs[0]
|
| 265 |
+
|
| 266 |
+
# Build a wrapper that accepts the sample input kwargs and returns forward output
|
| 267 |
+
class _ONNXWrapper(torch.nn.Module):
|
| 268 |
+
def __init__(self, model, sample_input):
|
| 269 |
+
super().__init__()
|
| 270 |
+
self.model = model
|
| 271 |
+
self._input_names = list(sample_input.keys())
|
| 272 |
+
|
| 273 |
+
def forward(self, input_ids, attention_mask, pixel_values=None,
|
| 274 |
+
image_grid_thw=None, **kwargs):
|
| 275 |
+
inputs = {"input_ids": input_ids, "attention_mask": attention_mask}
|
| 276 |
+
if pixel_values is not None:
|
| 277 |
+
inputs["pixel_values"] = pixel_values
|
| 278 |
+
if image_grid_thw is not None:
|
| 279 |
+
inputs["image_grid_thw"] = image_grid_thw
|
| 280 |
+
out = self.model(**inputs, use_cache=False)
|
| 281 |
+
return out.logits
|
| 282 |
+
|
| 283 |
+
wrapper = _ONNXWrapper(model, sample_input)
|
| 284 |
+
|
| 285 |
+
# Prepare dynamic axes — batch dim and sequence dim are dynamic
|
| 286 |
+
dynamic_axes = {
|
| 287 |
+
"input_ids": {0: "batch", 1: "seq"},
|
| 288 |
+
"attention_mask": {0: "batch", 1: "seq"},
|
| 289 |
+
"logits": {0: "batch", 1: "seq"},
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
# Build args tuple from sample input
|
| 293 |
+
args = (
|
| 294 |
+
sample_input["input_ids"],
|
| 295 |
+
sample_input["attention_mask"],
|
| 296 |
+
)
|
| 297 |
+
kwargs_onnx = {}
|
| 298 |
+
if "pixel_values" in sample_input:
|
| 299 |
+
args = args + (sample_input["pixel_values"],)
|
| 300 |
+
dynamic_axes["pixel_values"] = {0: "batch", 1: "patches"}
|
| 301 |
+
else:
|
| 302 |
+
args = args + (None,)
|
| 303 |
+
if "image_grid_thw" in sample_input:
|
| 304 |
+
args = args + (sample_input["image_grid_thw"],)
|
| 305 |
+
dynamic_axes["image_grid_thw"] = {0: "batch"}
|
| 306 |
+
else:
|
| 307 |
+
args = args + (None,)
|
| 308 |
+
|
| 309 |
+
print(f" Exporting to ONNX (opset={opset}) ...")
|
| 310 |
+
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
|
| 311 |
+
|
| 312 |
+
torch.onnx.export(
|
| 313 |
+
wrapper,
|
| 314 |
+
args,
|
| 315 |
+
output_path,
|
| 316 |
+
opset_version=opset,
|
| 317 |
+
input_names=[k for k in ["input_ids", "attention_mask",
|
| 318 |
+
"pixel_values", "image_grid_thw"]],
|
| 319 |
+
output_names=["logits"],
|
| 320 |
+
dynamic_axes=dynamic_axes,
|
| 321 |
+
use_external_data_format=True,
|
| 322 |
+
do_constant_folding=False,
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
size_mb = os.path.getsize(output_path) / 1e6
|
| 326 |
+
data_path = output_path + ".data"
|
| 327 |
+
if os.path.exists(data_path):
|
| 328 |
+
size_mb += os.path.getsize(data_path) / 1e6
|
| 329 |
+
print(f" Exported: {output_path} ({size_mb:.1f} MB)")
|
| 330 |
+
return output_path
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def quantize_onnx_nvfp4(model, calibration_inputs, output_dir="onnx_nvfp4"):
|
| 334 |
+
"""Quantize model to NVFP4 and export to ONNX with FLOAT4E2M1 weights.
|
| 335 |
+
|
| 336 |
+
This is the canonical ModelOpt 0.44.0 ONNX NVFP4 pipeline:
|
| 337 |
+
|
| 338 |
+
Step 1: PyTorch NVFP4 quantization via mtq.quantize() with NVFP4_AWQ_LITE_CFG.
|
| 339 |
+
Inserts TRT_FP4QDQ custom ops into the model graph.
|
| 340 |
+
|
| 341 |
+
Step 2: ONNX export via torch.onnx.export(). The TRT custom ops are
|
| 342 |
+
serialized as-is into the ONNX graph.
|
| 343 |
+
|
| 344 |
+
Step 3: Post-processing via NVFP4QuantExporter. Replaces each TRT_FP4QDQ
|
| 345 |
+
node with a chain of two DequantizeLinear nodes:
|
| 346 |
+
FLOAT4E2M1 weights → (DQ, FP8 block scales) → FP8 →
|
| 347 |
+
(DQ, FP32 per-tensor scale) → FP32 output
|
| 348 |
+
This is the standard 2×DQ dequantization pattern for NVFP4.
|
| 349 |
+
|
| 350 |
+
Note: modelopt.onnx.quantization.quantize() does NOT support
|
| 351 |
+
quantize_mode="nvfp4" — only int8/fp8/int4 are wired in the dispatch.
|
| 352 |
+
The NVFP4QuantExporter path is the only supported way to produce
|
| 353 |
+
FLOAT4E2M1 ONNX models in ModelOpt 0.44.0.
|
| 354 |
+
"""
|
| 355 |
+
import modelopt.torch.quantization as mtq
|
| 356 |
+
|
| 357 |
+
print("\n" + "=" * 70)
|
| 358 |
+
print("ONNX NVFP4 Quantization (PyTorch NVFP4 → ONNX → NVFP4QuantExporter)")
|
| 359 |
+
print("=" * 70)
|
| 360 |
+
|
| 361 |
+
if not os.path.exists(output_dir):
|
| 362 |
+
os.makedirs(output_dir)
|
| 363 |
+
|
| 364 |
+
# ------------------------------------------------------------------
|
| 365 |
+
# Step 1: PyTorch NVFP4 quantization
|
| 366 |
+
# ------------------------------------------------------------------
|
| 367 |
+
print("\n Step 1/3: PyTorch NVFP4 quantization ...")
|
| 368 |
+
try:
|
| 369 |
+
NVFP4_CFG = mtq.NVFP4_AWQ_LITE_CFG
|
| 370 |
+
except AttributeError:
|
| 371 |
+
# Fallback if built-in config not available
|
| 372 |
+
NVFP4_CFG = {
|
| 373 |
+
"quant_cfg": {
|
| 374 |
+
"*weight": {"num_bits": (2, 1), "block_sizes": {-1: 16},
|
| 375 |
+
"type": "dynamic", "scale_bits": (4, 3)},
|
| 376 |
+
"*input_quantizer": {"enable": False},
|
| 377 |
+
},
|
| 378 |
+
"algorithm": "awq_lite",
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
forward_loop = make_forward_loop(calibration_inputs)
|
| 382 |
+
|
| 383 |
+
t0 = time.time()
|
| 384 |
+
quantized_model = mtq.quantize(
|
| 385 |
+
model,
|
| 386 |
+
quant_cfg=NVFP4_CFG,
|
| 387 |
+
forward_loop=forward_loop,
|
| 388 |
+
)
|
| 389 |
+
elapsed = time.time() - t0
|
| 390 |
+
print(f" PyTorch NVFP4 quantization done in {elapsed:.1f}s")
|
| 391 |
+
|
| 392 |
+
# Save the quantized PyTorch model as intermediate
|
| 393 |
+
torch_save_path = os.path.join(output_dir, "unidrive_vla_nvfp4_torch")
|
| 394 |
+
quantized_model.save_pretrained(torch_save_path)
|
| 395 |
+
print(f" Saved PyTorch checkpoint: {torch_save_path}")
|
| 396 |
+
|
| 397 |
+
# ------------------------------------------------------------------
|
| 398 |
+
# Step 2: Export to ONNX
|
| 399 |
+
# ------------------------------------------------------------------
|
| 400 |
+
print("\n Step 2/3: ONNX export ...")
|
| 401 |
+
onnx_raw_path = os.path.join(output_dir, "unidrive_vla_nvfp4_raw.onnx")
|
| 402 |
+
|
| 403 |
+
try:
|
| 404 |
+
_export_quantized_model_to_onnx(
|
| 405 |
+
quantized_model, calibration_inputs,
|
| 406 |
+
output_path=onnx_raw_path,
|
| 407 |
+
opset=23, # opset 23+ required for FLOAT4E2M1
|
| 408 |
+
)
|
| 409 |
+
except Exception as e:
|
| 410 |
+
print(f" [WARN] Standard ONNX export failed: {e}")
|
| 411 |
+
print(f" Trying with opset=21 and do_constant_folding=True ...")
|
| 412 |
+
try:
|
| 413 |
+
_export_quantized_model_to_onnx(
|
| 414 |
+
quantized_model, calibration_inputs,
|
| 415 |
+
output_path=onnx_raw_path,
|
| 416 |
+
opset=21,
|
| 417 |
+
)
|
| 418 |
+
except Exception as e2:
|
| 419 |
+
print(f" [ERROR] ONNX export failed entirely: {e2}")
|
| 420 |
+
print(f" The quantized PyTorch model is still saved at: {torch_save_path}")
|
| 421 |
+
print(f" You can use modelopt.torch.export.export_hf_checkpoint() "
|
| 422 |
+
f"for TRT-LLM deployment instead.")
|
| 423 |
+
raise
|
| 424 |
+
|
| 425 |
+
# ------------------------------------------------------------------
|
| 426 |
+
# Step 3: NVFP4QuantExporter post-processing
|
| 427 |
+
# ------------------------------------------------------------------
|
| 428 |
+
print("\n Step 3/3: NVFP4QuantExporter post-processing ...")
|
| 429 |
+
onnx_final_path = os.path.join(output_dir, "unidrive_vla_nvfp4.onnx")
|
| 430 |
+
|
| 431 |
+
try:
|
| 432 |
+
from modelopt.onnx.export import NVFP4QuantExporter
|
| 433 |
+
|
| 434 |
+
exporter = NVFP4QuantExporter()
|
| 435 |
+
exporter.process(onnx_raw_path, onnx_final_path)
|
| 436 |
+
|
| 437 |
+
size_mb = os.path.getsize(onnx_final_path) / 1e6
|
| 438 |
+
data_path = onnx_final_path + ".data"
|
| 439 |
+
if os.path.exists(data_path):
|
| 440 |
+
size_mb += os.path.getsize(data_path) / 1e6
|
| 441 |
+
print(f" Post-processed ONNX: {onnx_final_path} ({size_mb:.1f} MB)")
|
| 442 |
+
|
| 443 |
+
# Clean up raw ONNX
|
| 444 |
+
if os.path.exists(onnx_raw_path):
|
| 445 |
+
os.remove(onnx_raw_path)
|
| 446 |
+
raw_data = onnx_raw_path + ".data"
|
| 447 |
+
if os.path.exists(raw_data):
|
| 448 |
+
os.remove(raw_data)
|
| 449 |
+
print(f" Cleaned up raw ONNX: {onnx_raw_path}")
|
| 450 |
+
|
| 451 |
+
except ImportError:
|
| 452 |
+
print(f" [WARN] NVFP4QuantExporter not available in this modelopt version.")
|
| 453 |
+
print(f" Using raw ONNX export as final output.")
|
| 454 |
+
if os.path.exists(onnx_raw_path):
|
| 455 |
+
os.rename(onnx_raw_path, onnx_final_path)
|
| 456 |
+
raw_data = onnx_raw_path + ".data"
|
| 457 |
+
if os.path.exists(raw_data):
|
| 458 |
+
os.rename(raw_data, onnx_final_path + ".data")
|
| 459 |
+
print(f" Final ONNX: {onnx_final_path}")
|
| 460 |
+
|
| 461 |
+
except Exception as e:
|
| 462 |
+
print(f" [WARN] NVFP4QuantExporter post-processing failed: {e}")
|
| 463 |
+
print(f" Using raw ONNX export as final output.")
|
| 464 |
+
if os.path.exists(onnx_raw_path):
|
| 465 |
+
import shutil
|
| 466 |
+
shutil.copy2(onnx_raw_path, onnx_final_path)
|
| 467 |
+
raw_data = onnx_raw_path + ".data"
|
| 468 |
+
if os.path.exists(raw_data):
|
| 469 |
+
shutil.copy2(raw_data, onnx_final_path + ".data")
|
| 470 |
+
print(f" Final ONNX (raw): {onnx_final_path}")
|
| 471 |
+
|
| 472 |
+
return quantized_model, onnx_final_path
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
# ---------------------------------------------------------------------------
|
| 476 |
+
# Evaluation wrapper
|
| 477 |
+
# ---------------------------------------------------------------------------
|
| 478 |
+
|
| 479 |
+
def evaluate_on_drivelm(
|
| 480 |
+
model,
|
| 481 |
+
processor,
|
| 482 |
+
samples: list[dict],
|
| 483 |
+
device: torch.device,
|
| 484 |
+
label: str,
|
| 485 |
+
max_new_tokens: int = 256,
|
| 486 |
+
subset: int = 0,
|
| 487 |
+
gpt_eval: bool = False,
|
| 488 |
+
openai_api_key: str | None = None,
|
| 489 |
+
) -> dict:
|
| 490 |
+
"""Evaluate a model on DriveLM and compute all metrics."""
|
| 491 |
+
print(f"\n{'='*70}")
|
| 492 |
+
print(f"Evaluating: {label}")
|
| 493 |
+
print(f"{'='*70}")
|
| 494 |
+
|
| 495 |
+
eval_samples = samples[:subset] if subset > 0 else samples
|
| 496 |
+
print(f" Samples: {len(eval_samples)}")
|
| 497 |
+
|
| 498 |
+
model.eval()
|
| 499 |
+
eval_result = evaluate_model(
|
| 500 |
+
model, processor, eval_samples, device,
|
| 501 |
+
max_new_tokens=max_new_tokens,
|
| 502 |
+
)
|
| 503 |
+
predictions = eval_result["predictions"]
|
| 504 |
+
|
| 505 |
+
# Compute metrics per tag
|
| 506 |
+
tag_metrics = {}
|
| 507 |
+
|
| 508 |
+
# Tag 0: Accuracy
|
| 509 |
+
if 0 in predictions and predictions[0]["predictions"]:
|
| 510 |
+
m = compute_accuracy(predictions[0]["predictions"], predictions[0]["references"])
|
| 511 |
+
tag_metrics[0] = m
|
| 512 |
+
print(f" [Tag 0] Accuracy: {m['accuracy']*100:.2f}% ({m['correct']}/{m['total']})")
|
| 513 |
+
else:
|
| 514 |
+
tag_metrics[0] = {"accuracy": 0.0, "correct": 0, "total": 0}
|
| 515 |
+
|
| 516 |
+
# Tag 2: Language Score
|
| 517 |
+
if 2 in predictions and predictions[2]["predictions"]:
|
| 518 |
+
m = compute_language_metrics(predictions[2]["predictions"], predictions[2]["references"])
|
| 519 |
+
tag_metrics[2] = m
|
| 520 |
+
print(f" [Tag 2] Language: BLEU-1={m['Bleu_1']:.4f}, ROUGE-L={m['ROUGE_L']:.4f}, "
|
| 521 |
+
f"Norm={m['language_score_normalized']:.4f}")
|
| 522 |
+
else:
|
| 523 |
+
tag_metrics[2] = {"language_score_normalized": 0.0}
|
| 524 |
+
|
| 525 |
+
# Tag 1: GPT Score
|
| 526 |
+
if 1 in predictions and predictions[1]["predictions"]:
|
| 527 |
+
if gpt_eval:
|
| 528 |
+
m = compute_gpt_score(
|
| 529 |
+
predictions[1]["questions"],
|
| 530 |
+
predictions[1]["predictions"],
|
| 531 |
+
predictions[1]["references"],
|
| 532 |
+
openai_api_key,
|
| 533 |
+
)
|
| 534 |
+
tag_metrics[1] = m
|
| 535 |
+
print(f" [Tag 1] GPT-Score: {m['gpt_score']:.2f}/100")
|
| 536 |
+
else:
|
| 537 |
+
tag_metrics[1] = {"gpt_score": 0.0}
|
| 538 |
+
print(f" [Tag 1] GPT-Score: SKIPPED")
|
| 539 |
+
else:
|
| 540 |
+
tag_metrics[1] = {"gpt_score": 0.0}
|
| 541 |
+
|
| 542 |
+
# Tag 3: Match Score
|
| 543 |
+
if 3 in predictions and predictions[3]["predictions"]:
|
| 544 |
+
if gpt_eval:
|
| 545 |
+
m = compute_match_score(
|
| 546 |
+
predictions[3]["questions"],
|
| 547 |
+
predictions[3]["predictions"],
|
| 548 |
+
predictions[3]["references"],
|
| 549 |
+
openai_api_key,
|
| 550 |
+
)
|
| 551 |
+
tag_metrics[3] = m
|
| 552 |
+
print(f" [Tag 3] Match: {m['match_score']:.2f} (F1={m['coord_f1']:.4f})")
|
| 553 |
+
else:
|
| 554 |
+
m = compute_match_score(
|
| 555 |
+
predictions[3]["questions"],
|
| 556 |
+
predictions[3]["predictions"],
|
| 557 |
+
predictions[3]["references"],
|
| 558 |
+
api_key=None,
|
| 559 |
+
)
|
| 560 |
+
tag_metrics[3] = m
|
| 561 |
+
print(f" [Tag 3] Match: F1={m['coord_f1']:.4f} (GPT skipped)")
|
| 562 |
+
else:
|
| 563 |
+
tag_metrics[3] = {"match_score": 0.0}
|
| 564 |
+
|
| 565 |
+
# Final score
|
| 566 |
+
accuracy = tag_metrics[0].get("accuracy", 0.0)
|
| 567 |
+
gpt_score = tag_metrics[1].get("gpt_score", 0.0)
|
| 568 |
+
language_score = tag_metrics[2].get("language_score_normalized", 0.0)
|
| 569 |
+
match_score = tag_metrics[3].get("match_score", 0.0)
|
| 570 |
+
|
| 571 |
+
final = compute_final_score(accuracy, gpt_score, language_score, match_score)
|
| 572 |
+
print(f" Final Score: {final:.4f}")
|
| 573 |
+
|
| 574 |
+
return {
|
| 575 |
+
"label": label,
|
| 576 |
+
"accuracy": accuracy,
|
| 577 |
+
"gpt_score": gpt_score,
|
| 578 |
+
"language_score": language_score,
|
| 579 |
+
"match_score": match_score,
|
| 580 |
+
"final_score": final,
|
| 581 |
+
"tag_metrics": tag_metrics,
|
| 582 |
+
"eval_result": eval_result,
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
|
| 586 |
+
# ---------------------------------------------------------------------------
|
| 587 |
+
# Summary printer
|
| 588 |
+
# ---------------------------------------------------------------------------
|
| 589 |
+
|
| 590 |
+
def print_summary(results: list[dict]):
|
| 591 |
+
"""Print a comparison table of all quantization modes."""
|
| 592 |
+
print("\n" + "=" * 70)
|
| 593 |
+
print("Quantization & Evaluation Summary")
|
| 594 |
+
print("=" * 70)
|
| 595 |
+
print(f"{'Mode':<12} {'Accuracy':>10} {'GPT':>10} {'Language':>10} {'Match':>10} {'Final':>10}")
|
| 596 |
+
print("-" * 70)
|
| 597 |
+
for r in results:
|
| 598 |
+
print(f"{r['label']:<12} "
|
| 599 |
+
f"{r['accuracy']*100:>9.2f}% "
|
| 600 |
+
f"{r['gpt_score']:>9.2f} "
|
| 601 |
+
f"{r['language_score']:>10.4f} "
|
| 602 |
+
f"{r['match_score']:>9.2f} "
|
| 603 |
+
f"{r['final_score']:>10.4f}")
|
| 604 |
+
print("=" * 70)
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
# ---------------------------------------------------------------------------
|
| 608 |
+
# Main
|
| 609 |
+
# ---------------------------------------------------------------------------
|
| 610 |
+
|
| 611 |
+
def main():
|
| 612 |
+
parser = argparse.ArgumentParser(
|
| 613 |
+
description="Quantize UniDriveVLA and evaluate on DriveLM"
|
| 614 |
+
)
|
| 615 |
+
parser.add_argument(
|
| 616 |
+
"--mode", type=str, nargs="*", default=ALL_MODES,
|
| 617 |
+
choices=ALL_MODES,
|
| 618 |
+
help=f"Quantization mode(s) to run (default: all). Choices: {ALL_MODES}",
|
| 619 |
+
)
|
| 620 |
+
parser.add_argument(
|
| 621 |
+
"--skip_baseline_eval", action="store_true",
|
| 622 |
+
help="Skip bf16 baseline evaluation",
|
| 623 |
+
)
|
| 624 |
+
parser.add_argument(
|
| 625 |
+
"--subset", type=int, default=200,
|
| 626 |
+
help="Evaluate on first N samples only (0 = all, default: 200)",
|
| 627 |
+
)
|
| 628 |
+
parser.add_argument(
|
| 629 |
+
"--split", type=str, default="train",
|
| 630 |
+
choices=["train", "val"],
|
| 631 |
+
help="DriveLM split for evaluation",
|
| 632 |
+
)
|
| 633 |
+
parser.add_argument(
|
| 634 |
+
"--calibration_samples", type=int, default=NUM_CALIBRATION_SAMPLES,
|
| 635 |
+
help=f"Number of calibration samples (default: {NUM_CALIBRATION_SAMPLES})",
|
| 636 |
+
)
|
| 637 |
+
parser.add_argument(
|
| 638 |
+
"--max_new_tokens", type=int, default=256,
|
| 639 |
+
help="Max tokens to generate per sample",
|
| 640 |
+
)
|
| 641 |
+
parser.add_argument(
|
| 642 |
+
"--gpt_eval", action="store_true",
|
| 643 |
+
help="Enable GPT-Score evaluation (requires OpenAI API key)",
|
| 644 |
+
)
|
| 645 |
+
parser.add_argument(
|
| 646 |
+
"--openai_api_key", type=str, default=None,
|
| 647 |
+
help="OpenAI API key for GPT-Score (or set OPENAI_API_KEY)",
|
| 648 |
+
)
|
| 649 |
+
parser.add_argument(
|
| 650 |
+
"--data_dir", type=str, default=None,
|
| 651 |
+
help="Local directory with DriveLM JSON + nuScenes images",
|
| 652 |
+
)
|
| 653 |
+
parser.add_argument(
|
| 654 |
+
"--nuscenes_dir", type=str, default=None,
|
| 655 |
+
help="Directory containing nuScenes samples/ folder",
|
| 656 |
+
)
|
| 657 |
+
args = parser.parse_args()
|
| 658 |
+
|
| 659 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 660 |
+
print(f"Device: {device}")
|
| 661 |
+
if torch.cuda.is_available():
|
| 662 |
+
print(f" GPU: {torch.cuda.get_device_name(0)}")
|
| 663 |
+
print(f" VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB")
|
| 664 |
+
|
| 665 |
+
# ------------------------------------------------------------------
|
| 666 |
+
# Load model & processor (bf16 baseline)
|
| 667 |
+
# ------------------------------------------------------------------
|
| 668 |
+
print(f"\nLoading {MODEL_NAME} (bf16) ...")
|
| 669 |
+
|
| 670 |
+
from transformers import Qwen3VLForConditionalGeneration, AutoProcessor
|
| 671 |
+
|
| 672 |
+
processor = AutoProcessor.from_pretrained(MODEL_NAME)
|
| 673 |
+
model = Qwen3VLForConditionalGeneration.from_pretrained(
|
| 674 |
+
MODEL_NAME,
|
| 675 |
+
torch_dtype=torch.bfloat16,
|
| 676 |
+
device_map="auto",
|
| 677 |
+
)
|
| 678 |
+
model.eval()
|
| 679 |
+
|
| 680 |
+
param_count = sum(p.numel() for p in model.parameters()) / 1e9
|
| 681 |
+
print(f" Architecture: Qwen3VLForConditionalGeneration")
|
| 682 |
+
print(f" Parameters: {param_count:.2f}B")
|
| 683 |
+
print(f" Dtype: bfloat16")
|
| 684 |
+
|
| 685 |
+
# ------------------------------------------------------------------
|
| 686 |
+
# Load dataset
|
| 687 |
+
# ------------------------------------------------------------------
|
| 688 |
+
print(f"\nLoading DriveLM dataset (split={args.split}) ...")
|
| 689 |
+
samples = load_drivelm_dataset(
|
| 690 |
+
split=args.split,
|
| 691 |
+
data_dir=args.data_dir,
|
| 692 |
+
nuscenes_dir=args.nuscenes_dir,
|
| 693 |
+
)
|
| 694 |
+
|
| 695 |
+
if not samples:
|
| 696 |
+
print("ERROR: No samples loaded. Check dataset access and paths.")
|
| 697 |
+
return
|
| 698 |
+
|
| 699 |
+
print(f" Total QA pairs: {len(samples)}")
|
| 700 |
+
|
| 701 |
+
# ------------------------------------------------------------------
|
| 702 |
+
# Prepare calibration data
|
| 703 |
+
# ------------------------------------------------------------------
|
| 704 |
+
print(f"\nPreparing calibration data ({args.calibration_samples} samples) ...")
|
| 705 |
+
calibration_inputs = prepare_calibration_data(
|
| 706 |
+
samples, processor, device, num_samples=args.calibration_samples
|
| 707 |
+
)
|
| 708 |
+
|
| 709 |
+
if not calibration_inputs:
|
| 710 |
+
print("ERROR: No calibration data prepared.")
|
| 711 |
+
return
|
| 712 |
+
|
| 713 |
+
# ------------------------------------------------------------------
|
| 714 |
+
# Collect results
|
| 715 |
+
# ------------------------------------------------------------------
|
| 716 |
+
all_results = []
|
| 717 |
+
|
| 718 |
+
# --- bf16 Baseline ---
|
| 719 |
+
if not args.skip_baseline_eval:
|
| 720 |
+
print("\n" + "=" * 70)
|
| 721 |
+
print("Phase 1: bf16 Baseline Evaluation")
|
| 722 |
+
print("=" * 70)
|
| 723 |
+
|
| 724 |
+
baseline_result = evaluate_on_drivelm(
|
| 725 |
+
model, processor, samples, device,
|
| 726 |
+
label="bf16",
|
| 727 |
+
max_new_tokens=args.max_new_tokens,
|
| 728 |
+
subset=args.subset,
|
| 729 |
+
gpt_eval=args.gpt_eval,
|
| 730 |
+
openai_api_key=args.openai_api_key,
|
| 731 |
+
)
|
| 732 |
+
all_results.append(baseline_result)
|
| 733 |
+
else:
|
| 734 |
+
print("\n Skipping bf16 baseline evaluation")
|
| 735 |
+
|
| 736 |
+
# --- INT8 ---
|
| 737 |
+
if "int8" in args.mode:
|
| 738 |
+
print("\n" + "=" * 70)
|
| 739 |
+
print("Phase 2: INT8 Quantization")
|
| 740 |
+
print("=" * 70)
|
| 741 |
+
|
| 742 |
+
try:
|
| 743 |
+
quantized_model, output_path = quantize_int8(
|
| 744 |
+
model, calibration_inputs, output_dir="int8"
|
| 745 |
+
)
|
| 746 |
+
|
| 747 |
+
int8_result = evaluate_on_drivelm(
|
| 748 |
+
quantized_model, processor, samples, device,
|
| 749 |
+
label="int8",
|
| 750 |
+
max_new_tokens=args.max_new_tokens,
|
| 751 |
+
subset=args.subset,
|
| 752 |
+
gpt_eval=args.gpt_eval,
|
| 753 |
+
openai_api_key=args.openai_api_key,
|
| 754 |
+
)
|
| 755 |
+
all_results.append(int8_result)
|
| 756 |
+
|
| 757 |
+
# Free memory
|
| 758 |
+
del quantized_model
|
| 759 |
+
gc.collect()
|
| 760 |
+
torch.cuda.empty_cache()
|
| 761 |
+
except Exception as e:
|
| 762 |
+
print(f" INT8 FAILED: {e}")
|
| 763 |
+
import traceback
|
| 764 |
+
traceback.print_exc()
|
| 765 |
+
|
| 766 |
+
# --- NVFP4 ---
|
| 767 |
+
if "nvfp4" in args.mode:
|
| 768 |
+
print("\n" + "=" * 70)
|
| 769 |
+
print("Phase 3: NVFP4 Quantization")
|
| 770 |
+
print("=" * 70)
|
| 771 |
+
|
| 772 |
+
# Reload bf16 model if needed
|
| 773 |
+
if "int8" in args.mode:
|
| 774 |
+
print(" Reloading bf16 model ...")
|
| 775 |
+
model = Qwen3VLForConditionalGeneration.from_pretrained(
|
| 776 |
+
MODEL_NAME,
|
| 777 |
+
torch_dtype=torch.bfloat16,
|
| 778 |
+
device_map="auto",
|
| 779 |
+
)
|
| 780 |
+
model.eval()
|
| 781 |
+
|
| 782 |
+
try:
|
| 783 |
+
quantized_model, output_path = quantize_nvfp4(
|
| 784 |
+
model, calibration_inputs, output_dir="nvfp4"
|
| 785 |
+
)
|
| 786 |
+
|
| 787 |
+
nvfp4_result = evaluate_on_drivelm(
|
| 788 |
+
quantized_model, processor, samples, device,
|
| 789 |
+
label="nvfp4",
|
| 790 |
+
max_new_tokens=args.max_new_tokens,
|
| 791 |
+
subset=args.subset,
|
| 792 |
+
gpt_eval=args.gpt_eval,
|
| 793 |
+
openai_api_key=args.openai_api_key,
|
| 794 |
+
)
|
| 795 |
+
all_results.append(nvfp4_result)
|
| 796 |
+
|
| 797 |
+
# Free memory
|
| 798 |
+
del quantized_model
|
| 799 |
+
gc.collect()
|
| 800 |
+
torch.cuda.empty_cache()
|
| 801 |
+
except Exception as e:
|
| 802 |
+
print(f" NVFP4 FAILED: {e}")
|
| 803 |
+
import traceback
|
| 804 |
+
traceback.print_exc()
|
| 805 |
+
|
| 806 |
+
# --- ONNX NVFP4 ---
|
| 807 |
+
if "onnx_nvfp4" in args.mode:
|
| 808 |
+
print("\n" + "=" * 70)
|
| 809 |
+
print("Phase 4: ONNX NVFP4 Quantization")
|
| 810 |
+
print("=" * 70)
|
| 811 |
+
|
| 812 |
+
# Reload bf16 model if needed (prior quantization modifies model in-place)
|
| 813 |
+
if "int8" in args.mode or "nvfp4" in args.mode:
|
| 814 |
+
print(" Reloading bf16 model ...")
|
| 815 |
+
del model
|
| 816 |
+
gc.collect()
|
| 817 |
+
torch.cuda.empty_cache()
|
| 818 |
+
model = Qwen3VLForConditionalGeneration.from_pretrained(
|
| 819 |
+
MODEL_NAME,
|
| 820 |
+
torch_dtype=torch.bfloat16,
|
| 821 |
+
device_map="auto",
|
| 822 |
+
)
|
| 823 |
+
model.eval()
|
| 824 |
+
|
| 825 |
+
try:
|
| 826 |
+
quantized_model, output_path = quantize_onnx_nvfp4(
|
| 827 |
+
model, calibration_inputs, output_dir="onnx_nvfp4"
|
| 828 |
+
)
|
| 829 |
+
|
| 830 |
+
# Evaluate the quantized PyTorch model (not ONNX — inference uses
|
| 831 |
+
# the torch model; ONNX is for TensorRT deployment)
|
| 832 |
+
onnx_nvfp4_result = evaluate_on_drivelm(
|
| 833 |
+
quantized_model, processor, samples, device,
|
| 834 |
+
label="onnx_nvfp4",
|
| 835 |
+
max_new_tokens=args.max_new_tokens,
|
| 836 |
+
subset=args.subset,
|
| 837 |
+
gpt_eval=args.gpt_eval,
|
| 838 |
+
openai_api_key=args.openai_api_key,
|
| 839 |
+
)
|
| 840 |
+
onnx_nvfp4_result["onnx_path"] = output_path
|
| 841 |
+
all_results.append(onnx_nvfp4_result)
|
| 842 |
+
|
| 843 |
+
# Free memory
|
| 844 |
+
del quantized_model
|
| 845 |
+
gc.collect()
|
| 846 |
+
torch.cuda.empty_cache()
|
| 847 |
+
except Exception as e:
|
| 848 |
+
print(f" ONNX NVFP4 FAILED: {e}")
|
| 849 |
+
import traceback
|
| 850 |
+
traceback.print_exc()
|
| 851 |
+
|
| 852 |
+
# ------------------------------------------------------------------
|
| 853 |
+
# Summary
|
| 854 |
+
# ------------------------------------------------------------------
|
| 855 |
+
if all_results:
|
| 856 |
+
print_summary(all_results)
|
| 857 |
+
|
| 858 |
+
# Save results to JSON
|
| 859 |
+
output_file = "unidrive_vla_quantization_results.json"
|
| 860 |
+
serializable_results = []
|
| 861 |
+
for r in all_results:
|
| 862 |
+
sr = {k: v for k, v in r.items() if k != "eval_result"}
|
| 863 |
+
serializable_results.append(sr)
|
| 864 |
+
|
| 865 |
+
with open(output_file, "w") as f:
|
| 866 |
+
json.dump(serializable_results, f, indent=2)
|
| 867 |
+
print(f"\nResults saved to {output_file}")
|
| 868 |
+
else:
|
| 869 |
+
print("\nNo results to display.")
|
| 870 |
+
|
| 871 |
+
|
| 872 |
+
if __name__ == "__main__":
|
| 873 |
+
main()
|