Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions crates/multimodal/src/registry/kimi_k25.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ impl ModelProcessorSpec for KimiK25VisionSpec {
}

fn matches(&self, metadata: &ModelMetadata) -> bool {
// Kimi-K3 reuses K2.5's MoonViT vision stack and `<|media_pad|>`
// placeholder (media_placeholder_token_id 163605), so it shares this
// spec.
// Kimi-K3 uses the same `<|media_pad|>` placeholder
// (media_placeholder_token_id 163605), patchification layout and
// prompt-replacement shape, so it shares this spec. Note that the two
// do *not* share a pixel pipeline — see `vision::processors::kimi_k3`
// for the patch budget and alpha-compositing differences.
let id = metadata.model_id.to_ascii_lowercase();
id.contains("kimi") && (id.contains("k2") || id.contains("k3"))
|| metadata
Expand Down
57 changes: 54 additions & 3 deletions crates/multimodal/src/vision/preprocessor_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,9 +278,14 @@ impl PreProcessorConfig {
.and_then(|v| v.as_u64())
.map(|v| v as usize);
}
// Also extract Kimi-specific limits into the extra map
// so processors can read them via get_extra()
for key in ["in_patch_limit", "patch_limit_on_one_side"] {
// Also extract Kimi-specific limits and the K3 transparency settings
// into the extra map so processors can read them via get_extra()
for key in [
"in_patch_limit",
"patch_limit_on_one_side",
"transparent_bg_config",
"transparent_bg_fill_stage",
] {
if !config.extra.contains_key(key) {
if let Some(v) = media_cfg.get(key) {
config.extra.insert(key.to_string(), v.clone());
Expand Down Expand Up @@ -448,6 +453,9 @@ impl PreProcessorConfig {
#[cfg(test)]
mod tests {
use super::*;
use crate::vision::transforms::{
TransparentBgConfig, TransparentBgFillStage, TransparentBgPattern,
};

#[test]
fn test_parse_clip_config() {
Expand Down Expand Up @@ -602,4 +610,47 @@ mod tests {
assert_eq!(config.get_patch_size(0), 14);
assert_eq!(config.merge_size, Some(2));
}

#[test]
fn test_parse_kimi_k3_transparency_settings() {
// Verbatim excerpt from moonshotai/Kimi-K3's preprocessor_config.json.
let json = r#"{
"media_proc_cfg": {
"in_patch_limit": 65536,
"patch_size": 14,
"merge_kernel_size": 2,
"patch_limit_on_one_side": 512,
"transparent_bg_config": {
"pattern": "chessboard",
"chessboard_square_size": 8,
"chessboard_square_on_top_left": true,
"chessboard_white_value": 255,
"chessboard_gray_value": 180
},
"transparent_bg_fill_stage": "after_resize"
}
}"#;

let config = PreProcessorConfig::from_json(json).unwrap();

assert_eq!(config.get_extra::<usize>("in_patch_limit"), Some(65536));
assert_eq!(
config.get_extra::<usize>("patch_limit_on_one_side"),
Some(512)
);

let bg = config
.get_extra::<TransparentBgConfig>("transparent_bg_config")
.expect("transparent_bg_config lifted out of media_proc_cfg");
assert_eq!(bg.pattern, TransparentBgPattern::Chessboard);
assert_eq!(bg.chessboard_square_size, 8);
assert!(bg.chessboard_square_on_top_left);
assert_eq!(bg.chessboard_white_value, 255);
assert_eq!(bg.chessboard_gray_value, 180);

assert_eq!(
config.get_extra::<TransparentBgFillStage>("transparent_bg_fill_stage"),
Some(TransparentBgFillStage::AfterResize)
);
}
}
40 changes: 37 additions & 3 deletions crates/multimodal/src/vision/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,9 @@ impl VisionProcessorRegistry {
Box::new(super::processors::Llama4VisionProcessor::new()),
);

// Register Kimi-K2.5 Vision (also used by Kimi-K3, same MoonViT stack)
// Register the Kimi MoonViT family. K3 shares the stack but ships a
// larger patch budget and chessboard alpha compositing, so it gets its
// own processor rather than reusing K2.5's.
registry.register(
"kimi-k2",
Box::new(super::processors::KimiK25Processor::new()),
Expand All @@ -336,11 +338,11 @@ impl VisionProcessorRegistry {
);
registry.register(
"kimi-k3",
Box::new(super::processors::KimiK25Processor::new()),
Box::new(super::processors::KimiK3Processor::new()),
);
registry.register(
"kimi_k3",
Box::new(super::processors::KimiK25Processor::new()),
Box::new(super::processors::KimiK3Processor::new()),
);

registry
Expand Down Expand Up @@ -394,6 +396,38 @@ mod tests {
assert_eq!(processor.model_name(), "inkling");
}

#[test]
fn test_registry_separates_kimi_k25_and_k3() {
// K3's pixel pipeline differs from K2.5's (larger patch budget, alpha
// compositing), so resolving a K3 id to the K2.5 processor is a bug.
let registry = VisionProcessorRegistry::with_defaults();

for id in ["moonshotai/Kimi-K3", "moonshotai/Kimi_K3-Instruct"] {
let processor = registry.find(id, None).expect("K3 vision processor");
assert_eq!(processor.model_name(), "kimi-k3", "{id}");
}
for id in ["moonshotai/Kimi-K2.5", "moonshotai/Kimi_K2-VL"] {
let processor = registry.find(id, None).expect("K2.5 vision processor");
assert_eq!(processor.model_name(), "kimi-k2.5", "{id}");
}

// model_type fallback must split the same way.
assert_eq!(
registry
.find("internal/checkpoint-final", Some("kimi_k3"))
.expect("K3 by model_type")
.model_name(),
"kimi-k3"
);
assert_eq!(
registry
.find("internal/checkpoint-final", Some("kimi_k2"))
.expect("K2.5 by model_type")
.model_name(),
"kimi-k2.5"
);
}

#[test]
fn test_registry_find() {
let mut registry = VisionProcessorRegistry::new();
Expand Down
Loading
Loading