mirror of
https://github.com/ollama/ollama.git
synced 2026-08-28 04:35:14 +00:00
llama/compat: load Ollama-format GGUFs in llama-server
Squashed from upstream/jmorganca/llama-compat on 2026-04-29. Source tip:0c33775d37. Original source commits: -25223160dllama/compat: add in-memory shim so llama-server can load Ollama-format GGUFs -7449b539allm,server: route Ollama-format gemma3 blobs through llama/compat -436f2e2b1llama/compat: make patch-apply idempotent -8c2c9d4c8llama/compat: extend gemma3 handler to cover 1B and 270M blobs -021389f7bllama/compat: shrink clip.cpp injection from 18 lines to 1 -61b367ec2llama/compat: shrink patch to pure call-site hooks (34 -> 20 lines) -36049361cllama/compat: simplify shim (gemma3-tested) -8fa664865llama/compat: add qwen35moe text handler -db0c74530llama/compat: add qwen35moe vision (clip) support -2a388da77llama/compat: split shared infra into a util TU -9a69a17dcllama/compat: document non-public API dependencies -d0f38a915llama/compat: add gpt-oss and lfm2 handlers -086071822llama/compat: add mistral3 text handler (vision TODO) -63bde9ff7llama/compat: add mistral3 vision (clip) support -3a57b89d5llama/compat: apply LLaMA RoPE permute to mistral3 vision Q/K -99cb87439llama/compat: add qwen35, gemma4, deepseek-ocr handlers -2c7850dballama/compat: add nemotron_h_moe handler (latent FFN + MTP skip) -9e3b54225llama/compat: add llama4 text + clip handlers -034fee349llama/compat: add gemma4 clip handler (gemma4v projector) -9945c5a93server: remove dhiltgen/* compat redirect table -5d4539101llama/compat: rewrite gemma4 tokenizer model to BPE -7e0765327llama/compat: add glm-ocr text handler + text-loader load-op hook -f1bd1a25allama/compat: add glm-ocr clip handler (glm4v projector) -4b5cf3420llama/compat: collapse text-loader hook back to one new patch line -eb4ecf4fcllama/compat: extend gemma4 clip handler to gemma4a (audio) -a23a5e76fllama/compat: fix gemma4a per-block norm tensor mapping -cd2dcaff4llama/compat: add embeddinggemma handler -1ce8a6b26llama/compat: add qwen3-vl + qwen2.5-vl handlers -fd98ffa1ellama/compat: add gemma3n + glm4moelite handlers -cc7bdf0bcllama/compat: handle null buft in maybe_load_tensor -0c33775d3llama/compat: disable mmap when load_op transforms text-side tensors
This commit is contained in:
parent
31e336791a
commit
868b63ff77
12 changed files with 2975 additions and 70 deletions
121
llama/compat/README.md
Normal file
121
llama/compat/README.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# llama.cpp compatibility shim
|
||||
|
||||
This directory holds an in-process compatibility layer that lets upstream
|
||||
`llama-server` load GGUFs produced by older versions of Ollama (and files
|
||||
pulled from the Ollama registry) without re-converting or re-downloading.
|
||||
|
||||
The layer is applied automatically at build time via CMake `FetchContent`'s
|
||||
`PATCH_COMMAND` — there is no separate "apply patches" step.
|
||||
|
||||
## Files
|
||||
|
||||
- `llama-ollama-compat.h`, `llama-ollama-compat.cpp` — the shim itself. These
|
||||
are regular source files owned by Ollama; they get copied into the fetched
|
||||
llama.cpp source tree during configure.
|
||||
- `upstream-edits.patch` — small additive edits to upstream files so the
|
||||
shim gets called. Currently ~48 lines touching 6 files. Kept as a real
|
||||
`git` patch so re-generation on upstream bumps is one command.
|
||||
|
||||
## What the shim does
|
||||
|
||||
The shim runs at two well-defined points in the loader:
|
||||
|
||||
1. **After `gguf_init_from_file`**, for both the main model loader and the
|
||||
`mtmd/clip` loader: inspects the just-parsed metadata and decides whether
|
||||
the file is an Ollama-format GGUF. If so, it mutates the in-memory
|
||||
`gguf_context` and `ggml_context` (KV names, tensor names, tensor types)
|
||||
so the rest of the loader sees an upstream-shape file.
|
||||
|
||||
2. **After `load_all_data`**: applies any numerical fix-ups that need the
|
||||
tensors in their final backend buffers (e.g. RMSNorm `+1` if a future
|
||||
arch needs it — gemma3 doesn't).
|
||||
|
||||
Non-Ollama files are detected by the absence of Ollama-specific KV keys
|
||||
(e.g. `gemma3.mm.tokens_per_image`) or embedded `v.*` / `mm.*` tensors in
|
||||
the main model file. When no markers are present every compat function is
|
||||
an immediate no-op.
|
||||
|
||||
## Currently supported architectures
|
||||
|
||||
| Arch | Text loader | Clip (mmproj) loader |
|
||||
|---|---|---|
|
||||
| `gemma3` | KV injection (`layer_norm_rms_epsilon`, `rope.freq_base`, `rope.freq_base_swa`), tokenizer vocab truncation, drop `v.*`/`mm.*` tensors | Arch rewrite to `clip`, KV synthesis (`clip.vision.*`, `clip.projector_type=gemma3`), tensor renames (`v.patch_embedding`→`v.patch_embd`, `mlp.fc{1,2}`→`ffn_{down,up}`, etc.), F16→F32 promotion for patch/position embeddings (Metal IM2COL requirement) |
|
||||
| `qwen35moe` | head_count_kv array → scalar, rope dimension_sections pad 3→4, `ssm_dt`→`ssm_dt.bias` rename, drop `v.*`/`mm.*`/`mtp.*` tensors | Arch rewrite to `clip`, KV synthesis (`clip.vision.*`, `clip.projector_type=qwen3vl_merger`), per-block QKV merge (concat at load time), patch_embed reshape + F16→F32 + slice-as-temporal-pair (reclaiming an orphan `v.blk.0.attn_k` slot for the second pair) |
|
||||
| `gptoss` | Arch rename `gptoss`→`gpt-oss` (incl. KV prefix), inject `gpt-oss.expert_feed_forward_length` from `ffn_gate_exps` shape, tensor renames (`attn_out`→`attn_output`, `attn_sinks`→`attn_sinks.weight`, `ffn_norm`→`post_attention_norm`) | n/a |
|
||||
| `lfm2` | Tensor rename `output_norm.weight`→`token_embd_norm.weight`, fix stale `lfm2.feed_forward_length` from `ffn_gate` shape | n/a |
|
||||
| `mistral3` | RoPE YaRN renames (`rope.scaling.beta_*`→`rope.scaling.yarn_beta_*`), `rope.scaling_beta`→`attention.temperature_scale`, drop `v.*`/`mm.*` tensors | Arch rewrite to `clip`, KV synthesis (`clip.vision.*`, `clip.projector_type=pixtral`), tensor renames (`v.patch_conv`→`v.patch_embd`, `v.encoder_norm`→`v.pre_ln`, `attn_output`→`attn_out`, `attn_norm`/`ffn_norm`→`ln1`/`ln2`, `mm.linear_{1,2}`→`mm.{1,2}`, `mm.norm`→`mm.input_norm`, `mm.patch_merger.merging_layer`→`mm.patch_merger`), zero-fill `v.token_embd.img_break` (reclaims `output_norm.weight` slot — Ollama's monolithic blob doesn't ship this tensor and per-row dequant of token_embd Q4_K is heavyweight; zero-fill makes [IMG_BREAK] insertion a no-op), F32 promote of `v.patch_embd.weight` (Metal IM2COL), LLaMA-style RoPE permute on vision Q/K (Ollama's converter skips repacking `v.*` tensors but pixtral expects HF-permuted layout) |
|
||||
| `qwen35` | Same fixes as `qwen35moe` (head_count_kv array→scalar, rope dimension_sections pad 3→4, `ssm_dt`→`ssm_dt.bias`, drop `v.*`/`mm.*`/`mtp.*`) but for the non-MoE qwen3.5 (e.g. 9B). Both arches share `apply_qwen35_text_fixes`. | n/a |
|
||||
| `gemma4` | Drop `a.*`/`v.*`/`mm.*` (audio + vision + projector) from the text loader. Covers both E2B/E4B (dense) and 26B-A4B (MoE). | n/a |
|
||||
| `deepseekocr` | Arch rename `deepseekocr`→`deepseek2-ocr` (incl. KV prefix), inject `expert_feed_forward_length` from `ffn_down_exps` shape, `expert_shared_count` from `ffn_down_shexp` shape, default `attention.layer_norm_rms_epsilon`, drop `s.*`/`v.*`/`mm.*` | Arch rewrite to `clip`, KV synthesis (`clip.vision.*`, `clip.vision.sam.*`, `clip.projector_type=deepseekocr`, defaults for `feed_forward_length`/`projection_dim`/`window_size`/image stats), prefix-only rename `s.*`→`v.sam.*` (substring rename would corrupt `mm.layers`), CLIP leaf renames (`self_attn.{out,qkv}_proj`→`attn_{out,qkv}`, `layer_norm{1,2}`→`ln{1,2}`, `mlp.fc{1,2}`→`ffn_{up,down}`, `pre_layrnorm`→`pre_ln`), SAM leaf renames (`attn.proj`→`attn.out`, `attn.rel_pos_{h,w}`→`attn.pos_{h,w}.weight`, `norm{1,2}`→`{pre,post}_ln`), projector renames (`mm.layers`→`mm.model.fc`, `mm.image_newline`/`view_seperator`→`v.*`), F32 promote of `v.patch_embd.weight`, `v.sam.patch_embd.weight`, `v.position_embd.weight` |
|
||||
| `nemotron_h_moe` | For latent-FFN variants (e.g. nemotron-3-super 120B-A12B): inject `moe_latent_size` from `ffn_latent_in.weight` ne[1], rename `ffn_latent_{in,out}`→`ffn_latent_{down,up}`. For all variants: drop `mtp.*` (Multi-Token Prediction tensors that Ollama emits as one-tensor-per-expert; ~1040 extras on the 120B). Standard variants (e.g. nemotron-cascade-2 30B-A3B) load with no rename, only the MTP skip. | n/a |
|
||||
|
||||
Usage:
|
||||
|
||||
```
|
||||
llama-server --model /path/to/ollama-blob --mmproj /path/to/ollama-blob
|
||||
```
|
||||
|
||||
Passing the same monolithic GGUF as both `--model` and `--mmproj` works —
|
||||
each loader applies its own translation.
|
||||
|
||||
Additional architectures are added by implementing a `handle_<arch>()`
|
||||
and (for vision models) `handle_<arch>_clip()` in `llama-ollama-compat.cpp`
|
||||
and dispatching them from `translate_metadata` / `translate_clip_metadata`.
|
||||
|
||||
## Regenerating `upstream-edits.patch`
|
||||
|
||||
After upstream changes the insertion points (rare), re-apply the edits to
|
||||
a fresh checkout and run:
|
||||
|
||||
```
|
||||
cd /path/to/llama.cpp
|
||||
git diff -- \
|
||||
ggml/include/gguf.h \
|
||||
ggml/src/gguf.cpp \
|
||||
src/CMakeLists.txt \
|
||||
src/llama-model-loader.cpp \
|
||||
src/llama-model.cpp \
|
||||
tools/mtmd/clip.cpp \
|
||||
> /path/to/ollama/llama/compat/upstream-edits.patch
|
||||
```
|
||||
|
||||
## Why not fork llama.cpp or vendor it?
|
||||
|
||||
Forking means tracking upstream manually. Vendoring means snapshotting all of
|
||||
llama.cpp's source in the Ollama tree (the old `llama/llama.cpp/` layout).
|
||||
This shim keeps upstream unmodified on disk and the Ollama-specific logic
|
||||
isolated in two files plus a small diff — upstream bumps are usually just
|
||||
`LLAMA_CPP_VERSION` changes.
|
||||
|
||||
## Maintenance: non-public API dependencies
|
||||
|
||||
The compat code is mostly written against stable public APIs (`gguf.h`,
|
||||
`ggml.h`, `ggml-backend.h`). There are three places where we lean on
|
||||
something that isn't strictly public:
|
||||
|
||||
| Hack | Why | Escape hatch if upstream changes |
|
||||
|---|---|---|
|
||||
| Direct writes to `ggml_tensor::type` / `ne[]` / `nb[]` | No sanctioned mutator exists for post-creation tensor reshape/retype. Struct is public so this works today. | Ask upstream to expose `ggml_tensor_set_{type,shape}` helpers, or introduce them in our compat util and submit a PR. |
|
||||
| `const_cast<char *>(gguf_get_tensor_name(...))` in `rename_tensor` | Pointer aims into a mutable `char[GGML_MAX_NAME]` buffer inside a `std::vector` element; the const is API hygiene. Lets us rename gguf tensors without a new public helper. | Add `gguf_rename_tensor` to `gguf.h` (10 lines) and drop the `const_cast`. |
|
||||
| `llama_model_loader` forward-decl from `src/llama-model-loader.h` | Used only as an opaque pointer key for our skip-prefix registry. Never dereferenced. | Replace with `const void *` in our registry signatures. Zero behavioral change. |
|
||||
|
||||
None of these have changed in years. If an upstream bump breaks any of
|
||||
them, each has a trivial workaround. See the top of
|
||||
`llama-ollama-compat-util.h` for the inline notes.
|
||||
|
||||
## Documented hacks inside per-arch handlers
|
||||
|
||||
- **`reclaim_slot_as` (qwen35moe patch_embed split)** — repurposes an
|
||||
orphaned `v.blk.0.attn_k` slot (left over after the QKV merge) as a
|
||||
newly-synthesized `v.patch_embd.weight.1`. Needed because clip.cpp's
|
||||
`ctx_meta` is sized for exactly the original tensor count (no_alloc
|
||||
branch of `gguf_init_from_file` uses `n_tensors * ggml_tensor_overhead()`
|
||||
with zero slack). Comment in the helper and call site explains the
|
||||
reasoning; replacement would be a 1-line upstream patch that adds small
|
||||
slack to the ctx size.
|
||||
|
||||
- **Load-op registry overrides `file_offset`** — `maybe_load_tensor` gets
|
||||
passed the gguf offset by its caller but ignores it when a registered
|
||||
op exists. Intentional: the ops capture their own source offsets at
|
||||
translate time (before our renames invalidate them). Documented in the
|
||||
op-registration helpers.
|
||||
44
llama/compat/apply-patch.cmake
Normal file
44
llama/compat/apply-patch.cmake
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Idempotent patch applier used by compat.cmake.
|
||||
#
|
||||
# Invocation (from a CMake PATCH_COMMAND):
|
||||
# cmake -DPATCH_FILE=<abs path> -P apply-patch.cmake
|
||||
#
|
||||
# The patch is applied in the current working directory (which ExternalProject
|
||||
# / FetchContent sets to the fetched source's SOURCE_DIR). If the patch is
|
||||
# already applied — detected via `git apply --reverse --check` — this script
|
||||
# is a no-op. This makes re-configuring and re-building the project safe.
|
||||
|
||||
if(NOT DEFINED PATCH_FILE)
|
||||
message(FATAL_ERROR "apply-patch.cmake: PATCH_FILE not set")
|
||||
endif()
|
||||
if(NOT EXISTS "${PATCH_FILE}")
|
||||
message(FATAL_ERROR "apply-patch.cmake: PATCH_FILE does not exist: ${PATCH_FILE}")
|
||||
endif()
|
||||
|
||||
find_package(Git QUIET REQUIRED)
|
||||
|
||||
# If the patch can be REVERSED cleanly, it's already applied. Skip.
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} apply --reverse --check "${PATCH_FILE}"
|
||||
RESULT_VARIABLE _reverse_check
|
||||
OUTPUT_QUIET ERROR_QUIET
|
||||
)
|
||||
if(_reverse_check EQUAL 0)
|
||||
message(STATUS "llama/compat: patch already applied, skipping")
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Otherwise, apply forward.
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} apply --whitespace=nowarn "${PATCH_FILE}"
|
||||
RESULT_VARIABLE _apply_result
|
||||
)
|
||||
if(NOT _apply_result EQUAL 0)
|
||||
message(FATAL_ERROR
|
||||
"llama/compat: failed to apply ${PATCH_FILE}\n"
|
||||
"This usually means upstream llama.cpp has drifted. "
|
||||
"Regenerate the patch (see llama/compat/README.md) against the "
|
||||
"pinned LLAMA_CPP_VERSION and retry.")
|
||||
endif()
|
||||
|
||||
message(STATUS "llama/compat: applied patch")
|
||||
56
llama/compat/compat.cmake
Normal file
56
llama/compat/compat.cmake
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# llama.cpp compatibility shim — CMake integration
|
||||
#
|
||||
# Include this file BEFORE calling FetchContent_Declare(llama_cpp ...) to
|
||||
# patch the fetched upstream llama.cpp with Ollama's in-process compat
|
||||
# layer. Example usage:
|
||||
#
|
||||
# include(${CMAKE_CURRENT_SOURCE_DIR}/../compat/compat.cmake)
|
||||
#
|
||||
# FetchContent_Declare(
|
||||
# llama_cpp
|
||||
# GIT_REPOSITORY ...
|
||||
# GIT_TAG ${LLAMA_CPP_GIT_TAG}
|
||||
# GIT_SHALLOW TRUE
|
||||
# PATCH_COMMAND ${OLLAMA_LLAMA_CPP_COMPAT_PATCH_COMMAND}
|
||||
# UPDATE_DISCONNECTED TRUE
|
||||
# )
|
||||
#
|
||||
# The compat layer consists of:
|
||||
# 1. Two new source files dropped into the fetched tree's src/
|
||||
# (llama-ollama-compat.{h,cpp}) — Ollama-owned.
|
||||
# 2. A small patch (upstream-edits.patch) that wires the new files into
|
||||
# the build and adds call-sites in upstream loaders.
|
||||
|
||||
set(_compat_dir ${CMAKE_CURRENT_LIST_DIR})
|
||||
|
||||
# Expose a single variable the main CMakeLists passes into FetchContent's
|
||||
# PATCH_COMMAND. The patch is applied via a small CMake script so the step
|
||||
# is idempotent — re-configuring or rebuilding won't fail with "already
|
||||
# applied".
|
||||
#
|
||||
# The compat source files (.h, .cpp) are NOT copied into the fetched tree.
|
||||
# Instead, llama/server/CMakeLists.txt does target_sources() on the llama
|
||||
# target after FetchContent_MakeAvailable. That keeps Ollama's code in
|
||||
# Ollama's tree and makes the patch pure call-site insertions.
|
||||
set(OLLAMA_LLAMA_CPP_COMPAT_PATCH_COMMAND
|
||||
${CMAKE_COMMAND}
|
||||
-DPATCH_FILE=${_compat_dir}/upstream-edits.patch
|
||||
-P ${_compat_dir}/apply-patch.cmake
|
||||
CACHE INTERNAL "llama.cpp compat patch command for FetchContent")
|
||||
|
||||
# Where the compat source files live, so the main CMakeLists can wire them
|
||||
# into the llama target.
|
||||
set(OLLAMA_LLAMA_CPP_COMPAT_DIR
|
||||
"${_compat_dir}"
|
||||
CACHE INTERNAL "Directory holding llama-ollama-compat.{h,cpp}")
|
||||
|
||||
# Also export the individual paths in case callers want to do something
|
||||
# custom (e.g. emit a dependency on the patch so reconfigures re-apply).
|
||||
set(OLLAMA_LLAMA_CPP_COMPAT_PATCH_FILE
|
||||
"${_compat_dir}/upstream-edits.patch"
|
||||
CACHE INTERNAL "Path to the llama.cpp compat patch")
|
||||
|
||||
set(OLLAMA_LLAMA_CPP_COMPAT_SOURCES
|
||||
"${_compat_dir}/llama-ollama-compat.h"
|
||||
"${_compat_dir}/llama-ollama-compat.cpp"
|
||||
CACHE INTERNAL "Source files copied into llama.cpp's src/ dir")
|
||||
378
llama/compat/llama-ollama-compat-util.cpp
vendored
Normal file
378
llama/compat/llama-ollama-compat-util.cpp
vendored
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
#include "llama-ollama-compat-util.h"
|
||||
|
||||
#include "llama-impl.h"
|
||||
#include "llama-model-loader.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace llama_ollama_compat::detail {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// gguf_context KV helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
bool has_key(const gguf_context * meta, const char * key) {
|
||||
return gguf_find_key(meta, key) >= 0;
|
||||
}
|
||||
|
||||
void copy_u32_kv(gguf_context * meta, const char * src, const char * dst) {
|
||||
if (has_key(meta, dst)) return;
|
||||
const int64_t k = gguf_find_key(meta, src);
|
||||
if (k < 0) return;
|
||||
gguf_set_val_u32(meta, dst, gguf_get_val_u32(meta, k));
|
||||
}
|
||||
|
||||
void copy_f32_kv(gguf_context * meta, const char * src, const char * dst) {
|
||||
if (has_key(meta, dst)) return;
|
||||
const int64_t k = gguf_find_key(meta, src);
|
||||
if (k < 0) return;
|
||||
gguf_set_val_f32(meta, dst, gguf_get_val_f32(meta, k));
|
||||
}
|
||||
|
||||
void copy_kv(gguf_context * meta, const char * src, const char * dst) {
|
||||
if (has_key(meta, dst)) return;
|
||||
const int64_t kid = gguf_find_key(meta, src);
|
||||
if (kid < 0) return;
|
||||
const enum gguf_type t = gguf_get_kv_type(meta, kid);
|
||||
switch (t) {
|
||||
case GGUF_TYPE_UINT8: gguf_set_val_u8 (meta, dst, gguf_get_val_u8 (meta, kid)); break;
|
||||
case GGUF_TYPE_INT8: gguf_set_val_i8 (meta, dst, gguf_get_val_i8 (meta, kid)); break;
|
||||
case GGUF_TYPE_UINT16: gguf_set_val_u16 (meta, dst, gguf_get_val_u16 (meta, kid)); break;
|
||||
case GGUF_TYPE_INT16: gguf_set_val_i16 (meta, dst, gguf_get_val_i16 (meta, kid)); break;
|
||||
case GGUF_TYPE_UINT32: gguf_set_val_u32 (meta, dst, gguf_get_val_u32 (meta, kid)); break;
|
||||
case GGUF_TYPE_INT32: gguf_set_val_i32 (meta, dst, gguf_get_val_i32 (meta, kid)); break;
|
||||
case GGUF_TYPE_FLOAT32: gguf_set_val_f32 (meta, dst, gguf_get_val_f32 (meta, kid)); break;
|
||||
case GGUF_TYPE_BOOL: gguf_set_val_bool(meta, dst, gguf_get_val_bool(meta, kid)); break;
|
||||
case GGUF_TYPE_STRING: gguf_set_val_str (meta, dst, gguf_get_val_str (meta, kid)); break;
|
||||
case GGUF_TYPE_UINT64: gguf_set_val_u64 (meta, dst, gguf_get_val_u64 (meta, kid)); break;
|
||||
case GGUF_TYPE_INT64: gguf_set_val_i64 (meta, dst, gguf_get_val_i64 (meta, kid)); break;
|
||||
case GGUF_TYPE_FLOAT64: gguf_set_val_f64 (meta, dst, gguf_get_val_f64 (meta, kid)); break;
|
||||
case GGUF_TYPE_ARRAY: {
|
||||
const enum gguf_type et = gguf_get_arr_type(meta, kid);
|
||||
const size_t n = gguf_get_arr_n(meta, kid);
|
||||
if (et == GGUF_TYPE_STRING) {
|
||||
std::vector<std::string> owned;
|
||||
owned.reserve(n);
|
||||
std::vector<const char *> ptrs;
|
||||
ptrs.reserve(n);
|
||||
for (size_t i = 0; i < n; ++i) owned.emplace_back(gguf_get_arr_str(meta, kid, i));
|
||||
for (const auto & s : owned) ptrs.push_back(s.c_str());
|
||||
gguf_set_arr_str(meta, dst, ptrs.data(), n);
|
||||
} else {
|
||||
gguf_set_arr_data(meta, dst, et, gguf_get_arr_data(meta, kid), n);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
void rename_kv_prefix(gguf_context * meta, const char * old_prefix,
|
||||
const char * new_prefix) {
|
||||
const size_t old_len = std::strlen(old_prefix);
|
||||
// Snapshot keys first; copy_kv() invalidates the kv index by appending.
|
||||
std::vector<std::string> matches;
|
||||
const int64_t n = gguf_get_n_kv(meta);
|
||||
for (int64_t i = 0; i < n; ++i) {
|
||||
const char * k = gguf_get_key(meta, i);
|
||||
if (std::strncmp(k, old_prefix, old_len) == 0) matches.emplace_back(k);
|
||||
}
|
||||
for (const auto & old_key : matches) {
|
||||
copy_kv(meta, old_key.c_str(),
|
||||
(std::string(new_prefix) + old_key.substr(old_len)).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void inject_u32_if_missing (gguf_context * meta, const char * key, uint32_t v) {
|
||||
if (!has_key(meta, key)) gguf_set_val_u32(meta, key, v);
|
||||
}
|
||||
void inject_f32_if_missing (gguf_context * meta, const char * key, float v) {
|
||||
if (!has_key(meta, key)) gguf_set_val_f32(meta, key, v);
|
||||
}
|
||||
void inject_str_if_missing (gguf_context * meta, const char * key, const char * v) {
|
||||
if (!has_key(meta, key)) gguf_set_val_str(meta, key, v);
|
||||
}
|
||||
void inject_bool_if_missing(gguf_context * meta, const char * key, bool v) {
|
||||
if (!has_key(meta, key)) gguf_set_val_bool(meta, key, v);
|
||||
}
|
||||
void inject_f32_arr_if_missing(gguf_context * meta, const char * key,
|
||||
const float * data, size_t n) {
|
||||
if (!has_key(meta, key)) gguf_set_arr_data(meta, key, GGUF_TYPE_FLOAT32, data, n);
|
||||
}
|
||||
|
||||
void truncate_str_arr(gguf_context * meta, const char * key, size_t new_n) {
|
||||
const int64_t kid = gguf_find_key(meta, key);
|
||||
if (kid < 0 || new_n >= gguf_get_arr_n(meta, kid)) return;
|
||||
|
||||
std::vector<std::string> owned;
|
||||
owned.reserve(new_n);
|
||||
std::vector<const char *> ptrs;
|
||||
ptrs.reserve(new_n);
|
||||
for (size_t i = 0; i < new_n; ++i) owned.emplace_back(gguf_get_arr_str(meta, kid, i));
|
||||
for (const auto & s : owned) ptrs.push_back(s.c_str());
|
||||
gguf_set_arr_str(meta, key, ptrs.data(), new_n);
|
||||
}
|
||||
|
||||
void truncate_data_arr(gguf_context * meta, const char * key,
|
||||
gguf_type elem_type, size_t elem_size, size_t new_n) {
|
||||
const int64_t kid = gguf_find_key(meta, key);
|
||||
if (kid < 0 || new_n >= gguf_get_arr_n(meta, kid)) return;
|
||||
|
||||
std::vector<uint8_t> copy(elem_size * new_n);
|
||||
std::memcpy(copy.data(), gguf_get_arr_data(meta, kid), elem_size * new_n);
|
||||
gguf_set_arr_data(meta, key, elem_type, copy.data(), new_n);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ggml_context tensor scans
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
bool any_tensor_with_prefix(const ggml_context * ctx, const char * prefix) {
|
||||
const size_t plen = std::strlen(prefix);
|
||||
for (ggml_tensor * t = ggml_get_first_tensor(ctx); t; t = ggml_get_next_tensor(ctx, t)) {
|
||||
if (std::strncmp(ggml_get_name(t), prefix, plen) == 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tensor renaming / reshaping (mutates both contexts)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// gguf_get_tensor_name returns a pointer into a mutable `char[GGML_MAX_NAME]`
|
||||
// inside a std::vector element; the const on the return type is API
|
||||
// courtesy, so writing through const_cast is defined.
|
||||
void rename_tensor(gguf_context * meta, ggml_context * ctx,
|
||||
const char * old_name, const char * new_name) {
|
||||
const int64_t id = gguf_find_tensor(meta, old_name);
|
||||
if (id < 0) return;
|
||||
if (char * p = const_cast<char *>(gguf_get_tensor_name(meta, id))) {
|
||||
std::strncpy(p, new_name, GGML_MAX_NAME - 1);
|
||||
p[GGML_MAX_NAME - 1] = '\0';
|
||||
}
|
||||
if (ggml_tensor * t = ggml_get_tensor(ctx, old_name)) ggml_set_name(t, new_name);
|
||||
}
|
||||
|
||||
void rename_tensors_containing(gguf_context * meta, ggml_context * ctx,
|
||||
const char * needle, const char * replacement) {
|
||||
std::vector<std::pair<std::string, std::string>> renames;
|
||||
const int64_t n = gguf_get_n_tensors(meta);
|
||||
const size_t needle_len = std::strlen(needle);
|
||||
for (int64_t i = 0; i < n; ++i) {
|
||||
std::string s(gguf_get_tensor_name(meta, i));
|
||||
const size_t pos = s.find(needle);
|
||||
if (pos == std::string::npos) continue;
|
||||
std::string ns = s;
|
||||
ns.replace(pos, needle_len, replacement);
|
||||
renames.emplace_back(std::move(s), std::move(ns));
|
||||
}
|
||||
for (const auto & [from, to] : renames) rename_tensor(meta, ctx, from.c_str(), to.c_str());
|
||||
}
|
||||
|
||||
void set_tensor_type(ggml_tensor * t, ggml_type type) {
|
||||
t->type = type;
|
||||
t->nb[0] = ggml_type_size(type);
|
||||
t->nb[1] = t->nb[0] * (t->ne[0] / ggml_blck_size(type));
|
||||
for (int i = 2; i < GGML_MAX_DIMS; ++i) t->nb[i] = t->nb[i - 1] * t->ne[i - 1];
|
||||
}
|
||||
|
||||
void set_tensor_shape(ggml_tensor * t, std::initializer_list<int64_t> shape) {
|
||||
int i = 0;
|
||||
for (auto v : shape) t->ne[i++] = v;
|
||||
for (; i < GGML_MAX_DIMS; ++i) t->ne[i] = 1;
|
||||
set_tensor_type(t, t->type);
|
||||
}
|
||||
|
||||
// Rename an orphan tensor slot as a new synthesized tensor. See header for
|
||||
// why this is the workaround of choice (clip's ctx_meta has no spare capacity).
|
||||
bool reclaim_slot_as(gguf_context * meta, ggml_context * ctx,
|
||||
const char * orphan_name, const char * new_name,
|
||||
std::initializer_list<int64_t> shape, ggml_type type) {
|
||||
if (gguf_find_tensor(meta, orphan_name) < 0) return false;
|
||||
rename_tensor(meta, ctx, orphan_name, new_name);
|
||||
ggml_tensor * t = ggml_get_tensor(ctx, new_name);
|
||||
if (!t) return false;
|
||||
set_tensor_shape(t, shape);
|
||||
set_tensor_type (t, type);
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t tensor_file_offset(const gguf_context * meta, const char * name) {
|
||||
const int64_t id = gguf_find_tensor(meta, name);
|
||||
if (id < 0) return 0;
|
||||
return gguf_get_data_offset(meta) + gguf_get_tensor_offset(meta, id);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Per-loader skip-prefix registry
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
std::mutex g_skip_mutex;
|
||||
std::unordered_map<const llama_model_loader *, std::vector<std::string>> g_skip_prefixes;
|
||||
} // anon
|
||||
|
||||
void add_skip_prefix(const llama_model_loader * ml, std::string prefix) {
|
||||
std::lock_guard<std::mutex> lk(g_skip_mutex);
|
||||
g_skip_prefixes[ml].push_back(std::move(prefix));
|
||||
}
|
||||
|
||||
bool should_skip_tensor_prefix(const llama_model_loader * ml, const char * name) {
|
||||
std::lock_guard<std::mutex> lk(g_skip_mutex);
|
||||
auto it = g_skip_prefixes.find(ml);
|
||||
if (it == g_skip_prefixes.end()) return false;
|
||||
for (const auto & prefix : it->second) {
|
||||
if (std::strncmp(name, prefix.c_str(), prefix.size()) == 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::mutex g_no_mmap_mutex;
|
||||
std::unordered_set<const llama_model_loader *> g_no_mmap;
|
||||
} // anon
|
||||
|
||||
void disable_mmap_for(const llama_model_loader * ml) {
|
||||
std::lock_guard<std::mutex> lk(g_no_mmap_mutex);
|
||||
g_no_mmap.insert(ml);
|
||||
}
|
||||
|
||||
bool is_mmap_disabled_for(const llama_model_loader * ml) {
|
||||
std::lock_guard<std::mutex> lk(g_no_mmap_mutex);
|
||||
return g_no_mmap.count(ml) > 0;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Load-time transform registry
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
std::mutex g_loadop_mutex;
|
||||
std::unordered_map<std::string, LoadOp> g_loadops;
|
||||
} // anon
|
||||
|
||||
void register_load_op(std::string dest_name, LoadOp op) {
|
||||
std::lock_guard<std::mutex> lk(g_loadop_mutex);
|
||||
g_loadops[std::move(dest_name)] = std::move(op);
|
||||
}
|
||||
|
||||
bool take_load_op(const char * dest_name, LoadOp & out) {
|
||||
std::lock_guard<std::mutex> lk(g_loadop_mutex);
|
||||
auto it = g_loadops.find(dest_name);
|
||||
if (it == g_loadops.end()) return false;
|
||||
out = std::move(it->second);
|
||||
g_loadops.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool read_at(const char * path, size_t offset, void * dst, size_t size) {
|
||||
FILE * f = std::fopen(path, "rb");
|
||||
if (!f) return false;
|
||||
bool ok = (std::fseek(f, (long) offset, SEEK_SET) == 0
|
||||
&& std::fread(dst, 1, size, f) == size);
|
||||
std::fclose(f);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Common high-level transforms
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
void promote_tensor_to_f32(gguf_context * meta, ggml_context * ctx, const char * name) {
|
||||
const int64_t tid = gguf_find_tensor(meta, name);
|
||||
if (tid < 0) return;
|
||||
ggml_tensor * t = ggml_get_tensor(ctx, name);
|
||||
if (!t || t->type != GGML_TYPE_F16) return;
|
||||
|
||||
const size_t src_offset = tensor_file_offset(meta, name);
|
||||
const size_t n_elem = ggml_nelements(t);
|
||||
const size_t src_size = n_elem * sizeof(uint16_t);
|
||||
|
||||
set_tensor_type(t, GGML_TYPE_F32);
|
||||
|
||||
register_load_op(name, LoadOp{
|
||||
[src_offset, src_size, n_elem](const char * path, void * dst, size_t dst_size) {
|
||||
(void) dst_size;
|
||||
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 (size_t i = 0; i < n_elem; ++i) dp[i] = ggml_fp16_to_fp32(sp[i]);
|
||||
return true;
|
||||
},
|
||||
"F16->F32 promote",
|
||||
});
|
||||
}
|
||||
|
||||
void register_concat_load(const gguf_context * meta, std::string dest_name,
|
||||
const std::vector<std::string> & src_names) {
|
||||
std::vector<std::pair<size_t, size_t>> regions;
|
||||
regions.reserve(src_names.size());
|
||||
for (const auto & n : src_names) {
|
||||
const int64_t id = gguf_find_tensor(meta, n.c_str());
|
||||
if (id < 0) return;
|
||||
regions.emplace_back(
|
||||
gguf_get_data_offset(meta) + gguf_get_tensor_offset(meta, id),
|
||||
gguf_get_tensor_size(meta, id));
|
||||
}
|
||||
register_load_op(std::move(dest_name), LoadOp{
|
||||
[regions](const char * path, void * dst, size_t dst_size) {
|
||||
size_t total = 0;
|
||||
for (auto & [_, sz] : regions) total += sz;
|
||||
if (total != dst_size) return false;
|
||||
uint8_t * p = static_cast<uint8_t *>(dst);
|
||||
for (auto & [off, sz] : regions) {
|
||||
if (!read_at(path, off, p, sz)) return false;
|
||||
p += sz;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
"concat sources",
|
||||
});
|
||||
}
|
||||
|
||||
void register_concat_load_to_f32(const gguf_context * meta,
|
||||
const ggml_context * ctx,
|
||||
std::string dest_name,
|
||||
const std::vector<std::string> & src_names) {
|
||||
struct Region { size_t offset; size_t size; ggml_type type; size_t n_elem; };
|
||||
std::vector<Region> regions;
|
||||
regions.reserve(src_names.size());
|
||||
for (const auto & n : src_names) {
|
||||
const int64_t id = gguf_find_tensor(meta, n.c_str());
|
||||
if (id < 0) return;
|
||||
const ggml_tensor * t = ggml_get_tensor(const_cast<ggml_context *>(ctx), n.c_str());
|
||||
if (!t) return;
|
||||
regions.push_back({
|
||||
gguf_get_data_offset(meta) + gguf_get_tensor_offset(meta, id),
|
||||
gguf_get_tensor_size(meta, id),
|
||||
t->type,
|
||||
(size_t) ggml_nelements(t),
|
||||
});
|
||||
}
|
||||
register_load_op(std::move(dest_name), LoadOp{
|
||||
[regions](const char * path, void * dst, size_t dst_size) {
|
||||
size_t total_elems = 0;
|
||||
for (auto & r : regions) total_elems += r.n_elem;
|
||||
if (total_elems * sizeof(float) != dst_size) return false;
|
||||
|
||||
float * dp = static_cast<float *>(dst);
|
||||
for (auto & r : regions) {
|
||||
std::vector<uint8_t> src(r.size);
|
||||
if (!read_at(path, r.offset, src.data(), r.size)) return false;
|
||||
const auto * tt = ggml_get_type_traits(r.type);
|
||||
if (!tt || !tt->to_float) return false;
|
||||
tt->to_float(src.data(), dp, (int64_t) r.n_elem);
|
||||
dp += r.n_elem;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
"concat sources (mixed types -> F32)",
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace llama_ollama_compat::detail
|
||||
139
llama/compat/llama-ollama-compat-util.h
vendored
Normal file
139
llama/compat/llama-ollama-compat-util.h
vendored
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
#pragma once
|
||||
|
||||
// Internal helpers shared by the per-architecture handlers in
|
||||
// llama-ollama-compat.cpp. Not part of the public API.
|
||||
//
|
||||
// Everything lives under namespace llama_ollama_compat::detail. The
|
||||
// definitions live in llama-ollama-compat-util.cpp, which also owns the
|
||||
// registry globals (tensor skip list, load-op table) that need a single
|
||||
// translation unit.
|
||||
//
|
||||
// ---- Non-public API dependencies (see also README.md "Maintenance") ----
|
||||
//
|
||||
// Mostly public: gguf_* and ggml_* accessors from ggml/include/ are all
|
||||
// stable. `ggml_backend_*` and `ggml_fp16_to_fp32` are stable too.
|
||||
//
|
||||
// Three pieces we rely on that aren't strictly guaranteed public:
|
||||
//
|
||||
// 1. Direct writes to `ggml_tensor::type`, `ne[]`, `nb[]` — the struct is
|
||||
// public and fields are spec'd, but there's no sanctioned mutator for
|
||||
// them post-creation. Used in set_tensor_type / set_tensor_shape /
|
||||
// reclaim_slot_as. Risk: upstream could in principle introduce an
|
||||
// opaque-tensor mode; in practice it hasn't in years.
|
||||
//
|
||||
// 2. `const_cast<char *>(gguf_get_tensor_name(...))` in rename_tensor.
|
||||
// The pointer returned points into a mutable char[GGML_MAX_NAME]
|
||||
// buffer inside a std::vector element. Defined behavior as long as
|
||||
// upstream keeps name storage in-line (has done so forever).
|
||||
//
|
||||
// 3. `llama_model_loader` forward decl from src/llama-model-loader.h
|
||||
// (internal, not llama.h). Only used as an opaque pointer key for
|
||||
// the skip-prefix registry — we never dereference it. Could swap for
|
||||
// `const void *` if upstream ever moved that type around.
|
||||
//
|
||||
// All three are trivially replaceable if upstream changes out from under
|
||||
// us. See llama/compat/README.md for the escape hatches.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "ggml.h"
|
||||
#include "ggml-backend.h"
|
||||
#include "gguf.h"
|
||||
|
||||
struct llama_model_loader;
|
||||
|
||||
namespace llama_ollama_compat::detail {
|
||||
|
||||
// -- gguf_context KV helpers --
|
||||
bool has_key(const gguf_context * meta, const char * key);
|
||||
void copy_u32_kv(gguf_context * meta, const char * src, const char * dst);
|
||||
void copy_f32_kv(gguf_context * meta, const char * src, const char * dst);
|
||||
// Generic copy that preserves the source's gguf_type. Skips if `src` is
|
||||
// missing or `dst` is already present. Arrays are copied verbatim
|
||||
// (including element type).
|
||||
void copy_kv(gguf_context * meta, const char * src, const char * dst);
|
||||
// Copy every KV whose key starts with `old_prefix` to a new key under
|
||||
// `new_prefix`. Old keys are left in place — harmless because the loader
|
||||
// looks up keys by exact name and only queries the new prefix.
|
||||
void rename_kv_prefix(gguf_context * meta, const char * old_prefix,
|
||||
const char * new_prefix);
|
||||
void inject_u32_if_missing (gguf_context * meta, const char * key, uint32_t v);
|
||||
void inject_f32_if_missing (gguf_context * meta, const char * key, float v);
|
||||
void inject_str_if_missing (gguf_context * meta, const char * key, const char * v);
|
||||
void inject_bool_if_missing(gguf_context * meta, const char * key, bool v);
|
||||
void inject_f32_arr_if_missing(gguf_context * meta, const char * key,
|
||||
const float * data, size_t n);
|
||||
void truncate_str_arr (gguf_context * meta, const char * key, size_t new_n);
|
||||
void truncate_data_arr(gguf_context * meta, const char * key,
|
||||
gguf_type elem_type, size_t elem_size, size_t new_n);
|
||||
|
||||
// -- ggml_context tensor scans --
|
||||
bool any_tensor_with_prefix(const ggml_context * ctx, const char * prefix);
|
||||
|
||||
// -- Tensor renaming / reshaping (mutates both gguf_context and ggml_context) --
|
||||
void rename_tensor(gguf_context * meta, ggml_context * ctx,
|
||||
const char * old_name, const char * new_name);
|
||||
void rename_tensors_containing(gguf_context * meta, ggml_context * ctx,
|
||||
const char * needle, const char * replacement);
|
||||
void set_tensor_type (ggml_tensor * t, ggml_type type);
|
||||
void set_tensor_shape(ggml_tensor * t, std::initializer_list<int64_t> shape);
|
||||
bool reclaim_slot_as (gguf_context * meta, ggml_context * ctx,
|
||||
const char * orphan_name, const char * new_name,
|
||||
std::initializer_list<int64_t> shape, ggml_type type);
|
||||
|
||||
// -- File-offset capture (before rename) --
|
||||
size_t tensor_file_offset(const gguf_context * meta, const char * name);
|
||||
|
||||
// -- Per-loader skip-prefix registry --
|
||||
void add_skip_prefix(const llama_model_loader * ml, std::string prefix);
|
||||
bool should_skip_tensor_prefix(const llama_model_loader * ml, const char * name);
|
||||
|
||||
// -- Per-loader "needs no-mmap" flag --
|
||||
// Handlers that register a load_op which transforms a TEXT-side tensor's
|
||||
// bytes (e.g. concat reshape) must call disable_mmap_for(ml). With mmap
|
||||
// the upstream loader binds the tensor directly to the file region, so
|
||||
// our load_op has no writable buffer to fill. translate_metadata reads
|
||||
// this flag and returns it back to the patch site.
|
||||
void disable_mmap_for(const llama_model_loader * ml);
|
||||
bool is_mmap_disabled_for(const llama_model_loader * ml);
|
||||
|
||||
// -- Load-time transform registry --
|
||||
struct LoadOp {
|
||||
std::function<bool(const char * src_file, void * dst, size_t dst_size)> apply;
|
||||
const char * description;
|
||||
};
|
||||
void register_load_op(std::string dest_name, LoadOp op);
|
||||
bool take_load_op (const char * dest_name, LoadOp & out); // removes + returns
|
||||
|
||||
// Read `size` bytes at `offset` from `path` into `dst`. Used by LoadOps.
|
||||
bool read_at(const char * path, size_t offset, void * dst, size_t size);
|
||||
|
||||
// -- Common high-level transforms --
|
||||
|
||||
// F16 -> F32 promotion. Captures the source file offset at registration
|
||||
// time so later renames/reshapes of this tensor don't invalidate the read.
|
||||
void promote_tensor_to_f32(gguf_context * meta, ggml_context * ctx, const char * name);
|
||||
|
||||
// Concatenate N source tensors into one destination. Captures each source's
|
||||
// file offset + byte size at registration time. Layout assumption: sources
|
||||
// concatenate cleanly along the destination's slow ggml axis, which in
|
||||
// C order means the destination bytes are src[0] || src[1] || ... .
|
||||
void register_concat_load(const gguf_context * meta, std::string dest_name,
|
||||
const std::vector<std::string> & src_names);
|
||||
|
||||
// Mixed-type variant of register_concat_load: dequantizes each source to
|
||||
// F32 via its ggml_type_traits.to_float and concatenates the F32 arrays.
|
||||
// Use when sources differ in quantization (e.g. F16 q/k + Q8_0 v in some
|
||||
// Ollama vision blobs). Caller must set the destination tensor's type to
|
||||
// GGML_TYPE_F32 so dst_size matches the F32 concat size.
|
||||
void register_concat_load_to_f32(const gguf_context * meta,
|
||||
const ggml_context * ctx,
|
||||
std::string dest_name,
|
||||
const std::vector<std::string> & src_names);
|
||||
|
||||
} // namespace llama_ollama_compat::detail
|
||||
2017
llama/compat/llama-ollama-compat.cpp
vendored
Normal file
2017
llama/compat/llama-ollama-compat.cpp
vendored
Normal file
File diff suppressed because it is too large
Load diff
82
llama/compat/llama-ollama-compat.h
vendored
Normal file
82
llama/compat/llama-ollama-compat.h
vendored
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#pragma once
|
||||
|
||||
// Ollama-format GGUF compatibility shim.
|
||||
//
|
||||
// Older Ollama builds ship GGUFs that differ from upstream in a handful of
|
||||
// ways per-architecture (arch names, KV keys, tensor names, file layout).
|
||||
// This shim detects those files during load and translates them in-memory
|
||||
// so the rest of llama.cpp can load them unmodified.
|
||||
//
|
||||
// Three upstream hook points call into this namespace — one per insertion:
|
||||
//
|
||||
// 1. llama-model-loader.cpp (main model load):
|
||||
// translate_metadata() — mutate KVs / tensor metadata
|
||||
// should_skip_tensor() — filter weights_map population
|
||||
//
|
||||
// 2. tools/mtmd/clip.cpp (mmproj load):
|
||||
// translate_clip_metadata() — rewrite KVs + tensor names for clip
|
||||
// maybe_load_tensor() — override file read (e.g. F16->F32)
|
||||
//
|
||||
// Detection is per-arch; for any non-Ollama file every entry point is a
|
||||
// no-op. Per-arch logic lives in anonymous-namespace handle_<arch>()
|
||||
// functions in the .cpp; adding a new arch is a new handler plus one
|
||||
// dispatch line in each translate_* entry point.
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
#include "ggml-backend.h" // for ggml_backend_buffer_type_t
|
||||
|
||||
struct gguf_context;
|
||||
struct ggml_context;
|
||||
struct ggml_tensor;
|
||||
struct llama_model_loader;
|
||||
|
||||
namespace llama_ollama_compat {
|
||||
|
||||
// Called from llama_model_loader's constructor, right after the arch is read.
|
||||
// `fname` is the model file path, captured here so later load-time hooks
|
||||
// (maybe_load_text_tensor) can read raw bytes from it.
|
||||
//
|
||||
// Returns true if the caller must disable mmap for this loader. Some
|
||||
// handlers transform tensor data via load_op (e.g. glm-ocr's gate+up
|
||||
// FFN concat), which is incompatible with the default mmap path:
|
||||
// the upstream loader binds tensors directly to the mmap'd file region,
|
||||
// so there's nowhere to write the transformed bytes. Disabling mmap
|
||||
// makes the loader pre-allocate real backend buffers, after which our
|
||||
// load_op overrides land in writable memory.
|
||||
bool translate_metadata(const llama_model_loader * ml,
|
||||
gguf_context * meta,
|
||||
ggml_context * ctx,
|
||||
std::string & arch_name,
|
||||
const char * fname);
|
||||
|
||||
// Called from llama_model_loader's weights_map population loop. Returns
|
||||
// true to drop a tensor from the loader — used to hide embedded vision
|
||||
// tensors from the text model's view without modifying the gguf_context.
|
||||
bool should_skip_tensor(const llama_model_loader * ml, const char * tensor_name);
|
||||
|
||||
// Called from clip_model_loader's constructor. Rewrites the clip-facing
|
||||
// view of the metadata (arch=clip, clip.vision.* KVs, renamed tensors)
|
||||
// so the rest of clip.cpp can load an Ollama monolithic GGUF unchanged.
|
||||
void translate_clip_metadata(gguf_context * meta, ggml_context * ctx);
|
||||
|
||||
// Called from clip.cpp's tensor-loading loop, before the normal file read.
|
||||
// If this tensor was marked for type promotion by translate_clip_metadata
|
||||
// (e.g. F16->F32), performs the conversion and writes the result into
|
||||
// `cur` (host memcpy or backend_tensor_set based on `buft`). Returns true
|
||||
// when the tensor was handled — caller should skip its normal read path.
|
||||
bool maybe_load_tensor(ggml_tensor * cur,
|
||||
const char * source_file,
|
||||
size_t file_offset,
|
||||
ggml_backend_buffer_type_t buft);
|
||||
|
||||
// Text-side counterpart to maybe_load_tensor. Self-contained: looks up
|
||||
// the model file path from the per-loader registry populated by
|
||||
// translate_metadata, and derives the buffer type from cur->buffer
|
||||
// internally — keeps the call site (and the upstream patch) to one line.
|
||||
bool maybe_load_text_tensor(const llama_model_loader * ml,
|
||||
ggml_tensor * cur,
|
||||
size_t file_offset);
|
||||
|
||||
} // namespace llama_ollama_compat
|
||||
78
llama/compat/upstream-edits.patch
Normal file
78
llama/compat/upstream-edits.patch
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
diff --git a/src/llama-model-loader.cpp b/src/llama-model-loader.cpp
|
||||
index 4e65a45a5..75836c683 100644
|
||||
--- a/src/llama-model-loader.cpp
|
||||
+++ b/src/llama-model-loader.cpp
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "ggml.h"
|
||||
#include "gguf.h"
|
||||
#include "llama-hparams.h"
|
||||
+#include "llama-ollama-compat.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -549,6 +550,7 @@ llama_model_loader::llama_model_loader(
|
||||
}
|
||||
|
||||
get_key(llm_kv(LLM_KV_GENERAL_ARCHITECTURE), arch_name, false);
|
||||
+ if (llama_ollama_compat::translate_metadata(this, metadata, ctx, arch_name, fname.c_str())) use_mmap = false;
|
||||
llm_kv = LLM_KV(llm_arch_from_string(arch_name));
|
||||
|
||||
files.emplace_back(new llama_file(fname.c_str(), "rb", use_direct_io));
|
||||
@@ -573,6 +575,9 @@ llama_model_loader::llama_model_loader(
|
||||
// so we build a unified tensors index for weights.
|
||||
for (ggml_tensor * cur = ggml_get_first_tensor(ctx); cur; cur = ggml_get_next_tensor(ctx, cur)) {
|
||||
std::string tensor_name = std::string(cur->name);
|
||||
+ if (llama_ollama_compat::should_skip_tensor(this, tensor_name.c_str())) {
|
||||
+ continue;
|
||||
+ }
|
||||
// make sure there is no duplicated tensor names
|
||||
if (weights_map.find(tensor_name) != weights_map.end()) {
|
||||
throw std::runtime_error(format("invalid model: tensor '%s' is duplicated", ggml_get_name(cur)));
|
||||
@@ -683,6 +688,9 @@ llama_model_loader::llama_model_loader(
|
||||
// Save tensors data offset info of the main file.
|
||||
for (ggml_tensor * cur = ggml_get_first_tensor(ctx); cur; cur = ggml_get_next_tensor(ctx, cur)) {
|
||||
std::string tensor_name = std::string(cur->name);
|
||||
+ if (llama_ollama_compat::should_skip_tensor(this, tensor_name.c_str())) {
|
||||
+ continue;
|
||||
+ }
|
||||
// make sure there is no duplicated tensor names
|
||||
if (weights_map.find(tensor_name) != weights_map.end()) {
|
||||
throw std::runtime_error(format("invalid model: tensor '%s' is duplicated", ggml_get_name(cur)));
|
||||
@@ -1535,3 +1542,4 @@ bool llama_model_loader::load_all_data(
|
||||
size_t n_size = ggml_nbytes(cur);
|
||||
|
||||
+ if (llama_ollama_compat::maybe_load_text_tensor(this, cur, weight->offs)) continue;
|
||||
if (use_mmap) {
|
||||
diff --git a/tools/mtmd/clip.cpp b/tools/mtmd/clip.cpp
|
||||
index f0e8786b6..35defa89d 100644
|
||||
--- a/tools/mtmd/clip.cpp
|
||||
+++ b/tools/mtmd/clip.cpp
|
||||
@@ -10,6 +10,8 @@
|
||||
#include "ggml-backend.h"
|
||||
#include "gguf.h"
|
||||
|
||||
+#include "llama-ollama-compat.h"
|
||||
+
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
@@ -985,6 +987,11 @@ struct clip_model_loader {
|
||||
|
||||
ctx_meta.reset(meta);
|
||||
|
||||
+ // If this is an Ollama-format monolithic GGUF (text + embedded
|
||||
+ // vision), translate its metadata and tensor names into the
|
||||
+ // upstream mmproj shape so the rest of this loader runs unchanged.
|
||||
+ llama_ollama_compat::translate_clip_metadata(ctx_gguf.get(), meta);
|
||||
+
|
||||
const int n_tensors = gguf_get_n_tensors(ctx_gguf.get());
|
||||
|
||||
// print gguf info
|
||||
@@ -2358,6 +2365,7 @@ struct clip_model_loader {
|
||||
auto it_off = tensor_offset.find(t->name);
|
||||
GGML_ASSERT(it_off != tensor_offset.end() && "no offset for tensor");
|
||||
const size_t offset = it_off->second;
|
||||
+ if (llama_ollama_compat::maybe_load_tensor(cur, fname.c_str(), offset, buft)) continue;
|
||||
fin.seekg(offset, std::ios::beg);
|
||||
if (!fin) {
|
||||
throw std::runtime_error(string_format("%s: failed to seek for tensor %s\n", __func__, t->name));
|
||||
|
|
@ -35,6 +35,20 @@ if(DEFINED ENV{OLLAMA_LLAMA_CPP_SOURCE})
|
|||
message(STATUS "Using local llama.cpp source: ${_src}")
|
||||
endif()
|
||||
|
||||
# Ollama-compat shim: overlays the fetched llama.cpp source with a tiny
|
||||
# in-memory translation layer that lets upstream llama-server load GGUFs
|
||||
# produced by older Ollama versions (e.g. existing ~/.ollama/models/blobs).
|
||||
# See llama/compat/README.md for details.
|
||||
#
|
||||
# The patch only runs when fetching from GitHub — if a local source override
|
||||
# is active, leave the developer's tree alone (they can apply by hand if
|
||||
# they want to iterate on the compat layer).
|
||||
set(_ollama_compat_patch_cmd "")
|
||||
if(NOT DEFINED ENV{OLLAMA_LLAMA_CPP_SOURCE})
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/../compat/compat.cmake)
|
||||
set(_ollama_compat_patch_cmd PATCH_COMMAND ${OLLAMA_LLAMA_CPP_COMPAT_PATCH_COMMAND})
|
||||
endif()
|
||||
|
||||
# Configure upstream build options BEFORE FetchContent_MakeAvailable.
|
||||
# When included via FetchContent, llama.cpp sets LLAMA_STANDALONE=OFF
|
||||
# so all optional builds default to OFF. We explicitly enable what we need.
|
||||
|
|
@ -53,9 +67,27 @@ FetchContent_Declare(
|
|||
GIT_REPOSITORY "https://github.com/ggml-org/llama.cpp.git"
|
||||
GIT_TAG ${LLAMA_CPP_GIT_TAG}
|
||||
GIT_SHALLOW TRUE
|
||||
${_ollama_compat_patch_cmd}
|
||||
)
|
||||
FetchContent_MakeAvailable(llama_cpp)
|
||||
|
||||
# Link the Ollama-compat source files into the fetched llama target.
|
||||
# Kept separate from the upstream-edits patch so our .cpp/.h stay
|
||||
# on-disk in llama/compat/ rather than being copied into _deps/.
|
||||
if(DEFINED OLLAMA_LLAMA_CPP_COMPAT_DIR)
|
||||
file(GLOB _compat_sources CONFIGURE_DEPENDS
|
||||
${OLLAMA_LLAMA_CPP_COMPAT_DIR}/*.cpp)
|
||||
target_sources(llama PRIVATE ${_compat_sources})
|
||||
target_include_directories(llama PRIVATE
|
||||
${OLLAMA_LLAMA_CPP_COMPAT_DIR})
|
||||
# mtmd's clip.cpp #include's the compat header too — add the same dir
|
||||
# to its PRIVATE include path (PRIVATE on llama doesn't propagate).
|
||||
if(TARGET mtmd)
|
||||
target_include_directories(mtmd PRIVATE
|
||||
${OLLAMA_LLAMA_CPP_COMPAT_DIR})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Find GPU toolkits for runtime dependency bundling.
|
||||
# The upstream llama.cpp build finds these internally, but we need the
|
||||
# variables (CUDAToolkit_LIBRARY_DIR, etc.) in our install scope.
|
||||
|
|
|
|||
|
|
@ -423,6 +423,33 @@ func NewLlamaServerRunner(
|
|||
// Check if this is an embedding model
|
||||
_, isEmbedding := f.KV()[fmt.Sprintf("%s.pooling_type", f.KV().Architecture())]
|
||||
|
||||
// Older Ollama-format GGUFs store vision tensors (v.*, mm.*) inline in
|
||||
// the main model file rather than in a separate projector layer. When
|
||||
// the arch has a llama/compat clip handler, we can point --mmproj at
|
||||
// the same file and the in-process shim translates the two views.
|
||||
//
|
||||
// If we auto-enable --mmproj for an arch whose clip handler doesn't
|
||||
// exist yet, upstream's clip loader sees un-translated Ollama tensors
|
||||
// and aborts model load. So gate on an explicit allowlist that mirrors
|
||||
// the compat layer's clip-side coverage in llama/compat/.
|
||||
compatClipArches := map[string]bool{
|
||||
"gemma3": true,
|
||||
"gemma4": true,
|
||||
"qwen35moe": true,
|
||||
"qwen25vl": true,
|
||||
"qwen3vl": true,
|
||||
"mistral3": true,
|
||||
"deepseekocr": true,
|
||||
"glmocr": true,
|
||||
"llama4": true,
|
||||
// Add entries as llama/compat grows clip handlers.
|
||||
}
|
||||
if len(projectors) == 0 &&
|
||||
len(f.Tensors().Items("v.")) > 0 &&
|
||||
compatClipArches[f.KV().Architecture()] {
|
||||
projectors = []string{modelPath}
|
||||
}
|
||||
|
||||
gpuLibs := ml.LibraryPaths(gpus)
|
||||
status := NewStatusWriter(os.Stderr)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,73 +1,10 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/ollama/ollama/internal/modelref"
|
||||
"github.com/ollama/ollama/types/model"
|
||||
)
|
||||
|
||||
// Temporary redirection logic to map incompatible library models to compatible versions
|
||||
var compatModelRedirects = []struct{ from, to string }{
|
||||
{"library/gpt-oss", "dhiltgen/gpt-oss"},
|
||||
{"library/gemma3", "dhiltgen/gemma3"},
|
||||
{"library/embeddinggemma", "dhiltgen/embeddinggemma"},
|
||||
{"library/snowflake-arctic-embed2", "dhiltgen/snowflake-arctic-embed2"},
|
||||
{"library/gemma3n", "dhiltgen/gemma3n"},
|
||||
{"library/glm-4.7-flash", "dhiltgen/glm-4.7-flash"},
|
||||
{"library/deepseek-ocr", "dhiltgen/deepseek-ocr"},
|
||||
{"library/glm-ocr", "dhiltgen/glm-ocr"},
|
||||
{"library/gemma4", "dhiltgen/gemma4"},
|
||||
{"library/qwen2.5vl", "dhiltgen/qwen2.5vl"},
|
||||
{"library/qwen3-vl", "dhiltgen/qwen3-vl"},
|
||||
}
|
||||
|
||||
// applyCompatRedirect checks if a model name matches a compat redirect and
|
||||
// returns the redirected name. Returns the original name if no redirect applies.
|
||||
func applyCompatRedirect(n model.Name) (model.Name, bool) {
|
||||
if strings.Contains(n.DisplayShortest(), "-cloud") {
|
||||
return n, false
|
||||
}
|
||||
|
||||
for _, r := range compatModelRedirects {
|
||||
fromNS, fromModel, _ := strings.Cut(r.from, "/")
|
||||
if fromNS == n.Namespace && fromModel == n.Model {
|
||||
redirected := n
|
||||
toNS, toRest, _ := strings.Cut(r.to, "/")
|
||||
redirected.Namespace = toNS
|
||||
// Support "namespace/model:tag" to override the tag
|
||||
if toModel, toTag, hasTag := strings.Cut(toRest, ":"); hasTag {
|
||||
redirected.Model = toModel
|
||||
redirected.Tag = toTag
|
||||
} else {
|
||||
redirected.Model = toRest
|
||||
}
|
||||
slog.Debug("redirecting to compatible model", "from", n.DisplayShortest(), "to", redirected.DisplayShortest())
|
||||
return redirected, true
|
||||
}
|
||||
}
|
||||
return n, false
|
||||
}
|
||||
|
||||
// reverseCompatRedirect maps a redirected name back to its original library name.
|
||||
// Used by PsHandler so users see the name they requested, not the internal redirect target.
|
||||
// TODO: consider removing this before merging — it papers over the fact that
|
||||
// the scheduler stores the redirected name instead of the user-facing name.
|
||||
func reverseCompatRedirect(n model.Name) model.Name {
|
||||
for _, r := range compatModelRedirects {
|
||||
toNS, toModel, _ := strings.Cut(r.to, "/")
|
||||
if toNS == n.Namespace && toModel == n.Model {
|
||||
fromNS, fromModel, _ := strings.Cut(r.from, "/")
|
||||
reversed := n
|
||||
reversed.Namespace = fromNS
|
||||
reversed.Model = fromModel
|
||||
return reversed
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
type modelSource = modelref.ModelSource
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -1084,9 +1084,6 @@ func getExistingName(n model.Name) (model.Name, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Redirect models that have been republished in a compatible format
|
||||
n, _ = applyCompatRedirect(n)
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
|
|
@ -2157,10 +2154,7 @@ func (s *Server) PsHandler(c *gin.Context) {
|
|||
|
||||
for _, v := range s.sched.loaded {
|
||||
m := v.model
|
||||
// Show the user-facing name (pre-redirect) so ps output matches
|
||||
// what the user originally requested.
|
||||
// TODO: consider removing before merging — see reverseCompatRedirect comment
|
||||
displayName := reverseCompatRedirect(model.ParseName(m.ShortName)).DisplayShortest()
|
||||
displayName := model.ParseName(m.ShortName).DisplayShortest()
|
||||
modelDetails := api.ModelDetails{
|
||||
Format: m.Config.ModelFormat,
|
||||
Family: m.Config.ModelFamily,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue