Files
allenai--olmocr/olmocr/train/compare_vllm_checkpoint.py
T
wehub-resource-sync 917eedffcf
Main / Python 3.11 - Docs (push) Has been cancelled
Main / Python 3.11 - Build (push) Has been cancelled
Main / Python 3.11 - Lint (push) Has been cancelled
Main / Python 3.11 - Style (push) Has been cancelled
Main / Python 3.11 - Test (push) Has been cancelled
Main / GPU CI (push) Has been cancelled
Main / Release (push) Has been cancelled
Main / Build and Push Docker Images (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:27:09 +08:00

380 lines
16 KiB
Python

#!/usr/bin/env python3
"""
Batch VLM inference comparison between vLLM and HuggingFace.
Processes prompts and images from WildVision-bench until finding significant mismatch.
"""
import argparse
import asyncio
import base64
import logging
import os
import random
import shutil
import tempfile
from io import BytesIO
from typing import Dict, List
import numpy as np
import PIL.Image
import torch
from huggingface_hub import snapshot_download
from transformers import AutoModelForVision2Seq, AutoProcessor
from vllm import LLM, SamplingParams
from olmocr.pipeline import build_page_query
from olmocr.s3_utils import download_directory
logger = logging.getLogger(__name__)
async def download_model(model_name_or_path: str, max_retries: int = 5):
for retry in range(max_retries):
try:
if model_name_or_path.startswith("s3://") or model_name_or_path.startswith("gs://") or model_name_or_path.startswith("weka://"):
logger.info(f"Downloading model directory from '{model_name_or_path}'")
model_cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "olmocr", "model")
# Delete existing model cache directory if it exists
if os.path.exists(model_cache_dir):
shutil.rmtree(model_cache_dir)
download_directory([model_name_or_path], model_cache_dir)
return model_cache_dir
elif os.path.isabs(model_name_or_path) and os.path.isdir(model_name_or_path):
logger.info(f"Using local model path at '{model_name_or_path}'")
return model_name_or_path
else:
logger.info(f"Downloading model with hugging face '{model_name_or_path}'")
snapshot_download(repo_id=model_name_or_path)
return model_name_or_path
except Exception:
if retry == max_retries - 1:
raise # Raise on final attempt and fail the job
logger.warning(f"Model download failed (attempt {retry + 1}/{max_retries}), retrying...")
await asyncio.sleep(2**retry) # Exponential backoff
def image_to_base64_data_url(image):
"""Convert PIL image to base64 data URL."""
buffered = BytesIO()
image.save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue()).decode()
return f"data:image/png;base64,{img_str}"
async def load_pdf_prompts(num_samples: int = 100, seed: int = 42, max_length: int = 2048) -> List[Dict[str, str]]:
"""Load prompts and images from olmOCR-mix-0225-benchmarkset dataset with fixed random seed."""
print(f"Loading olmOCR-mix-0225-benchmarkset dataset with {num_samples} samples and seed {seed}")
# Set random seed for reproducibility
random.seed(seed)
np.random.seed(seed)
# Import huggingface_hub utilities to list files
from huggingface_hub import hf_hub_download, list_repo_files
# List all PDF files in the repository
print("Listing PDF files in dataset...")
all_files = list_repo_files(repo_id="allenai/olmOCR-mix-0225-benchmarkset", repo_type="dataset")
# Filter for PDF files in the pdfs directory
pdf_files = [f for f in all_files if f.startswith("pdfs/") and f.endswith(".pdf")]
if not pdf_files:
raise ValueError("No PDF files found in the dataset")
print(f"Found {len(pdf_files)} PDF files in dataset")
# Randomly sample num_samples PDFs
if len(pdf_files) > num_samples:
sampled_pdf_files = random.sample(pdf_files, num_samples)
else:
sampled_pdf_files = pdf_files
print(f"Warning: Only {len(pdf_files)} PDFs available, less than requested {num_samples}")
print(f"Sampled {len(sampled_pdf_files)} PDFs to download")
# Download only the sampled PDFs and process them
queries = []
with tempfile.TemporaryDirectory() as temp_dir:
for pdf_file in sampled_pdf_files:
try:
# Download individual PDF file
print(f"Downloading {pdf_file}...")
local_pdf_path = hf_hub_download(repo_id="allenai/olmOCR-mix-0225-benchmarkset", filename=pdf_file, repo_type="dataset", local_dir=temp_dir)
# Build page query for page 1 of each PDF
query = await build_page_query(local_pdf_path=local_pdf_path, page=1, target_longest_image_dim=1280, image_rotation=0)
queries.append(query)
except Exception as e:
print(f"Error processing {os.path.basename(pdf_file)}: {e}")
continue
print(f"Successfully processed {len(queries)} PDFs")
return queries
def process_single_prompt(sample: Dict[str, any], llm, hf_model, processor, sampling_params, device, args):
"""Process a single prompt with image and return comparison results."""
# Track if we found the first mismatch for max_prob_first_diff
found_first_mismatch = False
max_prob_first_diff = 0.0
# Extract messages from the sample (which is the output of build_page_query)
messages = sample["messages"]
# Extract the text prompt and image from the messages
user_message = messages[0]
text_prompt = None
image_base64 = None
for content in user_message["content"]:
if content["type"] == "text":
text_prompt = content["text"]
elif content["type"] == "image_url":
image_url = content["image_url"]["url"]
# Extract base64 data after the comma
if "," in image_url:
image_base64 = image_url.split(",")[1]
else:
image_base64 = image_url
if text_prompt is None or image_base64 is None:
raise ValueError("Failed to extract text prompt or image from messages")
# Decode the base64 image to PIL Image
image_bytes = base64.b64decode(image_base64)
image = PIL.Image.open(BytesIO(image_bytes))
print(f"\n{'='*80}")
print(f"PROMPT: {text_prompt[:100]}..." if len(text_prompt) > 100 else f"PROMPT: {text_prompt}")
print(f"IMAGE: {image.size} {image.mode}")
# Generate with vLLM
print("\n=== vLLM Generation ===")
# For VLLM, use the messages just as comes out of build_page_query
outputs = llm.chat(messages, sampling_params)
output = outputs[0]
# Extract prompt and generated token IDs
prompt_token_ids = output.prompt_token_ids
generated_token_ids = output.outputs[0].token_ids
print(
f"Prompt tokens ({len(prompt_token_ids)}): {prompt_token_ids[:10]}..."
if len(prompt_token_ids) > 10
else f"Prompt tokens ({len(prompt_token_ids)}): {prompt_token_ids}"
)
print(f"Generated tokens ({len(generated_token_ids)}): {generated_token_ids}")
print(f"Generated text: {processor.decode(generated_token_ids, skip_special_tokens=True)}")
# Create input tensor from concatenated token IDs
# input_ids = torch.tensor([all_token_ids], device=device) # Not needed for HF VLM models
# HuggingFace forward pass
print("\n=== HuggingFace Forward Pass ===")
# Prepare inputs for HF model using the extracted image and text
conversation = [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": text_prompt}]}]
hf_text_prompt = processor.apply_chat_template(conversation, add_generation_prompt=True, tokenize=False)
inputs = processor(text=[hf_text_prompt], images=[image], return_tensors="pt").to(device)
print("INPUTS", inputs)
# Concatenate the generated tokens to the input_ids
generated_ids_tensor = torch.tensor([generated_token_ids], device=device)
inputs["input_ids"] = torch.cat([inputs["input_ids"], generated_ids_tensor], dim=1)
inputs["attention_mask"] = torch.ones_like(inputs["input_ids"])
with torch.no_grad():
outputs_hf = hf_model(**inputs)
logits = outputs_hf.logits[0] # [seq_len, vocab_size]
# Token-by-token comparison
print(f"\n{'Pos':>4} {'Token ID':>8} {'Token':>20} {'Type':>8} {'vLLM Prob':>12} {'HF Argmax':>10} {'HF Prob':>12} {'Match':>6} {'HF Token':>20}")
print("-" * 125)
# Get vLLM logprobs for generated tokens
vllm_logprobs = output.outputs[0].logprobs
# Track mismatch info
first_mismatch_idx = None
# Get all token IDs from the HF model's input
all_token_ids = inputs["input_ids"][0].tolist()
# Compare ALL tokens (prompt + generated)
for pos, token_id in enumerate(all_token_ids):
token_str = processor.decode([token_id], skip_special_tokens=False).replace("\n", "\\n").replace("\r", "\\r")
# Determine if this is a prompt or generated token
is_prompt = pos < len(prompt_token_ids)
token_type = "prompt" if is_prompt else "gen"
# vLLM probability (only for generated tokens)
vllm_prob_str = "N/A"
vllm_prob = None
if not is_prompt:
gen_idx = pos - len(prompt_token_ids)
if vllm_logprobs and gen_idx < len(vllm_logprobs):
# vLLM logprobs is a list of dicts mapping token_id to logprob
token_logprobs = vllm_logprobs[gen_idx]
if token_logprobs and token_id in token_logprobs:
# Convert logprob to probability
vllm_prob = torch.exp(torch.tensor(token_logprobs[token_id].logprob)).item()
vllm_prob_str = f"{vllm_prob:12.6f}"
# HF prediction - only for generated tokens (skip prompt tokens entirely)
if pos > 0 and not is_prompt:
hf_logits_at_pos = logits[pos - 1]
hf_probs = torch.softmax(hf_logits_at_pos, dim=-1)
hf_argmax = torch.argmax(hf_logits_at_pos).item()
hf_prob = hf_probs[token_id].item()
# Check if predictions match
match = "✓" if token_id == hf_argmax else "✗"
# Track first mismatch and probability difference
if token_id != hf_argmax:
if first_mismatch_idx is None:
first_mismatch_idx = pos - len(prompt_token_ids)
# Calculate probability difference only for the first mismatch
if vllm_prob is not None and not found_first_mismatch:
max_prob_first_diff = abs(vllm_prob - hf_prob)
found_first_mismatch = True
# Decode HF argmax token (only show if mismatch)
hf_token_str = ""
if token_id != hf_argmax:
hf_token_str = processor.decode([hf_argmax], skip_special_tokens=False).replace("\n", "\\n").replace("\r", "\\r")
print(f"{pos:>4} {token_id:>8} {token_str:>20} {token_type:>8} {vllm_prob_str:>12} {hf_argmax:>10} {hf_prob:>12.6f} {match:>6} {hf_token_str:>20}")
else:
# Prompt tokens or first token - no HF comparison
print(f"{pos:>4} {token_id:>8} {token_str:>20} {token_type:>8} {vllm_prob_str:>12} {'':>10} {'':>12} {'':>6} {'':<20}")
# Summary
print(f"\n=== Summary ===")
print(f"Total tokens generated: {len(generated_token_ids)}")
# Calculate match rate
matches = 0
for i, token_id in enumerate(generated_token_ids):
pos = len(prompt_token_ids) + i
hf_logits_at_pos = logits[pos - 1]
hf_argmax = torch.argmax(hf_logits_at_pos).item()
if token_id == hf_argmax:
matches += 1
match_rate = matches / len(generated_token_ids) * 100 if generated_token_ids else 0
print(f"Token match rate: {matches}/{len(generated_token_ids)} ({match_rate:.1f}%)")
# Report first mismatch index
if first_mismatch_idx is not None:
print(f"First mismatch at generation index: {first_mismatch_idx}")
print(f"First mismatch probability difference: {max_prob_first_diff:.6f}")
else:
print("No mismatches found in generated tokens")
return {
"first_mismatch_idx": first_mismatch_idx,
"max_prob_first_diff": max_prob_first_diff,
"match_rate": match_rate,
"num_generated": len(generated_token_ids),
}
async def async_main():
parser = argparse.ArgumentParser(description="Batch compare VLM inference between vLLM and HuggingFace")
parser.add_argument("--model", type=str, default="Qwen/Qwen2.5-VL-7B-Instruct", help="Model name or path")
parser.add_argument("--max-tokens", type=int, default=20, help="Maximum tokens to generate per prompt")
parser.add_argument("--temperature", type=float, default=0.0, help="Sampling temperature")
parser.add_argument("--num-prompts", type=int, default=100, help="Number of prompts to load from WildVision")
parser.add_argument("--prob-threshold", type=float, default=0.20, help="Probability difference threshold to stop")
parser.add_argument("--seed", type=int, default=42, help="Random seed for prompt selection")
args = parser.parse_args()
print(f"Model: {args.model}")
print(f"Max tokens: {args.max_tokens}")
print(f"Temperature: {args.temperature}")
print(f"Probability threshold: {args.prob_threshold}")
print(f"Loading {args.num_prompts} samples from olmOCR-mix-0225-benchmarkset\n")
# Download the model before loading prompts
model_path = await download_model(args.model)
# Load prompts and images
samples = await load_pdf_prompts(num_samples=args.num_prompts, seed=args.seed)
# Load HuggingFace model and processor first
print("\n=== Loading HuggingFace Model ===")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
processor_hf = AutoProcessor.from_pretrained(model_path, trust_remote_code=True)
hf_model = AutoModelForVision2Seq.from_pretrained(model_path, trust_remote_code=True, torch_dtype=torch.float16, device_map="auto")
hf_model.eval()
# Create vLLM engine once
print("\n=== Creating vLLM Engine ===")
llm = LLM(model=model_path, trust_remote_code=True, gpu_memory_utilization=0.5)
sampling_params = SamplingParams(temperature=args.temperature, max_tokens=args.max_tokens, logprobs=1) # Get top-1 logprobs
# Process samples until finding significant mismatch
print("\n=== Processing Samples ===")
# Initialize statistics tracking
all_results = []
for i, sample in enumerate(samples):
print(f"\n\n{'#'*80}")
print(f"### Processing sample {i+1}/{len(samples)}")
print(f"{'#'*80}")
# Process single sample
result = process_single_prompt(sample, llm, hf_model, processor_hf, sampling_params, device, args)
all_results.append(result)
# Check if we found significant mismatch
if result["first_mismatch_idx"] is not None and result["max_prob_first_diff"] > args.prob_threshold:
print(f"\n\n{'*'*80}")
print(f"*** FOUND SIGNIFICANT MISMATCH ***")
print(f"*** First mismatch probability difference: {result['max_prob_first_diff']:.6f} > {args.prob_threshold} ***")
print(f"*** Stopping after sample {i+1}/{len(samples)} ***")
print(f"{'*'*80}")
# Report aggregated statistics
print(f"\n\n{'='*80}")
print("=== AGGREGATED STATISTICS ===")
print(f"{'='*80}")
total_samples = len(all_results)
samples_with_mismatches = sum(1 for r in all_results if r["first_mismatch_idx"] is not None)
total_tokens_generated = sum(r["num_generated"] for r in all_results)
print(f"Total samples processed: {total_samples}")
print(f"Samples with mismatches: {samples_with_mismatches} ({samples_with_mismatches/total_samples*100:.1f}%)")
print(f"Total tokens generated: {total_tokens_generated}")
if samples_with_mismatches > 0:
avg_match_rate = sum(r["match_rate"] for r in all_results) / total_samples
max_prob_diffs = [r["max_prob_first_diff"] for r in all_results if r["first_mismatch_idx"] is not None]
avg_prob_diff = sum(max_prob_diffs) / len(max_prob_diffs)
max_prob_diff_overall = max(max_prob_diffs)
first_mismatch_positions = [r["first_mismatch_idx"] for r in all_results if r["first_mismatch_idx"] is not None]
avg_first_mismatch_pos = sum(first_mismatch_positions) / len(first_mismatch_positions)
print(f"\nMismatch Statistics:")
print(f" Average token match rate: {avg_match_rate:.1f}%")
print(f" Average first mismatch position: {avg_first_mismatch_pos:.1f}")
print(f" Average first mismatch prob diff: {avg_prob_diff:.6f}")
print(f" Max first mismatch prob diff: {max_prob_diff_overall:.6f}")
else:
print("\nNo mismatches found in any samples!")
print(f"\n{'='*80}")
def main():
asyncio.run(async_main())
if __name__ == "__main__":
main()