Skip to content

kimi-k3 : the MoonViT-3d vision tower and full-size loading fixes - #70

Open
danielhanchen wants to merge 4 commits into
kimi-k3-text-upstreamfrom
kimi-k3-vision-only
Open

kimi-k3 : the MoonViT-3d vision tower and full-size loading fixes#70
danielhanchen wants to merge 4 commits into
kimi-k3-text-upstreamfrom
kimi-k3-vision-only

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

ggml-org#26185 has taken most of what unslothai#44 carried. Its history now has its own copies of the Kimi K3 chat format, message_delimiters, the LLAMA_MAX_EXPERTS bump to 1024, the converter fixes and the archs-test support, and one commit says so directly: fix: apply nits from @ngxson and text fixes from @danielhanchen.

What it does not have is the vision half. MoonViT appears there only in conversion/kimivl.py; there is no tools/mtmd/models/kimik3.cpp, so the image path is absent, and the full-size loading fixes are missing too.

This is only that remainder, on top of 26185's current head (a614fab10b), so the diff is what we still add and nothing that is already upstream:

  • kimi-k3 : fixes for loading and running the full-size model (conversion/base.py, src/models/kimi-k3.cpp)
  • kimi-k3 : add the MoonViT-3d vision tower (image path) (the mtmd model, clip wiring, converter, constants)

Ten files, +209/-2, against unslothai#48's stack which duplicates the text-side work and has to be re-reconciled with 26185 every time either side moves.

One resolution worth noting: cherry-picking the vision commit brought a models/inkling.cpp line into tools/mtmd/CMakeLists.txt, since our old branch had Inkling merged. That file does not exist here and would have broken the build, so only models/kimik3.cpp is added. I checked every models/*.cpp the CMakeLists references resolves to a real file.

The base branch kimi-k3-text-upstream is 26185 at a614fab10b, so this PR reads as just our delta.

@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

llama.cpp/tools/mtmd/clip.cpp

Lines 1522 to 1525 in 6dd3474

if (min_pixels > 0 && max_pixels > 0) {
hparams.image_min_pixels = min_pixels;
hparams.image_max_pixels = max_pixels;
hparams.warmup_image_size = static_cast<int>(std::sqrt(max_pixels));

P2 Badge Warm up with a smaller Kimi-K3 image

With the converter's default in_patch_limit of 16384 and a 14-pixel patch, max_pixels is 3,211,264, so this initializes the model by building a 1792x1792 graph with 16,384 pre-merge attention tokens. On CPU and other backends without flash attention, the quadratic attention scores alone require over 1 GiB, so loading can OOM even when the user will send only small images; preserve the advertised maximum but override warmup to a modest representative token grid as the other dynamic projectors do.

ℹ️ 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 thread conversion/kimivl.py
Comment on lines +173 to +174
@ModelBase.register("KimiK3ForConditionalGeneration")
class KimiK3VisionModel(MmprojModel):

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 Register Kimi-K3 in the mmproj dispatch map

When convert_hf_to_gguf.py --mmproj processes a Kimi-K3 config, get_model_class(..., mmproj=True) checks MMPROJ_MODEL_MAP before importing this decorated class, but that map has no KimiK3ForConditionalGeneration entry. The command therefore raises NotImplementedError: Architecture 'KimiK3ForConditionalGeneration' not supported before any vision tensors are converted; add the architecture-to-kimivl dispatch entry alongside Kimi-K2.5.

Useful? React with 👍 / 👎.

Comment thread conversion/kimivl.py
elif "mm_projector.proj.2." in name:
name = name.replace(".proj.2.", ".proj.linear_2.")

yield from super().modify_tensors(data_torch, name, bid)

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 Map the Kimi-K3 post-projector RMSNorm tensor

After the mmproj dispatch is fixed, the checkpoint's mm_projector.post_norm.weight reaches this call unchanged, but the MMPROJ tensor map's V_MM_POST_NORM aliases do not include that Kimi-K3 name. ModelBase.map_tensor_name() consequently raises ValueError during conversion, while clip_model_loader later requires mm.post_norm.weight; rename this tensor here or add the corresponding tensor-map alias.

Useful? React with 👍 / 👎.

Comment thread conversion/kimivl.py
Comment on lines +213 to +217
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)

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 Exclude the unused temporal position parameter

Once dispatch reaches this converter, MoonViT's vision_tower.patch_embed.pos_emb.time_weight also passes this broad filter. The new graph explicitly supports only t == 1 and has no temporal-position input, while the MMPROJ tensor map recognizes only the spatial vision_tower.patch_embed.pos_emb; passing the temporal parameter to super().modify_tensors() therefore raises ValueError instead of producing an image-only mmproj. Filter this temporal-only parameter out.

Useful? React with 👍 / 👎.

danielhanchen added a commit that referenced this pull request Aug 11, 2026
The first pin for #95 was cut from fork master, whose merge base with the
current base tag is from June 10, so merging it dragged in the whole
fork/upstream divergence and conflicted across ~1600 files.

Rebased the change onto b10359 and repinned to e2e842a. #95 now sits on
penalties-upstream-base, matching how #70 and #91 are carried.

Verified: all five pins merge onto b10359 in pr-set.json order, #70 via the
additive resolver as before, everything else clean.
shimmyshimmer and others added 3 commits August 17, 2026 13:36
Graph node budget, LLM_TYPE for the 93-layer config, four hparams promoted to
required, and an E8M0 NaN check during the MXFP4 repack. Details in the PR
description.

Assisted-by: Claude Code
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
build_kda consumes ssm_a through a broadcast ggml_mul, never through
ggml_ssm_scan, but it was registered as LLM_TENSOR_SSM_A, which
llama-arch.cpp maps to GGML_OP_SSM_SCAN. create_tensor probes the buffer
type with that op through weight_buft_supported, no backend offers
SSM_SCAN for a [n_head] tensor, the probe fails, and ssm_a is placed on
the CPU along with everything downstream of it.

LLM_TENSOR_SSM_A_NOSCAN exists for exactly this case and maps to
GGML_OP_MUL. Both spellings resolve to the same "blk.%d.ssm_a" name, so
there is no GGUF change and no converter change. qwen3next.cpp and
qwen35moe.cpp already declare their A this way.

Measured on Kimi-K3-Mini across two GPUs, llama-bench pp2048:

  before   10705.91 +- 445.93 tok/s
  after    17009.96 +- 360.89 tok/s    1.59x

The loader reports it directly. Before the change it prints "tensor
'token_embd.weight' (bf16) (and 4 others) cannot be used with preferred
buffer type CUDA_Host, using CPU instead"; after it prints "(and 0
others)".
@nicholasshirley

Copy link
Copy Markdown

Tested 27fd568 on its own (cherry-picked with git am onto ggml-org master 749f688; applied
clean, also git apply --checks clean against master 8e93a97 as of 2026-09-02). Kimi-K3 image
input works end-to-end here.

Setup: unsloth Kimi-K3-UD-Q4_K_XL + mmproj-F16.gguf; llama-server (router mode); 1× RTX 4090
24 GB + 1.5 TB DDR5 (EPYC); experts and most attention on CPU via -ot, projector on CPU
(--no-mmproj-offload), -ub 4096, ctx 262144, -ctk/-ctv q8_0, --no-warmup, flash-attn auto.

Load log:

[mtmd] estimated worst-case memory usage of mmproj is 861.39 MiB (took 25.90 ms)
clip_ctx: CLIP using CPU backend
load_hparams: projector:          kimik3
load_hparams: image_size:         896
load_hparams: image_min_pixels:   1568
load_hparams: image_max_pixels:   12845056
srv load_model: loaded multimodal model, '.../mmproj-F16.gguf'

Results:

  • 1500×500 PNG (media/llama1-logo.png) → 972 image tokens, ViT encode 1.9 s on CPU, image
    prefill 47.6 s; description correct (logo, colours, the C-as-llama-face detail).
  • 2875×1500 PNG (media/matmul.png) → 5562 image tokens (batches 4096 + 1466), ViT encode 37.7 s,
    prefill 120 s; the model read the axis labels (ne00/ne01, row-/column-major, C^T = A·B^T) correctly.
  • Text decode (3.9 t/s) and tool calls unchanged.

One observation: --image-max-tokens is ignored on the kimik3 path — the GGUF min/max pixel
limits win (same as kimik25), so a max-size image is 16384 image tokens. May be intentional;
worth knowing for CPU-projector setups.

Happy to test if this goes to ggml-org.

# Conflicts:
#	src/models/kimi-k3.cpp
#	tools/mtmd/clip.cpp
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-03T08:44:25.471226Z 883f2c9 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@danielhanchen

Copy link
Copy Markdown
Member Author

Refreshed this onto b10775, and moved the base branch kimi-k3-text-upstream to the same tag so the diff here stays the nine kimi-k3 files instead of five weeks of upstream.

The pin had stopped merging in the nightly:

refused src/models/kimi-k3.cpp: merge base is not empty, so at least one side edited existing text
unslothai/llama.cpp#70 (edfd4c1a3) does not merge cleanly onto b10760 + the PRs listed before it

Upstream landed the same change as ggml-org#28121 (f28493c78, "models : appropriately flag noscan ssm_a tensors"), so the tip commit here was re-applying work the base already had, with a longer comment, and every merge came out as an edit/edit conflict. Resolved by taking upstream's line verbatim; the code is identical either way and holding our own wording would recreate the conflict on the next upstream edit of that line.

The two real fixes in that file are untouched:

    // required: a silent default here loads cleanly and produces garbage
    ml.get_key(LLM_KV_EXPERT_LATENT_LENGTH,       hparams.n_expert_latent);
...
    GGML_ASSERT(hparams.attn_res_block_size > 0 && "Kimi-K3 requires attn_res.block_size");
    GGML_ASSERT(hparams.n_expert_latent   > 0 && "Kimi-K3 requires expert_latent_length");

tools/mtmd/clip.cpp also collided, because upstream landed DeepSeek-V4-Flash-Vision (ggml-org#28133) into the same two switches. Both hunks are a PROJECTOR_TYPE_DEEPSEEK4V arm next to our PROJECTOR_TYPE_KIMIK3 arm, so both were resolved by keeping both arms, upstream's first.

git diff b10775..HEAD is exactly the nine files this PR is about, nothing else.

@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: 883f2c9ba7

ℹ️ 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 thread tools/mtmd/clip.cpp
Comment on lines +1631 to +1632
hparams.image_min_pixels = min_pixels;
hparams.image_max_pixels = max_pixels;

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 Honor custom token limits for Kimi-K3

When a Kimi-K3 mmproj contains the positive min/max pixel metadata emitted by this converter, these assignments always win, so --image-min-tokens and --image-max-tokens are ignored despite being copied into custom_image_*_tokens. In particular, users cannot lower the default 4096-token image budget to control memory use. Apply the custom values with the existing set_limit_image_tokens logic, including the patch_size * n_merge conversion, before computing the warmup size.

AGENTS.md reference: AGENTS.md:L80-L80

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants