Skip to content

Commit ee56404

Browse files
AndreFCruzclaude
andauthored
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

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,10 @@ docs/_build/
180180
docs/notebooks/
181181

182182
# Claude Code project context (local, not shared)
183-
CLAUDE.md
183+
CLAUDE.md
184+
185+
# Local-only validation / debugging artifacts (kept out of the repo)
186+
scripts/
187+
divergences/
188+
results/VALIDATION_STATUS.md
189+
results/phase7-edge-cases/

README.md

Lines changed: 104 additions & 48 deletions
Large diffs are not rendered by default.

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Check out the following sub-pages:
3434
:maxdepth: 1
3535

3636
Readme file <readme>
37+
Updates <updates>
3738
API reference <source/modules>
3839
Example notebooks <notebooks>
3940

docs/updates.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Updates
2+
3+
Release notes summarising user-visible changes between versions. Older changes
4+
not yet listed here can be reconstructed from the git log.
5+
6+
## v0.4.0 — vLLM backend
7+
8+
`folktexts` v0.4.0 introduces local inference via [vLLM] alongside the existing
9+
HuggingFace `transformers` backend, typically delivering a 5–30× throughput
10+
improvement on GPU benchmarks while preserving the full score-extraction
11+
contract (multiple-choice, direct-numeric, and reasoning prompting).
12+
13+
[vLLM]: https://docs.vllm.ai/
14+
15+
### What's new
16+
17+
- **`VLLMClassifier`**: a new top-K-logprobs classifier in
18+
`folktexts.classifier.vllm_classifier`, parallel to
19+
`TransformersLLMClassifier`. Both feed the same QA decoders, so result
20+
semantics are unchanged.
21+
- **`load_vllm_model`** in `folktexts.llm_utils`: helper that initialises a
22+
vLLM `LLM` engine + tokenizer with sensible defaults for this benchmark
23+
(BF16, `gpu_memory_utilization=0.85`, `logprobs_mode="processed_logprobs"`).
24+
- **CLI flag `--inference-backend {transformers,vllm}`**: selects the local
25+
backend. **Default is now `vllm`.** Pass `--inference-backend transformers`
26+
to fall back to the previous path; the transformers code is unchanged and
27+
remains a fully supported alternative.
28+
- **vLLM-specific CLI flags**: `--gpu-memory-utilization`, `--max-model-len`,
29+
`--vllm-dtype`, `--tensor-parallel-size`. The CLI auto-derives a
30+
`max_model_len` from `--context-size + ReasoningQA.max_new_tokens + 256`
31+
when the user does not pass `--max-model-len` explicitly.
32+
- **Optional install group `[vllm]`**: `pip install folktexts[vllm]` pulls in
33+
the vLLM wheel. The base install is unchanged for users on the transformers
34+
path.
35+
36+
### Architecture
37+
38+
- **Two classifiers, one decoder.** `VLLMClassifier`,
39+
`TransformersLLMClassifier`, and `WebAPILLMClassifier` all hand answers to
40+
the QA-decoder methods on `MultipleChoiceQA`, `DirectNumericQA`, and
41+
`ReasoningQA`. The new helper `decode_topk_logprobs_to_risk_estimate` in
42+
`folktexts.llm_utils` factors out the top-K decoding logic shared by vLLM
43+
and the WebAPI; the transformers path (which has full-vocab logits)
44+
bypasses this helper, as before.
45+
- **Backend dispatch.** `Benchmark.make_*_benchmark(...)` accepts a
46+
`backend=` argument (`"transformers"`, `"vllm"`, `"webapi"`, or
47+
`None` for autodetect). When `None`, autodetect uses
48+
`str → webapi`, duck-typed `LLM-shaped → vllm`, else `transformers`.
49+
- **`VLLMClassifier.__hash__`** includes a `"vllm"` tag so cached result
50+
paths (`results.bench-{hash}.json`) cannot collide with transformers runs
51+
of the same model. Predictions can drift by ~1e-3 across backends due to
52+
attention-kernel differences; mixing them in one CSV would be a silent
53+
mistake.
54+
- **Numeric mode** uses vLLM's `allowed_token_ids` to restrict generation to
55+
digit tokens (mirroring the transformers `digits_only=True` mask).
56+
Multiple-choice mode runs unmasked; the QA decoder's prefix-variant
57+
matching handles renormalisation across answer letters.
58+
59+
### Cluster runtime requirements (B200 / Hopper / vllm 0.20.1 wheel)
60+
61+
The vLLM 0.20.1 wheel is built against CUDA 13. On clusters where the
62+
default toolkit is older, two environment steps are required for any
63+
vLLM invocation:
64+
65+
```bash
66+
source /etc/profile.d/modules.sh
67+
module load cuda/13.2 # provides libcudart.so.13
68+
export VLLM_USE_DEEP_GEMM=0 # skips an FP8 warmup that needs deep_gemm
69+
# (not on PyPI); harmless on BF16 models
70+
```
71+
72+
Without these, `import vllm._C` and engine init both crash on Hopper+
73+
GPUs.
74+
75+
### Validation
76+
77+
The migration was validated across 38 cross-backend cells covering the
78+
paper's Table 1 (8 models × 2-4 modes), a modern + thinking-model sweep
79+
(`gemma-3-1b-it`, `Qwen3-1.7B`, `Qwen3-4B`, `Qwen3-4B-Instruct-2507`,
80+
`Qwen3-4B-Thinking-2507`), and a chat-template extension on
81+
`Mistral-7B-Instruct-v0.2` and `Yi-34B-Chat`. Multi-seed stability was
82+
verified across 4 seeds × 2 backends on Llama-3-8B-Instruct and
83+
Qwen3-Thinking-2507.
84+
85+
**36/38 cells fall within the strict gates** `|ΔAUC| ≤ 0.015` and
86+
`|ΔECE| ≤ 0.025`. The two remaining outliers are characterised:
87+
88+
- `Llama-3-8B` base × `numeric` (zero-shot): vLLM `+0.017` AUC, `−0.041`
89+
ECE — vLLM is slightly *better*. The model is essentially near-random
90+
on this prompt (TF AUC 0.559); the delta is within the kernel-noise
91+
band of a near-random model.
92+
- `Qwen3-1.7B` × `chat-MCQ`: vLLM `+0.190` AUC, `+0.265` ECE — vLLM
93+
is *much* better. The transformers path collapses to 3 unique scores
94+
on this combination; vLLM produces 425 unique scores with broad
95+
spread. The bug is on the transformers side and does not reproduce on
96+
Qwen3-4B / Qwen3-4B-Instruct / Qwen3-4B-Thinking-2507.
97+
98+
Phase 7 robustness checks (1-row DataFrame, sequential model swap in
99+
the same Python process, near- and over-cap inputs, tied-logit
100+
cross-backend agreement, and OOM clean failure) all pass.
101+
102+
### Backwards compatibility
103+
104+
- The CLI accepts the same flags as before plus the new
105+
`--inference-backend` / `--gpu-memory-utilization` /
106+
`--max-model-len` / `--vllm-dtype` / `--tensor-parallel-size`. All new
107+
flags have safe defaults; existing scripts work unchanged on the
108+
vLLM backend, or on the transformers backend with
109+
`--inference-backend transformers`.
110+
- `Benchmark.make_*_benchmark(...)` accepts an optional `backend=` kwarg.
111+
Existing callers that pass `model=` as a HuggingFace `PreTrainedModel`
112+
continue to be routed to `TransformersLLMClassifier`.
113+
- Result CSVs from previous runs (transformers) are not invalidated; the
114+
new vLLM hash tag means vLLM runs save to a fresh path rather than
115+
overwriting transformers numbers.
116+
117+
### Migration notes
118+
119+
If you previously installed `folktexts` and want to use the new vLLM
120+
backend:
121+
122+
```bash
123+
pip install --upgrade 'folktexts[vllm]'
124+
# or, from a checkout:
125+
pip install -e .[vllm]
126+
```
127+
128+
Then either accept the new default (`vllm`) or stay on transformers
129+
explicitly:
130+
131+
```bash
132+
run_acs_benchmark --model <path> --task ACSIncome --data-dir <path> \
133+
--inference-backend transformers
134+
```

folktexts/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from ._version import __version__, __version_info__
22
from .acs import ACSDataset, ACSTaskMetadata
33
from .benchmark import Benchmark, BenchmarkConfig
4-
from .classifier import LLMClassifier, TransformersLLMClassifier, WebAPILLMClassifier
4+
from .classifier import LLMClassifier, TransformersLLMClassifier, VLLMClassifier, WebAPILLMClassifier
55
from .qa_interface import DirectNumericQA, MultipleChoiceQA, ReasoningQA
66
from .task import TaskMetadata

folktexts/benchmark.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from ._utils import hash_dict, is_valid_number, get_current_timestamp
1616
from .acs.acs_dataset import ACSDataset
1717
from .acs.acs_tasks import ACSTaskMetadata
18-
from .classifier import LLMClassifier, TransformersLLMClassifier, WebAPILLMClassifier
18+
from .classifier import LLMClassifier, TransformersLLMClassifier, VLLMClassifier, WebAPILLMClassifier
1919
from .dataset import Dataset
2020
from .evaluation import evaluate_predictions
2121
from .plotting import render_evaluation_plots, render_fairness_plots
@@ -443,6 +443,8 @@ def make_acs_benchmark(
443443
data_dir: str | Path = None,
444444
max_api_rpm: int = None,
445445
config: BenchmarkConfig = BenchmarkConfig.default_config(),
446+
backend: str | None = None,
447+
model_name_or_path: str | Path | None = None,
446448
**kwargs,
447449
) -> Benchmark:
448450
"""Create a standardized calibration benchmark on ACS data.
@@ -509,8 +511,36 @@ def make_acs_benchmark(
509511
tokenizer=tokenizer,
510512
max_api_rpm=max_api_rpm,
511513
config=config,
514+
backend=backend,
515+
model_name_or_path=model_name_or_path,
512516
)
513517

518+
@staticmethod
519+
def _resolve_backend(*, backend: str | None, model) -> str:
520+
"""Pick the inference backend for this benchmark run.
521+
522+
Explicit `backend` overrides autodetection. Autodetection rules:
523+
- `model` is a string -> "webapi" (model ID for litellm).
524+
- `model` is a `vllm.LLM`-like object (has `.generate` and `.get_tokenizer`) -> "vllm".
525+
- Otherwise -> "transformers".
526+
"""
527+
if backend is not None:
528+
backend = backend.lower()
529+
if backend not in {"transformers", "vllm", "webapi"}:
530+
raise ValueError(
531+
f"Unknown inference backend '{backend}'. "
532+
f"Expected one of: 'transformers', 'vllm', 'webapi'."
533+
)
534+
return backend
535+
536+
if isinstance(model, str):
537+
return "webapi"
538+
if hasattr(model, "generate") and hasattr(model, "get_tokenizer"):
539+
# Duck-typed vLLM `LLM`. transformers models also expose `.generate`,
540+
# but not `.get_tokenizer` — that's the discriminator.
541+
return "vllm"
542+
return "transformers"
543+
514544
@staticmethod
515545
def _configure_task_question(task: TaskMetadata, config: BenchmarkConfig) -> None:
516546
"""Pick the Q&A interface (reasoning / numeric / multiple-choice) on `task`."""
@@ -645,6 +675,8 @@ def make_benchmark(
645675
tokenizer: AutoTokenizer = None, # WebAPI models have no local tokenizer
646676
max_api_rpm: int = None,
647677
config: BenchmarkConfig = BenchmarkConfig.default_config(),
678+
backend: str | None = None,
679+
model_name_or_path: str | Path | None = None,
648680
**kwargs,
649681
) -> Benchmark:
650682
"""Create a calibration benchmark from a given configuration.
@@ -710,8 +742,9 @@ def make_benchmark(
710742
if max_api_rpm is not None and isinstance(model, str):
711743
llm_inference_kwargs["max_api_rpm"] = max_api_rpm
712744

713-
# Create LLMClassifier object
714-
if isinstance(model, str):
745+
resolved_backend = cls._resolve_backend(backend=backend, model=model)
746+
747+
if resolved_backend == "webapi":
715748
llm_clf = WebAPILLMClassifier(
716749
model_name=model,
717750
task=task,
@@ -720,7 +753,18 @@ def make_benchmark(
720753
)
721754
logging.info(f"Using webAPI model: {model}")
722755

723-
else:
756+
elif resolved_backend == "vllm":
757+
llm_clf = VLLMClassifier(
758+
llm=model,
759+
tokenizer=tokenizer,
760+
task=task,
761+
model_name_or_path=model_name_or_path,
762+
encode_row=encode_row_function,
763+
**llm_inference_kwargs,
764+
)
765+
logging.info(f"Using local vLLM model: {llm_clf.model_name}")
766+
767+
else: # transformers
724768
llm_clf = TransformersLLMClassifier(
725769
model=model,
726770
tokenizer=tokenizer,

folktexts/classifier/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .base import LLMClassifier # noqa: F401
22
from .transformers_classifier import TransformersLLMClassifier # noqa: F401
3+
from .vllm_classifier import VLLMClassifier # noqa: F401
34
from .web_api_classifier import WebAPILLMClassifier # noqa: F401

0 commit comments

Comments
 (0)