Skip to content

test(multimodal): prove + document client-supplied embeddings via the custom-encoder path#11080

Closed
furionw wants to merge 4 commits into
qiwa/vision-encoder-cuda-graphsfrom
qiwa/vision-encoder-embeds
Closed

test(multimodal): prove + document client-supplied embeddings via the custom-encoder path#11080
furionw wants to merge 4 commits into
qiwa/vision-encoder-cuda-graphsfrom
qiwa/vision-encoder-embeds

Conversation

@furionw

@furionw furionw commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

PR6 of the custom vision-encoder series, stacked on PR4 #11039. Lets a
customer run dynamo.vllm with a stock text-only LM and send pre-computed
per-image embeddings
in the request, where their own small projector maps
embeddings → LM hidden dim and Dynamo splices the result in — no ViT in Dynamo.

Key point: this already works with no new runtime code. RawT on the
VisionEncoderBackend contract is generic, the handler passes the image_url
string to preprocess verbatim, and build_mixed_embeds splices whatever
forward_batch returns. So a client sends each embedding inline as a safetensors
data: URI
on a normal image_url content part (sending directly, not a URL
fetch), and writes a backend that decodes it in preprocess and projects
it in forward_batch. The projector is the client's code. This PR only proves
and documents
that path.

What's here (test + docs only — no feature/contract/handler/frontend change)

  • tests/utils/embeds_passthrough_encoder.py — a test-only stub
    EmbedsPassthroughEncoder (decodes the safetensors data: URI, identity
    passthrough — a real client projects here) + encode_embeds_data_uri /
    decode_embeds_data_uri helpers (the client wire format).
  • test_vllm_embeds_passthrough.py — unit smoke (CPU, no LM): encode/decode
    round-trip preserves shape+dtype; a non-data: URI is rejected (A5 barrier →
    clean request error); AsyncVisionEncoder.encode passthrough; build_mixed_embeds
    splices the decoded embeds at the placeholder rows.
  • examples/custom_encoder/README.md — the client contract: encode embeddings
    as a safetensors data: URI on image_url, plus a ~12-line projector-backend
    skeleton (decode in preprocess, project in forward_batch).
  • report_pytest_markers.py — stub safetensors for the marker hook.

Test plan

  • 9 unit tests (above) — model-free.
  • e2e on a Qwen3-VL-2B box: the client computes embed_tokens(" the Ultimate Question … ") ((11, 2048)), encodes it as a safetensors data: URI, sends it as
    image_url; the stub decodes + identity-passes-through → splice → the LM reads the
    client-supplied embeds → answers "42" (EMBEDS_PATH_OK). This is a scripted
    check rather than a committed serve profile: building the payload needs the LM's
    embed table client-side, which would force a multi-hundred-MB load at pytest
    collection. No Rust → no Graham pass.

Follow-up — PR7: first-class image_embeds field

The documented OpenAI-style contract: an ImageEmbeds content part in
lib/protocols/.../chat.rs, rendered in lib/llm/src/preprocessor.rs
multi_modal_data["image_embeds"], with a worker branch decoding the same bytes.
Heavier (Rust + bindings rebuild + Graham review); reuses this PR's helpers.

Note

Based on the series' validated merge-base; rebase onto main before merge.

🤖 Generated with Claude Code

@github-actions github-actions Bot added test documentation Improvements or additions to documentation backend::vllm Relates to the vllm backend labels Jun 30, 2026
@datadog-official

datadog-official Bot commented Jun 30, 2026

Copy link
Copy Markdown

Pipelines

⚠️ Warnings

🚦 5 Pipeline jobs failed

Docs link check | lychee   View in Datadog   GitHub Actions

PR | deploy-operator   View in Datadog   GitHub Actions

PR | deploy-status-check   View in Datadog   GitHub Actions

View all 5 failed jobs.

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: d3f8bc2 | Docs | Give us feedback!

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

furionw and others added 3 commits June 30, 2026 00:04
…er (no batcher) + e2e

PR2 of the custom vision-encoder series, stacked on PR1. Wires the contract into
the aggregated dynamo.vllm worker and proves the mixed-embeds splice path end to
end with a trivial backend and a DIRECT serial call — no micro-batcher yet.

- AsyncVisionEncoder (serial glue): off-thread preprocess pool + the A5
  all-or-nothing preprocess barrier + a single actor thread running
  build(model_id) / forward_batch / close. Forwards are serialized (no
  cross-request coalescing) — that lands in PR3. validate() fails fast if the
  backend's hardcoded image_token_id is unset/non-int. Stable public surface
  (load / encode / get_image_placeholder_token_id / shutdown).
- handlers.py: resolve a VisionEncoderBackend subclass from --custom-encoder-class,
  wrap in AsyncVisionEncoder, encoder.load(config.model) (no device), assemble the
  mixed EmbedsPrompt; requires --enable-prompt-embeds; image-only + text fallback.
- Examples: QwenVisionEncoderBackend base (hardcodes image_token_id=151655) +
  HitchhikersVisionEncoder (answers "42").
- agg_custom.sh launcher + a minimal qwen_vl.jinja template.
- e2e: the agg_custom topology profile + the "42" semantic payload.

Tests: serial-glue unit tests (encode, A5 barrier, single actor thread, NO
coalescing, fail-fast load + thread reaping). Integrated "42" e2e validated on the
series' integration branch.

Note: hand-set GPU util (reservation is a later milestone). Based on the series'
validated merge-base; rebase onto main before merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…coder (cross-request batching)

PR3 of the custom vision-encoder series, stacked on PR2. Replaces the serial
direct-call glue with cross-request batching to the author's max_batch_cost token
budget (scalar cost, one-dimensional packing — no shape buckets, no graph ladder;
that lands in PR4). handlers.py is unchanged — the AsyncVisionEncoder public
surface is the stable boundary.

- ThreadedMicroBatcher (generic, torch-free): one pinned non-daemon actor thread
  running on_start (build) / every fn(items) / on_stop (close); eager
  drain-on-completion (no timer; max_wait_ms is an opt-in knob); coalesce by
  scalar cost up to max_batch_cost (None = pass-through). Per-item idempotent
  finalizer; cancellation + repeat-cancel retirement; worker supervisor; optional
  max_outstanding_cost admission; cross-loop-safe shutdown. submit(items, costs).
- AsyncVisionEncoder now routes preprocessed items through the batcher (was a
  single-worker serial executor); the A5 preprocess barrier is unchanged; build is
  build(model_id) and the hardcoded image_token_id is validated at load.

Tests: batcher concurrency suite (eager-drain, cost budget, pass-through,
cancellation/retirement, supervisor, lifecycle, on_stop) + batcher-backed glue.

Note: based on the series' validated merge-base; rebase onto main before merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…) + Qwen3-VL ViT example

PR4 of the custom vision-encoder series, stacked on PR3. Introduces bucket-wise
graph support on the Dynamo side and a real graphed encoder example.

- ThreadedMicroBatcher: optional `buckets` ladder. When set, the batcher rounds a
  batch's packed sum(cost) UP to the nearest rung and passes it as `target_bucket`
  to fn(items, target_bucket). With max_batch_cost=None it derives the ceiling as
  max(buckets); buckets=None stays eager / pass-through (PR3 behavior unchanged).
- AsyncVisionEncoder passes backend.buckets to the batcher.
- Qwen3VLViTEncoder example: loads the real Qwen3-VL vision tower (build(model_id),
  picks its own device), captures one CUDA graph per rung via
  torch.compile(reduce-overhead), and in forward_batch pads sum(cost) up to
  target_bucket, replays, slices the real images back out (CPU). Hardcodes
  image_token_id via the Qwen base.

The author owns padding; Dynamo only picks the rung. `buckets`/`target_bucket`
were forward-compat in the contract since PR1; this PR makes them live.

Tests: target_bucket rounding (boundary + eager None + buckets-derive +
ladder-covers-budget). Graph smoke validated on the integration branch.

Deepstack limitation: Qwen3-VL deepstack features are not carried by the
one-tensor-per-image contract — this exercises the mechanism, not accuracy.

Note: based on the series' validated merge-base; rebase onto main before merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@furionw furionw force-pushed the qiwa/vision-encoder-cuda-graphs branch from f853fa4 to aa90a19 Compare June 30, 2026 07:12
… custom-encoder path

The in-process custom-encoder path already carries client-supplied per-image
embeddings with NO new runtime code: RawT is generic, the handler passes the
(data:) URL string to preprocess verbatim, and build_mixed_embeds splices whatever
forward_batch returns. A client sends embeddings inline as a safetensors data: URI
on image_url and writes a VisionEncoderBackend that decodes (preprocess) + projects
(forward_batch). This PR proves and documents that path — no feature code.

- tests/utils/embeds_passthrough_encoder.py: a TEST-ONLY stub
  EmbedsPassthroughEncoder (decode the safetensors data: URI, identity passthrough)
  + encode/decode_embeds_data_uri helpers (the client wire format). Hardcodes the
  image token id via the Qwen base.
- tests/.../test_vllm_embeds_passthrough.py: encode/decode round-trip; non-data URI
  rejected (A5 barrier); AsyncVisionEncoder.encode passthrough; build_mixed_embeds
  splice. CPU, no LM.
- examples/custom_encoder/README.md: the client contract (safetensors data: URI on
  image_url) + a projector-backend skeleton (decode in preprocess, project in
  forward_batch, hardcode image_token_id). Notes the planned image_embeds field.
- report_pytest_markers.py: stub safetensors for the marker hook.

Verified: 9 unit tests; e2e on Qwen3-VL-2B — client computes embed_tokens(phrase)
(11x2048) -> safetensors data: URI -> stub decode+passthrough -> splice -> LM -> "42".

Note: based on the series' validated merge-base; rebase onto main before merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@furionw furionw force-pushed the qiwa/vision-encoder-embeds branch from 59ade3d to d3f8bc2 Compare June 30, 2026 07:14
@furionw furionw force-pushed the qiwa/vision-encoder-cuda-graphs branch from aa90a19 to fc9b5d2 Compare June 30, 2026 17:08
@furionw furionw closed this Jul 7, 2026
@furionw furionw deleted the qiwa/vision-encoder-embeds branch July 7, 2026 20:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend::vllm Relates to the vllm backend documentation Improvements or additions to documentation size/L test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant