Commit ee56404
Add vLLM backend; default --inference-backend to vllm (#31)
* feat(vllm): add vLLM backend; default --inference-backend to vllm
Adds VLLMClassifier alongside TransformersLLMClassifier so local inference
can use vLLM's PagedAttention / continuous batching without changing the
score-extraction contract. Both backends share prompt construction, QA
decoders, and result-CSV format; only the model-call inner loop differs.
Highlights
- folktexts/llm_utils.py
- decode_topk_logprobs_to_risk_estimate: shared helper that scatters
exp(logprob) from sparse top-K dicts into a (n_passes, vocab_dim)
array, filters tokenizer vocab to in-range ids, and dispatches to
question.get_answer_from_model_output. WebAPI backend refactored to
use it (single source of truth for top-K decoding).
- load_vllm_model: soft-imported loader that mirrors the transformers
helper (calls add_pad_token, sets VLLM_LOGGING_LEVEL=WARNING).
- folktexts/classifier/vllm_classifier.py (new)
- MultipleChoiceQA: SamplingParams(max_tokens=1, logprobs=20).
- DirectNumericQA: max_tokens=num_forward_passes, logprobs=20,
allowed_token_ids=<digit ids> (mirrors transformers digit mask).
- ReasoningQA: temperature=0 generation, reuses _apply_chat_template_batch
and _postprocess_generated_text + the same reasoning-failure-rate
observability hooks as the transformers backend.
- __hash__ includes a "vllm" tag so result paths
(results.bench-{hash}.json) cannot collide with transformers runs of
the same model.
- vocab_dim resolved via AutoConfig.from_pretrained(...).vocab_size, the
same number model.config.vocab_size returns on the transformers path
(avoids vLLM-internals lookup; documented in CLAUDE.md gotchas).
- folktexts/benchmark.py
- make_benchmark / make_acs_benchmark gain backend= and
model_name_or_path= kwargs. _resolve_backend autodetects from model
type (str -> webapi, vllm-shaped -> vllm, else transformers); explicit
backend= overrides.
- folktexts/cli/run_acs_benchmark.py
- Default --inference-backend = "vllm". Adds --gpu-memory-utilization,
--max-model-len, --vllm-dtype, --tensor-parallel-size. Sizes
max_model_len as context_size + max_new_tokens + 256, with
max_new_tokens=5000 for ReasoningQA and 1 otherwise (avoids OOM from
Llama checkpoints whose max_position_embeddings = 131072).
- pyproject.toml + requirements/vllm.txt
- Optional install: pip install 'folktexts[vllm]'. Module-level imports
are deferred so folktexts still imports without vllm installed.
Validation toolchain
- scripts/compare_backends.py: small-slice equivalence harness. Runs the
same model + task + config through both backends, diffs predictions,
reports AUC/ECE/Brier deltas + argmax-token agreement rate. Acceptance
gates (|ΔAUC|≤0.005, |ΔECE|≤0.01, ≥95% rows |Δ|≤0.05) printed inline.
- scripts/reproduce_table1.py: extends the paper-reproduction harness
with --backend and --results-subdir; vLLM runs land in
results/paper-reproduction-vllm/ so transformers numbers in
results/paper-reproduction/ stay untouched.
- scripts/compare_table1_backends.py: writes
results/paper-reproduction-vllm/TABLE1_BACKEND_COMPARISON.md, bolding
cells with |ΔAUC|>0.01 or |ΔECE|>0.02 (looser-than-pre-flight gates).
Tests
- tests/test_logprob_decoding.py: 10 tests for the shared helper covering
MC, Numeric, prefix-variant matching, multi-digit tokens, missing /
out-of-range / negative ids.
- tests/test_vllm_classifier.py: 8 tests with a fake vllm injected into
sys.modules; exercises all three QA modes' SamplingParams and output
parsing without requiring a GPU or the real vllm package.
README updated: install command, new CLI flags, vLLM example block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(qa,vllm): uniform fallback when top-K excludes answer letters
Surfaced during the Table 1 vLLM sweep on Mistral-7B-Instruct-v0.2 in
zero-shot MCQ mode: the model emits prose continuations ("I", "The",
"Based"…) before any A/B answer letter, so neither letter (nor any prefix
variant) lands inside vLLM's top-20 logprobs. answers_sum_prob then equals
zero and the QA decoder divides by zero.
- folktexts/qa_interface.py: when answers_sum_prob <= 0 (or no prefix
variant is present in the supplied vocab), fall back to a uniform
distribution over self.choices — equivalent to the model saying "I
don't know." Also makes the warning's argmax-token lookup tolerant of
ids missing from the supplied vocab dict (it crashed before with
KeyError on the test stubs).
- folktexts/classifier/vllm_classifier.py: bump _TOPK_LOGPROBS from 20
to 50 for the MC and Numeric paths. 20 mirrored the WebAPI cap; 50 is
cheap on a local engine and reliably covers cases where the answer
letter sits below position 20 behind prose tokens. Models where A/B
are clearly top-2 are unaffected.
- tests/test_logprob_decoding.py: new
test_zero_mass_on_choices_falls_back_to_uniform to lock the fallback
in. Exercises the path where every top-K id is outside the answer-
letter vocab; helper must return a finite, uniform score.
Both changes also harden the WebAPI backend via the shared decoder.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(vllm): plumb max_logprobs through load_vllm_model
The previous commit bumped VLLMClassifier._TOPK_LOGPROBS to 50, but vLLM
caps per-request `logprobs` at the engine-level `max_logprobs` (default
20). Without raising the engine cap, every predict() call now fails:
VLLMValidationError: Requested sample logprobs of 50, which is
greater than max allowed: 20 (parameter=logprobs, value=50)
load_vllm_model now exposes `max_logprobs` (default 50, matches the
classifier constant) and forwards it to LLM(...). Documented the coupling
in the parameter docstring so a future bump to _TOPK_LOGPROBS makes its
matching engine-cap edit obvious.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add validation harnesses for vLLM migration phases 3-5
- scripts/multi_seed_stability.py: per (model, mode, backend) cross-seed
AUC mean ± std with cross-backend gate of 2× max(std).
- scripts/extended_sweep.py: modern + thinking-model coverage (gemma-3,
Qwen3, Qwen3-Thinking) across baseline/chat-MCQ/chat-numeric/reasoning
modes per spec. Tier1 by default; --tier {tier2,tier3,all} expands.
- scripts/audit_reasoning_failures.py: counts ReasoningQA's regex-failed
rows (risk_score == 0.5 sentinel) per cell and reports cross-backend
delta in percentage points.
Used to gate the migration before flipping the CLI default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add validation wrapper scripts: phase6 chat extension, status summary, debug repro
- scripts/phase6_chat_extension.py: monkey-patches CHAT_TEMPLATE_MODELS to
add Mistral-7B-Instruct-v0.2 and Yi-34B-Chat for the chat-template extent
experiment without modifying reproduce_table1.py.
- scripts/validation_summary.py: aggregates Phase 1-5 reports into a one-page
results/VALIDATION_STATUS.md with gate-pass counts.
- scripts/debug_llama3_numeric_divergence.py: standalone reproducer for the
Llama-3-8B base + numeric divergence (1100 multi-digit token bias under
vLLM's allowed_token_ids).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix vocab_size resolution for Gemma-3 multimodal config
Multimodal Gemma-3 (gemma-3-4b-it, gemma-3-12b-it, gemma-3-27b-it) puts
vocab_size under `config.text_config` instead of the top-level config.
Both backends previously raised AttributeError on these models.
- transformers path (`llm_utils.query_model_batch_multiple_passes`): probe
top-level then text_config; raise an explicit error if neither.
- vLLM path (`VLLMClassifier._resolve_vocab_dim`): same probe before
falling back to the tokenizer-derived value.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Make extended_sweep harness resilient to model-load failures
The original harness wrapped per-cell errors in try/except but did not wrap
the model-load step. A failed model load (e.g., Gemma-3-4B+ vision
preprocessor missing in cluster cache) crashed the whole sweep before
later models could be tried.
Now wraps model-load in try/except, marks every requested mode as failed,
logs the traceback, frees GPU memory, and moves to the next model.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add --modes filter to multi_seed_stability.py
Lets the harness run a subset of modes per model — needed for the
non-reasoning multi-seed sweep (reasoning takes 2.5h+/cell on
transformers, dominating the wall-time budget).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add reasoning_sweep.py for focused reasoning validation
Reasoning at standard subsampling=0.01 takes 2-3h per cell on
transformers, dominating wall time. This harness runs a focused subset
(Qwen3-4B-Thinking-2507 with/without thinking + Llama-3-8B-Instruct
plain reasoning) at subsampling=0.005 (~1.6k rows) — total ~5-6h
overnight on a single GPU.
Output: results/reasoning-sweep/REPORT.md with cross-backend deltas
plus a 0.5-rate column tracking ReasoningQA's regex-fallback frequency
per backend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Track reasoning-sweep results in validation_summary.py
Reads results/reasoning-sweep/REPORT.md alongside the existing extended-sweep
audit. Phase 5 status now reflects the dedicated reasoning sweep output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Bump ReasoningQA.max_new_tokens 5000 → 8000 for thinking-mode parity
Qwen3-4B-Thinking-2507 with `enable_thinking=True` exhausts the 5000-token
budget mid-CoT on ~13% of ACSIncome rows; the regex extractor then falls
back to 0.5 and drags AUC from 0.785 (thinking-off) to 0.737 (thinking-on).
8000 tokens gives the model headroom to close `</think>` and emit the final
answer. Validated on Qwen3-4B-Thinking-2507 (n=832, sub=0.005):
| Metric | 5k | 8k |
|---------------------|--------|--------|
| AUC | 0.7369 | 0.7990 |
| regex 0.5-fallback | 13.1% | 2.5% |
The CLI's `max_model_len` heuristic now derives from
`ReasoningQA.max_new_tokens` symbolically so the two stay in sync if the
budget is bumped again. `scripts/reasoning_sweep.py` does the same.
See `divergences/03_qwen3_thinking_max_tokens.md` for the full diagnosis.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix Llama-3 multi-digit numeric divergence (vLLM logprobs_mode)
vLLM's default `logprobs_mode="raw_logprobs"` returns top-K logprobs from
the unmasked distribution, computed BEFORE `apply_logits_processors`
applies the `allowed_token_ids` mask. For `DirectNumericQA` on Llama-3
(1100 multi-digit decimal tokens), the unconstrained pos-1 distribution
is dominated by `'\n'`, `<|end_of_text|>`, and `'.'` (id 13). The QA
decoder's `_get_numeric_tokens` includes `'.'`, so the leaked top-K let
the decoder pick `'.'` over the only-allowed digit — answer text "5." →
regex "5" → 0.5. Llama-3-8B base numeric collapsed to 99% of rows at
exactly 0.5 (AUC 0.5071 vs TF 0.5591).
`load_vllm_model` now sets `logprobs_mode="processed_logprobs"`, which
returns top-K from the post-mask distribution; non-digit tokens have
zero probability and the decoder picks the highest-logit digit (matching
the transformers path).
Validated:
| Cell | TF | vLLM pre | vLLM post |
|-------------------------------|-------|----------|-----------|
| Llama-3-8B base numeric | 0.559 | 0.507 | 0.576 |
| Llama-3-70B-Instruct numeric | 0.826 | 0.848 | 0.826 |
| Mistral-7B-v0.1 numeric (reg) | 0.736 | 0.742 | 0.742 |
Llama-3-8B base unique values: 4 (99% at 0.5) → 9; the 0.5 collapse is
gone. 70B-Instruct now matches TF to 4 dp. Non-Llama-3 tokenizers
(0–16 multi-digit tokens) are unaffected — the change is benign for them.
A follow-up prompt-wording probe confirmed the prompt itself is not the
lever: special-char placement is clean, and dropping the `0.` prefill
collapses Llama-3-8B base to 0.5 on 97.6% of rows (the model's "I don't
know" mode is `0.5\n`). The defensive prefill is essential for weak base
models.
See `divergences/01_llama3_numeric_multidigit.md` for the full root-cause
analysis, validation matrix, and prompt-wording follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 7 edge-case validation for vLLM backend
Six robustness checks on Llama-3.2-{1B,3B}-Instruct:
1. predict_proba on a 1-row DataFrame — returns valid (1, 2) shape.
2. Sequential model swap in same Python process (1B free, 3B load) —
distinct outputs (mean p1 0.5156 vs 0.7211); no engine state leakage.
3a. Near-cap input (1452 / 2048 tokens) — generates cleanly.
3b. Over-cap input (2814 / 1024 tokens) — vLLM raises VLLMValidationError
with explicit overflow message; no silent truncation.
4. Tied-logit cross-backend agreement on synthetic A-vs-B prompt — both
transformers and vLLM pick "B" (TF top-2 logit gap 0.25, vLLM ' B'/' A'
are top-2). Cross-backend kernel-noise band (~1e-3 logprob) is below
the gap, so determinism holds.
5. OOM with gpu_memory_utilization=0.005 — vLLM raises ValueError("No
available memory for the cache blocks") at engine init; no hang.
All checks pass. Harness: scripts/phase7_edge_cases.py (covers 1, 2, 3a,
4, 5) plus scripts/phase7_overcap.py (covers 3b). Report:
results/phase7-edge-cases/REPORT.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add scripts/build_postfix_comparison_table.py
Aggregates cross-backend (transformers vs vLLM-post-fix) AUC/ECE over
Phase 1 (paper Table 1), Phase 4 (extended sweep) and Phase 6
(chat-template extension). For cells affected by fixes #1/#3, prefers
the post-fix numbers in `results/divergence_fix_validation/` over the
pre-fix originals so the table reflects the current branch HEAD.
Used to generate the comparison comment on PR #31.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Release v0.4.0: docs/updates.md, untrack debug artifacts
- Bump version 0.3.0 → 0.4.0; release notes for the vLLM backend live
at `docs/updates.md` and are wired into the Sphinx toctree under a new
"Updates" section.
- Stop tracking the validation/debugging working directories (`scripts/`,
`divergences/`, the new markdown reports under `results/`). They were
only ever used to coordinate the migration; the distilled summary in
`docs/updates.md` is the authoritative changelog for v0.4.0.
- Add the matching ignore rules so the local working trees keep them but
future commits don't pull them back in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* README: refresh vLLM-related sections
- The `--max-model-len` row in the options table referenced the pre-fix
5000-token reasoning budget. Update to point at
`ReasoningQA.max_new_tokens` (currently 8000) so the math stays
in sync with `qa_interface.py`.
- The "Full list of options" block was a stale paste of `--help` from
before the migration; it didn't list `--inference-backend`,
`--gpu-memory-utilization`, `--max-model-len`, `--vllm-dtype`, or
`--tensor-parallel-size`. Regenerated from the current CLI.
The summary table, install instructions, example-usage snippet, and FAQ
already covered the vLLM functionality and are unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs/updates.md: drop the structural-fixes detour
Per request, omit the deep-dive on the two internal bug fixes shipped
with the migration; the changelog should be focused on user-visible
behaviour. Minor reword in the Validation section so it stands on its
own without the dropped reference.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* README: add v0.4.0 vLLM callout + collapsible quickstart
A short note right after the intro diagram flags the v0.4.0 vLLM backend
addition and points at docs/updates.md for full release notes. The
collapsible "Using the vLLM backend" section beneath it covers:
- the optional install (pip install 'folktexts[vllm]');
- default-on CLI usage and the four vLLM-specific knobs;
- VLLMClassifier usage from Python, with a matching code snippet.
Existing detail in the options table, example-usage block, and FAQ is
unchanged; this is purely an above-the-fold pointer for users coming to
the README fresh on the new release.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* README: collapse "Benchmark features and options" and "FAQ"
Wrap both sections in <details>/<summary> blocks so the README's
above-the-fold area stays focused on the intro, getting-started, and
v0.4.0 callout. The full content (options table + --help + FAQ
entries) is one click away. Matches the existing pattern used by the
"Evaluating feature importance" section.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* README: lead Example usage with vLLM, collapse alternatives
Restructures the README so the example-usage code block immediately
shows the new default backend (VLLMClassifier) and tucks the
transformers / WebAPI / full-benchmark / reasoning / threshold-fitting
snippets into separate <details> blocks. Drops the redundant
top-of-file "Using the vLLM backend" expandable in favor of a one-line
callout pointing at docs/updates.md, and collapses "Ready-to-use
datasets". Net -24 lines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 4ed4587 commit ee56404
19 files changed
Lines changed: 1606 additions & 780 deletions
File tree
- docs
- folktexts
- classifier
- cli
- requirements
- scripts
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
180 | 180 | | |
181 | 181 | | |
182 | 182 | | |
183 | | - | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
| 37 | + | |
37 | 38 | | |
38 | 39 | | |
39 | 40 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| |||
443 | 443 | | |
444 | 444 | | |
445 | 445 | | |
| 446 | + | |
| 447 | + | |
446 | 448 | | |
447 | 449 | | |
448 | 450 | | |
| |||
509 | 511 | | |
510 | 512 | | |
511 | 513 | | |
| 514 | + | |
| 515 | + | |
512 | 516 | | |
513 | 517 | | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
514 | 544 | | |
515 | 545 | | |
516 | 546 | | |
| |||
645 | 675 | | |
646 | 676 | | |
647 | 677 | | |
| 678 | + | |
| 679 | + | |
648 | 680 | | |
649 | 681 | | |
650 | 682 | | |
| |||
710 | 742 | | |
711 | 743 | | |
712 | 744 | | |
713 | | - | |
714 | | - | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
715 | 748 | | |
716 | 749 | | |
717 | 750 | | |
| |||
720 | 753 | | |
721 | 754 | | |
722 | 755 | | |
723 | | - | |
| 756 | + | |
| 757 | + | |
| 758 | + | |
| 759 | + | |
| 760 | + | |
| 761 | + | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
724 | 768 | | |
725 | 769 | | |
726 | 770 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
3 | 4 | | |
0 commit comments