-
Notifications
You must be signed in to change notification settings - Fork 44
kimi-k3 : the MoonViT-3d vision tower and full-size loading fixes #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: kimi-k3-text-upstream
Are you sure you want to change the base?
Changes from all commits
df20252
27fd568
edfd4c1
883f2c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -171,3 +171,79 @@ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iter | |
| name = name.replace("mm_projector.linear_", "mm_projector.proj.linear_", 1) | ||
|
|
||
| yield from super().modify_tensors(data_torch, name, bid) | ||
|
|
||
|
|
||
| @ModelBase.register("KimiK3ForConditionalGeneration") | ||
| class KimiK3VisionModel(MmprojModel): | ||
| """Kimi-K3 MoonViT-3d vision tower (image path). | ||
|
|
||
| Structurally the Kimi-K2.5 tower with RMSNorm, no biases, a non-square fused QKV | ||
| (qkv_hidden_size 1536 vs vt_hidden_size 1024) and a post-norm patchmergerv2 projector. | ||
| Video is out of scope: for t == 1 the temporal pool and temporal position term vanish. | ||
| """ | ||
|
|
||
| def __init__(self, *args, **kwargs): | ||
| super().__init__(*args, **kwargs) | ||
| assert self.hparams_vision is not None, "Kimi-K3 requires vision_config in config.json" | ||
| self.merge_kernel_size = tuple(self.hparams_vision.get("merge_kernel_size", [2, 2])) | ||
| self.patch_size = self.hparams_vision.get("patch_size", 14) | ||
| pos_emb_h = self.hparams_vision.get("init_pos_emb_height", 64) | ||
| self.hparams_vision["image_size"] = pos_emb_h * self.patch_size | ||
|
|
||
| def set_gguf_parameters(self): | ||
| super().set_gguf_parameters() | ||
| assert self.hparams_vision is not None | ||
| self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.KIMIK3) | ||
|
|
||
| # qkv width != n_embd, so the runtime cannot derive d_head | ||
| n_head = self.hparams_vision["vt_num_attention_heads"] | ||
| qkv_hidden = self.hparams_vision.get("qkv_hidden_size") or self.hparams_vision["vt_hidden_size"] | ||
| assert qkv_hidden % n_head == 0, f"qkv_hidden_size {qkv_hidden} not divisible by {n_head} heads" | ||
| self.gguf_writer.add_vision_head_dim(qkv_hidden // n_head) | ||
|
|
||
| self.gguf_writer.add_vision_use_gelu(True) # activation_func is gelu_pytorch_tanh | ||
| self.gguf_writer.add_vision_attention_layernorm_eps( | ||
| self.hparams_vision.get("projector_ln_eps", 1e-5)) | ||
| self.gguf_writer.add_vision_projector_scale_factor(self.merge_kernel_size[0]) | ||
|
|
||
| in_patch_limit = self.preprocessor_config.get("media_proc_cfg", {}).get( | ||
| "in_patch_limit", self.preprocessor_config.get("in_patch_limit", 16384)) | ||
| pixels_per_patch = self.patch_size ** 2 | ||
| self.gguf_writer.add_vision_min_pixels(8 * pixels_per_patch) | ||
| self.gguf_writer.add_vision_max_pixels(in_patch_limit * pixels_per_patch) | ||
|
|
||
| @classmethod | ||
| def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None: | ||
| name, _ = item | ||
| if not name.startswith(("vision_tower.", "mm_projector.")): | ||
| return None | ||
| return super().filter_tensors(item) | ||
|
Comment on lines
+216
to
+220
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Once dispatch reaches this converter, MoonViT's Useful? React with 👍 / 👎. |
||
|
|
||
| def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]: | ||
| assert self.hparams_vision is not None | ||
| n_head = self.hparams_vision["vt_num_attention_heads"] | ||
|
|
||
| if "wqkv" in name and "weight" in name: | ||
| # de-interleave Q/K so the runtime can use build_rope_2d(interleave_freq=false) | ||
| out_dim = data_torch.shape[0] | ||
| qkv_dim = out_dim // 3 | ||
| head_dim = qkv_dim // n_head | ||
| wq, wk, wv = (data_torch[:qkv_dim], data_torch[qkv_dim:2 * qkv_dim], data_torch[2 * qkv_dim:]) | ||
|
|
||
| def deinterleave(w: Tensor) -> Tensor: | ||
| return (w.reshape(n_head, head_dim // 4, 2, 2, w.shape[-1]) | ||
| .permute(0, 2, 1, 3, 4) | ||
| .reshape(w.shape[0], w.shape[-1])) | ||
|
|
||
| data_torch = torch.cat([deinterleave(wq), deinterleave(wk), wv], dim=0) | ||
|
|
||
| if "pos_emb.weight" in name: | ||
| # kept 3D: the runtime reads grid extents from ne[1]/ne[2] | ||
| pass | ||
|
|
||
| if "mm_projector.proj.0." in name: | ||
| name = name.replace(".proj.0.", ".proj.linear_1.") | ||
| elif "mm_projector.proj.2." in name: | ||
| name = name.replace(".proj.2.", ".proj.linear_2.") | ||
|
|
||
| yield from super().modify_tensors(data_torch, name, bid) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
After the mmproj dispatch is fixed, the checkpoint's Useful? React with 👍 / 👎. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1041,6 +1041,10 @@ static std::unique_ptr<clip_graph> clip_get_graph_builder(clip_ctx * ctx, const | |
| { | ||
| builder = std::make_unique<clip_graph_deepseek4v>(ctx, img); | ||
| } break; | ||
| case PROJECTOR_TYPE_KIMIK3: | ||
| { | ||
| builder = std::make_unique<clip_graph_kimik3>(ctx, img); | ||
| } break; | ||
| case PROJECTOR_TYPE_COGVLM: | ||
| { | ||
| builder = std::make_unique<clip_graph_cogvlm>(ctx, img); | ||
|
|
@@ -1614,6 +1618,23 @@ struct clip_model_loader { | |
| const int warmup_side = (int) std::sqrt((double) std::min(256, hparams.dsv4_max_n_token)); | ||
| hparams.set_warmup_n_tokens(warmup_side * warmup_side); | ||
| } break; | ||
| case PROJECTOR_TYPE_KIMIK3: | ||
| { | ||
| hparams.image_resize_algo = RESIZE_ALGO_BILINEAR; | ||
| hparams.rope_theta = 10000.0f; | ||
| get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false); | ||
|
|
||
| int min_pixels = 0, max_pixels = 0; | ||
| get_u32(KEY_IMAGE_MIN_PIXELS, min_pixels, false); | ||
| get_u32(KEY_IMAGE_MAX_PIXELS, max_pixels, false); | ||
| if (min_pixels > 0 && max_pixels > 0) { | ||
| hparams.image_min_pixels = min_pixels; | ||
| hparams.image_max_pixels = max_pixels; | ||
|
Comment on lines
+1631
to
+1632
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a Kimi-K3 mmproj contains the positive min/max pixel metadata emitted by this converter, these assignments always win, so AGENTS.md reference: AGENTS.md:L80-L80 Useful? React with 👍 / 👎. |
||
| hparams.warmup_image_size = static_cast<int>(std::sqrt(max_pixels)); | ||
| } else { | ||
| hparams.set_limit_image_tokens(2, 4096); | ||
| } | ||
| } break; | ||
| case PROJECTOR_TYPE_GEMMA3: | ||
| { | ||
| // default value (used by all model sizes in gemma 3 family) | ||
|
|
@@ -2732,6 +2753,13 @@ struct clip_model_loader { | |
| model.mm_2_w = get_tensor(string_format(TN_LLAVA_PROJ, 2, "weight")); | ||
| model.mm_2_b = get_tensor(string_format(TN_LLAVA_PROJ, 2, "bias")); | ||
| } break; | ||
| case PROJECTOR_TYPE_KIMIK3: | ||
| { | ||
| // patchmergerv2, bias-free, norm after the projection | ||
| model.mm_1_w = get_tensor(string_format(TN_LLAVA_PROJ, 1, "weight")); | ||
| model.mm_2_w = get_tensor(string_format(TN_LLAVA_PROJ, 2, "weight")); | ||
| model.mm_post_norm_w = get_tensor(string_format(TN_MM_POST_NORM, "weight")); | ||
| } break; | ||
| case PROJECTOR_TYPE_KIMIVL: | ||
| case PROJECTOR_TYPE_PADDLEOCR: | ||
| case PROJECTOR_TYPE_KIMIK25: | ||
|
|
@@ -4184,6 +4212,7 @@ int clip_n_output_tokens(const clip_ctx * ctx, const clip_image_f32 * img) { | |
| case PROJECTOR_TYPE_LFM2: | ||
| case PROJECTOR_TYPE_KIMIVL: | ||
| case PROJECTOR_TYPE_KIMIK25: | ||
| case PROJECTOR_TYPE_KIMIK3: | ||
| { | ||
| // dynamic size | ||
| int out_patch_size = params.patch_size * ctx->model.hparams.n_merge; | ||
|
|
@@ -5053,6 +5082,7 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) { | |
| case PROJECTOR_TYPE_PIXTRAL: | ||
| case PROJECTOR_TYPE_KIMIVL: | ||
| case PROJECTOR_TYPE_KIMIK25: | ||
| case PROJECTOR_TYPE_KIMIK3: | ||
| case PROJECTOR_TYPE_LIGHTONOCR: | ||
| { | ||
| // set the 2D positions | ||
|
|
@@ -5978,6 +6008,7 @@ int clip_n_mmproj_embd(const struct clip_ctx * ctx) { | |
| case PROJECTOR_TYPE_KIMIVL: | ||
| case PROJECTOR_TYPE_PADDLEOCR: | ||
| case PROJECTOR_TYPE_KIMIK25: | ||
| case PROJECTOR_TYPE_KIMIK3: | ||
| case PROJECTOR_TYPE_YASA2: | ||
| case PROJECTOR_TYPE_DEEPSEEK4V: | ||
| return ctx->model.mm_2_w->ne[1]; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| #include "models.h" | ||
|
|
||
| #include <cmath> | ||
| #include <cstring> | ||
|
|
||
| // Kimi-K3 MoonViT-3d, image path. | ||
| // Follows clip_graph_kimik25, but with RMSNorm, no biases, qkv width != n_embd, and a post-norm patchmergerv2 projector. | ||
| // Images only: at t == 1 the temporal pool and the temporal position term vanish. | ||
|
|
||
| ggml_tensor * clip_graph_kimik3::resize_position_embeddings_3d(uint32_t interpolation_mode) { | ||
| ggml_tensor * pos_embd = model.position_embeddings; | ||
| const int height = img.ny() / patch_size; | ||
| const int width = img.nx() / patch_size; | ||
|
|
||
| GGML_ASSERT(pos_embd); | ||
|
|
||
| const int64_t stored_c = pos_embd->ne[0]; | ||
| const int64_t orig_w = pos_embd->ne[1]; | ||
| const int64_t orig_h = pos_embd->ne[2]; | ||
|
|
||
| GGML_ASSERT(stored_c == n_embd); | ||
|
|
||
| if (height == (int) orig_h && width == (int) orig_w) { | ||
| return ggml_cont_2d(ctx0, pos_embd, n_embd, width * height); | ||
| } | ||
|
|
||
| pos_embd = ggml_permute(ctx0, pos_embd, 2, 1, 0, 3); | ||
| pos_embd = ggml_interpolate(ctx0, pos_embd, height, width, n_embd, 1, interpolation_mode); | ||
| pos_embd = ggml_permute(ctx0, pos_embd, 2, 1, 0, 3); | ||
| pos_embd = ggml_cont_2d(ctx0, pos_embd, n_embd, width * height); | ||
| return pos_embd; | ||
| } | ||
|
|
||
| ggml_cgraph * clip_graph_kimik3::build() { | ||
| ggml_tensor * pos_h = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_patches); | ||
| ggml_set_name(pos_h, "pos_h"); | ||
| ggml_set_input(pos_h); | ||
|
|
||
| ggml_tensor * pos_w = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_patches); | ||
| ggml_set_name(pos_w, "pos_w"); | ||
| ggml_set_input(pos_w); | ||
|
|
||
| ggml_tensor * learned_pos_embd = resize_position_embeddings_3d(GGML_SCALE_MODE_BILINEAR); | ||
|
|
||
| // Q/K are de-interleaved during conversion. | ||
| auto add_pos = [&](ggml_tensor * cur, const clip_layer &) { | ||
| return build_rope_2d(ctx0, cur, pos_w, pos_h, hparams.rope_theta, false); | ||
| }; | ||
|
|
||
| ggml_tensor * inp = build_inp(); | ||
| inp = ggml_add(ctx0, inp, learned_pos_embd); | ||
|
|
||
| ggml_tensor * cur = build_vit( | ||
| inp, n_patches, | ||
| NORM_TYPE_RMS, | ||
| hparams.ffn_op, | ||
| nullptr, | ||
| add_pos); | ||
| cb(cur, "vit_out", -1); | ||
|
|
||
| { | ||
| const int scale_factor = model.hparams.n_merge; | ||
| cur = build_patch_merge_permute(cur, scale_factor); | ||
|
|
||
| cur = build_ffn(cur, | ||
| model.mm_1_w, nullptr, | ||
| nullptr, nullptr, | ||
| model.mm_2_w, nullptr, | ||
| FFN_GELU, | ||
| -1); | ||
| cb(cur, "proj_mlp_out", -1); | ||
|
|
||
| cur = build_norm(cur, model.mm_post_norm_w, nullptr, NORM_TYPE_RMS, hparams.eps, -1); | ||
| cb(cur, "proj_out", -1); | ||
| } | ||
|
|
||
| ggml_build_forward_expand(gf, cur); | ||
|
|
||
| return gf; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
convert_hf_to_gguf.py --mmprojprocesses a Kimi-K3 config,get_model_class(..., mmproj=True)checksMMPROJ_MODEL_MAPbefore importing this decorated class, but that map has noKimiK3ForConditionalGenerationentry. The command therefore raisesNotImplementedError: Architecture 'KimiK3ForConditionalGeneration' not supportedbefore any vision tensors are converted; add the architecture-to-kimivldispatch entry alongside Kimi-K2.5.Useful? React with 👍 / 👎.