Skip to content

fix(multimodal): give Kimi-K3 its own vision processor and fix alpha ordering - #1984

Merged
key4ng merged 4 commits into
mainfrom
fix/kimi-k3-vision-processor
Jul 29, 2026
Merged

fix(multimodal): give Kimi-K3 its own vision processor and fix alpha ordering#1984
key4ng merged 4 commits into
mainfrom
fix/kimi-k3-vision-processor

Conversation

@key4ng

@key4ng key4ng commented Jul 28, 2026

Copy link
Copy Markdown
Member

Description

Problem

Kimi-K3 (#1968) was routed to KimiK25Processor. The two models share the MoonViT encoder, but not the pixel pipeline, so K3 images were preprocessed in ways that diverge from the reference (kimi_k3_vision_processing.py):

  1. Transparency is dropped instead of composited. K3 ships a transparent_bg_config (chessboard, 8px squares, 255/180, square on top-left) with transparent_bg_fill_stage: "after_resize", and the reference paints RGBA input over that generated board. SMG called to_rgb8(), which discards alpha without compositing — and a fully transparent pixel normally stores RGB (0,0,0), so the encoder saw solid black (normalized -1.0) where the reference sees a light checkerboard. Every image with an alpha channel (screenshots, logos, icons, charts exported with transparency) got a systematically wrong pixel tensor.

  2. The patch budget is a quarter of K3's. K3's in_patch_limit is 65536; K2.5's is 16384. The registry builds processors with ::new(), so K3 inherited the hardcoded K2.5 constant and images above the smaller budget were downscaled about 2x more per side than they should have been, losing resolution the model was trained to use.

  3. Alpha is dropped after the resize, not before — pre-existing on main and shared by K2.5. The reference converts at load time (_to_pil.convert("RGB")), so it convolves the RGB stored underneath transparent pixels. SMG fed the RGBA image to fast_image_resize, which premultiplies by default, and only dropped alpha afterwards, discarding that colour instead.

Solution

Split the two processors rather than widening KimiK25Processor, so the delta between the models stays explicit and small:

  • processors::moonvit holds the shared pipeline (patch-budget resize solve → resize → pad to patch_size * merge_size → normalize → NaViT patchify), following the existing qwen_vl_base.rs family-base convention.
  • KimiK3Processor carries K3's shipped defaults and resolves in_patch_limit, patch_limit_on_one_side, transparent_bg_config and transparent_bg_fill_stage from the model's preprocessor_config.json at call time, so a checkpoint shipping different values is honored instead of being overridden by compiled-in constants. An absent or malformed key keeps K3's board — preprocessor_config.json is optional in this runtime, and each processor is expected to supply its own model's defaults (grpc/multimodal/config.rs). A checkpoint that genuinely wants the reference's alpha-dropping path says so with an explicit "transparent_bg_config": null.
  • KimiK25Processor becomes a thin delegation that passes transparent_bg: None.
  • transforms::fill_transparent_bg implements the reference background formula (chessboard / black / white / grey) including partial-alpha blending, and both fill stages are honored.
  • The no-background path now converts to RGB before resizing, fixing (3). On the compositing path alpha survives the resize and is premultiplied, which is what PIL.Image.resize does for RGBA — verified against Pillow 11.2.1, where im.resize(...) is byte-identical to im.convert("RGBa").resize(...).convert("RGBA") and differs from convolving the bands independently.

The prompt/placeholder spec in registry/kimi_k25.rs stays shared (K3 uses the same <|media_pad|> and patch layout); a comment now records that the pixel pipelines are not.

Behavior change for K2.5: opaque input is bit-identical to before, but alpha-bearing input now matches the reference instead of being premultiplied first. Fix (3) is a divergence that predates #1968; it is bundled here because the fix lives in the shared moonvit path this PR introduces.

Two K3 differences are deliberately out of scope:

  • K3's placeholder text injects {width}x{height} alongside the media token. That belongs to the chat-template / prompt-encoding layer, not the vision processor.
  • Kimi (like Pixtral/Phi/Llama4) resizes with SIMD Catmull-Rom rather than the Pillow-bit-exact resize_bicubic_pil that qwen_vl_base.rs uses. transforms.rs itself warns that this divergence "amplifies into a large embedding shift". It predates feat(kimi-k3): add K3 support #1968 and applies equally to K2.5, so it deserves its own PR.

Changes

  • vision/processors/moonvit.rs (new) — shared MoonViT core: MoonVitParams, resize-config solve, resize/pad/normalize, patch extraction.
  • vision/processors/kimi_k3.rs (new)KimiK3Processor with K3's defaults and call-time config resolution.
  • vision/processors/kimi_k25.rs — reduced to a delegation over moonvit.
  • vision/transforms.rsTransparentBgPattern, TransparentBgConfig, TransparentBgFillStage, TransparentBg, fill_transparent_bg.
  • vision/preprocessor_config.rs — lift in_patch_limit, patch_limit_on_one_side, transparent_bg_config and transparent_bg_fill_stage out of nested media_proc_cfg into the extra map.
  • vision/processor.rs — register kimi-k3 / kimi_k3 as their own processor.
  • vision/processors/mod.rs — export the new modules.
  • registry/kimi_k25.rs — document that K3 shares the prompt spec, not the pixels.

Test Plan

$ cargo test -p llm-multimodal
test result: ok. 278 passed; 0 failed          # 269 before this PR
... all other test binaries: ok. 0 failed

$ cargo +nightly fmt --all -- --check                            # clean
$ cargo clippy -p llm-multimodal --all-targets -- -D warnings    # clean
$ cargo clippy --workspace --all-targets -- -D warnings          # clean
$ cargo build --workspace                                        # clean

New tests, and what they pin:

Test Before → after
kimi_k3::test_transparent_pixels_composite_over_chessboard fully transparent RGBA denormalized to 0 (black) → 255/180 chessboard
kimi_k3::test_chessboard_phase_matches_reference (0,0)=255, (8,0)=180 for square_on_top_left=true, matching the numpy reference
kimi_k3::test_semi_transparent_blends_toward_background alpha=128 blends toward the board instead of being ignored
kimi_k3::test_after_resize_ignores_colour_hidden_under_alpha colour under alpha=0 no longer bleeds through the resize
kimi_k3::test_larger_patch_budget_keeps_more_resolution K3 keeps more pixels than K2.5 at the same input size
kimi_k3::test_from_preprocessor_config_reads_limits, test_transparent_bg_config_overridden_by_model_config, test_fill_stage_before_resize_changes_output model config overrides compiled-in defaults
kimi_k3::test_explicit_null_config_disables_compositing "transparent_bg_config": null reaches the alpha-dropping path
kimi_k3::test_rgb_input_matches_k25_pipeline, test_opaque_pixels_are_untouched no change for opaque input
kimi_k25::test_k25_drops_alpha_before_resizing transparent red survives a downscale as red instead of being premultiplied away
kimi_k25::test_in_patch_limit_resolved_from_config a model shipping a larger budget is not capped by the K2.5 default
transforms::chessboard_background_matches_reference every cell checked against a local port of the Python formula, both on_top_left values
preprocessor_config::test_parse_kimi_k3_transparency_settings a verbatim excerpt of K3's real preprocessor_config.json parses into the lifted fields
processor::test_registry_separates_kimi_k25_and_k3 moonshotai/Kimi-K3kimi-k3, moonshotai/Kimi-K2.5kimi-k2.5, both by name and by model_type

Both alpha-ordering fixes were confirmed to be load-bearing by reverting each one and watching the matching test fail.

Note: cargo clippy --all-features (and therefore the clippy pre-commit hook) could not run locally — the opencv feature's build script needs a pkg-config / OpenCV toolchain that isn't installed on this machine. Default-feature clippy is clean workspace-wide; CI covers the full feature set.

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

  • New Features
    • Added support for Kimi-K3 vision processing with model-specific image settings and token budgeting.
    • Added configurable transparent-background handling, including chessboard backgrounds and compositing stages.
    • Added support for overriding vision preprocessing and patch limits through model configuration.
    • Improved image preprocessing consistency across Kimi-K2.5 and Kimi-K3.
  • Documentation
    • Clarified differences between the Kimi-K2.5 and Kimi-K3 vision pipelines.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

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

The PR adds shared MoonViT preprocessing and transparent-image compositing, introduces a Kimi-K3 processor, routes K3 models separately from K2.5, and refactors K2.5 to use the shared pipeline.

Changes

Kimi MoonViT preprocessing

Layer / File(s) Summary
Transparent image transforms
crates/multimodal/src/vision/transforms.rs
Adds configurable transparent-background compositing and alpha-aware resizing behavior with tests for blending, defaults, and alpha preservation.
Shared MoonViT pipeline
crates/multimodal/src/vision/processors/moonvit.rs
Adds shared parameter resolution, resize and padding, normalization, patch extraction, batch assembly, and grid metadata generation.
Kimi processor implementations and configuration
crates/multimodal/src/vision/processors/kimi_k25.rs, crates/multimodal/src/vision/processors/kimi_k3.rs, crates/multimodal/src/vision/preprocessor_config.rs
Refactors K2.5 to delegate to MoonViT, adds K3-specific limits and transparency behavior, and lifts nested configuration fields into extra.
Processor exports and registry routing
crates/multimodal/src/vision/processors/mod.rs, crates/multimodal/src/vision/processor.rs, crates/multimodal/src/registry/kimi_k25.rs
Exports K3 and routes K3 and K2.5 model identifiers to distinct processors, with updated registry tests and documentation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ModelRegistry
  participant KimiK3Processor
  participant moonvit_preprocess
  participant transforms
  participant EncoderInputs
  ModelRegistry->>KimiK3Processor: resolve K3 model identifier
  KimiK3Processor->>moonvit_preprocess: preprocess images with K3 parameters
  moonvit_preprocess->>transforms: composite transparent backgrounds and resize
  transforms-->>moonvit_preprocess: normalized image data
  moonvit_preprocess->>EncoderInputs: patches and grid metadata
Loading

Possibly related PRs

  • lightseekorg/smg#1991: Overlaps in the MoonViT pipeline, transparent-background configuration, K2.5 alpha handling, and K3 registration.

Suggested reviewers: catherinesue

Poem

A rabbit hops through pixels bright,
Checkerboards bloom in moonlit light.
K2.5 keeps alpha away,
K3 paints a new display.
Patches march in tidy rows—
The vision pipeline softly grows.

🚥 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 accurately summarizes the main changes: a dedicated Kimi-K3 vision processor and transparency/alpha handling fixes.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/kimi-k3-vision-processor

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

@github-actions github-actions Bot added the multimodal Multimodal crate changes label Jul 28, 2026

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

Clean, well-structured split. Reviewed the compositing formula, the before/after-resize stage dispatch, the straight-alpha resize path, config resolution with fallback defaults, and the zero-division guards. All paths trace correctly. Tests cover the key invariants thoroughly — no issues found.

@key4ng key4ng closed this Jul 28, 2026
@key4ng key4ng reopened this Jul 28, 2026
@key4ng
key4ng marked this pull request as ready for review July 28, 2026 22:45
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@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: 33c23fd299

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

height: u32,
filter: FilterType,
) -> DynamicImage {
resize_inner(image, width, height, filter, false)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve Pillow's premultiplied-alpha resize

For RGBA images that are actually resized and contain partially transparent edges, this straight-alpha path does not match the referenced Pillow pipeline: Pillow converts RGBA/LA images to premultiplied modes for non-nearest resampling, resizes, and converts back. Disabling alpha handling here lets RGB from transparent pixels bleed into neighboring pixels before the chessboard composite, producing incorrect K3 tensors around common antialiased boundaries; use the alpha-aware resize path before compositing instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Correct, and I had this backwards — thanks. I checked against Pillow 11.2.1: im.resize(...) on an RGBA image is byte-identical to im.convert("RGBa").resize(...).convert("RGBA"), and differs from convolving the bands independently. So PIL does premultiply, and resize_straight_alpha was the bug rather than the fix.

Dropped the function; the compositing path now uses the default premultiplying resize. kimi_k3::test_after_resize_ignores_colour_hidden_under_alpha pins it — verified to fail when premultiplication is disabled.

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

Clean, well-structured refactoring. The MoonViT split is the right call — the shared pipeline in moonvit.rs keeps the delta between K2.5 and K3 explicit, and the transparency compositing correctly matches the reference implementation. Test coverage is thorough. No issues found.

0 🔴 Important · 0 🟡 Nit · 0 🟣 Pre-existing

@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: 3

🤖 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/src/vision/processors/kimi_k25.rs`:
- Around line 301-321: The test test_k25_drops_alpha_rather_than_compositing
only validates fully transparent pixels, which cannot distinguish alpha dropping
from compositing. Extend it with a factor-aligned semi-transparent pixel case
such as Rgba([255, 255, 255, 128]) and assert the preprocessor output matches
convert("RGB") semantics by dropping alpha rather than blending it.

In `@crates/multimodal/src/vision/processors/moonvit.rs`:
- Around line 143-177: The no-background path in moonvit.rs lines 143-177 must
convert alpha-bearing images to RGB before passing them to the premultiplying
resize flow; update the source selection around transparent_bg and resize_fn so
transparent_bg=None with alpha uses image.to_rgb8(), while preserving the
existing fill and straight-alpha behavior for configured backgrounds. In
kimi_k25.rs lines 301-321, extend test_k25_drops_alpha_rather_than_compositing
with a semi-transparent input such as white RGBA alpha 128 so premultiplication
errors are detected.

In `@crates/multimodal/src/vision/transforms.rs`:
- Around line 1499-1610: Strengthen the test
straight_alpha_resize_keeps_rgba_and_ignores_premultiply by using non-uniform
source alpha, such as opaque and fully transparent rows, while keeping RGB
values under transparent pixels nonzero. Assert the resized output retains RGBA,
preserves straight RGB values, and produces the expected interpolated alpha
values rather than merely checking alpha remains zero.
🪄 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: 4253a23e-32d8-49d8-a44c-6e3a135f4c6e

📥 Commits

Reviewing files that changed from the base of the PR and between c5de762 and 33c23fd.

📒 Files selected for processing (8)
  • crates/multimodal/src/registry/kimi_k25.rs
  • crates/multimodal/src/vision/preprocessor_config.rs
  • crates/multimodal/src/vision/processor.rs
  • crates/multimodal/src/vision/processors/kimi_k25.rs
  • crates/multimodal/src/vision/processors/kimi_k3.rs
  • crates/multimodal/src/vision/processors/mod.rs
  • crates/multimodal/src/vision/processors/moonvit.rs
  • crates/multimodal/src/vision/transforms.rs

Comment thread crates/multimodal/src/vision/processors/kimi_k25.rs Outdated
Comment thread crates/multimodal/src/vision/processors/moonvit.rs
Comment thread crates/multimodal/src/vision/transforms.rs Outdated
@key4ng key4ng changed the title fix(multimodal): give Kimi-K3 its own vision processor fix(multimodal): give Kimi-K3 its own vision processor and fix alpha ordering Jul 28, 2026

@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: 1

🤖 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/src/vision/processors/kimi_k25.rs`:
- Around line 323-358: Correct the plane-size calculation in
test_k25_drops_alpha_before_resizing: derive the R-channel plane length from the
patch spatial dimensions in encoder_input.shape(), not from the fixed channel
dimension at shape()[1]. Keep the existing assertion checking flat[..planes] so
it validates the complete first channel plane of each patch.
🪄 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: acee7c23-6b54-41af-8f61-8514d48c9d3f

📥 Commits

Reviewing files that changed from the base of the PR and between 33c23fd and 216f20f.

📒 Files selected for processing (4)
  • crates/multimodal/src/vision/processors/kimi_k25.rs
  • crates/multimodal/src/vision/processors/kimi_k3.rs
  • crates/multimodal/src/vision/processors/moonvit.rs
  • crates/multimodal/src/vision/transforms.rs

Comment thread crates/multimodal/src/vision/processors/kimi_k25.rs

@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: 26fa1c150e

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +158 to +163
let resized = transforms::resize(
source,
cfg.new_width as u32,
cfg.new_height as u32,
image::imageops::FilterType::CatmullRom,
);

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 Convert unsupported alpha formats before resizing

When a K3 image decodes as ImageLumaA8, ImageLumaA16, or ImageRgba16 and requires the after_resize path, this passes that image directly to transforms::resize, but fir_image_to_dynamic only reconstructs Rgba8 among alpha formats and otherwise discards the premultiplied FIR result in favor of source.resize_exact. Fresh evidence beyond the earlier RGBA finding is the converter's missing LA/16-bit match arms at transforms.rs:416-428; these inputs therefore still use straight-alpha resizing and can bleed hidden color across transparent boundaries before chessboard compositing. Convert surviving-alpha inputs to RGBA8 first or add matching FIR output conversions.

Useful? React with 👍 / 👎.

@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: 0cf58df958

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +43 to +47
#[serde(rename_all = "lowercase")]
pub enum TransparentBgPattern {
White,
Black,
Gray,

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 Accept the reference grey background spelling

When a checkpoint selects the supported solid-grey background with "pattern": "grey", rename_all = "lowercase" only allows this variant as "gray". Deserializing the entire TransparentBgConfig therefore fails, and resolved_transparent_bg silently falls back to K3's chessboard default, producing different pixels for transparent images. Add a serde rename or alias for the reference spelling.

Useful? React with 👍 / 👎.

key4ng added 4 commits July 28, 2026 16:51
Kimi-K3 was routed to `KimiK25Processor`, which drops the alpha channel
instead of compositing over the chessboard background K3 ships in its
`preprocessor_config.json`, and hardcodes K2.5's `in_patch_limit` of
16384 instead of K3's 65536.

- Extract the shared MoonViT pipeline into `processors::moonvit` so the
  K2.5/K3 delta is explicit; K2.5 keeps its previous behavior (no
  transparency handling) bit-for-bit.
- Add `KimiK3Processor` with K3's shipped defaults, resolving
  `in_patch_limit`, `patch_limit_on_one_side`, `transparent_bg_config`
  and `transparent_bg_fill_stage` from the model config at call time.
- Add `transforms::fill_transparent_bg` (chessboard/black/white/grey
  patterns) plus `resize_straight_alpha`, so post-resize compositing
  convolves straight alpha the way the PIL reference does rather than
  premultiplying.
- Lift the Kimi limits and K3 transparency keys out of nested
  `media_proc_cfg` in `PreProcessorConfig`.
- Register `kimi-k3` / `kimi_k3` separately in the processor registry.

Signed-off-by: key4ng <rukeyang@gmail.com>
…licit null bg config

The MoonViT resize path handled alpha two ways, both diverging from the
reference:

- With no `transparent_bg_config` (K2.5), the RGBA image went straight
  into the resizer and alpha was dropped afterwards. The reference
  converts at load time (`_to_pil` -> `.convert("RGB")`), so it
  convolves the RGB stored under transparent pixels; premultiplying
  first discards it. Pre-existing on main, not introduced by K3.
- On the `after_resize` path, `resize_straight_alpha` disabled
  premultiplication on the theory that PIL convolves bands
  independently. It does not: on Pillow 11.2.1 `Image.resize` for RGBA
  is byte-identical to `convert("RGBa").resize(...).convert("RGBA")`.
  The default premultiplying resize was already correct, so drop the
  variant.

Also make K3's compiled-in board switchable. Defaults stay in the
processor because `preprocessor_config.json` is optional in this runtime,
but a checkpoint that wants the reference's alpha-dropping behaviour now
has an escape hatch via `"transparent_bg_config": null`.

Regression tests cover both orderings and were confirmed to fail with
each fix reverted.

Signed-off-by: key4ng <rukeyang@gmail.com>
… tests

Signed-off-by: key4ng <rukeyang@gmail.com>
… test

shape()[1] is the fixed channel dimension, so shape()[1] / 3 was always 1
and the assertion only covered a single pixel. Index the tensor directly
and check every patch's full R plane, plus G/B, so a bug corrupting only
part of a patch is caught too.

Signed-off-by: key4ng <rukeyang@gmail.com>
@key4ng
key4ng force-pushed the fix/kimi-k3-vision-processor branch from 0cf58df to 5bddbfb Compare July 28, 2026 23:52

@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: 1

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/processor.rs (1)

331-346: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a deterministic matcher if overlapping patterns are possible. VisionProcessorRegistry::find_in_candidate walks a HashMap, so pattern precedence is not stable; the new Kimi K2/K3 entries are disjoint and fine, but any future overlap will route nondeterministically.

🤖 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/processor.rs` around lines 331 - 346, Update
VisionProcessorRegistry::find_in_candidate to use deterministic pattern matching
rather than relying on HashMap iteration order, defining an explicit precedence
for overlapping registered patterns while preserving the existing Kimi K2 and K3
registrations.
🤖 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/src/vision/transforms.rs`:
- Around line 136-149: Guard the image conversion flow before the
`rgba.as_raw().chunks_exact(...)` loop so `width == 0` or another zero-sized
input returns an empty `RgbImage` without invoking `chunks_exact` with a zero
chunk size. Preserve the existing pixel conversion behavior for non-zero
dimensions.

---

Outside diff comments:
In `@crates/multimodal/src/vision/processor.rs`:
- Around line 331-346: Update VisionProcessorRegistry::find_in_candidate to use
deterministic pattern matching rather than relying on HashMap iteration order,
defining an explicit precedence for overlapping registered patterns while
preserving the existing Kimi K2 and K3 registrations.
🪄 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: 9f1b8dc6-95cb-4c23-962f-fafd0949ebca

📥 Commits

Reviewing files that changed from the base of the PR and between 0cf58df and 5bddbfb.

📒 Files selected for processing (8)
  • crates/multimodal/src/registry/kimi_k25.rs
  • crates/multimodal/src/vision/preprocessor_config.rs
  • crates/multimodal/src/vision/processor.rs
  • crates/multimodal/src/vision/processors/kimi_k25.rs
  • crates/multimodal/src/vision/processors/kimi_k3.rs
  • crates/multimodal/src/vision/processors/mod.rs
  • crates/multimodal/src/vision/processors/moonvit.rs
  • crates/multimodal/src/vision/transforms.rs

Comment thread crates/multimodal/src/vision/transforms.rs
@key4ng

key4ng commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

End-to-end validation on a B300 node: main vs this branch vs pure vLLM

I built four images that isolate each of the two defects, computed the expected numbers from the checkpoint's own vision processor, then served the same four images through three stacks: the pre-fix gateway, this branch, and a plain vllm/vllm-openai OpenAI server with no gateway in the path.

Results

case smg old (main) smg new (this PR) pure vLLM HTTP
A transparent 1024×768 1092 tok — 0/3, answers "NO TEXT VISIBLE" 1092 tok — 3/3 1168 tok — 3/3
B large opaque 4000×3000 4292 tok — 9/9 rungs 15536 tok — 9/9 rungs 15613 tok — 9/9 rungs
C transparent + large 4292 tok — 0/9, nine fabricated codes 15536 tok — 7/9 15613 tok — 7/9
D control opaque 1024×768 1092 tok — 3/3 1092 tok — 3/3 1168 tok — 3/3

This branch and pure vLLM agree case for case, including which rungs fail on C — both read R1–R7 and miss R8 (11px) and R9 (9px). Main disagrees with vLLM on A and C.

On A, main's reasoning trace is explicit about what it received: "an image that appears to be completely black … entirely black with no discernible text". On C it does not report a blank image — it invents nine plausible-looking 4-character codes, none of them correct.

Test images, expected numbers, token accounting, and setup

Test images

Generated by a deterministic script (fixed text, fixed positions, DejaVu Sans Bold), so the files are reproducible.

image what it isolates
A 1024×768 RGBA, alpha=0 everywhere except glyphs; transparent pixels store RGB(0,0,0); black glyphs alpha only — 1024×768 is under both patch budgets, so no resize happens and the token count is held constant
B 4000×3000 opaque RGB, "resolution ladder": 9 rows of Rn CODE NNpx at 96/64/44/30/22/16/13/11/9 px, each with an unguessable 4-char code patch budget only — no alpha channel at all
C 4000×3000 RGBA, same ladder on a transparent background both defects at once
D 1024×768 opaque RGB, three text lines control — must be byte-identical through every stack

A plain alpha-drop turns A and C into a uniformly black image; I verified this directly (RGB-plane luminance extrema (0, 0) for both).

Prompts: A/D ask for a transcription with an explicit "if you cannot see any text at all, reply NO TEXT VISIBLE" escape hatch, scored by substring hits on three known strings. B/C ask for exactly nine Rn: CODE lines with no escape hatch, scored per rung against the known codes. An earlier version of the B/C prompt offered an "ILLEGIBLE" option and the model spent its entire budget deliberating instead of answering, so the escape hatch was removed and per-rung accuracy is scored instead. Both requests are single-image chat completions, temperature=0, max_tokens=3000, base64 PNG data URL.

Expected numbers, from the checkpoint's own processor

AutoImageProcessor.from_pretrained(..., trust_remote_code=True) on /raid/models/moonshotai/Kimi-K3, run twice — once as shipped, once with in_patch_limit=16384 and transparent_bg_config=None to emulate how main treats K3:

config A / D B / C
as shipped (in_patch_limit=65536, chessboard, after_resize) 1036 tokens 15444 tokens, native 4000×3000
main-emulation (16384, no transparent bg) 1036 tokens 4200 tokens, resized to 2073×1554 (0.518×)

The shipped config is pattern: chessboard, chessboard_square_size: 8, chessboard_white_value: 255, chessboard_gray_value: 180, transparent_bg_fill_stage: after_resize, in_patch_limit: 65536, patch_limit_on_one_side: 512.

Before touching any server I also ran this branch's processors over the same four files through VisionProcessorRegistry::find() with the real preprocessor_config.json. The K3 processor matches the reference tensor to four decimal places on all four images (A 1036 / mean 0.5426 / std 0.5689, B 15444 / 0.9681 / 0.2493, C 15444 / 0.6787 / 0.3610, D 1036 / 0.8599 / 0.5060), while the alpha-dropping route yields std = 0.0000 — a constant −1.0 tensor — for A and C.

Token accounting

Text-only prompt_tokens were measured per server (same prompts, no image) so template cost can be separated from image cost. For vLLM: 122 tokens for the A/D prompt, 154 for the B/C prompt.

  • vLLM A/D: 1168 − 122 = 1046 = 1036 reference image tokens + a 10-token media wrapper
  • vLLM B/C: 15613 − 154 = 15459 = 15444 reference image tokens + a 15-token wrapper (the wrapper carries the image dimensions, so it varies slightly with size)
  • new − old on B/C: 15536 − 4292 = 11244, exactly 15444 − 4200 — the two gateway builds differ on that case by precisely the reference's image-token delta and nothing else
  • A/D are 1092 on both gateways: 1092 − 56 = 1036, so the alpha fix changes the tensor without changing the token count, which is what makes A a clean single-variable test

vLLM's totals run ~76 tokens above the gateway's for the same case because its chat template is more verbose. That is the separate SMG-vs-vLLM prompt-format difference, not something this PR touches.

Setup

  • 8×B300, TP8, model /raid/models/moonshotai/Kimi-K3.
  • Engine for both gateway runs: the vLLM gRPC image, --trust-remote-code --load-format fastsafetensors --moe-backend auto --gpu-memory-utilization 0.95 --tensor-parallel-size 8 --max-model-len 1000000 --kv-cache-dtype fp8 --max-num-batched-tokens 32768 --no-enable-prefix-caching --mm-processor-cache-gb 0 --served-model-name kimi-k3.
  • Pre-fix gateway: the published smg:kimi-k3.v2 image. Verified pre-fix and K3-aware by grepping its smg_rs.abi3.so directly: kimi-k3 and kimi_k3 are present, transparent_bg_config has zero occurrences.
  • This branch: cargo build --release --bin smg --features vendored-openssl at the branch head, run against the same engine. Both gateways use --reasoning-parser kimi_k3 --tool-call-parser kimi_k3.
  • Pure vLLM: vllm/vllm-openai:kimi-k3 serving over plain HTTP with the same engine flags plus --reasoning-parser kimi_k3, so the response shape matches.
Two honest caveats, and a methodology trap worth flagging for anyone reproducing this

Two honest caveats

  1. B does not discriminate on answer quality. K3 read all nine rungs even through main's 0.518× downscale, down to 9px text. B discriminates on token count and pipeline fidelity only (4200 vs the reference's 15444) — not on this particular OCR outcome.
  2. C is 7/9 after the fix, not 9/9. The reference's chessboard fill lowers contrast for the 11px and 9px rows, and pure vLLM misses exactly the same two. Faithfully reproducing the reference is the goal, and 7/9 with two honest misses beats 0/9 with nine fabrications.

A methodology trap worth flagging for anyone reproducing this

My first attempt produced a nonsense result: the pre-fix gateway appeared to read C at 8/9, from a tensor that is provably a constant. The cause is that mm_hashes is blake3 over the raw image bytes only and does not cover any preprocessing parameter, so an engine shared by two gateway versions will hand one of them the other's encoder output for the same file. This happened with --no-enable-prefix-caching --mm-processor-cache-gb 0 set and verified in the engine's own config dump (enable_prefix_caching=False, mm_processor_cache_gb=0.0, prefix cache hit rate 0.0% throughout).

  • cached tensor shorter than the placeholder count → EngineCore dies: Attempted to assign 4200 = 4200 multimodal tokens to 15444 placeholders
  • cached tensor longer → silently truncated. The pre-fix side received the first 4200 of the fixed side's 15444 embeddings, i.e. the top ~27% of the image, which happens to contain the entire ladder.

A fresh, never-shared transparent image confirmed the pre-fix behaviour: no answer at all, the full 3000 tokens spent hallucinating "a wide black background with nine small white text labels scattered". The numbers above were then produced by giving each stack its own byte-distinct but pixel-identical copy of every image (differing PNG tEXt chunk only; pixel equality asserted, file hashes confirmed distinct) after an engine restart.

The operational consequence is worth noting independently of this PR: rolling out a gateway whose preprocessing has changed, against an engine that keeps caching by raw-byte hash, can either kill EngineCore or silently serve truncated embeddings for the same image. Restarting or draining the engine alongside such an upgrade avoids both.

@key4ng
key4ng merged commit 3b72ccd into main Jul 29, 2026
44 checks passed
@key4ng
key4ng deleted the fix/kimi-k3-vision-processor branch July 29, 2026 04:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

multimodal Multimodal crate changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant