Skip to content

[Router][Bindings] Settle the image embedding over-dimension contract on /api/v1/embeddings#2639

Open
WUKUNTAI-0211 wants to merge 8 commits into
vllm-project:mainfrom
WUKUNTAI-0211:fix/2407-image-embedding-dimension-contract
Open

[Router][Bindings] Settle the image embedding over-dimension contract on /api/v1/embeddings#2639
WUKUNTAI-0211 wants to merge 8 commits into
vllm-project:mainfrom
WUKUNTAI-0211:fix/2407-image-embedding-dimension-contract

Conversation

@WUKUNTAI-0211

Copy link
Copy Markdown
Collaborator

Closes #2407

What

Settle the over-dimension contract for image inputs on /api/v1/embeddings.

The image path silently ignored the requested dimension: encode_image_with_dim
truncated only when the target was below the native 384 and otherwise returned the
native vector unchanged, unlike the text 2DMSE path which rejects target_dim > embedding_dim. Because the handler defaulted dimension to 768, a default mixed
text+image request returned some 768-dim (text) and some 384-dim (image) vectors in a
single 200 response, and isValidDimension excluded 384 — the one value that is the
image model's native dimension.

Contract

Image inputs route to the multimodal model, whose native dimension is a ceiling
(MRL only truncates down). The native dimension (384 by default) is below the text
default (768), so:

  • Accept the native dimension (384) in isValidDimension.
  • Default image-bearing requests to the native dimension (read from the model via
    a new FFI getter, not a hardcoded constant), so a plain image or mixed text+image
    request returns uniform vectors instead of failing or silently mixing dimensions.
  • Reject an explicit above-native dimension for image inputs with a 4xx, matching
    the text 2DMSE reject contract.
  • Report the loaded multimodal model id in model_used (base name of the
    configured path) instead of the hardcoded "multi-modal-embed".
  • Reject target_layer and a text-only model selector on image-only requests
    rather than silently ignoring them. Mixed text+image requests still honor model
    for the text side.

Note: #2407's body floated "clamp-and-warn per #2145" as the preferred direction.
On inspection #2145 only re-normalized below-native truncated embeddings; the only
existing over-dimension precedent is the text 2DMSE reject. This PR follows that
reject precedent (plus an image-aware default) so behavior is consistent across
modalities; the native default keeps the common image request working without a
mandatory explicit dimension.

Changes

  • candle-binding (Rust): reject target_dim > embedding_dim in
    encode_image_with_dim; add multimodal_get_embedding_dim() FFI export.
  • candle-binding (Go): MultiModalGetEmbeddingDim() wrapper + non-cgo mock.
  • Router pkg/apiserver: dimension defaulting/validation, model_used, and
    model/target_layer guards described above; dimension doc comment.

Test Plan

  • Rust unit test on the real multi-modal-embed-small checkpoint (CPU):
    test_image_invalid_dim_rejected — RED before the fix
    (image dim=768 (> native) should be rejected — pre-fix returned the native vector),
    GREEN after.
    MULTIMODAL_MODEL_PATH=<model> cargo test --release --no-default-features \
      test_image_invalid_dim_rejected -- --ignored
    
  • Go apiserver unit tests (pkg/apiserver): native-dimension default, mixed-request
    uniform default, unloaded-model fallback, accept-384, reject-above-native,
    reject target_layer/text-model on image-only, allow multimodal selector, allow text
    model on mixed, model_used propagation, and model-id base-naming. Full package green.
  • cargo build --release (candle/ml/nlp), cargo fmt --check, gofmt -l clean.

Model-gated multimodal tests only execute in CI once MULTIMODAL_MODEL_PATH is wired
there (tracked separately in #2319); the Rust assertion above is #[ignore]-gated for
that reason and was run locally against the real checkpoint.

…ve dim over FFI

encode_image_with_dim silently returned the native 384-dim vector for any
target_dim >= native, unlike the text 2DMSE path which rejects target_dim >
embedding_dim. Mirror that contract so an above-native image request errors
instead of quietly ignoring the requested dimension.

Add multimodal_get_embedding_dim() so the router can enforce the contract
server-side against the model's real native dimension rather than a hardcoded
constant (embedding_dim is overridable via the model config).

Signed-off-by: WUKUNTAI-0211 <83541875+WUKUNTAI-0211@users.noreply.github.com>
Wire the multimodal native-dimension FFI getter into the Go binding (and the
non-cgo mock), returning the loaded model's native embedding dimension or -1
when no multimodal model is loaded.

Signed-off-by: WUKUNTAI-0211 <83541875+WUKUNTAI-0211@users.noreply.github.com>
…1/embeddings

The image path defaulted dimension to 768 (the text default) while the
multimodal model's native dimension is 384, so a default mixed text+image
request returned some 768-dim and some 384-dim vectors in one 200 response,
and 384 was the one value isValidDimension rejected.

Give the image path an explicit contract:
- accept 384 (the multimodal native dimension) in isValidDimension;
- default image-bearing requests to the native dimension (via the FFI getter),
  so plain image and mixed requests return uniform vectors instead of failing
  or silently mixing dimensions;
- reject an explicit above-native dimension for image inputs (4xx), matching
  the text 2DMSE contract;
- report the loaded multimodal model id in model_used instead of the hardcoded
  "multi-modal-embed", derived from the configured path's base name;
- reject target_layer and a text-only model selector on image-only requests
  rather than silently ignoring them.

Signed-off-by: WUKUNTAI-0211 <83541875+WUKUNTAI-0211@users.noreply.github.com>
…e always uses native

Mixed text+image /api/v1/embeddings previously defaulted req.Dimension to
the multimodal native (384) so both sides returned uniform-width vectors.
The rationale was a "response shape is easier to consume" convenience, but
cross-encoder vectors live in different embedding spaces regardless of
dimension: uniform width does not enable cross-modal cosine, so the
consistency gain was illusory while the cost — silently downgrading text
recall from 768 to 384 for every mixed request — was real and hidden.

Revert mixed defaulting so req.Dimension follows the text default (768) and
controls the text side only. The image side always encodes at the
multimodal native dimension in mixed mode, independent of req.Dimension.
Per-item response.dimension already disambiguates modality width for the
caller.

- applyEmbeddingDefaults: mixed and text-only default to defaultEmbeddingDimension;
  only pure image-only requests default to the multimodal native dimension.
- validateEmbeddingRequest: the image-over-native ceiling now applies to
  image-only requests only. Mixed requests may pass a text-legal
  req.Dimension (e.g. 768) without being rejected as image-illegal.
- buildEmbeddingResults: image encode uses a new imageTargetDimension
  helper — req.Dimension for image-only, native for mixed. Text-side
  req.Dimension does not leak into the image encoder.
- config.go: EmbeddingRequest.Dimension doc updated to describe the split
  semantics and the mixed-mode image-native override.
- Tests renamed/rewritten to cover the new invariants: mixed defaults to
  text default, image-only defaults to native, mixed accepts above-native
  req.Dimension, imageTargetDimension resolves per-request correctly, and
  image-only encode honors req.Dimension verbatim.

Signed-off-by: WUKUNTAI-0211 <83541875+WUKUNTAI-0211@users.noreply.github.com>
…ests

Mixed text+image requests resolved the image side to the multimodal native
dimension unconditionally, so an explicit image-satisfiable dimension (e.g.
256 <= native 384) was ignored — the image came back at 384. That reintroduced
the "requested dimension silently dropped" gap this PR set out to close, now
scoped to mixed requests.

Resolve the image side to min(req.Dimension, native): honor an explicitly
requested dimension that fits the image ceiling (both modalities stay uniform),
and only diverge — capping the image at native — when req.Dimension is a
text-scale value above the ceiling (e.g. the 768 default). Text recall is still
preserved in the divergent case.

Signed-off-by: WUKUNTAI-0211 <83541875+WUKUNTAI-0211@users.noreply.github.com>
Use the multimodal native dimension for image-bearing requests, reject unsatisfiable mixed dimensions, and report the registry checkpoint id.\n\nCloses vllm-project#2407

Signed-off-by: WUKUNTAI-0211 <83541875+WUKUNTAI-0211@users.noreply.github.com>
@netlify

netlify Bot commented Jul 22, 2026

Copy link
Copy Markdown

Deploy Preview for vllm-semantic-router ready!

Name Link
🔨 Latest commit 1ef59cc
🔍 Latest deploy log https://app.netlify.com/projects/vllm-semantic-router/deploys/6a6112d0a3353e00086ee43f
😎 Deploy Preview https://deploy-preview-2639--vllm-semantic-router.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

👥 vLLM Semantic Team Notification

The following members have been identified for the changed files in this PR and have been automatically assigned when their GitHub accounts are assignable in this repository:

📁 candle-binding

Owners: @FAUST-BENCHOU, @shraderdm, @drivebyer, @ramkrishs, @WUKUNTAI-0211, @AayushSaini101, @siloteemu
Files changed:

  • candle-binding/semantic-router.go
  • candle-binding/semantic-router_mock.go
  • candle-binding/src/ffi/embedding.rs
  • candle-binding/src/model_architectures/embedding/multimodal_embedding.rs

📁 onnx-binding

Owners: @FAUST-BENCHOU, @shraderdm, @drivebyer, @ramkrishs, @WUKUNTAI-0211, @AayushSaini101, @siloteemu
Files changed:

  • onnx-binding/semantic-router.go

📁 src/semantic-router

Owners: @FAUST-BENCHOU, @shraderdm, @drivebyer, @ramkrishs, @WUKUNTAI-0211, @AayushSaini101, @siloteemu
Files changed:

  • src/semantic-router/pkg/apiserver/config.go
  • src/semantic-router/pkg/apiserver/route_embeddings.go
  • src/semantic-router/pkg/apiserver/route_embeddings_test.go

vLLM Semantic Router

🎉 Thanks for your contributions!

This comment was automatically generated based on the OWNER files in the repository.

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

🚨 Supply Chain Security Report — Issues Found

Scanner Status Findings
AST Codebase Scan (Py, Go, JS/TS, Rust) 🚨 88 finding(s) — HIGH: 7 · MEDIUM: 74 · LOW: 7
AST PR Diff Scan No issues detected
Regex Fallback Scan No issues detected

CRITICAL / HIGH findings in codebase

7 finding(s) — click to expand
Severity File Line Description
🟠 HIGH …er/pr-code/tools/security/ast_security_scanner.py 323 String references sensitive path: 169.254.169.254
🟠 HIGH …er/pr-code/tools/security/ast_security_scanner.py 323 String references sensitive path: 169.254.169.254
🟠 HIGH …er/pr-code/tools/security/ast_security_scanner.py 324 String references sensitive path: metadata.google.internal
🟠 HIGH …er/pr-code/tools/security/ast_security_scanner.py 324 String references sensitive path: metadata.google.internal
🟠 HIGH …er/pr-code/tools/security/ast_security_scanner.py 823 String references sensitive path: 169.254.169.254
🟠 HIGH …er/pr-code/tools/security/ast_security_scanner.py 823 String references sensitive path: 169.254.169.254
🟠 HIGH …er/pr-code/tools/security/ast_security_scanner.py 1176 Function '_check_download_and_execute' contains both download and execute patterns — possible dropper

Action required: CRITICAL and HIGH severity findings must be resolved before merge.


Scanned at 2026-07-22T18:59:23.577Z · View full workflow logs

The over-dimension contract calls candle_binding.MultiModalGetEmbeddingDim,
but the onnx build swaps candle-binding for onnx-binding (go.onnx.mod replace),
which lacked the function -> onnx-tag builds failed with an undefined symbol
(extproc, vllm-sr-cuda/rocm images and every integration-test that builds them).

The ONNX C ABI exposes no dimension getter, so discover the native dimension by
encoding a minimal probe at the native dimension (targetDim=0) and reading the
output length. The value is constant per loaded model, so cache it after the
first successful probe; return -1 when no multimodal model is loaded, matching
candle-binding semantics.

Signed-off-by: WUKUNTAI-0211 <83541875+WUKUNTAI-0211@users.noreply.github.com>
Split the image-specific checks out of validateEmbeddingRequest into
validateImageEmbeddingParams (cyclop 18 -> 11) and extract the dimension
default selection from applyEmbeddingDefaults into defaultEmbeddingDimensionFor
to flatten the nested conditionals (nestif). Behavior is unchanged; satisfies
the repo lint gate (cyclop max 12, nestif).

Signed-off-by: WUKUNTAI-0211 <83541875+WUKUNTAI-0211@users.noreply.github.com>
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.

[Bindings][Router] Multimodal image embeddings ignore requested dimension and return mixed-dimension vectors; settle the over-dimension contract

6 participants