lesterpjy/tts-small-vlm

Test-time scaling for small VLMs on multilingual visual MCQ

Python

1

8 commits

updated Sep 15, 2026

See the code

README

TTS for Small VLMs on Multilingual Visual MCQ

Code for the paper Test-Time Scaling for Small VLMs on Multilingual Visual MCQ.

πŸ† 1st place at the ImageCLEF 2026 Multimodal Reasoning shared task (Visual MCQ track).

We study test-time scaling (TTS) strategies for open-weight vision-language models (VLMs) on the EXAMS-V benchmark under a single-GPU, 7B-parameter budget.

Key Results

MethodPolicyAccuracyScale
Zero-shotQwen2.5-VL-7B56.8%val-full
Chain-of-thoughtQwen2.5-VL-7B63.1%val-full
Self-consistency N=8Qwen2.5-VL-7B66.4%val-full
Self-consistency N=8, 1k-tokQwen3.5-4B77.8%val-full
Self-consistency N=8, 2k-tokQwen3.5-4B81.5%val-full
Self-consistency N=16, 2k-tokQwen3.5-4B81.6%val-full
Best (N=16, 2k + guided repair)Qwen3.5-4B84.1%test

Three main findings:

  1. The dominant scaling axis is per-chain token budget, not chain count: doubling the budget recovers +3.7 pp by eliminating truncation; doubling chains adds only +0.15 pp.
  2. PRM-guided beam search underperforms flat self-consistency (-0.39 pp), with search collapsing to unanimous beams on 72% of questions.
  3. Neither a generative critic nor a trained PRM beats majority vote, replicated across two policies.

Repository Structure

tts-small-vlm/
β”œβ”€β”€ src/                    # Core pipeline modules
β”‚   β”œβ”€β”€ backend_vllm.py     # vLLM inference backend
β”‚   β”œβ”€β”€ backend.py          # HF Transformers backend (PRM model loading)
β”‚   β”œβ”€β”€ describe.py         # Stage 1: N-sample image description
β”‚   β”œβ”€β”€ reason.py           # Stage 2: M-sample text-only reasoning
β”‚   β”œβ”€β”€ verify.py           # Stage 3: majority vote, generative critic, PRM
β”‚   β”œβ”€β”€ search.py           # PRM-BAS beam-annealing search
β”‚   β”œβ”€β”€ pipeline.py         # Pipeline orchestrator
β”‚   └── utils/              # Data classes, answer extraction, logging, subsets
β”œβ”€β”€ scripts/
β”‚   β”œβ”€β”€ experiment.py       # Main experiment runner
β”‚   β”œβ”€β”€ analyze.py          # Post-hoc analysis (stratified metrics, scaling curves)
β”‚   β”œβ”€β”€ score_with_critic.py    # Generative critic rescoring (RQ4)
β”‚   β”œβ”€β”€ score_with_prm.py      # Qwen-VL-PRM rescoring (RQ4)
β”‚   β”œβ”€β”€ rescore_dtr_with_prm.py # DTR + PRM rescoring (Appendix)
β”‚   β”œβ”€β”€ repair_parse_failures.py # Guided parse repair
β”‚   β”œβ”€β”€ format_submission.py    # ImageCLEF competition submission formatting
β”‚   β”œβ”€β”€ analyze_parse_fail.py   # Parse failure diagnostics
β”‚   └── serve_vllm.sh          # vLLM server launcher
β”œβ”€β”€ configs/                # YAML configs, one per experiment
β”‚   β”œβ”€β”€ baselines/          # Zero-shot, CoT, SC (Q2.5 and Q3.5)
β”‚   β”œβ”€β”€ scaling/            # Token budget, chain count, temperature sweep
β”‚   β”œβ”€β”€ search/             # DTR, PRM-BAS, DTR+PRM
β”‚   └── test/               # Test-set submission config
β”œβ”€β”€ slurm/                  # SLURM job scripts (Snellius HPC)
β”œβ”€β”€ eval/                   # Stratified evaluation script
β”œβ”€β”€ paper/                  # Figure generation scripts
β”‚   └── figures/            # plot_figures.py, generated PDFs
β”œβ”€β”€ tests/                  # Unit tests (49 tests, fully mocked)
β”œβ”€β”€ notebook.ipynb          # Pipeline walkthrough with search and verification
β”œβ”€β”€ requirements.txt
β”œβ”€β”€ .env.example            # Environment variable template
└── LICENSE

Setup

Requirements: Python 3.12+, CUDA 12.x, a GPU with at least 40 GB VRAM (A40/A100).

# Clone and install
git clone <repo-url> && cd tts-small-vlm
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

# Configure environment (optional, for W&B tracking)
cp .env.example .env
# Edit .env with your API keys

# Run unit tests (no GPU required)
pytest tests/ -v

Models (downloaded automatically on first use):

Reproducing Paper Results

Each experiment is driven by a YAML config and a SLURM job script. All scripts assume execution from the repository root.

Running an experiment locally

# Start vLLM server (in a separate terminal)
bash scripts/serve_vllm.sh

# Run a single experiment
python scripts/experiment.py --config configs/scaling/q35_sc_n8_2k.yaml --verbose

Running on a SLURM cluster

Edit the --account field in the SLURM scripts, then:

sbatch slurm/baselines_q25.sbatch      # Table 1, Q2.5 rows
sbatch slurm/baselines_q35.sbatch      # Table 1, Q3.5 rows
sbatch slurm/scaling_q35.sbatch        # Table 2, scaling rows
sbatch slurm/temp_sweep_q35.sbatch     # Table 2, temperature sweep
sbatch slurm/search_q25_dev.sbatch     # Table 3, search comparison
sbatch slurm/prm_bas_q35_val.sbatch    # Section 5.2, PRM-BAS at val scale
sbatch slurm/selectors_q25.sbatch      # Table 4, Q2.5 selectors
sbatch slurm/selectors_q35.sbatch      # Table 4, Q3.5 selectors
sbatch slurm/dtr_prm_rescore_dev.sbatch # Appendix Table 10
sbatch slurm/test_submission.sbatch     # Best config + guided repair on test

Paper result to config mapping

Paper TableResultConfig
Tab 1Q2.5 Zero-shot (56.8%)baselines/q25_zero_shot.yaml
Tab 1Q2.5 CoT (63.1%)baselines/q25_cot.yaml
Tab 1Q2.5 SC-N=8 (66.4%)baselines/q25_sc_n8.yaml
Tab 1Q3.5 Zero-shot (57.1%)baselines/q35_zero_shot.yaml
Tab 1Q3.5 CoT (69.7%)baselines/q35_cot.yaml
Tab 1Q3.5 SC-N=8 1k (77.8%)scaling/q35_sc_n8_1k.yaml
Tab 1Q3.5 SC-N=8 2k (81.5%)scaling/q35_sc_n8_2k.yaml
Tab 1Q3.5 SC-N=16 2k (81.6%)scaling/q35_sc_n16_2k.yaml
Tab 2T=0.3 (80.8%)scaling/q35_sc_n8_2k_t03.yaml
Tab 2T=0.5 (81.0%)scaling/q35_sc_n8_2k_t05.yaml
Tab 2T=0.7 (81.5%)scaling/q35_sc_n8_2k.yaml
Tab 2T=0.9 (81.0%)scaling/q35_sc_n8_2k_t09.yaml
Tab 3SC-N=8 dev (65.5%)search/q25_sc_n8_dev.yaml
Tab 3DTR N=2,M=2 (55.0%)search/q25_dtr_n2m2.yaml
Tab 3DTR N=4,M=4 (61.5%)search/q25_dtr_n4m4.yaml
Tab 3PRM-BAS (60.5%)search/q25_prm_bas_dev.yaml
Tab 4R1/R2 selectorsPool from baselines/q25_sc_n8.yaml or scaling/q35_sc_n8_2k.yaml, rescored by score_with_critic.py / score_with_prm.py
App Tab 10DTR + PRM modesPool from search/q25_dtr_n4m4.yaml, rescored by rescore_dtr_with_prm.py
S-bestTest 84.1%test/q35_sc_n16_2k_test.yaml + repair_parse_failures.py

Post-hoc analysis and figures

# Stratified analysis of a completed run
python scripts/analyze.py --run-dir runs/<run_id>

# Generate paper figures (hardcoded data, no GPU needed)
python paper/figures/plot_figures.py

Notebook

notebook.ipynb walks through the full pipeline:

  1. Baselines (zero-shot, chain-of-thought, self-consistency)
  2. Search strategy (PRM-BAS beam-annealing search within a describe-then-reason scaffold)
  3. Verification strategies (training-free generative critic, Qwen-VL-PRM discriminative rescoring)
  4. Guided parse repair
  5. Stratified evaluation

GPU-dependent cells are guarded with a RUN_INFERENCE flag for readability without hardware.

Data

The EXAMS-V dataset is loaded automatically from HuggingFace. It contains approximately 25,000 real school exam questions across 13 languages and 20 subjects. Our experiments use the validation split (4,651 questions) and a 200-question stratified subset for ablations.

License

This project is licensed under the MIT License. See LICENSE for details.

mcq
test-time-scaling
vlm

lesterpjy/tts-small-vlm

Test-time scaling for small VLMs on multilingual visual MCQ

Python

1

8 commits

updated Sep 15, 2026

See the code

README

TTS for Small VLMs on Multilingual Visual MCQ

Code for the paper Test-Time Scaling for Small VLMs on Multilingual Visual MCQ.

πŸ† 1st place at the ImageCLEF 2026 Multimodal Reasoning shared task (Visual MCQ track).

We study test-time scaling (TTS) strategies for open-weight vision-language models (VLMs) on the EXAMS-V benchmark under a single-GPU, 7B-parameter budget.

Key Results

MethodPolicyAccuracyScale
Zero-shotQwen2.5-VL-7B56.8%val-full
Chain-of-thoughtQwen2.5-VL-7B63.1%val-full
Self-consistency N=8Qwen2.5-VL-7B66.4%val-full
Self-consistency N=8, 1k-tokQwen3.5-4B77.8%val-full
Self-consistency N=8, 2k-tokQwen3.5-4B81.5%val-full
Self-consistency N=16, 2k-tokQwen3.5-4B81.6%val-full
Best (N=16, 2k + guided repair)Qwen3.5-4B84.1%test

Three main findings:

  1. The dominant scaling axis is per-chain token budget, not chain count: doubling the budget recovers +3.7 pp by eliminating truncation; doubling chains adds only +0.15 pp.
  2. PRM-guided beam search underperforms flat self-consistency (-0.39 pp), with search collapsing to unanimous beams on 72% of questions.
  3. Neither a generative critic nor a trained PRM beats majority vote, replicated across two policies.

Repository Structure

tts-small-vlm/
β”œβ”€β”€ src/                    # Core pipeline modules
β”‚   β”œβ”€β”€ backend_vllm.py     # vLLM inference backend
β”‚   β”œβ”€β”€ backend.py          # HF Transformers backend (PRM model loading)
β”‚   β”œβ”€β”€ describe.py         # Stage 1: N-sample image description
β”‚   β”œβ”€β”€ reason.py           # Stage 2: M-sample text-only reasoning
β”‚   β”œβ”€β”€ verify.py           # Stage 3: majority vote, generative critic, PRM
β”‚   β”œβ”€β”€ search.py           # PRM-BAS beam-annealing search
β”‚   β”œβ”€β”€ pipeline.py         # Pipeline orchestrator
β”‚   └── utils/              # Data classes, answer extraction, logging, subsets
β”œβ”€β”€ scripts/
β”‚   β”œβ”€β”€ experiment.py       # Main experiment runner
β”‚   β”œβ”€β”€ analyze.py          # Post-hoc analysis (stratified metrics, scaling curves)
β”‚   β”œβ”€β”€ score_with_critic.py    # Generative critic rescoring (RQ4)
β”‚   β”œβ”€β”€ score_with_prm.py      # Qwen-VL-PRM rescoring (RQ4)
β”‚   β”œβ”€β”€ rescore_dtr_with_prm.py # DTR + PRM rescoring (Appendix)
β”‚   β”œβ”€β”€ repair_parse_failures.py # Guided parse repair
β”‚   β”œβ”€β”€ format_submission.py    # ImageCLEF competition submission formatting
β”‚   β”œβ”€β”€ analyze_parse_fail.py   # Parse failure diagnostics
β”‚   └── serve_vllm.sh          # vLLM server launcher
β”œβ”€β”€ configs/                # YAML configs, one per experiment
β”‚   β”œβ”€β”€ baselines/          # Zero-shot, CoT, SC (Q2.5 and Q3.5)
β”‚   β”œβ”€β”€ scaling/            # Token budget, chain count, temperature sweep
β”‚   β”œβ”€β”€ search/             # DTR, PRM-BAS, DTR+PRM
β”‚   └── test/               # Test-set submission config
β”œβ”€β”€ slurm/                  # SLURM job scripts (Snellius HPC)
β”œβ”€β”€ eval/                   # Stratified evaluation script
β”œβ”€β”€ paper/                  # Figure generation scripts
β”‚   └── figures/            # plot_figures.py, generated PDFs
β”œβ”€β”€ tests/                  # Unit tests (49 tests, fully mocked)
β”œβ”€β”€ notebook.ipynb          # Pipeline walkthrough with search and verification
β”œβ”€β”€ requirements.txt
β”œβ”€β”€ .env.example            # Environment variable template
└── LICENSE

Setup

Requirements: Python 3.12+, CUDA 12.x, a GPU with at least 40 GB VRAM (A40/A100).

# Clone and install
git clone <repo-url> && cd tts-small-vlm
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

# Configure environment (optional, for W&B tracking)
cp .env.example .env
# Edit .env with your API keys

# Run unit tests (no GPU required)
pytest tests/ -v

Models (downloaded automatically on first use):

Reproducing Paper Results

Each experiment is driven by a YAML config and a SLURM job script. All scripts assume execution from the repository root.

Running an experiment locally

# Start vLLM server (in a separate terminal)
bash scripts/serve_vllm.sh

# Run a single experiment
python scripts/experiment.py --config configs/scaling/q35_sc_n8_2k.yaml --verbose

Running on a SLURM cluster

Edit the --account field in the SLURM scripts, then:

sbatch slurm/baselines_q25.sbatch      # Table 1, Q2.5 rows
sbatch slurm/baselines_q35.sbatch      # Table 1, Q3.5 rows
sbatch slurm/scaling_q35.sbatch        # Table 2, scaling rows
sbatch slurm/temp_sweep_q35.sbatch     # Table 2, temperature sweep
sbatch slurm/search_q25_dev.sbatch     # Table 3, search comparison
sbatch slurm/prm_bas_q35_val.sbatch    # Section 5.2, PRM-BAS at val scale
sbatch slurm/selectors_q25.sbatch      # Table 4, Q2.5 selectors
sbatch slurm/selectors_q35.sbatch      # Table 4, Q3.5 selectors
sbatch slurm/dtr_prm_rescore_dev.sbatch # Appendix Table 10
sbatch slurm/test_submission.sbatch     # Best config + guided repair on test

Paper result to config mapping

Paper TableResultConfig
Tab 1Q2.5 Zero-shot (56.8%)baselines/q25_zero_shot.yaml
Tab 1Q2.5 CoT (63.1%)baselines/q25_cot.yaml
Tab 1Q2.5 SC-N=8 (66.4%)baselines/q25_sc_n8.yaml
Tab 1Q3.5 Zero-shot (57.1%)baselines/q35_zero_shot.yaml
Tab 1Q3.5 CoT (69.7%)baselines/q35_cot.yaml
Tab 1Q3.5 SC-N=8 1k (77.8%)scaling/q35_sc_n8_1k.yaml
Tab 1Q3.5 SC-N=8 2k (81.5%)scaling/q35_sc_n8_2k.yaml
Tab 1Q3.5 SC-N=16 2k (81.6%)scaling/q35_sc_n16_2k.yaml
Tab 2T=0.3 (80.8%)scaling/q35_sc_n8_2k_t03.yaml
Tab 2T=0.5 (81.0%)scaling/q35_sc_n8_2k_t05.yaml
Tab 2T=0.7 (81.5%)scaling/q35_sc_n8_2k.yaml
Tab 2T=0.9 (81.0%)scaling/q35_sc_n8_2k_t09.yaml
Tab 3SC-N=8 dev (65.5%)search/q25_sc_n8_dev.yaml
Tab 3DTR N=2,M=2 (55.0%)search/q25_dtr_n2m2.yaml
Tab 3DTR N=4,M=4 (61.5%)search/q25_dtr_n4m4.yaml
Tab 3PRM-BAS (60.5%)search/q25_prm_bas_dev.yaml
Tab 4R1/R2 selectorsPool from baselines/q25_sc_n8.yaml or scaling/q35_sc_n8_2k.yaml, rescored by score_with_critic.py / score_with_prm.py
App Tab 10DTR + PRM modesPool from search/q25_dtr_n4m4.yaml, rescored by rescore_dtr_with_prm.py
S-bestTest 84.1%test/q35_sc_n16_2k_test.yaml + repair_parse_failures.py

Post-hoc analysis and figures

# Stratified analysis of a completed run
python scripts/analyze.py --run-dir runs/<run_id>

# Generate paper figures (hardcoded data, no GPU needed)
python paper/figures/plot_figures.py

Notebook

notebook.ipynb walks through the full pipeline:

  1. Baselines (zero-shot, chain-of-thought, self-consistency)
  2. Search strategy (PRM-BAS beam-annealing search within a describe-then-reason scaffold)
  3. Verification strategies (training-free generative critic, Qwen-VL-PRM discriminative rescoring)
  4. Guided parse repair
  5. Stratified evaluation

GPU-dependent cells are guarded with a RUN_INFERENCE flag for readability without hardware.

Data

The EXAMS-V dataset is loaded automatically from HuggingFace. It contains approximately 25,000 real school exam questions across 13 languages and 20 subjects. Our experiments use the validation split (4,651 questions) and a 200-question stratified subset for ablations.

License

This project is licensed under the MIT License. See LICENSE for details.

mcq
test-time-scaling
vlm