Skip to content

[Bug]: Under async scheduling, stateful logits processors that read output_token_ids values see -1 placeholders — logitsprocs_need_output_token_ids only counts CLI custom processors #52461

Description

@danbo610

Your current environment

Observed on releases/v0.27.1 (6e448d0ea9), reading the same code paths on current main.
Runtime: vLLM 0.27.2.dev1 (source install), torch 2.13.0+cu130, 2x NVIDIA L20, TP=2, Python 3.12.
The issue is scheduling logic, not environment-specific.

🐛 Describe the bug

The v1 LogitsProcessor interface documents output_tok_ids as a live view of each request's generated tokens:

Key assumption: the output_tok_ids list (which is an element of each tuple in added) is a reference to the request's running output tokens list; via this reference, the logits processors always see the latest list of generated output tokens.

https://github.com/vllm-project/vllm/blob/6e448d0ea9/vllm/v1/sample/logits_processor/interface.py#L45-L48

Under async scheduling (default-on since 0.27), that assumption only holds conditionally. The worker-side lists are extended with -1 placeholders each step (real ids stay on the GPU in prev_sampled_token_ids):

https://github.com/vllm-project/vllm/blob/6e448d0ea9/vllm/v1/worker/gpu_model_runner.py#L3823-L3846

A repair mechanism exists precisely for this: set_async_sampled_token_ids / update_async_output_token_ids patch the -1s with real ids right before the logits processors run:

https://github.com/vllm-project/vllm/blob/6e448d0ea9/vllm/v1/worker/gpu_input_batch.py#L1030-L1056

But the repair is gated by needs_output_token_ids (penalties / bad_words / thinking budget / logitsprocs_need_output_token_ids):

https://github.com/vllm-project/vllm/blob/6e448d0ea9/vllm/v1/worker/gpu_input_batch.py#L908-L922

and the logitsprocs contribution to that gate only counts CLI-passed custom processors:

https://github.com/vllm-project/vllm/blob/6e448d0ea9/vllm/v1/worker/gpu_model_runner.py#L714-L726

# We currently don't know whether a particular custom logits processor
# uses output token ids so we set this conservatively. ...
logitsprocs_need_output_token_ids=bool(custom_logitsprocs),

So there are three under-/over-approximations of the same unknown:

  1. Entry-point plugins (vllm.logits_processors group) are not counted — being fixed by [Bugfix] Include entry-point logits processor plugins in output token… #38199.
  2. Builtin-registered processors are never counted. The three in-tree builtins happen to only read the list's length (MinTokensLogitsProcessor), never its values, so nothing in-tree notices. Any stateful processor registered via BUILTIN_LOGITS_PROCESSORS that reads values silently computes on placeholder garbage: full per-step overhead is paid, results are wrong, and there is no warning or error.
  3. Conversely, CLI custom processors that don't read values still force the repair path (the conservative True), paying an unnecessary sync in async mode.

Reproduction / real-world impact

We integrated Google DeepMind's SynthID-Text watermarking (via the HF transformers processor) as a builtin stateful logits processor on top of releases/v0.27.1 — the first "reads values" consumer of this contract we know of:

  • with --no-async-scheduling: 8/8 watermarked outputs detected, mean-g z-score avg 7.86 (range 5.51–11.47), 0 false positives on controls;
  • with default async scheduling and identical code: 0/8 detected (z avg 0.39 ≈ control 0.33), while still paying ~11% per-request latency for the watermark compute — the context window degrades to [-1, -1, -1, -1].

Branch with the processor and a fail-safe guard (refuses to watermark and warns when async scheduling is detected): https://github.com/danbo610/vllm/tree/feature/synthid-watermark (see commit f18c17a3bb for the guard).

Minimal repro sketch: register any processor that prints output_tok_ids from apply() in BUILTIN_LOGITS_PROCESSORS, run with defaults (no penalties, no CLI --logits-processors), and observe [-1, -1, ...]; pass --no-async-scheduling (or any CLI custom processor, which flips the flag) and observe real ids.

Suggested fix

A declarative capability on the interface would solve all three cases at once, mirroring is_argmax_invariant():

class LogitsProcessor(ABC):
    @classmethod
    def needs_output_token_ids(cls) -> bool:
        return True  # conservative default for out-of-tree processors
  • the three in-tree builtins override it to False (no behavior change, removes today's over-approximation for value-agnostic CLI procs);
  • build_logitsprocs ORs the declarations across builtin + entry-point + CLI processors and feeds logitsprocs_need_output_token_ids;
  • the interface.py "Key assumption" docstring gains a note that the live view is only guaranteed when this capability is declared (or async scheduling is off).

Short of that, amending the docstring plus a startup warning when async scheduling is enabled with non-in-tree processors would at least make the failure mode discoverable — today it is completely silent.

Happy to help validate a fix against the watermarking use case; we can also upstream the SynthID processor itself as a separate PR if there is interest.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions