Files
ollama_ollama/llama/compat/llama-ollama-compat.cpp
T
jmorganca fd98ffa1e6 llama/compat: add gemma3n + glm4moelite handlers
gemma3n (text):
  Ollama publishes gemma-3n with the multimodal vocab (262400 tokens) for
  the main token_embd and tokenizer arrays, but the per-layer token embed
  only has the text vocab (262144 tokens). Upstream's loader reads n_vocab
  from the tokenizer.tokens array length and applies that to BOTH embedding
  tensors — so per_layer fails the dim check:

    tensor 'per_layer_token_embd.weight' has wrong shape;
      expected 8960, 262400, got 8960, 262144

  Fix (mirroring handle_gemma3): truncate the tokenizer arrays AND the
  token_embd tensor's vocab dim down to the per_layer count. The dropped
  256 entries are multimodal special tokens (image/audio markers); upstream
  gemma3n is text-only, so they're unused. No load_op needed — the loader
  reads ggml_nbytes from the (newly-shrunk) tensor shape, so it just reads
  fewer rows from the same file offset.

glm4moelite (text):
  Ollama publishes GLM-4.7-Flash (and similar) with arch=glm4moelite using
  DeepSeek-V2 style MLA attention but with the older convention of writing
  PER-HEAD key/value dims. Upstream collapsed all of these into the
  deepseek2 arch with the MLA-absorbed convention (head_count_kv=1,
  key/value dims = the kv_lora_rank-relative absorbed sizes).

  Tensor structure is identical (844 tensors, exact name match including
  attn_kv_a_mqa, attn_k_b, attn_v_b, attn_q_a/b) — only KV semantics differ.

  Translation:
    * arch_name: glm4moelite → deepseek2
    * KV prefix: glm4moelite.* → deepseek2.*
    * head_count_kv: original num_kv_heads (e.g. 20) → 1 (MLA absorbed)
    * key_length:   head_dim → kv_lora_rank + rope.dimension_count (576)
    * value_length: head_dim → kv_lora_rank                        (512)
    * key_length_mla / value_length_mla: were the absorbed dims (576/512);
      upstream's _mla variants are per-head dims. Set to head_dim (256/256).
    * expert_group_count / expert_group_used_count: required by the
      deepseek2 loader. GLM-4-MoE-Lite doesn't use group routing — set to 1.

Verified via `ollama run`:
  * gemma3n: text inference works ("4" for 2+2).
  * glm-4.7-flash: chat inference works ("Hi! How can I help you today?")
    via /api/chat with thinking disabled. Thinking-enabled mode also works
    correctly — output goes to the reasoning_content field as expected
    (Ollama's GLM47Parser starts in CollectingThinking state).

Both confirmed not to regress earlier coverage; patch unchanged.
2026-04-20 09:30:27 -07:00

1999 lines
99 KiB
C++
Vendored

#include "llama-ollama-compat.h"
#include "llama-ollama-compat-util.h"
#include "llama-impl.h"
#include <cstdio>
#include <cstring>
#include <mutex>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
namespace llama_ollama_compat {
using namespace llama_ollama_compat::detail; // pull detail:: helpers into scope
namespace {
// Per-loader file path registry — set by translate_metadata, read by
// maybe_load_text_tensor so it can pass the path to load ops without a
// separate patch insertion in the model loader's load_all_data path.
std::mutex g_loader_path_mutex;
std::unordered_map<const llama_model_loader *, std::string> g_loader_paths;
// =========================================================================
// gemma3 (text side)
// =========================================================================
// An Ollama-format gemma3 file declares arch="gemma3" AND exhibits at
// least one converter quirk. Different converter versions produced
// different quirks (4B/12B/27B have embedded vision + mm KVs; 1B uses
// non-standard rope key names; all of them omit layer_norm_rms_epsilon).
bool detect_ollama_gemma3(const gguf_context * meta, const ggml_context * ctx) {
const int64_t arch_kid = gguf_find_key(meta, "general.architecture");
if (arch_kid < 0) return false;
if (std::strcmp(gguf_get_val_str(meta, arch_kid), "gemma3") != 0) return false;
return has_key(meta, "gemma3.mm.tokens_per_image")
|| any_tensor_with_prefix(ctx, "v.")
|| any_tensor_with_prefix(ctx, "mm.")
|| has_key(meta, "gemma3.rope.global.freq_base")
|| has_key(meta, "gemma3.rope.local.freq_base")
|| has_key(meta, "tokenizer.ggml.add_padding_token")
|| has_key(meta, "tokenizer.ggml.add_unknown_token")
|| !has_key(meta, "gemma3.attention.layer_norm_rms_epsilon");
}
void handle_gemma3(const llama_model_loader * ml, gguf_context * meta, ggml_context * ctx) {
if (!detect_ollama_gemma3(meta, ctx)) return;
LLAMA_LOG_INFO("%s: detected Ollama-format gemma3 GGUF; applying compatibility fixes\n", __func__);
// Old Ollama converters sometimes used nested rope key names. Copy
// them to the flat names upstream expects BEFORE injecting defaults.
copy_f32_kv(meta, "gemma3.rope.global.freq_base", "gemma3.rope.freq_base");
copy_f32_kv(meta, "gemma3.rope.local.freq_base", "gemma3.rope.freq_base_swa");
// Inject required KVs with their standard gemma3 defaults.
inject_f32_if_missing(meta, "gemma3.attention.layer_norm_rms_epsilon", 1e-6f);
inject_f32_if_missing(meta, "gemma3.rope.freq_base", 1000000.0f);
inject_f32_if_missing(meta, "gemma3.rope.freq_base_swa", 10000.0f);
// Gemma3 4B/12B/27B ship with {type: "linear", factor: 8.0} rope scaling
// in their HF config to extend the 16k trained context to 131072. Ollama's
// old converter didn't write these. The 1B has no scaling — detect by
// context length.
const int64_t ctx_key = gguf_find_key(meta, "gemma3.context_length");
if (ctx_key >= 0 && gguf_get_val_u32(meta, ctx_key) >= 131072) {
inject_str_if_missing(meta, "gemma3.rope.scaling.type", "linear");
inject_f32_if_missing(meta, "gemma3.rope.scaling.factor", 8.0f);
}
// Tokenizer vocab size vs embedding rows mismatch: Ollama leaves extra
// multimodal tokens (e.g. <image_soft_token>) in the tokenizer arrays.
// Truncate to match token_embd rows so llama.cpp's dim check passes.
for (ggml_tensor * t = ggml_get_first_tensor(ctx); t; t = ggml_get_next_tensor(ctx, t)) {
if (std::strcmp(ggml_get_name(t), "token_embd.weight") == 0) {
const size_t rows = t->ne[1]; // shape is [n_embd, n_vocab]
truncate_str_arr (meta, "tokenizer.ggml.tokens", rows);
truncate_data_arr(meta, "tokenizer.ggml.scores", GGUF_TYPE_FLOAT32, sizeof(float), rows);
truncate_data_arr(meta, "tokenizer.ggml.token_type", GGUF_TYPE_INT32, sizeof(int32_t), rows);
break;
}
}
// Hide embedded vision tensors from the text loader. Ollama's Go side
// re-passes the same blob as --mmproj so the clip loader picks them up.
add_skip_prefix(ml, "v.");
add_skip_prefix(ml, "mm.");
// Note: no RMSNorm weight shift needed. Ollama's published gemma3 blobs
// already have the +1 shift baked in, same as upstream's convert_hf.
}
// =========================================================================
// gemma3n (text side — vocab mismatch between token_embd and per_layer_token_embd)
// =========================================================================
//
// Ollama publishes gemma-3n with the multimodal vocab (262400 tokens) for
// the main token_embd and tokenizer arrays, but the per-layer token embed
// only has the text vocab (262144 tokens). Upstream's loader expects both
// embedding tensors to have the same n_vocab, and reads n_vocab from the
// tokenizer.tokens array length — so the larger value wins and per_layer
// fails the dim check:
//
// tensor 'per_layer_token_embd.weight' has wrong shape;
// expected 8960, 262400, got 8960, 262144
//
// Fix (mirroring handle_gemma3): truncate the tokenizer arrays AND the
// token_embd tensor's vocab dim down to the per_layer count. The dropped
// 256 entries are multimodal special tokens (image/audio markers); upstream
// gemma3n is text-only, so they're unused anyway.
//
// Note: tensor data isn't read at this point — the loader reads ggml_nbytes
// from the (newly-shrunk) tensor shape, so it just reads fewer rows from
// the same file offset. No load_op needed.
bool detect_ollama_gemma3n(const gguf_context * meta, const ggml_context * ctx) {
const int64_t arch_kid = gguf_find_key(meta, "general.architecture");
if (arch_kid < 0) return false;
if (std::strcmp(gguf_get_val_str(meta, arch_kid), "gemma3n") != 0) return false;
ggml_tensor * te = ggml_get_tensor(const_cast<ggml_context *>(ctx), "token_embd.weight");
ggml_tensor * pe = ggml_get_tensor(const_cast<ggml_context *>(ctx), "per_layer_token_embd.weight");
return te && pe && te->ne[1] != pe->ne[1];
}
void handle_gemma3n(const llama_model_loader * ml, gguf_context * meta, ggml_context * ctx) {
(void) ml;
if (!detect_ollama_gemma3n(meta, ctx)) return;
LLAMA_LOG_INFO("%s: detected Ollama-format gemma3n GGUF; truncating vocab to per_layer_token_embd size\n", __func__);
ggml_tensor * pe = ggml_get_tensor(ctx, "per_layer_token_embd.weight");
if (!pe) return;
const uint32_t target_vocab = (uint32_t) pe->ne[1];
if (ggml_tensor * t = ggml_get_tensor(ctx, "token_embd.weight")) {
set_tensor_shape(t, {t->ne[0], target_vocab});
}
truncate_str_arr (meta, "tokenizer.ggml.tokens", target_vocab);
truncate_data_arr(meta, "tokenizer.ggml.scores", GGUF_TYPE_FLOAT32, sizeof(float), target_vocab);
truncate_data_arr(meta, "tokenizer.ggml.token_type", GGUF_TYPE_INT32, sizeof(int32_t), target_vocab);
}
// =========================================================================
// embeddinggemma (text side — sentence-transformer dense projection)
// =========================================================================
//
// Ollama publishes embeddinggemma:300m with general.architecture=gemma3 and
// two extra dense layers stored as `dense.0.weight` / `dense.1.weight`
// (the sentence-transformers post-pooling projection that maps the 768-dim
// pooled embedding through 768→3072→768 for the matryoshka head).
//
// Upstream loads this model under arch=gemma-embedding, which:
// * disables causal attention (embeddings are bidirectional)
// * loads `dense_2.weight` and `dense_3.weight` by name (with shapes
// derived from gemma-embedding.dense_2_feat_in/out etc.)
//
// Without that arch, the gemma3 loader leaves dense.0/dense.1 unrequested
// and `done_getting_tensors` raises "wrong number of tensors" (2 unused).
//
// Detection: arch=gemma3 AND has dense.0.weight tensor (only embeddinggemma
// ships these — regular gemma3 chat models do not).
// Translation: switch arch_name to gemma-embedding, copy the gemma3.* KV
// prefix to gemma-embedding.*, derive dense_*_feat_* from the actual tensor
// shapes, and rename dense.0/dense.1 → dense_2/dense_3.
bool detect_ollama_embeddinggemma(const gguf_context * meta, const ggml_context * ctx) {
const int64_t arch_kid = gguf_find_key(meta, "general.architecture");
if (arch_kid < 0) return false;
if (std::strcmp(gguf_get_val_str(meta, arch_kid), "gemma3") != 0) return false;
return ggml_get_tensor(const_cast<ggml_context *>(ctx), "dense.0.weight") != nullptr;
}
void handle_embeddinggemma(const llama_model_loader * ml, gguf_context * meta,
ggml_context * ctx, std::string & arch_name) {
(void) ml;
if (!detect_ollama_embeddinggemma(meta, ctx)) return;
LLAMA_LOG_INFO("%s: detected Ollama-format embeddinggemma; translating to gemma-embedding\n", __func__);
// Switch architecture so upstream loads the embedding-specific code path
// (no causal attention, dense_2/dense_3 loaded by name).
arch_name = "gemma-embedding";
gguf_set_val_str(meta, "general.architecture", "gemma-embedding");
// Mirror gemma3.* hparams under the new arch prefix. rename_kv_prefix
// copies (does not remove); the leftover gemma3.* keys are unused.
rename_kv_prefix(meta, "gemma3.", "gemma-embedding.");
// Derive dense feat dims from the actual tensor shapes.
// dense.0.weight: [n_embd, dense_2_feat_out]
// dense.1.weight: [dense_3_feat_in, n_embd]
ggml_tensor * d0 = ggml_get_tensor(ctx, "dense.0.weight");
ggml_tensor * d1 = ggml_get_tensor(ctx, "dense.1.weight");
if (d0 && d1) {
gguf_set_val_u32(meta, "gemma-embedding.dense_2_feat_in", (uint32_t) d0->ne[0]);
gguf_set_val_u32(meta, "gemma-embedding.dense_2_feat_out", (uint32_t) d0->ne[1]);
gguf_set_val_u32(meta, "gemma-embedding.dense_3_feat_in", (uint32_t) d1->ne[0]);
gguf_set_val_u32(meta, "gemma-embedding.dense_3_feat_out", (uint32_t) d1->ne[1]);
}
rename_tensor(meta, ctx, "dense.0.weight", "dense_2.weight");
rename_tensor(meta, ctx, "dense.1.weight", "dense_3.weight");
}
// =========================================================================
// qwen35moe (text side)
// =========================================================================
// Shared text-side fixes for Ollama-format qwen35 / qwen35moe GGUFs.
// Both arches use the same SSM-hybrid + M-RoPE + MTP+vision-monolithic
// converter quirks; only the arch name (and KV prefix) differs.
void apply_qwen35_text_fixes(const llama_model_loader * ml, gguf_context * meta,
ggml_context * ctx, const char * arch_prefix) {
auto kv = [arch_prefix](const char * suffix) {
return std::string(arch_prefix) + suffix;
};
// 1. attention.head_count_kv — upstream expects UINT32; Ollama wrote
// an array (one entry per layer, 0 for SSM layers, 2/4 for attention).
// Collapse to the max non-zero value.
{
const std::string key = kv(".attention.head_count_kv");
const int64_t kid = gguf_find_key(meta, key.c_str());
if (kid >= 0 && gguf_get_kv_type(meta, kid) == GGUF_TYPE_ARRAY) {
const size_t n = gguf_get_arr_n(meta, kid);
const auto * arr = static_cast<const uint32_t *>(gguf_get_arr_data(meta, kid));
uint32_t max_kv = 0;
for (size_t i = 0; i < n; ++i) if (arr[i] > max_kv) max_kv = arr[i];
if (max_kv == 0) max_kv = 2; // safety fallback
gguf_remove_key (meta, key.c_str());
gguf_set_val_u32 (meta, key.c_str(), max_kv);
}
}
// 2. rope.dimension_sections — upstream expects a 4-element array
// (M-RoPE convention); Ollama wrote 3 elements. Pad with a trailing 0.
{
const std::string key = kv(".rope.dimension_sections");
const int64_t kid = gguf_find_key(meta, key.c_str());
if (kid >= 0 && gguf_get_arr_n(meta, kid) == 3) {
const auto * src = static_cast<const int32_t *>(gguf_get_arr_data(meta, kid));
const int32_t padded[4] = { src[0], src[1], src[2], 0 };
gguf_set_arr_data(meta, key.c_str(), GGUF_TYPE_INT32, padded, 4);
}
}
// 3. Tensor rename: Ollama's `blk.N.ssm_dt` is upstream's
// `blk.N.ssm_dt.bias` (same shape).
{
std::vector<std::string> targets;
const int64_t n = gguf_get_n_tensors(meta);
static const char suffix[] = ".ssm_dt";
const size_t slen = sizeof(suffix) - 1;
for (int64_t i = 0; i < n; ++i) {
std::string name(gguf_get_tensor_name(meta, i));
if (name.size() >= slen
&& name.compare(name.size() - slen, slen, suffix) == 0) {
targets.push_back(std::move(name));
}
}
for (const auto & from : targets) {
rename_tensor(meta, ctx, from.c_str(), (from + ".bias").c_str());
}
}
// 4. Drop embedded vision + MTP + projector tensors from the text loader.
add_skip_prefix(ml, "v.");
add_skip_prefix(ml, "mm.");
add_skip_prefix(ml, "mtp.");
}
bool detect_ollama_qwen35moe(const gguf_context * meta, const ggml_context * ctx) {
const int64_t arch_kid = gguf_find_key(meta, "general.architecture");
if (arch_kid < 0) return false;
if (std::strcmp(gguf_get_val_str(meta, arch_kid), "qwen35moe") != 0) return false;
// Any Ollama-ism. Upstream qwen35moe files have none of these — the
// vision KVs live in a separate mmproj, MTP tensors are dropped,
// head_count_kv is a scalar, and the extra rope / ssm / feed_forward
// KVs are either absent or stored differently.
return has_key(meta, "qwen35moe.vision.block_count")
|| has_key(meta, "qwen35moe.image_token_id")
|| has_key(meta, "qwen35moe.ssm.v_head_reordered")
|| has_key(meta, "qwen35moe.feed_forward_length")
|| has_key(meta, "qwen35moe.rope.mrope_interleaved")
|| any_tensor_with_prefix(ctx, "mtp.")
|| any_tensor_with_prefix(ctx, "v.");
}
void handle_qwen35moe(const llama_model_loader * ml, gguf_context * meta, ggml_context * ctx) {
if (!detect_ollama_qwen35moe(meta, ctx)) return;
LLAMA_LOG_INFO("%s: detected Ollama-format qwen35moe GGUF; applying compatibility fixes\n", __func__);
apply_qwen35_text_fixes(ml, meta, ctx, "qwen35moe");
}
// =========================================================================
// qwen35 (text side — non-MoE, e.g. qwen3.5:9b)
// =========================================================================
//
// Same converter quirks as qwen35moe but the arch name has no "moe" suffix.
// All the SSM-hybrid / M-RoPE / MTP / monolithic-vision fix-ups apply.
bool detect_ollama_qwen35(const gguf_context * meta, const ggml_context * ctx) {
const int64_t arch_kid = gguf_find_key(meta, "general.architecture");
if (arch_kid < 0) return false;
if (std::strcmp(gguf_get_val_str(meta, arch_kid), "qwen35") != 0) return false;
return has_key(meta, "qwen35.vision.block_count")
|| has_key(meta, "qwen35.image_token_id")
|| has_key(meta, "qwen35.ssm.v_head_reordered")
|| has_key(meta, "qwen35.rope.mrope_interleaved")
|| any_tensor_with_prefix(ctx, "mtp.")
|| any_tensor_with_prefix(ctx, "v.");
}
void handle_qwen35(const llama_model_loader * ml, gguf_context * meta, ggml_context * ctx) {
if (!detect_ollama_qwen35(meta, ctx)) return;
LLAMA_LOG_INFO("%s: detected Ollama-format qwen35 GGUF; applying compatibility fixes\n", __func__);
apply_qwen35_text_fixes(ml, meta, ctx, "qwen35");
}
// =========================================================================
// gemma4 (text side)
// =========================================================================
//
// Same arch name on both sides. Ollama publishes a monolithic GGUF that
// embeds the vision encoder + audio encoder + projector inline. Text-side
// KVs/tensor names match upstream verbatim — only fix is to hide the
// `a.*` / `v.*` / `mm.*` tensors from the text loader so n_tensors lines up.
bool detect_ollama_gemma4(const gguf_context * meta, const ggml_context * ctx) {
const int64_t arch_kid = gguf_find_key(meta, "general.architecture");
if (arch_kid < 0) return false;
if (std::strcmp(gguf_get_val_str(meta, arch_kid), "gemma4") != 0) return false;
return any_tensor_with_prefix(ctx, "a.")
|| any_tensor_with_prefix(ctx, "v.")
|| any_tensor_with_prefix(ctx, "mm.");
}
void handle_gemma4(const llama_model_loader * ml, gguf_context * meta, ggml_context * ctx) {
if (!detect_ollama_gemma4(meta, ctx)) return;
(void) ctx;
LLAMA_LOG_INFO("%s: detected Ollama-format gemma4 GGUF; applying compatibility fixes\n", __func__);
// Tokenizer fix: Ollama writes `tokenizer.ggml.model = 'llama'` (SPM) on
// gemma4 GGUFs, but gemma4 actually uses BPE — upstream-converted GGUFs
// use `'gemma4'` which selects LLAMA_VOCAB_TYPE_BPE in src/llama-vocab.cpp.
// With the wrong tokenizer type, gemma4's special tokens (e.g.
// `<|thought|>`, `<|turn>`, `<|channel>`) get split into multiple SPM
// subword pieces, so when the model emits them they come out as raw
// text instead of being recognized as control tokens.
//
// Ollama already supplies `tokenizer.ggml.merges` (needed for BPE) and
// `tokenizer.ggml.pre = 'gemma4'`, so flipping the model name is enough.
{
const int64_t kid = gguf_find_key(meta, "tokenizer.ggml.model");
if (kid >= 0) {
const char * cur = gguf_get_val_str(meta, kid);
if (cur && std::strcmp(cur, "llama") == 0) {
gguf_set_val_str(meta, "tokenizer.ggml.model", "gemma4");
}
}
}
// Hide embedded audio + vision + projector tensors from the text loader.
add_skip_prefix(ml, "a.");
add_skip_prefix(ml, "v.");
add_skip_prefix(ml, "mm.");
}
// =========================================================================
// deepseek-ocr (text side)
// =========================================================================
//
// Ollama uses arch name "deepseekocr" / KV prefix "deepseekocr.*".
// Upstream uses "deepseek2-ocr" (with hyphen) / "deepseek2-ocr.*".
//
// Aside from the prefix rename:
// * Inject `expert_feed_forward_length` from the per-expert ffn_down_exps
// shape (Ollama omits it; the value is the inner FFN dim of one expert,
// 896 for the 3B model).
// * Inject `expert_shared_count` from the ffn_down_shexp shape (Ollama
// omits it; the shared experts share their FFN dim with regular experts,
// so count = shexp_dim / expert_feed_forward_length).
// * Skip embedded vision (`v.*`), projector (`mm.*`), and the SAM encoder
// (`s.*`) tensors from the text loader.
bool detect_ollama_deepseekocr(const gguf_context * meta) {
const int64_t arch_kid = gguf_find_key(meta, "general.architecture");
if (arch_kid < 0) return false;
return std::strcmp(gguf_get_val_str(meta, arch_kid), "deepseekocr") == 0;
}
void handle_deepseekocr(const llama_model_loader * ml, gguf_context * meta,
ggml_context * ctx, std::string & arch_name) {
if (!detect_ollama_deepseekocr(meta)) return;
LLAMA_LOG_INFO("%s: detected Ollama-format deepseekocr GGUF; applying compatibility fixes\n", __func__);
gguf_set_val_str(meta, "general.architecture", "deepseek2-ocr");
rename_kv_prefix(meta, "deepseekocr.", "deepseek2-ocr.");
arch_name = "deepseek2-ocr";
// Inject defaults Ollama omitted entirely.
inject_f32_if_missing(meta, "deepseek2-ocr.attention.layer_norm_rms_epsilon",
1e-6f);
// Recover expert_feed_forward_length from blk.1 (first MoE block; blk.0
// is dense). ne[0] of ffn_down_exps is the per-expert inner dim.
if (!has_key(meta, "deepseek2-ocr.expert_feed_forward_length")) {
if (ggml_tensor * t = ggml_get_tensor(ctx, "blk.1.ffn_down_exps.weight")) {
gguf_set_val_u32(meta, "deepseek2-ocr.expert_feed_forward_length",
(uint32_t) t->ne[0]);
}
}
// Recover expert_shared_count from blk.1.ffn_down_shexp shape.
// shape ne[0] = expert_shared_count * expert_feed_forward_length
if (!has_key(meta, "deepseek2-ocr.expert_shared_count")) {
ggml_tensor * shexp = ggml_get_tensor(ctx, "blk.1.ffn_down_shexp.weight");
const int64_t fflen_kid = gguf_find_key(meta, "deepseek2-ocr.expert_feed_forward_length");
if (shexp && fflen_kid >= 0) {
const uint32_t fflen = gguf_get_val_u32(meta, fflen_kid);
if (fflen > 0) {
gguf_set_val_u32(meta, "deepseek2-ocr.expert_shared_count",
(uint32_t)(shexp->ne[0] / fflen));
}
}
}
// Hide embedded SAM (`s.*`), vision (`v.*`), and projector (`mm.*`)
// tensors from the text loader.
add_skip_prefix(ml, "s.");
add_skip_prefix(ml, "v.");
add_skip_prefix(ml, "mm.");
}
// =========================================================================
// nemotron_h_moe (text only)
// =========================================================================
//
// Same arch name on both sides. Most variants (e.g. nemotron-cascade-2)
// load as-is. The latent-FFN variants (e.g. nemotron-3-super 120B-A12B)
// rename `ffn_latent_in` / `ffn_latent_out` to `ffn_latent_down` /
// `ffn_latent_up`, and need `moe_latent_size` injected (derived from
// the latent tensor shape).
bool detect_ollama_nemotron_h_moe(const gguf_context * meta, const ggml_context * ctx) {
const int64_t arch_kid = gguf_find_key(meta, "general.architecture");
if (arch_kid < 0) return false;
if (std::strcmp(gguf_get_val_str(meta, arch_kid), "nemotron_h_moe") != 0) return false;
return any_tensor_with_prefix(ctx, "blk.1.ffn_latent_in")
|| any_tensor_with_prefix(ctx, "blk.0.ffn_latent_in")
|| any_tensor_with_prefix(ctx, "mtp.");
}
void handle_nemotron_h_moe(const llama_model_loader * ml, gguf_context * meta, ggml_context * ctx) {
if (!detect_ollama_nemotron_h_moe(meta, ctx)) return;
LLAMA_LOG_INFO("%s: detected Ollama-format nemotron_h_moe GGUF; applying compatibility fixes\n", __func__);
// Inject moe_latent_size for latent-FFN variants (e.g. super 120B-A12B).
// Standard variants (e.g. cascade-2 30B-A3B) have no latent tensors and
// use n_embd as the MoE inner dim — leave the key absent.
if (!has_key(meta, "nemotron_h_moe.moe_latent_size")) {
for (uint32_t b = 0; b < 1024; ++b) {
char name[64];
std::snprintf(name, sizeof(name), "blk.%u.ffn_latent_in.weight", b);
if (ggml_tensor * t = ggml_get_tensor(ctx, name)) {
gguf_set_val_u32(meta, "nemotron_h_moe.moe_latent_size",
(uint32_t) t->ne[1]);
break;
}
}
}
// Rename the latent projection tensors to upstream's naming (no-op when
// the file has no latent tensors).
rename_tensors_containing(meta, ctx, ".ffn_latent_in", ".ffn_latent_down");
rename_tensors_containing(meta, ctx, ".ffn_latent_out", ".ffn_latent_up");
// Drop MTP (Multi-Token Prediction) tensors — Ollama's converter emits
// them as one-tensor-per-expert (`mtp.layers.X.mixer.experts.Y.{up,down}_proj`)
// which upstream's nemotron_h_moe loader doesn't claim. Total: ~1040 extra
// tensors on super 120B.
add_skip_prefix(ml, "mtp.");
}
// =========================================================================
// llama4 (text side)
// =========================================================================
//
// Same arch name on both sides. Ollama publishes a monolithic GGUF that
// embeds the vision encoder + projector inline. Text-side KVs/tensor
// names match upstream verbatim — only fix is to hide `v.*`/`mm.*` from
// the text loader so n_tensors lines up.
bool detect_ollama_llama4(const gguf_context * meta, const ggml_context * ctx) {
const int64_t arch_kid = gguf_find_key(meta, "general.architecture");
if (arch_kid < 0) return false;
if (std::strcmp(gguf_get_val_str(meta, arch_kid), "llama4") != 0) return false;
return any_tensor_with_prefix(ctx, "v.")
|| any_tensor_with_prefix(ctx, "mm.");
}
void handle_llama4(const llama_model_loader * ml, gguf_context * meta, ggml_context * ctx) {
if (!detect_ollama_llama4(meta, ctx)) return;
(void) meta;
(void) ctx;
LLAMA_LOG_INFO("%s: detected Ollama-format llama4 GGUF; applying compatibility fixes\n", __func__);
add_skip_prefix(ml, "v.");
add_skip_prefix(ml, "mm.");
}
// =========================================================================
// glm-ocr (text side)
// =========================================================================
//
// Ollama uses arch name "glmocr" / KV prefix "glmocr.*" with 16 blocks.
// Upstream uses "glm4" / "glm4.*" — the GLM-OCR variant of LLM_ARCH_GLM4
// is identified by `n_layer = 17` (16 main + 1 nextn predict layer).
// Ollama drops the nextn layer entirely, so we report n_layer = 16 and
// leave `nextn_predict_layers` absent (defaults to 0 = no nextn path).
//
// Bigger surgery: GLM4 expects fused gate+up MLP weights stored at
// `blk.X.ffn_up.weight` with shape `[n_embd, n_ff*2]`. Ollama writes
// the gate and up halves as separate `ffn_gate.weight` / `ffn_up.weight`
// tensors (each `[n_embd, n_ff]`). We register a concat load op that
// reads gate+up bytes and stitches them into the fused upstream slot.
// Per-block: register a concat load that fuses Ollama's separate
// ffn_gate + ffn_up into upstream's single `blk.X.ffn_up.weight`
// tensor with doubled out dim. Capture source file offsets BEFORE any
// renames invalidate them (same pattern as qwen35moe QKV merge).
void register_glm4_ffn_concat(gguf_context * meta, ggml_context * ctx, int block_idx) {
char gate_n[64], up_n[64];
std::snprintf(gate_n, sizeof(gate_n), "blk.%d.ffn_gate.weight", block_idx);
std::snprintf(up_n, sizeof(up_n), "blk.%d.ffn_up.weight", block_idx);
if (!ggml_get_tensor(ctx, gate_n) || !ggml_get_tensor(ctx, up_n)) return;
// GLM4's fused ffn_up has gate as first half, up as second half
// (so ggml_swiglu's silu(first_half) * second_half gives silu(gate) * up).
register_concat_load(meta, up_n, {gate_n, up_n});
if (ggml_tensor * t = ggml_get_tensor(ctx, up_n)) {
set_tensor_shape(t, {t->ne[0], t->ne[1] * 2});
}
}
bool detect_ollama_glmocr(const gguf_context * meta) {
const int64_t arch_kid = gguf_find_key(meta, "general.architecture");
if (arch_kid < 0) return false;
return std::strcmp(gguf_get_val_str(meta, arch_kid), "glmocr") == 0;
}
void handle_glmocr(const llama_model_loader * ml, gguf_context * meta,
ggml_context * ctx, std::string & arch_name) {
if (!detect_ollama_glmocr(meta)) return;
LLAMA_LOG_INFO("%s: detected Ollama-format glmocr GGUF; applying compatibility fixes\n", __func__);
gguf_set_val_str(meta, "general.architecture", "glm4");
rename_kv_prefix(meta, "glmocr.", "glm4.");
arch_name = "glm4";
// M-RoPE: Ollama writes a 3-element `rope.mrope_section`, upstream expects
// a 4-element `rope.dimension_sections` (pad trailing 0).
{
const int64_t kid = gguf_find_key(meta, "glm4.rope.mrope_section");
if (kid >= 0 && gguf_get_arr_n(meta, kid) == 3) {
const auto * src = static_cast<const int32_t *>(gguf_get_arr_data(meta, kid));
const int32_t padded[4] = { src[0], src[1], src[2], 0 };
gguf_set_arr_data(meta, "glm4.rope.dimension_sections",
GGUF_TYPE_INT32, padded, 4);
}
}
// Inject `rope.dimension_count` from key_length (used as the rope dim).
if (!has_key(meta, "glm4.rope.dimension_count")) {
const int64_t kid = gguf_find_key(meta, "glm4.attention.key_length");
if (kid >= 0) {
gguf_set_val_u32(meta, "glm4.rope.dimension_count",
gguf_get_val_u32(meta, kid));
}
}
// Tokenizer pre-tokenizer: Ollama wrote `llama-bpe`, but glm-ocr uses
// `chatglm-bpe` (different regex split — wrong pre-tokenization can
// fragment GLM's special tokens).
{
const int64_t kid = gguf_find_key(meta, "tokenizer.ggml.pre");
if (kid >= 0) {
const char * cur = gguf_get_val_str(meta, kid);
if (cur && std::strcmp(cur, "chatglm-bpe") != 0) {
gguf_set_val_str(meta, "tokenizer.ggml.pre", "chatglm-bpe");
}
}
}
// Tensor renames (substring): each leaf appears once per block and
// doesn't overlap the others.
rename_tensors_containing(meta, ctx, ".attn_out", ".attn_output");
rename_tensors_containing(meta, ctx, ".post_attn_norm", ".post_attention_norm");
rename_tensors_containing(meta, ctx, ".post_ffn_norm", ".post_ffw_norm");
// Fuse ffn_gate + ffn_up → ffn_up[:, 2*n_ff] for every block, then mark
// the orphan ffn_gate tensors as skip so n_tensors lines up.
{
const int64_t n_blk_kid = gguf_find_key(meta, "glm4.block_count");
const uint32_t n_blocks = n_blk_kid >= 0 ? gguf_get_val_u32(meta, n_blk_kid) : 16;
for (uint32_t b = 0; b < n_blocks; ++b) {
register_glm4_ffn_concat(meta, ctx, (int) b);
char skip_pref[64];
std::snprintf(skip_pref, sizeof(skip_pref), "blk.%u.ffn_gate.", b);
add_skip_prefix(ml, skip_pref);
}
}
// Hide embedded vision + projector tensors from the text loader.
add_skip_prefix(ml, "v.");
add_skip_prefix(ml, "mm.");
}
// =========================================================================
// gpt-oss (text only)
// =========================================================================
//
// Ollama uses arch name "gptoss" (no hyphen) and KV prefix "gptoss.*".
// Upstream uses "gpt-oss" / "gpt-oss.*". Same tensor layout otherwise,
// except:
// * `blk.X.attn_sinks` -> `blk.X.attn_sinks.weight` (missing suffix)
// * `blk.X.ffn_norm.weight` -> `blk.X.post_attention_norm.weight`
// (the second-norm-per-block names differ between converters)
bool detect_ollama_gptoss(const gguf_context * meta) {
const int64_t arch_kid = gguf_find_key(meta, "general.architecture");
if (arch_kid < 0) return false;
return std::strcmp(gguf_get_val_str(meta, arch_kid), "gptoss") == 0;
}
// `arch_name` is mutated to "gpt-oss" so the caller's subsequent
// LLM_KV lookups query the renamed prefix.
void handle_gptoss(const llama_model_loader * ml, gguf_context * meta,
ggml_context * ctx, std::string & arch_name) {
if (!detect_ollama_gptoss(meta)) return;
(void) ml;
LLAMA_LOG_INFO("%s: detected Ollama-format gpt-oss GGUF; applying compatibility fixes\n", __func__);
gguf_set_val_str(meta, "general.architecture", "gpt-oss");
rename_kv_prefix(meta, "gptoss.", "gpt-oss.");
arch_name = "gpt-oss";
// Upstream's gpt-oss loader requires `gpt-oss.expert_feed_forward_length`
// (n_ff_exp). Ollama omitted it; recover from the ffn_gate_exps tensor
// shape — for gpt-oss the tensor is created as {n_embd, n_ff_exp, n_expert}
// so ne[1] is the per-expert FFN dim.
if (!has_key(meta, "gpt-oss.expert_feed_forward_length")) {
if (ggml_tensor * t = ggml_get_tensor(ctx, "blk.0.ffn_gate_exps.weight")) {
gguf_set_val_u32(meta, "gpt-oss.expert_feed_forward_length", (uint32_t) t->ne[1]);
}
}
// Tensor renames. `rename_tensors_containing` does a substring replace
// on first occurrence — each needle below appears exactly once per
// tensor name and the needles don't overlap each other.
rename_tensors_containing(meta, ctx, ".attn_out",
".attn_output"); // wo: out -> output
rename_tensors_containing(meta, ctx, ".attn_sinks",
".attn_sinks.weight"); // add missing suffix
rename_tensors_containing(meta, ctx, ".ffn_norm",
".post_attention_norm");
}
// =========================================================================
// lfm2 (text only)
// =========================================================================
//
// Same arch name ("lfm2") on both sides. Only difference is the
// pre-output-projection norm: Ollama writes `output_norm.weight`,
// upstream writes `token_embd_norm.weight` (with the LFM2-specific
// LLM_TENSOR_OUTPUT_NORM_LFM2 mapping). One tensor rename.
bool detect_ollama_lfm2(const gguf_context * meta, const ggml_context * ctx) {
const int64_t arch_kid = gguf_find_key(meta, "general.architecture");
if (arch_kid < 0) return false;
if (std::strcmp(gguf_get_val_str(meta, arch_kid), "lfm2") != 0) return false;
// Marker: Ollama-converted lfm2 has output_norm.weight, upstream has
// token_embd_norm.weight instead.
return ggml_get_tensor(const_cast<ggml_context *>(ctx), "output_norm.weight") != nullptr
&& ggml_get_tensor(const_cast<ggml_context *>(ctx), "token_embd_norm.weight") == nullptr;
}
void handle_lfm2(const llama_model_loader * ml, gguf_context * meta, ggml_context * ctx) {
if (!detect_ollama_lfm2(meta, ctx)) return;
(void) ml;
LLAMA_LOG_INFO("%s: detected Ollama-format lfm2 GGUF; applying compatibility fixes\n", __func__);
rename_tensor(meta, ctx, "output_norm.weight", "token_embd_norm.weight");
// Older Ollama converters wrote a stale `lfm2.feed_forward_length` that
// didn't match the actual ffn_gate tensor shape (e.g. claimed 12288 on
// a model whose ffn_gate is [2048, 8192]). Fix from the tensor shape.
if (ggml_tensor * t = ggml_get_tensor(ctx, "blk.0.ffn_gate.weight")) {
const uint32_t real_n_ff = (uint32_t) t->ne[1];
const int64_t kid = gguf_find_key(meta, "lfm2.feed_forward_length");
if (kid < 0 || gguf_get_val_u32(meta, kid) != real_n_ff) {
gguf_set_val_u32(meta, "lfm2.feed_forward_length", real_n_ff);
}
}
}
// =========================================================================
// mistral3 (text only — for now)
// =========================================================================
//
// Same arch name on both sides. Ollama publishes a monolithic GGUF that
// embeds the vision encoder + projector inline, similar to gemma3 and
// qwen35moe. Differences this handler addresses:
//
// * Embedded `v.*` / `mm.*` tensors must be hidden from the text
// loader (otherwise n_tensors mismatch).
// * RoPE YaRN parameters use unprefixed names: Ollama writes
// `rope.scaling.beta_fast`/`beta_slow`, upstream wants
// `rope.scaling.yarn_beta_fast`/`yarn_beta_slow`.
// * Attention temperature scale: Ollama writes `rope.scaling_beta`,
// upstream reads `attention.temperature_scale`. Same numeric value.
//
// Vision/clip translation is not implemented yet — the user has to skip
// `--mmproj` until a clip handler lands.
bool detect_ollama_mistral3(const gguf_context * meta, const ggml_context * ctx) {
const int64_t arch_kid = gguf_find_key(meta, "general.architecture");
if (arch_kid < 0) return false;
if (std::strcmp(gguf_get_val_str(meta, arch_kid), "mistral3") != 0) return false;
// Marker: Ollama-style monolithic file embeds v.*/mm.* tensors;
// upstream HF mistral3 ships these in a separate mmproj.
return any_tensor_with_prefix(ctx, "v.")
|| any_tensor_with_prefix(ctx, "mm.")
|| has_key(meta, "mistral3.rope.scaling.beta_fast")
|| has_key(meta, "mistral3.rope.scaling_beta");
}
void handle_mistral3(const llama_model_loader * ml, gguf_context * meta, ggml_context * ctx) {
if (!detect_ollama_mistral3(meta, ctx)) return;
(void) ctx;
LLAMA_LOG_INFO("%s: detected Ollama-format mistral3 GGUF; applying compatibility fixes\n", __func__);
// RoPE YaRN parameter renames.
copy_kv(meta, "mistral3.rope.scaling.beta_fast",
"mistral3.rope.scaling.yarn_beta_fast");
copy_kv(meta, "mistral3.rope.scaling.beta_slow",
"mistral3.rope.scaling.yarn_beta_slow");
// Attention temperature scale: same value, different home.
copy_kv(meta, "mistral3.rope.scaling_beta",
"mistral3.attention.temperature_scale");
// Hide embedded vision + projector tensors from the text loader.
add_skip_prefix(ml, "v.");
add_skip_prefix(ml, "mm.");
}
// =========================================================================
// glm4moelite (text side — GLM-4.x-Flash, arch translation to deepseek2)
// =========================================================================
//
// Ollama publishes GLM-4.7-Flash (and similar Flash variants) with
// general.architecture=glm4moelite using DeepSeek-V2 style MLA attention,
// but with the older convention of writing PER-HEAD key/value dims.
// Upstream collapsed all of these into the deepseek2 arch with the
// MLA-absorbed convention (head_count_kv=1, key/value dims = the
// kv_lora_rank-relative absorbed sizes).
//
// Tensor structure is identical to deepseek2 (844 tensors, exact name
// match including attn_kv_a_mqa, attn_k_b, attn_v_b, attn_q_a/b, etc.) —
// only KV semantics differ.
//
// Translation:
// * arch_name: glm4moelite → deepseek2
// * KV prefix: glm4moelite.* → deepseek2.*
// * head_count_kv: original num_kv_heads (e.g. 20) → 1 (MLA absorbed)
// * key_length: head_dim → kv_lora_rank + rope.dimension_count
// (e.g. 256 → 512+64=576)
// * value_length: head_dim → kv_lora_rank
// (e.g. 256 → 512)
// * key_length_mla / value_length_mla: were the absorbed dims (576/512);
// upstream's _mla variants are per-head dims
// (256/256). Swap to head_dim.
// * expert_group_count / expert_group_used_count: required by deepseek2
// loader; default to 1 (no group routing).
bool detect_ollama_glm4moelite(const gguf_context * meta) {
const int64_t arch_kid = gguf_find_key(meta, "general.architecture");
if (arch_kid < 0) return false;
return std::strcmp(gguf_get_val_str(meta, arch_kid), "glm4moelite") == 0;
}
void handle_glm4moelite(const llama_model_loader * ml, gguf_context * meta,
ggml_context * ctx, std::string & arch_name) {
(void) ml;
(void) ctx;
if (!detect_ollama_glm4moelite(meta)) return;
LLAMA_LOG_INFO("%s: detected Ollama-format glm4moelite GGUF; translating to deepseek2 (MLA conventions)\n", __func__);
arch_name = "deepseek2";
gguf_set_val_str(meta, "general.architecture", "deepseek2");
// Mirror glm4moelite.* hparams under deepseek2.* (rename copies; the
// original glm4moelite.* keys remain but are unread — only used as the
// "original head_dim" source below).
rename_kv_prefix(meta, "glm4moelite.", "deepseek2.");
// MLA absorbs all KV heads — upstream uses 1.
gguf_set_val_u32(meta, "deepseek2.attention.head_count_kv", 1);
// key/value lengths to MLA-absorbed dims, derived from kv_lora_rank
// and rope.dimension_count (both already mirrored under deepseek2.*).
{
const int64_t kv_lora_kid = gguf_find_key(meta, "deepseek2.attention.kv_lora_rank");
const int64_t rope_kid = gguf_find_key(meta, "deepseek2.rope.dimension_count");
if (kv_lora_kid >= 0 && rope_kid >= 0) {
const uint32_t kv_lora = gguf_get_val_u32(meta, kv_lora_kid);
const uint32_t rope_d = gguf_get_val_u32(meta, rope_kid);
gguf_set_val_u32(meta, "deepseek2.attention.key_length", kv_lora + rope_d);
gguf_set_val_u32(meta, "deepseek2.attention.value_length", kv_lora);
}
}
// *_mla variants: upstream wants per-head dims (head_dim). Read original
// head_dim from the un-renamed glm4moelite.attention.key_length (which
// held the per-head dim in Ollama's convention).
{
const int64_t hd_kid = gguf_find_key(meta, "glm4moelite.attention.key_length");
if (hd_kid >= 0) {
const uint32_t head_dim = gguf_get_val_u32(meta, hd_kid);
gguf_set_val_u32(meta, "deepseek2.attention.key_length_mla", head_dim);
gguf_set_val_u32(meta, "deepseek2.attention.value_length_mla", head_dim);
}
}
// DeepSeek-V3 expert grouping; GLM-4-MoE-Lite doesn't use it but the
// loader expects the keys to be present. Default to 1.
inject_u32_if_missing(meta, "deepseek2.expert_group_count", 1);
inject_u32_if_missing(meta, "deepseek2.expert_group_used_count", 1);
}
// =========================================================================
// qwen25vl (text side — Qwen2.5-VL, arch translation to qwen2vl)
// =========================================================================
//
// Ollama publishes Qwen2.5-VL with general.architecture=qwen25vl, but
// upstream loads both Qwen2-VL and Qwen2.5-VL under arch=qwen2vl
// (which reads the rope.dimension_sections KV for M-RoPE).
//
// Translation:
// * arch_name: qwen25vl → qwen2vl (loader uses arch as KV prefix)
// * KV prefix: qwen25vl.* → qwen2vl.*
// * rope.mrope_section (3 elements) → rope.dimension_sections (4, padded with 0)
// * Hide vision+projector tensors from the text loader.
bool detect_ollama_qwen25vl(const gguf_context * meta) {
const int64_t arch_kid = gguf_find_key(meta, "general.architecture");
if (arch_kid < 0) return false;
return std::strcmp(gguf_get_val_str(meta, arch_kid), "qwen25vl") == 0;
}
void handle_qwen25vl(const llama_model_loader * ml, gguf_context * meta,
ggml_context * ctx, std::string & arch_name) {
(void) ctx;
if (!detect_ollama_qwen25vl(meta)) return;
LLAMA_LOG_INFO("%s: detected Ollama-format qwen25vl GGUF; translating to qwen2vl\n", __func__);
// Switch architecture so the loader reads qwen2vl.* keys (and uses the
// qwen2vl model build path, which handles M-RoPE).
arch_name = "qwen2vl";
gguf_set_val_str(meta, "general.architecture", "qwen2vl");
// Mirror the qwen25vl.* KVs under qwen2vl.* (rename_kv_prefix copies;
// the original qwen25vl.* keys remain but are unread).
rename_kv_prefix(meta, "qwen25vl.", "qwen2vl.");
// Translate mrope_section (3 elems) → dimension_sections (4 elems, padded).
const int64_t kid = gguf_find_key(meta, "qwen2vl.rope.mrope_section");
if (kid >= 0 && gguf_get_arr_n(meta, kid) >= 3) {
const auto * src = static_cast<const int32_t *>(gguf_get_arr_data(meta, kid));
const int32_t padded[4] = { src[0], src[1], src[2], 0 };
gguf_set_arr_data(meta, "qwen2vl.rope.dimension_sections",
GGUF_TYPE_INT32, padded, 4);
}
add_skip_prefix(ml, "v.");
add_skip_prefix(ml, "mm.");
}
// =========================================================================
// qwen3vl (text side — Qwen3-VL)
// =========================================================================
//
// Ollama publishes Qwen3-VL with general.architecture=qwen3vl (matches
// upstream). Two missing KVs that the upstream qwen3vl loader requires:
//
// * qwen3vl.rope.dimension_sections — M-RoPE section sizes. Derived from
// the HF config (rope_scaling.mrope_section). Hardcoded here as
// [24, 20, 20, 0] which matches Qwen3-VL-8B (head_dim=128, sum=64).
// If new Qwen3-VL variants ship with a different mrope, derive from
// the head_dim or read from a published KV.
//
// * qwen3vl.n_deepstack_layers — count of deepstack adapters. Length of
// qwen3vl.vision.deepstack_visual_indexes (3 for Qwen3-VL-8B).
bool detect_ollama_qwen3vl(const gguf_context * meta, const ggml_context * ctx) {
(void) ctx;
const int64_t arch_kid = gguf_find_key(meta, "general.architecture");
if (arch_kid < 0) return false;
if (std::strcmp(gguf_get_val_str(meta, arch_kid), "qwen3vl") != 0) return false;
// Marker: upstream-converted qwen3vl always has rope.dimension_sections;
// Ollama's blob doesn't.
return !has_key(meta, "qwen3vl.rope.dimension_sections");
}
void handle_qwen3vl(const llama_model_loader * ml, gguf_context * meta, ggml_context * ctx) {
(void) ctx;
if (!detect_ollama_qwen3vl(meta, ctx)) return;
LLAMA_LOG_INFO("%s: detected Ollama-format qwen3vl GGUF; applying compatibility fixes\n", __func__);
// Inject required M-RoPE sections (Qwen3-VL-8B default).
const int32_t mrope[4] = { 24, 20, 20, 0 };
gguf_set_arr_data(meta, "qwen3vl.rope.dimension_sections",
GGUF_TYPE_INT32, mrope, 4);
// Derive n_deepstack_layers from the deepstack indexes array length.
const int64_t ds_kid = gguf_find_key(meta, "qwen3vl.vision.deepstack_visual_indexes");
const uint32_t n_ds = (ds_kid >= 0) ? (uint32_t) gguf_get_arr_n(meta, ds_kid) : 0;
inject_u32_if_missing(meta, "qwen3vl.n_deepstack_layers", n_ds);
add_skip_prefix(ml, "v.");
add_skip_prefix(ml, "mm.");
}
// =========================================================================
// gemma3 (clip side)
// =========================================================================
constexpr std::pair<const char *, const char *> kGemma3ClipRenames[] = {
{"v.patch_embedding", "v.patch_embd"},
{"v.position_embedding", "v.position_embd"},
{"v.post_layernorm", "v.post_ln"},
{".layer_norm1", ".ln1"},
{".layer_norm2", ".ln2"},
{".attn_output", ".attn_out"},
{".mlp.fc1", ".ffn_down"},
{".mlp.fc2", ".ffn_up"},
{"mm.mm_input_projection", "mm.input_projection"},
{"mm.mm_soft_emb_norm", "mm.soft_emb_norm"},
};
void handle_gemma3_clip(gguf_context * meta, ggml_context * ctx) {
copy_u32_kv(meta, "gemma3.vision.block_count", "clip.vision.block_count");
copy_u32_kv(meta, "gemma3.vision.embedding_length", "clip.vision.embedding_length");
copy_u32_kv(meta, "gemma3.vision.feed_forward_length", "clip.vision.feed_forward_length");
copy_u32_kv(meta, "gemma3.vision.image_size", "clip.vision.image_size");
copy_u32_kv(meta, "gemma3.vision.patch_size", "clip.vision.patch_size");
copy_u32_kv(meta, "gemma3.vision.attention.head_count", "clip.vision.attention.head_count");
copy_f32_kv(meta, "gemma3.vision.attention.layer_norm_epsilon", "clip.vision.attention.layer_norm_epsilon");
// projection_dim = text model's embedding_length (mmproj out == LM in).
copy_u32_kv(meta, "gemma3.embedding_length", "clip.vision.projection_dim");
static const float kHalfHalfHalf[3] = {0.5f, 0.5f, 0.5f};
inject_f32_arr_if_missing(meta, "clip.vision.image_mean", kHalfHalfHalf, 3);
inject_f32_arr_if_missing(meta, "clip.vision.image_std", kHalfHalfHalf, 3);
inject_bool_if_missing(meta, "clip.has_vision_encoder", true);
inject_bool_if_missing(meta, "clip.use_gelu", true);
gguf_set_val_str(meta, "clip.projector_type", "gemma3");
gguf_set_val_str(meta, "general.architecture", "clip");
for (const auto & [from, to] : kGemma3ClipRenames) {
rename_tensors_containing(meta, ctx, from, to);
}
// Upstream stores patch_embd/position_embd as F32 (Gemma3VisionModel
// tensor_force_quant); Ollama stored F16. Metal's IM2COL convolution
// requires F32, so promote both at load time.
promote_tensor_to_f32(meta, ctx, "v.patch_embd.weight");
promote_tensor_to_f32(meta, ctx, "v.position_embd.weight");
}
// =========================================================================
// qwen35moe (clip side)
// =========================================================================
constexpr std::pair<const char *, const char *> kQwen35moeClipRenames[] = {
{"v.pos_embed", "v.position_embd"},
{"v.patch_embed", "v.patch_embd"},
{"v.merger.norm", "v.post_ln"},
{"v.merger.linear_fc1", "mm.0"},
{"v.merger.linear_fc2", "mm.2"},
{".mlp.linear_fc1", ".ffn_up"},
{".mlp.linear_fc2", ".ffn_down"},
{".norm1", ".ln1"},
{".norm2", ".ln2"},
};
// Register a QKV merge for a single vision block: Ollama has separate
// attn_q, attn_k, attn_v tensors; upstream wants them concatenated along
// their slow axis. Capture source file offsets BEFORE renaming attn_q.
void register_qwen35moe_qkv_merge(gguf_context * meta, ggml_context * ctx, int block_idx) {
char q[64], k[64], v[64], qbias[64], kbias[64], vbias[64], qkv_w[64], qkv_b[64];
std::snprintf(q, sizeof(q), "v.blk.%d.attn_q.weight", block_idx);
std::snprintf(k, sizeof(k), "v.blk.%d.attn_k.weight", block_idx);
std::snprintf(v, sizeof(v), "v.blk.%d.attn_v.weight", block_idx);
std::snprintf(qbias, sizeof(qbias), "v.blk.%d.attn_q.bias", block_idx);
std::snprintf(kbias, sizeof(kbias), "v.blk.%d.attn_k.bias", block_idx);
std::snprintf(vbias, sizeof(vbias), "v.blk.%d.attn_v.bias", block_idx);
std::snprintf(qkv_w, sizeof(qkv_w), "v.blk.%d.attn_qkv.weight", block_idx);
std::snprintf(qkv_b, sizeof(qkv_b), "v.blk.%d.attn_qkv.bias", block_idx);
if (!ggml_get_tensor(ctx, q)) return; // no vision block at this index
// Capture source offsets for the concat BEFORE renaming.
register_concat_load(meta, qkv_w, {q, k, v});
register_concat_load(meta, qkv_b, {qbias, kbias, vbias});
// Rename attn_q -> attn_qkv and widen from [hidden, hidden] to [hidden, 3*hidden].
rename_tensor(meta, ctx, q, qkv_w);
if (ggml_tensor * t = ggml_get_tensor(ctx, qkv_w)) set_tensor_shape(t, {t->ne[0], t->ne[1] * 3});
rename_tensor(meta, ctx, qbias, qkv_b);
if (ggml_tensor * t = ggml_get_tensor(ctx, qkv_b)) set_tensor_shape(t, {t->ne[0] * 3});
}
// Register the patch_embed reshape + split + F16->F32.
//
// Source: one Ollama tensor `v.patch_embed.weight`, ggml shape
// [h=16, w=16, t=2, packed=3456] F16
// where `packed` is the PyTorch row-major flattening of HF's
// [out_c=1152, in_c=3, ...] dim pair, so packed_c = c_out*3 + c_in.
//
// Destination: two upstream tensors with ggml shape
// [h=16, w=16, c_in=3, c_out=1152] F32 each, one per temporal slice.
//
// For each output element (h, w, c_in, c_out):
// src_idx = h + w*W + t*W*H + (c_out*C_in + c_in)*W*H*T
// dst_idx = h + w*W + c_in*W*H + c_out*W*H*C_in
void register_qwen35moe_patch_embed_split(gguf_context * meta, ggml_context * ctx) {
const char * src_name = "v.patch_embed.weight";
if (gguf_find_tensor(meta, src_name) < 0) return;
const ggml_tensor * src_t = ggml_get_tensor(ctx, src_name);
if (!src_t) return;
const size_t src_offset = tensor_file_offset(meta, src_name);
const size_t src_size = ggml_nelements(src_t) * sizeof(uint16_t);
constexpr int H = 16, W = 16, T = 2, CIN = 3, COUT = 1152;
constexpr size_t HW = (size_t) H * W;
auto make_slice_op = [=](int slice_idx) {
return LoadOp{
[=](const char * path, void * dst, size_t dst_size) {
if (dst_size != (size_t) H * W * CIN * COUT * sizeof(float)) return false;
std::vector<uint8_t> src(src_size);
if (!read_at(path, src_offset, src.data(), src_size)) return false;
const uint16_t * sp = reinterpret_cast<const uint16_t *>(src.data());
float * dp = reinterpret_cast<float *>(dst);
for (int c_out = 0; c_out < COUT; ++c_out) {
for (int c_in = 0; c_in < CIN; ++c_in) {
const size_t packed = (size_t) c_out * CIN + c_in;
const uint16_t * in_base = sp + HW * (slice_idx + T * packed);
float * out_base = dp + HW * (c_in + CIN * c_out);
for (size_t i = 0; i < HW; ++i) out_base[i] = ggml_fp16_to_fp32(in_base[i]);
}
}
return true;
},
slice_idx == 0 ? "patch_embed slice 0 (permute+F16->F32)"
: "patch_embed slice 1 (permute+F16->F32)",
};
};
// Rename src -> `v.patch_embd.weight`, reshape to dest layout, register
// the slice-0 load op.
rename_tensor(meta, ctx, src_name, "v.patch_embd.weight");
if (ggml_tensor * dest0 = ggml_get_tensor(ctx, "v.patch_embd.weight")) {
set_tensor_shape(dest0, {H, W, CIN, COUT});
set_tensor_type (dest0, GGML_TYPE_F32);
}
register_load_op("v.patch_embd.weight", make_slice_op(0));
// Reclaim the `v.blk.0.attn_k.weight` slot (orphaned by the QKV merge)
// as the sibling `v.patch_embd.weight.1`.
reclaim_slot_as(meta, ctx,
"v.blk.0.attn_k.weight", "v.patch_embd.weight.1",
{H, W, CIN, COUT}, GGML_TYPE_F32);
register_load_op("v.patch_embd.weight.1", make_slice_op(1));
}
void handle_qwen35moe_clip(gguf_context * meta, ggml_context * ctx) {
LLAMA_LOG_INFO("%s: detected Ollama-format qwen35moe GGUF used as mmproj; translating\n", __func__);
copy_u32_kv(meta, "qwen35moe.vision.block_count", "clip.vision.block_count");
copy_u32_kv(meta, "qwen35moe.vision.embedding_length", "clip.vision.embedding_length");
copy_u32_kv(meta, "qwen35moe.vision.attention.head_count", "clip.vision.attention.head_count");
copy_u32_kv(meta, "qwen35moe.vision.patch_size", "clip.vision.patch_size");
copy_u32_kv(meta, "qwen35moe.vision.spatial_merge_size", "clip.vision.spatial_merge_size");
copy_u32_kv(meta, "qwen35moe.vision.num_channels", "clip.vision.num_channels");
// projection_dim = text model's embedding_length.
copy_u32_kv(meta, "qwen35moe.embedding_length", "clip.vision.projection_dim");
// Defaults for KVs Ollama omitted (match the Qwen3.5-35B-A3B reference mmproj).
inject_u32_if_missing(meta, "clip.vision.feed_forward_length", 4304);
inject_u32_if_missing(meta, "clip.vision.image_size", 768);
inject_f32_if_missing(meta, "clip.vision.attention.layer_norm_epsilon", 1e-6f);
static const float kHalfHalfHalf[3] = {0.5f, 0.5f, 0.5f};
inject_f32_arr_if_missing(meta, "clip.vision.image_mean", kHalfHalfHalf, 3);
inject_f32_arr_if_missing(meta, "clip.vision.image_std", kHalfHalfHalf, 3);
// is_deepstack_layers: qwen3.5 35B has no deepstack layers. Set 27 False.
if (!has_key(meta, "clip.vision.is_deepstack_layers")) {
uint8_t bools[27] = {};
gguf_set_arr_data(meta, "clip.vision.is_deepstack_layers", GGUF_TYPE_BOOL, bools, 27);
}
inject_bool_if_missing(meta, "clip.has_vision_encoder", true);
inject_bool_if_missing(meta, "clip.use_gelu", true);
gguf_set_val_str(meta, "clip.projector_type", "qwen3vl_merger");
gguf_set_val_str(meta, "general.architecture", "clip");
// QKV merge runs BEFORE substring renames so it can find attn_q/k/v by name.
const int64_t n_blocks_key = gguf_find_key(meta, "clip.vision.block_count");
const uint32_t n_blocks = n_blocks_key >= 0 ? gguf_get_val_u32(meta, n_blocks_key) : 27;
for (uint32_t b = 0; b < n_blocks; ++b) register_qwen35moe_qkv_merge(meta, ctx, (int) b);
// Also before renames: patch_embed references the source by name.
register_qwen35moe_patch_embed_split(meta, ctx);
// Simple substring renames.
for (const auto & [from, to] : kQwen35moeClipRenames) {
rename_tensors_containing(meta, ctx, from, to);
}
promote_tensor_to_f32(meta, ctx, "v.position_embd.weight");
}
// =========================================================================
// deepseek-ocr (clip side — SAM + CLIP + projector)
// =========================================================================
//
// Ollama's monolithic deepseek-ocr GGUF embeds three vision components:
// * SAM encoder under the `s.*` prefix (12 blocks)
// * CLIP encoder under the `v.*` prefix (24 blocks)
// * MLP projector under `mm.*`
// Upstream's PROJECTOR_TYPE_DEEPSEEKOCR loader expects:
// * SAM under `v.sam.*`
// * CLIP under `v.*` (different leaf names than Ollama)
// * Projector as `mm.model.fc.*` plus `v.image_newline` / `v.view_seperator`
constexpr std::pair<const char *, const char *> kDeepseekocrClipRenames[] = {
// CLIP block leaf renames (also affects v.sam.* but those names don't overlap).
{".self_attn.out_proj", ".attn_out"},
{".self_attn.qkv_proj", ".attn_qkv"},
{".layer_norm1", ".ln1"},
{".layer_norm2", ".ln2"},
{".mlp.fc1", ".ffn_up"},
{".mlp.fc2", ".ffn_down"},
{"v.pre_layrnorm", "v.pre_ln"}, // Ollama typo
// SAM block leaf renames (after `s.*` -> `v.sam.*` is applied).
{".attn.proj", ".attn.out"},
{".attn.rel_pos_h", ".attn.pos_h.weight"},
{".attn.rel_pos_w", ".attn.pos_w.weight"},
{".norm1", ".pre_ln"},
{".norm2", ".post_ln"},
// Projector renames.
{"mm.layers", "mm.model.fc"},
{"mm.image_newline", "v.image_newline"},
{"mm.view_seperator", "v.view_seperator"},
};
void handle_deepseekocr_clip(gguf_context * meta, ggml_context * ctx) {
LLAMA_LOG_INFO("%s: detected Ollama-format deepseekocr GGUF used as mmproj; translating\n", __func__);
// CLIP encoder hparams.
copy_u32_kv(meta, "deepseekocr.vision.block_count", "clip.vision.block_count");
copy_u32_kv(meta, "deepseekocr.vision.embedding_length", "clip.vision.embedding_length");
copy_u32_kv(meta, "deepseekocr.vision.head_count", "clip.vision.attention.head_count");
copy_u32_kv(meta, "deepseekocr.vision.image_size", "clip.vision.image_size");
copy_u32_kv(meta, "deepseekocr.vision.patch_size", "clip.vision.patch_size");
// SAM encoder hparams.
copy_u32_kv(meta, "deepseekocr.sam.block_count", "clip.vision.sam.block_count");
copy_u32_kv(meta, "deepseekocr.sam.embedding_length", "clip.vision.sam.embedding_length");
copy_u32_kv(meta, "deepseekocr.sam.head_count", "clip.vision.sam.head_count");
// Defaults pulled from the upstream-converted reference mmproj.
inject_u32_if_missing(meta, "clip.vision.feed_forward_length", 64);
inject_u32_if_missing(meta, "clip.vision.projection_dim", 1280);
inject_u32_if_missing(meta, "clip.vision.projector.scale_factor", 1);
inject_u32_if_missing(meta, "clip.vision.window_size", 14);
inject_f32_if_missing(meta, "clip.vision.attention.layer_norm_epsilon", 1e-6f);
static const float kHalfHalfHalf[3] = {0.5f, 0.5f, 0.5f};
inject_f32_arr_if_missing(meta, "clip.vision.image_mean", kHalfHalfHalf, 3);
inject_f32_arr_if_missing(meta, "clip.vision.image_std", kHalfHalfHalf, 3);
inject_bool_if_missing(meta, "clip.has_vision_encoder", true);
inject_bool_if_missing(meta, "clip.use_gelu", true);
gguf_set_val_str(meta, "clip.projector_type", "deepseekocr");
gguf_set_val_str(meta, "general.architecture", "clip");
// Step 1: rename SAM prefix `s.` -> `v.sam.` only at the start of names
// (substring rename would corrupt e.g. `mm.layers.weight` -> `mm.layerv.sam.weight`).
{
std::vector<std::string> sam_names;
const int64_t n = gguf_get_n_tensors(meta);
for (int64_t i = 0; i < n; ++i) {
std::string name(gguf_get_tensor_name(meta, i));
if (name.size() >= 2 && name[0] == 's' && name[1] == '.') {
sam_names.push_back(std::move(name));
}
}
for (const auto & old_name : sam_names) {
rename_tensor(meta, ctx, old_name.c_str(),
("v.sam." + old_name.substr(2)).c_str());
}
}
// Step 2: SAM `s.position_embd` (no `.weight` suffix) — handle exactly,
// since after the `s.` rename it lives at `v.sam.position_embd`.
rename_tensor(meta, ctx, "v.sam.position_embd", "v.sam.pos_embd.weight");
// Step 3: substring renames for CLIP, SAM block leaves, and projector.
for (const auto & [from, to] : kDeepseekocrClipRenames) {
rename_tensors_containing(meta, ctx, from, to);
}
// Metal IM2COL needs F32 patch_embd (same issue as gemma3 / mistral3).
promote_tensor_to_f32(meta, ctx, "v.patch_embd.weight");
promote_tensor_to_f32(meta, ctx, "v.sam.patch_embd.weight");
// CLIP position embedding too — Ollama stores F16, upstream stores F32.
promote_tensor_to_f32(meta, ctx, "v.position_embd.weight");
}
// =========================================================================
// gemma4 (clip side — gemma4v projector)
// =========================================================================
//
// Ollama's monolithic gemma4 GGUF embeds a SigLIP-style ViT plus the
// gemma4v projector (a single `mm.input_projection`). All v.* / mm.*
// tensor names already match upstream's PROJECTOR_TYPE_GEMMA4V — this
// handler only needs KV translation and an F32 promote of the patch
// embedding (Metal IM2COL).
//
// gemma4 vision uses image normalization mean=[0,0,0] / std=[1,1,1]
// (the LM does its own per-image normalization via v.std_bias /
// v.std_scale tensors) — different from the [0.5,0.5,0.5] used by
// most other arches.
void handle_gemma4_clip(gguf_context * meta, ggml_context * ctx) {
LLAMA_LOG_INFO("%s: detected Ollama-format gemma4 GGUF used as mmproj; translating\n", __func__);
gguf_set_val_str(meta, "general.architecture", "clip");
const bool has_vision = any_tensor_with_prefix(ctx, "v.");
const bool has_audio = any_tensor_with_prefix(ctx, "a.");
if (has_vision) {
copy_u32_kv(meta, "gemma4.vision.block_count", "clip.vision.block_count");
copy_u32_kv(meta, "gemma4.vision.embedding_length", "clip.vision.embedding_length");
copy_u32_kv(meta, "gemma4.vision.feed_forward_length", "clip.vision.feed_forward_length");
copy_u32_kv(meta, "gemma4.vision.attention.head_count", "clip.vision.attention.head_count");
copy_f32_kv(meta, "gemma4.vision.attention.layer_norm_epsilon", "clip.vision.attention.layer_norm_epsilon");
copy_u32_kv(meta, "gemma4.vision.patch_size", "clip.vision.patch_size");
// gemma4 vision is fixed at 224x224 patches.
inject_u32_if_missing(meta, "clip.vision.image_size", 224);
// projection_dim = LM embedding length.
copy_u32_kv(meta, "gemma4.embedding_length", "clip.vision.projection_dim");
static const float kZeros[3] = {0.0f, 0.0f, 0.0f};
static const float kOnes [3] = {1.0f, 1.0f, 1.0f};
inject_f32_arr_if_missing(meta, "clip.vision.image_mean", kZeros, 3);
inject_f32_arr_if_missing(meta, "clip.vision.image_std", kOnes, 3);
inject_bool_if_missing(meta, "clip.has_vision_encoder", true);
gguf_set_val_str(meta, "clip.vision.projector_type", "gemma4v");
// Metal IM2COL needs F32 patch_embd weights (same as other arches).
promote_tensor_to_f32(meta, ctx, "v.patch_embd.weight");
}
if (has_audio) {
// Audio (gemma4a — conformer encoder + audio multimodal embedder).
copy_u32_kv(meta, "gemma4.audio.block_count", "clip.audio.block_count");
copy_u32_kv(meta, "gemma4.audio.embedding_length", "clip.audio.embedding_length");
copy_u32_kv(meta, "gemma4.audio.feed_forward_length", "clip.audio.feed_forward_length");
copy_u32_kv(meta, "gemma4.audio.attention.head_count", "clip.audio.attention.head_count");
copy_f32_kv(meta, "gemma4.audio.attention.layer_norm_epsilon", "clip.audio.attention.layer_norm_epsilon");
// Defaults from the upstream-converted reference E2B mmproj.
inject_u32_if_missing(meta, "clip.audio.num_mel_bins", 128);
inject_u32_if_missing(meta, "clip.audio.projection_dim", 1536);
inject_bool_if_missing(meta, "clip.has_audio_encoder", true);
gguf_set_val_str(meta, "clip.audio.projector_type", "gemma4a");
// Top-level tensor renames. Ollama uses different leaf names for the
// SSCP input projection and the encoder output projection:
// a.pre_encode.out.weight → a.input_projection.weight (SSCP proj)
// mm.a.fc.{weight,bias} → a.pre_encode.out.{weight,bias}
// mm.a.input_projection.weight already matches.
rename_tensor(meta, ctx, "a.pre_encode.out.weight", "a.input_projection.weight");
rename_tensor(meta, ctx, "mm.a.fc.weight", "a.pre_encode.out.weight");
rename_tensor(meta, ctx, "mm.a.fc.bias", "a.pre_encode.out.bias");
// Per-block renames. Scoped to a.blk.* (NOT vision blocks, which also
// have ln1/ln2). Order matters: ln2 → attn_post_norm must run before
// layer_pre_norm → ln2 (otherwise the second rename collides).
//
// Semantic mapping (from Ollama's model_audio.go vs upstream gemma4a.cpp):
// ln1 → attn_pre_norm (pre-attention norm)
// ln2 → attn_post_norm (post-attention norm; NOT block out)
// layer_pre_norm → ln2 (final block output norm)
// linear_pos → attn_k_rel (relative-position K projection)
const int kid = gguf_find_key(meta, "gemma4.audio.block_count");
const uint32_t n_audio = (kid >= 0) ? gguf_get_val_u32(meta, kid) : 12;
for (uint32_t il = 0; il < n_audio; ++il) {
char from[GGML_MAX_NAME], to[GGML_MAX_NAME];
auto rn = [&](const char * a, const char * b) {
std::snprintf(from, sizeof(from), "a.blk.%u.%s.weight", il, a);
std::snprintf(to, sizeof(to), "a.blk.%u.%s.weight", il, b);
rename_tensor(meta, ctx, from, to);
};
rn("ln1", "attn_pre_norm");
rn("ln2", "attn_post_norm");
rn("layer_pre_norm", "ln2");
rn("linear_pos", "attn_k_rel");
}
}
}
// =========================================================================
// glm-ocr (clip side — glm4v projector)
// =========================================================================
//
// Ollama stores the GLM4V vision tower with v.blk.X.* tensor names that
// already match upstream's expectations (`attn_qkv`, `attn_out`,
// `attn_q_norm`, `attn_k_norm`, `ln1`/`ln2`, `ffn_{gate,up,down}`).
// Most of mm.* (mm.model.fc, mm.up/gate/down, mm.post_norm,
// mm.patch_merger) is also already named correctly. The two diffs:
// * `v.patch_embd_0.weight` / `v.patch_embd_1.weight` → upstream's
// pixel-shuffle patch-embed pair `v.patch_embd.weight` /
// `v.patch_embd.weight.1`.
// * F32 promote of patch_embd weights (Metal IM2COL).
void handle_glmocr_clip(gguf_context * meta, ggml_context * ctx) {
LLAMA_LOG_INFO("%s: detected Ollama-format glm-ocr GGUF used as mmproj; translating\n", __func__);
copy_u32_kv(meta, "glmocr.vision.block_count", "clip.vision.block_count");
copy_u32_kv(meta, "glmocr.vision.embedding_length", "clip.vision.embedding_length");
copy_u32_kv(meta, "glmocr.vision.intermediate_size", "clip.vision.feed_forward_length");
copy_u32_kv(meta, "glmocr.vision.attention.head_count", "clip.vision.attention.head_count");
copy_f32_kv(meta, "glmocr.vision.attention.layer_norm_rms_epsilon", "clip.vision.attention.layer_norm_epsilon");
copy_u32_kv(meta, "glmocr.vision.image_size", "clip.vision.image_size");
copy_u32_kv(meta, "glmocr.vision.patch_size", "clip.vision.patch_size");
copy_u32_kv(meta, "glmocr.vision.spatial_merge_size", "clip.vision.spatial_merge_size");
copy_u32_kv(meta, "glmocr.vision.out_hidden_size", "clip.vision.projection_dim");
// Ollama already shipped image_mean / image_std under glmocr.vision.*;
// copy them through.
{
const int64_t kid = gguf_find_key(meta, "glmocr.vision.image_mean");
if (kid >= 0 && !has_key(meta, "clip.vision.image_mean")) {
const size_t n = gguf_get_arr_n(meta, kid);
gguf_set_arr_data(meta, "clip.vision.image_mean", GGUF_TYPE_FLOAT32,
gguf_get_arr_data(meta, kid), n);
}
}
{
const int64_t kid = gguf_find_key(meta, "glmocr.vision.image_std");
if (kid >= 0 && !has_key(meta, "clip.vision.image_std")) {
const size_t n = gguf_get_arr_n(meta, kid);
gguf_set_arr_data(meta, "clip.vision.image_std", GGUF_TYPE_FLOAT32,
gguf_get_arr_data(meta, kid), n);
}
}
inject_bool_if_missing(meta, "clip.has_vision_encoder", true);
inject_bool_if_missing(meta, "clip.use_silu", true);
gguf_set_val_str(meta, "clip.projector_type", "glm4v");
gguf_set_val_str(meta, "general.architecture", "clip");
// Patch-embed temporal pair: Ollama uses _0/_1 suffixes, upstream uses
// unsuffixed/.1.
rename_tensor(meta, ctx, "v.patch_embd_0.weight", "v.patch_embd.weight");
rename_tensor(meta, ctx, "v.patch_embd_1.weight", "v.patch_embd.weight.1");
// F32 promote for IM2COL on Metal (same fix as gemma3 / mistral3).
promote_tensor_to_f32(meta, ctx, "v.patch_embd.weight");
promote_tensor_to_f32(meta, ctx, "v.patch_embd.weight.1");
}
// =========================================================================
// llama4 (clip side)
// =========================================================================
//
// Ollama's monolithic llama4 GGUF embeds the CLIP-style ViT and a 3-layer
// projector (`mm.linear_1` + `v.vision_adapter.mlp.fc1/fc2`). Upstream's
// PROJECTOR_TYPE_LLAMA4 expects the projector under `mm.model.fc` /
// `mm.model.mlp.{1,2}` and standard CLIP block leaf names.
constexpr std::pair<const char *, const char *> kLlama4ClipRenames[] = {
// Vision-adapter MLP -> upstream's MM-MLP slots. Run BEFORE the generic
// `.mlp.fc{1,2}` -> `.ffn_{up,down}` rename so the substring match stays
// pinned to the adapter prefix.
{"v.vision_adapter.mlp.fc1", "mm.model.mlp.1"},
{"v.vision_adapter.mlp.fc2", "mm.model.mlp.2"},
// Main projector.
{"mm.linear_1", "mm.model.fc"},
// Vision tower non-blk.
{"v.class_embedding", "v.class_embd"},
{"v.layernorm_post", "v.post_ln"},
{"v.layernorm_pre", "v.pre_ln"},
{"v.patch_embedding", "v.patch_embd"},
// Vision-tower block leaves.
{".attn_output", ".attn_out"},
{".attn_norm", ".ln1"},
{".ffn_norm", ".ln2"},
{".mlp.fc1", ".ffn_up"},
{".mlp.fc2", ".ffn_down"},
};
void handle_llama4_clip(gguf_context * meta, ggml_context * ctx) {
LLAMA_LOG_INFO("%s: detected Ollama-format llama4 GGUF used as mmproj; translating\n", __func__);
copy_u32_kv(meta, "llama4.vision.block_count", "clip.vision.block_count");
copy_u32_kv(meta, "llama4.vision.embedding_length", "clip.vision.embedding_length");
copy_u32_kv(meta, "llama4.vision.feed_forward_length", "clip.vision.feed_forward_length");
copy_u32_kv(meta, "llama4.vision.attention.head_count", "clip.vision.attention.head_count");
copy_u32_kv(meta, "llama4.vision.image_size", "clip.vision.image_size");
copy_u32_kv(meta, "llama4.vision.patch_size", "clip.vision.patch_size");
copy_f32_kv(meta, "llama4.vision.layer_norm_epsilon", "clip.vision.attention.layer_norm_epsilon");
// projection_dim = LM embedding length (= mm.model.fc output dim).
copy_u32_kv(meta, "llama4.embedding_length", "clip.vision.projection_dim");
// Defaults (match the upstream-converted reference mmproj).
inject_u32_if_missing(meta, "clip.vision.projector.scale_factor", 2);
static const float kHalfHalfHalf[3] = {0.5f, 0.5f, 0.5f};
inject_f32_arr_if_missing(meta, "clip.vision.image_mean", kHalfHalfHalf, 3);
inject_f32_arr_if_missing(meta, "clip.vision.image_std", kHalfHalfHalf, 3);
inject_bool_if_missing(meta, "clip.has_vision_encoder", true);
inject_bool_if_missing(meta, "clip.use_gelu", true);
gguf_set_val_str(meta, "clip.projector_type", "llama4");
gguf_set_val_str(meta, "general.architecture", "clip");
// Position embedding has no `.weight` suffix in Ollama; rename exactly.
rename_tensor(meta, ctx, "v.positional_embedding_vlm", "v.position_embd.weight");
for (const auto & [from, to] : kLlama4ClipRenames) {
rename_tensors_containing(meta, ctx, from, to);
}
}
// =========================================================================
// mistral3 (clip side — pixtral projector)
// =========================================================================
//
// Tensor renames Ollama → upstream pixtral:
// v.patch_conv -> v.patch_embd
// v.encoder_norm -> v.pre_ln
// v.blk.X.attn_output -> v.blk.X.attn_out
// v.blk.X.attn_norm -> v.blk.X.ln1
// v.blk.X.ffn_norm -> v.blk.X.ln2
// mm.linear_1 -> mm.1
// mm.linear_2 -> mm.2
// mm.norm -> mm.input_norm
// mm.patch_merger.merging_layer -> mm.patch_merger
//
// img_break: pixtral's loader requires `v.token_embd.img_break` (the
// embedding row for the [IMG_BREAK] token, used as a row separator).
// Ollama's monolithic blob doesn't ship it as a separate tensor; the
// "ideal" value is row 12 of token_embd.weight, but token_embd is
// quantized (Q4_K) and per-row dequant is heavyweight. Reclaim the
// orphan output_norm.weight slot (already [n_embd] F32) and zero-fill
// it — pixtral.cpp adds img_break to row separator embeddings, so a
// zero embedding makes [IMG_BREAK] insertion a no-op without breaking
// the rest of the vision graph.
constexpr std::pair<const char *, const char *> kMistral3ClipRenames[] = {
{"v.patch_conv", "v.patch_embd"},
{"v.encoder_norm", "v.pre_ln"},
{".attn_output", ".attn_out"},
{".attn_norm", ".ln1"},
{".ffn_norm", ".ln2"},
{"mm.linear_1", "mm.1"},
{"mm.linear_2", "mm.2"},
{"mm.patch_merger.merging_layer", "mm.patch_merger"},
{"mm.norm", "mm.input_norm"},
};
// Apply the LLaMA-style RoPE permutation to Ollama's vision Q/K weight.
//
// Ollama's mistral3 converter (convert/convert_mistral.go) only applies
// its repack to TEXT-side attn_q/attn_k (the `if !HasPrefix(name, "v.")`
// guard skips vision tensors). So vision Q/K leave the converter in raw
// HF/PyTorch order. Upstream's HF→GGUF flow (convert_hf_to_gguf.py
// Mistral3 path) DOES permute vision Q/K with the vision head count,
// because pixtral's clip graph uses `ggml_rope_ext` in mode 0 which
// expects the [n_head, head_dim/2, 2, ...] layout.
//
// To bridge the two: apply LlamaModel.permute equivalently — reshape
// to [n_head, 2, head_dim/2, in], swap axes 1↔2, reshape back. The
// permutation acts only on the output dim, which is ne[1] for ggml
// weights stored as [in_dim, out_dim], so we shuffle whole rows.
//
// Permutation formula: oa = h*head_dim + dp*2 + half (post-permute idx)
// ob = h*head_dim + half*(head_dim/2) + dp (HF idx)
// copy row ob in src → row oa in dst.
//
// Only F16 Q/K rows handled (V is not RoPE'd; quantized rows would need
// block-aware shuffling — Ollama keeps Q/K F16 for mistral3 8B).
void register_mistral3_vision_qk_permute(gguf_context * meta, ggml_context * ctx,
const char * tensor_name, int n_head) {
ggml_tensor * t = ggml_get_tensor(ctx, tensor_name);
if (!t || t->type != GGML_TYPE_F16) return;
const int total_out = (int) t->ne[1];
if (total_out % n_head != 0) return;
const size_t row_bytes = ggml_row_size(t->type, t->ne[0]);
const size_t total_bytes = ggml_nbytes(t);
const size_t src_offset = tensor_file_offset(meta, tensor_name);
const int head_dim = total_out / n_head;
const int head_dim2 = head_dim / 2;
register_load_op(tensor_name, LoadOp{
[=](const char * path, void * dst, size_t dst_size) {
if (dst_size != total_bytes) return false;
std::vector<uint8_t> src(total_bytes);
if (!read_at(path, src_offset, src.data(), total_bytes)) return false;
uint8_t * dp = static_cast<uint8_t *>(dst);
for (int oa = 0; oa < total_out; ++oa) {
const int h = oa / head_dim;
const int dp_ = (oa % head_dim) / 2;
const int hf = oa % 2;
const int ob = h * head_dim + hf * head_dim2 + dp_;
std::memcpy(dp + (size_t) oa * row_bytes,
src.data() + (size_t) ob * row_bytes, row_bytes);
}
return true;
},
"vision Q/K LLaMA permute",
});
}
void handle_mistral3_clip(gguf_context * meta, ggml_context * ctx) {
LLAMA_LOG_INFO("%s: detected Ollama-format mistral3 GGUF used as mmproj; translating\n", __func__);
copy_u32_kv(meta, "mistral3.vision.block_count", "clip.vision.block_count");
copy_u32_kv(meta, "mistral3.vision.embedding_length", "clip.vision.embedding_length");
copy_u32_kv(meta, "mistral3.vision.feed_forward_length", "clip.vision.feed_forward_length");
copy_u32_kv(meta, "mistral3.vision.attention.head_count", "clip.vision.attention.head_count");
copy_u32_kv(meta, "mistral3.vision.image_size", "clip.vision.image_size");
copy_u32_kv(meta, "mistral3.vision.patch_size", "clip.vision.patch_size");
copy_u32_kv(meta, "mistral3.vision.num_channels", "clip.vision.num_channels");
copy_u32_kv(meta, "mistral3.spatial_merge_size", "clip.vision.spatial_merge_size");
copy_f32_kv(meta, "mistral3.vision.rope.freq_base", "clip.rope.freq_base");
// projection_dim is required by the loader but pixtral derives the
// actual output dim from mm_2_w shape — any non-zero value works.
// Mirror the LM embedding length for diagnostics-friendliness.
copy_u32_kv(meta, "mistral3.embedding_length", "clip.vision.projection_dim");
inject_f32_if_missing(meta, "clip.vision.attention.layer_norm_epsilon", 1e-5f);
// Pixtral image stats (CLIP-style means).
static const float kPixtralMean[3] = {0.48145467f, 0.45782750f, 0.40821072f};
static const float kPixtralStd [3] = {0.26862955f, 0.26130259f, 0.27577710f};
inject_f32_arr_if_missing(meta, "clip.vision.image_mean", kPixtralMean, 3);
inject_f32_arr_if_missing(meta, "clip.vision.image_std", kPixtralStd, 3);
inject_bool_if_missing(meta, "clip.has_vision_encoder", true);
inject_bool_if_missing(meta, "clip.use_silu", true);
gguf_set_val_str(meta, "clip.projector_type", "pixtral");
gguf_set_val_str(meta, "general.architecture", "clip");
// Reclaim output_norm.weight as v.token_embd.img_break (zero-filled).
const int64_t lm_embd_kid = gguf_find_key(meta, "mistral3.embedding_length");
const uint32_t lm_embd = lm_embd_kid >= 0 ? gguf_get_val_u32(meta, lm_embd_kid) : 0;
if (lm_embd > 0 && reclaim_slot_as(meta, ctx,
"output_norm.weight", "v.token_embd.img_break",
{(int64_t) lm_embd}, GGML_TYPE_F32)) {
register_load_op("v.token_embd.img_break", LoadOp{
[](const char *, void * dst, size_t dst_size) {
std::memset(dst, 0, dst_size);
return true;
},
"img_break zero-fill",
});
}
// Apply LLaMA-style RoPE permutation to vision Q/K BEFORE renames
// (we capture offsets by current name). Ollama's converter only
// repacks TEXT-side q/k (skipping `v.*`), but pixtral's clip graph
// expects HF→GGUF's permuted layout for vision Q/K.
{
const int64_t v_hk = gguf_find_key(meta, "mistral3.vision.attention.head_count");
const int64_t n_blk_k = gguf_find_key(meta, "mistral3.vision.block_count");
if (v_hk >= 0 && n_blk_k >= 0) {
const int n_head = (int) gguf_get_val_u32(meta, v_hk);
const uint32_t n_blocks = gguf_get_val_u32(meta, n_blk_k);
for (uint32_t b = 0; b < n_blocks; ++b) {
char qn[64], kn[64];
std::snprintf(qn, sizeof(qn), "v.blk.%u.attn_q.weight", b);
std::snprintf(kn, sizeof(kn), "v.blk.%u.attn_k.weight", b);
register_mistral3_vision_qk_permute(meta, ctx, qn, n_head);
register_mistral3_vision_qk_permute(meta, ctx, kn, n_head);
}
}
}
for (const auto & [from, to] : kMistral3ClipRenames) {
rename_tensors_containing(meta, ctx, from, to);
}
// Upstream stores patch_embd as F32; Ollama stored F16. Metal's
// IM2COL convolution silently produces garbage with F16 weights
// (same issue as gemma3 — see handle_gemma3_clip). Promote to F32.
promote_tensor_to_f32(meta, ctx, "v.patch_embd.weight");
}
// =========================================================================
// qwen25vl (clip side — Qwen2.5-VL vision tower + merger)
// =========================================================================
//
// Ollama qwen25vl has a vision tower with mostly upstream-compatible
// tensor names. Five tensor renames + KV translation:
//
// v.merger.ln_q.weight → v.post_ln.weight (post-tower norm)
// v.merger.mlp.0.{weight,bias} → mm.0.{weight,bias} (LLaVA proj 0)
// v.merger.mlp.2.{weight,bias} → mm.2.{weight,bias} (LLaVA proj 2)
// v.patch_embd_0.weight → v.patch_embd.weight (slice 0)
// v.patch_embd_1.weight → v.patch_embd.weight.1 (slice 1)
//
// The KV side maps qwen25vl.vision.* → clip.vision.*, sets the projector
// type and use_silu, derives n_wa_pattern from fullatt_block_indexes[0]+1
// (per upstream's qwen2.5vl converter), and supplies image_size=560 and
// projection_dim (= text embedding_length, qwen25vl.embedding_length).
void handle_qwen25vl_clip(gguf_context * meta, ggml_context * ctx) {
LLAMA_LOG_INFO("%s: detected Ollama-format qwen25vl GGUF used as mmproj; translating\n", __func__);
copy_u32_kv(meta, "qwen25vl.vision.attention.head_count", "clip.vision.attention.head_count");
copy_f32_kv(meta, "qwen25vl.vision.attention.layer_norm_epsilon", "clip.vision.attention.layer_norm_epsilon");
copy_u32_kv(meta, "qwen25vl.vision.block_count", "clip.vision.block_count");
copy_u32_kv(meta, "qwen25vl.vision.embedding_length", "clip.vision.embedding_length");
copy_u32_kv(meta, "qwen25vl.vision.num_channels", "clip.vision.num_channels");
copy_u32_kv(meta, "qwen25vl.vision.patch_size", "clip.vision.patch_size");
copy_u32_kv(meta, "qwen25vl.vision.spatial_merge_size", "clip.vision.spatial_merge_size");
copy_u32_kv(meta, "qwen25vl.vision.window_size", "clip.vision.window_size");
copy_u32_kv(meta, "qwen25vl.embedding_length", "clip.vision.projection_dim");
// Derive feed_forward_length from the actual ffn_up shape if missing.
if (!has_key(meta, "clip.vision.feed_forward_length")) {
if (ggml_tensor * t = ggml_get_tensor(ctx, "v.blk.0.ffn_up.weight")) {
gguf_set_val_u32(meta, "clip.vision.feed_forward_length", (uint32_t) t->ne[1]);
}
}
// Derive n_wa_pattern from fullatt_block_indexes[0]+1 (upstream convention).
{
const int64_t kid = gguf_find_key(meta, "qwen25vl.vision.fullatt_block_indexes");
if (kid >= 0 && gguf_get_arr_n(meta, kid) >= 1) {
const auto * arr = static_cast<const int32_t *>(gguf_get_arr_data(meta, kid));
gguf_set_val_u32(meta, "clip.vision.n_wa_pattern", (uint32_t)(arr[0] + 1));
}
}
// Default image_size = 560 (Qwen2VLVisionModel default, no image_size in HF config).
inject_u32_if_missing(meta, "clip.vision.image_size", 560);
// Standard preprocessor mean/std for Qwen2.5-VL (CLIP convention).
static const float kMean[3] = {0.48145466f, 0.4578275f, 0.40821073f};
static const float kStd [3] = {0.26862954f, 0.26130258f, 0.27577711f};
inject_f32_arr_if_missing(meta, "clip.vision.image_mean", kMean, 3);
inject_f32_arr_if_missing(meta, "clip.vision.image_std", kStd, 3);
inject_bool_if_missing(meta, "clip.has_vision_encoder", true);
inject_bool_if_missing(meta, "clip.use_silu", true);
gguf_set_val_str(meta, "clip.projector_type", "qwen2.5vl_merger");
gguf_set_val_str(meta, "general.architecture", "clip");
// Tensor renames.
rename_tensor(meta, ctx, "v.merger.ln_q.weight", "v.post_ln.weight");
rename_tensor(meta, ctx, "v.merger.mlp.0.weight", "mm.0.weight");
rename_tensor(meta, ctx, "v.merger.mlp.0.bias", "mm.0.bias");
rename_tensor(meta, ctx, "v.merger.mlp.2.weight", "mm.2.weight");
rename_tensor(meta, ctx, "v.merger.mlp.2.bias", "mm.2.bias");
rename_tensor(meta, ctx, "v.patch_embd_0.weight", "v.patch_embd.weight");
rename_tensor(meta, ctx, "v.patch_embd_1.weight", "v.patch_embd.weight.1");
// Metal IM2COL needs F32 patch_embd (same issue as gemma3 / glmocr).
promote_tensor_to_f32(meta, ctx, "v.patch_embd.weight");
promote_tensor_to_f32(meta, ctx, "v.patch_embd.weight.1");
}
// =========================================================================
// qwen3vl (clip side — Qwen3-VL vision tower + deepstack adapters)
// =========================================================================
//
// Ollama qwen3vl monolithic GGUF embeds the vision tower (27 blocks),
// deepstack merger adapters (3 of them, indexed 0/1/2), and the merger
// MLP. Compared to upstream's qwen3vl_merger expectations:
//
// * Per-block leaf renames: norm1→ln1, norm2→ln2, mlp.linear_fc1→ffn_up,
// mlp.linear_fc2→ffn_down.
// * Merger renames: v.merger.norm→v.post_ln, v.merger.linear_fc1→mm.0,
// v.merger.linear_fc2→mm.2 (LLaVA proj).
// * Deepstack remap: v.deepstack_merger.X.* → v.deepstack.{indexes[X]}.*
// where indexes is qwen3vl.vision.deepstack_visual_indexes (e.g.
// [8, 16, 24] for Qwen3-VL-8B). The leaf names also rename:
// linear_fc1→fc1, linear_fc2→fc2.
// * Per-block QKV merge: upstream's qwen3vl graph reads a single
// attn_qkv tensor (shape [hidden, 3*hidden]); Ollama stores separate
// Q/K/V. Same merge as qwen35moe — reuse that helper.
// * Patch embed: split the merged Conv3D weight [W,H,T,OUT*IN] into two
// Conv2D weights [W,H,IN,OUT], one per temporal slice. Same logic and
// donor (orphaned attn_k from QKV merge) as qwen35moe; reuse that helper.
void handle_qwen3vl_clip(gguf_context * meta, ggml_context * ctx) {
LLAMA_LOG_INFO("%s: detected Ollama-format qwen3vl GGUF used as mmproj; translating\n", __func__);
copy_u32_kv(meta, "qwen3vl.vision.attention.head_count", "clip.vision.attention.head_count");
copy_f32_kv(meta, "qwen3vl.vision.attention.layer_norm_epsilon", "clip.vision.attention.layer_norm_epsilon");
copy_u32_kv(meta, "qwen3vl.vision.block_count", "clip.vision.block_count");
copy_u32_kv(meta, "qwen3vl.vision.embedding_length", "clip.vision.embedding_length");
copy_u32_kv(meta, "qwen3vl.vision.num_channels", "clip.vision.num_channels");
copy_u32_kv(meta, "qwen3vl.vision.patch_size", "clip.vision.patch_size");
copy_u32_kv(meta, "qwen3vl.vision.spatial_merge_size", "clip.vision.spatial_merge_size");
copy_u32_kv(meta, "qwen3vl.embedding_length", "clip.vision.projection_dim");
// Derive feed_forward_length from ffn_up / mlp.linear_fc1 shape.
if (!has_key(meta, "clip.vision.feed_forward_length")) {
if (ggml_tensor * t = ggml_get_tensor(ctx, "v.blk.0.mlp.linear_fc1.weight")) {
gguf_set_val_u32(meta, "clip.vision.feed_forward_length", (uint32_t) t->ne[1]);
}
}
// image_size = sqrt(num_position_embeddings) * patch_size. v.pos_embed
// shape is [n_embd, num_positions], so num_positions = ne[1].
if (!has_key(meta, "clip.vision.image_size")) {
ggml_tensor * pe = ggml_get_tensor(ctx, "v.pos_embed.weight");
const int64_t patch_kid = gguf_find_key(meta, "qwen3vl.vision.patch_size");
if (pe && patch_kid >= 0) {
const uint32_t patch = gguf_get_val_u32(meta, patch_kid);
const uint32_t side = (uint32_t) std::sqrt((double) pe->ne[1]);
gguf_set_val_u32(meta, "clip.vision.image_size", side * patch);
}
}
// Image mean/std (Qwen3-VL uses [0.5, 0.5, 0.5] for both, per HF config).
static const float kHalfHalfHalf[3] = {0.5f, 0.5f, 0.5f};
inject_f32_arr_if_missing(meta, "clip.vision.image_mean", kHalfHalfHalf, 3);
inject_f32_arr_if_missing(meta, "clip.vision.image_std", kHalfHalfHalf, 3);
inject_bool_if_missing(meta, "clip.has_vision_encoder", true);
inject_bool_if_missing(meta, "clip.use_gelu", true);
gguf_set_val_str(meta, "clip.projector_type", "qwen3vl_merger");
gguf_set_val_str(meta, "general.architecture", "clip");
// Per-block QKV merge: upstream's qwen3vl_merger graph reads a single
// `v.blk.X.attn_qkv.weight` (shape [hidden, 3*hidden]) — Ollama stores
// separate Q/K/V. Unlike qwen35moe (where Q/K/V are uniformly F16), the
// qwen3vl Ollama blob can mix F16 (Q/K) with Q8_0 (V), so a raw byte
// concat fails. Dequantize all three to F32 and concat in F32 instead.
// After the merge, attn_k/attn_v become orphaned in the clip ctx, which
// the patch_embed split then reclaims for `v.patch_embd.weight.1`.
const int64_t n_blocks_key = gguf_find_key(meta, "clip.vision.block_count");
const uint32_t n_blocks = n_blocks_key >= 0 ? gguf_get_val_u32(meta, n_blocks_key) : 27;
for (uint32_t b = 0; b < n_blocks; ++b) {
char q[64], k[64], v[64], qb[64], kb[64], vb[64], qkv_w[64], qkv_b[64];
std::snprintf(q, sizeof(q), "v.blk.%u.attn_q.weight", b);
std::snprintf(k, sizeof(k), "v.blk.%u.attn_k.weight", b);
std::snprintf(v, sizeof(v), "v.blk.%u.attn_v.weight", b);
std::snprintf(qb, sizeof(qb), "v.blk.%u.attn_q.bias", b);
std::snprintf(kb, sizeof(kb), "v.blk.%u.attn_k.bias", b);
std::snprintf(vb, sizeof(vb), "v.blk.%u.attn_v.bias", b);
std::snprintf(qkv_w, sizeof(qkv_w), "v.blk.%u.attn_qkv.weight", b);
std::snprintf(qkv_b, sizeof(qkv_b), "v.blk.%u.attn_qkv.bias", b);
if (!ggml_get_tensor(ctx, q)) continue;
register_concat_load_to_f32(meta, ctx, qkv_w, {q, k, v});
register_concat_load_to_f32(meta, ctx, qkv_b, {qb, kb, vb});
rename_tensor(meta, ctx, q, qkv_w);
if (ggml_tensor * t = ggml_get_tensor(ctx, qkv_w)) {
set_tensor_shape(t, {t->ne[0], t->ne[1] * 3});
set_tensor_type (t, GGML_TYPE_F32);
}
rename_tensor(meta, ctx, qb, qkv_b);
if (ggml_tensor * t = ggml_get_tensor(ctx, qkv_b)) {
set_tensor_shape(t, {t->ne[0] * 3});
set_tensor_type (t, GGML_TYPE_F32);
}
}
// Patch embed split runs BEFORE per-block substring renames so it can
// find the source by name `v.patch_embed.weight`. Same shape as
// qwen35moe (16x16 patches, 2 temporal slices, 3 in_ch, 1152 out_ch).
register_qwen35moe_patch_embed_split(meta, ctx);
// Top-level renames (full names) — must run before substring per-block
// renames so .linear_fc1 substring matches only inside .mlp.linear_fc1.
rename_tensor(meta, ctx, "v.merger.norm.weight", "v.post_ln.weight");
rename_tensor(meta, ctx, "v.merger.norm.bias", "v.post_ln.bias");
rename_tensor(meta, ctx, "v.merger.linear_fc1.weight", "mm.0.weight");
rename_tensor(meta, ctx, "v.merger.linear_fc1.bias", "mm.0.bias");
rename_tensor(meta, ctx, "v.merger.linear_fc2.weight", "mm.2.weight");
rename_tensor(meta, ctx, "v.merger.linear_fc2.bias", "mm.2.bias");
rename_tensor(meta, ctx, "v.patch_embed.bias", "v.patch_embd.bias");
rename_tensor(meta, ctx, "v.pos_embed.weight", "v.position_embd.weight");
// Deepstack remap: v.deepstack_merger.X.{norm,linear_fc1,linear_fc2}.{weight,bias}
// → v.deepstack.{deepstack_visual_indexes[X]}.{norm,fc1,fc2}.{weight,bias}.
// Upstream stores deepstack tensors at the absolute clip layer index
// (e.g. v.deepstack.8.* for the adapter that fires after layer 8).
{
const int64_t ds_kid = gguf_find_key(meta, "qwen3vl.vision.deepstack_visual_indexes");
if (ds_kid >= 0) {
const size_t n = gguf_get_arr_n(meta, ds_kid);
const auto * idx = static_cast<const int32_t *>(gguf_get_arr_data(meta, ds_kid));
for (size_t i = 0; i < n; ++i) {
char from[GGML_MAX_NAME], to[GGML_MAX_NAME];
auto rn = [&](const char * leaf_from, const char * leaf_to, const char * suffix) {
std::snprintf(from, sizeof(from), "v.deepstack_merger.%zu.%s.%s", i, leaf_from, suffix);
std::snprintf(to, sizeof(to), "v.deepstack.%d.%s.%s", idx[i], leaf_to, suffix);
rename_tensor(meta, ctx, from, to);
};
rn("norm", "norm", "weight");
rn("norm", "norm", "bias");
rn("linear_fc1", "fc1", "weight");
rn("linear_fc1", "fc1", "bias");
rn("linear_fc2", "fc2", "weight");
rn("linear_fc2", "fc2", "bias");
}
}
}
// Per-block substring renames (safe — these substrings now only appear
// in v.blk.X.* paths after the top-level/deepstack renames above).
rename_tensors_containing(meta, ctx, ".norm1", ".ln1");
rename_tensors_containing(meta, ctx, ".norm2", ".ln2");
rename_tensors_containing(meta, ctx, ".mlp.linear_fc1", ".ffn_up");
rename_tensors_containing(meta, ctx, ".mlp.linear_fc2", ".ffn_down");
// Position embed should be F32 (precision matters for resize_position_embeddings).
promote_tensor_to_f32(meta, ctx, "v.position_embd.weight");
}
} // anonymous namespace
// =========================================================================
// Public entry points
// =========================================================================
void translate_metadata(const llama_model_loader * ml,
gguf_context * meta,
ggml_context * ctx,
std::string & arch_name,
const char * fname) {
if (!meta) return;
{
std::lock_guard<std::mutex> lk(g_loader_path_mutex);
g_loader_paths[ml] = fname ? fname : "";
}
// embeddinggemma must run before gemma3: it switches arch_name to
// "gemma-embedding", which is what later checks (and the loader's KV
// prefix) need to see.
if (arch_name == "gemma3") handle_embeddinggemma(ml, meta, ctx, arch_name);
if (arch_name == "gemma3") handle_gemma3 (ml, meta, ctx);
if (arch_name == "gemma3n") handle_gemma3n (ml, meta, ctx);
if (arch_name == "gemma4") handle_gemma4 (ml, meta, ctx);
if (arch_name == "qwen35moe") handle_qwen35moe(ml, meta, ctx);
if (arch_name == "qwen35") handle_qwen35 (ml, meta, ctx);
if (arch_name == "gptoss") handle_gptoss (ml, meta, ctx, arch_name);
if (arch_name == "lfm2") handle_lfm2 (ml, meta, ctx);
if (arch_name == "mistral3") handle_mistral3 (ml, meta, ctx);
// qwen25vl must run before any qwen2vl-targeted handler — it switches
// arch_name to "qwen2vl" so the loader uses qwen2vl.* keys.
if (arch_name == "qwen25vl") handle_qwen25vl (ml, meta, ctx, arch_name);
if (arch_name == "qwen3vl") handle_qwen3vl (ml, meta, ctx);
// glm4moelite switches arch_name to "deepseek2" — same pattern.
if (arch_name == "glm4moelite") handle_glm4moelite (ml, meta, ctx, arch_name);
if (arch_name == "deepseekocr") handle_deepseekocr (ml, meta, ctx, arch_name);
if (arch_name == "nemotron_h_moe") handle_nemotron_h_moe(ml, meta, ctx);
if (arch_name == "llama4") handle_llama4 (ml, meta, ctx);
if (arch_name == "glmocr") handle_glmocr (ml, meta, ctx, arch_name);
// Dispatch. Add more arches as they are wired up.
}
void translate_clip_metadata(gguf_context * meta, ggml_context * ctx) {
if (!meta) return;
if (!any_tensor_with_prefix(ctx, "v.")) return; // nothing to translate
if (detect_ollama_gemma3(meta, ctx)) {
LLAMA_LOG_INFO("%s: detected Ollama-format gemma3 GGUF used as mmproj; translating\n", __func__);
handle_gemma3_clip(meta, ctx);
return;
}
if (detect_ollama_qwen35moe(meta, ctx)) {
handle_qwen35moe_clip(meta, ctx);
return;
}
if (detect_ollama_mistral3(meta, ctx)) {
handle_mistral3_clip(meta, ctx);
return;
}
if (detect_ollama_deepseekocr(meta)) {
handle_deepseekocr_clip(meta, ctx);
return;
}
if (detect_ollama_llama4(meta, ctx)) {
handle_llama4_clip(meta, ctx);
return;
}
if (detect_ollama_gemma4(meta, ctx)) {
handle_gemma4_clip(meta, ctx);
return;
}
if (detect_ollama_glmocr(meta)) {
handle_glmocr_clip(meta, ctx);
return;
}
if (detect_ollama_qwen25vl(meta)) {
handle_qwen25vl_clip(meta, ctx);
return;
}
if (detect_ollama_qwen3vl(meta, ctx)) {
handle_qwen3vl_clip(meta, ctx);
return;
}
}
bool should_skip_tensor(const llama_model_loader * ml, const char * tensor_name) {
return should_skip_tensor_prefix(ml, tensor_name);
}
bool maybe_load_tensor(ggml_tensor * cur,
const char * source_file,
size_t file_offset,
ggml_backend_buffer_type_t buft) {
(void) file_offset; // registered ops capture their own offsets
LoadOp op;
if (!take_load_op(ggml_get_name(cur), op)) return false;
const size_t dst_size = ggml_nbytes(cur);
std::vector<uint8_t> dst(dst_size);
if (!op.apply(source_file, dst.data(), dst_size)) {
LLAMA_LOG_ERROR("%s: %s failed for %s\n", __func__, op.description, ggml_get_name(cur));
return false;
}
if (ggml_backend_buft_is_host(buft)) std::memcpy(cur->data, dst.data(), dst_size);
else ggml_backend_tensor_set(cur, dst.data(), 0, dst_size);
LLAMA_LOG_INFO("%s: %s for %s (%zu bytes)\n", __func__, op.description, ggml_get_name(cur), dst_size);
return true;
}
bool maybe_load_text_tensor(const llama_model_loader * ml,
ggml_tensor * cur,
size_t file_offset) {
std::string path;
{
std::lock_guard<std::mutex> lk(g_loader_path_mutex);
auto it = g_loader_paths.find(ml);
if (it == g_loader_paths.end() || it->second.empty()) return false;
path = it->second;
}
ggml_backend_buffer_type_t buft = cur->buffer
? ggml_backend_buffer_get_type(cur->buffer)
: nullptr;
return maybe_load_tensor(cur, path.c_str(), file_offset, buft);
}
} // namespace llama_ollama_compat