Skip to content

fix: auto-cap max_tokens to the model window per prompt - #145

Merged
kaiitunnz merged 4 commits into
mainfrom
fix/vllm-auto-cap-max-tokens
Sep 19, 2026
Merged

kaiitunnz merged 4 commits into
mainfrom
fix/vllm-auto-cap-max-tokens

Conversation

@tjluyao

@tjluyao tjluyao commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

What

The vLLM executor passes a request's max_tokens straight through to SamplingParams. vLLM rejects a request when prompt_tokens + max_tokens > max_model_len, so a workflow that hard-codes a large max_tokens fails every invocation regardless of prompt size.

Concretely: a dsm_clause_entailment_audit op requesting max_tokens: 65536 against QuantTrio/Qwen3.6-27B-AWQ (native window 40960) can never succeed — even though the op only emits a short verdict. The surfaced error names neither the parameter nor the op:

mp(vllm) failed: This model's maximum context length is 8192 tokens.
However, you requested 0 output tokens and your prompt contains at least
8193 input tokens...

(The "0 output tokens" is vLLM clamping the output budget to zero once the prompt alone overflows; the real 65536 only becomes visible once the window is large enough to admit the prompt.) This was misdiagnosed as an auth/config problem more than once.

Fix

Auto-cap max_tokens per prompt to window − prompt_tokens − margin, bounded by a floor and never above what was requested. LLM.generate accepts either one SamplingParams or a per-prompt list, so the list is built only when a clamp actually bites — the common case keeps sharing one object.

Deliberately conservative:

  • Window unknown (engine shape unrecognised) → no clamp. A missing window must never silently shrink a caller's budget.
  • Prompt at/over the window → floor, leaving vLLM to raise the real "input too long" error rather than hiding it behind a 1-token generation.
  • Multimodal (TextPrompt) entries → keep the requested budget. Raw text undercounts image tokens, so clamping on text alone could permit more than fits; preserving current behaviour is the safe choice.

The window and max_tokens are read through typed attribute access on the live engine (llm_engine.model_config.max_model_len), guarded by try/except, so a vLLM version skew degrades to "no clamp" rather than crashing.

When a clamp bites it is surfaced to the caller on the result item's diagnostics field (diagnostics.auto_cap = {max_tokens, requested}), so a shorter-than-requested completion is self-explaining instead of a silent truncation. The grouped/table output path aggregates it per member. diagnostics is engine-owned and kept distinct from the user-owned metadata.

Tests

tests/worker/test_vllm_auto_cap_max_tokens.py covers the pure clamp (fit, window-unknown, already-fits, floor, never-above-requested) and the wiring on a bare instance with fakes (per-prompt list, mixed batch, multimodal passthrough, tokenizer-failure fallback) plus the grouped-remap aggregation. The pure clamp is builtins-only; the wiring needs no GPU deps. The suite passes under uv run pytest.

Note

Pairs with the deploy-side change that stopped Lumilake capping max_model_len at 8192 fleet-wide (deploy_infra#51). That fix lets the prompt fit the 40960 window; this fix stops an oversized max_tokens from overflowing it from the other side.

vLLM rejects a request when prompt_tokens + max_tokens > max_model_len.
A workflow that hard-codes a large max_tokens therefore fails EVERY
invocation regardless of prompt size: a dsm_clause_entailment_audit op
requesting max_tokens=65536 against QuantTrio/Qwen3.6-27B-AWQ (native
window 40960) could never succeed. The op only emits a short verdict,
so the 65536 is a caller mistake — but nothing in the worker defended
against it, and the error ("maximum context length … you requested 0
output tokens") names neither the parameter nor the op, so it was
misdiagnosed as an auth/config problem more than once.

Auto-cap max_tokens per prompt to window - prompt_tokens - margin,
bounded by a floor and never above what was requested. Because vLLM's
LLM.generate accepts either one SamplingParams or a per-prompt list, we
build the list only when a clamp actually bites; the common case keeps
sharing one object.

Deliberately conservative:
- Window unknown (engine shape unrecognised) → no clamp; a missing
  window must never silently shrink a caller's budget.
- Prompt already at/over the window → floor, leaving vLLM to raise the
  real "input too long" error rather than hiding it behind a 1-token
  generation.
- Multimodal (TextPrompt) entries keep the requested budget: the raw
  text undercounts image tokens, so clamping on text alone could permit
  MORE than fits — preserving current behaviour is the safe choice.

The window is read off the live engine (llm_engine.model_config
.max_model_len) with getattr fallbacks, so a vLLM version skew degrades
to "no clamp" instead of crashing.

Tests: the pure clamp is builtins-only (no vllm/torch); the wiring runs
on a bare instance with fakes. Both added in
tests/worker/test_vllm_auto_cap_max_tokens.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
@kaiitunnz kaiitunnz changed the title fix(vllm): auto-cap max_tokens to the model window per prompt fix: auto-cap max_tokens to the model window per prompt Sep 18, 2026
Follow-up on the auto-cap change from review.

Surface the clamp to callers: InferenceItem gains a `diagnostics` field
(engine-owned, keyed by producer), and a clamped prompt records
`{auto_cap: {max_tokens, requested}}`. The grouped-output remap
aggregates it per member, mirroring finish_reason. A single summary log
replaces the per-prompt one, and tokenizer failures log at debug.

Simplify the clamp to `min(requested, max(budget, floor))`, which also
stops a below-floor request from being raised up to the floor. Read the
window and max_tokens through typed attribute access instead of getattr,
guarded by try/except so a vLLM version skew still degrades to no clamp.

Thread the per-index clamp record through the return value rather than
instance attributes, removing a cross-run stale-state hazard.

Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
The SDK's InferenceItem is a field-for-field mirror of the shared schema,
enforced by tests/sdk/test_schema_compat.py. Add the diagnostics field so
the mirror matches after the server-side addition.

Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>
Trim the docstrings and comments added for the max_tokens auto-cap to the
level of detail they need, and give the helper docstrings a consistent
one-line summary plus description.

Signed-off-by: Noppanat Wadlom <noppanat.wad@gmail.com>

@kaiitunnz kaiitunnz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@kaiitunnz
kaiitunnz merged commit e78c397 into main Sep 19, 2026
11 checks passed
@kaiitunnz
kaiitunnz deleted the fix/vllm-auto-cap-max-tokens branch September 19, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants