Safetensors
GGUF
Turkish
llama
Llama-3
instruct
finetune
chatml
gpt4
synthetic data
distillation
function calling
json mode
axolotl
roleplaying
chat
Instructions to use tda45/TdAI with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- llama-cpp-python
How to use tda45/TdAI with llama-cpp-python:
# !pip install llama-cpp-python from llama_cpp import Llama llm = Llama.from_pretrained( repo_id="tda45/TdAI", filename="llama.cpp/models/ggml-vocab-aquila.gguf", )
output = llm( "Once upon a time,", max_tokens=512, echo=True ) print(output)
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use tda45/TdAI with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf tda45/TdAI # Run inference directly in the terminal: llama cli -hf tda45/TdAI
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf tda45/TdAI # Run inference directly in the terminal: llama cli -hf tda45/TdAI
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf tda45/TdAI # Run inference directly in the terminal: ./llama-cli -hf tda45/TdAI
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf tda45/TdAI # Run inference directly in the terminal: ./build/bin/llama-cli -hf tda45/TdAI
Use Docker
docker model run hf.co/tda45/TdAI
- LM Studio
- Jan
- Ollama
How to use tda45/TdAI with Ollama:
ollama run hf.co/tda45/TdAI
- Unsloth Studio
How to use tda45/TdAI with Unsloth Studio:
Install Unsloth Studio (macOS, Linux, WSL)
curl -fsSL https://unsloth.ai/install.sh | sh # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for tda45/TdAI to start chatting
Install Unsloth Studio (Windows)
irm https://unsloth.ai/install.ps1 | iex # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for tda45/TdAI to start chatting
Using HuggingFace Spaces for Unsloth
# No setup required # Open https://huggingface.co/spaces/unsloth/studio in your browser # Search for tda45/TdAI to start chatting
- Atomic Chat new
- Docker Model Runner
How to use tda45/TdAI with Docker Model Runner:
docker model run hf.co/tda45/TdAI
- Lemonade
How to use tda45/TdAI with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull tda45/TdAI
Run and chat with the model
lemonade run user.TdAI-{{QUANT_TAG}}List all available models
lemonade list
Upload folder using huggingface_hub (part 2)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +2 -0
- llama.cpp/conversion/llama.py +444 -0
- llama.cpp/conversion/llama4.py +38 -0
- llama.cpp/conversion/llava.py +129 -0
- llama.cpp/conversion/maincoder.py +14 -0
- llama.cpp/conversion/mamba.py +198 -0
- llama.cpp/conversion/mellum.py +61 -0
- llama.cpp/conversion/mimo.py +295 -0
- llama.cpp/conversion/minicpm.py +180 -0
- llama.cpp/conversion/minimax.py +54 -0
- llama.cpp/conversion/mistral.py +202 -0
- llama.cpp/conversion/mistral3.py +67 -0
- llama.cpp/conversion/mpt.py +49 -0
- llama.cpp/conversion/nemotron.py +385 -0
- llama.cpp/conversion/olmo.py +120 -0
- llama.cpp/conversion/openelm.py +83 -0
- llama.cpp/conversion/orion.py +37 -0
- llama.cpp/conversion/pangu.py +46 -0
- llama.cpp/conversion/phi.py +388 -0
- llama.cpp/conversion/pixtral.py +41 -0
- llama.cpp/conversion/plamo.py +195 -0
- llama.cpp/conversion/plm.py +23 -0
- llama.cpp/conversion/qwen.py +675 -0
- llama.cpp/conversion/qwen3vl.py +360 -0
- llama.cpp/conversion/qwenvl.py +200 -0
- llama.cpp/conversion/refact.py +68 -0
- llama.cpp/conversion/rwkv.py +302 -0
- llama.cpp/conversion/sarashina2.py +32 -0
- llama.cpp/conversion/smallthinker.py +82 -0
- llama.cpp/conversion/smolvlm.py +47 -0
- llama.cpp/conversion/stablelm.py +98 -0
- llama.cpp/conversion/starcoder.py +23 -0
- llama.cpp/conversion/step3.py +337 -0
- llama.cpp/conversion/t5.py +286 -0
- llama.cpp/conversion/talkie.py +53 -0
- llama.cpp/conversion/ultravox.py +203 -0
- llama.cpp/conversion/wavtokenizer.py +45 -0
- llama.cpp/conversion/xverse.py +90 -0
- llama.cpp/conversion/youtuvl.py +64 -0
- llama.cpp/convert_hf_to_gguf.py +298 -0
- llama.cpp/convert_hf_to_gguf_update.py +497 -0
- llama.cpp/convert_llama_ggml_to_gguf.py +450 -0
- llama.cpp/convert_lora_to_gguf.py +546 -0
- llama.cpp/docs/android.md +103 -0
- llama.cpp/docs/android/imported-into-android-studio.jpg +3 -0
- llama.cpp/docs/autoparser.md +534 -0
- llama.cpp/docs/backend/BLIS.md +60 -0
- llama.cpp/docs/backend/CANN.md +357 -0
- llama.cpp/docs/backend/CUDA-FEDORA.md +283 -0
- llama.cpp/docs/backend/OPENCL.md +294 -0
.gitattributes
CHANGED
|
@@ -34,3 +34,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
*.gguf filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
*.gguf filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
llama.cpp/docs/android/imported-into-android-studio.jpg filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
llama.cpp/docs/development/llama-star/idea-arch.key filter=lfs diff=lfs merge=lfs -text
|
llama.cpp/conversion/llama.py
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import math
|
| 5 |
+
|
| 6 |
+
from typing import Callable, Iterable, TYPE_CHECKING
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
|
| 11 |
+
if TYPE_CHECKING:
|
| 12 |
+
from torch import Tensor
|
| 13 |
+
|
| 14 |
+
from .base import ModelBase, TextModel, gguf, logger
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@ModelBase.register(
|
| 18 |
+
"LLaMAForCausalLM",
|
| 19 |
+
"LlamaForCausalLM",
|
| 20 |
+
"MistralForCausalLM",
|
| 21 |
+
"MixtralForCausalLM",
|
| 22 |
+
"VLlama3ForCausalLM",
|
| 23 |
+
"LlavaForConditionalGeneration",
|
| 24 |
+
"VoxtralForConditionalGeneration",
|
| 25 |
+
"LlamaForCausalLMEagle3",
|
| 26 |
+
"Eagle3LlamaForCausalLM",
|
| 27 |
+
"Eagle3Speculator",
|
| 28 |
+
"Eagle3DraftModel",
|
| 29 |
+
"IQuestCoderForCausalLM",
|
| 30 |
+
"LlamaModel")
|
| 31 |
+
class LlamaModel(TextModel):
|
| 32 |
+
model_arch = gguf.MODEL_ARCH.LLAMA
|
| 33 |
+
undo_permute = True
|
| 34 |
+
|
| 35 |
+
def __init__(self, *args, **kwargs):
|
| 36 |
+
super().__init__(*args, **kwargs)
|
| 37 |
+
# fix for SmolVLM2, missing `num_attention_heads` in config.json
|
| 38 |
+
if self.hf_arch == "VLlama3ForCausalLM":
|
| 39 |
+
self.hparams["num_attention_heads"] = self.hparams.get("num_attention_heads", 32)
|
| 40 |
+
# Mistral consolidated format has no config.json; origin_hf_arch is HF-only.
|
| 41 |
+
if self.is_mistral_format:
|
| 42 |
+
self.origin_hf_arch = None
|
| 43 |
+
else:
|
| 44 |
+
hparams = ModelBase.load_hparams(self.dir_model, is_mistral_format=False)
|
| 45 |
+
self.origin_hf_arch = hparams.get('architectures', [None])[0]
|
| 46 |
+
|
| 47 |
+
# Detect eagle3 draft checkpoint by hparams (some models don't use a distinct HF arch name)
|
| 48 |
+
if "draft_vocab_size" in self.hparams and self.hparams["num_hidden_layers"] == 1:
|
| 49 |
+
self.is_eagle3 = True
|
| 50 |
+
self.model_arch = gguf.MODEL_ARCH.EAGLE3
|
| 51 |
+
logger.info("Detected EAGLE-3 draft model, switching to EAGLE3 architecture")
|
| 52 |
+
# Re-initialize tensor_map with eagle3 architecture
|
| 53 |
+
self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
|
| 54 |
+
# Update gguf_writer architecture
|
| 55 |
+
self.gguf_writer.arch = gguf.MODEL_ARCH_NAMES[self.model_arch]
|
| 56 |
+
self.gguf_writer.add_architecture()
|
| 57 |
+
if self.target_model_dir is None:
|
| 58 |
+
raise ValueError(
|
| 59 |
+
"EAGLE-3 model requires --target-model-dir to be specified. "
|
| 60 |
+
"Please provide the path to the target model directory to read config.json"
|
| 61 |
+
)
|
| 62 |
+
# Read both eagle3 raw config and target model config
|
| 63 |
+
with open(self.dir_model / "config.json", 'r', encoding='utf-8') as f:
|
| 64 |
+
eagle3_raw_config = json.load(f)
|
| 65 |
+
with open(self.target_model_dir / "config.json", 'r', encoding='utf-8') as f:
|
| 66 |
+
target_config = json.load(f)
|
| 67 |
+
|
| 68 |
+
if "text_config" in target_config:
|
| 69 |
+
target_config = {**target_config, **target_config["text_config"]}
|
| 70 |
+
self.target_vocab_size = target_config["vocab_size"]
|
| 71 |
+
|
| 72 |
+
# target_layers: derived from target model layer count (low/mid/high)
|
| 73 |
+
target_num_layers = target_config["num_hidden_layers"]
|
| 74 |
+
target_layers = [2, target_num_layers // 2, target_num_layers - 3]
|
| 75 |
+
logger.info(f"EAGLE-3: target_layers = {target_layers} (target model has {target_num_layers} layers)")
|
| 76 |
+
self.gguf_writer.add_target_layers(target_layers)
|
| 77 |
+
|
| 78 |
+
# target_hidden_size: prefer eagle3 config, fallback to target config
|
| 79 |
+
if eagle3_raw_config.get("target_hidden_size") is not None:
|
| 80 |
+
target_hidden_size = eagle3_raw_config["target_hidden_size"]
|
| 81 |
+
src = "EAGLE-3 config"
|
| 82 |
+
else:
|
| 83 |
+
target_hidden_size = target_config["hidden_size"]
|
| 84 |
+
src = "target model config"
|
| 85 |
+
logger.info(f"EAGLE-3: target_hidden_size = {target_hidden_size} (from {src})")
|
| 86 |
+
self.gguf_writer.add_target_hidden_size(target_hidden_size)
|
| 87 |
+
|
| 88 |
+
# norm_before_residual (RedHat-style eagle3 specific)
|
| 89 |
+
norm_before_residual = eagle3_raw_config.get("norm_before_residual", False)
|
| 90 |
+
logger.info(f"EAGLE-3: norm_before_residual = {norm_before_residual}")
|
| 91 |
+
self.gguf_writer.add_norm_before_residual(norm_before_residual)
|
| 92 |
+
|
| 93 |
+
def set_vocab(self):
|
| 94 |
+
# eagle3: use tokenizer from target model if provided
|
| 95 |
+
original_dir_model = None
|
| 96 |
+
if getattr(self, 'is_eagle3', False):
|
| 97 |
+
assert self.target_model_dir is not None
|
| 98 |
+
logger.info(f"EAGLE-3: Using tokenizer from target model: {self.target_model_dir}")
|
| 99 |
+
original_dir_model = self.dir_model
|
| 100 |
+
self.dir_model = self.target_model_dir
|
| 101 |
+
|
| 102 |
+
if self.origin_hf_arch == "GlmasrModel":
|
| 103 |
+
return self._set_vocab_glmedge()
|
| 104 |
+
|
| 105 |
+
if self.is_mistral_format:
|
| 106 |
+
return self._set_vocab_mistral()
|
| 107 |
+
|
| 108 |
+
path_tekken_json = self.dir_model / "tekken.json"
|
| 109 |
+
path_tokenizer_json = self.dir_model / "tokenizer.json"
|
| 110 |
+
if path_tekken_json.is_file() and not path_tokenizer_json.is_file():
|
| 111 |
+
self._set_vocab_mistral()
|
| 112 |
+
|
| 113 |
+
tokenizer_config_file = self.dir_model / 'tokenizer_config.json'
|
| 114 |
+
if tokenizer_config_file.is_file():
|
| 115 |
+
with open(tokenizer_config_file, "r", encoding="utf-8") as f:
|
| 116 |
+
tokenizer_config_json = json.load(f)
|
| 117 |
+
if (add_prefix_space := tokenizer_config_json.get("add_prefix_space")) is not None:
|
| 118 |
+
self.gguf_writer.add_add_space_prefix(add_prefix_space)
|
| 119 |
+
if tokenizer_config_json.get("tokenizer_class") == "HybridDNATokenizer":
|
| 120 |
+
return self._set_vocab_hybriddna()
|
| 121 |
+
|
| 122 |
+
try:
|
| 123 |
+
self._set_vocab_sentencepiece()
|
| 124 |
+
except FileNotFoundError:
|
| 125 |
+
try:
|
| 126 |
+
self._set_vocab_llama_hf()
|
| 127 |
+
except (FileNotFoundError, TypeError):
|
| 128 |
+
# Llama 3
|
| 129 |
+
self._set_vocab_gpt2()
|
| 130 |
+
|
| 131 |
+
# Apply to CodeLlama only (and ignore for Llama 3 with a vocab size of 128256)
|
| 132 |
+
if self.hparams.get("vocab_size", 32000) == 32016:
|
| 133 |
+
special_vocab = gguf.SpecialVocab(
|
| 134 |
+
self.dir_model, load_merges=False,
|
| 135 |
+
special_token_types = ['prefix', 'suffix', 'middle', 'eot']
|
| 136 |
+
)
|
| 137 |
+
special_vocab._set_special_token("prefix", 32007)
|
| 138 |
+
special_vocab._set_special_token("suffix", 32008)
|
| 139 |
+
special_vocab._set_special_token("middle", 32009)
|
| 140 |
+
special_vocab._set_special_token("eot", 32010)
|
| 141 |
+
special_vocab.add_to_gguf(self.gguf_writer)
|
| 142 |
+
|
| 143 |
+
# Apply to granite small models only
|
| 144 |
+
if self.hparams.get("vocab_size", 32000) == 49152:
|
| 145 |
+
self.gguf_writer.add_add_bos_token(False)
|
| 146 |
+
|
| 147 |
+
# eagle3: Restore original dir_model
|
| 148 |
+
if original_dir_model is not None:
|
| 149 |
+
self.dir_model = original_dir_model
|
| 150 |
+
|
| 151 |
+
def set_gguf_parameters(self):
|
| 152 |
+
super().set_gguf_parameters()
|
| 153 |
+
hparams = self.hparams
|
| 154 |
+
|
| 155 |
+
if not self.is_mistral_format:
|
| 156 |
+
self.gguf_writer.add_vocab_size(hparams["vocab_size"])
|
| 157 |
+
|
| 158 |
+
if (rope_dim := hparams.get("head_dim")) is None:
|
| 159 |
+
rope_dim = hparams["hidden_size"] // hparams["num_attention_heads"]
|
| 160 |
+
self.gguf_writer.add_rope_dimension_count(rope_dim)
|
| 161 |
+
|
| 162 |
+
@staticmethod
|
| 163 |
+
def permute(weights: Tensor, n_head: int, n_head_kv: int | None):
|
| 164 |
+
if n_head_kv is not None and n_head != n_head_kv:
|
| 165 |
+
n_head = n_head_kv
|
| 166 |
+
return (weights.reshape(n_head, 2, weights.shape[0] // n_head // 2, *weights.shape[1:])
|
| 167 |
+
.swapaxes(1, 2)
|
| 168 |
+
.reshape(weights.shape))
|
| 169 |
+
|
| 170 |
+
def _repack_nvfp4(self, name: str, weight: Tensor, scale: Tensor, scale2: Tensor, input_scale: Tensor):
|
| 171 |
+
# Mirror the BF16 Q/K RoPE permutation site in modify_tensors; the NVFP4 path bypasses it.
|
| 172 |
+
if self.undo_permute:
|
| 173 |
+
n_head = self.find_hparam(["n_heads", "num_attention_heads"], optional=True)
|
| 174 |
+
n_kv_head = self.find_hparam(["n_kv_heads", "num_key_value_heads"], optional=True)
|
| 175 |
+
if n_head is not None:
|
| 176 |
+
if name.endswith("q_proj.weight"):
|
| 177 |
+
weight = LlamaModel.permute(weight, n_head, n_head)
|
| 178 |
+
scale = LlamaModel.permute(scale, n_head, n_head)
|
| 179 |
+
elif name.endswith("k_proj.weight"):
|
| 180 |
+
weight = LlamaModel.permute(weight, n_head, n_kv_head)
|
| 181 |
+
scale = LlamaModel.permute(scale, n_head, n_kv_head)
|
| 182 |
+
super()._repack_nvfp4(name, weight, scale, scale2, input_scale)
|
| 183 |
+
|
| 184 |
+
_experts: list[dict[str, Tensor]] | None = None
|
| 185 |
+
|
| 186 |
+
@classmethod
|
| 187 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 188 |
+
name, gen = item
|
| 189 |
+
|
| 190 |
+
if "text_model." in name:
|
| 191 |
+
name = name.replace("text_model.", "") # for SmolVLM
|
| 192 |
+
|
| 193 |
+
return super().filter_tensors((name, gen))
|
| 194 |
+
|
| 195 |
+
def index_tensors(self, remote_hf_model_id: str | None = None) -> dict[str, Callable[[], Tensor]]:
|
| 196 |
+
tensors = super().index_tensors(remote_hf_model_id)
|
| 197 |
+
|
| 198 |
+
# Handle Eagle3Speculator nested config
|
| 199 |
+
if "transformer_layer_config" in self.hparams:
|
| 200 |
+
self.hparams = {**self.hparams, **self.hparams["transformer_layer_config"]}
|
| 201 |
+
|
| 202 |
+
# eagle3 detection
|
| 203 |
+
if "draft_vocab_size" in self.hparams and self.hparams["num_hidden_layers"] == 1:
|
| 204 |
+
logger.info("EAGLE-3: renaming midlayer.* / layers.0.* to model.layers.0.*")
|
| 205 |
+
new_tensors = {}
|
| 206 |
+
for name, gen in tensors.items():
|
| 207 |
+
if name.startswith("midlayer."):
|
| 208 |
+
new_name = "model.layers.0." + name[len("midlayer."):]
|
| 209 |
+
new_tensors[new_name] = gen
|
| 210 |
+
elif name.startswith("layers.0."): # Eagle3Speculator format
|
| 211 |
+
new_name = "model." + name
|
| 212 |
+
new_tensors[new_name] = gen
|
| 213 |
+
else:
|
| 214 |
+
new_tensors[name] = gen
|
| 215 |
+
return new_tensors
|
| 216 |
+
|
| 217 |
+
return tensors
|
| 218 |
+
|
| 219 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 220 |
+
# eagle3: special tensors that bypass standard llama mapping
|
| 221 |
+
if getattr(self, 'is_eagle3', False):
|
| 222 |
+
if name == "fc.weight":
|
| 223 |
+
yield (name, data_torch)
|
| 224 |
+
return
|
| 225 |
+
if name == "d2t":
|
| 226 |
+
# store for manual int64 handling in prepare_tensors (avoid F32 conversion)
|
| 227 |
+
if not hasattr(self, '_eagle3_int_tensors'):
|
| 228 |
+
self._eagle3_int_tensors = {}
|
| 229 |
+
self._eagle3_int_tensors[name] = data_torch
|
| 230 |
+
return
|
| 231 |
+
if name == "t2d":
|
| 232 |
+
# not used at runtime, skip
|
| 233 |
+
return
|
| 234 |
+
if name.endswith(".hidden_norm.weight"):
|
| 235 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_NORM_2, bid), data_torch)
|
| 236 |
+
return
|
| 237 |
+
|
| 238 |
+
n_head = self.find_hparam(["n_heads", "num_attention_heads"])
|
| 239 |
+
n_kv_head = self.find_hparam(["n_kv_heads", "num_key_value_heads"])
|
| 240 |
+
|
| 241 |
+
if self.hf_arch == "LlamaModel":
|
| 242 |
+
name = "model." + name
|
| 243 |
+
|
| 244 |
+
if self.undo_permute:
|
| 245 |
+
if name.endswith(("q_proj.weight", "q_proj.bias")):
|
| 246 |
+
data_torch = LlamaModel.permute(data_torch, n_head, n_head)
|
| 247 |
+
if name.endswith(("k_proj.weight", "k_proj.bias")):
|
| 248 |
+
data_torch = LlamaModel.permute(data_torch, n_head, n_kv_head)
|
| 249 |
+
|
| 250 |
+
# process the experts separately
|
| 251 |
+
if name.find("block_sparse_moe.experts") != -1:
|
| 252 |
+
n_experts = self.hparams["num_local_experts"]
|
| 253 |
+
|
| 254 |
+
assert bid is not None
|
| 255 |
+
|
| 256 |
+
if self._experts is None:
|
| 257 |
+
self._experts = [{} for _ in range(self.block_count)]
|
| 258 |
+
|
| 259 |
+
self._experts[bid][name] = data_torch
|
| 260 |
+
|
| 261 |
+
if len(self._experts[bid]) >= n_experts * 3:
|
| 262 |
+
# merge the experts into a single 3d tensor
|
| 263 |
+
for wid in ["w1", "w2", "w3"]:
|
| 264 |
+
datas: list[Tensor] = []
|
| 265 |
+
|
| 266 |
+
for xid in range(n_experts):
|
| 267 |
+
ename = f"model.layers.{bid}.block_sparse_moe.experts.{xid}.{wid}.weight"
|
| 268 |
+
datas.append(self._experts[bid][ename])
|
| 269 |
+
del self._experts[bid][ename]
|
| 270 |
+
|
| 271 |
+
data_torch = torch.stack(datas, dim=0)
|
| 272 |
+
|
| 273 |
+
merged_name = f"layers.{bid}.feed_forward.experts.{wid}.weight"
|
| 274 |
+
|
| 275 |
+
yield from super().modify_tensors(data_torch, merged_name, bid)
|
| 276 |
+
return
|
| 277 |
+
else:
|
| 278 |
+
return
|
| 279 |
+
|
| 280 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 281 |
+
|
| 282 |
+
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
|
| 283 |
+
if rope_params := self.rope_parameters.get("full_attention", self.rope_parameters):
|
| 284 |
+
if rope_params.get("rope_type", '').lower() == "llama3":
|
| 285 |
+
base = rope_params.get("rope_theta", 10000.0)
|
| 286 |
+
if (dim := self.hparams.get("head_dim")) is None:
|
| 287 |
+
dim = self.hparams["hidden_size"] // self.hparams["num_attention_heads"]
|
| 288 |
+
freqs = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
|
| 289 |
+
|
| 290 |
+
factor = rope_params.get("factor", 8.0)
|
| 291 |
+
low_freq_factor = rope_params.get("low_freq_factor", 1.0)
|
| 292 |
+
high_freq_factor = rope_params.get("high_freq_factor", 4.0)
|
| 293 |
+
old_context_len = rope_params.get("original_max_position_embeddings", 8192)
|
| 294 |
+
|
| 295 |
+
low_freq_wavelen = old_context_len / low_freq_factor
|
| 296 |
+
high_freq_wavelen = old_context_len / high_freq_factor
|
| 297 |
+
# assert low_freq_wavelen != high_freq_wavelen # Errors for Llama4
|
| 298 |
+
|
| 299 |
+
rope_factors = []
|
| 300 |
+
for freq in freqs:
|
| 301 |
+
wavelen = 2 * math.pi / freq
|
| 302 |
+
if wavelen < high_freq_wavelen:
|
| 303 |
+
rope_factors.append(1)
|
| 304 |
+
elif wavelen > low_freq_wavelen:
|
| 305 |
+
rope_factors.append(factor)
|
| 306 |
+
else:
|
| 307 |
+
smooth = (old_context_len / wavelen - low_freq_factor) / (high_freq_factor - low_freq_factor)
|
| 308 |
+
rope_factors.append(1 / ((1 - smooth) / factor + smooth))
|
| 309 |
+
|
| 310 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FREQS), torch.tensor(rope_factors, dtype=torch.float32))
|
| 311 |
+
|
| 312 |
+
def prepare_tensors(self):
|
| 313 |
+
# eagle3: collect d2t original dtype before parent converts tensors to F32
|
| 314 |
+
eagle3_original_dtypes = {}
|
| 315 |
+
if getattr(self, 'is_eagle3', False):
|
| 316 |
+
for name, data_torch in self.get_tensors():
|
| 317 |
+
if name == "d2t":
|
| 318 |
+
eagle3_original_dtypes[name] = data_torch.dtype
|
| 319 |
+
|
| 320 |
+
super().prepare_tensors()
|
| 321 |
+
|
| 322 |
+
# eagle3: write d2t as absolute target token ids
|
| 323 |
+
if getattr(self, 'is_eagle3', False) and hasattr(self, '_eagle3_int_tensors'):
|
| 324 |
+
for name, data_torch in self._eagle3_int_tensors.items():
|
| 325 |
+
old_dtype = eagle3_original_dtypes.get(name, data_torch.dtype)
|
| 326 |
+
data = data_torch.to(torch.int64).cpu().numpy()
|
| 327 |
+
if name == "d2t":
|
| 328 |
+
data = data.reshape(-1)
|
| 329 |
+
data = data + np.arange(data.size, dtype=np.int64)
|
| 330 |
+
if np.any((data < 0) | (data >= self.target_vocab_size)):
|
| 331 |
+
raise ValueError(f"EAGLE-3 d2t target ids out of range for target vocab size {self.target_vocab_size}")
|
| 332 |
+
if np.unique(data).size != data.size:
|
| 333 |
+
raise ValueError("EAGLE-3 d2t contains duplicate target ids")
|
| 334 |
+
data_qtype = gguf.GGMLQuantizationType.I64
|
| 335 |
+
|
| 336 |
+
shape_str = f"{{{', '.join(str(n) for n in reversed(data.shape))}}}"
|
| 337 |
+
logger.info(f"{name + ',':<30} {old_dtype} --> {data_qtype.name}, shape = {shape_str}")
|
| 338 |
+
self.gguf_writer.add_tensor(name, data, raw_dtype=data_qtype)
|
| 339 |
+
|
| 340 |
+
if self._experts is not None:
|
| 341 |
+
# flatten `list[dict[str, Tensor]]` into `list[str]`
|
| 342 |
+
experts = [k for d in self._experts for k in d.keys()]
|
| 343 |
+
if len(experts) > 0:
|
| 344 |
+
raise ValueError(f"Unprocessed experts: {experts}")
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
@ModelBase.register("ArceeForCausalLM")
|
| 348 |
+
class ArceeModel(LlamaModel):
|
| 349 |
+
model_arch = gguf.MODEL_ARCH.ARCEE
|
| 350 |
+
|
| 351 |
+
def set_gguf_parameters(self):
|
| 352 |
+
super().set_gguf_parameters()
|
| 353 |
+
self._try_set_pooling_type()
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
@ModelBase.register(
|
| 357 |
+
"Llama4ForConditionalGeneration",
|
| 358 |
+
"Llama4ForCausalLM",
|
| 359 |
+
)
|
| 360 |
+
class Llama4Model(LlamaModel):
|
| 361 |
+
model_arch = gguf.MODEL_ARCH.LLAMA4
|
| 362 |
+
undo_permute = False
|
| 363 |
+
|
| 364 |
+
def __init__(self, *args, **kwargs):
|
| 365 |
+
super().__init__(*args, **kwargs)
|
| 366 |
+
# IMPORTANT: the normal "intermediate_size" is renamed to "intermediate_size_mlp", we need to undo this
|
| 367 |
+
self.hparams["intermediate_size_moe"] = self.hparams["intermediate_size"]
|
| 368 |
+
self.hparams["intermediate_size"] = self.hparams["intermediate_size_mlp"]
|
| 369 |
+
|
| 370 |
+
def set_vocab(self):
|
| 371 |
+
self._set_vocab_gpt2()
|
| 372 |
+
|
| 373 |
+
def set_gguf_parameters(self):
|
| 374 |
+
super().set_gguf_parameters()
|
| 375 |
+
self.gguf_writer.add_interleave_moe_layer_step(self.hparams["interleave_moe_layer_step"])
|
| 376 |
+
self.gguf_writer.add_expert_feed_forward_length(self.hparams["intermediate_size_moe"])
|
| 377 |
+
if "layer_types" in self.hparams:
|
| 378 |
+
if all(lt == "full_attention" for lt in self.hparams["layer_types"]):
|
| 379 |
+
# all layers are full attention (for MobileLLM), disable swa
|
| 380 |
+
self.gguf_writer.add_sliding_window(0)
|
| 381 |
+
|
| 382 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None):
|
| 383 |
+
# split the gate_up into gate and up
|
| 384 |
+
if "gate_up_proj" in name:
|
| 385 |
+
name_up = name.replace("gate_up_proj", "up_proj.weight")
|
| 386 |
+
name_gate = name.replace("gate_up_proj", "gate_proj.weight")
|
| 387 |
+
dim_half = data_torch.shape[-1] // 2
|
| 388 |
+
gate_proj_weight, up_proj_weight = data_torch.transpose(-1, -2).split(dim_half, dim=-2)
|
| 389 |
+
yield from super().modify_tensors(gate_proj_weight, name_gate, bid)
|
| 390 |
+
yield from super().modify_tensors(up_proj_weight, name_up, bid)
|
| 391 |
+
return
|
| 392 |
+
|
| 393 |
+
if name.endswith("down_proj"):
|
| 394 |
+
name += ".weight"
|
| 395 |
+
data_torch = data_torch.transpose(-1, -2)
|
| 396 |
+
|
| 397 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
@ModelBase.register("LlamaBidirectionalModel")
|
| 401 |
+
class LlamaEmbedNemotronModel(LlamaModel):
|
| 402 |
+
model_arch = gguf.MODEL_ARCH.LLAMA_EMBED
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
@ModelBase.register("SmolLM3ForCausalLM")
|
| 406 |
+
class SmolLM3Model(LlamaModel):
|
| 407 |
+
model_arch = gguf.MODEL_ARCH.SMOLLM3
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
@ModelBase.register("ApertusForCausalLM")
|
| 411 |
+
class ApertusModel(LlamaModel):
|
| 412 |
+
model_arch = gguf.MODEL_ARCH.APERTUS
|
| 413 |
+
undo_permute = False
|
| 414 |
+
|
| 415 |
+
_alpha_n = {}
|
| 416 |
+
_alpha_p = {}
|
| 417 |
+
_beta = {}
|
| 418 |
+
_eps = {}
|
| 419 |
+
|
| 420 |
+
def modify_tensors(self, data_torch, name, bid):
|
| 421 |
+
# Handle xIELU activation parameters
|
| 422 |
+
n_layers = self.hparams["num_hidden_layers"]
|
| 423 |
+
if name.endswith(".act_fn.alpha_n"):
|
| 424 |
+
self._alpha_n[bid] = data_torch.to("cpu").float().item()
|
| 425 |
+
if (len(self._alpha_n) == n_layers):
|
| 426 |
+
self.gguf_writer.add_xielu_alpha_n([self._alpha_n[k] for k in sorted(self._alpha_n)])
|
| 427 |
+
return
|
| 428 |
+
if name.endswith(".act_fn.alpha_p"):
|
| 429 |
+
self._alpha_p[bid] = data_torch.to("cpu").float().item()
|
| 430 |
+
if (len(self._alpha_p) == n_layers):
|
| 431 |
+
self.gguf_writer.add_xielu_alpha_p([self._alpha_p[k] for k in sorted(self._alpha_p)])
|
| 432 |
+
return
|
| 433 |
+
if name.endswith(".act_fn.beta"):
|
| 434 |
+
self._beta[bid] = data_torch.to("cpu").float().item()
|
| 435 |
+
if (len(self._beta) == n_layers):
|
| 436 |
+
self.gguf_writer.add_xielu_beta([self._beta[k] for k in sorted(self._beta)])
|
| 437 |
+
return
|
| 438 |
+
if name.endswith(".act_fn.eps"):
|
| 439 |
+
self._eps[bid] = data_torch.to("cpu").float().item()
|
| 440 |
+
if (len(self._eps) == n_layers):
|
| 441 |
+
self.gguf_writer.add_xielu_eps([self._eps[k] for k in sorted(self._eps)])
|
| 442 |
+
return
|
| 443 |
+
|
| 444 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
llama.cpp/conversion/llama4.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Callable, Iterable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
if TYPE_CHECKING:
|
| 6 |
+
from torch import Tensor
|
| 7 |
+
|
| 8 |
+
from .base import MmprojModel, ModelBase, gguf
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@ModelBase.register("Llama4ForConditionalGeneration")
|
| 12 |
+
class Llama4VisionModel(MmprojModel):
|
| 13 |
+
def set_gguf_parameters(self):
|
| 14 |
+
super().set_gguf_parameters()
|
| 15 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.LLAMA4)
|
| 16 |
+
self.gguf_writer.add_vision_attention_layernorm_eps(self.hparams["norm_eps"])
|
| 17 |
+
self.gguf_writer.add_vision_projector_scale_factor(int(1.0 / self.hparams["pixel_shuffle_ratio"]))
|
| 18 |
+
assert self.hparams["hidden_act"] == "gelu"
|
| 19 |
+
self.gguf_writer.add_vision_use_gelu(True)
|
| 20 |
+
|
| 21 |
+
@classmethod
|
| 22 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 23 |
+
name, gen = item
|
| 24 |
+
|
| 25 |
+
if "multi_modal_projector" not in name and "vision_model" not in name:
|
| 26 |
+
return None
|
| 27 |
+
|
| 28 |
+
if "positional_embedding_vlm" in name and ".weight" not in name:
|
| 29 |
+
name += ".weight"
|
| 30 |
+
|
| 31 |
+
return super().filter_tensors((name, gen))
|
| 32 |
+
|
| 33 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 34 |
+
if "multi_modal_projector.linear_1" in name:
|
| 35 |
+
# despite the name with number postfix, this is a single fully connected layer
|
| 36 |
+
yield (gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_MMPROJ_FC] + '.weight', data_torch)
|
| 37 |
+
else:
|
| 38 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
llama.cpp/conversion/llava.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
from typing import Iterable, TYPE_CHECKING
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING:
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
from .base import MmprojModel, ModelBase, gguf, logger
|
| 11 |
+
|
| 12 |
+
from .llama import LlamaModel
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@ModelBase.register(
|
| 16 |
+
"LlavaForConditionalGeneration", # pixtral
|
| 17 |
+
"Mistral3ForConditionalGeneration", # mistral small 3.1
|
| 18 |
+
)
|
| 19 |
+
class LlavaVisionModel(MmprojModel):
|
| 20 |
+
img_break_tok_id = -1
|
| 21 |
+
use_break_tok = True
|
| 22 |
+
|
| 23 |
+
def __init__(self, *args, **kwargs):
|
| 24 |
+
super().__init__(*args, **kwargs)
|
| 25 |
+
if self.hparams.get("model_type") == "pixtral":
|
| 26 |
+
# layer_norm_eps is not in config.json, it is hard-coded in modeling_pixtral.py
|
| 27 |
+
self.hparams["layer_norm_eps"] = self.hparams.get("layer_norm_eps", 1e-5)
|
| 28 |
+
if self.use_break_tok:
|
| 29 |
+
self.img_break_tok_id = self.get_token_id("[IMG_BREAK]")
|
| 30 |
+
elif self.is_mistral_format:
|
| 31 |
+
# hparams is already vision config here so norm_eps is only defined in global_config.
|
| 32 |
+
self.hparams["norm_eps"] = self.global_config.get("norm_eps", None)
|
| 33 |
+
assert self.hparams["norm_eps"] is not None, "norm_eps not found in params.json"
|
| 34 |
+
if self.use_break_tok:
|
| 35 |
+
self.img_break_tok_id = self.find_vparam(["image_break_token_id"])
|
| 36 |
+
|
| 37 |
+
# params.json may ship -1 placeholders (Mistral Medium 3.5)
|
| 38 |
+
# resolve the real id from the bundled tokenizer in that case
|
| 39 |
+
if self.img_break_tok_id < 0:
|
| 40 |
+
self.img_break_tok_id = self.get_mistral_token_id("[IMG_BREAK]")
|
| 41 |
+
else:
|
| 42 |
+
raise ValueError(f"Unsupported model type: {self.hparams['model_type']}")
|
| 43 |
+
logger.info(f"Image break token id: {self.img_break_tok_id}")
|
| 44 |
+
|
| 45 |
+
def get_token_id(self, token: str) -> int:
|
| 46 |
+
tokenizer_config_file = self.dir_model / 'tokenizer_config.json'
|
| 47 |
+
with open(tokenizer_config_file, "r", encoding="utf-8") as f:
|
| 48 |
+
added_tokens_decoder = json.load(f).get('added_tokens_decoder') or {}
|
| 49 |
+
for id_, token_data in added_tokens_decoder.items():
|
| 50 |
+
if token_data.get("content") == token:
|
| 51 |
+
return int(id_)
|
| 52 |
+
# fallthrough to tokenizer.json
|
| 53 |
+
with open(self.dir_model / "tokenizer.json", "r", encoding="utf-8") as f:
|
| 54 |
+
tokenizer_json = json.load(f)
|
| 55 |
+
for token_data in tokenizer_json["added_tokens"]:
|
| 56 |
+
if token_data["content"] == token:
|
| 57 |
+
return int(token_data["id"])
|
| 58 |
+
raise ValueError(f"Token '{token}' not found in tokenizer config.")
|
| 59 |
+
|
| 60 |
+
def get_mistral_token_id(self, token: str) -> int:
|
| 61 |
+
# mistral native format ships tekken.json or a versioned spm tokenizer
|
| 62 |
+
tekken_file = self.dir_model / "tekken.json"
|
| 63 |
+
if tekken_file.is_file():
|
| 64 |
+
with open(tekken_file, "r", encoding="utf-8") as f:
|
| 65 |
+
data = json.load(f)
|
| 66 |
+
for entry in data.get("special_tokens", []):
|
| 67 |
+
if entry.get("token_str") == token:
|
| 68 |
+
return int(entry["rank"])
|
| 69 |
+
tokenizer_json_file = self.dir_model / "tokenizer.json"
|
| 70 |
+
if tokenizer_json_file.is_file():
|
| 71 |
+
with open(tokenizer_json_file, "r", encoding="utf-8") as f:
|
| 72 |
+
data = json.load(f)
|
| 73 |
+
for entry in data.get("added_tokens", []):
|
| 74 |
+
if entry.get("content") == token:
|
| 75 |
+
return int(entry["id"])
|
| 76 |
+
raise ValueError(f"Token '{token}' not found in mistral tokenizer files.")
|
| 77 |
+
|
| 78 |
+
def set_gguf_parameters(self):
|
| 79 |
+
super().set_gguf_parameters()
|
| 80 |
+
hparams = self.hparams
|
| 81 |
+
if hparams.get("model_type") == "pixtral":
|
| 82 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.PIXTRAL)
|
| 83 |
+
self.gguf_writer.add_vision_attention_layernorm_eps(hparams["layer_norm_eps"])
|
| 84 |
+
|
| 85 |
+
# hidden_act
|
| 86 |
+
if hparams["hidden_act"] == "silu":
|
| 87 |
+
self.gguf_writer.add_vision_use_silu(True)
|
| 88 |
+
elif hparams["hidden_act"] == "gelu":
|
| 89 |
+
self.gguf_writer.add_vision_use_gelu(True)
|
| 90 |
+
else:
|
| 91 |
+
raise ValueError(f"Unsupported hidden_act: {hparams['hidden_act']}")
|
| 92 |
+
|
| 93 |
+
# spatial_merge_size
|
| 94 |
+
if "spatial_merge_size" in self.global_config:
|
| 95 |
+
self.gguf_writer.add_vision_spatial_merge_size(self.global_config["spatial_merge_size"])
|
| 96 |
+
|
| 97 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 98 |
+
n_head = (
|
| 99 |
+
self.hparams["num_attention_heads"] if not self.is_mistral_format else self.find_vparam(["num_attention_heads"])
|
| 100 |
+
)
|
| 101 |
+
n_kv_head = n_head
|
| 102 |
+
|
| 103 |
+
valid_prefixes = (
|
| 104 |
+
"multi_modal_projector.",
|
| 105 |
+
"vision_tower.",
|
| 106 |
+
"vision_encoder.",
|
| 107 |
+
"vision_language_adapter.",
|
| 108 |
+
"patch_merger.",
|
| 109 |
+
"pre_mm_projector_norm",
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
if any(name.startswith(prefix) for prefix in valid_prefixes):
|
| 113 |
+
# process vision tensors
|
| 114 |
+
if name.endswith(("q_proj.weight", "q_proj.bias")) and not self.is_mistral_format:
|
| 115 |
+
data_torch = LlamaModel.permute(data_torch, n_head, n_head)
|
| 116 |
+
if name.endswith(("k_proj.weight", "k_proj.bias")) and not self.is_mistral_format:
|
| 117 |
+
data_torch = LlamaModel.permute(data_torch, n_head, n_kv_head)
|
| 118 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 119 |
+
return
|
| 120 |
+
|
| 121 |
+
embed_key = "embed_tokens.weight" if not self.is_mistral_format else "tok_embeddings.weight"
|
| 122 |
+
if self.img_break_tok_id > 0 and embed_key in name:
|
| 123 |
+
logger.info(f"Extracting [IMG_BREAK] token embedding from {name}")
|
| 124 |
+
# for pixtral model, we need to extract the [IMG_BREAK] token embedding
|
| 125 |
+
img_break_embd = data_torch[self.img_break_tok_id]
|
| 126 |
+
name = gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_TOK_EMBD_IMG_BREAK]
|
| 127 |
+
yield from super().modify_tensors(img_break_embd, name, bid)
|
| 128 |
+
|
| 129 |
+
return # skip other tensors
|
llama.cpp/conversion/maincoder.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from .base import ModelBase, TextModel, gguf
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@ModelBase.register("MaincoderForCausalLM")
|
| 7 |
+
class MaincoderModel(TextModel):
|
| 8 |
+
model_arch = gguf.MODEL_ARCH.MAINCODER
|
| 9 |
+
|
| 10 |
+
def set_gguf_parameters(self):
|
| 11 |
+
super().set_gguf_parameters()
|
| 12 |
+
|
| 13 |
+
if (head_dim := self.hparams.get("head_dim")) is not None:
|
| 14 |
+
self.gguf_writer.add_rope_dimension_count(head_dim)
|
llama.cpp/conversion/mamba.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Callable, Iterable, TYPE_CHECKING
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
if TYPE_CHECKING:
|
| 11 |
+
from torch import Tensor
|
| 12 |
+
|
| 13 |
+
from .base import ModelBase, TextModel, gguf, logger
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@ModelBase.register("MambaForCausalLM", "MambaLMHeadModel", "FalconMambaForCausalLM")
|
| 17 |
+
class MambaModel(TextModel):
|
| 18 |
+
model_arch = gguf.MODEL_ARCH.MAMBA
|
| 19 |
+
|
| 20 |
+
def __init__(self, dir_model: Path, *args, **kwargs):
|
| 21 |
+
# Avoid using AutoConfig for hparams
|
| 22 |
+
hparams = kwargs.pop("hparams", None)
|
| 23 |
+
if hparams is None:
|
| 24 |
+
with open(dir_model / "config.json", "r", encoding="utf-8") as f:
|
| 25 |
+
hparams = json.load(f)
|
| 26 |
+
super().__init__(dir_model, *args, hparams=hparams, **kwargs)
|
| 27 |
+
|
| 28 |
+
def set_vocab(self):
|
| 29 |
+
vocab_size = self.hparams["vocab_size"]
|
| 30 |
+
# Round vocab size to next multiple of 8
|
| 31 |
+
pad_vocab = self.hparams.get("pad_vocab_size_multiple", 8)
|
| 32 |
+
# pad using ceiling division
|
| 33 |
+
# ref: https://stackoverflow.com/a/17511341/22827863
|
| 34 |
+
vocab_size = -(vocab_size // -pad_vocab) * pad_vocab
|
| 35 |
+
self.hparams["vocab_size"] = vocab_size
|
| 36 |
+
|
| 37 |
+
if (self.dir_model / "tokenizer.json").is_file():
|
| 38 |
+
self._set_vocab_gpt2()
|
| 39 |
+
elif (self.dir_model / "tokenizer.model").is_file():
|
| 40 |
+
self._set_vocab_sentencepiece()
|
| 41 |
+
else:
|
| 42 |
+
# Use the GPT-NeoX tokenizer when no tokenizer files are present
|
| 43 |
+
self._set_vocab_builtin("gpt-neox", vocab_size)
|
| 44 |
+
|
| 45 |
+
def set_gguf_parameters(self):
|
| 46 |
+
d_model = self.find_hparam(["hidden_size", "d_model"])
|
| 47 |
+
d_conv = self.find_hparam(["conv_kernel", "d_conv"], optional=True) or 4
|
| 48 |
+
d_inner = self.find_hparam(["intermediate_size", "d_inner"], optional=True) or 2 * d_model
|
| 49 |
+
d_state = self.find_hparam(["state_size", "d_state"], optional=True) or 16
|
| 50 |
+
# ceiling division
|
| 51 |
+
# ref: https://stackoverflow.com/a/17511341/22827863
|
| 52 |
+
# ref: https://github.com/state-spaces/mamba/blob/ce59daea3a090d011d6476c6e5b97f6d58ddad8b/mamba_ssm/modules/mamba_simple.py#L58
|
| 53 |
+
dt_rank = self.find_hparam(["time_step_rank", "dt_rank"], optional=True) or -(d_model // -16)
|
| 54 |
+
rms_norm_eps = self.find_hparam(["layer_norm_epsilon", "rms_norm_eps"], optional=True) or 1e-5
|
| 55 |
+
use_dt_b_c_norm = False
|
| 56 |
+
# For falconmamba we do apply RMS norm on B / DT and C layers
|
| 57 |
+
if self.find_hparam(["model_type"], optional=True) in ("falcon_mamba",):
|
| 58 |
+
use_dt_b_c_norm = True
|
| 59 |
+
# Fail early for models which don't have a block expansion factor of 2
|
| 60 |
+
assert d_inner == 2 * d_model
|
| 61 |
+
|
| 62 |
+
self.gguf_writer.add_context_length(2**20) # arbitrary value; for those who use the default
|
| 63 |
+
self.gguf_writer.add_embedding_length(d_model)
|
| 64 |
+
self.gguf_writer.add_feed_forward_length(0) # unused, but seemingly required when loading
|
| 65 |
+
self.gguf_writer.add_head_count(0) # unused, but seemingly required when loading
|
| 66 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 67 |
+
self.gguf_writer.add_ssm_conv_kernel(d_conv)
|
| 68 |
+
self.gguf_writer.add_ssm_inner_size(d_inner)
|
| 69 |
+
self.gguf_writer.add_ssm_state_size(d_state)
|
| 70 |
+
self.gguf_writer.add_ssm_time_step_rank(dt_rank)
|
| 71 |
+
self.gguf_writer.add_layer_norm_rms_eps(rms_norm_eps)
|
| 72 |
+
self.gguf_writer.add_ssm_dt_b_c_rms(use_dt_b_c_norm) # For classic Mamba we don't apply rms norm on B / DT layers
|
| 73 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 74 |
+
|
| 75 |
+
_tok_embd = None
|
| 76 |
+
|
| 77 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 78 |
+
output_name = self.format_tensor_name(gguf.MODEL_TENSOR.OUTPUT)
|
| 79 |
+
tok_embd_name = self.format_tensor_name(gguf.MODEL_TENSOR.TOKEN_EMBD)
|
| 80 |
+
|
| 81 |
+
new_name = self.map_tensor_name(name)
|
| 82 |
+
|
| 83 |
+
if name.endswith(".A_log"):
|
| 84 |
+
logger.debug("A_log --> A ==> " + new_name)
|
| 85 |
+
data_torch = -torch.exp(data_torch)
|
| 86 |
+
|
| 87 |
+
# [4 1 8192 1] -> [4 8192 1 1]
|
| 88 |
+
if self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.SSM_CONV1D, bid):
|
| 89 |
+
data_torch = data_torch.squeeze()
|
| 90 |
+
|
| 91 |
+
# assuming token_embd.weight is seen before output.weight
|
| 92 |
+
if self._tok_embd is not None and new_name == output_name:
|
| 93 |
+
if torch.equal(self._tok_embd, data_torch):
|
| 94 |
+
logger.debug(f"{output_name} is equivalent to {tok_embd_name}, omitting")
|
| 95 |
+
return
|
| 96 |
+
elif new_name == tok_embd_name:
|
| 97 |
+
self._tok_embd = data_torch
|
| 98 |
+
|
| 99 |
+
yield from super().modify_tensors(data_torch, new_name, bid)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
@ModelBase.register("Mamba2ForCausalLM")
|
| 103 |
+
class Mamba2Model(TextModel):
|
| 104 |
+
model_arch = gguf.MODEL_ARCH.MAMBA2
|
| 105 |
+
|
| 106 |
+
def __init__(self, dir_model: Path, *args, **kwargs):
|
| 107 |
+
# Avoid using AutoConfig for hparams
|
| 108 |
+
# It wrongly assumes all Mamba2 models are Mamba-Codestral-7B-v0.1
|
| 109 |
+
hparams = kwargs.pop("hparams", None)
|
| 110 |
+
if hparams is None:
|
| 111 |
+
with open(dir_model / "config.json", "r", encoding="utf-8") as f:
|
| 112 |
+
hparams = json.load(f)
|
| 113 |
+
if "llm_config" in hparams:
|
| 114 |
+
hparams["text_config"] = hparams["llm_config"]
|
| 115 |
+
super().__init__(dir_model, *args, hparams=hparams, **kwargs)
|
| 116 |
+
self.d_model = self.find_hparam(["hidden_size", "d_model", "dim"])
|
| 117 |
+
self.expand = self.find_hparam(["mamba_expand", "expand"], optional=True) or 2
|
| 118 |
+
self.d_inner = self.find_hparam(["mamba_d_ssm", "intermediate_size", "d_inner"], optional=True) or self.expand * self.d_model
|
| 119 |
+
self.n_group = self.find_hparam(["n_groups"], optional=True) or 1
|
| 120 |
+
|
| 121 |
+
def set_vocab(self):
|
| 122 |
+
vocab_size = self.hparams["vocab_size"]
|
| 123 |
+
# Round vocab size to next multiple of 16
|
| 124 |
+
pad_vocab = self.hparams.get("pad_vocab_size_multiple", 16)
|
| 125 |
+
# pad using ceiling division
|
| 126 |
+
# ref: https://stackoverflow.com/a/17511341/22827863
|
| 127 |
+
vocab_size = -(vocab_size // -pad_vocab) * pad_vocab
|
| 128 |
+
self.hparams["vocab_size"] = vocab_size
|
| 129 |
+
|
| 130 |
+
if (self.dir_model / "tokenizer.model").is_file():
|
| 131 |
+
self._set_vocab_sentencepiece()
|
| 132 |
+
elif (self.dir_model / "tokenizer.model.v3").is_file():
|
| 133 |
+
# mamba-codestral
|
| 134 |
+
raise NotImplementedError(f"Please rename {self.dir_model / 'tokenizer.model.v3'} to {self.dir_model / 'tokenizer.model'}")
|
| 135 |
+
elif (self.dir_model / "tokenizer.json").is_file():
|
| 136 |
+
self._set_vocab_gpt2()
|
| 137 |
+
else:
|
| 138 |
+
# Use the GPT-NeoX tokenizer when no tokenizer files are present
|
| 139 |
+
self._set_vocab_builtin("gpt-neox", vocab_size)
|
| 140 |
+
|
| 141 |
+
def set_gguf_parameters(self):
|
| 142 |
+
d_conv = self.find_hparam(["conv_kernel", "d_conv"], optional=True) or 4
|
| 143 |
+
d_state = self.find_hparam(["state_size", "d_state"], optional=True) or 128
|
| 144 |
+
head_dim = self.find_hparam(["mamba_d_head", "head_dim"], optional=True) or 64
|
| 145 |
+
|
| 146 |
+
rms_norm_eps = self.find_hparam(["layer_norm_epsilon", "rms_norm_eps"], optional=True) or 1e-5
|
| 147 |
+
|
| 148 |
+
# skip the assertion for FalconH1 Model
|
| 149 |
+
if self.model_arch != gguf.MODEL_ARCH.FALCON_H1:
|
| 150 |
+
assert self.d_inner == self.expand * self.d_model
|
| 151 |
+
assert self.d_inner % head_dim == 0
|
| 152 |
+
|
| 153 |
+
self.gguf_writer.add_context_length(2**20) # arbitrary value; for those who use the default
|
| 154 |
+
self.gguf_writer.add_embedding_length(self.d_model)
|
| 155 |
+
self.gguf_writer.add_feed_forward_length(0) # unused, but seemingly required when loading
|
| 156 |
+
self.gguf_writer.add_head_count(0) # unused, but seemingly required when loading
|
| 157 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 158 |
+
self.gguf_writer.add_ssm_conv_kernel(d_conv)
|
| 159 |
+
self.gguf_writer.add_ssm_inner_size(self.d_inner)
|
| 160 |
+
self.gguf_writer.add_ssm_state_size(d_state)
|
| 161 |
+
self.gguf_writer.add_ssm_time_step_rank(self.d_inner // head_dim)
|
| 162 |
+
self.gguf_writer.add_ssm_group_count(self.n_group)
|
| 163 |
+
self.gguf_writer.add_layer_norm_rms_eps(rms_norm_eps)
|
| 164 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 165 |
+
|
| 166 |
+
@classmethod
|
| 167 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 168 |
+
name, gen = item
|
| 169 |
+
|
| 170 |
+
if name.startswith(("model.backbone", "model.lm_head")):
|
| 171 |
+
# map Mamba-Codestral-7B-v0.1 tensor names to the names used by Mamba-2
|
| 172 |
+
name = name.removeprefix("model.")
|
| 173 |
+
|
| 174 |
+
if name.endswith(".dt_bias"):
|
| 175 |
+
name = name.rpartition(".dt_bias")[0] + ".dt_proj.bias"
|
| 176 |
+
|
| 177 |
+
return super().filter_tensors((name, gen))
|
| 178 |
+
|
| 179 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 180 |
+
new_name = self.map_tensor_name(name)
|
| 181 |
+
|
| 182 |
+
if self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.SSM_CONV1D, bid):
|
| 183 |
+
data_torch = data_torch.squeeze()
|
| 184 |
+
elif any(self.match_model_tensor_name(new_name, t, bid, suffix="") for t in [
|
| 185 |
+
gguf.MODEL_TENSOR.SSM_A,
|
| 186 |
+
gguf.MODEL_TENSOR.SSM_D,
|
| 187 |
+
]):
|
| 188 |
+
# unsqueeze A to use similar shape semantics as Mamba-1
|
| 189 |
+
# (D is also unsqueezed, but for more straightforward broadcast internally)
|
| 190 |
+
data_torch = data_torch.reshape((*data_torch.shape, 1))
|
| 191 |
+
elif self.match_model_tensor_name(new_name, gguf.MODEL_TENSOR.SSM_NORM, bid):
|
| 192 |
+
data_torch = data_torch.reshape((self.n_group, self.d_inner // self.n_group))
|
| 193 |
+
|
| 194 |
+
if name.endswith(".A_log"):
|
| 195 |
+
logger.debug("A_log --> A ==> " + new_name)
|
| 196 |
+
data_torch = -torch.exp(data_torch)
|
| 197 |
+
|
| 198 |
+
yield (new_name, data_torch)
|
llama.cpp/conversion/mellum.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Iterable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING:
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
from .base import ModelBase, TextModel, gguf, logger
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@ModelBase.register("MellumForCausalLM")
|
| 14 |
+
class MellumModel(TextModel):
|
| 15 |
+
model_arch = gguf.MODEL_ARCH.MELLUM
|
| 16 |
+
|
| 17 |
+
def set_gguf_parameters(self):
|
| 18 |
+
super().set_gguf_parameters()
|
| 19 |
+
if (moe_intermediate_size := self.hparams.get("moe_intermediate_size")) is not None:
|
| 20 |
+
self.gguf_writer.add_expert_feed_forward_length(moe_intermediate_size)
|
| 21 |
+
logger.info(f"gguf: expert feed forward length = {moe_intermediate_size}")
|
| 22 |
+
|
| 23 |
+
use_sliding_window = self.hparams.get("use_sliding_window")
|
| 24 |
+
sliding_window = self.hparams.get("sliding_window")
|
| 25 |
+
if (use_sliding_window is True or use_sliding_window is None) and sliding_window is not None:
|
| 26 |
+
self.gguf_writer.add_sliding_window(sliding_window)
|
| 27 |
+
logger.info(f"gguf: sliding window = {sliding_window}")
|
| 28 |
+
self.gguf_writer.add_sliding_window_pattern([t == "sliding_attention" for t in self.hparams["layer_types"]])
|
| 29 |
+
logger.info(f"gguf: sliding window pattern length = {len(self.hparams['layer_types'])}")
|
| 30 |
+
|
| 31 |
+
_experts: list[dict[str, Tensor]] | None = None
|
| 32 |
+
|
| 33 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 34 |
+
if name.find("experts") != -1:
|
| 35 |
+
n_experts = self.find_hparam(["num_local_experts", "num_experts"])
|
| 36 |
+
assert bid is not None
|
| 37 |
+
|
| 38 |
+
if self._experts is None:
|
| 39 |
+
self._experts = [{} for _ in range(self.block_count)]
|
| 40 |
+
|
| 41 |
+
self._experts[bid][name] = data_torch
|
| 42 |
+
|
| 43 |
+
if len(self._experts[bid]) >= n_experts * 3:
|
| 44 |
+
for w_name in ["down_proj", "gate_proj", "up_proj"]:
|
| 45 |
+
datas: list[Tensor] = []
|
| 46 |
+
|
| 47 |
+
for xid in range(n_experts):
|
| 48 |
+
ename = f"model.layers.{bid}.mlp.experts.{xid}.{w_name}.weight"
|
| 49 |
+
datas.append(self._experts[bid][ename])
|
| 50 |
+
del self._experts[bid][ename]
|
| 51 |
+
|
| 52 |
+
data_torch = torch.stack(datas, dim=0)
|
| 53 |
+
|
| 54 |
+
merged_name = f"model.layers.{bid}.mlp.experts.{w_name}.weight"
|
| 55 |
+
|
| 56 |
+
yield from super().modify_tensors(data_torch, merged_name, bid)
|
| 57 |
+
return
|
| 58 |
+
else:
|
| 59 |
+
return
|
| 60 |
+
|
| 61 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
llama.cpp/conversion/mimo.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
|
| 5 |
+
from typing import Callable, TYPE_CHECKING
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
if TYPE_CHECKING:
|
| 10 |
+
from torch import Tensor
|
| 11 |
+
|
| 12 |
+
from .base import MmprojModel, ModelBase, TextModel, gguf
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@ModelBase.register("MiMoV2FlashForCausalLM", "MiMoV2ForCausalLM")
|
| 16 |
+
class MimoV2Model(TextModel):
|
| 17 |
+
model_arch = gguf.MODEL_ARCH.MIMO2
|
| 18 |
+
|
| 19 |
+
# MiMo V2-Flash, V2.5 and V2.5-Pro all ship 3 trained MTP layers under model.mtp.layers.{0,1,2}.
|
| 20 |
+
# The HF config does not expose the count, so it's hardcoded to match the count found in the safetensors.
|
| 21 |
+
_n_nextn = 3
|
| 22 |
+
|
| 23 |
+
def __init__(self, *args, **kwargs):
|
| 24 |
+
super().__init__(*args, **kwargs)
|
| 25 |
+
|
| 26 |
+
self.block_count = self.hparams["num_hidden_layers"] + self._n_nextn
|
| 27 |
+
self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
|
| 28 |
+
|
| 29 |
+
@staticmethod
|
| 30 |
+
def _tp_aware_qkv_dequant(weight: Tensor, scale_inv: Tensor,
|
| 31 |
+
n_q: int, n_kv: int, hd: int, vhd: int,
|
| 32 |
+
bs: int = 128) -> Tensor:
|
| 33 |
+
# MiMo-V2.5 (TP=4) and V2.5-Pro (TP=8) ship qkv_proj sharded across TP
|
| 34 |
+
# ranks; per rank, rows are stacked as [Q_per | K_per | V_per].
|
| 35 |
+
# weight_scale_inv has ceil(rows_per_rank/bs) block-rows per rank (last
|
| 36 |
+
# may extend past rows_per_rank with phantom rows not in the weight).
|
| 37 |
+
# Naive repeat_interleave aligns rank 0 only and mis-applies scales to
|
| 38 |
+
# later ranks once rows_per_rank isn't a multiple of bs.
|
| 39 |
+
# Re-group the per-rank [Q_per|K_per|V_per] rows into a single fused
|
| 40 |
+
# [Q | K | V] tensor matching the un-sharded original layout.
|
| 41 |
+
q_size = n_q * hd
|
| 42 |
+
k_size = n_kv * hd
|
| 43 |
+
v_size = n_kv * vhd
|
| 44 |
+
total_rows = q_size + k_size + v_size
|
| 45 |
+
if weight.shape[0] != total_rows:
|
| 46 |
+
raise ValueError(f"qkv_proj weight rows {weight.shape[0]} != q+k+v {total_rows}")
|
| 47 |
+
|
| 48 |
+
# detect TP from scale_inv block count, descending order so larger matches first
|
| 49 |
+
tp = None
|
| 50 |
+
for cand in (8, 4):
|
| 51 |
+
if total_rows % cand != 0:
|
| 52 |
+
continue
|
| 53 |
+
rpr = total_rows // cand
|
| 54 |
+
bpr = (rpr + bs - 1) // bs
|
| 55 |
+
if scale_inv.shape[0] == cand * bpr:
|
| 56 |
+
tp = cand
|
| 57 |
+
break
|
| 58 |
+
if tp is None:
|
| 59 |
+
raise ValueError(
|
| 60 |
+
f"qkv_proj: cannot detect TP - scale_inv rows {scale_inv.shape[0]}, "
|
| 61 |
+
f"q+k+v {total_rows}")
|
| 62 |
+
|
| 63 |
+
q_per = q_size // tp
|
| 64 |
+
k_per = k_size // tp
|
| 65 |
+
v_per = v_size // tp
|
| 66 |
+
rows_per_rank = q_per + k_per + v_per
|
| 67 |
+
blocks_per_rank = (rows_per_rank + bs - 1) // bs
|
| 68 |
+
|
| 69 |
+
scale_inv = scale_inv.float()
|
| 70 |
+
# per-row scale-row index: rank * blocks_per_rank + (rr_in_rank // bs)
|
| 71 |
+
row_idx = torch.arange(total_rows)
|
| 72 |
+
rr = row_idx % rows_per_rank
|
| 73 |
+
rank = row_idx // rows_per_rank
|
| 74 |
+
scale_row_idx = rank * blocks_per_rank + (rr // bs)
|
| 75 |
+
# gather: (total_rows, n_col_blocks)
|
| 76 |
+
scale_per_row_block = scale_inv[scale_row_idx]
|
| 77 |
+
# expand col-blocks -> cols: each block-col covers `bs` weight cols
|
| 78 |
+
scale_full = scale_per_row_block.repeat_interleave(bs, dim=1)
|
| 79 |
+
# crop to weight col count (in case last col-block isn't full)
|
| 80 |
+
scale_full = scale_full[:, : weight.shape[1]]
|
| 81 |
+
dequant = weight.float() * scale_full
|
| 82 |
+
|
| 83 |
+
if tp == 1:
|
| 84 |
+
return dequant
|
| 85 |
+
|
| 86 |
+
# Re-group per-rank [Q_per|K_per|V_per] rows into unified [Q | K | V]
|
| 87 |
+
qs, ks, vs = [], [], []
|
| 88 |
+
for r in range(tp):
|
| 89 |
+
base = r * rows_per_rank
|
| 90 |
+
qs.append(dequant[base : base + q_per])
|
| 91 |
+
ks.append(dequant[base + q_per : base + q_per + k_per])
|
| 92 |
+
vs.append(dequant[base + q_per + k_per : base + rows_per_rank])
|
| 93 |
+
return torch.cat(qs + ks + vs, dim=0)
|
| 94 |
+
|
| 95 |
+
def dequant_model(self):
|
| 96 |
+
# Capture raw FP8 (weight, scale_inv) lambdas for qkv_proj BEFORE super
|
| 97 |
+
# rewrites them with the existing dequant. Replace super's lambda after
|
| 98 |
+
# it runs so scale_inv removal still happens via the standard path.
|
| 99 |
+
qkv_overrides: dict[str, tuple[Callable, Callable, int]] = {}
|
| 100 |
+
qc = self.hparams.get("quantization_config")
|
| 101 |
+
if isinstance(qc, dict) and qc.get("quant_method") == "fp8":
|
| 102 |
+
pat = re.compile(r"^model\.layers\.(\d+)\.self_attn\.qkv_proj\.weight_scale_inv$")
|
| 103 |
+
for name in list(self.model_tensors.keys()):
|
| 104 |
+
m = pat.match(name)
|
| 105 |
+
if not m:
|
| 106 |
+
continue
|
| 107 |
+
weight_name = name.removesuffix("_scale_inv")
|
| 108 |
+
if weight_name not in self.model_tensors:
|
| 109 |
+
continue
|
| 110 |
+
qkv_overrides[weight_name] = (
|
| 111 |
+
self.model_tensors[weight_name],
|
| 112 |
+
self.model_tensors[name],
|
| 113 |
+
int(m.group(1)),
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
super().dequant_model()
|
| 117 |
+
|
| 118 |
+
if not qkv_overrides:
|
| 119 |
+
return
|
| 120 |
+
|
| 121 |
+
n_q = self.hparams["num_attention_heads"]
|
| 122 |
+
hd = self.hparams["head_dim"]
|
| 123 |
+
vhd = self.hparams["v_head_dim"]
|
| 124 |
+
hybrid = self.hparams["hybrid_layer_pattern"]
|
| 125 |
+
n_layer_text = self.hparams["num_hidden_layers"]
|
| 126 |
+
for weight_name, (w_fn, s_fn, bid) in qkv_overrides.items():
|
| 127 |
+
# MTP layers (bid >= n_layer_text) use SWA-style attention dims
|
| 128 |
+
is_swa = True if bid >= n_layer_text else hybrid[bid] == 1
|
| 129 |
+
n_kv = self.hparams["swa_num_key_value_heads" if is_swa else "num_key_value_heads"]
|
| 130 |
+
self.model_tensors[weight_name] = (
|
| 131 |
+
lambda w_fn=w_fn, s_fn=s_fn, n_q=n_q, n_kv=n_kv, hd=hd, vhd=vhd:
|
| 132 |
+
MimoV2Model._tp_aware_qkv_dequant(w_fn(), s_fn(), n_q, n_kv, hd, vhd)
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
def set_gguf_parameters(self):
|
| 136 |
+
super().set_gguf_parameters()
|
| 137 |
+
|
| 138 |
+
assert self.hparams["swa_head_dim"] == self.hparams["head_dim"]
|
| 139 |
+
assert self.hparams["swa_num_attention_heads"] == self.hparams["num_attention_heads"]
|
| 140 |
+
assert self.hparams["swa_v_head_dim"] == self.hparams["v_head_dim"]
|
| 141 |
+
assert self.hparams["topk_method"] == "noaux_tc"
|
| 142 |
+
|
| 143 |
+
n_head_kv = self.hparams["num_key_value_heads"]
|
| 144 |
+
n_head_kv_swa = self.hparams["swa_num_key_value_heads"]
|
| 145 |
+
# Extend the per-layer pattern with SWA entries for the MTP blocks so the
|
| 146 |
+
# runtime arrays (sized to extended block_count) are fully populated.
|
| 147 |
+
hybrid = list(self.hparams["hybrid_layer_pattern"]) + [1] * self._n_nextn
|
| 148 |
+
n_head_kv_arr = [n_head_kv_swa if use_swa == 1 else n_head_kv for use_swa in hybrid]
|
| 149 |
+
self.gguf_writer.add_head_count_kv(n_head_kv_arr)
|
| 150 |
+
|
| 151 |
+
self.gguf_writer.add_sliding_window(self.hparams["sliding_window"])
|
| 152 |
+
self.gguf_writer.add_sliding_window_pattern(hybrid)
|
| 153 |
+
self.gguf_writer.add_value_length(self.hparams["v_head_dim"])
|
| 154 |
+
self.gguf_writer.add_expert_count(self.hparams["n_routed_experts"])
|
| 155 |
+
self.gguf_writer.add_expert_feed_forward_length(self.hparams["moe_intermediate_size"])
|
| 156 |
+
|
| 157 |
+
rope_dim = int(self.hparams["head_dim"] * self.rope_parameters["partial_rotary_factor"])
|
| 158 |
+
self.gguf_writer.add_rope_dimension_count(rope_dim)
|
| 159 |
+
|
| 160 |
+
self.gguf_writer.add_layer_norm_rms_eps(self.hparams.get("layernorm_epsilon", 1e-5))
|
| 161 |
+
|
| 162 |
+
v_scale = self.hparams.get("attention_value_scale")
|
| 163 |
+
if v_scale is not None:
|
| 164 |
+
self.gguf_writer.add_attn_value_scale(float(v_scale))
|
| 165 |
+
|
| 166 |
+
self.gguf_writer.add_nextn_predict_layers(self._n_nextn)
|
| 167 |
+
|
| 168 |
+
_experts: list[dict[str, Tensor]] | None = None
|
| 169 |
+
|
| 170 |
+
@classmethod
|
| 171 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 172 |
+
name, gen = item
|
| 173 |
+
|
| 174 |
+
if "attention_sink" in name and not name.endswith(".weight"):
|
| 175 |
+
name += ".weight"
|
| 176 |
+
|
| 177 |
+
return super().filter_tensors((name, gen))
|
| 178 |
+
|
| 179 |
+
def modify_tensors(self, data_torch, name, bid):
|
| 180 |
+
# Remap MTP/NextN tensors to additional layer slots so the standard tensor map handles them.
|
| 181 |
+
# HF: model.mtp.layers.{i}.foo -> model.layers.{n_layer_text + i}.foo
|
| 182 |
+
m = re.match(r"^model\.mtp\.layers\.(\d+)\.(.*)$", name)
|
| 183 |
+
if m is not None:
|
| 184 |
+
mtp_idx = int(m.group(1))
|
| 185 |
+
assert mtp_idx < self._n_nextn, f"MTP layer index {mtp_idx} >= _n_nextn ({self._n_nextn})"
|
| 186 |
+
rest = m.group(2)
|
| 187 |
+
n_layer_text = self.hparams["num_hidden_layers"]
|
| 188 |
+
new_bid = n_layer_text + mtp_idx
|
| 189 |
+
name = f"model.layers.{new_bid}.{rest}"
|
| 190 |
+
bid = new_bid
|
| 191 |
+
|
| 192 |
+
# process the experts separately
|
| 193 |
+
if name.find("mlp.experts") != -1:
|
| 194 |
+
n_experts = self.hparams["n_routed_experts"]
|
| 195 |
+
assert bid is not None
|
| 196 |
+
|
| 197 |
+
if self._experts is None:
|
| 198 |
+
self._experts = [{} for _ in range(self.block_count)]
|
| 199 |
+
|
| 200 |
+
self._experts[bid][name] = data_torch
|
| 201 |
+
|
| 202 |
+
if len(self._experts[bid]) >= n_experts * 3:
|
| 203 |
+
# merge the experts into a single 3d tensor
|
| 204 |
+
for w_name in ["gate_proj", "up_proj", "down_proj"]:
|
| 205 |
+
datas: list[Tensor] = []
|
| 206 |
+
|
| 207 |
+
for xid in range(n_experts):
|
| 208 |
+
ename_to_retrieve = f"model.layers.{bid}.mlp.experts.{xid}.{w_name}.weight"
|
| 209 |
+
datas.append(self._experts[bid][ename_to_retrieve])
|
| 210 |
+
del self._experts[bid][ename_to_retrieve]
|
| 211 |
+
|
| 212 |
+
data_torch = torch.stack(datas, dim=0)
|
| 213 |
+
merged_name = f"model.layers.{bid}.mlp.experts.{w_name}.weight"
|
| 214 |
+
|
| 215 |
+
yield from super().modify_tensors(data_torch, merged_name, bid)
|
| 216 |
+
return
|
| 217 |
+
else:
|
| 218 |
+
return
|
| 219 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 220 |
+
|
| 221 |
+
def prepare_tensors(self):
|
| 222 |
+
super().prepare_tensors()
|
| 223 |
+
|
| 224 |
+
if self._experts is not None:
|
| 225 |
+
# flatten `list[dict[str, Tensor]]` into `list[str]`
|
| 226 |
+
experts = [k for d in self._experts for k in d.keys()]
|
| 227 |
+
if len(experts) > 0:
|
| 228 |
+
raise ValueError(f"Unprocessed experts: {experts}")
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
@ModelBase.register("MiMoV2ForCausalLM")
|
| 232 |
+
class MiMoV2VisionModel(MmprojModel):
|
| 233 |
+
def __init__(self, *args, **kwargs):
|
| 234 |
+
super().__init__(*args, **kwargs)
|
| 235 |
+
assert self.hparams_vision is not None
|
| 236 |
+
hp = self.hparams_vision
|
| 237 |
+
|
| 238 |
+
hp["image_size"] = hp.get("image_size", 560)
|
| 239 |
+
hp["num_attention_heads"] = hp.get("num_heads", 32)
|
| 240 |
+
hp["num_hidden_layers"] = hp.get("depth", 28)
|
| 241 |
+
|
| 242 |
+
self.n_q_heads = int(hp["num_heads"])
|
| 243 |
+
self.num_kv_heads = int(hp.get("num_key_value_heads", 8))
|
| 244 |
+
self.head_dim = int(hp.get("qk_channels", 64))
|
| 245 |
+
self.spatial_merge_size = int(hp["spatial_merge_size"])
|
| 246 |
+
# MiMoV2 vision RMSNorm: HF uses getattr(config, "rms_norm_eps", 1e-6) and the
|
| 247 |
+
# field is absent from MiMo-V2.5's vision_config
|
| 248 |
+
self.rms_norm_eps = float(hp.get("rms_norm_eps", 1e-6))
|
| 249 |
+
|
| 250 |
+
# fullatt_block_indexes are also reflected in vit_window_attn_types as -1
|
| 251 |
+
self.fullatt_block_indexes = list(hp.get("fullatt_block_indexes") or [])
|
| 252 |
+
self.vit_window_attn_types = list(hp.get("vit_window_attn_types") or [])
|
| 253 |
+
self.visual_token_window_size = int(hp.get("visual_token_window_size", -1))
|
| 254 |
+
self.use_sink = bool(hp.get("use_sink", False))
|
| 255 |
+
|
| 256 |
+
def set_gguf_parameters(self):
|
| 257 |
+
super().set_gguf_parameters()
|
| 258 |
+
|
| 259 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.MIMOVL)
|
| 260 |
+
self.gguf_writer.add_vision_use_silu(True)
|
| 261 |
+
self.gguf_writer.add_vision_head_count_kv(self.num_kv_heads)
|
| 262 |
+
self.gguf_writer.add_vision_spatial_merge_size(self.spatial_merge_size)
|
| 263 |
+
self.gguf_writer.add_uint32(gguf.Keys.ClipVision.WINDOW_SIZE, self.visual_token_window_size)
|
| 264 |
+
self.gguf_writer.add_vision_wa_pattern_mode(self.vit_window_attn_types)
|
| 265 |
+
self.gguf_writer.add_vision_attention_layernorm_eps(self.rms_norm_eps)
|
| 266 |
+
self.gguf_writer.add_vision_min_pixels(int(self.preprocessor_config["min_pixels"]))
|
| 267 |
+
self.gguf_writer.add_vision_max_pixels(int(self.preprocessor_config["max_pixels"]))
|
| 268 |
+
|
| 269 |
+
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
| 270 |
+
# Sinks must be F32: any sink-style softmax/mask add in ggml requires
|
| 271 |
+
# F32, and we fold sinks into a host-built F32 mask at encode time.
|
| 272 |
+
if new_name.endswith(".attn_sinks"):
|
| 273 |
+
return gguf.GGMLQuantizationType.F32
|
| 274 |
+
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
| 275 |
+
|
| 276 |
+
@classmethod
|
| 277 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 278 |
+
name, _ = item
|
| 279 |
+
if not name.startswith("visual."):
|
| 280 |
+
return None
|
| 281 |
+
return super().filter_tensors(item)
|
| 282 |
+
|
| 283 |
+
def modify_tensors(self, data_torch, name, bid):
|
| 284 |
+
# Conv3D patch embed: split along the temporal axis (kt=2) into two Conv2D
|
| 285 |
+
# weights that the existing qwen2vl-style two-Conv2D path consumes.
|
| 286 |
+
if name == "visual.patch_embed.proj.weight":
|
| 287 |
+
_, _, kt, _, _ = data_torch.shape
|
| 288 |
+
if kt != 2:
|
| 289 |
+
raise ValueError(f"unexpected temporal_patch_size: {kt}")
|
| 290 |
+
embd_name = gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_ENC_EMBD_PATCH]
|
| 291 |
+
yield (embd_name + ".weight", data_torch[:, :, 0, ...])
|
| 292 |
+
yield (embd_name + ".weight.1", data_torch[:, :, 1, ...])
|
| 293 |
+
return
|
| 294 |
+
|
| 295 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
llama.cpp/conversion/minicpm.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Callable, Iterable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING:
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
from .base import MmprojModel, ModelBase, TextModel, gguf, logger
|
| 11 |
+
|
| 12 |
+
from .llama import LlamaModel
|
| 13 |
+
from .qwen import Qwen3_5TextModel
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@ModelBase.register("MiniCPMForCausalLM")
|
| 17 |
+
class MiniCPMModel(TextModel):
|
| 18 |
+
model_arch = gguf.MODEL_ARCH.MINICPM
|
| 19 |
+
|
| 20 |
+
def set_gguf_parameters(self):
|
| 21 |
+
super().set_gguf_parameters()
|
| 22 |
+
embedding_scale = float(self.hparams["scale_emb"])
|
| 23 |
+
self.gguf_writer.add_embedding_scale(embedding_scale)
|
| 24 |
+
logger.info(f"gguf: (minicpm) embedding_scale = {embedding_scale}")
|
| 25 |
+
residual_scale = self.hparams["scale_depth"] / self.hparams["num_hidden_layers"] ** 0.5
|
| 26 |
+
self.gguf_writer.add_residual_scale(residual_scale)
|
| 27 |
+
logger.info(f"gguf: (minicpm) residual_scale = {residual_scale}")
|
| 28 |
+
logit_scale = self.hparams["hidden_size"] / self.hparams["dim_model_base"]
|
| 29 |
+
self.gguf_writer.add_logit_scale(logit_scale)
|
| 30 |
+
logger.info(f"gguf: (minicpm) logit_scale = {logit_scale}")
|
| 31 |
+
|
| 32 |
+
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
|
| 33 |
+
rope_dims = self.hparams["hidden_size"] // self.hparams["num_attention_heads"]
|
| 34 |
+
|
| 35 |
+
long_factors = self.rope_parameters.get('long_factor')
|
| 36 |
+
short_factors = self.rope_parameters.get('short_factor')
|
| 37 |
+
if long_factors or short_factors:
|
| 38 |
+
if long_factors is None or short_factors is None:
|
| 39 |
+
raise KeyError('Missing the required key rope_scaling.long_factor or rope_scaling_short_factor')
|
| 40 |
+
|
| 41 |
+
if len(long_factors) != len(short_factors) or len(long_factors) != rope_dims / 2:
|
| 42 |
+
raise ValueError(f'The length of rope long and short factors must be {rope_dims / 2}')
|
| 43 |
+
|
| 44 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FACTORS_LONG), torch.tensor(long_factors, dtype=torch.float32))
|
| 45 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FACTORS_SHORT), torch.tensor(short_factors, dtype=torch.float32))
|
| 46 |
+
|
| 47 |
+
def set_vocab(self):
|
| 48 |
+
self._set_vocab_sentencepiece()
|
| 49 |
+
|
| 50 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 51 |
+
n_head = self.hparams["num_attention_heads"]
|
| 52 |
+
n_kv_head = self.hparams.get("num_key_value_heads")
|
| 53 |
+
|
| 54 |
+
# HF models permute some of the tensors, so we need to undo that
|
| 55 |
+
if name.endswith(("q_proj.weight")):
|
| 56 |
+
data_torch = LlamaModel.permute(data_torch, n_head, n_head)
|
| 57 |
+
if name.endswith(("k_proj.weight")):
|
| 58 |
+
data_torch = LlamaModel.permute(data_torch, n_head, n_kv_head)
|
| 59 |
+
|
| 60 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@ModelBase.register("MiniCPM3ForCausalLM")
|
| 64 |
+
class MiniCPM3Model(TextModel):
|
| 65 |
+
model_arch = gguf.MODEL_ARCH.MINICPM3
|
| 66 |
+
|
| 67 |
+
def set_gguf_parameters(self):
|
| 68 |
+
hparams = self.hparams
|
| 69 |
+
|
| 70 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 71 |
+
self.gguf_writer.add_context_length(hparams["max_position_embeddings"])
|
| 72 |
+
self.gguf_writer.add_embedding_length(hparams["hidden_size"])
|
| 73 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 74 |
+
self.gguf_writer.add_feed_forward_length(hparams["intermediate_size"])
|
| 75 |
+
self.gguf_writer.add_head_count(hparams["num_attention_heads"])
|
| 76 |
+
self.gguf_writer.add_head_count_kv(hparams["num_key_value_heads"])
|
| 77 |
+
self.gguf_writer.add_layer_norm_rms_eps(hparams["rms_norm_eps"])
|
| 78 |
+
self.gguf_writer.add_vocab_size(hparams["vocab_size"])
|
| 79 |
+
if "q_lora_rank" in hparams and hparams["q_lora_rank"] is not None:
|
| 80 |
+
self.gguf_writer.add_q_lora_rank(hparams["q_lora_rank"])
|
| 81 |
+
self.gguf_writer.add_kv_lora_rank(hparams["kv_lora_rank"])
|
| 82 |
+
self.gguf_writer.add_key_length(hparams["qk_nope_head_dim"] + hparams["qk_rope_head_dim"])
|
| 83 |
+
self.gguf_writer.add_rope_dimension_count(hparams["qk_rope_head_dim"])
|
| 84 |
+
|
| 85 |
+
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
|
| 86 |
+
long_factors = self.rope_parameters.get('long_factor')
|
| 87 |
+
short_factors = self.rope_parameters.get('short_factor')
|
| 88 |
+
if long_factors or short_factors:
|
| 89 |
+
rope_dims = self.hparams["qk_rope_head_dim"]
|
| 90 |
+
|
| 91 |
+
if long_factors is None or short_factors is None:
|
| 92 |
+
raise KeyError('Missing the required key rope_scaling.long_factor or rope_scaling_short_factor')
|
| 93 |
+
|
| 94 |
+
if len(long_factors) != len(short_factors) or len(long_factors) != rope_dims / 2:
|
| 95 |
+
raise ValueError(f'The length of rope long and short factors must be {rope_dims / 2}')
|
| 96 |
+
|
| 97 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FACTORS_LONG), torch.tensor(long_factors, dtype=torch.float32))
|
| 98 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FACTORS_SHORT), torch.tensor(short_factors, dtype=torch.float32))
|
| 99 |
+
|
| 100 |
+
def set_vocab(self):
|
| 101 |
+
self._set_vocab_sentencepiece()
|
| 102 |
+
|
| 103 |
+
def _reverse_hf_permute(self, weights: Tensor, n_head: int, n_kv_head: int | None = None) -> Tensor:
|
| 104 |
+
if n_kv_head is not None and n_head != n_kv_head:
|
| 105 |
+
n_head //= n_kv_head
|
| 106 |
+
|
| 107 |
+
return (
|
| 108 |
+
weights.reshape(n_head, 2, weights.shape[0] // n_head // 2, *weights.shape[1:])
|
| 109 |
+
.swapaxes(1, 2)
|
| 110 |
+
.reshape(weights.shape)
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
# MiniCPM-V 4.6: text tower is Qwen3.5 (linear+full hybrid attention) wrapped under
|
| 115 |
+
# `model.language_model.*`; vision tower is SigLIP + a window-attention ViT merger
|
| 116 |
+
# + a final DownsampleMLP merger. The same HF arch is registered twice below: once as
|
| 117 |
+
# the LM (text mode) and once as the mmproj (vision mode), mirroring the Qwen3-VL setup.
|
| 118 |
+
|
| 119 |
+
@ModelBase.register("MiniCPMV4_6ForConditionalGeneration")
|
| 120 |
+
class MiniCPMV4_6TextModel(Qwen3_5TextModel):
|
| 121 |
+
model_arch = gguf.MODEL_ARCH.QWEN35
|
| 122 |
+
|
| 123 |
+
@classmethod
|
| 124 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 125 |
+
name, gen = item
|
| 126 |
+
|
| 127 |
+
if name.startswith("model.merger."):
|
| 128 |
+
return None
|
| 129 |
+
# MTP tensors are not used at inference yet; align with Qwen3Next behaviour
|
| 130 |
+
if name.startswith("mtp"):
|
| 131 |
+
return None
|
| 132 |
+
|
| 133 |
+
return super().filter_tensors(item)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
@ModelBase.register("MiniCPMV4_6ForConditionalGeneration")
|
| 137 |
+
class MiniCPMV4_6VisionModel(MmprojModel):
|
| 138 |
+
def __init__(self, *args, **kwargs):
|
| 139 |
+
super().__init__(*args, **kwargs)
|
| 140 |
+
if self.hparams_vision is not None:
|
| 141 |
+
# In MiniCPM-V 4.6 `vision_config.image_size` (980) describes the SigLIP
|
| 142 |
+
# positional embedding bucket grid (70 x 70), while the per-slice processing
|
| 143 |
+
# resolution is the preprocessor's `scale_resolution` (typically 448).
|
| 144 |
+
# The CLIP loader in tools/mtmd/clip.cpp consumes `clip.vision.image_size`
|
| 145 |
+
# as the slice size and warmup resolution, so report `scale_resolution` there
|
| 146 |
+
# to match the upstream MiniCPMV4_6ImageProcessorPil slicing rules.
|
| 147 |
+
scale_resolution = self.preprocessor_config.get("scale_resolution")
|
| 148 |
+
if scale_resolution is not None:
|
| 149 |
+
self.hparams_vision["image_size"] = int(scale_resolution)
|
| 150 |
+
|
| 151 |
+
def set_gguf_parameters(self):
|
| 152 |
+
super().set_gguf_parameters()
|
| 153 |
+
assert self.hparams_vision is not None
|
| 154 |
+
|
| 155 |
+
# projector type string is consumed by clip_projector_type_from_string() in clip.cpp
|
| 156 |
+
# (mapped to PROJECTOR_TYPE_MINICPMV4_6).
|
| 157 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.MINICPMV4_6)
|
| 158 |
+
|
| 159 |
+
# ViT merger 2x2 + final merger 2x2 = 4x spatial merge per dimension; used for slice alignment
|
| 160 |
+
self.gguf_writer.add_vision_projector_scale_factor(4)
|
| 161 |
+
|
| 162 |
+
# borrow wa_layer_indexes for vit_merger insertion point
|
| 163 |
+
insert_layer_id = int(self.global_config.get(
|
| 164 |
+
"insert_layer_id", self.hparams_vision.get("insert_layer_id", 6)))
|
| 165 |
+
self.gguf_writer.add_vision_wa_layer_indexes([insert_layer_id])
|
| 166 |
+
|
| 167 |
+
# SigLIP vision body uses gelu_pytorch_tanh, which matches ggml_gelu (tanh approx).
|
| 168 |
+
self.gguf_writer.add_vision_use_gelu(True)
|
| 169 |
+
self.gguf_writer.add_vision_attention_layernorm_eps(
|
| 170 |
+
self.hparams_vision.get("layer_norm_eps", 1e-6))
|
| 171 |
+
|
| 172 |
+
@classmethod
|
| 173 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 174 |
+
name, gen = item
|
| 175 |
+
|
| 176 |
+
# lm_head / MTP -> belong to the LM file
|
| 177 |
+
if name.startswith(("lm_head.", "mtp")):
|
| 178 |
+
return None
|
| 179 |
+
|
| 180 |
+
return super().filter_tensors(item)
|
llama.cpp/conversion/minimax.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING:
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
from .base import ModelBase, TextModel, gguf
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@ModelBase.register("MiniMaxM2ForCausalLM")
|
| 14 |
+
class MiniMaxM2Model(TextModel):
|
| 15 |
+
model_arch = gguf.MODEL_ARCH.MINIMAXM2
|
| 16 |
+
_experts_cache: dict[int, dict[str, Tensor]] = {}
|
| 17 |
+
|
| 18 |
+
def set_gguf_parameters(self):
|
| 19 |
+
super().set_gguf_parameters()
|
| 20 |
+
|
| 21 |
+
self.gguf_writer.add_expert_feed_forward_length(self.find_hparam(["intermediate_size"]))
|
| 22 |
+
self.gguf_writer.add_rope_dimension_count(self.find_hparam(["rotary_dim"]))
|
| 23 |
+
|
| 24 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None):
|
| 25 |
+
# merge expert weights
|
| 26 |
+
if 'experts' in name:
|
| 27 |
+
n_experts = self.find_hparam(["num_local_experts", "num_experts"])
|
| 28 |
+
assert bid is not None
|
| 29 |
+
|
| 30 |
+
expert_cache = self._experts_cache.setdefault(bid, {})
|
| 31 |
+
expert_cache[name] = data_torch
|
| 32 |
+
expert_weights = ["w1", "w2", "w3"]
|
| 33 |
+
|
| 34 |
+
# not enough expert weights to merge
|
| 35 |
+
if len(expert_cache) < n_experts * len(expert_weights):
|
| 36 |
+
return
|
| 37 |
+
|
| 38 |
+
for w_name in expert_weights:
|
| 39 |
+
datas: list[Tensor] = []
|
| 40 |
+
|
| 41 |
+
for xid in range(n_experts):
|
| 42 |
+
ename = f"model.layers.{bid}.block_sparse_moe.experts.{xid}.{w_name}.weight"
|
| 43 |
+
datas.append(expert_cache[ename])
|
| 44 |
+
del expert_cache[ename]
|
| 45 |
+
|
| 46 |
+
data_torch = torch.stack(datas, dim=0)
|
| 47 |
+
merged_name = f"model.layers.{bid}.block_sparse_moe.experts.{w_name}.weight"
|
| 48 |
+
new_name = self.map_tensor_name(merged_name)
|
| 49 |
+
yield from super().modify_tensors(data_torch, new_name, bid)
|
| 50 |
+
|
| 51 |
+
del self._experts_cache[bid]
|
| 52 |
+
return
|
| 53 |
+
|
| 54 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
llama.cpp/conversion/mistral.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Callable, TYPE_CHECKING
|
| 5 |
+
|
| 6 |
+
if TYPE_CHECKING:
|
| 7 |
+
from torch import Tensor
|
| 8 |
+
|
| 9 |
+
from .base import MistralTokenizerType, MistralVocab, _mistral_common_installed, _mistral_import_error_msg, gguf, logger
|
| 10 |
+
|
| 11 |
+
from .deepseek import DeepseekV2Model
|
| 12 |
+
from .llama import LlamaModel
|
| 13 |
+
|
| 14 |
+
if _mistral_common_installed:
|
| 15 |
+
from mistral_common.tokens.tokenizers.base import TokenizerVersion # type: ignore[import-not-found, ty:unresolved-import]
|
| 16 |
+
from mistral_common.tokens.tokenizers.tekken import Tekkenizer # type: ignore[import-not-found, ty:unresolved-import]
|
| 17 |
+
from mistral_common.tokens.tokenizers.sentencepiece import SentencePieceTokenizer # type: ignore[import-not-found, ty:unresolved-import]
|
| 18 |
+
else:
|
| 19 |
+
TokenizerVersion = None # type: ignore[assignment]
|
| 20 |
+
Tekkenizer = None # type: ignore[assignment]
|
| 21 |
+
SentencePieceTokenizer = None # type: ignore[assignment]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class MistralModel(LlamaModel):
|
| 25 |
+
model_arch = gguf.MODEL_ARCH.MISTRAL3
|
| 26 |
+
model_name = "Mistral"
|
| 27 |
+
hf_arch = ""
|
| 28 |
+
is_mistral_format = True
|
| 29 |
+
undo_permute = False
|
| 30 |
+
|
| 31 |
+
def __init__(self, *args, **kwargs):
|
| 32 |
+
super().__init__(*args, **kwargs)
|
| 33 |
+
# for compatibility, we use LLAMA arch for older models
|
| 34 |
+
# TODO: remove this once everyone migrates to newer version of llama.cpp
|
| 35 |
+
if "llama_4_scaling" not in self.hparams:
|
| 36 |
+
self.model_arch = gguf.MODEL_ARCH.LLAMA
|
| 37 |
+
self.gguf_writer.arch = gguf.MODEL_ARCH_NAMES[self.model_arch]
|
| 38 |
+
self.gguf_writer.add_architecture()
|
| 39 |
+
self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
|
| 40 |
+
|
| 41 |
+
def dequant_model(self):
|
| 42 |
+
# transform quantization config into HF format
|
| 43 |
+
quant_config = self.hparams.get("quantization")
|
| 44 |
+
if quant_config is not None:
|
| 45 |
+
assert quant_config["qformat_weight"] == "fp8_e4m3"
|
| 46 |
+
self.hparams["quantization_config"] = {
|
| 47 |
+
"activation_scheme": "static",
|
| 48 |
+
"quant_method": "fp8",
|
| 49 |
+
"weight_block_size": None,
|
| 50 |
+
}
|
| 51 |
+
return super().dequant_model()
|
| 52 |
+
|
| 53 |
+
@staticmethod
|
| 54 |
+
def get_community_chat_template(vocab: MistralVocab, templates_dir: Path, is_mistral_format: bool):
|
| 55 |
+
assert TokenizerVersion is not None and Tekkenizer is not None and SentencePieceTokenizer is not None, _mistral_import_error_msg
|
| 56 |
+
assert isinstance(vocab.tokenizer, (Tekkenizer, SentencePieceTokenizer)), (
|
| 57 |
+
f"Expected Tekkenizer or SentencePieceTokenizer, got {type(vocab.tokenizer)}"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
if vocab.tokenizer.version == TokenizerVersion.v1:
|
| 61 |
+
return "mistral-v1"
|
| 62 |
+
elif vocab.tokenizer.version == TokenizerVersion.v3 and vocab.tokenizer_type == MistralTokenizerType.spm:
|
| 63 |
+
return "mistral-v3"
|
| 64 |
+
elif vocab.tokenizer.version == TokenizerVersion.v3 and vocab.tokenizer_type == MistralTokenizerType.tekken:
|
| 65 |
+
return "mistral-v3-tekken"
|
| 66 |
+
elif vocab.tokenizer.version == TokenizerVersion.v7 and vocab.tokenizer_type == MistralTokenizerType.spm:
|
| 67 |
+
return "mistral-v7"
|
| 68 |
+
elif vocab.tokenizer.version == TokenizerVersion.v7 and vocab.tokenizer_type == MistralTokenizerType.tekken:
|
| 69 |
+
return "mistral-v7-tekken"
|
| 70 |
+
elif vocab.tokenizer.version == TokenizerVersion.v11:
|
| 71 |
+
template_file = "Mistral-Small-3.2-24B-Instruct-2506.jinja"
|
| 72 |
+
elif vocab.tokenizer.version == TokenizerVersion.v13:
|
| 73 |
+
template_file = "unsloth-mistral-Devstral-Small-2507.jinja"
|
| 74 |
+
else:
|
| 75 |
+
err_message = f"Unknown tokenizer type: {vocab.tokenizer_type} and version {vocab.tokenizer.version}"
|
| 76 |
+
if is_mistral_format:
|
| 77 |
+
err_message += (
|
| 78 |
+
" . Please pass --disable-mistral-community-chat-template argument to the CLI "
|
| 79 |
+
"if you want to skip this error and use the Mistral official `mistral-common` pre-processing library."
|
| 80 |
+
)
|
| 81 |
+
raise ValueError(err_message)
|
| 82 |
+
|
| 83 |
+
template_path = templates_dir / template_file
|
| 84 |
+
if not template_path.exists():
|
| 85 |
+
raise FileNotFoundError(f"Template file not found: {template_path}")
|
| 86 |
+
|
| 87 |
+
with open(template_path, "r", encoding="utf-8") as f:
|
| 88 |
+
template = f.read()
|
| 89 |
+
|
| 90 |
+
return template
|
| 91 |
+
|
| 92 |
+
def set_gguf_parameters(self):
|
| 93 |
+
super().set_gguf_parameters()
|
| 94 |
+
MistralModel.set_mistral_config(self.gguf_writer, self.hparams)
|
| 95 |
+
|
| 96 |
+
@staticmethod
|
| 97 |
+
def set_mistral_config(gguf_writer: gguf.GGUFWriter, hparams: dict):
|
| 98 |
+
if "yarn" in hparams:
|
| 99 |
+
yarn_params = hparams["yarn"]
|
| 100 |
+
mscale_all_dim = 1.0 if not yarn_params["apply_scale"] else 0.0
|
| 101 |
+
gguf_writer.add_rope_scaling_type(gguf.RopeScalingType.YARN)
|
| 102 |
+
gguf_writer.add_rope_scaling_factor(yarn_params["factor"])
|
| 103 |
+
gguf_writer.add_rope_scaling_yarn_beta_fast(yarn_params["beta"])
|
| 104 |
+
gguf_writer.add_rope_scaling_yarn_beta_slow(yarn_params["alpha"])
|
| 105 |
+
gguf_writer.add_rope_scaling_yarn_log_mul(mscale_all_dim)
|
| 106 |
+
gguf_writer.add_rope_scaling_orig_ctx_len(yarn_params["original_max_position_embeddings"])
|
| 107 |
+
|
| 108 |
+
llama_4_scaling = hparams.get("llama_4_scaling")
|
| 109 |
+
if llama_4_scaling is not None:
|
| 110 |
+
gguf_writer.add_attn_temperature_scale(llama_4_scaling["beta"])
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class MistralMoeModel(DeepseekV2Model):
|
| 114 |
+
model_arch = gguf.MODEL_ARCH.DEEPSEEK2
|
| 115 |
+
model_name = "Mistral"
|
| 116 |
+
hf_arch = ""
|
| 117 |
+
is_mistral_format = True
|
| 118 |
+
|
| 119 |
+
def __init__(self, *args, **kwargs):
|
| 120 |
+
super().__init__(*args, **kwargs)
|
| 121 |
+
logger.info("Using MistralMoeModel")
|
| 122 |
+
# remap hparams from Mistral MoE format to DeepseekV2 format
|
| 123 |
+
# we do this way to be able to reuse DeepseekV2Model set_gguf_parameters logic
|
| 124 |
+
# ref: https://github.com/vllm-project/vllm/blob/b294e28db2c5dee61bc25157664edcada8b90b31/vllm/transformers_utils/configs/mistral.py
|
| 125 |
+
config = self.hparams
|
| 126 |
+
# Mistral key -> HF key
|
| 127 |
+
config_mapping = {
|
| 128 |
+
"dim": "hidden_size",
|
| 129 |
+
"norm_eps": "rms_norm_eps",
|
| 130 |
+
"n_kv_heads": "num_key_value_heads",
|
| 131 |
+
"n_layers": "num_hidden_layers",
|
| 132 |
+
"n_heads": "num_attention_heads",
|
| 133 |
+
"hidden_dim": "intermediate_size",
|
| 134 |
+
}
|
| 135 |
+
# HF key -> (Mistral key, default value)
|
| 136 |
+
top_level_mapping_with_default = {
|
| 137 |
+
"model_type": ("model_type", "transformer"),
|
| 138 |
+
"hidden_act": ("activation", "silu"),
|
| 139 |
+
"tie_word_embeddings": ("tied_embeddings", False),
|
| 140 |
+
"max_seq_len": ("max_seq_len", config.get("max_position_embeddings", 128_000)),
|
| 141 |
+
"max_position_embeddings": ("max_position_embeddings", 128_000),
|
| 142 |
+
}
|
| 143 |
+
# mapping top-level keys
|
| 144 |
+
for key, new_key in config_mapping.items():
|
| 145 |
+
if key in config:
|
| 146 |
+
config[new_key] = config[key]
|
| 147 |
+
for new_key, (key, default_value) in top_level_mapping_with_default.items():
|
| 148 |
+
config[new_key] = config.get(key, default_value)
|
| 149 |
+
# mapping MoE-specific keys
|
| 150 |
+
moe_config_map = {
|
| 151 |
+
"route_every_n": "moe_layer_freq",
|
| 152 |
+
"first_k_dense_replace": "first_k_dense_replace",
|
| 153 |
+
"num_experts_per_tok": "num_experts_per_tok",
|
| 154 |
+
"num_experts": "n_routed_experts",
|
| 155 |
+
"expert_hidden_dim": "moe_intermediate_size",
|
| 156 |
+
"routed_scale": "routed_scaling_factor",
|
| 157 |
+
"num_shared_experts": "n_shared_experts",
|
| 158 |
+
"num_expert_groups": "n_group",
|
| 159 |
+
"num_expert_groups_per_tok": "topk_group",
|
| 160 |
+
}
|
| 161 |
+
moe = config["moe"]
|
| 162 |
+
for key, new_key in moe_config_map.items():
|
| 163 |
+
if key in moe:
|
| 164 |
+
config[new_key] = moe[key]
|
| 165 |
+
# provide missing values
|
| 166 |
+
config["topk_method"] = None
|
| 167 |
+
config["norm_topk_prob"] = True
|
| 168 |
+
config["scoring_func"] = "softmax"
|
| 169 |
+
|
| 170 |
+
def set_vocab(self):
|
| 171 |
+
self._set_vocab_mistral()
|
| 172 |
+
|
| 173 |
+
def set_gguf_parameters(self):
|
| 174 |
+
super().set_gguf_parameters()
|
| 175 |
+
MistralModel.set_mistral_config(self.gguf_writer, self.hparams)
|
| 176 |
+
yarn_params = self.hparams["yarn"]
|
| 177 |
+
self.gguf_writer.add_attn_temperature_length(yarn_params["original_max_position_embeddings"])
|
| 178 |
+
|
| 179 |
+
# [TAG_DEEPSEEK2_YARN_LOG_MUL_FIX]
|
| 180 |
+
# note: for legacy reasons, this is not consistent with the other usages of self.gguf_writer.add_rope_scaling_yarn_log_mul
|
| 181 |
+
# ref https://github.com/ggml-org/llama.cpp/pull/17945
|
| 182 |
+
self.gguf_writer.add_rope_scaling_yarn_log_mul(0.1) # mscale_all_dim * 0.1
|
| 183 |
+
|
| 184 |
+
@classmethod
|
| 185 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 186 |
+
name, gen = item
|
| 187 |
+
|
| 188 |
+
# rename certain tensors so that we can reuse DeepseekV2Model modify_tensors logic
|
| 189 |
+
if name.endswith(".qscale_act"):
|
| 190 |
+
name = name.replace(".qscale_act", ".input_scale")
|
| 191 |
+
if name.endswith(".qscale_weight"):
|
| 192 |
+
name = name.replace(".qscale_weight", ".weight_scale")
|
| 193 |
+
if ".wkv_b." in name:
|
| 194 |
+
name = name.replace(".wkv_b.", ".kv_b_proj.")
|
| 195 |
+
if ".experts." in name:
|
| 196 |
+
name = name.replace(".experts.", ".mlp.experts.")
|
| 197 |
+
name = name.replace(".w1.", ".gate_proj.")
|
| 198 |
+
name = name.replace(".w2.", ".down_proj.")
|
| 199 |
+
name = name.replace(".w3.", ".up_proj.")
|
| 200 |
+
name = "model." + name
|
| 201 |
+
|
| 202 |
+
return super().filter_tensors((name, gen))
|
llama.cpp/conversion/mistral3.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
if TYPE_CHECKING:
|
| 6 |
+
from torch import Tensor
|
| 7 |
+
|
| 8 |
+
from .base import ModelBase, TextModel, gguf
|
| 9 |
+
|
| 10 |
+
from .deepseek import DeepseekV2Model
|
| 11 |
+
from .llama import LlamaModel
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@ModelBase.register(
|
| 15 |
+
"Mistral3ForConditionalGeneration",
|
| 16 |
+
"Ministral3ForCausalLM",
|
| 17 |
+
)
|
| 18 |
+
class Mistral3Model(TextModel):
|
| 19 |
+
class Ministral3Model(LlamaModel):
|
| 20 |
+
model_arch = gguf.MODEL_ARCH.MISTRAL3
|
| 21 |
+
|
| 22 |
+
def set_gguf_parameters(self):
|
| 23 |
+
super().set_gguf_parameters()
|
| 24 |
+
rope_params = self.rope_parameters
|
| 25 |
+
if self.hparams.get("model_type") == "ministral3":
|
| 26 |
+
assert rope_params, "ministral3 must have 'rope_parameters' config"
|
| 27 |
+
assert rope_params["rope_type"] == "yarn", "ministral3 rope_type must be 'yarn'"
|
| 28 |
+
self.gguf_writer.add_rope_scaling_yarn_log_mul(rope_params["mscale_all_dim"])
|
| 29 |
+
self.gguf_writer.add_attn_temperature_scale(rope_params["llama_4_scaling_beta"])
|
| 30 |
+
|
| 31 |
+
class Mistral4Model(DeepseekV2Model):
|
| 32 |
+
model_arch = gguf.MODEL_ARCH.MISTRAL4
|
| 33 |
+
skip_mtp = False # model contains no MTP layers, so no need to skip
|
| 34 |
+
merge_expert = False # experts are already stacked as 3D
|
| 35 |
+
|
| 36 |
+
def modify_tensors(self, data_torch, name, bid):
|
| 37 |
+
if name.endswith(".down_proj") or name.endswith(".gate_up_proj"):
|
| 38 |
+
name = name + ".weight"
|
| 39 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 40 |
+
|
| 41 |
+
model_arch = gguf.MODEL_ARCH.MISTRAL3 # unused
|
| 42 |
+
impl: TextModel
|
| 43 |
+
|
| 44 |
+
def __init__(self, *args, **kwargs):
|
| 45 |
+
super().__init__(*args, **kwargs)
|
| 46 |
+
if self.hparams.get("model_type") == "mistral4":
|
| 47 |
+
self.impl = Mistral3Model.Mistral4Model(*args, **kwargs)
|
| 48 |
+
else:
|
| 49 |
+
self.impl = Mistral3Model.Ministral3Model(*args, **kwargs)
|
| 50 |
+
|
| 51 |
+
def set_vocab(self):
|
| 52 |
+
self.impl.set_vocab()
|
| 53 |
+
|
| 54 |
+
def set_gguf_parameters(self):
|
| 55 |
+
self.impl.set_gguf_parameters()
|
| 56 |
+
|
| 57 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None):
|
| 58 |
+
yield from self.impl.modify_tensors(data_torch, name, bid)
|
| 59 |
+
|
| 60 |
+
def prepare_tensors(self):
|
| 61 |
+
self.impl.prepare_tensors()
|
| 62 |
+
|
| 63 |
+
def write_vocab(self):
|
| 64 |
+
self.impl.write_vocab()
|
| 65 |
+
|
| 66 |
+
def write(self):
|
| 67 |
+
self.impl.write()
|
llama.cpp/conversion/mpt.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Iterable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
if TYPE_CHECKING:
|
| 6 |
+
from torch import Tensor
|
| 7 |
+
|
| 8 |
+
from .base import ModelBase, TextModel, gguf
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@ModelBase.register("MPTForCausalLM")
|
| 12 |
+
class MPTModel(TextModel):
|
| 13 |
+
model_arch = gguf.MODEL_ARCH.MPT
|
| 14 |
+
|
| 15 |
+
def set_vocab(self):
|
| 16 |
+
try:
|
| 17 |
+
self._set_vocab_gpt2()
|
| 18 |
+
except Exception:
|
| 19 |
+
# Fallback for SEA-LION model
|
| 20 |
+
self._set_vocab_sentencepiece()
|
| 21 |
+
self.gguf_writer.add_add_bos_token(False)
|
| 22 |
+
self.gguf_writer.add_pad_token_id(3)
|
| 23 |
+
self.gguf_writer.add_eos_token_id(1)
|
| 24 |
+
self.gguf_writer.add_unk_token_id(0)
|
| 25 |
+
|
| 26 |
+
def set_gguf_parameters(self):
|
| 27 |
+
self.gguf_writer.add_context_length(self.hparams["max_seq_len"])
|
| 28 |
+
self.gguf_writer.add_embedding_length(self.hparams["d_model"])
|
| 29 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 30 |
+
self.gguf_writer.add_feed_forward_length(4 * self.hparams["d_model"])
|
| 31 |
+
self.gguf_writer.add_head_count(self.hparams["n_heads"])
|
| 32 |
+
if kv_n_heads := self.hparams["attn_config"].get("kv_n_heads"):
|
| 33 |
+
self.gguf_writer.add_head_count_kv(kv_n_heads)
|
| 34 |
+
self.gguf_writer.add_layer_norm_eps(1e-5)
|
| 35 |
+
if self.hparams["attn_config"]["clip_qkv"] is not None:
|
| 36 |
+
self.gguf_writer.add_clamp_kqv(self.hparams["attn_config"]["clip_qkv"])
|
| 37 |
+
if self.hparams["attn_config"]["alibi"]:
|
| 38 |
+
self.gguf_writer.add_max_alibi_bias(self.hparams["attn_config"]["alibi_bias_max"])
|
| 39 |
+
else:
|
| 40 |
+
self.gguf_writer.add_max_alibi_bias(0.0)
|
| 41 |
+
|
| 42 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 43 |
+
if "scales" in name:
|
| 44 |
+
new_name = self.map_tensor_name(name, try_suffixes=(".weight", ".bias", ".scales"))
|
| 45 |
+
new_name = new_name.replace("scales", "act.scales")
|
| 46 |
+
else:
|
| 47 |
+
new_name = self.map_tensor_name(name, try_suffixes=(".weight", ".bias"))
|
| 48 |
+
|
| 49 |
+
yield from super().modify_tensors(data_torch, new_name, bid)
|
llama.cpp/conversion/nemotron.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any, Callable, Iterable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING:
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
from .base import MmprojModel, ModelBase, TextModel, gguf, logger
|
| 11 |
+
|
| 12 |
+
from .granite import GraniteHybridModel
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@ModelBase.register(
|
| 16 |
+
"NemotronH_Nano_VL_V2",
|
| 17 |
+
"RADIOModel",
|
| 18 |
+
)
|
| 19 |
+
class NemotronNanoV2VLModel(MmprojModel):
|
| 20 |
+
# ViT-Huge architecture parameters for RADIO v2.5-h
|
| 21 |
+
_vit_hidden_size = 1280
|
| 22 |
+
_vit_intermediate_size = 5120
|
| 23 |
+
_vit_num_layers = 32
|
| 24 |
+
_vit_num_heads = 16
|
| 25 |
+
|
| 26 |
+
def get_vision_config(self) -> dict[str, Any] | None:
|
| 27 |
+
# RADIO config doesn't have standard ViT parameters, so they need to be constructed manually
|
| 28 |
+
vision_config = self.global_config.get("vision_config")
|
| 29 |
+
if vision_config is None:
|
| 30 |
+
return None
|
| 31 |
+
# Add ViT-H parameters
|
| 32 |
+
vision_config = {
|
| 33 |
+
**vision_config,
|
| 34 |
+
"hidden_size": self._vit_hidden_size,
|
| 35 |
+
"intermediate_size": self._vit_intermediate_size,
|
| 36 |
+
"num_hidden_layers": self._vit_num_layers,
|
| 37 |
+
"num_attention_heads": self._vit_num_heads,
|
| 38 |
+
"image_size": self.global_config.get("force_image_size", 512),
|
| 39 |
+
}
|
| 40 |
+
return vision_config
|
| 41 |
+
|
| 42 |
+
def set_gguf_parameters(self):
|
| 43 |
+
if "image_mean" not in self.preprocessor_config:
|
| 44 |
+
self.preprocessor_config["image_mean"] = [0.485, 0.456, 0.406]
|
| 45 |
+
if "image_std" not in self.preprocessor_config:
|
| 46 |
+
self.preprocessor_config["image_std"] = [0.229, 0.224, 0.225]
|
| 47 |
+
|
| 48 |
+
super().set_gguf_parameters()
|
| 49 |
+
hparams = self.global_config
|
| 50 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.NEMOTRON_V2_VL)
|
| 51 |
+
self.gguf_writer.add_vision_attention_layernorm_eps(1e-6)
|
| 52 |
+
self.gguf_writer.add_vision_use_gelu(True)
|
| 53 |
+
downsample_ratio = hparams.get("downsample_ratio", 0.5)
|
| 54 |
+
self.gguf_writer.add_vision_projector_scale_factor(int(1.0 / downsample_ratio))
|
| 55 |
+
|
| 56 |
+
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
| 57 |
+
if ".position_embd." in new_name or "pos_embed" in new_name:
|
| 58 |
+
return gguf.GGMLQuantizationType.F32
|
| 59 |
+
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
| 60 |
+
|
| 61 |
+
@classmethod
|
| 62 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 63 |
+
name, gen = item
|
| 64 |
+
|
| 65 |
+
if "input_conditioner" in name:
|
| 66 |
+
return None
|
| 67 |
+
|
| 68 |
+
# mtmd does not support video yet so skip tensors related to video.
|
| 69 |
+
if "radio_model.model.patch_generator.video_embedder" in name:
|
| 70 |
+
return None
|
| 71 |
+
|
| 72 |
+
if not name.startswith("vision_model.radio_model.model.") and not name.startswith("mlp1."):
|
| 73 |
+
return None
|
| 74 |
+
|
| 75 |
+
if "patch_generator.pos_embed" in name:
|
| 76 |
+
if not name.endswith(".weight"):
|
| 77 |
+
name += ".weight"
|
| 78 |
+
|
| 79 |
+
return super().filter_tensors((name, gen))
|
| 80 |
+
|
| 81 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 82 |
+
# RADIO's pos_embed doesn't have .weight suffix, but clip.cpp expects it
|
| 83 |
+
if "patch_generator.pos_embed" in name:
|
| 84 |
+
# Downsample position embeddings for fixed 512x512 image size
|
| 85 |
+
import torch.nn.functional as F
|
| 86 |
+
n_embd = self.hparams["hidden_size"]
|
| 87 |
+
image_size = self.global_config.get("force_image_size", 512)
|
| 88 |
+
patch_size = self.hparams["patch_size"]
|
| 89 |
+
target_patches_per_side = image_size // patch_size # 32
|
| 90 |
+
max_patches_per_side = int((data_torch.shape[1]) ** 0.5) # 128
|
| 91 |
+
if target_patches_per_side != max_patches_per_side:
|
| 92 |
+
# Reshape to grid, interpolate, flatten back
|
| 93 |
+
data_torch = data_torch.reshape(1, max_patches_per_side, max_patches_per_side, n_embd)
|
| 94 |
+
data_torch = data_torch.permute(0, 3, 1, 2).float() # [1, n_embd, 128, 128]
|
| 95 |
+
data_torch = F.interpolate(data_torch, size=(target_patches_per_side, target_patches_per_side),
|
| 96 |
+
mode='bilinear', align_corners=True)
|
| 97 |
+
data_torch = data_torch.permute(0, 2, 3, 1) # [1, 32, 32, n_embd]
|
| 98 |
+
data_torch = data_torch.reshape(1, target_patches_per_side * target_patches_per_side, n_embd)
|
| 99 |
+
|
| 100 |
+
# Reshape linear patch embedding to conv2d format for ggml_conv_2d
|
| 101 |
+
# From [n_embd, patch_size*patch_size*3] to [n_embd, 3, patch_size, patch_size]
|
| 102 |
+
if "patch_generator.embedder" in name:
|
| 103 |
+
patch_size = self.hparams["patch_size"]
|
| 104 |
+
n_embd = self.hparams["hidden_size"]
|
| 105 |
+
data_torch = data_torch.reshape(n_embd, 3, patch_size, patch_size)
|
| 106 |
+
|
| 107 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
@ModelBase.register("NemotronForCausalLM")
|
| 111 |
+
class NemotronModel(TextModel):
|
| 112 |
+
model_arch = gguf.MODEL_ARCH.NEMOTRON
|
| 113 |
+
|
| 114 |
+
def set_vocab(self):
|
| 115 |
+
self._set_vocab_sentencepiece()
|
| 116 |
+
self.gguf_writer.add_pad_token_id(0)
|
| 117 |
+
self.gguf_writer.add_unk_token_id(1)
|
| 118 |
+
|
| 119 |
+
def set_gguf_parameters(self):
|
| 120 |
+
super().set_gguf_parameters()
|
| 121 |
+
hparams = self.hparams
|
| 122 |
+
self.gguf_writer.add_vocab_size(hparams["vocab_size"])
|
| 123 |
+
|
| 124 |
+
f_norm_eps = self.find_hparam(["layer_norm_eps", "layer_norm_epsilon", "norm_epsilon", "norm_eps"])
|
| 125 |
+
self.gguf_writer.add_layer_norm_eps(f_norm_eps)
|
| 126 |
+
|
| 127 |
+
# * Partial RoPE
|
| 128 |
+
rot_pct = self.rope_parameters["partial_rotary_factor"]
|
| 129 |
+
n_embd = self.find_hparam(["hidden_size", "n_embd"])
|
| 130 |
+
n_head = self.find_hparam(["num_attention_heads", "n_head"])
|
| 131 |
+
self.gguf_writer.add_rope_dimension_count(int(rot_pct * n_embd) // n_head)
|
| 132 |
+
|
| 133 |
+
# * RopeScaling for Nemotron
|
| 134 |
+
factor = self.hparams.get("factor") or self.rope_parameters.get("factor")
|
| 135 |
+
if factor is None:
|
| 136 |
+
self.gguf_writer.add_rope_scaling_type(gguf.RopeScalingType.NONE)
|
| 137 |
+
else:
|
| 138 |
+
self.gguf_writer.add_rope_scaling_type(gguf.RopeScalingType.LINEAR)
|
| 139 |
+
self.gguf_writer.add_rope_scaling_factor(factor)
|
| 140 |
+
|
| 141 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 142 |
+
# * Adding +1 to LayerNorm's weights here to implement layernorm1p w/o changing anything on the GGML engine side
|
| 143 |
+
# model.layers.{l}.input_layernorm.weight
|
| 144 |
+
# model.layers.{l}.post_attention_layernorm.weight
|
| 145 |
+
# model.norm.weight
|
| 146 |
+
if name.endswith("norm.weight"):
|
| 147 |
+
data_torch = data_torch + 1
|
| 148 |
+
|
| 149 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
@ModelBase.register("NemotronHForCausalLM")
|
| 153 |
+
class NemotronHModel(GraniteHybridModel):
|
| 154 |
+
"""Hybrid mamba2/attention model from NVIDIA"""
|
| 155 |
+
model_arch = gguf.MODEL_ARCH.NEMOTRON_H
|
| 156 |
+
is_moe: bool = False
|
| 157 |
+
|
| 158 |
+
def __init__(self, *args, **kwargs):
|
| 159 |
+
# We have to determine the correct model architecture (MoE vs non-MoE) before
|
| 160 |
+
# calling the parent __init__. This is because the parent constructor
|
| 161 |
+
# uses self.model_arch to build the tensor name map, and all MoE-specific
|
| 162 |
+
# mappings would be missed if it were called with the default non-MoE arch.
|
| 163 |
+
hparams = ModelBase.load_hparams(args[0], self.is_mistral_format)
|
| 164 |
+
has_moe_params = (
|
| 165 |
+
"num_experts_per_tok" in hparams
|
| 166 |
+
or (isinstance(hparams.get("llm_config"), dict) and "num_experts_per_tok" in hparams["llm_config"])
|
| 167 |
+
)
|
| 168 |
+
if has_moe_params:
|
| 169 |
+
self.model_arch = gguf.MODEL_ARCH.NEMOTRON_H_MOE
|
| 170 |
+
self.is_moe = True
|
| 171 |
+
|
| 172 |
+
super().__init__(*args, **kwargs)
|
| 173 |
+
|
| 174 |
+
# Save the top-level head_dim for later
|
| 175 |
+
self.head_dim = self.hparams.get("head_dim", self.hparams.get("attention_head_dim"))
|
| 176 |
+
assert self.head_dim is not None, "Could not find the attention head dim in config"
|
| 177 |
+
|
| 178 |
+
# Don't use expand to calculate d_inner
|
| 179 |
+
self.d_inner = self.find_hparam(["num_heads"]) * self.d_model
|
| 180 |
+
|
| 181 |
+
# Update the ssm / attn / mlp layers
|
| 182 |
+
# M: Mamba2, *: Attention, -: MLP
|
| 183 |
+
# MoE:
|
| 184 |
+
# M: Mamba2, *: Attention, E: Expert
|
| 185 |
+
pattern = self.hparams.get("hybrid_override_pattern") or self.hparams.get("layers_block_type")
|
| 186 |
+
if pattern is None:
|
| 187 |
+
self._ssm_layers = []
|
| 188 |
+
self._mlp_layers = []
|
| 189 |
+
elif isinstance(pattern, str):
|
| 190 |
+
self._ssm_layers = [i for i, val in enumerate(pattern) if val == "M"]
|
| 191 |
+
self._mlp_layers = [i for i, val in enumerate(pattern) if val == ("E" if self.is_moe else "-")]
|
| 192 |
+
else:
|
| 193 |
+
self._ssm_layers = [i for i, val in enumerate(pattern) if val == "mamba"]
|
| 194 |
+
self._mlp_layers = [i for i, val in enumerate(pattern) if val == "moe"]
|
| 195 |
+
|
| 196 |
+
def get_attn_layers(self):
|
| 197 |
+
pattern = self.hparams.get("hybrid_override_pattern") or self.hparams.get("layers_block_type")
|
| 198 |
+
if pattern is None:
|
| 199 |
+
return []
|
| 200 |
+
assert len(pattern) == self.block_count, f"Mismatch between pattern ({len(pattern)}) and block_count ({self.block_count})!"
|
| 201 |
+
if isinstance(pattern, str):
|
| 202 |
+
return [i for i, val in enumerate(pattern) if val == "*"]
|
| 203 |
+
|
| 204 |
+
return [i for i, val in enumerate(pattern) if val == "attention"]
|
| 205 |
+
|
| 206 |
+
def set_gguf_parameters(self):
|
| 207 |
+
super().set_gguf_parameters()
|
| 208 |
+
|
| 209 |
+
head_dim = self.head_dim
|
| 210 |
+
if head_dim is None:
|
| 211 |
+
raise ValueError("Could not find the attention head dim in config")
|
| 212 |
+
self.gguf_writer.add_key_length(head_dim)
|
| 213 |
+
self.gguf_writer.add_value_length(head_dim)
|
| 214 |
+
|
| 215 |
+
# Set feed_forward_length
|
| 216 |
+
# NOTE: This will trigger an override warning. This is preferable to
|
| 217 |
+
# duplicating all the parent logic
|
| 218 |
+
if not self.is_moe:
|
| 219 |
+
n_ff = self.find_hparam(["intermediate_size", "n_inner", "hidden_dim"])
|
| 220 |
+
self.gguf_writer.add_feed_forward_length([
|
| 221 |
+
n_ff if i in self._mlp_layers else 0 for i in range(self.block_count)
|
| 222 |
+
])
|
| 223 |
+
else:
|
| 224 |
+
moe_intermediate_size = self.hparams["moe_intermediate_size"]
|
| 225 |
+
self.gguf_writer.add_feed_forward_length([
|
| 226 |
+
moe_intermediate_size if i in self._mlp_layers else 0 for i in range(self.block_count)
|
| 227 |
+
])
|
| 228 |
+
self.gguf_writer.add_expert_used_count(self.hparams["num_experts_per_tok"])
|
| 229 |
+
self.gguf_writer.add_expert_feed_forward_length(self.hparams["moe_intermediate_size"])
|
| 230 |
+
self.gguf_writer.add_expert_shared_feed_forward_length(self.hparams["moe_shared_expert_intermediate_size"])
|
| 231 |
+
self.gguf_writer.add_expert_count(self.hparams["n_routed_experts"])
|
| 232 |
+
self.gguf_writer.add_expert_shared_count(self.hparams["n_shared_experts"])
|
| 233 |
+
self.gguf_writer.add_expert_weights_norm(self.hparams["norm_topk_prob"])
|
| 234 |
+
self.gguf_writer.add_expert_weights_scale(self.hparams["routed_scaling_factor"])
|
| 235 |
+
self.gguf_writer.add_expert_group_count(self.hparams["n_group"])
|
| 236 |
+
|
| 237 |
+
# number of experts used per token (top-k)
|
| 238 |
+
if (n_experts_used := self.hparams.get("num_experts_per_tok")) is not None:
|
| 239 |
+
self.gguf_writer.add_expert_used_count(n_experts_used)
|
| 240 |
+
|
| 241 |
+
if (latent_size := self.hparams.get("moe_latent_size")) is not None:
|
| 242 |
+
self.gguf_writer.add_moe_latent_size(latent_size)
|
| 243 |
+
|
| 244 |
+
def set_vocab(self):
|
| 245 |
+
# The NemotronH config uses pattern characters (e.g. '-') that may not
|
| 246 |
+
# be supported by the installed transformers version. AutoTokenizer
|
| 247 |
+
# internally calls AutoConfig which triggers this parsing failure.
|
| 248 |
+
# Using trust_remote_code=True to load the model's own config class.
|
| 249 |
+
tokens: list[str] = []
|
| 250 |
+
toktypes: list[int] = []
|
| 251 |
+
|
| 252 |
+
from transformers import AutoTokenizer
|
| 253 |
+
tokenizer = AutoTokenizer.from_pretrained(self.dir_model, trust_remote_code=True)
|
| 254 |
+
|
| 255 |
+
# Pad vocab size (from Mamba2Model/GraniteHybridModel)
|
| 256 |
+
self.hparams["pad_vocab_size_multiple"] = 8 # Setting this here since GraniteHybridModel.set_vocab() isn't being invoked now.
|
| 257 |
+
# From Mamba2Model.set_vocab():
|
| 258 |
+
vocab_size = self.hparams["vocab_size"]
|
| 259 |
+
pad_vocab = self.hparams.get("pad_vocab_size_multiple", 16)
|
| 260 |
+
# ref: https://stackoverflow.com/a/17511341/22827863
|
| 261 |
+
vocab_size = -(vocab_size // -pad_vocab) * pad_vocab
|
| 262 |
+
self.hparams["vocab_size"] = vocab_size
|
| 263 |
+
|
| 264 |
+
assert max(tokenizer.vocab.values()) < vocab_size # ty: ignore[unresolved-attribute]
|
| 265 |
+
|
| 266 |
+
tokpre = self.get_vocab_base_pre(tokenizer)
|
| 267 |
+
|
| 268 |
+
reverse_vocab = {id_: encoded_tok for encoded_tok, id_ in tokenizer.vocab.items()} # ty: ignore[unresolved-attribute]
|
| 269 |
+
added_vocab = tokenizer.get_added_vocab() # ty: ignore[unresolved-attribute]
|
| 270 |
+
|
| 271 |
+
added_tokens_decoder = tokenizer.added_tokens_decoder # ty: ignore[unresolved-attribute]
|
| 272 |
+
|
| 273 |
+
for i in range(vocab_size):
|
| 274 |
+
if i not in reverse_vocab:
|
| 275 |
+
tokens.append(f"[PAD{i}]")
|
| 276 |
+
toktypes.append(gguf.TokenType.UNUSED)
|
| 277 |
+
else:
|
| 278 |
+
token: str = reverse_vocab[i]
|
| 279 |
+
if token in added_vocab:
|
| 280 |
+
if not added_tokens_decoder[i].normalized:
|
| 281 |
+
previous_token = token
|
| 282 |
+
token = tokenizer.decode(tokenizer.encode(token, add_special_tokens=False)) # ty: ignore[unresolved-attribute, invalid-assignment]
|
| 283 |
+
if previous_token != token:
|
| 284 |
+
logger.info(f"{repr(previous_token)} is encoded and decoded back to {repr(token)} using AutoTokenizer")
|
| 285 |
+
|
| 286 |
+
if added_tokens_decoder[i].special or self.does_token_look_special(token):
|
| 287 |
+
toktypes.append(gguf.TokenType.CONTROL)
|
| 288 |
+
else:
|
| 289 |
+
token = token.replace(b"\xe2\x96\x81".decode("utf-8"), " ") # pre-normalize user-defined spaces
|
| 290 |
+
toktypes.append(gguf.TokenType.USER_DEFINED)
|
| 291 |
+
else:
|
| 292 |
+
toktypes.append(gguf.TokenType.NORMAL)
|
| 293 |
+
tokens.append(token)
|
| 294 |
+
|
| 295 |
+
# From TextModel.set_vocab_gpt2():
|
| 296 |
+
self.gguf_writer.add_tokenizer_model("gpt2")
|
| 297 |
+
self.gguf_writer.add_tokenizer_pre(tokpre)
|
| 298 |
+
self.gguf_writer.add_token_list(tokens)
|
| 299 |
+
self.gguf_writer.add_token_types(toktypes)
|
| 300 |
+
|
| 301 |
+
special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=True)
|
| 302 |
+
special_vocab.add_to_gguf(self.gguf_writer)
|
| 303 |
+
|
| 304 |
+
# The tokenizer _does_ add a BOS token (via post_processor type
|
| 305 |
+
# TemplateProcessing) but does not set add_bos_token to true in the
|
| 306 |
+
# config, so we need to explicitly override it here.
|
| 307 |
+
if not self.is_moe:
|
| 308 |
+
self.gguf_writer.add_add_bos_token(True)
|
| 309 |
+
|
| 310 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 311 |
+
if self.is_moe and bid is not None:
|
| 312 |
+
# Skip Multi-Token Prediction (MTP) tensors. These are used for
|
| 313 |
+
# for speculative decoding but we don't include them in this model
|
| 314 |
+
# conversion. See https://github.com/ggml-org/llama.cpp/pull/18886
|
| 315 |
+
if name.startswith("mtp."):
|
| 316 |
+
logger.info(f"gguf: Skipping MTP (Speculative) layer: {name}")
|
| 317 |
+
return
|
| 318 |
+
|
| 319 |
+
if name.endswith("mixer.gate.e_score_correction.bias"):
|
| 320 |
+
yield from ModelBase.modify_tensors(self, data_torch, name, bid)
|
| 321 |
+
return
|
| 322 |
+
|
| 323 |
+
if name.endswith("mixer.dt_bias"):
|
| 324 |
+
new_name = name.replace("dt_bias", "dt.bias")
|
| 325 |
+
yield from ModelBase.modify_tensors(self, data_torch, new_name, bid)
|
| 326 |
+
return
|
| 327 |
+
|
| 328 |
+
if name.endswith("mixer.conv1d.weight"):
|
| 329 |
+
squeezed_data = data_torch.squeeze()
|
| 330 |
+
yield from ModelBase.modify_tensors(self, squeezed_data, name, bid)
|
| 331 |
+
return
|
| 332 |
+
|
| 333 |
+
if name.endswith("mixer.A_log"):
|
| 334 |
+
transformed_data = -torch.exp(data_torch)
|
| 335 |
+
reshaped_data = transformed_data.squeeze().reshape(-1, 1)
|
| 336 |
+
yield from ModelBase.modify_tensors(self, reshaped_data, name, bid)
|
| 337 |
+
return
|
| 338 |
+
|
| 339 |
+
if name.endswith("mixer.D"):
|
| 340 |
+
reshaped_data = data_torch.squeeze().reshape(-1, 1)
|
| 341 |
+
yield from ModelBase.modify_tensors(self, reshaped_data, name, bid)
|
| 342 |
+
return
|
| 343 |
+
|
| 344 |
+
if name.endswith("mixer.norm.weight"):
|
| 345 |
+
reshaped_data = data_torch.reshape(self.n_group, -1)
|
| 346 |
+
yield from ModelBase.modify_tensors(self, reshaped_data, name, bid)
|
| 347 |
+
return
|
| 348 |
+
|
| 349 |
+
if name.find("mixer.experts") != -1:
|
| 350 |
+
n_experts = self.hparams["n_routed_experts"]
|
| 351 |
+
assert bid is not None
|
| 352 |
+
|
| 353 |
+
if self._experts is None:
|
| 354 |
+
self._experts = [{} for _ in range(self.block_count)]
|
| 355 |
+
|
| 356 |
+
self._experts[bid][name] = data_torch
|
| 357 |
+
|
| 358 |
+
if len(self._experts[bid]) >= n_experts * 2:
|
| 359 |
+
# merge the experts into a single tensor
|
| 360 |
+
for w_name in ["down_proj", "up_proj"]:
|
| 361 |
+
datas: list[Tensor] = []
|
| 362 |
+
|
| 363 |
+
for xid in range(n_experts):
|
| 364 |
+
ename = f"backbone.layers.{bid}.mixer.experts.{xid}.{w_name}.weight"
|
| 365 |
+
datas.append(self._experts[bid][ename])
|
| 366 |
+
del self._experts[bid][ename]
|
| 367 |
+
|
| 368 |
+
data_torch = torch.stack(datas, dim=0)
|
| 369 |
+
merged_name = f"model.layers.{bid}.mlp.experts.{w_name}.weight"
|
| 370 |
+
|
| 371 |
+
yield from ModelBase.modify_tensors(self, data_torch, merged_name, bid)
|
| 372 |
+
return
|
| 373 |
+
else:
|
| 374 |
+
return
|
| 375 |
+
|
| 376 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 377 |
+
|
| 378 |
+
def prepare_tensors(self):
|
| 379 |
+
super().prepare_tensors()
|
| 380 |
+
|
| 381 |
+
if self._experts is not None:
|
| 382 |
+
# flatten `list[dict[str, Tensor]]` into `list[str]`
|
| 383 |
+
experts = [k for d in self._experts for k in d.keys()]
|
| 384 |
+
if len(experts) > 0:
|
| 385 |
+
raise ValueError(f"Unprocessed experts: {experts}")
|
llama.cpp/conversion/olmo.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Iterable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING:
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
from .base import ModelBase, TextModel, gguf
|
| 11 |
+
|
| 12 |
+
from .llama import LlamaModel
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@ModelBase.register("OlmoForCausalLM")
|
| 16 |
+
@ModelBase.register("OLMoForCausalLM")
|
| 17 |
+
class OlmoModel(TextModel):
|
| 18 |
+
model_arch = gguf.MODEL_ARCH.OLMO
|
| 19 |
+
|
| 20 |
+
def set_gguf_parameters(self):
|
| 21 |
+
super().set_gguf_parameters()
|
| 22 |
+
self.gguf_writer.add_layer_norm_eps(1e-5)
|
| 23 |
+
clip_qkv = self.hparams.get("clip_qkv")
|
| 24 |
+
if clip_qkv is not None:
|
| 25 |
+
self.gguf_writer.add_clamp_kqv(clip_qkv)
|
| 26 |
+
|
| 27 |
+
# Same as super class, but permuting q_proj, k_proj
|
| 28 |
+
# Copied from: LlamaModel
|
| 29 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 30 |
+
n_head = self.hparams["num_attention_heads"]
|
| 31 |
+
n_kv_head = self.hparams.get("num_key_value_heads")
|
| 32 |
+
|
| 33 |
+
if name.endswith("q_proj.weight"):
|
| 34 |
+
data_torch = LlamaModel.permute(data_torch, n_head, n_head)
|
| 35 |
+
if name.endswith("k_proj.weight"):
|
| 36 |
+
data_torch = LlamaModel.permute(data_torch, n_head, n_kv_head)
|
| 37 |
+
|
| 38 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@ModelBase.register("SeedOssForCausalLM")
|
| 42 |
+
class SeedOssModel(TextModel):
|
| 43 |
+
model_arch = gguf.MODEL_ARCH.SEED_OSS
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@ModelBase.register("Olmo2ForCausalLM")
|
| 47 |
+
@ModelBase.register("Olmo3ForCausalLM")
|
| 48 |
+
class Olmo2Model(TextModel):
|
| 49 |
+
model_arch = gguf.MODEL_ARCH.OLMO2
|
| 50 |
+
|
| 51 |
+
def set_gguf_parameters(self):
|
| 52 |
+
super().set_gguf_parameters()
|
| 53 |
+
|
| 54 |
+
if "sliding_window" in self.hparams:
|
| 55 |
+
self.gguf_writer.add_sliding_window(self.hparams["sliding_window"])
|
| 56 |
+
|
| 57 |
+
sliding_window_pattern = []
|
| 58 |
+
if "layer_types" in self.hparams:
|
| 59 |
+
sliding_window_pattern = [t == "sliding_attention" for t in self.hparams["layer_types"]]
|
| 60 |
+
else:
|
| 61 |
+
# Olmo2 does not use sliding window attention.
|
| 62 |
+
# Olmo3 defaults to using sliding window for all layers except every 4th.
|
| 63 |
+
for i in range(self.hparams["num_hidden_layers"]):
|
| 64 |
+
sliding_window_pattern.append((i + 1) % 4 != 0)
|
| 65 |
+
|
| 66 |
+
self.gguf_writer.add_sliding_window_pattern(sliding_window_pattern)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@ModelBase.register("OlmoeForCausalLM")
|
| 70 |
+
class OlmoeModel(TextModel):
|
| 71 |
+
model_arch = gguf.MODEL_ARCH.OLMOE
|
| 72 |
+
|
| 73 |
+
def set_gguf_parameters(self):
|
| 74 |
+
super().set_gguf_parameters()
|
| 75 |
+
self.gguf_writer.add_layer_norm_rms_eps(1e-5)
|
| 76 |
+
|
| 77 |
+
_experts: list[dict[str, Tensor]] | None = None
|
| 78 |
+
|
| 79 |
+
# Copied from: Qwen2MoeModel
|
| 80 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 81 |
+
# process the experts separately
|
| 82 |
+
if name.find("experts") != -1:
|
| 83 |
+
n_experts = self.find_hparam(["num_local_experts", "num_experts"])
|
| 84 |
+
assert bid is not None
|
| 85 |
+
|
| 86 |
+
if self._experts is None:
|
| 87 |
+
self._experts = [{} for _ in range(self.block_count)]
|
| 88 |
+
|
| 89 |
+
self._experts[bid][name] = data_torch
|
| 90 |
+
|
| 91 |
+
if len(self._experts[bid]) >= n_experts * 3:
|
| 92 |
+
# merge the experts into a single 3d tensor
|
| 93 |
+
for w_name in ["down_proj", "gate_proj", "up_proj"]:
|
| 94 |
+
datas: list[Tensor] = []
|
| 95 |
+
|
| 96 |
+
for xid in range(n_experts):
|
| 97 |
+
ename = f"model.layers.{bid}.mlp.experts.{xid}.{w_name}.weight"
|
| 98 |
+
datas.append(self._experts[bid][ename])
|
| 99 |
+
del self._experts[bid][ename]
|
| 100 |
+
|
| 101 |
+
data_torch = torch.stack(datas, dim=0)
|
| 102 |
+
|
| 103 |
+
merged_name = f"model.layers.{bid}.mlp.experts.{w_name}.weight"
|
| 104 |
+
|
| 105 |
+
yield from super().modify_tensors(data_torch, merged_name, bid)
|
| 106 |
+
return
|
| 107 |
+
else:
|
| 108 |
+
return
|
| 109 |
+
|
| 110 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 111 |
+
|
| 112 |
+
# Copied from: Qwen2MoeModel
|
| 113 |
+
def prepare_tensors(self):
|
| 114 |
+
super().prepare_tensors()
|
| 115 |
+
|
| 116 |
+
if self._experts is not None:
|
| 117 |
+
# flatten `list[dict[str, Tensor]]` into `list[str]`
|
| 118 |
+
experts = [k for d in self._experts for k in d.keys()]
|
| 119 |
+
if len(experts) > 0:
|
| 120 |
+
raise ValueError(f"Unprocessed experts: {experts}")
|
llama.cpp/conversion/openelm.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any, Iterable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
if TYPE_CHECKING:
|
| 6 |
+
from torch import Tensor
|
| 7 |
+
|
| 8 |
+
from .base import ModelBase, TextModel, gguf
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@ModelBase.register("OpenELMForCausalLM")
|
| 12 |
+
class OpenELMModel(TextModel):
|
| 13 |
+
model_arch = gguf.MODEL_ARCH.OPENELM
|
| 14 |
+
|
| 15 |
+
@staticmethod
|
| 16 |
+
def _make_divisible(v: float | int, divisor: int) -> int:
|
| 17 |
+
# ref: https://huggingface.co/apple/OpenELM-270M-Instruct/blob/eb111ff2e6724348e5b905984063d4064d4bc579/configuration_openelm.py#L34-L38
|
| 18 |
+
new_v = max(divisor, int(v + divisor / 2) // divisor * divisor)
|
| 19 |
+
# Make sure that round down does not go down by more than 10%.
|
| 20 |
+
if new_v < 0.9 * v:
|
| 21 |
+
new_v += divisor
|
| 22 |
+
return new_v
|
| 23 |
+
|
| 24 |
+
def __init__(self, *args, **kwargs):
|
| 25 |
+
super().__init__(*args, **kwargs)
|
| 26 |
+
|
| 27 |
+
ffn_multipliers: list[float] = self.hparams["ffn_multipliers"]
|
| 28 |
+
ffn_dim_divisor: int = self.hparams["ffn_dim_divisor"]
|
| 29 |
+
self._n_embd: int = self.hparams["model_dim"]
|
| 30 |
+
self._num_kv_heads: list[int] = self.hparams["num_kv_heads"]
|
| 31 |
+
self._num_query_heads: list[int] = self.hparams["num_query_heads"]
|
| 32 |
+
self._ffn_dims: list[int] = [
|
| 33 |
+
OpenELMModel._make_divisible(multiplier * self._n_embd, ffn_dim_divisor)
|
| 34 |
+
for multiplier in ffn_multipliers
|
| 35 |
+
]
|
| 36 |
+
assert isinstance(self._num_kv_heads, list) and isinstance(self._num_kv_heads[0], int)
|
| 37 |
+
assert isinstance(self._num_query_heads, list) and isinstance(self._num_query_heads[0], int)
|
| 38 |
+
|
| 39 |
+
# Uses the tokenizer from meta-llama/Llama-2-7b-hf
|
| 40 |
+
def set_vocab(self):
|
| 41 |
+
try:
|
| 42 |
+
self._set_vocab_sentencepiece()
|
| 43 |
+
except FileNotFoundError:
|
| 44 |
+
self._set_vocab_builtin("llama-spm", self.hparams["vocab_size"])
|
| 45 |
+
|
| 46 |
+
def set_gguf_parameters(self):
|
| 47 |
+
n_embd = self._n_embd
|
| 48 |
+
head_dim = self.hparams["head_dim"]
|
| 49 |
+
rot_pct = 1.0
|
| 50 |
+
assert self.block_count == len(self._num_kv_heads)
|
| 51 |
+
assert self.block_count == len(self._num_query_heads)
|
| 52 |
+
assert self.block_count == len(self._ffn_dims)
|
| 53 |
+
|
| 54 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 55 |
+
self.gguf_writer.add_context_length(self.hparams["max_context_length"])
|
| 56 |
+
self.gguf_writer.add_embedding_length(n_embd)
|
| 57 |
+
self.gguf_writer.add_feed_forward_length(self._ffn_dims)
|
| 58 |
+
self.gguf_writer.add_head_count(self._num_query_heads)
|
| 59 |
+
self.gguf_writer.add_head_count_kv(self._num_kv_heads)
|
| 60 |
+
self.gguf_writer.add_rope_freq_base(self.hparams["rope_freq_constant"])
|
| 61 |
+
# https://huggingface.co/apple/OpenELM-270M-Instruct/blob/c401df2/modeling_openelm.py#L30
|
| 62 |
+
self.gguf_writer.add_layer_norm_rms_eps(1e-6)
|
| 63 |
+
self.gguf_writer.add_rope_dimension_count(int(rot_pct * head_dim))
|
| 64 |
+
self.gguf_writer.add_key_length(head_dim)
|
| 65 |
+
self.gguf_writer.add_value_length(head_dim)
|
| 66 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 67 |
+
|
| 68 |
+
def find_hparam(self, keys: Iterable[str], optional: bool = False) -> Any:
|
| 69 |
+
if "n_layers" in keys:
|
| 70 |
+
return self.hparams["num_transformer_layers"]
|
| 71 |
+
|
| 72 |
+
return super().find_hparam(keys, optional)
|
| 73 |
+
|
| 74 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 75 |
+
|
| 76 |
+
# split ff
|
| 77 |
+
if bid is not None and name == f"transformer.layers.{bid}.ffn.proj_1.weight":
|
| 78 |
+
ff_dim = self._ffn_dims[bid]
|
| 79 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.FFN_GATE, bid), data_torch[:ff_dim])
|
| 80 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.FFN_UP, bid), data_torch[ff_dim:])
|
| 81 |
+
return
|
| 82 |
+
|
| 83 |
+
yield (self.map_tensor_name(name), data_torch)
|
llama.cpp/conversion/orion.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from .base import ModelBase, TextModel, gguf
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@ModelBase.register("OrionForCausalLM")
|
| 7 |
+
class OrionModel(TextModel):
|
| 8 |
+
model_arch = gguf.MODEL_ARCH.ORION
|
| 9 |
+
|
| 10 |
+
def set_vocab(self):
|
| 11 |
+
self._set_vocab_sentencepiece()
|
| 12 |
+
|
| 13 |
+
def set_gguf_parameters(self):
|
| 14 |
+
head_count = self.hparams["num_attention_heads"]
|
| 15 |
+
head_count_kv = self.hparams.get("num_key_value_heads", head_count)
|
| 16 |
+
|
| 17 |
+
ctx_length = 0
|
| 18 |
+
if "max_sequence_length" in self.hparams:
|
| 19 |
+
ctx_length = self.hparams["max_sequence_length"]
|
| 20 |
+
elif "max_position_embeddings" in self.hparams:
|
| 21 |
+
ctx_length = self.hparams["max_position_embeddings"]
|
| 22 |
+
elif "model_max_length" in self.hparams:
|
| 23 |
+
ctx_length = self.hparams["model_max_length"]
|
| 24 |
+
else:
|
| 25 |
+
raise ValueError("gguf: can not find ctx length parameter.")
|
| 26 |
+
|
| 27 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 28 |
+
self.gguf_writer.add_tensor_data_layout("Meta AI original pth")
|
| 29 |
+
self.gguf_writer.add_context_length(ctx_length)
|
| 30 |
+
self.gguf_writer.add_embedding_length(self.hparams["hidden_size"])
|
| 31 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 32 |
+
self.gguf_writer.add_feed_forward_length(self.hparams["intermediate_size"])
|
| 33 |
+
self.gguf_writer.add_head_count(head_count)
|
| 34 |
+
self.gguf_writer.add_head_count_kv(head_count_kv)
|
| 35 |
+
# note: config provides rms norm but it is actually layer norm
|
| 36 |
+
# ref: https://huggingface.co/OrionStarAI/Orion-14B-Chat/blob/276a17221ce42beb45f66fac657a41540e71f4f5/modeling_orion.py#L570-L571
|
| 37 |
+
self.gguf_writer.add_layer_norm_eps(self.hparams["rms_norm_eps"])
|
llama.cpp/conversion/pangu.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
from typing import Iterable, TYPE_CHECKING
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING:
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
from .base import ModelBase, TextModel, gguf, logger
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@ModelBase.register("PanguEmbeddedForCausalLM")
|
| 14 |
+
class PanguEmbeddedModel(TextModel):
|
| 15 |
+
model_arch = gguf.MODEL_ARCH.PANGU_EMBED
|
| 16 |
+
|
| 17 |
+
def set_vocab(self):
|
| 18 |
+
self._set_vocab_sentencepiece()
|
| 19 |
+
|
| 20 |
+
tokenizer_config_file = self.dir_model / 'tokenizer_config.json'
|
| 21 |
+
if tokenizer_config_file.is_file():
|
| 22 |
+
with open(tokenizer_config_file, "r", encoding="utf-8") as f:
|
| 23 |
+
tokenizer_config_json = json.load(f)
|
| 24 |
+
if "add_prefix_space" in tokenizer_config_json:
|
| 25 |
+
self.gguf_writer.add_add_space_prefix(tokenizer_config_json["add_prefix_space"])
|
| 26 |
+
|
| 27 |
+
def set_gguf_parameters(self):
|
| 28 |
+
super().set_gguf_parameters()
|
| 29 |
+
hparams = self.hparams
|
| 30 |
+
self.gguf_writer.add_vocab_size(hparams["vocab_size"])
|
| 31 |
+
|
| 32 |
+
# PanguEmbedded's hparam loaded from config.json without head_dim
|
| 33 |
+
if (rope_dim := hparams.get("head_dim")) is None:
|
| 34 |
+
rope_dim = hparams["hidden_size"] // hparams["num_attention_heads"]
|
| 35 |
+
self.gguf_writer.add_rope_dimension_count(rope_dim)
|
| 36 |
+
|
| 37 |
+
if hparams.get("head_dim") is None:
|
| 38 |
+
self.gguf_writer.add_key_length(rope_dim)
|
| 39 |
+
self.gguf_writer.add_value_length(rope_dim)
|
| 40 |
+
|
| 41 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 42 |
+
if name == "lm_head.weight":
|
| 43 |
+
if self.hparams.get("tie_word_embeddings", False):
|
| 44 |
+
logger.info("Skipping tied output layer 'lm_head.weight'")
|
| 45 |
+
return
|
| 46 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
llama.cpp/conversion/phi.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import math
|
| 5 |
+
|
| 6 |
+
from typing import Callable, Iterable, TYPE_CHECKING
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
if TYPE_CHECKING:
|
| 11 |
+
from torch import Tensor
|
| 12 |
+
|
| 13 |
+
from .base import MmprojModel, ModelBase, SentencePieceTokenTypes, TextModel, gguf, logger
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@ModelBase.register("PhiForCausalLM")
|
| 17 |
+
class Phi2Model(TextModel):
|
| 18 |
+
model_arch = gguf.MODEL_ARCH.PHI2
|
| 19 |
+
|
| 20 |
+
def set_gguf_parameters(self):
|
| 21 |
+
rot_pct = self.rope_parameters["partial_rotary_factor"]
|
| 22 |
+
n_embd = self.find_hparam(["hidden_size", "n_embd"])
|
| 23 |
+
n_head = self.find_hparam(["num_attention_heads", "n_head"])
|
| 24 |
+
|
| 25 |
+
self.gguf_writer.add_context_length(self.find_hparam(["n_positions", "max_position_embeddings"]))
|
| 26 |
+
|
| 27 |
+
self.gguf_writer.add_embedding_length(n_embd)
|
| 28 |
+
self.gguf_writer.add_feed_forward_length(4 * n_embd)
|
| 29 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 30 |
+
self.gguf_writer.add_head_count(n_head)
|
| 31 |
+
self.gguf_writer.add_head_count_kv(n_head)
|
| 32 |
+
self.gguf_writer.add_layer_norm_eps(self.find_hparam(["layer_norm_epsilon", "layer_norm_eps"]))
|
| 33 |
+
self.gguf_writer.add_rope_dimension_count(int(rot_pct * n_embd) // n_head)
|
| 34 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 35 |
+
self.gguf_writer.add_add_bos_token(False)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@ModelBase.register("Phi3ForCausalLM", "Phi4ForCausalLMV")
|
| 39 |
+
class Phi3MiniModel(TextModel):
|
| 40 |
+
model_arch = gguf.MODEL_ARCH.PHI3
|
| 41 |
+
|
| 42 |
+
def set_vocab(self):
|
| 43 |
+
# Phi-4 model uses GPT2Tokenizer
|
| 44 |
+
tokenizer_config_file = self.dir_model / 'tokenizer_config.json'
|
| 45 |
+
if tokenizer_config_file.is_file():
|
| 46 |
+
with open(tokenizer_config_file, "r", encoding="utf-8") as f:
|
| 47 |
+
tokenizer_config_json = json.load(f)
|
| 48 |
+
tokenizer_class = tokenizer_config_json['tokenizer_class']
|
| 49 |
+
if tokenizer_class == 'GPT2Tokenizer':
|
| 50 |
+
return self._set_vocab_gpt2()
|
| 51 |
+
|
| 52 |
+
from sentencepiece import SentencePieceProcessor
|
| 53 |
+
|
| 54 |
+
tokenizer_path = self.dir_model / 'tokenizer.model'
|
| 55 |
+
|
| 56 |
+
if not tokenizer_path.is_file():
|
| 57 |
+
raise ValueError(f'Error: Missing {tokenizer_path}')
|
| 58 |
+
|
| 59 |
+
tokenizer = SentencePieceProcessor()
|
| 60 |
+
tokenizer.LoadFromFile(str(tokenizer_path))
|
| 61 |
+
|
| 62 |
+
vocab_size = self.hparams.get('vocab_size', tokenizer.vocab_size())
|
| 63 |
+
|
| 64 |
+
tokens: list[bytes] = [f"[PAD{i}]".encode("utf-8") for i in range(vocab_size)]
|
| 65 |
+
scores: list[float] = [-10000.0] * vocab_size
|
| 66 |
+
toktypes: list[int] = [SentencePieceTokenTypes.UNUSED] * vocab_size
|
| 67 |
+
|
| 68 |
+
for token_id in range(tokenizer.vocab_size()):
|
| 69 |
+
|
| 70 |
+
piece = tokenizer.IdToPiece(token_id)
|
| 71 |
+
text = piece.encode("utf-8")
|
| 72 |
+
score = tokenizer.GetScore(token_id)
|
| 73 |
+
|
| 74 |
+
toktype = SentencePieceTokenTypes.NORMAL
|
| 75 |
+
if tokenizer.IsUnknown(token_id):
|
| 76 |
+
toktype = SentencePieceTokenTypes.UNKNOWN
|
| 77 |
+
elif tokenizer.IsControl(token_id):
|
| 78 |
+
toktype = SentencePieceTokenTypes.CONTROL
|
| 79 |
+
elif tokenizer.IsUnused(token_id):
|
| 80 |
+
toktype = SentencePieceTokenTypes.UNUSED
|
| 81 |
+
elif tokenizer.IsByte(token_id):
|
| 82 |
+
toktype = SentencePieceTokenTypes.BYTE
|
| 83 |
+
|
| 84 |
+
tokens[token_id] = text
|
| 85 |
+
scores[token_id] = score
|
| 86 |
+
toktypes[token_id] = toktype
|
| 87 |
+
|
| 88 |
+
added_tokens_file = self.dir_model / 'added_tokens.json'
|
| 89 |
+
if added_tokens_file.is_file():
|
| 90 |
+
with open(added_tokens_file, "r", encoding="utf-8") as f:
|
| 91 |
+
added_tokens_json = json.load(f)
|
| 92 |
+
|
| 93 |
+
for key in added_tokens_json:
|
| 94 |
+
token_id = added_tokens_json[key]
|
| 95 |
+
if token_id >= vocab_size:
|
| 96 |
+
logger.debug(f'ignore token {token_id}: id is out of range, max={vocab_size - 1}')
|
| 97 |
+
continue
|
| 98 |
+
|
| 99 |
+
tokens[token_id] = key.encode("utf-8")
|
| 100 |
+
scores[token_id] = -1000.0
|
| 101 |
+
toktypes[token_id] = SentencePieceTokenTypes.USER_DEFINED
|
| 102 |
+
|
| 103 |
+
tokenizer_config_file = self.dir_model / 'tokenizer_config.json'
|
| 104 |
+
if tokenizer_config_file.is_file():
|
| 105 |
+
with open(tokenizer_config_file, "r", encoding="utf-8") as f:
|
| 106 |
+
tokenizer_config_json = json.load(f)
|
| 107 |
+
added_tokens_decoder = tokenizer_config_json.get("added_tokens_decoder", {})
|
| 108 |
+
for token_id, foken_data in added_tokens_decoder.items():
|
| 109 |
+
token_id = int(token_id)
|
| 110 |
+
token = foken_data["content"].encode("utf-8")
|
| 111 |
+
if toktypes[token_id] != SentencePieceTokenTypes.UNUSED:
|
| 112 |
+
if tokens[token_id] != token:
|
| 113 |
+
logger.warning(f'replacing token {token_id}: {tokens[token_id].decode("utf-8")!r} -> {token.decode("utf-8")!r}')
|
| 114 |
+
tokens[token_id] = token
|
| 115 |
+
scores[token_id] = -1000.0
|
| 116 |
+
toktypes[token_id] = SentencePieceTokenTypes.USER_DEFINED
|
| 117 |
+
if foken_data.get("special"):
|
| 118 |
+
toktypes[token_id] = SentencePieceTokenTypes.CONTROL
|
| 119 |
+
|
| 120 |
+
tokenizer_file = self.dir_model / 'tokenizer.json'
|
| 121 |
+
if tokenizer_file.is_file():
|
| 122 |
+
with open(tokenizer_file, "r", encoding="utf-8") as f:
|
| 123 |
+
tokenizer_json = json.load(f)
|
| 124 |
+
added_tokens = tokenizer_json.get("added_tokens", [])
|
| 125 |
+
for foken_data in added_tokens:
|
| 126 |
+
token_id = int(foken_data["id"])
|
| 127 |
+
token = foken_data["content"].encode("utf-8")
|
| 128 |
+
if toktypes[token_id] != SentencePieceTokenTypes.UNUSED:
|
| 129 |
+
if tokens[token_id] != token:
|
| 130 |
+
logger.warning(f'replacing token {token_id}: {tokens[token_id].decode("utf-8")!r} -> {token.decode("utf-8")!r}')
|
| 131 |
+
tokens[token_id] = token
|
| 132 |
+
scores[token_id] = -1000.0
|
| 133 |
+
toktypes[token_id] = SentencePieceTokenTypes.USER_DEFINED
|
| 134 |
+
if foken_data.get("special"):
|
| 135 |
+
toktypes[token_id] = SentencePieceTokenTypes.CONTROL
|
| 136 |
+
|
| 137 |
+
self.gguf_writer.add_tokenizer_model("llama")
|
| 138 |
+
self.gguf_writer.add_tokenizer_pre("default")
|
| 139 |
+
self.gguf_writer.add_token_list(tokens)
|
| 140 |
+
self.gguf_writer.add_token_scores(scores)
|
| 141 |
+
self.gguf_writer.add_token_types(toktypes)
|
| 142 |
+
|
| 143 |
+
special_vocab = gguf.SpecialVocab(self.dir_model, n_vocab=len(tokens))
|
| 144 |
+
special_vocab.add_to_gguf(self.gguf_writer)
|
| 145 |
+
|
| 146 |
+
def set_gguf_parameters(self):
|
| 147 |
+
n_embd = self.find_hparam(["hidden_size", "n_embd"])
|
| 148 |
+
n_head = self.find_hparam(["num_attention_heads", "n_head"])
|
| 149 |
+
n_head_kv = self.find_hparam(["num_key_value_heads", "n_head_kv"])
|
| 150 |
+
rms_eps = self.find_hparam(["rms_norm_eps"])
|
| 151 |
+
max_pos_embds = self.find_hparam(["n_positions", "max_position_embeddings"])
|
| 152 |
+
orig_max_pos_embds = self.rope_parameters["original_max_position_embeddings"]
|
| 153 |
+
rot_pct = self.rope_parameters.get("partial_rotary_factor", 1.0)
|
| 154 |
+
rope_dims = int(rot_pct * n_embd) // n_head
|
| 155 |
+
|
| 156 |
+
self.gguf_writer.add_context_length(max_pos_embds)
|
| 157 |
+
self.gguf_writer.add_rope_scaling_orig_ctx_len(orig_max_pos_embds)
|
| 158 |
+
self.gguf_writer.add_embedding_length(n_embd)
|
| 159 |
+
self.gguf_writer.add_feed_forward_length(self.find_hparam(["intermediate_size"]))
|
| 160 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 161 |
+
self.gguf_writer.add_head_count(n_head)
|
| 162 |
+
self.gguf_writer.add_head_count_kv(n_head_kv)
|
| 163 |
+
self.gguf_writer.add_layer_norm_rms_eps(rms_eps)
|
| 164 |
+
self.gguf_writer.add_rope_dimension_count(rope_dims)
|
| 165 |
+
self.gguf_writer.add_rope_freq_base(self.rope_parameters.get("full_attention", self.rope_parameters)["rope_theta"])
|
| 166 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 167 |
+
sliding_window = self.hparams.get("sliding_window")
|
| 168 |
+
# use zero value of sliding_window to distinguish Phi-4 from other PHI3 models
|
| 169 |
+
if sliding_window is None:
|
| 170 |
+
sliding_window = 0
|
| 171 |
+
self.gguf_writer.add_sliding_window(sliding_window)
|
| 172 |
+
|
| 173 |
+
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
|
| 174 |
+
n_embd = self.find_hparam(["hidden_size", "n_embd"])
|
| 175 |
+
n_head = self.find_hparam(["num_attention_heads", "n_head"])
|
| 176 |
+
max_pos_embds = self.find_hparam(["n_positions", "max_position_embeddings"])
|
| 177 |
+
orig_max_pos_embds = self.rope_parameters["original_max_position_embeddings"]
|
| 178 |
+
rot_pct = self.rope_parameters.get("partial_rotary_factor", 1.0)
|
| 179 |
+
rope_dims = int(rot_pct * n_embd) // n_head
|
| 180 |
+
|
| 181 |
+
# write rope scaling for long context (128k) model
|
| 182 |
+
long_factors = self.rope_parameters.get('long_factor')
|
| 183 |
+
short_factors = self.rope_parameters.get('short_factor')
|
| 184 |
+
if not long_factors:
|
| 185 |
+
return
|
| 186 |
+
|
| 187 |
+
scale = max_pos_embds / orig_max_pos_embds
|
| 188 |
+
|
| 189 |
+
rope_scaling_type = self.rope_parameters.get('rope_type', '').lower()
|
| 190 |
+
if len(rope_scaling_type) == 0:
|
| 191 |
+
raise KeyError('Missing the required key rope_scaling.type')
|
| 192 |
+
|
| 193 |
+
if rope_scaling_type == 'su' or rope_scaling_type == 'longrope':
|
| 194 |
+
attn_factor = math.sqrt(1 + math.log(scale) / math.log(orig_max_pos_embds)) if scale > 1.0 else 1.0
|
| 195 |
+
elif rope_scaling_type == 'yarn':
|
| 196 |
+
attn_factor = 0.1 * math.log(scale) + 1.0 if scale > 1.0 else 1.0
|
| 197 |
+
else:
|
| 198 |
+
raise NotImplementedError(f'The rope scaling type {rope_scaling_type} is not supported yet')
|
| 199 |
+
|
| 200 |
+
self.gguf_writer.add_rope_scaling_attn_factors(attn_factor)
|
| 201 |
+
|
| 202 |
+
if long_factors is None or short_factors is None:
|
| 203 |
+
raise KeyError('Missing the required key rope_scaling.long_factor or rope_scaling_short_factor')
|
| 204 |
+
|
| 205 |
+
if len(long_factors) != len(short_factors) or len(long_factors) != rope_dims / 2:
|
| 206 |
+
raise ValueError(f'The length of rope long and short factors must be {rope_dims / 2}. long_factors = {len(long_factors)}, short_factors = {len(short_factors)}.')
|
| 207 |
+
|
| 208 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FACTORS_LONG), torch.tensor(long_factors, dtype=torch.float32))
|
| 209 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FACTORS_SHORT), torch.tensor(short_factors, dtype=torch.float32))
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
@ModelBase.register("Phi4ForCausalLMV")
|
| 213 |
+
class Phi4VisionMmprojModel(MmprojModel):
|
| 214 |
+
def __init__(self, *args, **kwargs):
|
| 215 |
+
super().__init__(*args, **kwargs)
|
| 216 |
+
assert self.hparams_vision is not None
|
| 217 |
+
|
| 218 |
+
self.vision_total_layers = int(self.find_vparam(self.n_block_keys))
|
| 219 |
+
if self.vision_total_layers < 2:
|
| 220 |
+
raise ValueError(
|
| 221 |
+
f"Phi-4 vision mmproj conversion requires at least 2 vision layers, got {self.vision_total_layers}"
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
# Phi-4 uses SigLIP2 hidden_states[-2], so export one fewer encoder block and
|
| 225 |
+
# drop post-layernorm/head weights. This makes the GGUF runtime output match
|
| 226 |
+
# the feature map consumed by the patched siglip.cpp Phi-4 projector path.
|
| 227 |
+
self.vision_export_layers = self.vision_total_layers - 1
|
| 228 |
+
self.vision_last_layer_idx = self.vision_total_layers - 1
|
| 229 |
+
|
| 230 |
+
for key in self.n_block_keys:
|
| 231 |
+
if key in self.hparams_vision:
|
| 232 |
+
self.hparams_vision[key] = self.vision_export_layers
|
| 233 |
+
break
|
| 234 |
+
|
| 235 |
+
self.block_count = self.vision_export_layers
|
| 236 |
+
self.tensor_map = gguf.get_tensor_name_map(gguf.MODEL_ARCH.MMPROJ, self.block_count)
|
| 237 |
+
|
| 238 |
+
patch_size = self.preprocessor_config.get("patch_size")
|
| 239 |
+
if patch_size is None:
|
| 240 |
+
raise KeyError("Phi-4 vision mmproj conversion requires patch_size in preprocessor_config.json")
|
| 241 |
+
|
| 242 |
+
self.hparams_vision["patch_size"] = patch_size
|
| 243 |
+
|
| 244 |
+
pos_emb_name = next(
|
| 245 |
+
(
|
| 246 |
+
name for name in self.model_tensors
|
| 247 |
+
if name.endswith("vision_model.embeddings.position_embedding.weight")
|
| 248 |
+
),
|
| 249 |
+
None,
|
| 250 |
+
)
|
| 251 |
+
if pos_emb_name is None:
|
| 252 |
+
raise KeyError("Phi-4 vision mmproj conversion could not find position_embedding.weight")
|
| 253 |
+
|
| 254 |
+
pos_emb_shape = self.model_tensors[pos_emb_name]().shape
|
| 255 |
+
base_grid_tokens = int(pos_emb_shape[0])
|
| 256 |
+
grid_side = math.isqrt(base_grid_tokens)
|
| 257 |
+
if grid_side * grid_side != base_grid_tokens:
|
| 258 |
+
raise ValueError(f"Unexpected Phi-4 position embedding shape: {tuple(pos_emb_shape)}")
|
| 259 |
+
|
| 260 |
+
self.hparams_vision["image_size"] = grid_side * patch_size
|
| 261 |
+
|
| 262 |
+
min_num_patches = self.preprocessor_config.get("min_num_patches", self.global_config.get("min_num_patches"))
|
| 263 |
+
max_num_patches = self.preprocessor_config.get("max_num_patches", self.global_config.get("max_num_patches"))
|
| 264 |
+
if min_num_patches is None or max_num_patches is None:
|
| 265 |
+
raise KeyError("Phi-4 vision mmproj conversion requires min_num_patches and max_num_patches")
|
| 266 |
+
|
| 267 |
+
self.min_pixels = int(min_num_patches) * patch_size * patch_size
|
| 268 |
+
self.max_pixels = int(max_num_patches) * patch_size * patch_size
|
| 269 |
+
|
| 270 |
+
def set_gguf_parameters(self):
|
| 271 |
+
super().set_gguf_parameters()
|
| 272 |
+
assert self.hparams_vision is not None
|
| 273 |
+
|
| 274 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.PHI4)
|
| 275 |
+
self.gguf_writer.add_vision_min_pixels(self.min_pixels)
|
| 276 |
+
self.gguf_writer.add_vision_max_pixels(self.max_pixels)
|
| 277 |
+
self.gguf_writer.add_vision_use_gelu(True)
|
| 278 |
+
self.gguf_writer.add_vision_attention_layernorm_eps(self.hparams_vision.get("layer_norm_eps", 1e-6))
|
| 279 |
+
|
| 280 |
+
@classmethod
|
| 281 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 282 |
+
name, gen = item
|
| 283 |
+
|
| 284 |
+
name = name.replace("model.vision_tower.vision_tower.", "vision_tower.")
|
| 285 |
+
|
| 286 |
+
if not name.startswith(("vision_tower.", "model.mm_projector.", "mm_projector.")):
|
| 287 |
+
return None
|
| 288 |
+
|
| 289 |
+
if ".vision_model.head." in name:
|
| 290 |
+
return None
|
| 291 |
+
|
| 292 |
+
if ".vision_model.post_layernorm." in name:
|
| 293 |
+
return None
|
| 294 |
+
|
| 295 |
+
return super().filter_tensors((name, gen))
|
| 296 |
+
|
| 297 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 298 |
+
if name.startswith("vision_tower."):
|
| 299 |
+
if bid is not None and bid == self.vision_last_layer_idx:
|
| 300 |
+
return
|
| 301 |
+
|
| 302 |
+
if name.endswith("vision_model.embeddings.patch_embedding.weight"):
|
| 303 |
+
assert self.hparams_vision is not None
|
| 304 |
+
if data_torch.ndim != 2:
|
| 305 |
+
raise ValueError(f"Unexpected Phi-4 patch embedding shape: {tuple(data_torch.shape)}")
|
| 306 |
+
|
| 307 |
+
patch_area = self.hparams_vision["patch_size"] ** 2
|
| 308 |
+
in_features = data_torch.shape[1]
|
| 309 |
+
if in_features % patch_area != 0:
|
| 310 |
+
raise ValueError(
|
| 311 |
+
f"Phi-4 patch embedding input dim {in_features} is not divisible by patch area {patch_area}"
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
num_channels = in_features // patch_area
|
| 315 |
+
patch_size = self.hparams_vision["patch_size"]
|
| 316 |
+
data_torch = data_torch.view(data_torch.shape[0], patch_size, patch_size, num_channels)
|
| 317 |
+
data_torch = data_torch.permute(0, 3, 1, 2)
|
| 318 |
+
|
| 319 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 320 |
+
return
|
| 321 |
+
|
| 322 |
+
if name.startswith(("model.mm_projector.", "mm_projector.")):
|
| 323 |
+
local_name = name
|
| 324 |
+
local_name = local_name.replace("model.mm_projector.", "")
|
| 325 |
+
local_name = local_name.replace("mm_projector.", "")
|
| 326 |
+
|
| 327 |
+
if not (local_name.startswith("0.") or local_name.startswith("2.")):
|
| 328 |
+
return
|
| 329 |
+
|
| 330 |
+
suffix = ".bias" if local_name.endswith(".bias") else ".weight"
|
| 331 |
+
mm_idx = int(local_name.split(".", maxsplit=1)[0])
|
| 332 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.V_MMPROJ, mm_idx, suffix=suffix), data_torch)
|
| 333 |
+
return
|
| 334 |
+
|
| 335 |
+
return
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
@ModelBase.register("PhiMoEForCausalLM")
|
| 339 |
+
class PhiMoeModel(Phi3MiniModel):
|
| 340 |
+
model_arch = gguf.MODEL_ARCH.PHIMOE
|
| 341 |
+
|
| 342 |
+
_experts: list[dict[str, Tensor]] | None = None
|
| 343 |
+
|
| 344 |
+
def set_gguf_parameters(self):
|
| 345 |
+
super().set_gguf_parameters()
|
| 346 |
+
self.gguf_writer.add_expert_used_count(self.find_hparam(["num_experts_per_tok", "num_experts_per_token"]))
|
| 347 |
+
self.gguf_writer.add_expert_count(self.find_hparam(["num_local_experts", "num_experts"]))
|
| 348 |
+
|
| 349 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 350 |
+
# process the experts separately
|
| 351 |
+
if name.find("block_sparse_moe.experts") != -1:
|
| 352 |
+
n_experts = self.find_hparam(["num_local_experts", "num_experts"])
|
| 353 |
+
assert bid is not None
|
| 354 |
+
|
| 355 |
+
if self._experts is None:
|
| 356 |
+
self._experts = [{} for _ in range(self.block_count)]
|
| 357 |
+
|
| 358 |
+
self._experts[bid][name] = data_torch
|
| 359 |
+
|
| 360 |
+
if len(self._experts[bid]) >= n_experts * 3:
|
| 361 |
+
# merge the experts into a single 3d tensor
|
| 362 |
+
for w_name in ["w1", "w2", "w3"]:
|
| 363 |
+
datas: list[Tensor] = []
|
| 364 |
+
|
| 365 |
+
for xid in range(n_experts):
|
| 366 |
+
ename = f"model.layers.{bid}.block_sparse_moe.experts.{xid}.{w_name}.weight"
|
| 367 |
+
datas.append(self._experts[bid][ename])
|
| 368 |
+
del self._experts[bid][ename]
|
| 369 |
+
|
| 370 |
+
data_torch = torch.stack(datas, dim=0)
|
| 371 |
+
|
| 372 |
+
merged_name = f"model.layers.{bid}.block_sparse_moe.experts.{w_name}.weight"
|
| 373 |
+
|
| 374 |
+
yield from super().modify_tensors(data_torch, merged_name, bid)
|
| 375 |
+
return
|
| 376 |
+
else:
|
| 377 |
+
return
|
| 378 |
+
|
| 379 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 380 |
+
|
| 381 |
+
def prepare_tensors(self):
|
| 382 |
+
super().prepare_tensors()
|
| 383 |
+
|
| 384 |
+
if self._experts is not None:
|
| 385 |
+
# flatten `list[dict[str, Tensor]]` into `list[str]`
|
| 386 |
+
experts = [k for d in self._experts for k in d.keys()]
|
| 387 |
+
if len(experts) > 0:
|
| 388 |
+
raise ValueError(f"Unprocessed experts: {experts}")
|
llama.cpp/conversion/pixtral.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Sequence
|
| 4 |
+
|
| 5 |
+
from .base import gguf
|
| 6 |
+
|
| 7 |
+
from .llava import LlavaVisionModel
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class PixtralModel(LlavaVisionModel):
|
| 11 |
+
model_name = "Pixtral"
|
| 12 |
+
hf_arch = ""
|
| 13 |
+
is_mistral_format = True
|
| 14 |
+
|
| 15 |
+
def set_gguf_parameters(self):
|
| 16 |
+
super().set_gguf_parameters()
|
| 17 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.PIXTRAL)
|
| 18 |
+
|
| 19 |
+
self.gguf_writer.add_vision_attention_layernorm_eps(
|
| 20 |
+
self.find_hparam(["norm_eps"])
|
| 21 |
+
)
|
| 22 |
+
self.gguf_writer.add_rope_freq_base(self.find_vparam(["rope_theta"]))
|
| 23 |
+
|
| 24 |
+
self.gguf_writer.add_vision_use_silu(True)
|
| 25 |
+
|
| 26 |
+
# spatial_merge_size
|
| 27 |
+
if self.find_vparam(["mm_projector_id"], optional=True) == "patch_merge":
|
| 28 |
+
self.gguf_writer.add_vision_spatial_merge_size(
|
| 29 |
+
self.find_vparam(["spatial_merge_size"])
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
def map_tensor_name(self, name: str, try_suffixes: Sequence[str] = (".weight", ".bias")) -> str:
|
| 33 |
+
if name == "vision_language_adapter.w_in.weight":
|
| 34 |
+
return "mm.1.weight"
|
| 35 |
+
elif name == "vision_language_adapter.w_in.bias":
|
| 36 |
+
return "mm.1.bias"
|
| 37 |
+
elif name == "vision_language_adapter.w_out.weight":
|
| 38 |
+
return "mm.2.weight"
|
| 39 |
+
elif name == "vision_language_adapter.w_out.bias":
|
| 40 |
+
return "mm.2.bias"
|
| 41 |
+
return super().map_tensor_name(name, try_suffixes)
|
llama.cpp/conversion/plamo.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
from typing import Iterable, TYPE_CHECKING
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
if TYPE_CHECKING:
|
| 10 |
+
from torch import Tensor
|
| 11 |
+
|
| 12 |
+
from .base import ModelBase, TextModel, gguf
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@ModelBase.register("PlamoForCausalLM")
|
| 16 |
+
class PlamoModel(TextModel):
|
| 17 |
+
model_arch = gguf.MODEL_ARCH.PLAMO
|
| 18 |
+
|
| 19 |
+
def set_vocab(self):
|
| 20 |
+
self._set_vocab_sentencepiece()
|
| 21 |
+
|
| 22 |
+
def set_gguf_parameters(self):
|
| 23 |
+
hparams = self.hparams
|
| 24 |
+
|
| 25 |
+
self.gguf_writer.add_context_length(4096) # not in config.json
|
| 26 |
+
self.gguf_writer.add_embedding_length(hparams["hidden_size"])
|
| 27 |
+
self.gguf_writer.add_feed_forward_length(hparams["intermediate_size"])
|
| 28 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 29 |
+
self.gguf_writer.add_head_count(hparams["num_attention_heads"])
|
| 30 |
+
self.gguf_writer.add_head_count_kv(5) # hparams["num_key_value_heads"]) is wrong
|
| 31 |
+
self.gguf_writer.add_layer_norm_rms_eps(hparams["rms_norm_eps"])
|
| 32 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 33 |
+
|
| 34 |
+
def shuffle_attn_q_weight(self, data_torch):
|
| 35 |
+
assert data_torch.size() == (5120, 5120)
|
| 36 |
+
data_torch = data_torch.reshape(8, 5, 128, 5120)
|
| 37 |
+
data_torch = torch.permute(data_torch, (1, 0, 2, 3))
|
| 38 |
+
data_torch = torch.reshape(data_torch, (5120, 5120))
|
| 39 |
+
return data_torch
|
| 40 |
+
|
| 41 |
+
def shuffle_attn_output_weight(self, data_torch):
|
| 42 |
+
assert data_torch.size() == (5120, 5120)
|
| 43 |
+
data_torch = data_torch.reshape(5120, 8, 5, 128)
|
| 44 |
+
data_torch = torch.permute(data_torch, (0, 2, 1, 3))
|
| 45 |
+
data_torch = torch.reshape(data_torch, (5120, 5120))
|
| 46 |
+
return data_torch
|
| 47 |
+
|
| 48 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 49 |
+
new_name = self.map_tensor_name(name)
|
| 50 |
+
|
| 51 |
+
# shuffle for broadcasting of gqa in ggml_mul_mat
|
| 52 |
+
if new_name.endswith("attn_q.weight"):
|
| 53 |
+
data_torch = self.shuffle_attn_q_weight(data_torch)
|
| 54 |
+
elif new_name.endswith("attn_output.weight"):
|
| 55 |
+
data_torch = self.shuffle_attn_output_weight(data_torch)
|
| 56 |
+
|
| 57 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@ModelBase.register("Plamo2ForCausalLM", "PLaMo2ForCausalLM")
|
| 61 |
+
class Plamo2Model(TextModel):
|
| 62 |
+
model_arch = gguf.MODEL_ARCH.PLAMO2
|
| 63 |
+
|
| 64 |
+
def set_vocab(self):
|
| 65 |
+
self._set_vocab_plamo()
|
| 66 |
+
|
| 67 |
+
def set_gguf_parameters(self):
|
| 68 |
+
hparams = self.hparams
|
| 69 |
+
self.gguf_writer.add_vocab_size(self.hparams["vocab_size"])
|
| 70 |
+
|
| 71 |
+
# Which layers are Mamba layers
|
| 72 |
+
# PLaMo 2 uses mamba_step to indicate the pattern (e.g., 2 means every other layer)
|
| 73 |
+
# This logic matches modeling_plamo.py's is_mamba function
|
| 74 |
+
mamba_step = hparams.get("mamba_step", 2)
|
| 75 |
+
mamba_enabled = hparams.get("mamba_enabled", True)
|
| 76 |
+
num_key_value_heads = []
|
| 77 |
+
num_attention_heads = []
|
| 78 |
+
|
| 79 |
+
if mamba_enabled:
|
| 80 |
+
for i in range(self.block_count):
|
| 81 |
+
if self.block_count <= (mamba_step // 2):
|
| 82 |
+
# use attention in last layer
|
| 83 |
+
is_mamba = (i != self.block_count - 1)
|
| 84 |
+
else:
|
| 85 |
+
is_mamba = (i % mamba_step) != (mamba_step // 2)
|
| 86 |
+
if is_mamba:
|
| 87 |
+
num_key_value_heads.append(0)
|
| 88 |
+
num_attention_heads.append(0)
|
| 89 |
+
else:
|
| 90 |
+
num_key_value_heads.append(hparams.get("num_key_value_heads", 4))
|
| 91 |
+
num_attention_heads.append(hparams.get("num_attention_heads", 32))
|
| 92 |
+
|
| 93 |
+
if num_key_value_heads and num_attention_heads:
|
| 94 |
+
self.gguf_writer.add_head_count_kv(num_key_value_heads)
|
| 95 |
+
self.gguf_writer.add_head_count(num_attention_heads)
|
| 96 |
+
|
| 97 |
+
self.gguf_writer.add_context_length(hparams.get("max_position_embeddings", 2048))
|
| 98 |
+
self.gguf_writer.add_embedding_length(hparams.get("hidden_size", 4096))
|
| 99 |
+
self.gguf_writer.add_key_length(hparams.get("hidden_size_per_head", 128))
|
| 100 |
+
self.gguf_writer.add_value_length(hparams.get("hidden_size_per_head", 128))
|
| 101 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 102 |
+
self.gguf_writer.add_layer_norm_rms_eps(hparams.get("rms_norm_eps", 1e-06))
|
| 103 |
+
self.gguf_writer.add_rope_freq_base(self.rope_parameters.get("rope_theta", 10000))
|
| 104 |
+
|
| 105 |
+
# Mamba parameters
|
| 106 |
+
self.gguf_writer.add_ssm_state_size(hparams.get("mamba_d_state", 64))
|
| 107 |
+
self.gguf_writer.add_ssm_conv_kernel(hparams.get("mamba_d_conv", 4))
|
| 108 |
+
self.gguf_writer.add_ssm_time_step_rank(hparams.get("mamba_num_heads", 64))
|
| 109 |
+
intermediate_size = hparams.get("mamba_num_heads", 64) * hparams.get("hidden_size_per_head", 128)
|
| 110 |
+
self.gguf_writer.add_ssm_inner_size(intermediate_size)
|
| 111 |
+
self.gguf_writer.add_ssm_group_count(0)
|
| 112 |
+
|
| 113 |
+
# MLP feed forward parameters (for attention layers)
|
| 114 |
+
self.gguf_writer.add_feed_forward_length(hparams.get("intermediate_size", 13312))
|
| 115 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 116 |
+
|
| 117 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 118 |
+
if name.endswith(".A_log"):
|
| 119 |
+
data_torch = -torch.exp(data_torch)
|
| 120 |
+
elif name.endswith(".dt_bias"):
|
| 121 |
+
name = name.rpartition(".dt_bias")[0] + ".dt_proj.bias"
|
| 122 |
+
elif name.endswith(".dt_norm_weight"):
|
| 123 |
+
name = name.rpartition(".dt_norm_weight")[0] + ".dt_norm.weight"
|
| 124 |
+
elif name.endswith(".B_norm_weight"):
|
| 125 |
+
name = name.rpartition(".B_norm_weight")[0] + ".B_norm.weight"
|
| 126 |
+
elif name.endswith(".C_norm_weight"):
|
| 127 |
+
name = name.rpartition(".C_norm_weight")[0] + ".C_norm.weight"
|
| 128 |
+
elif name.endswith(".k_weight"):
|
| 129 |
+
name = name.rpartition(".k_weight")[0] + ".k.weight"
|
| 130 |
+
elif name.endswith(".q_weight"):
|
| 131 |
+
name = name.rpartition(".q_weight")[0] + ".q.weight"
|
| 132 |
+
elif name.endswith(".conv1d.weight"):
|
| 133 |
+
data_torch = torch.squeeze(data_torch) # remove (, 1, )
|
| 134 |
+
assert data_torch.ndim == 2
|
| 135 |
+
elif name.endswith(".pre_mixer_norm.weight"):
|
| 136 |
+
data_torch += 1.0
|
| 137 |
+
elif name.endswith(".post_mixer_norm.weight"):
|
| 138 |
+
data_torch += 1.0 / 5
|
| 139 |
+
elif name.endswith(".pre_mlp_norm.weight"):
|
| 140 |
+
data_torch += 1.0
|
| 141 |
+
elif name.endswith(".post_mlp_norm.weight"):
|
| 142 |
+
data_torch += 1.0 / (5**1.5)
|
| 143 |
+
elif name.endswith(".norm.weight"):
|
| 144 |
+
data_torch += 1.0
|
| 145 |
+
|
| 146 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
@ModelBase.register("Plamo3ForCausalLM", "PLaMo3ForCausalLM")
|
| 150 |
+
class Plamo3Model(TextModel):
|
| 151 |
+
model_arch = gguf.MODEL_ARCH.PLAMO3
|
| 152 |
+
|
| 153 |
+
def set_vocab(self):
|
| 154 |
+
self._set_vocab_plamo()
|
| 155 |
+
|
| 156 |
+
tokenizer_config_path = self.dir_model / "tokenizer_config.json"
|
| 157 |
+
tokenizer_config = {}
|
| 158 |
+
|
| 159 |
+
if tokenizer_config_path.is_file():
|
| 160 |
+
with open(tokenizer_config_path, encoding="utf-8") as f:
|
| 161 |
+
tokenizer_config = json.load(f)
|
| 162 |
+
|
| 163 |
+
chat_template = tokenizer_config.get("chat_template")
|
| 164 |
+
chat_template_jinja = self.dir_model / "chat_template.jinja"
|
| 165 |
+
|
| 166 |
+
if chat_template_jinja.is_file():
|
| 167 |
+
with open(chat_template_jinja, encoding="utf-8") as f:
|
| 168 |
+
chat_template = f.read()
|
| 169 |
+
|
| 170 |
+
if chat_template:
|
| 171 |
+
self.gguf_writer.add_chat_template(chat_template)
|
| 172 |
+
|
| 173 |
+
def set_gguf_parameters(self):
|
| 174 |
+
super().set_gguf_parameters()
|
| 175 |
+
self.gguf_writer.add_vocab_size(self.hparams["vocab_size"])
|
| 176 |
+
if (sliding_window := self.find_hparam(["window_size", "sliding_window"], optional=True)) is not None:
|
| 177 |
+
self.gguf_writer.add_sliding_window(sliding_window)
|
| 178 |
+
self.gguf_writer.add_sliding_window_pattern(self.hparams["sliding_window_pattern"])
|
| 179 |
+
|
| 180 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 181 |
+
|
| 182 |
+
if name.endswith(".pre_mixer_norm.weight"):
|
| 183 |
+
data_torch = data_torch + 1.0
|
| 184 |
+
elif name.endswith(".post_mixer_norm.weight"):
|
| 185 |
+
data_torch = data_torch + 1.0 / 5
|
| 186 |
+
elif name.endswith(".pre_mlp_norm.weight"):
|
| 187 |
+
data_torch = data_torch + 1.0
|
| 188 |
+
elif name.endswith(".post_mlp_norm.weight"):
|
| 189 |
+
data_torch = data_torch + 1.0 / (5**1.5)
|
| 190 |
+
elif name.endswith((".mixer.q_norm.weight", ".mixer.k_norm.weight")):
|
| 191 |
+
data_torch = data_torch + 1.0
|
| 192 |
+
elif name.endswith(".norm.weight"):
|
| 193 |
+
data_torch = data_torch + 1.0
|
| 194 |
+
|
| 195 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
llama.cpp/conversion/plm.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from .base import ModelBase, TextModel, gguf
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@ModelBase.register("PLMForCausalLM")
|
| 7 |
+
class PLMModel(TextModel):
|
| 8 |
+
model_arch = gguf.MODEL_ARCH.PLM
|
| 9 |
+
|
| 10 |
+
def set_vocab(self):
|
| 11 |
+
self._set_vocab_gpt2()
|
| 12 |
+
|
| 13 |
+
def set_gguf_parameters(self):
|
| 14 |
+
super().set_gguf_parameters()
|
| 15 |
+
hparams = self.hparams
|
| 16 |
+
self.gguf_writer.add_vocab_size(hparams["vocab_size"])
|
| 17 |
+
self.gguf_writer.add_kv_lora_rank(hparams["kv_lora_rank"])
|
| 18 |
+
self.gguf_writer.add_key_length(hparams["qk_nope_head_dim"] + hparams["qk_rope_head_dim"])
|
| 19 |
+
self.gguf_writer.add_value_length(hparams["v_head_dim"])
|
| 20 |
+
self.gguf_writer.add_rope_dimension_count(hparams["qk_rope_head_dim"])
|
| 21 |
+
|
| 22 |
+
def prepare_tensors(self):
|
| 23 |
+
super().prepare_tensors()
|
llama.cpp/conversion/qwen.py
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any, Callable, Iterable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING:
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
from .base import ModelBase, TextModel, gguf, logger
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@ModelBase.register("QWenLMHeadModel")
|
| 14 |
+
class QwenModel(TextModel):
|
| 15 |
+
model_arch = gguf.MODEL_ARCH.QWEN
|
| 16 |
+
|
| 17 |
+
@staticmethod
|
| 18 |
+
def token_bytes_to_string(b):
|
| 19 |
+
from transformers.models.gpt2.tokenization_gpt2 import bytes_to_unicode # ty: ignore[unresolved-import]
|
| 20 |
+
byte_encoder = bytes_to_unicode()
|
| 21 |
+
return ''.join([byte_encoder[ord(char)] for char in b.decode('latin-1')])
|
| 22 |
+
|
| 23 |
+
@staticmethod
|
| 24 |
+
def bpe(mergeable_ranks: dict[bytes, int], token: bytes, max_rank: int | None = None) -> list[bytes]:
|
| 25 |
+
parts = [bytes([b]) for b in token]
|
| 26 |
+
while True:
|
| 27 |
+
min_idx = None
|
| 28 |
+
min_rank = None
|
| 29 |
+
for i, pair in enumerate(zip(parts[:-1], parts[1:])):
|
| 30 |
+
rank = mergeable_ranks.get(pair[0] + pair[1])
|
| 31 |
+
if rank is not None and (min_rank is None or rank < min_rank):
|
| 32 |
+
min_idx = i
|
| 33 |
+
min_rank = rank
|
| 34 |
+
if min_rank is None or (max_rank is not None and min_rank >= max_rank):
|
| 35 |
+
break
|
| 36 |
+
assert min_idx is not None
|
| 37 |
+
parts = parts[:min_idx] + [parts[min_idx] + parts[min_idx + 1]] + parts[min_idx + 2:]
|
| 38 |
+
return parts
|
| 39 |
+
|
| 40 |
+
def set_vocab(self):
|
| 41 |
+
self._set_vocab_qwen()
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@ModelBase.register(
|
| 45 |
+
"Qwen2Model",
|
| 46 |
+
"Qwen2ForCausalLM",
|
| 47 |
+
"Qwen2AudioForConditionalGeneration",
|
| 48 |
+
"KORMoForCausalLM",
|
| 49 |
+
"AudioFlamingo3ForConditionalGeneration",
|
| 50 |
+
"DotsOCRForCausalLM",
|
| 51 |
+
)
|
| 52 |
+
class Qwen2Model(TextModel):
|
| 53 |
+
model_arch = gguf.MODEL_ARCH.QWEN2
|
| 54 |
+
|
| 55 |
+
def set_vocab(self):
|
| 56 |
+
try:
|
| 57 |
+
self._set_vocab_sentencepiece()
|
| 58 |
+
except FileNotFoundError:
|
| 59 |
+
self._set_vocab_gpt2()
|
| 60 |
+
|
| 61 |
+
def set_gguf_parameters(self):
|
| 62 |
+
super().set_gguf_parameters()
|
| 63 |
+
self._try_set_pooling_type()
|
| 64 |
+
|
| 65 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 66 |
+
if self.hf_arch == "Qwen2Model":
|
| 67 |
+
name = f"model.{name}" # map to Qwen2ForCausalLM tensors
|
| 68 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
@ModelBase.register("Qwen2MoeForCausalLM")
|
| 72 |
+
class Qwen2MoeModel(TextModel):
|
| 73 |
+
model_arch = gguf.MODEL_ARCH.QWEN2MOE
|
| 74 |
+
|
| 75 |
+
def set_gguf_parameters(self):
|
| 76 |
+
super().set_gguf_parameters()
|
| 77 |
+
if (moe_intermediate_size := self.hparams.get("moe_intermediate_size")) is not None:
|
| 78 |
+
self.gguf_writer.add_expert_feed_forward_length(moe_intermediate_size)
|
| 79 |
+
logger.info(f"gguf: expert feed forward length = {moe_intermediate_size}")
|
| 80 |
+
if (shared_expert_intermediate_size := self.hparams.get('shared_expert_intermediate_size')) is not None:
|
| 81 |
+
self.gguf_writer.add_expert_shared_feed_forward_length(shared_expert_intermediate_size)
|
| 82 |
+
logger.info(f"gguf: expert shared feed forward length = {shared_expert_intermediate_size}")
|
| 83 |
+
|
| 84 |
+
_experts: list[dict[str, Tensor]] | None = None
|
| 85 |
+
|
| 86 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 87 |
+
# handle aggregated expert tensors
|
| 88 |
+
# GGUF stores dimensions reversed from PyTorch, so:
|
| 89 |
+
# PyTorch (A,B,C) -> GGUF writes [C,B,A] -> GGML reads ne={C,B,A}
|
| 90 |
+
# Input shapes from HF: (n_expert, n_ff_exp, n_embd) or (n_expert, n_embd, n_ff_exp)
|
| 91 |
+
# Expected GGML ne: {n_embd, n_ff_exp, n_expert} for gate/up, {n_ff_exp, n_embd, n_expert} for down
|
| 92 |
+
if name.endswith("mlp.experts.down_proj") or name.endswith("mlp.experts.down_proj.weight"):
|
| 93 |
+
mapped = f"{name}.weight" if not name.endswith(".weight") else name
|
| 94 |
+
# HF: [n_expert, n_embd, n_ff] -> GGML: {n_ff, n_embd, n_expert}
|
| 95 |
+
yield from super().modify_tensors(data_torch, mapped, bid)
|
| 96 |
+
return
|
| 97 |
+
|
| 98 |
+
if name.endswith("mlp.experts.gate_up_proj") or name.endswith("mlp.experts.gate_up_proj.weight"):
|
| 99 |
+
if data_torch.ndim < 3 or data_torch.shape[-2] % 2 != 0:
|
| 100 |
+
raise ValueError(f"Unexpected gate_up_proj shape for {name}: {tuple(data_torch.shape)}")
|
| 101 |
+
# HF: [n_expert, 2*n_ff, n_embd] -> split on dim=-2
|
| 102 |
+
n_ff = data_torch.shape[-2] // 2
|
| 103 |
+
gate = data_torch[..., :n_ff, :].contiguous()
|
| 104 |
+
up = data_torch[..., n_ff:, :].contiguous()
|
| 105 |
+
# gate/up: [n_expert, n_ff, n_embd] -> GGML: {n_embd, n_ff, n_expert}
|
| 106 |
+
base_name = name.removesuffix(".weight").removesuffix(".gate_up_proj")
|
| 107 |
+
mapped_gate = f"{base_name}.gate_proj.weight"
|
| 108 |
+
mapped_up = f"{base_name}.up_proj.weight"
|
| 109 |
+
yield from super().modify_tensors(gate, mapped_gate, bid)
|
| 110 |
+
yield from super().modify_tensors(up, mapped_up, bid)
|
| 111 |
+
return
|
| 112 |
+
|
| 113 |
+
if name.find("experts") != -1:
|
| 114 |
+
n_experts = self.find_hparam(["num_local_experts", "num_experts"])
|
| 115 |
+
assert bid is not None
|
| 116 |
+
|
| 117 |
+
if self._experts is None:
|
| 118 |
+
self._experts = [{} for _ in range(self.block_count)]
|
| 119 |
+
|
| 120 |
+
self._experts[bid][name] = data_torch
|
| 121 |
+
|
| 122 |
+
if len(self._experts[bid]) >= n_experts * 3:
|
| 123 |
+
# merge the experts into a single 3d tensor
|
| 124 |
+
for w_name in ["down_proj", "gate_proj", "up_proj"]:
|
| 125 |
+
datas: list[Tensor] = []
|
| 126 |
+
|
| 127 |
+
for xid in range(n_experts):
|
| 128 |
+
ename = f"model.layers.{bid}.mlp.experts.{xid}.{w_name}.weight"
|
| 129 |
+
datas.append(self._experts[bid][ename])
|
| 130 |
+
del self._experts[bid][ename]
|
| 131 |
+
|
| 132 |
+
data_torch = torch.stack(datas, dim=0)
|
| 133 |
+
|
| 134 |
+
merged_name = f"model.layers.{bid}.mlp.experts.{w_name}.weight"
|
| 135 |
+
|
| 136 |
+
yield from super().modify_tensors(data_torch, merged_name, bid)
|
| 137 |
+
return
|
| 138 |
+
else:
|
| 139 |
+
return
|
| 140 |
+
|
| 141 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 142 |
+
|
| 143 |
+
def prepare_tensors(self):
|
| 144 |
+
super().prepare_tensors()
|
| 145 |
+
|
| 146 |
+
if self._experts is not None:
|
| 147 |
+
# flatten `list[dict[str, Tensor]]` into `list[str]`
|
| 148 |
+
experts = [k for d in self._experts for k in d.keys()]
|
| 149 |
+
if len(experts) > 0:
|
| 150 |
+
raise ValueError(f"Unprocessed experts: {experts}")
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
@ModelBase.register("Qwen3ForCausalLM", "Qwen3Model")
|
| 154 |
+
class Qwen3Model(Qwen2Model):
|
| 155 |
+
model_arch = gguf.MODEL_ARCH.QWEN3
|
| 156 |
+
|
| 157 |
+
# extra logic for rerank models
|
| 158 |
+
is_rerank: bool = False
|
| 159 |
+
is_tied_embeddings: bool = False
|
| 160 |
+
token_false_id: int | None = None
|
| 161 |
+
token_true_id: int | None = None
|
| 162 |
+
|
| 163 |
+
def __init__(self, *args, **kwargs):
|
| 164 |
+
super().__init__(*args, **kwargs)
|
| 165 |
+
|
| 166 |
+
# track for intern-s1-mini
|
| 167 |
+
hparams = ModelBase.load_hparams(self.dir_model, is_mistral_format=False)
|
| 168 |
+
self.origin_hf_arch = hparams.get('architectures', [None])[0]
|
| 169 |
+
|
| 170 |
+
if self._is_qwen3_reranker():
|
| 171 |
+
self._find_rerank_config()
|
| 172 |
+
|
| 173 |
+
def _is_qwen3_reranker(self) -> bool:
|
| 174 |
+
readme_path = self.dir_model / "README.md"
|
| 175 |
+
readme_text = ""
|
| 176 |
+
if readme_path.exists():
|
| 177 |
+
with readme_path.open("r", encoding="utf-8") as f:
|
| 178 |
+
readme_text = f.read()
|
| 179 |
+
|
| 180 |
+
name_hints = [
|
| 181 |
+
str(self.dir_model.name),
|
| 182 |
+
str(self.hparams.get("_name_or_path", "")),
|
| 183 |
+
str(self.hparams.get("model_type", "")),
|
| 184 |
+
str(self.origin_hf_arch or ""),
|
| 185 |
+
]
|
| 186 |
+
name_hints = [hint.lower() for hint in name_hints if hint]
|
| 187 |
+
|
| 188 |
+
if "# qwen3-reranker" in readme_text.lower() or "# qwen3-vl-reranker" in readme_text.lower():
|
| 189 |
+
return True
|
| 190 |
+
|
| 191 |
+
if any("qwen3-reranker" in hint or "qwen3-vl-reranker" in hint for hint in name_hints):
|
| 192 |
+
return True
|
| 193 |
+
|
| 194 |
+
return "sequenceclassification" in (self.origin_hf_arch or "").lower()
|
| 195 |
+
|
| 196 |
+
def set_vocab(self):
|
| 197 |
+
# deal with intern-s1-mini
|
| 198 |
+
if self.origin_hf_arch == 'InternS1ForConditionalGeneration':
|
| 199 |
+
self._set_vocab_interns1()
|
| 200 |
+
return
|
| 201 |
+
|
| 202 |
+
super().set_vocab()
|
| 203 |
+
|
| 204 |
+
def _find_rerank_config(self):
|
| 205 |
+
from transformers import AutoTokenizer
|
| 206 |
+
tokenizer = AutoTokenizer.from_pretrained(self.dir_model)
|
| 207 |
+
|
| 208 |
+
self.is_rerank = True
|
| 209 |
+
self.is_tied_embeddings = self.hparams.get("tie_word_embeddings", False)
|
| 210 |
+
self.token_false_id = tokenizer.convert_tokens_to_ids("no") # ty: ignore[unresolved-attribute, invalid-assignment]
|
| 211 |
+
self.token_true_id = tokenizer.convert_tokens_to_ids("yes") # ty: ignore[unresolved-attribute, invalid-assignment]
|
| 212 |
+
self.sep_token_id = tokenizer.convert_tokens_to_ids("|") # ty: ignore[unresolved-attribute]
|
| 213 |
+
|
| 214 |
+
assert self.token_false_id is not None and self.token_true_id is not None
|
| 215 |
+
|
| 216 |
+
def set_gguf_parameters(self):
|
| 217 |
+
super().set_gguf_parameters()
|
| 218 |
+
if self.is_rerank:
|
| 219 |
+
self.gguf_writer.add_pooling_type(gguf.PoolingType.RANK)
|
| 220 |
+
self.gguf_writer.add_classifier_output_labels(["yes", "no"])
|
| 221 |
+
self.gguf_writer.add_chat_template([{
|
| 222 |
+
"name": "rerank",
|
| 223 |
+
"template": "<|im_start|>system\nJudge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be \"yes\" or \"no\".<|im_end|>\n"
|
| 224 |
+
"<|im_start|>user\n<Instruct>: Given a web search query, retrieve relevant passages that answer the query\n<Query>: {query}\n<Document>: {document}<|im_end|>\n"
|
| 225 |
+
"<|im_start|>assistant\n<think>\n\n</think>\n\n"
|
| 226 |
+
}])
|
| 227 |
+
|
| 228 |
+
def _get_cls_out_tensor(self, data_torch: Tensor) -> Tensor:
|
| 229 |
+
# extract "yes" and "no" tokens from the output lm_head tensor
|
| 230 |
+
false_row = data_torch[self.token_false_id]
|
| 231 |
+
true_row = data_torch[self.token_true_id]
|
| 232 |
+
return torch.stack([true_row, false_row], dim=0)
|
| 233 |
+
|
| 234 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 235 |
+
if self.is_rerank:
|
| 236 |
+
is_tied_head = self.is_tied_embeddings and "embed_tokens" in name
|
| 237 |
+
is_real_head = not self.is_tied_embeddings and "lm_head" in name
|
| 238 |
+
if is_tied_head or is_real_head:
|
| 239 |
+
cls_out_head = (
|
| 240 |
+
gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.CLS_OUT] + ".weight",
|
| 241 |
+
self._get_cls_out_tensor(data_torch),
|
| 242 |
+
)
|
| 243 |
+
yield cls_out_head
|
| 244 |
+
if is_tied_head:
|
| 245 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 246 |
+
return
|
| 247 |
+
|
| 248 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
@ModelBase.register("Qwen3MoeForCausalLM")
|
| 252 |
+
class Qwen3MoeModel(Qwen2MoeModel):
|
| 253 |
+
model_arch = gguf.MODEL_ARCH.QWEN3MOE
|
| 254 |
+
|
| 255 |
+
def __init__(self, *args, **kwargs):
|
| 256 |
+
super().__init__(*args, **kwargs)
|
| 257 |
+
hparams = ModelBase.load_hparams(self.dir_model, False)
|
| 258 |
+
self.origin_hf_arch = hparams.get('architectures', [None])[0]
|
| 259 |
+
|
| 260 |
+
def set_vocab(self):
|
| 261 |
+
# deal with intern-s1
|
| 262 |
+
if self.origin_hf_arch == 'InternS1ForConditionalGeneration':
|
| 263 |
+
self._set_vocab_interns1()
|
| 264 |
+
return
|
| 265 |
+
|
| 266 |
+
super().set_vocab()
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
@ModelBase.register("Qwen3NextForCausalLM")
|
| 270 |
+
class Qwen3NextModel(Qwen2MoeModel):
|
| 271 |
+
model_arch = gguf.MODEL_ARCH.QWEN3NEXT
|
| 272 |
+
|
| 273 |
+
def set_gguf_parameters(self):
|
| 274 |
+
super().set_gguf_parameters()
|
| 275 |
+
self.gguf_writer.add_ssm_conv_kernel(self.hparams["linear_conv_kernel_dim"])
|
| 276 |
+
self.gguf_writer.add_ssm_state_size(self.hparams["linear_key_head_dim"])
|
| 277 |
+
self.gguf_writer.add_ssm_group_count(self.hparams["linear_num_key_heads"])
|
| 278 |
+
self.gguf_writer.add_ssm_time_step_rank(self.hparams["linear_num_value_heads"])
|
| 279 |
+
self.gguf_writer.add_ssm_inner_size(self.hparams["linear_value_head_dim"] * self.hparams["linear_num_value_heads"])
|
| 280 |
+
self.gguf_writer.add_full_attention_interval(self.hparams.get("full_attention_interval", 4))
|
| 281 |
+
if (rope_dim := self.hparams.get("head_dim")) is None:
|
| 282 |
+
rope_dim = self.hparams["hidden_size"] // self.hparams["num_attention_heads"]
|
| 283 |
+
self.gguf_writer.add_rope_dimension_count(int(rope_dim * self.rope_parameters.get("partial_rotary_factor", 0.25)))
|
| 284 |
+
|
| 285 |
+
@classmethod
|
| 286 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 287 |
+
name, gen = item
|
| 288 |
+
|
| 289 |
+
if name.startswith("mtp"):
|
| 290 |
+
# ignore MTP layers for now
|
| 291 |
+
return None
|
| 292 |
+
|
| 293 |
+
return super().filter_tensors(item)
|
| 294 |
+
|
| 295 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 296 |
+
if name.endswith(".A_log"):
|
| 297 |
+
data_torch = -torch.exp(data_torch)
|
| 298 |
+
elif name.endswith(".dt_bias"):
|
| 299 |
+
name = name.rpartition(".dt_bias")[0] + ".dt_proj.bias"
|
| 300 |
+
elif "conv1d" in name:
|
| 301 |
+
data_torch = data_torch.squeeze()
|
| 302 |
+
elif name.endswith("norm.weight") and not name.endswith("linear_attn.norm.weight"):
|
| 303 |
+
data_torch = data_torch + 1
|
| 304 |
+
|
| 305 |
+
if "in_proj_qkvz.weight" in name:
|
| 306 |
+
# original order: [q, k, v, z] * head_count
|
| 307 |
+
# corrected order: [q * head_count, k * head_count, v * head_count, z * head_count]
|
| 308 |
+
head_k_dim = self.hparams["linear_key_head_dim"]
|
| 309 |
+
head_v_dim = self.hparams["linear_value_head_dim"]
|
| 310 |
+
num_v_heads = self.hparams["linear_num_value_heads"]
|
| 311 |
+
num_k_heads = self.hparams["linear_num_key_heads"]
|
| 312 |
+
hidden_size = self.hparams["hidden_size"]
|
| 313 |
+
split_arg_list_qkvz = [
|
| 314 |
+
head_k_dim, # q partition
|
| 315 |
+
head_k_dim, # k partition
|
| 316 |
+
(num_v_heads // num_k_heads * head_v_dim), # v partition
|
| 317 |
+
(num_v_heads // num_k_heads * head_v_dim), # z partition
|
| 318 |
+
]
|
| 319 |
+
# view as (n_embd, head_count, [q+k+v+z])
|
| 320 |
+
data_torch = data_torch.permute(1, 0).contiguous()
|
| 321 |
+
data_torch = data_torch.view(-1, num_k_heads, sum(split_arg_list_qkvz))
|
| 322 |
+
# split into q, k, v, z
|
| 323 |
+
q, k, v, z = torch.split(data_torch, split_arg_list_qkvz, dim=-1)
|
| 324 |
+
# flatten dim + head_count
|
| 325 |
+
q = q.contiguous().view(hidden_size, -1)
|
| 326 |
+
k = k.contiguous().view(hidden_size, -1)
|
| 327 |
+
v = v.contiguous().view(hidden_size, -1)
|
| 328 |
+
z = z.contiguous().view(hidden_size, -1)
|
| 329 |
+
# stack back
|
| 330 |
+
qkv = torch.cat([q, k, v], dim=-1).permute(1, 0).contiguous()
|
| 331 |
+
z = z.permute(1, 0).contiguous()
|
| 332 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_QKV, bid, ".weight"), qkv)
|
| 333 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_GATE, bid, ".weight"), z)
|
| 334 |
+
else:
|
| 335 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
@ModelBase.register("RND1")
|
| 339 |
+
class RND1Model(Qwen2MoeModel):
|
| 340 |
+
model_arch = gguf.MODEL_ARCH.RND1
|
| 341 |
+
|
| 342 |
+
def set_gguf_parameters(self):
|
| 343 |
+
super().set_gguf_parameters()
|
| 344 |
+
|
| 345 |
+
# RND1 specific parameters
|
| 346 |
+
# RND1 uses bidirectional attention
|
| 347 |
+
self.gguf_writer.add_causal_attention(False)
|
| 348 |
+
|
| 349 |
+
if (mask_token_id := self.hparams.get("mask_token_id")) is not None:
|
| 350 |
+
self.gguf_writer.add_mask_token_id(mask_token_id)
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
class _LinearAttentionVReorderBase(Qwen3NextModel):
|
| 354 |
+
model_arch = gguf.MODEL_ARCH.QWEN3NEXT # overridden by subclasses
|
| 355 |
+
"""reorders V heads from grouped to tiled order for ggml broadcast
|
| 356 |
+
|
| 357 |
+
see https://github.com/ggml-org/llama.cpp/pull/19468#discussion_r2786394306
|
| 358 |
+
|
| 359 |
+
Linear attention may has num_k_heads < num_v_heads. The HF weights store
|
| 360 |
+
V heads grouped by K head: [G0_v0..v{r-1}, G1_v0..v{r-1}, ...].
|
| 361 |
+
ggml binary ops use tiled broadcast: [K0, K1, ..., K0, K1, ...].
|
| 362 |
+
We reorder V heads to tiled order so ggml_repeat can replace the expensive
|
| 363 |
+
interleaved repeat: [G0_v0, G1_v0, ..., G0_v1, G1_v1, ...].
|
| 364 |
+
"""
|
| 365 |
+
|
| 366 |
+
@staticmethod
|
| 367 |
+
def _reorder_v_heads(tensor: Tensor, dim: int, num_k_heads: int, num_v_per_k: int, head_dim: int) -> Tensor:
|
| 368 |
+
"""Reorder V heads from grouped (by K head) to tiled order along the given dimension."""
|
| 369 |
+
shape = list(tensor.shape)
|
| 370 |
+
if dim < 0:
|
| 371 |
+
dim += len(shape)
|
| 372 |
+
new_shape = shape[:dim] + [num_k_heads, num_v_per_k, head_dim] + shape[dim + 1:]
|
| 373 |
+
tensor = tensor.reshape(*new_shape)
|
| 374 |
+
perm = list(range(len(new_shape)))
|
| 375 |
+
perm[dim], perm[dim + 1] = perm[dim + 1], perm[dim]
|
| 376 |
+
return tensor.permute(*perm).contiguous().reshape(*shape)
|
| 377 |
+
|
| 378 |
+
def _transform_nvfp4_weight(self, name: str, weight: Tensor, scale: Tensor) -> tuple[Tensor, Tensor]:
|
| 379 |
+
if not name.endswith((
|
| 380 |
+
".linear_attn.in_proj_qkv.weight",
|
| 381 |
+
".linear_attn.in_proj_z.weight",
|
| 382 |
+
".linear_attn.in_proj_a.weight",
|
| 383 |
+
".linear_attn.in_proj_b.weight",
|
| 384 |
+
".linear_attn.out_proj.weight",
|
| 385 |
+
)):
|
| 386 |
+
return weight, scale
|
| 387 |
+
|
| 388 |
+
num_k_heads = self.hparams["linear_num_key_heads"]
|
| 389 |
+
num_v_heads = self.hparams["linear_num_value_heads"]
|
| 390 |
+
head_k_dim = self.hparams["linear_key_head_dim"]
|
| 391 |
+
head_v_dim = self.hparams["linear_value_head_dim"]
|
| 392 |
+
num_v_per_k = num_v_heads // num_k_heads
|
| 393 |
+
|
| 394 |
+
def unpack_nibbles(qs: Tensor) -> Tensor:
|
| 395 |
+
lo = torch.bitwise_and(qs, 0x0F)
|
| 396 |
+
hi = torch.bitwise_right_shift(qs, 4)
|
| 397 |
+
return torch.stack((lo, hi), dim=-1).reshape(*qs.shape[:-1], qs.shape[-1] * 2)
|
| 398 |
+
|
| 399 |
+
def pack_nibbles(codes: Tensor) -> Tensor:
|
| 400 |
+
codes = codes.reshape(*codes.shape[:-1], codes.shape[-1] // 2, 2)
|
| 401 |
+
lo = torch.bitwise_and(codes[..., 0], 0x0F)
|
| 402 |
+
hi = torch.bitwise_left_shift(torch.bitwise_and(codes[..., 1], 0x0F), 4)
|
| 403 |
+
return torch.bitwise_or(lo, hi).contiguous()
|
| 404 |
+
|
| 405 |
+
def apply_col_perm(qs: Tensor, scales: Tensor, col_perm: Tensor) -> tuple[Tensor, Tensor]:
|
| 406 |
+
assert qs.ndim >= 2
|
| 407 |
+
assert scales.ndim >= 2
|
| 408 |
+
|
| 409 |
+
k = qs.shape[-1] * 2
|
| 410 |
+
assert col_perm.numel() == k
|
| 411 |
+
assert k % 16 == 0
|
| 412 |
+
|
| 413 |
+
group_cols = col_perm.reshape(-1, 16)
|
| 414 |
+
group_starts = group_cols[:, 0]
|
| 415 |
+
expected = group_starts.unsqueeze(1) + torch.arange(16, dtype=col_perm.dtype)
|
| 416 |
+
assert torch.equal(group_cols, expected)
|
| 417 |
+
assert torch.all(group_starts % 16 == 0)
|
| 418 |
+
|
| 419 |
+
group_perm = (group_starts // 16).to(dtype=torch.long)
|
| 420 |
+
expected_groups = torch.arange(scales.shape[-1], dtype=torch.long)
|
| 421 |
+
assert group_perm.numel() == scales.shape[-1]
|
| 422 |
+
assert torch.equal(torch.sort(group_perm).values, expected_groups)
|
| 423 |
+
|
| 424 |
+
codes = unpack_nibbles(qs)
|
| 425 |
+
codes = codes.index_select(-1, col_perm.to(device=qs.device, dtype=torch.long))
|
| 426 |
+
qs = pack_nibbles(codes)
|
| 427 |
+
scales = scales.index_select(-1, group_perm.to(device=scales.device))
|
| 428 |
+
return qs, scales
|
| 429 |
+
|
| 430 |
+
def reorder_rows(qs: Tensor, scales: Tensor, head_dim: int) -> tuple[Tensor, Tensor]:
|
| 431 |
+
row_perm = self._reorder_v_heads(
|
| 432 |
+
torch.arange(num_v_heads * head_dim, dtype=torch.long).unsqueeze(-1),
|
| 433 |
+
0, num_k_heads, num_v_per_k, head_dim,
|
| 434 |
+
).squeeze(-1)
|
| 435 |
+
return (
|
| 436 |
+
qs.index_select(0, row_perm.to(device=qs.device)),
|
| 437 |
+
scales.index_select(0, row_perm.to(device=scales.device)),
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
if name.endswith(".linear_attn.in_proj_qkv.weight"):
|
| 441 |
+
q_dim = head_k_dim * num_k_heads
|
| 442 |
+
k_dim = head_k_dim * num_k_heads
|
| 443 |
+
q = weight[:q_dim]
|
| 444 |
+
k = weight[q_dim:q_dim + k_dim]
|
| 445 |
+
v = weight[q_dim + k_dim:]
|
| 446 |
+
q_scale = scale[:q_dim]
|
| 447 |
+
k_scale = scale[q_dim:q_dim + k_dim]
|
| 448 |
+
v_scale = scale[q_dim + k_dim:]
|
| 449 |
+
v, v_scale = reorder_rows(v, v_scale, head_v_dim)
|
| 450 |
+
return torch.cat([q, k, v], dim=0), torch.cat([q_scale, k_scale, v_scale], dim=0)
|
| 451 |
+
|
| 452 |
+
if name.endswith(".linear_attn.in_proj_z.weight"):
|
| 453 |
+
weight, scale = reorder_rows(weight, scale, head_v_dim)
|
| 454 |
+
elif name.endswith((".linear_attn.in_proj_a.weight", ".linear_attn.in_proj_b.weight")):
|
| 455 |
+
weight, scale = reorder_rows(weight, scale, 1)
|
| 456 |
+
elif name.endswith(".linear_attn.out_proj.weight"):
|
| 457 |
+
col_perm = self._reorder_v_heads(
|
| 458 |
+
torch.arange(num_v_heads * head_v_dim, dtype=torch.long).unsqueeze(0),
|
| 459 |
+
1, num_k_heads, num_v_per_k, head_v_dim,
|
| 460 |
+
).squeeze(0)
|
| 461 |
+
weight, scale = apply_col_perm(weight, scale, col_perm)
|
| 462 |
+
|
| 463 |
+
return weight, scale
|
| 464 |
+
|
| 465 |
+
def _repack_nvfp4(self, name: str, weight: Tensor, scale: Tensor, scale2: Tensor, input_scale: Tensor):
|
| 466 |
+
weight, scale = self._transform_nvfp4_weight(name, weight, scale)
|
| 467 |
+
super()._repack_nvfp4(name, weight, scale, scale2, input_scale)
|
| 468 |
+
|
| 469 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 470 |
+
num_k_heads = self.hparams.get("linear_num_key_heads", 0)
|
| 471 |
+
num_v_heads = self.hparams.get("linear_num_value_heads", 0)
|
| 472 |
+
|
| 473 |
+
if num_k_heads > 0 and num_v_heads > 0 and num_k_heads != num_v_heads and "linear_attn." in name:
|
| 474 |
+
head_k_dim = self.hparams["linear_key_head_dim"]
|
| 475 |
+
head_v_dim = self.hparams["linear_value_head_dim"]
|
| 476 |
+
num_v_per_k = num_v_heads // num_k_heads
|
| 477 |
+
|
| 478 |
+
if ".in_proj_qkv." in name:
|
| 479 |
+
# QKV weight: reorder only the V rows
|
| 480 |
+
q_dim = head_k_dim * num_k_heads
|
| 481 |
+
k_dim = head_k_dim * num_k_heads
|
| 482 |
+
q = data_torch[:q_dim]
|
| 483 |
+
k = data_torch[q_dim:q_dim + k_dim]
|
| 484 |
+
v = data_torch[q_dim + k_dim:]
|
| 485 |
+
v = self._reorder_v_heads(v, 0, num_k_heads, num_v_per_k, head_v_dim)
|
| 486 |
+
data_torch = torch.cat([q, k, v], dim=0)
|
| 487 |
+
|
| 488 |
+
elif ".in_proj_z." in name:
|
| 489 |
+
# Z gate weight: reorder rows (num_v_heads * head_v_dim)
|
| 490 |
+
data_torch = self._reorder_v_heads(data_torch, 0, num_k_heads, num_v_per_k, head_v_dim)
|
| 491 |
+
|
| 492 |
+
elif ".in_proj_b." in name or ".in_proj_a." in name:
|
| 493 |
+
# Beta/Alpha weight: reorder rows (num_v_heads, head_dim=1)
|
| 494 |
+
data_torch = self._reorder_v_heads(data_torch, 0, num_k_heads, num_v_per_k, 1)
|
| 495 |
+
|
| 496 |
+
elif ".A_log" in name or ".dt_bias" in name or ".dt_proj" in name:
|
| 497 |
+
# A_log / dt_bias: 1D parameters with num_v_heads elements
|
| 498 |
+
if data_torch.ndim == 1:
|
| 499 |
+
data_torch = self._reorder_v_heads(
|
| 500 |
+
data_torch.unsqueeze(-1), 0, num_k_heads, num_v_per_k, 1
|
| 501 |
+
).squeeze(-1)
|
| 502 |
+
else:
|
| 503 |
+
data_torch = self._reorder_v_heads(data_torch, -1, num_k_heads, num_v_per_k, 1)
|
| 504 |
+
|
| 505 |
+
elif ".conv1d" in name:
|
| 506 |
+
# Conv1d kernel: reorder only the V channel portion
|
| 507 |
+
data = data_torch.squeeze()
|
| 508 |
+
qk_channels = head_k_dim * num_k_heads * 2
|
| 509 |
+
qk_part = data[:qk_channels]
|
| 510 |
+
v_part = data[qk_channels:]
|
| 511 |
+
v_part = self._reorder_v_heads(v_part, 0, num_k_heads, num_v_per_k, head_v_dim)
|
| 512 |
+
data_torch = torch.cat([qk_part, v_part], dim=0)
|
| 513 |
+
|
| 514 |
+
elif ".out_proj." in name:
|
| 515 |
+
# Out projection weight: reorder columns (input dimension)
|
| 516 |
+
data_torch = self._reorder_v_heads(data_torch, 1, num_k_heads, num_v_per_k, head_v_dim)
|
| 517 |
+
|
| 518 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
class _Qwen35MRopeMixin:
|
| 522 |
+
# Qwen3.5 always applies interleaved MRoPE (see Qwen3_5RotaryEmbedding in transformers);
|
| 523 |
+
# the upstream default mrope_section is [11, 11, 10] and llama.cpp's QWEN35 / QWEN35MOE
|
| 524 |
+
# loaders treat qwen35.rope.dimension_sections as required, so make sure it is always
|
| 525 |
+
# written even when a particular checkpoint omits the field in `rope_parameters`.
|
| 526 |
+
_QWEN35_DEFAULT_MROPE_SECTION = [11, 11, 10, 0]
|
| 527 |
+
|
| 528 |
+
gguf_writer: gguf.GGUFWriter
|
| 529 |
+
rope_parameters: dict
|
| 530 |
+
|
| 531 |
+
def set_gguf_parameters(self):
|
| 532 |
+
super().set_gguf_parameters() # ty: ignore[unresolved-attribute]
|
| 533 |
+
if "mrope_section" not in self.rope_parameters:
|
| 534 |
+
self.gguf_writer.add_rope_dimension_sections(self._QWEN35_DEFAULT_MROPE_SECTION)
|
| 535 |
+
|
| 536 |
+
|
| 537 |
+
class _Qwen35MtpMixin:
|
| 538 |
+
"""Shared MTP wiring for Qwen3.5/3.6 text variants. The HF config carries
|
| 539 |
+
the MTP block under `mtp_num_hidden_layers` and the tensors under
|
| 540 |
+
`mtp.*`; we extend block_count, emit the nextn metadata key, and remap
|
| 541 |
+
`mtp.*` to the standard layer-indexed nextn naming so the existing
|
| 542 |
+
tensor_map handles them."""
|
| 543 |
+
|
| 544 |
+
hparams: dict[str, Any]
|
| 545 |
+
model_arch: gguf.MODEL_ARCH
|
| 546 |
+
gguf_writer: gguf.GGUFWriter
|
| 547 |
+
block_count: int
|
| 548 |
+
tensor_map: gguf.TensorNameMap
|
| 549 |
+
no_mtp: bool
|
| 550 |
+
mtp_only: bool
|
| 551 |
+
_original_block_count: int | None = None
|
| 552 |
+
|
| 553 |
+
def __init__(self, *args, **kwargs):
|
| 554 |
+
super().__init__(*args, **kwargs)
|
| 555 |
+
self.block_count = self.hparams["num_hidden_layers"]
|
| 556 |
+
if not self.no_mtp:
|
| 557 |
+
self.block_count += self.hparams.get("mtp_num_hidden_layers", 0)
|
| 558 |
+
self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
|
| 559 |
+
|
| 560 |
+
def index_tensors(self, remote_hf_model_id: str | None = None) -> dict[str, Callable[[], Tensor]]:
|
| 561 |
+
hparams = {**self.hparams, **self.hparams.get("text_config", {})}
|
| 562 |
+
key = next((k for k in ["n_layers", "num_hidden_layers", "n_layer", "num_layers"] if k in hparams), None)
|
| 563 |
+
type(self)._original_block_count = hparams.get(key)
|
| 564 |
+
return super().index_tensors(remote_hf_model_id=remote_hf_model_id) # ty: ignore[unresolved-attribute]
|
| 565 |
+
|
| 566 |
+
@classmethod
|
| 567 |
+
def filter_tensors(cls, item):
|
| 568 |
+
assert cls._original_block_count is not None
|
| 569 |
+
# TODO: change TextModel to super()
|
| 570 |
+
if (titem := TextModel.filter_tensors(item)) is None:
|
| 571 |
+
return None
|
| 572 |
+
name, gen = titem
|
| 573 |
+
if name.startswith("model.mtp."):
|
| 574 |
+
name = name.replace("model.", "", 1)
|
| 575 |
+
if name.startswith("mtp."):
|
| 576 |
+
if cls.no_mtp:
|
| 577 |
+
return None
|
| 578 |
+
remapper = {
|
| 579 |
+
"fc": "eh_proj",
|
| 580 |
+
"pre_fc_norm_embedding": "enorm",
|
| 581 |
+
"pre_fc_norm_hidden": "hnorm",
|
| 582 |
+
"norm": "shared_head.norm",
|
| 583 |
+
}
|
| 584 |
+
parts = name.split(".", 3)
|
| 585 |
+
if len(parts) == 4 and parts[1] == "layers" and parts[2].isdecimal():
|
| 586 |
+
mtp_idx = int(parts[2])
|
| 587 |
+
name = f"model.layers.{cls._original_block_count + mtp_idx}.{parts[3]}"
|
| 588 |
+
elif len(parts) == 3 and parts[1] in remapper:
|
| 589 |
+
name = f"model.layers.{cls._original_block_count}.{remapper[parts[1]]}.{parts[2]}"
|
| 590 |
+
elif cls.mtp_only:
|
| 591 |
+
keep = name in (
|
| 592 |
+
"model.embed_tokens.weight", "model.norm.weight", "lm_head.weight",
|
| 593 |
+
"embed_tokens.weight", "norm.weight",
|
| 594 |
+
)
|
| 595 |
+
if not keep:
|
| 596 |
+
return None
|
| 597 |
+
return name, gen
|
| 598 |
+
|
| 599 |
+
def set_gguf_parameters(self):
|
| 600 |
+
super().set_gguf_parameters() # ty: ignore[unresolved-attribute]
|
| 601 |
+
if self.no_mtp:
|
| 602 |
+
return
|
| 603 |
+
if (n := self.hparams.get("mtp_num_hidden_layers", 0)) > 0:
|
| 604 |
+
self.gguf_writer.add_nextn_predict_layers(n)
|
| 605 |
+
|
| 606 |
+
def prepare_metadata(self, vocab_only: bool):
|
| 607 |
+
from_dir = self.fname_out.is_dir()
|
| 608 |
+
super().prepare_metadata(vocab_only=vocab_only) # ty: ignore[unresolved-attribute]
|
| 609 |
+
|
| 610 |
+
if not self.mtp_only or not from_dir:
|
| 611 |
+
return
|
| 612 |
+
|
| 613 |
+
output_type: str = self.ftype.name.partition("_")[2] # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
|
| 614 |
+
fname_default: str = gguf.naming_convention(
|
| 615 |
+
self.metadata.name, self.metadata.basename, self.metadata.finetune, # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
|
| 616 |
+
self.metadata.version, size_label=None, output_type=output_type, model_type=None) # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
|
| 617 |
+
self.fname_out = self.fname_out.parent / f"mtp-{fname_default}.gguf"
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
@ModelBase.register("Qwen3_5ForConditionalGeneration", "Qwen3_5ForCausalLM")
|
| 621 |
+
class Qwen3_5TextModel(_Qwen35MtpMixin, _Qwen35MRopeMixin, _LinearAttentionVReorderBase):
|
| 622 |
+
model_arch = gguf.MODEL_ARCH.QWEN35
|
| 623 |
+
|
| 624 |
+
|
| 625 |
+
@ModelBase.register("Qwen3_5MoeForConditionalGeneration", "Qwen3_5MoeForCausalLM")
|
| 626 |
+
class Qwen3_5MoeTextModel(_Qwen35MtpMixin, _Qwen35MRopeMixin, _LinearAttentionVReorderBase):
|
| 627 |
+
model_arch = gguf.MODEL_ARCH.QWEN35MOE
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
@ModelBase.register("DFlashDraftModel")
|
| 631 |
+
class DFlashModel(Qwen3Model):
|
| 632 |
+
model_arch = gguf.MODEL_ARCH.DFLASH
|
| 633 |
+
|
| 634 |
+
def set_vocab(self):
|
| 635 |
+
if self.target_model_dir is None:
|
| 636 |
+
raise ValueError(
|
| 637 |
+
"DFlash draft model requires --target-model-dir to be specified. "
|
| 638 |
+
"Please provide the path to the target model directory containing the tokenizer."
|
| 639 |
+
)
|
| 640 |
+
logger.info(f"DFlash: Using tokenizer from target model: {self.target_model_dir}")
|
| 641 |
+
original_dir = self.dir_model
|
| 642 |
+
self.dir_model = self.target_model_dir
|
| 643 |
+
super().set_vocab()
|
| 644 |
+
self.dir_model = original_dir
|
| 645 |
+
|
| 646 |
+
mask_token_id = self.hparams.get("dflash_config", {}).get("mask_token_id")
|
| 647 |
+
if mask_token_id is not None:
|
| 648 |
+
self.gguf_writer.add_mask_token_id(mask_token_id)
|
| 649 |
+
|
| 650 |
+
def set_gguf_parameters(self):
|
| 651 |
+
super().set_gguf_parameters()
|
| 652 |
+
|
| 653 |
+
block_size = self.hparams.get("block_size", 16)
|
| 654 |
+
self.gguf_writer.add_block_size(block_size)
|
| 655 |
+
dflash_config = self.hparams.get("dflash_config", {})
|
| 656 |
+
|
| 657 |
+
target_layer_ids = dflash_config.get("target_layer_ids", [])
|
| 658 |
+
if target_layer_ids:
|
| 659 |
+
extract_layer_ids = [i + 1 for i in target_layer_ids]
|
| 660 |
+
self.gguf_writer.add_target_layers(extract_layer_ids)
|
| 661 |
+
|
| 662 |
+
use_sliding_window = self.hparams.get("use_sliding_window", False)
|
| 663 |
+
sliding_window = self.hparams.get("sliding_window")
|
| 664 |
+
layer_types = self.hparams.get("layer_types")
|
| 665 |
+
if use_sliding_window and sliding_window and layer_types:
|
| 666 |
+
is_swa = [lt == "sliding_attention" for lt in layer_types]
|
| 667 |
+
self.gguf_writer.add_sliding_window(sliding_window)
|
| 668 |
+
self.gguf_writer.add_sliding_window_pattern(is_swa)
|
| 669 |
+
|
| 670 |
+
@classmethod
|
| 671 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 672 |
+
name, gen = item
|
| 673 |
+
if not name.startswith("model."):
|
| 674 |
+
name = "model." + name
|
| 675 |
+
return super().filter_tensors((name, gen))
|
llama.cpp/conversion/qwen3vl.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
from typing import Any, Callable, Iterable, TYPE_CHECKING
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING:
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
from .base import MmprojModel, ModelBase, gguf, logger
|
| 11 |
+
|
| 12 |
+
from .qwen import Qwen3Model, Qwen3MoeModel
|
| 13 |
+
from .qwenvl import Qwen25AudioModel
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@ModelBase.register("Qwen3VLForConditionalGeneration", "Qwen3VLMoeForConditionalGeneration", "Qwen3_5ForConditionalGeneration", "Qwen3_5MoeForConditionalGeneration")
|
| 17 |
+
class Qwen3VLVisionModel(MmprojModel):
|
| 18 |
+
def __init__(self, *args, **kwargs):
|
| 19 |
+
super().__init__(*args, **kwargs)
|
| 20 |
+
if self.hparams_vision is None:
|
| 21 |
+
logger.info("No vision config found, skipping vision tensor processing")
|
| 22 |
+
return
|
| 23 |
+
|
| 24 |
+
# Compute image_size if not present
|
| 25 |
+
if "image_size" not in self.hparams_vision:
|
| 26 |
+
# For Qwen3VL/Qwen3VLMoe, compute from num_position_embeddings
|
| 27 |
+
num_pos = self.hparams_vision.get("num_position_embeddings", 2304)
|
| 28 |
+
patch_size = self.hparams_vision.get("patch_size", 16)
|
| 29 |
+
# num_position_embeddings = (image_size / patch_size) ** 2
|
| 30 |
+
# So image_size = sqrt(num_position_embeddings) * patch_size
|
| 31 |
+
image_size = int(num_pos**0.5 * patch_size)
|
| 32 |
+
self.hparams_vision["image_size"] = image_size
|
| 33 |
+
|
| 34 |
+
# Rename config values for compatibility
|
| 35 |
+
self.hparams_vision["num_attention_heads"] = self.hparams_vision.get("num_heads")
|
| 36 |
+
self.hparams_vision["num_hidden_layers"] = self.hparams_vision.get("depth")
|
| 37 |
+
|
| 38 |
+
self.is_deepstack_layers = [False] * int(self.hparams_vision["num_hidden_layers"] or 0)
|
| 39 |
+
for idx in self.hparams_vision.get("deepstack_visual_indexes", []):
|
| 40 |
+
self.is_deepstack_layers[idx] = True
|
| 41 |
+
|
| 42 |
+
def set_gguf_parameters(self):
|
| 43 |
+
super().set_gguf_parameters()
|
| 44 |
+
# in case mixed modalities, the arch will be handled by subclass
|
| 45 |
+
if not self.has_audio_encoder:
|
| 46 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.QWEN3VL)
|
| 47 |
+
self.gguf_writer.add_vision_use_gelu(True)
|
| 48 |
+
|
| 49 |
+
if self.hparams_vision is not None:
|
| 50 |
+
merge_size = self.hparams_vision.get("spatial_merge_size")
|
| 51 |
+
if merge_size is not None:
|
| 52 |
+
self.gguf_writer.add_vision_spatial_merge_size(int(merge_size))
|
| 53 |
+
|
| 54 |
+
# Use text config's rms_norm_eps for vision attention layernorm eps
|
| 55 |
+
rms_norm_eps = self.global_config.get("text_config", {}).get("rms_norm_eps", 1e-6)
|
| 56 |
+
self.gguf_writer.add_vision_attention_layernorm_eps(rms_norm_eps)
|
| 57 |
+
|
| 58 |
+
if self.is_deepstack_layers:
|
| 59 |
+
self.gguf_writer.add_vision_is_deepstack_layers(self.is_deepstack_layers)
|
| 60 |
+
|
| 61 |
+
@classmethod
|
| 62 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 63 |
+
name, gen = item
|
| 64 |
+
|
| 65 |
+
# Skip text model tensors
|
| 66 |
+
if name.startswith("lm_head."):
|
| 67 |
+
return None
|
| 68 |
+
|
| 69 |
+
# Skip MTP tensors
|
| 70 |
+
if name.startswith("mtp."):
|
| 71 |
+
return None
|
| 72 |
+
|
| 73 |
+
if name.startswith("model.visual."):
|
| 74 |
+
name = name.replace("model.visual.", "visual.", 1)
|
| 75 |
+
|
| 76 |
+
if not name.startswith("visual."):
|
| 77 |
+
return None
|
| 78 |
+
|
| 79 |
+
return super().filter_tensors((name, gen))
|
| 80 |
+
|
| 81 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 82 |
+
assert self.hparams_vision is not None
|
| 83 |
+
|
| 84 |
+
if name.startswith("visual.deepstack_merger_list."):
|
| 85 |
+
prefix, rest = name.split(".", maxsplit=3)[2:]
|
| 86 |
+
# prefix is the layer index, convert to absolute clip layer index!
|
| 87 |
+
idx = self.hparams_vision.get("deepstack_visual_indexes", [])[int(prefix)]
|
| 88 |
+
target = rest
|
| 89 |
+
|
| 90 |
+
tensor_type: gguf.MODEL_TENSOR
|
| 91 |
+
if target.startswith("norm."):
|
| 92 |
+
tensor_type = gguf.MODEL_TENSOR.V_DS_NORM
|
| 93 |
+
suffix = target.split(".", 1)[1]
|
| 94 |
+
elif target.startswith("linear_fc1."):
|
| 95 |
+
tensor_type = gguf.MODEL_TENSOR.V_DS_FC1
|
| 96 |
+
suffix = target.split(".", 1)[1]
|
| 97 |
+
elif target.startswith("linear_fc2."):
|
| 98 |
+
tensor_type = gguf.MODEL_TENSOR.V_DS_FC2
|
| 99 |
+
suffix = target.split(".", 1)[1]
|
| 100 |
+
else:
|
| 101 |
+
raise ValueError(f"Unexpected deepstack tensor: {name}")
|
| 102 |
+
|
| 103 |
+
new_name = self.format_tensor_name(tensor_type, idx, suffix=f".{suffix}")
|
| 104 |
+
yield from super().modify_tensors(data_torch, new_name, bid)
|
| 105 |
+
return
|
| 106 |
+
|
| 107 |
+
if name.startswith("visual.merger."):
|
| 108 |
+
suffix = name.split(".", 2)[2]
|
| 109 |
+
if suffix.startswith("linear_fc"):
|
| 110 |
+
fc_idx_str, tail = suffix.split(".", 1)
|
| 111 |
+
fc_num = int(fc_idx_str.replace("linear_fc", ""))
|
| 112 |
+
# Qwen3VL has linear_fc1 and linear_fc2
|
| 113 |
+
# Map to indices 0 and 2 (matching Qwen2VL which uses indices 0 and 2)
|
| 114 |
+
if fc_num == 1:
|
| 115 |
+
fc_idx = 0
|
| 116 |
+
elif fc_num == 2:
|
| 117 |
+
fc_idx = 2
|
| 118 |
+
else:
|
| 119 |
+
raise ValueError(f"unexpected fc index {fc_num} in {name}")
|
| 120 |
+
new_name = self.format_tensor_name(gguf.MODEL_TENSOR.V_MMPROJ, fc_idx, suffix=f".{tail}")
|
| 121 |
+
elif suffix.startswith("norm."):
|
| 122 |
+
new_name = self.format_tensor_name(gguf.MODEL_TENSOR.V_POST_NORM, suffix=f".{suffix.split('.', 1)[1]}")
|
| 123 |
+
else:
|
| 124 |
+
raise ValueError(f"Unexpected merger tensor: {name}")
|
| 125 |
+
yield (new_name, data_torch)
|
| 126 |
+
return
|
| 127 |
+
|
| 128 |
+
if name == "visual.patch_embed.proj.weight":
|
| 129 |
+
# split Conv3D into Conv2Ds along temporal dimension
|
| 130 |
+
c1, c2, kt, _, _ = data_torch.shape
|
| 131 |
+
del c1, c2
|
| 132 |
+
if kt != 2:
|
| 133 |
+
raise ValueError("Current implementation only supports temporal_patch_size of 2")
|
| 134 |
+
yield (gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_ENC_EMBD_PATCH] + ".weight", data_torch[:, :, 0, ...])
|
| 135 |
+
yield (gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_ENC_EMBD_PATCH] + ".weight.1", data_torch[:, :, 1, ...])
|
| 136 |
+
return
|
| 137 |
+
|
| 138 |
+
if name == "visual.patch_embed.proj.bias":
|
| 139 |
+
# Include the bias - it's used by the C++ code
|
| 140 |
+
yield (gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_ENC_EMBD_PATCH] + ".bias", data_torch)
|
| 141 |
+
return
|
| 142 |
+
|
| 143 |
+
yield from MmprojModel.modify_tensors(self, data_torch, name, bid)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
@ModelBase.register("Qwen3OmniMoeForConditionalGeneration")
|
| 147 |
+
class Qwen3OmniMmprojModel(Qwen3VLVisionModel, Qwen25AudioModel):
|
| 148 |
+
has_audio_encoder = True
|
| 149 |
+
has_vision_encoder = True
|
| 150 |
+
|
| 151 |
+
def get_vision_config(self) -> dict[str, Any] | None:
|
| 152 |
+
if self.has_vision_encoder:
|
| 153 |
+
return self.global_config["thinker_config"].get("vision_config")
|
| 154 |
+
else:
|
| 155 |
+
return None
|
| 156 |
+
|
| 157 |
+
def get_audio_config(self) -> dict[str, Any] | None:
|
| 158 |
+
if self.has_audio_encoder:
|
| 159 |
+
return self.global_config["thinker_config"].get("audio_config")
|
| 160 |
+
else:
|
| 161 |
+
return None
|
| 162 |
+
|
| 163 |
+
def set_gguf_parameters(self):
|
| 164 |
+
if self.has_vision_encoder:
|
| 165 |
+
Qwen3VLVisionModel.set_gguf_parameters(self)
|
| 166 |
+
self.gguf_writer.add_clip_vision_projector_type(gguf.VisionProjectorType.QWEN3VL)
|
| 167 |
+
if self.has_audio_encoder:
|
| 168 |
+
Qwen25AudioModel.set_gguf_parameters(self)
|
| 169 |
+
self.gguf_writer.add_clip_audio_projector_type(gguf.VisionProjectorType.QWEN3A)
|
| 170 |
+
|
| 171 |
+
@classmethod
|
| 172 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 173 |
+
name, gen = item
|
| 174 |
+
|
| 175 |
+
# Skip text model tensors
|
| 176 |
+
if name.startswith("lm_head."):
|
| 177 |
+
return None
|
| 178 |
+
|
| 179 |
+
# Skip MTP tensors
|
| 180 |
+
if name.startswith("mtp."):
|
| 181 |
+
return None
|
| 182 |
+
|
| 183 |
+
if name.startswith("model.visual."):
|
| 184 |
+
name = name.replace("model.visual.", "visual.", 1)
|
| 185 |
+
|
| 186 |
+
if name.startswith("thinker.audio_tower."):
|
| 187 |
+
name = name.replace("thinker.audio_tower.", "audio_tower.", 1)
|
| 188 |
+
|
| 189 |
+
if "visual." not in name and "audio_tower." not in name:
|
| 190 |
+
return None
|
| 191 |
+
|
| 192 |
+
return MmprojModel.filter_tensors((name, gen))
|
| 193 |
+
|
| 194 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 195 |
+
if "visual." in name:
|
| 196 |
+
if not self.has_vision_encoder:
|
| 197 |
+
raise ValueError(f"Model does not have vision encoder, but found tensor {name}")
|
| 198 |
+
# need to transform vision tensor naming, so that modify_tensors() logic can be used correctly
|
| 199 |
+
name = name.replace("thinker.visual.", "model.visual.")
|
| 200 |
+
if ".merger_list." in name:
|
| 201 |
+
name = name.replace(".merger_list.", ".deepstack_merger_list.")
|
| 202 |
+
name = name.replace(".ln_q", ".norm")
|
| 203 |
+
name = name.replace(".mlp.0", ".linear_fc1")
|
| 204 |
+
name = name.replace(".mlp.2", ".linear_fc2")
|
| 205 |
+
elif ".merger." in name:
|
| 206 |
+
name = name.replace(".ln_q", ".norm")
|
| 207 |
+
name = name.replace(".mlp.0", ".linear_fc1")
|
| 208 |
+
name = name.replace(".mlp.2", ".linear_fc2")
|
| 209 |
+
yield from Qwen3VLVisionModel.modify_tensors(self, data_torch, name, bid)
|
| 210 |
+
elif "audio_tower." in name:
|
| 211 |
+
if not self.has_audio_encoder:
|
| 212 |
+
raise ValueError(f"Model does not have audio encoder, but found tensor {name}")
|
| 213 |
+
if "conv2d" in name and name.endswith(".bias"):
|
| 214 |
+
# transform conv2d bias [n_embd] --> [1, 1, n_embd]
|
| 215 |
+
data_torch = data_torch.unsqueeze(-1).unsqueeze(-1)
|
| 216 |
+
yield from Qwen25AudioModel.modify_tensors(self, data_torch, name, bid)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
@ModelBase.register("Qwen3ASRForConditionalGeneration")
|
| 220 |
+
class Qwen3ASRMmprojModel(Qwen3OmniMmprojModel):
|
| 221 |
+
has_audio_encoder = True
|
| 222 |
+
has_vision_encoder = False
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
@ModelBase.register("Glm4vForConditionalGeneration", "Glm4vMoeForConditionalGeneration", "GlmOcrForConditionalGeneration")
|
| 226 |
+
class Glm4VVisionModel(Qwen3VLVisionModel):
|
| 227 |
+
def set_gguf_parameters(self):
|
| 228 |
+
MmprojModel.set_gguf_parameters(self) # skip Qwen3VLVisionModel parameters
|
| 229 |
+
assert self.hparams_vision is not None
|
| 230 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.GLM4V)
|
| 231 |
+
|
| 232 |
+
hidden_act = str(self.hparams_vision.get("hidden_act", "")).lower()
|
| 233 |
+
if hidden_act == "gelu":
|
| 234 |
+
self.gguf_writer.add_vision_use_gelu(True)
|
| 235 |
+
elif hidden_act == "silu":
|
| 236 |
+
self.gguf_writer.add_vision_use_silu(True)
|
| 237 |
+
|
| 238 |
+
rms_norm_eps = self.hparams_vision.get("rms_norm_eps", 1e-5)
|
| 239 |
+
self.gguf_writer.add_vision_attention_layernorm_eps(rms_norm_eps)
|
| 240 |
+
|
| 241 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 242 |
+
if name.startswith("visual.merger."):
|
| 243 |
+
yield from ModelBase.modify_tensors(self, data_torch, name, bid)
|
| 244 |
+
return
|
| 245 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
@ModelBase.register("Qwen3VLForConditionalGeneration")
|
| 249 |
+
class Qwen3VLTextModel(Qwen3Model):
|
| 250 |
+
model_arch = gguf.MODEL_ARCH.QWEN3VL
|
| 251 |
+
|
| 252 |
+
def set_gguf_parameters(self):
|
| 253 |
+
super().set_gguf_parameters()
|
| 254 |
+
if "thinker_config" in self.hparams:
|
| 255 |
+
vision_config = self.hparams["thinker_config"].get("vision_config", {})
|
| 256 |
+
else:
|
| 257 |
+
vision_config = self.hparams.get("vision_config", {})
|
| 258 |
+
deepstack_layer_num = len(vision_config.get("deepstack_visual_indexes", []))
|
| 259 |
+
self.gguf_writer.add_num_deepstack_layers(deepstack_layer_num)
|
| 260 |
+
|
| 261 |
+
@classmethod
|
| 262 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 263 |
+
name, gen = item
|
| 264 |
+
|
| 265 |
+
name = name.replace("thinker.", "")
|
| 266 |
+
|
| 267 |
+
return super().filter_tensors((name, gen))
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
@ModelBase.register("Qwen3VLMoeForConditionalGeneration")
|
| 271 |
+
class Qwen3VLMoeTextModel(Qwen3MoeModel):
|
| 272 |
+
model_arch = gguf.MODEL_ARCH.QWEN3VLMOE
|
| 273 |
+
|
| 274 |
+
def set_gguf_parameters(self):
|
| 275 |
+
super().set_gguf_parameters()
|
| 276 |
+
vision_config = self.hparams.get("vision_config", {})
|
| 277 |
+
deepstack_layer_num = len(vision_config.get("deepstack_visual_indexes", []))
|
| 278 |
+
self.gguf_writer.add_num_deepstack_layers(deepstack_layer_num)
|
| 279 |
+
|
| 280 |
+
@classmethod
|
| 281 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 282 |
+
name, gen = item
|
| 283 |
+
|
| 284 |
+
name = name.replace("thinker.", "")
|
| 285 |
+
|
| 286 |
+
return super().filter_tensors((name, gen))
|
| 287 |
+
|
| 288 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 289 |
+
# Qwen3VL has transposed packed tensors, so we treat it differently from general Qwen2MoE packed tensors
|
| 290 |
+
if name.endswith("mlp.experts.down_proj") or name.endswith("mlp.experts.down_proj.weight"):
|
| 291 |
+
mapped = f"{name}.weight" if not name.endswith(".weight") else name
|
| 292 |
+
permuted = data_torch.permute(0, 2, 1).contiguous()
|
| 293 |
+
yield from ModelBase.modify_tensors(self, permuted, mapped, bid)
|
| 294 |
+
return
|
| 295 |
+
|
| 296 |
+
if name.endswith("mlp.experts.gate_up_proj") or name.endswith("mlp.experts.gate_up_proj.weight"):
|
| 297 |
+
if data_torch.ndim < 3 or data_torch.shape[-1] % 2 != 0:
|
| 298 |
+
raise ValueError(f"Unexpected gate_up_proj shape for {name}: {tuple(data_torch.shape)}")
|
| 299 |
+
split_dim = data_torch.shape[-1] // 2
|
| 300 |
+
gate = data_torch[..., :split_dim].contiguous()
|
| 301 |
+
up = data_torch[..., split_dim:].contiguous()
|
| 302 |
+
# Input gate/up: (n_expert=128, n_embd=2048, n_ff_exp=768)
|
| 303 |
+
# Want GGML ne: {n_embd, n_ff_exp, n_expert} = {2048, 768, 128}
|
| 304 |
+
# Need PyTorch: (128, 768, 2048) [reversed of GGML]
|
| 305 |
+
# So: permute(0, 2, 1): (128, 2048, 768) -> (128, 768, 2048)
|
| 306 |
+
base_name = name.removesuffix(".weight")
|
| 307 |
+
base = base_name.rsplit('.', 1)[0]
|
| 308 |
+
mapped_gate = f"{base}.gate_proj.weight"
|
| 309 |
+
mapped_up = f"{base}.up_proj.weight"
|
| 310 |
+
perm_gate = gate.permute(0, 2, 1).contiguous()
|
| 311 |
+
perm_up = up.permute(0, 2, 1).contiguous()
|
| 312 |
+
yield from ModelBase.modify_tensors(self, perm_gate, mapped_gate, bid)
|
| 313 |
+
yield from ModelBase.modify_tensors(self, perm_up, mapped_up, bid)
|
| 314 |
+
return
|
| 315 |
+
|
| 316 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
@ModelBase.register("Qwen3OmniMoeForConditionalGeneration")
|
| 320 |
+
class Qwen3OmniMoeTextModel(Qwen3VLMoeTextModel):
|
| 321 |
+
model_arch = gguf.MODEL_ARCH.QWEN3VLMOE
|
| 322 |
+
|
| 323 |
+
def set_vocab(self):
|
| 324 |
+
super().set_vocab()
|
| 325 |
+
# correct BOS/EOS tokens
|
| 326 |
+
with open(self.dir_model / "tokenizer_config.json", "r", encoding="utf-8") as f:
|
| 327 |
+
tokenizer_config = json.load(f)
|
| 328 |
+
added_tokens = tokenizer_config.get("added_tokens_decoder", {})
|
| 329 |
+
for token_id, data in added_tokens.items():
|
| 330 |
+
if data.get("content") == "<|im_end|>":
|
| 331 |
+
self.gguf_writer.add_bos_token_id(int(token_id))
|
| 332 |
+
self.gguf_writer.add_eos_token_id(int(token_id))
|
| 333 |
+
break
|
| 334 |
+
|
| 335 |
+
def set_gguf_parameters(self):
|
| 336 |
+
super().set_gguf_parameters()
|
| 337 |
+
self.gguf_writer.add_num_deepstack_layers(0)
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
@ModelBase.register("Qwen3ASRForConditionalGeneration")
|
| 341 |
+
class Qwen3ASRTextModel(Qwen3VLTextModel):
|
| 342 |
+
model_arch = gguf.MODEL_ARCH.QWEN3VL
|
| 343 |
+
|
| 344 |
+
def set_gguf_parameters(self):
|
| 345 |
+
super().set_gguf_parameters()
|
| 346 |
+
self.gguf_writer.add_num_deepstack_layers(0)
|
| 347 |
+
|
| 348 |
+
def set_vocab(self):
|
| 349 |
+
super().set_vocab()
|
| 350 |
+
# fix chat template, use correct chatml format
|
| 351 |
+
self.gguf_writer.add_chat_template("{% for message in messages %}{{'<|im_start|>' + message['role'] + '\\n' + message['content'] + '<|im_end|>' + '\\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\\n' }}{% endif %}")
|
| 352 |
+
# correct BOS/EOS tokens
|
| 353 |
+
with open(self.dir_model / "tokenizer_config.json", "r", encoding="utf-8") as f:
|
| 354 |
+
tokenizer_config = json.load(f)
|
| 355 |
+
added_tokens = tokenizer_config.get("added_tokens_decoder", {})
|
| 356 |
+
for token_id, data in added_tokens.items():
|
| 357 |
+
if data.get("content") == "<|im_end|>":
|
| 358 |
+
self.gguf_writer.add_bos_token_id(int(token_id))
|
| 359 |
+
self.gguf_writer.add_eos_token_id(int(token_id))
|
| 360 |
+
break
|
llama.cpp/conversion/qwenvl.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any, Callable, Iterable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
if TYPE_CHECKING:
|
| 9 |
+
from torch import Tensor
|
| 10 |
+
|
| 11 |
+
from .base import MmprojModel, ModelBase, TextModel, gguf
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@ModelBase.register(
|
| 15 |
+
"Qwen2VLModel",
|
| 16 |
+
"Qwen2VLForConditionalGeneration",
|
| 17 |
+
"Qwen2_5_VLForConditionalGeneration",
|
| 18 |
+
"Qwen2_5OmniModel",
|
| 19 |
+
)
|
| 20 |
+
class Qwen2VLModel(TextModel):
|
| 21 |
+
model_arch = gguf.MODEL_ARCH.QWEN2VL
|
| 22 |
+
|
| 23 |
+
def set_gguf_parameters(self):
|
| 24 |
+
super().set_gguf_parameters()
|
| 25 |
+
|
| 26 |
+
def set_vocab(self):
|
| 27 |
+
try:
|
| 28 |
+
self._set_vocab_sentencepiece()
|
| 29 |
+
except FileNotFoundError:
|
| 30 |
+
self._set_vocab_gpt2()
|
| 31 |
+
|
| 32 |
+
@classmethod
|
| 33 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 34 |
+
name, gen = item
|
| 35 |
+
|
| 36 |
+
if name.startswith("thinker."):
|
| 37 |
+
name = name.replace("thinker.", "")
|
| 38 |
+
|
| 39 |
+
return super().filter_tensors((name, gen))
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@ModelBase.register("Qwen2VLModel", "Qwen2VLForConditionalGeneration", "Qwen2_5_VLForConditionalGeneration")
|
| 43 |
+
class Qwen2VLVisionModel(MmprojModel):
|
| 44 |
+
def __init__(self, *args, **kwargs):
|
| 45 |
+
super().__init__(*args, **kwargs)
|
| 46 |
+
assert self.hparams_vision is not None
|
| 47 |
+
self.hparams_vision["image_size"] = self.hparams_vision.get("image_size", 560)
|
| 48 |
+
# rename config.json values
|
| 49 |
+
self.hparams_vision["num_attention_heads"] = self.hparams_vision.get("num_heads")
|
| 50 |
+
self.hparams_vision["num_hidden_layers"] = self.hparams_vision.get("depth")
|
| 51 |
+
if "embed_dim" in self.hparams_vision: # qwen2vl
|
| 52 |
+
self.hparams_vision["intermediate_size"] = self.hparams_vision.get("hidden_size")
|
| 53 |
+
self.hparams_vision["hidden_size"] = self.hparams_vision.get("embed_dim")
|
| 54 |
+
|
| 55 |
+
def set_gguf_parameters(self):
|
| 56 |
+
super().set_gguf_parameters()
|
| 57 |
+
assert self.hparams_vision is not None
|
| 58 |
+
hparams = self.hparams_vision
|
| 59 |
+
model_type = self.global_config['model_type']
|
| 60 |
+
if model_type == 'qwen2_vl':
|
| 61 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.QWEN2VL)
|
| 62 |
+
elif model_type == 'qwen2_5_vl' or model_type == 'qwen2_5_omni':
|
| 63 |
+
if model_type == 'qwen2_5_omni':
|
| 64 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.QWEN25O)
|
| 65 |
+
else:
|
| 66 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.QWEN25VL)
|
| 67 |
+
self.gguf_writer.add_vision_use_silu(True)
|
| 68 |
+
# find n_wa_pattern (window attention pattern)
|
| 69 |
+
fullatt_block_indexes = hparams.get("fullatt_block_indexes")
|
| 70 |
+
assert fullatt_block_indexes is not None, "fullatt_block_indexes is required for qwen2_5_vl"
|
| 71 |
+
n_wa_pattern = fullatt_block_indexes[0] + 1
|
| 72 |
+
# validate n_wa_pattern
|
| 73 |
+
for i in range(1, len(fullatt_block_indexes)):
|
| 74 |
+
if fullatt_block_indexes[i] - fullatt_block_indexes[i - 1] != n_wa_pattern:
|
| 75 |
+
raise ValueError(f"Invalid fullatt_block_indexes: {fullatt_block_indexes}")
|
| 76 |
+
self.gguf_writer.add_vision_n_wa_pattern(n_wa_pattern)
|
| 77 |
+
else:
|
| 78 |
+
raise ValueError(f"Unknown QwenVL model type: {self.global_config['model_type']}")
|
| 79 |
+
# default values below are taken from HF tranformers code
|
| 80 |
+
self.gguf_writer.add_vision_attention_layernorm_eps(self.global_config.get("rms_norm_eps", 1e-6))
|
| 81 |
+
|
| 82 |
+
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
| 83 |
+
if ".position_embd." in new_name:
|
| 84 |
+
return gguf.GGMLQuantizationType.F32
|
| 85 |
+
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
| 86 |
+
|
| 87 |
+
@classmethod
|
| 88 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 89 |
+
name, gen = item
|
| 90 |
+
|
| 91 |
+
if not name.startswith("visual."):
|
| 92 |
+
return None
|
| 93 |
+
|
| 94 |
+
return super().filter_tensors(item)
|
| 95 |
+
|
| 96 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 97 |
+
# split QKV tensors if needed
|
| 98 |
+
if ".qkv." in name:
|
| 99 |
+
if data_torch.ndim == 2: # weight
|
| 100 |
+
c3, _ = data_torch.shape
|
| 101 |
+
else: # bias
|
| 102 |
+
c3 = data_torch.shape[0]
|
| 103 |
+
assert c3 % 3 == 0
|
| 104 |
+
c = c3 // 3
|
| 105 |
+
wq = data_torch[:c]
|
| 106 |
+
wk = data_torch[c: c * 2]
|
| 107 |
+
wv = data_torch[c * 2:]
|
| 108 |
+
yield from super().modify_tensors(wq, name.replace("qkv", "q"), bid)
|
| 109 |
+
yield from super().modify_tensors(wk, name.replace("qkv", "k"), bid)
|
| 110 |
+
yield from super().modify_tensors(wv, name.replace("qkv", "v"), bid)
|
| 111 |
+
elif 'patch_embed.proj.weight' in name:
|
| 112 |
+
# split Conv3D into Conv2Ds
|
| 113 |
+
c1, c2, kt, kh, kw = data_torch.shape
|
| 114 |
+
del c1, c2, kh, kw # unused
|
| 115 |
+
assert kt == 2, "Current implementation only support temporal_patch_size of 2"
|
| 116 |
+
yield (gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_ENC_EMBD_PATCH] + ".weight" , data_torch[:, :, 0, ...])
|
| 117 |
+
yield (gguf.TENSOR_NAMES[gguf.MODEL_TENSOR.V_ENC_EMBD_PATCH] + ".weight.1", data_torch[:, :, 1, ...])
|
| 118 |
+
else:
|
| 119 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class Qwen25AudioModel(MmprojModel):
|
| 123 |
+
has_audio_encoder = True
|
| 124 |
+
|
| 125 |
+
def __init__(self, *args, **kwargs):
|
| 126 |
+
super().__init__(*args, **kwargs)
|
| 127 |
+
assert self.hparams_audio is not None
|
| 128 |
+
self.hparams_audio["hidden_size"] = self.hparams_audio["d_model"]
|
| 129 |
+
self.hparams_audio["intermediate_size"] = self.hparams_audio["encoder_ffn_dim"]
|
| 130 |
+
self.hparams_audio["num_attention_heads"] = self.hparams_audio["encoder_attention_heads"]
|
| 131 |
+
|
| 132 |
+
def set_gguf_parameters(self):
|
| 133 |
+
super().set_gguf_parameters()
|
| 134 |
+
assert self.hparams_audio is not None
|
| 135 |
+
self.gguf_writer.add_audio_num_mel_bins(self.hparams_audio["num_mel_bins"])
|
| 136 |
+
self.gguf_writer.add_audio_attention_layernorm_eps(self.hparams_audio.get("layer_norm_eps", 1e-5))
|
| 137 |
+
|
| 138 |
+
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
|
| 139 |
+
# SinusoidsPositionEmbedding
|
| 140 |
+
assert self.hparams_audio is not None
|
| 141 |
+
max_timescale = 10000
|
| 142 |
+
length = 1500
|
| 143 |
+
channels = self.hparams_audio["hidden_size"]
|
| 144 |
+
log_timescale_increment = np.log(max_timescale) / (channels // 2 - 1)
|
| 145 |
+
inv_timescales = torch.exp(-log_timescale_increment * torch.arange(channels // 2).float())
|
| 146 |
+
scaled_time = torch.arange(length)[:, np.newaxis] * inv_timescales[np.newaxis, :]
|
| 147 |
+
pos_embd = torch.cat([torch.sin(scaled_time), torch.cos(scaled_time)], dim=1).to(dtype=torch.float32)
|
| 148 |
+
yield ("audio_tower.embed_positions.weight", pos_embd)
|
| 149 |
+
|
| 150 |
+
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
| 151 |
+
if ".conv" in name and ".weight" in name:
|
| 152 |
+
return gguf.GGMLQuantizationType.F16
|
| 153 |
+
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
| 154 |
+
|
| 155 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 156 |
+
if "conv1.bias" in name or "conv2.bias" in name:
|
| 157 |
+
# transpose conv1 and conv2 bias
|
| 158 |
+
data_torch = data_torch.unsqueeze(-1)
|
| 159 |
+
|
| 160 |
+
yield from MmprojModel.modify_tensors(self, data_torch, name, bid)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
@ModelBase.register("Qwen2_5OmniModel")
|
| 164 |
+
class Qwen25OmniModel(Qwen2VLVisionModel, Qwen25AudioModel):
|
| 165 |
+
has_audio_encoder = True
|
| 166 |
+
has_vision_encoder = True
|
| 167 |
+
|
| 168 |
+
def get_vision_config(self) -> dict[str, Any] | None:
|
| 169 |
+
return self.global_config["thinker_config"].get("vision_config")
|
| 170 |
+
|
| 171 |
+
def get_audio_config(self) -> dict[str, Any] | None:
|
| 172 |
+
return self.global_config["thinker_config"].get("audio_config")
|
| 173 |
+
|
| 174 |
+
def set_gguf_parameters(self):
|
| 175 |
+
super().set_gguf_parameters()
|
| 176 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.QWEN25O)
|
| 177 |
+
|
| 178 |
+
@classmethod
|
| 179 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 180 |
+
name, gen = item
|
| 181 |
+
|
| 182 |
+
if not name.startswith("visual.") and not name.startswith("audio_tower."):
|
| 183 |
+
return None
|
| 184 |
+
|
| 185 |
+
if name.startswith("thinker."):
|
| 186 |
+
name = name.replace("thinker.", "")
|
| 187 |
+
|
| 188 |
+
if "audio_bos_eos_token" in name:
|
| 189 |
+
# this tensor is left unused in transformers code
|
| 190 |
+
# https://github.com/huggingface/transformers/blob/6e3063422c4b1c014aa60c32b9254fd2902f0f28/src/transformers/models/qwen2_5_omni/modular_qwen2_5_omni.py#L1809
|
| 191 |
+
return None
|
| 192 |
+
|
| 193 |
+
return MmprojModel.filter_tensors((name, gen))
|
| 194 |
+
|
| 195 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 196 |
+
if "visual." in name:
|
| 197 |
+
yield from Qwen2VLVisionModel.modify_tensors(self, data_torch, name, bid)
|
| 198 |
+
elif "audio_tower." in name:
|
| 199 |
+
yield from Qwen25AudioModel.modify_tensors(self, data_torch, name, bid)
|
| 200 |
+
return # skip other tensors
|
llama.cpp/conversion/refact.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Iterable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
if TYPE_CHECKING:
|
| 6 |
+
from torch import Tensor
|
| 7 |
+
|
| 8 |
+
from .base import ModelBase, TextModel, gguf
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@ModelBase.register("GPTRefactForCausalLM")
|
| 12 |
+
class RefactModel(TextModel):
|
| 13 |
+
model_arch = gguf.MODEL_ARCH.REFACT
|
| 14 |
+
|
| 15 |
+
def set_vocab(self):
|
| 16 |
+
super().set_vocab()
|
| 17 |
+
|
| 18 |
+
# TODO: how to determine special FIM tokens automatically?
|
| 19 |
+
special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=False,
|
| 20 |
+
special_token_types = ['prefix', 'suffix', 'middle', 'eot'])
|
| 21 |
+
special_vocab._set_special_token("prefix", 1)
|
| 22 |
+
special_vocab._set_special_token("suffix", 3)
|
| 23 |
+
special_vocab._set_special_token("middle", 2)
|
| 24 |
+
special_vocab.chat_template = None # do not add it twice
|
| 25 |
+
special_vocab.add_to_gguf(self.gguf_writer)
|
| 26 |
+
|
| 27 |
+
def set_gguf_parameters(self):
|
| 28 |
+
hidden_dim = self.hparams["n_embd"]
|
| 29 |
+
inner_dim = 4 * hidden_dim
|
| 30 |
+
hidden_dim = int(2 * inner_dim / 3)
|
| 31 |
+
multiple_of = 256
|
| 32 |
+
ff_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)
|
| 33 |
+
|
| 34 |
+
# refact uses Alibi. So this is from config.json which might be used by training.
|
| 35 |
+
self.gguf_writer.add_context_length(self.hparams["n_positions"])
|
| 36 |
+
self.gguf_writer.add_embedding_length(self.hparams["n_embd"])
|
| 37 |
+
|
| 38 |
+
self.gguf_writer.add_feed_forward_length(ff_dim)
|
| 39 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 40 |
+
self.gguf_writer.add_head_count(self.hparams["n_head"])
|
| 41 |
+
self.gguf_writer.add_head_count_kv(1)
|
| 42 |
+
self.gguf_writer.add_layer_norm_rms_eps(self.hparams["layer_norm_epsilon"])
|
| 43 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 44 |
+
|
| 45 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 46 |
+
hidden_dim = self.hparams["n_embd"]
|
| 47 |
+
inner_dim = 4 * hidden_dim
|
| 48 |
+
hidden_dim = int(2 * inner_dim / 3)
|
| 49 |
+
multiple_of = 256
|
| 50 |
+
ff_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)
|
| 51 |
+
n_head = self.hparams["n_head"]
|
| 52 |
+
n_head_kv = 1
|
| 53 |
+
head_dim = self.hparams["n_embd"] // n_head
|
| 54 |
+
|
| 55 |
+
if bid is not None:
|
| 56 |
+
if name == f"transformer.h.{bid}.attn.kv.weight":
|
| 57 |
+
yield from super().modify_tensors(data_torch[:n_head_kv * head_dim], self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_K, bid), bid)
|
| 58 |
+
yield from super().modify_tensors(data_torch[n_head_kv * head_dim:], self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_V, bid), bid)
|
| 59 |
+
return
|
| 60 |
+
if name == f"transformer.h.{bid}.attn.q.weight":
|
| 61 |
+
yield from super().modify_tensors(data_torch, self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_Q, bid), bid)
|
| 62 |
+
return
|
| 63 |
+
if name == f"transformer.h.{bid}.mlp.gate_up_proj.weight":
|
| 64 |
+
yield from super().modify_tensors(data_torch[:ff_dim], self.format_tensor_name(gguf.MODEL_TENSOR.FFN_GATE, bid), bid)
|
| 65 |
+
yield from super().modify_tensors(data_torch[ff_dim:], self.format_tensor_name(gguf.MODEL_TENSOR.FFN_UP, bid), bid)
|
| 66 |
+
return
|
| 67 |
+
|
| 68 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
llama.cpp/conversion/rwkv.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Callable, Iterable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING:
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
from .base import ModelBase, TextModel, gguf
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@ModelBase.register("Rwkv6ForCausalLM")
|
| 14 |
+
class Rwkv6Model(TextModel):
|
| 15 |
+
model_arch = gguf.MODEL_ARCH.RWKV6
|
| 16 |
+
|
| 17 |
+
def set_vocab(self):
|
| 18 |
+
self._set_vocab_rwkv_world()
|
| 19 |
+
|
| 20 |
+
def set_gguf_parameters(self):
|
| 21 |
+
head_size = self.hparams["head_size"]
|
| 22 |
+
hidden_size = self.hparams["hidden_size"]
|
| 23 |
+
layer_norm_eps = self.hparams["layer_norm_epsilon"]
|
| 24 |
+
rescale_every_n_layers = self.hparams["rescale_every"]
|
| 25 |
+
intermediate_size = self.hparams["intermediate_size"] if self.hparams["intermediate_size"] is not None else int((hidden_size * 3.5) // 32 * 32)
|
| 26 |
+
time_mix_extra_dim = 64 if hidden_size == 4096 else 32
|
| 27 |
+
time_decay_extra_dim = 128 if hidden_size == 4096 else 64
|
| 28 |
+
|
| 29 |
+
# RWKV isn't context limited
|
| 30 |
+
self.gguf_writer.add_context_length(1048576)
|
| 31 |
+
self.gguf_writer.add_embedding_length(hidden_size)
|
| 32 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 33 |
+
self.gguf_writer.add_layer_norm_eps(layer_norm_eps)
|
| 34 |
+
self.gguf_writer.add_rescale_every_n_layers(rescale_every_n_layers)
|
| 35 |
+
self.gguf_writer.add_wkv_head_size(head_size)
|
| 36 |
+
self.gguf_writer.add_time_mix_extra_dim(time_mix_extra_dim)
|
| 37 |
+
self.gguf_writer.add_time_decay_extra_dim(time_decay_extra_dim)
|
| 38 |
+
self.gguf_writer.add_feed_forward_length(intermediate_size)
|
| 39 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 40 |
+
|
| 41 |
+
# required by llama.cpp, unused
|
| 42 |
+
self.gguf_writer.add_head_count(0)
|
| 43 |
+
|
| 44 |
+
lerp_weights: dict[int, dict[str, Tensor]] = {}
|
| 45 |
+
|
| 46 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 47 |
+
new_name = self.map_tensor_name(name)
|
| 48 |
+
|
| 49 |
+
if not (new_name.endswith(".weight") or new_name.endswith(".bias")):
|
| 50 |
+
new_name += ".weight"
|
| 51 |
+
|
| 52 |
+
if new_name.endswith("time_mix_w1.weight") or new_name.endswith("time_mix_decay_w1.weight") or new_name.endswith("time_mix_decay_w2.weight"):
|
| 53 |
+
data_torch = data_torch.transpose(0, 1)
|
| 54 |
+
|
| 55 |
+
if new_name.endswith("time_mix_w2.weight"):
|
| 56 |
+
data_torch = data_torch.permute(0, 2, 1)
|
| 57 |
+
|
| 58 |
+
if new_name.endswith("time_mix_decay.weight") or "lerp" in new_name:
|
| 59 |
+
data_torch = data_torch.squeeze()
|
| 60 |
+
|
| 61 |
+
try:
|
| 62 |
+
rescale_every_n_layers = self.hparams["rescale_every"]
|
| 63 |
+
if rescale_every_n_layers > 0:
|
| 64 |
+
if new_name.endswith("time_mix_output.weight") or new_name.endswith("channel_mix_value.weight"):
|
| 65 |
+
data_torch = data_torch.div_(2 ** int(bid // rescale_every_n_layers))
|
| 66 |
+
except KeyError:
|
| 67 |
+
pass
|
| 68 |
+
|
| 69 |
+
# concat time_mix_lerp weights to reduce some cpu overhead
|
| 70 |
+
# also reduces the number of tensors in the model
|
| 71 |
+
if bid is not None and "time_mix_lerp" in new_name and "time_mix_lerp_x" not in new_name:
|
| 72 |
+
try:
|
| 73 |
+
self.lerp_weights[bid][new_name] = data_torch
|
| 74 |
+
except KeyError:
|
| 75 |
+
self.lerp_weights[bid] = {new_name: data_torch}
|
| 76 |
+
if all(f"blk.{bid}.time_mix_lerp_{i}.weight" in self.lerp_weights[bid].keys() for i in ["w", "k", "v", "r", "g"]):
|
| 77 |
+
new_name = f"blk.{bid}.time_mix_lerp_fused.weight"
|
| 78 |
+
data = torch.stack([self.lerp_weights[bid][f"blk.{bid}.time_mix_lerp_{i}.weight"].unsqueeze(0) for i in ["w", "k", "v", "r", "g"]], dim=0).unsqueeze(1)
|
| 79 |
+
yield (new_name, data)
|
| 80 |
+
return
|
| 81 |
+
|
| 82 |
+
yield (new_name, data_torch)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@ModelBase.register("RWKV6Qwen2ForCausalLM")
|
| 86 |
+
class RWKV6Qwen2Model(Rwkv6Model):
|
| 87 |
+
model_arch = gguf.MODEL_ARCH.RWKV6QWEN2
|
| 88 |
+
|
| 89 |
+
def set_vocab(self):
|
| 90 |
+
try:
|
| 91 |
+
self._set_vocab_sentencepiece()
|
| 92 |
+
except FileNotFoundError:
|
| 93 |
+
self._set_vocab_gpt2()
|
| 94 |
+
|
| 95 |
+
def set_gguf_parameters(self):
|
| 96 |
+
num_attention_heads = self.hparams["num_attention_heads"]
|
| 97 |
+
num_key_value_heads = self.hparams["num_key_value_heads"]
|
| 98 |
+
hidden_size = self.hparams["hidden_size"]
|
| 99 |
+
head_size = hidden_size // num_attention_heads
|
| 100 |
+
rms_norm_eps = self.hparams["rms_norm_eps"]
|
| 101 |
+
intermediate_size = self.hparams["intermediate_size"]
|
| 102 |
+
time_mix_extra_dim = self.hparams.get("lora_rank_tokenshift", 64 if hidden_size >= 4096 else 32)
|
| 103 |
+
time_decay_extra_dim = self.hparams.get("lora_rank_decay", 128 if hidden_size >= 4096 else 64)
|
| 104 |
+
|
| 105 |
+
# RWKV isn't context limited
|
| 106 |
+
self.gguf_writer.add_context_length(1048576)
|
| 107 |
+
self.gguf_writer.add_embedding_length(hidden_size)
|
| 108 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 109 |
+
self.gguf_writer.add_wkv_head_size(head_size)
|
| 110 |
+
self.gguf_writer.add_time_mix_extra_dim(time_mix_extra_dim)
|
| 111 |
+
self.gguf_writer.add_time_decay_extra_dim(time_decay_extra_dim)
|
| 112 |
+
self.gguf_writer.add_feed_forward_length(intermediate_size)
|
| 113 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 114 |
+
|
| 115 |
+
# special parameters for time_mixing in RWKV6QWEN2
|
| 116 |
+
self.gguf_writer.add_layer_norm_rms_eps(rms_norm_eps)
|
| 117 |
+
self.gguf_writer.add_token_shift_count(1)
|
| 118 |
+
# RWKV6QWEN2 use grouped key/value like GQA
|
| 119 |
+
self.gguf_writer.add_head_count_kv(num_key_value_heads)
|
| 120 |
+
|
| 121 |
+
# required by llama.cpp, unused
|
| 122 |
+
self.gguf_writer.add_head_count(0)
|
| 123 |
+
|
| 124 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 125 |
+
for new_name, data in super().modify_tensors(data_torch, name, bid):
|
| 126 |
+
if "time_mix_w1" in new_name or "time_mix_w2" in new_name:
|
| 127 |
+
data = data.view(5, -1, data.shape[-1])
|
| 128 |
+
# rwkv6qwen2 has a different order of rkvwg instead of the original wkvrg
|
| 129 |
+
# permute them here to avoid code changes
|
| 130 |
+
data = torch.stack([data[3], data[1], data[2], data[0], data[4]], dim=0).view(-1, data.shape[-1])
|
| 131 |
+
if "w2" in new_name:
|
| 132 |
+
data = data.view(5, -1, data.shape[-1])
|
| 133 |
+
yield (new_name, data)
|
| 134 |
+
continue
|
| 135 |
+
yield (new_name, data)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
@ModelBase.register("Rwkv7ForCausalLM", "RWKV7ForCausalLM")
|
| 139 |
+
class Rwkv7Model(TextModel):
|
| 140 |
+
model_arch = gguf.MODEL_ARCH.RWKV7
|
| 141 |
+
|
| 142 |
+
def set_vocab(self):
|
| 143 |
+
self._set_vocab_rwkv_world()
|
| 144 |
+
|
| 145 |
+
def calc_lora_rank(self, hidden_size, exponent, multiplier):
|
| 146 |
+
return max(1, round(hidden_size ** exponent * multiplier / 32)) * 32
|
| 147 |
+
|
| 148 |
+
def set_gguf_parameters(self):
|
| 149 |
+
try:
|
| 150 |
+
head_size = self.hparams["head_size"]
|
| 151 |
+
layer_norm_eps = self.hparams["layer_norm_epsilon"]
|
| 152 |
+
except KeyError:
|
| 153 |
+
head_size = self.hparams["head_dim"]
|
| 154 |
+
layer_norm_eps = self.hparams["norm_eps"]
|
| 155 |
+
hidden_size = self.hparams["hidden_size"]
|
| 156 |
+
intermediate_size = self.hparams["intermediate_size"] if self.hparams["intermediate_size"] is not None else (hidden_size * 4)
|
| 157 |
+
|
| 158 |
+
# ICLR: In-Context-Learning-Rate
|
| 159 |
+
try:
|
| 160 |
+
lora_rank_decay = self.hparams["lora_rank_decay"] if self.hparams["lora_rank_decay"] is not None else self.calc_lora_rank(hidden_size, 0.5, 1.8)
|
| 161 |
+
lora_rank_iclr = self.hparams["lora_rank_iclr"] if self.hparams["lora_rank_iclr"] is not None else self.calc_lora_rank(hidden_size, 0.5, 1.8)
|
| 162 |
+
lora_rank_value_residual_mix = self.hparams["lora_rank_value_residual_mix"] if self.hparams["lora_rank_value_residual_mix"] is not None else self.calc_lora_rank(hidden_size, 0.5, 1.3)
|
| 163 |
+
lora_rank_gate = self.hparams["lora_rank_gate"] if self.hparams["lora_rank_gate"] is not None else self.calc_lora_rank(hidden_size, 0.8, 0.6)
|
| 164 |
+
except KeyError:
|
| 165 |
+
lora_rank_decay = self.hparams["decay_low_rank_dim"] if self.hparams["decay_low_rank_dim"] is not None else self.calc_lora_rank(hidden_size, 0.5, 1.8)
|
| 166 |
+
lora_rank_iclr = self.hparams["a_low_rank_dim"] if self.hparams["a_low_rank_dim"] is not None else self.calc_lora_rank(hidden_size, 0.5, 1.8)
|
| 167 |
+
lora_rank_value_residual_mix = self.hparams["v_low_rank_dim"] if self.hparams["v_low_rank_dim"] is not None else self.calc_lora_rank(hidden_size, 0.5, 1.3)
|
| 168 |
+
lora_rank_gate = self.hparams["gate_low_rank_dim"] if self.hparams["gate_low_rank_dim"] is not None else self.calc_lora_rank(hidden_size, 0.8, 0.6)
|
| 169 |
+
|
| 170 |
+
# RWKV isn't context limited
|
| 171 |
+
self.gguf_writer.add_context_length(1048576)
|
| 172 |
+
self.gguf_writer.add_embedding_length(hidden_size)
|
| 173 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 174 |
+
self.gguf_writer.add_layer_norm_eps(layer_norm_eps)
|
| 175 |
+
self.gguf_writer.add_wkv_head_size(head_size)
|
| 176 |
+
self.gguf_writer.add_decay_lora_rank(lora_rank_decay)
|
| 177 |
+
self.gguf_writer.add_iclr_lora_rank(lora_rank_iclr)
|
| 178 |
+
self.gguf_writer.add_value_residual_mix_lora_rank(lora_rank_value_residual_mix)
|
| 179 |
+
self.gguf_writer.add_gate_lora_rank(lora_rank_gate)
|
| 180 |
+
self.gguf_writer.add_feed_forward_length(intermediate_size)
|
| 181 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 182 |
+
|
| 183 |
+
# required by llama.cpp, unused
|
| 184 |
+
self.gguf_writer.add_head_count(0)
|
| 185 |
+
|
| 186 |
+
lerp_weights: dict[int, dict[str, Tensor]] = {}
|
| 187 |
+
lora_needs_transpose: bool = True
|
| 188 |
+
|
| 189 |
+
@classmethod
|
| 190 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 191 |
+
name, gen = item
|
| 192 |
+
|
| 193 |
+
# unify tensor names here to make life easier
|
| 194 |
+
name = name.replace("blocks", "layers").replace("ffn", "feed_forward")
|
| 195 |
+
name = name.replace("self_attn", "attention").replace("attn", "attention")
|
| 196 |
+
name = name.replace("time_mixer.", "")
|
| 197 |
+
|
| 198 |
+
name = name.replace("feed_forward_norm", "ln2")
|
| 199 |
+
name = name.replace("g_norm", "ln_x")
|
| 200 |
+
|
| 201 |
+
return super().filter_tensors((name, gen))
|
| 202 |
+
|
| 203 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 204 |
+
# lora layer names in fla-hub's impl
|
| 205 |
+
if "_lora.lora" in name:
|
| 206 |
+
self.lora_needs_transpose = False
|
| 207 |
+
name = name.replace("_lora.lora.0.weight", "1.weight")
|
| 208 |
+
name = name.replace("_lora.lora.2.weight", "2.weight")
|
| 209 |
+
name = name.replace("_lora.lora.2.bias", "0.weight")
|
| 210 |
+
|
| 211 |
+
if "attention.v" in name and "value" not in self.map_tensor_name(name) and bid == 0:
|
| 212 |
+
# some models have dummy v0/v1/v2 on first layer while others don't
|
| 213 |
+
# ignore them all since they are not used
|
| 214 |
+
return
|
| 215 |
+
|
| 216 |
+
wkv_has_gate = self.hparams.get("wkv_has_gate", True)
|
| 217 |
+
lerp_list = ["r", "w", "k", "v", "a", "g"] if wkv_has_gate else ["r", "w", "k", "v", "a"]
|
| 218 |
+
|
| 219 |
+
if bid is not None and "attention.x_" in name:
|
| 220 |
+
if "attention.x_x" in name:
|
| 221 |
+
# already concatenated
|
| 222 |
+
new_name = f"blk.{bid}.time_mix_lerp_fused.weight"
|
| 223 |
+
data = data_torch.reshape(len(lerp_list), 1, 1, -1)
|
| 224 |
+
yield (new_name, data)
|
| 225 |
+
else:
|
| 226 |
+
try:
|
| 227 |
+
self.lerp_weights[bid][name] = data_torch
|
| 228 |
+
except KeyError:
|
| 229 |
+
self.lerp_weights[bid] = {name: data_torch}
|
| 230 |
+
if all(f"model.layers.{bid}.attention.x_{i}" in self.lerp_weights[bid].keys() for i in lerp_list):
|
| 231 |
+
new_name = f"blk.{bid}.time_mix_lerp_fused.weight"
|
| 232 |
+
data = torch.stack([self.lerp_weights[bid][f"model.layers.{bid}.attention.x_{i}"] for i in lerp_list], dim=0)
|
| 233 |
+
yield (new_name, data)
|
| 234 |
+
return
|
| 235 |
+
else:
|
| 236 |
+
data_torch = data_torch.squeeze()
|
| 237 |
+
new_name = self.map_tensor_name(name)
|
| 238 |
+
|
| 239 |
+
if not (new_name.endswith(".weight") or new_name.endswith(".bias")):
|
| 240 |
+
new_name += ".weight"
|
| 241 |
+
|
| 242 |
+
if self.lora_needs_transpose and any(
|
| 243 |
+
new_name.endswith(t) for t in [
|
| 244 |
+
"time_mix_w1.weight", "time_mix_w2.weight",
|
| 245 |
+
"time_mix_a1.weight", "time_mix_a2.weight",
|
| 246 |
+
"time_mix_v1.weight", "time_mix_v2.weight",
|
| 247 |
+
"time_mix_g1.weight", "time_mix_g2.weight",
|
| 248 |
+
]
|
| 249 |
+
):
|
| 250 |
+
data_torch = data_torch.transpose(0, 1)
|
| 251 |
+
|
| 252 |
+
if 'r_k' in new_name:
|
| 253 |
+
data_torch = data_torch.flatten()
|
| 254 |
+
|
| 255 |
+
if bid == 0 and "time_mix_a" in new_name:
|
| 256 |
+
# dummy v0/v1/v2 on first layer
|
| 257 |
+
# easiest way to make llama happy
|
| 258 |
+
yield (new_name.replace("time_mix_a", "time_mix_v"), data_torch)
|
| 259 |
+
|
| 260 |
+
yield (new_name, data_torch)
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
@ModelBase.register("RwkvHybridForCausalLM")
|
| 264 |
+
class ARwkv7Model(Rwkv7Model):
|
| 265 |
+
model_arch = gguf.MODEL_ARCH.ARWKV7
|
| 266 |
+
|
| 267 |
+
def set_vocab(self):
|
| 268 |
+
try:
|
| 269 |
+
self._set_vocab_sentencepiece()
|
| 270 |
+
except FileNotFoundError:
|
| 271 |
+
self._set_vocab_gpt2()
|
| 272 |
+
|
| 273 |
+
def set_gguf_parameters(self):
|
| 274 |
+
hidden_size = self.hparams["hidden_size"]
|
| 275 |
+
head_size = self.hparams["head_size"]
|
| 276 |
+
rms_norm_eps = self.hparams["rms_norm_eps"]
|
| 277 |
+
intermediate_size = self.hparams["intermediate_size"]
|
| 278 |
+
wkv_has_gate = self.hparams["wkv_has_gate"]
|
| 279 |
+
assert self.hparams["wkv_version"] == 7
|
| 280 |
+
|
| 281 |
+
# ICLR: In-Context-Learning-Rate
|
| 282 |
+
lora_rank_decay = 64
|
| 283 |
+
lora_rank_iclr = 64
|
| 284 |
+
lora_rank_value_residual_mix = 32
|
| 285 |
+
lora_rank_gate = 128 if wkv_has_gate else 0
|
| 286 |
+
|
| 287 |
+
# RWKV isn't context limited
|
| 288 |
+
self.gguf_writer.add_context_length(1048576)
|
| 289 |
+
self.gguf_writer.add_embedding_length(hidden_size)
|
| 290 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 291 |
+
self.gguf_writer.add_layer_norm_rms_eps(rms_norm_eps)
|
| 292 |
+
self.gguf_writer.add_wkv_head_size(head_size)
|
| 293 |
+
self.gguf_writer.add_decay_lora_rank(lora_rank_decay)
|
| 294 |
+
self.gguf_writer.add_iclr_lora_rank(lora_rank_iclr)
|
| 295 |
+
self.gguf_writer.add_value_residual_mix_lora_rank(lora_rank_value_residual_mix)
|
| 296 |
+
self.gguf_writer.add_gate_lora_rank(lora_rank_gate)
|
| 297 |
+
self.gguf_writer.add_feed_forward_length(intermediate_size)
|
| 298 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 299 |
+
self.gguf_writer.add_token_shift_count(1)
|
| 300 |
+
|
| 301 |
+
# required by llama.cpp, unused
|
| 302 |
+
self.gguf_writer.add_head_count(0)
|
llama.cpp/conversion/sarashina2.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Callable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
if TYPE_CHECKING:
|
| 6 |
+
from torch import Tensor
|
| 7 |
+
|
| 8 |
+
from .base import ModelBase, gguf
|
| 9 |
+
|
| 10 |
+
from .llama import LlamaModel
|
| 11 |
+
from .qwenvl import Qwen2VLVisionModel
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@ModelBase.register("Sarashina2VisionForCausalLM")
|
| 15 |
+
class Sarashina2VLTextModel(LlamaModel):
|
| 16 |
+
model_arch = gguf.MODEL_ARCH.LLAMA
|
| 17 |
+
|
| 18 |
+
@classmethod
|
| 19 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 20 |
+
name, gen = item
|
| 21 |
+
if name.startswith("llm."):
|
| 22 |
+
name = name.replace("llm.", "", 1)
|
| 23 |
+
elif name.startswith("norm."):
|
| 24 |
+
return None
|
| 25 |
+
return super().filter_tensors((name, gen))
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@ModelBase.register("Sarashina2VisionForCausalLM")
|
| 29 |
+
class Sarashina2VLVisionModel(Qwen2VLVisionModel):
|
| 30 |
+
def __init__(self, *args, **kwargs):
|
| 31 |
+
super().__init__(*args, **kwargs)
|
| 32 |
+
self.global_config['model_type'] = "qwen2_vl"
|
llama.cpp/conversion/smallthinker.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Iterable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING:
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
from .base import ModelBase, TextModel, gguf, logger
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@ModelBase.register("SmallThinkerForCausalLM")
|
| 14 |
+
class SmallThinkerModel(TextModel):
|
| 15 |
+
model_arch = gguf.MODEL_ARCH.SMALLTHINKER
|
| 16 |
+
|
| 17 |
+
def set_gguf_parameters(self):
|
| 18 |
+
super().set_gguf_parameters()
|
| 19 |
+
if (n_experts := self.hparams.get("moe_num_primary_experts")) is not None:
|
| 20 |
+
self.gguf_writer.add_expert_count(n_experts)
|
| 21 |
+
if (n_experts_used := self.hparams.get("moe_num_active_primary_experts")) is not None:
|
| 22 |
+
self.gguf_writer.add_expert_used_count(n_experts_used)
|
| 23 |
+
if (moe_intermediate_size := self.hparams.get("moe_ffn_hidden_size")) is not None:
|
| 24 |
+
self.gguf_writer.add_expert_feed_forward_length(moe_intermediate_size)
|
| 25 |
+
self.gguf_writer.add_feed_forward_length(moe_intermediate_size)
|
| 26 |
+
logger.info(f"gguf: expert feed forward length = {moe_intermediate_size}")
|
| 27 |
+
if (self.hparams.get('moe_primary_router_apply_softmax')):
|
| 28 |
+
self.gguf_writer.add_expert_gating_func(gguf.ExpertGatingFuncType.SOFTMAX)
|
| 29 |
+
else:
|
| 30 |
+
self.gguf_writer.add_expert_gating_func(gguf.ExpertGatingFuncType.SIGMOID)
|
| 31 |
+
|
| 32 |
+
sliding_window_layout = self.hparams.get("sliding_window_layout")
|
| 33 |
+
if sliding_window_layout:
|
| 34 |
+
for i in sliding_window_layout:
|
| 35 |
+
if i != 0:
|
| 36 |
+
sliding_window = self.hparams.get("sliding_window_size")
|
| 37 |
+
if sliding_window:
|
| 38 |
+
self.gguf_writer.add_sliding_window(sliding_window)
|
| 39 |
+
break
|
| 40 |
+
|
| 41 |
+
_experts: list[dict[str, Tensor]] | None = None
|
| 42 |
+
|
| 43 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 44 |
+
# process the experts separately
|
| 45 |
+
if name.find("experts") != -1:
|
| 46 |
+
n_experts = self.hparams.get("moe_num_primary_experts") or self.find_hparam(["num_local_experts", "num_experts"])
|
| 47 |
+
assert bid is not None
|
| 48 |
+
|
| 49 |
+
if self._experts is None:
|
| 50 |
+
self._experts = [{} for _ in range(self.block_count)]
|
| 51 |
+
|
| 52 |
+
self._experts[bid][name] = data_torch
|
| 53 |
+
|
| 54 |
+
if len(self._experts[bid]) >= n_experts * 3:
|
| 55 |
+
# merge the experts into a single 3d tensor
|
| 56 |
+
for w_name in ["down", "gate", "up"]:
|
| 57 |
+
datas: list[Tensor] = []
|
| 58 |
+
|
| 59 |
+
for xid in range(n_experts):
|
| 60 |
+
ename = f"model.layers.{bid}.block_sparse_moe.experts.{xid}.{w_name}.weight"
|
| 61 |
+
datas.append(self._experts[bid][ename])
|
| 62 |
+
del self._experts[bid][ename]
|
| 63 |
+
|
| 64 |
+
data_torch = torch.stack(datas, dim=0)
|
| 65 |
+
|
| 66 |
+
merged_name = f"model.layers.{bid}.block_sparse_moe.experts.{w_name}.weight"
|
| 67 |
+
|
| 68 |
+
yield from super().modify_tensors(data_torch, merged_name, bid)
|
| 69 |
+
return
|
| 70 |
+
else:
|
| 71 |
+
return
|
| 72 |
+
|
| 73 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 74 |
+
|
| 75 |
+
def prepare_tensors(self):
|
| 76 |
+
super().prepare_tensors()
|
| 77 |
+
|
| 78 |
+
if self._experts is not None:
|
| 79 |
+
# flatten `list[dict[str, Tensor]]` into `list[str]`
|
| 80 |
+
experts = [k for d in self._experts for k in d.keys()]
|
| 81 |
+
if len(experts) > 0:
|
| 82 |
+
raise ValueError(f"Unprocessed experts: {experts}")
|
llama.cpp/conversion/smolvlm.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Callable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
if TYPE_CHECKING:
|
| 6 |
+
from torch import Tensor
|
| 7 |
+
|
| 8 |
+
from .base import MmprojModel, ModelBase, gguf
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@ModelBase.register("Idefics3ForConditionalGeneration", "SmolVLMForConditionalGeneration")
|
| 12 |
+
class SmolVLMModel(MmprojModel):
|
| 13 |
+
def __init__(self, *args, **kwargs):
|
| 14 |
+
super().__init__(*args, **kwargs)
|
| 15 |
+
if self.hparams["model_type"] == "smolvlm_vision":
|
| 16 |
+
# fix for SmolVLM2, missing some keys in config.json
|
| 17 |
+
# default values are taken from transformers code
|
| 18 |
+
self.hparams["hidden_size"] = self.hparams.get("hidden_size", 1152)
|
| 19 |
+
self.hparams["num_attention_heads"] = self.hparams.get("num_attention_heads", 16)
|
| 20 |
+
self.hparams["intermediate_size"] = self.hparams.get("intermediate_size", 3072)
|
| 21 |
+
|
| 22 |
+
def set_gguf_parameters(self):
|
| 23 |
+
super().set_gguf_parameters()
|
| 24 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.IDEFICS3)
|
| 25 |
+
self.gguf_writer.add_vision_attention_layernorm_eps(self.hparams.get("layer_norm_eps", 1e-5))
|
| 26 |
+
self.gguf_writer.add_vision_projector_scale_factor(self.global_config.get("scale_factor", 2))
|
| 27 |
+
self.gguf_writer.add_vision_use_gelu(True)
|
| 28 |
+
|
| 29 |
+
# Add the preprocessor longest edge size
|
| 30 |
+
preproc_image_size = self.preprocessor_config.get("size", {}).get("longest_edge", self.image_size)
|
| 31 |
+
self.gguf_writer.add_vision_preproc_image_size(preproc_image_size)
|
| 32 |
+
|
| 33 |
+
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
| 34 |
+
if ".embeddings." in name:
|
| 35 |
+
return gguf.GGMLQuantizationType.F32
|
| 36 |
+
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
| 37 |
+
|
| 38 |
+
@classmethod
|
| 39 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 40 |
+
name, gen = item
|
| 41 |
+
|
| 42 |
+
is_vision_tensor = "vision_tower" in name or "vision_model" in name or "model.connector" in name
|
| 43 |
+
|
| 44 |
+
if not is_vision_tensor:
|
| 45 |
+
return None
|
| 46 |
+
|
| 47 |
+
return super().filter_tensors(item)
|
llama.cpp/conversion/stablelm.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Iterable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING:
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
from .base import ModelBase, TextModel, gguf
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@ModelBase.register("StableLmForCausalLM", "StableLMEpochForCausalLM", "LlavaStableLMEpochForCausalLM")
|
| 14 |
+
class StableLMModel(TextModel):
|
| 15 |
+
model_arch = gguf.MODEL_ARCH.STABLELM
|
| 16 |
+
|
| 17 |
+
def set_vocab(self):
|
| 18 |
+
if (self.dir_model / "tokenizer.json").is_file():
|
| 19 |
+
self._set_vocab_gpt2()
|
| 20 |
+
else:
|
| 21 |
+
# StableLM 2 1.6B used to have a vocab in a similar format to Qwen's vocab
|
| 22 |
+
self._set_vocab_qwen()
|
| 23 |
+
|
| 24 |
+
def set_gguf_parameters(self):
|
| 25 |
+
hparams = self.hparams
|
| 26 |
+
|
| 27 |
+
self.gguf_writer.add_context_length(hparams["max_position_embeddings"])
|
| 28 |
+
self.gguf_writer.add_embedding_length(hparams["hidden_size"])
|
| 29 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 30 |
+
self.gguf_writer.add_feed_forward_length(hparams["intermediate_size"])
|
| 31 |
+
rotary_factor = self.rope_parameters["partial_rotary_factor"]
|
| 32 |
+
self.gguf_writer.add_rope_dimension_count(int(rotary_factor * (hparams["hidden_size"] // hparams["num_attention_heads"])))
|
| 33 |
+
self.gguf_writer.add_head_count(hparams["num_attention_heads"])
|
| 34 |
+
self.gguf_writer.add_head_count_kv(hparams["num_key_value_heads"])
|
| 35 |
+
self.gguf_writer.add_parallel_residual(hparams["use_parallel_residual"] if "use_parallel_residual" in hparams else True)
|
| 36 |
+
self.gguf_writer.add_layer_norm_eps(self.find_hparam(["layer_norm_eps", "norm_eps"]))
|
| 37 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 38 |
+
|
| 39 |
+
_q_norms: list[dict[str, Tensor]] | None = None
|
| 40 |
+
_k_norms: list[dict[str, Tensor]] | None = None
|
| 41 |
+
|
| 42 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 43 |
+
n_head = self.hparams["num_attention_heads"]
|
| 44 |
+
n_kv_head = self.hparams["num_key_value_heads"]
|
| 45 |
+
|
| 46 |
+
if name.find("q_layernorm.norms") != -1:
|
| 47 |
+
assert bid is not None
|
| 48 |
+
|
| 49 |
+
if self._q_norms is None:
|
| 50 |
+
self._q_norms = [{} for _ in range(self.block_count)]
|
| 51 |
+
|
| 52 |
+
self._q_norms[bid][name] = data_torch
|
| 53 |
+
|
| 54 |
+
if len(self._q_norms[bid]) >= n_head:
|
| 55 |
+
return self._stack_qk_norm(bid, n_head, self._q_norms[bid], "q_layernorm")
|
| 56 |
+
else:
|
| 57 |
+
return
|
| 58 |
+
|
| 59 |
+
if name.find("k_layernorm.norms") != -1:
|
| 60 |
+
assert bid is not None
|
| 61 |
+
|
| 62 |
+
if self._k_norms is None:
|
| 63 |
+
self._k_norms = [{} for _ in range(self.block_count)]
|
| 64 |
+
|
| 65 |
+
self._k_norms[bid][name] = data_torch
|
| 66 |
+
|
| 67 |
+
if len(self._k_norms[bid]) >= n_kv_head:
|
| 68 |
+
return self._stack_qk_norm(bid, n_kv_head, self._k_norms[bid], "k_layernorm")
|
| 69 |
+
else:
|
| 70 |
+
return
|
| 71 |
+
|
| 72 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 73 |
+
|
| 74 |
+
def _stack_qk_norm(self, bid: int, n_head: int, norms: dict[str, Tensor], layer_name: str = "q_layernorm"):
|
| 75 |
+
datas: list[Tensor] = []
|
| 76 |
+
# extract the norms in order
|
| 77 |
+
for xid in range(n_head):
|
| 78 |
+
ename = f"model.layers.{bid}.self_attn.{layer_name}.norms.{xid}.weight"
|
| 79 |
+
datas.append(norms[ename])
|
| 80 |
+
del norms[ename]
|
| 81 |
+
data_torch = torch.stack(datas, dim=0)
|
| 82 |
+
|
| 83 |
+
merged_name = f"model.layers.{bid}.self_attn.{layer_name}.weight"
|
| 84 |
+
|
| 85 |
+
yield from super().modify_tensors(data_torch, merged_name, bid)
|
| 86 |
+
|
| 87 |
+
def prepare_tensors(self):
|
| 88 |
+
super().prepare_tensors()
|
| 89 |
+
|
| 90 |
+
if self._q_norms is not None or self._k_norms is not None:
|
| 91 |
+
# flatten two `list[dict[str, Tensor]]` into a single `list[str]`
|
| 92 |
+
norms = (
|
| 93 |
+
[k for d in self._q_norms for k in d.keys()] if self._q_norms is not None else []
|
| 94 |
+
) + (
|
| 95 |
+
[k for d in self._k_norms for k in d.keys()] if self._k_norms is not None else []
|
| 96 |
+
)
|
| 97 |
+
if len(norms) > 0:
|
| 98 |
+
raise ValueError(f"Unprocessed norms: {norms}")
|
llama.cpp/conversion/starcoder.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from .base import ModelBase, TextModel, gguf
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
@ModelBase.register("GPTBigCodeForCausalLM")
|
| 7 |
+
class StarCoderModel(TextModel):
|
| 8 |
+
model_arch = gguf.MODEL_ARCH.STARCODER
|
| 9 |
+
|
| 10 |
+
def set_gguf_parameters(self):
|
| 11 |
+
self.gguf_writer.add_context_length(self.hparams["n_positions"])
|
| 12 |
+
self.gguf_writer.add_embedding_length(self.hparams["n_embd"])
|
| 13 |
+
self.gguf_writer.add_feed_forward_length(4 * self.hparams["n_embd"])
|
| 14 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 15 |
+
self.gguf_writer.add_head_count(self.hparams["n_head"])
|
| 16 |
+
self.gguf_writer.add_head_count_kv(1)
|
| 17 |
+
self.gguf_writer.add_layer_norm_eps(self.hparams["layer_norm_epsilon"])
|
| 18 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@ModelBase.register("Starcoder2ForCausalLM")
|
| 22 |
+
class StarCoder2Model(TextModel):
|
| 23 |
+
model_arch = gguf.MODEL_ARCH.STARCODER2
|
llama.cpp/conversion/step3.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
import re
|
| 5 |
+
|
| 6 |
+
from typing import Callable, Iterable, TYPE_CHECKING
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
if TYPE_CHECKING:
|
| 11 |
+
from torch import Tensor
|
| 12 |
+
|
| 13 |
+
from .base import MmprojModel, ModelBase, TextModel, _MISTRAL_COMMON_DATASET_MEAN, _MISTRAL_COMMON_DATASET_STD, gguf
|
| 14 |
+
|
| 15 |
+
from .qwen import Qwen3Model
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@ModelBase.register("StepVLForConditionalGeneration", "Step3p7ForConditionalGeneration")
|
| 19 |
+
class Step3VLVisionModel(MmprojModel):
|
| 20 |
+
def __init__(self, *args, **kwargs):
|
| 21 |
+
super().__init__(*args, **kwargs)
|
| 22 |
+
assert self.hparams_vision is not None
|
| 23 |
+
|
| 24 |
+
if not self.hparams_vision.get("intermediate_size"):
|
| 25 |
+
hidden_size = self.hparams_vision.get("hidden_size") or self.hparams_vision.get("width") or 0
|
| 26 |
+
assert hidden_size > 0
|
| 27 |
+
mlp_ratio = float(self.hparams_vision.get("mlp_ratio", 8960 / 1536))
|
| 28 |
+
self.hparams_vision["intermediate_size"] = int(round(hidden_size * mlp_ratio))
|
| 29 |
+
|
| 30 |
+
self.preprocessor_config.setdefault("image_mean", list(_MISTRAL_COMMON_DATASET_MEAN))
|
| 31 |
+
self.preprocessor_config.setdefault("image_std", list(_MISTRAL_COMMON_DATASET_STD))
|
| 32 |
+
|
| 33 |
+
def set_gguf_parameters(self):
|
| 34 |
+
super().set_gguf_parameters()
|
| 35 |
+
assert self.hparams_vision is not None
|
| 36 |
+
|
| 37 |
+
projector_stride = int(self.global_config.get("understand_projector_stride", -1))
|
| 38 |
+
hidden_size = int(self.hparams_vision.get("hidden_size", self.hparams_vision.get("width", -1)))
|
| 39 |
+
num_layers = int(self.hparams_vision.get("num_hidden_layers", self.hparams_vision.get("layers", -1)))
|
| 40 |
+
assert (projector_stride, int(self.hparams_vision.get("image_size", -1)), hidden_size, num_layers) == (2, 728, 1536, 47), (
|
| 41 |
+
"current Step3-VL conversion path is only validated for Step3-VL-10B"
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.STEP3VL)
|
| 45 |
+
self.gguf_writer.add_vision_attention_layernorm_eps(float(self.hparams_vision.get("layer_norm_eps", 1e-5)))
|
| 46 |
+
self.gguf_writer.add_vision_projector_scale_factor(projector_stride ** 2)
|
| 47 |
+
# 3024 max resize comes from step3-vl-10b processing_step3.py.
|
| 48 |
+
self.gguf_writer.add_vision_preproc_image_size(3024)
|
| 49 |
+
|
| 50 |
+
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
| 51 |
+
if ".position_embd." in new_name:
|
| 52 |
+
return gguf.GGMLQuantizationType.F32
|
| 53 |
+
if ("mm.0." in new_name or "mm.1." in new_name) and new_name.endswith(".weight"):
|
| 54 |
+
return gguf.GGMLQuantizationType.F16 if self.ftype == gguf.LlamaFileType.MOSTLY_F16 else gguf.GGMLQuantizationType.F32
|
| 55 |
+
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
| 56 |
+
|
| 57 |
+
@classmethod
|
| 58 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 59 |
+
name, gen = item
|
| 60 |
+
|
| 61 |
+
if name.startswith(("model.", "lm_head.")):
|
| 62 |
+
return None
|
| 63 |
+
|
| 64 |
+
return super().filter_tensors(item)
|
| 65 |
+
|
| 66 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 67 |
+
if name.startswith("vision_model.vit_downsampler"):
|
| 68 |
+
match = re.match(r"vision_model\.vit_downsampler(\d+)\.(weight|bias)", name)
|
| 69 |
+
if match is None:
|
| 70 |
+
raise ValueError(f"Unexpected Step3-VL projector tensor {name!r}")
|
| 71 |
+
|
| 72 |
+
proj_id = int(match.group(1)) - 1
|
| 73 |
+
suffix = f".{match.group(2)}"
|
| 74 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.V_MMPROJ, proj_id, suffix=suffix), data_torch)
|
| 75 |
+
return
|
| 76 |
+
|
| 77 |
+
if name == "vit_large_projector.weight":
|
| 78 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.V_MMPROJ_FC), data_torch)
|
| 79 |
+
return
|
| 80 |
+
|
| 81 |
+
if name.startswith("vision_model."):
|
| 82 |
+
if name == "vision_model.positional_embedding":
|
| 83 |
+
name += ".weight"
|
| 84 |
+
elif name.endswith(".gamma") and ".ls_" in name:
|
| 85 |
+
name = name.removesuffix(".gamma") + ".weight"
|
| 86 |
+
|
| 87 |
+
name = name.replace("attn.in_proj_weight", "attn.in_proj.weight")
|
| 88 |
+
name = name.replace("attn.in_proj_bias", "attn.in_proj.bias")
|
| 89 |
+
|
| 90 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
@ModelBase.register("StepVLForConditionalGeneration")
|
| 94 |
+
class Step3VLTextModel(Qwen3Model):
|
| 95 |
+
model_arch = gguf.MODEL_ARCH.QWEN3
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@ModelBase.register("Step3p5ForCausalLM", "Step3p7ForConditionalGeneration")
|
| 99 |
+
class Step35Model(TextModel):
|
| 100 |
+
model_arch = gguf.MODEL_ARCH.STEP35
|
| 101 |
+
|
| 102 |
+
# The --mtp / --no-mtp toggles are ModelBase.mtp_only / no_mtp (set in
|
| 103 |
+
# convert_hf_to_gguf.py main()). Unlike Qwen3.5, which stores MTP under a
|
| 104 |
+
# `mtp.*` namespace, Step3.5 appends MTP layers at
|
| 105 |
+
# `model.layers.{num_hidden_layers + i}`, so we filter them by layer index.
|
| 106 |
+
# The trunk layer count is captured before indexing so the classmethod
|
| 107 |
+
# filter_tensors can tell the appended MTP block(s) apart from the trunk.
|
| 108 |
+
_n_main_layers: int | None = None
|
| 109 |
+
|
| 110 |
+
def __init__(self, *args, **kwargs):
|
| 111 |
+
super().__init__(*args, **kwargs)
|
| 112 |
+
# NextN/MTP layers are appended past num_hidden_layers; extend the
|
| 113 |
+
# tensor map to cover them so the MTP block's tensors get correctly
|
| 114 |
+
# indexed names. When --no-mtp drops the MTP blocks, fall back to the
|
| 115 |
+
# base num_hidden_layers so we don't reserve unused slots.
|
| 116 |
+
n_nextn = int(self.hparams.get("num_nextn_predict_layers", 0))
|
| 117 |
+
if n_nextn > 0 and not self.no_mtp:
|
| 118 |
+
self.block_count += n_nextn
|
| 119 |
+
self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
|
| 120 |
+
|
| 121 |
+
def index_tensors(self, remote_hf_model_id: str | None = None):
|
| 122 |
+
# filter_tensors is a classmethod and can't reach self.hparams; stash
|
| 123 |
+
# the trunk layer count here (before indexing runs) so it can detect
|
| 124 |
+
# the appended MTP layers by index.
|
| 125 |
+
hparams = {**self.hparams, **self.hparams.get("text_config", {})}
|
| 126 |
+
key = next((k for k in ["n_layers", "num_hidden_layers", "n_layer", "num_layers"] if k in hparams), None)
|
| 127 |
+
type(self)._n_main_layers = hparams.get(key)
|
| 128 |
+
return super().index_tensors(remote_hf_model_id=remote_hf_model_id)
|
| 129 |
+
|
| 130 |
+
def set_gguf_parameters(self):
|
| 131 |
+
rope_theta = self.hparams.get("rope_theta")
|
| 132 |
+
if isinstance(rope_theta, list):
|
| 133 |
+
self.hparams["rope_theta"] = float(rope_theta[0])
|
| 134 |
+
self.hparams["local_rope_theta"] = float(rope_theta[1])
|
| 135 |
+
self.rope_parameters["rope_theta"] = self.hparams["rope_theta"]
|
| 136 |
+
self.rope_parameters["sliding_attention"] = {"rope_theta": self.hparams["local_rope_theta"]}
|
| 137 |
+
|
| 138 |
+
super().set_gguf_parameters()
|
| 139 |
+
|
| 140 |
+
layer_types = self.hparams.get("layer_types") or []
|
| 141 |
+
partial_rotary_factors = self.hparams.get("partial_rotary_factors") or []
|
| 142 |
+
attn_other = self.hparams.get("attention_other_setting") or {}
|
| 143 |
+
|
| 144 |
+
n_head_base = self.hparams["num_attention_heads"]
|
| 145 |
+
n_kv_base = self.hparams["num_attention_groups"]
|
| 146 |
+
|
| 147 |
+
n_head_swa = attn_other.get("num_attention_heads", n_head_base)
|
| 148 |
+
n_kv_swa = attn_other.get("num_attention_groups", n_kv_base)
|
| 149 |
+
|
| 150 |
+
n_nextn = int(self.hparams.get("num_nextn_predict_layers", 0))
|
| 151 |
+
|
| 152 |
+
# The Step3p5 HF checkpoint stores layer_types/partial_rotary_factors
|
| 153 |
+
# entries for the MTP blocks past num_hidden_layers; preserve them so
|
| 154 |
+
# the MTP layer's attention shape, SWA flag, and partial RoPE dim are
|
| 155 |
+
# set correctly. Pad with full-attention defaults if the checkpoint
|
| 156 |
+
# truncated them.
|
| 157 |
+
def _pad(arr, n, default):
|
| 158 |
+
arr = list(arr)
|
| 159 |
+
if len(arr) < n:
|
| 160 |
+
arr = arr + [default] * (n - len(arr))
|
| 161 |
+
return arr[:n]
|
| 162 |
+
|
| 163 |
+
layer_types = _pad(layer_types, self.block_count, "full_attention")
|
| 164 |
+
partial_rotary_factors = _pad(
|
| 165 |
+
partial_rotary_factors,
|
| 166 |
+
self.block_count,
|
| 167 |
+
0.5, # full_attention default for Step3p5
|
| 168 |
+
)
|
| 169 |
+
assert [1.0 if lt == "sliding_attention" else 0.5 for lt in layer_types] == partial_rotary_factors
|
| 170 |
+
head_arr = [n_head_swa if lt == "sliding_attention" else n_head_base for lt in layer_types]
|
| 171 |
+
kv_arr = [n_kv_swa if lt == "sliding_attention" else n_kv_base for lt in layer_types]
|
| 172 |
+
swa_pat = [lt == "sliding_attention" for lt in layer_types]
|
| 173 |
+
|
| 174 |
+
self.gguf_writer.add_head_count(head_arr)
|
| 175 |
+
self.gguf_writer.add_head_count_kv(kv_arr)
|
| 176 |
+
|
| 177 |
+
self.gguf_writer.add_sliding_window(self.hparams["sliding_window"])
|
| 178 |
+
self.gguf_writer.add_sliding_window_pattern(swa_pat)
|
| 179 |
+
|
| 180 |
+
self.gguf_writer.add_value_length(self.hparams["head_dim"])
|
| 181 |
+
|
| 182 |
+
# MoE params
|
| 183 |
+
self.gguf_writer.add_expert_count(self.hparams["moe_num_experts"])
|
| 184 |
+
self.gguf_writer.add_expert_used_count(self.hparams["moe_top_k"])
|
| 185 |
+
self.gguf_writer.add_expert_feed_forward_length(self.hparams["moe_intermediate_size"])
|
| 186 |
+
self.gguf_writer.add_expert_shared_feed_forward_length(self.hparams["share_expert_dim"])
|
| 187 |
+
|
| 188 |
+
if (moe_router_scaling_factor := self.hparams.get("moe_router_scaling_factor")) is not None:
|
| 189 |
+
self.gguf_writer.add_expert_weights_scale(moe_router_scaling_factor)
|
| 190 |
+
if (norm_expert_weight := self.hparams.get("norm_expert_weight")) is not None:
|
| 191 |
+
self.gguf_writer.add_expert_weights_norm(norm_expert_weight)
|
| 192 |
+
|
| 193 |
+
# leading dense blocks
|
| 194 |
+
leading_dense = 0
|
| 195 |
+
moe_layers_enum = self.hparams.get("moe_layers_enum")
|
| 196 |
+
if isinstance(moe_layers_enum, str) and moe_layers_enum.strip():
|
| 197 |
+
moe_layers = sorted(int(i) for i in moe_layers_enum.strip().split(","))
|
| 198 |
+
if moe_layers:
|
| 199 |
+
leading_dense = max(0, moe_layers[0])
|
| 200 |
+
self.gguf_writer.add_leading_dense_block_count(leading_dense)
|
| 201 |
+
self.gguf_writer.add_moe_every_n_layers(int(self.hparams.get("moe_every_n_layer", 1)))
|
| 202 |
+
|
| 203 |
+
self.gguf_writer.add_layer_norm_rms_eps(self.hparams.get("rms_norm_eps", 1e-5))
|
| 204 |
+
|
| 205 |
+
# Optional per-layer SwiGLU clamps. MTP layers default to no clamping (0.0).
|
| 206 |
+
if (limits := self.hparams.get("swiglu_limits")) is not None:
|
| 207 |
+
limits_f = _pad(
|
| 208 |
+
[0.0 if v is None else float(v) for v in limits],
|
| 209 |
+
self.block_count,
|
| 210 |
+
0.0,
|
| 211 |
+
)
|
| 212 |
+
self.gguf_writer.add_swiglu_clamp_exp(limits_f)
|
| 213 |
+
if (limits_shared := self.hparams.get("swiglu_limits_shared")) is not None:
|
| 214 |
+
limits_shared_f = _pad(
|
| 215 |
+
[0.0 if v is None else float(v) for v in limits_shared],
|
| 216 |
+
self.block_count,
|
| 217 |
+
0.0,
|
| 218 |
+
)
|
| 219 |
+
self.gguf_writer.add_swiglu_clamp_shexp(limits_shared_f)
|
| 220 |
+
|
| 221 |
+
if n_nextn > 0 and not self.no_mtp:
|
| 222 |
+
self.gguf_writer.add_nextn_predict_layers(n_nextn)
|
| 223 |
+
|
| 224 |
+
@classmethod
|
| 225 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 226 |
+
if (titem := super().filter_tensors(item)) is None:
|
| 227 |
+
return None
|
| 228 |
+
name, gen = titem
|
| 229 |
+
|
| 230 |
+
# Map router bias (expert selection bias) to a GGUF bias tensor
|
| 231 |
+
if name.endswith(".moe.router_bias"):
|
| 232 |
+
name += ".bias"
|
| 233 |
+
|
| 234 |
+
# Step3.5 appends the MTP block(s) past num_hidden_layers.
|
| 235 |
+
assert cls._n_main_layers is not None
|
| 236 |
+
is_mtp = (m := re.match(r"model\.layers\.(\d+)\.", name)) is not None and int(m.group(1)) >= cls._n_main_layers
|
| 237 |
+
|
| 238 |
+
# --no-mtp: drop the appended MTP block(s) entirely.
|
| 239 |
+
if is_mtp and cls.no_mtp:
|
| 240 |
+
return None
|
| 241 |
+
# --mtp: keep ONLY MTP-block tensors plus the shared embeddings/norm/
|
| 242 |
+
# lm_head (so the resulting GGUF carries just the draft head).
|
| 243 |
+
if cls.mtp_only and not is_mtp and name not in (
|
| 244 |
+
"model.embed_tokens.weight", "model.norm.weight", "lm_head.weight",
|
| 245 |
+
):
|
| 246 |
+
return None
|
| 247 |
+
|
| 248 |
+
# The checkpoint nests the per-MTP-layer shared head under
|
| 249 |
+
# `model.layers.{N+i}.transformer.shared_head.{norm,output}.weight`;
|
| 250 |
+
# strip the `transformer.` infix and rename `output` → `head` so the
|
| 251 |
+
# existing NEXTN_SHARED_HEAD_{NORM,HEAD} tensor mapping picks them up.
|
| 252 |
+
# Mirrors vllm's `_rewrite_spec_layer_name` (step3p5_mtp.py).
|
| 253 |
+
if is_mtp:
|
| 254 |
+
name = name.replace(".transformer.", ".")
|
| 255 |
+
name = name.replace("shared_head.output", "shared_head.head")
|
| 256 |
+
|
| 257 |
+
return name, gen
|
| 258 |
+
|
| 259 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None):
|
| 260 |
+
if name.endswith("norm.weight"):
|
| 261 |
+
data_torch += 1.0
|
| 262 |
+
|
| 263 |
+
if name.endswith((".self_attn.g_proj.weight", ".moe.gate.weight", ".moe.up_proj.weight", ".moe.gate_proj.weight", ".moe.down_proj.weight")):
|
| 264 |
+
data_torch = data_torch.squeeze().contiguous()
|
| 265 |
+
|
| 266 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 267 |
+
|
| 268 |
+
def prepare_metadata(self, vocab_only: bool):
|
| 269 |
+
from_dir = self.fname_out.is_dir()
|
| 270 |
+
super().prepare_metadata(vocab_only=vocab_only)
|
| 271 |
+
|
| 272 |
+
# Mirror Qwen3.5's behavior: when emitting a draft-only file into a
|
| 273 |
+
# directory, prefix with "mtp-" so it doesn't collide with the trunk.
|
| 274 |
+
if not self.mtp_only or not from_dir:
|
| 275 |
+
return
|
| 276 |
+
|
| 277 |
+
output_type: str = self.ftype.name.partition("_")[2]
|
| 278 |
+
fname_default: str = gguf.naming_convention(
|
| 279 |
+
self.metadata.name, self.metadata.basename, self.metadata.finetune,
|
| 280 |
+
self.metadata.version, size_label=None, output_type=output_type, model_type=None)
|
| 281 |
+
self.fname_out = self.fname_out.parent / f"mtp-{fname_default}.gguf"
|
| 282 |
+
|
| 283 |
+
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
|
| 284 |
+
# Step35 can optionally use Llama-3 style RoPE scaling (HF: rope_scaling.rope_type == "llama3").
|
| 285 |
+
# llama.cpp represents this via a single extra tensor: "rope_freqs.weight" (aka MODEL_TENSOR.ROPE_FREQS).
|
| 286 |
+
rope_params = self.rope_parameters.get("full_attention", self.rope_parameters)
|
| 287 |
+
rope_type = rope_params.get("rope_type") or ""
|
| 288 |
+
if rope_type.lower() != "llama3":
|
| 289 |
+
return
|
| 290 |
+
|
| 291 |
+
# Step35 configs can carry per-layer rope_theta as a list; for llama3 rope factors we use the base value.
|
| 292 |
+
rope_theta = self.hparams.get("rope_theta", 10000.0)
|
| 293 |
+
if isinstance(rope_theta, list):
|
| 294 |
+
rope_theta = rope_theta[0]
|
| 295 |
+
base = float(rope_theta)
|
| 296 |
+
|
| 297 |
+
if (storage_dim := self.hparams.get("head_dim")) is None:
|
| 298 |
+
storage_dim = self.hparams["hidden_size"] // self.hparams["num_attention_heads"]
|
| 299 |
+
storage_dim = int(storage_dim)
|
| 300 |
+
|
| 301 |
+
# Llama 3 factors apply only to the rotary dims used by full_attention layers
|
| 302 |
+
# (partial_rotary_factor * head_dim). Remaining slots are padded with 1.0 so
|
| 303 |
+
# sliding_attention layers remain unaffected. set_gguf_parameters already
|
| 304 |
+
# guarantees at least one full_attention layer.
|
| 305 |
+
layer_types = (self.hparams.get("layer_types") or [])[: self.block_count]
|
| 306 |
+
partial_rotary_factors = (self.hparams.get("partial_rotary_factors") or [])[: self.block_count]
|
| 307 |
+
full_attention_factor = next(
|
| 308 |
+
float(f) for lt, f in zip(layer_types, partial_rotary_factors) if lt == "full_attention"
|
| 309 |
+
)
|
| 310 |
+
rotary_dim = int(storage_dim * full_attention_factor)
|
| 311 |
+
|
| 312 |
+
freqs = 1.0 / (base ** (torch.arange(0, rotary_dim, 2, dtype=torch.float32) / rotary_dim))
|
| 313 |
+
|
| 314 |
+
factor = float(rope_params.get("factor", 8.0))
|
| 315 |
+
low_freq_factor = float(rope_params.get("low_freq_factor", 1.0))
|
| 316 |
+
high_freq_factor = float(rope_params.get("high_freq_factor", 4.0))
|
| 317 |
+
old_context_len = int(rope_params.get("original_max_position_embeddings", 8192))
|
| 318 |
+
|
| 319 |
+
low_freq_wavelen = old_context_len / low_freq_factor
|
| 320 |
+
high_freq_wavelen = old_context_len / high_freq_factor
|
| 321 |
+
|
| 322 |
+
rope_factors: list[float] = []
|
| 323 |
+
for freq in freqs:
|
| 324 |
+
wavelen = 2 * math.pi / float(freq)
|
| 325 |
+
if wavelen < high_freq_wavelen:
|
| 326 |
+
rope_factors.append(1.0)
|
| 327 |
+
elif wavelen > low_freq_wavelen:
|
| 328 |
+
rope_factors.append(factor)
|
| 329 |
+
else:
|
| 330 |
+
smooth = (old_context_len / wavelen - low_freq_factor) / (high_freq_factor - low_freq_factor)
|
| 331 |
+
rope_factors.append(1.0 / ((1.0 - smooth) / factor + smooth))
|
| 332 |
+
|
| 333 |
+
# Pad to head_dim/2 with 1.0 so non-scaled layers remain neutral.
|
| 334 |
+
if len(rope_factors) < storage_dim // 2:
|
| 335 |
+
rope_factors.extend([1.0] * (storage_dim // 2 - len(rope_factors)))
|
| 336 |
+
|
| 337 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FREQS), torch.tensor(rope_factors, dtype=torch.float32))
|
llama.cpp/conversion/t5.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
from typing import Iterable, TYPE_CHECKING
|
| 7 |
+
|
| 8 |
+
if TYPE_CHECKING:
|
| 9 |
+
from torch import Tensor
|
| 10 |
+
|
| 11 |
+
from .base import ModelBase, SentencePieceTokenTypes, TextModel, gguf, logger
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@ModelBase.register("T5WithLMHeadModel")
|
| 15 |
+
@ModelBase.register("T5ForConditionalGeneration")
|
| 16 |
+
@ModelBase.register("MT5ForConditionalGeneration")
|
| 17 |
+
@ModelBase.register("UMT5ForConditionalGeneration")
|
| 18 |
+
@ModelBase.register("UMT5Model")
|
| 19 |
+
class T5Model(TextModel):
|
| 20 |
+
model_arch = gguf.MODEL_ARCH.T5
|
| 21 |
+
|
| 22 |
+
def __init__(self, *args, **kwargs):
|
| 23 |
+
super().__init__(*args, **kwargs)
|
| 24 |
+
self.shared_token_embeddings_found = False
|
| 25 |
+
|
| 26 |
+
def set_vocab(self):
|
| 27 |
+
# to avoid TypeError: Descriptors cannot be created directly
|
| 28 |
+
# exception when importing sentencepiece_model_pb2
|
| 29 |
+
os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python"
|
| 30 |
+
from sentencepiece import SentencePieceProcessor
|
| 31 |
+
from sentencepiece import sentencepiece_model_pb2 as model
|
| 32 |
+
|
| 33 |
+
tokenizer_path = self.dir_model / 'tokenizer.model'
|
| 34 |
+
|
| 35 |
+
# many older models use spiece.model tokenizer model filename
|
| 36 |
+
if not tokenizer_path.is_file():
|
| 37 |
+
tokenizer_path = self.dir_model / 'spiece.model'
|
| 38 |
+
|
| 39 |
+
if not tokenizer_path.is_file():
|
| 40 |
+
raise FileNotFoundError(f"File not found: {tokenizer_path}")
|
| 41 |
+
|
| 42 |
+
sentencepiece_model = model.ModelProto() # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
|
| 43 |
+
sentencepiece_model.ParseFromString(open(tokenizer_path, "rb").read())
|
| 44 |
+
|
| 45 |
+
# some models like Pile-T5 family use BPE tokenizer instead of Unigram
|
| 46 |
+
if sentencepiece_model.trainer_spec.model_type == 2: # BPE
|
| 47 |
+
# assure the tokenizer model file name is correct
|
| 48 |
+
assert tokenizer_path.name == 'tokenizer.model'
|
| 49 |
+
return self._set_vocab_sentencepiece()
|
| 50 |
+
else:
|
| 51 |
+
assert sentencepiece_model.trainer_spec.model_type == 1 # UNIGRAM
|
| 52 |
+
|
| 53 |
+
add_prefix = sentencepiece_model.normalizer_spec.add_dummy_prefix
|
| 54 |
+
remove_whitespaces = sentencepiece_model.normalizer_spec.remove_extra_whitespaces
|
| 55 |
+
precompiled_charsmap = sentencepiece_model.normalizer_spec.precompiled_charsmap
|
| 56 |
+
|
| 57 |
+
tokenizer = SentencePieceProcessor()
|
| 58 |
+
tokenizer.LoadFromFile(str(tokenizer_path))
|
| 59 |
+
|
| 60 |
+
vocab_size = self.hparams.get('vocab_size', tokenizer.vocab_size())
|
| 61 |
+
|
| 62 |
+
tokens: list[bytes] = [f"[PAD{i}]".encode("utf-8") for i in range(vocab_size)]
|
| 63 |
+
scores: list[float] = [-10000.0] * vocab_size
|
| 64 |
+
toktypes: list[int] = [SentencePieceTokenTypes.UNUSED] * vocab_size
|
| 65 |
+
|
| 66 |
+
for token_id in range(tokenizer.vocab_size()):
|
| 67 |
+
piece = tokenizer.IdToPiece(token_id)
|
| 68 |
+
text = piece.encode("utf-8")
|
| 69 |
+
score = tokenizer.GetScore(token_id)
|
| 70 |
+
|
| 71 |
+
toktype = SentencePieceTokenTypes.NORMAL
|
| 72 |
+
if tokenizer.IsUnknown(token_id):
|
| 73 |
+
toktype = SentencePieceTokenTypes.UNKNOWN
|
| 74 |
+
elif tokenizer.IsControl(token_id):
|
| 75 |
+
toktype = SentencePieceTokenTypes.CONTROL
|
| 76 |
+
elif tokenizer.IsUnused(token_id):
|
| 77 |
+
toktype = SentencePieceTokenTypes.UNUSED
|
| 78 |
+
elif tokenizer.IsByte(token_id):
|
| 79 |
+
toktype = SentencePieceTokenTypes.BYTE
|
| 80 |
+
|
| 81 |
+
tokens[token_id] = text
|
| 82 |
+
scores[token_id] = score
|
| 83 |
+
toktypes[token_id] = toktype
|
| 84 |
+
|
| 85 |
+
added_tokens_file = self.dir_model / 'added_tokens.json'
|
| 86 |
+
if added_tokens_file.is_file():
|
| 87 |
+
with open(added_tokens_file, "r", encoding="utf-8") as f:
|
| 88 |
+
added_tokens_json = json.load(f)
|
| 89 |
+
for key in added_tokens_json:
|
| 90 |
+
token_id = added_tokens_json[key]
|
| 91 |
+
if token_id >= vocab_size:
|
| 92 |
+
logger.warning(f'ignore token {token_id}: id is out of range, max={vocab_size - 1}')
|
| 93 |
+
continue
|
| 94 |
+
|
| 95 |
+
tokens[token_id] = key.encode("utf-8")
|
| 96 |
+
scores[token_id] = -1000.0
|
| 97 |
+
toktypes[token_id] = SentencePieceTokenTypes.USER_DEFINED
|
| 98 |
+
|
| 99 |
+
if vocab_size > len(tokens):
|
| 100 |
+
pad_count = vocab_size - len(tokens)
|
| 101 |
+
logger.debug(f"Padding vocab with {pad_count} token(s) - [PAD1] through [PAD{pad_count}]")
|
| 102 |
+
for i in range(1, pad_count + 1):
|
| 103 |
+
tokens.append(bytes(f"[PAD{i}]", encoding="utf-8"))
|
| 104 |
+
scores.append(-1000.0)
|
| 105 |
+
toktypes.append(SentencePieceTokenTypes.UNUSED)
|
| 106 |
+
|
| 107 |
+
self.gguf_writer.add_tokenizer_model("t5")
|
| 108 |
+
self.gguf_writer.add_tokenizer_pre("default")
|
| 109 |
+
self.gguf_writer.add_token_list(tokens)
|
| 110 |
+
self.gguf_writer.add_token_scores(scores)
|
| 111 |
+
self.gguf_writer.add_token_types(toktypes)
|
| 112 |
+
self.gguf_writer.add_add_space_prefix(add_prefix)
|
| 113 |
+
self.gguf_writer.add_remove_extra_whitespaces(remove_whitespaces)
|
| 114 |
+
if precompiled_charsmap:
|
| 115 |
+
self.gguf_writer.add_precompiled_charsmap(precompiled_charsmap)
|
| 116 |
+
|
| 117 |
+
special_vocab = gguf.SpecialVocab(self.dir_model, n_vocab=len(tokens))
|
| 118 |
+
special_vocab.add_to_gguf(self.gguf_writer)
|
| 119 |
+
|
| 120 |
+
def set_gguf_parameters(self):
|
| 121 |
+
if (n_ctx := self.find_hparam(["n_positions"], optional=True)) is None:
|
| 122 |
+
logger.warning("Couldn't find context length in config.json, assuming default value of 512")
|
| 123 |
+
n_ctx = 512
|
| 124 |
+
self.gguf_writer.add_context_length(n_ctx)
|
| 125 |
+
self.gguf_writer.add_embedding_length(self.hparams["d_model"])
|
| 126 |
+
self.gguf_writer.add_feed_forward_length(self.hparams["d_ff"])
|
| 127 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 128 |
+
if (dec_n_layer := self.hparams.get("num_decoder_layers")) is not None:
|
| 129 |
+
self.gguf_writer.add_decoder_block_count(dec_n_layer)
|
| 130 |
+
self.gguf_writer.add_head_count(self.hparams["num_heads"])
|
| 131 |
+
self.gguf_writer.add_key_length(self.hparams["d_kv"])
|
| 132 |
+
self.gguf_writer.add_value_length(self.hparams["d_kv"])
|
| 133 |
+
self.gguf_writer.add_layer_norm_eps(self.hparams["layer_norm_epsilon"])
|
| 134 |
+
self.gguf_writer.add_relative_attn_buckets_count(self.hparams["relative_attention_num_buckets"])
|
| 135 |
+
self.gguf_writer.add_layer_norm_rms_eps(self.hparams["layer_norm_epsilon"])
|
| 136 |
+
self.gguf_writer.add_decoder_start_token_id(self.hparams["decoder_start_token_id"])
|
| 137 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 138 |
+
|
| 139 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 140 |
+
# T5 based models contain shared token embeddings tensors saved randomly as either "encoder.embed_tokens.weight",
|
| 141 |
+
# "decoder.embed_tokens.weight" or "shared.weight" tensor. In some models there are even multiple of them stored
|
| 142 |
+
# in the safetensors files. We use the first tensor from these three as the token embeddings for both encoder
|
| 143 |
+
# and decoder and ignore the remaining ones.
|
| 144 |
+
if name in ["decoder.embed_tokens.weight", "encoder.embed_tokens.weight", "shared.weight"]:
|
| 145 |
+
if not self.shared_token_embeddings_found:
|
| 146 |
+
name = "shared.weight"
|
| 147 |
+
self.shared_token_embeddings_found = True
|
| 148 |
+
else:
|
| 149 |
+
logger.debug(f"Skipping shared tensor {name!r} in safetensors so that convert can end normally.")
|
| 150 |
+
return
|
| 151 |
+
|
| 152 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
@ModelBase.register("T5EncoderModel")
|
| 156 |
+
class T5EncoderModel(TextModel):
|
| 157 |
+
model_arch = gguf.MODEL_ARCH.T5ENCODER
|
| 158 |
+
|
| 159 |
+
def __init__(self, *args, **kwargs):
|
| 160 |
+
super().__init__(*args, **kwargs)
|
| 161 |
+
self.shared_token_embeddings_found = False
|
| 162 |
+
|
| 163 |
+
def set_vocab(self):
|
| 164 |
+
# to avoid TypeError: Descriptors cannot be created directly
|
| 165 |
+
# exception when importing sentencepiece_model_pb2
|
| 166 |
+
os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python"
|
| 167 |
+
from sentencepiece import SentencePieceProcessor
|
| 168 |
+
from sentencepiece import sentencepiece_model_pb2 as model
|
| 169 |
+
|
| 170 |
+
tokenizer_path = self.dir_model / 'tokenizer.model'
|
| 171 |
+
|
| 172 |
+
# many older models use spiece.model tokenizer model filename
|
| 173 |
+
if not tokenizer_path.is_file():
|
| 174 |
+
tokenizer_path = self.dir_model / 'spiece.model'
|
| 175 |
+
|
| 176 |
+
if not tokenizer_path.is_file():
|
| 177 |
+
raise FileNotFoundError(f"File not found: {tokenizer_path}")
|
| 178 |
+
|
| 179 |
+
sentencepiece_model = model.ModelProto() # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
|
| 180 |
+
sentencepiece_model.ParseFromString(open(tokenizer_path, "rb").read())
|
| 181 |
+
|
| 182 |
+
# some models like Pile-T5 family use BPE tokenizer instead of Unigram
|
| 183 |
+
if sentencepiece_model.trainer_spec.model_type == 2: # BPE
|
| 184 |
+
# assure the tokenizer model file name is correct
|
| 185 |
+
assert tokenizer_path.name == 'tokenizer.model'
|
| 186 |
+
return self._set_vocab_sentencepiece()
|
| 187 |
+
else:
|
| 188 |
+
assert sentencepiece_model.trainer_spec.model_type == 1 # UNIGRAM
|
| 189 |
+
|
| 190 |
+
add_prefix = sentencepiece_model.normalizer_spec.add_dummy_prefix
|
| 191 |
+
remove_whitespaces = sentencepiece_model.normalizer_spec.remove_extra_whitespaces
|
| 192 |
+
precompiled_charsmap = sentencepiece_model.normalizer_spec.precompiled_charsmap
|
| 193 |
+
|
| 194 |
+
tokenizer = SentencePieceProcessor()
|
| 195 |
+
tokenizer.LoadFromFile(str(tokenizer_path))
|
| 196 |
+
|
| 197 |
+
vocab_size = self.hparams.get('vocab_size', tokenizer.vocab_size())
|
| 198 |
+
|
| 199 |
+
tokens: list[bytes] = [f"[PAD{i}]".encode("utf-8") for i in range(vocab_size)]
|
| 200 |
+
scores: list[float] = [-10000.0] * vocab_size
|
| 201 |
+
toktypes: list[int] = [SentencePieceTokenTypes.UNUSED] * vocab_size
|
| 202 |
+
|
| 203 |
+
for token_id in range(tokenizer.vocab_size()):
|
| 204 |
+
piece = tokenizer.IdToPiece(token_id)
|
| 205 |
+
text = piece.encode("utf-8")
|
| 206 |
+
score = tokenizer.GetScore(token_id)
|
| 207 |
+
|
| 208 |
+
toktype = SentencePieceTokenTypes.NORMAL
|
| 209 |
+
if tokenizer.IsUnknown(token_id):
|
| 210 |
+
toktype = SentencePieceTokenTypes.UNKNOWN
|
| 211 |
+
elif tokenizer.IsControl(token_id):
|
| 212 |
+
toktype = SentencePieceTokenTypes.CONTROL
|
| 213 |
+
elif tokenizer.IsUnused(token_id):
|
| 214 |
+
toktype = SentencePieceTokenTypes.UNUSED
|
| 215 |
+
elif tokenizer.IsByte(token_id):
|
| 216 |
+
toktype = SentencePieceTokenTypes.BYTE
|
| 217 |
+
|
| 218 |
+
tokens[token_id] = text
|
| 219 |
+
scores[token_id] = score
|
| 220 |
+
toktypes[token_id] = toktype
|
| 221 |
+
|
| 222 |
+
added_tokens_file = self.dir_model / 'added_tokens.json'
|
| 223 |
+
if added_tokens_file.is_file():
|
| 224 |
+
with open(added_tokens_file, "r", encoding="utf-8") as f:
|
| 225 |
+
added_tokens_json = json.load(f)
|
| 226 |
+
for key in added_tokens_json:
|
| 227 |
+
token_id = added_tokens_json[key]
|
| 228 |
+
if token_id >= vocab_size:
|
| 229 |
+
logger.warning(f'ignore token {token_id}: id is out of range, max={vocab_size - 1}')
|
| 230 |
+
continue
|
| 231 |
+
|
| 232 |
+
tokens[token_id] = key.encode("utf-8")
|
| 233 |
+
scores[token_id] = -1000.0
|
| 234 |
+
toktypes[token_id] = SentencePieceTokenTypes.USER_DEFINED
|
| 235 |
+
|
| 236 |
+
if vocab_size > len(tokens):
|
| 237 |
+
pad_count = vocab_size - len(tokens)
|
| 238 |
+
logger.debug(f"Padding vocab with {pad_count} token(s) - [PAD1] through [PAD{pad_count}]")
|
| 239 |
+
for i in range(1, pad_count + 1):
|
| 240 |
+
tokens.append(bytes(f"[PAD{i}]", encoding="utf-8"))
|
| 241 |
+
scores.append(-1000.0)
|
| 242 |
+
toktypes.append(SentencePieceTokenTypes.UNUSED)
|
| 243 |
+
|
| 244 |
+
self.gguf_writer.add_tokenizer_model("t5")
|
| 245 |
+
self.gguf_writer.add_tokenizer_pre("default")
|
| 246 |
+
self.gguf_writer.add_token_list(tokens)
|
| 247 |
+
self.gguf_writer.add_token_scores(scores)
|
| 248 |
+
self.gguf_writer.add_token_types(toktypes)
|
| 249 |
+
self.gguf_writer.add_add_space_prefix(add_prefix)
|
| 250 |
+
self.gguf_writer.add_remove_extra_whitespaces(remove_whitespaces)
|
| 251 |
+
if precompiled_charsmap:
|
| 252 |
+
self.gguf_writer.add_precompiled_charsmap(precompiled_charsmap)
|
| 253 |
+
|
| 254 |
+
special_vocab = gguf.SpecialVocab(self.dir_model, n_vocab=len(tokens))
|
| 255 |
+
special_vocab.add_to_gguf(self.gguf_writer)
|
| 256 |
+
|
| 257 |
+
def set_gguf_parameters(self):
|
| 258 |
+
if (n_ctx := self.find_hparam(["n_positions"], optional=True)) is None:
|
| 259 |
+
logger.warning("Couldn't find context length in config.json, assuming default value of 512")
|
| 260 |
+
n_ctx = 512
|
| 261 |
+
self.gguf_writer.add_context_length(n_ctx)
|
| 262 |
+
self.gguf_writer.add_embedding_length(self.hparams["d_model"])
|
| 263 |
+
self.gguf_writer.add_feed_forward_length(self.hparams["d_ff"])
|
| 264 |
+
self.gguf_writer.add_block_count(self.block_count)
|
| 265 |
+
self.gguf_writer.add_head_count(self.hparams["num_heads"])
|
| 266 |
+
self.gguf_writer.add_key_length(self.hparams["d_kv"])
|
| 267 |
+
self.gguf_writer.add_value_length(self.hparams["d_kv"])
|
| 268 |
+
self.gguf_writer.add_layer_norm_eps(self.hparams["layer_norm_epsilon"])
|
| 269 |
+
self.gguf_writer.add_relative_attn_buckets_count(self.hparams["relative_attention_num_buckets"])
|
| 270 |
+
self.gguf_writer.add_layer_norm_rms_eps(self.hparams["layer_norm_epsilon"])
|
| 271 |
+
self.gguf_writer.add_file_type(self.ftype)
|
| 272 |
+
|
| 273 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 274 |
+
# T5 based models contain shared token embeddings tensors saved randomly as either "encoder.embed_tokens.weight",
|
| 275 |
+
# "decoder.embed_tokens.weight" or "shared.weight" tensor. In some models there are even multiple of them stored
|
| 276 |
+
# in the safetensors files. We use the first tensor from these three as the token embeddings for both encoder
|
| 277 |
+
# and decoder and ignore the remaining ones.
|
| 278 |
+
if name in ["decoder.embed_tokens.weight", "encoder.embed_tokens.weight", "shared.weight"]:
|
| 279 |
+
if not self.shared_token_embeddings_found:
|
| 280 |
+
name = "shared.weight"
|
| 281 |
+
self.shared_token_embeddings_found = True
|
| 282 |
+
else:
|
| 283 |
+
logger.debug(f"Skipping shared tensor {name!r} in safetensors so that convert can end normally.")
|
| 284 |
+
return
|
| 285 |
+
|
| 286 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
llama.cpp/conversion/talkie.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Iterable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING:
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
from .base import LazyTorchTensor, ModelBase, TextModel, gguf
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@ModelBase.register("TalkieForCausalLM")
|
| 14 |
+
class TalkieModel(TextModel):
|
| 15 |
+
model_arch = gguf.MODEL_ARCH.TALKIE
|
| 16 |
+
|
| 17 |
+
def set_gguf_parameters(self):
|
| 18 |
+
super().set_gguf_parameters()
|
| 19 |
+
# Talkie used F.rms_norm without an explicit eps
|
| 20 |
+
self.gguf_writer.add_layer_norm_rms_eps(torch.finfo(torch.float32).eps)
|
| 21 |
+
|
| 22 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 23 |
+
prefix = f"model.blocks.{bid}." if bid is not None else ""
|
| 24 |
+
suffix = name.removeprefix(prefix)
|
| 25 |
+
|
| 26 |
+
if suffix == "attn_gain.a_g":
|
| 27 |
+
yield self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_OUT, bid, ".scale"), data_torch
|
| 28 |
+
return
|
| 29 |
+
elif suffix == "mlp_gain.a_g":
|
| 30 |
+
yield self.format_tensor_name(gguf.MODEL_TENSOR.FFN_DOWN, bid, ".scale"), data_torch
|
| 31 |
+
return
|
| 32 |
+
elif suffix == "lm_head_gain.w_g":
|
| 33 |
+
self.gguf_writer.add_logit_scale(LazyTorchTensor.to_eager(data_torch).item())
|
| 34 |
+
return
|
| 35 |
+
elif suffix in ("attn.attn_query.weight", "attn.attn_key.weight"):
|
| 36 |
+
# absorb inverse rope
|
| 37 |
+
head_dim = self.hparams["head_dim"]
|
| 38 |
+
shape = data_torch.shape
|
| 39 |
+
data_torch = torch.reshape(data_torch, (-1, head_dim, shape[-1]))
|
| 40 |
+
signs = torch.ones((1, head_dim, 1), dtype=data_torch.dtype)
|
| 41 |
+
signs[:, head_dim // 2 :, :] = -1
|
| 42 |
+
if self.lazy:
|
| 43 |
+
signs = LazyTorchTensor.from_eager(signs)
|
| 44 |
+
# (n_head, head_dim, n_in) -> (n_out, n_in)
|
| 45 |
+
data_torch = torch.reshape(data_torch * signs, shape)
|
| 46 |
+
elif suffix == "attn.head_gain.head_g":
|
| 47 |
+
# allow head gain to broadcast
|
| 48 |
+
data_torch = data_torch.unsqueeze(-1)
|
| 49 |
+
|
| 50 |
+
if not name.endswith(".weight"):
|
| 51 |
+
name += ".weight"
|
| 52 |
+
|
| 53 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
llama.cpp/conversion/ultravox.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any, Callable, Iterable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
if TYPE_CHECKING:
|
| 6 |
+
from torch import Tensor
|
| 7 |
+
|
| 8 |
+
from .base import MmprojModel, ModelBase, TextModel, gguf
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@ModelBase.register("UltravoxModel")
|
| 12 |
+
class UltravoxModel(TextModel):
|
| 13 |
+
model_arch = gguf.MODEL_ARCH.LLAMA # dummy
|
| 14 |
+
|
| 15 |
+
def __init__(self, *args, **kwargs):
|
| 16 |
+
super().__init__(*args, **kwargs)
|
| 17 |
+
raise NotImplementedError("Ultravox does not have text decoder. Instead, it uses Llama or other models for text. If you want to get the audio encoder, please use --mmproj argument")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@ModelBase.register("GlmasrModel")
|
| 21 |
+
class GlmASRWhisperEncoderModel(MmprojModel):
|
| 22 |
+
has_vision_encoder = False
|
| 23 |
+
has_audio_encoder = True
|
| 24 |
+
|
| 25 |
+
def __init__(self, *args, **kwargs):
|
| 26 |
+
super().__init__(*args, **kwargs)
|
| 27 |
+
if "hidden_size" not in self.hparams and "intermediate_size" not in self.hparams:
|
| 28 |
+
self.hparams["hidden_size"] = self.hparams["d_model"]
|
| 29 |
+
self.hparams["intermediate_size"] = self.hparams["encoder_ffn_dim"]
|
| 30 |
+
self.hparams["num_attention_heads"] = self.hparams["encoder_attention_heads"]
|
| 31 |
+
|
| 32 |
+
def set_gguf_parameters(self):
|
| 33 |
+
super().set_gguf_parameters()
|
| 34 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.GLMA)
|
| 35 |
+
self.gguf_writer.add_audio_num_mel_bins(self.hparams["num_mel_bins"])
|
| 36 |
+
self.gguf_writer.add_audio_attention_layernorm_eps(self.hparams.get("layer_norm_eps", 1e-5))
|
| 37 |
+
self.gguf_writer.add_audio_stack_factor(self.global_config["merge_factor"])
|
| 38 |
+
|
| 39 |
+
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
| 40 |
+
if ".conv" in name and ".weight" in name:
|
| 41 |
+
return gguf.GGMLQuantizationType.F16
|
| 42 |
+
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
| 43 |
+
|
| 44 |
+
@classmethod
|
| 45 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 46 |
+
name, gen = item
|
| 47 |
+
|
| 48 |
+
if name.startswith(("model.", "lm_head.")):
|
| 49 |
+
# skip language model tensors
|
| 50 |
+
return None
|
| 51 |
+
|
| 52 |
+
if name.startswith("audio_encoder.whisper."):
|
| 53 |
+
name = name.replace("audio_encoder.whisper.","audio_tower.")
|
| 54 |
+
if "audio_encoder.layer_norm." in name or "audio_encoder.proj." in name:
|
| 55 |
+
name = name.replace("audio_encoder.", "audio_encoder.adapting.")
|
| 56 |
+
if name.startswith("audio_encoder.adapting."):
|
| 57 |
+
name = name.replace("audio_encoder.adapting.","audio.multi_modal_projector.")
|
| 58 |
+
if ".layer_norm." in name:
|
| 59 |
+
name = name.replace(".layer_norm.", ".ln_pre.")
|
| 60 |
+
if ".0." in name:
|
| 61 |
+
name = name.replace(".0.", ".linear_1.")
|
| 62 |
+
if ".2." in name:
|
| 63 |
+
name = name.replace(".2.", ".linear_2.")
|
| 64 |
+
|
| 65 |
+
return super().filter_tensors((name, gen))
|
| 66 |
+
|
| 67 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 68 |
+
if name.startswith("audio_encoder.audio_bos_eos_token."):
|
| 69 |
+
yield from super().modify_tensors(data_torch[0], "model.vision.boi", bid)
|
| 70 |
+
yield from super().modify_tensors(data_torch[1], "model.vision.eoi", bid)
|
| 71 |
+
return
|
| 72 |
+
|
| 73 |
+
if name.startswith("audio_encoder.adapting."):
|
| 74 |
+
if ".proj." in name:
|
| 75 |
+
return
|
| 76 |
+
|
| 77 |
+
if "conv1.bias" in name or "conv2.bias" in name:
|
| 78 |
+
# transpose conv1 and conv2 bias
|
| 79 |
+
data_torch = data_torch.unsqueeze(-1)
|
| 80 |
+
|
| 81 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@ModelBase.register("Qwen2AudioForConditionalGeneration")
|
| 85 |
+
class WhisperEncoderModel(MmprojModel):
|
| 86 |
+
has_vision_encoder = False # no vision encoder
|
| 87 |
+
has_audio_encoder = True
|
| 88 |
+
|
| 89 |
+
def __init__(self, *args, **kwargs):
|
| 90 |
+
super().__init__(*args, **kwargs)
|
| 91 |
+
if "hidden_size" not in self.hparams and "intermediate_size" not in self.hparams:
|
| 92 |
+
self.hparams["hidden_size"] = self.hparams["d_model"]
|
| 93 |
+
self.hparams["intermediate_size"] = self.hparams["encoder_ffn_dim"]
|
| 94 |
+
self.hparams["num_attention_heads"] = self.hparams["encoder_attention_heads"]
|
| 95 |
+
|
| 96 |
+
def set_gguf_parameters(self):
|
| 97 |
+
super().set_gguf_parameters()
|
| 98 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.QWEN2A)
|
| 99 |
+
self.gguf_writer.add_audio_num_mel_bins(self.hparams["num_mel_bins"])
|
| 100 |
+
self.gguf_writer.add_audio_attention_layernorm_eps(self.hparams.get("layer_norm_eps", 1e-5))
|
| 101 |
+
|
| 102 |
+
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
| 103 |
+
if ".conv" in name and ".weight" in name:
|
| 104 |
+
return gguf.GGMLQuantizationType.F16
|
| 105 |
+
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
| 106 |
+
|
| 107 |
+
@classmethod
|
| 108 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 109 |
+
name, gen = item
|
| 110 |
+
|
| 111 |
+
# prevent clash naming with vision tensors
|
| 112 |
+
if name.startswith("multi_modal_projector"):
|
| 113 |
+
name = "audio." + name
|
| 114 |
+
|
| 115 |
+
return super().filter_tensors((name, gen))
|
| 116 |
+
|
| 117 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 118 |
+
if "conv1.bias" in name or "conv2.bias" in name:
|
| 119 |
+
# transpose conv1 and conv2 bias
|
| 120 |
+
data_torch = data_torch.unsqueeze(-1)
|
| 121 |
+
|
| 122 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
@ModelBase.register("UltravoxModel")
|
| 126 |
+
class UltravoxWhisperEncoderModel(WhisperEncoderModel):
|
| 127 |
+
has_vision_encoder = False # no vision encoder
|
| 128 |
+
has_audio_encoder = True
|
| 129 |
+
|
| 130 |
+
def set_gguf_parameters(self):
|
| 131 |
+
super().set_gguf_parameters()
|
| 132 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.ULTRAVOX)
|
| 133 |
+
self.gguf_writer.add_audio_stack_factor(self.global_config["stack_factor"])
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
@ModelBase.register("MERaLiON2ForConditionalGeneration")
|
| 137 |
+
class MERaLiONWhisperEncoderModel(WhisperEncoderModel):
|
| 138 |
+
has_vision_encoder = False
|
| 139 |
+
has_audio_encoder = True
|
| 140 |
+
|
| 141 |
+
def get_audio_config(self) -> dict[str, Any] | None:
|
| 142 |
+
return self.global_config.get("speech_config")
|
| 143 |
+
|
| 144 |
+
def set_gguf_parameters(self):
|
| 145 |
+
super().set_gguf_parameters()
|
| 146 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.MERALION)
|
| 147 |
+
self.gguf_writer.add_audio_stack_factor(self.global_config.get("speech_mlp_scale_factor", 15))
|
| 148 |
+
|
| 149 |
+
@classmethod
|
| 150 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 151 |
+
name, gen = item
|
| 152 |
+
|
| 153 |
+
if name.startswith("text_decoder."):
|
| 154 |
+
return None
|
| 155 |
+
|
| 156 |
+
if name.startswith("speech_encoder."):
|
| 157 |
+
name = name.replace("speech_encoder.", "audio_tower.")
|
| 158 |
+
|
| 159 |
+
return super().filter_tensors((name, gen))
|
| 160 |
+
|
| 161 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 162 |
+
suffix = "." + name.rsplit(".", 1)[-1]
|
| 163 |
+
|
| 164 |
+
if name.startswith("ln_speech."):
|
| 165 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_MM_NORM_PRE, suffix=suffix), data_torch)
|
| 166 |
+
return
|
| 167 |
+
|
| 168 |
+
if name.startswith("speech_audio_adapter."):
|
| 169 |
+
if ".mlp_adapter.0." in name:
|
| 170 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_MMPROJ, 0, suffix=suffix), data_torch)
|
| 171 |
+
elif ".gate_proj." in name:
|
| 172 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_MMPROJ, 1, suffix=suffix), data_torch)
|
| 173 |
+
elif ".pool_proj." in name:
|
| 174 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_MMPROJ, 2, suffix=suffix), data_torch)
|
| 175 |
+
elif ".out_proj." in name:
|
| 176 |
+
yield (self.format_tensor_name(gguf.MODEL_TENSOR.A_MMPROJ, 3, suffix=suffix), data_torch)
|
| 177 |
+
return
|
| 178 |
+
|
| 179 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
@ModelBase.register("VoxtralForConditionalGeneration")
|
| 183 |
+
class VoxtralWhisperEncoderModel(WhisperEncoderModel):
|
| 184 |
+
has_vision_encoder = False # no vision encoder
|
| 185 |
+
has_audio_encoder = True
|
| 186 |
+
|
| 187 |
+
def set_gguf_parameters(self):
|
| 188 |
+
super().set_gguf_parameters()
|
| 189 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.VOXTRAL)
|
| 190 |
+
self.gguf_writer.add_audio_stack_factor(4) # == intermediate_size // hidden_size
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
@ModelBase.register("AudioFlamingo3ForConditionalGeneration")
|
| 194 |
+
class AudioFlamingo3WhisperEncoderModel(WhisperEncoderModel):
|
| 195 |
+
def set_gguf_parameters(self):
|
| 196 |
+
super().set_gguf_parameters()
|
| 197 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.MUSIC_FLAMINGO)
|
| 198 |
+
|
| 199 |
+
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
| 200 |
+
if ".conv" in name and ".weight" in name:
|
| 201 |
+
# Was trained in BF16, being safe, avoiding quantizing to FP16
|
| 202 |
+
return gguf.GGMLQuantizationType.F32
|
| 203 |
+
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
llama.cpp/conversion/wavtokenizer.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Callable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
if TYPE_CHECKING:
|
| 6 |
+
from torch import Tensor
|
| 7 |
+
|
| 8 |
+
from .base import ModelBase, TextModel, gguf, logger
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@ModelBase.register("WavTokenizerDec")
|
| 12 |
+
class WavTokenizerDecModel(TextModel):
|
| 13 |
+
model_arch = gguf.MODEL_ARCH.WAVTOKENIZER_DEC
|
| 14 |
+
|
| 15 |
+
@classmethod
|
| 16 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 17 |
+
name, gen = item
|
| 18 |
+
|
| 19 |
+
if \
|
| 20 |
+
name.endswith("codebook.cluster_size") or \
|
| 21 |
+
name.endswith("codebook.embed_avg") or \
|
| 22 |
+
name.endswith("codebook.inited"):
|
| 23 |
+
logger.debug(f"Skipping {name!r}")
|
| 24 |
+
return None
|
| 25 |
+
|
| 26 |
+
return super().filter_tensors(item)
|
| 27 |
+
|
| 28 |
+
def set_vocab(self):
|
| 29 |
+
self._set_vocab_none()
|
| 30 |
+
|
| 31 |
+
def set_gguf_parameters(self):
|
| 32 |
+
super().set_gguf_parameters()
|
| 33 |
+
self.gguf_writer.add_vocab_size (self.hparams["vocab_size"])
|
| 34 |
+
self.gguf_writer.add_features_length (self.hparams["n_embd_features"])
|
| 35 |
+
self.gguf_writer.add_feed_forward_length(self.hparams["n_ff"])
|
| 36 |
+
self.gguf_writer.add_group_norm_eps (self.hparams["group_norm_epsilon"])
|
| 37 |
+
self.gguf_writer.add_group_norm_groups (self.hparams["group_norm_groups"])
|
| 38 |
+
|
| 39 |
+
self.gguf_writer.add_posnet_embedding_length(self.hparams["posnet"]["n_embd"])
|
| 40 |
+
self.gguf_writer.add_posnet_block_count (self.hparams["posnet"]["n_layer"])
|
| 41 |
+
|
| 42 |
+
self.gguf_writer.add_convnext_embedding_length(self.hparams["convnext"]["n_embd"])
|
| 43 |
+
self.gguf_writer.add_convnext_block_count (self.hparams["convnext"]["n_layer"])
|
| 44 |
+
|
| 45 |
+
self.gguf_writer.add_causal_attention(False)
|
llama.cpp/conversion/xverse.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
|
| 5 |
+
from typing import Iterable, TYPE_CHECKING
|
| 6 |
+
|
| 7 |
+
if TYPE_CHECKING:
|
| 8 |
+
from torch import Tensor
|
| 9 |
+
|
| 10 |
+
from .base import ModelBase, TextModel, gguf
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@ModelBase.register("XverseForCausalLM")
|
| 14 |
+
class XverseModel(TextModel):
|
| 15 |
+
model_arch = gguf.MODEL_ARCH.XVERSE
|
| 16 |
+
|
| 17 |
+
def set_vocab(self):
|
| 18 |
+
assert (self.dir_model / "tokenizer.json").is_file()
|
| 19 |
+
dir_model = self.dir_model
|
| 20 |
+
hparams = self.hparams
|
| 21 |
+
|
| 22 |
+
tokens: list[bytes] = []
|
| 23 |
+
toktypes: list[int] = []
|
| 24 |
+
|
| 25 |
+
from transformers import AutoTokenizer
|
| 26 |
+
tokenizer = AutoTokenizer.from_pretrained(dir_model)
|
| 27 |
+
vocab_size = hparams.get("vocab_size", len(tokenizer.vocab)) # ty: ignore[unresolved-attribute]
|
| 28 |
+
# Since we are checking the maximum index, we need to ensure it's strictly less than vocab_size,
|
| 29 |
+
# because vocab_size is the count of items, and indexes start at 0.
|
| 30 |
+
max_vocab_index = max(tokenizer.get_vocab().values()) # ty: ignore[unresolved-attribute]
|
| 31 |
+
if max_vocab_index >= vocab_size:
|
| 32 |
+
raise ValueError("Vocabulary size exceeds expected maximum size.")
|
| 33 |
+
|
| 34 |
+
reverse_vocab: dict[int, str] = {id_: encoded_tok for encoded_tok, id_ in tokenizer.vocab.items()} # ty: ignore[unresolved-attribute]
|
| 35 |
+
added_vocab = tokenizer.get_added_vocab() # ty: ignore[unresolved-attribute]
|
| 36 |
+
|
| 37 |
+
for token_id in range(vocab_size):
|
| 38 |
+
token_text = reverse_vocab[token_id].encode('utf-8')
|
| 39 |
+
# replace "\x00" to string with length > 0
|
| 40 |
+
if token_text == b"\x00":
|
| 41 |
+
toktype = gguf.TokenType.BYTE # special
|
| 42 |
+
token_text = f"<{token_text}>".encode('utf-8')
|
| 43 |
+
elif re.fullmatch(br"<0x[0-9A-Fa-f]{2}>", token_text):
|
| 44 |
+
toktype = gguf.TokenType.BYTE # special
|
| 45 |
+
elif reverse_vocab[token_id] in added_vocab:
|
| 46 |
+
if tokenizer.added_tokens_decoder[token_id].special: # ty: ignore[unresolved-attribute]
|
| 47 |
+
toktype = gguf.TokenType.CONTROL
|
| 48 |
+
else:
|
| 49 |
+
toktype = gguf.TokenType.USER_DEFINED
|
| 50 |
+
else:
|
| 51 |
+
toktype = gguf.TokenType.NORMAL
|
| 52 |
+
|
| 53 |
+
tokens.append(token_text)
|
| 54 |
+
toktypes.append(toktype)
|
| 55 |
+
|
| 56 |
+
self.gguf_writer.add_tokenizer_model("llama")
|
| 57 |
+
self.gguf_writer.add_tokenizer_pre("default")
|
| 58 |
+
self.gguf_writer.add_token_list(tokens)
|
| 59 |
+
self.gguf_writer.add_token_types(toktypes)
|
| 60 |
+
|
| 61 |
+
special_vocab = gguf.SpecialVocab(dir_model, n_vocab=len(tokens))
|
| 62 |
+
special_vocab.add_to_gguf(self.gguf_writer)
|
| 63 |
+
|
| 64 |
+
def set_gguf_parameters(self):
|
| 65 |
+
super().set_gguf_parameters()
|
| 66 |
+
|
| 67 |
+
self.gguf_writer.add_tensor_data_layout("Meta AI original pth")
|
| 68 |
+
self.gguf_writer.add_rope_dimension_count(self.hparams["hidden_size"] // self.hparams["num_attention_heads"])
|
| 69 |
+
|
| 70 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 71 |
+
head_count = self.hparams["num_attention_heads"]
|
| 72 |
+
head_count_kv = self.hparams.get("num_key_value_heads", head_count)
|
| 73 |
+
|
| 74 |
+
# HF models permute some of the tensors, so we need to undo that
|
| 75 |
+
if name.endswith("q_proj.weight"):
|
| 76 |
+
data_torch = self._reverse_hf_permute(data_torch, head_count, head_count)
|
| 77 |
+
if name.endswith("k_proj.weight"):
|
| 78 |
+
data_torch = self._reverse_hf_permute(data_torch, head_count, head_count_kv)
|
| 79 |
+
|
| 80 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 81 |
+
|
| 82 |
+
def _reverse_hf_permute(self, weights: Tensor, n_head: int, n_kv_head: int | None = None) -> Tensor:
|
| 83 |
+
if n_kv_head is not None and n_head != n_kv_head:
|
| 84 |
+
n_head //= n_kv_head
|
| 85 |
+
|
| 86 |
+
return (
|
| 87 |
+
weights.reshape(n_head, 2, weights.shape[0] // n_head // 2, *weights.shape[1:])
|
| 88 |
+
.swapaxes(1, 2)
|
| 89 |
+
.reshape(weights.shape)
|
| 90 |
+
)
|
llama.cpp/conversion/youtuvl.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Callable, Iterable, TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
if TYPE_CHECKING:
|
| 6 |
+
from torch import Tensor
|
| 7 |
+
|
| 8 |
+
from .base import MmprojModel, ModelBase, gguf, logger
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@ModelBase.register("YoutuVLForConditionalGeneration")
|
| 12 |
+
class YoutuVLVisionModel(MmprojModel):
|
| 13 |
+
def __init__(self, *args, **kwargs):
|
| 14 |
+
super().__init__(*args, **kwargs)
|
| 15 |
+
assert self.hparams_vision is not None
|
| 16 |
+
self.hparams_vision["image_size"] = self.hparams_vision.get("image_size", 560)
|
| 17 |
+
|
| 18 |
+
def set_gguf_parameters(self):
|
| 19 |
+
super().set_gguf_parameters()
|
| 20 |
+
|
| 21 |
+
self.gguf_writer.add_clip_projector_type(gguf.VisionProjectorType.YOUTUVL)
|
| 22 |
+
self.gguf_writer.add_vision_attention_layernorm_eps(self.hparams.get("layer_norm_eps", 1e-6))
|
| 23 |
+
|
| 24 |
+
# Handle activation function
|
| 25 |
+
hidden_act = str(self.hparams.get("hidden_act", "gelu_pytorch_tanh")).lower()
|
| 26 |
+
if hidden_act in ("gelu", "gelu_pytorch_tanh", "gelu_fast", "gelu_new", "gelu_accurate"):
|
| 27 |
+
self.gguf_writer.add_vision_use_gelu(True)
|
| 28 |
+
elif hidden_act == "silu":
|
| 29 |
+
self.gguf_writer.add_vision_use_silu(True)
|
| 30 |
+
else:
|
| 31 |
+
raise ValueError(f"Unsupported activation function for YOUTUVL: {hidden_act}")
|
| 32 |
+
|
| 33 |
+
self.gguf_writer.add_vision_spatial_merge_size(self.hparams.get("spatial_merge_size", 2))
|
| 34 |
+
|
| 35 |
+
window_size = self.hparams.get("window_size")
|
| 36 |
+
if window_size is not None:
|
| 37 |
+
self.gguf_writer.add_vision_window_size(window_size)
|
| 38 |
+
# fullatt_block_indexes contains explicit layer indices that use full attention
|
| 39 |
+
# e.g., [2, 5, 8, 11] means layers 2, 5, 8, 11 use full attention
|
| 40 |
+
# All other layers use window attention
|
| 41 |
+
fullatt_block_indexes = self.hparams.get("fullatt_block_indexes")
|
| 42 |
+
assert fullatt_block_indexes is not None, "fullatt_block_indexes is required for youtuvl"
|
| 43 |
+
# Store the explicit layer indices for YoutuVL (irregular pattern approach)
|
| 44 |
+
self.gguf_writer.add_vision_wa_layer_indexes(layers=fullatt_block_indexes)
|
| 45 |
+
|
| 46 |
+
@classmethod
|
| 47 |
+
def filter_tensors(cls, item: tuple[str, Callable[[], Tensor]]) -> tuple[str, Callable[[], Tensor]] | None:
|
| 48 |
+
name, gen = item
|
| 49 |
+
|
| 50 |
+
# Skip language model tensors
|
| 51 |
+
skip_prefixes = ('lm_head.', 'model.layers.', 'model.embed_tokens.', 'model.norm.')
|
| 52 |
+
if name.startswith(skip_prefixes):
|
| 53 |
+
return None
|
| 54 |
+
|
| 55 |
+
return super().filter_tensors(item)
|
| 56 |
+
|
| 57 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 58 |
+
# Try to map the tensor using TensorNameMap (handles vision encoder and projector)
|
| 59 |
+
try:
|
| 60 |
+
yield from super().modify_tensors(data_torch, name, bid)
|
| 61 |
+
except ValueError:
|
| 62 |
+
# If mapping fails, log warning and skip
|
| 63 |
+
logger.warning(f"Cannot map tensor: {name}")
|
| 64 |
+
return
|
llama.cpp/convert_hf_to_gguf.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import logging
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
|
| 14 |
+
if 'NO_LOCAL_GGUF' not in os.environ:
|
| 15 |
+
sys.path.insert(1, str(Path(__file__).parent / 'gguf-py'))
|
| 16 |
+
import gguf
|
| 17 |
+
|
| 18 |
+
from conversion import (
|
| 19 |
+
ModelBase,
|
| 20 |
+
ModelType,
|
| 21 |
+
get_model_architecture,
|
| 22 |
+
get_model_class,
|
| 23 |
+
logger,
|
| 24 |
+
print_registered_models,
|
| 25 |
+
_mistral_common_installed,
|
| 26 |
+
_mistral_import_error_msg,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def split_str_to_n_bytes(split_str: str) -> int:
|
| 31 |
+
if split_str.endswith("K"):
|
| 32 |
+
n = int(split_str[:-1]) * 1000
|
| 33 |
+
elif split_str.endswith("M"):
|
| 34 |
+
n = int(split_str[:-1]) * 1000 * 1000
|
| 35 |
+
elif split_str.endswith("G"):
|
| 36 |
+
n = int(split_str[:-1]) * 1000 * 1000 * 1000
|
| 37 |
+
elif split_str.isnumeric():
|
| 38 |
+
n = int(split_str)
|
| 39 |
+
else:
|
| 40 |
+
raise ValueError(f"Invalid split size: {split_str}, must be a number, optionally followed by K, M, or G")
|
| 41 |
+
|
| 42 |
+
if n < 0:
|
| 43 |
+
raise ValueError(f"Invalid split size: {split_str}, must be positive")
|
| 44 |
+
|
| 45 |
+
return n
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def parse_args() -> argparse.Namespace:
|
| 49 |
+
parser = argparse.ArgumentParser(
|
| 50 |
+
description="Convert a huggingface model to a GGML compatible file")
|
| 51 |
+
parser.add_argument(
|
| 52 |
+
"--vocab-only", action="store_true",
|
| 53 |
+
help="extract only the vocab",
|
| 54 |
+
)
|
| 55 |
+
parser.add_argument(
|
| 56 |
+
"--outfile", type=Path,
|
| 57 |
+
help="path to write to; default: based on input. {ftype} will be replaced by the outtype.",
|
| 58 |
+
)
|
| 59 |
+
parser.add_argument(
|
| 60 |
+
"--outtype", type=str, choices=["f32", "f16", "bf16", "q8_0", "tq1_0", "tq2_0", "auto"], default="auto",
|
| 61 |
+
help="output format - use f32 for float32, f16 for float16, bf16 for bfloat16, q8_0 for Q8_0, tq1_0 or tq2_0 for ternary, and auto for the highest-fidelity 16-bit float type",
|
| 62 |
+
)
|
| 63 |
+
parser.add_argument(
|
| 64 |
+
"--bigendian", action="store_true",
|
| 65 |
+
help="model is executed on big endian machine",
|
| 66 |
+
)
|
| 67 |
+
parser.add_argument(
|
| 68 |
+
"model", type=str,
|
| 69 |
+
help="directory containing model file or huggingface repository ID (if --remote)",
|
| 70 |
+
nargs="?",
|
| 71 |
+
)
|
| 72 |
+
parser.add_argument(
|
| 73 |
+
"--use-temp-file", action="store_true",
|
| 74 |
+
help="use the tempfile library while processing (helpful when running out of memory, process killed)",
|
| 75 |
+
)
|
| 76 |
+
parser.add_argument(
|
| 77 |
+
"--no-lazy", action="store_true",
|
| 78 |
+
help="use more RAM by computing all outputs before writing (use in case lazy evaluation is broken)",
|
| 79 |
+
)
|
| 80 |
+
parser.add_argument(
|
| 81 |
+
"--model-name", type=str, default=None,
|
| 82 |
+
help="name of the model",
|
| 83 |
+
)
|
| 84 |
+
parser.add_argument(
|
| 85 |
+
"--verbose", action="store_true",
|
| 86 |
+
help="increase output verbosity",
|
| 87 |
+
)
|
| 88 |
+
parser.add_argument(
|
| 89 |
+
"--split-max-tensors", type=int, default=0,
|
| 90 |
+
help="max tensors in each split",
|
| 91 |
+
)
|
| 92 |
+
parser.add_argument(
|
| 93 |
+
"--split-max-size", type=str, default="0",
|
| 94 |
+
help="max size per split N(M|G)",
|
| 95 |
+
)
|
| 96 |
+
parser.add_argument(
|
| 97 |
+
"--dry-run", action="store_true",
|
| 98 |
+
help="only print out a split plan and exit, without writing any new files",
|
| 99 |
+
)
|
| 100 |
+
parser.add_argument(
|
| 101 |
+
"--no-tensor-first-split", action="store_true",
|
| 102 |
+
help="do not add tensors to the first split (disabled by default)"
|
| 103 |
+
)
|
| 104 |
+
parser.add_argument(
|
| 105 |
+
"--metadata", type=Path,
|
| 106 |
+
help="Specify the path for an authorship metadata override file"
|
| 107 |
+
)
|
| 108 |
+
parser.add_argument(
|
| 109 |
+
"--print-supported-models", action="store_true",
|
| 110 |
+
help="Print the supported models"
|
| 111 |
+
)
|
| 112 |
+
parser.add_argument(
|
| 113 |
+
"--remote", action="store_true",
|
| 114 |
+
help="(Experimental) Read safetensors file remotely without downloading to disk. Config and tokenizer files will still be downloaded. To use this feature, you need to specify Hugging Face model repo name instead of a local directory. For example: 'HuggingFaceTB/SmolLM2-1.7B-Instruct'. Note: To access gated repo, set HF_TOKEN environment variable to your Hugging Face token.",
|
| 115 |
+
)
|
| 116 |
+
parser.add_argument(
|
| 117 |
+
"--mmproj", action="store_true",
|
| 118 |
+
help="Export multimodal projector (mmproj) for vision models. This will only work on some vision models. An 'mmproj-' prefix will be added to the output file name.",
|
| 119 |
+
)
|
| 120 |
+
parser.add_argument(
|
| 121 |
+
"--mtp", action="store_true",
|
| 122 |
+
help="Export only the multi-token prediction (MTP) head as a separate GGUF, suitable for use as a speculative draft. An 'mtp-' prefix will be added to the output file name.",
|
| 123 |
+
)
|
| 124 |
+
parser.add_argument(
|
| 125 |
+
"--no-mtp", action="store_true",
|
| 126 |
+
help="Exclude the multi-token prediction (MTP) head from the converted GGUF. Pair with --mtp on a second run to publish trunk and MTP as two files. Note: the split form duplicates embeddings, but even though the bundled default is more space-efficient overall, this allows differing quantization which may be more performant.",
|
| 127 |
+
)
|
| 128 |
+
parser.add_argument(
|
| 129 |
+
"--mistral-format", action="store_true",
|
| 130 |
+
help="Whether the model is stored following the Mistral format.",
|
| 131 |
+
)
|
| 132 |
+
parser.add_argument(
|
| 133 |
+
"--disable-mistral-community-chat-template", action="store_true",
|
| 134 |
+
help=(
|
| 135 |
+
"Whether to disable usage of Mistral community chat templates. If set, use the Mistral official `mistral-common` library for tokenization and detokenization of Mistral models. "
|
| 136 |
+
"Using `mistral-common` ensure correctness and zero-day support of tokenization for models converted from the Mistral format but requires to manually setup the tokenization server."
|
| 137 |
+
)
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
parser.add_argument(
|
| 141 |
+
"--sentence-transformers-dense-modules", action="store_true",
|
| 142 |
+
help=("Whether to include sentence-transformers dense modules. "
|
| 143 |
+
"It can be used for sentence-transformers models, like google/embeddinggemma-300m. "
|
| 144 |
+
"Default these modules are not included.")
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
parser.add_argument(
|
| 148 |
+
"--fuse-gate-up-exps", action="store_true",
|
| 149 |
+
help="Fuse gate_exps and up_exps tensors into a single gate_up_exps tensor for MoE models.",
|
| 150 |
+
)
|
| 151 |
+
parser.add_argument(
|
| 152 |
+
"--fp8-as-q8", action="store_true",
|
| 153 |
+
help="Store tensors dequantized from FP8 as Q8_0 instead of BF16/F16.",
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
parser.add_argument(
|
| 157 |
+
"--target-model-dir", type=str, default=None,
|
| 158 |
+
help=(
|
| 159 |
+
"path to the target model directory; required when converting a standalone draft model "
|
| 160 |
+
"(e.g. EAGLE3 / DFlash) that needs target-model metadata such as tokenizer, hidden size, and "
|
| 161 |
+
"layer count to populate its GGUF."
|
| 162 |
+
),
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
args = parser.parse_args()
|
| 166 |
+
if not args.print_supported_models and args.model is None:
|
| 167 |
+
parser.error("the following arguments are required: model")
|
| 168 |
+
return args
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def main() -> None:
|
| 172 |
+
args = parse_args()
|
| 173 |
+
|
| 174 |
+
if args.print_supported_models:
|
| 175 |
+
logger.error("Supported models:")
|
| 176 |
+
print_registered_models()
|
| 177 |
+
sys.exit(0)
|
| 178 |
+
|
| 179 |
+
if args.verbose:
|
| 180 |
+
logging.basicConfig(level=logging.DEBUG)
|
| 181 |
+
else:
|
| 182 |
+
logging.basicConfig(level=logging.INFO)
|
| 183 |
+
|
| 184 |
+
if args.remote:
|
| 185 |
+
hf_repo_id = args.model
|
| 186 |
+
from huggingface_hub import snapshot_download
|
| 187 |
+
allowed_patterns = ["LICENSE", "*.json", "*.md", "*.txt", "tokenizer.model"]
|
| 188 |
+
if args.sentence_transformers_dense_modules:
|
| 189 |
+
# include sentence-transformers dense modules safetensors files
|
| 190 |
+
allowed_patterns.append("*.safetensors")
|
| 191 |
+
local_dir = snapshot_download(
|
| 192 |
+
repo_id=hf_repo_id,
|
| 193 |
+
allow_patterns=allowed_patterns)
|
| 194 |
+
dir_model = Path(local_dir)
|
| 195 |
+
logger.info(f"Downloaded config and tokenizer to {local_dir}")
|
| 196 |
+
else:
|
| 197 |
+
hf_repo_id = None
|
| 198 |
+
dir_model = Path(args.model)
|
| 199 |
+
|
| 200 |
+
if not dir_model.is_dir():
|
| 201 |
+
logger.error(f'Error: {dir_model} is not a directory')
|
| 202 |
+
sys.exit(1)
|
| 203 |
+
|
| 204 |
+
ftype_map: dict[str, gguf.LlamaFileType] = {
|
| 205 |
+
"f32": gguf.LlamaFileType.ALL_F32,
|
| 206 |
+
"f16": gguf.LlamaFileType.MOSTLY_F16,
|
| 207 |
+
"bf16": gguf.LlamaFileType.MOSTLY_BF16,
|
| 208 |
+
"q8_0": gguf.LlamaFileType.MOSTLY_Q8_0,
|
| 209 |
+
"tq1_0": gguf.LlamaFileType.MOSTLY_TQ1_0,
|
| 210 |
+
"tq2_0": gguf.LlamaFileType.MOSTLY_TQ2_0,
|
| 211 |
+
"auto": gguf.LlamaFileType.GUESSED,
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
is_split = args.split_max_tensors > 0 or args.split_max_size != "0"
|
| 215 |
+
if args.use_temp_file and is_split:
|
| 216 |
+
logger.error("Error: Cannot use temp file when splitting")
|
| 217 |
+
sys.exit(1)
|
| 218 |
+
|
| 219 |
+
if args.outfile is not None:
|
| 220 |
+
fname_out = args.outfile
|
| 221 |
+
elif hf_repo_id:
|
| 222 |
+
# if remote, use the model ID as the output file name
|
| 223 |
+
fname_out = Path("./" + hf_repo_id.replace("/", "-") + "-{ftype}.gguf")
|
| 224 |
+
else:
|
| 225 |
+
fname_out = dir_model
|
| 226 |
+
|
| 227 |
+
logger.info(f"Loading model: {dir_model.name}")
|
| 228 |
+
|
| 229 |
+
is_mistral_format = args.mistral_format
|
| 230 |
+
if is_mistral_format and not _mistral_common_installed:
|
| 231 |
+
raise ImportError(_mistral_import_error_msg)
|
| 232 |
+
disable_mistral_community_chat_template = args.disable_mistral_community_chat_template
|
| 233 |
+
|
| 234 |
+
with torch.inference_mode():
|
| 235 |
+
output_type = ftype_map[args.outtype]
|
| 236 |
+
model_type = ModelType.MMPROJ if args.mmproj else ModelType.TEXT
|
| 237 |
+
hparams = ModelBase.load_hparams(dir_model, is_mistral_format)
|
| 238 |
+
if not is_mistral_format:
|
| 239 |
+
model_architecture = get_model_architecture(hparams, model_type)
|
| 240 |
+
logger.info(f"Model architecture: {model_architecture}")
|
| 241 |
+
try:
|
| 242 |
+
model_class = get_model_class(model_architecture, mmproj=(model_type == ModelType.MMPROJ))
|
| 243 |
+
except NotImplementedError:
|
| 244 |
+
logger.error(f"Model {model_architecture} is not supported")
|
| 245 |
+
sys.exit(1)
|
| 246 |
+
elif args.mmproj:
|
| 247 |
+
assert hparams.get("vision_encoder") is not None, "This model does not support multimodal"
|
| 248 |
+
from conversion.pixtral import PixtralModel
|
| 249 |
+
model_class = PixtralModel
|
| 250 |
+
elif hparams.get("moe") is not None:
|
| 251 |
+
from conversion.mistral import MistralMoeModel
|
| 252 |
+
model_class = MistralMoeModel
|
| 253 |
+
else:
|
| 254 |
+
from conversion.mistral import MistralModel
|
| 255 |
+
model_class = MistralModel
|
| 256 |
+
|
| 257 |
+
if args.mtp and args.no_mtp:
|
| 258 |
+
logger.error("--mtp and --no-mtp are mutually exclusive")
|
| 259 |
+
sys.exit(1)
|
| 260 |
+
|
| 261 |
+
if args.mtp or args.no_mtp:
|
| 262 |
+
from conversion.qwen import _Qwen35MtpMixin
|
| 263 |
+
from conversion.step3 import Step35Model
|
| 264 |
+
if not (issubclass(model_class, _Qwen35MtpMixin) or issubclass(model_class, Step35Model)):
|
| 265 |
+
logger.error("--mtp / --no-mtp are only supported for Qwen3.5/3.6 and Step3.5 text variants today")
|
| 266 |
+
sys.exit(1)
|
| 267 |
+
if args.no_mtp:
|
| 268 |
+
model_class.no_mtp = True
|
| 269 |
+
if args.mtp:
|
| 270 |
+
model_class.mtp_only = True
|
| 271 |
+
|
| 272 |
+
model_instance = model_class(dir_model, output_type, fname_out,
|
| 273 |
+
is_big_endian=args.bigendian, use_temp_file=args.use_temp_file,
|
| 274 |
+
eager=args.no_lazy,
|
| 275 |
+
metadata_override=args.metadata, model_name=args.model_name,
|
| 276 |
+
split_max_tensors=args.split_max_tensors,
|
| 277 |
+
split_max_size=split_str_to_n_bytes(args.split_max_size), dry_run=args.dry_run,
|
| 278 |
+
small_first_shard=args.no_tensor_first_split,
|
| 279 |
+
remote_hf_model_id=hf_repo_id, disable_mistral_community_chat_template=disable_mistral_community_chat_template,
|
| 280 |
+
sentence_transformers_dense_modules=args.sentence_transformers_dense_modules,
|
| 281 |
+
target_model_dir=Path(args.target_model_dir) if args.target_model_dir else None,
|
| 282 |
+
fuse_gate_up_exps=args.fuse_gate_up_exps,
|
| 283 |
+
fp8_as_q8=args.fp8_as_q8,
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
if args.vocab_only:
|
| 287 |
+
logger.info("Exporting model vocab...")
|
| 288 |
+
model_instance.write_vocab()
|
| 289 |
+
logger.info(f"Model vocab successfully exported to {model_instance.fname_out}")
|
| 290 |
+
else:
|
| 291 |
+
logger.info("Exporting model...")
|
| 292 |
+
model_instance.write()
|
| 293 |
+
out_path = f"{model_instance.fname_out.parent}{os.sep}" if is_split else model_instance.fname_out
|
| 294 |
+
logger.info(f"Model successfully exported to {out_path}")
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
if __name__ == '__main__':
|
| 298 |
+
main()
|
llama.cpp/convert_hf_to_gguf_update.py
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
import logging
|
| 5 |
+
import os
|
| 6 |
+
import pathlib
|
| 7 |
+
import re
|
| 8 |
+
|
| 9 |
+
import requests
|
| 10 |
+
import json
|
| 11 |
+
import shutil
|
| 12 |
+
import argparse
|
| 13 |
+
|
| 14 |
+
from hashlib import sha256
|
| 15 |
+
from enum import IntEnum, auto
|
| 16 |
+
from transformers import AutoTokenizer
|
| 17 |
+
|
| 18 |
+
logging.basicConfig(level=logging.DEBUG)
|
| 19 |
+
logger = logging.getLogger("convert_hf_to_gguf_update")
|
| 20 |
+
sess = requests.Session()
|
| 21 |
+
|
| 22 |
+
convert_py_pth = pathlib.Path("conversion/base.py")
|
| 23 |
+
convert_py = convert_py_pth.read_text(encoding="utf-8")
|
| 24 |
+
hf_token_pth = pathlib.Path.home() / ".cache" / "huggingface" / "token"
|
| 25 |
+
hf_token = hf_token_pth.read_text(encoding="utf-8").strip() if hf_token_pth.exists() else None
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class TOKENIZER_TYPE(IntEnum):
|
| 29 |
+
SPM = auto()
|
| 30 |
+
BPE = auto()
|
| 31 |
+
WPM = auto()
|
| 32 |
+
UGM = auto()
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
DOC_STRING = """
|
| 36 |
+
This script downloads the tokenizer models of the specified models from Huggingface and
|
| 37 |
+
generates the get_vocab_base_pre() function for convert_hf_to_gguf.py
|
| 38 |
+
|
| 39 |
+
/!\\ It is intended to be used by contributors and is not meant to be run by end users
|
| 40 |
+
|
| 41 |
+
This is necessary in order to analyze the type of pre-tokenizer used by the model and
|
| 42 |
+
provide the necessary information to llama.cpp via the GGUF header in order to implement
|
| 43 |
+
the same pre-tokenizer.
|
| 44 |
+
|
| 45 |
+
ref: https://github.com/ggml-org/llama.cpp/pull/6920
|
| 46 |
+
|
| 47 |
+
Instructions:
|
| 48 |
+
|
| 49 |
+
- Add a new model to the "models" list
|
| 50 |
+
- Run the script with your huggingface token
|
| 51 |
+
By default, token will be read from ~/.cache/huggingface/token
|
| 52 |
+
- The convert_hf_to_gguf.py script will have had its get_vocab_base_pre() function updated
|
| 53 |
+
- Update llama.cpp with the new pre-tokenizer if necessary
|
| 54 |
+
"""
|
| 55 |
+
# TODO: generate tokenizer tests for llama.cpp
|
| 56 |
+
|
| 57 |
+
parser = argparse.ArgumentParser(description=DOC_STRING, formatter_class=argparse.RawTextHelpFormatter)
|
| 58 |
+
parser.add_argument(
|
| 59 |
+
"--full", action="store_true",
|
| 60 |
+
help="download full list of models - make sure you have access to all of them",
|
| 61 |
+
)
|
| 62 |
+
parser.add_argument(
|
| 63 |
+
"--check-missing", action="store_true",
|
| 64 |
+
help="only check for missing pre-tokenizer hashes",
|
| 65 |
+
)
|
| 66 |
+
parser.add_argument(
|
| 67 |
+
"hf_token",
|
| 68 |
+
help="optional HF token",
|
| 69 |
+
nargs="?",
|
| 70 |
+
)
|
| 71 |
+
args = parser.parse_args()
|
| 72 |
+
hf_token = args.hf_token if args.hf_token is not None else hf_token
|
| 73 |
+
|
| 74 |
+
if hf_token is None:
|
| 75 |
+
logger.warning("HF token not found. You can provide it as an argument or set it in ~/.cache/huggingface/token")
|
| 76 |
+
|
| 77 |
+
if args.check_missing and args.full:
|
| 78 |
+
logger.warning("Downloading full list of models requested, ignoring --check-missing!")
|
| 79 |
+
args.check_missing = False
|
| 80 |
+
|
| 81 |
+
# TODO: this string has to exercise as much pre-tokenizer functionality as possible
|
| 82 |
+
# will be updated with time - contributions welcome
|
| 83 |
+
CHK_TXT = '\n \n\n \n\n\n \t \t\t \t\n \n \n \n \n🚀 (normal) 😶🌫️ (multiple emojis concatenated) ✅ 🦙🦙 3 33 333 3333 33333 333333 3333333 33333333 3.3 3..3 3...3 កាន់តែពិសេសអាច😁 ?我想在apple工作1314151天~ ------======= нещо на Български \'\'\'\'\'\'```````\"\"\"\"......!!!!!!?????? I\'ve been \'told he\'s there, \'RE you sure? \'M not sure I\'ll make it, \'D you like some tea? We\'Ve a\'lL'
|
| 84 |
+
|
| 85 |
+
# TODO: add models here, base models preferred
|
| 86 |
+
models = [
|
| 87 |
+
{"name": "llama-spm", "tokt": TOKENIZER_TYPE.SPM, "repo": "https://huggingface.co/meta-llama/Llama-2-7b-hf", },
|
| 88 |
+
{"name": "llama-bpe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/meta-llama/Meta-Llama-3-8B", },
|
| 89 |
+
{"name": "phi-3", "tokt": TOKENIZER_TYPE.SPM, "repo": "https://huggingface.co/microsoft/Phi-3-mini-4k-instruct", },
|
| 90 |
+
{"name": "deepseek-llm", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/deepseek-ai/deepseek-llm-7b-base", },
|
| 91 |
+
{"name": "deepseek-coder", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/deepseek-ai/deepseek-coder-6.7b-base", },
|
| 92 |
+
{"name": "falcon", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tiiuae/falcon-7b", },
|
| 93 |
+
{"name": "bert-bge", "tokt": TOKENIZER_TYPE.WPM, "repo": "https://huggingface.co/BAAI/bge-small-en-v1.5", },
|
| 94 |
+
{"name": "falcon3", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tiiuae/Falcon3-7B-Base", },
|
| 95 |
+
{"name": "bert-bge-large", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/BAAI/bge-large-zh-v1.5", },
|
| 96 |
+
{"name": "mpt", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/mosaicml/mpt-7b", },
|
| 97 |
+
{"name": "starcoder", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/bigcode/starcoder2-3b", },
|
| 98 |
+
{"name": "gpt-2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/openai-community/gpt2", },
|
| 99 |
+
{"name": "stablelm2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/stabilityai/stablelm-2-zephyr-1_6b", },
|
| 100 |
+
{"name": "refact", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/smallcloudai/Refact-1_6-base", },
|
| 101 |
+
{"name": "command-r", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/CohereForAI/c4ai-command-r-v01", },
|
| 102 |
+
{"name": "tiny_aya", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/CohereLabs/tiny-aya-base", },
|
| 103 |
+
{"name": "cohere2moe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/CohereLabs/North-Mini-Code-1.0", },
|
| 104 |
+
{"name": "qwen2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/Qwen/Qwen1.5-7B", },
|
| 105 |
+
{"name": "olmo", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/allenai/OLMo-1.7-7B-hf", },
|
| 106 |
+
{"name": "dbrx", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/databricks/dbrx-base", },
|
| 107 |
+
{"name": "jina-v1-en", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/jinaai/jina-reranker-v1-tiny-en", },
|
| 108 |
+
{"name": "jina-v2-en", "tokt": TOKENIZER_TYPE.WPM, "repo": "https://huggingface.co/jinaai/jina-embeddings-v2-base-en", }, # WPM!
|
| 109 |
+
{"name": "jina-v2-es", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/jinaai/jina-embeddings-v2-base-es", },
|
| 110 |
+
{"name": "jina-v2-de", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/jinaai/jina-embeddings-v2-base-de", },
|
| 111 |
+
{"name": "jina-v5-nano", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/jinaai/jina-embeddings-v5-text-nano", },
|
| 112 |
+
{"name": "smaug-bpe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/abacusai/Smaug-Llama-3-70B-Instruct", },
|
| 113 |
+
{"name": "poro-chat", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/LumiOpen/Poro-34B-chat", },
|
| 114 |
+
{"name": "jina-v2-code", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/jinaai/jina-embeddings-v2-base-code", },
|
| 115 |
+
{"name": "viking", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/LumiOpen/Viking-7B", }, # Also used for Viking 13B and 33B
|
| 116 |
+
{"name": "gemma", "tokt": TOKENIZER_TYPE.SPM, "repo": "https://huggingface.co/google/gemma-2b", },
|
| 117 |
+
{"name": "gemma-2", "tokt": TOKENIZER_TYPE.SPM, "repo": "https://huggingface.co/google/gemma-2-9b", },
|
| 118 |
+
{"name": "jais", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/core42/jais-13b", },
|
| 119 |
+
{"name": "jais-2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/inceptionai/Jais-2-8B-Chat", },
|
| 120 |
+
{"name": "t5", "tokt": TOKENIZER_TYPE.UGM, "repo": "https://huggingface.co/google-t5/t5-small", },
|
| 121 |
+
{"name": "codeshell", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/WisdomShell/CodeShell-7B", },
|
| 122 |
+
{"name": "tekken", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/mistralai/Mistral-Nemo-Base-2407", },
|
| 123 |
+
{"name": "smollm", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/HuggingFaceTB/SmolLM-135M", },
|
| 124 |
+
{'name': "bloom", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/bigscience/bloom", },
|
| 125 |
+
{'name': "gpt3-finnish", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/TurkuNLP/gpt3-finnish-small", },
|
| 126 |
+
{"name": "exaone", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/LGAI-EXAONE/EXAONE-3.0-7.8B-Instruct", },
|
| 127 |
+
{"name": "phi-2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/microsoft/phi-2", },
|
| 128 |
+
{"name": "chameleon", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/facebook/chameleon-7b", },
|
| 129 |
+
{"name": "roberta-bpe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/sentence-transformers/stsb-roberta-base"},
|
| 130 |
+
{"name": "gigachat", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/ai-sage/GigaChat-20B-A3B-instruct"},
|
| 131 |
+
{"name": "megrez", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/Infinigence/Megrez-3B-Instruct"},
|
| 132 |
+
{"name": "deepseek-v3", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/deepseek-ai/DeepSeek-V3"},
|
| 133 |
+
{"name": "deepseek-r1-qwen", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B"},
|
| 134 |
+
{"name": "gpt-4o", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/Xenova/gpt-4o", },
|
| 135 |
+
{"name": "superbpe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/UW/OLMo2-8B-SuperBPE-t180k", },
|
| 136 |
+
{"name": "trillion", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/trillionlabs/Trillion-7B-preview", },
|
| 137 |
+
{"name": "bailingmoe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/inclusionAI/Ling-lite", },
|
| 138 |
+
{"name": "llama4", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/meta-llama/Llama-4-Scout-17B-16E-Instruct", },
|
| 139 |
+
{"name": "pixtral", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/mistral-community/pixtral-12b", },
|
| 140 |
+
{"name": "seed-coder", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/ByteDance-Seed/Seed-Coder-8B-Base", },
|
| 141 |
+
{"name": "a.x-4.0", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/skt/A.X-4.0", },
|
| 142 |
+
{"name": "midm-2.0", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/K-intelligence/Midm-2.0-Base-Instruct", },
|
| 143 |
+
{"name": "lfm2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/LiquidAI/LFM2.5-350M", },
|
| 144 |
+
{"name": "exaone4", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/LGAI-EXAONE/EXAONE-4.0-32B", },
|
| 145 |
+
{"name": "mellum", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/JetBrains/Mellum-4b-base", },
|
| 146 |
+
{"name": "modern-bert", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/answerdotai/ModernBERT-base", },
|
| 147 |
+
{"name": "afmoe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/arcee-ai/Trinity-Tokenizer", },
|
| 148 |
+
{"name": "bailingmoe2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/inclusionAI/Ling-mini-base-2.0", },
|
| 149 |
+
{"name": "granite-docling", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/ibm-granite/granite-docling-258M", },
|
| 150 |
+
{"name": "minimax-m2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/MiniMaxAI/MiniMax-M2", },
|
| 151 |
+
{"name": "kormo", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/KORMo-Team/KORMo-tokenizer", },
|
| 152 |
+
{"name": "youtu", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tencent/Youtu-LLM-2B", },
|
| 153 |
+
{"name": "solar-open", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/upstage/Solar-Open-100B", },
|
| 154 |
+
{"name": "exaone-moe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/LGAI-EXAONE/K-EXAONE-236B-A23B", },
|
| 155 |
+
{"name": "qwen35", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/Qwen/Qwen3.5-9B-Instruct", },
|
| 156 |
+
{"name": "joyai-llm", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/jdopensource/JoyAI-LLM-Flash", },
|
| 157 |
+
{"name": "kanana2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/kakaocorp/kanana-2-30b-a3b-instruct-2601", },
|
| 158 |
+
{"name": "f2llmv2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/codefuse-ai/F2LLM-v2-4B", },
|
| 159 |
+
{"name": "sarvam-moe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/sarvamai/sarvam-30b", },
|
| 160 |
+
{"name": "talkie", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/lewtun/talkie-1930-13b-it-hf", },
|
| 161 |
+
{"name": "minicpm5", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/openbmb/MiniCPM5-1B"},
|
| 162 |
+
{"name": "granite-embed-multi-97m", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/ibm-granite/granite-embedding-97m-multilingual-r2", },
|
| 163 |
+
{"name": "granite-embed-multi-311m", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/ibm-granite/granite-embedding-311m-multilingual-r2", },
|
| 164 |
+
{"name": "mellum2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/JetBrains/Mellum2-12B-A2.5B-Base"},
|
| 165 |
+
]
|
| 166 |
+
|
| 167 |
+
# some models are known to be broken upstream, so we will skip them as exceptions
|
| 168 |
+
pre_computed_hashes = [
|
| 169 |
+
# chatglm-bpe has 2 hashes, why?
|
| 170 |
+
{"name": "chatglm-bpe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/THUDM/glm-4-9b-chat", "chkhsh": "b6e8e1518dc4305be2fe39c313ed643381c4da5db34a98f6a04c093f8afbe99b"},
|
| 171 |
+
{"name": "chatglm-bpe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/THUDM/glm-4-9b-chat", "chkhsh": "81d72c7348a9f0ebe86f23298d37debe0a5e71149e29bd283904c02262b27516"},
|
| 172 |
+
{"name": "glm4", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/THUDM/glm-4-9b-hf", "chkhsh": "a1336059768a55c99a734006ffb02203cd450fed003e9a71886c88acf24fdbc2"},
|
| 173 |
+
{"name": "glm4", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/zai-org/GLM-4.5-Air", "chkhsh": "9ca2dd618e8afaf09731a7cf6e2105b373ba6a1821559f258b272fe83e6eb902"},
|
| 174 |
+
{"name": "glm4", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/zai-org/GLM-4.7-Flash", "chkhsh": "cdf5f35325780597efd76153d4d1c16778f766173908894c04afc20108536267"},
|
| 175 |
+
{"name": "minerva-7b", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/sapienzanlp/Minerva-7B-base-v1.0", "chkhsh": "1431a23e583c97432bc230bff598d103ddb5a1f89960c8f1d1051aaa944d0b35"},
|
| 176 |
+
{"name": "hunyuan", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tencent/Hunyuan-A13B-Instruct", "chkhsh": "7e57df22b1fe23a7b1e1c7f3dc4e3f96d43a4eb0836d0c6bdc3436d7b2f1c664"},
|
| 177 |
+
{"name": "hunyuan-dense", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tencent/Hunyuan-4B-Instruct", "chkhsh": "bba3b3366b646dbdded5dbc42d59598b849371afc42f7beafa914afaa5b70aa6"},
|
| 178 |
+
# falcon-h1 series uses 4 different tokenizers across model sizes (0.5b - 34b), hence we need to define 4 different hashes
|
| 179 |
+
{"name": "falcon-h1", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tiiuae/Falcon-H1-0.5B-Base", "chkhsh": "a6b57017d60e6edb4d88ecc2845188e0eb333a70357e45dcc9b53964a73bbae6"},
|
| 180 |
+
{"name": "falcon-h1", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tiiuae/Falcon-H1-1B-Base", "chkhsh": "60476e1243776c4fb1b993dbd7a5f15ac22f83c80afdf425fa5ae01c8d44ef86"},
|
| 181 |
+
{"name": "falcon-h1", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tiiuae/Falcon-H1-7B-Base", "chkhsh": "3eda48b4c4dc7de733d1a8b3e3b4a85243dbbf704da2ee9d42c6beced8897896"},
|
| 182 |
+
{"name": "falcon-h1", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/tiiuae/Falcon-H1-34B-Base", "chkhsh": "48f8e02c0359c0bbdd82f26909171fac1c18a457bb47573ed1fe3bbb2c1cfd4b"},
|
| 183 |
+
{"name": "kimi-k2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/moonshotai/Kimi-K2-Base", "chkhsh": "81212dc7cdb7e0c1074ca62c5aeab0d43c9f52b8a737be7b12a777c953027890"},
|
| 184 |
+
{"name": "qwen2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/Qwen/Qwen3-Embedding-0.6B", "chkhsh": "d4540891389ea895b53b399da6ac824becc30f2fba0e9ddbb98f92e55ca0e97c"},
|
| 185 |
+
{"name": "qwen35", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/openbmb/MiniCPM-V-4_6", "chkhsh": "1444df51289cfa8063b96f0e62b1125440111bc79a52003ea14b6eac7016fd5f"},
|
| 186 |
+
{"name": "grok-2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/alvarobartt/grok-2-tokenizer", "chkhsh": "66b8d4e19ab16c3bfd89bce5d785fb7e0155e8648708a1f42077cb9fe002c273"},
|
| 187 |
+
# jina-v2-de variants
|
| 188 |
+
{"name": "jina-v2-de", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/aari1995/German_Semantic_V3", "chkhsh": "b3d1dd861f1d4c5c0d2569ce36baf3f90fe8a102db3de50dd71ff860d91be3df"},
|
| 189 |
+
{"name": "gpt-2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/evilfreelancer/ruGPT3XL", "chkhsh": "0fe1cf6eda062318a1af7270f3331a85c539a01778ff948e24388e949c5282f4"},
|
| 190 |
+
# lfm2 variants
|
| 191 |
+
{"name": "lfm2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/LiquidAI/LFM2.5-8B-A1B", "chkhsh": "9e454714343b69b99b71795c1d27a68c2a1d15dab111f4d353109f966af29da7"},
|
| 192 |
+
]
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def download_file_with_auth(url, token, save_path):
|
| 196 |
+
headers = {"Authorization": f"Bearer {token}"} if token else None
|
| 197 |
+
response = sess.get(url, headers=headers)
|
| 198 |
+
response.raise_for_status()
|
| 199 |
+
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
| 200 |
+
with open(save_path, 'wb') as downloaded_file:
|
| 201 |
+
downloaded_file.write(response.content)
|
| 202 |
+
logger.info(f"File {save_path} downloaded successfully")
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def download_model(model):
|
| 206 |
+
name = model["name"]
|
| 207 |
+
repo = model["repo"]
|
| 208 |
+
tokt = model["tokt"]
|
| 209 |
+
|
| 210 |
+
os.makedirs(f"models/tokenizers/{name}", exist_ok=True)
|
| 211 |
+
|
| 212 |
+
files = ["config.json", "tokenizer.json", "tokenizer_config.json"]
|
| 213 |
+
|
| 214 |
+
if name == "gpt-4o":
|
| 215 |
+
# Xenova/gpt-4o is tokenizer-only, it does not contain config.json
|
| 216 |
+
files = ["tokenizer.json", "tokenizer_config.json"]
|
| 217 |
+
|
| 218 |
+
if tokt == TOKENIZER_TYPE.SPM:
|
| 219 |
+
files.append("tokenizer.model")
|
| 220 |
+
|
| 221 |
+
if tokt == TOKENIZER_TYPE.UGM:
|
| 222 |
+
files.append("spiece.model")
|
| 223 |
+
|
| 224 |
+
if os.path.isdir(repo):
|
| 225 |
+
# If repo is a path on the file system, copy the directory
|
| 226 |
+
for file in files:
|
| 227 |
+
src_path = os.path.join(repo, file)
|
| 228 |
+
dst_path = f"models/tokenizers/{name}/{file}"
|
| 229 |
+
if os.path.isfile(dst_path):
|
| 230 |
+
logger.info(f"{name}: File {dst_path} already exists - skipping")
|
| 231 |
+
continue
|
| 232 |
+
if os.path.isfile(src_path):
|
| 233 |
+
shutil.copy2(src_path, dst_path)
|
| 234 |
+
logger.info(f"{name}: Copied {src_path} to {dst_path}")
|
| 235 |
+
else:
|
| 236 |
+
logger.warning(f"{name}: Source file {src_path} does not exist")
|
| 237 |
+
else:
|
| 238 |
+
# If repo is a URL, download the files
|
| 239 |
+
for file in files:
|
| 240 |
+
save_path = f"models/tokenizers/{name}/{file}"
|
| 241 |
+
if os.path.isfile(save_path):
|
| 242 |
+
logger.info(f"{name}: File {save_path} already exists - skipping")
|
| 243 |
+
continue
|
| 244 |
+
download_file_with_auth(f"{repo}/resolve/main/{file}", hf_token, save_path)
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
# get list of existing models and chkhsh from the convert_hf_to_gguf.py file
|
| 248 |
+
# returns mapping res --> chkhsh
|
| 249 |
+
def get_existing_models(convert_py):
|
| 250 |
+
pattern = r'if chkhsh == "([a-f0-9]{64})":\s*\n\s*.*\s*res = "([^"]+)"'
|
| 251 |
+
matches = re.findall(pattern, convert_py)
|
| 252 |
+
output = {}
|
| 253 |
+
for chkhsh, res in matches:
|
| 254 |
+
output[res] = chkhsh
|
| 255 |
+
return output
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
existing_models = {}
|
| 259 |
+
all_models = models.copy()
|
| 260 |
+
if not args.full:
|
| 261 |
+
# Filter out models that already exist in convert_hf_to_gguf.py
|
| 262 |
+
existing_models = get_existing_models(convert_py)
|
| 263 |
+
all_models = models.copy()
|
| 264 |
+
models = [model for model in all_models if model["name"] not in existing_models]
|
| 265 |
+
|
| 266 |
+
if not args.check_missing:
|
| 267 |
+
logging.info(f"Downloading {len(models)} models...")
|
| 268 |
+
for model in models:
|
| 269 |
+
try:
|
| 270 |
+
download_model(model)
|
| 271 |
+
except Exception as e:
|
| 272 |
+
logger.error(f"Failed to download model {model['name']}. Error: {e}")
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
# generate the source code for the convert_hf_to_gguf.py:get_vocab_base_pre() function:
|
| 276 |
+
|
| 277 |
+
src_ifs = ""
|
| 278 |
+
for model in [*pre_computed_hashes, *all_models]:
|
| 279 |
+
name = model["name"]
|
| 280 |
+
tokt = model["tokt"]
|
| 281 |
+
chkhsh = model.get("chkhsh")
|
| 282 |
+
|
| 283 |
+
if tokt == TOKENIZER_TYPE.SPM or tokt == TOKENIZER_TYPE.UGM:
|
| 284 |
+
continue
|
| 285 |
+
|
| 286 |
+
# create the tokenizer
|
| 287 |
+
if chkhsh is not None:
|
| 288 |
+
# if the model has a pre-computed hash, use it
|
| 289 |
+
logger.info(f"Using pre-computed hash for model {name}: {chkhsh}")
|
| 290 |
+
elif name in existing_models:
|
| 291 |
+
# if the model already exists in convert_hf_to_gguf.py, skip compute hash
|
| 292 |
+
chkhsh = existing_models[name]
|
| 293 |
+
else:
|
| 294 |
+
# otherwise, compute the hash of the tokenizer
|
| 295 |
+
|
| 296 |
+
# Fail if the tokenizer folder with config does not exist or there are other download issues previously
|
| 297 |
+
if not os.path.isfile(f"models/tokenizers/{name}/tokenizer_config.json"):
|
| 298 |
+
raise OSError(f"Config for tokenizer {name} not found. The model may not exist or is not accessible with the provided token.")
|
| 299 |
+
|
| 300 |
+
try:
|
| 301 |
+
logger.info(f"Loading tokenizer from {f'models/tokenizers/{name}'}...")
|
| 302 |
+
if name == "t5":
|
| 303 |
+
tokenizer = AutoTokenizer.from_pretrained(f"models/tokenizers/{name}", use_fast=False)
|
| 304 |
+
else:
|
| 305 |
+
tokenizer = AutoTokenizer.from_pretrained(f"models/tokenizers/{name}")
|
| 306 |
+
except Exception as e:
|
| 307 |
+
raise OSError(f"Error loading tokenizer for model {name}.") from e
|
| 308 |
+
|
| 309 |
+
chktok = tokenizer.encode(CHK_TXT) # ty: ignore[unresolved-attribute]
|
| 310 |
+
chkhsh = sha256(str(chktok).encode()).hexdigest()
|
| 311 |
+
|
| 312 |
+
logger.info(f"model: {name}")
|
| 313 |
+
logger.info(f"tokt: {tokt}")
|
| 314 |
+
logger.info(f"repo: {model['repo']}")
|
| 315 |
+
logger.info(f"chktok: {chktok}")
|
| 316 |
+
logger.info(f"chkhsh: {chkhsh}")
|
| 317 |
+
|
| 318 |
+
# print the "pre_tokenizer" content from the tokenizer.json
|
| 319 |
+
with open(f"models/tokenizers/{name}/tokenizer.json", "r", encoding="utf-8") as f:
|
| 320 |
+
cfg = json.load(f)
|
| 321 |
+
normalizer = cfg["normalizer"]
|
| 322 |
+
logger.info("normalizer: " + json.dumps(normalizer, indent=4))
|
| 323 |
+
pre_tokenizer = cfg["pre_tokenizer"]
|
| 324 |
+
logger.info("pre_tokenizer: " + json.dumps(pre_tokenizer, indent=4))
|
| 325 |
+
if "ignore_merges" in cfg["model"]:
|
| 326 |
+
logger.info("ignore_merges: " + json.dumps(cfg["model"]["ignore_merges"], indent=4))
|
| 327 |
+
|
| 328 |
+
logger.info("")
|
| 329 |
+
|
| 330 |
+
src_ifs += f" if chkhsh == \"{chkhsh}\":\n"
|
| 331 |
+
src_ifs += f" # ref: {model['repo']}\n"
|
| 332 |
+
src_ifs += f" res = \"{name}\"\n"
|
| 333 |
+
|
| 334 |
+
src_func = f"""
|
| 335 |
+
def get_vocab_base_pre(self, tokenizer) -> str:
|
| 336 |
+
# encoding this string and hashing the resulting tokens would (hopefully) give us a unique identifier that
|
| 337 |
+
# is specific for the BPE pre-tokenizer used by the model
|
| 338 |
+
# we will use this unique identifier to write a "tokenizer.ggml.pre" entry in the GGUF file which we can
|
| 339 |
+
# use in llama.cpp to implement the same pre-tokenizer
|
| 340 |
+
|
| 341 |
+
chktxt = {repr(CHK_TXT)}
|
| 342 |
+
|
| 343 |
+
chktok = tokenizer.encode(chktxt)
|
| 344 |
+
chkhsh = sha256(str(chktok).encode()).hexdigest()
|
| 345 |
+
|
| 346 |
+
logger.debug(f"chktok: {{chktok}}")
|
| 347 |
+
logger.debug(f"chkhsh: {{chkhsh}}")
|
| 348 |
+
|
| 349 |
+
res = None
|
| 350 |
+
|
| 351 |
+
# NOTE: if you get an error here, you need to update the convert_hf_to_gguf_update.py script
|
| 352 |
+
# or pull the latest version of the model from Huggingface
|
| 353 |
+
# don't edit the hashes manually!
|
| 354 |
+
{src_ifs}
|
| 355 |
+
if res is None:
|
| 356 |
+
logger.warning("\\n")
|
| 357 |
+
logger.warning("**************************************************************************************")
|
| 358 |
+
logger.warning("** WARNING: The BPE pre-tokenizer was not recognized!")
|
| 359 |
+
logger.warning("** There are 2 possible reasons for this:")
|
| 360 |
+
logger.warning("** - the model has not been added to convert_hf_to_gguf_update.py yet")
|
| 361 |
+
logger.warning("** - the pre-tokenization config has changed upstream")
|
| 362 |
+
logger.warning("** Check your model files and convert_hf_to_gguf_update.py and update them accordingly.")
|
| 363 |
+
logger.warning("** ref: https://github.com/ggml-org/llama.cpp/pull/6920")
|
| 364 |
+
logger.warning("**")
|
| 365 |
+
logger.warning(f"** chkhsh: {{chkhsh}}")
|
| 366 |
+
logger.warning("**************************************************************************************")
|
| 367 |
+
logger.warning("\\n")
|
| 368 |
+
raise NotImplementedError("BPE pre-tokenizer was not recognized - update get_vocab_base_pre()")
|
| 369 |
+
|
| 370 |
+
logger.debug(f"tokenizer.ggml.pre: {{repr(res)}}")
|
| 371 |
+
logger.debug(f"chkhsh: {{chkhsh}}")
|
| 372 |
+
|
| 373 |
+
return res
|
| 374 |
+
"""
|
| 375 |
+
|
| 376 |
+
convert_py = re.sub(
|
| 377 |
+
r"(# Marker: Start get_vocab_base_pre)(.+?)( +# Marker: End get_vocab_base_pre)",
|
| 378 |
+
lambda m: m.group(1) + src_func + m.group(3),
|
| 379 |
+
convert_py,
|
| 380 |
+
flags=re.DOTALL | re.MULTILINE,
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
convert_py_pth.write_text(convert_py, encoding="utf-8")
|
| 384 |
+
|
| 385 |
+
logger.info(f"+++ {convert_py_pth} was updated")
|
| 386 |
+
|
| 387 |
+
# generate tests for each tokenizer model
|
| 388 |
+
|
| 389 |
+
tests = [
|
| 390 |
+
"ied 4 ½ months",
|
| 391 |
+
"Äpfel",
|
| 392 |
+
"",
|
| 393 |
+
" ",
|
| 394 |
+
" ",
|
| 395 |
+
" ",
|
| 396 |
+
"\t",
|
| 397 |
+
"\n",
|
| 398 |
+
"\n\n",
|
| 399 |
+
"\n\n\n",
|
| 400 |
+
"\t\n",
|
| 401 |
+
"Hello world",
|
| 402 |
+
" Hello world",
|
| 403 |
+
"Hello World",
|
| 404 |
+
" Hello World",
|
| 405 |
+
" Hello World!",
|
| 406 |
+
"Hello, world!",
|
| 407 |
+
" Hello, world!",
|
| 408 |
+
" this is 🦙.cpp",
|
| 409 |
+
"w048 7tuijk dsdfhu",
|
| 410 |
+
"нещо на Български",
|
| 411 |
+
"កាន់តែពិសេសអាចខលចេញ",
|
| 412 |
+
"🚀 (normal) 😶🌫️ (multiple emojis concatenated) ✅ (only emoji that has its own token)",
|
| 413 |
+
"Hello",
|
| 414 |
+
" Hello",
|
| 415 |
+
" Hello",
|
| 416 |
+
" Hello",
|
| 417 |
+
" Hello",
|
| 418 |
+
" Hello\n Hello",
|
| 419 |
+
" (",
|
| 420 |
+
"\n =",
|
| 421 |
+
"' era",
|
| 422 |
+
"Hello, y'all! How are you 😁 ?我想在apple工作1314151天~",
|
| 423 |
+
"!!!!!!",
|
| 424 |
+
"3",
|
| 425 |
+
"33",
|
| 426 |
+
"333",
|
| 427 |
+
"3333",
|
| 428 |
+
"33333",
|
| 429 |
+
"333333",
|
| 430 |
+
"3333333",
|
| 431 |
+
"33333333",
|
| 432 |
+
"333333333",
|
| 433 |
+
"Cửa Việt", # llama-bpe fails on this
|
| 434 |
+
" discards",
|
| 435 |
+
CHK_TXT,
|
| 436 |
+
]
|
| 437 |
+
|
| 438 |
+
# write the tests to ./models/ggml-vocab-{name}.gguf.inp
|
| 439 |
+
# the format is:
|
| 440 |
+
#
|
| 441 |
+
# test0
|
| 442 |
+
# __ggml_vocab_test__
|
| 443 |
+
# test1
|
| 444 |
+
# __ggml_vocab_test__
|
| 445 |
+
# ...
|
| 446 |
+
#
|
| 447 |
+
|
| 448 |
+
# with each model, encode all tests and write the results in ./models/ggml-vocab-{name}.gguf.out
|
| 449 |
+
# for each test, write the resulting tokens on a separate line
|
| 450 |
+
|
| 451 |
+
for model in models:
|
| 452 |
+
name = model["name"]
|
| 453 |
+
tokt = model["tokt"]
|
| 454 |
+
|
| 455 |
+
# Skip if the tokenizer folder does not exist or there are other download issues previously
|
| 456 |
+
if not os.path.exists(f"models/tokenizers/{name}"):
|
| 457 |
+
logger.warning(f"Directory for tokenizer {name} not found. Skipping...")
|
| 458 |
+
continue
|
| 459 |
+
|
| 460 |
+
# create the tokenizer
|
| 461 |
+
try:
|
| 462 |
+
if name == "t5":
|
| 463 |
+
tokenizer = AutoTokenizer.from_pretrained(f"models/tokenizers/{name}", use_fast=False)
|
| 464 |
+
else:
|
| 465 |
+
tokenizer = AutoTokenizer.from_pretrained(f"models/tokenizers/{name}")
|
| 466 |
+
except (OSError, TypeError) as e:
|
| 467 |
+
logger.error(f"Failed to load tokenizer for model {name}. Error: {e}")
|
| 468 |
+
continue # Skip this model and continue with the next one in the loop
|
| 469 |
+
|
| 470 |
+
if not os.path.exists(f"models/ggml-vocab-{name}.gguf"):
|
| 471 |
+
logger.info(f"Skip vocab files for model {name}, no GGUF file found")
|
| 472 |
+
continue
|
| 473 |
+
|
| 474 |
+
with open(f"models/ggml-vocab-{name}.gguf.inp", "w", encoding="utf-8") as f:
|
| 475 |
+
for text in tests:
|
| 476 |
+
f.write(f"{text}")
|
| 477 |
+
f.write("\n__ggml_vocab_test__\n")
|
| 478 |
+
|
| 479 |
+
with open(f"models/ggml-vocab-{name}.gguf.out", "w") as f:
|
| 480 |
+
for text in tests:
|
| 481 |
+
res = tokenizer.encode(text, add_special_tokens=False) # ty: ignore[unresolved-attribute]
|
| 482 |
+
for r in res:
|
| 483 |
+
f.write(f" {r}")
|
| 484 |
+
f.write("\n")
|
| 485 |
+
|
| 486 |
+
logger.info(f"Tests for {name} written in ./models/ggml-vocab-{name}.gguf.*")
|
| 487 |
+
|
| 488 |
+
# generate commands for creating vocab files
|
| 489 |
+
|
| 490 |
+
logger.info("\nRun the following commands to generate the vocab files for testing:\n")
|
| 491 |
+
|
| 492 |
+
for model in models:
|
| 493 |
+
name = model["name"]
|
| 494 |
+
|
| 495 |
+
print(f"python3 convert_hf_to_gguf.py models/tokenizers/{name}/ --outfile models/ggml-vocab-{name}.gguf --vocab-only") # noqa: NP100
|
| 496 |
+
|
| 497 |
+
logger.info("\n")
|
llama.cpp/convert_llama_ggml_to_gguf.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import logging
|
| 5 |
+
import argparse
|
| 6 |
+
import os
|
| 7 |
+
import struct
|
| 8 |
+
import sys
|
| 9 |
+
from enum import IntEnum
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
|
| 14 |
+
if 'NO_LOCAL_GGUF' not in os.environ:
|
| 15 |
+
sys.path.insert(1, str(Path(__file__).parent / 'gguf-py'))
|
| 16 |
+
import gguf
|
| 17 |
+
|
| 18 |
+
logger = logging.getLogger("ggml-to-gguf")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class GGMLFormat(IntEnum):
|
| 22 |
+
GGML = 0
|
| 23 |
+
GGMF = 1
|
| 24 |
+
GGJT = 2
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class GGMLFType(IntEnum):
|
| 28 |
+
ALL_F32 = 0
|
| 29 |
+
MOSTLY_F16 = 1
|
| 30 |
+
MOSTLY_Q4_0 = 2
|
| 31 |
+
MOSTLY_Q4_1 = 3
|
| 32 |
+
MOSTLY_Q4_1_SOME_F16 = 4
|
| 33 |
+
MOSTLY_Q8_0 = 7
|
| 34 |
+
MOSTLY_Q5_0 = 8
|
| 35 |
+
MOSTLY_Q5_1 = 9
|
| 36 |
+
MOSTLY_Q2_K = 10
|
| 37 |
+
MOSTLY_Q3_K_S = 11
|
| 38 |
+
MOSTLY_Q3_K_M = 12
|
| 39 |
+
MOSTLY_Q3_K_L = 13
|
| 40 |
+
MOSTLY_Q4_K_S = 14
|
| 41 |
+
MOSTLY_Q4_K_M = 15
|
| 42 |
+
MOSTLY_Q5_K_S = 16
|
| 43 |
+
MOSTLY_Q5_K_M = 17
|
| 44 |
+
MOSTLY_Q6_K = 18
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class Hyperparameters:
|
| 48 |
+
def __init__(self):
|
| 49 |
+
self.n_vocab = self.n_embd = self.n_mult = self.n_head = 0
|
| 50 |
+
self.n_layer = self.n_rot = self.n_ff = 0
|
| 51 |
+
self.ftype = GGMLFType.ALL_F32
|
| 52 |
+
|
| 53 |
+
def set_n_ff(self, model):
|
| 54 |
+
ff_tensor_idx = model.tensor_map.get(b'layers.0.feed_forward.w1.weight')
|
| 55 |
+
assert ff_tensor_idx is not None, 'Missing layer 0 FF tensor'
|
| 56 |
+
ff_tensor = model.tensors[ff_tensor_idx]
|
| 57 |
+
self.n_ff = ff_tensor.dims[1]
|
| 58 |
+
|
| 59 |
+
def load(self, data, offset):
|
| 60 |
+
(
|
| 61 |
+
self.n_vocab,
|
| 62 |
+
self.n_embd,
|
| 63 |
+
self.n_mult,
|
| 64 |
+
self.n_head,
|
| 65 |
+
self.n_layer,
|
| 66 |
+
self.n_rot,
|
| 67 |
+
ftype,
|
| 68 |
+
) = struct.unpack('<7I', data[offset:offset + (4 * 7)])
|
| 69 |
+
try:
|
| 70 |
+
self.ftype = GGMLFType(ftype)
|
| 71 |
+
except ValueError:
|
| 72 |
+
raise ValueError(f'Invalid ftype {ftype}')
|
| 73 |
+
return 4 * 7
|
| 74 |
+
|
| 75 |
+
def __str__(self):
|
| 76 |
+
return f'<Hyperparameters: n_vocab={self.n_vocab}, n_embd={self.n_embd}, n_mult={self.n_mult}, n_head={self.n_head}, n_layer={self.n_layer}, n_rot={self.n_rot}, n_ff={self.n_ff}, ftype={self.ftype.name}>'
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class Vocab:
|
| 80 |
+
def __init__(self, load_scores = True):
|
| 81 |
+
self.items = []
|
| 82 |
+
self.load_scores = load_scores
|
| 83 |
+
|
| 84 |
+
def load(self, data, offset, n_vocab):
|
| 85 |
+
orig_offset = offset
|
| 86 |
+
for _ in range(n_vocab):
|
| 87 |
+
itemlen = struct.unpack('<I', data[offset:offset + 4])[0]
|
| 88 |
+
assert itemlen < 4096, 'Absurd vocab item length'
|
| 89 |
+
offset += 4
|
| 90 |
+
item_text = bytes(data[offset:offset + itemlen])
|
| 91 |
+
offset += itemlen
|
| 92 |
+
if self.load_scores:
|
| 93 |
+
item_score = struct.unpack('<f', data[offset:offset + 4])[0]
|
| 94 |
+
offset += 4
|
| 95 |
+
else:
|
| 96 |
+
item_score = 0.0
|
| 97 |
+
self.items.append((item_text, item_score))
|
| 98 |
+
return offset - orig_offset
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
class Tensor:
|
| 102 |
+
def __init__(self, use_padding = True):
|
| 103 |
+
self.name = None
|
| 104 |
+
self.dims: tuple[int, ...] = ()
|
| 105 |
+
self.dtype = None
|
| 106 |
+
self.start_offset = 0
|
| 107 |
+
self.len_bytes = np.int64(0)
|
| 108 |
+
self.use_padding = use_padding
|
| 109 |
+
|
| 110 |
+
def load(self, data, offset):
|
| 111 |
+
orig_offset = offset
|
| 112 |
+
(n_dims, name_len, dtype) = struct.unpack('<3I', data[offset:offset + 12])
|
| 113 |
+
assert n_dims >= 0 and n_dims <= 4, f'Invalid tensor dimensions {n_dims}'
|
| 114 |
+
assert name_len < 4096, 'Absurd tensor name length'
|
| 115 |
+
self.dtype = gguf.GGMLQuantizationType(dtype)
|
| 116 |
+
quant = gguf.GGML_QUANT_SIZES.get(self.dtype)
|
| 117 |
+
assert quant is not None, 'Unknown tensor type'
|
| 118 |
+
(blksize, tysize) = quant
|
| 119 |
+
offset += 12
|
| 120 |
+
self.dims = struct.unpack(f'<{n_dims}I', data[offset:offset + (4 * n_dims)])
|
| 121 |
+
offset += 4 * n_dims
|
| 122 |
+
self.name = bytes(data[offset:offset + name_len])
|
| 123 |
+
offset += name_len
|
| 124 |
+
pad = ((offset + 31) & ~31) - offset if self.use_padding else 0
|
| 125 |
+
offset += pad
|
| 126 |
+
n_elems = np.prod(self.dims)
|
| 127 |
+
n_bytes = np.int64(np.int64(n_elems) * np.int64(tysize)) // np.int64(blksize)
|
| 128 |
+
self.start_offset = offset
|
| 129 |
+
self.len_bytes = n_bytes
|
| 130 |
+
offset += n_bytes
|
| 131 |
+
return offset - orig_offset
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
class GGMLModel:
|
| 135 |
+
|
| 136 |
+
file_format: GGMLFormat
|
| 137 |
+
format_version: int
|
| 138 |
+
|
| 139 |
+
def __init__(self):
|
| 140 |
+
self.hyperparameters = None
|
| 141 |
+
self.vocab = None
|
| 142 |
+
self.tensor_map = {}
|
| 143 |
+
self.tensors = []
|
| 144 |
+
|
| 145 |
+
def validate_header(self, data, offset):
|
| 146 |
+
magic = bytes(data[offset:offset + 4])
|
| 147 |
+
if magic == b'GGUF':
|
| 148 |
+
raise ValueError('File is already in GGUF format.')
|
| 149 |
+
if magic == b'lmgg':
|
| 150 |
+
self.file_format = GGMLFormat.GGML
|
| 151 |
+
self.format_version = 1
|
| 152 |
+
return 4
|
| 153 |
+
version = struct.unpack('<I', data[offset + 4:offset + 8])[0]
|
| 154 |
+
if magic == b'fmgg':
|
| 155 |
+
if version != 1:
|
| 156 |
+
raise ValueError(f'Cannot handle unexpected GGMF file version {version}')
|
| 157 |
+
self.file_format = GGMLFormat.GGMF
|
| 158 |
+
self.format_version = version
|
| 159 |
+
return 8
|
| 160 |
+
if magic == b'tjgg':
|
| 161 |
+
if version < 1 or version > 3:
|
| 162 |
+
raise ValueError(f'Cannot handle unexpected GGJT file version {version}')
|
| 163 |
+
self.file_format = GGMLFormat.GGJT
|
| 164 |
+
self.format_version = version
|
| 165 |
+
return 8
|
| 166 |
+
raise ValueError(f"Unexpected file magic {magic!r}! This doesn't look like a GGML format file.")
|
| 167 |
+
|
| 168 |
+
def validate_conversion(self, ftype):
|
| 169 |
+
err = ''
|
| 170 |
+
if (self.file_format < GGMLFormat.GGJT or self.format_version < 2):
|
| 171 |
+
if ftype not in (GGMLFType.ALL_F32, GGMLFType.MOSTLY_F16):
|
| 172 |
+
err = 'Quantizations changed in GGJTv2. Can only convert unquantized GGML files older than GGJTv2.'
|
| 173 |
+
elif (self.file_format == GGMLFormat.GGJT and self.format_version == 2):
|
| 174 |
+
if ftype in (GGMLFType.MOSTLY_Q4_0, GGMLFType.MOSTLY_Q4_1,
|
| 175 |
+
GGMLFType.MOSTLY_Q4_1_SOME_F16, GGMLFType.MOSTLY_Q8_0):
|
| 176 |
+
err = 'Q4 and Q8 quantizations changed in GGJTv3.'
|
| 177 |
+
if len(err) > 0:
|
| 178 |
+
raise ValueError(f'{err} Sorry, your {self.file_format.name}v{self.format_version} file of type {ftype.name} is not eligible for conversion.')
|
| 179 |
+
|
| 180 |
+
def load(self, data, offset):
|
| 181 |
+
offset += self.validate_header(data, offset)
|
| 182 |
+
hp = Hyperparameters()
|
| 183 |
+
offset += hp.load(data, offset)
|
| 184 |
+
logger.info(f'* File format: {self.file_format.name}v{self.format_version} with ftype {hp.ftype.name}')
|
| 185 |
+
self.validate_conversion(hp.ftype)
|
| 186 |
+
vocab = Vocab(load_scores = self.file_format > GGMLFormat.GGML)
|
| 187 |
+
offset += vocab.load(data, offset, hp.n_vocab)
|
| 188 |
+
tensors: list[Tensor] = []
|
| 189 |
+
tensor_map = {}
|
| 190 |
+
while offset < len(data):
|
| 191 |
+
tensor = Tensor(use_padding = self.file_format > GGMLFormat.GGMF)
|
| 192 |
+
offset += tensor.load(data, offset)
|
| 193 |
+
tensor_map[tensor.name] = len(tensors)
|
| 194 |
+
tensors.append(tensor)
|
| 195 |
+
self.hyperparameters = hp
|
| 196 |
+
self.vocab = vocab
|
| 197 |
+
self.tensors = tensors
|
| 198 |
+
self.tensor_map = tensor_map
|
| 199 |
+
hp.set_n_ff(self)
|
| 200 |
+
return offset
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
class GGMLToGGUF:
|
| 204 |
+
def __init__(self, ggml_model, data, cfg, params_override = None, vocab_override = None, special_vocab = None):
|
| 205 |
+
hp = ggml_model.hyperparameters
|
| 206 |
+
self.model = ggml_model
|
| 207 |
+
self.data = data
|
| 208 |
+
self.cfg = cfg
|
| 209 |
+
self.params_override = params_override
|
| 210 |
+
self.vocab_override = vocab_override
|
| 211 |
+
self.special_vocab = special_vocab
|
| 212 |
+
if params_override is not None:
|
| 213 |
+
n_kv_head = params_override.n_head_kv
|
| 214 |
+
else:
|
| 215 |
+
if cfg.gqa == 1:
|
| 216 |
+
n_kv_head = hp.n_head
|
| 217 |
+
else:
|
| 218 |
+
gqa = float(cfg.gqa)
|
| 219 |
+
n_kv_head = None
|
| 220 |
+
for x in range(1, 256):
|
| 221 |
+
if float(hp.n_head) / float(x) == gqa:
|
| 222 |
+
n_kv_head = x
|
| 223 |
+
assert n_kv_head is not None, "Couldn't determine n_kv_head from GQA param"
|
| 224 |
+
logger.info(f'- Guessed n_kv_head = {n_kv_head} based on GQA {cfg.gqa}')
|
| 225 |
+
self.n_kv_head = n_kv_head
|
| 226 |
+
self.name_map = gguf.get_tensor_name_map(gguf.MODEL_ARCH.LLAMA, ggml_model.hyperparameters.n_layer)
|
| 227 |
+
|
| 228 |
+
def save(self):
|
| 229 |
+
logger.info('* Preparing to save GGUF file')
|
| 230 |
+
gguf_writer = gguf.GGUFWriter(
|
| 231 |
+
self.cfg.output,
|
| 232 |
+
gguf.MODEL_ARCH_NAMES[gguf.MODEL_ARCH.LLAMA],
|
| 233 |
+
use_temp_file = False)
|
| 234 |
+
self.add_params(gguf_writer)
|
| 235 |
+
self.add_vocab(gguf_writer)
|
| 236 |
+
if self.special_vocab is not None:
|
| 237 |
+
self.special_vocab.add_to_gguf(gguf_writer)
|
| 238 |
+
self.add_tensors(gguf_writer)
|
| 239 |
+
logger.info(" gguf: write header")
|
| 240 |
+
gguf_writer.write_header_to_file()
|
| 241 |
+
logger.info(" gguf: write metadata")
|
| 242 |
+
gguf_writer.write_kv_data_to_file()
|
| 243 |
+
logger.info(" gguf: write tensors")
|
| 244 |
+
gguf_writer.write_tensors_to_file()
|
| 245 |
+
gguf_writer.close()
|
| 246 |
+
|
| 247 |
+
def add_params(self, gguf_writer):
|
| 248 |
+
hp = self.model.hyperparameters
|
| 249 |
+
cfg = self.cfg
|
| 250 |
+
if cfg.desc is not None:
|
| 251 |
+
desc = cfg.desc
|
| 252 |
+
else:
|
| 253 |
+
desc = f'converted from legacy {self.model.file_format.name}v{self.model.format_version} {hp.ftype.name} format'
|
| 254 |
+
try:
|
| 255 |
+
# Filenames aren't necessarily valid UTF8.
|
| 256 |
+
name = cfg.name if cfg.name is not None else cfg.input.name
|
| 257 |
+
except UnicodeDecodeError:
|
| 258 |
+
name = None
|
| 259 |
+
logger.info('* Adding model parameters and KV items')
|
| 260 |
+
if name is not None:
|
| 261 |
+
gguf_writer.add_name(name)
|
| 262 |
+
gguf_writer.add_description(desc)
|
| 263 |
+
gguf_writer.add_file_type(int(hp.ftype))
|
| 264 |
+
if self.params_override is not None:
|
| 265 |
+
po = self.params_override
|
| 266 |
+
assert po.n_embd == hp.n_embd, 'Model hyperparams mismatch'
|
| 267 |
+
assert po.n_layer == hp.n_layer, 'Model hyperparams mismatch'
|
| 268 |
+
assert po.n_head == hp.n_head, 'Model hyperparams mismatch'
|
| 269 |
+
gguf_writer.add_context_length (po.n_ctx)
|
| 270 |
+
gguf_writer.add_embedding_length (po.n_embd)
|
| 271 |
+
gguf_writer.add_block_count (po.n_layer)
|
| 272 |
+
gguf_writer.add_feed_forward_length (po.n_ff)
|
| 273 |
+
gguf_writer.add_rope_dimension_count(po.n_embd // po.n_head)
|
| 274 |
+
gguf_writer.add_head_count (po.n_head)
|
| 275 |
+
gguf_writer.add_head_count_kv (po.n_head_kv)
|
| 276 |
+
gguf_writer.add_layer_norm_rms_eps (po.f_norm_eps)
|
| 277 |
+
return
|
| 278 |
+
gguf_writer.add_context_length(cfg.context_length)
|
| 279 |
+
gguf_writer.add_embedding_length(hp.n_embd)
|
| 280 |
+
gguf_writer.add_block_count(hp.n_layer)
|
| 281 |
+
gguf_writer.add_feed_forward_length(hp.n_ff)
|
| 282 |
+
gguf_writer.add_rope_dimension_count(hp.n_embd // hp.n_head)
|
| 283 |
+
gguf_writer.add_head_count(hp.n_head)
|
| 284 |
+
gguf_writer.add_head_count_kv(self.n_kv_head)
|
| 285 |
+
gguf_writer.add_layer_norm_rms_eps(float(cfg.eps))
|
| 286 |
+
|
| 287 |
+
def add_vocab(self, gguf_writer):
|
| 288 |
+
hp = self.model.hyperparameters
|
| 289 |
+
gguf_writer.add_tokenizer_model('llama')
|
| 290 |
+
gguf_writer.add_tokenizer_pre('default')
|
| 291 |
+
tokens = []
|
| 292 |
+
scores = []
|
| 293 |
+
toktypes = []
|
| 294 |
+
if self.vocab_override is not None:
|
| 295 |
+
vo = self.vocab_override
|
| 296 |
+
logger.info('* Adding vocab item(s)')
|
| 297 |
+
for (_, (vbytes, score, ttype)) in enumerate(vo.all_tokens()):
|
| 298 |
+
tokens.append(vbytes)
|
| 299 |
+
scores.append(score)
|
| 300 |
+
toktypes.append(ttype)
|
| 301 |
+
assert len(tokens) == hp.n_vocab, \
|
| 302 |
+
f'Override vocab has a different number of items than hyperparameters - override = {len(tokens)} but n_vocab={hp.n_vocab}'
|
| 303 |
+
gguf_writer.add_token_list(tokens)
|
| 304 |
+
gguf_writer.add_token_scores(scores)
|
| 305 |
+
if len(toktypes) > 0:
|
| 306 |
+
gguf_writer.add_token_types(toktypes)
|
| 307 |
+
return
|
| 308 |
+
logger.info(f'* Adding {hp.n_vocab} vocab item(s)')
|
| 309 |
+
assert len(self.model.vocab.items) >= 3, 'Cannot handle unexpectedly short model vocab'
|
| 310 |
+
for (tokid, (vbytes, vscore)) in enumerate(self.model.vocab.items):
|
| 311 |
+
tt = 1 # Normal
|
| 312 |
+
# Special handling for UNK, BOS, EOS tokens.
|
| 313 |
+
if tokid <= 2:
|
| 314 |
+
if tokid == 0:
|
| 315 |
+
vbytes = b'<unk>'
|
| 316 |
+
tt = 2
|
| 317 |
+
elif tokid == 1:
|
| 318 |
+
vbytes = b'<s>'
|
| 319 |
+
tt = 3
|
| 320 |
+
else:
|
| 321 |
+
vbytes = b'</s>'
|
| 322 |
+
tt = 3
|
| 323 |
+
elif len(vbytes) == 0:
|
| 324 |
+
tt = 3 # Control
|
| 325 |
+
elif tokid >= 3 and tokid <= 258 and len(vbytes) == 1:
|
| 326 |
+
vbytes = bytes(f'<0x{vbytes[0]:02X}>', encoding = 'UTF-8')
|
| 327 |
+
tt = 6 # Byte
|
| 328 |
+
else:
|
| 329 |
+
vbytes = vbytes.replace(b' ', b'\xe2\x96\x81')
|
| 330 |
+
toktypes.append(tt)
|
| 331 |
+
tokens.append(vbytes)
|
| 332 |
+
scores.append(vscore)
|
| 333 |
+
gguf_writer.add_token_list(tokens)
|
| 334 |
+
gguf_writer.add_token_scores(scores)
|
| 335 |
+
gguf_writer.add_token_types(toktypes)
|
| 336 |
+
gguf_writer.add_unk_token_id(0)
|
| 337 |
+
gguf_writer.add_bos_token_id(1)
|
| 338 |
+
gguf_writer.add_eos_token_id(2)
|
| 339 |
+
|
| 340 |
+
def add_tensors(self, gguf_writer):
|
| 341 |
+
tensor_map = self.name_map
|
| 342 |
+
data = self.data
|
| 343 |
+
logger.info(f'* Adding {len(self.model.tensors)} tensor(s)')
|
| 344 |
+
for tensor in self.model.tensors:
|
| 345 |
+
name = str(tensor.name, 'UTF-8')
|
| 346 |
+
mapped_name = tensor_map.get_name(name, try_suffixes = (".weight", ".bias"))
|
| 347 |
+
assert mapped_name is not None, f'Bad name {name}'
|
| 348 |
+
tempdims = list(tensor.dims[:])
|
| 349 |
+
if len(tempdims) > 1:
|
| 350 |
+
temp = tempdims[1]
|
| 351 |
+
tempdims[1] = tempdims[0]
|
| 352 |
+
tempdims[0] = temp
|
| 353 |
+
gguf_writer.add_tensor(
|
| 354 |
+
mapped_name,
|
| 355 |
+
data[tensor.start_offset:tensor.start_offset + tensor.len_bytes],
|
| 356 |
+
raw_shape = tempdims,
|
| 357 |
+
raw_dtype = tensor.dtype)
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
def handle_metadata(cfg, hp):
|
| 361 |
+
import examples.convert_legacy_llama as convert
|
| 362 |
+
|
| 363 |
+
assert cfg.model_metadata_dir.is_dir(), 'Metadata dir is not a directory'
|
| 364 |
+
hf_config_path = cfg.model_metadata_dir / "config.json"
|
| 365 |
+
orig_config_path = cfg.model_metadata_dir / "params.json"
|
| 366 |
+
# We pass a fake model here. "original" mode will check the shapes of some
|
| 367 |
+
# tensors if information is missing in the .json file: other than that, the
|
| 368 |
+
# model data isn't used so this should be safe (at least for now).
|
| 369 |
+
fakemodel = {
|
| 370 |
+
'tok_embeddings.weight': convert.LazyTensor.__new__(convert.LazyTensor),
|
| 371 |
+
'layers.0.feed_forward.w1.weight': convert.LazyTensor.__new__(convert.LazyTensor),
|
| 372 |
+
}
|
| 373 |
+
fakemodel['tok_embeddings.weight'].shape = [hp.n_vocab]
|
| 374 |
+
fakemodel['layers.0.feed_forward.w1.weight'].shape = [hp.n_ff]
|
| 375 |
+
if hf_config_path.exists():
|
| 376 |
+
params = convert.Params.loadHFTransformerJson(fakemodel, hf_config_path)
|
| 377 |
+
elif orig_config_path.exists():
|
| 378 |
+
params = convert.Params.loadOriginalParamsJson(fakemodel, orig_config_path)
|
| 379 |
+
else:
|
| 380 |
+
raise ValueError('Unable to load metadata')
|
| 381 |
+
vocab_path = Path(cfg.vocab_dir if cfg.vocab_dir is not None else cfg.model_metadata_dir)
|
| 382 |
+
vocab_factory = convert.VocabFactory(vocab_path)
|
| 383 |
+
vocab, special_vocab = vocab_factory.load_vocab(cfg.vocabtype.split(","), cfg.model_metadata_dir)
|
| 384 |
+
convert.check_vocab_size(params, vocab)
|
| 385 |
+
return params, vocab, special_vocab
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def handle_args():
|
| 389 |
+
parser = argparse.ArgumentParser(description = 'Convert GGML models to GGUF')
|
| 390 |
+
parser.add_argument('--input', '-i', type = Path, required = True,
|
| 391 |
+
help = 'Input GGMLv3 filename')
|
| 392 |
+
parser.add_argument('--output', '-o', type = Path, required = True,
|
| 393 |
+
help ='Output GGUF filename')
|
| 394 |
+
parser.add_argument('--name',
|
| 395 |
+
help = 'Set model name')
|
| 396 |
+
parser.add_argument('--desc',
|
| 397 |
+
help = 'Set model description')
|
| 398 |
+
parser.add_argument('--gqa', type = int, default = 1,
|
| 399 |
+
help = 'grouped-query attention factor (use 8 for LLaMA2 70B)')
|
| 400 |
+
parser.add_argument('--eps', default = '5.0e-06',
|
| 401 |
+
help = 'RMS norm eps: Use 1e-6 for LLaMA1 and OpenLLaMA, use 1e-5 for LLaMA2')
|
| 402 |
+
parser.add_argument('--context-length', '-c', type=int, default = 2048,
|
| 403 |
+
help = 'Default max context length: LLaMA1 is typically 2048, LLaMA2 is typically 4096')
|
| 404 |
+
parser.add_argument('--model-metadata-dir', '-m', type = Path,
|
| 405 |
+
help ='Load HuggingFace/.pth vocab and metadata from the specified directory')
|
| 406 |
+
parser.add_argument("--vocab-dir", type=Path,
|
| 407 |
+
help="directory containing tokenizer.model, if separate from model file - only meaningful with --model-metadata-dir")
|
| 408 |
+
parser.add_argument("--vocabtype", default="spm,hfft",
|
| 409 |
+
help="vocab format - only meaningful with --model-metadata-dir and/or --vocab-dir (default: spm,hfft)")
|
| 410 |
+
parser.add_argument("--verbose", action="store_true", help="increase output verbosity")
|
| 411 |
+
return parser.parse_args()
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
def main():
|
| 415 |
+
cfg = handle_args()
|
| 416 |
+
logging.basicConfig(level=logging.DEBUG if cfg.verbose else logging.INFO)
|
| 417 |
+
logger.info(f'* Using config: {cfg}')
|
| 418 |
+
logger.warning('=== WARNING === Be aware that this conversion script is best-effort. Use a native GGUF model if possible. === WARNING ===')
|
| 419 |
+
if cfg.model_metadata_dir is None and (cfg.gqa == 1 or cfg.eps == '5.0e-06'):
|
| 420 |
+
logger.info('- Note: If converting LLaMA2, specifying "--eps 1e-5" is required. 70B models also need "--gqa 8".')
|
| 421 |
+
data = np.memmap(cfg.input, mode = 'r')
|
| 422 |
+
model = GGMLModel()
|
| 423 |
+
logger.info('* Scanning GGML input file')
|
| 424 |
+
offset = model.load(data, 0) # noqa
|
| 425 |
+
logger.info(f'* GGML model hyperparameters: {model.hyperparameters}')
|
| 426 |
+
vocab_override = None
|
| 427 |
+
params_override = None
|
| 428 |
+
special_vocab = None
|
| 429 |
+
if cfg.model_metadata_dir is not None:
|
| 430 |
+
(params_override, vocab_override, special_vocab) = handle_metadata(cfg, model.hyperparameters)
|
| 431 |
+
logger.info('!! Note: When overriding params the --gqa, --eps and --context-length options are ignored.')
|
| 432 |
+
logger.info(f'* Overriding params: {params_override}')
|
| 433 |
+
logger.info(f'* Overriding vocab: {vocab_override}')
|
| 434 |
+
logger.info(f'* Special vocab: {special_vocab}')
|
| 435 |
+
else:
|
| 436 |
+
logger.warning('\n=== WARNING === Special tokens may not be converted correctly. Use --model-metadata-dir if possible === WARNING ===\n')
|
| 437 |
+
if model.file_format == GGMLFormat.GGML:
|
| 438 |
+
logger.info('! This is a very old GGML file that does not contain vocab scores. Strongly recommend using model metadata!')
|
| 439 |
+
converter = GGMLToGGUF(
|
| 440 |
+
model, data, cfg,
|
| 441 |
+
params_override = params_override,
|
| 442 |
+
vocab_override = vocab_override,
|
| 443 |
+
special_vocab = special_vocab
|
| 444 |
+
)
|
| 445 |
+
converter.save()
|
| 446 |
+
logger.info(f'* Successful completion. Output saved to: {cfg.output}')
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
if __name__ == '__main__':
|
| 450 |
+
main()
|
llama.cpp/convert_lora_to_gguf.py
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
import logging
|
| 8 |
+
import argparse
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
import json
|
| 12 |
+
from math import prod
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, Sequence, SupportsIndex, cast
|
| 15 |
+
from transformers import AutoConfig, AutoTokenizer
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
|
| 19 |
+
if TYPE_CHECKING:
|
| 20 |
+
from torch import Tensor
|
| 21 |
+
|
| 22 |
+
if 'NO_LOCAL_GGUF' not in os.environ:
|
| 23 |
+
sys.path.insert(1, str(Path(__file__).parent / 'gguf-py'))
|
| 24 |
+
import gguf
|
| 25 |
+
from gguf.constants import GGUFValueType
|
| 26 |
+
|
| 27 |
+
# reuse model definitions from the conversion/ package
|
| 28 |
+
from conversion import LazyTorchTensor, ModelBase, get_model_class, ModelType, get_model_architecture
|
| 29 |
+
|
| 30 |
+
logger = logging.getLogger("lora-to-gguf")
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@dataclass
|
| 34 |
+
class PartialLoraTensor:
|
| 35 |
+
A: Tensor | None = None
|
| 36 |
+
B: Tensor | None = None
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# magic to support tensor shape modifications and splitting
|
| 40 |
+
class LoraTorchTensor:
|
| 41 |
+
_lora_A: Tensor # (n_rank, row_size)
|
| 42 |
+
_lora_B: Tensor # (col_size, n_rank)
|
| 43 |
+
_rank: int
|
| 44 |
+
|
| 45 |
+
def __init__(self, A: Tensor, B: Tensor):
|
| 46 |
+
assert len(A.shape) == len(B.shape)
|
| 47 |
+
assert A.shape[-2] == B.shape[-1]
|
| 48 |
+
if A.dtype != B.dtype:
|
| 49 |
+
A = A.to(torch.float32)
|
| 50 |
+
B = B.to(torch.float32)
|
| 51 |
+
self._lora_A = A
|
| 52 |
+
self._lora_B = B
|
| 53 |
+
self._rank = B.shape[-1]
|
| 54 |
+
|
| 55 |
+
def get_lora_A_B(self) -> tuple[Tensor, Tensor]:
|
| 56 |
+
return (self._lora_A, self._lora_B)
|
| 57 |
+
|
| 58 |
+
def __getitem__(
|
| 59 |
+
self,
|
| 60 |
+
indices: (
|
| 61 |
+
SupportsIndex
|
| 62 |
+
| slice
|
| 63 |
+
| tuple[SupportsIndex | slice | Tensor, ...] # TODO: add ellipsis in the type signature
|
| 64 |
+
),
|
| 65 |
+
) -> LoraTorchTensor:
|
| 66 |
+
shape = self.shape
|
| 67 |
+
if isinstance(indices, SupportsIndex):
|
| 68 |
+
if len(shape) > 2:
|
| 69 |
+
return LoraTorchTensor(self._lora_A[indices], self._lora_B[indices])
|
| 70 |
+
else:
|
| 71 |
+
raise NotImplementedError # can't return a vector
|
| 72 |
+
elif isinstance(indices, slice):
|
| 73 |
+
if len(shape) > 2:
|
| 74 |
+
return LoraTorchTensor(self._lora_A[indices], self._lora_B[indices])
|
| 75 |
+
else:
|
| 76 |
+
return LoraTorchTensor(self._lora_A, self._lora_B[indices])
|
| 77 |
+
elif isinstance(indices, tuple):
|
| 78 |
+
assert len(indices) > 0
|
| 79 |
+
if indices[-1] is Ellipsis:
|
| 80 |
+
return self[indices[:-1]]
|
| 81 |
+
# expand ellipsis
|
| 82 |
+
indices = tuple(
|
| 83 |
+
u
|
| 84 |
+
for v in (
|
| 85 |
+
(
|
| 86 |
+
(slice(None, None) for _ in range(len(indices) - 1))
|
| 87 |
+
if i is Ellipsis
|
| 88 |
+
else (i,)
|
| 89 |
+
)
|
| 90 |
+
for i in indices
|
| 91 |
+
)
|
| 92 |
+
for u in v
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
if len(indices) < len(shape):
|
| 96 |
+
indices = (*indices, *(slice(None, None) for _ in range(len(indices), len(shape))))
|
| 97 |
+
|
| 98 |
+
# TODO: make sure this is correct
|
| 99 |
+
indices_A = (
|
| 100 |
+
*(
|
| 101 |
+
(
|
| 102 |
+
j.__index__() % self._lora_A.shape[i]
|
| 103 |
+
if isinstance(j, SupportsIndex)
|
| 104 |
+
else slice(None, None)
|
| 105 |
+
)
|
| 106 |
+
for i, j in enumerate(indices[:-2])
|
| 107 |
+
),
|
| 108 |
+
slice(None, None),
|
| 109 |
+
indices[-1],
|
| 110 |
+
)
|
| 111 |
+
indices_B = indices[:-1]
|
| 112 |
+
return LoraTorchTensor(self._lora_A[indices_A], self._lora_B[indices_B])
|
| 113 |
+
else:
|
| 114 |
+
raise NotImplementedError # unknown indice type
|
| 115 |
+
|
| 116 |
+
@property
|
| 117 |
+
def dtype(self) -> torch.dtype:
|
| 118 |
+
assert self._lora_A.dtype == self._lora_B.dtype
|
| 119 |
+
return self._lora_A.dtype
|
| 120 |
+
|
| 121 |
+
@property
|
| 122 |
+
def shape(self) -> tuple[int, ...]:
|
| 123 |
+
assert len(self._lora_A.shape) == len(self._lora_B.shape)
|
| 124 |
+
return (*self._lora_B.shape[:-1], self._lora_A.shape[-1])
|
| 125 |
+
|
| 126 |
+
def size(self, dim=None):
|
| 127 |
+
assert dim is None
|
| 128 |
+
return self.shape
|
| 129 |
+
|
| 130 |
+
def contiguous(self) -> LoraTorchTensor:
|
| 131 |
+
return LoraTorchTensor(
|
| 132 |
+
self._lora_A.contiguous(),
|
| 133 |
+
self._lora_B.contiguous(),
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
def reshape(self, *shape: int | tuple[int, ...]) -> LoraTorchTensor:
|
| 137 |
+
if isinstance(shape[0], tuple):
|
| 138 |
+
new_shape: tuple[int, ...] = shape[0]
|
| 139 |
+
else:
|
| 140 |
+
new_shape = cast(tuple[int, ...], shape)
|
| 141 |
+
orig_shape = self.shape
|
| 142 |
+
if len(new_shape) < 2:
|
| 143 |
+
raise NotImplementedError # can't become a vector
|
| 144 |
+
|
| 145 |
+
# expand -1 in the shape
|
| 146 |
+
if any(dim == -1 for dim in new_shape):
|
| 147 |
+
n_elems = prod(orig_shape)
|
| 148 |
+
n_new_elems = prod(dim if dim != -1 else 1 for dim in new_shape)
|
| 149 |
+
assert n_elems % n_new_elems == 0
|
| 150 |
+
new_shape = (*(dim if dim != -1 else n_elems // n_new_elems for dim in new_shape),)
|
| 151 |
+
|
| 152 |
+
if new_shape[-1] != orig_shape[-1]:
|
| 153 |
+
raise NotImplementedError # can't reshape the row size trivially
|
| 154 |
+
|
| 155 |
+
shape_A = (*(1 for _ in new_shape[:-2]), self._rank, orig_shape[-1])
|
| 156 |
+
shape_B = (*new_shape[:-1], self._rank)
|
| 157 |
+
return LoraTorchTensor(
|
| 158 |
+
self._lora_A.reshape(shape_A),
|
| 159 |
+
self._lora_B.reshape(shape_B),
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
def reshape_as(self, other: Tensor) -> LoraTorchTensor:
|
| 163 |
+
return self.reshape(*other.shape)
|
| 164 |
+
|
| 165 |
+
def view(self, *size: int) -> LoraTorchTensor:
|
| 166 |
+
return self.reshape(*size)
|
| 167 |
+
|
| 168 |
+
def permute(self, *dims: int) -> LoraTorchTensor:
|
| 169 |
+
shape = self.shape
|
| 170 |
+
dims = tuple(dim - len(shape) if dim >= 0 else dim for dim in dims)
|
| 171 |
+
if dims[-1] == -1:
|
| 172 |
+
# TODO: support higher dimensional A shapes bigger than 1
|
| 173 |
+
assert all(dim == 1 for dim in self._lora_A.shape[:-2])
|
| 174 |
+
return LoraTorchTensor(self._lora_A, self._lora_B.permute(*dims))
|
| 175 |
+
if len(shape) == 2 and dims[-1] == -2 and dims[-2] == -1:
|
| 176 |
+
return LoraTorchTensor(self._lora_B.permute(*dims), self._lora_A.permute(*dims))
|
| 177 |
+
else:
|
| 178 |
+
# TODO: compose the above two
|
| 179 |
+
raise NotImplementedError
|
| 180 |
+
|
| 181 |
+
def transpose(self, dim0: int, dim1: int) -> LoraTorchTensor:
|
| 182 |
+
shape = self.shape
|
| 183 |
+
dims = [i for i in range(len(shape))]
|
| 184 |
+
dims[dim0], dims[dim1] = dims[dim1], dims[dim0]
|
| 185 |
+
return self.permute(*dims)
|
| 186 |
+
|
| 187 |
+
def swapaxes(self, axis0: int, axis1: int) -> LoraTorchTensor:
|
| 188 |
+
return self.transpose(axis0, axis1)
|
| 189 |
+
|
| 190 |
+
def split(self, split_size: int | Sequence[int], dim: int = 0) -> tuple[LoraTorchTensor, ...]:
|
| 191 |
+
shape = self.shape
|
| 192 |
+
ndim = len(shape)
|
| 193 |
+
if dim < 0:
|
| 194 |
+
dim += ndim
|
| 195 |
+
if dim == ndim - 1:
|
| 196 |
+
A_chunks = self._lora_A.split(split_size, dim=-1)
|
| 197 |
+
return tuple(LoraTorchTensor(a, self._lora_B) for a in A_chunks)
|
| 198 |
+
elif dim == ndim - 2:
|
| 199 |
+
B_chunks = self._lora_B.split(split_size, dim=-2)
|
| 200 |
+
return tuple(LoraTorchTensor(self._lora_A, b) for b in B_chunks)
|
| 201 |
+
else:
|
| 202 |
+
B_chunks = self._lora_B.split(split_size, dim=dim)
|
| 203 |
+
if self._lora_A.shape[dim] == 1:
|
| 204 |
+
return tuple(LoraTorchTensor(self._lora_A, b) for b in B_chunks)
|
| 205 |
+
A_chunks = self._lora_A.split(split_size, dim=dim)
|
| 206 |
+
return tuple(LoraTorchTensor(a, b) for a, b in zip(A_chunks, B_chunks))
|
| 207 |
+
|
| 208 |
+
def to(self, *args, **kwargs):
|
| 209 |
+
return LoraTorchTensor(self._lora_A.to(*args, **kwargs), self._lora_B.to(*args, **kwargs))
|
| 210 |
+
|
| 211 |
+
def __mul__(self, other) -> LoraTorchTensor:
|
| 212 |
+
# Only output-side multiplication for now
|
| 213 |
+
# W = B @ A, so M_out * W == (M_out * B) @ A
|
| 214 |
+
if not isinstance(other, (int, float)) and other.shape and other.shape[-1] != 1:
|
| 215 |
+
raise NotImplementedError
|
| 216 |
+
return LoraTorchTensor(self._lora_A, self._lora_B * other)
|
| 217 |
+
|
| 218 |
+
def __rmul__(self, other) -> LoraTorchTensor:
|
| 219 |
+
return self * other
|
| 220 |
+
|
| 221 |
+
@classmethod
|
| 222 |
+
def __torch_function__(cls, func: Callable, types, args=(), kwargs=None):
|
| 223 |
+
del types # unused
|
| 224 |
+
|
| 225 |
+
if kwargs is None:
|
| 226 |
+
kwargs = {}
|
| 227 |
+
|
| 228 |
+
if func is torch.permute:
|
| 229 |
+
assert len(args)
|
| 230 |
+
return type(args[0]).permute(*args, **kwargs)
|
| 231 |
+
elif func is torch.reshape:
|
| 232 |
+
assert len(args)
|
| 233 |
+
return type(args[0]).reshape(*args, **kwargs)
|
| 234 |
+
elif func is torch.stack:
|
| 235 |
+
assert len(args)
|
| 236 |
+
assert isinstance(args[0], Sequence)
|
| 237 |
+
dim = kwargs.get("dim", 0)
|
| 238 |
+
assert dim == 0
|
| 239 |
+
return LoraTorchTensor(
|
| 240 |
+
torch.stack([a._lora_A for a in args[0]], dim),
|
| 241 |
+
torch.stack([b._lora_B for b in args[0]], dim),
|
| 242 |
+
)
|
| 243 |
+
elif func is torch.cat:
|
| 244 |
+
assert len(args)
|
| 245 |
+
assert isinstance(args[0], Sequence)
|
| 246 |
+
dim = kwargs.get("dim", 0)
|
| 247 |
+
assert dim == 0
|
| 248 |
+
if len(args[0][0].shape) > 2:
|
| 249 |
+
return LoraTorchTensor(
|
| 250 |
+
torch.cat([a._lora_A for a in args[0]], dim),
|
| 251 |
+
torch.cat([b._lora_B for b in args[0]], dim),
|
| 252 |
+
)
|
| 253 |
+
elif all(torch.equal(args[0][0]._lora_A, t._lora_A) for t in args[0][1:]):
|
| 254 |
+
return LoraTorchTensor(
|
| 255 |
+
args[0][0]._lora_A,
|
| 256 |
+
torch.cat([b._lora_B for b in args[0]], dim),
|
| 257 |
+
)
|
| 258 |
+
else:
|
| 259 |
+
raise NotImplementedError
|
| 260 |
+
elif func is torch.split:
|
| 261 |
+
assert len(args) and len(args) >= 2
|
| 262 |
+
tensor, split_size = args[0], args[1]
|
| 263 |
+
dim = args[2] if len(args) > 2 else kwargs.get("dim", 0)
|
| 264 |
+
return tensor.split(split_size, dim=dim)
|
| 265 |
+
else:
|
| 266 |
+
raise NotImplementedError
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def get_base_tensor_name(lora_tensor_name: str) -> str:
|
| 270 |
+
base_name = lora_tensor_name.replace("base_model.model.", "")
|
| 271 |
+
base_name = base_name.replace(".lora_A.weight", ".weight")
|
| 272 |
+
base_name = base_name.replace(".lora_B.weight", ".weight")
|
| 273 |
+
# models produced by mergekit-extract-lora have token embeddings in the adapter
|
| 274 |
+
base_name = base_name.replace(".lora_embedding_A", ".weight")
|
| 275 |
+
base_name = base_name.replace(".lora_embedding_B", ".weight")
|
| 276 |
+
return base_name
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def parse_args() -> argparse.Namespace:
|
| 280 |
+
parser = argparse.ArgumentParser(
|
| 281 |
+
description="Convert a Hugging Face PEFT LoRA adapter to a GGUF file")
|
| 282 |
+
parser.add_argument(
|
| 283 |
+
"--outfile", type=Path,
|
| 284 |
+
help="path to write to; default: based on input. {ftype} will be replaced by the outtype.",
|
| 285 |
+
)
|
| 286 |
+
parser.add_argument(
|
| 287 |
+
"--outtype", type=str, choices=["f32", "f16", "bf16", "q8_0", "auto"], default="f32",
|
| 288 |
+
help="output format - use f32 for float32, f16 for float16, bf16 for bfloat16, q8_0 for Q8_0, auto for the highest-fidelity 16-bit float type depending on the first loaded tensor type",
|
| 289 |
+
)
|
| 290 |
+
parser.add_argument(
|
| 291 |
+
"--bigendian", action="store_true",
|
| 292 |
+
help="model is executed on big endian machine",
|
| 293 |
+
)
|
| 294 |
+
parser.add_argument(
|
| 295 |
+
"--no-lazy", action="store_true",
|
| 296 |
+
help="use more RAM by computing all outputs before writing (use in case lazy evaluation is broken)",
|
| 297 |
+
)
|
| 298 |
+
parser.add_argument(
|
| 299 |
+
"--verbose", action="store_true",
|
| 300 |
+
help="increase output verbosity",
|
| 301 |
+
)
|
| 302 |
+
parser.add_argument(
|
| 303 |
+
"--dry-run", action="store_true",
|
| 304 |
+
help="only print out what will be done, without writing any new files",
|
| 305 |
+
)
|
| 306 |
+
parser.add_argument(
|
| 307 |
+
"--base", type=Path,
|
| 308 |
+
help="directory containing Hugging Face model config files (config.json, tokenizer.json) for the base model that the adapter is based on - only config is needed, actual model weights are not required. If base model is unspecified, it will be loaded from Hugging Face hub based on the adapter config",
|
| 309 |
+
)
|
| 310 |
+
parser.add_argument(
|
| 311 |
+
"--base-model-id", type=str,
|
| 312 |
+
help="the model ID of the base model, if it is not available locally or in the adapter config. If specified, it will ignore --base and load the base model config from the Hugging Face hub (Example: 'meta-llama/Llama-3.2-1B-Instruct')",
|
| 313 |
+
)
|
| 314 |
+
parser.add_argument(
|
| 315 |
+
"--trust-remote-code", default=False, action="store_true",
|
| 316 |
+
help="trust remote code in the model",
|
| 317 |
+
)
|
| 318 |
+
parser.add_argument(
|
| 319 |
+
"lora_path", type=Path,
|
| 320 |
+
help="directory containing Hugging Face PEFT LoRA config (adapter_model.json) and weights (adapter_model.safetensors or adapter_model.bin)",
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
return parser.parse_args()
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def load_hparams_from_hf(hf_model_id: str, trust_remote_code: bool) -> tuple[dict[str, Any], Path | None]:
|
| 327 |
+
from huggingface_hub import try_to_load_from_cache
|
| 328 |
+
|
| 329 |
+
# normally, adapter does not come with base model config, we need to load it from AutoConfig
|
| 330 |
+
config = AutoConfig.from_pretrained(hf_model_id, trust_remote_code=trust_remote_code)
|
| 331 |
+
cache_dir = try_to_load_from_cache(hf_model_id, "config.json")
|
| 332 |
+
cache_dir = Path(cache_dir).parent if isinstance(cache_dir, str) else None
|
| 333 |
+
|
| 334 |
+
return config.to_dict(), cache_dir
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
if __name__ == '__main__':
|
| 338 |
+
args = parse_args()
|
| 339 |
+
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
|
| 340 |
+
|
| 341 |
+
ftype_map: dict[str, gguf.LlamaFileType] = {
|
| 342 |
+
"f32": gguf.LlamaFileType.ALL_F32,
|
| 343 |
+
"f16": gguf.LlamaFileType.MOSTLY_F16,
|
| 344 |
+
"bf16": gguf.LlamaFileType.MOSTLY_BF16,
|
| 345 |
+
"q8_0": gguf.LlamaFileType.MOSTLY_Q8_0,
|
| 346 |
+
"auto": gguf.LlamaFileType.GUESSED,
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
ftype = ftype_map[args.outtype]
|
| 350 |
+
|
| 351 |
+
dir_base_model: Path | None = args.base
|
| 352 |
+
dir_lora: Path = args.lora_path
|
| 353 |
+
base_model_id: str | None = args.base_model_id
|
| 354 |
+
lora_config = dir_lora / "adapter_config.json"
|
| 355 |
+
input_model = dir_lora / "adapter_model.safetensors"
|
| 356 |
+
|
| 357 |
+
if args.outfile is not None:
|
| 358 |
+
fname_out = args.outfile
|
| 359 |
+
else:
|
| 360 |
+
# output in the same directory as the model by default
|
| 361 |
+
fname_out = dir_lora
|
| 362 |
+
|
| 363 |
+
if os.path.exists(input_model):
|
| 364 |
+
# lazy import load_file only if lora is in safetensors format.
|
| 365 |
+
from safetensors.torch import load_file
|
| 366 |
+
|
| 367 |
+
lora_model = load_file(input_model, device="cpu")
|
| 368 |
+
else:
|
| 369 |
+
input_model = os.path.join(dir_lora, "adapter_model.bin")
|
| 370 |
+
lora_model = torch.load(input_model, map_location="cpu", weights_only=True)
|
| 371 |
+
|
| 372 |
+
# load LoRA config
|
| 373 |
+
with open(lora_config, "r") as f:
|
| 374 |
+
lparams: dict[str, Any] = json.load(f)
|
| 375 |
+
|
| 376 |
+
# load base model
|
| 377 |
+
if base_model_id is not None:
|
| 378 |
+
logger.info(f"Loading base model from Hugging Face: {base_model_id}")
|
| 379 |
+
hparams, dir_base_model = load_hparams_from_hf(base_model_id, args.trust_remote_code)
|
| 380 |
+
elif dir_base_model is None:
|
| 381 |
+
if "base_model_name_or_path" in lparams:
|
| 382 |
+
model_id = lparams["base_model_name_or_path"]
|
| 383 |
+
logger.info(f"Loading base model from Hugging Face: {model_id}")
|
| 384 |
+
try:
|
| 385 |
+
hparams, dir_base_model = load_hparams_from_hf(model_id, args.trust_remote_code)
|
| 386 |
+
except OSError as e:
|
| 387 |
+
logger.error(f"Failed to load base model config: {e}")
|
| 388 |
+
logger.error("Please try downloading the base model and add its path to --base")
|
| 389 |
+
sys.exit(1)
|
| 390 |
+
else:
|
| 391 |
+
logger.error("'base_model_name_or_path' is not found in adapter_config.json")
|
| 392 |
+
logger.error("Base model config is required. Please download the base model and add its path to --base")
|
| 393 |
+
sys.exit(1)
|
| 394 |
+
else:
|
| 395 |
+
logger.info(f"Loading base model: {dir_base_model.name}")
|
| 396 |
+
hparams = ModelBase.load_hparams(dir_base_model, False)
|
| 397 |
+
|
| 398 |
+
with torch.inference_mode():
|
| 399 |
+
model_arch = get_model_architecture(hparams, ModelType.TEXT)
|
| 400 |
+
try:
|
| 401 |
+
model_class = get_model_class(model_arch)
|
| 402 |
+
logger.info("Using model architecture: %s", model_arch)
|
| 403 |
+
except NotImplementedError:
|
| 404 |
+
logger.error(f"Model {model_arch} is not supported")
|
| 405 |
+
sys.exit(1)
|
| 406 |
+
|
| 407 |
+
class LoraModel(model_class): # ty: ignore[unsupported-base]
|
| 408 |
+
model_arch = model_class.model_arch
|
| 409 |
+
|
| 410 |
+
lora_alpha: float
|
| 411 |
+
|
| 412 |
+
def __init__(self, *args, dir_lora_model: Path, lora_alpha: float, **kwargs):
|
| 413 |
+
|
| 414 |
+
super().__init__(*args, **kwargs)
|
| 415 |
+
|
| 416 |
+
self.dir_model_card = dir_lora_model
|
| 417 |
+
self.lora_alpha = float(lora_alpha)
|
| 418 |
+
|
| 419 |
+
def set_vocab(self):
|
| 420 |
+
pass
|
| 421 |
+
|
| 422 |
+
def set_type(self):
|
| 423 |
+
self.gguf_writer.add_type(gguf.GGUFType.ADAPTER)
|
| 424 |
+
self.gguf_writer.add_string(gguf.Keys.Adapter.TYPE, "lora")
|
| 425 |
+
|
| 426 |
+
def set_gguf_parameters(self):
|
| 427 |
+
logger.debug("GGUF KV: %s = %d", gguf.Keys.Adapter.LORA_ALPHA, self.lora_alpha)
|
| 428 |
+
self.gguf_writer.add_float32(gguf.Keys.Adapter.LORA_ALPHA, self.lora_alpha)
|
| 429 |
+
alora_invocation_tokens = lparams.get("alora_invocation_tokens")
|
| 430 |
+
invocation_string = lparams.get("invocation_string")
|
| 431 |
+
if invocation_string and not alora_invocation_tokens:
|
| 432 |
+
logger.debug("Tokenizing invocation_string -> alora_invocation_tokens")
|
| 433 |
+
base_model_path_or_id = hparams.get("_name_or_path")
|
| 434 |
+
try:
|
| 435 |
+
tokenizer = AutoTokenizer.from_pretrained(base_model_path_or_id)
|
| 436 |
+
except ValueError:
|
| 437 |
+
logger.error("Unable to load tokenizer from %s", base_model_path_or_id)
|
| 438 |
+
raise
|
| 439 |
+
# NOTE: There's an off-by-one with the older aLoRAs where
|
| 440 |
+
# the invocation string includes the "<|start_of_turn|>"
|
| 441 |
+
# token, but the adapters themselves were trained to
|
| 442 |
+
# activate _after_ that first token, so we drop it here.
|
| 443 |
+
alora_invocation_tokens = tokenizer(invocation_string)["input_ids"][1:] # ty: ignore[call-non-callable]
|
| 444 |
+
if alora_invocation_tokens:
|
| 445 |
+
logger.debug("GGUF KV: %s = %s", gguf.Keys.Adapter.ALORA_INVOCATION_TOKENS, alora_invocation_tokens)
|
| 446 |
+
self.gguf_writer.add_key_value(
|
| 447 |
+
gguf.Keys.Adapter.ALORA_INVOCATION_TOKENS,
|
| 448 |
+
alora_invocation_tokens,
|
| 449 |
+
GGUFValueType.ARRAY,
|
| 450 |
+
GGUFValueType.UINT32,
|
| 451 |
+
)
|
| 452 |
+
|
| 453 |
+
def generate_extra_tensors(self) -> Iterable[tuple[str, Tensor]]:
|
| 454 |
+
# Never add extra tensors (e.g. rope_freqs) for LoRA adapters
|
| 455 |
+
return ()
|
| 456 |
+
|
| 457 |
+
def get_tensors(self) -> Iterator[tuple[str, Tensor]]:
|
| 458 |
+
tensor_map: dict[str, PartialLoraTensor] = {}
|
| 459 |
+
|
| 460 |
+
for name, tensor in lora_model.items():
|
| 461 |
+
if self.lazy:
|
| 462 |
+
tensor = LazyTorchTensor.from_eager(tensor)
|
| 463 |
+
base_name = get_base_tensor_name(name)
|
| 464 |
+
# filter base name, ignore tensor transformations for now
|
| 465 |
+
data_gen = lambda g=tensor: g # noqa: E731
|
| 466 |
+
if (titem := self.filter_tensors((base_name, data_gen))) is None:
|
| 467 |
+
continue
|
| 468 |
+
base_name, _ = titem
|
| 469 |
+
# note: mergekit-extract-lora also adds token embeddings to the adapter
|
| 470 |
+
is_lora_a = ".lora_A.weight" in name or ".lora_embedding_A" in name
|
| 471 |
+
is_lora_b = ".lora_B.weight" in name or ".lora_embedding_B" in name
|
| 472 |
+
if not is_lora_a and not is_lora_b:
|
| 473 |
+
if ".base_layer.weight" in name:
|
| 474 |
+
continue
|
| 475 |
+
# mergekit-extract-lora add these layernorm to the adapter, we need to keep them
|
| 476 |
+
if "_layernorm" in name or ".norm" in name:
|
| 477 |
+
yield (base_name, tensor)
|
| 478 |
+
continue
|
| 479 |
+
logger.error(f"Unexpected name '{name}': Not a lora_A or lora_B tensor")
|
| 480 |
+
if ".embed_tokens.weight" in name or ".lm_head.weight" in name:
|
| 481 |
+
logger.error("Embeddings is present in the adapter. This can be due to new tokens added during fine tuning")
|
| 482 |
+
logger.error("Please refer to https://github.com/ggml-org/llama.cpp/pull/9948")
|
| 483 |
+
sys.exit(1)
|
| 484 |
+
|
| 485 |
+
if base_name in tensor_map:
|
| 486 |
+
if is_lora_a:
|
| 487 |
+
tensor_map[base_name].A = tensor
|
| 488 |
+
else:
|
| 489 |
+
tensor_map[base_name].B = tensor
|
| 490 |
+
else:
|
| 491 |
+
if is_lora_a:
|
| 492 |
+
tensor_map[base_name] = PartialLoraTensor(A=tensor)
|
| 493 |
+
else:
|
| 494 |
+
tensor_map[base_name] = PartialLoraTensor(B=tensor)
|
| 495 |
+
|
| 496 |
+
for name, tensor in tensor_map.items():
|
| 497 |
+
assert tensor.A is not None
|
| 498 |
+
assert tensor.B is not None
|
| 499 |
+
yield (name, cast(torch.Tensor, LoraTorchTensor(tensor.A, tensor.B)))
|
| 500 |
+
|
| 501 |
+
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
| 502 |
+
dest = list(super().modify_tensors(data_torch, name, bid))
|
| 503 |
+
# some archs may have the same tensor for lm_head and output (tie word embeddings)
|
| 504 |
+
# in this case, adapters targeting lm_head will fail when using llama-export-lora
|
| 505 |
+
# therefore, we ignore them for now
|
| 506 |
+
# see: https://github.com/ggml-org/llama.cpp/issues/9065
|
| 507 |
+
if name == "lm_head.weight" and len(dest) == 0:
|
| 508 |
+
raise ValueError("lm_head is present in adapter, but is ignored in base model")
|
| 509 |
+
for dest_name, dest_data in dest:
|
| 510 |
+
# mergekit-extract-lora add these layernorm to the adapter
|
| 511 |
+
if "_norm" in dest_name:
|
| 512 |
+
assert dest_data.dim() == 1
|
| 513 |
+
yield (dest_name, dest_data)
|
| 514 |
+
continue
|
| 515 |
+
|
| 516 |
+
# otherwise, we must get the lora_A and lora_B tensors
|
| 517 |
+
assert isinstance(dest_data, LoraTorchTensor)
|
| 518 |
+
lora_a, lora_b = dest_data.get_lora_A_B()
|
| 519 |
+
|
| 520 |
+
# note: mergekit-extract-lora flip and transpose A and B
|
| 521 |
+
# here we only need to transpose token_embd.lora_a, see llm_build_inp_embd()
|
| 522 |
+
if "token_embd.weight" in dest_name:
|
| 523 |
+
lora_a = lora_a.T
|
| 524 |
+
|
| 525 |
+
yield (dest_name + ".lora_a", lora_a)
|
| 526 |
+
yield (dest_name + ".lora_b", lora_b)
|
| 527 |
+
|
| 528 |
+
alpha: float = lparams["lora_alpha"]
|
| 529 |
+
|
| 530 |
+
model_instance = LoraModel(
|
| 531 |
+
dir_base_model,
|
| 532 |
+
ftype,
|
| 533 |
+
fname_out,
|
| 534 |
+
is_big_endian=args.bigendian,
|
| 535 |
+
use_temp_file=False,
|
| 536 |
+
eager=args.no_lazy,
|
| 537 |
+
dry_run=args.dry_run,
|
| 538 |
+
dir_lora_model=dir_lora,
|
| 539 |
+
lora_alpha=alpha,
|
| 540 |
+
hparams=hparams,
|
| 541 |
+
remote_hf_model_id=base_model_id,
|
| 542 |
+
)
|
| 543 |
+
|
| 544 |
+
logger.info("Exporting model...")
|
| 545 |
+
model_instance.write()
|
| 546 |
+
logger.info(f"Model successfully exported to {model_instance.fname_out}")
|
llama.cpp/docs/android.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# Android
|
| 3 |
+
|
| 4 |
+
## Build GUI binding using Android Studio
|
| 5 |
+
|
| 6 |
+
Import the `examples/llama.android` directory into Android Studio, then perform a Gradle sync and build the project.
|
| 7 |
+

|
| 8 |
+
|
| 9 |
+
This Android binding supports hardware acceleration up to `SME2` for **Arm** and `AMX` for **x86-64** CPUs on Android and ChromeOS devices.
|
| 10 |
+
It automatically detects the host's hardware to load compatible kernels. As a result, it runs seamlessly on both the latest premium devices and older devices that may lack modern CPU features or have limited RAM, without requiring any manual configuration.
|
| 11 |
+
|
| 12 |
+
A minimal Android app frontend is included to showcase the binding’s core functionalities:
|
| 13 |
+
1. **Parse GGUF metadata** via `GgufMetadataReader` from either a `ContentResolver` provided `Uri` from shared storage, or a local `File` from your app's private storage.
|
| 14 |
+
2. **Obtain a `InferenceEngine`** instance through the `AiChat` facade and load your selected model via its app-private file path.
|
| 15 |
+
3. **Send a raw user prompt** for automatic template formatting, prefill, and batch decoding. Then collect the generated tokens in a Kotlin `Flow`.
|
| 16 |
+
|
| 17 |
+
For a production-ready experience that leverages advanced features such as system prompts and benchmarks, plus friendly UI features such as model management and Arm feature visualizer, check out [Arm AI Chat](https://play.google.com/store/apps/details?id=com.arm.aichat) on Google Play.
|
| 18 |
+
This project is made possible through a collaborative effort by Arm's **CT-ML**, **CE-ML** and **STE** groups:
|
| 19 |
+
|
| 20 |
+
|  |  |  |
|
| 21 |
+
|:------------------------------------------------------:|:----------------------------------------------------:|:--------------------------------------------------------:|
|
| 22 |
+
| Home screen | System prompt | "Haiku" |
|
| 23 |
+
|
| 24 |
+
## Build CLI on Android using Termux
|
| 25 |
+
|
| 26 |
+
[Termux](https://termux.dev/en/) is an Android terminal emulator and Linux environment app (no root required). As of writing, Termux is available experimentally in the Google Play Store; otherwise, it may be obtained directly from the project repo or on F-Droid.
|
| 27 |
+
|
| 28 |
+
With Termux, you can install and run `llama.cpp` as if the environment were Linux. Once in the Termux shell:
|
| 29 |
+
|
| 30 |
+
```
|
| 31 |
+
$ apt update && apt upgrade -y
|
| 32 |
+
$ apt install git cmake libandroid-spawn
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
Then, follow the [build instructions](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md), specifically for CMake.
|
| 36 |
+
|
| 37 |
+
Once the binaries are built, download your model of choice (e.g., from Hugging Face). It's recommended to place it in the `~/` directory for best performance:
|
| 38 |
+
|
| 39 |
+
```
|
| 40 |
+
$ curl -L {model-url} -o ~/{model}.gguf
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
Then, if you are not already in the repo directory, `cd` into `llama.cpp` and:
|
| 44 |
+
|
| 45 |
+
```
|
| 46 |
+
$ ./build/bin/llama-cli -m ~/{model}.gguf -c {context-size} -p "{your-prompt}"
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
Here, we show `llama-cli`, but any of the executables under `examples` should work, in theory. Be sure to set `context-size` to a reasonable number (say, 4096) to start with; otherwise, memory could spike and kill your terminal.
|
| 50 |
+
|
| 51 |
+
To see what it might look like visually, here's an old demo of an interactive session running on a Pixel 5 phone:
|
| 52 |
+
|
| 53 |
+
https://user-images.githubusercontent.com/271616/225014776-1d567049-ad71-4ef2-b050-55b0b3b9274c.mp4
|
| 54 |
+
|
| 55 |
+
## Cross-compile CLI using Android NDK
|
| 56 |
+
It's possible to build `llama.cpp` for Android on your host system via CMake and the Android NDK. If you are interested in this path, ensure you already have an environment prepared to cross-compile programs for Android (i.e., install the Android SDK). Note that, unlike desktop environments, the Android environment ships with a limited set of native libraries, and so only those libraries are available to CMake when building with the Android NDK (see: https://developer.android.com/ndk/guides/stable_apis.)
|
| 57 |
+
|
| 58 |
+
Once you're ready and have cloned `llama.cpp`, invoke the following in the project directory:
|
| 59 |
+
|
| 60 |
+
```
|
| 61 |
+
$ cmake \
|
| 62 |
+
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
|
| 63 |
+
-DANDROID_ABI=arm64-v8a \
|
| 64 |
+
-DANDROID_PLATFORM=android-28 \
|
| 65 |
+
-DCMAKE_C_FLAGS="-march=armv8.7a" \
|
| 66 |
+
-DCMAKE_CXX_FLAGS="-march=armv8.7a" \
|
| 67 |
+
-DGGML_OPENMP=OFF \
|
| 68 |
+
-DGGML_LLAMAFILE=OFF \
|
| 69 |
+
-B build-android
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
Notes:
|
| 73 |
+
- While later versions of Android NDK ship with OpenMP, it must still be installed by CMake as a dependency, which is not supported at this time
|
| 74 |
+
- `llamafile` does not appear to support Android devices (see: https://github.com/Mozilla-Ocho/llamafile/issues/325)
|
| 75 |
+
|
| 76 |
+
The above command should configure `llama.cpp` with the most performant options for modern devices. Even if your device is not running `armv8.7a`, `llama.cpp` includes runtime checks for available CPU features it can use.
|
| 77 |
+
|
| 78 |
+
Feel free to adjust the Android ABI for your target. Once the project is configured:
|
| 79 |
+
|
| 80 |
+
```
|
| 81 |
+
$ cmake --build build-android --config Release -j{n}
|
| 82 |
+
$ cmake --install build-android --prefix {install-dir} --config Release
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
After installing, go ahead and download the model of your choice to your host system. Then:
|
| 86 |
+
|
| 87 |
+
```
|
| 88 |
+
$ adb shell "mkdir /data/local/tmp/llama.cpp"
|
| 89 |
+
$ adb push {install-dir} /data/local/tmp/llama.cpp/
|
| 90 |
+
$ adb push {model}.gguf /data/local/tmp/llama.cpp/
|
| 91 |
+
$ adb shell
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
In the `adb shell`:
|
| 95 |
+
|
| 96 |
+
```
|
| 97 |
+
$ cd /data/local/tmp/llama.cpp
|
| 98 |
+
$ LD_LIBRARY_PATH=lib ./bin/llama-simple -m {model}.gguf -c {context-size} -p "{your-prompt}"
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
That's it!
|
| 102 |
+
|
| 103 |
+
Be aware that Android will not find the library path `lib` on its own, so we must specify `LD_LIBRARY_PATH` in order to run the installed executables. Android does support `RPATH` in later API levels, so this could change in the future. Refer to the previous section for information about `context-size` (very important!) and running other `examples`.
|
llama.cpp/docs/android/imported-into-android-studio.jpg
ADDED
|
Git LFS Details
|
llama.cpp/docs/autoparser.md
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Auto-Parser Architecture
|
| 2 |
+
|
| 3 |
+
The auto-parser automatically analyzes chat templates to determine how to parse model outputs, including content, reasoning, and tool calls.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
|
| 7 |
+
The unified auto-parser uses a pure differential, compositional approach (inspired by the `git diff` algorithm) to analyze chat templates:
|
| 8 |
+
|
| 9 |
+
**Core Philosophy**:
|
| 10 |
+
|
| 11 |
+
- **Minimize Hardcoded Patterns**: All markers extracted through template comparison (the only heuristic is JSON detection to distinguish `JSON_NATIVE` from tag-based formats)
|
| 12 |
+
- **Compositional Architecture**: Separate analyzer structs for reasoning, content, and tools — each responsible for its own analysis and parser construction
|
| 13 |
+
|
| 14 |
+
**Analysis + Parser Building in Two Steps**:
|
| 15 |
+
|
| 16 |
+
1. `autoparser::autoparser tmpl_analysis(tmpl)` — runs all differential comparisons and populates the analysis structs
|
| 17 |
+
2. `autoparser::peg_generator::generate_parser(tmpl, generation_params, tmpl_analysis)` — uses the analysis to build a PEG parser and optional GBNF grammar
|
| 18 |
+
|
| 19 |
+
## Data Structures
|
| 20 |
+
|
| 21 |
+
All structs are defined in [common/chat-auto-parser.h](common/chat-auto-parser.h).
|
| 22 |
+
|
| 23 |
+
### Top-Level: `autoparser` (main analyzer and generator)
|
| 24 |
+
|
| 25 |
+
[common/chat-auto-parser.h:367-388](common/chat-auto-parser.h#L367-L388) — top-level analysis result aggregating `jinja_caps`, `reasoning`, `content`, and `tools` sub-analyses, plus `preserved_tokens` (union of all non-empty markers).
|
| 26 |
+
|
| 27 |
+
### `analyze_reasoning`
|
| 28 |
+
|
| 29 |
+
[common/chat-auto-parser.h:254-274](common/chat-auto-parser.h#L254-L274) — reasoning analysis result: `mode` enum, `start` marker (e.g. `<think>`), and `end` marker (e.g. `</think>`).
|
| 30 |
+
|
| 31 |
+
### `analyze_content`
|
| 32 |
+
|
| 33 |
+
[common/chat-auto-parser.h:280-295](common/chat-auto-parser.h#L280-L295) — content analysis result: `mode` enum, `start`/`end` markers, and `requires_nonnull_content` flag.
|
| 34 |
+
|
| 35 |
+
### `analyze_tools` and its sub-structs
|
| 36 |
+
|
| 37 |
+
- [common/chat-auto-parser.h:176-194](common/chat-auto-parser.h#L176-L194) — `tool_format_analysis`: `mode` enum, `section_start/end`, `per_call_start/end`, JSON field names (`function_field`, `name_field`, `args_field`, `id_field`, `gen_id_field`), and format flags (`fun_name_is_key`, `tools_array_wrapped`)
|
| 38 |
+
- [common/chat-auto-parser.h:196-200](common/chat-auto-parser.h#L196-L200) — `tool_function_analysis`: `name_prefix`, `name_suffix`, `close` markers around function names
|
| 39 |
+
- [common/chat-auto-parser.h:202-210](common/chat-auto-parser.h#L202-L210) — `tool_arguments_analysis`: `start/end` container markers, `name_prefix/suffix`, `value_prefix/suffix`, `separator`
|
| 40 |
+
- [common/chat-auto-parser.h:212-217](common/chat-auto-parser.h#L212-L217) — `tool_id_analysis`: `pos` enum, `prefix`/`suffix` markers around call ID values
|
| 41 |
+
- [common/chat-auto-parser.h:301-361](common/chat-auto-parser.h#L301-L361) — `analyze_tools`: aggregates the four sub-structs above
|
| 42 |
+
|
| 43 |
+
### Enums
|
| 44 |
+
|
| 45 |
+
**`reasoning_mode`**: How the template handles reasoning/thinking blocks.
|
| 46 |
+
|
| 47 |
+
| Value | Description |
|
| 48 |
+
|-----------------|-----------------------------------------------------------------------------------|
|
| 49 |
+
| `NONE` | No reasoning markers detected |
|
| 50 |
+
| `TAG_BASED` | Tag-based: `<think>...</think>` (start can be empty for delimiter-style formats) |
|
| 51 |
+
| `TOOLS_ONLY` | Reasoning only appears in tool call responses, not plain content |
|
| 52 |
+
|
| 53 |
+
**Generation Prompt & Reasoning Prefill**: Computed in `common_chat_templates_apply_jinja` before invoking either the specialized handlers or the auto-parser, by rendering the template twice — once with `add_generation_prompt=false` and once with `add_generation_prompt=true` — and storing the diff suffix as `generation_params::generation_prompt`. This string is propagated into `common_chat_params::generation_prompt` and `common_chat_parser_params::generation_prompt`.
|
| 54 |
+
|
| 55 |
+
The generation prompt is prepended to model output before PEG parsing via `wrap_for_generation_prompt()`. The portion *before* the reasoning start marker (if any) is prepended as a literal to ensure any boilerplate added by the template is consumed. The full string is also fed to the grammar sampler via `llama_sampler_accept` (stored in `common_params_sampling::grammar_prefill`), advancing the grammar past tokens already in the prompt. It is used to determine the reasoning budget sampler's initial state — COUNTING if the prefill tokens begin with the reasoning start sequence (but don't also contain the end sequence), IDLE otherwise.
|
| 56 |
+
|
| 57 |
+
**`grammar_prefill`** (`common_params_sampling`): The generation prompt string tokenized and accepted by the grammar sampler at init time. Only applied when `grammar_external` is false (i.e., the grammar was not set explicitly by the user).
|
| 58 |
+
|
| 59 |
+
Three outcomes for reasoning-prefill handling (in `generate_parser()`):
|
| 60 |
+
|
| 61 |
+
1. **Start+end in generation prompt** (e.g. `<think></think>\n`): the parser sees reasoning as opened and immediately closed; whitespace-only reasoning content is discarded.
|
| 62 |
+
2. **Only start in generation prompt** (e.g. `<think>\n`): the parser sees reasoning as already open.
|
| 63 |
+
3. **Start marker present but not at the end** (e.g. Apriel's `<|begin_assistant|>` followed by boilerplate): the marker is a template artifact; the start literal is cleared so reasoning uses delimiter-style (end-only). For templates that ignore `add_generation_prompt` (empty diff), the rendered `data.prompt` is used as fallback — but only for non-TOOLS_ONLY modes, since in TOOLS_ONLY the start tag is model-generated and may appear in prior conversation turns.
|
| 64 |
+
|
| 65 |
+
**`content_mode`**: How the template wraps assistant content.
|
| 66 |
+
|
| 67 |
+
| Value | Description |
|
| 68 |
+
|--------------------------|----------------------------------------------------------------|
|
| 69 |
+
| `PLAIN` | No content markers |
|
| 70 |
+
| `ALWAYS_WRAPPED` | Content always wrapped: `<response>...</response>` |
|
| 71 |
+
| `WRAPPED_WITH_REASONING` | Content wrapped only when reasoning is present |
|
| 72 |
+
|
| 73 |
+
**`tool_format`**: Classification of tool call structure.
|
| 74 |
+
|
| 75 |
+
| Value | Description |
|
| 76 |
+
|------------------|------------------------------------------------------------------|
|
| 77 |
+
| `NONE` | No tool support detected |
|
| 78 |
+
| `JSON_NATIVE` | Pure JSON: `{"name": "X", "arguments": {...}}` |
|
| 79 |
+
| `TAG_WITH_JSON` | Tag-based with JSON args: `<function=X>{...}</function>` |
|
| 80 |
+
| `TAG_WITH_TAGGED`| Tag-based with tagged args: `<param=key>value</param>` |
|
| 81 |
+
|
| 82 |
+
**`call_id_position`**: Where call IDs appear in tag-based formats.
|
| 83 |
+
|
| 84 |
+
| Value | Description |
|
| 85 |
+
|--------------------------|----------------------------------------------|
|
| 86 |
+
| `NONE` | No call ID support detected |
|
| 87 |
+
| `PRE_FUNC_NAME` | Before function name |
|
| 88 |
+
| `BETWEEN_FUNC_AND_ARGS` | Between function name and arguments |
|
| 89 |
+
| `POST_ARGS` | After arguments |
|
| 90 |
+
|
| 91 |
+
## Tool Calling Formats
|
| 92 |
+
|
| 93 |
+
### JSON_NATIVE
|
| 94 |
+
|
| 95 |
+
**Structure**: The entire tool call (function name, arguments, values) is in JSON format. Optional enclosing tags around the section.
|
| 96 |
+
|
| 97 |
+
**Detection**: Function name appears inside a JSON structure (quotes preceded by `{` or `:`).
|
| 98 |
+
|
| 99 |
+
**Examples**:
|
| 100 |
+
|
| 101 |
+
Standard OpenAI-style:
|
| 102 |
+
|
| 103 |
+
```json
|
| 104 |
+
<tool_call>
|
| 105 |
+
{"name": "get_weather", "arguments": {"location": "Paris", "unit": "celsius"}}
|
| 106 |
+
</tool_call>
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
Mistral Nemo with array wrapper:
|
| 110 |
+
|
| 111 |
+
```json
|
| 112 |
+
[TOOL_CALLS]
|
| 113 |
+
[{"name": "calculate", "arguments": {"expr": "2+2"}}]
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
Function name as JSON key (Apertus style):
|
| 117 |
+
|
| 118 |
+
```json
|
| 119 |
+
{"get_weather": {"location": "Paris"}}
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
---
|
| 123 |
+
|
| 124 |
+
### TAG_WITH_JSON
|
| 125 |
+
|
| 126 |
+
**Structure**: Function name is outside JSON, in tag attributes or XML-style tags. Arguments are a JSON object.
|
| 127 |
+
|
| 128 |
+
**Detection**: Function name not in JSON, but argument names appear in JSON context.
|
| 129 |
+
|
| 130 |
+
**Examples**:
|
| 131 |
+
|
| 132 |
+
Functionary v3.1:
|
| 133 |
+
|
| 134 |
+
```xml
|
| 135 |
+
<function=get_weather>{"location": "Paris", "unit": "celsius"}</function>
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
MiniMax:
|
| 139 |
+
|
| 140 |
+
```xml
|
| 141 |
+
<minimax:tool_call>
|
| 142 |
+
<tool_name>calculate</tool_name>
|
| 143 |
+
<arguments>{"expr": "2+2"}</arguments>
|
| 144 |
+
</minimax:tool_call>
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
---
|
| 148 |
+
|
| 149 |
+
### TAG_WITH_TAGGED
|
| 150 |
+
|
| 151 |
+
**Structure**: Both function name and argument names are in XML-style tags. String values are unquoted; non-string values are JSON-formatted.
|
| 152 |
+
|
| 153 |
+
**Detection**: Neither function name nor argument names appear in a JSON context.
|
| 154 |
+
|
| 155 |
+
**Examples**:
|
| 156 |
+
|
| 157 |
+
Qwen/Hermes XML format:
|
| 158 |
+
|
| 159 |
+
```xml
|
| 160 |
+
<function=get_weather>
|
| 161 |
+
<param=location>Paris</param>
|
| 162 |
+
<param=unit>celsius</param>
|
| 163 |
+
</function>
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
Mixed types:
|
| 167 |
+
|
| 168 |
+
```xml
|
| 169 |
+
<function=calculate>
|
| 170 |
+
<param=expr>2+2</param>
|
| 171 |
+
<param=precision>2</param>
|
| 172 |
+
<param=options>{"round": true}</param>
|
| 173 |
+
</function>
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
String values (`Paris`, `celsius`, `2+2`) are unquoted; `options` (object type) is JSON-formatted.
|
| 177 |
+
|
| 178 |
+
---
|
| 179 |
+
|
| 180 |
+
## Analysis Flow
|
| 181 |
+
|
| 182 |
+
```text
|
| 183 |
+
autoparser::autoparser(tmpl)
|
| 184 |
+
|
|
| 185 |
+
|-- Phase 1: analyze_reasoning(tmpl, jinja_caps.supports_tool_calls)
|
| 186 |
+
| |-- R1: compare_reasoning_presence() — with/without reasoning_content field
|
| 187 |
+
| |-- R2: compare_thinking_enabled() — enable_thinking=false vs true
|
| 188 |
+
| '-- R3: compare_reasoning_scope() — reasoning+content vs reasoning+tools
|
| 189 |
+
| (only if supports_tool_calls)
|
| 190 |
+
|
|
| 191 |
+
|-- Phase 2: analyze_content(tmpl, reasoning)
|
| 192 |
+
| '-- C1: compares content-only vs tools output and content-only vs reasoning output
|
| 193 |
+
|
|
| 194 |
+
|-- Phase 3: analyze_tools(tmpl, jinja_caps, reasoning)
|
| 195 |
+
| (skipped entirely if !jinja_caps.supports_tool_calls)
|
| 196 |
+
| |
|
| 197 |
+
| |-- T1: analyze_tool_calls() — no tools vs with tools; classifies format
|
| 198 |
+
| | |-- JSON path → analyze_tool_call_format_json_native()
|
| 199 |
+
| | '-- tag path → analyze_tool_call_format_non_json()
|
| 200 |
+
| |
|
| 201 |
+
| (if format != NONE and format != JSON_NATIVE:)
|
| 202 |
+
| |
|
| 203 |
+
| |-- T2: check_per_call_markers() — 1 call vs 2 calls; moves section→per-call if needed
|
| 204 |
+
| | (only if supports_parallel_tool_calls)
|
| 205 |
+
| |
|
| 206 |
+
| |-- T3: extract_function_markers() — func_alpha vs func_beta; extracts name prefix/suffix/close
|
| 207 |
+
| |
|
| 208 |
+
| |-- T4: analyze_arguments() — (TAG_WITH_TAGGED only)
|
| 209 |
+
| | |-- A1: extract_argument_name_markers() — arg_name_A vs arg_name_B
|
| 210 |
+
| | '-- A2: extract_argument_value_markers() — value "XXXX" vs "YYYY"
|
| 211 |
+
| |
|
| 212 |
+
| |-- T5: extract_argument_separator() — 1 arg vs 2 args; finds separator between args
|
| 213 |
+
| |
|
| 214 |
+
| |-- T6: extract_args_markers() — 0 args vs 1 arg; finds args container markers
|
| 215 |
+
| |
|
| 216 |
+
| '-- T7: extract_call_id_markers() — call_id "call00001" vs "call99999"
|
| 217 |
+
|
|
| 218 |
+
'-- collect_preserved_tokens() — union of all non-empty markers
|
| 219 |
+
|
|
| 220 |
+
'-- apply workarounds() — post-hoc patches for edge-case templates
|
| 221 |
+
|
|
| 222 |
+
v
|
| 223 |
+
autoparser (analysis result)
|
| 224 |
+
|
|
| 225 |
+
v
|
| 226 |
+
autoparser::peg_generator::generate_parser(tmpl, inputs, analysis)
|
| 227 |
+
|-- analysis.build_parser(inputs) — builds PEG parser arena
|
| 228 |
+
| |-- reasoning.build_parser(ctx) — reasoning parser (mode-dependent)
|
| 229 |
+
| |-- content.build_parser(ctx) — content parser (mode-dependent)
|
| 230 |
+
| '-- tools.build_parser(ctx) — tool parser (dispatches by tool_format)
|
| 231 |
+
| |-- build_tool_parser_json_native()
|
| 232 |
+
| |-- build_tool_parser_tag_json()
|
| 233 |
+
| '-- build_tool_parser_tag_tagged()
|
| 234 |
+
|
|
| 235 |
+
|-- Build GBNF grammar (if tools present and trigger_marker non-empty)
|
| 236 |
+
'-- Set grammar_triggers from section_start or per_call_start
|
| 237 |
+
|
|
| 238 |
+
v
|
| 239 |
+
common_chat_params (prompt, parser, grammar, triggers, preserved_tokens)
|
| 240 |
+
```
|
| 241 |
+
|
| 242 |
+
## Entry Point
|
| 243 |
+
|
| 244 |
+
The auto-parser is invoked in [common/chat.cpp:1280-1310](common/chat.cpp#L1280-L1310) in `common_chat_templates_apply_jinja`. A few specialized templates are handled first (Ministral/Magistral Large 3, GPT-OSS with `<|channel|>`, Functionary v3.2 with `>>>all`), then the auto-parser handles everything else via `autoparser::autoparser` + `peg_generator::generate_parser`.
|
| 245 |
+
|
| 246 |
+
## Algorithm Details
|
| 247 |
+
|
| 248 |
+
### Core Mechanism: Differential Comparison
|
| 249 |
+
|
| 250 |
+
All analysis phases use the same factorized comparison function declared in [common/chat-auto-parser-helpers.h:68](common/chat-auto-parser-helpers.h#L68):
|
| 251 |
+
|
| 252 |
+
```cpp
|
| 253 |
+
compare_variants(tmpl, params_A, params_modifier)
|
| 254 |
+
```
|
| 255 |
+
|
| 256 |
+
This creates variant B by applying a modifier lambda to a copy of `params_A`, renders both through the template, and computes a `diff_split` ([common/chat-auto-parser.h:28-37](common/chat-auto-parser.h#L28-L37)):
|
| 257 |
+
|
| 258 |
+
- `prefix` — common prefix between A and B
|
| 259 |
+
- `suffix` — common suffix between A and B
|
| 260 |
+
- `left` — unique to variant A
|
| 261 |
+
- `right` — unique to variant B
|
| 262 |
+
|
| 263 |
+
The diff is computed via `calculate_diff_split()`, which finds the longest-common-prefix and longest-common-suffix, then iteratively moves incomplete `<...>` or `[...]` markers from the prefix/suffix into left/right until stable (tag boundary fixing).
|
| 264 |
+
|
| 265 |
+
Text is segmentized into markers and non-marker fragments using `segmentize_markers()`, which splits on `<...>` and `[...]` boundaries.
|
| 266 |
+
|
| 267 |
+
### Phase 1: Reasoning Analysis
|
| 268 |
+
|
| 269 |
+
**R1 — `compare_reasoning_presence()`**: Compares assistant message with vs without a `reasoning_content` field.
|
| 270 |
+
|
| 271 |
+
- Searches `diff.right` (output with reasoning) for the reasoning content needle
|
| 272 |
+
- Uses PEG parsers to find surrounding markers:
|
| 273 |
+
- If both pre/post markers found in `diff.right` → `TAG_BASED`
|
| 274 |
+
- If both found but post marker only in the full output B → `TAG_BASED` (template forces markers; handled via prefill)
|
| 275 |
+
- If only post marker found → `TAG_BASED` (delimiter-style, empty start)
|
| 276 |
+
- Sets `reasoning.start` and `reasoning.end`
|
| 277 |
+
|
| 278 |
+
**R2 — `compare_thinking_enabled()`**: Compares `enable_thinking=false` vs `true` with a generation prompt.
|
| 279 |
+
|
| 280 |
+
- Detects template-added reasoning markers: `enable_thinking=true` appends a non-empty marker → sets `reasoning.start`, mode = `TAG_BASED`
|
| 281 |
+
- Handles the reverse case (`enable_thinking=false` appends the marker instead): extracts both start (from the preceding segment) and end markers; mode = `TAG_BASED`
|
| 282 |
+
- The reasoning prefill (markers added by the template) is later extracted in `common_chat_templates_apply_jinja` and prepended to model output before parsing
|
| 283 |
+
|
| 284 |
+
**R3 — `compare_reasoning_scope()`**: Compares assistant message with reasoning+text-content vs reasoning+tool-calls.
|
| 285 |
+
|
| 286 |
+
- Only runs if `jinja_caps.supports_tool_calls`
|
| 287 |
+
- Detects `TOOLS_ONLY`: reasoning content present in B (with tools) but not in A (with text content)
|
| 288 |
+
- Extracts reasoning markers from the tool call output using PEG parsers
|
| 289 |
+
|
| 290 |
+
### Phase 2: Content Analysis
|
| 291 |
+
|
| 292 |
+
**C1**: Two comparisons in the `analyze_content` constructor:
|
| 293 |
+
|
| 294 |
+
- Comparison 1: content-only output vs tool-call output → `diff_tools`
|
| 295 |
+
- Comparison 2: content-only output vs reasoning+empty-content output → `diff_reasoning`
|
| 296 |
+
|
| 297 |
+
Classification logic:
|
| 298 |
+
|
| 299 |
+
- `PLAIN`: `diff_tools.left` equals the response string (content is the entire diff, no wrapper)
|
| 300 |
+
- `ALWAYS_WRAPPED`: markers found surrounding the content text in `pure_content` → extracts `start`/`end`
|
| 301 |
+
|
| 302 |
+
### Phase 3: Tool Call Analysis
|
| 303 |
+
|
| 304 |
+
**T1 — `analyze_tool_calls()`**: Compares no-tools vs with-tools output.
|
| 305 |
+
|
| 306 |
+
- Extracts the tool call section as `diff.right`
|
| 307 |
+
- Calls `analyze_tool_call_format()` which first strips reasoning markers from the haystack, then:
|
| 308 |
+
- Calls `in_json_haystack()` for both function name and argument name needles
|
| 309 |
+
- `in_json_haystack()` uses a PEG parser to check whether the needle appears in a JSON context (preceded by `{` or `:` with surrounding quotes)
|
| 310 |
+
- If function name is in JSON → `JSON_NATIVE` → `analyze_tool_call_format_json_native()`
|
| 311 |
+
- If function name not in JSON, arg name is in JSON → `TAG_WITH_JSON`
|
| 312 |
+
- If neither in JSON → `TAG_WITH_TAGGED`
|
| 313 |
+
- `analyze_tool_call_format_json_native()`: parses the JSON object, matches field values to needles to populate `name_field`, `args_field`, `id_field`, `gen_id_field`; detects `tools_array_wrapped`; extracts `section_start`/`section_end`
|
| 314 |
+
- `analyze_tool_call_format_non_json()`: uses PEG parsers on the haystack to find up to two opening markers (section + per-call) then up to two closing markers
|
| 315 |
+
|
| 316 |
+
**T2 — `check_per_call_markers()`**: Compares 1 call vs 2 calls.
|
| 317 |
+
|
| 318 |
+
- Computes a secondary diff of the second call portion vs the common suffix
|
| 319 |
+
- If the second call content starts with `section_start` → the section marker is actually per-call → moves `section_start/end` to `per_call_start/end` and clears the section markers
|
| 320 |
+
|
| 321 |
+
**T3 — `extract_function_markers()`**: Compares function name `FUN_FIRST` vs `FUN_SECOND` (two different named functions).
|
| 322 |
+
|
| 323 |
+
- Finds where the function name appears in `diff.left`
|
| 324 |
+
- Extracts `function.name_prefix` from the common prefix up to the function marker, and `function.name_suffix` from after the name up to the next marker
|
| 325 |
+
- Extends `name_suffix` into `diff.suffix` (to the first marker for TAG_WITH_TAGGED; to the first `{` or `[` for TAG_WITH_JSON)
|
| 326 |
+
- Extracts `function.close` from after the last argument value up to the per-call/section end marker
|
| 327 |
+
|
| 328 |
+
**T4 — `analyze_arguments()`** (TAG_WITH_TAGGED only):
|
| 329 |
+
|
| 330 |
+
- **A1 `extract_argument_name_markers()`**: Compares `arg_name_A` vs `arg_name_B` (two different argument names).
|
| 331 |
+
- Finds shared surrounding structure → `arguments.name_prefix`, `arguments.name_suffix`
|
| 332 |
+
- **A2 `extract_argument_value_markers()`**: Compares argument value `"XXXX"` vs `"YYYY"` (same arg, different value).
|
| 333 |
+
- Finds markers surrounding the value → `arguments.value_prefix`, `arguments.value_suffix`
|
| 334 |
+
|
| 335 |
+
**T5 — `extract_argument_separator()`**: Compares 1 argument vs 2 arguments (same function).
|
| 336 |
+
|
| 337 |
+
- Uses `until_common_prefix(diff.right, ARG_FIRST, ARG_SECOND)` to find what separates the two argument blocks
|
| 338 |
+
|
| 339 |
+
**T6 — `extract_args_markers()`**: Compares 0 arguments vs 1 argument.
|
| 340 |
+
|
| 341 |
+
- Uses `until_common_prefix()` and `after_common_suffix()` with the empty and single-arg JSON strings as anchors to find container markers (`arguments.start`, `arguments.end`)
|
| 342 |
+
|
| 343 |
+
**T7 — `extract_call_id_markers()`**: Compares call IDs `"call00001"` vs `"call99999"`.
|
| 344 |
+
|
| 345 |
+
- Determines whether function name appears in `diff.prefix` or `diff.suffix` to classify position:
|
| 346 |
+
- Function name in prefix only → `BETWEEN_FUNC_AND_ARGS` or `POST_ARGS` (further distinguished by where `{` appears)
|
| 347 |
+
- Function name in suffix only → `PRE_FUNC_NAME`
|
| 348 |
+
- Extracts `call_id.prefix` and `call_id.suffix` markers around the call ID value
|
| 349 |
+
- Clears `per_call_end` if it incorrectly incorporated the call ID suffix
|
| 350 |
+
|
| 351 |
+
### Workarounds
|
| 352 |
+
|
| 353 |
+
A workaround array in `common/chat-diff-analyzer.cpp` applies post-hoc patches after analysis. Each workaround is a lambda that inspects the template source and overrides analysis results. Current workarounds:
|
| 354 |
+
|
| 355 |
+
1. **Old Qwen/DeepSeek thinking templates** — source contains `content.split('</think>')` but not `<SPECIAL_12>`: sets `reasoning.mode = TAG_BASED` with `<think>`/`</think>` markers if no reasoning was detected
|
| 356 |
+
2. **Granite 3.3** — source contains specific "Write your thoughts" text: forces `TAG_BASED` reasoning with `<think>`/`</think>` and `WRAPPED_WITH_REASONING` content with `<response>`/`</response>`
|
| 357 |
+
3. **Cohere Command R+** — source contains `<|CHATBOT_TOKEN|>`: sets `ALWAYS_WRAPPED` content mode if no content start is already set
|
| 358 |
+
4. **Functionary 3.1** — source contains `set has_code_interpreter`: forces `PLAIN` content, specific `per_call_start/end`, clears preserved tokens to only keep Functionary-specific markers
|
| 359 |
+
5. **DeepSeek-R1-Distill-Qwen** — source contains `tool▁calls▁begin` markers: overrides tool section/per-call markers with the correct Unicode block characters
|
| 360 |
+
|
| 361 |
+
### Parser Building
|
| 362 |
+
|
| 363 |
+
Each analyzer struct (`analyze_reasoning`, `analyze_content`, `analyze_tools`) implements `build_parser(parser_build_context&)`. They share a `parser_build_context` that carries the PEG builder, inference inputs, the pre-built reasoning parser, and a pointer to the content analyzer.
|
| 364 |
+
|
| 365 |
+
#### Reasoning Parser (`analyze_reasoning::build_parser`)
|
| 366 |
+
|
| 367 |
+
| Mode | Parser |
|
| 368 |
+
|-----------------------------------------------|---------------------------------------------------------------------------|
|
| 369 |
+
| Not extracting reasoning | `eps()` |
|
| 370 |
+
| `TAG_BASED` or `TOOLS_ONLY` (non-empty start) | `optional(start + reasoning(until(end)) + end + space())` |
|
| 371 |
+
| `TAG_BASED` or `TOOLS_ONLY` (empty start) | `optional(reasoning(until(end)) + end + space())` — delimiter-style |
|
| 372 |
+
|
| 373 |
+
Note: The start marker may be empty either because the analyzer detected delimiter-style reasoning, or because `generate_parser()` cleared a template artifact start marker (see Generation Prompt & Reasoning Prefill above). Whitespace-only reasoning content (e.g. from a `<think></think>` prefill) is discarded by the mapper.
|
| 374 |
+
|
| 375 |
+
#### Content Parser (`analyze_content::build_parser`)
|
| 376 |
+
|
| 377 |
+
| Condition | Parser |
|
| 378 |
+
|----------------------------------------|---------------------------------------------------------------------------------|
|
| 379 |
+
| `json_schema` present | `reasoning + space() + content(schema(json(), "response-format", ...)) + end()` |
|
| 380 |
+
| Tools present | Dispatches to `analyze_tools::build_parser()` |
|
| 381 |
+
| `ALWAYS_WRAPPED` with reasoning | `reasoning + start + content(until(end)) + end + end()` |
|
| 382 |
+
| `ALWAYS_WRAPPED` without reasoning | `content(until(start)) + start + content(until(end)) + end + end()` |
|
| 383 |
+
| Default (PLAIN) | `reasoning + content(rest()) + end()` |
|
| 384 |
+
|
| 385 |
+
#### Tool Parsers (`analyze_tools::build_parser`)
|
| 386 |
+
|
| 387 |
+
Dispatches by `format.mode`:
|
| 388 |
+
|
| 389 |
+
**`build_tool_parser_json_native()`**: Calls `p.standard_json_tools()` which internally dispatches to:
|
| 390 |
+
|
| 391 |
+
- `build_json_tools_function_is_key()` — function name is the JSON key: `{"get_weather": {...}}`
|
| 392 |
+
- `build_json_tools_nested_keys()` — nested: `{"function": {"name": "X", "arguments": {...}}}`
|
| 393 |
+
- `build_json_tools_flat_keys()` — flat: `{"name": "X", "arguments": {...}}`
|
| 394 |
+
|
| 395 |
+
Handles content wrappers, array wrapping (`tools_array_wrapped`), parallel calls, and `parameter_order`.
|
| 396 |
+
|
| 397 |
+
**`build_tool_parser_tag_json()`**: For each tool function:
|
| 398 |
+
|
| 399 |
+
```text
|
| 400 |
+
tool_open(name_prefix + tool_name(literal(name)) + name_suffix) +
|
| 401 |
+
call_id_section +
|
| 402 |
+
tool_args(schema(json(), tool_schema))
|
| 403 |
+
[+ function.close if non-empty]
|
| 404 |
+
```
|
| 405 |
+
|
| 406 |
+
Wrapped in per-call markers (with optional parallel call repetition) then optionally in section markers.
|
| 407 |
+
|
| 408 |
+
**`build_tool_parser_tag_tagged()`**: For each tool function, builds one parser per argument:
|
| 409 |
+
|
| 410 |
+
- String types: `tool_arg_string_value(schema(until(value_suffix), ...))`
|
| 411 |
+
- JSON types: `tool_arg_json_value(schema(json(), ...))`
|
| 412 |
+
- Required args are plain; optional args wrapped in `optional()`
|
| 413 |
+
- Arguments joined with `space()` between consecutive parsers
|
| 414 |
+
|
| 415 |
+
For closing: uses `function.close` if present; otherwise uses `peek(per_call_end)` to avoid premature close during partial streaming; falls back to `tool_close(space())` to trigger mapper callbacks.
|
| 416 |
+
|
| 417 |
+
All three tool parsers return:
|
| 418 |
+
|
| 419 |
+
```text
|
| 420 |
+
reasoning + optional(content(until(trigger_marker))) + tool_calls + end()
|
| 421 |
+
```
|
| 422 |
+
|
| 423 |
+
Each returned parser is wrapped by `wrap_for_generation_prompt()`, which prepends a literal for any boilerplate prefix of the generation prompt (the portion before the reasoning start marker).
|
| 424 |
+
|
| 425 |
+
## Mapper
|
| 426 |
+
|
| 427 |
+
`common_chat_peg_mapper` maps PEG parse results (AST nodes) into `common_chat_msg` structures. Key design:
|
| 428 |
+
|
| 429 |
+
- **Buffered arguments**: Before `tool_name` is known, argument text goes to `args_buffer`; once the name is set, the buffer is flushed to `current_tool->arguments`
|
| 430 |
+
- **`args_target()`**: Returns a reference to whichever destination is currently active (buffer or tool args), eliminating branching
|
| 431 |
+
- **`closing_quote_pending`**: Tracks whether a closing `"` needs to be appended when a string argument value is finalized (for schema-declared string types in tagged format)
|
| 432 |
+
- **Whitespace-only reasoning**: Reasoning content that consists entirely of whitespace (e.g. from a `<think></think>` prefill) is cleared so the message shows no reasoning
|
| 433 |
+
- **Brace auto-closing**: At tool close, unclosed `{` braces are closed automatically
|
| 434 |
+
|
| 435 |
+
## Files
|
| 436 |
+
|
| 437 |
+
| File | Purpose |
|
| 438 |
+
|-------------------------------------------|---------------------------------------------------------------------------------|
|
| 439 |
+
| `common/chat-auto-parser.h` | All analysis structs, enums, `autoparser`, `peg_generator`, `generation_params` |
|
| 440 |
+
| `common/chat-auto-parser-generator.cpp` | Parser generator: `generate_parser()` and `build_parser()` methods |
|
| 441 |
+
| `common/chat-diff-analyzer.cpp` | Differential analysis implementation and workarounds |
|
| 442 |
+
| `common/chat-auto-parser-helpers.h/cpp` | `calculate_diff_split()`, `segmentize_markers()`, `compare_variants()`, |
|
| 443 |
+
| | `wrap_for_generation_prompt()`, string helpers |
|
| 444 |
+
| `common/chat-peg-parser.h/cpp` | `common_chat_peg_builder`, `common_chat_peg_mapper`, and helpers |
|
| 445 |
+
| `common/chat.cpp` | Entry point: `common_chat_templates_apply_jinja()` |
|
| 446 |
+
| `tools/parser/debug-template-parser.cpp` | Debug tool for template analysis |
|
| 447 |
+
| `tools/parser/template-analysis.cpp` | Template analysis tool |
|
| 448 |
+
|
| 449 |
+
## Testing & Debugging
|
| 450 |
+
|
| 451 |
+
### Debug Tools
|
| 452 |
+
|
| 453 |
+
**Template Debugger**: `tools/parser/debug-template-parser.cpp`
|
| 454 |
+
|
| 455 |
+
- Usage: `./bin/llama-debug-template-parser path/to/template.jinja`
|
| 456 |
+
- Shows detected format, markers, generated parser, and GBNF grammar
|
| 457 |
+
|
| 458 |
+
**Template Analysis**: `tools/parser/template-analysis.cpp`
|
| 459 |
+
|
| 460 |
+
- Usage: `./bin/llama-template-analysis path/to/template.jinja`
|
| 461 |
+
|
| 462 |
+
**Debug Logging**: Enable with `LLAMA_ARG_LOG_VERBOSITY=2`
|
| 463 |
+
|
| 464 |
+
- Shows detailed analysis steps, pattern extraction results, and generated parser structure
|
| 465 |
+
|
| 466 |
+
**PEG Test Builder**: Fluent API for creating test cases — see [tests/test-chat.cpp:947-1043](tests/test-chat.cpp#L947-L1043). Example usage:
|
| 467 |
+
|
| 468 |
+
```cpp
|
| 469 |
+
auto tst = peg_tester("models/templates/Template.jinja");
|
| 470 |
+
tst.test("input text")
|
| 471 |
+
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
| 472 |
+
.tools({tool_json})
|
| 473 |
+
.parallel_tool_calls(true)
|
| 474 |
+
.enable_thinking(true)
|
| 475 |
+
.expect(expected_message)
|
| 476 |
+
.run();
|
| 477 |
+
```
|
| 478 |
+
|
| 479 |
+
### Tested Templates
|
| 480 |
+
|
| 481 |
+
The following templates have active tests in `tests/test-chat.cpp`:
|
| 482 |
+
|
| 483 |
+
| Template | Format | Notes |
|
| 484 |
+
| -------- | ------ | ----- |
|
| 485 |
+
| Ministral-3-14B-Reasoning | Reasoning | `[THINK]...[/THINK]` tags (specialized handler) |
|
| 486 |
+
| NVIDIA-Nemotron-3-Nano-30B | TAG_WITH_TAGGED | Reasoning + tools |
|
| 487 |
+
| CohereForAI Command-R7B | JSON_NATIVE | `<\|START_THINKING\|>`/`<\|START_RESPONSE\|>` markers |
|
| 488 |
+
| Google Gemma 2 2B | Content only | No tool support |
|
| 489 |
+
| Qwen-QwQ-32B | Reasoning | Forced-open thinking |
|
| 490 |
+
| NousResearch Hermes 2 Pro | JSON_NATIVE | `<tool_call>` wrapper |
|
| 491 |
+
| IBM Granite 3.3 | JSON_NATIVE | `<think></think>` + `<response></response>` |
|
| 492 |
+
| IBM Granite 4.0 | JSON_NATIVE | `<tool_call>` wrapper (same template used by 4.1) |
|
| 493 |
+
| ByteDance Seed-OSS | TAG_WITH_TAGGED | Custom `<seed:think>` and `<seed:tool_call>` tags |
|
| 494 |
+
| Qwen3-Coder | TAG_WITH_TAGGED | XML-style tool format |
|
| 495 |
+
| DeepSeek V3.1 | JSON_NATIVE | Forced thinking mode |
|
| 496 |
+
| GLM-4.6 | TAG_WITH_TAGGED | `<tool_call>name\n<arg_key>...<arg_value>...` format |
|
| 497 |
+
| GLM-4.7-Flash | TAG_WITH_TAGGED | Updated GLM format |
|
| 498 |
+
| Kimi-K2-Thinking | JSON_NATIVE | Reasoning + JSON tools |
|
| 499 |
+
| Apertus-8B-Instruct | JSON_NATIVE | Function name as JSON key |
|
| 500 |
+
| MiniMax-M2 | TAG_WITH_JSON | XML invoke with JSON args |
|
| 501 |
+
| NVIDIA-Nemotron-Nano-v2 | JSON_NATIVE | `<TOOLCALL>` wrapper (nested) |
|
| 502 |
+
| CohereForAI Command-R Plus | JSON_NATIVE | Markdown code block format |
|
| 503 |
+
| Mistral-Nemo-Instruct-2407 | JSON_NATIVE | `[TOOL_CALLS]` wrapper with ID field |
|
| 504 |
+
| Functionary v3.1 | TAG_WITH_JSON | `<function=X>` format |
|
| 505 |
+
| Functionary v3.2 | Specialized | `>>>` recipient delimiter (dedicated handler) |
|
| 506 |
+
| Fireworks Firefunction v2 | TAG_WITH_JSON | Fireworks tool format |
|
| 507 |
+
| DeepSeek R1 Distill (Llama/Qwen) | Reasoning | Forced-open thinking |
|
| 508 |
+
| llama-cpp-deepseek-r1 | Reasoning | Forced-open thinking |
|
| 509 |
+
| Kimi-K2 / Kimi-K2-Instruct | JSON_NATIVE | JSON tools with special markers |
|
| 510 |
+
| Llama 3.1/3.2/3.3 | JSON_NATIVE | Standard Llama tool format |
|
| 511 |
+
| OpenAI GPT-OSS | Specialized | Channel-based (dedicated handler) |
|
| 512 |
+
| Apriel 1.5 | JSON_NATIVE | `<tool_calls>` wrapper with JSON array |
|
| 513 |
+
| Apriel 1.6 Thinker | Reasoning | Implicit reasoning start |
|
| 514 |
+
| Mistral Small 3.2 | JSON_NATIVE | `[TOOL_CALLS]func[ARGS]{...}` with call ID |
|
| 515 |
+
| Devstral | JSON_NATIVE | `[TOOL_CALLS]func[ARGS]{...}` without call ID |
|
| 516 |
+
| StepFun 3.5 Flash | TAG_WITH_TAGGED | `<function=X><parameter=Y>` format |
|
| 517 |
+
|
| 518 |
+
## Adding Support for New Templates
|
| 519 |
+
|
| 520 |
+
To support a new template format:
|
| 521 |
+
|
| 522 |
+
1. **If it follows standard patterns** — The auto-parser should detect it automatically. Run `llama-debug-template-parser` to verify markers are correctly extracted.
|
| 523 |
+
2. **If differential analysis extracts incorrect markers** — Add a workaround lambda to the `workarounds` vector in `common/chat-diff-analyzer.cpp`. Inspect the template source for a unique identifying substring.
|
| 524 |
+
3. **If it needs fundamentally different handling** — Add a dedicated handler function in `chat.cpp` before the auto-parser block (as done for GPT-OSS, Functionary v3.2, and Ministral).
|
| 525 |
+
|
| 526 |
+
## Edge Cases and Quirks
|
| 527 |
+
|
| 528 |
+
1. **Generation Prompt & Reasoning Prefill**: The generation prompt is extracted by diffing `add_generation_prompt=false` vs `true` in `common_chat_templates_apply_jinja`, so it contains exactly what the template appends — avoiding false positives from prior conversation turns.
|
| 529 |
+
2. **Per-Call vs Per-Section Markers**: Some templates wrap each tool call individually (`per_call_start/end`); others wrap the entire section (`section_start/end`). T2 (`check_per_call_markers()`) disambiguates by checking if the second call in a two-call output starts with the section marker.
|
| 530 |
+
3. **Tag Boundary Fixing**: `calculate_diff_split()` iteratively adjusts prefix/suffix boundaries to avoid splitting `<tag>` or `[marker]` tokens, ensuring clean extraction.
|
| 531 |
+
4. **Call ID Side Effects**: When a call ID is detected, `per_call_end` may have been incorrectly set to include the call ID suffix. T7 clears `per_call_end` in this case.
|
| 532 |
+
5. **Tool Analysis Gating**: `analyze_tools` is only constructed (and all tool analysis phases run) when `jinja_caps.supports_tool_calls` is true. Within tool analysis, `check_per_call_markers()` (T2) only runs if `jinja_caps.supports_parallel_tool_calls`.
|
| 533 |
+
6. **`analyze_arguments()` Gating**: Within tool analysis, A1 and A2 (argument name/value marker extraction) only run for `TAG_WITH_TAGGED` format. `extract_argument_separator()` and `extract_args_markers()` run for all non-`JSON_NATIVE` formats.
|
| 534 |
+
7. **Undetected Tool Format**: If `analyze_tools` concludes tool calling is supported but cannot determine the format, `build_parser()` logs an error and returns `eps()` (graceful degradation) rather than aborting.
|
llama.cpp/docs/backend/BLIS.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BLIS Installation Manual
|
| 2 |
+
------------------------
|
| 3 |
+
|
| 4 |
+
BLIS is a portable software framework for high-performance BLAS-like dense linear algebra libraries. It has received awards and recognition, including the 2023 James H. Wilkinson Prize for Numerical Software and the 2020 SIAM Activity Group on Supercomputing Best Paper Prize. BLIS provides a new BLAS-like API and a compatibility layer for traditional BLAS routine calls. It offers features such as object-based API, typed API, BLAS and CBLAS compatibility layers.
|
| 5 |
+
|
| 6 |
+
Project URL: https://github.com/flame/blis
|
| 7 |
+
|
| 8 |
+
### Prepare:
|
| 9 |
+
|
| 10 |
+
Compile BLIS:
|
| 11 |
+
|
| 12 |
+
```bash
|
| 13 |
+
git clone https://github.com/flame/blis
|
| 14 |
+
cd blis
|
| 15 |
+
./configure --enable-cblas -t openmp,pthreads auto
|
| 16 |
+
# will install to /usr/local/ by default.
|
| 17 |
+
make -j
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
Install BLIS:
|
| 21 |
+
|
| 22 |
+
```bash
|
| 23 |
+
sudo make install
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
We recommend using openmp since it's easier to modify the cores being used.
|
| 27 |
+
|
| 28 |
+
### llama.cpp compilation
|
| 29 |
+
|
| 30 |
+
CMake:
|
| 31 |
+
|
| 32 |
+
```bash
|
| 33 |
+
mkdir build
|
| 34 |
+
cd build
|
| 35 |
+
cmake -DGGML_BLAS=ON -DGGML_BLAS_VENDOR=FLAME ..
|
| 36 |
+
make -j
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
### llama.cpp execution
|
| 40 |
+
|
| 41 |
+
According to the BLIS documentation, we could set the following
|
| 42 |
+
environment variables to modify the behavior of openmp:
|
| 43 |
+
|
| 44 |
+
```bash
|
| 45 |
+
export GOMP_CPU_AFFINITY="0-19"
|
| 46 |
+
export BLIS_NUM_THREADS=14
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
And then run the binaries as normal.
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
### Intel specific issue
|
| 53 |
+
|
| 54 |
+
Some might get the error message saying that `libimf.so` cannot be found.
|
| 55 |
+
Please follow this [stackoverflow page](https://stackoverflow.com/questions/70687930/intel-oneapi-2022-libimf-so-no-such-file-or-directory-during-openmpi-compila).
|
| 56 |
+
|
| 57 |
+
### Reference:
|
| 58 |
+
|
| 59 |
+
1. https://github.com/flame/blis#getting-started
|
| 60 |
+
2. https://github.com/flame/blis/blob/master/docs/Multithreading.md
|
llama.cpp/docs/backend/CANN.md
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# llama.cpp for CANN
|
| 2 |
+
|
| 3 |
+
- [Background](#background)
|
| 4 |
+
- [News](#news)
|
| 5 |
+
- [OS](#os)
|
| 6 |
+
- [Hardware](#hardware)
|
| 7 |
+
- [Model Supports](#model-supports)
|
| 8 |
+
- [DataType Supports](#datatype-supports)
|
| 9 |
+
- [Docker](#docker)
|
| 10 |
+
- [Linux](#linux)
|
| 11 |
+
- [Environment variable setup](#environment-variable-setup)
|
| 12 |
+
- [TODO](#todo)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
## Background
|
| 16 |
+
|
| 17 |
+
**Ascend NPU** is a range of AI processors using Neural Processing Unit. It will efficiently handle matrix-matrix multiplication, dot-product and scalars.
|
| 18 |
+
|
| 19 |
+
**CANN** (Compute Architecture for Neural Networks) is a heterogeneous computing architecture for AI scenarios, providing support for multiple AI frameworks on the top and serving AI processors and programming at the bottom. It plays a crucial role in bridging the gap between upper and lower layers, and is a key platform for improving the computing efficiency of Ascend AI processors. Meanwhile, it offers a highly efficient and easy-to-use programming interface for diverse application scenarios, allowing users to rapidly build AI applications and services based on the Ascend platform.
|
| 20 |
+
|
| 21 |
+
**Llama.cpp + CANN**
|
| 22 |
+
|
| 23 |
+
The llama.cpp CANN backend is designed to support Ascend NPU. It utilize the ability of AscendC and ACLNN which are integrated to CANN Toolkit and kernels to using Ascend NPU directly.
|
| 24 |
+
|
| 25 |
+
## News
|
| 26 |
+
|
| 27 |
+
- 2024.11
|
| 28 |
+
- Support F16 and F32 data type model for Ascend 310P NPU.
|
| 29 |
+
- 2024.8
|
| 30 |
+
- Support `Q4_0` and `Q8_0` data type for Ascend NPU.
|
| 31 |
+
- 2024.7
|
| 32 |
+
- Create CANN backend for Ascend NPU.
|
| 33 |
+
|
| 34 |
+
## OS
|
| 35 |
+
|
| 36 |
+
| OS | Status | Verified |
|
| 37 |
+
|:-------:|:-------:|:----------------------------------------------:|
|
| 38 |
+
| Linux | Support | Ubuntu 22.04, OpenEuler22.03 |
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
## Hardware
|
| 42 |
+
|
| 43 |
+
### Ascend NPU
|
| 44 |
+
|
| 45 |
+
You can retrieve your Ascend device IDs using the following command:
|
| 46 |
+
|
| 47 |
+
```sh
|
| 48 |
+
lspci -n | grep -Eo '19e5:d[0-9a-f]{3}' | cut -d: -f2
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
**Devices**
|
| 52 |
+
|
| 53 |
+
| Device Id | Product Series | Product Models | Chip Model | Verified Status |
|
| 54 |
+
|:---------:|----------------|----------------|:----------:|:---------------:|
|
| 55 |
+
| d803 | Atlas A3 Train | | 910C | |
|
| 56 |
+
| d803 | Atlas A3 Infer | | 910C | |
|
| 57 |
+
| d802 | Atlas A2 Train | | 910B | |
|
| 58 |
+
| d802 | Atlas A2 Infer | Atlas 300I A2 | 910B | Support |
|
| 59 |
+
| d801 | Atlas Train | | 910 | |
|
| 60 |
+
| d500 | Atlas Infer | Atlas 300I Duo | 310P | Support |
|
| 61 |
+
|
| 62 |
+
*Notes:*
|
| 63 |
+
|
| 64 |
+
- If you have trouble with Ascend NPU device, please create a issue with **[CANN]** prefix/tag.
|
| 65 |
+
- If you run successfully with your Ascend NPU device, please help update the upper table.
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
## Model Supports
|
| 69 |
+
|
| 70 |
+
<details>
|
| 71 |
+
<summary>Text-only</summary>
|
| 72 |
+
|
| 73 |
+
| Model Name | FP16 | Q4_0 | Q8_0 |
|
| 74 |
+
|:----------------------------|:-----:|:----:|:----:|
|
| 75 |
+
| Llama-2 | √ | √ | √ |
|
| 76 |
+
| Llama-3 | √ | √ | √ |
|
| 77 |
+
| Mistral-7B | √ | √ | √ |
|
| 78 |
+
| Mistral MOE | √ | √ | √ |
|
| 79 |
+
| DBRX | - | - | - |
|
| 80 |
+
| Falcon | √ | √ | √ |
|
| 81 |
+
| Chinese LLaMA/Alpaca | √ | √ | √ |
|
| 82 |
+
| Vigogne(French) | √ | √ | √ |
|
| 83 |
+
| BERT | x | x | x |
|
| 84 |
+
| Koala | √ | √ | √ |
|
| 85 |
+
| Baichuan | √ | √ | √ |
|
| 86 |
+
| Aquila 1 & 2 | √ | √ | √ |
|
| 87 |
+
| Starcoder models | √ | √ | √ |
|
| 88 |
+
| Refact | √ | √ | √ |
|
| 89 |
+
| MPT | √ | √ | √ |
|
| 90 |
+
| Bloom | √ | √ | √ |
|
| 91 |
+
| Yi models | √ | √ | √ |
|
| 92 |
+
| stablelm models | √ | √ | √ |
|
| 93 |
+
| DeepSeek models | x | x | x |
|
| 94 |
+
| Qwen models | √ | √ | √ |
|
| 95 |
+
| PLaMo-13B | √ | √ | √ |
|
| 96 |
+
| Phi models | √ | √ | √ |
|
| 97 |
+
| PhiMoE | √ | √ | √ |
|
| 98 |
+
| GPT-2 | √ | √ | √ |
|
| 99 |
+
| Orion | √ | √ | √ |
|
| 100 |
+
| InternlLM2 | √ | √ | √ |
|
| 101 |
+
| CodeShell | √ | √ | √ |
|
| 102 |
+
| Gemma | √ | √ | √ |
|
| 103 |
+
| Mamba | √ | √ | √ |
|
| 104 |
+
| Xverse | √ | √ | √ |
|
| 105 |
+
| command-r models | √ | √ | √ |
|
| 106 |
+
| Grok-1 | - | - | - |
|
| 107 |
+
| SEA-LION | √ | √ | √ |
|
| 108 |
+
| GritLM-7B | √ | √ | √ |
|
| 109 |
+
| OLMo | √ | √ | √ |
|
| 110 |
+
| OLMo 2 | √ | �� | √ |
|
| 111 |
+
| OLMoE | √ | √ | √ |
|
| 112 |
+
| Granite models | √ | √ | √ |
|
| 113 |
+
| GPT-NeoX | √ | √ | √ |
|
| 114 |
+
| Pythia | √ | √ | √ |
|
| 115 |
+
| Snowflake-Arctic MoE | - | - | - |
|
| 116 |
+
| Smaug | √ | √ | √ |
|
| 117 |
+
| Poro 34B | √ | √ | √ |
|
| 118 |
+
| Bitnet b1.58 models | √ | x | x |
|
| 119 |
+
| Flan-T5 | √ | √ | √ |
|
| 120 |
+
| Open Elm models | x | √ | √ |
|
| 121 |
+
| chatGLM3-6B + ChatGLM4-9b + GLMEdge-1.5b + GLMEdge-4b | √ | √ | √ |
|
| 122 |
+
| GLM-4-0414 | √ | √ | √ |
|
| 123 |
+
| SmolLM | √ | √ | √ |
|
| 124 |
+
| EXAONE-3.0-7.8B-Instruct | √ | √ | √ |
|
| 125 |
+
| FalconMamba Models | √ | √ | √ |
|
| 126 |
+
| Jais Models | - | x | x |
|
| 127 |
+
| Bielik-11B-v2.3 | √ | √ | √ |
|
| 128 |
+
| RWKV-6 | - | √ | √ |
|
| 129 |
+
| QRWKV-6 | √ | √ | √ |
|
| 130 |
+
| GigaChat-20B-A3B | x | x | x |
|
| 131 |
+
| Trillion-7B-preview | √ | √ | √ |
|
| 132 |
+
| Ling models | √ | √ | √ |
|
| 133 |
+
|
| 134 |
+
</details>
|
| 135 |
+
|
| 136 |
+
<details>
|
| 137 |
+
<summary>Multimodal</summary>
|
| 138 |
+
|
| 139 |
+
| Model Name | FP16 | Q4_0 | Q8_0 |
|
| 140 |
+
|:----------------------------|:-----:|:----:|:----:|
|
| 141 |
+
| LLaVA 1.5 models, LLaVA 1.6 models | x | x | x |
|
| 142 |
+
| BakLLaVA | √ | √ | √ |
|
| 143 |
+
| Obsidian | √ | - | - |
|
| 144 |
+
| ShareGPT4V | x | - | - |
|
| 145 |
+
| MobileVLM 1.7B/3B models | - | - | - |
|
| 146 |
+
| Yi-VL | - | - | - |
|
| 147 |
+
| Mini CPM | √ | √ | √ |
|
| 148 |
+
| Moondream | √ | √ | √ |
|
| 149 |
+
| Bunny | √ | - | - |
|
| 150 |
+
| GLM-EDGE | √ | √ | √ |
|
| 151 |
+
| Qwen2-VL | √ | √ | √ |
|
| 152 |
+
|
| 153 |
+
</details>
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
## DataType Supports
|
| 158 |
+
|
| 159 |
+
| DataType | 910B | 310P |
|
| 160 |
+
|:----------------------:|:-------:|:-------:|
|
| 161 |
+
| FP16 | Support | Support |
|
| 162 |
+
| Q8_0 | Support | Partial |
|
| 163 |
+
| Q4_0 | Support | Partial |
|
| 164 |
+
| BF16 | Support | |
|
| 165 |
+
|
| 166 |
+
> **310P note**
|
| 167 |
+
> - `Q8_0`: data transform / buffer path is implemented, and `GET_ROWS` is supported, but quantized `MUL_MAT` / `MUL_MAT_ID` are not supported.
|
| 168 |
+
> - `Q4_0`: data transform / buffer path is implemented, but quantized `MUL_MAT` / `MUL_MAT_ID` are not supported.
|
| 169 |
+
|
| 170 |
+
## Docker
|
| 171 |
+
|
| 172 |
+
### Build Images
|
| 173 |
+
You can get a image with llama.cpp in one command.
|
| 174 |
+
```sh
|
| 175 |
+
docker build -t llama-cpp-cann -f .devops/llama-cli-cann.Dockerfile .
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
### Run container
|
| 179 |
+
|
| 180 |
+
```sh
|
| 181 |
+
# Find all cards.
|
| 182 |
+
npu-smi info
|
| 183 |
+
|
| 184 |
+
# Select the cards that you want to use, make sure these cards are not used by someone.
|
| 185 |
+
# Following using cards of device0.
|
| 186 |
+
docker run --name llamacpp \
|
| 187 |
+
--device /dev/davinci0 \
|
| 188 |
+
--device /dev/davinci_manager \
|
| 189 |
+
--device /dev/devmm_svm \
|
| 190 |
+
--device /dev/hisi_hdc \
|
| 191 |
+
-v /usr/local/dcmi:/usr/local/dcmi \
|
| 192 |
+
-v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
|
| 193 |
+
-v /usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/ \
|
| 194 |
+
-v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
|
| 195 |
+
-v /PATH_TO_YOUR_MODELS/:/app/models \
|
| 196 |
+
-it llama-cpp-cann \
|
| 197 |
+
-m /app/models/MODEL_PATH \
|
| 198 |
+
-ngl 32 \
|
| 199 |
+
-p "Building a website can be done in 10 simple steps:"
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
*Notes:*
|
| 203 |
+
|
| 204 |
+
- You may need to install Ascend Driver and firmware on the **host** machine *(Please refer to the [Linux configuration](#linux) for details)*.
|
| 205 |
+
|
| 206 |
+
## Linux
|
| 207 |
+
|
| 208 |
+
### I. Setup Environment
|
| 209 |
+
|
| 210 |
+
1. **Configure Ascend user and group**
|
| 211 |
+
|
| 212 |
+
```sh
|
| 213 |
+
sudo groupadd HwHiAiUser
|
| 214 |
+
sudo useradd -g HwHiAiUser -d /home/HwHiAiUser -m HwHiAiUser -s /bin/bash
|
| 215 |
+
sudo usermod -aG HwHiAiUser $USER
|
| 216 |
+
```
|
| 217 |
+
|
| 218 |
+
2. **Install dependencies**
|
| 219 |
+
|
| 220 |
+
**Ubuntu/Debian:**
|
| 221 |
+
```sh
|
| 222 |
+
sudo apt-get update
|
| 223 |
+
sudo apt-get install -y gcc python3 python3-pip linux-headers-$(uname -r)
|
| 224 |
+
```
|
| 225 |
+
|
| 226 |
+
**RHEL/CentOS:**
|
| 227 |
+
```sh
|
| 228 |
+
sudo yum makecache
|
| 229 |
+
sudo yum install -y gcc python3 python3-pip kernel-headers-$(uname -r) kernel-devel-$(uname -r)
|
| 230 |
+
```
|
| 231 |
+
|
| 232 |
+
3. **Install CANN (driver + toolkit)**
|
| 233 |
+
|
| 234 |
+
> The `Ascend-cann` package includes both the driver and toolkit.
|
| 235 |
+
> `$ARCH` can be `x86_64` or `aarch64`, `$CHIP` can be `910b` or `310p`.
|
| 236 |
+
|
| 237 |
+
```sh
|
| 238 |
+
wget https://ascend-repo.obs.cn-east-2.myhuaweicloud.com/CANN/CANN%208.5.T63/Ascend-cann_8.5.0_linux-$ARCH.run
|
| 239 |
+
sudo bash ./Ascend-cann_8.5.0_linux-$ARCH.run --install
|
| 240 |
+
|
| 241 |
+
wget https://ascend-repo.obs.cn-east-2.myhuaweicloud.com/CANN/CANN%208.5.T63/Ascend-cann-$CHIP-ops_8.5.0_linux-$ARCH.run
|
| 242 |
+
sudo bash ./Ascend-cann-$CHIP-ops_8.5.0_linux-$ARCH.run --install
|
| 243 |
+
```
|
| 244 |
+
|
| 245 |
+
4. **Verify installation**
|
| 246 |
+
|
| 247 |
+
```sh
|
| 248 |
+
npu-smi info
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
If device information is displayed correctly, the driver is functioning properly.
|
| 252 |
+
|
| 253 |
+
```sh
|
| 254 |
+
# Set environment variables (adjust path if needed)
|
| 255 |
+
source /usr/local/Ascend/cann/set_env.sh
|
| 256 |
+
|
| 257 |
+
python3 -c "import acl; print(acl.get_soc_name())"
|
| 258 |
+
```
|
| 259 |
+
|
| 260 |
+
If the command outputs the chip model, the installation was successful.
|
| 261 |
+
|
| 262 |
+
### II. Build llama.cpp
|
| 263 |
+
|
| 264 |
+
```sh
|
| 265 |
+
cmake -B build -DGGML_CANN=on -DCMAKE_BUILD_TYPE=release
|
| 266 |
+
cmake --build build --config release
|
| 267 |
+
```
|
| 268 |
+
|
| 269 |
+
### III. Run the inference
|
| 270 |
+
|
| 271 |
+
1. **Retrieve and prepare model**
|
| 272 |
+
|
| 273 |
+
You can refer to the general [*Obtaining and quantizing models*](../../README.md#obtaining-and-quantizing-models) guide for model prepration.
|
| 274 |
+
|
| 275 |
+
**Notes**:
|
| 276 |
+
|
| 277 |
+
- CANN backend only supports FP16/Q4_0/Q8_0 models currently.
|
| 278 |
+
|
| 279 |
+
2. **Launch inference**
|
| 280 |
+
|
| 281 |
+
There are two device selection modes:
|
| 282 |
+
|
| 283 |
+
- Single device: Use one device target specified by the user.
|
| 284 |
+
- Multiple devices: Automatically choose the devices with the same backend.
|
| 285 |
+
|
| 286 |
+
| Device selection | Parameter |
|
| 287 |
+
|:----------------:|:--------------------------------------:|
|
| 288 |
+
| Single device | --split-mode none --main-gpu DEVICE_ID |
|
| 289 |
+
| Multiple devices | --split-mode layer (default) |
|
| 290 |
+
|
| 291 |
+
Examples:
|
| 292 |
+
|
| 293 |
+
- Use device 0:
|
| 294 |
+
|
| 295 |
+
```sh
|
| 296 |
+
./build/bin/llama-cli -m path_to_model -p "Building a website can be done in 10 simple steps:" -n 400 -e -ngl 33 -sm none -mg 0
|
| 297 |
+
```
|
| 298 |
+
|
| 299 |
+
- Use multiple devices:
|
| 300 |
+
|
| 301 |
+
```sh
|
| 302 |
+
./build/bin/llama-cli -m path_to_model -p "Building a website can be done in 10 simple steps:" -n 400 -e -ngl 33 -sm layer
|
| 303 |
+
```
|
| 304 |
+
|
| 305 |
+
### **GitHub contribution**:
|
| 306 |
+
Please add the **[CANN]** prefix/tag in issues/PRs titles to help the CANN-team check/address them without delay.
|
| 307 |
+
|
| 308 |
+
## Updates
|
| 309 |
+
### Basic Flash Attention Support
|
| 310 |
+
The basic FA kernel with aclnnops has been added in aclnn_ops.cpp.
|
| 311 |
+
Currently, the FA only supports the cases with FP16 KV tensors and NO logit softcap.
|
| 312 |
+
Since the aclnn interface for flash attention cannot support the logit softcap, we will only update the quantized version in the future.
|
| 313 |
+
|
| 314 |
+
Authors from Peking University: Bizhao Shi (bshi@pku.edu.cn), Yuxin Yang (yxyang@pku.edu.cn), Ruiyang Ma (ruiyang@stu.pku.edu.cn), and Guojie Luo (gluo@pku.edu.cn).
|
| 315 |
+
|
| 316 |
+
We would like to thank Tuo Dai, Shanni Li, and all of the project maintainers from Huawei Technologies Co., Ltd for their help during the code development and pull request.
|
| 317 |
+
|
| 318 |
+
## Environment variable setup
|
| 319 |
+
|
| 320 |
+
### GGML_CANN_MEM_POOL
|
| 321 |
+
|
| 322 |
+
Specifies the memory pool management strategy, Default is vmm.
|
| 323 |
+
|
| 324 |
+
- vmm: Utilizes a virtual memory manager pool. If hardware support for VMM is unavailable, falls back to the legacy (leg) memory pool.
|
| 325 |
+
|
| 326 |
+
- prio: Employs a priority queue-based memory pool management.
|
| 327 |
+
|
| 328 |
+
- leg: Uses a fixed-size buffer pool.
|
| 329 |
+
|
| 330 |
+
### GGML_CANN_DISABLE_BUF_POOL_CLEAN
|
| 331 |
+
|
| 332 |
+
Controls automatic cleanup of the memory pool. This option is only effective when using the prio or leg memory pool strategies.
|
| 333 |
+
|
| 334 |
+
### GGML_CANN_WEIGHT_NZ
|
| 335 |
+
|
| 336 |
+
Converting the matmul weight format from ND to NZ to improve performance. Enabled by default.
|
| 337 |
+
|
| 338 |
+
### GGML_CANN_ACL_GRAPH
|
| 339 |
+
|
| 340 |
+
Operators are executed using ACL graph execution, rather than in op-by-op (eager) mode. Enabled by default. This option is only effective if `USE_ACL_GRAPH` was enabled at compilation time. To enable it, recompile using:
|
| 341 |
+
|
| 342 |
+
```sh
|
| 343 |
+
cmake -B build -DGGML_CANN=on -DCMAKE_BUILD_TYPE=release -DUSE_ACL_GRAPH=ON
|
| 344 |
+
cmake --build build --config release
|
| 345 |
+
```
|
| 346 |
+
|
| 347 |
+
### GGML_CANN_GRAPH_CACHE_CAPACITY
|
| 348 |
+
|
| 349 |
+
Maximum number of compiled CANN graphs kept in the LRU cache, default is 12. When the number of cached graphs exceeds this capacity, the least recently used graph will be evicted.
|
| 350 |
+
|
| 351 |
+
### GGML_CANN_PREFILL_USE_GRAPH
|
| 352 |
+
|
| 353 |
+
Enable ACL graph execution during the prefill stage, default is false. This option is only effective when FA is enabled.
|
| 354 |
+
|
| 355 |
+
### GGML_CANN_OPERATOR_FUSION
|
| 356 |
+
|
| 357 |
+
Enable operator fusion during computation, default is false. This option fuses compatible operators (e.g., ADD + RMS_NORM) to reduce overhead and improve performance.
|
llama.cpp/docs/backend/CUDA-FEDORA.md
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Setting Up CUDA on Fedora
|
| 2 |
+
|
| 3 |
+
In this guide we setup [Nvidia CUDA](https://docs.nvidia.com/cuda/) in a toolbox container. This guide is applicable for:
|
| 4 |
+
|
| 5 |
+
- [Fedora Workstation](https://fedoraproject.org/workstation/)
|
| 6 |
+
- [Atomic Desktops for Fedora](https://fedoraproject.org/atomic-desktops/)
|
| 7 |
+
- [Fedora Spins](https://fedoraproject.org/spins)
|
| 8 |
+
- [Other Distributions](https://containertoolbx.org/distros/), including `Red Hat Enterprise Linux >= 8.5`, `Arch Linux`, and `Ubuntu`.
|
| 9 |
+
|
| 10 |
+
## Table of Contents
|
| 11 |
+
|
| 12 |
+
- [Prerequisites](#prerequisites)
|
| 13 |
+
- [Using the Fedora 41 CUDA Repository](#using-the-fedora-41-cuda-repository)
|
| 14 |
+
- [Creating a Fedora Toolbox Environment](#creating-a-fedora-toolbox-environment)
|
| 15 |
+
- [Installing Essential Development Tools](#installing-essential-development-tools)
|
| 16 |
+
- [Adding the CUDA Repository](#adding-the-cuda-repository)
|
| 17 |
+
- [Installing Nvidia Driver Libraries](#installing-nvidia-driver-libraries)
|
| 18 |
+
- [Installing the CUDA Meta-Package](#installing-the-cuda-meta-package)
|
| 19 |
+
- [Configuring the Environment](#configuring-the-environment)
|
| 20 |
+
- [Verifying the Installation](#verifying-the-installation)
|
| 21 |
+
- [Conclusion](#conclusion)
|
| 22 |
+
- [Troubleshooting](#troubleshooting)
|
| 23 |
+
- [Additional Notes](#additional-notes)
|
| 24 |
+
- [References](#references)
|
| 25 |
+
|
| 26 |
+
## Prerequisites
|
| 27 |
+
|
| 28 |
+
- **Toolbox Installed on the Host System** `Fedora Silverblue` and `Fedora Workstation` both have toolbox by default, other distributions may need to install the [toolbox package](https://containertoolbx.org/install/).
|
| 29 |
+
- **NVIDIA Drivers and Graphics Card installed on Host System (recommended)** To run CUDA program, such as `llama.cpp`, the host should be setup to access your NVIDIA hardware. Fedora Hosts can use the [RPM Fusion Repository](https://rpmfusion.org/Howto/NVIDIA).
|
| 30 |
+
- **Internet connectivity** to download packages.
|
| 31 |
+
|
| 32 |
+
### Using the Fedora 41 CUDA Repository
|
| 33 |
+
|
| 34 |
+
The latest release is 41.
|
| 35 |
+
|
| 36 |
+
- [Fedora 41 CUDA Repository](https://developer.download.nvidia.com/compute/cuda/repos/fedora41/x86_64/)
|
| 37 |
+
|
| 38 |
+
**Note:** We recommend using a toolbox environment to prevent system conflicts.
|
| 39 |
+
|
| 40 |
+
## Creating a Fedora Toolbox Environment
|
| 41 |
+
|
| 42 |
+
This guide focuses on Fedora hosts, but with small adjustments, it can work for other hosts. Using the Fedora Toolbox allows us to install the necessary packages without affecting the host system.
|
| 43 |
+
|
| 44 |
+
**Note:** Toolbox is available for other systems, and even without Toolbox, it is possible to use Podman or Docker.
|
| 45 |
+
|
| 46 |
+
1. **Create a Fedora 41 Toolbox:**
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
toolbox create --image registry.fedoraproject.org/fedora-toolbox:41 --container fedora-toolbox-41-cuda
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
2. **Enter the Toolbox:**
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
toolbox enter --container fedora-toolbox-41-cuda
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
Inside the toolbox, you have root privileges and can install packages without affecting the host system.
|
| 59 |
+
|
| 60 |
+
## Installing Essential Development Tools
|
| 61 |
+
|
| 62 |
+
1. **Synchronize the DNF Package Manager:**
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
sudo dnf distro-sync
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
2. **Install **Vim** the default text editor (Optional):**
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
sudo dnf install vim-default-editor --allowerasing
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
The `--allowerasing` flag will allow the removal of the conflicting `nano-default-editor` package.
|
| 75 |
+
|
| 76 |
+
3. **Install Development Tools and Libraries:**
|
| 77 |
+
|
| 78 |
+
```bash
|
| 79 |
+
sudo dnf install @c-development @development-tools cmake
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
This installs essential packages for compiling software, including `gcc`, `make`, and other development headers.
|
| 83 |
+
|
| 84 |
+
## Adding the CUDA Repository
|
| 85 |
+
|
| 86 |
+
Add the NVIDIA CUDA repository to your DNF configuration:
|
| 87 |
+
|
| 88 |
+
```bash
|
| 89 |
+
sudo dnf config-manager addrepo --from-repofile=https://developer.download.nvidia.com/compute/cuda/repos/fedora41/x86_64/cuda-fedora41.repo
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
After adding the repository, synchronize the package manager again:
|
| 93 |
+
|
| 94 |
+
```bash
|
| 95 |
+
sudo dnf distro-sync
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
## Installing Nvidia Driver Libraries
|
| 99 |
+
|
| 100 |
+
First, we need to detect if the host is supplying the [NVIDIA driver libraries into the toolbox](https://github.com/containers/toolbox/blob/main/src/pkg/nvidia/nvidia.go):
|
| 101 |
+
|
| 102 |
+
```bash
|
| 103 |
+
ls -la /usr/lib64/libcuda.so.1
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
### If *`libcuda.so.1`* is missing:
|
| 107 |
+
|
| 108 |
+
```
|
| 109 |
+
ls: cannot access '/usr/lib64/libcuda.so.1': No such file or directory
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
**Explanation:**
|
| 113 |
+
The host dose not supply the CUDA drivers, **install them now:**
|
| 114 |
+
|
| 115 |
+
#### Install the Nvidia Driver Libraries on Guest:
|
| 116 |
+
|
| 117 |
+
```bash
|
| 118 |
+
sudo dnf install nvidia-driver-cuda nvidia-driver-libs nvidia-driver-cuda-libs nvidia-persistenced
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
### If *`libcuda.so.1`* exists:
|
| 122 |
+
```
|
| 123 |
+
lrwxrwxrwx. 1 root root 21 Mar 24 11:26 /usr/lib64/libcuda.so.1 -> libcuda.so.570.133.07
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
**Explanation:**
|
| 127 |
+
The host is supply the CUDA drivers, **we need to update the guest RPM Database accordingly:**
|
| 128 |
+
|
| 129 |
+
#### Update the Toolbox RPM Database to include the Host-Supplied Libraries:
|
| 130 |
+
|
| 131 |
+
Note: we do not actually install the libraries, we just update the DB so that the guest system knows they are supplied by the host.
|
| 132 |
+
|
| 133 |
+
##### 1. Download `nvidia-` parts that are supplied by the host RPM's (with dependencies)
|
| 134 |
+
|
| 135 |
+
```bash
|
| 136 |
+
sudo dnf download --destdir=/tmp/nvidia-driver-libs --resolve --arch x86_64 nvidia-driver-cuda nvidia-driver-libs nvidia-driver-cuda-libs nvidia-persistenced
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
##### 2. Update the RPM database to assume the installation of these packages.
|
| 140 |
+
|
| 141 |
+
```bash
|
| 142 |
+
sudo rpm --install --verbose --hash --justdb /tmp/nvidia-driver-libs/*
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
**Note:**
|
| 146 |
+
|
| 147 |
+
- The `--justdb` option only updates the RPM database, without touching the filesystem elsewhere.
|
| 148 |
+
|
| 149 |
+
##### Check that the RPM Database has been correctly updated:
|
| 150 |
+
|
| 151 |
+
**Note:** This is the same command as in the *"Install the Nvidia Driver Libraries on Guest"* for if *`libcuda.so.1`* was missing.
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
```bash
|
| 155 |
+
sudo dnf install nvidia-driver-cuda nvidia-driver-libs nvidia-driver-cuda-libs nvidia-persistenced
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
*(this time it will not install anything, as the database things that these packages are already installed)*
|
| 159 |
+
|
| 160 |
+
```
|
| 161 |
+
Updating and loading repositories:
|
| 162 |
+
Repositories loaded.
|
| 163 |
+
Package "nvidia-driver-cuda-3:570.124.06-1.fc41.x86_64" is already installed.
|
| 164 |
+
Package "nvidia-driver-libs-3:570.124.06-1.fc41.x86_64" is already installed.
|
| 165 |
+
Package "nvidia-driver-cuda-libs-3:570.124.06-1.fc41.x86_64" is already installed.
|
| 166 |
+
Package "nvidia-persistenced-3:570.124.06-1.fc41.x86_64" is already installed.
|
| 167 |
+
|
| 168 |
+
Nothing to do.
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
## Installing the CUDA Meta-Package
|
| 172 |
+
|
| 173 |
+
Now that the driver libraries are installed, proceed to install CUDA:
|
| 174 |
+
|
| 175 |
+
```bash
|
| 176 |
+
sudo dnf install cuda
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
This installs the CUDA toolkit and associated packages.
|
| 180 |
+
|
| 181 |
+
## Configuring the Environment
|
| 182 |
+
|
| 183 |
+
To use CUDA, add its binary directory to your system's `PATH`.
|
| 184 |
+
|
| 185 |
+
1. **Create a Profile Script:**
|
| 186 |
+
|
| 187 |
+
```bash
|
| 188 |
+
sudo sh -c 'echo "export PATH=\$PATH:/usr/local/cuda/bin" >> /etc/profile.d/cuda.sh'
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
**Explanation:**
|
| 192 |
+
|
| 193 |
+
- We add to `/etc/profile.d/` as the `/etc/` folder is unique to this particular container, and is not shared with other containers or the host system.
|
| 194 |
+
- The backslash `\` before `$PATH` ensures the variable is correctly written into the script.
|
| 195 |
+
|
| 196 |
+
2. **Make the Script Executable:**
|
| 197 |
+
|
| 198 |
+
```bash
|
| 199 |
+
sudo chmod +x /etc/profile.d/cuda.sh
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
3. **Source the Script to Update Your Environment:**
|
| 203 |
+
|
| 204 |
+
```bash
|
| 205 |
+
source /etc/profile.d/cuda.sh
|
| 206 |
+
```
|
| 207 |
+
|
| 208 |
+
**Note:** This command updates your current shell session with the new `PATH`. The `/etc/profile.d/cuda.sh` script ensures that the CUDA binaries are available in your `PATH` for all future sessions.
|
| 209 |
+
|
| 210 |
+
## Verifying the Installation
|
| 211 |
+
|
| 212 |
+
To confirm that CUDA is correctly installed and configured, check the version of the NVIDIA CUDA Compiler (`nvcc`):
|
| 213 |
+
|
| 214 |
+
```bash
|
| 215 |
+
nvcc --version
|
| 216 |
+
```
|
| 217 |
+
|
| 218 |
+
You should see output similar to:
|
| 219 |
+
|
| 220 |
+
```
|
| 221 |
+
nvcc: NVIDIA (R) Cuda compiler driver
|
| 222 |
+
Copyright (c) 2005-2025 NVIDIA Corporation
|
| 223 |
+
Built on Fri_Feb_21_20:23:50_PST_2025
|
| 224 |
+
Cuda compilation tools, release 12.8, V12.8.93
|
| 225 |
+
Build cuda_12.8.r12.8/compiler.35583870_0
|
| 226 |
+
```
|
| 227 |
+
|
| 228 |
+
This output confirms that the CUDA compiler is accessible and indicates the installed version.
|
| 229 |
+
|
| 230 |
+
## Conclusion
|
| 231 |
+
|
| 232 |
+
You have successfully set up CUDA on Fedora within a toolbox environment using the Fedora 41 CUDA repository. By manually updating the RPM db and configuring the environment, you can develop CUDA applications without affecting your host system.
|
| 233 |
+
|
| 234 |
+
## Troubleshooting
|
| 235 |
+
|
| 236 |
+
- **Installation Failures:**
|
| 237 |
+
|
| 238 |
+
- If you encounter errors during installation, carefully read the error messages. They often indicate conflicting files or missing dependencies.
|
| 239 |
+
- You may use the `--excludepath` option with `rpm` to exclude conflicting files during manual RPM installations.
|
| 240 |
+
|
| 241 |
+
- **Rebooting the Container:**
|
| 242 |
+
|
| 243 |
+
- Sometimes there may be a bug in the NVIDIA driver host passthrough (such as missing a shared library). Rebooting the container may solve this issue:
|
| 244 |
+
|
| 245 |
+
```bash
|
| 246 |
+
# on the host system
|
| 247 |
+
podman container restart --all
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
- **Environment Variables Not Set:**
|
| 251 |
+
- If `nvcc` is not found after installation, ensure that `/usr/local/cuda/bin` is in your `PATH`.
|
| 252 |
+
- Run `echo $PATH` to check if the path is included.
|
| 253 |
+
- Re-source the profile script or open a new terminal session.
|
| 254 |
+
|
| 255 |
+
## Additional Notes
|
| 256 |
+
|
| 257 |
+
- **Updating CUDA in the Future:**
|
| 258 |
+
|
| 259 |
+
- Keep an eye on the official NVIDIA repositories for updates to your Fedora version.
|
| 260 |
+
- When an updated repository becomes available, adjust your `dnf` configuration accordingly.
|
| 261 |
+
|
| 262 |
+
- **Building `llama.cpp`:**
|
| 263 |
+
|
| 264 |
+
- With CUDA installed, you can follow these [build instructions for `llama.cpp`](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md) to compile it with CUDA support.
|
| 265 |
+
- Ensure that any CUDA-specific build flags or paths are correctly set in your build configuration.
|
| 266 |
+
|
| 267 |
+
- **Using the Toolbox Environment:**
|
| 268 |
+
- The toolbox environment is isolated from your host system, which helps prevent conflicts.
|
| 269 |
+
- Remember that system files and configurations inside the toolbox are separate from the host. By default the home directory of the user is shared between the host and the toolbox.
|
| 270 |
+
|
| 271 |
+
---
|
| 272 |
+
|
| 273 |
+
**Disclaimer:** Manually installing and modifying system packages can lead to instability of the container. The above steps are provided as a guideline and may need adjustments based on your specific system configuration. Always back up important data before making significant system changes, especially as your home folder is writable and shared with the toolbox.
|
| 274 |
+
|
| 275 |
+
**Acknowledgments:** Special thanks to the Fedora community and NVIDIA documentation for providing resources that assisted in creating this guide.
|
| 276 |
+
|
| 277 |
+
## References
|
| 278 |
+
|
| 279 |
+
- [Fedora Toolbox Documentation](https://docs.fedoraproject.org/en-US/fedora-silverblue/toolbox/)
|
| 280 |
+
- [NVIDIA CUDA Installation Guide](https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html)
|
| 281 |
+
- [Podman Documentation](https://podman.io/get-started)
|
| 282 |
+
|
| 283 |
+
---
|
llama.cpp/docs/backend/OPENCL.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# llama.cpp for OpenCL
|
| 2 |
+
|
| 3 |
+
- [llama.cpp for OpenCL](#llamacpp-for-opencl)
|
| 4 |
+
- [Background](#background)
|
| 5 |
+
- [Llama.cpp + OpenCL](#llamacpp--opencl)
|
| 6 |
+
- [OS](#os)
|
| 7 |
+
- [Hardware](#hardware)
|
| 8 |
+
- [Adreno GPU](#adreno-gpu)
|
| 9 |
+
- [DataType Supports](#datatype-supports)
|
| 10 |
+
- [Model Preparation](#model-preparation)
|
| 11 |
+
- [Binary Kernel Library](#binary-kernel-library)
|
| 12 |
+
- [CMake Options](#cmake-options)
|
| 13 |
+
- [Android](#android)
|
| 14 |
+
- [I. Setup Environment](#i-setup-environment)
|
| 15 |
+
- [II. Build llama.cpp](#ii-build-llamacpp)
|
| 16 |
+
- [Windows 11 Arm64](#windows-11-arm64)
|
| 17 |
+
- [I. Setup Environment](#i-setup-environment-1)
|
| 18 |
+
- [II. Build llama.cpp](#ii-build-llamacpp-1)
|
| 19 |
+
- [Linux](#linux)
|
| 20 |
+
- [I. Setup Environment](#i-setup-environment-2)
|
| 21 |
+
- [II. Build llama.cpp](#ii-build-llamacpp-2)
|
| 22 |
+
- [Known Issues](#known-issues)
|
| 23 |
+
- [TODO](#todo)
|
| 24 |
+
|
| 25 |
+
## Background
|
| 26 |
+
|
| 27 |
+
OpenCL (Open Computing Language) is an open, royalty-free standard for cross-platform, parallel programming of diverse accelerators found in supercomputers, cloud servers, personal computers, mobile devices and embedded platforms. OpenCL specifies a programming language (based on C99) for programming these devices and application programming interfaces (APIs) to control the platform and execute programs on the compute devices. Similar to CUDA, OpenCL has been widely used to program GPUs and is supported by most GPU vendors.
|
| 28 |
+
|
| 29 |
+
### Llama.cpp + OpenCL
|
| 30 |
+
|
| 31 |
+
The llama.cpp OpenCL backend is designed to enable llama.cpp on **Qualcomm Adreno GPU** firstly via OpenCL. Thanks to the portabilty of OpenCL, the OpenCL backend can also run on certain Intel GPUs such as those that do not have [SYCL](/docs/backend/SYCL.md) support although the performance is not optimal.
|
| 32 |
+
|
| 33 |
+
## OS
|
| 34 |
+
|
| 35 |
+
| OS | Status | Verified |
|
| 36 |
+
|---------|---------|------------------------------------------------|
|
| 37 |
+
| Android | Support | Snapdragon 8 Gen 3, Snapdragon 8 Elite |
|
| 38 |
+
| Windows | Support | Windows 11 Arm64 with Snapdragon X Elite |
|
| 39 |
+
| Linux | Support | Ubuntu 22.04 WSL2 with Intel 12700H |
|
| 40 |
+
|
| 41 |
+
## Hardware
|
| 42 |
+
|
| 43 |
+
### Adreno GPU
|
| 44 |
+
|
| 45 |
+
**Verified devices**
|
| 46 |
+
|
| 47 |
+
| Adreno GPU | Status |
|
| 48 |
+
|:-------------------------------------:|:-------:|
|
| 49 |
+
| Adreno 750 (Snapdragon 8 Gen 3) | Support |
|
| 50 |
+
| Adreno 830 (Snapdragon 8 Elite) | Support |
|
| 51 |
+
| Adreno 840 (Snapdragon 8 Elite Gen 5) | Support |
|
| 52 |
+
| Adreno X1-85 (Snapdragon X Elite) | Support |
|
| 53 |
+
| Adreno X2-90 (Snapdragon X2 Elite) | Support |
|
| 54 |
+
|
| 55 |
+
> A6x GPUs with a recent driver and compiler are supported; they are usually found in IoT platforms.
|
| 56 |
+
However, A6x GPUs in phones are likely not supported due to the outdated driver and compiler.
|
| 57 |
+
|
| 58 |
+
## DataType Supports
|
| 59 |
+
|
| 60 |
+
| DataType | Status |
|
| 61 |
+
|:----------------------:|:--------------------------:|
|
| 62 |
+
| Q1_0 | Support |
|
| 63 |
+
| Q4_0 | Support |
|
| 64 |
+
| Q4_1 | Support |
|
| 65 |
+
| Q5_0 | Support |
|
| 66 |
+
| Q5_1 | Support |
|
| 67 |
+
| Q8_0 | Support |
|
| 68 |
+
| Q4_K | Support |
|
| 69 |
+
| Q5_K | Support |
|
| 70 |
+
| Q6_K | Support |
|
| 71 |
+
| MXFP4 | Support |
|
| 72 |
+
| IQ4_NL | Support |
|
| 73 |
+
|
| 74 |
+
## Model Preparation
|
| 75 |
+
|
| 76 |
+
Since common quantizations are supported now, it is recommanded to download GGUF models directly from Huggingface.
|
| 77 |
+
|
| 78 |
+
## Binary Kernel Library
|
| 79 |
+
|
| 80 |
+
A prebuilt binary kernel library has been introduced for Adreno GPUs.
|
| 81 |
+
It currently targets X2 GPUs (X2-90, X2-85 and X2-45) found in Snapdragon X2 SoC.
|
| 82 |
+
The library currently contains kernels for MUL_MAT_ID with Q4_0, Q4_1, Q4_K, MXFP4.
|
| 83 |
+
The library must be manually downloaded from https://softwarecenter.qualcomm.com/catalog/item/Adreno_Kernel_Library_GGML.
|
| 84 |
+
|
| 85 |
+
To allow using the kernel library, add `-DGGML_OPENCL_USE_ADRENO_BIN_KERNELS=ON` when configuring with CMake.
|
| 86 |
+
Then, extract `adreno-opencl-kernels.dll` from the zip file downloaded from the above URL and put it alongside the executables.
|
| 87 |
+
If kernels compatible with the current GPU are found in the library, they will be loaded and used.
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
## CMake Options
|
| 91 |
+
|
| 92 |
+
The OpenCL backend has the following CMake options that control the behavior of the backend.
|
| 93 |
+
|
| 94 |
+
| CMake options | Default value | Description |
|
| 95 |
+
|:------------------------------------:|:--------------:|:------------------------------------------|
|
| 96 |
+
| `GGML_OPENCL_EMBED_KERNELS` | `ON` | Embed OpenCL kernels into the executable. |
|
| 97 |
+
| `GGML_OPENCL_USE_ADRENO_KERNELS` | `ON` | Use kernels optimized for Adreno. |
|
| 98 |
+
| `GGML_OPENCL_USE_ADRENO_BIN_KERNELS` | `OFF` | Allow using binary kernel lib for Adreno. |
|
| 99 |
+
|
| 100 |
+
## Android
|
| 101 |
+
|
| 102 |
+
Ubuntu 22.04 is used for targeting Android. Make sure the following tools are accessible from command line,
|
| 103 |
+
|
| 104 |
+
* Git
|
| 105 |
+
* CMake 3.29
|
| 106 |
+
* Ninja
|
| 107 |
+
* Python3
|
| 108 |
+
|
| 109 |
+
### I. Setup Environment
|
| 110 |
+
|
| 111 |
+
1. **Install NDK**
|
| 112 |
+
|
| 113 |
+
```sh
|
| 114 |
+
cd ~
|
| 115 |
+
wget https://dl.google.com/android/repository/commandlinetools-linux-8512546_latest.zip && \
|
| 116 |
+
unzip commandlinetools-linux-8512546_latest.zip && \
|
| 117 |
+
mkdir -p ~/android-sdk/cmdline-tools && \
|
| 118 |
+
mv cmdline-tools latest && \
|
| 119 |
+
mv latest ~/android-sdk/cmdline-tools/ && \
|
| 120 |
+
rm -rf commandlinetools-linux-8512546_latest.zip
|
| 121 |
+
|
| 122 |
+
yes | ~/android-sdk/cmdline-tools/latest/bin/sdkmanager "ndk;26.3.11579264"
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
2. **Install OpenCL Headers and Library**
|
| 126 |
+
|
| 127 |
+
```sh
|
| 128 |
+
mkdir -p ~/dev/llm
|
| 129 |
+
cd ~/dev/llm
|
| 130 |
+
|
| 131 |
+
git clone https://github.com/KhronosGroup/OpenCL-Headers && \
|
| 132 |
+
cd OpenCL-Headers && \
|
| 133 |
+
cp -r CL ~/android-sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include
|
| 134 |
+
|
| 135 |
+
cd ~/dev/llm
|
| 136 |
+
|
| 137 |
+
git clone https://github.com/KhronosGroup/OpenCL-ICD-Loader && \
|
| 138 |
+
cd OpenCL-ICD-Loader && \
|
| 139 |
+
mkdir build_ndk26 && cd build_ndk26 && \
|
| 140 |
+
cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
| 141 |
+
-DCMAKE_TOOLCHAIN_FILE=$HOME/android-sdk/ndk/26.3.11579264/build/cmake/android.toolchain.cmake \
|
| 142 |
+
-DOPENCL_ICD_LOADER_HEADERS_DIR=$HOME/android-sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include \
|
| 143 |
+
-DANDROID_ABI=arm64-v8a \
|
| 144 |
+
-DANDROID_PLATFORM=24 \
|
| 145 |
+
-DANDROID_STL=c++_shared && \
|
| 146 |
+
ninja && \
|
| 147 |
+
cp libOpenCL.so ~/android-sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
### II. Build llama.cpp
|
| 151 |
+
|
| 152 |
+
```sh
|
| 153 |
+
cd ~/dev/llm
|
| 154 |
+
|
| 155 |
+
git clone https://github.com/ggml-org/llama.cpp && \
|
| 156 |
+
cd llama.cpp && \
|
| 157 |
+
mkdir build-android && cd build-android
|
| 158 |
+
|
| 159 |
+
cmake .. -G Ninja \
|
| 160 |
+
-DCMAKE_TOOLCHAIN_FILE=$HOME/android-sdk/ndk/26.3.11579264/build/cmake/android.toolchain.cmake \
|
| 161 |
+
-DANDROID_ABI=arm64-v8a \
|
| 162 |
+
-DANDROID_PLATFORM=android-28 \
|
| 163 |
+
-DBUILD_SHARED_LIBS=OFF \
|
| 164 |
+
-DGGML_OPENCL=ON
|
| 165 |
+
|
| 166 |
+
ninja
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
## Windows 11 Arm64
|
| 170 |
+
|
| 171 |
+
A Snapdragon X Elite device with Windows 11 Arm64 is used. Make sure the following tools are accessible from command line,
|
| 172 |
+
|
| 173 |
+
* Git
|
| 174 |
+
* CMake 3.29
|
| 175 |
+
* Clang 19
|
| 176 |
+
* Ninja
|
| 177 |
+
* Visual Studio 2022
|
| 178 |
+
* Powershell 7
|
| 179 |
+
* Python
|
| 180 |
+
|
| 181 |
+
Visual Studio provides necessary headers and libraries although it is not directly used for building.
|
| 182 |
+
Alternatively, Visual Studio Build Tools can be installed instead of the full Visual Studio.
|
| 183 |
+
|
| 184 |
+
> Note that building using Visual Studio's cl compiler is not supported. Clang must be used. Clang depends on libraries provided by Visual Studio to work. Therefore, Visual Studio must be installed. Alternatively, Visual Studio Build Tools can be installed instead of the full Visual Studio.
|
| 185 |
+
|
| 186 |
+
Powershell 7 is used for the following commands.
|
| 187 |
+
If an older version of Powershell is used, these commands may not work as they are.
|
| 188 |
+
|
| 189 |
+
### I. Setup Environment
|
| 190 |
+
|
| 191 |
+
1. **Install OpenCL Headers and Library**
|
| 192 |
+
|
| 193 |
+
```powershell
|
| 194 |
+
mkdir -p ~/dev/llm
|
| 195 |
+
|
| 196 |
+
cd ~/dev/llm
|
| 197 |
+
git clone https://github.com/KhronosGroup/OpenCL-Headers && cd OpenCL-Headers
|
| 198 |
+
mkdir build && cd build
|
| 199 |
+
cmake .. -G Ninja `
|
| 200 |
+
-DBUILD_TESTING=OFF `
|
| 201 |
+
-DOPENCL_HEADERS_BUILD_TESTING=OFF `
|
| 202 |
+
-DOPENCL_HEADERS_BUILD_CXX_TESTS=OFF `
|
| 203 |
+
-DCMAKE_INSTALL_PREFIX="$HOME/dev/llm/opencl"
|
| 204 |
+
cmake --build . --target install
|
| 205 |
+
|
| 206 |
+
cd ~/dev/llm
|
| 207 |
+
git clone https://github.com/KhronosGroup/OpenCL-ICD-Loader && cd OpenCL-ICD-Loader
|
| 208 |
+
mkdir build && cd build
|
| 209 |
+
cmake .. -G Ninja `
|
| 210 |
+
-DCMAKE_BUILD_TYPE=Release `
|
| 211 |
+
-DCMAKE_PREFIX_PATH="$HOME/dev/llm/opencl" `
|
| 212 |
+
-DCMAKE_INSTALL_PREFIX="$HOME/dev/llm/opencl"
|
| 213 |
+
cmake --build . --target install
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
### II. Build llama.cpp
|
| 217 |
+
|
| 218 |
+
```powershell
|
| 219 |
+
|
| 220 |
+
mkdir -p ~/dev/llm
|
| 221 |
+
cd ~/dev/llm
|
| 222 |
+
|
| 223 |
+
git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp
|
| 224 |
+
mkdir build && cd build
|
| 225 |
+
|
| 226 |
+
cmake .. -G Ninja `
|
| 227 |
+
-DCMAKE_TOOLCHAIN_FILE="$HOME/dev/llm/llama.cpp/cmake/arm64-windows-llvm.cmake" `
|
| 228 |
+
-DCMAKE_BUILD_TYPE=Release `
|
| 229 |
+
-DCMAKE_PREFIX_PATH="$HOME/dev/llm/opencl" `
|
| 230 |
+
-DBUILD_SHARED_LIBS=OFF `
|
| 231 |
+
-DGGML_OPENCL=ON
|
| 232 |
+
ninja
|
| 233 |
+
```
|
| 234 |
+
|
| 235 |
+
## Linux
|
| 236 |
+
|
| 237 |
+
The two steps just above also apply to Linux. When building for linux, the commands are mostly the same as those for PowerShell on Windows, but in the second step they do not have the `-DCMAKE_TOOLCHAIN_FILE` parameter, and then in both steps the backticks are replaced with back slashes.
|
| 238 |
+
|
| 239 |
+
If not installed already, install Git, CMake, Clang, Ninja and Python, then run in the terminal the following:
|
| 240 |
+
|
| 241 |
+
### I. Setup Environment
|
| 242 |
+
|
| 243 |
+
1. **Install OpenCL Headers and Library**
|
| 244 |
+
|
| 245 |
+
```bash
|
| 246 |
+
mkdir -p ~/dev/llm
|
| 247 |
+
|
| 248 |
+
cd ~/dev/llm
|
| 249 |
+
git clone https://github.com/KhronosGroup/OpenCL-Headers && cd OpenCL-Headers
|
| 250 |
+
mkdir build && cd build
|
| 251 |
+
cmake .. -G Ninja \
|
| 252 |
+
-DBUILD_TESTING=OFF \
|
| 253 |
+
-DOPENCL_HEADERS_BUILD_TESTING=OFF \
|
| 254 |
+
-DOPENCL_HEADERS_BUILD_CXX_TESTS=OFF \
|
| 255 |
+
-DCMAKE_INSTALL_PREFIX="$HOME/dev/llm/opencl"
|
| 256 |
+
cmake --build . --target install
|
| 257 |
+
|
| 258 |
+
cd ~/dev/llm
|
| 259 |
+
git clone https://github.com/KhronosGroup/OpenCL-ICD-Loader && cd OpenCL-ICD-Loader
|
| 260 |
+
mkdir build && cd build
|
| 261 |
+
cmake .. -G Ninja \
|
| 262 |
+
-DCMAKE_BUILD_TYPE=Release \
|
| 263 |
+
-DCMAKE_PREFIX_PATH="$HOME/dev/llm/opencl" \
|
| 264 |
+
-DCMAKE_INSTALL_PREFIX="$HOME/dev/llm/opencl"
|
| 265 |
+
cmake --build . --target install
|
| 266 |
+
```
|
| 267 |
+
|
| 268 |
+
### II. Build llama.cpp
|
| 269 |
+
|
| 270 |
+
```bash
|
| 271 |
+
mkdir -p ~/dev/llm
|
| 272 |
+
cd ~/dev/llm
|
| 273 |
+
|
| 274 |
+
git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp
|
| 275 |
+
mkdir build && cd build
|
| 276 |
+
|
| 277 |
+
cmake .. -G Ninja \
|
| 278 |
+
-DCMAKE_BUILD_TYPE=Release \
|
| 279 |
+
-DCMAKE_PREFIX_PATH="$HOME/dev/llm/opencl" \
|
| 280 |
+
-DBUILD_SHARED_LIBS=OFF \
|
| 281 |
+
-DGGML_OPENCL=ON
|
| 282 |
+
ninja
|
| 283 |
+
```
|
| 284 |
+
|
| 285 |
+
## Known Issues
|
| 286 |
+
|
| 287 |
+
- Flash attention does not always improve performance.
|
| 288 |
+
- Currently OpenCL backend works on A6xx GPUs with recent drivers and compilers (usually found in IoT platforms).
|
| 289 |
+
However, it does not work on A6xx GPUs found in phones with old drivers and compilers.
|
| 290 |
+
|
| 291 |
+
## TODO
|
| 292 |
+
|
| 293 |
+
- Improve flash attention
|
| 294 |
+
- Improve OpenCL C kernels performance
|