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: -25223160dllama/compat: add in-memory shim so llama-server can load Ollama-format GGUFs -7449b539allm,server: route Ollama-format gemma3 blobs through llama/compat -436f2e2b1llama/compat: make patch-apply idempotent -8c2c9d4c8llama/compat: extend gemma3 handler to cover 1B and 270M blobs -021389f7bllama/compat: shrink clip.cpp injection from 18 lines to 1 -61b367ec2llama/compat: shrink patch to pure call-site hooks (34 -> 20 lines) -36049361cllama/compat: simplify shim (gemma3-tested) -8fa664865llama/compat: add qwen35moe text handler -db0c74530llama/compat: add qwen35moe vision (clip) support -2a388da77llama/compat: split shared infra into a util TU -9a69a17dcllama/compat: document non-public API dependencies -d0f38a915llama/compat: add gpt-oss and lfm2 handlers -086071822llama/compat: add mistral3 text handler (vision TODO) -63bde9ff7llama/compat: add mistral3 vision (clip) support -3a57b89d5llama/compat: apply LLaMA RoPE permute to mistral3 vision Q/K -99cb87439llama/compat: add qwen35, gemma4, deepseek-ocr handlers -2c7850dballama/compat: add nemotron_h_moe handler (latent FFN + MTP skip) -9e3b54225llama/compat: add llama4 text + clip handlers -034fee349llama/compat: add gemma4 clip handler (gemma4v projector) -9945c5a93server: remove dhiltgen/* compat redirect table -5d4539101llama/compat: rewrite gemma4 tokenizer model to BPE -7e0765327llama/compat: add glm-ocr text handler + text-loader load-op hook -f1bd1a25allama/compat: add glm-ocr clip handler (glm4v projector) -4b5cf3420llama/compat: collapse text-loader hook back to one new patch line -eb4ecf4fcllama/compat: extend gemma4 clip handler to gemma4a (audio) -a23a5e76fllama/compat: fix gemma4a per-block norm tensor mapping -cd2dcaff4llama/compat: add embeddinggemma handler -1ce8a6b26llama/compat: add qwen3-vl + qwen2.5-vl handlers -fd98ffa1ellama/compat: add gemma3n + glm4moelite handlers -cc7bdf0bcllama/compat: handle null buft in maybe_load_tensor -0c33775d3llama/compat: disable mmap when load_op transforms text-side tensors * 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:
+72
-3
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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.",
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user