9db4bdbad6
* 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>
1580 lines
44 KiB
Go
1580 lines
44 KiB
Go
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"
|
|
)
|
|
|
|
type qwen3NextRopeScaling struct {
|
|
Type string `json:"type"`
|
|
Factor ropeFactor `json:"factor"`
|
|
MropeSection []int32 `json:"mrope_section"`
|
|
}
|
|
|
|
type qwen3NextRopeParams struct {
|
|
MRopeInterleaved bool `json:"mrope_interleaved"`
|
|
MropeSection []int32 `json:"mrope_section"`
|
|
RopeType string `json:"rope_type"`
|
|
RopeTheta float32 `json:"rope_theta"`
|
|
PartialRotaryFactor float32 `json:"partial_rotary_factor"`
|
|
}
|
|
|
|
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"`
|
|
HeadDim uint32 `json:"head_dim"`
|
|
RopeTheta float32 `json:"rope_theta"`
|
|
RMSNormEPS float32 `json:"rms_norm_eps"`
|
|
|
|
// MoE config
|
|
NumExperts uint32 `json:"num_experts"`
|
|
NumExpertsPerToken uint32 `json:"num_experts_per_tok"`
|
|
NormTopkProb *bool `json:"norm_topk_prob"`
|
|
MoEIntermediateSize uint32 `json:"moe_intermediate_size"`
|
|
SharedExpertIntermSize uint32 `json:"shared_expert_intermediate_size"`
|
|
|
|
// Hybrid attention config
|
|
FullAttentionInterval uint32 `json:"full_attention_interval"`
|
|
LayerTypes []string `json:"layer_types"`
|
|
|
|
// Linear attention (Gated Delta Net) config
|
|
LinearConvKernelDim uint32 `json:"linear_conv_kernel_dim"`
|
|
LinearKeyHeadDim uint32 `json:"linear_key_head_dim"`
|
|
LinearNumKeyHeads uint32 `json:"linear_num_key_heads"`
|
|
LinearNumValueHeads uint32 `json:"linear_num_value_heads"`
|
|
LinearValueHeadDim uint32 `json:"linear_value_head_dim"`
|
|
|
|
// RoPE config
|
|
PartialRotaryFactor float32 `json:"partial_rotary_factor"`
|
|
RopeScaling qwen3NextRopeScaling `json:"rope_scaling"`
|
|
RopeParameters qwen3NextRopeParams `json:"rope_parameters"`
|
|
}
|
|
|
|
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"`
|
|
RopeTheta float32 `json:"rope_theta"`
|
|
TemporalPatchSize uint32 `json:"temporal_patch_size"`
|
|
DeepstackVisualIndexes []int32 `json:"deepstack_visual_indexes"`
|
|
|
|
Size struct {
|
|
ShortestEdge uint32 `json:"shortest_edge"`
|
|
LongestEdge uint32 `json:"longest_edge"`
|
|
} `json:"size"`
|
|
|
|
ImageMean []float32 `json:"image_mean"`
|
|
ImageStd []float32 `json:"image_std"`
|
|
}
|
|
|
|
type qwen3NextModel struct {
|
|
ModelParameters
|
|
qwen3NextTextConfig
|
|
|
|
TextConfig *qwen3NextTextConfig `json:"text_config"`
|
|
VisionModel qwen3NextVisionConfig `json:"vision_config"`
|
|
|
|
ImageTokenID uint32 `json:"image_token_id"`
|
|
VisionStartTokenID uint32 `json:"vision_start_token_id"`
|
|
VisionEndTokenID uint32 `json:"vision_end_token_id"`
|
|
}
|
|
|
|
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
|
|
}
|
|
if q.PartialRotaryFactor == 0 {
|
|
q.PartialRotaryFactor = q.RopeParameters.PartialRotaryFactor
|
|
}
|
|
|
|
if q.RopeScaling.Type == "" && q.RopeParameters.RopeType != "" {
|
|
q.RopeScaling.Type = q.RopeParameters.RopeType
|
|
}
|
|
|
|
// Pull vision preprocessing fields when present.
|
|
if q.VisionModel.Depth > 0 {
|
|
if bts, err := fs.ReadFile(fsys, "preprocessor_config.json"); err == nil {
|
|
var pre struct {
|
|
Size struct {
|
|
ShortestEdge uint32 `json:"shortest_edge"`
|
|
LongestEdge uint32 `json:"longest_edge"`
|
|
} `json:"size"`
|
|
PatchSize uint32 `json:"patch_size"`
|
|
TemporalPatchSize uint32 `json:"temporal_patch_size"`
|
|
MergeSize uint32 `json:"merge_size"`
|
|
ImageMean []float32 `json:"image_mean"`
|
|
ImageStd []float32 `json:"image_std"`
|
|
}
|
|
if json.Unmarshal(bts, &pre) == nil {
|
|
if q.VisionModel.PatchSize == 0 {
|
|
q.VisionModel.PatchSize = pre.PatchSize
|
|
}
|
|
if q.VisionModel.TemporalPatchSize == 0 {
|
|
q.VisionModel.TemporalPatchSize = pre.TemporalPatchSize
|
|
}
|
|
if q.VisionModel.SpatialMergeSize == 0 {
|
|
q.VisionModel.SpatialMergeSize = pre.MergeSize
|
|
}
|
|
if q.VisionModel.Size.ShortestEdge == 0 {
|
|
q.VisionModel.Size.ShortestEdge = pre.Size.ShortestEdge
|
|
}
|
|
if q.VisionModel.Size.LongestEdge == 0 {
|
|
q.VisionModel.Size.LongestEdge = pre.Size.LongestEdge
|
|
}
|
|
if len(q.VisionModel.ImageMean) == 0 {
|
|
q.VisionModel.ImageMean = pre.ImageMean
|
|
}
|
|
if len(q.VisionModel.ImageStd) == 0 {
|
|
q.VisionModel.ImageStd = pre.ImageStd
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if q.NumHiddenLayers == 0 {
|
|
return fmt.Errorf("qwen3next: num_hidden_layers must be set")
|
|
}
|
|
if q.NumAttentionHeads == 0 {
|
|
return fmt.Errorf("qwen3next: num_attention_heads must be set")
|
|
}
|
|
if q.NumKeyValueHeads == 0 {
|
|
return fmt.Errorf("qwen3next: num_key_value_heads must be set")
|
|
}
|
|
if q.HeadDim == 0 {
|
|
return fmt.Errorf("qwen3next: head_dim must be set")
|
|
}
|
|
if q.RopeTheta == 0 {
|
|
return fmt.Errorf("qwen3next: rope_theta must be set")
|
|
}
|
|
if q.PartialRotaryFactor <= 0 || q.PartialRotaryFactor > 1 {
|
|
return fmt.Errorf("qwen3next: partial_rotary_factor must be in (0,1], got %v", q.PartialRotaryFactor)
|
|
}
|
|
if q.LinearNumKeyHeads == 0 || q.LinearNumValueHeads == 0 || q.LinearKeyHeadDim == 0 || q.LinearValueHeadDim == 0 {
|
|
return fmt.Errorf("qwen3next: linear attention config must be set (linear_num_key_heads, linear_num_value_heads, linear_key_head_dim, linear_value_head_dim)")
|
|
}
|
|
if _, err := q.kvHeadCounts(); err != nil {
|
|
return err
|
|
}
|
|
|
|
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)
|
|
hasFull := false
|
|
hasRecurrent := false
|
|
for i := range q.NumHiddenLayers {
|
|
layerType := ""
|
|
if i < uint32(len(q.LayerTypes)) {
|
|
layerType = q.LayerTypes[i]
|
|
}
|
|
if layerType == "full_attention" {
|
|
kv[i] = q.NumKeyValueHeads
|
|
hasFull = true
|
|
} else {
|
|
hasRecurrent = true
|
|
}
|
|
}
|
|
if !hasFull || !hasRecurrent {
|
|
return nil, fmt.Errorf("qwen3next: layer_types must include both full_attention and linear_attention")
|
|
}
|
|
return kv, nil
|
|
}
|
|
|
|
if q.FullAttentionInterval == 0 {
|
|
return nil, fmt.Errorf("qwen3next: full_attention_interval must be set")
|
|
}
|
|
if q.FullAttentionInterval > q.NumHiddenLayers {
|
|
return nil, fmt.Errorf("qwen3next: full_attention_interval (%d) exceeds num_hidden_layers (%d)", q.FullAttentionInterval, q.NumHiddenLayers)
|
|
}
|
|
|
|
kv := make([]uint32, q.NumHiddenLayers)
|
|
hasFull := false
|
|
for i := range q.NumHiddenLayers {
|
|
if (i+1)%q.FullAttentionInterval == 0 {
|
|
kv[i] = q.NumKeyValueHeads
|
|
hasFull = true
|
|
}
|
|
}
|
|
if !hasFull {
|
|
return nil, fmt.Errorf("qwen3next: head_count_kv would be all zeros (full_attention_interval=%d, num_hidden_layers=%d)", q.FullAttentionInterval, q.NumHiddenLayers)
|
|
}
|
|
return kv, nil
|
|
}
|
|
|
|
func (q *qwen3NextModel) ropeSections() []int32 {
|
|
if len(q.RopeParameters.MropeSection) > 0 {
|
|
return q.RopeParameters.MropeSection
|
|
}
|
|
return q.RopeScaling.MropeSection
|
|
}
|
|
|
|
func (q *qwen3NextModel) shouldReorderVHeads() bool {
|
|
modelType := strings.ToLower(q.ModelType)
|
|
if strings.Contains(modelType, "qwen3_next") || strings.Contains(modelType, "qwen3next") {
|
|
return false
|
|
}
|
|
|
|
for _, arch := range q.Architectures {
|
|
arch = strings.ToLower(arch)
|
|
if strings.Contains(arch, "qwen3next") || strings.Contains(arch, "qwen3_next") {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Default to qwen3.5 layout for all other qwen3next-family imports.
|
|
return true
|
|
}
|
|
|
|
func (q *qwen3NextModel) KV(t *Tokenizer) KV {
|
|
kv := q.ModelParameters.KV(t)
|
|
|
|
arch := "qwen35"
|
|
if q.NumExperts > 0 {
|
|
arch = "qwen35moe"
|
|
}
|
|
kv["general.architecture"] = arch
|
|
kv["tokenizer.ggml.pre"] = "qwen35"
|
|
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
|
|
kv["attention.head_count"] = q.NumAttentionHeads
|
|
|
|
headDim := q.HeadDim
|
|
if headDim == 0 && q.NumAttentionHeads > 0 {
|
|
headDim = q.HiddenSize / q.NumAttentionHeads
|
|
}
|
|
kv["attention.key_length"] = headDim
|
|
kv["attention.value_length"] = headDim
|
|
kv["attention.layer_norm_rms_epsilon"] = q.RMSNormEPS
|
|
kv["rope.freq_base"] = q.RopeTheta
|
|
|
|
partialRotary := q.PartialRotaryFactor
|
|
if partialRotary > 0 && partialRotary <= 1 {
|
|
kv["rope.dimension_count"] = uint32(float32(headDim) * partialRotary)
|
|
}
|
|
|
|
if sections := q.ropeSections(); len(sections) > 0 {
|
|
kv["mrope_sections"] = sections
|
|
kv["rope.mrope_section"] = 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
|
|
}
|
|
|
|
if q.RopeScaling.Type != "" && q.RopeScaling.Type != "default" {
|
|
kv["rope.scaling.type"] = q.RopeScaling.Type
|
|
kv["rope.scaling.factor"] = q.RopeScaling.Factor
|
|
}
|
|
|
|
if q.NumExperts > 0 {
|
|
kv["expert_count"] = q.NumExperts
|
|
kv["expert_used_count"] = q.NumExpertsPerToken
|
|
if q.NormTopkProb != nil {
|
|
kv["norm_top_k_prob"] = *q.NormTopkProb
|
|
}
|
|
if q.MoEIntermediateSize > 0 {
|
|
kv["expert_feed_forward_length"] = q.MoEIntermediateSize
|
|
}
|
|
if q.SharedExpertIntermSize > 0 {
|
|
kv["expert_shared_feed_forward_length"] = q.SharedExpertIntermSize
|
|
}
|
|
}
|
|
|
|
dInner := q.LinearValueHeadDim * q.LinearNumValueHeads
|
|
kv["ssm.inner_size"] = dInner
|
|
kv["ssm.state_size"] = q.LinearKeyHeadDim
|
|
kv["ssm.group_count"] = q.LinearNumKeyHeads
|
|
kv["ssm.time_step_rank"] = q.LinearNumValueHeads
|
|
kv["ssm.conv_kernel"] = q.LinearConvKernelDim
|
|
if q.shouldReorderVHeads() {
|
|
kv["ssm.v_head_reordered"] = true
|
|
}
|
|
if q.FullAttentionInterval > 0 {
|
|
kv["full_attention_interval"] = q.FullAttentionInterval
|
|
}
|
|
|
|
if headCounts, err := q.kvHeadCounts(); err == nil {
|
|
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 {
|
|
kv["vision.patch_size"] = q.VisionModel.PatchSize
|
|
}
|
|
if q.VisionModel.SpatialMergeSize > 0 {
|
|
kv["vision.spatial_merge_size"] = q.VisionModel.SpatialMergeSize
|
|
}
|
|
if q.VisionModel.RMSNormEps > 0 {
|
|
kv["vision.attention.layer_norm_epsilon"] = q.VisionModel.RMSNormEps
|
|
}
|
|
if q.VisionModel.RopeTheta > 0 {
|
|
kv["vision.rope.freq_base"] = q.VisionModel.RopeTheta
|
|
}
|
|
if q.VisionModel.TemporalPatchSize > 0 {
|
|
kv["vision.temporal_patch_size"] = q.VisionModel.TemporalPatchSize
|
|
}
|
|
kv["vision.deepstack_visual_indexes"] = q.VisionModel.DeepstackVisualIndexes
|
|
if q.VisionModel.Size.ShortestEdge > 0 {
|
|
kv["vision.shortest_edge"] = q.VisionModel.Size.ShortestEdge
|
|
}
|
|
if q.VisionModel.Size.LongestEdge > 0 {
|
|
kv["vision.longest_edge"] = q.VisionModel.Size.LongestEdge
|
|
}
|
|
if len(q.VisionModel.ImageMean) > 0 {
|
|
kv["vision.image_mean"] = q.VisionModel.ImageMean
|
|
}
|
|
if len(q.VisionModel.ImageStd) > 0 {
|
|
kv["vision.image_std"] = q.VisionModel.ImageStd
|
|
}
|
|
}
|
|
|
|
if q.ImageTokenID > 0 {
|
|
kv["image_token_id"] = q.ImageTokenID
|
|
}
|
|
if q.VisionStartTokenID > 0 {
|
|
kv["vision_start_token_id"] = q.VisionStartTokenID
|
|
}
|
|
if q.VisionEndTokenID > 0 {
|
|
kv["vision_end_token_id"] = q.VisionEndTokenID
|
|
}
|
|
|
|
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
|
|
|
|
merges := make([]merge, q.NumHiddenLayers*3)
|
|
for i := range q.NumHiddenLayers {
|
|
merges[i*3+0] = merge{
|
|
fmt.Sprintf("blk.%d.mlp.experts.*.gate_proj.weight", i),
|
|
fmt.Sprintf("blk.%d.ffn_gate_exps.weight", i),
|
|
}
|
|
merges[i*3+1] = merge{
|
|
fmt.Sprintf("blk.%d.mlp.experts.*.up_proj.weight", i),
|
|
fmt.Sprintf("blk.%d.ffn_up_exps.weight", i),
|
|
}
|
|
merges[i*3+2] = merge{
|
|
fmt.Sprintf("blk.%d.mlp.experts.*.down_proj.weight", i),
|
|
fmt.Sprintf("blk.%d.ffn_down_exps.weight", i),
|
|
}
|
|
}
|
|
|
|
merged, remaining := mergeTensors(ts, merges...)
|
|
out = append(out, merged...)
|
|
|
|
for _, t := range remaining {
|
|
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)
|
|
continue
|
|
}
|
|
panic(fmt.Sprintf("qwen3next: failed to split %s into attn_qkv/attn_gate (shape=%v)", name, shape))
|
|
}
|
|
|
|
switch {
|
|
case strings.Contains(name, ".mlp.experts.gate_up_proj"):
|
|
out = append(out, slices.Collect(splitDim(t, 1,
|
|
split{Replacer: strings.NewReplacer(".mlp.experts.gate_up_proj", ".ffn_gate_exps.weight")},
|
|
split{Replacer: strings.NewReplacer(".mlp.experts.gate_up_proj", ".ffn_up_exps.weight")},
|
|
))...)
|
|
|
|
case strings.Contains(name, ".mlp.experts.down_proj"):
|
|
out = append(out, &ggml.Tensor{
|
|
Name: strings.NewReplacer(".mlp.experts.down_proj", ".ffn_down_exps.weight").Replace(name),
|
|
Kind: t.Kind(),
|
|
Shape: slices.Clone(shape),
|
|
WriterTo: t,
|
|
})
|
|
|
|
case strings.HasPrefix(name, "v.blk.") && strings.Contains(name, ".attn_qkv"):
|
|
out = append(out, slices.Collect(splitDim(t, 0,
|
|
split{Replacer: strings.NewReplacer("attn_qkv", "attn_q")},
|
|
split{Replacer: strings.NewReplacer("attn_qkv", "attn_k")},
|
|
split{Replacer: strings.NewReplacer("attn_qkv", "attn_v")},
|
|
))...)
|
|
|
|
case strings.Contains(name, "patch_embed") && strings.HasSuffix(name, "weight"):
|
|
out = append(out, &ggml.Tensor{
|
|
Name: name,
|
|
Kind: t.Kind(),
|
|
Shape: append([]uint64{shape[0] * shape[1]}, shape[2:]...),
|
|
WriterTo: t,
|
|
})
|
|
|
|
case strings.HasSuffix(name, "_norm.weight") && !strings.HasSuffix(name, ".ssm_norm.weight"):
|
|
t.SetRepacker(q.addOne)
|
|
out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(shape), WriterTo: t})
|
|
|
|
case strings.HasSuffix(name, ".ssm_a"):
|
|
t.SetRepacker(q.repackSSMA())
|
|
out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(shape), WriterTo: t})
|
|
|
|
case strings.HasSuffix(name, ".attn_qkv.weight"):
|
|
if q.shouldReorderVHeads() {
|
|
t.SetRepacker(q.repackAttnQKV())
|
|
}
|
|
out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(shape), WriterTo: t})
|
|
|
|
case strings.HasSuffix(name, ".attn_gate.weight"):
|
|
if q.shouldReorderVHeads() {
|
|
// HF tensor layout is [out_features, in_features]; reorder rows.
|
|
t.SetRepacker(q.repackReorderDim(0, int(q.LinearValueHeadDim)))
|
|
}
|
|
out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(shape), WriterTo: t})
|
|
|
|
case strings.HasSuffix(name, ".ssm_beta.weight"), strings.HasSuffix(name, ".ssm_alpha.weight"):
|
|
if q.shouldReorderVHeads() {
|
|
// HF tensor layout is [out_features, in_features]; reorder rows.
|
|
t.SetRepacker(q.repackReorderDim(0, 1))
|
|
}
|
|
out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(shape), WriterTo: t})
|
|
|
|
case strings.HasSuffix(name, ".ssm_dt"), strings.HasSuffix(name, ".ssm_dt.bias"):
|
|
if q.shouldReorderVHeads() {
|
|
t.SetRepacker(q.repackReorderDim(0, 1))
|
|
}
|
|
out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(shape), WriterTo: t})
|
|
|
|
case strings.HasSuffix(name, ".ssm_out.weight"):
|
|
if q.shouldReorderVHeads() {
|
|
// HF out_proj layout is [out_features, in_features]; reorder columns.
|
|
t.SetRepacker(q.repackReorderDim(1, int(q.LinearValueHeadDim)))
|
|
}
|
|
out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(shape), WriterTo: t})
|
|
|
|
case strings.HasSuffix(name, ".ssm_conv1d.weight"):
|
|
newShape := slices.Clone(shape)
|
|
if len(shape) == 3 {
|
|
if shape[0] == 1 {
|
|
newShape = []uint64{shape[1], shape[2]}
|
|
} else if shape[1] == 1 {
|
|
newShape = []uint64{shape[0], shape[2]}
|
|
}
|
|
}
|
|
if q.shouldReorderVHeads() {
|
|
t.SetRepacker(q.repackConv1D())
|
|
}
|
|
out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: newShape, WriterTo: t})
|
|
|
|
default:
|
|
out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(shape), WriterTo: t})
|
|
}
|
|
}
|
|
|
|
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() {
|
|
return data, nil
|
|
}
|
|
numK := int(q.LinearNumKeyHeads)
|
|
numVPerK := int(q.LinearNumValueHeads / q.LinearNumKeyHeads)
|
|
return reorderHeadLayout(data, shape, dim, numK, numVPerK, headDim)
|
|
}
|
|
}
|
|
|
|
func (q *qwen3NextModel) repackAttnQKV() Repacker {
|
|
return func(_ string, data []float32, shape []uint64) ([]float32, error) {
|
|
if !q.shouldReorderVHeads() || len(shape) != 2 {
|
|
return data, nil
|
|
}
|
|
|
|
rows := int(shape[0])
|
|
cols := int(shape[1])
|
|
numK := int(q.LinearNumKeyHeads)
|
|
numV := int(q.LinearNumValueHeads)
|
|
headK := int(q.LinearKeyHeadDim)
|
|
headV := int(q.LinearValueHeadDim)
|
|
qDim := headK * numK
|
|
kDim := headK * numK
|
|
vDim := headV * numV
|
|
qkvDim := qDim + kDim + vDim
|
|
|
|
switch {
|
|
case rows == qkvDim:
|
|
// HF layout: [out_features, in_features]. Keep Q/K rows unchanged and
|
|
// reorder only V rows from grouped -> tiled head layout.
|
|
out := make([]float32, len(data))
|
|
qkRows := qDim + kDim
|
|
qkSize := qkRows * cols
|
|
copy(out[:qkSize], data[:qkSize])
|
|
|
|
vStart := qkSize
|
|
vEnd := vStart + vDim*cols
|
|
reorderedV, err := reorderHeadLayout(data[vStart:vEnd], []uint64{uint64(vDim), uint64(cols)}, 0, numK, numV/numK, headV)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copy(out[vStart:vEnd], reorderedV)
|
|
copy(out[vEnd:], data[vEnd:])
|
|
return out, nil
|
|
|
|
case cols == qkvDim:
|
|
// Fallback for already-transposed [in_features, out_features] tensors.
|
|
out := make([]float32, len(data))
|
|
copy(out, data)
|
|
for r := range rows {
|
|
base := r * cols
|
|
vStart := base + qDim + kDim
|
|
vEnd := vStart + vDim
|
|
reorderedV, err := reorderHeadLayout(out[vStart:vEnd], []uint64{uint64(vDim)}, 0, numK, numV/numK, headV)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copy(out[vStart:vEnd], reorderedV)
|
|
}
|
|
return out, nil
|
|
|
|
default:
|
|
return data, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (q *qwen3NextModel) repackConv1D() Repacker {
|
|
return func(_ string, data []float32, shape []uint64) ([]float32, error) {
|
|
if !q.shouldReorderVHeads() {
|
|
return data, nil
|
|
}
|
|
|
|
normShape := slices.Clone(shape)
|
|
if len(shape) == 3 {
|
|
if shape[0] == 1 {
|
|
normShape = []uint64{shape[1], shape[2]}
|
|
} else if shape[1] == 1 {
|
|
normShape = []uint64{shape[0], shape[2]}
|
|
}
|
|
}
|
|
if len(normShape) != 2 {
|
|
return data, nil
|
|
}
|
|
|
|
rows := int(normShape[0])
|
|
cols := int(normShape[1])
|
|
numK := int(q.LinearNumKeyHeads)
|
|
numV := int(q.LinearNumValueHeads)
|
|
headK := int(q.LinearKeyHeadDim)
|
|
headV := int(q.LinearValueHeadDim)
|
|
qkChannels := 2 * headK * numK
|
|
totalChannels := qkChannels + headV*numV
|
|
if qkChannels <= 0 {
|
|
return data, nil
|
|
}
|
|
|
|
switch {
|
|
case rows == totalChannels:
|
|
// HF layout after squeeze: [channels, kernel]
|
|
out := make([]float32, len(data))
|
|
prefix := qkChannels * cols
|
|
copy(out[:prefix], data[:prefix])
|
|
reorderedV, err := reorderHeadLayout(data[prefix:], []uint64{uint64(totalChannels - qkChannels), uint64(cols)}, 0, numK, numV/numK, headV)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copy(out[prefix:], reorderedV)
|
|
return out, nil
|
|
case cols == totalChannels:
|
|
// Fallback for transposed [kernel, channels]
|
|
out := make([]float32, len(data))
|
|
copy(out, data)
|
|
vChannels := totalChannels - qkChannels
|
|
for r := range rows {
|
|
base := r * cols
|
|
vStart := base + qkChannels
|
|
vEnd := vStart + vChannels
|
|
reorderedV, err := reorderHeadLayout(out[vStart:vEnd], []uint64{uint64(vChannels)}, 0, numK, numV/numK, headV)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copy(out[vStart:vEnd], reorderedV)
|
|
}
|
|
return out, nil
|
|
default:
|
|
return data, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (q *qwen3NextModel) repackSSMA() Repacker {
|
|
return func(_ string, data []float32, shape []uint64) ([]float32, error) {
|
|
result := make([]float32, len(data))
|
|
for i, v := range data {
|
|
result[i] = -float32(math.Exp(float64(v)))
|
|
}
|
|
if !q.shouldReorderVHeads() {
|
|
return result, nil
|
|
}
|
|
numK := int(q.LinearNumKeyHeads)
|
|
numVPerK := int(q.LinearNumValueHeads / q.LinearNumKeyHeads)
|
|
return reorderHeadLayout(result, shape, 0, numK, numVPerK, 1)
|
|
}
|
|
}
|
|
|
|
func reorderHeadLayout(data []float32, shape []uint64, dim int, numKHeads, numVPerK, headDim int) ([]float32, error) {
|
|
if len(shape) == 0 || numKHeads <= 0 || numVPerK <= 0 || headDim <= 0 {
|
|
return data, nil
|
|
}
|
|
|
|
dims := make([]int, len(shape))
|
|
for i := range shape {
|
|
dims[i] = int(shape[i])
|
|
}
|
|
if dim < 0 {
|
|
dim += len(dims)
|
|
}
|
|
if dim < 0 || dim >= len(dims) {
|
|
return data, nil
|
|
}
|
|
|
|
expected := numKHeads * numVPerK * headDim
|
|
if dims[dim] != expected {
|
|
return data, nil
|
|
}
|
|
|
|
newShape := make([]int, 0, len(dims)+2)
|
|
newShape = append(newShape, dims[:dim]...)
|
|
newShape = append(newShape, numKHeads, numVPerK, headDim)
|
|
newShape = append(newShape, dims[dim+1:]...)
|
|
|
|
var tt tensor.Tensor = tensor.New(tensor.WithShape(dims...), tensor.WithBacking(data))
|
|
if err := tt.Reshape(newShape...); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
perm := make([]int, len(newShape))
|
|
for i := range perm {
|
|
perm[i] = i
|
|
}
|
|
perm[dim], perm[dim+1] = perm[dim+1], perm[dim]
|
|
|
|
tt, err := tensor.Transpose(tt, perm...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tt = tensor.Materialize(tt)
|
|
|
|
total := 1
|
|
for _, d := range dims {
|
|
total *= d
|
|
}
|
|
if err := tt.Reshape(total); err != nil {
|
|
return nil, err
|
|
}
|
|
return native.VectorF32(tt.(*tensor.Dense))
|
|
}
|
|
|
|
type qkvzSplitSpec struct {
|
|
hidden int
|
|
headKDim int
|
|
headVDim int
|
|
numKHeads int
|
|
numVHeads int
|
|
qkvzDim int
|
|
qkvOut int
|
|
gateOut int
|
|
}
|
|
|
|
func (q *qwen3NextModel) qkvzSpec(shape []uint64) (qkvzSplitSpec, bool) {
|
|
if len(shape) != 2 {
|
|
return qkvzSplitSpec{}, false
|
|
}
|
|
|
|
numKHeads := int(q.LinearNumKeyHeads)
|
|
numVHeads := int(q.LinearNumValueHeads)
|
|
headKDim := int(q.LinearKeyHeadDim)
|
|
headVDim := int(q.LinearValueHeadDim)
|
|
if numKHeads == 0 || numVHeads == 0 || headKDim == 0 || headVDim == 0 {
|
|
return qkvzSplitSpec{}, false
|
|
}
|
|
if numVHeads%numKHeads != 0 {
|
|
return qkvzSplitSpec{}, false
|
|
}
|
|
|
|
hidden := int(shape[1])
|
|
vPerHead := headVDim * (numVHeads / numKHeads)
|
|
qkvzDim := 2*headKDim + 2*vPerHead
|
|
expectedOut := qkvzDim * numKHeads
|
|
if int(shape[0]) != expectedOut {
|
|
return qkvzSplitSpec{}, false
|
|
}
|
|
|
|
return qkvzSplitSpec{
|
|
hidden: hidden,
|
|
headKDim: headKDim,
|
|
headVDim: headVDim,
|
|
numKHeads: numKHeads,
|
|
numVHeads: numVHeads,
|
|
qkvzDim: qkvzDim,
|
|
qkvOut: 2*headKDim*numKHeads + headVDim*numVHeads,
|
|
gateOut: headVDim * numVHeads,
|
|
}, true
|
|
}
|
|
|
|
func (q *qwen3NextModel) splitQKVZTensor(t Tensor) (*ggml.Tensor, *ggml.Tensor, bool) {
|
|
spec, ok := q.qkvzSpec(t.Shape())
|
|
if !ok {
|
|
return nil, nil, false
|
|
}
|
|
|
|
qkvTensor := t.Clone()
|
|
qkvTensor.SetRepacker(q.repackQKVZ(spec, false))
|
|
|
|
gateTensor := t.Clone()
|
|
gateTensor.SetRepacker(q.repackQKVZ(spec, true))
|
|
|
|
qkvName := strings.Replace(t.Name(), "ssm_in", "attn_qkv", 1)
|
|
gateName := strings.Replace(t.Name(), "ssm_in", "attn_gate", 1)
|
|
|
|
return &ggml.Tensor{
|
|
Name: qkvName,
|
|
Kind: t.Kind(),
|
|
Shape: []uint64{uint64(spec.qkvOut), uint64(spec.hidden)},
|
|
WriterTo: qkvTensor,
|
|
}, &ggml.Tensor{
|
|
Name: gateName,
|
|
Kind: t.Kind(),
|
|
Shape: []uint64{uint64(spec.gateOut), uint64(spec.hidden)},
|
|
WriterTo: gateTensor,
|
|
}, true
|
|
}
|
|
|
|
func (q *qwen3NextModel) repackQKVZ(spec qkvzSplitSpec, extractGate bool) Repacker {
|
|
vPerHead := spec.headVDim * (spec.numVHeads / spec.numKHeads)
|
|
|
|
return 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))
|
|
var err error
|
|
|
|
tt, err = tensor.Transpose(tt, 1, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tt = tensor.Materialize(tt)
|
|
|
|
if err := tt.Reshape(spec.hidden, spec.numKHeads, spec.qkvzDim); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
offset := 0
|
|
qSlice, err := tt.Slice(nil, nil, tensor.S(offset, offset+spec.headKDim))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
offset += spec.headKDim
|
|
kSlice, err := tt.Slice(nil, nil, tensor.S(offset, offset+spec.headKDim))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
offset += spec.headKDim
|
|
vSlice, err := tt.Slice(nil, nil, tensor.S(offset, offset+vPerHead))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
offset += vPerHead
|
|
zSlice, err := tt.Slice(nil, nil, tensor.S(offset, offset+vPerHead))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
qMat := tensor.Materialize(qSlice).(*tensor.Dense)
|
|
kMat := tensor.Materialize(kSlice).(*tensor.Dense)
|
|
vMat := tensor.Materialize(vSlice).(*tensor.Dense)
|
|
zMat := tensor.Materialize(zSlice).(*tensor.Dense)
|
|
|
|
if err := qMat.Reshape(spec.hidden, spec.numKHeads*spec.headKDim); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := kMat.Reshape(spec.hidden, spec.numKHeads*spec.headKDim); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := vMat.Reshape(spec.hidden, spec.numKHeads*vPerHead); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := zMat.Reshape(spec.hidden, spec.numKHeads*vPerHead); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var out tensor.Tensor
|
|
if extractGate {
|
|
out = zMat
|
|
} else {
|
|
out, err = tensor.Concat(1, qMat, kMat, vMat)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
out = tensor.Materialize(out)
|
|
out, err = tensor.Transpose(out, 1, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = tensor.Materialize(out)
|
|
|
|
if err := out.Reshape(out.Shape().TotalSize()); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return native.VectorF32(out.(*tensor.Dense))
|
|
}
|
|
}
|
|
|
|
func (*qwen3NextModel) addOne(_ string, data []float32, shape []uint64) ([]float32, error) {
|
|
n := tensor.New(tensor.WithShape(int(shape[0])), tensor.WithBacking(data))
|
|
ones := tensor.Ones(tensor.Float32, int(shape[0]))
|
|
|
|
n, err := n.Add(ones)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ts, err := native.SelectF32(n, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var f32s []float32
|
|
for _, t := range ts {
|
|
f32s = append(f32s, t...)
|
|
}
|
|
|
|
return f32s, nil
|
|
}
|
|
|
|
func (q *qwen3NextModel) Replacements() []string {
|
|
return []string{
|
|
// Embeddings and output
|
|
"lm_head", "output",
|
|
"model.language_model.embed_tokens", "token_embd",
|
|
"model.language_model.norm", "output_norm",
|
|
"model.language_model.layers", "blk",
|
|
"model.embed_tokens", "token_embd",
|
|
"model.norm", "output_norm",
|
|
"model.layers", "blk",
|
|
|
|
// Vision
|
|
"model.visual", "v",
|
|
"patch_embed.proj", "patch_embed",
|
|
"blocks", "blk",
|
|
"attn.qkv", "attn_qkv",
|
|
"attn.proj", "attn_out",
|
|
"deepstack_merger_list", "deepstack_merger",
|
|
|
|
// Layer norms
|
|
"input_layernorm", "attn_norm",
|
|
"post_attention_layernorm", "post_attention_norm",
|
|
|
|
// Full attention (self_attn)
|
|
"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",
|
|
|
|
// Linear attention (legacy qwen3next)
|
|
"linear_attn.in_proj_qkvz", "ssm_in",
|
|
"linear_attn.in_proj_ba", "ssm_ba",
|
|
|
|
// Linear attention (qwen35)
|
|
"linear_attn.in_proj_qkv", "attn_qkv",
|
|
"linear_attn.in_proj_z", "attn_gate",
|
|
"linear_attn.in_proj_a", "ssm_alpha",
|
|
"linear_attn.in_proj_b", "ssm_beta",
|
|
|
|
"linear_attn.conv1d", "ssm_conv1d",
|
|
"linear_attn.dt_bias", "ssm_dt.bias",
|
|
"linear_attn.dt_proj", "ssm_dt",
|
|
"linear_attn.A_log", "ssm_a",
|
|
"linear_attn.norm", "ssm_norm",
|
|
"linear_attn.out_proj", "ssm_out",
|
|
|
|
// MoE
|
|
"mlp.gate.weight", "ffn_gate_inp.weight",
|
|
"mlp.shared_expert.down_proj", "ffn_down_shexp",
|
|
"mlp.shared_expert.gate_proj", "ffn_gate_shexp",
|
|
"mlp.shared_expert.up_proj", "ffn_up_shexp",
|
|
"mlp.shared_expert_gate", "ffn_gate_inp_shexp",
|
|
|
|
// Dense FFN
|
|
"mlp.down_proj", "ffn_down",
|
|
"mlp.gate_proj", "ffn_gate",
|
|
"mlp.up_proj", "ffn_up",
|
|
}
|
|
}
|