| |
| """Fast-dDrive — Waymo E2E batch inference. |
| |
| Runs one of three decoding paths over a Waymo validation JSON and writes |
| ``predictions.json`` + ``metrics.json``. The output ``predictions.json`` is |
| consumed by :file:`evaluate_waymo_metrics.py` for official ADE / RFS scoring. |
| |
| Usage:: |
| |
| python fast_ddrive/eval/batch_inference.py \\ |
| --model_path /path/to/fast_ddrive_ckpt \\ |
| --eval_json /path/to/waymo_val.json \\ |
| --image_root /path/to/image_root \\ |
| --output_dir eval_outputs/run0 \\ |
| --mode scaffold_spec \\ |
| --num_gpus 8 |
| |
| See :file:`../run_eval.sh` for the canonical launcher. |
| """ |
|
|
| import argparse |
| import json |
| import os |
| import re |
| import sys |
| import time |
| import traceback |
| import types |
| from datetime import datetime |
| from pathlib import Path |
| from typing import Any, Dict, List, Optional, Tuple, Union |
|
|
| import numpy as np |
| import torch |
| import torch.multiprocessing as mp |
| from PIL import Image |
| from tqdm import tqdm |
| from transformers import AutoModelForCausalLM, AutoProcessor, AutoTokenizer |
|
|
| |
| |
| |
| |
|
|
|
|
| |
| |
| |
|
|
| def _resolve_single_image_path(image_path: str, image_root: str) -> str: |
| rel = image_path |
| for prefix in ("dataset/", "dataset\\"): |
| if rel.startswith(prefix): |
| rel = rel[len(prefix):] |
| break |
| return os.path.join(image_root, rel) |
|
|
|
|
| def resolve_image_path(image_field, image_root: str): |
| """Resolve image path(s). Supports both str and list[str].""" |
| if isinstance(image_field, list): |
| return [_resolve_single_image_path(p, image_root) for p in image_field] |
| return _resolve_single_image_path(image_field, image_root) |
|
|
|
|
| def _extract_float_pairs(s: str) -> Optional[List[List[float]]]: |
| pairs = re.findall(r'\[\s*([+-]?\d+\.?\d*)\s*,\s*([+-]?\d+\.?\d*)\s*\]', s) |
| if not pairs: |
| return None |
| return [[float(a), float(b)] for a, b in pairs] |
|
|
|
|
| def parse_trajectory(response: str) -> Optional[List[List[float]]]: |
| if not response: |
| return None |
|
|
| try: |
| obj = json.loads(response) |
| traj = obj.get("trajectory") |
| if traj is not None: |
| if isinstance(traj, str): |
| result = _extract_float_pairs(traj) |
| if result: |
| return result |
| traj = json.loads(traj) |
| if isinstance(traj, list) and len(traj) > 0: |
| return [[float(p[0]), float(p[1])] for p in traj] |
| except Exception: |
| pass |
|
|
| patterns = [ |
| r'"trajectory"\s*:\s*"(\[\[.*?\]\])"', |
| r'"trajectory"\s*:\s*(\[\[.*?\]\])', |
| |
| r'"trajectory"\s*:\s*"[^"]*?(\[\[.*?\]\])[^"]*?"', |
| r'trajectory[^[]*(\[\[[^\]]*\]\])', |
| ] |
| for pat in patterns: |
| m = re.search(pat, response, re.DOTALL) |
| if m: |
| result = _extract_float_pairs(m.group(1)) |
| if result: |
| return result |
| try: |
| traj = json.loads(m.group(1)) |
| return [[float(p[0]), float(p[1])] for p in traj] |
| except Exception: |
| pass |
|
|
| |
| result = _extract_float_pairs(response) |
| if result and len(result) >= 3: |
| return result |
|
|
| return None |
|
|
|
|
| def parse_full_output(response: str) -> Optional[Dict]: |
| if not response: |
| return None |
| try: |
| return json.loads(response) |
| except Exception: |
| pass |
| m = re.search(r'\{.*\}', response, re.DOTALL) |
| if m: |
| try: |
| return json.loads(m.group(0)) |
| except Exception: |
| pass |
| return None |
|
|
|
|
| def compute_trajectory_metrics( |
| pred_traj: Optional[List[List[float]]], |
| gt_traj: List[List[float]], |
| ) -> Dict[str, Any]: |
| if pred_traj is None or len(pred_traj) == 0: |
| return {"ade": None, "fde": None, "valid": False, "num_points": 0} |
|
|
| pred = np.array(pred_traj, dtype=np.float64) |
| gt = np.array(gt_traj, dtype=np.float64) |
|
|
| n = min(len(pred), len(gt)) |
| if n == 0: |
| return {"ade": None, "fde": None, "valid": False, "num_points": 0} |
|
|
| pred, gt = pred[:n], gt[:n] |
| dists = np.linalg.norm(pred - gt, axis=1) |
| return { |
| "ade": float(dists.mean()), |
| "fde": float(dists[-1]), |
| "valid": True, |
| "num_points": n, |
| } |
|
|
|
|
| |
| |
| |
|
|
| class WaymoEvaluatorSpeculative: |
| """加载模型并使用 self-speculative decoding 推理。""" |
|
|
| def __init__( |
| self, |
| model_path: str, |
| base_model: str = "Qwen/Qwen2.5-VL-3B-Instruct", |
| device: str = "cuda", |
| torch_dtype: torch.dtype = torch.bfloat16, |
| min_pixels: Optional[int] = None, |
| max_pixels: Optional[int] = None, |
| block_size: Optional[int] = None, |
| sub_block_size: Optional[int] = None, |
| confidence_threshold: float = 0.0, |
| use_mdm: int = 1, |
| use_quadratic: int = 0, |
| temperature: float = 0.0, |
| top_p: float = 1.0, |
| top_k: int = 50, |
| section_block_steps: Optional[dict] = None, |
| section_token_budgets: Optional[dict] = None, |
| ): |
| self.device = device |
| self.is_mdm = False |
| self.block_size = block_size |
| self.sub_block_size = sub_block_size |
| self.mask_id = None |
| self.confidence_threshold = confidence_threshold |
| self.use_mdm = use_mdm |
| self.use_quadratic = use_quadratic |
| self.temperature = temperature |
| self.top_p = top_p |
| self.top_k = top_k |
| self.section_block_steps = section_block_steps |
| self.section_token_budgets = section_token_budgets |
|
|
| print(f"[{device}] 加载 processor: {base_model}", flush=True) |
| proc_kwargs = {} |
| if min_pixels is not None: |
| proc_kwargs["min_pixels"] = min_pixels |
| if max_pixels is not None: |
| proc_kwargs["max_pixels"] = max_pixels |
|
|
| self.processor = AutoProcessor.from_pretrained( |
| base_model, trust_remote_code=True, **proc_kwargs |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| print(f"[{device}] 加载模型: {model_path}", flush=True) |
| self.model = AutoModelForCausalLM.from_pretrained( |
| model_path, |
| torch_dtype=torch_dtype, |
| device_map=device, |
| trust_remote_code=True, |
| ).eval() |
|
|
| cfg = self.model.config |
| if hasattr(cfg, "bd_size") and cfg.bd_size is not None: |
| self.is_mdm = True |
| if self.block_size is None: |
| self.block_size = cfg.bd_size |
| tokenizer = AutoTokenizer.from_pretrained(model_path) |
| self.processor.tokenizer = tokenizer |
| mask_ids = tokenizer.encode("|<MASK>|", add_special_tokens=False) |
| self.mask_id = mask_ids[0] if mask_ids else 151665 |
| self._tokenizer = tokenizer |
| print( |
| f"[{device}] 检测到 MDM 模型,bd_size={self.block_size}," |
| f"mask_id={self.mask_id}", |
| flush=True, |
| ) |
| print( |
| f"[{device}] SECTION_VERSION={os.environ.get('SECTION_VERSION', '(unset)')}, " |
| f"confidence_threshold={self.confidence_threshold}", |
| flush=True, |
| ) |
| else: |
| print(f"[{device}] 检测到标准 Qwen2.5-VL / AR 模型", flush=True) |
| if self.use_mdm == 0: |
| self.is_mdm = False |
|
|
| |
|
|
| def _init_dav_config(self): |
| """Parse DaV parameters from environment variables (called once at init).""" |
| self._dav_num_candidates = int(os.environ.get("DAV_NUM_CANDIDATES", "4")) |
| self._dav_greedy_margin = float(os.environ.get("DAV_GREEDY_MARGIN", "0.05")) |
| self._dav_scoring_mode = os.environ.get("DAV_SCORING_MODE", "denoising") |
| self._dav_temperatures_list = [float(x) for x in os.environ.get("DAV_TEMPERATURES", "0.0,0.3,0.5,0.7").split(",")] |
| self._dav_noise_levels_list = [float(x) for x in os.environ.get("DAV_NOISE_LEVELS", "0.3,0.6").split(",")] |
| _sw_str = os.environ.get("DAV_SCORE_WEIGHTS", "") |
| self._dav_score_weights = json.loads(_sw_str) if _sw_str else None |
| self._last_dav_stats = None |
|
|
| def _build_value_mask(self, traj_tokens): |
| """Classify trajectory tokens as value (True) or structural (False). |
| |
| Structural tokens contain only characters from "[], \\n\\t" and are |
| kept frozen during re-denoising and scoring. |
| """ |
| structural_chars = set("[], \n\t") |
| traj_len = len(traj_tokens) |
| texts = self._tokenizer.batch_decode([[t.item()] for t in traj_tokens]) |
| value_mask = torch.zeros(traj_len, dtype=torch.bool, device=self.device) |
| for i, tok_text in enumerate(texts): |
| if not all(c in structural_chars for c in tok_text): |
| value_mask[i] = True |
| return value_mask |
|
|
| def _find_trajectory_token_range(self, output_ids, prompt_len): |
| """Find the token range of trajectory coordinate content in the output. |
| |
| Returns (abs_start, abs_end) as absolute positions in output_ids, |
| or (None, None) if trajectory cannot be found. |
| """ |
| gen_ids = output_ids[0, prompt_len:].tolist() |
| full_text = self._tokenizer.decode(gen_ids, skip_special_tokens=False) |
|
|
| traj_key = '"trajectory"' |
| key_idx = full_text.rfind(traj_key) |
| if key_idx < 0: |
| return None, None |
|
|
| content_start = full_text.find("[[", key_idx) |
| if content_start < 0: |
| content_start = full_text.find("[+", key_idx) |
| if content_start < 0: |
| return None, None |
|
|
| |
| token_texts = self._tokenizer.batch_decode([[t] for t in gen_ids]) |
| cum_len = 0 |
| traj_token_start = len(gen_ids) |
| for i, tt in enumerate(token_texts): |
| cum_len += len(tt) |
| if cum_len > content_start: |
| traj_token_start = i |
| break |
|
|
| |
| traj_token_end = len(gen_ids) |
| _stop_tokens = (151645, 151643) |
| while traj_token_end > traj_token_start and gen_ids[traj_token_end - 1] in _stop_tokens: |
| traj_token_end -= 1 |
|
|
| return prompt_len + traj_token_start, prompt_len + traj_token_end |
|
|
| @torch.no_grad() |
| def _redenoise_trajectory( |
| self, output_ids, traj_start, traj_end, temperature, |
| block_size, pixel_values=None, image_grid_thw=None, |
| mask_ratio=0.3, |
| ): |
| """Generate a trajectory variant by lightly perturbing greedy tokens. |
| |
| Randomly masks ``mask_ratio`` of value tokens (digits, signs, decimals) |
| while keeping all structural tokens frozen, then fills the masked |
| positions in a single forward pass with temperature sampling. |
| |
| This preserves trajectory format while creating diversity. |
| """ |
| import torch.nn.functional as F |
|
|
| traj_len = traj_end - traj_start |
| greedy_traj = output_ids[0, traj_start:traj_end].clone() |
|
|
| value_mask = self._build_value_mask(greedy_traj) |
| if value_mask.sum().item() == 0: |
| return greedy_traj |
|
|
| |
| noise = torch.rand(traj_len, device=self.device) |
| should_mask = (noise < mask_ratio) & value_mask |
| if should_mask.sum().item() == 0: |
| return greedy_traj |
|
|
| x_t = output_ids.clone() |
| x_t[0, traj_start:traj_end][should_mask] = self.mask_id |
|
|
| self.model.bd_size = block_size |
|
|
| fwd_kwargs = dict(input_ids=x_t, use_cache=False) |
| if pixel_values is not None: |
| fwd_kwargs["pixel_values"] = pixel_values |
| if image_grid_thw is not None: |
| fwd_kwargs["image_grid_thw"] = image_grid_thw |
|
|
| logits = self.model.forward(**fwd_kwargs).logits |
|
|
| traj_logits = torch.cat([ |
| logits[:, traj_start - 1 : traj_start, :], |
| logits[:, traj_start : traj_end - 1, :], |
| ], dim=1) |
|
|
| |
| result = greedy_traj.clone() |
| if temperature > 0: |
| scaled = traj_logits[0] / temperature |
| probs = F.softmax(scaled, dim=-1) |
| sampled = torch.multinomial(probs, num_samples=1).squeeze(-1) |
| else: |
| sampled = traj_logits[0].argmax(dim=-1) |
|
|
| result[should_mask] = sampled[should_mask] |
| return result |
|
|
| @torch.no_grad() |
| def _score_trajectory_denoising( |
| self, output_ids, traj_tokens, traj_start, traj_end, |
| noise_levels, pixel_values=None, image_grid_thw=None, |
| block_size=32, samples_per_level=1, |
| ): |
| """Score a trajectory candidate via denoising likelihood. |
| |
| Only value tokens (numeric) are masked for scoring; structural tokens |
| ([, ], comma, space) are kept frozen. Higher score → better candidate. |
| """ |
| import torch.nn.functional as F |
|
|
| traj_len = traj_end - traj_start |
| total_log_prob = 0.0 |
| total_count = 0 |
|
|
| value_mask = self._build_value_mask(traj_tokens) |
| if value_mask.sum().item() == 0: |
| return 0.0 |
|
|
| self.model.bd_size = block_size |
|
|
| for t in noise_levels: |
| for _ in range(samples_per_level): |
| noise = torch.rand(traj_len, device=self.device) |
| should_mask = (noise < t) & value_mask |
| n_masked = should_mask.sum().item() |
| if n_masked == 0: |
| continue |
|
|
| x_score = output_ids.clone() |
| x_score[0, traj_start:traj_end] = traj_tokens.clone() |
| x_score[0, traj_start:traj_end][should_mask] = self.mask_id |
|
|
| fwd_kwargs = dict(input_ids=x_score, use_cache=False) |
| if pixel_values is not None: |
| fwd_kwargs["pixel_values"] = pixel_values |
| if image_grid_thw is not None: |
| fwd_kwargs["image_grid_thw"] = image_grid_thw |
|
|
| logits = self.model.forward(**fwd_kwargs).logits |
|
|
| traj_logits = torch.cat([ |
| logits[:, traj_start - 1 : traj_start, :], |
| logits[:, traj_start : traj_end - 1, :], |
| ], dim=1) |
|
|
| log_probs = F.log_softmax(traj_logits[0], dim=-1) |
| token_lp = log_probs.gather(1, traj_tokens.unsqueeze(1)).squeeze(1) |
| total_log_prob += token_lp[should_mask].sum().item() |
| total_count += n_masked |
|
|
| return total_log_prob / max(total_count, 1) |
|
|
| |
|
|
| def _parse_traj_from_tokens(self, traj_tokens): |
| """Decode trajectory tokens → list of [x, y] waypoints.""" |
| import re |
| text = self._tokenizer.decode(traj_tokens.tolist(), skip_special_tokens=True) |
| pairs = re.findall(r'\[\s*([+-]?\d+\.?\d*)\s*,\s*([+-]?\d+\.?\d*)\s*\]', text) |
| if not pairs: |
| pairs = re.findall(r'([+-]?\d+\.\d+)\s*,\s*([+-]?\d+\.\d+)', text) |
| if not pairs: |
| return None |
| return [[float(a), float(b)] for a, b in pairs] |
|
|
| @staticmethod |
| def _smoothness_score(waypoints): |
| """Negative mean squared jerk — penalises jittery trajectories.""" |
| pts = np.array(waypoints, dtype=np.float64) |
| if len(pts) < 4: |
| return 0.0 |
| jerk = np.diff(pts, n=3, axis=0) |
| return -float(np.mean(np.sum(jerk ** 2, axis=1))) |
|
|
| @staticmethod |
| def _physics_score(waypoints): |
| """Soft penalty for implausible speed/acceleration (1 Hz, meters).""" |
| pts = np.array(waypoints, dtype=np.float64) |
| if len(pts) < 2: |
| return 0.0 |
| vel = np.diff(pts, axis=0) |
| speed = np.linalg.norm(vel, axis=1) |
| penalty = float(np.sum(np.maximum(speed - 35.0, 0.0) ** 2)) |
| if len(pts) >= 3: |
| acc = np.diff(vel, axis=0) |
| acc_mag = np.linalg.norm(acc, axis=1) |
| penalty += float(np.sum(np.maximum(acc_mag - 6.0, 0.0) ** 2)) * 2.0 |
| return -penalty |
|
|
| @staticmethod |
| def _consistency_score(waypoints, fmb_text): |
| """Check trajectory consistency with future_meta_behavior text.""" |
| pts = np.array(waypoints, dtype=np.float64) |
| if len(pts) < 2: |
| return 0.0 |
| fmb = (fmb_text or "").lower() |
| score = 0.0 |
| lat_disp = abs(pts[-1, 1] - pts[0, 1]) |
| if "straight" in fmb and lat_disp > 2.0: |
| score -= (lat_disp - 2.0) * 0.5 |
| seg = np.linalg.norm(np.diff(pts, axis=0), axis=1) |
| if len(seg) >= 2: |
| if ("decelerate" in fmb or "slow" in fmb) and seg[-1] > seg[0] * 1.3: |
| score -= 1.0 |
| if "stop" in fmb and seg[-1] > 2.0: |
| score -= 2.0 |
| if "accelerate" in fmb and seg[-1] < seg[0] * 0.7: |
| score -= 1.0 |
| return score |
|
|
| @staticmethod |
| def _consensus_score(candidate_wps, all_candidates_wps): |
| """Negative mean distance to centroid trajectory.""" |
| valid = [np.array(w, dtype=np.float64) for w in all_candidates_wps |
| if w is not None and len(w) >= 3] |
| if len(valid) < 2: |
| return 0.0 |
| min_len = min(len(v) for v in valid) |
| valid = [v[:min_len] for v in valid] |
| centroid = np.mean(valid, axis=0) |
| cand = np.array(candidate_wps, dtype=np.float64)[:min_len] |
| return -float(np.linalg.norm(cand - centroid, axis=1).mean()) |
|
|
| def _score_candidates( |
| self, output_ids, candidates, traj_start, traj_end, |
| scoring_mode="denoising", noise_levels=None, block_size=32, |
| pixel_values=None, image_grid_thw=None, |
| ): |
| """Score trajectory candidates using the specified mode. |
| |
| Supported modes: |
| - ``denoising``: denoising log-likelihood (requires forward passes) |
| - ``geometric``: smoothness + physics + consensus (no model calls) |
| - ``hybrid``: weighted combination of denoising + geometric |
| - ``consistency``: geometric + future_meta_behavior consistency |
| - ``consensus_only``: pick the candidate closest to the centroid |
| |
| Returns (scores_list, details_dict). |
| """ |
| N = len(candidates) |
| noise_levels = noise_levels or [0.3, 0.6] |
|
|
| |
| all_wps = [self._parse_traj_from_tokens(c) for c in candidates] |
|
|
| |
| if scoring_mode in ("denoising", "hybrid"): |
| den_scores = [] |
| for traj_tokens in candidates: |
| s = self._score_trajectory_denoising( |
| output_ids, traj_tokens, traj_start, traj_end, |
| noise_levels=noise_levels, block_size=block_size, |
| pixel_values=pixel_values, image_grid_thw=image_grid_thw, |
| ) |
| den_scores.append(s) |
| else: |
| den_scores = [0.0] * N |
|
|
| |
| |
| fmb_text = "" |
| if scoring_mode in ("consistency", "hybrid"): |
| full_text = self._tokenizer.decode( |
| output_ids[0].tolist(), skip_special_tokens=True) |
| fmb_idx = full_text.find("future_meta_behavior") |
| if fmb_idx >= 0: |
| traj_idx = full_text.find("trajectory", fmb_idx) |
| fmb_text = full_text[fmb_idx:traj_idx] if traj_idx > fmb_idx else full_text[fmb_idx:fmb_idx+200] |
|
|
| |
| valid_wps = [np.array(w, dtype=np.float64) for w in all_wps |
| if w is not None and len(w) >= 3] |
| if len(valid_wps) >= 2: |
| _min_len = min(len(v) for v in valid_wps) |
| _trimmed = [v[:_min_len] for v in valid_wps] |
| _centroid = np.mean(_trimmed, axis=0) |
| else: |
| _centroid = None |
| _min_len = 0 |
|
|
| geo_components = [] |
| for i, wps in enumerate(all_wps): |
| if wps is None or len(wps) < 2: |
| geo_components.append({ |
| "smoothness": -10.0, |
| "physics": -10.0, |
| "consensus": -10.0, |
| "consistency": -10.0, |
| }) |
| else: |
| if _centroid is not None: |
| cand = np.array(wps, dtype=np.float64)[:_min_len] |
| consensus = -float(np.linalg.norm(cand - _centroid, axis=1).mean()) |
| else: |
| consensus = 0.0 |
|
|
| geo_components.append({ |
| "smoothness": self._smoothness_score(wps), |
| "physics": self._physics_score(wps), |
| "consensus": consensus, |
| "consistency": self._consistency_score(wps, fmb_text), |
| }) |
|
|
| |
| if scoring_mode == "denoising": |
| scores = den_scores |
|
|
| elif scoring_mode == "geometric": |
| |
| weights = {"smoothness": 0.30, "physics": 0.25, |
| "consensus": 0.30, "consistency": 0.15} |
| scores = self._hybrid_combine(geo_components, weights) |
|
|
| elif scoring_mode == "hybrid": |
| |
| weights = {"denoising": 0.40, "smoothness": 0.15, "physics": 0.15, |
| "consensus": 0.20, "consistency": 0.10} |
| combined = [] |
| for i in range(N): |
| d = dict(geo_components[i]) |
| d["denoising"] = den_scores[i] |
| combined.append(d) |
| scores = self._hybrid_combine(combined, weights) |
|
|
| elif scoring_mode == "consistency": |
| weights = {"smoothness": 0.25, "physics": 0.20, |
| "consensus": 0.25, "consistency": 0.30} |
| scores = self._hybrid_combine(geo_components, weights) |
|
|
| elif scoring_mode == "consensus_only": |
| scores = [g["consensus"] for g in geo_components] |
|
|
| else: |
| scores = den_scores |
|
|
| details = { |
| "den_scores": den_scores, |
| "geo_components": geo_components, |
| "all_wps_valid": [w is not None for w in all_wps], |
| } |
| return scores, details |
|
|
| @staticmethod |
| def _hybrid_combine(component_scores_list, weights): |
| """Z-normalise each component across candidates, then weighted sum.""" |
| N = len(component_scores_list) |
| if N == 0: |
| return [] |
| components = list(weights.keys()) |
| raw = {c: np.array([s.get(c, 0.0) for s in component_scores_list], |
| dtype=np.float64) for c in components} |
| normed = {} |
| for c in components: |
| std = raw[c].std() |
| normed[c] = (raw[c] - raw[c].mean()) / std if std > 1e-8 else np.zeros(N) |
| final = np.zeros(N) |
| for c in components: |
| final += weights.get(c, 0.0) * normed[c] |
| return final.tolist() |
|
|
| @staticmethod |
| def _to_trajectory_only_question(question: str) -> str: |
| """Keep the original input block but ask only for trajectory prediction.""" |
| traj_header = ( |
| "You are an expert autonomous driving agent.\n" |
| "Task: Trajectory Prediction\n" |
| "Predict the optimal 5-second future trajectory (5 waypoints, 1 s intervals).\n\n" |
| "Input:" |
| ) |
| input_marker = "Input:" |
| if input_marker in question: |
| return traj_header + question.split(input_marker, 1)[1] |
| return traj_header + "\n" + question |
|
|
| @torch.no_grad() |
| def generate( |
| self, |
| image, |
| question: str, |
| max_new_tokens: int = 1024, |
| ) -> Union[str, tuple]: |
| """对单张或多张图 + 问题进行推理,返回原始文本输出。 |
| |
| image: PIL.Image.Image 或 List[PIL.Image.Image] |
| Returns str normally, or (str, dav_stats) when DaV mode is active. |
| """ |
| if isinstance(image, list): |
| images_list = image |
| else: |
| images_list = [image] |
|
|
| _sv = getattr(self, '_section_version', None) or os.environ.get("SECTION_VERSION") |
| if str(_sv) == "traj_only_spec": |
| question = self._to_trajectory_only_question(question) |
|
|
| content = [] |
| image_idx = 0 |
| if '<image>' in question: |
| parts = question.split('<image>') |
| for idx, part in enumerate(parts): |
| if part: |
| content.append({"type": "text", "text": part}) |
| if idx < len(parts) - 1: |
| if idx == len(parts) - 2: |
| while image_idx < len(images_list): |
| content.append({"type": "image", "image": images_list[image_idx]}) |
| image_idx += 1 |
| else: |
| if image_idx < len(images_list): |
| content.append({"type": "image", "image": images_list[image_idx]}) |
| image_idx += 1 |
| else: |
| content.append({"type": "text", "text": question}) |
| while image_idx < len(images_list): |
| content.append({"type": "image", "image": images_list[image_idx]}) |
| image_idx += 1 |
|
|
| messages = [ |
| { |
| "role": "user", |
| "content": content, |
| } |
| ] |
|
|
| text = self.processor.apply_chat_template( |
| messages, tokenize=False, add_generation_prompt=True |
| ) |
| inputs = self.processor( |
| text=[text], |
| images=images_list, |
| padding=True, |
| return_tensors="pt", |
| ).to(self.device) |
|
|
| if self.is_mdm and self.use_mdm == 1: |
| if hasattr(self.model, 'mdm_sample_deep_scaffold'): |
| |
| spec_kwargs = dict( |
| input_ids=inputs.input_ids, |
| tokenizer=self._tokenizer, |
| section_token_budgets=self.section_token_budgets, |
| max_tokens=max_new_tokens, |
| mask_id=self.mask_id, |
| threshold=self.confidence_threshold, |
| debug=False, |
| stream=False, |
| ) |
| if self.sub_block_size is not None: |
| spec_kwargs["sub_block_size"] = self.sub_block_size |
| if hasattr(inputs, "pixel_values") and inputs.pixel_values is not None: |
| spec_kwargs["pixel_values"] = inputs.pixel_values |
| if hasattr(inputs, "image_grid_thw") and inputs.image_grid_thw is not None: |
| spec_kwargs["image_grid_thw"] = inputs.image_grid_thw |
|
|
| _sv = os.environ.get("SECTION_VERSION", "scaffold_spec") |
| if str(_sv) == "deep": |
| |
| deep_kwargs = dict( |
| input_ids=inputs.input_ids, |
| tokenizer=self._tokenizer, |
| max_tokens=max_new_tokens, |
| mask_id=self.mask_id, |
| threshold=self.confidence_threshold, |
| block_size=self.block_size or 32, |
| ) |
| if hasattr(inputs, "pixel_values") and inputs.pixel_values is not None: |
| deep_kwargs["pixel_values"] = inputs.pixel_values |
| if hasattr(inputs, "image_grid_thw") and inputs.image_grid_thw is not None: |
| deep_kwargs["image_grid_thw"] = inputs.image_grid_thw |
| print(f"Using mdm_sample_deep_scaffold!!!!!!!!!!!!!!!!!!!!") |
| output_ids = self.model.mdm_sample_deep_scaffold(**deep_kwargs) |
| elif str(_sv) == "scaffold_spec": |
| |
| |
| ss_kwargs = dict( |
| input_ids=inputs.input_ids, |
| tokenizer=self._tokenizer, |
| block_size=self.block_size or 32, |
| max_tokens=max_new_tokens, |
| mask_id=self.mask_id, |
| threshold=self.confidence_threshold, |
| ) |
| if hasattr(inputs, "pixel_values") and inputs.pixel_values is not None: |
| ss_kwargs["pixel_values"] = inputs.pixel_values |
| if hasattr(inputs, "image_grid_thw") and inputs.image_grid_thw is not None: |
| ss_kwargs["image_grid_thw"] = inputs.image_grid_thw |
| print(f"Using scaffold_speculative_sample!!!!!!!!!!!!!!!!!!!!") |
| output_ids = self.model.scaffold_speculative_sample(**ss_kwargs) |
| elif str(_sv) == "scaffold_spec_ss_multi_traj": |
| |
| |
| |
| |
| ss_kwargs = dict( |
| input_ids=inputs.input_ids, |
| tokenizer=self._tokenizer, |
| block_size=self.block_size or 32, |
| max_tokens=max_new_tokens, |
| mask_id=self.mask_id, |
| threshold=self.confidence_threshold, |
| ) |
| if hasattr(inputs, "pixel_values") and inputs.pixel_values is not None: |
| ss_kwargs["pixel_values"] = inputs.pixel_values |
| if hasattr(inputs, "image_grid_thw") and inputs.image_grid_thw is not None: |
| ss_kwargs["image_grid_thw"] = inputs.image_grid_thw |
| ss_kwargs["num_traj_rollouts"] = int(os.environ.get("NUM_TRAJ_ROLLOUTS", "4")) |
| ss_kwargs["traj_verify_temperature"] = float(os.environ.get("TRAJ_VERIFY_TEMP", "0.5")) |
| ss_kwargs["traj_draft_temperature"] = float(os.environ.get("TRAJ_DRAFT_TEMP", "0.0")) |
| print( |
| f"Using scaffold_spec_with_ss_multi_traj " |
| f"(N={ss_kwargs['num_traj_rollouts']}, vt={ss_kwargs['traj_verify_temperature']})" |
| "!!!!!!!!!!!!!!!!!!!!" |
| ) |
| output_ids = self.model.scaffold_spec_with_ss_multi_traj(**ss_kwargs) |
| else: |
| |
| deep_kwargs = dict( |
| input_ids=inputs.input_ids, |
| tokenizer=self._tokenizer, |
| max_tokens=max_new_tokens, |
| mask_id=self.mask_id, |
| threshold=self.confidence_threshold, |
| block_size=self.block_size or 32, |
| ) |
| if hasattr(inputs, "pixel_values") and inputs.pixel_values is not None: |
| deep_kwargs["pixel_values"] = inputs.pixel_values |
| if hasattr(inputs, "image_grid_thw") and inputs.image_grid_thw is not None: |
| deep_kwargs["image_grid_thw"] = inputs.image_grid_thw |
| output_ids = self.model.mdm_sample_deep_scaffold(**deep_kwargs) |
| else: |
| |
| _sv = getattr(self, '_section_version', None) or os.environ.get("SECTION_VERSION") |
|
|
| common_kwargs = dict( |
| input_ids=inputs.input_ids, |
| tokenizer=self.processor.tokenizer, |
| block_size=self.block_size, |
| max_tokens=max_new_tokens, |
| mask_id=self.mask_id, |
| debug=False, |
| ) |
| if self.sub_block_size is not None: |
| common_kwargs["sub_block_size"] = self.sub_block_size |
| if hasattr(inputs, "pixel_values") and inputs.pixel_values is not None: |
| common_kwargs["pixel_values"] = inputs.pixel_values |
| if hasattr(inputs, "image_grid_thw") and inputs.image_grid_thw is not None: |
| common_kwargs["image_grid_thw"] = inputs.image_grid_thw |
|
|
| if str(_sv) == "ar": |
| common_kwargs["temperature"] = self.temperature |
| common_kwargs["top_p"] = self.top_p |
| common_kwargs["top_k"] = self.top_k |
| print(f"Using ar_sample_cache!!!!!!!!!!!!!!!!!!!!") |
| output_ids = self.model.ar_sample_cache(**common_kwargs) |
| elif str(_sv) == "block_causal": |
| common_kwargs["threshold"] = self.confidence_threshold |
| print(f"Using block_causal_sample_cache!!!!!!!!!!!!!!!!!!!!") |
| output_ids = self.model.block_causal_sample_cache(**common_kwargs) |
| else: |
| |
| spec_kwargs = dict(common_kwargs) |
| spec_kwargs["stream"] = False |
| |
| if not self.use_quadratic: |
| spec_kwargs["threshold"] = self.confidence_threshold |
| print(f"Using speculative_block_causal_sample_cache!!!!!!!!!!!!!!!!!!!!") |
| output_ids = self.model.speculative_block_causal_sample_cache(**spec_kwargs) |
|
|
| |
| if str(_sv) == "spec_dav": |
| prompt_len = inputs.input_ids.shape[1] |
| traj_start, traj_end = self._find_trajectory_token_range( |
| output_ids, prompt_len, |
| ) |
|
|
| if traj_start is not None and traj_end is not None and traj_end > traj_start: |
| N = self._dav_num_candidates |
| temperatures = self._dav_temperatures_list |
| noise_levels = self._dav_noise_levels_list |
| block_size = self.block_size or 32 |
|
|
| pv = getattr(inputs, "pixel_values", None) |
| igt = getattr(inputs, "image_grid_thw", None) |
|
|
| |
| candidates = [output_ids[0, traj_start:traj_end].clone()] |
|
|
| |
| for cand_idx in range(1, N): |
| temp = temperatures[cand_idx % len(temperatures)] |
| traj_tokens = self._redenoise_trajectory( |
| output_ids, traj_start, traj_end, |
| temperature=temp, block_size=block_size, |
| pixel_values=pv, image_grid_thw=igt, |
| ) |
| candidates.append(traj_tokens) |
|
|
| |
| scoring_mode = getattr(self, '_dav_scoring_mode', 'denoising') |
| scores, score_details = self._score_candidates( |
| output_ids, candidates, traj_start, traj_end, |
| scoring_mode=scoring_mode, |
| noise_levels=noise_levels, block_size=block_size, |
| pixel_values=pv, image_grid_thw=igt, |
| ) |
|
|
| |
| raw_best = max(range(N), key=lambda i: scores[i]) |
| greedy_margin = self._dav_greedy_margin |
| if raw_best != 0 and greedy_margin > 0: |
| best_idx = 0 if (scores[raw_best] - scores[0]) < greedy_margin else raw_best |
| else: |
| best_idx = raw_best |
|
|
| if best_idx != 0: |
| output_ids = output_ids.clone() |
| output_ids[0, traj_start:traj_end] = candidates[best_idx] |
|
|
| self._last_dav_stats = { |
| "mode": f"spec_dav_{scoring_mode}", |
| "scores": scores, |
| "best_idx": best_idx, |
| "raw_best_idx": raw_best, |
| "temperatures": [temperatures[i % len(temperatures)] for i in range(N)], |
| "greedy_fallback": best_idx != raw_best, |
| "score_details": score_details, |
| } |
| else: |
| |
| gen_kwargs = { |
| "max_new_tokens": max_new_tokens, |
| "do_sample": self.temperature > 0, |
| } |
| if self.temperature > 0: |
| gen_kwargs["temperature"] = self.temperature |
| gen_kwargs["top_p"] = self.top_p |
| gen_kwargs["top_k"] = self.top_k |
|
|
| output_ids = self.model.generate(**inputs, **gen_kwargs) |
|
|
| generated_ids = output_ids[:, inputs.input_ids.shape[1]:] |
| response = self.processor.batch_decode( |
| generated_ids, skip_special_tokens=True |
| )[0] |
| return response.strip() |
|
|
|
|
| |
| |
| |
|
|
| def evaluate_samples( |
| evaluator: WaymoEvaluatorSpeculative, |
| samples: List[Dict], |
| image_root: str, |
| max_new_tokens: int, |
| rank: int = 0, |
| ) -> List[Dict]: |
| results = [] |
|
|
| for item in tqdm(samples, desc=f"GPU{rank} 推理", position=rank, leave=True): |
| sample_id = item.get("sample_id", "") |
| image_field = item.get("image", "") |
| gt_traj = item.get("future waypoints", []) |
| if isinstance(gt_traj, str): |
| try: |
| gt_traj = json.loads(gt_traj) |
| except Exception: |
| gt_traj = [] |
| nav_cmd = item.get("navigation_command", "") |
| conversations = item.get("conversations", []) |
|
|
| question = "" |
| gt_answer_template = "" |
| for msg in conversations: |
| if msg.get("from") == "human": |
| question = msg.get("value", "") |
| elif msg.get("from") == "assistant": |
| gt_answer_template = msg.get("value", "") |
|
|
| image_path = resolve_image_path(image_field, image_root) |
|
|
| entry = { |
| "sample_id": sample_id, |
| "image": image_field, |
| "image_full_path": image_path, |
| "navigation_command": nav_cmd, |
| "question": question, |
| "gt_answer_template": gt_answer_template, |
| "gt_trajectory": gt_traj, |
| "model_output_raw": None, |
| "model_output_parsed": None, |
| "pred_trajectory": None, |
| "metrics": {"ade": None, "fde": None, "valid": False}, |
| "error": None, |
| } |
|
|
| if not question: |
| entry["error"] = "missing question in conversations" |
| results.append(entry) |
| continue |
|
|
| |
| if isinstance(image_path, list): |
| missing = [p for p in image_path if not os.path.isfile(p)] |
| if missing: |
| entry["error"] = f"image not found: {missing}" |
| results.append(entry) |
| continue |
| else: |
| if not os.path.isfile(image_path): |
| entry["error"] = f"image not found: {image_path}" |
| results.append(entry) |
| continue |
|
|
| try: |
| if isinstance(image_path, list): |
| image = [Image.open(p).convert("RGB") for p in image_path] |
| else: |
| image = Image.open(image_path).convert("RGB") |
| raw_output = evaluator.generate(image, question, max_new_tokens=max_new_tokens) |
| entry["model_output_raw"] = raw_output |
| entry["model_output_parsed"] = parse_full_output(raw_output) |
| entry["pred_trajectory"] = parse_trajectory(raw_output) |
| entry["metrics"] = compute_trajectory_metrics(entry["pred_trajectory"], gt_traj) |
| |
| if hasattr(evaluator, '_last_dav_stats') and evaluator._last_dav_stats is not None: |
| stats = evaluator._last_dav_stats |
| |
| entry["dav_stats"] = { |
| k: (v.tolist() if hasattr(v, 'tolist') else v) |
| for k, v in stats.items() |
| } |
| evaluator._last_dav_stats = None |
| except Exception as e: |
| entry["error"] = traceback.format_exc() |
| print(f"\n[GPU{rank}] 错误 sample_id={sample_id}: {e}", flush=True) |
|
|
| results.append(entry) |
|
|
| return results |
|
|
|
|
| |
| |
| |
|
|
| def worker_fn( |
| rank: int, |
| world_size: int, |
| model_path: str, |
| base_model: str, |
| all_samples: List[Dict], |
| image_root: str, |
| max_new_tokens: int, |
| min_pixels: Optional[int], |
| max_pixels: Optional[int], |
| return_dict: dict, |
| block_size: Optional[int] = None, |
| sub_block_size: Optional[int] = None, |
| confidence_threshold: float = 0.0, |
| use_mdm: int = 1, |
| use_quadratic: int = 0, |
| temperature: float = 0.0, |
| top_p: float = 1.0, |
| top_k: int = 50, |
| section_block_steps: Optional[dict] = None, |
| section_token_budgets: Optional[dict] = None, |
| ): |
| torch.cuda.set_device(rank) |
| device = f"cuda:{rank}" |
| shard = all_samples[rank::world_size] |
|
|
| try: |
| evaluator = WaymoEvaluatorSpeculative( |
| model_path=model_path, |
| base_model=base_model, |
| device=device, |
| min_pixels=min_pixels, |
| max_pixels=max_pixels, |
| block_size=block_size, |
| sub_block_size=sub_block_size, |
| confidence_threshold=confidence_threshold, |
| use_mdm=use_mdm, |
| use_quadratic=use_quadratic, |
| temperature=temperature, |
| top_p=top_p, |
| top_k=top_k, |
| section_block_steps=section_block_steps, |
| section_token_budgets=section_token_budgets, |
| ) |
| results = evaluate_samples(evaluator, shard, image_root, max_new_tokens, rank) |
| except Exception as e: |
| err_msg = traceback.format_exc() |
| print(f"\n[GPU{rank}] Worker 崩溃: {e}\n{err_msg}", flush=True) |
| results = [] |
| for item in shard: |
| results.append({ |
| "sample_id": item.get("sample_id", ""), |
| "image": item.get("image", ""), |
| "navigation_command": item.get("navigation_command", ""), |
| "question": "", |
| "gt_answer_template": "", |
| "gt_trajectory": item.get("future waypoints", []), |
| "model_output_raw": None, |
| "model_output_parsed": None, |
| "pred_trajectory": None, |
| "metrics": {"ade": None, "fde": None, "valid": False}, |
| "error": f"Worker {rank} crashed: {e}", |
| }) |
|
|
| return_dict[rank] = results |
|
|
|
|
| |
| |
| |
|
|
| def aggregate_metrics(predictions: List[Dict]) -> Dict[str, Any]: |
| total = len(predictions) |
| failed = sum(1 for p in predictions if p.get("error")) |
| evaluated = total - failed |
|
|
| valid_preds = [p for p in predictions if p["metrics"].get("valid")] |
| n_valid = len(valid_preds) |
|
|
| agg = { |
| "total_samples": total, |
| "evaluated": evaluated, |
| "failed": failed, |
| "valid_trajectory": n_valid, |
| "valid_trajectory_ratio": n_valid / evaluated if evaluated > 0 else 0.0, |
| } |
|
|
| if n_valid > 0: |
| ades = [p["metrics"]["ade"] for p in valid_preds] |
| fdes = [p["metrics"]["fde"] for p in valid_preds] |
| agg["ade_mean"] = float(np.mean(ades)) |
| agg["ade_std"] = float(np.std(ades)) |
| agg["ade_median"] = float(np.median(ades)) |
| agg["fde_mean"] = float(np.mean(fdes)) |
| agg["fde_std"] = float(np.std(fdes)) |
| agg["fde_median"] = float(np.median(fdes)) |
| else: |
| agg["ade_mean"] = agg["fde_mean"] = None |
|
|
| return agg |
|
|
|
|
| |
| |
| |
|
|
| |
| _MODES = { |
| "section_diffusion": ("deep", 0.9), |
| "scaffold_spec": ("scaffold_spec", 0.0), |
| "inference_scaling": ("scaffold_spec_ss_multi_traj", 0.0), |
| } |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser( |
| description="Fast-dDrive — Waymo E2E open-loop evaluation.", |
| formatter_class=argparse.ArgumentDefaultsHelpFormatter, |
| ) |
| parser.add_argument("--model_path", default="Efficient-Large-Model/Fast-dDrive", |
| help="Fast-dDrive checkpoint directory or HuggingFace id " |
| "(default: Efficient-Large-Model/Fast-dDrive paper checkpoint).") |
| parser.add_argument("--eval_json", required=True, |
| help="Waymo E2E validation JSON.") |
| parser.add_argument("--image_root", required=True, |
| help="Root directory that the image paths in --eval_json are relative to.") |
| parser.add_argument("--output_dir", required=True, |
| help="Where to write predictions.json + metrics.json.") |
| parser.add_argument("--mode", default="scaffold_spec", choices=sorted(_MODES), |
| help="Decoding path. scaffold_spec is the paper canonical (Scaffold Spec, SS).") |
| parser.add_argument("--confidence_threshold", type=float, default=None, |
| help="Override the per-mode default threshold " |
| "(0.0 for scaffold_spec / inference_scaling, 0.9 for section_diffusion).") |
| parser.add_argument("--num_gpus", type=int, default=1, |
| help="Number of GPUs to shard inference across.") |
| parser.add_argument("--max_samples", type=int, default=-1, |
| help="Cap on number of samples (-1 = all).") |
| args = parser.parse_args() |
|
|
| section_version, default_threshold = _MODES[args.mode] |
| confidence_threshold = args.confidence_threshold if args.confidence_threshold is not None else default_threshold |
| os.environ["SECTION_VERSION"] = section_version |
|
|
| |
| block_size = 32 |
| base_model = "Qwen/Qwen2.5-VL-3B-Instruct" |
| min_pixels = max_pixels = 200704 |
| max_new_tokens = 1024 |
|
|
| project_root = os.getcwd() |
| eval_json = args.eval_json if os.path.isabs(args.eval_json) else os.path.join(project_root, args.eval_json) |
| image_root = args.image_root if os.path.isabs(args.image_root) else os.path.join(project_root, args.image_root) |
| output_dir = args.output_dir if os.path.isabs(args.output_dir) else os.path.join(project_root, args.output_dir) |
| os.makedirs(output_dir, exist_ok=True) |
|
|
| print(f"Loading eval JSON: {eval_json}", flush=True) |
| with open(eval_json, "r") as f: |
| all_samples = json.load(f) |
| if args.max_samples > 0: |
| all_samples = all_samples[:args.max_samples] |
| print(f" {len(all_samples)} samples; mode={args.mode} (SECTION_VERSION={section_version}, " |
| f"threshold={confidence_threshold})", flush=True) |
|
|
| t0 = time.time() |
| if args.num_gpus > 1: |
| mp.set_start_method("spawn", force=True) |
| manager = mp.Manager() |
| return_dict = manager.dict() |
| processes = [] |
| for rank in range(args.num_gpus): |
| p = mp.Process( |
| target=worker_fn, |
| args=( |
| rank, args.num_gpus, args.model_path, base_model, |
| all_samples, image_root, max_new_tokens, |
| min_pixels, max_pixels, return_dict, |
| block_size, None, confidence_threshold, 1, 0, |
| 0.0, 1.0, 50, None, None, |
| ), |
| ) |
| p.start() |
| processes.append(p) |
| for p in processes: |
| p.join() |
|
|
| shards = [] |
| for r in range(args.num_gpus): |
| if r in return_dict: |
| shards.append(return_dict[r]) |
| else: |
| print(f"[warn] GPU{r} returned no results", flush=True) |
| shards.append([]) |
|
|
| predictions = [None] * len(all_samples) |
| for rank, shard in enumerate(shards): |
| for local_i, result in enumerate(shard): |
| global_i = rank + local_i * args.num_gpus |
| if global_i < len(predictions): |
| predictions[global_i] = result |
| predictions = [p for p in predictions if p is not None] |
| else: |
| evaluator = WaymoEvaluatorSpeculative( |
| model_path=args.model_path, |
| base_model=base_model, |
| device="cuda", |
| min_pixels=min_pixels, |
| max_pixels=max_pixels, |
| block_size=block_size, |
| sub_block_size=None, |
| confidence_threshold=confidence_threshold, |
| use_mdm=1, |
| use_quadratic=0, |
| temperature=0.0, |
| top_p=1.0, |
| top_k=50, |
| section_block_steps=None, |
| section_token_budgets=None, |
| ) |
| predictions = evaluate_samples( |
| evaluator, all_samples, image_root, max_new_tokens, rank=0 |
| ) |
|
|
| elapsed = time.time() - t0 |
| print(f"\nInference done in {elapsed:.1f}s ({elapsed/max(len(predictions),1):.2f}s/sample)", flush=True) |
|
|
| agg = aggregate_metrics(predictions) |
| output = { |
| "metadata": { |
| "model_path": args.model_path, |
| "eval_json": eval_json, |
| "image_root": image_root, |
| "timestamp": datetime.now().isoformat(), |
| "num_gpus": args.num_gpus, |
| "elapsed_s": round(elapsed, 2), |
| "mode": args.mode, |
| "section_version": section_version, |
| "confidence_threshold": confidence_threshold, |
| "block_size": block_size, |
| }, |
| "aggregate_metrics": agg, |
| "predictions": predictions, |
| } |
|
|
| predictions_path = os.path.join(output_dir, "predictions.json") |
| metrics_path = os.path.join(output_dir, "metrics.json") |
| with open(predictions_path, "w", encoding="utf-8") as f: |
| json.dump(output, f, ensure_ascii=False, indent=2) |
| with open(metrics_path, "w", encoding="utf-8") as f: |
| json.dump({"metadata": output["metadata"], "aggregate_metrics": agg}, f, ensure_ascii=False, indent=2) |
|
|
| print("\n" + "=" * 60) |
| print(f"Fast-dDrive eval — mode={args.mode}") |
| print("=" * 60) |
| print(f" samples : {agg['total_samples']}") |
| print(f" succeeded : {agg['evaluated']}") |
| print(f" failed : {agg['failed']}") |
| print(f" trajectory parse : {agg['valid_trajectory']} ({agg['valid_trajectory_ratio']*100:.1f}%)") |
| if agg.get("ade_mean") is not None: |
| print(f" ADE (mean / median): {agg['ade_mean']:.4f} / {agg['ade_median']:.4f} m") |
| print(f" FDE (mean / median): {agg['fde_mean']:.4f} / {agg['fde_median']:.4f} m") |
| print("=" * 60) |
| print(f"Saved:\n {predictions_path}\n {metrics_path}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|