# Reproducing Paper Results > **Paper**: *PixelRAG: Retrieval and Generation in Pixel Space over Millions of Web Screenshots* > > This document maps every table and figure in the paper to the exact commands needed to reproduce the numbers. ## Prerequisites ### Infrastructure | Component | Description | Where | |-----------|-------------|-------| | **Wikipedia tile index (base)** | 28M vectors, Qwen3-VL-Embedding-2B (pretrained) | `pixelrag-data/search_index/` (215 GB FAISS IVF, dim=2048) | | **Wikipedia tile index (fine-tuned)** | 26M vectors, LoRA checkpoint-200 | `pixelrag-data/search_index_lora_vit_ckpt200_v2/` (202 GB) | | **Wikipedia text index** | 15.7M text chunks (1024 tokens, Trafilatura) | `pixelrag-data/text_search_index_1024/` (121 GB) | | **Article metadata** | URL↔tile mapping for 7.1M articles | `pixelrag-data/articles.json` (199 MB) | | **Tile images** | ~30M PNG tiles (1024×1024) | Remote NFS or local SSD (~5.6 TB) | | **News tile index** | 3.6M tiles (BBC/AP/CNN) for LiveVQA | S3: `s3://wiki-screenshot-tiles-backup/kiwix_tiles/news_image_search_index/` | | **News text index** | 866K text chunks for news | S3: `s3://wiki-screenshot-tiles-backup/kiwix_tiles/news_text_search_index/` | | **News tiles** | Raw PNG tiles for news articles | S3: `s3://wiki-screenshot-tiles-backup/kiwix_tiles/news_tiles/` | | **LoRA adapter** | Fine-tuned embedding LoRA weights | S3: `s3://wiki-screenshot-tiles-backup/kiwix_tiles/adapters/lora_vit_ckpt200/` | | **Kiwix ZIM** | Offline Wikipedia for HTML baselines | S3: `s3://wiki-screenshot-tiles-backup/kiwix_tiles/zim/` | All S3 paths use AWS profile `leann` (`aws s3 --profile leann ...`). ### Services to Start ```bash # 1. Screenshot search API (port 30888) — serves the pixel tile index pixelrag-serve \ --index-dir pixelrag-data/search_index \ # or search_index_lora_vit_ckpt200_v2 --tiles-dir /path/to/wikipedia_tiles \ --articles-json pixelrag-data/articles.json \ --model Qwen/Qwen3-VL-Embedding-2B \ --device cuda --port 30888 # 2. Text search API (port 30889) — serves the text chunk index pixelrag-serve \ --index-dir pixelrag-data/text_search_index_1024 \ --tiles-dir /path/to/text_chunks \ --articles-json pixelrag-data/articles.json \ --model Qwen/Qwen3-VL-Embedding-2B \ --device cuda --port 30889 # 3. Reader model (port 8000) — vLLM serving Qwen3.5-4B (default reader) vllm serve Qwen/Qwen3.5-4B-Instruct \ --port 8000 --tensor-parallel-size 1 \ --max-model-len 32768 ``` ### Environment ```bash cd ~/pixelrag/eval # Install eval dependencies (one-time) uv pip install pandas tqdm trafilatura openai aiohttp datasets huggingface-hub # For grading export OPENAI_API_KEY=sk-... # GPT-4.1 judge ``` --- ## Table 1: Main Results (6 Benchmarks × 4 Methods) **Reader**: Qwen3.5-4B, **k=3**, **Grader**: GPT-4.1 judge (except LiveVQA = exact match) ### No Retrieval (baseline) ```bash # SimpleQA — no retrieval python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --num-examples 1000 --no-think # NQ — no retrieval python run_bench.py \ --task nq --model Qwen/Qwen3.5-4B-Instruct \ --num-examples 1000 --no-think # NQ-Tables — no retrieval python run_bench.py \ --task nq_tables --model Qwen/Qwen3.5-4B-Instruct \ --num-examples 1000 --no-think # MMSearch — no retrieval (300 examples) python run_bench.py \ --task mmsearch --model Qwen/Qwen3.5-4B-Instruct \ --num-examples 300 --no-think # EVQA — no retrieval (landmarks, automatic only, n=749) python run_bench.py \ --task encyclopedic_vqa --model Qwen/Qwen3.5-4B-Instruct \ --evqa-dataset-filter landmarks --evqa-question-type-filter automatic \ --num-examples 749 --no-think # LiveVQA — see "LiveVQA Separate Pipeline" section below ``` ### Text Retrieval — Trafilatura (Text → Text) Requires: text search API on port 30889 with Trafilatura-parsed text chunks. ```bash # SimpleQA — Trafilatura text retrieval python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --text-api --text-api-url http://localhost:30889/search \ --retrieval-top-k 3 --num-examples 1000 --no-think # NQ — Trafilatura text retrieval python run_bench.py \ --task nq --model Qwen/Qwen3.5-4B-Instruct \ --text-api --text-api-url http://localhost:30889/search \ --retrieval-top-k 3 --num-examples 1000 --no-think # NQ-Tables python run_bench.py \ --task nq_tables --model Qwen/Qwen3.5-4B-Instruct \ --text-api --text-api-url http://localhost:30889/search \ --retrieval-top-k 3 --num-examples 1000 --no-think # MMSearch (multimodal query: text + image → text index) python run_bench.py \ --task mmsearch --model Qwen/Qwen3.5-4B-Instruct \ --text-api --text-api-url http://localhost:30889/search \ --retrieval-top-k 3 --num-examples 300 --no-think # EVQA python run_bench.py \ --task encyclopedic_vqa --model Qwen/Qwen3.5-4B-Instruct \ --text-api --text-api-url http://localhost:30889/search \ --evqa-dataset-filter landmarks --evqa-question-type-filter automatic \ --retrieval-top-k 3 --num-examples 749 --no-think # LiveVQA — see "LiveVQA Separate Pipeline" section below ``` ### Text Retrieval — mwparserfromhell Same as Trafilatura but requires a separate text index built with mwparserfromhell parser. The text API must be started pointing to that index. ```bash # Same commands as Trafilatura above, but --text-api-url points to # the mwparserfromhell text index API (different port or index-dir). # The parser choice is baked into the index at build time, not a runtime flag. ``` ### PixelRAG (base) — Screenshot → Screenshot Requires: screenshot search API on port 30888 with base (pretrained) embedding index. ```bash # SimpleQA — pixel retrieval (base) python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --local-api --local-api-url http://localhost:30888/search \ --retrieval-top-k 3 --num-examples 1000 --no-think # NQ — pixel retrieval (base) python run_bench.py \ --task nq --model Qwen/Qwen3.5-4B-Instruct \ --local-api --local-api-url http://localhost:30888/search \ --retrieval-top-k 3 --num-examples 1000 --no-think # NQ-Tables python run_bench.py \ --task nq_tables --model Qwen/Qwen3.5-4B-Instruct \ --local-api --local-api-url http://localhost:30888/search \ --retrieval-top-k 3 --num-examples 1000 --no-think # MMSearch (multimodal: query image sent alongside text) python run_bench.py \ --task mmsearch --model Qwen/Qwen3.5-4B-Instruct \ --local-api --local-api-url http://localhost:30888/search \ --retrieval-top-k 3 --num-examples 300 --no-think # EVQA (multimodal: landmark photo + question text) python run_bench.py \ --task encyclopedic_vqa --model Qwen/Qwen3.5-4B-Instruct \ --local-api --local-api-url http://localhost:30888/search \ --evqa-dataset-filter landmarks --evqa-question-type-filter automatic \ --retrieval-top-k 3 --num-examples 749 --no-think # LiveVQA — see "LiveVQA Separate Pipeline" section below ``` ### PixelRAG (fine-tuned) — Screenshot → Screenshot with LoRA embedding Same commands as PixelRAG (base), but the search API must be started with the fine-tuned index: ```bash # Start search API with fine-tuned index pixelrag-serve \ --index-dir pixelrag-data/search_index_lora_vit_ckpt200_v2 \ --tiles-dir /path/to/wikipedia_tiles \ --articles-json pixelrag-data/articles.json \ --model Qwen/Qwen3-VL-Embedding-2B \ --peft-adapter /path/to/lora_checkpoint_200 \ --device cuda --port 30888 ``` Then run the same `--local-api` commands above. ### Grading ```bash cd ~/pixelrag/eval # Grade with GPT-4.1 judge (Wikipedia QA tasks) python grade.py simpleqa eval_output/simpleqa_*.jsonl python grade.py encyclopedic_vqa eval_output/encyclopedic_vqa_*.jsonl python grade.py mmsearch eval_output/mmsearch_*.jsonl # For NQ/NQ-Tables (with LLM judge for paper numbers) python grade.py nq eval_output/nq_*.jsonl --llm-judge python grade.py nq_tables eval_output/nq_tables_*.jsonl --llm-judge # For LiveVQA (exact letter match — handled by the LiveVQA pipeline scripts) ``` --- ## Table 3: Retrieval–Reader Modality Ablation **Task**: SimpleQA (1000) + LiveVQA (6632), **Reader**: Qwen3.5-4B, **k=3**, **Embedding**: Qwen3-VL-Embedding-2B (base, no LoRA) | Row | Retrieval | Reader Input | Flags | |-----|-----------|-------------|-------| | Screenshot → Screenshot | Pixel index | Raw tile images | `--local-api` | | Screenshot → OCR text | Pixel index | OCR'd text from tiles | `--local-api --read-as-text-ocr` | | Text → Rendered image | Text index | Text chunks rendered as PNG | `--text-api --render-as-image` | | Text → Text | Text index | Raw text chunks | `--text-api` | | Text → HTML | Text index | Raw HTML from kiwix | `--text-api --html-dom-lookup` | ```bash # Screenshot → Screenshot (same as main results PixelRAG base) python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --local-api --local-api-url http://localhost:30888/search \ --retrieval-top-k 3 --num-examples 1000 --no-think # Screenshot → OCR text python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --local-api --local-api-url http://localhost:30888/search \ --read-as-text-ocr --ocr-url http://localhost:8202/v1 \ --retrieval-top-k 3 --num-examples 1000 --no-think # Text → Rendered image python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --text-api --text-api-url http://localhost:30889/search \ --render-as-image \ --retrieval-top-k 3 --num-examples 1000 --no-think # Text → Text (same as main results Trafilatura) python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --text-api --text-api-url http://localhost:30889/search \ --retrieval-top-k 3 --num-examples 1000 --no-think # Text → HTML (DOM lookup) python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --text-api --text-api-url http://localhost:30889/search \ --html-dom-lookup \ --retrieval-top-k 3 --num-examples 1000 --no-think ``` For LiveVQA, use the separate pipeline (see "LiveVQA Separate Pipeline" section) with the corresponding ablation scripts. --- ## Table 4: Embedding Training Recipe Ablation **Evaluated on mini-datastore** (400 queries, 7426 tiles). This ablation uses `--prebuilt-tiles-dir` pointing to the pre-built mini-datastore, with different embedding checkpoints. Each row corresponds to a different embedding training recipe: ```bash # Base model (no fine-tuning) python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --use-tiled-retrieval --use-qwen3vl-embedding \ --qwen3vl-model Qwen/Qwen3-VL-Embedding-2B \ --embedding-backend hf \ --prebuilt-tiles-dir tiles-hard-mini/ \ --retrieval-top-k 3 --num-examples 400 --no-think # With LoRA checkpoint (dynamic hard negatives + ViT unfrozen) python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --use-tiled-retrieval --use-qwen3vl-embedding \ --qwen3vl-model Qwen/Qwen3-VL-Embedding-2B \ --embedding-backend biqwen3 \ --peft-adapter /path/to/checkpoint-200 \ --prebuilt-tiles-dir tiles-hard-mini/ \ --retrieval-top-k 3 --num-examples 400 --no-think ``` The intermediate checkpoints (in-batch negatives, naive hard negatives, dynamic hard negatives frozen) each have their own PEFT adapter path. --- ## Figure 2: Token Efficiency (SimpleQA, k=1,2,3, 4 readers) **Task**: SimpleQA (1000), **Readers**: Qwen3.5-4B, Qwen3.5-9B, Qwen3.5-27B, Qwen3.6-35B-A3B For each reader × k × retrieval method, run: ```bash # Example: Qwen3.5-4B, k=1, PixelRAG (fine-tuned) python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --local-api --local-api-url http://localhost:30888/search \ --retrieval-top-k 3 --reader-top-k 1 \ --num-examples 1000 --no-think # Example: Qwen3.5-4B, k=2, PixelRAG (fine-tuned) python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --local-api --local-api-url http://localhost:30888/search \ --retrieval-top-k 3 --reader-top-k 2 \ --num-examples 1000 --no-think # Example: Qwen3.5-4B, k=3, PixelRAG (fine-tuned) python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --local-api --local-api-url http://localhost:30888/search \ --retrieval-top-k 3 \ --num-examples 1000 --no-think ``` > **Optimization**: Use `--retrieval-top-k 3 --reader-top-k N` to retrieve once at k=3 and evaluate at k=1,2,3 from the same JSONL (the full retrieved set is stored in `retrieved_images`). For each reader, change `--model` and start the appropriate vLLM server. Repeat for text retrieval (Trafilatura: `--text-api`) and PixelRAG base (base index). The plot script is at `arxiv/figures/plot_token_efficiency.py`. --- ## Figure 3: Agentic Multi-Hop QA (MoNaCo) **Task**: MoNaCo (1315 questions), **Agent**: GPT-5 ReAct, **k=5 per search** Uses `eval/run_monaco.py` — a ReAct agent that issues search tool calls. ```bash cd ~/pixelrag/eval # PixelRAG backend python run_monaco.py \ --reader gpt-5 \ --retrieval pixel \ --pixel-api http://localhost:30888/search \ --default-top-k 5 # Text retrieval backend (Trafilatura) python run_monaco.py \ --reader gpt-5 \ --retrieval text \ --text-api http://localhost:30889/search \ --default-top-k 5 # Grade (token F1 computed inline; add --judge for LLM judge F1) python run_monaco.py \ --reader gpt-5 \ --retrieval pixel \ --judge --judge-model gpt-4.1-2025-04-14 # Or grade existing predictions: python grade.py monaco eval_output/monaco/ ``` The dataset (`monaco_version_1_release.jsonl`) should be placed at `eval/data/monaco/` or passed via `--data-path`. --- ## Figure 4: Image Compression Curve **Task**: SimpleQA (1000), **Reader**: Qwen3.5-4B (base + SFT), k=1..5, compression c=1×/2×/3× ```bash # No compression (c=1×), k=3 python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --local-api --local-api-url http://localhost:30888/search \ --retrieval-top-k 5 --reader-top-k 3 \ --num-examples 1000 --no-think # 2× compression, k=3 python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --local-api --local-api-url http://localhost:30888/search \ --retrieval-top-k 5 --reader-top-k 3 \ --pixel-compress-ratio 2.0 \ --num-examples 1000 --no-think # 3× compression, k=3 python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --local-api --local-api-url http://localhost:30888/search \ --retrieval-top-k 5 --reader-top-k 3 \ --pixel-compress-ratio 3.0 \ --num-examples 1000 --no-think ``` For the SFT reader, replace `--model` with the SFT checkpoint path and serve it via vLLM. The plot script is at `arxiv/figures/plot_sft_compression_curve.py`. --- ## Table 8: Full Reader-Model Sweep (31 VLMs) **Task**: SimpleQA (1000), **k=3**, pixel retrieval (base) vs text retrieval (Trafilatura) For each of the 31 reader models, run two jobs: ```bash # Pixel retrieval python run_bench.py \ --task simpleqa --model \ --local-api --local-api-url http://localhost:30888/search \ --retrieval-top-k 3 --num-examples 1000 --no-think # Text retrieval python run_bench.py \ --task simpleqa --model \ --text-api --text-api-url http://localhost:30889/search \ --retrieval-top-k 3 --num-examples 1000 --no-think ``` where `` is one of: - `liuhaotian/llava-v1.5-7b` - `meta-llama/Llama-3.2-11B-Vision-Instruct` (k=1 for pixel due to architecture limit) - `meta-llama/Llama-3.2-90B-Vision-Instruct` (k=1 for pixel) - `meta-llama/Llama-4-Scout-17B-16E-Instruct` - `meta-llama/Llama-4-Maverick-17B-128E-Instruct` - `Qwen/Qwen2-VL-2B-Instruct` through `Qwen/Qwen2-VL-72B-Instruct` - `Qwen/Qwen2.5-VL-3B-Instruct` through `Qwen/Qwen2.5-VL-72B-Instruct` - `Qwen/Qwen3-VL-2B` through `Qwen/Qwen3-VL-235B-A22B` - `Qwen/Qwen3.5-0.8B` through `Qwen/Qwen3.5-35B-A3B` - `Qwen/Qwen3.6-27B`, `Qwen/Qwen3.6-35B-A3B` For reasoning-mode models, omit `--no-think`. Each model requires its own vLLM instance (or OpenRouter/Commonstack for API models). --- ## LiveVQA (Table 1 + Table 3) LiveVQA uses `eval/run_livevqa.py` — a dedicated script for the news corpus. **Requires**: News pixel search API (port 30890), news text search API (port 30892), LiveVQA v4 JSON dataset, vLLM reader. ```bash cd ~/pixelrag/eval # No retrieval python run_livevqa.py --mode naive \ --model Qwen/Qwen3.5-4B-Instruct \ --output eval_output/livevqa_naive.jsonl # PixelRAG (screenshot → screenshot) python run_livevqa.py --mode pixel \ --pixel-api http://localhost:30890/search \ --model Qwen/Qwen3.5-4B-Instruct \ --output eval_output/livevqa_pixel.jsonl # Text retrieval (Trafilatura) python run_livevqa.py --mode text \ --text-api http://localhost:30892/search \ --model Qwen/Qwen3.5-4B-Instruct \ --output eval_output/livevqa_text.jsonl # Hybrid (pixel + text) python run_livevqa.py --mode hybrid \ --pixel-api http://localhost:30890/search \ --text-api http://localhost:30892/search \ --model Qwen/Qwen3.5-4B-Instruct \ --output eval_output/livevqa_hybrid.jsonl ``` Grading is automatic (5-option MC exact letter match) — printed at the end of each run. --- ## Known Issues (Blockers for Reproduction) ### ~~0. Missing simpleqa modules~~ (FIXED) `screenshot.py` and `pixel_query.py` have been copied into `eval/lib/`. Selenium import is deferred so it doesn't block `--local-api` users. ### ~~1. `dr_agent` not importable~~ (FIXED) Dataset loaders extracted into `eval/lib/benchmarks.py`. The `run_bench.py` import now reads from `simpleqa.datasets_loader` instead of `dr_agent`. ### ~~2. Grading script not in this repo~~ (FIXED) `eval/grade.py` implements GPT-4.1 3-way grading (CORRECT/INCORRECT/NOT_ATTEMPTED) using the same prompt template as the paper. No dependency on the old repo's evaluation framework. For the legacy full evaluation framework (per-example HTML reports, etc.), the original is still at `~/pixelrag-src/Vis-RAG/agent/scripts/evaluate.py`. ### 3. Hardcoded paths in retrieval.py `eval/lib/retrieval.py` lines 84–88 have placeholder paths (`/path/to/project`, `/path/to/data`) for the local kiwix tile store. These are only used by `LocalWikiTiledScreenshotRetriever` (ground-truth screenshot mode), not by the production `--local-api` mode. ### ~~4. LiveVQA uses separate pipeline~~ (FIXED) `eval/run_livevqa.py` handles all LiveVQA modes (naive, pixel, text, hybrid). ### ~~5. MoNaCo runs from old repo~~ (FIXED) `eval/run_monaco.py` implements the full ReAct agent loop with pixel/text retrieval backends. ### 6. mwparserfromhell text index The paper's second text baseline uses mwparserfromhell parser. The text index must be built separately with this parser — the parser choice is embedded at index build time, not at query time. The build pipeline for this variant needs to be documented. ### 7. News corpus indexes LiveVQA requires separate tile and text indexes built over the news corpus (BBC/AP/CNN). These indexes are on a different machine/path and need their own `pixelrag-serve` instances. --- ## Grading Protocol Summary | Benchmark | Metric | Grader | |-----------|--------|--------| | SimpleQA | CORRECT/INCORRECT/NOT_ATTEMPTED → accuracy | GPT-4.1 (temp=0, seed=42) | | NQ | Same 3-way judge | GPT-4.1 (temp=0, seed=42) | | NQ-Tables | Same 3-way judge (up to 10 gold aliases joined with OR) | GPT-4.1 | | MMSearch | Same 3-way judge | GPT-4.1 | | EVQA | Same 3-way judge (reference_list → "Any of: ref1 \| ref2") | GPT-4.1 | | LiveVQA | 5-option multiple-choice exact letter match | No LLM | | MoNaCo | Token-level F1 (primary), LLM judge F1 (secondary) | GPT-4.1 | --- ## Quick Smoke Test (Verify Pipeline Works) Run a single example end-to-end before committing to full runs: ```bash # 1. Verify search API is responding curl -s http://localhost:30888/status | python -m json.tool # 2. Run 5 examples, no retrieval python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --num-examples 5 --no-think --force # 3. Run 5 examples, pixel retrieval python run_bench.py \ --task simpleqa --model Qwen/Qwen3.5-4B-Instruct \ --local-api --local-api-url http://localhost:30888/search \ --retrieval-top-k 3 --num-examples 5 --no-think --force # 4. Grade cd ~/pixelrag-src/Vis-RAG/agent python scripts/evaluate.py simpleqa ~/pixelrag/eval/eval_output/.jsonl ``` --- ## Output File Convention All outputs go to `eval_output/` with auto-generated filenames: ``` eval_output/{task}_{mode}_{model_safe}_{n}.jsonl ``` Examples: - `eval_output/simpleqa_naive_qwen_qwen3.5_4b_instruct_1000.jsonl` - `eval_output/simpleqa_local_api_qwen_qwen3.5_4b_instruct_1000.jsonl` - `eval_output/nq_text_api_qwen_qwen3.5_4b_instruct_1000.jsonl` Grading results are saved alongside as `*_eval_results.json`.