#!/usr/bin/env python3 """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 # The model (`Fast_dLLM_Qwen2_5_VLForConditionalGeneration`) and its # inference-time decoding paths (Section Diffusion, Scaffold Spec, # Scaffold Spec multi-rollout) are loaded directly from the HF release via # ``trust_remote_code=True``; no local registration step is needed. # ══════════════════════════════════════════════════════════════════ # 工具函数 # ══════════════════════════════════════════════════════════════════ 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*(\[\[.*?\]\])', # Handle <|mdm_start|>/<|mdm_end|> wrapped trajectories in corrupted JSON 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 # Last resort: extract any float pairs from the raw response 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 ) # Load the released HF model. ``trust_remote_code=True`` pulls # ``Fast_dLLM_Qwen2_5_VLForConditionalGeneration`` (defined in the HF # repo's ``modeling.py``) and attaches the three decoding paths # (``mdm_sample_deep_scaffold`` / ``scaffold_speculative_sample`` / # ``scaffold_spec_with_ss_multi_traj``) onto the model class at import # time, so no manual method binding is needed here. 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("||", 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 # ── spec_dav helpers ──────────────────────────────────────────── 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 # Build cumulative text-length array via per-token decode (single pass) token_texts = self._tokenizer.batch_decode([[t] for t in gen_ids]) cum_len = 0 traj_token_start = len(gen_ids) # fallback for i, tt in enumerate(token_texts): cum_len += len(tt) if cum_len > content_start: traj_token_start = i break # Skip trailing special tokens (e.g. <|im_end|>, <|endoftext|>) 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 # Randomly select mask_ratio of value tokens to re-denoise 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) # Fill masked positions with temperature sampling 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) # ── Geometric & hybrid scoring helpers for spec_dav ──────────── 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] # Parse waypoints for each candidate (needed by geometric modes) all_wps = [self._parse_traj_from_tokens(c) for c in candidates] # ── Denoising scores ─────────────────────────────────────── 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 # ── Geometric component scores ───────────────────────────── # Extract fmb_text once (used by consistency/hybrid modes) 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] # Pre-compute consensus centroid once 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), }) # ── Combine scores per mode ──────────────────────────────── if scoring_mode == "denoising": scores = den_scores elif scoring_mode == "geometric": # Z-normalize and weighted sum of geometric components weights = {"smoothness": 0.30, "physics": 0.25, "consensus": 0.30, "consistency": 0.15} scores = self._hybrid_combine(geo_components, weights) elif scoring_mode == "hybrid": # Combine denoising + geometric 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 # fallback 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 '' in question: parts = question.split('') 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'): # Section-aware speculative decoding 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 scaffold: pre-fill entire JSON structure, denoise values only 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": # Scaffold-guided self-speculative: scaffold structure is # deterministic, value tokens generated via self-spec with KV cache 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": # Shared-prefix multi-trajectory rollout: SS for sections 1-3 # at vt=0, then fork the KV cache N times and run SS on the # trajectory section with verify_temperature > 0, finally # equal-weight averaging the N trajectories. 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: # Default to deep scaffold for any unrecognized section version 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: # Check for spec_dav mode (works with any MDM checkpoint) _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: # Self-speculative decoding (standard or quadratic) spec_kwargs = dict(common_kwargs) spec_kwargs["stream"] = False # quadratic 版本不接受 threshold 参数 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) # ── spec_dav: re-rank trajectory via denoising likelihood ── 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) # Candidate 0 = greedy (already generated) candidates = [output_ids[0, traj_start:traj_end].clone()] # Candidates 1..N-1: re-denoise trajectory with temperature 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) # Score each candidate using the configured scoring mode 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, ) # Select best with greedy margin 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 # Validate image path(s) exist 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) # Capture DaV stats if available if hasattr(evaluator, '_last_dav_stats') and evaluator._last_dav_stats is not None: stats = evaluator._last_dav_stats # Convert to serializable format 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 # ══════════════════════════════════════════════════════════════════ # 多 GPU 并行 # ══════════════════════════════════════════════════════════════════ 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 # ══════════════════════════════════════════════════════════════════ # 入口 # ══════════════════════════════════════════════════════════════════ # Mode → (SECTION_VERSION env value, default confidence threshold). _MODES = { "section_diffusion": ("deep", 0.9), # Section Diffusion (SD): iterative MDM denoising over the scaffold. "scaffold_spec": ("scaffold_spec", 0.0), # Scaffold Spec (SS): scaffold-aware self-speculative decoding (paper canonical). "inference_scaling": ("scaffold_spec_ss_multi_traj", 0.0), # SS + multi-trajectory rollouts (test-time inference scaling). } 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 # Frozen-by-release knobs (defaults from the paper). 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()