Skip to content

Commit efa97f0

Browse files
shimmyshimmerworthant
authored andcommitted
kimi-k3 : add the MoonViT-3d vision tower (image path)
New kimik3 projector type, its graph builder and the mmproj converter. Also adds an optional clip.%s.attention.head_dim so build_vit stops deriving d_head from n_embd, which is wrong whenever a tower's qkv width differs from n_embd. Assisted-by: Claude Code
1 parent b2f13d7 commit efa97f0

9 files changed

Lines changed: 206 additions & 3 deletions

File tree

conversion/kimivl.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,79 @@ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iter
168168
name = name.replace("mm_projector.linear_", "mm_projector.proj.linear_", 1)
169169

170170
yield from super().modify_tensors(data_torch, name, bid)
171+
172+
173+
@ModelBase.register("KimiK3ForConditionalGeneration")
174+
class KimiK3VisionModel(MmprojModel):
175+
"""Kimi-K3 MoonViT-3d vision tower (image path).
176+
177+
Structurally the Kimi-K2.5 tower with RMSNorm, no biases, a non-square fused QKV
178+
(qkv_hidden_size 1536 vs vt_hidden_size 1024) and a post-norm patchmergerv2 projector.
179+
Video is out of scope: for t == 1 the temporal pool and temporal position term vanish.
180+
"""
181+
182+
def __init__(self, *args, **kwargs):
183+
super().__init__(*args, **kwargs)
184+
assert self.hparams_vision is not None, "Kimi-K3 requires vision_config in config.json"
185+
self.merge_kernel_size = tuple(self.hparams_vision.get("merge_kernel_size", [2, 2]))
186+
self.patch_size = self.hparams_vision.get("patch_size", 14)
187+
pos_emb_h = self.hparams_vision.get("init_pos_emb_height", 64)
188+
self.hparams_vision["image_size"] = pos_emb_h * self.patch_size
189+
190+
def set_gguf_parameters(self):
191+
super().set_gguf_parameters()
192+
assert self.hparams_vision is not None
193+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.KIMIK3)
194+
195+
# qkv width != n_embd, so the runtime cannot derive d_head
196+
n_head = self.hparams_vision["vt_num_attention_heads"]
197+
qkv_hidden = self.hparams_vision.get("qkv_hidden_size") or self.hparams_vision["vt_hidden_size"]
198+
assert qkv_hidden % n_head == 0, f"qkv_hidden_size {qkv_hidden} not divisible by {n_head} heads"
199+
self.gguf_writer.add_vision_head_dim(qkv_hidden // n_head)
200+
201+
self.gguf_writer.add_vision_use_gelu(True) # activation_func is gelu_pytorch_tanh
202+
self.gguf_writer.add_vision_attention_layernorm_eps(
203+
self.hparams_vision.get("projector_ln_eps", 1e-5))
204+
self.gguf_writer.add_vision_projector_scale_factor(self.merge_kernel_size[0])
205+
206+
in_patch_limit = self.preprocessor_config.get("media_proc_cfg", {}).get(
207+
"in_patch_limit", self.preprocessor_config.get("in_patch_limit", 16384))
208+
pixels_per_patch = self.patch_size ** 2
209+
self.gguf_writer.add_vision_min_pixels(8 * pixels_per_patch)
210+
self.gguf_writer.add_vision_max_pixels(in_patch_limit * pixels_per_patch)
211+
212+
@classmethod
213+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
214+
name, _ = item
215+
if not name.startswith(("vision_tower.", "mm_projector.")):
216+
return None
217+
return super().filter_tensors(item)
218+
219+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
220+
assert self.hparams_vision is not None
221+
n_head = self.hparams_vision["vt_num_attention_heads"]
222+
223+
if "wqkv" in name and "weight" in name:
224+
# de-interleave Q/K so the runtime can use build_rope_2d(interleave_freq=false)
225+
out_dim = data_torch.shape[0]
226+
qkv_dim = out_dim // 3
227+
head_dim = qkv_dim // n_head
228+
wq, wk, wv = (data_torch[:qkv_dim], data_torch[qkv_dim:2 * qkv_dim], data_torch[2 * qkv_dim:])
229+
230+
def deinterleave(w: Tensor) -> Tensor:
231+
return (w.reshape(n_head, head_dim // 4, 2, 2, w.shape[-1])
232+
.permute(0, 2, 1, 3, 4)
233+
.reshape(w.shape[0], w.shape[-1]))
234+
235+
data_torch = torch.cat([deinterleave(wq), deinterleave(wk), wv], dim=0)
236+
237+
if "pos_emb.weight" in name:
238+
# kept 3D: the runtime reads grid extents from ne[1]/ne[2]
239+
pass
240+
241+
if "mm_projector.proj.0." in name:
242+
name = name.replace(".proj.0.", ".proj.linear_1.")
243+
elif "mm_projector.proj.2." in name:
244+
name = name.replace(".proj.2.", ".proj.linear_2.")
245+
246+
yield from super().modify_tensors(data_torch, name, bid)

gguf-py/gguf/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4857,6 +4857,7 @@ class VisionProjectorType:
48574857
KIMIVL = "kimivl"
48584858
PADDLEOCR = "paddleocr"
48594859
KIMIK25 = "kimik25"
4860+
KIMIK3 = "kimik3"
48604861
LIGHTONOCR = "lightonocr"
48614862
COGVLM = "cogvlm"
48624863
JANUS_PRO = "janus_pro"

tools/mtmd/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ add_library(mtmd
3838
models/granite4-vision.cpp
3939
models/hunyuanvl.cpp
4040
models/internvl.cpp
41+
models/kimik3.cpp
4142
models/kimivl.cpp
4243
models/kimik25.cpp
4344
models/nemotron-v2-vl.cpp

tools/mtmd/clip-impl.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
#define KEY_PROJ_DIM "clip.%s.projection_dim"
4242
#define KEY_N_HEAD "clip.%s.attention.head_count"
4343
#define KEY_N_HEAD_KV "clip.%s.attention.head_count_kv"
44+
#define KEY_N_EMBD_HEAD "clip.%s.attention.head_dim"
4445
#define KEY_LAYER_NORM_EPS "clip.%s.attention.layer_norm_epsilon"
4546
#define KEY_FEATURE_LAYERS "clip.%s.feature_layer"
4647

@@ -366,6 +367,7 @@ enum projector_type {
366367
PROJECTOR_TYPE_YOUTUVL,
367368
PROJECTOR_TYPE_YASA2,
368369
PROJECTOR_TYPE_KIMIK25,
370+
PROJECTOR_TYPE_KIMIK3,
369371
PROJECTOR_TYPE_NEMOTRON_V2_VL,
370372
PROJECTOR_TYPE_HUNYUANVL,
371373
PROJECTOR_TYPE_EXAONE4_5,
@@ -421,6 +423,7 @@ static std::map<projector_type, std::string> PROJECTOR_TYPE_NAMES = {
421423
{ PROJECTOR_TYPE_YOUTUVL, "youtuvl"},
422424
{ PROJECTOR_TYPE_YASA2, "yasa2"},
423425
{ PROJECTOR_TYPE_KIMIK25, "kimik25"},
426+
{ PROJECTOR_TYPE_KIMIK3, "kimik3"},
424427
{ PROJECTOR_TYPE_NEMOTRON_V2_VL, "nemotron_v2_vl"},
425428
{ PROJECTOR_TYPE_EXAONE4_5, "exaone4_5"},
426429
{ PROJECTOR_TYPE_HUNYUANVL, "hunyuanvl"},

tools/mtmd/clip-model.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ struct clip_hparams {
5454
int32_t projection_dim = 0;
5555
int32_t n_head = 0;
5656
int32_t n_head_kv = 0;
57+
// 0 = derive from n_embd; set when qkv width != n_embd
58+
int32_t n_embd_head = 0;
5759
int32_t n_layer = 0;
5860
int32_t n_merge = 1; // number of patch merges **per-side**
5961

tools/mtmd/clip.cpp

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ clip_graph::clip_graph(clip_ctx * ctx, const clip_image_f32 & img) :
253253
n_embd(hparams.n_embd),
254254
n_head(hparams.n_head),
255255
n_head_kv(hparams.n_head_kv),
256-
d_head(n_head > 0 ? n_embd / n_head : 0),
256+
d_head(hparams.n_embd_head > 0 ? hparams.n_embd_head : (n_head > 0 ? n_embd / n_head : 0)),
257257
n_layer(hparams.n_layer),
258258
n_mmproj_embd(clip_n_mmproj_embd(ctx)),
259259
eps(hparams.eps),
@@ -367,13 +367,13 @@ ggml_tensor * clip_graph::build_vit(
367367
/* nb1 */ ggml_row_size(cur->type, d_head),
368368
/* nb2 */ cur->nb[1],
369369
/* nb3 */ cur->nb[1] * n_pos,
370-
/* offset */ ggml_row_size(cur->type, n_embd));
370+
/* offset */ ggml_row_size(cur->type, n_head * d_head));
371371

372372
Vcur = ggml_view_4d(ctx0, cur, d_head, n_head, n_pos, B,
373373
/* nb1 */ ggml_row_size(cur->type, d_head),
374374
/* nb2 */ cur->nb[1],
375375
/* nb3 */ cur->nb[1] * n_pos,
376-
/* offset */ ggml_row_size(cur->type, 2 * n_embd));
376+
/* offset */ ggml_row_size(cur->type, 2 * n_head * d_head));
377377

378378
if (layer.q_norm) {
379379
GGML_ASSERT(layer.q_norm->ne[0] == Qcur->ne[0]);
@@ -964,6 +964,10 @@ static std::unique_ptr<clip_graph> clip_get_graph_builder(clip_ctx * ctx, const
964964
{
965965
builder = std::make_unique<clip_graph_kimik25>(ctx, img);
966966
} break;
967+
case PROJECTOR_TYPE_KIMIK3:
968+
{
969+
builder = std::make_unique<clip_graph_kimik3>(ctx, img);
970+
} break;
967971
case PROJECTOR_TYPE_COGVLM:
968972
{
969973
builder = std::make_unique<clip_graph_cogvlm>(ctx, img);
@@ -1173,6 +1177,7 @@ struct clip_model_loader {
11731177
const char * prefix = is_vision ? "vision" : "audio";
11741178
get_u32(string_format(KEY_N_EMBD, prefix), hparams.n_embd);
11751179
get_u32(string_format(KEY_N_HEAD, prefix), hparams.n_head);
1180+
get_u32(string_format(KEY_N_EMBD_HEAD, prefix), hparams.n_embd_head, false);
11761181
get_u32(string_format(KEY_N_FF, prefix), hparams.n_ff);
11771182
get_u32(string_format(KEY_N_BLOCK, prefix), hparams.n_layer);
11781183
get_u32(string_format(KEY_PROJ_DIM, prefix), hparams.projection_dim);
@@ -1410,6 +1415,23 @@ struct clip_model_loader {
14101415
hparams.rope_theta = 10000.0f;
14111416
get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false);
14121417

1418+
int min_pixels = 0, max_pixels = 0;
1419+
get_u32(KEY_IMAGE_MIN_PIXELS, min_pixels, false);
1420+
get_u32(KEY_IMAGE_MAX_PIXELS, max_pixels, false);
1421+
if (min_pixels > 0 && max_pixels > 0) {
1422+
hparams.image_min_pixels = min_pixels;
1423+
hparams.image_max_pixels = max_pixels;
1424+
hparams.warmup_image_size = static_cast<int>(std::sqrt(max_pixels));
1425+
} else {
1426+
hparams.set_limit_image_tokens(2, 4096);
1427+
}
1428+
} break;
1429+
case PROJECTOR_TYPE_KIMIK3:
1430+
{
1431+
hparams.image_resize_algo = RESIZE_ALGO_BILINEAR;
1432+
hparams.rope_theta = 10000.0f;
1433+
get_u32(KEY_PROJ_SCALE_FACTOR, hparams.n_merge, false);
1434+
14131435
int min_pixels = 0, max_pixels = 0;
14141436
get_u32(KEY_IMAGE_MIN_PIXELS, min_pixels, false);
14151437
get_u32(KEY_IMAGE_MAX_PIXELS, max_pixels, false);
@@ -2343,6 +2365,13 @@ struct clip_model_loader {
23432365
model.mm_2_w = get_tensor(string_format(TN_LLAVA_PROJ, 2, "weight"));
23442366
model.mm_2_b = get_tensor(string_format(TN_LLAVA_PROJ, 2, "bias"));
23452367
} break;
2368+
case PROJECTOR_TYPE_KIMIK3:
2369+
{
2370+
// patchmergerv2, bias-free, norm after the projection
2371+
model.mm_1_w = get_tensor(string_format(TN_LLAVA_PROJ, 1, "weight"));
2372+
model.mm_2_w = get_tensor(string_format(TN_LLAVA_PROJ, 2, "weight"));
2373+
model.mm_post_norm_w = get_tensor(string_format(TN_MM_POST_NORM, "weight"));
2374+
} break;
23462375
case PROJECTOR_TYPE_KIMIVL:
23472376
case PROJECTOR_TYPE_PADDLEOCR:
23482377
case PROJECTOR_TYPE_KIMIK25:
@@ -3424,6 +3453,7 @@ int clip_n_output_tokens(const clip_ctx * ctx, const clip_image_f32 * img) {
34243453
case PROJECTOR_TYPE_LFM2:
34253454
case PROJECTOR_TYPE_KIMIVL:
34263455
case PROJECTOR_TYPE_KIMIK25:
3456+
case PROJECTOR_TYPE_KIMIK3:
34273457
{
34283458
// dynamic size
34293459
int out_patch_size = params.patch_size * ctx->model.hparams.n_merge;
@@ -4108,6 +4138,7 @@ bool clip_image_batch_encode(clip_ctx * ctx, int n_threads, const clip_image_f32
41084138
case PROJECTOR_TYPE_PIXTRAL:
41094139
case PROJECTOR_TYPE_KIMIVL:
41104140
case PROJECTOR_TYPE_KIMIK25:
4141+
case PROJECTOR_TYPE_KIMIK3:
41114142
case PROJECTOR_TYPE_LIGHTONOCR:
41124143
{
41134144
// set the 2D positions
@@ -4661,6 +4692,7 @@ int clip_n_mmproj_embd(const struct clip_ctx * ctx) {
46614692
case PROJECTOR_TYPE_KIMIVL:
46624693
case PROJECTOR_TYPE_PADDLEOCR:
46634694
case PROJECTOR_TYPE_KIMIK25:
4695+
case PROJECTOR_TYPE_KIMIK3:
46644696
case PROJECTOR_TYPE_YASA2:
46654697
return ctx->model.mm_2_w->ne[1];
46664698
case PROJECTOR_TYPE_HUNYUANVL:

tools/mtmd/models/kimik3.cpp

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#include "models.h"
2+
3+
#include <cmath>
4+
#include <cstring>
5+
6+
// Kimi-K3 MoonViT-3d, image path.
7+
// Follows clip_graph_kimik25, but with RMSNorm, no biases, qkv width != n_embd, and a post-norm patchmergerv2 projector.
8+
// Images only: at t == 1 the temporal pool and the temporal position term vanish.
9+
10+
ggml_tensor * clip_graph_kimik3::resize_position_embeddings_3d(uint32_t interpolation_mode) {
11+
ggml_tensor * pos_embd = model.position_embeddings;
12+
const int height = img.ny() / patch_size;
13+
const int width = img.nx() / patch_size;
14+
15+
GGML_ASSERT(pos_embd);
16+
17+
const int64_t stored_c = pos_embd->ne[0];
18+
const int64_t orig_w = pos_embd->ne[1];
19+
const int64_t orig_h = pos_embd->ne[2];
20+
21+
GGML_ASSERT(stored_c == n_embd);
22+
23+
if (height == (int) orig_h && width == (int) orig_w) {
24+
return ggml_cont_2d(ctx0, pos_embd, n_embd, width * height);
25+
}
26+
27+
pos_embd = ggml_permute(ctx0, pos_embd, 2, 1, 0, 3);
28+
pos_embd = ggml_interpolate(ctx0, pos_embd, height, width, n_embd, 1, interpolation_mode);
29+
pos_embd = ggml_permute(ctx0, pos_embd, 2, 1, 0, 3);
30+
pos_embd = ggml_cont_2d(ctx0, pos_embd, n_embd, width * height);
31+
return pos_embd;
32+
}
33+
34+
ggml_cgraph * clip_graph_kimik3::build() {
35+
ggml_tensor * pos_h = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_patches);
36+
ggml_set_name(pos_h, "pos_h");
37+
ggml_set_input(pos_h);
38+
39+
ggml_tensor * pos_w = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_patches);
40+
ggml_set_name(pos_w, "pos_w");
41+
ggml_set_input(pos_w);
42+
43+
ggml_tensor * learned_pos_embd = resize_position_embeddings_3d(GGML_SCALE_MODE_BILINEAR);
44+
45+
// Q/K are de-interleaved during conversion.
46+
auto add_pos = [&](ggml_tensor * cur, const clip_layer &) {
47+
return build_rope_2d(ctx0, cur, pos_w, pos_h, hparams.rope_theta, false);
48+
};
49+
50+
ggml_tensor * inp = build_inp();
51+
inp = ggml_add(ctx0, inp, learned_pos_embd);
52+
53+
ggml_tensor * cur = build_vit(
54+
inp, n_patches,
55+
NORM_TYPE_RMS,
56+
hparams.ffn_op,
57+
nullptr,
58+
add_pos);
59+
cb(cur, "vit_out", -1);
60+
61+
{
62+
const int scale_factor = model.hparams.n_merge;
63+
cur = build_patch_merge_permute(cur, scale_factor);
64+
65+
cur = build_ffn(cur,
66+
model.mm_1_w, nullptr,
67+
nullptr, nullptr,
68+
model.mm_2_w, nullptr,
69+
FFN_GELU,
70+
-1);
71+
cb(cur, "proj_mlp_out", -1);
72+
73+
cur = build_norm(cur, model.mm_post_norm_w, nullptr, NORM_TYPE_RMS, hparams.eps, -1);
74+
cb(cur, "proj_out", -1);
75+
}
76+
77+
ggml_build_forward_expand(gf, cur);
78+
79+
return gf;
80+
}

tools/mtmd/models/models.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,13 @@ struct clip_graph_kimik25 : clip_graph {
217217
ggml_tensor * resize_position_embeddings_3d(uint32_t interpolation_mode);
218218
};
219219

220+
struct clip_graph_kimik3 : clip_graph {
221+
clip_graph_kimik3(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
222+
ggml_cgraph * build() override;
223+
224+
ggml_tensor * resize_position_embeddings_3d(uint32_t interpolation_mode);
225+
};
226+
220227
struct clip_graph_exaone4_5 : clip_graph {
221228
clip_graph_exaone4_5(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
222229
ggml_cgraph * build() override;

tools/mtmd/mtmd.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,7 @@ struct mtmd_context {
561561
image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
562562
} break;
563563
case PROJECTOR_TYPE_KIMIK25:
564+
case PROJECTOR_TYPE_KIMIK3:
564565
{
565566
// GLM-5.2-V reuses the Kimi-K2.5 vision encoder and projector, but marks
566567
// images with its own tokens, so decide based on the text model vocab

0 commit comments

Comments
 (0)