LLM-judge output parsing#
The published benchmark numbers come from a strict regex parser: an answer only
counts if it sits inside <answer></answer> tags (see
Parsing and summarizing). That strictness has a
cost — a model that measures correctly but writes \boxed{407.02, 325.62} or
**Answer:** 408.2, 326.4 is scored as a miss, which mixes “the model can’t
measure” together with “the model didn’t follow the output format”.
The LLM-judge pipeline (script/llm-parsing/) separates the two. It re-reads
every raw response with a second, offline language model whose only job is to
find the answer wherever it was written. You end up with two versions of every
report: the published one, and a format-robust one. The gap between them is how
much of a model’s apparent failure was formatting.
Across the 18-model roster the regex rejects a substantial, non-random slice:
Task |
Responses |
Rejected by the regex |
Worst model |
|---|---|---|---|
Tumour/Lesion |
43,938 |
23.0% |
Llama-3.2-11B, 85% |
Angle/Distance |
37,080 |
26.2% |
Qwen2.5-VL-32B, 90.8% |
Detection |
415,278 |
6.6% |
GLM-4.6V, 40.0% |
Measured effect of the re-parse: T/L success 77.0% → 89.7%, A/D 73.8% → 90.1%, Detection 93.4% → 98.4%. Llama-3.2-11B’s T/L success goes from 14.5% to 97.9% — almost all of its “failure” was formatting.
What the judge does — and is not allowed to do#
The judge is an extraction device, never an evaluator. It never sees the image, never sees the ground truth, and is never asked whether a value is correct — so it cannot flatter or penalise a model.
It is also not trusted to copy numbers. The judge must quote the sentence it found the answer in; the pipeline then locates that quote in the original response (span verification) and re-reads the digits out of the quote itself. A number the model never wrote therefore cannot enter the results, even if the judge claims otherwise.
Two invariants keep the published numbers safe:
If the regex already found an answer, that answer stands. The judge can only add answers the regex missed; it can never revise a published number.
The metrics are the existing ones. MAE, MRE, IoU and SuccessRate come from the same summarizer code that produced the published numbers, just pointed at the re-parsed records. There is no second definition of any metric.
For T/L and A/D the judge also extracts the intermediate steps the prompt asked for (landmark coordinates, axis endpoints, the computed value). Those are saved but not scored.
What you get#
Re-parsed records land in a sibling llm-parsed/ folder next to each model’s
parsed/; the original parsed/ files are never touched. Summary reports gain
a __llm-parsed suffix so they sit beside the published ones instead of
overwriting them.
Every record carries one of four outcomes:
Outcome |
Meaning |
Counts as parsed |
|---|---|---|
answer in expected format |
the regex already had it |
yes |
answer in another format |
found and span-verified by the judge |
yes |
no answer stated |
the response never gives one (declined, or stopped early) |
no |
undetermined |
the judge was unusable and the regex failed |
no |
“Undetermined” is the pipeline’s own error rate (currently ≈ 0.4% of all responses) — recoveries that were never attempted, so the reported improvement is a floor, not a ceiling. Each recovered record also keeps the quoted sentence, so any recovered number can be checked by eye.
Running the pipeline#
The pipeline lives in script/llm-parsing/ and runs in four resumable stages:
build per-task work queues from the model roster, sweep them on GPU with offline
vLLM, verify spans and merge into llm-parsed/, then report. The judge reader
comes from a registry; the current (and only) entry is
google/gemma-4-31B-it —
~62 GB of bf16 weights, run with 2 GPUs per process.
1. Build the judge environment#
The judge needs its own Python environment (a newer vLLM than the evaluation code pins, and Transformers 5.x, which cannot resolve against vLLM’s declared bounds in a single pip pass — the install runs in two phases and prints expected dependency-conflict errors, then imports every package that objected to prove the bounds were conservative):
bash script/llm-parsing/setup_judge_env.sh # builds <repo>/.cache/judge-env_gemma-4-31b
export PYTHON=<target>/bin/python # the script prints this line
The setup ends with a GPU allocation check, so a bad CUDA/PyTorch pairing fails
here, in minutes — not thirteen hours into a sweep. If your driver is older than
CUDA 12.8, point the build at a matching wheel index:
TORCH_INDEX_URL=https://download.pytorch.org/whl/cu126 bash script/llm-parsing/setup_judge_env.sh.
2. Run everything#
bash script/llm-parsing/run_llm_parsing.sh # all stages, in order
bash script/llm-parsing/run_llm_parsing.sh --list # show the steps, run nothing
bash script/llm-parsing/run_llm_parsing.sh analyze # only the CPU stages + reports
bash script/llm-parsing/run_llm_parsing.sh --fresh # start over (archives old judge output)
The driver stops at the first failed check and is resumable — finished work is skipped, so it can be killed and restarted freely. All settings are optional environment variables; the ones you are most likely to touch:
Variable |
Meaning |
|---|---|
|
interpreter with the judge’s PyTorch + vLLM (from step 1) |
|
which registered reader to use (default |
|
default |
|
GPUs one judge process spans (default per reader; 2 for gemma-4-31b) |
|
independent processes (default: visible GPUs ÷ |
|
exercise the whole pipeline on CPU with a stand-in — never report its numbers |
Every visible GPU is used: the work list is split across independent processes (cheap, near-linear speedup) and each process spans the fewest GPUs the model fits in (expensive tensor parallelism, capacity not speed). Rough cost on two H100s: a one-off model download, minutes of CPU preparation, ~13 hours of GPU for the full sweep, and ~1 hour of CPU for the reports.
Tip
Every judge answer is stamped with a fingerprint of the exact prompt and token budget that produced it, and the pipeline refuses to mix stamps — a prompt edit invalidates the queues instead of silently blending two prompts into one report. To repair a minority of bad rows without re-judging everything, see “Repairing a partial run” in the pipeline README.
Summarizing the re-parsed records#
The standard summarizers (see Parsing and summarizing) read judge output through two flags, so the strict and format-robust columns share one code path:
python -m medvision_bm.benchmark.summarize_TL_task \
--task_dir Results/MedVision-TL-v2-CoT -p 32 --skip_model_wo_parsed_files \
--removed_samples_dir Data/Datasets \
--parsed_dirname llm-parsed --resps_key LLM_filtered_resps
Flag |
Meaning |
|---|---|
|
per-model subfolder to read: |
|
record field holding the prediction: |
|
restrict the run to the configured roster (a results tree also carries superseded |
The driver’s analyze step runs these for you; the flags matter when you
aggregate by hand.
Reading the results#
Two reports per task land side by side — the published one and its
__llm-parsed twin. Diff them. The judge report additionally splits each
model’s failures into wrong format vs no answer given, and reports how often
the judge agreed with the regex on responses the regex could already read — a
free reliability check, which is why every response is re-read rather than only
the failures.
Warning
Judge output is not reproducible run to run, even with greedy decoding on
identical hardware — the cause is numerical non-determinism in the inference
stack, not sampling. Treat the saved judge-out_*.jsonl files as the artefact
of record: release those, don’t re-run and expect the same rows back. The full
root-cause analysis is in
docs/LLM-Judge-Reproducibility.md
(measured on a since-retired reader; its headline rates do not transfer to the
current one, but the operating rules do).
A low success rate that survives re-parsing is not automatically a measurement failure either: if a model’s responses pile up against its token limit it ran out of room, which must be checked against the run’s generation settings.
Further reading#
script/llm-parsing/README.md— full user guide: environments, GPU layout, alternative readers, repairs.script/llm-parsing/DESIGN.md— module map, the outcome decision table, verification tiers, edit hazards.Clinical Decision Agreement can score the judge re-parse instead of the strict parse, giving a format-robust view of clinical agreement as well.