Skip to content

perf(tokenspeed): optimize multimodal tensor transport - #1604

Merged
slin1237 merged 28 commits into
smg-project:mainfrom
yechank-nvidia:yechan/mm-transport-opt
Jun 22, 2026
Merged

perf(tokenspeed): optimize multimodal tensor transport#1604
slin1237 merged 28 commits into
smg-project:mainfrom
yechank-nvidia:yechan/mm-transport-opt

Conversation

@yechank-nvidia

@yechank-nvidia yechank-nvidia commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Description

Problem

Two multimodal issues on the SMG ↔ TokenSpeed path:

  1. Tensor-transport overhead. Large precomputed multimodal encoder inputs (image/video pixel_values/embeddings) are serialized inline over gRPC, adding serialization + memory overhead under high-concurrency image/video load, and shared-memory payloads had no cleanup if a request failed mid-construction.

  2. Image preprocessing diverged from the HF/PIL pipeline vLLM uses, which cost accuracy and speed:

    • The eval harness sends chat content as [text, image]; SMG rendered the image after the whole question (content order), TokenSpeed was ~7pp below vLLM on MMBench(image).
    • JPEG decode + bicubic resize were not bit-for-bit identical to Pillow,
    • The Rust preprocessing (resize/normalize/patchify) ran single-threaded and was slower than HF transformers on large images.

Solution

  • Transport: route large tensor payloads through shared memory instead of inline — inline-vs-SHM-capable storage, SHM payload creation above a configurable threshold, env controls, reduced-precision encoder-input dtypes, stronger validation, SHM cleanup on request/send failure, inline kept as the default fallback. (Builds on the generalized multimodal ABI + video/Qwen work.)
  • Preprocessing parity (accuracy): bring SMG's image path to bit-for-bit parity with HF/PIL — hoist image/video placeholders to the front of chat content, libjpeg-turbo decode (Pillow chroma upsampling), Pillow-exact BICUBIC resize, and HF-equivalent Qwen3-VL preprocess.
  • Preprocessing performance: parallelize resize, normalize, and patchify with std::thread::scopebit-identical (each output element/block is independent; serial fallback for small images).

Changes

Transport

  • grpc_servicer/.../tokenspeed/servicer.py: SHM tensor read + ShmTensorHandle, unlink-after-read, validation, MM timing logs
  • model_gateway/.../grpc/{multimodal,proto_wrapper,client}.rs: SHM-capable tensor storage, threshold/env controls, reduced-dtype payloads, failure cleanup
  • model_gateway/.../observability/metrics.rs: smg_* MM-transport metrics
  • crates/grpc_client/proto/tokenspeed_scheduler.proto: payload field(s)

Preprocessing parity & accuracy

  • model_gateway/.../grpc/utils/chat_utils.rs (transform_content_field): media-first content ordering for String + OpenAI formats; TODO(interleave)for interleave_mm_strings opt-out
  • crates/multimodal/src/jpeg_turbo.rs (+ build.rs, lib.rs, media.rs): libjpeg-turbo RGB decode FFI (PIL-compatible)
  • crates/multimodal/src/vision/transforms.rs: Pillow-exact resize_bicubic_pil + parallelized resize/normalize
  • crates/multimodal/src/vision/processors/qwen_vl_base.rs: HF-equivalent Qwen3-VL preprocess + parallelized patchify

Tests

  • tests/resize_fingerprint.rs, tests/preprocess_fingerprint.rs: bit-identity guards (fnv) — parallel output == serial, byte-for-byte
  • tests/decode_preprocess_bench.rs: SMG-vs-HF decode/preprocess microbench

Test Plan

Accuracy (MMBench_DEV_EN_V11, 397B-A17B-NVFP4, 8×B200): image-first fix recovers TS 87% → 96% (150-q sample, 0 errors), matching/exceeding vLLM(94%); e.g. idx 3508 (key C) flips from wrong A (image-last) to correct C.

Parity: resize_bicubic_pil_bit_identity + preprocess_bit_identity assert the parallel path reproduces the serial output bit-for-bit; golden parity vs HF unchanged.

Preprocess speed (Qwen3-VL, 224-core host): decode at parity with PIL (both libjpeg-turbo); preprocess 1280×960 15.5→9.0 ms, 1920×1440 49.6→28.6 ms, 12MP 862→173 ms — faster than HF on 1280²/12MP, all bit-identical.

Checklist
  • cargo +nightly fmt passes
  • cargo clippy --all-targets --all-features -- -D warnings passes
  • (Optional) Documentation updated
  • (Optional) Please join us on Slack #sig-smg to discuss, review, and merge PRs

Summary by CodeRabbit

Release Notes

  • New Features
    • Added a turbo-first JPEG decoding fast path (when available) with exported JPEG helpers.
    • Added deterministic, Pillow-aligned bicubic resizing APIs and resize_to_fit.
    • Introduced opt-in SHM-backed multimodal tensor transport for TokenSpeed encoder inputs.
  • Performance Improvements
    • Faster, more deterministic vision preprocessing via improved parallelism and byte-identical resize/patch behavior.
  • Bug Fixes
    • Safer SHM lifecycle handling with improved validation and automatic fallback to inline on SHM write failures.
  • Observability
    • Added optional multimodal timing logs and new tensor/SHM write-failure Prometheus metrics.
  • Behavior Changes
    • Updated chat prompt rendering to place media before text.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds SHM-backed tensor transport for TokenSpeed (gateway write + servicer read), PIL-exact bicubic resize and libjpeg-turbo decode for Qwen-VL preprocessing, media-before-text chat reordering, and comprehensive timing/metrics instrumentation across all transport and preprocessing paths.

Changes

TokenSpeed SHM Transport, PIL Preprocessing, and Media Ordering

Layer / File(s) Summary
Proto comments and TokenSpeedTensor data model
crates/grpc_client/proto/tokenspeed_scheduler.proto, model_gateway/src/routers/grpc/proto_wrapper.rs
Updates proto oneof payload comments to clarify shm vs remote transport; introduces TokenSpeedTensor, TokenSpeedTensorStorage (Inline/Shm variants), and per-request shm_enabled flag on TokenSpeedMultimodalData/Item as the typed tensor-transport contract.
SHM environment config, writability, orphan cleanup, and handle lifecycle
model_gateway/src/routers/grpc/proto_wrapper.rs
Adds environment accessors for transport mode and min-bytes threshold, one-time /dev/shm writability probe via OnceLock, orphan SHM segment sweep for dead producer PIDs, unique segment naming, collection of handles from proto messages, and best-effort cleanup with name validation.
SHM segment write with validation and tensor payload selection
model_gateway/src/routers/grpc/proto_wrapper.rs
Implements write_tokenspeed_shm_with for segment creation, buffered writing, flush, and length validation with rollback; adds tensor_bytes_to_tokenspeed and tokenspeed_tensor_payload logic to select inline vs SHM based on shm_enabled, threshold, and write success with fallback and metrics recording.
Request finalization and error-path cleanup
model_gateway/src/routers/grpc/proto_wrapper.rs
Updates finish_tokenspeed_request to clean up SHM refs on build failure; adds cleanup_tokenspeed_items_encoder_shm for partial-item rollback; updates unit tests for inline and SHM encoding paths, including new test asserting SHM-backed encoder produces Payload::Shm.
Multimodal processing phase timing and consolidated logging
model_gateway/src/routers/grpc/multimodal.rs
Instruments media fetch, config/spec lookup, preprocessing, token expansion, and total processing phases in process_multimodal_parts; logs all measured durations plus derived media/token counts as single consolidated info log when SMG_LOG_MM_TIMING is enabled.
Router TokenSpeed assembly with SHM resolution and per-item timing
model_gateway/src/routers/grpc/multimodal.rs
Resolves per-request shm_enabled using worker locality and /dev/shm sharing detection; refactors item assembly into imperative loop with dtype-aware tensor encoding, SHM direct-write fast path, per-item timing, and targeted SHM cleanup on failure.
Tensor serialization with dtype wire formats and SHM fast path
model_gateway/src/routers/grpc/multimodal.rs
Updates serialize_array to preserve row-major order for non-C-contiguous tensors; adds serialize_array_as_tokenspeed_tensor selecting inline vs SHM; introduces serialize_array_as_u16_bytes for endianness-correct float16/bfloat16 conversion.
SHM transport resolution and worker locality detection
model_gateway/src/routers/grpc/multimodal.rs
Implements resolve_tokenspeed_shm_enabled with mode-based selection and /dev/shm sharing checks; includes one-time config logging; computes local SHM namespace identity via boot_id and st_dev; adds Linux-specific tests for namespace format.
Gateway client SHM cleanup, metrics, and configuration
model_gateway/src/routers/grpc/client.rs, model_gateway/src/observability/metrics.rs, docs/reference/configuration.md
Updates GrpcClient TokenSpeed paths to collect and cleanup SHM handles on error; extends ServerInfo label extraction for shm_namespace_id; registers tensor transport Prometheus counters; documents router/servicer SHM environment variables.
TokenSpeed servicer SHM read, validation, and timing
grpc_servicer/smg_grpc_servicer/tokenspeed/servicer.py
Reads SHM payloads via os.pread with length validation and optional unlink; returns ShmTensorHandle for zero-copy constraints; adds multimodal build and first-chunk streaming latency logs; computes /dev/shm namespace identity for worker locality verification.
libjpeg-turbo FFI runtime decode
crates/multimodal/Cargo.toml, crates/multimodal/src/jpeg_turbo.rs, crates/multimodal/src/lib.rs, crates/multimodal/src/media.rs
Adds libloading dependency; implements is_jpeg SOI-marker detection and decode_jpeg_rgb via dlopen FFI with OnceLock process-wide caching; integrates as preferred decode fast path in decode_image before ImageReader fallback.
PIL-exact bicubic resize and parallel deinterleave
crates/multimodal/src/vision/transforms.rs
Adds par_threads heuristic and parallelizes deinterleave_rgb_to_planes; implements fixed-point PIL-exact bicubic (kernel precomputation, 8-bit clipping, deterministic two-pass resampling); exposes resize_bicubic_pil, resize_bicubic_pil_rgb, and resize_to_fit with validation and parity tests.
Qwen preprocessing: PIL defaults and resize dispatch
crates/multimodal/src/vision/processors/qwen_vl_base.rs
Pins default resampling filter to PIL BICUBIC (value 3) across all preprocessing paths; dispatches resize_bicubic_pil only for CatmullRom, otherwise uses generic resize; rewrites patchify_into to parallel block-band strategy via thread::scope; adds parity test between dynamic and raw-RGB paths.
Media-first chat content ordering
model_gateway/src/routers/grpc/utils/chat_utils.rs
Rewrites transform_content_field to hoist media placeholders before text for String and OpenAI formats; updates documentation and tests to enforce stable media-first ordering across single and multi-media scenarios.
Fingerprint guards and decode/preprocess benchmarks
crates/multimodal/tests/*
Adds resize_bicubic_pil_bit_identity and preprocess_bit_identity fingerprint guard tests with deterministic generators and capture-mode baseline collection; adds ignored bench_decode_preprocess microbenchmark for real JPEG + Qwen3-VL timing.

Sequence Diagram(s)

sequenceDiagram
    participant Client as GrpcClient
    participant MW as multimodal.rs
    participant PW as proto_wrapper.rs
    participant SHM as /dev/shm
    participant TS as TokenSpeed Servicer

    Client->>MW: assemble_tokenspeed(items, worker_url)
    MW->>PW: resolve_tokenspeed_shm_enabled(worker_url)
    PW-->>MW: shm_enabled
    loop per encoder item
        MW->>PW: serialize_array_as_tokenspeed_tensor(array, shm_enabled)
        alt shm_enabled and bytes >= threshold
            PW->>SHM: write_tokenspeed_shm_with(nbytes, write_fn)
            SHM-->>PW: ShmHandle or error
            alt write succeeded
                PW->>MW: record_mm_tensor(shm)
                PW-->>MW: TokenSpeedTensor::Shm
            else write failed
                PW->>MW: record_mm_shm_write_failure()
                PW-->>MW: TokenSpeedTensor::Inline fallback
            end
        else skip SHM attempt
            PW->>MW: record_mm_tensor(inline)
            PW-->>MW: TokenSpeedTensor::Inline
        end
    end
    MW-->>Client: TokenSpeedMultimodalData{shm_enabled}
    Client->>TS: generate RPC with Payload::Shm or Payload::Inline
    alt Payload::Shm
        TS->>SHM: os.pread(shm_name, nbytes)
        SHM-->>TS: tensor bytes
        TS->>TS: validate byte length
        TS->>SHM: optional unlink(shm_name)
    else Payload::Inline
        TS->>TS: use inline bytes directly
    end
    TS-->>Client: stream response
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

Possibly related PRs

  • lightseekorg/smg#1515: This PR directly extends the TokenSpeed multimodal tensor wiring introduced in that PR by implementing full SHM-backed payload handling with lifecycle cleanup in proto_wrapper, client, and router assembly.
  • lightseekorg/smg#1602: Both PRs modify the TensorData oneof payload contract for shm/remote in the TokenSpeed proto and corresponding conversion code; this PR adds the full SHM read/write infrastructure on top of that contract.
  • lightseekorg/smg#1012: Both PRs modify deinterleave_rgb_to_planes and related tensor-conversion logic in crates/multimodal/src/vision/transforms.rs; this PR adds conditional parallelization and PIL-exact bicubic resize on top.

Suggested reviewers

  • key4ng
  • slin1237
  • CatherineSue

Poem

🐇 Hops along the SHM lane,
No more slow inline bytes again!
PIL bicubic, turbo-decoded art,
Media first—a brand new start!
Metrics counting every frame,
The rabbit's preprocessing game. 🎨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'perf(tokenspeed): optimize multimodal tensor transport' accurately and specifically describes the main optimization objective—improving multimodal tensor transport performance in TokenSpeed integration.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added python-bindings Python bindings changes dependencies Dependency updates ci CI/CD configuration changes grpc gRPC client and router changes docker Docker configuration changes multimodal Multimodal crate changes model-gateway Model gateway crate changes labels Jun 5, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces comprehensive video and multimodal processing support across the repository. Key changes include updating the gRPC protobuf definitions to support itemized multimodal payloads (images, audio, and video) and shared memory (SHM) tensor transport, implementing video decoding backends using OpenCV and FFmpeg in the multimodal crate, and updating the Qwen3-VL model spec to handle video pad replacements. Additionally, the Python gRPC servicer and Rust model gateway are updated to support SHM-based tensor transport and video preprocessor configurations. Feedback on the changes highlights a performance issue in the OpenCV video decoding loop, where failing to break early on a failed frame grab can lead to redundant and blocking operations.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread crates/multimodal/src/media.rs Outdated
@lightseek-bot

Copy link
Copy Markdown
Collaborator

Hi @yechank-nvidia may you help fix the conflicts

@yechank-nvidia
yechank-nvidia force-pushed the yechan/mm-transport-opt branch from 25cc70d to 0e6246a Compare June 21, 2026 15:09
@github-actions github-actions Bot added tests Test changes and removed python-bindings Python bindings changes dependencies Dependency updates ci CI/CD configuration changes docker Docker configuration changes labels Jun 21, 2026
@yechank-nvidia
yechank-nvidia force-pushed the yechan/mm-transport-opt branch from 0e6246a to c8eb9ea Compare June 21, 2026 15:22
@yechank-nvidia
yechank-nvidia marked this pull request as ready for review June 21, 2026 15:22
@mergify

mergify Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Hi @yechank-nvidia, this PR has merge conflicts that must be resolved before it can be merged. Please rebase your branch:

git fetch origin main
git rebase origin/main
# resolve any conflicts, then:
git push --force-with-lease

@mergify mergify Bot added needs-rebase PR has merge conflicts that need to be resolved and removed needs-rebase PR has merge conflicts that need to be resolved labels Jun 21, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8eb9ea2d9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/routers/grpc/multimodal.rs Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/multimodal/src/vision/processors/qwen_vl_base.rs (1)

746-747: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Use PIL-exact bicubic in video resize paths when filter is CatmullRom.

Line 746 and Line 882 still use non-PIL resize paths for bicubic (CatmullRom). That breaks the parity contract now enforced in image preprocessing and can shift video/video-rgb encoder inputs.

Suggested direction
- let resized = resize(frame, tw32, th32, filter);
+ let resized = if filter == FilterType::CatmullRom {
+     resize_bicubic_pil(frame, tw32, th32)
+ } else {
+     resize(frame, tw32, th32, filter)
+ };
- let resized = resize_rgb_bytes(
-     frame.data, frame.width, frame.height, tw32, th32, filter,
- )?;
+ let resized = if filter == FilterType::CatmullRom {
+     // Convert bytes -> RGB image -> PIL-exact resize for parity path.
+     let src = image::RgbImage::from_raw(frame.width, frame.height, frame.data.to_vec())
+         .ok_or_else(|| TransformError::ShapeError("invalid RGB frame buffer".to_string()))?;
+     resize_bicubic_pil(&DynamicImage::ImageRgb8(src), tw32, th32).to_rgb8()
+ } else {
+     resize_rgb_bytes(frame.data, frame.width, frame.height, tw32, th32, filter)?
+ };

Also applies to: 882-889

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/multimodal/src/vision/processors/qwen_vl_base.rs` around lines 746 -
747, The resize calls using the filter parameter at the locations where bicubic
filtering (CatmullRom) is requested are not using PIL-exact bicubic resizing,
which breaks parity with the image preprocessing contract. When the filter
parameter is CatmullRom, replace the current resize function call with a
PIL-exact bicubic resize implementation instead of using the generic resize
path. Apply this fix to both occurrences of the resize calls (around line 746 in
the main path and around line 882 in the alternate path) to ensure consistent
bicubic filtering behavior across video and image preprocessing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/multimodal/build.rs`:
- Line 5: The println statement in build.rs contains a hardcoded library path
that only works on x86_64 Debian/Ubuntu Linux systems. Replace this hardcoded
path with platform-aware logic that detects the target OS and architecture at
build time. Use cfg attributes (such as cfg(target_os = "...") and
cfg(target_arch = "...")) to conditionally set the correct library search path
for each supported platform, or alternatively use pkg-config to dynamically
locate the turbojpeg library. This ensures the build succeeds on macOS, Windows,
aarch64 Linux, and other supported platforms with appropriate paths for each
system.

In `@crates/multimodal/src/jpeg_turbo.rs`:
- Around line 62-63: The SAFETY comment in the unsafe block needs to be expanded
to explicitly document all the assumptions that make the unsafe operation sound.
Currently it covers the handle null check and buffer sizing, but you need to add
explicit explanations for: why the buf pointer is valid and safe to pass to the
FFI function (it comes from a valid, owned Vec), why there is no aliasing
concern during the FFI call, and how the return value check guarantees that no
out-of-bounds write occurred. Expand the existing SAFETY comment to
comprehensively cover all three of these invariants in addition to what is
already documented.

In `@crates/multimodal/tests/decode_preprocess_bench.rs`:
- Around line 18-20: The REAL_JPEG environment variable uses a hardcoded
machine-specific fallback path that causes unpredictable test failures outside
that particular environment. Replace the unwrap_or_else call on the REAL_JPEG
variable lookup with an expect call that requires the environment variable to be
explicitly set, following the same pattern used for PP_CONFIG, and provide a
clear error message instructing users to set the REAL_JPEG environment variable.

In `@model_gateway/src/routers/grpc/multimodal.rs`:
- Around line 1463-1471: The direct SHM writer path allocates a full Vec<u16>
before writing, causing excessive memory usage for large inputs. Instead of
collecting all conversions at once with `encoder_slice.iter().map(|&value|
convert(value)).collect()`, implement chunked processing similar to the
non-contiguous path by iterating through the encoder slice in manageable chunks,
converting each chunk to u16 values, and writing each chunk to the writer via
`writer.write_all(bytemuck::cast_slice(...))` separately. This approach keeps
peak memory bounded while maintaining the same functionality for the
little-endian target configuration.

In `@model_gateway/src/routers/grpc/proto_wrapper.rs`:
- Around line 431-438: The TokenSpeed SHM file creation in the OpenOptions
configuration uses create(true).truncate(true) which is vulnerable to symlink
attacks and file clobbering in world-writable directories. Replace
create(true).truncate(true) with create_new(true) to ensure exclusive file
creation that fails if the file already exists. Additionally, add explicit file
permission settings to restrict access to the owner only (using 0o600 on Unix
systems). This security fix should be applied to all SHM file creation
locations, including the probe file creation block starting around line 431 and
the other location around lines 507-510. Consider centralizing this SHM file
creation logic into a reusable function that consistently applies these security
measures across all instances.

In `@model_gateway/src/routers/grpc/utils/chat_utils.rs`:
- Around line 246-252: The pattern match in the String format branch is missing
audio_url handling. Currently, the match statement at line 246 only handles
image_url and video_url, causing audio_url to fall through to the _ case and be
silently dropped. Add audio_url to the pattern match alongside image_url and
video_url so that audio parts are also added to media_parts using the
image_placeholder, ensuring consistent handling with the OpenAI format branch at
line 271. This prevents silent data loss when messages contain audio content.

---

Outside diff comments:
In `@crates/multimodal/src/vision/processors/qwen_vl_base.rs`:
- Around line 746-747: The resize calls using the filter parameter at the
locations where bicubic filtering (CatmullRom) is requested are not using
PIL-exact bicubic resizing, which breaks parity with the image preprocessing
contract. When the filter parameter is CatmullRom, replace the current resize
function call with a PIL-exact bicubic resize implementation instead of using
the generic resize path. Apply this fix to both occurrences of the resize calls
(around line 746 in the main path and around line 882 in the alternate path) to
ensure consistent bicubic filtering behavior across video and image
preprocessing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 45317c29-d6ab-477b-9b46-0e874c98b0ec

📥 Commits

Reviewing files that changed from the base of the PR and between 85cd02a and c8eb9ea.

📒 Files selected for processing (16)
  • crates/grpc_client/proto/tokenspeed_scheduler.proto
  • crates/multimodal/build.rs
  • crates/multimodal/src/jpeg_turbo.rs
  • crates/multimodal/src/lib.rs
  • crates/multimodal/src/media.rs
  • crates/multimodal/src/vision/processors/qwen_vl_base.rs
  • crates/multimodal/src/vision/transforms.rs
  • crates/multimodal/tests/decode_preprocess_bench.rs
  • crates/multimodal/tests/preprocess_fingerprint.rs
  • crates/multimodal/tests/resize_fingerprint.rs
  • grpc_servicer/smg_grpc_servicer/tokenspeed/servicer.py
  • model_gateway/src/observability/metrics.rs
  • model_gateway/src/routers/grpc/client.rs
  • model_gateway/src/routers/grpc/multimodal.rs
  • model_gateway/src/routers/grpc/proto_wrapper.rs
  • model_gateway/src/routers/grpc/utils/chat_utils.rs

Comment thread crates/multimodal/build.rs Outdated
Comment thread crates/multimodal/src/jpeg_turbo.rs Outdated
Comment thread crates/multimodal/tests/decode_preprocess_bench.rs Outdated
Comment thread model_gateway/src/routers/grpc/multimodal.rs Outdated
Comment thread model_gateway/src/routers/grpc/proto_wrapper.rs
Comment thread model_gateway/src/routers/grpc/utils/chat_utils.rs Outdated

// TokenSpeed multimodal tensor transport (shm vs inline)
describe_counter!(
"smg_tokenspeed_mm_tensors_total",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we should not hardcode a metrics to a runtime specific. And runtime already has a label. Please change this

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thx for the comment.

Renamed the counters to neutral names (smg_mm_tensors_total, smg_mm_tensor_bytes_total, smg_mm_shm_write_failures_total) and moved the engine onto a runtime label, matching the existing smg_pd_* runtime-labeled metrics.

record_mm_tensor / record_mm_shm_write_failure now take a runtime arg; the TokenSpeed call sites pass "tokenspeed".

_ => unreachable!("caller guarantees matching variant"),
});
let req = client.build_generate_request_from_messages(
let shm_handles = tokenspeed_mm

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same here. Only protocol can be engine specific. Everywhere else should be neutral. Tho there can be a case where one engine has a feature implemented and others don't. But the code should be neutral

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed both — metrics are now engine-neutral with a runtime label, and the TokenSpeed SHM lifecycle moved into the protocol layer so the dispatch is neutral.

@mergify

mergify Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Hi @yechank-nvidia, this PR has merge conflicts that must be resolved before it can be merged. Please rebase your branch:

git fetch origin main
git rebase origin/main
# resolve any conflicts, then:
git push --force-with-lease

@mergify mergify Bot added the needs-rebase PR has merge conflicts that need to be resolved label Jun 21, 2026
Add counters so the shm-vs-inline transport split is observable:
  - smg_tokenspeed_mm_tensors_total{path}        tensors sent per path
  - smg_tokenspeed_mm_tensor_bytes_total{path}   bytes sent per path
  - smg_tokenspeed_mm_shm_write_failures_total   SHM writes that fell back to inline

Each tensor is metered exactly once: encoder inputs written directly to SHM are
counted at proto conversion (Shm storage arm); all other tensors (inline-storage
encoders + model_specific) are counted inside tokenspeed_tensor_payload. Exposed
on the existing Prometheus endpoint (:29000/metrics).

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
Qwen2VL/Qwen3VL HF image processors default to BICUBIC (PIL resample=3) when
the preprocessor config omits `resample`, but SMG's pil_to_filter fell back to
bilinear (Triangle). Bilinear produces smoother encoder-input features; against
vLLM on Qwen3.5-397B-A17B-NVFP4 / MMBench the SMG pixel_values matched HF only
at corr 0.9994 (max abs diff 0.23). Pinning bicubic for the Qwen path raises the
match to corr 0.99996 (max 0.04), aligning SMG with the reference HF/vLLM
preprocessing.

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
The eval harness sends multimodal content as [text, image]. SMG rendered the
content parts in order, so the image landed AFTER the whole question — which
measurably degrades VQA grounding (MMBench answers flip vs. image-first). vLLM
always prepends media placeholders to the front (its default
interleave_mm_strings=false); match that.

transform_content_field now emits media (image/video/audio) before text in both
content formats: OpenAI uses a stable partition; String collects placeholders
first then text, joined by "\n". No-op for text-only/string content and content
already media-first. A TODO(interleave) documents how to thread vLLM's
interleave_mm_strings opt-out if ever needed.

Verified: 10 unit tests plus an e2e render test against the real Qwen3.5
chat_template.jinja (image now at char 17 < question at char 60). End-to-end
MMBench_DEV_EN_V11 (150-q sample) recovered 87% -> 96%.

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
The Pillow-exact BICUBIC resize (resize_bicubic_pil), used for vLLM/PIL
preprocessing parity, ran both passes single-threaded. On large images its
scalar fixed-point arithmetic dominated preprocessing (≈4x slower than HF on
12MP). Each output row is an independent fixed-point integer sum, so band the
horizontal/vertical passes across threads with std::thread::scope: identical
arithmetic and inner-sum order => BIT-IDENTICAL output, no new dependency.
Small images stay serial (par_threads gates on output size / rows-per-thread)
to avoid spawn overhead.

Accuracy is preserved unconditionally: resize_fingerprint.rs pins the exact
byte output (fnv1a) of the serial implementation across up/downscale cases and
asserts the parallel version reproduces it bit-for-bit. Golden parity
(qwen35_parity) diffs vs HF are unchanged.

Measured (Qwen3-VL preprocess, 224-core host): 12MP 862 -> 345 ms (2.5x),
small/medium unchanged (resize not the hotspot there).

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
…rity

Bring SMG's image path to bit-for-bit parity with the HF/PIL pipeline that
vLLM uses, so vision-encoder inputs match and image accuracy is preserved:

- jpeg_turbo: minimal libjpeg-turbo (TurboJPEG) FFI for RGB JPEG decode,
  matching Pillow's chroma-upsampling (the pure-Rust decoder differs by a few
  levels and shifts embeddings). build.rs links turbojpeg; media.rs routes
  JPEG decode through it (falling back to the `image` crate otherwise).
- qwen_vl_base: Qwen3-VL preprocess (smart_resize, grid/token calc, fused
  normalize, patchify) producing HF-equivalent pixel_values + image_grid_thw.

Pairs with the Pillow-exact BICUBIC resize already in transforms.rs.

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
Profiling the preprocess on full-resolution images (this config does not
downscale) showed the hotspot was normalize + patchify, not resize:
1920x1440 = normalize 23.9ms + patchify 26.2ms (resize skipped). Both stages
are elementwise / per-block independent, so band them across threads with
std::thread::scope -- identical arithmetic and write order => BIT-IDENTICAL:

- deinterleave_rgb_to_planes (normalize): split the pixel range; each output
  element depends only on its own input byte. 7.5-20x faster.
- patchify_into: split the (gt,pr,pc) blocks; each writes a contiguous,
  deterministic output region of pure copies (memory-bound, ~1.2-1.4x).

par_threads gates on output size / rows-per-thread so small images stay
serial. preprocess_fingerprint.rs pins the exact f32 encoder_input bytes and
asserts the parallel path reproduces the serial output bit-for-bit (accuracy
preserved unconditionally). decode_preprocess_bench.rs is the SMG-vs-HF harness.

Measured (Qwen3-VL preprocess, 224-core host): 1280x960 15.5->9.0ms,
1920x1440 49.6->28.6ms, 12MP 862->173ms -- now faster than HF on 1280/12MP.

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
…re REAL_JPEG

- build.rs: derive the libturbojpeg search dir from the target arch (x86_64 /
  aarch64 Linux) and only add it when it exists, instead of hardcoding
  /usr/lib/x86_64-linux-gnu, which broke aarch64/macOS/Windows builds. The
  link-lib directive is always emitted; other platforms resolve via the default
  linker search path. (coderabbitai)
- jpeg_turbo.rs: expand the SAFETY comment to cover handle lifetime, input
  buffer validity/no-aliasing, output buffer sizing, and the rc==0 guard.
  (coderabbitai)
- decode_preprocess_bench.rs: require REAL_JPEG explicitly (like PP_CONFIG)
  instead of a machine-specific fallback path. (coderabbitai)

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
…ly abort

- proto_wrapper.rs: create TokenSpeed SHM files with create_new(true) (no
  clobber/symlink in world-writable /dev/shm) and owner-only 0o600 mode
  (cfg(unix)), for both the writability probe and the payload writer.
  (coderabbitai, Major)
- multimodal.rs / proto_wrapper.rs: assemble_tokenspeed now builds items
  imperatively and, if any item fails after its encoder input was serialized to
  SHM, unlinks the already-created /dev/shm segments (prior items + the pending
  tensor) before returning. Previously the partial TokenSpeedTensor::Shm handles
  were dropped without reaching the send-path cleanup, leaking files on repeated
  malformed/unsupported multimodal requests. (chatgpt-codex, P2)

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
…us path

write_array_as_u16's contiguous fast path converted the whole bf16/f16 tensor
into a single Vec<u16> before writing, so peak memory scaled with encoder-input
size. Convert in CHUNK_VALUES-sized blocks (reusing one buffer) like the strided
path already does, bounding peak conversion memory to ~512 KiB regardless of
tensor size. (coderabbitai)

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
CI failed linking the Go/Python bindings: `cargo:rustc-link-lib=turbojpeg`
made libturbojpeg a hard link-time dependency for every consumer of
llm-multimodal, but the binding/CI runners don't ship it
(`rust-lld: unable to find library -lturbojpeg`). The earlier build.rs path
tweak didn't help — the link requirement itself was the problem.

Load libturbojpeg via dlopen (libloading) at first use instead: try the runtime
soname then the dev/macOS names, resolve the four TurboJPEG symbols, and cache
the handle. No build script, no link-time dependency, so the crate and all
consumers compile on any platform. Where the library is present (the serving
image) decode still goes through it for PIL/vLLM parity; where it's absent
decode_jpeg_rgb returns None and the caller falls back to the pure-Rust decoder.

Removes build.rs and the #[link]/extern block; adds libloading. Verified the
runtime path still decodes (3508.jpg 259x194, ~0.1ms/img).

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
…in1237)

Per review: SMG should stay engine-neutral; only the protocol layer may be
engine-specific.

- metrics: rename the runtime-baked counters smg_tokenspeed_mm_* ->
  smg_mm_{tensors,tensor_bytes,shm_write_failures}_total and carry the engine as
  a `runtime` label (matching the existing runtime-labeled metrics) instead of
  the metric name. record_mm_tensor / record_mm_shm_write_failure take
  `runtime`; TokenSpeed call sites pass "tokenspeed".
- client.rs: the per-engine build_chat/build_messages dispatch arms no longer
  inline TokenSpeed SHM-handle collection + error-path cleanup. That
  engine-specific SHM lifecycle moves into proto_wrapper::finish_tokenspeed_request
  (the protocol layer), so the dispatch arms are thin like the other engines'.

No behavior change (88 grpc tests pass); metric series are renamed + gain a
runtime label.

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
Clippy (deny warnings) flagged the new multimodal code:
- use #[expect(clippy::too_many_arguments, reason=...)] instead of #[allow]
  for the band/patchify helpers (allow_attributes lint)
- par_threads: .min(MAX).max(1) -> .clamp(1, MAX) (manual_clamp)
- add `;` to the thread-spawn closures (semicolon_if_nothing_returned)
- resize_bicubic_pil: gate the construction expect() with
  #[expect(clippy::expect_used, reason=...)] (buffer is sized by construction)
- bring Metrics into scope at the two MM-metric call sites instead of fully
  qualifying (proto_wrapper, multimodal)
- de-qualify HashMap and drop diagnostic eprintln! in the chat-template e2e test
- fingerprint/bench tests: crate-level allow for unwrap/expect/print in tests;
  iterate to_le_bytes() by value

No behavior change.

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
- qwen_vl: route default-bicubic video frame resizing through the PIL-exact
  path (resize_bicubic_pil / new resize_bicubic_pil_rgb) in both preprocess_video
  and preprocess_video_rgb, matching the image path so video encoder inputs equal
  HF/vLLM bit-for-bit instead of diverging to the SIMD CatmullRom resizer.
- grpc: do not infer a shared /dev/shm from TCP loopback. auto-mode SHM now
  requires a unix-domain-socket worker (proven same-host) or an explicit
  SMG_TOKENSPEED_SHM_ASSUME_LOOPBACK_SHARED operator assertion; loopback alone
  falls back to inline. Explicit shm mode is unchanged.
- grpc: SHM cleanup only unlinks names carrying the smg-tokenspeed- prefix this
  transport creates, never arbitrary top-level /dev/shm entries.

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
The String chat-content branch only matched image_url/video_url, so an
audio_url part was silently dropped (no placeholder emitted) while the
OpenAI branch already handled it. Match audio_url too, keeping the two
content-format branches consistent and media-first.

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
…MM transport env

- MM tensor serialization (serialize_array, write_array_as_f32/u16,
  serialize_array_as_u16_bytes) only fast-paths C-contiguous arrays now.
  The as_slice_memory_order() fallback serialized Fortran-contiguous views in
  the wrong dimension order — the exact hazard serialize_array's own comment
  warned about. Non-C-contiguous arrays fall through to logical .iter(); inputs
  are C-contiguous in practice, so the wire bytes are unchanged.
- Refresh the stale 'worker is local' comment in assemble_tokenspeed to match
  worker_shares_dev_shm (unix socket / operator-asserted loopback).
- Document the TokenSpeed MM tensor transport env vars (transport mode, shm
  min bytes, loopback assertion, timing) plus the worker-side companions in
  the configuration reference.

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
SMG_TOKENSPEED_TENSOR_TRANSPORT and SMG_TOKENSPEED_SHM_MIN_BYTES were
introduced earlier in this same (unmerged) branch and then renamed to the
SMG_TOKENSPEED_MM_* form for clarity, with the old names kept as 'legacy'
fallbacks. Since neither name ever shipped on main, there is nothing to be
backward-compatible with — drop the aliases and keep only the canonical
SMG_TOKENSPEED_MM_* vars.

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
auto-mode SHM previously inferred a shared /dev/shm from the worker URL and
fell back to an operator env (SMG_TOKENSPEED_SHM_ASSUME_LOOPBACK_SHARED). URL
locality only proves network reachability — a loopback/sidecar worker can have
a private /dev/shm — so the env pushed a guess onto operators.

Instead negotiate it (NIXL-style): the TokenSpeed worker advertises its
/dev/shm filesystem identity (<boot_id>:<st_dev of /dev/shm>) in GetServerInfo's
scheduler_info; discovery surfaces it as the worker's shm_namespace_id label;
the router compares it to its own and enables SHM only on a match. boot_id pins
the host; st_dev is the tmpfs superblock device, identical iff the same tmpfs
backs both /dev/shm mounts — so it correctly detects sharing even across
separate containers that share /dev/shm via --ipc/bind-mount (where mount
namespaces differ but the superblock is the same). Any mismatch/missing token
=> inline.

Verified on the live split-container deployment: a canary written to the
router's /dev/shm is visible in the worker container, and both sides compute
the identical token (<boot_id>:28) — i.e. token-match <=> actually-shared. (An
earlier mount-namespace-inode token was rejected: it differs across those
containers despite the shared tmpfs, which would wrongly force inline.)

No new env, no proto change (reuses the existing scheduler_info Struct), and
strictly more correct than URL inference.

- client.rs: extract shm_namespace_id from scheduler_info into worker labels.
- multimodal.rs: worker_shares_dev_shm compares the worker's token to the
  router's local <boot_id>:<st_dev>; remove url_is_loopback + the env; add a
  regression test that the local token resolves on Linux.
- servicer.py: advertise _shm_namespace_id() in GetServerInfo.
- docs: drop the loopback-assertion env; document the verified auto behavior.

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
The video resize path added for HF/vLLM parity had no test: the existing
video tests use 4x4 frames that never resize, so the new resize_bicubic_pil_rgb
/ CatmullRom branch was never exercised — while images already have fingerprint
+ golden bit-identity guards.

- transforms: resize_bicubic_pil_rgb must equal the DynamicImage resize_bicubic_pil
  byte-for-byte (the core invariant making video frames match images/HF/vLLM),
  plus a wrong-length-buffer rejection test.
- qwen_vl_base: with a frame that actually needs resizing (odd 7x9 -> factor-
  aligned target), preprocess_video and preprocess_video_rgb must produce
  bit-identical encoder inputs; asserts a resize was forced so the branch runs.

Signed-off-by: yechank-nvidia <161688079+yechank-nvidia@users.noreply.github.com>
@slin1237
slin1237 force-pushed the yechan/mm-transport-opt branch from 95e0dd6 to 1c766de Compare June 22, 2026 12:47
@mergify mergify Bot removed the needs-rebase PR has merge conflicts that need to be resolved label Jun 22, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1c766de148

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include worker UID in SHM auto eligibility

In deployments where the router and TokenSpeed worker share /dev/shm but run as different non-root UIDs, auto mode can still choose SHM because worker_shares_dev_shm only compares the tmpfs identity. These payload files are created owner-only, and the worker later opens the path from the servicer/scheduler side, so a different UID gets EACCES and the request fails instead of falling back to inline; either include readable ownership/UID in the handshake or create the segment with permissions the verified worker can read.

Useful? React with 👍 / 👎.

@slin1237
slin1237 merged commit 6cf37bd into smg-project:main Jun 22, 2026
16 of 20 checks passed
slin1237 added a commit that referenced this pull request Jun 23, 2026
`test_multi_images_mixed` sends the same pug twice (mixed base64 + URL) and
asserted the model counts "3". Engines legitimately differ on byte-identical
multimodal inputs: vLLM deduplicates them (encodes the duplicate once), sglang
keeps both — so the literal count is engine-dependent. This surfaced after
#1604 made image decode bit-deterministic, so identical inputs now produce
identical pixel tensors that vLLM dedups (sglang is unaffected).

Drop the exact-count assertion and keep the duplicate-detection assertion
(which both engines satisfy) plus the dog/pug content checks, so the test still
validates multi-image + mixed base64/URL + duplicate handling.

Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Dependency updates documentation Improvements or additions to documentation grpc gRPC client and router changes model-gateway Model gateway crate changes multimodal Multimodal crate changes priority:high High priority tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants