richard.lin commited on
Commit
ecfbb2c
·
1 Parent(s): 620f331

add: unidrive_vla_nusc_base_evaluation

Browse files
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
- --epochs │ 3 │ QAT 微调周期数 │
38
- ├─────────────────┼────────┼─────────────────────────────────┤
39
- --lr │ 1e-5 学习率 │
40
- ├─────────────────┼────────┼─────────────────────────────────┤
41
- --batch-size 32 │ 训练/评估的批处理大小 │
42
- ├─────────────────┼────────┼─────────────────────────────────┤
43
- --calib-samples 1000 │ PTQ 校准图像数量 │
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
- --epochs │ 3 │ QAT 微调周期数 │
65
- ├─────────────────┼────────┼─────────────────────────────────┤
66
- --lr │ 1e-5 │ 学习率 │
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
- --epochs │ 3 │ QAT 微调周期数 │
83
- ├─────────────────┼────────┼─────────────────────────────────┤
84
- --lr │ 1e-5 学习率 │
85
- ├─────────────────┼────────┼─────────────────────────────────┤
86
- --batch-size 32 │ 训练/评估的批处理大小 │
87
- ├─────────────────┼────────┼─────────────────────────────────┤
88
- --calib-samples │ 1000 PTQ 校准图像数量 │
89
- ├─────────────────┼────────┼─────────────────────────────────┤
90
- --train-samples 0 │ 限制 QAT 训练样本(0=全部) │
91
- ├─────────────────┼────────┼─────────────────────────────────┤
92
- --device cpu 训练设备(如可用则使用 cuda)
93
- ├─────────────────┼────────┼─────────────────────────────────┤
94
- --subset │ 0 │ 评估前 N 张图像(0=所有 5万张) │
95
- --lr │ 1e-5 学习率 │
96
- ├─────────────────┼────────┼─────────────────────────────────┤
97
- --batch-size 32 │ 训练/评估的批处理大小 │
98
- ├─────────────────┼────────┼─────────────────────────────────┤
99
- --calib-samples 1000 │ PTQ 校准图像数量 │
100
- ├─────────────────┼────────┼─────────────────────────────────┤
101
- --train-samples 0 限制 QAT 训练样本(0=全部
102
- ├─────────────────┼────────┼─────────────────────────────────┤
103
- --device cpu │ 训练设备如可用则使用 cuda
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
- --epochs │ 3 │ QAT 微调周期数 │
134
- ├─────────────────┼────────┼─────────────────────────────────┤
135
- --lr │ 1e-5 学习率 │
136
- ├────��────────────┼────────┼─────────────────────────────────┤
137
- --batch-size 32 │ 训练/评估的批处理大小 │
138
- ├─────────────────┼────────┼─────────────────────────────────┤
139
- --calib-samples 1000 │ PTQ 校准图像数量 │
140
- ├─────────────────┼────────┼─────────────────────────────────┤
141
- --train-samples │ 0 限制 QAT 训练样本(0=全部) │
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
- MODULES_PLAY │ 7.8 GB /mnt/vepfs/share/GW00387266/MODULES_PLAY/ │
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
- GPU │ NVIDIA H20 48GB │
185
- ├──────────────────┼────────────────────────┼──────┤
186
- CUDA │ 12.6 │
187
- ├──────────────────┼────────────────────────┼──────┤
188
- PyTorch │ 2.6.0+cu126 │
189
- ├──────────────────┼────────────────────────┼──────┤
190
- ONNX Runtime GPU │ 1.21.0 (CUDA+TensorRT)
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()