Skip to content

feat(multimodal): add Kimi-K2.5 vision model spec and image processor - #1097

Closed
ConnorLi96 wants to merge 1 commit into
mainfrom
connorli/kimi-k25-vlm
Closed

ConnorLi96 wants to merge 1 commit into
mainfrom
connorli/kimi-k25-vlm

Conversation

@ConnorLi96

@ConnorLi96 ConnorLi96 commented Apr 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

SMG has no multimodal support for Kimi-K2.5, which uses MoonViT3d with a NaViT-style architecture.

Solution

Add KimiK25Spec to the model registry and KimiK25Processor to the image processor registry. The processor reuses QwenVLProcessorBase with Kimi-specific defaults (patch_size=14, merge_size=2, mean/std=[0.5,0.5,0.5], max 16384 patches).

Prompt replacements keep 1 placeholder per image — TRT-LLM's KimiK25InputProcessor handles expansion to N vision tokens server-side.

Changes

  • crates/multimodal/src/registry/kimi_k25.rs (new): model spec with config-driven placeholder token, field layouts, tests
  • crates/multimodal/src/registry/mod.rs: register spec
  • crates/multimodal/src/vision/processors/kimi_k25.rs (new): image processor delegating to QwenVLProcessorBase
  • crates/multimodal/src/vision/processors/mod.rs: export
  • crates/multimodal/src/vision/image_processor.rs: register processor

Test Plan

  • Unit tests: match by model_type, match by model_id, prompt replacement token count
  • cargo clippy + cargo +nightly fmt clean

Checklist:

  • cargo +nightly fmt passes
  • cargo clippy --all-targets --all-features -- -D warnings passes

Made with Cursor

Summary by CodeRabbit

Release Notes

  • New Features
    • Added support for Kimi K2.5 models with automatic model recognition via model type or model ID matching.
    • Integrated Kimi K2.5 image processor with configurable vision input handling, supporting batch processing and multiple image inputs.

@github-actions github-actions Bot added the multimodal Multimodal crate changes label Apr 11, 2026
@coderabbitai

coderabbitai Bot commented Apr 11, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The PR adds support for the Kimi K2.5 model family by introducing a model processor spec that defines image handling (10-image max, <|media_pad|> placeholder token), an image processor wrapping QwenVLProcessorBase with Kimi-specific defaults (patch size 14, merge size 2, [0.5, 0.5, 0.5] normalization), and registers both in the multimodal and processor registries for model discovery.

Changes

Cohort / File(s) Summary
Model Registry & Spec
crates/multimodal/src/registry/kimi_k25.rs, crates/multimodal/src/registry/mod.rs
Added new KimiK25Spec implementing ModelProcessorSpec with model type and ID matching logic, image placeholder token configuration, 10-image modality limit, field layouts for vision inputs (flat patches per image, batched grid_thws/patches_per_image), and one placeholder token per image for prompt replacement. Registered in ModelRegistry::new().
Image Processor Implementation
crates/multimodal/src/vision/processors/kimi_k25.rs
Implemented KimiK25Processor wrapping QwenVLProcessorBase with Kimi-K2.5-specific config: patch size 14, merge size 2, pixel bounds [14*14*4, 14*14*16384], normalization [0.5, 0.5, 0.5], delegating preprocessing and token calculation to base class.
Processor Registry & Exports
crates/multimodal/src/vision/image_processor.rs, crates/multimodal/src/vision/processors/mod.rs
Registered KimiK25Processor in ImageProcessorRegistry::with_defaults() for "kimi" model pattern matching, and exported KimiK25Processor type from processors module.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

Suggested labels

multimodal, tests

Suggested reviewers

  • slin1237
  • key4ng

Poem

🐰 A rabbit hops through registries with glee,
Kimi K2.5 now joins the family tree,
Patch sizes squared, with tokens so neat,
QwenVL's base makes processors complete! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding Kimi-K2.5 vision model support with both spec and processor components.

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

✨ 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 connorli/kimi-k25-vlm

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.

@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 support for the Kimi-K2.5 model by implementing the KimiK25Spec for model registration and the KimiK25Processor for vision processing, which reuses the Qwen-VL base architecture. Feedback identifies a critical field name mismatch for grid information between the processor and the registry, suggests keeping the patches_per_image metadata on the CPU for host-side logic, and recommends overriding get_processed_size to ensure consistent behavior with the underlying dynamic resolution processor.

Comment on lines +52 to +58
fn preprocess(
&self,
images: &[DynamicImage],
config: &PreProcessorConfig,
) -> Result<PreprocessedImages, TransformError> {
self.inner.preprocess(images, config)
}

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.

high

The QwenVLProcessorBase produces a model-specific field named "image_grid_thw", but the KimiK25Spec defines and expects "grid_thws". This mismatch will prevent the model from receiving the necessary grid information. The key should be renamed in the processor's preprocess method to match the spec.

Suggested change
fn preprocess(
&self,
images: &[DynamicImage],
config: &PreProcessorConfig,
) -> Result<PreprocessedImages, TransformError> {
self.inner.preprocess(images, config)
}
fn preprocess(
&self,
images: &[DynamicImage],
config: &PreProcessorConfig,
) -> Result<PreprocessedImages, TransformError> {
let mut preprocessed = self.inner.preprocess(images, config)?;
if let Some(grid_thw) = preprocessed.model_specific.remove("image_grid_thw") {
preprocessed.model_specific.insert("grid_thws".to_string(), grid_thw);
}
Ok(preprocessed)
}
References
  1. For protocol data structures that mirror an external API (e.g., OpenAI), prioritize alignment with the external specification over internal consistency.

Comment on lines +92 to +94
fn keep_on_cpu_keys(&self) -> Vec<String> {
vec!["grid_thws".to_string()]
}

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.

medium

The patches_per_image field is a metadata tensor used for batching and slicing. Similar to grid_thws, it should be kept on the CPU to ensure it is accessible to the host-side scheduling logic and to avoid unnecessary GPU transfers if the model expects it on the host.

Suggested change
fn keep_on_cpu_keys(&self) -> Vec<String> {
vec!["grid_thws".to_string()]
}
fn keep_on_cpu_keys(&self) -> Vec<String> {
vec!["grid_thws".to_string(), "patches_per_image".to_string()]
}

Comment on lines +60 to +62
fn calculate_num_tokens(&self, width: u32, height: u32, config: &PreProcessorConfig) -> usize {
self.inner.calculate_num_tokens(width, height, config)
}

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.

medium

The KimiK25Processor should override get_processed_size to delegate to the inner processor. Since Kimi-K2.5 uses dynamic resolution (NaViT-style), it doesn't have a single fixed processed size, and the base implementation correctly returns None.

Suggested change
fn calculate_num_tokens(&self, width: u32, height: u32, config: &PreProcessorConfig) -> usize {
self.inner.calculate_num_tokens(width, height, config)
}
fn calculate_num_tokens(&self, width: u32, height: u32, config: &PreProcessorConfig) -> usize {
self.inner.calculate_num_tokens(width, height, config)
}
fn get_processed_size(&self, config: &PreProcessorConfig) -> Option<(u32, u32)> {
self.inner.get_processed_size(config)
}

Add KimiK25Spec to the multimodal model registry and KimiK25Processor
to the image processor registry, enabling SMG to handle Kimi-K2.5 VLM
requests (image placeholder expansion, token computation, raw image
byte passthrough for TRT-LLM).

Kimi-K2.5 uses MoonViT3d with NaViT-style architecture (patch_size=14,
merge_kernel_size=2x2, dynamic resolution), which is structurally
similar to Qwen VL. The processor reuses QwenVLProcessorBase with
Kimi-specific defaults (mean/std=[0.5,0.5,0.5], max 16384 patches).

Signed-off-by: ConnorLi96 <ConnorLi96@users.noreply.github.com>
Made-with: Cursor
Signed-off-by: ConnorLi96 <ConnorLi96@users.noreply.github.com>
Made-with: Cursor
Comment on lines +87 to +93
("grid_thws".to_string(), FieldLayout::Batched),
("patches_per_image".to_string(), FieldLayout::Batched),
])
}

fn keep_on_cpu_keys(&self) -> Vec<String> {
vec!["grid_thws".to_string()]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Important: Key mismatch — "grid_thws" here doesn't match the actual tensor key emitted by the preprocessor.

KimiK25Processor delegates to QwenVLProcessorBase::preprocess(), which stores the grid tensor under the key "image_grid_thw" (see qwen_vl_base.rs:400). The Qwen VL and Qwen3 VL specs both use "image_grid_thw" in their field_layouts() and keep_on_cpu_keys() for this reason.

Because of this mismatch:

  1. The FieldLayout::Batched declaration for "grid_thws" won't match any tensor, so the actual "image_grid_thw" tensor gets the default layout instead of Batched.
  2. The keep_on_cpu_keys entry "grid_thws" won't match either, so the grid tensor will be sent to GPU instead of staying on CPU.
Suggested change
("grid_thws".to_string(), FieldLayout::Batched),
("patches_per_image".to_string(), FieldLayout::Batched),
])
}
fn keep_on_cpu_keys(&self) -> Vec<String> {
vec!["grid_thws".to_string()]
("image_grid_thw".to_string(), FieldLayout::Batched),
("patches_per_image".to_string(), FieldLayout::Batched),
])
}
fn keep_on_cpu_keys(&self) -> Vec<String> {
vec!["image_grid_thw".to_string()]

@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: 2585607c64

ℹ️ 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".

"pixel_values".to_string(),
FieldLayout::flat("patches_per_image"),
),
("grid_thws".to_string(), FieldLayout::Batched),

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 Align Kimi field layout key with preprocessor output

KimiK25Spec::field_layouts declares a batched tensor named grid_thws, but KimiK25Processor delegates to QwenVLProcessorBase::preprocess, which emits image_grid_thw instead. Because the declared key never appears in model_specific_tensors, vLLM falls back to treating image_grid_thw as a shared tensor rather than per-image batched data; with multi-image prompts this misroutes grid metadata and can produce incorrect multimodal packing. Please use the actual emitted key name (and matching CPU hint key) in the spec.

Useful? React with 👍 / 👎.

@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 the current code and only fix it if needed.

Inline comments:
In `@crates/multimodal/src/registry/kimi_k25.rs`:
- Around line 79-94: The field-layout and keep-on-CPU key names in KimiK25Spec
are wrong: update the key used in field_layouts (function field_layouts) and
keep_on_cpu_keys (method keep_on_cpu_keys) from "grid_thws" to the actual
emitted tensor name "image_grid_thw" so the FieldLayout::Batched hint and CPU
pinning apply to the tensor produced by QwenVLBase (the mismatch between
"grid_thws" and "image_grid_thw" is causing Kimi tensor handling to fail).
🪄 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: 9e906e91-8134-4d62-8808-9eb35d7efcc6

📥 Commits

Reviewing files that changed from the base of the PR and between 381c5f6 and 2585607.

📒 Files selected for processing (5)
  • crates/multimodal/src/registry/kimi_k25.rs
  • crates/multimodal/src/registry/mod.rs
  • crates/multimodal/src/vision/image_processor.rs
  • crates/multimodal/src/vision/processors/kimi_k25.rs
  • crates/multimodal/src/vision/processors/mod.rs

Comment on lines +79 to +94
fn field_layouts(&self) -> HashMap<String, FieldLayout> {
// pixel_values is patchified: [total_patches, C, patch_h, patch_w].
// grid_thws is [num_images, 3] with (t, h, w) per image.
HashMap::from([
(
"pixel_values".to_string(),
FieldLayout::flat("patches_per_image"),
),
("grid_thws".to_string(), FieldLayout::Batched),
("patches_per_image".to_string(), FieldLayout::Batched),
])
}

fn keep_on_cpu_keys(&self) -> Vec<String> {
vec!["grid_thws".to_string()]
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify actual model_specific key naming in QwenVL base and spec contracts.

rg -n -C3 'image_grid_thw|grid_thws|patches_per_image|model_specific' crates/multimodal/src/vision/processors/qwen_vl_base.rs
rg -n -C3 'field_layouts|keep_on_cpu_keys|image_grid_thw|grid_thws|patches_per_image' crates/multimodal/src/registry/qwen_vl.rs crates/multimodal/src/registry/kimi_k25.rs

Repository: lightseekorg/smg

Length of output: 5511


Field-layout key name mismatch: grid_thws vs image_grid_thw will break Kimi tensor handling.

The KimiK25Spec declares field layouts and keep-on-CPU rules for "grid_thws" (lines 87, 93), but the underlying QwenVLBase processor emits "image_grid_thw" (qwen_vl_base.rs:400). This key mismatch prevents layout hints from applying to the actual tensor, breaking Kimi request assembly.

🔧 Proposed fix
    fn field_layouts(&self) -> HashMap<String, FieldLayout> {
        // pixel_values is patchified: [total_patches, C, patch_h, patch_w].
-       // grid_thws is [num_images, 3] with (t, h, w) per image.
+       // image_grid_thw is [num_images, 3] with (t, h, w) per image.
        HashMap::from([
            (
                "pixel_values".to_string(),
                FieldLayout::flat("patches_per_image"),
            ),
-           ("grid_thws".to_string(), FieldLayout::Batched),
+           ("image_grid_thw".to_string(), FieldLayout::Batched),
            ("patches_per_image".to_string(), FieldLayout::Batched),
        ])
    }

    fn keep_on_cpu_keys(&self) -> Vec<String> {
-       vec!["grid_thws".to_string()]
+       vec!["image_grid_thw".to_string()]
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn field_layouts(&self) -> HashMap<String, FieldLayout> {
// pixel_values is patchified: [total_patches, C, patch_h, patch_w].
// grid_thws is [num_images, 3] with (t, h, w) per image.
HashMap::from([
(
"pixel_values".to_string(),
FieldLayout::flat("patches_per_image"),
),
("grid_thws".to_string(), FieldLayout::Batched),
("patches_per_image".to_string(), FieldLayout::Batched),
])
}
fn keep_on_cpu_keys(&self) -> Vec<String> {
vec!["grid_thws".to_string()]
}
fn field_layouts(&self) -> HashMap<String, FieldLayout> {
// pixel_values is patchified: [total_patches, C, patch_h, patch_w].
// image_grid_thw is [num_images, 3] with (t, h, w) per image.
HashMap::from([
(
"pixel_values".to_string(),
FieldLayout::flat("patches_per_image"),
),
("image_grid_thw".to_string(), FieldLayout::Batched),
("patches_per_image".to_string(), FieldLayout::Batched),
])
}
fn keep_on_cpu_keys(&self) -> Vec<String> {
vec!["image_grid_thw".to_string()]
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/multimodal/src/registry/kimi_k25.rs` around lines 79 - 94, The
field-layout and keep-on-CPU key names in KimiK25Spec are wrong: update the key
used in field_layouts (function field_layouts) and keep_on_cpu_keys (method
keep_on_cpu_keys) from "grid_thws" to the actual emitted tensor name
"image_grid_thw" so the FieldLayout::Batched hint and CPU pinning apply to the
tensor produced by QwenVLBase (the mismatch between "grid_thws" and
"image_grid_thw" is causing Kimi tensor handling to fail).

@mergify

mergify Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Hi @ConnorLi96, 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 Apr 15, 2026
@github-actions

Copy link
Copy Markdown

This pull request has been automatically marked as stale because it has not had any activity within 14 days. It will be automatically closed if no further activity occurs within 16 days. Leave a comment if you feel this pull request should remain open. Thank you!

@github-actions github-actions Bot added the stale PR has been inactive for 14+ days label Apr 30, 2026
@zhyncs
zhyncs deleted the connorli/kimi-k25-vlm branch May 9, 2026 00:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

multimodal Multimodal crate changes needs-rebase PR has merge conflicts that need to be resolved stale PR has been inactive for 14+ days

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants