runner: Remove CGO engines, use llama-server exclusively for GGML models (#16031)

* broad lint fixes to sidestep CI scope glitch

* runner: Remove CGO engines, use llama-server exclusively for GGML models

Remove the vendored GGML and llama.cpp backend, CGO runner, Go model
implementations, and sample.  llama-server (built from upstream llama.cpp via
FetchContent) is now the sole inference engine for GGUF-based models.
(Safetensor based models continue to run on the new MLX engine.)  This allows
us to more rapidly pick up new capabilities and fixes from llama.cpp as they
come out.

On windows this now requires recent AMD driver versions to support ROCm v7 as
llama.cpp currently does not support building against v6.

* 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:
- 25223160d llama/compat: add in-memory shim so llama-server can load Ollama-format GGUFs
- 7449b539a llm,server: route Ollama-format gemma3 blobs through llama/compat
- 436f2e2b1 llama/compat: make patch-apply idempotent
- 8c2c9d4c8 llama/compat: extend gemma3 handler to cover 1B and 270M blobs
- 021389f7b llama/compat: shrink clip.cpp injection from 18 lines to 1
- 61b367ec2 llama/compat: shrink patch to pure call-site hooks (34 -> 20 lines)
- 36049361c llama/compat: simplify shim (gemma3-tested)
- 8fa664865 llama/compat: add qwen35moe text handler
- db0c74530 llama/compat: add qwen35moe vision (clip) support
- 2a388da77 llama/compat: split shared infra into a util TU
- 9a69a17dc llama/compat: document non-public API dependencies
- d0f38a915 llama/compat: add gpt-oss and lfm2 handlers
- 086071822 llama/compat: add mistral3 text handler (vision TODO)
- 63bde9ff7 llama/compat: add mistral3 vision (clip) support
- 3a57b89d5 llama/compat: apply LLaMA RoPE permute to mistral3 vision Q/K
- 99cb87439 llama/compat: add qwen35, gemma4, deepseek-ocr handlers
- 2c7850dba llama/compat: add nemotron_h_moe handler (latent FFN + MTP skip)
- 9e3b54225 llama/compat: add llama4 text + clip handlers
- 034fee349 llama/compat: add gemma4 clip handler (gemma4v projector)
- 9945c5a93 server: remove dhiltgen/* compat redirect table
- 5d4539101 llama/compat: rewrite gemma4 tokenizer model to BPE
- 7e0765327 llama/compat: add glm-ocr text handler + text-loader load-op hook
- f1bd1a25a llama/compat: add glm-ocr clip handler (glm4v projector)
- 4b5cf3420 llama/compat: collapse text-loader hook back to one new patch line
- eb4ecf4fc llama/compat: extend gemma4 clip handler to gemma4a (audio)
- a23a5e76f llama/compat: fix gemma4a per-block norm tensor mapping
- cd2dcaff4 llama/compat: add embeddinggemma handler
- 1ce8a6b26 llama/compat: add qwen3-vl + qwen2.5-vl handlers
- fd98ffa1e llama/compat: add gemma3n + glm4moelite handlers
- cc7bdf0bc llama/compat: handle null buft in maybe_load_tensor
- 0c33775d3 llama/compat: disable mmap when load_op transforms text-side tensors

* refine implementation

* ci: fix windows MLX build

* ci: fix windows llama-server build

* ci: fix windows rocm build

* ci: windows mlx tuning

Shorten long-tail on build, and get OllamaSetup.exe back under 2g limit

* ci: fix windows dependencies

* win: fix dependency gathering

* disable openmp

* win: arm64 cross-compile build

also DRY out CI steps

* scheduler improvements

* ci: improvements from #15982

* win: favor ninja for faster developer builds

* win: fix build

* win: fix arm64 cross-compile

* win: avoid spaces in compiler path

* misc discovery fixes, and bos handling

* lint fixes

* win: fix arm cross-compile build/CI bugs

* llama.cpp update

* win: handle multiple CRT dirs

* vulkan: add windows iGPU detection

* fix creation bugs for patched models, other refactoring work

* tune batch size for better performance

* ci and lint fixes

* fix repeat_last_n bug

* build: revamp build for better developer UX

* amd, sampler, qwen3next fixes

* version bump

* fix mlx build

* revamp GPU discovery

Scanning the output of llama-server is turning out to be too error prone across
llama.cpp updates, so this switches to a thin dynamic library load against the
bundled GGML libraries so more details can be gathered from the API.

* version bump

* missing file

* ci: fix cache miss on rocm build

* refine vulkan dep handling

* fix ps reporting bug on full GPU load

* improve cmake wiring for customized local builds

* version bump

* docker build arg cleanup

* improve windows exit error logs

* fix community gemma4 support and ci flakes

* fix mlx unit test

* tighten up ps logic to avoid double counting fit log lines

* version bump

* fix ps view for full gpu layer offload

* add MTP wiring for llama-server and create with GGUFs

* pick best template by capabilities

* version bump

* ci: harden apt repos

* remove unused cpu core discovery

* adjust batch default logic to reduce OOMs

* support larger tool calls

* fix audio support, template show

* qwen35 mtp patch support

* flesh out dtypes

* rocm deps

* version bump

* lint fix

* block broken gfx1150 on windows

* fix qwen3.5 moe mtp tensors in patch

* mmproj oom fallback and vulkan on by default

* qwen MTP compat fix

* version bump

* ci: fix WoA cross-compile

* ci: workaround ui tool in cross-compile

* version bump

* win: enable OpenMP for CPU builds

* build: improve developer UX

* ci: windows path workaround for CPU build

* win: fix WoA dependencies

* win: fix large offset reads for mmproj patched loads

* version bump

* fix vulkan dup detection

* add OLLAMA_IGPU_ENABLE and largely disable iGPUs by default

* opt-in MTP, win large offset, integraton fixes

* fix unit test scheduler interaction hang

* fix multi-gpu filtering

* version bump

* review comments

* fix thinking level

* fix linux rocm ordering and granite 3.3 template

* version bump

* ci fix - non-shallow MLX checkout

* bypass linux sysfs unit test on windows

---------

Co-authored-by: jmorganca <jmorganca@gmail.com>
This commit is contained in:
Daniel Hiltgen
2026-05-29 13:35:47 -07:00
committed by GitHub
parent f63eea3d27
commit 9db4bdbad6
1100 changed files with 28510 additions and 430069 deletions
+72 -3
View File
@@ -147,7 +147,9 @@ func (ModelParameters) KV(t *Tokenizer) KV {
}
for _, sv := range t.SpecialVocabulary {
kv[fmt.Sprintf("tokenizer.ggml.add_%s_token", sv.Key())] = sv.AddToken
if sv.AddTokenSet {
kv[fmt.Sprintf("tokenizer.ggml.add_%s_token", sv.Key())] = sv.AddToken
}
kv[fmt.Sprintf("tokenizer.ggml.%s_token_id", sv.Key())] = uint32(sv.ID)
if len(sv.IDs) > 0 {
kv[fmt.Sprintf("tokenizer.ggml.%s_token_ids", sv.Key())] = sv.IDs
@@ -200,10 +202,32 @@ type ModelConverter interface {
specialTokenTypes() []string
}
// MultimodalConverter splits checkpoints with embedded vision/projector
// weights into a text model GGUF and a separate projector GGUF.
type MultimodalConverter interface {
ModelConverter
TextKV(*Tokenizer) KV
TextTensors([]Tensor, *Tokenizer) []*ggml.Tensor
ProjectorKV(*Tokenizer) KV
ProjectorTensors([]Tensor) []*ggml.Tensor
}
type moreParser interface {
parseMore(fs.FS) error
}
type extraTensorParser interface {
extraTensors(fs.FS) ([]Tensor, error)
}
type tokenizerAdjuster interface {
adjustTokenizer(*Tokenizer)
}
type tokenizerAwareTensorConverter interface {
TensorsWithTokenizer([]Tensor, *Tokenizer) []*ggml.Tensor
}
type AdapterConverter interface {
// KV maps parameters to LLM key-values
KV(ofs.Config) KV
@@ -288,6 +312,8 @@ func LoadModelMetadata(fsys fs.FS) (ModelKV, *Tokenizer, error) {
conv = &gemma2Model{}
case "Gemma3ForCausalLM", "Gemma3ForConditionalGeneration":
conv = &gemma3Model{Architecture: p.Architectures[0]}
case "Gemma3TextModel":
conv = &embeddingGemmaModel{}
case "Gemma3nForConditionalGeneration":
conv = &gemma3nModel{}
case "Gemma4ForCausalLM", "Gemma4ForConditionalGeneration":
@@ -348,6 +374,9 @@ func LoadModelMetadata(fsys fs.FS) (ModelKV, *Tokenizer, error) {
if err != nil {
return nil, nil, err
}
if ta, ok := conv.(tokenizerAdjuster); ok {
ta.adjustTokenizer(t)
}
vocabSize := int(cmp.Or(p.VocabSize, p.TextModel.VocabSize))
@@ -375,7 +404,7 @@ func LoadModelMetadata(fsys fs.FS) (ModelKV, *Tokenizer, error) {
// and files it finds in the input path.
// Supported input model formats include safetensors.
// Supported input tokenizers files include tokenizer.json (preferred) and tokenizer.model.
func ConvertModel(fsys fs.FS, f *os.File) error {
func ConvertModel(fsys fs.FS, f *os.File, projectorFiles ...*os.File) error {
kv, t, err := LoadModelMetadata(fsys)
if err != nil {
return err
@@ -387,7 +416,47 @@ func ConvertModel(fsys fs.FS, f *os.File) error {
return err
}
return writeFile(f, conv.KV(t), conv.Tensors(ts))
if tp, ok := conv.(extraTensorParser); ok {
extra, err := tp.extraTensors(fsys)
if err != nil {
return err
}
ts = append(ts, extra...)
}
if err := ensureUniqueTensorNames(ts); err != nil {
return err
}
if mc, ok := conv.(MultimodalConverter); ok && len(projectorFiles) > 0 && projectorFiles[0] != nil {
projectorTensors := mc.ProjectorTensors(ts)
if len(projectorTensors) > 0 {
if err := writeFile(f, mc.TextKV(t), mc.TextTensors(ts, t)); err != nil {
return err
}
return writeFile(projectorFiles[0], mc.ProjectorKV(t), projectorTensors)
}
}
var tensors []*ggml.Tensor
if tc, ok := conv.(tokenizerAwareTensorConverter); ok {
tensors = tc.TensorsWithTokenizer(ts, t)
} else {
tensors = conv.Tensors(ts)
}
return writeFile(f, conv.KV(t), tensors)
}
func ensureUniqueTensorNames(ts []Tensor) error {
names := make(map[string]struct{}, len(ts))
for _, t := range ts {
if _, ok := names[t.Name()]; ok {
return fmt.Errorf("duplicate tensor name '%s' was found for this model", t.Name())
}
names[t.Name()] = struct{}{}
}
return nil
}
func writeFile(f *os.File, kv KV, ts []*ggml.Tensor) error {
+280
View File
@@ -0,0 +1,280 @@
package convert
import (
"cmp"
"encoding/json"
"errors"
"fmt"
"io/fs"
"path"
"slices"
"strings"
"github.com/ollama/ollama/fs/ggml"
)
type embeddingGemmaModel struct {
gemmaModel
RopeLocalTheta float32 `json:"rope_local_base_freq"`
RopeTheta float32 `json:"rope_theta"`
SlidingWindow uint32 `json:"sliding_window"`
poolingType uint32
denseModules []embeddingGemmaDenseModule
}
type embeddingGemmaDenseModule struct {
path string
tensorName string
in, out uint32
}
var (
_ ModelConverter = (*embeddingGemmaModel)(nil)
_ moreParser = (*embeddingGemmaModel)(nil)
_ extraTensorParser = (*embeddingGemmaModel)(nil)
_ tokenizerAdjuster = (*embeddingGemmaModel)(nil)
)
func (m *embeddingGemmaModel) KV(t *Tokenizer) KV {
kv := m.ModelParameters.KV(t)
kv["general.architecture"] = "gemma-embedding"
kv["gemma-embedding.context_length"] = cmp.Or(m.MaxPositionEmbeddings, uint32(2048))
kv["gemma-embedding.embedding_length"] = m.HiddenSize
kv["gemma-embedding.block_count"] = m.HiddenLayers
kv["gemma-embedding.feed_forward_length"] = m.IntermediateSize
kv["gemma-embedding.attention.head_count"] = m.NumAttentionHeads
kv["gemma-embedding.attention.head_count_kv"] = m.NumKeyValueHeads
kv["gemma-embedding.attention.layer_norm_rms_epsilon"] = cmp.Or(m.RMSNormEPS, float32(1e-6))
kv["gemma-embedding.attention.key_length"] = m.HeadDim
kv["gemma-embedding.attention.value_length"] = m.HeadDim
kv["gemma-embedding.attention.sliding_window"] = m.SlidingWindow
kv["gemma-embedding.rope.freq_base"] = cmp.Or(m.RopeTheta, float32(1000000.0))
kv["gemma-embedding.rope.freq_base_swa"] = cmp.Or(m.RopeLocalTheta, float32(10000.0))
kv["gemma-embedding.pooling_type"] = cmp.Or(m.poolingType, uint32(1))
for _, dense := range m.denseModules {
kv["gemma-embedding."+dense.tensorName+"_feat_in"] = dense.in
kv["gemma-embedding."+dense.tensorName+"_feat_out"] = dense.out
}
return kv
}
func (m *embeddingGemmaModel) parseMore(fsys fs.FS) error {
bts, err := fs.ReadFile(fsys, "modules.json")
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return errors.New("embeddinggemma requires sentence-transformers modules.json")
}
return err
}
var modules []struct {
Type string `json:"type"`
Path string `json:"path"`
}
if err := json.Unmarshal(bts, &modules); err != nil {
return err
}
m.poolingType = 1
m.denseModules = nil
for _, module := range modules {
switch module.Type {
case "sentence_transformers.models.Pooling":
poolingType, err := embeddingGemmaPoolingType(fsys, module.Path)
if err != nil {
return err
}
if poolingType != 0 {
m.poolingType = poolingType
}
case "sentence_transformers.models.Dense":
dense, ok, err := embeddingGemmaDenseModuleConfig(fsys, module.Path)
if err != nil {
return err
}
if ok {
m.denseModules = append(m.denseModules, dense)
}
}
}
slices.SortFunc(m.denseModules, func(a, b embeddingGemmaDenseModule) int {
return strings.Compare(a.tensorName, b.tensorName)
})
if len(m.denseModules) != 2 ||
m.denseModules[0].tensorName != "dense_2" ||
m.denseModules[1].tensorName != "dense_3" {
return errors.New("embeddinggemma requires sentence-transformers 2_Dense and 3_Dense modules")
}
return nil
}
func (m *embeddingGemmaModel) adjustTokenizer(t *Tokenizer) {
n := int(m.VocabSize)
if n == 0 || len(t.Vocabulary.Tokens) <= n {
return
}
t.Vocabulary.Tokens = t.Vocabulary.Tokens[:n]
if len(t.Vocabulary.Scores) > n {
t.Vocabulary.Scores = t.Vocabulary.Scores[:n]
}
if len(t.Vocabulary.Types) > n {
t.Vocabulary.Types = t.Vocabulary.Types[:n]
}
}
func embeddingGemmaPoolingType(fsys fs.FS, modulePath string) (uint32, error) {
if modulePath == "" {
return 0, nil
}
bts, err := fs.ReadFile(fsys, path.Join(modulePath, "config.json"))
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return 0, nil
}
return 0, err
}
var cfg struct {
PoolingModeMeanTokens bool `json:"pooling_mode_mean_tokens"`
PoolingModeCLSToken bool `json:"pooling_mode_cls_token"`
}
if err := json.Unmarshal(bts, &cfg); err != nil {
return 0, err
}
switch {
case cfg.PoolingModeMeanTokens:
return 1, nil
case cfg.PoolingModeCLSToken:
return 2, nil
default:
return 0, nil
}
}
func embeddingGemmaDenseModuleConfig(fsys fs.FS, modulePath string) (embeddingGemmaDenseModule, bool, error) {
tensorName, ok := embeddingGemmaDenseTensorName(modulePath)
if !ok {
return embeddingGemmaDenseModule{}, false, nil
}
weightsPath := path.Join(modulePath, "model.safetensors")
if _, err := fs.Stat(fsys, weightsPath); err != nil {
if errors.Is(err, fs.ErrNotExist) {
return embeddingGemmaDenseModule{}, false, nil
}
return embeddingGemmaDenseModule{}, false, err
}
bts, err := fs.ReadFile(fsys, path.Join(modulePath, "config.json"))
if err != nil {
return embeddingGemmaDenseModule{}, false, err
}
var cfg struct {
InFeatures uint32 `json:"in_features"`
OutFeatures uint32 `json:"out_features"`
Bias bool `json:"bias"`
}
if err := json.Unmarshal(bts, &cfg); err != nil {
return embeddingGemmaDenseModule{}, false, err
}
if cfg.InFeatures == 0 || cfg.OutFeatures == 0 {
return embeddingGemmaDenseModule{}, false, errors.New("embeddinggemma dense layer config missing in/out features")
}
if cfg.Bias {
return embeddingGemmaDenseModule{}, false, fmt.Errorf("embeddinggemma dense layer %s has unsupported bias", modulePath)
}
return embeddingGemmaDenseModule{
path: weightsPath,
tensorName: tensorName,
in: cfg.InFeatures,
out: cfg.OutFeatures,
}, true, nil
}
func embeddingGemmaDenseTensorName(modulePath string) (string, bool) {
switch modulePath {
case "2_Dense":
return "dense_2", true
case "3_Dense":
return "dense_3", true
default:
return "", false
}
}
func (m *embeddingGemmaModel) extraTensors(fsys fs.FS) ([]Tensor, error) {
var extra []Tensor
for _, dense := range m.denseModules {
ts, err := parseSafetensors(fsys, strings.NewReplacer("linear.", dense.tensorName+"."), dense.path)
if err != nil {
return nil, err
}
foundWeight := false
for _, t := range ts {
if t.Name() == dense.tensorName+".weight" {
extra = append(extra, t)
foundWeight = true
}
}
if !foundWeight {
return nil, fmt.Errorf("embeddinggemma dense module %s missing linear.weight", dense.path)
}
}
return extra, nil
}
func (m *embeddingGemmaModel) Tensors(ts []Tensor) []*ggml.Tensor {
out := make([]*ggml.Tensor, 0, len(ts))
for _, t := range ts {
name := t.Name()
if name == "norm.weight" {
name = "output_norm.weight"
}
if strings.HasSuffix(name, "_norm.weight") {
t.SetRepacker(m.addOne)
}
out = append(out, &ggml.Tensor{
Name: name,
Kind: t.Kind(),
Shape: t.Shape(),
WriterTo: t,
})
}
return out
}
func (m *embeddingGemmaModel) Replacements() []string {
return []string{
"embed_tokens.", "token_embd.",
"layers.", "blk.",
"input_layernorm", "attn_norm",
"self_attn.q_proj", "attn_q",
"self_attn.q_norm", "attn_q_norm",
"self_attn.k_proj", "attn_k",
"self_attn.k_norm", "attn_k_norm",
"self_attn.v_proj", "attn_v",
"self_attn.o_proj", "attn_output",
"mlp.gate_proj", "ffn_gate",
"mlp.down_proj", "ffn_down",
"mlp.up_proj", "ffn_up",
"post_attention_layernorm", "post_attention_norm",
"pre_feedforward_layernorm", "ffn_norm",
"post_feedforward_layernorm", "post_ffw_norm",
}
}
+229
View File
@@ -0,0 +1,229 @@
package convert
import (
"bytes"
"encoding/binary"
"encoding/json"
"io"
"math"
"os"
"path/filepath"
"slices"
"testing"
"github.com/ollama/ollama/fs/ggml"
)
func TestConvertEmbeddingGemmaSentenceTransformers(t *testing.T) {
tempDir := t.TempDir()
writeJSONFile(t, filepath.Join(tempDir, "config.json"), map[string]any{
"architectures": []string{"Gemma3TextModel"},
"vocab_size": uint32(4),
"max_position_embeddings": uint32(2048),
"hidden_size": uint32(8),
"num_hidden_layers": uint32(1),
"intermediate_size": uint32(12),
"num_attention_heads": uint32(1),
"num_key_value_heads": uint32(1),
"head_dim": uint32(8),
"rms_norm_eps": float32(1e-6),
"rope_theta": float32(1000000),
"rope_local_base_freq": float32(10000),
"sliding_window": uint32(512),
"use_bidirectional_attention": true,
})
writeJSONFile(t, filepath.Join(tempDir, "tokenizer.json"), map[string]any{
"model": map[string]any{
"vocab": map[string]int{
"<pad>": 0,
"<eos>": 1,
"<bos>": 2,
"<unk>": 3,
},
},
"added_tokens": []map[string]any{
{"id": 4, "content": "<image_soft_token>", "special": true},
},
})
writeJSONFile(t, filepath.Join(tempDir, "modules.json"), []map[string]string{
{"type": "sentence_transformers.models.Transformer", "path": ""},
{"type": "sentence_transformers.models.Pooling", "path": "1_Pooling"},
{"type": "sentence_transformers.models.Dense", "path": "2_Dense"},
{"type": "sentence_transformers.models.Dense", "path": "3_Dense"},
{"type": "sentence_transformers.models.Normalize", "path": "4_Normalize"},
})
writeJSONFile(t, filepath.Join(tempDir, "1_Pooling", "config.json"), map[string]any{
"pooling_mode_mean_tokens": true,
})
writeJSONFile(t, filepath.Join(tempDir, "2_Dense", "config.json"), map[string]any{
"in_features": uint32(8),
"out_features": uint32(16),
"bias": false,
})
writeJSONFile(t, filepath.Join(tempDir, "3_Dense", "config.json"), map[string]any{
"in_features": uint32(16),
"out_features": uint32(8),
"bias": false,
})
writeSafetensorsFile(t, filepath.Join(tempDir, "model.safetensors"), []safetensorFixtureTensor{
{name: "embed_tokens.weight", shape: []int{4, 8}},
{name: "norm.weight", shape: []int{8}},
{name: "layers.0.input_layernorm.weight", shape: []int{8}},
{name: "layers.0.self_attn.q_proj.weight", shape: []int{8, 8}},
})
writeSafetensorsFile(t, filepath.Join(tempDir, "2_Dense", "model.safetensors"), []safetensorFixtureTensor{
{name: "linear.weight", shape: []int{16, 8}},
})
writeSafetensorsFile(t, filepath.Join(tempDir, "3_Dense", "model.safetensors"), []safetensorFixtureTensor{
{name: "linear.weight", shape: []int{8, 16}},
})
f, kv, tensors := convertFull(t, os.DirFS(tempDir))
defer f.Close()
if got := kv.Architecture(); got != "gemma-embedding" {
t.Fatalf("architecture = %q, want gemma-embedding", got)
}
for key, want := range map[string]uint32{
"dense_2_feat_in": 8,
"dense_2_feat_out": 16,
"dense_3_feat_in": 16,
"dense_3_feat_out": 8,
"pooling_type": 1,
"attention.sliding_window": 512,
} {
if got := kv.Uint(key); got != want {
t.Errorf("%s = %d, want %d", key, got, want)
}
}
if got := kv.Float("rope.freq_base_swa"); got != 10000 {
t.Errorf("rope.freq_base_swa = %v, want 10000", got)
}
if got := kv.Strings("tokenizer.ggml.tokens"); len(got) != 4 {
t.Errorf("token count = %d, want 4", len(got))
}
names := tensorNames(tensors)
for _, name := range []string{
"token_embd.weight",
"output_norm.weight",
"blk.0.attn_norm.weight",
"blk.0.attn_q.weight",
"dense_2.weight",
"dense_3.weight",
} {
if !slices.Contains(names, name) {
t.Errorf("missing tensor %s", name)
}
}
assertF32TensorValues(t, f, tensors, "output_norm.weight", 1)
assertF32TensorValues(t, f, tensors, "blk.0.attn_norm.weight", 1)
}
type safetensorFixtureTensor struct {
name string
shape []int
}
func writeJSONFile(t *testing.T, path string, value any) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
bts, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, bts, 0o644); err != nil {
t.Fatal(err)
}
}
func writeSafetensorsFile(t *testing.T, path string, tensors []safetensorFixtureTensor) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
offset := 0
metadata := map[string]*tensorData{}
for _, tensor := range tensors {
size := 4
for _, dim := range tensor.shape {
size *= dim
}
metadata[tensor.name] = &tensorData{
Offsets: []int{offset, offset + size},
Type: "F32",
Shape: tensor.shape,
}
offset += size
}
header, err := json.Marshal(metadata)
if err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
if err := binary.Write(&buf, binary.LittleEndian, int64(len(header))); err != nil {
t.Fatal(err)
}
if _, err := buf.Write(header); err != nil {
t.Fatal(err)
}
if _, err := buf.Write(make([]byte, offset)); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
t.Fatal(err)
}
}
func tensorNames(tensors ggml.Tensors) []string {
names := make([]string, 0, len(tensors.Items()))
for _, tensor := range tensors.Items() {
names = append(names, tensor.Name)
}
return names
}
func assertF32TensorValues(t *testing.T, f *os.File, tensors ggml.Tensors, name string, want float32) {
t.Helper()
var tensor *ggml.Tensor
for _, item := range tensors.Items() {
if item.Name == name {
tensor = item
break
}
}
if tensor == nil {
t.Fatalf("missing tensor %s", name)
}
if tensor.Kind != uint32(ggml.TensorTypeF32) {
t.Fatalf("%s kind = %d, want F32", name, tensor.Kind)
}
bts := make([]byte, tensor.Size())
reader := io.NewSectionReader(f, int64(tensors.Offset+tensor.Offset), int64(tensor.Size()))
if _, err := io.ReadFull(reader, bts); err != nil {
t.Fatal(err)
}
for i := 0; i < len(bts); i += 4 {
if got := math.Float32frombits(binary.LittleEndian.Uint32(bts[i:])); got != want {
t.Fatalf("%s[%d] = %v, want %v", name, i/4, got, want)
}
}
}
+43
View File
@@ -2,7 +2,11 @@ package convert
import (
"cmp"
"fmt"
"slices"
"strings"
"github.com/ollama/ollama/fs/ggml"
)
type gemma3Model struct {
@@ -178,3 +182,42 @@ func (p *gemma3Model) Replacements() []string {
"multi_modal_projector", "mm",
}
}
func (p *gemma3Model) TensorsWithTokenizer(ts []Tensor, t *Tokenizer) []*ggml.Tensor {
vocabSize := uint64(0)
if t != nil && t.Vocabulary != nil {
vocabSize = uint64(len(t.Vocabulary.Tokens))
}
var out []*ggml.Tensor
for _, tensor := range ts {
name := tensor.Name()
gt := &ggml.Tensor{
Name: name,
Kind: tensor.Kind(),
Shape: tensor.Shape(),
WriterTo: tensor,
}
if !strings.HasPrefix(name, "v.") && strings.HasSuffix(name, "_norm.weight") {
tensor.SetRepacker(p.addOne)
}
if vocabSize > 0 && name == "token_embd.weight" && len(gt.Shape) >= 2 && gt.Shape[0] > vocabSize {
gt.Shape = slices.Clone(gt.Shape)
embdDim := gt.Shape[1]
gt.Shape[0] = vocabSize
tensor.SetRepacker(func(_ string, data []float32, _ []uint64) ([]float32, error) {
n := vocabSize * embdDim
if uint64(len(data)) < n {
return nil, fmt.Errorf("gemma3 token_embd.weight has %d values, need %d", len(data), n)
}
return data[:n], nil
})
}
out = append(out, gt)
}
return out
}
+34
View File
@@ -0,0 +1,34 @@
package convert
import (
"slices"
"testing"
)
func TestGemma3TensorsWithTokenizerTruncatesPaddedEmbedding(t *testing.T) {
p := gemma3Model{}
embedding := &fakeTensor{
name: "token_embd.weight",
shape: []uint64{5, 2},
data: []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
}
out := p.TensorsWithTokenizer([]Tensor{embedding}, &Tokenizer{
Vocabulary: &Vocabulary{Tokens: []string{"a", "b", "<image>"}},
})
if len(out) != 1 {
t.Fatalf("expected 1 tensor, got %d", len(out))
}
if got, want := out[0].Shape, []uint64{3, 2}; !slices.Equal(got, want) {
t.Fatalf("token_embd.weight shape = %v, want %v", got, want)
}
got, err := embedding.repacker(embedding.name, embedding.data, embedding.shape)
if err != nil {
t.Fatalf("unexpected repacker error: %v", err)
}
if want := embedding.data[:6]; !slices.Equal(got, want) {
t.Fatalf("truncated embedding = %v, want %v", got, want)
}
}
+51 -21
View File
@@ -1,6 +1,8 @@
package convert
import (
"encoding/json"
"fmt"
"slices"
"strings"
@@ -14,30 +16,58 @@ type gemma3nModel struct {
ModelParameters
TextModel struct {
ActivationSparsityPattern []float32 `json:"activation_sparsity_pattern"`
AltupActiveIdx uint32 `json:"altup_active_idx"`
AltupCoefClip float32 `json:"altup_coef_clip"`
AltupCorrectScale bool `json:"altup_correct_scale"`
AltupLRMultiplier float32 `json:"altup_lr_multiplier"`
AltupNumInputs uint32 `json:"altup_num_inputs"`
HeadDim uint32 `json:"head_dim"`
HiddenSize uint32 `json:"hidden_size"`
HiddenSizePerLayerInput uint32 `json:"hidden_size_per_layer_input"`
IntermediateSize uint32 `json:"intermediate_size"`
MaxPositionEmbeddings uint32 `json:"max_position_embeddings"`
NumAttentionHeads uint32 `json:"num_attention_heads"`
NumHiddenLayers uint32 `json:"num_hidden_layers"`
NumKeyValueHeads uint32 `json:"num_key_value_heads"`
NumKVSharedLayers uint32 `json:"num_kv_shared_layers"`
RMSNormEPS float32 `json:"rms_norm_eps"`
RopeLocalBaseFreq float32 `json:"rope_local_base_freq"`
RopeTheta float32 `json:"rope_theta"`
SlidingWindow uint32 `json:"sliding_window"`
LayerTypes []string `json:"layer_types"`
ActivationSparsityPattern []float32 `json:"activation_sparsity_pattern"`
AltupActiveIdx uint32 `json:"altup_active_idx"`
AltupCoefClip float32 `json:"altup_coef_clip"`
AltupCorrectScale bool `json:"altup_correct_scale"`
AltupLRMultiplier float32 `json:"altup_lr_multiplier"`
AltupNumInputs uint32 `json:"altup_num_inputs"`
HeadDim uint32 `json:"head_dim"`
HiddenSize uint32 `json:"hidden_size"`
HiddenSizePerLayerInput uint32 `json:"hidden_size_per_layer_input"`
IntermediateSize gemma3nIntermediateSize `json:"intermediate_size"`
MaxPositionEmbeddings uint32 `json:"max_position_embeddings"`
NumAttentionHeads uint32 `json:"num_attention_heads"`
NumHiddenLayers uint32 `json:"num_hidden_layers"`
NumKeyValueHeads uint32 `json:"num_key_value_heads"`
NumKVSharedLayers uint32 `json:"num_kv_shared_layers"`
RMSNormEPS float32 `json:"rms_norm_eps"`
RopeLocalBaseFreq float32 `json:"rope_local_base_freq"`
RopeTheta float32 `json:"rope_theta"`
SlidingWindow uint32 `json:"sliding_window"`
LayerTypes []string `json:"layer_types"`
} `json:"text_config"`
VisionModel struct{} `json:"vision_config"`
}
type gemma3nIntermediateSize uint32
func (s *gemma3nIntermediateSize) UnmarshalJSON(data []byte) error {
var scalar uint32
if err := json.Unmarshal(data, &scalar); err == nil {
*s = gemma3nIntermediateSize(scalar)
return nil
}
var values []uint32
if err := json.Unmarshal(data, &values); err != nil {
return err
}
if len(values) == 0 {
return fmt.Errorf("intermediate_size must not be empty")
}
first := values[0]
for _, v := range values[1:] {
if v != first {
return fmt.Errorf("intermediate_size values must match")
}
}
*s = gemma3nIntermediateSize(first)
return nil
}
func (m *gemma3nModel) KV(t *Tokenizer) KV {
kv := m.ModelParameters.KV(t)
kv["general.architecture"] = "gemma3n"
@@ -69,7 +99,7 @@ func (m *gemma3nModel) KV(t *Tokenizer) KV {
kv["gemma3n.context_length"] = m.TextModel.MaxPositionEmbeddings
kv["gemma3n.embedding_length_per_layer_input"] = m.TextModel.HiddenSizePerLayerInput
kv["gemma3n.embedding_length"] = m.TextModel.HiddenSize
kv["gemma3n.feed_forward_length"] = m.TextModel.IntermediateSize
kv["gemma3n.feed_forward_length"] = uint32(m.TextModel.IntermediateSize)
kv["gemma3n.head_dim"] = m.TextModel.HeadDim
kv["gemma3n.rope.freq_base_local"] = m.TextModel.RopeLocalBaseFreq
kv["gemma3n.rope.freq_base"] = m.TextModel.RopeTheta
+55
View File
@@ -0,0 +1,55 @@
package convert
import (
"encoding/json"
"testing"
)
func TestGemma3nIntermediateSize(t *testing.T) {
tests := []struct {
name string
json string
want gemma3nIntermediateSize
wantErr bool
}{
{
name: "scalar",
json: `8192`,
want: 8192,
},
{
name: "uniform array",
json: `[8192,8192,8192]`,
want: 8192,
},
{
name: "mixed array",
json: `[8192,4096]`,
wantErr: true,
},
{
name: "empty array",
json: `[]`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got gemma3nIntermediateSize
err := json.Unmarshal([]byte(tt.json), &got)
if tt.wantErr {
if err == nil {
t.Fatal("expected error")
}
return
}
if err != nil {
t.Fatal(err)
}
if got != tt.want {
t.Fatalf("got %d, want %d", got, tt.want)
}
})
}
}
+49 -25
View File
@@ -39,48 +39,72 @@ type glm4MoeLiteModel struct {
ExpertWeightsScale float32 `json:"routed_scaling_factor"`
LeadingDenseBlockCount uint32 `json:"first_k_dense_replace"`
ExpertGroupCount uint32 `json:"n_group"`
ExpertGroupUsedCount uint32 `json:"topk_group"`
}
func (p *glm4MoeLiteModel) KV(t *Tokenizer) KV {
kv := p.ModelParameters.KV(t)
kv["general.architecture"] = "glm4moelite"
kv["general.architecture"] = "deepseek2"
kv["general.type"] = "model"
kv["glm4moelite.block_count"] = p.HiddenLayers
kv["deepseek2.block_count"] = p.HiddenLayers
numHeads := p.NumAttentionHeads
numKVHeads := p.NumKeyValueHeads
kv["glm4moelite.attention.head_count"] = numHeads
kv["glm4moelite.attention.head_count_kv"] = numKVHeads
kv["glm4moelite.attention.key_length"] = p.QKNopeHeadDim + p.QKRopeHeadDim
kv["glm4moelite.attention.kv_lora_rank"] = p.KVLoraRank
kv["glm4moelite.attention.layer_norm_rms_epsilon"] = p.RMSNormEPS
kv["glm4moelite.attention.q_lora_rank"] = p.QLoraRank
kv["glm4moelite.attention.value_length"] = p.VHeadDim
kv["glm4moelite.context_length"] = p.MaxPositionEmbeddings
kv["glm4moelite.embedding_length"] = p.HiddenSize
kv["glm4moelite.expert_count"] = p.ExpertCount
kv["glm4moelite.expert_feed_forward_length"] = p.ExpertIntermediateSize
kv["glm4moelite.expert_shared_count"] = p.ExpertSharedCount
kv["deepseek2.attention.head_count"] = numHeads
kv["deepseek2.attention.head_count_kv"] = uint32(1)
kv["deepseek2.attention.key_length"] = p.KVLoraRank + p.QKRopeHeadDim
kv["deepseek2.attention.kv_lora_rank"] = p.KVLoraRank
kv["deepseek2.attention.layer_norm_rms_epsilon"] = p.RMSNormEPS
kv["deepseek2.attention.q_lora_rank"] = p.QLoraRank
kv["deepseek2.attention.value_length"] = p.KVLoraRank
kv["deepseek2.context_length"] = p.MaxPositionEmbeddings
kv["deepseek2.embedding_length"] = p.HiddenSize
kv["deepseek2.expert_count"] = p.ExpertCount
kv["deepseek2.expert_feed_forward_length"] = p.ExpertIntermediateSize
kv["deepseek2.expert_shared_count"] = p.ExpertSharedCount
kv["glm4moelite.expert_gating_func"] = uint32(2)
kv["glm4moelite.expert_used_count"] = p.ExpertUsedCount
kv["glm4moelite.expert_weights_norm"] = p.ExpertWeightsNorm
kv["glm4moelite.expert_weights_scale"] = p.ExpertWeightsScale
kv["glm4moelite.feed_forward_length"] = p.IntermediateSize
kv["glm4moelite.leading_dense_block_count"] = p.LeadingDenseBlockCount
kv["deepseek2.expert_gating_func"] = uint32(2)
kv["deepseek2.expert_group_count"] = cmp.Or(p.ExpertGroupCount, uint32(1))
kv["deepseek2.expert_group_used_count"] = cmp.Or(p.ExpertGroupUsedCount, uint32(1))
kv["deepseek2.expert_used_count"] = p.ExpertUsedCount
kv["deepseek2.expert_weights_norm"] = p.ExpertWeightsNorm
kv["deepseek2.expert_weights_scale"] = p.ExpertWeightsScale
kv["deepseek2.feed_forward_length"] = p.IntermediateSize
kv["deepseek2.leading_dense_block_count"] = p.LeadingDenseBlockCount
kv["glm4moelite.rope.dimension_count"] = p.QKRopeHeadDim
kv["glm4moelite.rope.freq_base"] = cmp.Or(p.RopeTheta, float32(1000000.0))
kv["deepseek2.rope.dimension_count"] = p.QKRopeHeadDim
kv["deepseek2.rope.freq_base"] = cmp.Or(p.RopeTheta, float32(1000000.0))
kv["glm4moelite.attention.key_length_mla"] = p.KVLoraRank + p.QKRopeHeadDim
kv["glm4moelite.attention.value_length_mla"] = p.KVLoraRank
kv["deepseek2.attention.key_length_mla"] = p.QKNopeHeadDim + p.QKRopeHeadDim
kv["deepseek2.attention.value_length_mla"] = p.VHeadDim
kv["tokenizer.ggml.pre"] = "glm4"
setGLM4MoeLiteExtraEOGFromEOSIDs(kv)
return kv
}
func setGLM4MoeLiteExtraEOGFromEOSIDs(kv KV) {
switch ids := kv["tokenizer.ggml.eos_token_ids"].(type) {
case []int32:
if len(ids) >= 2 && ids[1] >= 0 {
kv["tokenizer.ggml.eot_token_id"] = uint32(ids[1])
}
if len(ids) >= 3 && ids[2] >= 0 {
kv["tokenizer.ggml.eom_token_id"] = uint32(ids[2])
}
case []uint32:
if len(ids) >= 2 {
kv["tokenizer.ggml.eot_token_id"] = ids[1]
}
if len(ids) >= 3 {
kv["tokenizer.ggml.eom_token_id"] = ids[2]
}
}
}
func (p *glm4MoeLiteModel) Replacements() []string {
return []string{
"lm_head", "output",
+68
View File
@@ -0,0 +1,68 @@
package convert
import "testing"
func TestGLM4MoeLiteKVUsesLlamaCppMetadata(t *testing.T) {
p := glm4MoeLiteModel{
ModelParameters: ModelParameters{VocabSize: 151552},
MaxPositionEmbeddings: 202752,
HiddenSize: 2048,
HiddenLayers: 47,
IntermediateSize: 10240,
NumAttentionHeads: 20,
NumKeyValueHeads: 20,
RMSNormEPS: 1e-5,
RopeTheta: 1000000,
QKNopeHeadDim: 128,
QKRopeHeadDim: 64,
KVLoraRank: 512,
QLoraRank: 768,
VHeadDim: 128,
ExpertCount: 64,
ExpertSharedCount: 1,
ExpertUsedCount: 4,
ExpertWeightsNorm: true,
ExpertWeightsScale: 1.8,
}
kv := p.KV(&Tokenizer{Vocabulary: &Vocabulary{Model: "gpt2", Tokens: []string{"a"}}})
if got := kv.Architecture(); got != "deepseek2" {
t.Fatalf("architecture = %q, want deepseek2", got)
}
for key, want := range map[string]uint32{
"attention.head_count": 20,
"attention.head_count_kv": 1,
"attention.key_length": 576,
"attention.value_length": 512,
"attention.key_length_mla": 192,
"attention.value_length_mla": 128,
"expert_group_count": 1,
"expert_group_used_count": 1,
"expert_gating_func": 2,
"rope.dimension_count": 64,
} {
if got := kv.Uint(key); got != want {
t.Errorf("%s = %d, want %d", key, got, want)
}
}
if got := kv.String("tokenizer.ggml.pre"); got != "glm4" {
t.Errorf("tokenizer.ggml.pre = %q, want glm4", got)
}
}
func TestGLM4MoeLiteKVPromotesExtraEOSIDs(t *testing.T) {
kv := KV{
"general.architecture": "deepseek2",
"tokenizer.ggml.eos_token_ids": []int32{151329, 151330, 151336},
}
setGLM4MoeLiteExtraEOGFromEOSIDs(kv)
if got := kv.Uint("tokenizer.ggml.eot_token_id"); got != 151330 {
t.Errorf("eot token = %d, want 151330", got)
}
if got := kv.Uint("tokenizer.ggml.eom_token_id"); got != 151336 {
t.Errorf("eom token = %d, want 151336", got)
}
}
+240 -20
View File
@@ -83,6 +83,7 @@ type glmOcrModel struct {
HiddenSize uint32 `json:"hidden_size"`
IntermediateSize uint32 `json:"intermediate_size"`
NumHiddenLayers uint32 `json:"num_hidden_layers"`
NumNextNPredict uint32 `json:"num_nextn_predict_layers"`
NumAttentionHeads uint32 `json:"num_attention_heads"`
NumKeyValueHeads uint32 `json:"num_key_value_heads"`
HeadDim uint32 `json:"head_dim"`
@@ -131,7 +132,7 @@ type glmOcrModel struct {
} `json:"-"`
}
var _ ModelConverter = (*glmOcrModel)(nil)
var _ MultimodalConverter = (*glmOcrModel)(nil)
func (m *glmOcrModel) parseMore(fsys fs.FS) error {
bts, err := fs.ReadFile(fsys, "preprocessor_config.json")
@@ -145,9 +146,14 @@ func (m *glmOcrModel) parseMore(fsys fs.FS) error {
func (m *glmOcrModel) KV(t *Tokenizer) KV {
kv := m.ModelParameters.KV(t)
kv["general.architecture"] = "glmocr"
applyGlmOcrTokenizerKV(kv, t)
// Text model parameters
kv["glmocr.block_count"] = cmp.Or(m.TextConfig.NumHiddenLayers, 16)
numHiddenLayers := cmp.Or(m.TextConfig.NumHiddenLayers, 16)
kv["glmocr.block_count"] = numHiddenLayers + m.TextConfig.NumNextNPredict
if m.TextConfig.NumNextNPredict > 0 {
kv["glmocr.nextn_predict_layers"] = m.TextConfig.NumNextNPredict
}
kv["glmocr.embedding_length"] = cmp.Or(m.TextConfig.HiddenSize, 1536)
kv["glmocr.attention.head_count"] = cmp.Or(m.TextConfig.NumAttentionHeads, 16)
kv["glmocr.attention.head_count_kv"] = cmp.Or(m.TextConfig.NumKeyValueHeads, 8)
@@ -175,8 +181,6 @@ func (m *glmOcrModel) KV(t *Tokenizer) KV {
kv["glmocr.vision.intermediate_size"] = cmp.Or(m.VisionConfig.IntermediateSize, 4096)
kv["glmocr.vision.attention.layer_norm_rms_epsilon"] = cmp.Or(m.VisionConfig.RMSNormEps, 1e-5)
// Preprocessor-derived image settings (min/max pixels and normalization)
// Note: fs.Config.keyValue() auto-prepends architecture prefix, so use full key
if m.Preprocessor.Size.ShortestEdge > 0 {
kv["glmocr.vision.min_pixels"] = m.Preprocessor.Size.ShortestEdge
}
@@ -190,7 +194,6 @@ func (m *glmOcrModel) KV(t *Tokenizer) KV {
kv["glmocr.vision.image_std"] = m.Preprocessor.ImageStd
}
// Special tokens
kv["glmocr.image_token_id"] = m.ImageTokenID
kv["glmocr.image_start_token_id"] = m.ImageStartTokenID
kv["glmocr.image_end_token_id"] = m.ImageEndTokenID
@@ -201,32 +204,249 @@ func (m *glmOcrModel) KV(t *Tokenizer) KV {
return kv
}
func applyGlmOcrTokenizerKV(kv KV, t *Tokenizer) {
kv["tokenizer.ggml.pre"] = "chatglm-bpe"
if id, ok := glmOcrTokenID(t, "<|endoftext|>"); ok {
kv["tokenizer.ggml.bos_token_id"] = uint32(id)
kv["tokenizer.ggml.unknown_token_id"] = uint32(id)
}
if id, ok := glmOcrTokenID(t, "<|user|>"); ok {
kv["tokenizer.ggml.eot_token_id"] = uint32(id)
}
}
func (m *glmOcrModel) TextKV(t *Tokenizer) KV {
kv := m.ModelParameters.KV(t)
kv["general.architecture"] = "glm4"
applyGlmOcrTokenizerKV(kv, t)
numHiddenLayers := cmp.Or(m.TextConfig.NumHiddenLayers, 16)
kv["block_count"] = numHiddenLayers + m.TextConfig.NumNextNPredict
if m.TextConfig.NumNextNPredict > 0 {
kv["nextn_predict_layers"] = m.TextConfig.NumNextNPredict
}
kv["embedding_length"] = cmp.Or(m.TextConfig.HiddenSize, 1536)
kv["attention.head_count"] = cmp.Or(m.TextConfig.NumAttentionHeads, 16)
kv["attention.head_count_kv"] = cmp.Or(m.TextConfig.NumKeyValueHeads, 8)
headDim := cmp.Or(m.TextConfig.HeadDim, m.TextConfig.HiddenSize/m.TextConfig.NumAttentionHeads)
kv["attention.key_length"] = headDim
kv["attention.value_length"] = headDim
kv["feed_forward_length"] = cmp.Or(m.TextConfig.IntermediateSize, 4608)
kv["attention.layer_norm_rms_epsilon"] = cmp.Or(m.TextConfig.RMSNormEps, 1e-5)
kv["context_length"] = cmp.Or(m.TextConfig.MaxPositionEmbed, 131072)
kv["rope.freq_base"] = cmp.Or(m.TextConfig.RopeParameters.RopeTheta, float32(10000))
partialRotaryFactor := cmp.Or(m.TextConfig.RopeParameters.PartialRotaryFactor, m.TextConfig.PartialRotaryFactor, float32(1.0))
kv["rope.dimension_count"] = uint32(float32(headDim) * partialRotaryFactor)
if len(m.TextConfig.RopeParameters.MRopeSection) > 0 {
sections := append([]int32(nil), m.TextConfig.RopeParameters.MRopeSection...)
for len(sections) < 4 {
sections = append(sections, 0)
}
kv["rope.dimension_sections"] = sections
}
return kv
}
func (m *glmOcrModel) ProjectorKV(*Tokenizer) KV {
kv := KV{
"general.architecture": "clip",
"general.type": "mmproj",
"general.file_type": uint32(1),
"general.quantization_version": uint32(2),
"clip.has_vision_encoder": true,
"clip.projector_type": "glm4v",
"clip.use_silu": true,
"clip.vision.block_count": cmp.Or(m.VisionConfig.Depth, 24),
"clip.vision.embedding_length": cmp.Or(m.VisionConfig.HiddenSize, 1024),
"clip.vision.attention.head_count": cmp.Or(m.VisionConfig.NumHeads, 16),
"clip.vision.image_size": cmp.Or(m.VisionConfig.ImageSize, 336),
"clip.vision.patch_size": cmp.Or(m.VisionConfig.PatchSize, m.Preprocessor.PatchSize, 14),
"clip.vision.spatial_merge_size": cmp.Or(m.VisionConfig.SpatialMergeSize, m.Preprocessor.MergeSize, 2),
"clip.vision.temporal_patch_size": cmp.Or(m.VisionConfig.TemporalPatchSize, m.Preprocessor.TemporalPatchSize, 2),
"clip.vision.projection_dim": cmp.Or(m.VisionConfig.OutHiddenSize, 1536),
"clip.vision.out_hidden_size": cmp.Or(m.VisionConfig.OutHiddenSize, 1536),
"clip.vision.feed_forward_length": cmp.Or(m.VisionConfig.IntermediateSize, 4096),
"clip.vision.intermediate_size": cmp.Or(m.VisionConfig.IntermediateSize, 4096),
"clip.vision.attention.layer_norm_epsilon": cmp.Or(m.VisionConfig.RMSNormEps, 1e-5),
"clip.vision.image_token_id": m.ImageTokenID,
"clip.vision.image_start_token_id": m.ImageStartTokenID,
"clip.vision.image_end_token_id": m.ImageEndTokenID,
}
if m.Preprocessor.Size.ShortestEdge > 0 {
kv["clip.vision.min_pixels"] = m.Preprocessor.Size.ShortestEdge
}
if m.Preprocessor.Size.LongestEdge > 0 {
kv["clip.vision.max_pixels"] = m.Preprocessor.Size.LongestEdge
}
if len(m.Preprocessor.ImageMean) == 3 {
kv["clip.vision.image_mean"] = m.Preprocessor.ImageMean
}
if len(m.Preprocessor.ImageStd) == 3 {
kv["clip.vision.image_std"] = m.Preprocessor.ImageStd
}
return kv
}
func glmOcrTokenID(t *Tokenizer, token string) (int, bool) {
if t == nil || t.Vocabulary == nil {
return 0, false
}
for i, candidate := range t.Vocabulary.Tokens {
if candidate == token {
return i, true
}
}
return 0, false
}
func isGlmOcrVisionTensor(name string) bool {
return strings.HasPrefix(name, "v.") || strings.HasPrefix(name, "mm.")
}
func (m *glmOcrModel) TextTensors(ts []Tensor, t *Tokenizer) []*ggml.Tensor {
textOnly := make([]Tensor, 0, len(ts))
for _, tensor := range ts {
if !isGlmOcrVisionTensor(tensor.Name()) {
textOnly = append(textOnly, tensor)
}
}
return m.Tensors(textOnly)
}
func (m *glmOcrModel) ProjectorTensors(ts []Tensor) []*ggml.Tensor {
var out []*ggml.Tensor
for _, t := range ts {
if !isGlmOcrVisionTensor(t.Name()) {
continue
}
name := t.Name()
switch {
case strings.HasSuffix(name, "patch_embd_0.weight"):
name = strings.Replace(name, "patch_embd_0.weight", "patch_embd.weight", 1)
case strings.HasSuffix(name, "patch_embd_1.weight"):
name = strings.Replace(name, "patch_embd_1.weight", "patch_embd.weight.1", 1)
case strings.HasSuffix(name, "patch_embd.weight.0"):
name = strings.Replace(name, "patch_embd.weight.0", "patch_embd.weight", 1)
}
if strings.HasSuffix(name, "patch_embd.weight") {
shape := t.Shape()
if len(shape) == 5 && shape[2] == 2 {
newShape := []uint64{shape[0], shape[1], shape[3], shape[4]}
t0 := t.Clone()
t0.SetRepacker(func(_ string, data []float32, shape []uint64) ([]float32, error) {
dims := make([]int, len(shape))
for i := range shape {
dims[i] = int(shape[i])
}
var tt tensor.Tensor = tensor.New(tensor.WithShape(dims...), tensor.WithBacking(data))
tt, err := tt.Slice(nil, nil, tensor.S(0, 1), nil, nil)
if err != nil {
return nil, err
}
tt = tensor.Materialize(tt)
newDims := []int{int(shape[0]), int(shape[1]), int(shape[3]), int(shape[4])}
if err := tt.Reshape(newDims...); err != nil {
return nil, err
}
if err := tt.Reshape(tt.Shape().TotalSize()); err != nil {
return nil, err
}
return native.VectorF32(tt.(*tensor.Dense))
})
out = append(out, &ggml.Tensor{
Name: strings.Replace(name, "patch_embd.weight", "patch_embd.weight", 1),
Kind: t.Kind(),
Shape: newShape,
WriterTo: t0,
})
t1 := t.Clone()
t1.SetRepacker(func(_ string, data []float32, shape []uint64) ([]float32, error) {
dims := make([]int, len(shape))
for i := range shape {
dims[i] = int(shape[i])
}
var tt tensor.Tensor = tensor.New(tensor.WithShape(dims...), tensor.WithBacking(data))
tt, err := tt.Slice(nil, nil, tensor.S(1, 2), nil, nil)
if err != nil {
return nil, err
}
tt = tensor.Materialize(tt)
newDims := []int{int(shape[0]), int(shape[1]), int(shape[3]), int(shape[4])}
if err := tt.Reshape(newDims...); err != nil {
return nil, err
}
if err := tt.Reshape(tt.Shape().TotalSize()); err != nil {
return nil, err
}
return native.VectorF32(tt.(*tensor.Dense))
})
out = append(out, &ggml.Tensor{
Name: strings.Replace(name, "patch_embd.weight", "patch_embd.weight.1", 1),
Kind: t.Kind(),
Shape: newShape,
WriterTo: t1,
})
continue
}
}
out = append(out, &ggml.Tensor{
Name: name,
Kind: t.Kind(),
Shape: t.Shape(),
WriterTo: t,
})
}
return out
}
func (m *glmOcrModel) Tensors(ts []Tensor) []*ggml.Tensor {
var out []*ggml.Tensor
// Skip layers >= num_hidden_layers (Multi-Token Prediction layers not needed for basic inference)
numLayers := int(cmp.Or(m.TextConfig.NumHiddenLayers, 16))
skipLayer := func(name string) bool {
// Tensor names are already replaced to "blk.N.xxx" format
re := regexp.MustCompile(`^blk\.(\d+)`)
matches := re.FindStringSubmatch(name)
maxLayers := numLayers + int(m.TextConfig.NumNextNPredict)
layerRe := regexp.MustCompile(`^blk\.(\d+)`)
layerIndex := func(name string) (int, bool) {
matches := layerRe.FindStringSubmatch(name)
if matches == nil {
return false
return 0, false
}
blkNum, err := strconv.Atoi(matches[1])
if err != nil {
return false
return 0, false
}
return blkNum >= numLayers
return blkNum, true
}
for _, t := range ts {
name := t.Name()
// Skip next-n prediction layers (layers >= num_hidden_layers)
if skipLayer(name) {
blkNum, hasLayer := layerIndex(name)
if hasLayer && blkNum >= maxLayers {
continue
}
if hasLayer && blkNum >= numLayers {
switch {
case strings.HasSuffix(name, ".embed_tokens.weight"):
name = strings.Replace(name, ".embed_tokens.weight", ".nextn.embed_tokens.weight", 1)
case strings.HasSuffix(name, ".eh_proj.weight"):
name = strings.Replace(name, ".eh_proj.weight", ".nextn.eh_proj.weight", 1)
case strings.HasSuffix(name, ".enorm.weight"):
name = strings.Replace(name, ".enorm.weight", ".nextn.enorm.weight", 1)
case strings.HasSuffix(name, ".hnorm.weight"):
name = strings.Replace(name, ".hnorm.weight", ".nextn.hnorm.weight", 1)
case strings.HasSuffix(name, ".shared_head.head.weight"):
name = strings.Replace(name, ".shared_head.head.weight", ".nextn.shared_head_head.weight", 1)
case strings.HasSuffix(name, ".shared_head.norm.weight"):
name = strings.Replace(name, ".shared_head.norm.weight", ".nextn.shared_head_norm.weight", 1)
}
}
// Split ffn_gate_up into separate gate and up projections
if strings.Contains(name, "ffn_gate_up") {
@@ -440,16 +660,16 @@ func (m *glmOcrModel) Replacements() []string {
"self_attn.q_proj", "attn_q",
"self_attn.k_proj", "attn_k",
"self_attn.v_proj", "attn_v",
"self_attn.o_proj", "attn_out",
"self_attn.o_proj", "attn_output",
// Language model norms
"input_layernorm", "attn_norm",
"post_attention_layernorm", "ffn_norm",
"post_self_attn_layernorm", "post_attn_norm",
"post_mlp_layernorm", "post_ffn_norm",
"post_self_attn_layernorm", "post_attention_norm",
"post_mlp_layernorm", "post_ffw_norm",
// Language model MLP (remove mlp. prefix so ffn_* names work)
"mlp.gate_up_proj", "ffn_gate_up",
// Language model MLP
"mlp.gate_up_proj", "ffn_up",
"mlp.down_proj", "ffn_down",
}
}
+36 -23
View File
@@ -30,7 +30,11 @@ type gptossModel struct {
RopeTheta float32 `json:"rope_theta"`
RopeScalingFactor float32 `json:"rope_scaling_factor"`
RopeScaling struct {
Factor float32 `json:"factor"`
Type string `json:"rope_type"`
Factor float32 `json:"factor"`
OriginalMaxPositionEmbeddings uint32 `json:"original_max_position_embeddings"`
BetaFast float32 `json:"beta_fast"`
BetaSlow float32 `json:"beta_slow"`
} `json:"rope_scaling"`
SlidingWindow uint32 `json:"sliding_window"`
}
@@ -39,23 +43,32 @@ var _ ModelConverter = (*gptossModel)(nil)
func (m *gptossModel) KV(t *Tokenizer) KV {
kv := m.ModelParameters.KV(t)
kv["general.architecture"] = "gptoss"
kv["general.architecture"] = "gpt-oss"
kv["general.file_type"] = uint32(4)
kv["gptoss.context_length"] = cmp.Or(m.MaxPositionEmbeddings, uint32(m.RopeScalingFactor*float32(m.InitialContextLength)))
kv["gptoss.block_count"] = m.HiddenLayers
kv["gptoss.embedding_length"] = m.HiddenSize
kv["gptoss.feed_forward_length"] = m.IntermediateSize
kv["gptoss.expert_count"] = cmp.Or(m.Experts, m.LocalExperts)
kv["gptoss.expert_used_count"] = m.ExpertsPerToken
kv["gptoss.attention.head_count"] = m.AttentionHeads
kv["gptoss.attention.head_count_kv"] = m.KeyValueHeads
kv["gptoss.attention.key_length"] = m.HeadDim
kv["gptoss.attention.value_length"] = m.HeadDim
kv["gptoss.attention.layer_norm_rms_epsilon"] = cmp.Or(m.RMSNormEpsilon, 1e-5)
kv["gptoss.attention.sliding_window"] = m.SlidingWindow
kv["gptoss.rope.freq_base"] = m.RopeTheta
kv["gptoss.rope.scaling.factor"] = cmp.Or(m.RopeScalingFactor, m.RopeScaling.Factor)
kv["gptoss.rope.scaling.original_context_length"] = m.InitialContextLength
kv["gpt-oss.context_length"] = cmp.Or(m.MaxPositionEmbeddings, uint32(m.RopeScalingFactor*float32(m.InitialContextLength)))
kv["gpt-oss.block_count"] = m.HiddenLayers
kv["gpt-oss.embedding_length"] = m.HiddenSize
kv["gpt-oss.feed_forward_length"] = m.IntermediateSize
kv["gpt-oss.expert_feed_forward_length"] = m.IntermediateSize
kv["gpt-oss.expert_count"] = cmp.Or(m.Experts, m.LocalExperts)
kv["gpt-oss.expert_used_count"] = m.ExpertsPerToken
kv["gpt-oss.attention.head_count"] = m.AttentionHeads
kv["gpt-oss.attention.head_count_kv"] = m.KeyValueHeads
kv["gpt-oss.attention.key_length"] = m.HeadDim
kv["gpt-oss.attention.value_length"] = m.HeadDim
kv["gpt-oss.attention.layer_norm_rms_epsilon"] = cmp.Or(m.RMSNormEpsilon, 1e-5)
kv["gpt-oss.attention.sliding_window"] = m.SlidingWindow
kv["gpt-oss.rope.freq_base"] = m.RopeTheta
kv["gpt-oss.rope.scaling.type"] = cmp.Or(m.RopeScaling.Type, "yarn")
kv["gpt-oss.rope.scaling.factor"] = cmp.Or(m.RopeScalingFactor, m.RopeScaling.Factor)
kv["gpt-oss.rope.scaling.original_context_length"] = cmp.Or(m.RopeScaling.OriginalMaxPositionEmbeddings, m.InitialContextLength)
if m.RopeScaling.BetaFast != 0 {
kv["gpt-oss.rope.scaling.yarn_beta_fast"] = m.RopeScaling.BetaFast
}
if m.RopeScaling.BetaSlow != 0 {
kv["gpt-oss.rope.scaling.yarn_beta_slow"] = m.RopeScaling.BetaSlow
}
kv["tokenizer.ggml.pre"] = "gpt-4o"
kv["tokenizer.ggml.bos_token_id"] = uint32(199998) // <|startoftext|>
kv["tokenizer.ggml.add_bos_token"] = false
kv["tokenizer.ggml.eos_token_id"] = uint32(199999) // <|endoftext|>
@@ -152,9 +165,9 @@ func (m *gptossModel) Replacements() []string {
"self_attn.q_proj", "attn_q",
"self_attn.k_proj", "attn_k",
"self_attn.v_proj", "attn_v",
"self_attn.o_proj", "attn_out",
"self_attn.sinks", "attn_sinks",
"post_attention_layernorm", "ffn_norm",
"self_attn.o_proj", "attn_output",
"self_attn.sinks", "attn_sinks.weight",
"post_attention_layernorm", "post_attention_norm",
"mlp.router", "ffn_gate_inp",
"mlp.experts.gate_up_proj_", "ffn_gate_up_exps.",
"mlp.experts.down_proj_", "ffn_down_exps.",
@@ -169,9 +182,9 @@ func (m *gptossModel) Replacements() []string {
"block", "blk",
"attn.norm", "attn_norm",
"attn.qkv", "attn_qkv",
"attn.sinks", "attn_sinks",
"attn.out", "attn_out",
"mlp.norm", "ffn_norm",
"attn.sinks", "attn_sinks.weight",
"attn.out", "attn_output",
"mlp.norm", "post_attention_norm",
"mlp.gate", "ffn_gate_inp",
"mlp.mlp1_", "ffn_gate_up_exps.",
"mlp.mlp2_", "ffn_down_exps.",
+73
View File
@@ -0,0 +1,73 @@
package convert
import (
"strings"
"testing"
)
func TestGptOssCreatesLlamaCppMetadataAndNames(t *testing.T) {
m := &gptossModel{
HiddenLayers: 24,
MaxPositionEmbeddings: 131072,
HiddenSize: 2880,
IntermediateSize: 2880,
AttentionHeads: 64,
KeyValueHeads: 8,
HeadDim: 64,
LocalExperts: 32,
ExpertsPerToken: 4,
RopeTheta: 150000,
InitialContextLength: 4096,
SlidingWindow: 128,
}
m.RopeScaling.Type = "yarn"
m.RopeScaling.Factor = 32
m.RopeScaling.OriginalMaxPositionEmbeddings = 4096
m.RopeScaling.BetaFast = 32
m.RopeScaling.BetaSlow = 1
kv := m.KV(&Tokenizer{Vocabulary: &Vocabulary{Model: "gpt2"}, Pre: "default"})
for k, want := range map[string]any{
"general.architecture": "gpt-oss",
"tokenizer.ggml.pre": "gpt-4o",
"gpt-oss.context_length": uint32(131072),
"gpt-oss.expert_feed_forward_length": uint32(2880),
"gpt-oss.rope.scaling.type": "yarn",
"gpt-oss.rope.scaling.factor": float32(32),
"gpt-oss.rope.scaling.original_context_length": uint32(4096),
"gpt-oss.rope.scaling.yarn_beta_fast": float32(32),
"gpt-oss.rope.scaling.yarn_beta_slow": float32(1),
} {
if got := kv[k]; got != want {
t.Fatalf("%s = %v (%T), want %v (%T)", k, got, got, want, want)
}
}
if _, ok := kv["gptoss.context_length"]; ok {
t.Fatal("unexpected Ollama-format gptoss metadata")
}
replacer := strings.NewReplacer(m.Replacements()...)
for name, want := range map[string]string{
"model.layers.0.self_attn.o_proj.weight": "blk.0.attn_output.weight",
"model.layers.0.self_attn.sinks": "blk.0.attn_sinks.weight",
"model.layers.0.post_attention_layernorm.weight": "blk.0.post_attention_norm.weight",
"model.layers.0.mlp.experts.gate_up_proj_blocks": "blk.0.ffn_gate_up_exps.blocks",
"model.layers.0.mlp.experts.down_proj_scales": "blk.0.ffn_down_exps.scales",
} {
if got := replacer.Replace(name); got != want {
t.Fatalf("Replace(%q) = %q, want %q", name, got, want)
}
}
m.MaxPositionEmbeddings = 0
replacer = strings.NewReplacer(m.Replacements()...)
for name, want := range map[string]string{
"block.0.attn.out.weight": "blk.0.attn_output.weight",
"block.0.attn.sinks": "blk.0.attn_sinks.weight",
"block.0.mlp.norm.weight": "blk.0.post_attention_norm.weight",
} {
if got := replacer.Replace(name); got != want {
t.Fatalf("Replace(%q) = %q, want %q", name, got, want)
}
}
}
+37 -26
View File
@@ -34,8 +34,6 @@ type llamaModel struct {
LowFrequencyFactor float32 `json:"low_freq_factor"`
HighFrequencyFactor float32 `json:"high_freq_factor"`
OriginalMaxPositionEmbeddings uint32 `json:"original_max_position_embeddings"`
factors ropeFactor
} `json:"rope_scaling"`
RMSNormEPS float32 `json:"rms_norm_eps"`
LayerNormEPS float32 `json:"layer_norm_eps"`
@@ -83,27 +81,6 @@ func (p *llamaModel) KV(t *Tokenizer) KV {
if p.RopeScaling.Type == "linear" {
kv["llama.rope.scaling.type"] = p.RopeScaling.Type
kv["llama.rope.scaling.factor"] = p.RopeScaling.Factor
} else if p.RopeScaling.RopeType == "llama3" {
dim := p.HiddenSize / p.NumAttentionHeads
for i := uint32(0); i < dim; i += 2 {
factor := cmp.Or(p.RopeScaling.Factor, 8.0)
factorLow := cmp.Or(p.RopeScaling.LowFrequencyFactor, 1.0)
factorHigh := cmp.Or(p.RopeScaling.HighFrequencyFactor, 4.0)
original := cmp.Or(p.RopeScaling.OriginalMaxPositionEmbeddings, 8192)
lambdaLow := float32(original) / factorLow
lambdaHigh := float32(original) / factorHigh
lambda := 2 * math.Pi * math.Pow(float64(p.RopeTheta), float64(i)/float64(dim))
if lambda < float64(lambdaHigh) {
p.RopeScaling.factors = append(p.RopeScaling.factors, 1.0)
} else if lambda > float64(lambdaLow) {
p.RopeScaling.factors = append(p.RopeScaling.factors, factor)
} else {
smooth := (float32(original)/float32(lambda) - factorLow) / (factorHigh - factorLow)
p.RopeScaling.factors = append(p.RopeScaling.factors, 1.0/((1-smooth)/factor+smooth))
}
}
}
if p.NumKeyValueHeads > 0 {
@@ -129,12 +106,12 @@ func (p *llamaModel) KV(t *Tokenizer) KV {
func (p *llamaModel) Tensors(ts []Tensor) []*ggml.Tensor {
var out []*ggml.Tensor
if p.RopeScaling.factors != nil {
if factors := p.ropeFactors(); factors != nil {
out = append(out, &ggml.Tensor{
Name: "rope_freqs.weight",
Kind: 0,
Shape: []uint64{uint64(len(p.RopeScaling.factors))},
WriterTo: p.RopeScaling.factors,
Shape: []uint64{uint64(len(factors))},
WriterTo: factors,
})
}
@@ -157,6 +134,40 @@ func (p *llamaModel) Tensors(ts []Tensor) []*ggml.Tensor {
return out
}
func (p *llamaModel) ropeFactors() ropeFactor {
if p.RopeScaling.RopeType != "llama3" || p.HiddenSize == 0 || p.NumAttentionHeads == 0 || p.RopeTheta == 0 {
return nil
}
dim := p.HiddenSize / p.NumAttentionHeads
if dim == 0 {
return nil
}
factors := make(ropeFactor, 0, dim/2)
for i := uint32(0); i < dim; i += 2 {
factor := cmp.Or(p.RopeScaling.Factor, float32(8))
factorLow := cmp.Or(p.RopeScaling.LowFrequencyFactor, float32(1))
factorHigh := cmp.Or(p.RopeScaling.HighFrequencyFactor, float32(4))
original := cmp.Or(p.RopeScaling.OriginalMaxPositionEmbeddings, uint32(8192))
lambdaLow := float32(original) / factorLow
lambdaHigh := float32(original) / factorHigh
lambda := 2 * math.Pi * math.Pow(float64(p.RopeTheta), float64(i)/float64(dim))
if lambda < float64(lambdaHigh) {
factors = append(factors, 1)
} else if lambda > float64(lambdaLow) {
factors = append(factors, factor)
} else {
smooth := (float32(original)/float32(lambda) - factorLow) / (factorHigh - factorLow)
factors = append(factors, 1/((1-smooth)/factor+smooth))
}
}
return factors
}
func (p *llamaModel) Replacements() []string {
return []string{
"lm_head", "output",
+34
View File
@@ -0,0 +1,34 @@
package convert
import "testing"
func TestLlama3RopeFactorsTensorDoesNotDependOnKVOrder(t *testing.T) {
m := &llamaModel{
HiddenSize: 2048,
NumAttentionHeads: 32,
RopeTheta: 500000,
}
m.RopeScaling.RopeType = "llama3"
m.RopeScaling.Factor = 32
m.RopeScaling.LowFrequencyFactor = 1
m.RopeScaling.HighFrequencyFactor = 4
m.RopeScaling.OriginalMaxPositionEmbeddings = 8192
tensors := m.Tensors(nil)
if len(tensors) != 1 {
t.Fatalf("expected rope tensor only, got %d tensors", len(tensors))
}
if tensors[0].Name != "rope_freqs.weight" {
t.Fatalf("expected rope_freqs.weight, got %q", tensors[0].Name)
}
if len(tensors[0].Shape) != 1 || tensors[0].Shape[0] != 32 {
t.Fatalf("expected rope tensor shape [32], got %v", tensors[0].Shape)
}
_ = m.KV(&Tokenizer{Vocabulary: &Vocabulary{}})
afterKV := m.Tensors(nil)
if len(afterKV) != 1 || afterKV[0].Name != "rope_freqs.weight" {
t.Fatalf("expected one rope tensor after KV call, got %#v", afterKV)
}
}
+4 -7
View File
@@ -79,20 +79,17 @@ func (p *mistral3Model) KV(t *Tokenizer) KV {
kv["mistral3.rope.freq_base"] = cmp.Or(p.TextModel.RopeTheta, p.TextModel.RopeParameters.RopeTheta)
kv["mistral3.rope.scaling.factor"] = p.TextModel.RopeParameters.Factor
kv["mistral3.rope.scaling.type"] = p.TextModel.RopeParameters.RopeType
kv["mistral3.rope.scaling.beta_fast"] = p.TextModel.RopeParameters.BetaFast
kv["mistral3.rope.scaling.beta_slow"] = p.TextModel.RopeParameters.BetaSlow
kv["mistral3.rope.scaling.yarn_beta_fast"] = p.TextModel.RopeParameters.BetaFast
kv["mistral3.rope.scaling.yarn_beta_slow"] = p.TextModel.RopeParameters.BetaSlow
if p.TextModel.RopeParameters.Mscale != nil {
kv["mistral3.rope.scaling.mscale"] = *p.TextModel.RopeParameters.Mscale
}
if p.TextModel.RopeParameters.MscaleAllDim != nil {
kv["mistral3.rope.scaling.mscale_all_dim"] = *p.TextModel.RopeParameters.MscaleAllDim
kv["mistral3.rope.scaling.yarn_log_multiplier"] = *p.TextModel.RopeParameters.MscaleAllDim
}
if p.TextModel.RopeParameters.OrigMaxPositionEmbeddings > 0 {
kv["mistral3.rope.scaling.original_context_length"] = p.TextModel.RopeParameters.OrigMaxPositionEmbeddings
}
if p.TextModel.RopeParameters.Llama4ScalingBeta != nil {
kv["mistral3.rope.scaling_beta"] = *p.TextModel.RopeParameters.Llama4ScalingBeta
kv["mistral3.attention.temperature_scale"] = *p.TextModel.RopeParameters.Llama4ScalingBeta
}
// Vision configuration
+4 -9
View File
@@ -58,24 +58,19 @@ func (p *mistral3CausalModel) KV(t *Tokenizer) KV {
kv["mistral3.rope.freq_base"] = cmp.Or(p.RopeTheta, p.RopeParameters.RopeTheta)
kv["mistral3.rope.scaling.factor"] = p.RopeParameters.Factor
kv["mistral3.rope.scaling.type"] = p.RopeParameters.RopeType
kv["mistral3.rope.scaling.beta_fast"] = p.RopeParameters.BetaFast
kv["mistral3.rope.scaling.beta_slow"] = p.RopeParameters.BetaSlow
if p.RopeParameters.Mscale != nil {
kv["mistral3.rope.scaling.mscale"] = *p.RopeParameters.Mscale
}
kv["mistral3.rope.scaling.yarn_beta_fast"] = p.RopeParameters.BetaFast
kv["mistral3.rope.scaling.yarn_beta_slow"] = p.RopeParameters.BetaSlow
if p.RopeParameters.MscaleAllDim != nil {
kv["mistral3.rope.scaling.mscale_all_dim"] = *p.RopeParameters.MscaleAllDim
kv["mistral3.rope.scaling.yarn_log_multiplier"] = *p.RopeParameters.MscaleAllDim
}
if p.RopeParameters.OrigMaxPositionEmbeddings > 0 {
kv["mistral3.rope.scaling.original_context_length"] = p.RopeParameters.OrigMaxPositionEmbeddings
kv["mistral3.rope.scaling_beta"] = *p.RopeParameters.Llama4ScalingBeta
}
if p.RopeParameters.Llama4ScalingBeta != nil {
kv["mistral3.rope.scaling_beta"] = *p.RopeParameters.Llama4ScalingBeta
kv["mistral3.attention.temperature_scale"] = *p.RopeParameters.Llama4ScalingBeta
}
return kv
+70
View File
@@ -0,0 +1,70 @@
package convert
import "testing"
func TestMistral3KVUsesLlamaCppRopeScalingKeys(t *testing.T) {
mscale := float32(0.75)
mscaleAllDim := float32(0)
temperatureScale := float32(0.125)
multimodal := &mistral3Model{}
multimodal.TextModel.NumAttentionHeads = 1
multimodal.TextModel.HeadDim = 64
multimodal.TextModel.RopeParameters.BetaFast = 32
multimodal.TextModel.RopeParameters.BetaSlow = 1
multimodal.TextModel.RopeParameters.Mscale = &mscale
multimodal.TextModel.RopeParameters.MscaleAllDim = &mscaleAllDim
multimodal.TextModel.RopeParameters.Llama4ScalingBeta = &temperatureScale
causal := &mistral3CausalModel{NumAttentionHeads: 1, HeadDim: 64}
causal.RopeParameters.BetaFast = 32
causal.RopeParameters.BetaSlow = 1
causal.RopeParameters.Mscale = &mscale
causal.RopeParameters.MscaleAllDim = &mscaleAllDim
causal.RopeParameters.Llama4ScalingBeta = &temperatureScale
tests := []struct {
name string
kv KV
}{
{name: "multimodal", kv: multimodal.KV(mistralTestTokenizer())},
{name: "causal", kv: causal.KV(mistralTestTokenizer())},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assertKVEquals(t, tt.kv, "mistral3.rope.scaling.yarn_beta_fast", float32(32))
assertKVEquals(t, tt.kv, "mistral3.rope.scaling.yarn_beta_slow", float32(1))
assertKVEquals(t, tt.kv, "mistral3.rope.scaling.yarn_log_multiplier", mscaleAllDim)
assertKVEquals(t, tt.kv, "mistral3.attention.temperature_scale", temperatureScale)
for _, key := range []string{
"mistral3.rope.scaling.beta_fast",
"mistral3.rope.scaling.beta_slow",
"mistral3.rope.scaling.mscale",
"mistral3.rope.scaling.mscale_all_dim",
"mistral3.rope.scaling_beta",
} {
if _, ok := tt.kv[key]; ok {
t.Fatalf("unexpected legacy key %q", key)
}
}
})
}
}
func mistralTestTokenizer() *Tokenizer {
return &Tokenizer{Vocabulary: &Vocabulary{}}
}
func assertKVEquals[T comparable](t *testing.T, kv KV, key string, want T) {
t.Helper()
got, ok := kv[key]
if !ok {
t.Fatalf("missing key %q", key)
}
if got != want {
t.Fatalf("%s = %v, want %v", key, got, want)
}
}
+4 -2
View File
@@ -131,8 +131,10 @@ type radioConfig struct {
} `json:"args"`
}
var _ ModelConverter = (*nemotronHModel)(nil)
var _ ModelConverter = (*nemotronHNanoVLModel)(nil)
var (
_ ModelConverter = (*nemotronHModel)(nil)
_ ModelConverter = (*nemotronHNanoVLModel)(nil)
)
func (n *nemotronHNanoVLModel) parseMore(fsys fs.FS) error {
if n.MaxSequenceLength > 0 {
+15 -15
View File
@@ -36,39 +36,39 @@ var _ ModelConverter = (*olmoModel)(nil)
func (p *olmoModel) KV(t *Tokenizer) KV {
kv := p.ModelParameters.KV(t)
kv["general.architecture"] = "olmo3"
kv["olmo3.block_count"] = p.NumHiddenLayers
kv["olmo3.context_length"] = p.MaxPositionEmbeddings
kv["olmo3.embedding_length"] = p.HiddenSize
kv["olmo3.feed_forward_length"] = p.IntermediateSize
kv["olmo3.attention.head_count"] = p.NumAttentionHeads
kv["olmo3.attention.head_count_kv"] = cmp.Or(p.NumKeyValueHeads, p.NumAttentionHeads)
kv["general.architecture"] = "olmo2"
kv["olmo2.block_count"] = p.NumHiddenLayers
kv["olmo2.context_length"] = p.MaxPositionEmbeddings
kv["olmo2.embedding_length"] = p.HiddenSize
kv["olmo2.feed_forward_length"] = p.IntermediateSize
kv["olmo2.attention.head_count"] = p.NumAttentionHeads
kv["olmo2.attention.head_count_kv"] = cmp.Or(p.NumKeyValueHeads, p.NumAttentionHeads)
if p.RopeTheta > 0 {
kv["olmo3.rope.freq_base"] = p.RopeTheta
kv["olmo2.rope.freq_base"] = p.RopeTheta
}
if p.RopeScaling != nil {
if p.RopeScaling.Factor > 0 {
kv["olmo3.rope.scaling.factor"] = p.RopeScaling.Factor
kv["olmo2.rope.scaling.factor"] = p.RopeScaling.Factor
}
if p.RopeScaling.OriginalMaxPositionEmbeds > 0 {
kv["olmo3.rope.scaling.original_context_length"] = p.RopeScaling.OriginalMaxPositionEmbeds
kv["olmo2.rope.scaling.original_context_length"] = p.RopeScaling.OriginalMaxPositionEmbeds
}
if p.RopeScaling.AttentionFactor > 0 {
kv["olmo3.rope.scaling.attn_factor"] = p.RopeScaling.AttentionFactor
kv["olmo2.rope.scaling.attn_factor"] = p.RopeScaling.AttentionFactor
}
if p.RopeScaling.RopeType != "" {
kv["olmo3.rope.scaling.type"] = p.RopeScaling.RopeType
kv["olmo2.rope.scaling.type"] = p.RopeScaling.RopeType
}
}
if p.RMSNormEPS > 0 {
kv["olmo3.attention.layer_norm_rms_epsilon"] = p.RMSNormEPS
kv["olmo2.attention.layer_norm_rms_epsilon"] = p.RMSNormEPS
}
if p.SlidingWindow > 0 {
kv["olmo3.attention.sliding_window"] = p.SlidingWindow
kv["olmo2.attention.sliding_window"] = p.SlidingWindow
}
if len(p.LayerTypes) > 0 {
@@ -76,7 +76,7 @@ func (p *olmoModel) KV(t *Tokenizer) KV {
for i, layerType := range p.LayerTypes {
slidingPattern[i] = (layerType == "sliding_attention")
}
kv["olmo3.attention.sliding_window_pattern"] = slidingPattern
kv["olmo2.attention.sliding_window_pattern"] = slidingPattern
}
return kv
+639 -6
View File
@@ -1,15 +1,24 @@
package convert
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"io/fs"
"maps"
"math"
"os"
"slices"
"strconv"
"strings"
"github.com/d4l3k/go-bfloat16"
"github.com/pdevine/tensor"
"github.com/pdevine/tensor/native"
"github.com/x448/float16"
"github.com/ollama/ollama/fs/ggml"
)
@@ -32,6 +41,8 @@ type qwen3NextTextConfig struct {
MaxPositionEmbeddings uint32 `json:"max_position_embeddings"`
HiddenSize uint32 `json:"hidden_size"`
NumHiddenLayers uint32 `json:"num_hidden_layers"`
NumNextNPredictLayers uint32 `json:"num_nextn_predict_layers"`
MTPNumHiddenLayers uint32 `json:"mtp_num_hidden_layers"`
IntermediateSize uint32 `json:"intermediate_size"`
NumAttentionHeads uint32 `json:"num_attention_heads"`
NumKeyValueHeads uint32 `json:"num_key_value_heads"`
@@ -66,8 +77,11 @@ type qwen3NextTextConfig struct {
type qwen3NextVisionConfig struct {
Depth uint32 `json:"depth"`
HiddenSize uint32 `json:"hidden_size"`
IntermediateSize uint32 `json:"intermediate_size"`
NumHeads uint32 `json:"num_heads"`
NumPositionEmbeddings uint32 `json:"num_position_embeddings"`
InChannels uint32 `json:"in_channels"`
OutHiddenSize uint32 `json:"out_hidden_size"`
PatchSize uint32 `json:"patch_size"`
SpatialMergeSize uint32 `json:"spatial_merge_size"`
RMSNormEps float32 `json:"layer_norm_epsilon"`
@@ -96,12 +110,25 @@ type qwen3NextModel struct {
VisionEndTokenID uint32 `json:"vision_end_token_id"`
}
var _ ModelConverter = (*qwen3NextModel)(nil)
var (
_ ModelConverter = (*qwen3NextModel)(nil)
_ MultimodalConverter = (*qwen3NextModel)(nil)
)
func (q *qwen3NextModel) parseMore(fsys fs.FS) error {
if q.TextConfig != nil {
q.qwen3NextTextConfig = *q.TextConfig
}
if q.NumNextNPredictLayers == 0 {
q.NumNextNPredictLayers = q.MTPNumHiddenLayers
}
if q.NumNextNPredictLayers == 0 {
nextn, err := qwen3NextInferNextNPredictLayers(fsys)
if err != nil {
return err
}
q.NumNextNPredictLayers = nextn
}
if q.RopeTheta == 0 {
q.RopeTheta = q.RopeParameters.RopeTheta
@@ -182,6 +209,150 @@ func (q *qwen3NextModel) parseMore(fsys fs.FS) error {
return nil
}
func qwen3NextInferNextNPredictLayers(fsys fs.FS) (uint32, error) {
paths, err := fs.Glob(fsys, "*.safetensors")
if err != nil {
return 0, err
}
maxLayer := -1
hasMTP := false
for _, p := range paths {
f, err := fsys.Open(p)
if err != nil {
return 0, err
}
var n int64
if err := binary.Read(f, binary.LittleEndian, &n); err != nil {
f.Close()
return 0, err
}
b := bytes.NewBuffer(make([]byte, 0, n))
if _, err = io.CopyN(b, f, n); err != nil {
f.Close()
return 0, err
}
f.Close()
var headers map[string]safetensorMetadata
if err := json.NewDecoder(b).Decode(&headers); err != nil {
return 0, err
}
for name, value := range headers {
if value.Type == "" || !strings.HasPrefix(name, "mtp.") {
continue
}
hasMTP = true
rest := strings.TrimPrefix(name, "mtp.layers.")
layer, suffix, ok := strings.Cut(rest, ".")
if !ok {
continue
}
n, err := strconv.Atoi(layer)
if err == nil && n > maxLayer && suffix != "" {
maxLayer = n
}
}
}
if maxLayer >= 0 {
return uint32(maxLayer + 1), nil
}
if hasMTP {
return 1, nil
}
return 0, nil
}
func ConvertQwen35MTPDraft(fsys fs.FS, f *os.File, baseKV ggml.KV, baseTensors []*ggml.Tensor) error {
arch := baseKV.Architecture()
if arch != "qwen35" && arch != "qwen35moe" {
return fmt.Errorf("MTP draft safetensors require a qwen3.5 base model, got %q", arch)
}
baseBlocks := baseKV.Uint("block_count")
if baseBlocks == 0 {
return fmt.Errorf("MTP draft safetensors require a base model with block_count")
}
if baseKV.Uint("nextn_predict_layers") > 0 {
return fmt.Errorf("MTP draft safetensors require a base model without embedded MTP layers")
}
nextn, err := qwen3NextInferNextNPredictLayers(fsys)
if err != nil {
return err
}
if nextn == 0 {
return fmt.Errorf("MTP draft safetensors did not contain mtp tensors")
}
q := &qwen3NextModel{
qwen3NextTextConfig: qwen3NextTextConfig{
NumHiddenLayers: baseBlocks,
NumNextNPredictLayers: nextn,
},
}
ts, err := parseTensors(fsys, strings.NewReplacer(q.Replacements()...))
if err != nil {
return err
}
if err := ensureUniqueTensorNames(ts); err != nil {
return err
}
mtpTensors := q.Tensors(ts)
if len(mtpTensors) == 0 {
return fmt.Errorf("MTP draft safetensors did not produce GGUF tensors")
}
for _, tensor := range mtpTensors {
if !qwen35MTPDraftTensorName(tensor.Name, baseBlocks, nextn) {
return fmt.Errorf("MTP draft safetensors produced unexpected tensor %q", tensor.Name)
}
tensor.Shape = slices.Clone(tensor.Shape)
slices.Reverse(tensor.Shape)
}
kv := maps.Clone(baseKV)
qwen35RemoveSplitMetadata(kv, arch)
kv[arch+".block_count"] = baseBlocks + nextn
kv[arch+".nextn_predict_layers"] = nextn
tensors := make([]*ggml.Tensor, 0, len(baseTensors)+len(mtpTensors))
tensors = append(tensors, baseTensors...)
tensors = append(tensors, mtpTensors...)
var parameters uint64
for _, tensor := range tensors {
parameters += tensor.Elements()
}
kv["general.parameter_count"] = parameters
return ggml.WriteGGUF(f, kv, tensors)
}
func qwen35RemoveSplitMetadata(kv ggml.KV, arch string) {
for _, key := range []string{
"split.no",
"split.count",
"split.tensors.count",
} {
delete(kv, key)
delete(kv, arch+"."+key)
}
}
func qwen35MTPDraftTensorName(name string, base, nextn uint32) bool {
for i := range nextn {
if strings.HasPrefix(name, fmt.Sprintf("blk.%d.", base+i)) {
return true
}
}
return false
}
func (q *qwen3NextModel) kvHeadCounts() ([]uint32, error) {
if len(q.LayerTypes) > 0 {
kv := make([]uint32, q.NumHiddenLayers)
@@ -259,7 +430,10 @@ func (q *qwen3NextModel) KV(t *Tokenizer) KV {
}
kv["general.architecture"] = arch
kv["tokenizer.ggml.pre"] = "qwen35"
kv["block_count"] = q.NumHiddenLayers
kv["block_count"] = q.NumHiddenLayers + q.NumNextNPredictLayers
if q.NumNextNPredictLayers > 0 {
kv["nextn_predict_layers"] = q.NumNextNPredictLayers
}
kv["context_length"] = q.MaxPositionEmbeddings
kv["embedding_length"] = q.HiddenSize
kv["feed_forward_length"] = q.IntermediateSize
@@ -282,7 +456,11 @@ func (q *qwen3NextModel) KV(t *Tokenizer) KV {
if sections := q.ropeSections(); len(sections) > 0 {
kv["mrope_sections"] = sections
kv["rope.mrope_section"] = sections
kv["rope.dimension_sections"] = sections
dimensionSections := append([]int32(nil), sections...)
if len(dimensionSections) == 3 {
dimensionSections = append(dimensionSections, 0)
}
kv["rope.dimension_sections"] = dimensionSections
}
if q.RopeParameters.MRopeInterleaved {
kv["rope.mrope_interleaved"] = true
@@ -321,12 +499,21 @@ func (q *qwen3NextModel) KV(t *Tokenizer) KV {
}
if headCounts, err := q.kvHeadCounts(); err == nil {
kv["attention.head_count_kv"] = headCounts
var maxKV uint32
for _, count := range headCounts {
if count > maxKV {
maxKV = count
}
}
kv["attention.head_count_kv"] = maxKV
}
if q.VisionModel.Depth > 0 {
kv["vision.block_count"] = q.VisionModel.Depth
kv["vision.embedding_length"] = q.VisionModel.HiddenSize
if q.VisionModel.IntermediateSize > 0 {
kv["vision.feed_forward_length"] = q.VisionModel.IntermediateSize
}
kv["vision.attention.head_count"] = q.VisionModel.NumHeads
kv["vision.num_channels"] = q.VisionModel.InChannels
if q.VisionModel.PatchSize > 0 {
@@ -372,6 +559,378 @@ func (q *qwen3NextModel) KV(t *Tokenizer) KV {
return kv
}
func (q *qwen3NextModel) TextKV(t *Tokenizer) KV {
kv := q.KV(t)
for _, key := range []string{
"vision.block_count",
"vision.embedding_length",
"vision.feed_forward_length",
"vision.attention.head_count",
"vision.num_channels",
"vision.patch_size",
"vision.spatial_merge_size",
"vision.attention.layer_norm_epsilon",
"vision.rope.freq_base",
"vision.temporal_patch_size",
"vision.deepstack_visual_indexes",
"vision.shortest_edge",
"vision.longest_edge",
"vision.image_mean",
"vision.image_std",
"image_token_id",
"vision_start_token_id",
"vision_end_token_id",
"mrope_sections",
"rope.mrope_section",
"rope.mrope_interleaved",
"ssm.v_head_reordered",
} {
delete(kv, key)
}
return kv
}
func (q *qwen3NextModel) ProjectorKV(*Tokenizer) KV {
depth := q.VisionModel.Depth
deepstack := make([]bool, depth)
for _, idx := range q.VisionModel.DeepstackVisualIndexes {
if idx >= 0 && uint32(idx) < depth {
deepstack[idx] = true
}
}
imageSize := uint32(768)
if q.VisionModel.NumPositionEmbeddings > 0 && q.VisionModel.PatchSize > 0 {
root := uint32(math.Sqrt(float64(q.VisionModel.NumPositionEmbeddings)))
if root*root == q.VisionModel.NumPositionEmbeddings {
imageSize = root * q.VisionModel.PatchSize
}
}
projectionDim := q.VisionModel.OutHiddenSize
if projectionDim == 0 {
projectionDim = q.HiddenSize
}
layerNormEps := q.VisionModel.RMSNormEps
if layerNormEps == 0 {
layerNormEps = 1e-6
}
kv := KV{
"general.architecture": "clip",
"general.type": "mmproj",
"general.file_type": uint32(1),
"general.quantization_version": uint32(2),
"clip.has_vision_encoder": true,
"clip.projector_type": "qwen3vl_merger",
"clip.use_gelu": true,
"clip.vision.block_count": depth,
"clip.vision.embedding_length": q.VisionModel.HiddenSize,
"clip.vision.feed_forward_length": q.VisionModel.IntermediateSize,
"clip.vision.attention.head_count": q.VisionModel.NumHeads,
"clip.vision.image_size": imageSize,
"clip.vision.patch_size": q.VisionModel.PatchSize,
"clip.vision.projection_dim": projectionDim,
"clip.vision.spatial_merge_size": q.VisionModel.SpatialMergeSize,
"clip.vision.attention.layer_norm_epsilon": layerNormEps,
"clip.vision.is_deepstack_layers": deepstack,
}
if len(q.VisionModel.ImageMean) > 0 {
kv["clip.vision.image_mean"] = q.VisionModel.ImageMean
}
if len(q.VisionModel.ImageStd) > 0 {
kv["clip.vision.image_std"] = q.VisionModel.ImageStd
}
return kv
}
func (q *qwen3NextModel) TextTensors(ts []Tensor, _ *Tokenizer) []*ggml.Tensor {
var text []Tensor
for _, t := range ts {
if qwen3NextVisionTensor(t.Name()) {
continue
}
text = append(text, t)
}
return q.Tensors(text)
}
func (q *qwen3NextModel) ProjectorTensors(ts []Tensor) []*ggml.Tensor {
if q.VisionModel.Depth == 0 {
return nil
}
rename := strings.NewReplacer(
"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",
)
var out []*ggml.Tensor
for _, t := range ts {
name := t.Name()
if !qwen3NextVisionTensor(name) {
continue
}
if name == "v.patch_embed.weight" {
out = append(out, q.qwen35PatchEmbedTensors(t)...)
continue
}
outName := rename.Replace(name)
kind := t.Kind()
writer := io.WriterTo(t)
if outName == "v.position_embd.weight" {
kind = tensorKindFP32
writer = tensorFloat32Writer{tensor: t}
} else if sourceDType(t) == "BF16" && kind == tensorKindFP16 {
kind = tensorKindBF16
writer = tensorBF16Writer{tensor: t}
}
out = append(out, &ggml.Tensor{
Name: outName,
Kind: kind,
Shape: slices.Clone(t.Shape()),
WriterTo: writer,
})
}
return out
}
func qwen3NextVisionTensor(name string) bool {
return strings.HasPrefix(name, "v.")
}
func (q *qwen3NextModel) qwen35PatchEmbedTensors(t Tensor) []*ggml.Tensor {
shape := t.Shape()
if len(shape) != 5 || shape[2] != 2 {
return nil
}
outShape := []uint64{shape[0], shape[1], shape[3], shape[4]}
return []*ggml.Tensor{
{
Name: "v.patch_embd.weight",
Kind: tensorKindFP32,
Shape: slices.Clone(outShape),
WriterTo: tensorFloat32Writer{tensor: t, repacker: q.qwen35PatchEmbedSlice(0)},
},
{
Name: "v.patch_embd.weight.1",
Kind: tensorKindFP32,
Shape: slices.Clone(outShape),
WriterTo: tensorFloat32Writer{tensor: t, repacker: q.qwen35PatchEmbedSlice(1)},
},
}
}
func (q *qwen3NextModel) qwen35PatchEmbedSlice(slice int) Repacker {
return func(_ string, data []float32, shape []uint64) ([]float32, error) {
if len(shape) != 5 || shape[2] != 2 {
return nil, fmt.Errorf("qwen3next: unexpected patch_embed shape %v", shape)
}
outChannels := int(shape[0])
inChannels := int(shape[1])
frames := int(shape[2])
height := int(shape[3])
width := int(shape[4])
if slice < 0 || slice >= frames {
return nil, fmt.Errorf("qwen3next: patch_embed slice %d out of range", slice)
}
expected := outChannels * inChannels * frames * height * width
if len(data) != expected {
return nil, fmt.Errorf("qwen3next: patch_embed data size %d, expected %d", len(data), expected)
}
out := make([]float32, outChannels*inChannels*height*width)
for oc := range outChannels {
for ic := range inChannels {
for y := range height {
for x := range width {
src := ((((oc*inChannels+ic)*frames+slice)*height + y) * width) + x
dst := (((oc*inChannels+ic)*height + y) * width) + x
out[dst] = data[src]
}
}
}
}
return out, nil
}
}
type tensorBF16Writer struct {
tensor Tensor
repacker Repacker
}
func (w tensorBF16Writer) WriteTo(dst io.Writer) (int64, error) {
data, err := tensorFloat32Data(w.tensor)
if err != nil {
return 0, err
}
if w.repacker != nil {
data, err = w.repacker(w.tensor.Name(), data, w.tensor.Shape())
if err != nil {
return 0, err
}
}
u8s := bfloat16.EncodeFloat32(data)
if _, err := dst.Write(u8s); err != nil {
return 0, err
}
return int64(len(u8s)), nil
}
type tensorFloat32Writer struct {
tensor Tensor
repacker Repacker
}
func (w tensorFloat32Writer) WriteTo(dst io.Writer) (int64, error) {
data, err := tensorFloat32Data(w.tensor)
if err != nil {
return 0, err
}
if w.repacker != nil {
data, err = w.repacker(w.tensor.Name(), data, w.tensor.Shape())
if err != nil {
return 0, err
}
}
if err := binary.Write(dst, binary.LittleEndian, data); err != nil {
return 0, err
}
return int64(len(data) * 4), nil
}
func tensorFloat32Data(t Tensor) ([]float32, error) {
if st, ok := tensorSafetensor(t); ok {
return safetensorFloat32Data(st)
}
var buf bytes.Buffer
if _, err := t.WriteTo(&buf); err != nil {
return nil, err
}
switch t.Kind() {
case tensorKindFP32:
out := make([]float32, buf.Len()/4)
if err := binary.Read(bytes.NewReader(buf.Bytes()), binary.LittleEndian, out); err != nil {
return nil, err
}
return out, nil
case tensorKindFP16:
raw := make([]uint16, buf.Len()/2)
if err := binary.Read(bytes.NewReader(buf.Bytes()), binary.LittleEndian, raw); err != nil {
return nil, err
}
out := make([]float32, len(raw))
for i, v := range raw {
out[i] = float16.Frombits(v).Float32()
}
return out, nil
case tensorKindBF16:
return bfloat16.DecodeFloat32(buf.Bytes()), nil
default:
return nil, fmt.Errorf("unsupported tensor kind %d for F32 writer", t.Kind())
}
}
func tensorSafetensor(t Tensor) (safetensor, bool) {
switch t := t.(type) {
case safetensor:
return t, true
case *safetensor:
return *t, true
default:
return safetensor{}, false
}
}
func safetensorFloat32Data(st safetensor) ([]float32, error) {
f, err := st.fs.Open(st.path)
if err != nil {
return nil, err
}
defer f.Close()
var r io.Reader
if readerAt, ok := f.(io.ReaderAt); ok {
r = io.NewSectionReader(readerAt, st.offset, st.size)
} else if seeker, ok := f.(io.Seeker); ok {
if _, err := seeker.Seek(st.offset, io.SeekStart); err != nil {
return nil, err
}
r = f
} else {
if _, err := io.CopyN(io.Discard, f, st.offset); err != nil {
return nil, err
}
r = f
}
br := bufio.NewReaderSize(r, min(32<<10, int(st.size)))
var out []float32
switch st.dtype {
case "F32":
out = make([]float32, st.size/4)
if err := binary.Read(br, binary.LittleEndian, out); err != nil {
return nil, err
}
case "F16":
raw := make([]uint16, st.size/2)
if err := binary.Read(br, binary.LittleEndian, raw); err != nil {
return nil, err
}
out = make([]float32, len(raw))
for i, v := range raw {
out[i] = float16.Frombits(v).Float32()
}
case "BF16":
raw := make([]uint8, st.size)
if err := binary.Read(br, binary.LittleEndian, raw); err != nil {
return nil, err
}
out = bfloat16.DecodeFloat32(raw)
case "F8_E4M3":
raw := make([]uint8, st.size)
if err := binary.Read(br, binary.LittleEndian, raw); err != nil {
return nil, err
}
out, err = st.decodeFP8E4M3(raw)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported safetensor dtype %q", st.dtype)
}
if st.repacker != nil {
out, err = st.repacker(st.Name(), out, st.Shape())
if err != nil {
return nil, err
}
}
return out, nil
}
func (q *qwen3NextModel) Tensors(ts []Tensor) []*ggml.Tensor {
var out []*ggml.Tensor
@@ -398,6 +957,13 @@ func (q *qwen3NextModel) Tensors(ts []Tensor) []*ggml.Tensor {
name := t.Name()
shape := t.Shape()
if names := q.mtpTensorNames(name); len(names) > 0 {
for _, name := range names {
out = q.appendDirectTensor(out, t, name)
}
continue
}
if strings.HasSuffix(name, ".ssm_in.weight") {
if qkv, gate, ok := q.splitQKVZTensor(t); ok {
out = append(out, qkv, gate)
@@ -464,7 +1030,7 @@ func (q *qwen3NextModel) Tensors(ts []Tensor) []*ggml.Tensor {
}
out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(shape), WriterTo: t})
case strings.HasSuffix(name, ".ssm_dt"):
case strings.HasSuffix(name, ".ssm_dt"), strings.HasSuffix(name, ".ssm_dt.bias"):
if q.shouldReorderVHeads() {
t.SetRepacker(q.repackReorderDim(0, 1))
}
@@ -499,6 +1065,73 @@ func (q *qwen3NextModel) Tensors(ts []Tensor) []*ggml.Tensor {
return out
}
func (q *qwen3NextModel) appendDirectTensor(out []*ggml.Tensor, t Tensor, name string) []*ggml.Tensor {
if qwen3NextShouldShiftNorm(name) {
t = t.Clone()
t.SetRepacker(q.addOne)
}
return append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(t.Shape()), WriterTo: t})
}
func qwen3NextShouldShiftNorm(name string) bool {
if strings.HasSuffix(name, ".ssm_norm.weight") {
return false
}
return strings.HasSuffix(name, "_norm.weight") ||
strings.HasSuffix(name, ".nextn.enorm.weight") ||
strings.HasSuffix(name, ".nextn.hnorm.weight")
}
func (q *qwen3NextModel) mtpTensorNames(name string) []string {
if !strings.HasPrefix(name, "mtp.") {
return nil
}
base := q.NumHiddenLayers
nextn := q.NumNextNPredictLayers
if nextn == 0 {
nextn = 1
}
if rest := strings.TrimPrefix(name, "mtp.layers."); rest != name {
layer, suffix, ok := strings.Cut(rest, ".")
if !ok {
return nil
}
idx, err := strconv.ParseUint(layer, 10, 32)
if err != nil {
return nil
}
return []string{fmt.Sprintf("blk.%d.%s", base+uint32(idx), suffix)}
}
var suffix string
switch name {
case "mtp.fc.weight":
suffix = "nextn.eh_proj.weight"
case "mtp.pre_fc_norm_embedding.weight":
suffix = "nextn.enorm.weight"
case "mtp.pre_fc_norm_hidden.weight":
suffix = "nextn.hnorm.weight"
case "mtp.norm.weight":
suffix = "nextn.shared_head_norm.weight"
case "mtp.embed_tokens.weight":
suffix = "nextn.embed_tokens.weight"
case "mtp.shared_head.head.weight":
suffix = "nextn.shared_head_head.weight"
case "mtp.shared_head.norm.weight":
suffix = "nextn.shared_head_norm.weight"
default:
return nil
}
names := make([]string, 0, nextn)
for i := range nextn {
names = append(names, fmt.Sprintf("blk.%d.%s", base+i, suffix))
}
return names
}
func (q *qwen3NextModel) repackReorderDim(dim, headDim int) Repacker {
return func(_ string, data []float32, shape []uint64) ([]float32, error) {
if !q.shouldReorderVHeads() {
@@ -925,7 +1558,7 @@ func (q *qwen3NextModel) Replacements() []string {
"linear_attn.in_proj_b", "ssm_beta",
"linear_attn.conv1d", "ssm_conv1d",
"linear_attn.dt_bias", "ssm_dt",
"linear_attn.dt_bias", "ssm_dt.bias",
"linear_attn.dt_proj", "ssm_dt",
"linear_attn.A_log", "ssm_a",
"linear_attn.norm", "ssm_norm",
+287 -11
View File
@@ -4,10 +4,12 @@ import (
"bytes"
"encoding/binary"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"github.com/d4l3k/go-bfloat16"
"github.com/ollama/ollama/fs/ggml"
)
@@ -106,11 +108,7 @@ func TestQwen3NextKVLegacyConfig(t *testing.T) {
t.Fatalf("unexpected tokenizer pre: got %v want %v", got, want)
}
headCountKV, ok := kv["attention.head_count_kv"].([]uint32)
if !ok {
t.Fatalf("attention.head_count_kv has unexpected type: %T", kv["attention.head_count_kv"])
}
if got, want := headCountKV, []uint32{0, 2, 0, 2}; !slices.Equal(got, want) {
if got, want := kv["attention.head_count_kv"], uint32(2); got != want {
t.Fatalf("unexpected attention.head_count_kv: got %v want %v", got, want)
}
@@ -198,6 +196,7 @@ func TestQwen35KVFromTextConfig(t *testing.T) {
VisionModel: qwen3NextVisionConfig{
Depth: 2,
HiddenSize: 128,
IntermediateSize: 512,
NumHeads: 4,
InChannels: 3,
PatchSize: 16,
@@ -225,11 +224,7 @@ func TestQwen35KVFromTextConfig(t *testing.T) {
t.Fatalf("unexpected architecture: got %v want %v", got, want)
}
headCountKV, ok := kv["attention.head_count_kv"].([]uint32)
if !ok {
t.Fatalf("attention.head_count_kv has unexpected type: %T", kv["attention.head_count_kv"])
}
if got, want := headCountKV, []uint32{0, 4, 0, 4}; !slices.Equal(got, want) {
if got, want := kv["attention.head_count_kv"], uint32(4); got != want {
t.Fatalf("unexpected attention.head_count_kv: got %v want %v", got, want)
}
@@ -248,7 +243,7 @@ func TestQwen35KVFromTextConfig(t *testing.T) {
if !ok {
t.Fatalf("rope.dimension_sections has unexpected type: %T", kv["rope.dimension_sections"])
}
if got, want := ropeSections, []int32{11, 11, 10}; !slices.Equal(got, want) {
if got, want := ropeSections, []int32{11, 11, 10, 0}; !slices.Equal(got, want) {
t.Fatalf("unexpected rope.dimension_sections: got %v want %v", got, want)
}
@@ -259,6 +254,254 @@ func TestQwen35KVFromTextConfig(t *testing.T) {
if got, want := kv["vision.block_count"], uint32(2); got != want {
t.Fatalf("unexpected vision.block_count: got %v want %v", got, want)
}
if got, want := kv["vision.feed_forward_length"], uint32(512); got != want {
t.Fatalf("unexpected vision.feed_forward_length: got %v want %v", got, want)
}
}
func TestQwen35MTPTensors(t *testing.T) {
m := &qwen3NextModel{
ModelParameters: ModelParameters{
ModelType: "qwen3_5",
},
qwen3NextTextConfig: qwen3NextTextConfig{
NumHiddenLayers: 32,
NumNextNPredictLayers: 1,
},
}
kv := m.KV(&Tokenizer{Vocabulary: &Vocabulary{}})
if got, want := kv["block_count"], uint32(33); got != want {
t.Fatalf("unexpected block_count: got %v want %v", got, want)
}
if got, want := kv["nextn_predict_layers"], uint32(1); got != want {
t.Fatalf("unexpected nextn_predict_layers: got %v want %v", got, want)
}
tensors := m.Tensors([]Tensor{
&fakeTensor{name: "mtp.fc.weight", shape: []uint64{2, 2}, data: make([]float32, 4)},
&fakeTensor{name: "mtp.pre_fc_norm_embedding.weight", shape: []uint64{2}, data: []float32{0, 1}},
&fakeTensor{name: "mtp.pre_fc_norm_hidden.weight", shape: []uint64{2}, data: []float32{0, 1}},
&fakeTensor{name: "mtp.norm.weight", shape: []uint64{2}, data: []float32{0, 1}},
&fakeTensor{name: "mtp.layers.0.attn_q.weight", shape: []uint64{2, 2}, data: make([]float32, 4)},
&fakeTensor{name: "mtp.layers.0.ffn_down.weight", shape: []uint64{2, 2}, data: make([]float32, 4)},
})
byName := map[string]*ggml.Tensor{}
for _, tensor := range tensors {
byName[tensor.Name] = tensor
}
for _, name := range []string{
"blk.32.nextn.eh_proj.weight",
"blk.32.nextn.enorm.weight",
"blk.32.nextn.hnorm.weight",
"blk.32.nextn.shared_head_norm.weight",
"blk.32.attn_q.weight",
"blk.32.ffn_down.weight",
} {
if _, ok := byName[name]; !ok {
t.Fatalf("missing MTP tensor %q", name)
}
}
for _, name := range []string{
"blk.32.nextn.enorm.weight",
"blk.32.nextn.hnorm.weight",
"blk.32.nextn.shared_head_norm.weight",
} {
if got, want := readTensorData(t, byName[name]), []float32{1, 2}; !slices.Equal(got, want) {
t.Fatalf("unexpected shifted norm values for %s: got %v want %v", name, got, want)
}
}
}
func TestQwen35NativeSplitKV(t *testing.T) {
m := &qwen3NextModel{
ModelParameters: ModelParameters{
ModelType: "qwen3_5",
},
TextConfig: &qwen3NextTextConfig{
MaxPositionEmbeddings: 16384,
HiddenSize: 2560,
NumHiddenLayers: 4,
IntermediateSize: 9216,
NumAttentionHeads: 16,
NumKeyValueHeads: 4,
HeadDim: 256,
RMSNormEPS: 1e-6,
FullAttentionInterval: 2,
LinearConvKernelDim: 4,
LinearKeyHeadDim: 128,
LinearNumKeyHeads: 16,
LinearNumValueHeads: 32,
LinearValueHeadDim: 128,
RopeParameters: qwen3NextRopeParams{
MRopeInterleaved: true,
MropeSection: []int32{11, 11, 10},
RopeTheta: 10_000_000,
PartialRotaryFactor: 0.25,
},
},
VisionModel: qwen3NextVisionConfig{
Depth: 24,
HiddenSize: 1024,
IntermediateSize: 4096,
NumHeads: 16,
NumPositionEmbeddings: 2304,
InChannels: 3,
OutHiddenSize: 2560,
PatchSize: 16,
SpatialMergeSize: 2,
},
ImageTokenID: 248056,
VisionStartTokenID: 248053,
VisionEndTokenID: 248054,
}
m.VisionModel.ImageMean = []float32{0.5, 0.5, 0.5}
m.VisionModel.ImageStd = []float32{0.5, 0.5, 0.5}
if err := m.parseMore(os.DirFS(t.TempDir())); err != nil {
t.Fatal(err)
}
textKV := m.TextKV(&Tokenizer{Vocabulary: &Vocabulary{}})
for _, key := range []string{
"vision.block_count",
"image_token_id",
"vision_start_token_id",
"vision_end_token_id",
"mrope_sections",
"rope.mrope_section",
"rope.mrope_interleaved",
"ssm.v_head_reordered",
} {
if _, ok := textKV[key]; ok {
t.Fatalf("TextKV retained %q", key)
}
}
if got, want := textKV["rope.dimension_sections"], []int32{11, 11, 10, 0}; !slices.Equal(got.([]int32), want) {
t.Fatalf("unexpected rope.dimension_sections: got %v want %v", got, want)
}
projectorKV := m.ProjectorKV(&Tokenizer{Vocabulary: &Vocabulary{}})
if got, want := projectorKV["general.architecture"], "clip"; got != want {
t.Fatalf("unexpected projector architecture: got %v want %v", got, want)
}
if got, want := projectorKV["clip.projector_type"], "qwen3vl_merger"; got != want {
t.Fatalf("unexpected projector type: got %v want %v", got, want)
}
if got, want := projectorKV["clip.vision.feed_forward_length"], uint32(4096); got != want {
t.Fatalf("unexpected projector feed_forward_length: got %v want %v", got, want)
}
if got, want := projectorKV["clip.vision.image_size"], uint32(768); got != want {
t.Fatalf("unexpected projector image_size: got %v want %v", got, want)
}
if got, want := projectorKV["clip.vision.projection_dim"], uint32(2560); got != want {
t.Fatalf("unexpected projector projection_dim: got %v want %v", got, want)
}
}
func TestQwen35ProjectorTensors(t *testing.T) {
m := &qwen3NextModel{
VisionModel: qwen3NextVisionConfig{Depth: 1},
}
patch := &fakeTensor{
name: "v.patch_embed.weight",
shape: []uint64{2, 2, 2, 1, 2},
data: []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
}
tensors := m.ProjectorTensors([]Tensor{
patch,
&fakeTensor{name: "v.pos_embed.weight", shape: []uint64{4, 2}, data: []float32{0, 1, 2, 3, 4, 5, 6, 7}},
&fakeTensor{name: "v.blk.0.attn_qkv.weight", shape: []uint64{6, 2}, data: make([]float32, 12), sourceDType: "BF16", kind: tensorKindFP16},
&fakeTensor{name: "v.blk.0.mlp.linear_fc1.weight", shape: []uint64{8, 2}, data: make([]float32, 16), sourceDType: "BF16", kind: tensorKindFP16},
&fakeTensor{name: "token_embd.weight", shape: []uint64{2, 2}, data: make([]float32, 4)},
&fakeTensor{name: "mtp.fc.weight", shape: []uint64{2, 2}, data: make([]float32, 4)},
})
byName := map[string]*ggml.Tensor{}
for _, tensor := range tensors {
byName[tensor.Name] = tensor
}
if _, ok := byName["token_embd.weight"]; ok {
t.Fatalf("projector tensors included text tensor")
}
if _, ok := byName["mtp.fc.weight"]; ok {
t.Fatalf("projector tensors included MTP tensor")
}
if got := byName["v.position_embd.weight"]; got == nil || got.Kind != tensorKindFP32 {
t.Fatalf("position embedding was not promoted to F32: %#v", got)
}
if got := byName["v.blk.0.attn_qkv.weight"]; got == nil {
t.Fatalf("attn_qkv tensor missing")
} else if got.Kind != tensorKindBF16 {
t.Fatalf("attn_qkv tensor was not preserved as BF16: %#v", got)
}
if got := byName["v.blk.0.ffn_up.weight"]; got == nil {
t.Fatalf("ffn_up tensor missing")
} else if got.Kind != tensorKindBF16 {
t.Fatalf("ffn_up tensor was not preserved as BF16: %#v", got)
}
first := byName["v.patch_embd.weight"]
if first == nil {
t.Fatalf("first patch embedding slice missing")
}
if got, want := first.Shape, []uint64{2, 2, 1, 2}; !slices.Equal(got, want) {
t.Fatalf("unexpected first patch shape: got %v want %v", got, want)
}
if got, want := readTensorData(t, first), []float32{0, 1, 4, 5, 8, 9, 12, 13}; !slices.Equal(got, want) {
t.Fatalf("unexpected first patch data: got %v want %v", got, want)
}
second := byName["v.patch_embd.weight.1"]
if second == nil {
t.Fatalf("second patch embedding slice missing")
}
if got, want := readTensorData(t, second), []float32{2, 3, 6, 7, 10, 11, 14, 15}; !slices.Equal(got, want) {
t.Fatalf("unexpected second patch data: got %v want %v", got, want)
}
}
func TestQwen35BF16ProjectorWriterPreservesSource(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "tensor.bin")
values := []float32{1, -2, 3.5, 4.25}
raw := bfloat16.EncodeFloat32(values)
if err := os.WriteFile(path, raw, 0o644); err != nil {
t.Fatal(err)
}
st := safetensor{
fs: os.DirFS(dir),
path: "tensor.bin",
dtype: "BF16",
offset: 0,
size: int64(len(raw)),
tensorBase: &tensorBase{
name: "v.blk.0.attn_qkv.weight",
shape: []uint64{2, 2},
},
}
tensor := &ggml.Tensor{
Name: "v.blk.0.attn_qkv.weight",
Kind: tensorKindBF16,
Shape: []uint64{2, 2},
WriterTo: tensorBF16Writer{tensor: st},
}
var got bytes.Buffer
if n, err := tensor.WriteTo(&got); err != nil {
t.Fatal(err)
} else if n != int64(len(raw)) {
t.Fatalf("unexpected byte count: got %d want %d", n, len(raw))
}
if !bytes.Equal(got.Bytes(), raw) {
t.Fatalf("BF16 writer changed source bytes: got %x want %x", got.Bytes(), raw)
}
}
func TestQwen3NextReplacements(t *testing.T) {
@@ -273,6 +516,12 @@ func TestQwen3NextReplacements(t *testing.T) {
if got, want := r.Replace("model.layers.1.linear_attn.in_proj_qkvz.weight"), "blk.1.ssm_in.weight"; got != want {
t.Fatalf("unexpected legacy replacement: got %q want %q", got, want)
}
if got, want := r.Replace("model.layers.1.linear_attn.dt_bias"), "blk.1.ssm_dt.bias"; got != want {
t.Fatalf("unexpected dt bias replacement: got %q want %q", got, want)
}
if got, want := r.Replace("model.layers.1.linear_attn.dt_proj.weight"), "blk.1.ssm_dt.weight"; got != want {
t.Fatalf("unexpected dt projection replacement: got %q want %q", got, want)
}
}
func TestQwen35ReordersVHeads(t *testing.T) {
@@ -399,6 +648,33 @@ func TestQwen35ReordersSsmBetaRows(t *testing.T) {
}
}
func TestQwen35ReordersSsmDtBias(t *testing.T) {
m := &qwen3NextModel{
ModelParameters: ModelParameters{
ModelType: "qwen3_5",
},
qwen3NextTextConfig: qwen3NextTextConfig{
LinearNumKeyHeads: 2,
LinearNumValueHeads: 4,
},
}
out := m.Tensors([]Tensor{
&fakeTensor{
name: "blk.0.ssm_dt.bias",
shape: []uint64{4},
data: []float32{0, 1, 2, 3},
},
})
if len(out) != 1 {
t.Fatalf("unexpected output tensor count: got %d want 1", len(out))
}
if got, want := readTensorData(t, out[0]), []float32{0, 2, 1, 3}; !slices.Equal(got, want) {
t.Fatalf("unexpected ssm_dt.bias data: got %v want %v", got, want)
}
}
func TestQwen35ReordersConv1DChannelDim(t *testing.T) {
m := &qwen3NextModel{
ModelParameters: ModelParameters{
+257 -2
View File
@@ -3,8 +3,13 @@ package convert
import (
"cmp"
"encoding/json"
"fmt"
"io"
"io/fs"
"math"
"regexp"
"slices"
"strconv"
"strings"
"github.com/ollama/ollama/fs/ggml"
@@ -25,6 +30,9 @@ type qwen3VLModel struct {
RopeTheta float32 `json:"rope_theta"`
TemporalPatchSize uint32 `json:"temporal_patch_size"`
DeepstackVisualIndexes []int32 `json:"deepstack_visual_indexes"`
IntermediateSize uint32 `json:"intermediate_size"`
OutHiddenSize uint32 `json:"out_hidden_size"`
NumPositionEmbeddings uint32 `json:"num_position_embeddings"`
Size struct {
ShortestEdge uint32 `json:"shortest_edge"`
@@ -36,6 +44,8 @@ type qwen3VLModel struct {
} `json:"vision_config"`
}
var _ MultimodalConverter = (*qwen3VLModel)(nil)
func (m *qwen3VLModel) parseMore(fsys fs.FS) error {
bts, err := fs.ReadFile(fsys, "preprocessor_config.json")
if err != nil {
@@ -55,8 +65,20 @@ func (m *qwen3VLModel) KV(t *Tokenizer) KV {
// override architecture
kv["general.architecture"] = arch
if sections := m.RopeScaling.MropeSection; len(sections) > 0 {
dimensionSections := append([]int32(nil), sections...)
if len(dimensionSections) == 3 {
dimensionSections = append(dimensionSections, 0)
}
kv["rope.dimension_sections"] = dimensionSections
}
kv["n_deepstack_layers"] = uint32(len(m.VisionModel.DeepstackVisualIndexes))
kv["vision.block_count"] = cmp.Or(m.VisionModel.Depth, 32)
kv["vision.embedding_length"] = m.VisionModel.HiddenSize
if m.VisionModel.IntermediateSize > 0 {
kv["vision.feed_forward_length"] = m.VisionModel.IntermediateSize
}
kv["vision.attention.head_count"] = cmp.Or(m.VisionModel.NumHeads, 16)
kv["vision.num_channels"] = m.VisionModel.InChannels
kv["vision.patch_size"] = cmp.Or(m.VisionModel.PatchSize, 14)
@@ -75,6 +97,234 @@ func (m *qwen3VLModel) KV(t *Tokenizer) KV {
return kv
}
func (m *qwen3VLModel) TextKV(t *Tokenizer) KV {
kv := m.KV(t)
for _, key := range []string{
"vision.block_count",
"vision.embedding_length",
"vision.feed_forward_length",
"vision.attention.head_count",
"vision.num_channels",
"vision.patch_size",
"vision.spatial_merge_size",
"vision.attention.layer_norm_epsilon",
"vision.rope.freq_base",
"vision.temporal_patch_size",
"vision.deepstack_visual_indexes",
"vision.shortest_edge",
"vision.longest_edge",
"vision.image_mean",
"vision.image_std",
"rope.mrope_section",
} {
delete(kv, key)
}
return kv
}
func (m *qwen3VLModel) ProjectorKV(*Tokenizer) KV {
depth := cmp.Or(m.VisionModel.Depth, uint32(32))
deepstack := make([]bool, depth)
for _, idx := range m.VisionModel.DeepstackVisualIndexes {
if idx >= 0 && uint32(idx) < depth {
deepstack[idx] = true
}
}
projectionDim := m.VisionModel.OutHiddenSize
if projectionDim == 0 {
projectionDim = m.HiddenSize
}
layerNormEps := m.VisionModel.RMSNormEps
if layerNormEps == 0 {
layerNormEps = 1e-6
}
kv := KV{
"general.architecture": "clip",
"general.type": "mmproj",
"general.file_type": uint32(1),
"general.quantization_version": uint32(2),
"clip.has_vision_encoder": true,
"clip.projector_type": "qwen3vl_merger",
"clip.use_gelu": true,
"clip.vision.block_count": depth,
"clip.vision.embedding_length": m.VisionModel.HiddenSize,
"clip.vision.feed_forward_length": cmp.Or(m.VisionModel.IntermediateSize, m.VisionModel.HiddenSize*4),
"clip.vision.attention.head_count": cmp.Or(m.VisionModel.NumHeads, uint32(16)),
"clip.vision.attention.layer_norm_epsilon": layerNormEps,
"clip.vision.num_channels": m.VisionModel.InChannels,
"clip.vision.patch_size": cmp.Or(m.VisionModel.PatchSize, uint32(14)),
"clip.vision.spatial_merge_size": cmp.Or(m.VisionModel.SpatialMergeSize, uint32(2)),
"clip.vision.image_size": m.projectorImageSize(),
"clip.vision.projection_dim": projectionDim,
"clip.vision.temporal_patch_size": cmp.Or(m.VisionModel.TemporalPatchSize, uint32(2)),
"clip.vision.rope.freq_base": cmp.Or(m.VisionModel.RopeTheta, float32(1e4)),
"clip.vision.is_deepstack_layers": deepstack,
}
if m.VisionModel.Size.ShortestEdge > 0 {
kv["clip.vision.image_min_pixels"] = m.VisionModel.Size.ShortestEdge
}
if m.VisionModel.Size.LongestEdge > 0 {
kv["clip.vision.image_max_pixels"] = m.VisionModel.Size.LongestEdge
}
if len(m.VisionModel.ImageMean) == 3 {
kv["clip.vision.image_mean"] = m.VisionModel.ImageMean
}
if len(m.VisionModel.ImageStd) == 3 {
kv["clip.vision.image_std"] = m.VisionModel.ImageStd
}
return kv
}
func (m *qwen3VLModel) projectorImageSize() uint32 {
if m.VisionModel.NumPositionEmbeddings > 0 && m.VisionModel.PatchSize > 0 {
root := uint32(math.Sqrt(float64(m.VisionModel.NumPositionEmbeddings)))
if root*root == m.VisionModel.NumPositionEmbeddings {
return root * m.VisionModel.PatchSize
}
}
return uint32(768)
}
func qwen3VLVisionTensor(name string) bool {
return strings.HasPrefix(name, "v.") || strings.HasPrefix(name, "mm.")
}
func (m *qwen3VLModel) TextTensors(ts []Tensor, _ *Tokenizer) []*ggml.Tensor {
var textOnly []Tensor
for _, t := range ts {
if qwen3VLVisionTensor(t.Name()) {
continue
}
textOnly = append(textOnly, t)
}
return m.qwen3Model.Tensors(textOnly)
}
func (m *qwen3VLModel) qwen3VLProjectorRename(name string) string {
if strings.HasPrefix(name, "v.merger.") {
name = strings.Replace(name, "v.merger.linear_fc1", "mm.0", 1)
name = strings.Replace(name, "v.merger.linear_fc2", "mm.2", 1)
name = strings.Replace(name, "v.merger.norm", "v.post_ln", 1)
return name
}
if strings.HasPrefix(name, "v.deepstack.") {
re := regexp.MustCompile(`^v\.deepstack\.(\d+)\.(.+)$`)
if matches := re.FindStringSubmatch(name); matches != nil {
seqIdx, err := strconv.Atoi(matches[1])
if err == nil && seqIdx < len(m.VisionModel.DeepstackVisualIndexes) {
blockIdx := m.VisionModel.DeepstackVisualIndexes[seqIdx]
suffix := matches[2]
suffix = strings.Replace(suffix, "linear_fc1", "fc1", 1)
suffix = strings.Replace(suffix, "linear_fc2", "fc2", 1)
return fmt.Sprintf("v.deepstack.%d.%s", blockIdx, suffix)
}
}
}
return name
}
func (m *qwen3VLModel) ProjectorTensors(ts []Tensor) []*ggml.Tensor {
var out []*ggml.Tensor
for _, t := range ts {
if !qwen3VLVisionTensor(t.Name()) {
continue
}
name := m.qwen3VLProjectorRename(t.Name())
if name == "v.patch_embd.weight" {
out = append(out, m.qwen3VLPatchEmbedTensors(t)...)
continue
}
kind := t.Kind()
var writer io.WriterTo = t
if name == "v.position_embd.weight" {
kind = tensorKindFP32
writer = tensorFloat32Writer{tensor: t}
} else if sourceDType(t) == "BF16" && kind == tensorKindFP16 {
kind = tensorKindBF16
writer = tensorBF16Writer{tensor: t}
}
out = append(out, &ggml.Tensor{
Name: name,
Kind: kind,
Shape: slices.Clone(t.Shape()),
WriterTo: writer,
})
}
return out
}
func (m *qwen3VLModel) qwen3VLPatchEmbedTensors(t Tensor) []*ggml.Tensor {
shape := t.Shape()
if len(shape) != 5 || shape[2] != 2 {
return nil
}
outShape := []uint64{shape[0], shape[1], shape[3], shape[4]}
return []*ggml.Tensor{
{
Name: "v.patch_embd.weight",
Kind: tensorKindFP32,
Shape: slices.Clone(outShape),
WriterTo: tensorFloat32Writer{tensor: t, repacker: qwenTemporalPatchEmbedSlice(0)},
},
{
Name: "v.patch_embd.weight.1",
Kind: tensorKindFP32,
Shape: slices.Clone(outShape),
WriterTo: tensorFloat32Writer{tensor: t, repacker: qwenTemporalPatchEmbedSlice(1)},
},
}
}
func qwenTemporalPatchEmbedSlice(slice int) Repacker {
return func(_ string, data []float32, shape []uint64) ([]float32, error) {
if len(shape) != 5 || shape[2] != 2 {
return nil, fmt.Errorf("qwen temporal patch embedding shape %v", shape)
}
outChannels := int(shape[0])
inChannels := int(shape[1])
frames := int(shape[2])
height := int(shape[3])
width := int(shape[4])
if slice < 0 || slice >= frames {
return nil, fmt.Errorf("qwen temporal patch embedding slice %d out of range", slice)
}
expected := outChannels * inChannels * frames * height * width
if len(data) != expected {
return nil, fmt.Errorf("qwen temporal patch embedding data size %d, expected %d", len(data), expected)
}
out := make([]float32, outChannels*inChannels*height*width)
for oc := range outChannels {
for ic := range inChannels {
for y := range height {
for x := range width {
src := ((((oc*inChannels+ic)*frames+slice)*height + y) * width) + x
dst := (((oc*inChannels+ic)*height + y) * width) + x
out[dst] = data[src]
}
}
}
}
return out, nil
}
}
func (m *qwen3VLModel) Tensors(ts []Tensor) []*ggml.Tensor {
var rest []Tensor
var out []*ggml.Tensor
@@ -107,10 +357,15 @@ func (m *qwen3VLModel) Replacements() []string {
m.qwen3Model.Replacements(),
"model.language_", "",
"model.visual", "v",
"patch_embed.proj", "patch_embed",
"patch_embed.proj", "patch_embd",
"pos_embed", "position_embd",
"blocks", "blk",
"attn.qkv", "attn_qkv",
"attn.proj", "attn_out",
"deepstack_merger_list", "deepstack_merger",
"norm1", "ln1",
"norm2", "ln2",
"mlp.linear_fc1", "ffn_up",
"mlp.linear_fc2", "ffn_down",
"deepstack_merger_list", "deepstack",
)
}
+147
View File
@@ -0,0 +1,147 @@
package convert
import (
"slices"
"testing"
"github.com/ollama/ollama/fs/ggml"
)
func TestQwen3VLTextAndProjectorKV(t *testing.T) {
m := &qwen3VLModel{
qwen3Model: qwen3Model{
HiddenSize: 2048,
},
}
m.RopeScaling.Type = "mrope"
m.RopeScaling.MropeSection = []int32{24, 20, 20}
m.VisionModel.Depth = 24
m.VisionModel.HiddenSize = 1024
m.VisionModel.IntermediateSize = 4096
m.VisionModel.OutHiddenSize = 2048
m.VisionModel.NumHeads = 16
m.VisionModel.InChannels = 3
m.VisionModel.PatchSize = 16
m.VisionModel.SpatialMergeSize = 2
m.VisionModel.NumPositionEmbeddings = 2304
m.VisionModel.TemporalPatchSize = 2
m.VisionModel.RMSNormEps = 1e-6
m.VisionModel.RopeTheta = 10000
m.VisionModel.DeepstackVisualIndexes = []int32{5, 11, 17}
m.VisionModel.ImageMean = []float32{0.5, 0.5, 0.5}
m.VisionModel.ImageStd = []float32{0.5, 0.5, 0.5}
textKV := m.TextKV(&Tokenizer{Vocabulary: &Vocabulary{}})
if got, want := textKV["general.architecture"], "qwen3vl"; got != want {
t.Fatalf("unexpected text architecture: got %v want %v", got, want)
}
if got, want := textKV["rope.dimension_sections"], []int32{24, 20, 20, 0}; !slices.Equal(got.([]int32), want) {
t.Fatalf("unexpected rope.dimension_sections: got %v want %v", got, want)
}
if got, want := textKV["n_deepstack_layers"], uint32(3); got != want {
t.Fatalf("unexpected n_deepstack_layers: got %v want %v", got, want)
}
for _, key := range []string{"vision.block_count", "vision.deepstack_visual_indexes", "rope.mrope_section"} {
if _, ok := textKV[key]; ok {
t.Fatalf("TextKV retained %q", key)
}
}
projectorKV := m.ProjectorKV(&Tokenizer{Vocabulary: &Vocabulary{}})
if got, want := projectorKV["general.architecture"], "clip"; got != want {
t.Fatalf("unexpected projector architecture: got %v want %v", got, want)
}
if got, want := projectorKV["general.type"], "mmproj"; got != want {
t.Fatalf("unexpected projector type: got %v want %v", got, want)
}
if got, want := projectorKV["clip.projector_type"], "qwen3vl_merger"; got != want {
t.Fatalf("unexpected projector type: got %v want %v", got, want)
}
if got, want := projectorKV["clip.vision.feed_forward_length"], uint32(4096); got != want {
t.Fatalf("unexpected feed_forward_length: got %v want %v", got, want)
}
if got, want := projectorKV["clip.vision.image_size"], uint32(768); got != want {
t.Fatalf("unexpected image_size: got %v want %v", got, want)
}
mask, ok := projectorKV["clip.vision.is_deepstack_layers"].([]bool)
if !ok {
t.Fatalf("deepstack mask has unexpected type: %T", projectorKV["clip.vision.is_deepstack_layers"])
}
if len(mask) != 24 || !mask[5] || !mask[11] || !mask[17] {
t.Fatalf("unexpected deepstack mask: %v", mask)
}
}
func TestQwen3VLProjectorTensors(t *testing.T) {
m := &qwen3VLModel{}
m.VisionModel.DeepstackVisualIndexes = []int32{5, 11, 17}
tensors := m.ProjectorTensors([]Tensor{
&fakeTensor{
name: "v.patch_embd.weight",
shape: []uint64{2, 2, 2, 1, 2},
data: []float32{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
},
&fakeTensor{name: "v.position_embd.weight", shape: []uint64{4, 2}, data: []float32{0, 1, 2, 3, 4, 5, 6, 7}},
&fakeTensor{name: "v.merger.linear_fc1.weight", shape: []uint64{4, 2}, data: make([]float32, 8)},
&fakeTensor{name: "v.merger.linear_fc2.bias", shape: []uint64{4}, data: make([]float32, 4)},
&fakeTensor{name: "v.merger.norm.weight", shape: []uint64{2}, data: make([]float32, 2)},
&fakeTensor{name: "v.deepstack.0.linear_fc1.weight", shape: []uint64{4, 2}, data: make([]float32, 8)},
&fakeTensor{name: "v.deepstack.1.norm.bias", shape: []uint64{2}, data: make([]float32, 2)},
&fakeTensor{name: "v.blk.0.attn_qkv.weight", shape: []uint64{6, 2}, data: make([]float32, 12), sourceDType: "BF16", kind: tensorKindFP16},
&fakeTensor{name: "token_embd.weight", shape: []uint64{2, 2}, data: make([]float32, 4)},
})
byName := map[string]uint32{}
for _, tensor := range tensors {
byName[tensor.Name] = tensor.Kind
}
if _, ok := byName["token_embd.weight"]; ok {
t.Fatalf("projector tensors included text tensor")
}
if got := byName["v.position_embd.weight"]; got != tensorKindFP32 {
t.Fatalf("position embedding was not promoted to F32: %d", got)
}
if got := byName["v.blk.0.attn_qkv.weight"]; got != tensorKindBF16 {
t.Fatalf("BF16 projector tensor was not preserved: %d", got)
}
for _, name := range []string{
"mm.0.weight",
"mm.2.bias",
"v.post_ln.weight",
"v.deepstack.5.fc1.weight",
"v.deepstack.11.norm.bias",
} {
if _, ok := byName[name]; !ok {
t.Fatalf("missing projector tensor %q", name)
}
}
firstTensor := tensorsByName(tensors)["v.patch_embd.weight"]
if firstTensor == nil {
t.Fatalf("first patch embedding slice missing")
}
if got, want := firstTensor.Shape, []uint64{2, 2, 1, 2}; !slices.Equal(got, want) {
t.Fatalf("unexpected first patch shape: got %v want %v", got, want)
}
if got, want := readTensorData(t, firstTensor), []float32{0, 1, 4, 5, 8, 9, 12, 13}; !slices.Equal(got, want) {
t.Fatalf("unexpected first patch data: got %v want %v", got, want)
}
secondTensor := tensorsByName(tensors)["v.patch_embd.weight.1"]
if secondTensor == nil {
t.Fatalf("second patch embedding slice missing")
}
if got, want := readTensorData(t, secondTensor), []float32{2, 3, 6, 7, 10, 11, 14, 15}; !slices.Equal(got, want) {
t.Fatalf("unexpected second patch data: got %v want %v", got, want)
}
}
func tensorsByName(tensors []*ggml.Tensor) map[string]*ggml.Tensor {
byName := map[string]*ggml.Tensor{}
for _, tensor := range tensors {
byName[tensor.Name] = tensor
}
return byName
}
+5
View File
@@ -22,6 +22,7 @@ type fakeTensor struct {
data []float32
sourceDType string
kind uint32
repacker Repacker
}
@@ -34,6 +35,9 @@ func (f fakeTensor) Shape() []uint64 {
}
func (f fakeTensor) Kind() uint32 {
if f.kind != 0 {
return f.kind
}
return 0
}
@@ -51,6 +55,7 @@ func (f fakeTensor) Clone() Tensor {
shape: slices.Clone(f.shape),
data: slices.Clone(f.data),
sourceDType: f.sourceDType,
kind: f.kind,
repacker: f.repacker,
}
}
+5
View File
@@ -149,6 +149,7 @@ func parseTokenizer(fsys fs.FS, specialTokenTypes []string) (*Tokenizer, error)
if err := json.Unmarshal(bts, &sv.AddToken); err != nil {
return nil, err
}
sv.AddTokenSet = true
}
if bts, ok := p[fmt.Sprintf("%s_token", st)]; ok {
@@ -314,6 +315,10 @@ type SpecialVocabulary struct {
ID int
Content string
AddToken bool
// AddTokenSet tracks whether tokenizer_config.json explicitly defined the
// add_*_token setting. Missing and explicit false have different GGUF
// semantics for some tokenizers.
AddTokenSet bool
// IDs is populated by generation_config.json
IDs []int32
+24 -4
View File
@@ -184,8 +184,8 @@ func TestParseTokenizer(t *testing.T) {
},
SpecialVocabulary: []*SpecialVocabulary{
{Type: "pad", Content: "<pad>", ID: 0, AddToken: false},
{Type: "eos", Content: "<eos>", ID: 1, AddToken: false},
{Type: "bos", Content: "<bos>", ID: 2, AddToken: true},
{Type: "eos", Content: "<eos>", ID: 1, AddToken: false, AddTokenSet: true},
{Type: "bos", Content: "<bos>", ID: 2, AddToken: true, AddTokenSet: true},
{Type: "unk", Content: "<unk>", ID: 3, AddToken: false},
},
Pre: "default",
@@ -380,8 +380,8 @@ func TestParseTokenizer(t *testing.T) {
Types: []int32{3, 3, 3, 3},
},
SpecialVocabulary: []*SpecialVocabulary{
{Type: "eos", Content: "<eos>", ID: 1, IDs: []int32{1, 2, 3}, AddToken: false},
{Type: "bos", Content: "<bos>", ID: 0, AddToken: true},
{Type: "eos", Content: "<eos>", ID: 1, IDs: []int32{1, 2, 3}, AddToken: false, AddTokenSet: true},
{Type: "bos", Content: "<bos>", ID: 0, AddToken: true, AddTokenSet: true},
},
Pre: "default",
},
@@ -423,3 +423,23 @@ func TestParseTokenizer(t *testing.T) {
})
}
}
func TestModelParametersKVOmitsMissingAddToken(t *testing.T) {
kv := ModelParameters{}.KV(&Tokenizer{
Vocabulary: &Vocabulary{Model: "gpt2"},
SpecialVocabulary: []*SpecialVocabulary{
{Type: "bos", Content: "<bos>", ID: 1},
{Type: "eos", Content: "<eos>", ID: 2, AddToken: false, AddTokenSet: true},
},
})
if _, ok := kv["tokenizer.ggml.add_bos_token"]; ok {
t.Errorf("tokenizer.ggml.add_bos_token should be omitted when add_bos_token is absent")
}
if got := kv["tokenizer.ggml.bos_token_id"]; got != uint32(1) {
t.Errorf("tokenizer.ggml.bos_token_id = %v, want 1", got)
}
if got, ok := kv["tokenizer.ggml.add_eos_token"]; !ok || got != false {
t.Errorf("tokenizer.ggml.add_eos_token = %v, %v; want explicit false", got, ok)
}
}