mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-10-22 04:37:44 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7170cad4d0 | ||
|
|
d88040c704 | ||
|
|
e197cef455 | ||
|
|
bf039c8496 | ||
|
|
dfeea52005 | ||
|
|
e3797b184d | ||
|
|
0b1281aeef | ||
|
|
4912ac24f1 | ||
|
|
84746c5c94 | ||
|
|
0682a576a7 | ||
|
|
a6b83d426f | ||
|
|
c7fc509417 | ||
|
|
a37cc46104 | ||
|
|
99fb997184 | ||
|
|
7775ffb7e4 |
@@ -425,7 +425,12 @@ const func_builtins & global_builtins() {
|
||||
throw raised_exception("Jinja Exception: " + msg);
|
||||
}},
|
||||
{"dict", toobject},
|
||||
{"namespace", toobject},
|
||||
{"namespace", [](const func_args & args) -> value {
|
||||
// unlike a dict, a namespace has no methods: ns.items is the attribute, as in jinja2
|
||||
value out = toobject(args);
|
||||
cast_val<value_object>(out)->has_builtins = false;
|
||||
return out;
|
||||
}},
|
||||
{"strftime_now", [](const func_args & args) -> value {
|
||||
args.ensure_vals<value_string>();
|
||||
std::string format = args.get_pos(0)->as_string().str();
|
||||
|
||||
@@ -17,8 +17,10 @@ __all__ = [
|
||||
|
||||
|
||||
TEXT_MODEL_MAP: dict[str, str] = {
|
||||
"NEOChatModel": "sensenova_u1",
|
||||
"AfmoeForCausalLM": "afmoe",
|
||||
"LagunaForCausalLM": "laguna",
|
||||
"FlintForCausalLM": "flint",
|
||||
"ApertusForCausalLM": "llama",
|
||||
"ArceeForCausalLM": "llama",
|
||||
"ArcticForCausalLM": "arctic",
|
||||
@@ -367,6 +369,7 @@ MMPROJ_MODEL_MAP: dict[str, str] = {
|
||||
"MuseSparkForConditionalGeneration": "muse_spark",
|
||||
"PaddleOCRVisionModel": "ernie",
|
||||
"Phi4ForCausalLMV": "phi",
|
||||
"NEOVisionModel": "sensenova_u1",
|
||||
"Qwen2AudioForConditionalGeneration": "ultravox",
|
||||
"Qwen2VLForConditionalGeneration": "qwenvl",
|
||||
"Qwen2VLModel": "qwenvl",
|
||||
|
||||
+5
-2
@@ -1701,7 +1701,7 @@ class TextModel(ModelBase):
|
||||
self.gguf_writer.add_expert_group_used_count(n_group_used)
|
||||
logger.info(f"gguf: expert groups used count = {n_group_used}")
|
||||
|
||||
if (score_func := self.find_hparam(["score_function", "scoring_func", "score_func", "moe_router_activation", "moe_router_activation_func", "expert_selection_fn", "router_score_func"], optional=True)) is not None:
|
||||
if (score_func := self.find_hparam(["score_function", "scoring_func", "score_func", "moe_router_activation", "moe_router_activation_func", "expert_selection_fn", "router_score_func", "router_score_function"], optional=True)) is not None:
|
||||
if score_func == "sigmoid":
|
||||
self.gguf_writer.add_expert_gating_func(gguf.ExpertGatingFuncType.SIGMOID)
|
||||
elif score_func == "softmax":
|
||||
@@ -1939,6 +1939,9 @@ class TextModel(ModelBase):
|
||||
if chkhsh == "52df12b4c8d4176e7481aab4b6e8454d1fd0a210a04a574f6d4e067d10e23c3e":
|
||||
# ref: https://huggingface.co/CohereLabs/North-Mini-Code-1.0
|
||||
res = "cohere2moe"
|
||||
if chkhsh == "d5420e601e979078c670bc503d9ac411c311fa76b76a76267c31e0d6ba816637":
|
||||
# ref: https://huggingface.co/RecursiveSI/flint
|
||||
res = "flint"
|
||||
if chkhsh == "e636dc30a262dcc0d8c323492e32ae2b70728f4df7dfe9737d9f920a282b8aea":
|
||||
# ref: https://huggingface.co/Qwen/Qwen1.5-7B
|
||||
res = "qwen2"
|
||||
@@ -3143,7 +3146,7 @@ def get_model_architecture(hparams: dict[str, Any], model_type: ModelType) -> st
|
||||
# Kimi-K3's text_config reports "KimiLinearForCausalLM", which is the older
|
||||
# Kimi-Linear-48B architecture and cannot load K3 (no attention residuals,
|
||||
# latent MoE, situ, ...). Route on the top-level architecture instead.
|
||||
if model_type == ModelType.TEXT and arch in ("StepVLForConditionalGeneration", "Sarashina2VisionForCausalLM", "Exaone4_5_ForConditionalGeneration", "Step3p7ForConditionalGeneration", "KimiK3ForConditionalGeneration"):
|
||||
if model_type == ModelType.TEXT and arch in ("NEOChatModel", "StepVLForConditionalGeneration", "Sarashina2VisionForCausalLM", "Exaone4_5_ForConditionalGeneration", "Step3p7ForConditionalGeneration", "KimiK3ForConditionalGeneration"):
|
||||
return arch
|
||||
|
||||
# if "architectures" is found in the sub-config, use that instead
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import ModelBase, gguf
|
||||
from .qwen import Qwen2MoeModel
|
||||
|
||||
|
||||
@ModelBase.register("FlintForCausalLM")
|
||||
@ModelBase.example("RecursiveSI/flint")
|
||||
class FlintModel(Qwen2MoeModel):
|
||||
model_arch = gguf.MODEL_ARCH.FLINT
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
dense = self.hparams["mlp_only_layers"]
|
||||
if dense != list(range(len(dense))):
|
||||
raise ValueError("Flint dense layers must precede the MoE layers")
|
||||
self.gguf_writer.add_leading_dense_block_count(len(dense))
|
||||
self.gguf_writer.add_expert_shared_count(
|
||||
self.hparams["shared_expert_intermediate_size"] // self.hparams["moe_intermediate_size"]
|
||||
)
|
||||
self.gguf_writer.add_expert_weights_norm(self.hparams["norm_topk_prob"])
|
||||
self.gguf_writer.add_expert_weights_scale(1.0)
|
||||
self.gguf_writer.add_sliding_window(self.hparams["sliding_window"])
|
||||
self.gguf_writer.add_sliding_window_pattern([
|
||||
kind == "sliding_attention" for kind in self.hparams["layer_types"]
|
||||
])
|
||||
self.gguf_writer.add_rope_pattern([
|
||||
i not in self.hparams["no_rope_layer_indices"] for i in range(self.block_count)
|
||||
])
|
||||
|
||||
def modify_tensors(self, data_torch, name, bid):
|
||||
if name.endswith(".softmax_offset"):
|
||||
name += ".weight"
|
||||
if name.endswith(".gate.expert_bias"):
|
||||
if not self.hparams["router_expert_bias"]:
|
||||
return
|
||||
name += ".bias"
|
||||
yield from super().modify_tensors(data_torch, name, bid)
|
||||
@@ -0,0 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
|
||||
from .base import MmprojModel, ModelBase, gguf
|
||||
from .qwen import Qwen2Model
|
||||
|
||||
|
||||
@ModelBase.example("sensenova/SenseNova-U1.5-8B-MoT")
|
||||
@ModelBase.register("NEOChatModel")
|
||||
class SenseNovaU1Model(Qwen2Model):
|
||||
model_arch = gguf.MODEL_ARCH.SENSENOVA_U1
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item):
|
||||
name, _ = item
|
||||
if not name.startswith("language_model.") or "_mot_gen" in name:
|
||||
return None
|
||||
return super().filter_tensors(item)
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
if int(self.hparams.get("num_experts") or 0) > 1:
|
||||
raise ValueError("SenseNova U1 conversion currently supports the dense understanding branch")
|
||||
if self.rope_parameters.get("rope_type", "default") not in ("default", None):
|
||||
raise ValueError("SenseNova U1 scaled RoPE is not supported")
|
||||
if self.hparams.get("attention_bias", False):
|
||||
raise ValueError("SenseNova U1 attention bias is not supported")
|
||||
if self.hparams.get("use_sliding_window", False):
|
||||
raise ValueError("SenseNova U1 sliding attention is not supported")
|
||||
super().set_gguf_parameters()
|
||||
self.gguf_writer.add_rope_freq_base_spatial(self.hparams["rope_theta_hw"])
|
||||
|
||||
def get_tensors(self):
|
||||
for name, gen in self.model_tensors.items():
|
||||
if name.endswith((".q_norm_hw.weight", ".k_norm_hw.weight")):
|
||||
continue
|
||||
data = gen()
|
||||
if name.endswith((".q_norm.weight", ".k_norm.weight")):
|
||||
spatial = self.model_tensors[name.replace("_norm.weight", "_norm_hw.weight")]()
|
||||
# Keep one weight vector; the graph normalizes its two halves independently.
|
||||
data = torch.cat((data, spatial), dim=0)
|
||||
yield name, data
|
||||
|
||||
|
||||
@ModelBase.register("NEOVisionModel")
|
||||
@ModelBase.example("sensenova/SenseNova-U1.5-8B-MoT")
|
||||
class SenseNovaU1VisionModel(MmprojModel):
|
||||
def __init__(self, dir_model: Path, *args, **kwargs):
|
||||
hparams = kwargs.pop("hparams", None)
|
||||
if hparams is None:
|
||||
hparams = ModelBase.load_hparams(dir_model, is_mistral_format=False)
|
||||
else:
|
||||
hparams = dict(hparams)
|
||||
|
||||
vision_config = dict(hparams["vision_config"])
|
||||
# NEOVisionModel has no transformer blocks
|
||||
# these defaults satisfy the generic mmproj metadata contract without replacing checkpoint values
|
||||
vision_config.setdefault("image_size", 0)
|
||||
vision_config.setdefault("intermediate_size", 0)
|
||||
vision_config.setdefault("num_hidden_layers", 0)
|
||||
vision_config.setdefault("num_attention_heads", 1)
|
||||
hparams["vision_config"] = vision_config
|
||||
super().__init__(dir_model, *args, hparams=hparams, **kwargs)
|
||||
|
||||
self.preprocessor_config.setdefault("image_mean", [0.485, 0.456, 0.406])
|
||||
self.preprocessor_config.setdefault("image_std", [0.229, 0.224, 0.225])
|
||||
|
||||
@classmethod
|
||||
def filter_tensors(cls, item):
|
||||
name, gen = item
|
||||
if not name.startswith("vision_model."):
|
||||
return None
|
||||
return super().filter_tensors((name, gen))
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
self.gguf_writer.add_clip_projector_type("sensenova_u1")
|
||||
vision_config = self.hparams["vision_config"]
|
||||
# These defaults match the known U1.5 checkpoint when older configs omit fields.
|
||||
hidden_size = int(vision_config.get("hidden_size", 1024))
|
||||
min_pixels = int(vision_config.get("min_pixels", 65536))
|
||||
max_pixels = int(vision_config.get("max_pixels", 16777216))
|
||||
downsample_ratio = float(vision_config.get("downsample_ratio", 0.5))
|
||||
if downsample_ratio <= 0:
|
||||
raise ValueError(f"SenseNova U1 downsample_ratio must be positive, got {downsample_ratio}")
|
||||
merge_size = round(1.0 / downsample_ratio)
|
||||
if abs((1.0 / downsample_ratio) - merge_size) > 1e-6:
|
||||
raise ValueError(f"SenseNova U1 downsample_ratio must have an integer reciprocal, got {downsample_ratio}")
|
||||
norm_eps = float(vision_config.get("layer_norm_eps", vision_config.get("rms_norm_eps", 1e-6)))
|
||||
self.gguf_writer.add_vision_head_dim(hidden_size)
|
||||
self.gguf_writer.add_vision_attention_layernorm_eps(norm_eps)
|
||||
self.gguf_writer.add_vision_min_pixels(min_pixels)
|
||||
self.gguf_writer.add_vision_max_pixels(max_pixels)
|
||||
self.gguf_writer.add_vision_spatial_merge_size(merge_size)
|
||||
|
||||
def modify_tensors(self, data_torch, name, bid):
|
||||
tensor_names = {
|
||||
"vision_model.embeddings.patch_embedding.weight": "v.patch_embd.weight",
|
||||
"vision_model.embeddings.patch_embedding.bias": "v.patch_embd.bias",
|
||||
"vision_model.embeddings.dense_embedding.weight": self.format_tensor_name(gguf.MODEL_TENSOR.V_MMPROJ, 0),
|
||||
"vision_model.embeddings.dense_embedding.bias": self.format_tensor_name(
|
||||
gguf.MODEL_TENSOR.V_MMPROJ, 0, suffix=".bias"
|
||||
),
|
||||
}
|
||||
if name not in tensor_names:
|
||||
raise ValueError(f"Unexpected SenseNova U1 vision tensor: {name!r}")
|
||||
return [(tensor_names[name], data_torch)]
|
||||
|
||||
def tensor_force_quant(self, name, new_name, bid, n_dims):
|
||||
if new_name == self.format_tensor_name(gguf.MODEL_TENSOR.V_MMPROJ, 0):
|
||||
return gguf.GGMLQuantizationType.F16 if self.ftype == gguf.LlamaFileType.MOSTLY_F16 else gguf.GGMLQuantizationType.F32
|
||||
return super().tensor_force_quant(name, new_name, bid, n_dims)
|
||||
@@ -101,6 +101,7 @@ models = [
|
||||
{"name": "command-r", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/CohereForAI/c4ai-command-r-v01", },
|
||||
{"name": "tiny_aya", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/CohereLabs/tiny-aya-base", },
|
||||
{"name": "cohere2moe", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/CohereLabs/North-Mini-Code-1.0", },
|
||||
{"name": "flint", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/RecursiveSI/flint", },
|
||||
{"name": "qwen2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/Qwen/Qwen1.5-7B", },
|
||||
{"name": "olmo", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/allenai/OLMo-1.7-7B-hf", },
|
||||
{"name": "dbrx", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/databricks/dbrx-base", },
|
||||
|
||||
+5
-5
@@ -27,7 +27,7 @@ Legend:
|
||||
| COL2IM_1D | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| CONCAT | ❌ | ✅ | ✅ | 🟡 | 🟡 | 🟡 | ✅ | 🟡 | ✅ | ✅ | 🟡 | ❌ | ❌ |
|
||||
| CONT | ❌ | 🟡 | ✅ | ✅ | 🟡 | 🟡 | ✅ | 🟡 | ✅ | ✅ | 🟡 | ❌ | ❌ |
|
||||
| CONV_1D_DW | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| CONV_1D_DW | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
|
||||
| CONV_2D | ❌ | ❌ | 🟡 | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | 🟡 | ✅ | ❌ | ❌ |
|
||||
| CONV_2D_DW | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| CONV_3D | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
@@ -76,10 +76,10 @@ Legend:
|
||||
| MUL_MAT | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 |
|
||||
| MUL_MAT_HADAMARD | ❌ | ❌ | ✅ | ❌ | ✅ | 🟡 | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | 🟡 |
|
||||
| MUL_MAT_ID | ❌ | 🟡 | ✅ | ✅ | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | ✅ | 🟡 | 🟡 | ❌ |
|
||||
| MUL_MAT_ID_W4A4 | ❌ | ❌ | ✅ | ❌ | ❌ | 🟡 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| MUL_MAT_ID_W4A8 | ❌ | ❌ | ✅ | ❌ | ❌ | 🟡 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| MUL_MAT_W4A4 | ❌ | ❌ | ✅ | ❌ | ❌ | 🟡 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| MUL_MAT_W4A8 | ❌ | ❌ | ✅ | ❌ | ❌ | 🟡 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| MUL_MAT_ID_W4A4 | ❌ | ❌ | ✅ | ❌ | ❌ | 🟡 | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| MUL_MAT_ID_W4A8 | ❌ | ❌ | ✅ | ❌ | ❌ | 🟡 | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| MUL_MAT_W4A4 | ❌ | ❌ | ✅ | ❌ | ❌ | 🟡 | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| MUL_MAT_W4A8 | ❌ | ❌ | ✅ | ❌ | ❌ | 🟡 | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| NEG | ❌ | ✅ | ✅ | 🟡 | 🟡 | 🟡 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| NORM | ❌ | ✅ | ✅ | ✅ | 🟡 | 🟡 | ✅ | ✅ | ✅ | ✅ | 🟡 | ❌ | ❌ |
|
||||
| OPT_STEP_ADAMW | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
|
||||
+7793
-6047
File diff suppressed because it is too large
Load Diff
@@ -650,9 +650,9 @@ function(ggml_add_cpu_backend_variant_impl tag_name)
|
||||
|
||||
# Fetch KleidiAI sources:
|
||||
include(FetchContent)
|
||||
set(KLEIDIAI_COMMIT_TAG "v1.31.0")
|
||||
set(KLEIDIAI_COMMIT_TAG "v1.32.0")
|
||||
set(KLEIDIAI_DOWNLOAD_URL "https://github.com/ARM-software/kleidiai/releases/download/${KLEIDIAI_COMMIT_TAG}/kleidiai-${KLEIDIAI_COMMIT_TAG}-src.tar.gz")
|
||||
set(KLEIDIAI_RELEASE_ARCHIVE_MD5 "930495f3aeb16b25dc18a2bde45924f4")
|
||||
set(KLEIDIAI_RELEASE_ARCHIVE_MD5 "05f1953428ffe8e8c7f585dc6d508887")
|
||||
|
||||
set(KLEIDIAI_FETCH_ARGS
|
||||
URL ${KLEIDIAI_DOWNLOAD_URL}
|
||||
|
||||
@@ -201,85 +201,117 @@ static struct kai_matmul_uker_config q4_k_matmul_config() {
|
||||
return config;
|
||||
}
|
||||
|
||||
template<struct kai_matmul_uker_api (*GetApi)(void)>
|
||||
static size_t q4_k_get_m_step() {
|
||||
const struct kai_matmul_uker_api api = GetApi();
|
||||
const struct kai_matmul_uker_config config = q4_k_matmul_config();
|
||||
return api.get_step(&config).m;
|
||||
static struct kai_matmul_uker_config default_matmul_config() {
|
||||
return {};
|
||||
}
|
||||
|
||||
template<struct kai_matmul_uker_api (*GetApi)(void)>
|
||||
static size_t q4_k_get_n_step() {
|
||||
const struct kai_matmul_uker_api api = GetApi();
|
||||
const struct kai_matmul_uker_config config = q4_k_matmul_config();
|
||||
return api.get_step(&config).n;
|
||||
}
|
||||
template<
|
||||
struct kai_matmul_uker_api (*GetApi)(void),
|
||||
struct kai_matmul_uker_config (*GetConfig)(void),
|
||||
size_t MR, size_t NR, size_t KR, size_t SR>
|
||||
struct matmul_uker_adapter {
|
||||
static size_t get_m_step() {
|
||||
const struct kai_matmul_uker_api api = GetApi();
|
||||
const struct kai_matmul_uker_config config = GetConfig();
|
||||
return api.get_step(&config).m;
|
||||
}
|
||||
|
||||
template<size_t Value>
|
||||
static size_t q4_k_get_constant() {
|
||||
return Value;
|
||||
}
|
||||
static size_t get_n_step() {
|
||||
const struct kai_matmul_uker_api api = GetApi();
|
||||
const struct kai_matmul_uker_config config = GetConfig();
|
||||
return api.get_step(&config).n;
|
||||
}
|
||||
|
||||
template<struct kai_matmul_uker_api (*GetApi)(void)>
|
||||
static size_t q4_k_get_dst_offset(size_t m_idx, size_t n_idx, size_t dst_stride) {
|
||||
const struct kai_matmul_uker_api api = GetApi();
|
||||
const struct kai_matmul_uker_config config = q4_k_matmul_config();
|
||||
const struct kai_matmul_uker_dst_dim_args index = { m_idx, n_idx };
|
||||
const struct kai_matmul_uker_dst_stride_args stride = { dst_stride };
|
||||
return api.get_dst_offset(&config, &index, &stride);
|
||||
}
|
||||
static size_t get_mr() {
|
||||
if constexpr (MR == 0) {
|
||||
return get_m_step();
|
||||
}
|
||||
return MR;
|
||||
}
|
||||
|
||||
template<struct kai_matmul_uker_api (*GetApi)(void)>
|
||||
static size_t q4_k_get_lhs_packed_offset(size_t m_idx, size_t k, size_t bl) {
|
||||
GGML_ASSERT(bl == KLEIDIAI_Q4_K_BLOCK_LENGTH);
|
||||
static size_t get_nr() {
|
||||
if constexpr (NR == 0) {
|
||||
return get_n_step();
|
||||
}
|
||||
return NR;
|
||||
}
|
||||
|
||||
const struct kai_matmul_uker_api api = GetApi();
|
||||
const struct kai_matmul_uker_config config = q4_k_matmul_config();
|
||||
const struct kai_matmul_uker_lhs_dim_args shape = { m_idx, k };
|
||||
const struct kai_matmul_uker_lhs_dim_args index = { m_idx, 0 };
|
||||
const struct kai_matmul_uker_lhs_stride_args stride = api.get_lhs_stride(&config, &shape);
|
||||
return api.get_lhs_offset(&config, &index, &stride);
|
||||
}
|
||||
static size_t get_kr() {
|
||||
return KR;
|
||||
}
|
||||
|
||||
template<struct kai_matmul_uker_api (*GetApi)(void)>
|
||||
static size_t q4_k_get_rhs_packed_offset(size_t n_idx, size_t k, size_t bl) {
|
||||
GGML_ASSERT(bl == KLEIDIAI_Q4_K_BLOCK_LENGTH);
|
||||
static size_t get_sr() {
|
||||
return SR;
|
||||
}
|
||||
|
||||
const struct kai_matmul_uker_api api = GetApi();
|
||||
const struct kai_matmul_uker_config config = q4_k_matmul_config();
|
||||
const struct kai_matmul_uker_rhs_dim_args shape = { n_idx, k };
|
||||
const struct kai_matmul_uker_rhs_dim_args index = { n_idx, 0 };
|
||||
const struct kai_matmul_uker_rhs_stride_args stride = api.get_rhs_stride(&config, &shape);
|
||||
return api.get_rhs_offset(&config, &index, &stride);
|
||||
}
|
||||
static size_t get_dst_offset(size_t m_idx, size_t n_idx, size_t dst_stride) {
|
||||
const struct kai_matmul_uker_api api = GetApi();
|
||||
const struct kai_matmul_uker_config config = GetConfig();
|
||||
const struct kai_matmul_uker_dst_dim_args index = { m_idx, n_idx };
|
||||
const struct kai_matmul_uker_dst_stride_args stride = { dst_stride };
|
||||
return api.get_dst_offset(&config, &index, &stride);
|
||||
}
|
||||
|
||||
template<struct kai_matmul_uker_api (*GetApi)(void)>
|
||||
static void q4_k_run_matmul(
|
||||
size_t m, size_t n, size_t k, size_t bl,
|
||||
const void * lhs_packed, const void * rhs_packed,
|
||||
void * dst, size_t dst_stride_row, size_t dst_stride_col,
|
||||
float clamp_min, float clamp_max) {
|
||||
GGML_ASSERT(bl == KLEIDIAI_Q4_K_BLOCK_LENGTH);
|
||||
GGML_ASSERT(dst_stride_col == sizeof(float));
|
||||
static size_t get_lhs_packed_offset(size_t m_idx, size_t k, size_t bl) {
|
||||
const struct kai_matmul_uker_api api = GetApi();
|
||||
const struct kai_matmul_uker_config config = GetConfig();
|
||||
GGML_ASSERT(bl == config.format.bl);
|
||||
const struct kai_matmul_uker_lhs_dim_args shape = { m_idx, k };
|
||||
const struct kai_matmul_uker_lhs_dim_args index = { m_idx, 0 };
|
||||
const struct kai_matmul_uker_lhs_stride_args stride = api.get_lhs_stride(&config, &shape);
|
||||
return api.get_lhs_offset(&config, &index, &stride);
|
||||
}
|
||||
|
||||
const struct kai_matmul_uker_api api = GetApi();
|
||||
const struct kai_matmul_uker_config config = q4_k_matmul_config();
|
||||
const struct kai_matmul_uker_lhs_dim_args lhs_shape = { m, k };
|
||||
const struct kai_matmul_uker_rhs_dim_args rhs_shape = { n, k };
|
||||
static size_t get_rhs_packed_offset(size_t n_idx, size_t k, size_t bl) {
|
||||
const struct kai_matmul_uker_api api = GetApi();
|
||||
const struct kai_matmul_uker_config config = GetConfig();
|
||||
GGML_ASSERT(bl == config.format.bl);
|
||||
const struct kai_matmul_uker_rhs_dim_args shape = { n_idx, k };
|
||||
const struct kai_matmul_uker_rhs_dim_args index = { n_idx, 0 };
|
||||
const struct kai_matmul_uker_rhs_stride_args stride = api.get_rhs_stride(&config, &shape);
|
||||
return api.get_rhs_offset(&config, &index, &stride);
|
||||
}
|
||||
|
||||
struct kai_matmul_uker_args args = {};
|
||||
args.flags = KAI_MATMUL_UKER_FLAGS_ARGS_CLAMP;
|
||||
args.shape = { m, n, k };
|
||||
args.operand.lhs.ptr = lhs_packed;
|
||||
args.operand.lhs.stride = api.get_lhs_stride(&config, &lhs_shape);
|
||||
args.operand.rhs.ptr = rhs_packed;
|
||||
args.operand.rhs.stride = api.get_rhs_stride(&config, &rhs_shape);
|
||||
args.operand.dst.ptr = dst;
|
||||
args.operand.dst.stride.m = dst_stride_row;
|
||||
args.activation.clamp.min_ptr = &clamp_min;
|
||||
args.activation.clamp.max_ptr = &clamp_max;
|
||||
api.run(&config, &args);
|
||||
}
|
||||
static void run(
|
||||
size_t m, size_t n, size_t k, size_t bl,
|
||||
const void * lhs_packed, const void * rhs_packed,
|
||||
void * dst, size_t dst_stride_row, size_t dst_stride_col,
|
||||
float clamp_min, float clamp_max) {
|
||||
const struct kai_matmul_uker_api api = GetApi();
|
||||
const struct kai_matmul_uker_config config = GetConfig();
|
||||
GGML_ASSERT(bl == config.format.bl);
|
||||
GGML_ASSERT(dst_stride_col == sizeof(float));
|
||||
const struct kai_matmul_uker_lhs_dim_args lhs_shape = { m, k };
|
||||
const struct kai_matmul_uker_rhs_dim_args rhs_shape = { n, k };
|
||||
|
||||
struct kai_matmul_uker_args args = {};
|
||||
args.flags = KAI_MATMUL_UKER_FLAGS_ARGS_CLAMP;
|
||||
args.shape = { m, n, k };
|
||||
args.operand.lhs.ptr = lhs_packed;
|
||||
args.operand.lhs.stride = api.get_lhs_stride(&config, &lhs_shape);
|
||||
args.operand.rhs.ptr = rhs_packed;
|
||||
args.operand.rhs.stride = api.get_rhs_stride(&config, &rhs_shape);
|
||||
args.operand.dst.ptr = dst;
|
||||
args.operand.dst.stride.m = dst_stride_row;
|
||||
args.activation.clamp.min_ptr = &clamp_min;
|
||||
args.activation.clamp.max_ptr = &clamp_max;
|
||||
api.run(&config, &args);
|
||||
}
|
||||
};
|
||||
|
||||
using q4_k_gemm_adapter = matmul_uker_adapter<
|
||||
kai_matmul_clamp_f32_f16p4vsx2_qai4c32p16vsx4s1s0sf16_4vsx16vs_sme2_mopa,
|
||||
q4_k_matmul_config, 0, 0, 4, 2>;
|
||||
using q4_k_gemv_adapter = matmul_uker_adapter<
|
||||
kai_matmul_clamp_f32_qsi8d32p1x4_qai4c32p16vsx4s1s0sf16_1x16vs_sme2_dot,
|
||||
q4_k_matmul_config, 0, 0, 4, 2>;
|
||||
// GEMM and GEMV share the RHS layout only at SVE256 (NR=8, KR=8, SR=1).
|
||||
using sve_q8_gemm_adapter = matmul_uker_adapter<
|
||||
kai_matmul_clamp_f32_qai8dxp4x8sf32_qsi8cxp4vsx8sf32bf32_16x4vs_sve_i8mm,
|
||||
default_matmul_config, 4, 0, 8, 1>;
|
||||
using sve_q8_gemv_adapter = matmul_uker_adapter<
|
||||
kai_matmul_clamp_f32_qai8dxp1x8_qsi8cxp8x8_1x8_sve_dot,
|
||||
default_matmul_config, 1, 8, 8, 1>;
|
||||
|
||||
static struct kai_matmul_pack_lhs_uker_config q4_k_lhs_pack_config() {
|
||||
struct kai_matmul_pack_lhs_uker_config config = {};
|
||||
@@ -628,7 +660,7 @@ static ggml_kleidiai_kernels gemm_gemv_kernels[] = {
|
||||
/* .packed_size_ex = */ &rhs_ps_fn5<kai_get_rhs_packed_size_rhs_pack_nxk_qsi4c32pscalef16_qsu4c32s16s0>,
|
||||
/* .pack_func_ex = */ &rhs_pack_fn12<kai_run_rhs_pack_nxk_qsi4c32pscalef16_qsu4c32s16s0>,
|
||||
},
|
||||
/* .required_cpu = */ CPU_FEATURE_SVE | CPU_FEATURE_I8MM | CPU_FEATURE_DOTPROD,
|
||||
/* .required_cpu = */ CPU_FEATURE_SVE | CPU_FEATURE_SVEI8MM | CPU_FEATURE_DOTPROD,
|
||||
/* .lhs_type = */ GGML_TYPE_F32,
|
||||
/* .rhs_type = */ GGML_TYPE_Q4_0,
|
||||
/* .op_type = */ GGML_TYPE_F32,
|
||||
@@ -737,16 +769,16 @@ static ggml_kleidiai_kernels gemm_gemv_kernels_q4_k[] = {
|
||||
{
|
||||
/* SME2 GEMM */
|
||||
{
|
||||
/* .get_m_step = */ &q4_k_get_m_step<kai_matmul_clamp_f32_f16p4vsx2_qai4c32p16vsx4s1s0sf16_4vsx16vs_sme2_mopa>,
|
||||
/* .get_n_step = */ &q4_k_get_n_step<kai_matmul_clamp_f32_f16p4vsx2_qai4c32p16vsx4s1s0sf16_4vsx16vs_sme2_mopa>,
|
||||
/* .get_mr = */ &q4_k_get_m_step<kai_matmul_clamp_f32_f16p4vsx2_qai4c32p16vsx4s1s0sf16_4vsx16vs_sme2_mopa>,
|
||||
/* .get_nr = */ &q4_k_get_n_step<kai_matmul_clamp_f32_f16p4vsx2_qai4c32p16vsx4s1s0sf16_4vsx16vs_sme2_mopa>,
|
||||
/* .get_kr = */ &q4_k_get_constant<4>,
|
||||
/* .get_sr = */ &q4_k_get_constant<2>,
|
||||
/* .get_dst_offset = */ &q4_k_get_dst_offset<kai_matmul_clamp_f32_f16p4vsx2_qai4c32p16vsx4s1s0sf16_4vsx16vs_sme2_mopa>,
|
||||
/* .get_lhs_offset_ex = */ &q4_k_get_lhs_packed_offset<kai_matmul_clamp_f32_f16p4vsx2_qai4c32p16vsx4s1s0sf16_4vsx16vs_sme2_mopa>,
|
||||
/* .get_rhs_packed_offset_ex = */ &q4_k_get_rhs_packed_offset<kai_matmul_clamp_f32_f16p4vsx2_qai4c32p16vsx4s1s0sf16_4vsx16vs_sme2_mopa>,
|
||||
/* .run_kernel_ex = */ &q4_k_run_matmul<kai_matmul_clamp_f32_f16p4vsx2_qai4c32p16vsx4s1s0sf16_4vsx16vs_sme2_mopa>,
|
||||
/* .get_m_step = */ q4_k_gemm_adapter::get_m_step,
|
||||
/* .get_n_step = */ q4_k_gemm_adapter::get_n_step,
|
||||
/* .get_mr = */ q4_k_gemm_adapter::get_mr,
|
||||
/* .get_nr = */ q4_k_gemm_adapter::get_nr,
|
||||
/* .get_kr = */ q4_k_gemm_adapter::get_kr,
|
||||
/* .get_sr = */ q4_k_gemm_adapter::get_sr,
|
||||
/* .get_dst_offset = */ q4_k_gemm_adapter::get_dst_offset,
|
||||
/* .get_lhs_offset_ex = */ q4_k_gemm_adapter::get_lhs_packed_offset,
|
||||
/* .get_rhs_packed_offset_ex = */ q4_k_gemm_adapter::get_rhs_packed_offset,
|
||||
/* .run_kernel_ex = */ q4_k_gemm_adapter::run,
|
||||
},
|
||||
{
|
||||
/* .get_offset = */ kai_get_lhs_offset_lhs_pack_f16pmrx2_f32_neon,
|
||||
@@ -756,16 +788,16 @@ static ggml_kleidiai_kernels gemm_gemv_kernels_q4_k[] = {
|
||||
},
|
||||
/* SME2 GEMV */
|
||||
{
|
||||
/* .get_m_step = */ &q4_k_get_m_step<kai_matmul_clamp_f32_qsi8d32p1x4_qai4c32p16vsx4s1s0sf16_1x16vs_sme2_dot>,
|
||||
/* .get_n_step = */ &q4_k_get_n_step<kai_matmul_clamp_f32_qsi8d32p1x4_qai4c32p16vsx4s1s0sf16_1x16vs_sme2_dot>,
|
||||
/* .get_mr = */ &q4_k_get_m_step<kai_matmul_clamp_f32_qsi8d32p1x4_qai4c32p16vsx4s1s0sf16_1x16vs_sme2_dot>,
|
||||
/* .get_nr = */ &q4_k_get_n_step<kai_matmul_clamp_f32_qsi8d32p1x4_qai4c32p16vsx4s1s0sf16_1x16vs_sme2_dot>,
|
||||
/* .get_kr = */ &q4_k_get_constant<4>,
|
||||
/* .get_sr = */ &q4_k_get_constant<2>,
|
||||
/* .get_dst_offset = */ &q4_k_get_dst_offset<kai_matmul_clamp_f32_qsi8d32p1x4_qai4c32p16vsx4s1s0sf16_1x16vs_sme2_dot>,
|
||||
/* .get_lhs_offset_ex = */ &q4_k_get_lhs_packed_offset<kai_matmul_clamp_f32_qsi8d32p1x4_qai4c32p16vsx4s1s0sf16_1x16vs_sme2_dot>,
|
||||
/* .get_rhs_packed_offset_ex = */ &q4_k_get_rhs_packed_offset<kai_matmul_clamp_f32_qsi8d32p1x4_qai4c32p16vsx4s1s0sf16_1x16vs_sme2_dot>,
|
||||
/* .run_kernel_ex = */ &q4_k_run_matmul<kai_matmul_clamp_f32_qsi8d32p1x4_qai4c32p16vsx4s1s0sf16_1x16vs_sme2_dot>,
|
||||
/* .get_m_step = */ q4_k_gemv_adapter::get_m_step,
|
||||
/* .get_n_step = */ q4_k_gemv_adapter::get_n_step,
|
||||
/* .get_mr = */ q4_k_gemv_adapter::get_mr,
|
||||
/* .get_nr = */ q4_k_gemv_adapter::get_nr,
|
||||
/* .get_kr = */ q4_k_gemv_adapter::get_kr,
|
||||
/* .get_sr = */ q4_k_gemv_adapter::get_sr,
|
||||
/* .get_dst_offset = */ q4_k_gemv_adapter::get_dst_offset,
|
||||
/* .get_lhs_offset_ex = */ q4_k_gemv_adapter::get_lhs_packed_offset,
|
||||
/* .get_rhs_packed_offset_ex = */ q4_k_gemv_adapter::get_rhs_packed_offset,
|
||||
/* .run_kernel_ex = */ q4_k_gemv_adapter::run,
|
||||
},
|
||||
{
|
||||
/* .get_offset = */ q4_k_lhs_get_offset,
|
||||
@@ -883,6 +915,54 @@ static ggml_kleidiai_kernels gemm_gemv_kernels_q8[] = {
|
||||
/* .rhs_type = */ GGML_TYPE_Q8_0,
|
||||
/* .op_type = */ GGML_TYPE_F32,
|
||||
},
|
||||
{
|
||||
/* SVE I8MM GEMM */
|
||||
{
|
||||
/* .get_m_step = */ sve_q8_gemm_adapter::get_m_step,
|
||||
/* .get_n_step = */ sve_q8_gemm_adapter::get_n_step,
|
||||
/* .get_mr = */ sve_q8_gemm_adapter::get_mr,
|
||||
/* .get_nr = */ sve_q8_gemm_adapter::get_nr,
|
||||
/* .get_kr = */ sve_q8_gemm_adapter::get_kr,
|
||||
/* .get_sr = */ sve_q8_gemm_adapter::get_sr,
|
||||
/* .get_dst_offset = */ sve_q8_gemm_adapter::get_dst_offset,
|
||||
/* .get_lhs_offset_ex = */ sve_q8_gemm_adapter::get_lhs_packed_offset,
|
||||
/* .get_rhs_packed_offset_ex = */ sve_q8_gemm_adapter::get_rhs_packed_offset,
|
||||
/* .run_kernel_ex = */ sve_q8_gemm_adapter::run,
|
||||
},
|
||||
/* .gemm_lhs_info = */ {
|
||||
/* .get_offset = */ kai_get_lhs_offset_lhs_quant_pack_qai8dxp_f32,
|
||||
/* .get_packed_offset_ex = */ &lhs_offs_fn5<kai_get_lhs_packed_offset_lhs_quant_pack_qai8dxp_f32>,
|
||||
/* .packed_size_ex = */ &lhs_ps_fn5<kai_get_lhs_packed_size_lhs_quant_pack_qai8dxp_f32>,
|
||||
/* .pack_func_ex = */ &lhs_pack_float_fn9_no_bl<kai_run_lhs_quant_pack_qai8dxp_f32>,
|
||||
},
|
||||
/* SVE DOT GEMV */
|
||||
{
|
||||
/* .get_m_step = */ sve_q8_gemv_adapter::get_m_step,
|
||||
/* .get_n_step = */ sve_q8_gemv_adapter::get_n_step,
|
||||
/* .get_mr = */ sve_q8_gemv_adapter::get_mr,
|
||||
/* .get_nr = */ sve_q8_gemv_adapter::get_nr,
|
||||
/* .get_kr = */ sve_q8_gemv_adapter::get_kr,
|
||||
/* .get_sr = */ sve_q8_gemv_adapter::get_sr,
|
||||
/* .get_dst_offset = */ sve_q8_gemv_adapter::get_dst_offset,
|
||||
/* .get_lhs_offset_ex = */ sve_q8_gemv_adapter::get_lhs_packed_offset,
|
||||
/* .get_rhs_packed_offset_ex = */ sve_q8_gemv_adapter::get_rhs_packed_offset,
|
||||
/* .run_kernel_ex = */ sve_q8_gemv_adapter::run,
|
||||
},
|
||||
/* .gemv_lhs_info = */ {
|
||||
/* .get_offset = */ kai_get_lhs_offset_lhs_quant_pack_qai8dxp_f32,
|
||||
/* .get_packed_offset_ex = */ &lhs_offs_fn5<kai_get_lhs_packed_offset_lhs_quant_pack_qai8dxp_f32>,
|
||||
/* .packed_size_ex = */ &lhs_ps_fn5<kai_get_lhs_packed_size_lhs_quant_pack_qai8dxp_f32>,
|
||||
/* .pack_func_ex = */ &lhs_pack_float_fn9_no_bl<kai_run_lhs_quant_pack_qai8dxp_f32>,
|
||||
},
|
||||
/* .rhs_info = */ {
|
||||
/* .packed_size_ex = */ &rhs_ps_fn5<kai_get_rhs_packed_size_rhs_pack_nxk_qsi8cxp_qsi8cx_neon>,
|
||||
/* .pack_func_ex = */ &rhs_pack_scale_fn12<kai_run_rhs_pack_nxk_qsi8cxp_qsi8cx_neon>,
|
||||
},
|
||||
/* .required_cpu = */ CPU_FEATURE_SVE | CPU_FEATURE_SVEI8MM | CPU_FEATURE_DOTPROD,
|
||||
/* .lhs_type = */ GGML_TYPE_F32,
|
||||
/* .rhs_type = */ GGML_TYPE_Q8_0,
|
||||
/* .op_type = */ GGML_TYPE_F32,
|
||||
},
|
||||
{
|
||||
/* I8MM GEMM */
|
||||
{
|
||||
|
||||
@@ -13,7 +13,8 @@ enum cpu_feature {
|
||||
CPU_FEATURE_SVE = 4,
|
||||
CPU_FEATURE_SME = 8,
|
||||
CPU_FEATURE_SME2 = 16,
|
||||
CPU_FEATURE_FP16 = 32
|
||||
CPU_FEATURE_FP16 = 32,
|
||||
CPU_FEATURE_SVEI8MM = 64
|
||||
};
|
||||
|
||||
inline cpu_feature& operator|=(cpu_feature& lhs, cpu_feature rhs) {
|
||||
|
||||
@@ -319,8 +319,10 @@ static void init_kleidiai_context(void) {
|
||||
|
||||
size_t detected_smcus = 0;
|
||||
|
||||
// The fixed-width SVE kernels require 32-byte vectors, including the Q8 shared RHS layout.
|
||||
ctx.features = (runtime_feat.has_dotprod ? CPU_FEATURE_DOTPROD : CPU_FEATURE_NONE) |
|
||||
(runtime_feat.has_i8mm ? CPU_FEATURE_I8MM : CPU_FEATURE_NONE) |
|
||||
(runtime_feat.has_svei8mm ? CPU_FEATURE_SVEI8MM : CPU_FEATURE_NONE) |
|
||||
(runtime_feat.has_fp16 ? CPU_FEATURE_FP16 : CPU_FEATURE_NONE) |
|
||||
(runtime_feat.sve_cnt == QK8_0 ? CPU_FEATURE_SVE : CPU_FEATURE_NONE);
|
||||
|
||||
@@ -1616,6 +1618,11 @@ public:
|
||||
const size_t kr = kernel->get_kr();
|
||||
const size_t sr = kernel->get_sr();
|
||||
const ggml_type rhs_type = kernels->rhs_type;
|
||||
if (rhs_type == GGML_TYPE_Q8_0 && (kernels->required_cpu & CPU_FEATURE_SVE)) {
|
||||
GGML_ASSERT(nr == kernels->gemv.get_nr());
|
||||
GGML_ASSERT(kr == kernels->gemv.get_kr());
|
||||
GGML_ASSERT(sr == kernels->gemv.get_sr());
|
||||
}
|
||||
const size_t block_len = rhs_type == GGML_TYPE_Q8_0 ? QK8_0 :
|
||||
rhs_type == GGML_TYPE_Q4_0 ? QK4_0 :
|
||||
rhs_type == GGML_TYPE_Q4_K ? GGML_KLEIDIAI_Q4_K_BL :
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
#define HWCAP2_I8MM (1ULL << 13)
|
||||
#endif
|
||||
|
||||
#if !defined(HWCAP2_SVEI8MM)
|
||||
#define HWCAP2_SVEI8MM (1ULL << 9)
|
||||
#endif
|
||||
|
||||
#if !defined(HWCAP_ASIMDDP)
|
||||
#define HWCAP_ASIMDDP (1 << 20)
|
||||
#endif
|
||||
@@ -63,6 +67,10 @@
|
||||
#define PF_ARM_SVE2_INSTRUCTIONS_AVAILABLE 47
|
||||
#endif
|
||||
|
||||
#if !defined(PF_ARM_SVE_I8MM_INSTRUCTIONS_AVAILABLE)
|
||||
#define PF_ARM_SVE_I8MM_INSTRUCTIONS_AVAILABLE 57
|
||||
#endif
|
||||
|
||||
#if !defined(PF_ARM_V82_I8MM_INSTRUCTIONS_AVAILABLE)
|
||||
#define PF_ARM_V82_I8MM_INSTRUCTIONS_AVAILABLE 66
|
||||
#endif
|
||||
@@ -87,6 +95,7 @@ typedef struct ggml_feats_arch64_runtime {
|
||||
bool has_sve;
|
||||
bool has_sve2;
|
||||
bool has_i8mm;
|
||||
bool has_svei8mm;
|
||||
bool has_sme;
|
||||
bool has_sme2;
|
||||
int sve_cnt;
|
||||
@@ -104,6 +113,7 @@ static inline ggml_feats_arch64_runtime_t ggml_feats_get_arch64_runtime(void) {
|
||||
runtime_feat.has_sve = !!(hwcap & HWCAP_SVE);
|
||||
runtime_feat.has_sve2 = !!(hwcap2 & HWCAP2_SVE2);
|
||||
runtime_feat.has_i8mm = !!(hwcap2 & HWCAP2_I8MM);
|
||||
runtime_feat.has_svei8mm = !!(hwcap2 & HWCAP2_SVEI8MM);
|
||||
runtime_feat.has_sme = !!(hwcap2 & HWCAP2_SME);
|
||||
runtime_feat.has_sme2 = !!(hwcap2 & HWCAP2_SME2);
|
||||
|
||||
@@ -153,6 +163,7 @@ static inline ggml_feats_arch64_runtime_t ggml_feats_get_arch64_runtime(void) {
|
||||
runtime_feat.has_sve = IsProcessorFeaturePresent(PF_ARM_SVE_INSTRUCTIONS_AVAILABLE) != 0;
|
||||
runtime_feat.has_sve2 = IsProcessorFeaturePresent(PF_ARM_SVE2_INSTRUCTIONS_AVAILABLE) != 0;
|
||||
runtime_feat.has_i8mm = IsProcessorFeaturePresent(PF_ARM_V82_I8MM_INSTRUCTIONS_AVAILABLE) != 0;
|
||||
runtime_feat.has_svei8mm = IsProcessorFeaturePresent(PF_ARM_SVE_I8MM_INSTRUCTIONS_AVAILABLE) != 0;
|
||||
runtime_feat.has_sme = IsProcessorFeaturePresent(PF_ARM_SME_INSTRUCTIONS_AVAILABLE) != 0;
|
||||
runtime_feat.has_sme2 = IsProcessorFeaturePresent(PF_ARM_SME2_INSTRUCTIONS_AVAILABLE) != 0;
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ set(GGML_OPENCL_KERNELS
|
||||
add
|
||||
add_id
|
||||
moe_add_id_glu
|
||||
argmax
|
||||
argsort
|
||||
tri
|
||||
fill
|
||||
|
||||
@@ -646,8 +646,8 @@ struct ggml_backend_opencl_context {
|
||||
ggml_cl_version platform_version;
|
||||
ggml_cl_version opencl_c_version;
|
||||
|
||||
// argsort is loaded in supports_op because its availability depends on how
|
||||
// many workgroups are allowed, which requires kernel compilation.
|
||||
// argmax and argsort are loaded in supports_op; argsort availability depends on the compiled kernel's workgroup limit.
|
||||
bool kernels_loaded_argmax = false;
|
||||
bool kernels_loaded_argsort = false;
|
||||
// rest of the kernels are currently always loaded in alloc_buffer.
|
||||
bool kernels_loaded = false;
|
||||
@@ -972,6 +972,7 @@ struct ggml_backend_opencl_context {
|
||||
cl_kernel kernel_solve_tri_f32;
|
||||
cl_kernel kernel_im2col_f32, kernel_im2col_f16;
|
||||
cl_kernel kernel_im2col_3d_f32, kernel_im2col_3d_f16;
|
||||
cl_kernel kernel_argmax_f32_i32;
|
||||
cl_kernel kernel_argsort_f32_i32;
|
||||
cl_kernel kernel_sum_rows_f32, kernel_sum_rows_f32_4;
|
||||
cl_kernel kernel_cumsum_blk, kernel_cumsum_add;
|
||||
@@ -1485,6 +1486,27 @@ static cl_program build_program_from_binary(cl_context ctx, cl_device_id dev, co
|
||||
return p;
|
||||
}
|
||||
|
||||
static void load_cl_kernels_argmax(ggml_backend_opencl_context *backend_ctx) {
|
||||
auto opencl_c_std =
|
||||
std::string("CL") + std::to_string(backend_ctx->opencl_c_version.major) + "." + std::to_string(backend_ctx->opencl_c_version.minor);
|
||||
std::string compile_opts = std::string("-cl-std=") + opencl_c_std;
|
||||
|
||||
if (!backend_ctx->kernels_loaded_argmax) {
|
||||
cl_int err;
|
||||
#ifdef GGML_OPENCL_EMBED_KERNELS
|
||||
const std::string kernel_src {
|
||||
#include "argmax.cl.h"
|
||||
};
|
||||
#else
|
||||
const std::string kernel_src = read_file("argmax.cl");
|
||||
#endif
|
||||
cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts);
|
||||
CL_CHECK((backend_ctx->kernel_argmax_f32_i32 = clCreateKernel(prog, "kernel_argmax_f32_i32", &err), err));
|
||||
CL_CHECK(clReleaseProgram(prog));
|
||||
backend_ctx->kernels_loaded_argmax = true;
|
||||
}
|
||||
}
|
||||
|
||||
static void load_cl_kernels_argsort(ggml_backend_opencl_context *backend_ctx) {
|
||||
// compiler options for general kernels
|
||||
auto opencl_c_std =
|
||||
@@ -9693,6 +9715,11 @@ static bool ggml_opencl_supports_op(ggml_backend_dev_t dev, const struct ggml_te
|
||||
return true;
|
||||
case GGML_OP_IM2COL_3D:
|
||||
return true;
|
||||
case GGML_OP_ARGMAX:
|
||||
load_cl_kernels_argmax(backend_ctx);
|
||||
return op->src[0]->type == GGML_TYPE_F32 && op->type == GGML_TYPE_I32 &&
|
||||
ggml_is_contiguous(op->src[0]) && ggml_is_contiguous(op) &&
|
||||
backend_ctx->get_kernel_workgroup_size(backend_ctx->kernel_argmax_f32_i32) >= 64;
|
||||
case GGML_OP_ARGSORT: {
|
||||
load_cl_kernels_argsort(backend_ctx);
|
||||
|
||||
@@ -30164,6 +30191,41 @@ static void ggml_cl_im2col_3d(ggml_backend_t backend, const ggml_tensor * src0,
|
||||
backend_ctx->enqueue_ndrange_kernel(kernel, 3, global_work_size, local_work_size, dst);
|
||||
}
|
||||
|
||||
static void ggml_cl_argmax(ggml_backend_t backend, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst) {
|
||||
GGML_ASSERT(src0);
|
||||
GGML_ASSERT(src0->extra);
|
||||
GGML_ASSERT(dst);
|
||||
GGML_ASSERT(dst->extra);
|
||||
GGML_UNUSED(src1);
|
||||
|
||||
GGML_ASSERT(src0->type == GGML_TYPE_F32);
|
||||
GGML_ASSERT(dst->type == GGML_TYPE_I32);
|
||||
GGML_ASSERT(ggml_is_contiguous(src0));
|
||||
GGML_ASSERT(ggml_is_contiguous(dst));
|
||||
|
||||
ggml_backend_opencl_context * backend_ctx = (ggml_backend_opencl_context *) backend->context;
|
||||
ggml_tensor_extra_cl * extra0 = (ggml_tensor_extra_cl *) src0->extra;
|
||||
ggml_tensor_extra_cl * extrad = (ggml_tensor_extra_cl *) dst->extra;
|
||||
|
||||
const cl_ulong offset0 = extra0->offset + src0->view_offs;
|
||||
const cl_ulong offsetd = extrad->offset + dst->view_offs;
|
||||
const int ne00 = src0->ne[0];
|
||||
const int nrows = ggml_nrows(src0);
|
||||
constexpr int nth = 64;
|
||||
|
||||
cl_kernel kernel = backend_ctx->kernel_argmax_f32_i32;
|
||||
CL_CHECK(clSetKernelArg(kernel, 0, sizeof(cl_mem), &extra0->data_device));
|
||||
CL_CHECK(clSetKernelArg(kernel, 1, sizeof(cl_ulong), &offset0));
|
||||
CL_CHECK(clSetKernelArg(kernel, 2, sizeof(cl_mem), &extrad->data_device));
|
||||
CL_CHECK(clSetKernelArg(kernel, 3, sizeof(cl_ulong), &offsetd));
|
||||
CL_CHECK(clSetKernelArg(kernel, 4, sizeof(int), &ne00));
|
||||
CL_CHECK(clSetKernelArg(kernel, 5, sizeof(int), &nrows));
|
||||
|
||||
size_t global_work_size[] = { (size_t) nth, (size_t) CEIL_DIV(nrows, 2), 1 };
|
||||
size_t local_work_size[] = { (size_t) nth, 1, 1 };
|
||||
backend_ctx->enqueue_ndrange_kernel(kernel, 3, global_work_size, local_work_size, dst);
|
||||
}
|
||||
|
||||
static void ggml_cl_argsort(ggml_backend_t backend, const ggml_tensor * src0, const ggml_tensor * src1, ggml_tensor * dst) {
|
||||
GGML_ASSERT(src0);
|
||||
GGML_ASSERT(src0->extra);
|
||||
@@ -31071,6 +31133,12 @@ bool ggml_cl_compute_forward(ggml_backend_t backend, struct ggml_tensor * tensor
|
||||
}
|
||||
func = ggml_cl_im2col_3d;
|
||||
break;
|
||||
case GGML_OP_ARGMAX:
|
||||
if (!any_on_device) {
|
||||
return false;
|
||||
}
|
||||
func = ggml_cl_argmax;
|
||||
break;
|
||||
case GGML_OP_ARGSORT:
|
||||
if (!any_on_device) {
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
#define ARGMAX_LANES_PER_ROW 32
|
||||
#define ARGMAX_ROWS_PER_GROUP 2
|
||||
#define ARGMAX_BLOCK_SIZE (ARGMAX_LANES_PER_ROW * ARGMAX_ROWS_PER_GROUP)
|
||||
|
||||
kernel void kernel_argmax_f32_i32(
|
||||
global const char * src0,
|
||||
ulong offset0,
|
||||
global char * dst,
|
||||
ulong offsetd,
|
||||
int ne00,
|
||||
int nrows) {
|
||||
const int local_id = get_local_id(0);
|
||||
const int col = local_id % ARGMAX_LANES_PER_ROW;
|
||||
const int row = get_group_id(1) * ARGMAX_ROWS_PER_GROUP + local_id / ARGMAX_LANES_PER_ROW;
|
||||
|
||||
float max_value = -INFINITY;
|
||||
int max_index = 0;
|
||||
|
||||
if (row < nrows) {
|
||||
global const float * src_row = (global const float *) (src0 + offset0) + row * ne00;
|
||||
for (int i = col; i < ne00; i += ARGMAX_LANES_PER_ROW) {
|
||||
const float value = src_row[i];
|
||||
if (value > max_value || (value == max_value && i > max_index)) {
|
||||
max_value = value;
|
||||
max_index = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
local float max_values[ARGMAX_BLOCK_SIZE];
|
||||
local int max_indices[ARGMAX_BLOCK_SIZE];
|
||||
|
||||
max_values[local_id] = max_value;
|
||||
max_indices[local_id] = max_index;
|
||||
|
||||
barrier(CLK_LOCAL_MEM_FENCE);
|
||||
|
||||
const int row_offset = local_id / ARGMAX_LANES_PER_ROW * ARGMAX_LANES_PER_ROW;
|
||||
for (int stride = ARGMAX_LANES_PER_ROW / 2; stride > 0; stride /= 2) {
|
||||
if (col < stride) {
|
||||
const float value = max_values[row_offset + col + stride];
|
||||
const int index = max_indices[row_offset + col + stride];
|
||||
if (value > max_values[row_offset + col] || (value == max_values[row_offset + col] && index > max_indices[row_offset + col])) {
|
||||
max_values[row_offset + col] = value;
|
||||
max_indices[row_offset + col] = index;
|
||||
}
|
||||
}
|
||||
barrier(CLK_LOCAL_MEM_FENCE);
|
||||
}
|
||||
|
||||
if (col == 0 && row < nrows) {
|
||||
global int * dst_row = (global int *) (dst + offsetd) + row;
|
||||
dst_row[0] = max_indices[row_offset];
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,16 @@ static inline int64_t whcn_output_index(int64_t n, int64_t c, int64_t y, int64_t
|
||||
return n * (P.OC * P.OW * P.OH) + c * P.OW * P.OH + y * P.OW + x;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
// cwhn layout helpers: buffers are [N][H][W][C] input and [KH][KW][IC][OC] kernel
|
||||
static inline int64_t cwhn_input_index(int64_t n, int64_t c, int64_t y, int64_t x, const conv2d_params & P) {
|
||||
return ((n * P.IH + y) * P.IW + x) * P.IC + c;
|
||||
}
|
||||
|
||||
static inline int64_t cwhn_kernel_index(int64_t c_out, int64_t c_in, int64_t ky, int64_t kx, const conv2d_params & P) {
|
||||
return ((ky * P.KW + kx) * P.IC + c_in) * P.OC + c_out;
|
||||
}
|
||||
|
||||
template <typename T, bool CWHN>
|
||||
static void conv2d_kernel(const float * input, const T * kernel, float * output,
|
||||
const conv2d_params P, const sycl::nd_item<3> & item_ct1) {
|
||||
const int64_t global_idx = item_ct1.get_local_id(2) +
|
||||
@@ -77,8 +86,8 @@ static void conv2d_kernel(const float * input, const T * kernel, float * output,
|
||||
const int64_t in_y = calculate_input_coord(out_y, ky, P.ST_Y, P.DL_Y, P.PD_Y);
|
||||
for (int64_t kx = bounds.x_min; kx < bounds.x_max; ++kx) {
|
||||
const int64_t in_x = calculate_input_coord(out_x, kx, P.ST_X, P.DL_X, P.PD_X);
|
||||
const float input_val = input[whcn_input_index(n, c_in, in_y, in_x, P)];
|
||||
const T kernel_val = kernel[whcn_kernel_index(c_out, c_in, ky, kx, P)];
|
||||
const float input_val = input[CWHN ? cwhn_input_index(n, c_in, in_y, in_x, P) : whcn_input_index(n, c_in, in_y, in_x, P)];
|
||||
const T kernel_val = kernel[CWHN ? cwhn_kernel_index(c_out, c_in, ky, kx, P) : whcn_kernel_index(c_out, c_in, ky, kx, P)];
|
||||
acc += input_val * ggml_sycl_cast<float>(kernel_val);
|
||||
}
|
||||
}
|
||||
@@ -87,7 +96,7 @@ static void conv2d_kernel(const float * input, const T * kernel, float * output,
|
||||
output[whcn_output_index(n, c_out, out_y, out_x, P)] = acc;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
template <typename T, bool CWHN>
|
||||
static void conv2d_sycl(const float * X_D, const T * K_D, float * Y_D,
|
||||
const conv2d_params P, const queue_ptr & stream) {
|
||||
const int num_blocks = (P.TOTAL + SYCL_CONV2D_BLOCK_SIZE - 1) / SYCL_CONV2D_BLOCK_SIZE;
|
||||
@@ -95,7 +104,7 @@ static void conv2d_sycl(const float * X_D, const T * K_D, float * Y_D,
|
||||
const sycl::range<3> block_nums(1, 1, num_blocks);
|
||||
stream->parallel_for(sycl::nd_range<3>(block_nums * block_dims, block_dims),
|
||||
[=](sycl::nd_item<3> item_ct1) {
|
||||
conv2d_kernel<T>(X_D, K_D, Y_D, P, item_ct1);
|
||||
conv2d_kernel<T, CWHN>(X_D, K_D, Y_D, P, item_ct1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,7 +117,14 @@ void ggml_sycl_op_conv2d(ggml_backend_sycl_context & ctx, ggml_tensor * dst) {
|
||||
const float * X_D = (const float *) input->data;
|
||||
float * Y_D = (float *) dst->data;
|
||||
|
||||
GGML_ASSERT(ggml_is_contiguous(kernel));
|
||||
// WHCN: contiguous [KW,KH,IC,OC] kernel and [W,H,C,N] input
|
||||
// CWHN: channel-fastest buffer [N][H][W][C] input and [KH][KW][IC][OC] kernel (OC fastest)
|
||||
const bool whcn = ggml_is_contiguous(kernel) && ggml_is_contiguous(input);
|
||||
const bool cwhn = input->nb[2] == ggml_type_size(input->type) &&
|
||||
kernel->nb[3] == ggml_type_size(kernel->type);
|
||||
GGML_ASSERT(whcn || cwhn);
|
||||
const bool cwhn_layout = !whcn && cwhn; // prefer whcn when both match (degenerate size-1 dims)
|
||||
|
||||
GGML_ASSERT(kernel->type == GGML_TYPE_F16 || kernel->type == GGML_TYPE_F32);
|
||||
GGML_ASSERT(input->type == GGML_TYPE_F32);
|
||||
GGML_ASSERT(dst->type == GGML_TYPE_F32);
|
||||
@@ -126,9 +142,6 @@ void ggml_sycl_op_conv2d(ggml_backend_sycl_context & ctx, ggml_tensor * dst) {
|
||||
const int DL_X = p[4];
|
||||
const int DL_Y = p[5];
|
||||
|
||||
// no cwhn layout support
|
||||
GGML_ASSERT(p[6] == 0);
|
||||
|
||||
const int IW = input->ne[0];
|
||||
const int IH = input->ne[1];
|
||||
const int OW = dst->ne[0];
|
||||
@@ -142,9 +155,17 @@ void ggml_sycl_op_conv2d(ggml_backend_sycl_context & ctx, ggml_tensor * dst) {
|
||||
const int64_t total = (int64_t) B * OC * OH * OW;
|
||||
const conv2d_params params = { IW, IH, OW, OH, KW, KH, ST_X, ST_Y, PD_X, PD_Y, DL_X, DL_Y, IC, OC, B, total };
|
||||
|
||||
if (kernel->type == GGML_TYPE_F16) {
|
||||
conv2d_sycl<sycl::half>(X_D, (const sycl::half *) K_D, Y_D, params, stream);
|
||||
if (cwhn_layout) {
|
||||
if (kernel->type == GGML_TYPE_F16) {
|
||||
conv2d_sycl<sycl::half, true>(X_D, (const sycl::half *) K_D, Y_D, params, stream);
|
||||
} else {
|
||||
conv2d_sycl<float, true>(X_D, K_D, Y_D, params, stream);
|
||||
}
|
||||
} else {
|
||||
conv2d_sycl<float>(X_D, K_D, Y_D, params, stream);
|
||||
if (kernel->type == GGML_TYPE_F16) {
|
||||
conv2d_sycl<sycl::half, false>(X_D, (const sycl::half *) K_D, Y_D, params, stream);
|
||||
} else {
|
||||
conv2d_sycl<float, false>(X_D, K_D, Y_D, params, stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+46
-124
@@ -657,58 +657,68 @@ static void convert_unary_sycl(const void * vx, dst_t * y, const int64_t k, dpct
|
||||
}
|
||||
|
||||
|
||||
to_fp16_sycl_t ggml_get_to_fp16_sycl(ggml_type type, ggml_tensor * dst) {
|
||||
static bool is_reordered_src0(const ggml_tensor * dst) {
|
||||
return dst->src[0]->extra &&
|
||||
((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder;
|
||||
}
|
||||
|
||||
template <typename dst_t>
|
||||
static to_t_sycl_t<dst_t> ggml_get_dequant_sycl(ggml_type type, ggml_tensor * dst) {
|
||||
switch (type) {
|
||||
case GGML_TYPE_Q1_0:
|
||||
return dequantize_block_sycl<QK1_0, QR1_0, dequantize_q1_0>;
|
||||
return dequantize_block_sycl<QK1_0, QR1_0, dequantize_q1_0, dst_t>;
|
||||
case GGML_TYPE_Q2_0:
|
||||
return dequantize_block_sycl<QK2_0, QR2_0, dequantize_q2_0>;
|
||||
return dequantize_block_sycl<QK2_0, QR2_0, dequantize_q2_0, dst_t>;
|
||||
case GGML_TYPE_Q4_0:
|
||||
if (dst->src[0]->extra &&
|
||||
((ggml_tensor_extra_gpu*)dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
if (is_reordered_src0(dst)) {
|
||||
return dequantize_row_q4_0_sycl_reorder;
|
||||
} else if constexpr (std::is_same_v<dst_t, float>) {
|
||||
return dequantize_row_q4_0_sycl;
|
||||
} else {
|
||||
return dequantize_block_sycl<QK4_0, QR4_0, dequantize_q4_0>;
|
||||
return dequantize_block_sycl<QK4_0, QR4_0, dequantize_q4_0, dst_t>;
|
||||
}
|
||||
case GGML_TYPE_Q4_1:
|
||||
return dequantize_block_sycl<QK4_1, QR4_1, dequantize_q4_1>;
|
||||
if constexpr (std::is_same_v<dst_t, float>) {
|
||||
return dequantize_row_q4_1_sycl;
|
||||
} else {
|
||||
return dequantize_block_sycl<QK4_1, QR4_1, dequantize_q4_1, dst_t>;
|
||||
}
|
||||
case GGML_TYPE_Q5_0:
|
||||
return dequantize_block_sycl<QK5_0, QR5_0, dequantize_q5_0>;
|
||||
return dequantize_block_sycl<QK5_0, QR5_0, dequantize_q5_0, dst_t>;
|
||||
case GGML_TYPE_Q5_1:
|
||||
return dequantize_block_sycl<QK5_1, QR5_1, dequantize_q5_1>;
|
||||
return dequantize_block_sycl<QK5_1, QR5_1, dequantize_q5_1, dst_t>;
|
||||
case GGML_TYPE_Q8_0:
|
||||
if (dst->src[0]->extra &&
|
||||
((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
if (is_reordered_src0(dst)) {
|
||||
return dequantize_row_q8_0_sycl_reorder;
|
||||
} else {
|
||||
return dequantize_block_sycl<QK8_0, QR8_0, dequantize_q8_0>;
|
||||
return dequantize_block_sycl<QK8_0, QR8_0, dequantize_q8_0, dst_t>;
|
||||
}
|
||||
case GGML_TYPE_Q2_K:
|
||||
if (dst->src[0]->extra && ((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
if (is_reordered_src0(dst)) {
|
||||
return dequantize_row_q2_K_sycl_reorder;
|
||||
} else {
|
||||
return dequantize_row_q2_K_sycl;
|
||||
}
|
||||
case GGML_TYPE_Q3_K:
|
||||
if (dst->src[0]->extra && ((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
if (is_reordered_src0(dst)) {
|
||||
return dequantize_row_q3_K_sycl_reorder;
|
||||
} else {
|
||||
return dequantize_row_q3_K_sycl;
|
||||
}
|
||||
case GGML_TYPE_Q4_K:
|
||||
if (dst->src[0]->extra && ((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
if (is_reordered_src0(dst)) {
|
||||
return dequantize_row_q4_K_sycl_reorder;
|
||||
} else {
|
||||
return dequantize_row_q4_K_sycl;
|
||||
}
|
||||
case GGML_TYPE_Q5_K:
|
||||
if (dst->src[0]->extra && ((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
if (is_reordered_src0(dst)) {
|
||||
return dequantize_row_q5_K_sycl_reorder;
|
||||
} else {
|
||||
return dequantize_row_q5_K_sycl;
|
||||
}
|
||||
case GGML_TYPE_Q6_K:
|
||||
if (dst->src[0]->extra && ((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
if (is_reordered_src0(dst)) {
|
||||
return dequantize_row_q6_K_sycl_reorder;
|
||||
} else {
|
||||
return dequantize_row_q6_K_sycl;
|
||||
@@ -724,15 +734,13 @@ to_fp16_sycl_t ggml_get_to_fp16_sycl(ggml_type type, ggml_tensor * dst) {
|
||||
case GGML_TYPE_IQ2_S:
|
||||
return dequantize_row_iq2_s_sycl;
|
||||
case GGML_TYPE_IQ3_XXS:
|
||||
if (dst->src[0]->extra &&
|
||||
((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
if (is_reordered_src0(dst)) {
|
||||
return dequantize_row_iq3_xxs_sycl_reorder;
|
||||
} else {
|
||||
return dequantize_row_iq3_xxs_sycl;
|
||||
}
|
||||
case GGML_TYPE_IQ3_S:
|
||||
if (dst->src[0]->extra &&
|
||||
((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
if (is_reordered_src0(dst)) {
|
||||
return dequantize_row_iq3_s_sycl_reorder;
|
||||
} else {
|
||||
return dequantize_row_iq3_s_sycl;
|
||||
@@ -742,124 +750,38 @@ to_fp16_sycl_t ggml_get_to_fp16_sycl(ggml_type type, ggml_tensor * dst) {
|
||||
case GGML_TYPE_IQ4_NL:
|
||||
return dequantize_row_iq4_nl_sycl;
|
||||
case GGML_TYPE_MXFP4:
|
||||
if (dst->src[0]->extra && ((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
if (is_reordered_src0(dst)) {
|
||||
return dequantize_row_mxfp4_sycl_reorder;
|
||||
}
|
||||
return dequantize_row_mxfp4_sycl;
|
||||
case GGML_TYPE_NVFP4:
|
||||
return dequantize_row_nvfp4_sycl;
|
||||
case GGML_TYPE_F32:
|
||||
return convert_unary_sycl<float>;
|
||||
if constexpr (std::is_same_v<dst_t, sycl::half>) {
|
||||
return convert_unary_sycl<float>;
|
||||
}
|
||||
break;
|
||||
case GGML_TYPE_F16:
|
||||
if constexpr (std::is_same_v<dst_t, float>) {
|
||||
return convert_unary_sycl<sycl::half>;
|
||||
}
|
||||
break;
|
||||
#ifdef GGML_SYCL_HAS_BF16
|
||||
case GGML_TYPE_BF16:
|
||||
return convert_unary_sycl<sycl::ext::oneapi::bfloat16>;
|
||||
#endif
|
||||
default:
|
||||
GGML_ABORT("fatal error: unsupport data type=%s\n", ggml_type_name(type));
|
||||
break;
|
||||
}
|
||||
GGML_ABORT("fatal error: unsupport data type=%s\n", ggml_type_name(type));
|
||||
}
|
||||
|
||||
to_fp16_sycl_t ggml_get_to_fp16_sycl(ggml_type type, ggml_tensor * dst) {
|
||||
return ggml_get_dequant_sycl<sycl::half>(type, dst);
|
||||
}
|
||||
|
||||
to_fp32_sycl_t ggml_get_to_fp32_sycl(ggml_type type, ggml_tensor *dst) {
|
||||
switch (type) {
|
||||
case GGML_TYPE_Q1_0:
|
||||
return dequantize_block_sycl<QK1_0, QR1_0, dequantize_q1_0>;
|
||||
case GGML_TYPE_Q2_0:
|
||||
return dequantize_block_sycl<QK2_0, QR2_0, dequantize_q2_0>;
|
||||
case GGML_TYPE_Q4_0:
|
||||
if (dst->src[0]->extra &&
|
||||
((ggml_tensor_extra_gpu*)dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
return dequantize_row_q4_0_sycl_reorder;
|
||||
} else {
|
||||
return dequantize_row_q4_0_sycl;
|
||||
}
|
||||
case GGML_TYPE_Q4_1:
|
||||
return dequantize_row_q4_1_sycl;
|
||||
case GGML_TYPE_Q5_0:
|
||||
return dequantize_block_sycl<QK5_0, QR5_0, dequantize_q5_0>;
|
||||
case GGML_TYPE_Q5_1:
|
||||
return dequantize_block_sycl<QK5_1, QR5_1, dequantize_q5_1>;
|
||||
case GGML_TYPE_Q8_0:
|
||||
if (dst->src[0]->extra &&
|
||||
((ggml_tensor_extra_gpu*)dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
return dequantize_row_q8_0_sycl_reorder;
|
||||
} else {
|
||||
return dequantize_block_sycl<QK8_0, QR8_0, dequantize_q8_0>;
|
||||
}
|
||||
case GGML_TYPE_Q2_K:
|
||||
if (dst->src[0]->extra && ((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
return dequantize_row_q2_K_sycl_reorder;
|
||||
} else {
|
||||
return dequantize_row_q2_K_sycl;
|
||||
}
|
||||
case GGML_TYPE_Q3_K:
|
||||
if (dst->src[0]->extra && ((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
return dequantize_row_q3_K_sycl_reorder;
|
||||
} else {
|
||||
return dequantize_row_q3_K_sycl;
|
||||
}
|
||||
case GGML_TYPE_Q4_K:
|
||||
if (dst->src[0]->extra &&
|
||||
((ggml_tensor_extra_gpu*)dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
return dequantize_row_q4_K_sycl_reorder;
|
||||
} else {
|
||||
return dequantize_row_q4_K_sycl;
|
||||
}
|
||||
case GGML_TYPE_Q5_K:
|
||||
if (dst->src[0]->extra && ((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
return dequantize_row_q5_K_sycl_reorder;
|
||||
} else {
|
||||
return dequantize_row_q5_K_sycl;
|
||||
}
|
||||
case GGML_TYPE_Q6_K:
|
||||
if (dst->src[0]->extra && ((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
return dequantize_row_q6_K_sycl_reorder;
|
||||
} else {
|
||||
return dequantize_row_q6_K_sycl;
|
||||
}
|
||||
case GGML_TYPE_IQ1_S:
|
||||
return dequantize_row_iq1_s_sycl;
|
||||
case GGML_TYPE_IQ1_M:
|
||||
return dequantize_row_iq1_m_sycl;
|
||||
case GGML_TYPE_IQ2_XXS:
|
||||
return dequantize_row_iq2_xxs_sycl;
|
||||
case GGML_TYPE_IQ2_XS:
|
||||
return dequantize_row_iq2_xs_sycl;
|
||||
case GGML_TYPE_IQ2_S:
|
||||
return dequantize_row_iq2_s_sycl;
|
||||
case GGML_TYPE_IQ3_XXS:
|
||||
if (dst->src[0]->extra &&
|
||||
((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
return dequantize_row_iq3_xxs_sycl_reorder;
|
||||
} else {
|
||||
return dequantize_row_iq3_xxs_sycl;
|
||||
}
|
||||
case GGML_TYPE_IQ3_S:
|
||||
if (dst->src[0]->extra &&
|
||||
((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
return dequantize_row_iq3_s_sycl_reorder;
|
||||
} else {
|
||||
return dequantize_row_iq3_s_sycl;
|
||||
}
|
||||
case GGML_TYPE_IQ4_XS:
|
||||
return dequantize_row_iq4_xs_sycl;
|
||||
case GGML_TYPE_IQ4_NL:
|
||||
return dequantize_row_iq4_nl_sycl;
|
||||
case GGML_TYPE_MXFP4:
|
||||
if (dst->src[0]->extra && ((ggml_tensor_extra_gpu *) dst->src[0]->extra)->optimized_feature.reorder) {
|
||||
return dequantize_row_mxfp4_sycl_reorder;
|
||||
}
|
||||
return dequantize_row_mxfp4_sycl;
|
||||
case GGML_TYPE_NVFP4:
|
||||
return dequantize_row_nvfp4_sycl;
|
||||
case GGML_TYPE_F16:
|
||||
return convert_unary_sycl<sycl::half>;
|
||||
#ifdef GGML_SYCL_HAS_BF16
|
||||
case GGML_TYPE_BF16:
|
||||
return convert_unary_sycl<sycl::ext::oneapi::bfloat16>;
|
||||
#endif
|
||||
default:
|
||||
GGML_ABORT("fatal error: unsupport data type=%s\n", ggml_type_name(type));
|
||||
}
|
||||
return ggml_get_dequant_sycl<float>(type, dst);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6912,6 +6912,11 @@ static bool do_ggml_backend_sycl_device_supports_op(ggml_backend_dev_t dev, cons
|
||||
return false;
|
||||
}
|
||||
case GGML_OP_CONV_2D:
|
||||
// WHCN: contiguous [KW,KH,IC,OC] kernel and [W,H,C,N] input
|
||||
// CWHN: channel-fastest input (C fastest) and kernel with OC fastest
|
||||
return (ggml_is_contiguous(op->src[0]) && ggml_is_contiguous(op->src[1])) ||
|
||||
(op->src[1]->nb[2] == ggml_type_size(op->src[1]->type) &&
|
||||
op->src[0]->nb[3] == ggml_type_size(op->src[0]->type));
|
||||
case GGML_OP_CONV_2D_DW:
|
||||
case GGML_OP_CONV_TRANSPOSE_2D:
|
||||
return true;
|
||||
|
||||
@@ -1655,6 +1655,7 @@ class ggml_webgpu_shader_lib {
|
||||
|
||||
switch (key.src_type) {
|
||||
case GGML_TYPE_Q1_0:
|
||||
case GGML_TYPE_Q2_0:
|
||||
case GGML_TYPE_Q4_0:
|
||||
case GGML_TYPE_Q5_0:
|
||||
case GGML_TYPE_Q8_0:
|
||||
@@ -1699,7 +1700,7 @@ class ggml_webgpu_shader_lib {
|
||||
} else if ((key.src_type >= GGML_TYPE_Q4_0 && key.src_type <= GGML_TYPE_Q8_1) ||
|
||||
key.src_type == GGML_TYPE_IQ4_NL || key.src_type == GGML_TYPE_MXFP4) {
|
||||
defines.push_back("BLOCK_SIZE=32u");
|
||||
} else if (key.src_type == GGML_TYPE_NVFP4) {
|
||||
} else if (key.src_type == GGML_TYPE_Q2_0 || key.src_type == GGML_TYPE_NVFP4) {
|
||||
defines.push_back("BLOCK_SIZE=64u");
|
||||
} else if (key.src_type >= GGML_TYPE_Q2_K) {
|
||||
defines.push_back("BLOCK_SIZE=256u");
|
||||
|
||||
@@ -4395,6 +4395,7 @@ static bool ggml_backend_webgpu_device_supports_buft(ggml_backend_dev_t dev, ggm
|
||||
static bool ggml_webgpu_supported_qtype(ggml_type type) {
|
||||
switch (type) {
|
||||
case GGML_TYPE_Q1_0:
|
||||
case GGML_TYPE_Q2_0:
|
||||
case GGML_TYPE_Q4_0:
|
||||
case GGML_TYPE_Q4_1:
|
||||
case GGML_TYPE_Q5_0:
|
||||
@@ -4500,6 +4501,7 @@ static bool ggml_backend_webgpu_device_supports_op(ggml_backend_dev_t dev, const
|
||||
case GGML_TYPE_F16:
|
||||
case GGML_TYPE_BF16:
|
||||
case GGML_TYPE_Q1_0:
|
||||
case GGML_TYPE_Q2_0:
|
||||
case GGML_TYPE_Q4_0:
|
||||
case GGML_TYPE_Q4_1:
|
||||
case GGML_TYPE_Q5_0:
|
||||
|
||||
@@ -51,6 +51,20 @@ fn copy_elements(src_base: u32, dst_base: u32, offset: u32) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef Q2_0
|
||||
fn copy_elements(src_base: u32, dst_base: u32, offset: u32) {
|
||||
let block_byte_base = (src_base + offset) * 18u;
|
||||
let d = load_f16_as_f32_at_src(block_byte_base);
|
||||
for (var j = 0u; j < 4u; j++) {
|
||||
let q_packed = load_u32_at_src(block_byte_base + 2u + j * 4u);
|
||||
for (var k = 0u; k < 16u; k++) {
|
||||
let q = (q_packed >> (2u * k)) & 3u;
|
||||
dst[dst_base + offset * 64u + j * 16u + k] = (f32(q) - 1.0) * d;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef Q4_0
|
||||
fn copy_elements(src_base: u32, dst_base: u32, offset: u32) {
|
||||
let block_byte_base = (src_base + offset) * 18; // Block stride: 18 bytes
|
||||
|
||||
@@ -111,6 +111,29 @@ fn init_shmem_src0(thread_id: u32, batch_offset: u32, offset_m: u32, k_outer: u3
|
||||
}
|
||||
#endif // INIT_SRC0_SHMEM_Q1_0
|
||||
|
||||
#ifdef INIT_SRC0_SHMEM_Q2_0
|
||||
const BLOCK_SIZE = 64u;
|
||||
const BLOCK_SIZE_BYTES = 18u;
|
||||
const NQ = 16u;
|
||||
|
||||
fn init_shmem_src0(thread_id: u32, batch_offset: u32, offset_m: u32, k_outer: u32) {
|
||||
for (var i = thread_id * NQ; i < TILE_SRC0_SHMEM; i += TOTAL_WORKGROUP_SIZE * NQ) {
|
||||
let global_m = offset_m + i / TILE_K;
|
||||
let global_k = k_outer + i % TILE_K;
|
||||
if (global_m < params.m && global_k < params.k) {
|
||||
let block_idx = batch_offset + global_m * params.stride_01 + global_k / BLOCK_SIZE;
|
||||
let block_byte_base = block_idx * BLOCK_SIZE_BYTES;
|
||||
let d = load_f16_at_src0(block_byte_base);
|
||||
let q_packed = load_u32_at_src0(block_byte_base + 2u + (global_k % BLOCK_SIZE) / 4u);
|
||||
for (var k = 0u; k < NQ; k++) {
|
||||
let q = (q_packed >> (2u * k)) & 3u;
|
||||
shmem[i + k] = (f16(q) - 1.0) * d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // INIT_SRC0_SHMEM_Q2_0
|
||||
|
||||
// legacy-quants
|
||||
#if defined(INIT_SRC0_SHMEM_Q4_0) || defined(INIT_SRC0_SHMEM_Q4_1) || defined(INIT_SRC0_SHMEM_Q5_0) || defined(INIT_SRC0_SHMEM_Q5_1) || defined(INIT_SRC0_SHMEM_Q8_0) || defined(INIT_SRC0_SHMEM_Q8_1) || defined(INIT_SRC0_SHMEM_MXFP4)
|
||||
const BLOCK_SIZE = 32u;
|
||||
|
||||
@@ -119,6 +119,46 @@ fn accumulate_vec_dot(thread_id: u32, row_base: u32, src0_batch_offset: u32, src
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef MUL_ACC_Q2_0
|
||||
#define BLOCK_SIZE 64
|
||||
#define BLOCK_SIZE_BYTES 18
|
||||
#define THREADS_PER_BLOCK 4
|
||||
#define ELEMS_PER_THREAD (BLOCK_SIZE/THREADS_PER_BLOCK)
|
||||
fn accumulate_vec_dot(thread_id: u32, row_base: u32, src0_batch_offset: u32, src1_idx_base: u32) -> array<array<f32, OUTPUTS_PER_WG>, NUM_COLS> {
|
||||
var acc: array<array<f32, OUTPUTS_PER_WG>, NUM_COLS>;
|
||||
|
||||
let num_blocks = params.k / BLOCK_SIZE;
|
||||
let thread_within_block = thread_id % THREADS_PER_BLOCK;
|
||||
for (var block = thread_id / THREADS_PER_BLOCK; block < num_blocks; block += WG_SIZE / THREADS_PER_BLOCK) {
|
||||
let x_base = src1_idx_base + block * BLOCK_SIZE + thread_within_block * ELEMS_PER_THREAD;
|
||||
var x_block: array<array<f32, ELEMS_PER_THREAD>, NUM_COLS>;
|
||||
for (var col = 0u; col < NUM_COLS; col++) {
|
||||
for (var i = 0u; i < ELEMS_PER_THREAD; i++) {
|
||||
x_block[col][i] = f32(SRC1[x_base + col * params.stride_11 + i]);
|
||||
}
|
||||
}
|
||||
for (var row = 0u; row < OUTPUTS_PER_WG; row++) {
|
||||
let output_row = row_base + row;
|
||||
if (output_row < params.m) {
|
||||
let block_byte_base = (src0_batch_offset + output_row * params.stride_01 + block) * BLOCK_SIZE_BYTES;
|
||||
let d = f32(load_f16_at_src0(block_byte_base));
|
||||
let q_packed = load_u32_at_src0(block_byte_base + 2u + 4u * thread_within_block);
|
||||
for (var col = 0u; col < NUM_COLS; col++) {
|
||||
var row_sum = 0.0;
|
||||
for (var i = 0u; i < ELEMS_PER_THREAD; i++) {
|
||||
let q = (q_packed >> (2u * i)) & 3u;
|
||||
row_sum += (f32(q) - 1.0) * x_block[col][i];
|
||||
}
|
||||
acc[col][row] += d * row_sum;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef MUL_ACC_Q4_0
|
||||
#define BLOCK_SIZE 32
|
||||
#define BLOCK_SIZE_BYTES 18
|
||||
|
||||
@@ -263,6 +263,7 @@ class Keys:
|
||||
DIMENSION_SECTIONS = "{arch}.rope.dimension_sections"
|
||||
SECTION_ORDER = "{arch}.rope.section_order"
|
||||
FREQ_BASE = "{arch}.rope.freq_base"
|
||||
FREQ_BASE_SPATIAL = "{arch}.rope.freq_base_spatial"
|
||||
FREQ_BASE_SWA = "{arch}.rope.freq_base_swa"
|
||||
SCALING_TYPE = "{arch}.rope.scaling.type"
|
||||
SCALING_FACTOR = "{arch}.rope.scaling.factor"
|
||||
@@ -370,6 +371,7 @@ class Keys:
|
||||
DNA_AUTO_TAGS = "tokenizer.ggml.dna_auto_tags"
|
||||
ADD_PREFIX = "tokenizer.ggml.add_space_prefix"
|
||||
REMOVE_EXTRA_WS = "tokenizer.ggml.remove_extra_whitespaces"
|
||||
CLEAN_SPACES = "tokenizer.ggml.clean_spaces"
|
||||
PRECOMPILED_CHARSMAP = "tokenizer.ggml.precompiled_charsmap"
|
||||
SUPPRESS_TOKENS = "tokenizer.ggml.suppress_tokens"
|
||||
HF_JSON = "tokenizer.huggingface.json"
|
||||
@@ -573,8 +575,10 @@ class MODEL_ARCH(IntEnum):
|
||||
QWEN2 = auto()
|
||||
QWEN2MOE = auto()
|
||||
QWEN2VL = auto()
|
||||
SENSENOVA_U1 = auto()
|
||||
QWEN3 = auto()
|
||||
QWEN3MOE = auto()
|
||||
FLINT = auto()
|
||||
QWEN3NEXT = auto()
|
||||
HRM_TEXT = auto()
|
||||
BEAM = auto()
|
||||
@@ -1390,7 +1394,9 @@ MODEL_ARCH_NAMES: dict[MODEL_ARCH, str] = {
|
||||
MODEL_ARCH.QWEN2: "qwen2",
|
||||
MODEL_ARCH.QWEN2MOE: "qwen2moe",
|
||||
MODEL_ARCH.QWEN2VL: "qwen2vl",
|
||||
MODEL_ARCH.SENSENOVA_U1: "sensenova_u1",
|
||||
MODEL_ARCH.QWEN3: "qwen3",
|
||||
MODEL_ARCH.FLINT: "flint",
|
||||
MODEL_ARCH.QWEN3MOE: "qwen3moe",
|
||||
MODEL_ARCH.QWEN3NEXT: "qwen3next",
|
||||
MODEL_ARCH.HRM_TEXT: "hrm_text",
|
||||
@@ -2916,6 +2922,24 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
|
||||
MODEL_TENSOR.FFN_DOWN_SHEXP,
|
||||
MODEL_TENSOR.FFN_UP_SHEXP,
|
||||
],
|
||||
MODEL_ARCH.SENSENOVA_U1: [
|
||||
MODEL_TENSOR.TOKEN_EMBD,
|
||||
MODEL_TENSOR.OUTPUT_NORM,
|
||||
MODEL_TENSOR.OUTPUT,
|
||||
MODEL_TENSOR.ROPE_FREQS,
|
||||
MODEL_TENSOR.ATTN_NORM,
|
||||
MODEL_TENSOR.ATTN_QKV,
|
||||
MODEL_TENSOR.ATTN_Q,
|
||||
MODEL_TENSOR.ATTN_Q_NORM,
|
||||
MODEL_TENSOR.ATTN_K,
|
||||
MODEL_TENSOR.ATTN_K_NORM,
|
||||
MODEL_TENSOR.ATTN_V,
|
||||
MODEL_TENSOR.ATTN_OUT,
|
||||
MODEL_TENSOR.FFN_NORM,
|
||||
MODEL_TENSOR.FFN_GATE,
|
||||
MODEL_TENSOR.FFN_DOWN,
|
||||
MODEL_TENSOR.FFN_UP,
|
||||
],
|
||||
MODEL_ARCH.QWEN3: [
|
||||
MODEL_TENSOR.TOKEN_EMBD,
|
||||
MODEL_TENSOR.OUTPUT_NORM,
|
||||
@@ -2953,6 +2977,33 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
|
||||
MODEL_TENSOR.FFN_DOWN_EXP,
|
||||
MODEL_TENSOR.FFN_UP_EXP,
|
||||
],
|
||||
|
||||
MODEL_ARCH.FLINT: [
|
||||
MODEL_TENSOR.TOKEN_EMBD,
|
||||
MODEL_TENSOR.OUTPUT_NORM,
|
||||
MODEL_TENSOR.OUTPUT,
|
||||
MODEL_TENSOR.ATTN_NORM,
|
||||
MODEL_TENSOR.ATTN_QKV,
|
||||
MODEL_TENSOR.ATTN_Q,
|
||||
MODEL_TENSOR.ATTN_Q_NORM,
|
||||
MODEL_TENSOR.ATTN_K,
|
||||
MODEL_TENSOR.ATTN_K_NORM,
|
||||
MODEL_TENSOR.ATTN_V,
|
||||
MODEL_TENSOR.ATTN_OUT,
|
||||
MODEL_TENSOR.FFN_NORM,
|
||||
MODEL_TENSOR.FFN_GATE_INP,
|
||||
MODEL_TENSOR.FFN_GATE_EXP,
|
||||
MODEL_TENSOR.FFN_DOWN_EXP,
|
||||
MODEL_TENSOR.FFN_UP_EXP,
|
||||
MODEL_TENSOR.ATTN_SINKS,
|
||||
MODEL_TENSOR.FFN_GATE,
|
||||
MODEL_TENSOR.FFN_UP,
|
||||
MODEL_TENSOR.FFN_DOWN,
|
||||
MODEL_TENSOR.FFN_GATE_SHEXP,
|
||||
MODEL_TENSOR.FFN_UP_SHEXP,
|
||||
MODEL_TENSOR.FFN_DOWN_SHEXP,
|
||||
MODEL_TENSOR.FFN_EXP_PROBS_B,
|
||||
],
|
||||
MODEL_ARCH.QWEN3NEXT: [
|
||||
MODEL_TENSOR.TOKEN_EMBD,
|
||||
MODEL_TENSOR.OUTPUT_NORM,
|
||||
@@ -6485,6 +6536,7 @@ KEY_ATTENTION_LAYERNORM_RMS_EPS = Keys.Attention.LAYERNORM_RMS_EPS
|
||||
# RoPE
|
||||
KEY_ROPE_DIMENSION_COUNT = Keys.Rope.DIMENSION_COUNT
|
||||
KEY_ROPE_FREQ_BASE = Keys.Rope.FREQ_BASE
|
||||
KEY_ROPE_FREQ_BASE_SPATIAL = Keys.Rope.FREQ_BASE_SPATIAL
|
||||
KEY_ROPE_SCALING_TYPE = Keys.Rope.SCALING_TYPE
|
||||
KEY_ROPE_SCALING_FACTOR = Keys.Rope.SCALING_FACTOR
|
||||
KEY_ROPE_SCALING_ORIG_CTX_LEN = Keys.Rope.SCALING_ORIG_CTX_LEN
|
||||
|
||||
@@ -1167,6 +1167,9 @@ class GGUFWriter:
|
||||
def add_rope_freq_base(self, value: float) -> None:
|
||||
self.add_float32(Keys.Rope.FREQ_BASE.format(arch=self.arch), value)
|
||||
|
||||
def add_rope_freq_base_spatial(self, value: float) -> None:
|
||||
self.add_float32(Keys.Rope.FREQ_BASE_SPATIAL.format(arch=self.arch), value)
|
||||
|
||||
def add_rope_scaling_type(self, value: RopeScalingType) -> None:
|
||||
self.add_string(Keys.Rope.SCALING_TYPE.format(arch=self.arch), value.value)
|
||||
|
||||
@@ -1323,6 +1326,9 @@ class GGUFWriter:
|
||||
def add_remove_extra_whitespaces(self, value: bool) -> None:
|
||||
self.add_bool(Keys.Tokenizer.REMOVE_EXTRA_WS, value)
|
||||
|
||||
def add_clean_spaces(self, value: bool) -> None:
|
||||
self.add_bool(Keys.Tokenizer.CLEAN_SPACES, value)
|
||||
|
||||
def add_precompiled_charsmap(self, charsmap: bytes) -> None:
|
||||
self.add_array(Keys.Tokenizer.PRECOMPILED_CHARSMAP, charsmap)
|
||||
|
||||
|
||||
@@ -429,6 +429,7 @@ class TensorNameMap:
|
||||
),
|
||||
|
||||
MODEL_TENSOR.ATTN_SINKS: (
|
||||
"model.layers.{bid}.self_attn.softmax_offset", # flint
|
||||
"model.layers.{bid}.self_attn.sinks", # openai-moe
|
||||
"model.layers.{bid}.self_attn.attention_sink_bias", # mimov2
|
||||
"model.layers.{bid}.self_attn.learnable_sink_param", # hy-v4
|
||||
|
||||
@@ -55,6 +55,7 @@ class SpecialVocab:
|
||||
normalizer_nfc: bool | None
|
||||
normalizer_lowercase: bool | None
|
||||
normalizer_strip_accents: bool | None
|
||||
clean_spaces: bool | None
|
||||
|
||||
def __init__(
|
||||
self, path: str | os.PathLike[str], load_merges: bool = False,
|
||||
@@ -70,6 +71,7 @@ class SpecialVocab:
|
||||
self.normalizer_nfc = None
|
||||
self.normalizer_lowercase = None
|
||||
self.normalizer_strip_accents = None
|
||||
self.clean_spaces = None
|
||||
if special_token_types is not None:
|
||||
self.special_token_types = special_token_types
|
||||
else:
|
||||
@@ -120,6 +122,10 @@ class SpecialVocab:
|
||||
if not quiet:
|
||||
logger.info(f'Setting normalizer_strip_accents to {self.normalizer_strip_accents}')
|
||||
gw.add_normalizer_strip_accents(self.normalizer_strip_accents)
|
||||
if self.clean_spaces is not None:
|
||||
if not quiet:
|
||||
logger.info(f'Setting clean_spaces to {self.clean_spaces}')
|
||||
gw.add_clean_spaces(self.clean_spaces)
|
||||
|
||||
def _load(self, path: Path) -> None:
|
||||
self._try_load_from_tokenizer_json(path)
|
||||
@@ -345,6 +351,10 @@ class SpecialVocab:
|
||||
self.chat_template = chat_template
|
||||
else:
|
||||
logger.warning(f'Bad type for chat_template field in {tokenizer_config_file!r} - ignoring')
|
||||
# decode clean-up of spaces before punctuation (transformers clean_up_tokenization_spaces)
|
||||
clean_spaces = tokenizer_config.get('clean_up_tokenization_spaces')
|
||||
if isinstance(clean_spaces, bool):
|
||||
self.clean_spaces = clean_spaces
|
||||
for typ in self.special_token_types:
|
||||
add_entry = tokenizer_config.get(f'add_{typ}_token')
|
||||
if isinstance(add_entry, bool):
|
||||
|
||||
@@ -334,9 +334,10 @@ static void llama_adapter_lora_init_impl(llama_model & model, FILE * file, llama
|
||||
if (!model_tensor) {
|
||||
throw std::runtime_error("LoRA tensor '" + name + "' does not exist in base model (hint: maybe wrong base model?)");
|
||||
}
|
||||
// the LoRA delta of a Hadamard-folded weight reads the transformed input, so the result is wrong
|
||||
if (model.hdmd.weight_blocks.count(name) || model.hdmd.inverse_blocks.count(name)) {
|
||||
throw std::runtime_error("LoRA tensor '" + name + "' targets a prism.hadamard folded weight, which is not supported");
|
||||
// build_lora_mm feeds a folded weight's LoRA delta the primal input, so folded matmul weights are fine;
|
||||
// a latent lookup table (token_embd, also the head when the output is tied) stores rotated rows, refuse it
|
||||
if (model.hdmd.inverse_blocks.count(name)) {
|
||||
throw std::runtime_error("LoRA tensor '" + name + "' targets a prism.hadamard latent lookup table, which is not supported");
|
||||
}
|
||||
|
||||
auto * buft = ggml_backend_buffer_get_type(model_tensor->buffer);
|
||||
|
||||
@@ -33,7 +33,9 @@ static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = {
|
||||
{ LLM_ARCH_QWEN2, "qwen2" },
|
||||
{ LLM_ARCH_QWEN2MOE, "qwen2moe" },
|
||||
{ LLM_ARCH_QWEN2VL, "qwen2vl" },
|
||||
{ LLM_ARCH_SENSENOVA_U1, "sensenova_u1" },
|
||||
{ LLM_ARCH_QWEN3, "qwen3" },
|
||||
{ LLM_ARCH_FLINT, "flint" },
|
||||
{ LLM_ARCH_QWEN3MOE, "qwen3moe" },
|
||||
{ LLM_ARCH_QWEN3NEXT, "qwen3next" },
|
||||
{ LLM_ARCH_QWEN3VL, "qwen3vl" },
|
||||
@@ -339,6 +341,7 @@ static const std::map<llm_kv, const char *> LLM_KV_NAMES = {
|
||||
{ LLM_KV_ROPE_DIMENSION_SECTIONS, "%s.rope.dimension_sections" },
|
||||
{ LLM_KV_ROPE_SECTION_ORDER, "%s.rope.section_order" },
|
||||
{ LLM_KV_ROPE_FREQ_BASE, "%s.rope.freq_base" },
|
||||
{ LLM_KV_ROPE_FREQ_BASE_SPATIAL, "%s.rope.freq_base_spatial" },
|
||||
{ LLM_KV_ROPE_FREQ_BASE_SWA, "%s.rope.freq_base_swa" },
|
||||
{ LLM_KV_ROPE_SCALE_LINEAR, "%s.rope.scale_linear" },
|
||||
{ LLM_KV_ROPE_SCALING_TYPE, "%s.rope.scaling.type" },
|
||||
@@ -434,6 +437,7 @@ static const std::map<llm_kv, const char *> LLM_KV_NAMES = {
|
||||
{ LLM_KV_TOKENIZER_DNA_AUTO_TAGS, "tokenizer.ggml.dna_auto_tags" },
|
||||
{ LLM_KV_TOKENIZER_ADD_PREFIX, "tokenizer.ggml.add_space_prefix" },
|
||||
{ LLM_KV_TOKENIZER_REMOVE_EXTRA_WS, "tokenizer.ggml.remove_extra_whitespaces" },
|
||||
{ LLM_KV_TOKENIZER_CLEAN_SPACES, "tokenizer.ggml.clean_spaces" },
|
||||
{ LLM_KV_TOKENIZER_PRECOMPILED_CHARSMAP, "tokenizer.ggml.precompiled_charsmap" },
|
||||
{ LLM_KV_TOKENIZER_HF_JSON, "tokenizer.huggingface.json" },
|
||||
{ LLM_KV_TOKENIZER_RWKV, "tokenizer.rwkv.world" },
|
||||
|
||||
@@ -38,8 +38,10 @@ enum llm_arch {
|
||||
LLM_ARCH_QWEN2,
|
||||
LLM_ARCH_QWEN2MOE,
|
||||
LLM_ARCH_QWEN2VL,
|
||||
LLM_ARCH_SENSENOVA_U1,
|
||||
LLM_ARCH_QWEN3,
|
||||
LLM_ARCH_QWEN3MOE,
|
||||
LLM_ARCH_FLINT,
|
||||
LLM_ARCH_QWEN3NEXT,
|
||||
LLM_ARCH_QWEN3VL,
|
||||
LLM_ARCH_QWEN3VLMOE,
|
||||
@@ -344,6 +346,7 @@ enum llm_kv {
|
||||
LLM_KV_ROPE_DIMENSION_SECTIONS,
|
||||
LLM_KV_ROPE_SECTION_ORDER,
|
||||
LLM_KV_ROPE_FREQ_BASE,
|
||||
LLM_KV_ROPE_FREQ_BASE_SPATIAL,
|
||||
LLM_KV_ROPE_FREQ_BASE_SWA,
|
||||
LLM_KV_ROPE_SCALE_LINEAR,
|
||||
LLM_KV_ROPE_SCALING_TYPE,
|
||||
@@ -397,6 +400,7 @@ enum llm_kv {
|
||||
LLM_KV_TOKENIZER_DNA_AUTO_TAGS,
|
||||
LLM_KV_TOKENIZER_ADD_PREFIX,
|
||||
LLM_KV_TOKENIZER_REMOVE_EXTRA_WS,
|
||||
LLM_KV_TOKENIZER_CLEAN_SPACES,
|
||||
LLM_KV_TOKENIZER_PRECOMPILED_CHARSMAP,
|
||||
LLM_KV_TOKENIZER_HF_JSON,
|
||||
LLM_KV_TOKENIZER_RWKV,
|
||||
|
||||
+12
-7
@@ -1630,8 +1630,9 @@ ggml_tensor * llm_graph_context::build_lora_mm(
|
||||
ggml_tensor * w,
|
||||
ggml_tensor * cur,
|
||||
ggml_tensor * w_s) const {
|
||||
cur = build_hadamard_input(w, cur);
|
||||
ggml_tensor * res = ggml_mul_mat(ctx0, w, cur);
|
||||
// a Hadamard-folded weight reads the transformed input; a LoRA delta lives in the primal basis and reads `cur`
|
||||
ggml_tensor * cur_mm = build_hadamard_input(w, cur);
|
||||
ggml_tensor * res = ggml_mul_mat(ctx0, w, cur_mm);
|
||||
|
||||
if (prec_policy) {
|
||||
prec_policy->apply(res);
|
||||
@@ -1684,7 +1685,10 @@ llm_graph_amax_src1 llm_graph_context::build_amax_src1_f16(
|
||||
constexpr float k_ln_2 = 0.6931471805599453f;
|
||||
|
||||
ggml_tensor * e;
|
||||
e = ggml_scale(ctx0, amax, 1.0f/32768.0f);
|
||||
// note: this can technically be:
|
||||
// e = ggml_scale(ctx0, amax, 1.0f/32768.0f);
|
||||
// but we leave x32 headroom for the F16 sums of 32 values in Q8 activation blocks
|
||||
e = ggml_scale(ctx0, amax, 1.0f/1024.0f);
|
||||
e = ggml_log (ctx0, e);
|
||||
e = ggml_scale(ctx0, e, k_log2_e);
|
||||
e = ggml_ceil (ctx0, e);
|
||||
@@ -1706,11 +1710,12 @@ ggml_tensor * llm_graph_context::build_lora_mm_id(
|
||||
ggml_tensor * ids,
|
||||
ggml_tensor * w_s,
|
||||
ggml_tensor * slots) const {
|
||||
cur = build_hadamard_input(w, cur);
|
||||
// as in build_lora_mm: the folded weight reads the transformed input, the LoRA delta reads `cur`
|
||||
ggml_tensor * cur_mm = build_hadamard_input(w, cur);
|
||||
// the experts in the MoE cache are selected by their slots
|
||||
ggml_tensor * res = slots == nullptr ?
|
||||
ggml_mul_mat_id(ctx0, w, cur, ids) :
|
||||
ggml_mul_mat_id(ctx0, moe_cache->get_experts(w), cur, slots);
|
||||
ggml_mul_mat_id(ctx0, w, cur_mm, ids) :
|
||||
ggml_mul_mat_id(ctx0, moe_cache->get_experts(w), cur_mm, slots);
|
||||
|
||||
if (prec_policy) {
|
||||
prec_policy->apply(res);
|
||||
@@ -2505,7 +2510,7 @@ ggml_tensor * llm_graph_context::build_moe_ffn(
|
||||
// ref: https://github.com/ggml-org/llama.cpp/pull/30656#issuecomment-6269597922
|
||||
llm_graph_amax_src1 amax_src1 = { nullptr, nullptr };
|
||||
|
||||
if (arch == LLM_ARCH_MISTRAL4 || arch == LLM_ARCH_MELLUM) {
|
||||
if (arch == LLM_ARCH_MISTRAL4 || arch == LLM_ARCH_MELLUM || arch == LLM_ARCH_FLINT) {
|
||||
amax_src1 = build_amax_src1_f16(cur);
|
||||
cur = amax_src1.res;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1122,7 +1122,7 @@ struct llm_graph_context {
|
||||
ggml_tensor * slots = nullptr) const;
|
||||
|
||||
// rescale `cur` (the src1 of a matmul) by a power of two, computed from its maximum absolute
|
||||
// when the values already fit in F16, the factors are exactly 1.0f
|
||||
// keep both individual values and Q8 activation block sums within F16 range
|
||||
llm_graph_amax_src1 build_amax_src1_f16(
|
||||
ggml_tensor * cur) const;
|
||||
|
||||
|
||||
@@ -882,7 +882,10 @@ bool llama_kv_cache::update(llama_context * lctx, bool do_shift, const stream_co
|
||||
LLAMA_LOG_DEBUG("%s: applying K-shift\n", __func__);
|
||||
|
||||
// apply K-shift if needed
|
||||
if (hparams.rope_type != LLAMA_ROPE_TYPE_NONE) {
|
||||
if (hparams.rope_type != LLAMA_ROPE_TYPE_NONE &&
|
||||
std::any_of(layers.begin(), layers.end(), [&](const auto & layer) {
|
||||
return hparams.has_rope(layer.il);
|
||||
})) {
|
||||
ggml_backend_sched_reset(sched);
|
||||
|
||||
auto * res = lctx->get_gf_res_reserve();
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
bool llama_model_saver_supports_arch(llm_arch arch) {
|
||||
switch (arch) {
|
||||
case LLM_ARCH_SENSENOVA_U1:
|
||||
case LLM_ARCH_GEMMA3N:
|
||||
case LLM_ARCH_BITNET:
|
||||
case LLM_ARCH_T5:
|
||||
@@ -423,6 +424,7 @@ void llama_model_saver::add_kv_from_model() {
|
||||
add_kv(LLM_KV_TOKENIZER_ADD_SEP, vocab.get_add_sep());
|
||||
add_kv(LLM_KV_TOKENIZER_ADD_PREFIX, vocab.get_add_space_prefix());
|
||||
add_kv(LLM_KV_TOKENIZER_REMOVE_EXTRA_WS, vocab.get_remove_extra_whitespaces());
|
||||
add_kv(LLM_KV_TOKENIZER_CLEAN_SPACES, vocab.get_clean_spaces());
|
||||
add_kv(LLM_KV_TOKENIZER_PRECOMPILED_CHARSMAP, vocab.get_precompiled_charsmap());
|
||||
// add_kv(LLM_KV_TOKENIZER_HF_JSON, ???);
|
||||
// add_kv(LLM_KV_TOKENIZER_RWKV, ???);
|
||||
|
||||
@@ -110,8 +110,12 @@ static llama_model * llama_model_mapping(llm_arch arch, const llama_model_params
|
||||
return new llama_model_qwen2vl(params);
|
||||
case LLM_ARCH_QWEN2MOE:
|
||||
return new llama_model_qwen2moe(params);
|
||||
case LLM_ARCH_SENSENOVA_U1:
|
||||
return new llama_model_sensenova_u1(params);
|
||||
case LLM_ARCH_QWEN3:
|
||||
return new llama_model_qwen3(params);
|
||||
case LLM_ARCH_FLINT:
|
||||
return new llama_model_flint(params);
|
||||
case LLM_ARCH_QWEN3MOE:
|
||||
return new llama_model_qwen3moe(params);
|
||||
case LLM_ARCH_QWEN3VL:
|
||||
@@ -3569,6 +3573,7 @@ llama_rope_type llama_model_rope_type(const llama_model * model) {
|
||||
case LLM_ARCH_QWEN2MOE:
|
||||
case LLM_ARCH_QWEN3:
|
||||
case LLM_ARCH_VLLM_DECISION2:
|
||||
case LLM_ARCH_FLINT:
|
||||
case LLM_ARCH_QWEN3MOE:
|
||||
case LLM_ARCH_LLADA_MOE:
|
||||
case LLM_ARCH_RND1:
|
||||
@@ -3640,6 +3645,7 @@ llama_rope_type llama_model_rope_type(const llama_model * model) {
|
||||
// DSV4 DSpark drafters use DeepSeek-V4's normal RoPE; legacy DFlash backbones are NeoX
|
||||
return model->hparams.dsv4_hc_mult > 0 ? LLAMA_ROPE_TYPE_NORM : LLAMA_ROPE_TYPE_NEOX;
|
||||
|
||||
case LLM_ARCH_SENSENOVA_U1:
|
||||
case LLM_ARCH_QWEN2VL:
|
||||
case LLM_ARCH_PADDLEOCR:
|
||||
return LLAMA_ROPE_TYPE_MROPE;
|
||||
|
||||
+3
-2
@@ -779,8 +779,9 @@ struct llama_model {
|
||||
struct ggml_tensor * per_layer_proj_norm = nullptr;
|
||||
|
||||
// eagle3 / dflash feature fusion layer
|
||||
struct ggml_tensor * fc = nullptr;
|
||||
struct ggml_tensor * fc_s = nullptr;
|
||||
struct ggml_tensor * fc = nullptr;
|
||||
struct ggml_tensor * fc_s = nullptr;
|
||||
struct ggml_tensor * fc_in_s = nullptr;
|
||||
struct ggml_tensor * d2t = nullptr; // draft to target vocabulary mapping
|
||||
|
||||
// dspark
|
||||
|
||||
+27
-4
@@ -281,6 +281,13 @@ struct llm_tokenizer_bpe : llm_tokenizer {
|
||||
llm_tokenizer_bpe(const llama_vocab & vocab) {
|
||||
GGML_ASSERT(vocab.get_type() == LLAMA_VOCAB_TYPE_BPE);
|
||||
switch (vocab.get_pre_type()) {
|
||||
case LLAMA_VOCAB_PRE_TYPE_FLINT:
|
||||
regex_exprs = {
|
||||
"\\p{N}{1,3}",
|
||||
"[一-龥-ゟ゠-ヿ]+",
|
||||
"[^\\r\\n\\p{L}\\p{N}]?[\\p{Lu}\\p{Lt}\\p{Lm}\\p{Lo}\\p{M}]*[\\p{Ll}\\p{Lm}\\p{Lo}\\p{M}]+(?:'[sS]|'[tT]|'[rR][eE]|'[vV][eE]|'[mM]|'[lL][lL]|'[dD])?|[^\\r\\n\\p{L}\\p{N}]?[\\p{Lu}\\p{Lt}\\p{Lm}\\p{Lo}\\p{M}]+[\\p{Ll}\\p{Lm}\\p{Lo}\\p{M}]*(?:'[sS]|'[tT]|'[rR][eE]|'[vV][eE]|'[mM]|'[lL][lL]|'[dD])?| ?[\\p{P}\\p{S}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
|
||||
};
|
||||
break;
|
||||
case LLAMA_VOCAB_PRE_TYPE_LLAMA3:
|
||||
regex_exprs = {
|
||||
// original regex from tokenizer.json
|
||||
@@ -2417,24 +2424,32 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
|
||||
LLAMA_LOG_WARN("%s: ************************************ \n", __func__);
|
||||
LLAMA_LOG_WARN("%s: \n", __func__);
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_DEFAULT;
|
||||
} else if (tokenizer_pre == "flint") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_FLINT;
|
||||
clean_spaces = false;
|
||||
} else if (tokenizer_pre == "default") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_DEFAULT;
|
||||
} else if (tokenizer_pre == "minicpm5") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_MINICPM5;
|
||||
ignore_merges = true;
|
||||
clean_spaces = false;
|
||||
} else if (
|
||||
tokenizer_pre == "llama3" ||
|
||||
tokenizer_pre == "llama-v3" ||
|
||||
tokenizer_pre == "llama-bpe"||
|
||||
tokenizer_pre == "falcon3" ||
|
||||
tokenizer_pre == "falcon-h1" ||
|
||||
tokenizer_pre == "pixtral" ||
|
||||
tokenizer_pre == "midm-2.0" ||
|
||||
tokenizer_pre == "lfm2" ||
|
||||
tokenizer_pre == "jina-v5-nano") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_LLAMA3;
|
||||
ignore_merges = true;
|
||||
add_bos = true;
|
||||
} else if (
|
||||
tokenizer_pre == "lfm2") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_LLAMA3;
|
||||
ignore_merges = true;
|
||||
add_bos = true;
|
||||
clean_spaces = false;
|
||||
} else if (
|
||||
tokenizer_pre == "deepseek-llm") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_DEEPSEEK_LLM;
|
||||
@@ -2485,6 +2500,7 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
|
||||
tokenizer_pre == "granite-embed-multi-311m") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_GEMMA4;
|
||||
escape_whitespaces = true;
|
||||
clean_spaces = false;
|
||||
} else if (
|
||||
tokenizer_pre == "mmbert") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_MMBERT;
|
||||
@@ -2531,6 +2547,7 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
|
||||
} else if (
|
||||
tokenizer_pre == "dbrx") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_DBRX;
|
||||
clean_spaces = false;
|
||||
} else if (
|
||||
tokenizer_pre == "smaug-bpe") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_SMAUG;
|
||||
@@ -2542,12 +2559,14 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
|
||||
tokenizer_pre == "chatglm-bpe") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_CHATGLM4;
|
||||
special_bos_id = LLAMA_TOKEN_NULL;
|
||||
clean_spaces = false;
|
||||
} else if (
|
||||
tokenizer_pre == "glm4" ||
|
||||
tokenizer_pre == "glm5") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_CHATGLM4;
|
||||
special_bos_id = LLAMA_TOKEN_NULL;
|
||||
ignore_merges = true;
|
||||
clean_spaces = false;
|
||||
} else if (
|
||||
tokenizer_pre == "viking") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_VIKING;
|
||||
@@ -2556,7 +2575,8 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
|
||||
tokenizer_pre == "jais") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_JAIS;
|
||||
} else if (
|
||||
tokenizer_pre == "tekken") {
|
||||
tokenizer_pre == "tekken" ||
|
||||
tokenizer_pre == "pixtral") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_TEKKEN;
|
||||
clean_spaces = false;
|
||||
ignore_merges = true;
|
||||
@@ -2580,6 +2600,7 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
|
||||
} else if (
|
||||
tokenizer_pre == "exaone4") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_GPT2;
|
||||
clean_spaces = false;
|
||||
} else if (
|
||||
tokenizer_pre == "exaone-moe") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_EXAONE_MOE;
|
||||
@@ -2680,6 +2701,7 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
|
||||
} else if (
|
||||
tokenizer_pre == "mellum2") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_MELLUM2;
|
||||
clean_spaces = false;
|
||||
} else if (
|
||||
tokenizer_pre == "k2-horizon") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_K2_HORIZON;
|
||||
@@ -2716,6 +2738,7 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
|
||||
|
||||
ml.get_key(LLM_KV_TOKENIZER_ADD_PREFIX, add_space_prefix, false);
|
||||
ml.get_key(LLM_KV_TOKENIZER_REMOVE_EXTRA_WS, remove_extra_whitespaces, false);
|
||||
ml.get_key(LLM_KV_TOKENIZER_CLEAN_SPACES, clean_spaces, false);
|
||||
}
|
||||
|
||||
const int token_idx = gguf_find_key(ctx, kv(LLM_KV_TOKENIZER_LIST).c_str());
|
||||
@@ -3147,7 +3170,6 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
|
||||
// this is currently determined based on the token text, which is obviously not ideal
|
||||
// ref: https://github.com/ggml-org/llama.cpp/issues/9606
|
||||
special_eog_ids.clear();
|
||||
|
||||
if (special_fim_pad_id != LLAMA_TOKEN_NULL && special_eog_ids.count(special_fim_pad_id) == 0) {
|
||||
special_eog_ids.insert(special_fim_pad_id);
|
||||
}
|
||||
@@ -3181,6 +3203,7 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
|
||||
|| t.first == "[EOT]" // Kimi-K2
|
||||
|| t.first == "[EOS]" // Kimi-K2
|
||||
|| t.first == "<|end_of_text|>"
|
||||
|| (pre_type == LLAMA_VOCAB_PRE_TYPE_FLINT && (t.first == "<|user|>" || t.first == "<|observation|>"))
|
||||
|| t.first == "<end_of_utterance>" // smoldocling
|
||||
|| t.first == "<eos>" // gemma4
|
||||
|| t.first == "<turn|>" // gemma4
|
||||
|
||||
@@ -70,6 +70,7 @@ enum llama_vocab_pre_type {
|
||||
LLAMA_VOCAB_PRE_TYPE_UFAKZEKA = 59,
|
||||
LLAMA_VOCAB_PRE_TYPE_MMBERT = 60,
|
||||
LLAMA_VOCAB_PRE_TYPE_K2_HORIZON = 61,
|
||||
LLAMA_VOCAB_PRE_TYPE_FLINT = 62,
|
||||
};
|
||||
|
||||
struct LLM_KV;
|
||||
|
||||
@@ -238,6 +238,7 @@ void llama_model_dflash::load_arch_tensors(llama_model_loader &) {
|
||||
|
||||
fc = create_tensor(tn(LLM_TENSOR_FC, "weight"), { n_embd_inp, n_embd }, 0);
|
||||
fc_s = create_tensor(tn(LLM_TENSOR_FC, "scale"), { 1 }, TENSOR_NOT_REQUIRED);
|
||||
fc_in_s = create_tensor(tn(LLM_TENSOR_FC, "input_scale"), { 1 }, TENSOR_NOT_REQUIRED);
|
||||
output_norm_enc = create_tensor(tn(LLM_TENSOR_ENC_OUTPUT_NORM, "weight"), { n_embd }, 0); // encoder hidden_norm (after fc)
|
||||
output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM, "weight"), { n_embd }, 0); // decoder final norm
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
#include "models.h"
|
||||
|
||||
void llama_model_flint::load_arch_hparams(llama_model_loader & ml) {
|
||||
ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps);
|
||||
ml.get_key(LLM_KV_LEADING_DENSE_BLOCK_COUNT, hparams.n_layer_dense_lead);
|
||||
ml.get_key_or_arr(LLM_KV_EXPERT_FEED_FORWARD_LENGTH, hparams.n_ff_exp_arr, hparams.n_layer_all);
|
||||
ml.get_key(LLM_KV_EXPERT_SHARED_COUNT, hparams.n_expert_shared);
|
||||
ml.get_key(LLM_KV_EXPERT_WEIGHTS_SCALE, hparams.expert_weights_scale, false);
|
||||
ml.get_key(LLM_KV_EXPERT_WEIGHTS_NORM, hparams.expert_weights_norm);
|
||||
ml.get_key(LLM_KV_EXPERT_GATING_FUNC, hparams.expert_gating_func);
|
||||
|
||||
ml.get_key(LLM_KV_EXPERT_SHARED_FEED_FORWARD_LENGTH, hparams.n_ff_shexp);
|
||||
ml.get_arr(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, hparams.is_swa_impl);
|
||||
ml.get_arr(LLM_KV_ATTENTION_ROPE_PATTERN, hparams.rope_pattern);
|
||||
ml.get_key(LLM_KV_ATTENTION_SLIDING_WINDOW, hparams.n_swa);
|
||||
hparams.swa_type = LLAMA_SWA_TYPE_STANDARD;
|
||||
type = LLM_TYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
void llama_model_flint::load_arch_tensors(llama_model_loader &) {
|
||||
LLAMA_LOAD_LOCALS;
|
||||
|
||||
const int64_t n_ff_exp = hparams.n_ff_exp();
|
||||
|
||||
tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0);
|
||||
|
||||
output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM, "weight"), {n_embd}, 0);
|
||||
output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), {n_embd, n_vocab}, 0);
|
||||
|
||||
for (int i = 0; i < n_layer; ++i) {
|
||||
auto & layer = layers[i];
|
||||
|
||||
layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0);
|
||||
|
||||
create_tensor_qkv(layer, i, n_embd, n_embd_head_k * n_head, n_embd_k_gqa, n_embd_v_gqa, 0);
|
||||
layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd_head_k * n_head, n_embd}, 0);
|
||||
|
||||
layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", i), {n_embd_head_k}, 0);
|
||||
layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", i), {n_embd_head_k}, 0);
|
||||
|
||||
layer.attn_sinks = create_tensor(tn(LLM_TENSOR_ATTN_SINKS, "weight", i), {n_head}, 0);
|
||||
|
||||
layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, 0);
|
||||
|
||||
if (i < (int) hparams.n_layer_dense_lead) {
|
||||
layer.ffn_gate = create_tensor(tn(LLM_TENSOR_FFN_GATE, "weight", i), {n_embd, n_ff}, 0);
|
||||
layer.ffn_down = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "weight", i), { n_ff, n_embd}, 0);
|
||||
layer.ffn_up = create_tensor(tn(LLM_TENSOR_FFN_UP, "weight", i), {n_embd, n_ff}, 0);
|
||||
} else {
|
||||
layer.ffn_gate_inp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP, "weight", i), {n_embd, n_expert}, 0);
|
||||
layer.ffn_exp_probs_b = create_tensor(tn(LLM_TENSOR_FFN_EXP_PROBS_B, "bias", i), {n_expert}, TENSOR_NOT_REQUIRED);
|
||||
|
||||
if (n_expert == 0) {
|
||||
throw std::runtime_error("n_expert must be > 0");
|
||||
}
|
||||
if (n_expert_used == 0) {
|
||||
throw std::runtime_error("n_expert_used must be > 0");
|
||||
}
|
||||
|
||||
// MoE branch
|
||||
layer.ffn_gate_exps = create_tensor(tn(LLM_TENSOR_FFN_GATE_EXPS, "weight", i), { n_embd, n_ff_exp, n_expert}, 0);
|
||||
layer.ffn_down_exps = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXPS, "weight", i), {n_ff_exp, n_embd, n_expert}, 0);
|
||||
layer.ffn_up_exps = create_tensor(tn(LLM_TENSOR_FFN_UP_EXPS, "weight", i), { n_embd, n_ff_exp, n_expert}, 0);
|
||||
|
||||
// Shared expert branch
|
||||
layer.ffn_gate_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_SHEXP, "weight", i), {n_embd, hparams.n_ff_shexp}, 0);
|
||||
layer.ffn_down_shexp = create_tensor(tn(LLM_TENSOR_FFN_DOWN_SHEXP, "weight", i), { hparams.n_ff_shexp, n_embd}, 0);
|
||||
layer.ffn_up_shexp = create_tensor(tn(LLM_TENSOR_FFN_UP_SHEXP, "weight", i), {n_embd, hparams.n_ff_shexp}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<llm_graph_context> llama_model_flint::build_arch_graph(const llm_graph_params & params) const {
|
||||
return std::make_unique<graph>(*this, params);
|
||||
}
|
||||
|
||||
llama_model_flint::graph::graph(const llama_model & model, const llm_graph_params & params) :
|
||||
llm_graph_context(params) {
|
||||
const int64_t n_embd_head = hparams.n_embd_head_v();
|
||||
|
||||
GGML_ASSERT(n_embd_head == hparams.n_embd_head_k());
|
||||
GGML_ASSERT(n_embd_head == n_rot);
|
||||
|
||||
ggml_tensor * cur;
|
||||
ggml_tensor * inpL;
|
||||
|
||||
inpL = build_inp_embd(model.tok_embd);
|
||||
|
||||
// inp_pos - contains the positions
|
||||
ggml_tensor * inp_pos = build_inp_pos();
|
||||
|
||||
auto * inp_attn = build_attn_inp_kv_iswa();
|
||||
|
||||
ggml_tensor * inp_out_ids = build_inp_out_ids();
|
||||
|
||||
for (int il = 0; il < n_layer; ++il) {
|
||||
res->t_layer_inp[il] = inpL;
|
||||
ggml_tensor * inpSA = inpL;
|
||||
|
||||
// norm
|
||||
cur = build_norm(inpL, model.layers[il].attn_norm, NULL, LLM_NORM_RMS, il);
|
||||
cb(cur, "attn_norm", il);
|
||||
|
||||
// self_attention
|
||||
{
|
||||
// compute Q and K and RoPE them
|
||||
auto [Qcur, Kcur, Vcur] = build_qkv(model.layers[il], cur,
|
||||
n_embd_head, n_head, n_head_kv, il);
|
||||
|
||||
Qcur = build_norm(Qcur, model.layers[il].attn_q_norm, nullptr, LLM_NORM_RMS, il);
|
||||
Kcur = build_norm(Kcur, model.layers[il].attn_k_norm, nullptr, LLM_NORM_RMS, il);
|
||||
if (hparams.has_rope(il)) {
|
||||
Qcur = ggml_rope_ext(ctx0, Qcur, inp_pos, nullptr, n_rot, rope_type, n_ctx_orig,
|
||||
freq_base, freq_scale, ext_factor, attn_factor, beta_fast, beta_slow);
|
||||
Kcur = ggml_rope_ext(ctx0, Kcur, inp_pos, nullptr, n_rot, rope_type, n_ctx_orig,
|
||||
freq_base, freq_scale, ext_factor, attn_factor, beta_fast, beta_slow);
|
||||
}
|
||||
cb(Qcur, "Qcur", il);
|
||||
cb(Kcur, "Kcur", il);
|
||||
cb(Vcur, "Vcur", il);
|
||||
|
||||
cur = build_attn(inp_attn,
|
||||
model.layers[il].wo, model.layers[il].wo_b, model.layers[il].wo_s,
|
||||
Qcur, Kcur, Vcur, nullptr, model.layers[il].attn_sinks, nullptr, 1.0f / sqrtf(float(n_embd_head)), il);
|
||||
}
|
||||
if (il == n_layer - 1 && inp_out_ids) {
|
||||
cur = ggml_get_rows(ctx0, cur, inp_out_ids);
|
||||
inpSA = ggml_get_rows(ctx0, inpSA, inp_out_ids);
|
||||
}
|
||||
ggml_tensor * ffn_inp = ggml_add(ctx0, cur, inpSA);
|
||||
cb(ffn_inp, "ffn_inp", il);
|
||||
|
||||
// MoE branch
|
||||
cur = build_norm(ffn_inp, model.layers[il].ffn_norm, NULL, LLM_NORM_RMS, il);
|
||||
cb(cur, "ffn_norm", il);
|
||||
|
||||
if ((uint32_t) il < hparams.n_layer_dense_lead) {
|
||||
cur = build_ffn(cur,
|
||||
model.layers[il].ffn_up, NULL, model.layers[il].ffn_up_s,
|
||||
model.layers[il].ffn_gate, NULL, model.layers[il].ffn_gate_s,
|
||||
model.layers[il].ffn_down, NULL, model.layers[il].ffn_down_s,
|
||||
NULL, LLM_FFN_SILU, LLM_FFN_PAR, il);
|
||||
cb(cur, "ffn_out", il);
|
||||
} else {
|
||||
ggml_tensor * moe_out = build_moe_ffn(cur,
|
||||
model.layers[il].ffn_gate_inp,
|
||||
model.layers[il].ffn_up_exps,
|
||||
model.layers[il].ffn_gate_exps,
|
||||
model.layers[il].ffn_down_exps,
|
||||
model.layers[il].ffn_exp_probs_b,
|
||||
n_expert, n_expert_used,
|
||||
LLM_FFN_SILU, hparams.expert_weights_norm,
|
||||
hparams.expert_weights_scale,
|
||||
(llama_expert_gating_func_type) hparams.expert_gating_func,
|
||||
il, nullptr, nullptr,
|
||||
model.layers[il].ffn_up_exps_s,
|
||||
model.layers[il].ffn_gate_exps_s,
|
||||
model.layers[il].ffn_down_exps_s);
|
||||
cb(moe_out, "ffn_moe_out", il);
|
||||
|
||||
{
|
||||
ggml_tensor * ffn_shexp =
|
||||
build_ffn(cur,
|
||||
model.layers[il].ffn_up_shexp, NULL, model.layers[il].ffn_up_shexp_s,
|
||||
model.layers[il].ffn_gate_shexp, NULL, model.layers[il].ffn_gate_shexp_s,
|
||||
model.layers[il].ffn_down_shexp, NULL, model.layers[il].ffn_down_shexp_s,
|
||||
NULL, LLM_FFN_SILU, LLM_FFN_PAR, il);
|
||||
cb(ffn_shexp, "ffn_shexp", il);
|
||||
|
||||
cur = ggml_add(ctx0, moe_out, ffn_shexp);
|
||||
cb(cur, "ffn_out", il);
|
||||
}
|
||||
}
|
||||
cur = ggml_add(ctx0, cur, ffn_inp);
|
||||
|
||||
cur = build_cvec(cur, il);
|
||||
cb(cur, "l_out", il);
|
||||
|
||||
// input for next layer
|
||||
inpL = cur;
|
||||
}
|
||||
cur = inpL;
|
||||
|
||||
cur = build_norm(cur, model.output_norm, NULL, LLM_NORM_RMS, -1);
|
||||
|
||||
cb(cur, "result_norm", -1);
|
||||
res->t_embd = cur;
|
||||
|
||||
// lm_head
|
||||
cur = build_lora_mm(model.output, cur, model.output_s);
|
||||
|
||||
cb(cur, "result_output", -1);
|
||||
res->t_logits = cur;
|
||||
}
|
||||
@@ -153,6 +153,8 @@ llama_model_granite_hybrid::graph::graph(const llama_model & model, const llm_gr
|
||||
}
|
||||
|
||||
for (int il = 0; il < n_layer; ++il) {
|
||||
res->t_layer_inp[il] = inpL;
|
||||
|
||||
struct ggml_tensor * inpSA = inpL;
|
||||
|
||||
// norm
|
||||
|
||||
@@ -154,6 +154,8 @@ llama_model_granite_swa::graph::graph(
|
||||
cb(inpL, "deepstack_in", il);
|
||||
}
|
||||
|
||||
res->t_layer_inp[il] = inpL;
|
||||
|
||||
ggml_tensor * inpSA = inpL;
|
||||
|
||||
// norm
|
||||
|
||||
@@ -293,6 +293,8 @@ llama_model_granite_switch::graph::graph(
|
||||
ggml_tensor * cur;
|
||||
|
||||
for (int il = 0; il < n_layer; ++il) {
|
||||
res->t_layer_inp[il] = inpL;
|
||||
|
||||
ggml_tensor * inpSA = inpL;
|
||||
|
||||
cur = build_norm(inpL, model.layers[il].attn_norm, NULL, LLM_NORM_RMS, il);
|
||||
|
||||
@@ -159,6 +159,8 @@ llama_model_granite::graph::graph(
|
||||
cb(inpL, "deepstack_in", il);
|
||||
}
|
||||
|
||||
res->t_layer_inp[il] = inpL;
|
||||
|
||||
ggml_tensor * inpSA = inpL;
|
||||
|
||||
// norm
|
||||
|
||||
@@ -614,6 +614,21 @@ struct llama_model_qwen2moe : public llama_model_base {
|
||||
};
|
||||
|
||||
|
||||
struct llama_model_sensenova_u1 : public llama_model_base {
|
||||
llama_model_sensenova_u1(const struct llama_model_params & params) : llama_model_base(params) {}
|
||||
void load_arch_hparams(llama_model_loader & ml) override;
|
||||
void load_arch_tensors(llama_model_loader & ml) override;
|
||||
|
||||
float rope_freq_base_spatial = 10000.0f;
|
||||
|
||||
struct graph : public llm_graph_context {
|
||||
graph(const llama_model_sensenova_u1 & model, const llm_graph_params & params);
|
||||
};
|
||||
|
||||
std::unique_ptr<llm_graph_context> build_arch_graph(const llm_graph_params & params) const override;
|
||||
};
|
||||
|
||||
|
||||
struct llama_model_qwen3 : public llama_model_base {
|
||||
llama_model_qwen3(const struct llama_model_params & params) : llama_model_base(params) {}
|
||||
void load_arch_hparams(llama_model_loader & ml) override;
|
||||
@@ -678,6 +693,19 @@ struct llama_model_qwen3moe : public llama_model_base {
|
||||
};
|
||||
|
||||
|
||||
struct llama_model_flint : public llama_model_base {
|
||||
llama_model_flint(const struct llama_model_params & params) : llama_model_base(params) {}
|
||||
void load_arch_hparams(llama_model_loader & ml) override;
|
||||
void load_arch_tensors(llama_model_loader & ml) override;
|
||||
|
||||
struct graph : public llm_graph_context {
|
||||
graph(const llama_model & model, const llm_graph_params & params);
|
||||
};
|
||||
|
||||
std::unique_ptr<llm_graph_context> build_arch_graph(const llm_graph_params & params) const override;
|
||||
};
|
||||
|
||||
|
||||
struct llama_model_qwen3vl : public llama_model_base {
|
||||
llama_model_qwen3vl(const struct llama_model_params & params) : llama_model_base(params) {}
|
||||
void load_arch_hparams(llama_model_loader & ml) override;
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
#include "models.h"
|
||||
|
||||
namespace {
|
||||
class llm_graph_input_pos_sensenova_u1 : public llm_graph_input_pos {
|
||||
public:
|
||||
llm_graph_input_pos_sensenova_u1() : llm_graph_input_pos(4) {}
|
||||
|
||||
void set_input(const llama_ubatch * ubatch) override {
|
||||
if (ubatch->pos && pos) {
|
||||
const int64_t n_tokens = ubatch->n_tokens;
|
||||
|
||||
// U1 expects text-token positions in the temporal plane only: [t, 0, 0, 0].
|
||||
// The generic M-RoPE input uses [t, t, t, 0], so U1 needs a custom input class.
|
||||
if (ubatch->token) {
|
||||
std::vector<llama_pos> pos_data(n_tokens*n_pos_per_embd, 0);
|
||||
for (int i = 0; i < n_tokens; ++i) {
|
||||
pos_data[i] = ubatch->pos[i];
|
||||
}
|
||||
ggml_backend_tensor_set(pos, pos_data.data(), 0, pos_data.size()*ggml_element_size(pos));
|
||||
} else {
|
||||
ggml_backend_tensor_set(pos, ubatch->pos, 0, n_tokens*n_pos_per_embd*ggml_element_size(pos));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void llama_model_sensenova_u1::load_arch_hparams(llama_model_loader & ml) {
|
||||
ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps);
|
||||
|
||||
ml.get_key(LLM_KV_ROPE_FREQ_BASE_SPATIAL, rope_freq_base_spatial);
|
||||
if (hparams.n_embd_head_k() % 4 != 0 || hparams.n_embd_head_v() != hparams.n_embd_head_k()) {
|
||||
throw std::runtime_error("SenseNova U1 requires equal Q/K/V head sizes divisible by four");
|
||||
}
|
||||
switch (hparams.n_layer()) {
|
||||
case 42: type = LLM_TYPE_8B; break;
|
||||
default: type = LLM_TYPE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
void llama_model_sensenova_u1::load_arch_tensors(llama_model_loader &) {
|
||||
LLAMA_LOAD_LOCALS;
|
||||
|
||||
tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0);
|
||||
|
||||
// output
|
||||
output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM, "weight"), {n_embd}, 0);
|
||||
output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), {n_embd, n_vocab}, TENSOR_NOT_REQUIRED);
|
||||
// if output is NULL, init from the input tok embed
|
||||
if (output == NULL) {
|
||||
output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, TENSOR_DUPLICATED);
|
||||
}
|
||||
|
||||
for (int i = 0; i < n_layer; ++i) {
|
||||
auto & layer = layers[i];
|
||||
|
||||
layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0);
|
||||
|
||||
create_tensor_qkv(layer, i, n_embd, n_embd_head_k * n_head, n_embd_gqa, n_embd_gqa, 0);
|
||||
layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd_head_k * n_head, n_embd}, 0);
|
||||
|
||||
layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", i), {n_embd_head_k}, 0);
|
||||
layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", i), {n_embd_head_k}, 0);
|
||||
|
||||
layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, 0);
|
||||
layer.ffn_gate = create_tensor(tn(LLM_TENSOR_FFN_GATE, "weight", i), {n_embd, n_ff}, 0);
|
||||
layer.ffn_down = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "weight", i), { n_ff, n_embd}, 0);
|
||||
layer.ffn_up = create_tensor(tn(LLM_TENSOR_FFN_UP, "weight", i), {n_embd, n_ff}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<llm_graph_context> llama_model_sensenova_u1::build_arch_graph(const llm_graph_params & params) const {
|
||||
return std::make_unique<graph>(*this, params);
|
||||
}
|
||||
|
||||
llama_model_sensenova_u1::graph::graph(const llama_model_sensenova_u1 & model, const llm_graph_params & params) : llm_graph_context(params) {
|
||||
const int64_t n_embd_head = hparams.n_embd_head_v();
|
||||
|
||||
GGML_ASSERT(n_embd_head == hparams.n_embd_head_k());
|
||||
|
||||
ggml_tensor * cur;
|
||||
ggml_tensor * inpL;
|
||||
|
||||
auto normalize = [&](ggml_tensor * x, ggml_tensor * weight) {
|
||||
x = ggml_rms_norm(ctx0, x, hparams.f_norm_rms_eps);
|
||||
|
||||
return ggml_mul(ctx0, x, weight);
|
||||
};
|
||||
|
||||
inpL = build_inp_embd(model.tok_embd);
|
||||
|
||||
ggml_tensor * pos[3]{};
|
||||
auto inp = std::make_unique<llm_graph_input_pos_sensenova_u1>();
|
||||
auto & inp_pos = inp->pos;
|
||||
inp_pos = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, (int64_t)n_tokens*hparams.n_pos_per_embd());
|
||||
ggml_set_input(inp_pos);
|
||||
res->add_input(std::move(inp));
|
||||
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
pos[axis] = ggml_view_1d(ctx0, inp_pos, n_tokens, axis*n_tokens*sizeof(int32_t));
|
||||
}
|
||||
|
||||
auto normalize_and_rotate = [&](ggml_tensor * x, ggml_tensor * weight) {
|
||||
const int64_t heads = x->ne[1];
|
||||
x = ggml_reshape_4d(ctx0, x, n_embd_head/2, 2, heads, n_tokens);
|
||||
x = normalize(x, ggml_reshape_2d(ctx0, weight, n_embd_head/2, 2));
|
||||
x = ggml_reshape_3d(ctx0, x, n_embd_head, heads, n_tokens);
|
||||
|
||||
// Normalize temporal and spatial halves separately; rotate time, height, then width.
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
const int dims = axis == 0 ? n_embd_head/2 : n_embd_head/4;
|
||||
const int offset = axis == 0 ? 0 : n_embd_head/2 + (axis - 1)*n_embd_head/4;
|
||||
const float base = axis == 0 ? freq_base : model.rope_freq_base_spatial;
|
||||
x = ggml_rope_ext(ctx0, x, pos[axis], nullptr, dims, GGML_ROPE_TYPE_NEOX,
|
||||
n_ctx_orig, base, 1.0f, 0.0f, 1.0f, 32.0f, 1.0f);
|
||||
ggml_rope_set_offset(x, offset);
|
||||
}
|
||||
return x;
|
||||
};
|
||||
|
||||
auto * inp_attn = build_attn_inp_kv();
|
||||
|
||||
ggml_tensor * inp_out_ids = build_inp_out_ids();
|
||||
|
||||
for (int il = 0; il < n_layer; ++il) {
|
||||
res->t_layer_inp[il] = inpL;
|
||||
|
||||
ggml_tensor * inpSA = inpL;
|
||||
|
||||
// norm
|
||||
cur = normalize(inpL, model.layers[il].attn_norm);
|
||||
cb(cur, "attn_norm", il);
|
||||
|
||||
// self-attention
|
||||
{
|
||||
// compute Q and K and RoPE them
|
||||
auto [Qcur, Kcur, Vcur] = build_qkv(model.layers[il], cur,
|
||||
n_embd_head, n_head, n_head_kv, il);
|
||||
|
||||
Qcur = normalize_and_rotate(Qcur, model.layers[il].attn_q_norm);
|
||||
Kcur = normalize_and_rotate(Kcur, model.layers[il].attn_k_norm);
|
||||
|
||||
cb(Qcur, "Qcur", il);
|
||||
cb(Kcur, "Kcur", il);
|
||||
cb(Vcur, "Vcur", il);
|
||||
|
||||
cur = build_attn(inp_attn,
|
||||
model.layers[il].wo, model.layers[il].wo_b, model.layers[il].wo_s,
|
||||
Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, 1.0f/sqrtf(float(n_embd_head)), il);
|
||||
}
|
||||
if (il == n_layer - 1 && inp_out_ids) {
|
||||
cur = ggml_get_rows(ctx0, cur, inp_out_ids);
|
||||
inpSA = ggml_get_rows(ctx0, inpSA, inp_out_ids);
|
||||
}
|
||||
ggml_tensor * ffn_inp = ggml_add(ctx0, cur, inpSA);
|
||||
cb(ffn_inp, "ffn_inp", il);
|
||||
|
||||
// feed-forward network
|
||||
cur = normalize(ffn_inp, model.layers[il].ffn_norm);
|
||||
cb(cur, "ffn_norm", il);
|
||||
|
||||
cur = build_ffn(cur,
|
||||
model.layers[il].ffn_up, NULL, model.layers[il].ffn_up_s,
|
||||
model.layers[il].ffn_gate, NULL, model.layers[il].ffn_gate_s,
|
||||
model.layers[il].ffn_down, NULL, model.layers[il].ffn_down_s,
|
||||
NULL,
|
||||
LLM_FFN_SILU, LLM_FFN_PAR, il);
|
||||
cb(cur, "ffn_out", il);
|
||||
|
||||
cur = ggml_add(ctx0, cur, ffn_inp);
|
||||
|
||||
cur = build_cvec(cur, il);
|
||||
cb(cur, "l_out", il);
|
||||
|
||||
// input for next layer
|
||||
inpL = cur;
|
||||
}
|
||||
cur = inpL;
|
||||
|
||||
cur = normalize(cur, model.output_norm);
|
||||
|
||||
cb(cur, "result_norm", -1);
|
||||
res->t_embd = cur;
|
||||
|
||||
// lm_head
|
||||
cur = build_lora_mm(model.output, cur, model.output_s);
|
||||
|
||||
cb(cur, "result_output", -1);
|
||||
res->t_logits = cur;
|
||||
}
|
||||
@@ -142,7 +142,7 @@ llama_test(test-tokenizer-0 NAME test-tokenizer-0-command-r ARGS ${PROJE
|
||||
llama_test(test-tokenizer-0 NAME test-tokenizer-0-deepseek-coder ARGS ${PROJECT_SOURCE_DIR}/models/ggml-vocab-deepseek-coder.gguf)
|
||||
llama_test(test-tokenizer-0 NAME test-tokenizer-0-deepseek-llm ARGS ${PROJECT_SOURCE_DIR}/models/ggml-vocab-deepseek-llm.gguf)
|
||||
llama_test(test-tokenizer-0 NAME test-tokenizer-0-falcon ARGS ${PROJECT_SOURCE_DIR}/models/ggml-vocab-falcon.gguf)
|
||||
llama_test(test-tokenizer-0 NAME test-tokenizer-0-gemma-4 ARGS ${PROJECT_SOURCE_DIR}/models/ggml-vocab-gemma-4.gguf)
|
||||
llama_test(test-tokenizer-0 NAME test-tokenizer-0-gemma-4 ARGS ${PROJECT_SOURCE_DIR}/models/ggml-vocab-gemma-4.gguf --check-decode)
|
||||
llama_test(test-tokenizer-0 NAME test-tokenizer-0-gpt-2 ARGS ${PROJECT_SOURCE_DIR}/models/ggml-vocab-gpt-2.gguf)
|
||||
llama_test(test-tokenizer-0 NAME test-tokenizer-0-llama-bpe ARGS ${PROJECT_SOURCE_DIR}/models/ggml-vocab-llama-bpe.gguf)
|
||||
llama_test(test-tokenizer-0 NAME test-tokenizer-0-llama-spm ARGS ${PROJECT_SOURCE_DIR}/models/ggml-vocab-llama-spm.gguf)
|
||||
|
||||
@@ -71,6 +71,9 @@ falcon ,0 ,any ,ADD+ADD , 2
|
||||
falcon ,0 ,any ,NORM+MUL+ADD , 5
|
||||
falcon-h1 ,0 ,any ,ADD+ADD , 2
|
||||
falcon-h1 ,0 ,any ,RMS_NORM+MUL , 9
|
||||
flint ,0 ,any ,ADD+ADD , 1
|
||||
flint ,0 ,any ,MUL+ADD , 2
|
||||
flint ,0 ,any ,RMS_NORM+MUL , 9
|
||||
gemma ,0 ,any ,RMS_NORM+MUL , 5
|
||||
gemma2 ,0 ,any ,RMS_NORM+MUL , 5
|
||||
gemma2 ,0 ,any ,RMS_NORM+MUL+ADD , 4
|
||||
|
||||
|
@@ -1299,6 +1299,18 @@ static void test_namespace(testing & t) {
|
||||
json::object(),
|
||||
"3"
|
||||
);
|
||||
|
||||
test_template(t, "namespace attribute named like a dict method",
|
||||
"{% set ns = namespace(items=[]) %}{{ 'yes' if ns.items else 'no' }}{% set ns.items = ns.items + [1] %}{{ ns.items | length }}{{ 'yes' if ns.items else 'no' }}",
|
||||
json::object(),
|
||||
"no1yes"
|
||||
);
|
||||
|
||||
test_template(t, "dict keeps its methods",
|
||||
"{% set d = dict(items=1) %}{% for k, v in d.items() %}{{ k }}={{ v }}{% endfor %}",
|
||||
json::object(),
|
||||
"items=1"
|
||||
);
|
||||
}
|
||||
|
||||
static void test_tests(testing & t) {
|
||||
|
||||
@@ -329,6 +329,9 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
|
||||
// partial rotary: n_rot must not exceed the indexer key length (64)
|
||||
ms.add_kv(LLM_KV_ROPE_DIMENSION_COUNT, uint32_t(64));
|
||||
}
|
||||
if (arch == LLM_ARCH_SENSENOVA_U1) {
|
||||
ms.add_kv(LLM_KV_ROPE_FREQ_BASE_SPATIAL, 10000.0f);
|
||||
}
|
||||
ms.add_kv(LLM_KV_ATTENTION_CLAMP_KQV, 1.0f);
|
||||
ms.add_kv(LLM_KV_ATTENTION_LAYERNORM_EPS, 1e-5f);
|
||||
ms.add_kv(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, 1e-5f);
|
||||
@@ -358,13 +361,16 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
|
||||
ms.add_kv(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, uint32_t(5));
|
||||
} else if (arch == LLM_ARCH_COHERE2MOE || arch == LLM_ARCH_MIMO2 || arch == LLM_ARCH_STEP35 || arch == LLM_ARCH_SPARK2_5 ||
|
||||
arch == LLM_ARCH_MUSE_GLIMMER || arch == LLM_ARCH_MUSE_SPARK || arch == LLM_ARCH_GRANITE_SWA || arch == LLM_ARCH_DOTS3NOTE ||
|
||||
arch == LLM_ARCH_KOLIBRI1 || arch == LLM_ARCH_MAPLE) {
|
||||
arch == LLM_ARCH_KOLIBRI1 || arch == LLM_ARCH_MAPLE || arch == LLM_ARCH_FLINT) {
|
||||
std::vector<uint32_t> pattern;
|
||||
pattern.reserve(n_layer);
|
||||
for (uint32_t il = 0; il < n_layer; il++) {
|
||||
pattern.push_back(il % 2);
|
||||
}
|
||||
ms.add_kv(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, pattern);
|
||||
if (arch == LLM_ARCH_FLINT) {
|
||||
ms.add_kv(LLM_KV_ATTENTION_ROPE_PATTERN, pattern);
|
||||
}
|
||||
} else {
|
||||
ms.add_kv(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, uint32_t(2));
|
||||
}
|
||||
@@ -522,6 +528,9 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
|
||||
ms.add_kv(LLM_KV_EXPERT_GATING_FUNC, gating_func); // sigmoid : softmax : sqrtsoftplus
|
||||
ms.add_kv(LLM_KV_EXPERT_GROUP_SCALE, 1.0f);
|
||||
ms.add_kv(LLM_KV_EXPERTS_PER_GROUP, uint32_t(1));
|
||||
if (arch == LLM_ARCH_FLINT) {
|
||||
ms.add_kv(LLM_KV_EXPERT_WEIGHTS_NORM, true);
|
||||
}
|
||||
if (arch == LLM_ARCH_K2_HORIZON) {
|
||||
ms.add_kv(LLM_KV_ATTENTION_VALUE_EXPERT_COUNT, uint32_t(2));
|
||||
ms.add_kv(LLM_KV_ATTENTION_VALUE_EXPERT_USED_COUNT, uint32_t(2));
|
||||
@@ -751,6 +760,7 @@ static bool check_causal_attn_toggle(
|
||||
|
||||
static bool moe_mandatory(const llm_arch arch) {
|
||||
switch (arch) {
|
||||
case LLM_ARCH_FLINT:
|
||||
case LLM_ARCH_LLAMA4:
|
||||
case LLM_ARCH_MAION_CODER:
|
||||
case LLM_ARCH_COHERE2MOE:
|
||||
|
||||
@@ -125,7 +125,7 @@ static llama_tests read_tests(const std::string & fname_inp, const std::string &
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "Usage: %s vocab-file [text-file]\n", argv[0]);
|
||||
fprintf(stderr, "Usage: %s vocab-file [text-file] [--check-decode]\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -135,8 +135,13 @@ int main(int argc, char **argv) {
|
||||
const std::string fname_out = fname + ".out";
|
||||
|
||||
std::string fname_text;
|
||||
if (argc > 2) {
|
||||
fname_text = argv[2];
|
||||
bool check_decode = false; // also check that the expected tokens detokenize to the input text
|
||||
for (int i = 2; i < argc; i++) {
|
||||
if (std::string(argv[i]) == "--check-decode") {
|
||||
check_decode = true;
|
||||
} else {
|
||||
fname_text = argv[i];
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "%s : reading vocab from: '%s'\n", __func__, fname.c_str());
|
||||
@@ -244,6 +249,14 @@ int main(int argc, char **argv) {
|
||||
|
||||
success = false;
|
||||
}
|
||||
|
||||
if (check_decode) {
|
||||
const std::string dec = common_detokenize(ctx, test_kv.second);
|
||||
if (dec != test_kv.first) {
|
||||
fprintf(stderr, "%s : failed decode: '%s' detokenized to '%s'\n", __func__, test_kv.first.c_str(), dec.c_str());
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ add_library(mtmd
|
||||
models/paddleocr.cpp
|
||||
models/pixtral.cpp
|
||||
models/qwen2vl.cpp
|
||||
models/sensenova-u1.cpp
|
||||
models/minimax-m3.cpp
|
||||
models/qwen3vl.cpp
|
||||
models/ling3vl.cpp
|
||||
|
||||
@@ -547,6 +547,7 @@ enum projector_type {
|
||||
PROJECTOR_TYPE_POCKETTTS_SPKENC,
|
||||
PROJECTOR_TYPE_POCKETTTS_GEN,
|
||||
PROJECTOR_TYPE_MUSE_GLIMMER,
|
||||
PROJECTOR_TYPE_SENSENOVA_U1,
|
||||
PROJECTOR_TYPE_COHERE2V,
|
||||
PROJECTOR_TYPE_MUSE_SPARK_AUDIO,
|
||||
PROJECTOR_TYPE_UNKNOWN,
|
||||
@@ -621,6 +622,7 @@ static std::map<projector_type, std::string> PROJECTOR_TYPE_NAMES = {
|
||||
{ PROJECTOR_TYPE_POCKETTTS_SPKENC, "pockettts_spkenc"},
|
||||
{ PROJECTOR_TYPE_POCKETTTS_GEN, "pockettts_gen"},
|
||||
{ PROJECTOR_TYPE_MUSE_GLIMMER, "muse-glimmer"},
|
||||
{ PROJECTOR_TYPE_SENSENOVA_U1, "sensenova_u1"},
|
||||
{ PROJECTOR_TYPE_COHERE2V, "cohere2v"},
|
||||
{ PROJECTOR_TYPE_MUSE_SPARK_AUDIO, "muse-spark-audio"},
|
||||
};
|
||||
|
||||
@@ -997,6 +997,10 @@ static std::unique_ptr<clip_graph> clip_get_graph_builder(clip_ctx * ctx, const
|
||||
{
|
||||
builder = std::make_unique<clip_graph_qwen3vl>(ctx, img);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_SENSENOVA_U1:
|
||||
{
|
||||
builder = std::make_unique<clip_graph_sensenova_u1>(ctx, img);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_LING3VL:
|
||||
{
|
||||
builder = std::make_unique<clip_graph_ling3vl>(ctx, img);
|
||||
@@ -1795,6 +1799,20 @@ struct clip_model_loader {
|
||||
LOG_WRN("%s: more info: https://github.com/ggml-org/llama.cpp/issues/16842\n\n", __func__);
|
||||
}
|
||||
} break;
|
||||
case PROJECTOR_TYPE_SENSENOVA_U1:
|
||||
{
|
||||
hparams.n_merge = 2;
|
||||
hparams.image_resize_algo = RESIZE_ALGO_BICUBIC;
|
||||
get_u32(KEY_SPATIAL_MERGE_SIZE, hparams.n_merge, false);
|
||||
get_u32(KEY_IMAGE_MIN_PIXELS, hparams.image_min_pixels);
|
||||
get_u32(KEY_IMAGE_MAX_PIXELS, hparams.image_max_pixels);
|
||||
hparams.rope_theta = 10000.0f;
|
||||
hparams.set_limit_image_tokens(16, 4096);
|
||||
if (hparams.n_merge != 2) {
|
||||
throw std::runtime_error(string_format("%s: SenseNova U1 requires spatial_merge_size=2, got %u", __func__, hparams.n_merge));
|
||||
}
|
||||
hparams.set_warmup_n_tokens(16*16);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_MINIMAX_M3:
|
||||
{
|
||||
hparams.n_merge = 2; // spatial_merge_size
|
||||
@@ -3837,6 +3855,11 @@ struct clip_model_loader {
|
||||
}
|
||||
|
||||
} break;
|
||||
case PROJECTOR_TYPE_SENSENOVA_U1:
|
||||
{
|
||||
model.mm_0_w = get_tensor(string_format(TN_LLAVA_PROJ, 0, "weight"));
|
||||
model.mm_0_b = get_tensor(string_format(TN_LLAVA_PROJ, 0, "bias"));
|
||||
} break;
|
||||
default:
|
||||
GGML_ASSERT(false && "unknown projector type");
|
||||
}
|
||||
@@ -4311,6 +4334,7 @@ int clip_n_output_tokens_x(const clip_ctx * ctx, const clip_image_f32 * img) {
|
||||
case PROJECTOR_TYPE_QWEN2VL:
|
||||
case PROJECTOR_TYPE_QWEN25VL:
|
||||
case PROJECTOR_TYPE_QWEN3VL:
|
||||
case PROJECTOR_TYPE_SENSENOVA_U1:
|
||||
case PROJECTOR_TYPE_LING3VL:
|
||||
case PROJECTOR_TYPE_EXAONE4_5:
|
||||
case PROJECTOR_TYPE_MIMOVL:
|
||||
@@ -4341,6 +4365,7 @@ int clip_n_output_tokens_y(const clip_ctx * ctx, const clip_image_f32 * img) {
|
||||
case PROJECTOR_TYPE_QWEN2VL:
|
||||
case PROJECTOR_TYPE_QWEN25VL:
|
||||
case PROJECTOR_TYPE_QWEN3VL:
|
||||
case PROJECTOR_TYPE_SENSENOVA_U1:
|
||||
case PROJECTOR_TYPE_LING3VL:
|
||||
case PROJECTOR_TYPE_EXAONE4_5:
|
||||
case PROJECTOR_TYPE_MIMOVL:
|
||||
@@ -4426,6 +4451,7 @@ int clip_n_output_tokens(const clip_ctx * ctx, const clip_image_f32 * img) {
|
||||
case PROJECTOR_TYPE_QWEN2VL:
|
||||
case PROJECTOR_TYPE_QWEN25VL:
|
||||
case PROJECTOR_TYPE_QWEN3VL:
|
||||
case PROJECTOR_TYPE_SENSENOVA_U1:
|
||||
case PROJECTOR_TYPE_LING3VL:
|
||||
case PROJECTOR_TYPE_EXAONE4_5:
|
||||
case PROJECTOR_TYPE_MIMOVL:
|
||||
@@ -5525,6 +5551,19 @@ bool clip_encode(struct clip_ctx * ctx, struct clip_encode_params * params) {
|
||||
}
|
||||
set_input_i32("pos_w", pos_data);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_SENSENOVA_U1:
|
||||
{
|
||||
const int n_patches_per_row = image_size_width / patch_size;
|
||||
std::vector<int> pos_data(n_pos);
|
||||
for (int i = 0; i < n_pos; i++) {
|
||||
pos_data[i] = i / n_patches_per_row;
|
||||
}
|
||||
set_input_i32("pos_y", pos_data);
|
||||
for (int i = 0; i < n_pos; i++) {
|
||||
pos_data[i] = i % n_patches_per_row;
|
||||
}
|
||||
set_input_i32("pos_x", pos_data);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_DEEPSEEK4V:
|
||||
{
|
||||
// set the 2D positions (mrope layout, only the first 2 channels are used)
|
||||
@@ -6411,6 +6450,8 @@ int clip_n_mmproj_embd(const struct clip_ctx * ctx) {
|
||||
case PROJECTOR_TYPE_JANUS_PRO:
|
||||
case PROJECTOR_TYPE_YOUTUVL:
|
||||
return ctx->model.mm_1_b->ne[0];
|
||||
case PROJECTOR_TYPE_SENSENOVA_U1:
|
||||
return ctx->model.hparams.projection_dim;
|
||||
case PROJECTOR_TYPE_QWEN3VL:
|
||||
// main path + deepstack paths
|
||||
return ctx->model.mm_1_b->ne[0] * (1 + ctx->model.n_deepstack_layers);
|
||||
|
||||
@@ -50,6 +50,11 @@ struct clip_graph_qwen3vl : clip_graph_qwen2vl {
|
||||
ggml_cgraph * build() override;
|
||||
};
|
||||
|
||||
struct clip_graph_sensenova_u1 : clip_graph {
|
||||
clip_graph_sensenova_u1(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
|
||||
ggml_cgraph * build() override;
|
||||
};
|
||||
|
||||
struct clip_graph_ling3vl : clip_graph_qwen3vl {
|
||||
clip_graph_ling3vl(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph_qwen3vl(ctx, img) {}
|
||||
ggml_cgraph * build() override;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#include "models.h"
|
||||
|
||||
ggml_cgraph * clip_graph_sensenova_u1::build() {
|
||||
GGML_ASSERT(model.patch_embeddings_0 != nullptr);
|
||||
GGML_ASSERT(model.patch_bias != nullptr);
|
||||
GGML_ASSERT(model.mm_0_w != nullptr);
|
||||
GGML_ASSERT(model.mm_0_b != nullptr);
|
||||
GGML_ASSERT(hparams.n_merge == 2);
|
||||
|
||||
ggml_tensor * pos_x = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_patches);
|
||||
ggml_set_name(pos_x, "pos_x");
|
||||
ggml_set_input(pos_x);
|
||||
|
||||
ggml_tensor * pos_y = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_patches);
|
||||
ggml_set_name(pos_y, "pos_y");
|
||||
ggml_set_input(pos_y);
|
||||
|
||||
ggml_tensor * inp = build_inp();
|
||||
inp = ggml_gelu(ctx0, inp);
|
||||
|
||||
// Apply adjacent-pair 2D RoPE to patch embeddings before downsampling.
|
||||
inp = ggml_reshape_4d(ctx0, inp, n_embd, 1, n_patches, n_batch);
|
||||
inp = build_rope_2d(ctx0, inp, pos_x, pos_y, hparams.rope_theta, false);
|
||||
inp = ggml_reshape_3d(ctx0, inp, n_embd, n_patches, n_batch);
|
||||
cb(inp, "patch_embd_rope", -1);
|
||||
|
||||
inp = ggml_reshape_4d(ctx0, inp, n_embd, n_patches_x, n_patches_y, n_batch);
|
||||
inp = ggml_cont(ctx0, ggml_permute(ctx0, inp, 2, 0, 1, 3));
|
||||
inp = ggml_conv_2d(ctx0, model.mm_0_w, inp,
|
||||
hparams.n_merge, hparams.n_merge, 0, 0, 1, 1);
|
||||
|
||||
const int n_output_patches = n_patches / (hparams.n_merge * hparams.n_merge);
|
||||
inp = ggml_cont(ctx0, ggml_permute(ctx0, inp, 1, 2, 0, 3));
|
||||
inp = ggml_reshape_3d(ctx0, inp, n_mmproj_embd, n_output_patches, n_batch);
|
||||
inp = ggml_add(ctx0, inp, model.mm_0_b);
|
||||
cb(inp, "proj_out", -1);
|
||||
|
||||
ggml_build_forward_expand(gf, inp);
|
||||
return gf;
|
||||
}
|
||||
+23
-1
@@ -225,6 +225,7 @@ enum mtmd_pos_type {
|
||||
MTMD_POS_TYPE_NORMAL, // number of positions equals to number of tokens
|
||||
MTMD_POS_TYPE_MROPE, // qwen-vl mrope style, each image takes max(t,h,w) position indexes
|
||||
MTMD_POS_TYPE_HUNYUANVL, // HunyuanVL mrope + BOI/EOI/newline layout with XD-RoPE dim-3
|
||||
MTMD_POS_TYPE_SENSENOVA_U1, // U1 uses zero-based H/W and one temporal position per image
|
||||
MTMD_POS_TYPE_CANVAS, // MiniCPM-V 4.7: overview + slices in one chunk, sharing one 2D canvas (see mtmd_image_tokens::canvas_tile_grid)
|
||||
MTMD_POS_TYPE_COUNT, // for validation
|
||||
};
|
||||
@@ -751,6 +752,12 @@ struct mtmd_context {
|
||||
img_end = "]<]end of image[>[";
|
||||
image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_SENSENOVA_U1:
|
||||
{
|
||||
img_beg = "<img>";
|
||||
img_end = "</img>";
|
||||
image_preproc = std::make_unique<mtmd_image_preprocessor_dyn_size>(ctx_v);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_MUSE_GLIMMER:
|
||||
{
|
||||
img_beg = "<|image_start|>";
|
||||
@@ -1615,10 +1622,15 @@ struct mtmd_tokenizer {
|
||||
image_tokens->ny = 1;
|
||||
}
|
||||
image_tokens->pos = ctx->pos_type;
|
||||
// U1 uses M-RoPE, but unlike Qwen its image H/W coordinates are
|
||||
// zero-based and the whole image occupies one temporal position.
|
||||
if (ctx->proj_type_v() == PROJECTOR_TYPE_SENSENOVA_U1) {
|
||||
image_tokens->pos = MTMD_POS_TYPE_SENSENOVA_U1;
|
||||
}
|
||||
// HunyuanVL wraps the image grid with BOI/EOI and adds one newline per row,
|
||||
// and uses XD-RoPE (dim-3 = image index). Override the position type so that
|
||||
// n_tokens() and mtmd_image_tokens_get_decoder_pos pick the HunyuanVL layout.
|
||||
if (ctx->proj_type_v() == PROJECTOR_TYPE_HUNYUANVL) {
|
||||
else if (ctx->proj_type_v() == PROJECTOR_TYPE_HUNYUANVL) {
|
||||
image_tokens->pos = MTMD_POS_TYPE_HUNYUANVL;
|
||||
image_tokens->image_idx = n_images_added;
|
||||
GGML_ASSERT(n_tokens == (size_t)image_tokens->n_tokens());
|
||||
@@ -2398,6 +2410,7 @@ bool mtmd_decode_use_non_causal(const mtmd_context * ctx, const mtmd_input_chunk
|
||||
}
|
||||
case PROJECTOR_TYPE_GEMMA4UV:
|
||||
case PROJECTOR_TYPE_GEMMA3:
|
||||
case PROJECTOR_TYPE_SENSENOVA_U1:
|
||||
case PROJECTOR_TYPE_DEEPSEEK4V:
|
||||
return true;
|
||||
default:
|
||||
@@ -2761,6 +2774,13 @@ mtmd_decoder_pos mtmd_image_tokens_get_decoder_pos(const mtmd_image_tokens * ima
|
||||
pos.y = pos_0 + i;
|
||||
pos.z = pos_0 + i;
|
||||
} break;
|
||||
case MTMD_POS_TYPE_SENSENOVA_U1:
|
||||
{
|
||||
pos.t = pos_0;
|
||||
pos.x = i % image_tokens->nx;
|
||||
pos.y = i / image_tokens->nx;
|
||||
pos.z = 0;
|
||||
} break;
|
||||
case MTMD_POS_TYPE_HUNYUANVL:
|
||||
{
|
||||
// HunyuanVL layout: [BOI] [row0 tokens + newline] ... [row(ny-1) tokens + newline] [EOI]
|
||||
@@ -2813,6 +2833,8 @@ llama_pos mtmd_image_tokens_get_n_pos(const mtmd_image_tokens * image_tokens) {
|
||||
return std::max(image_tokens->nx, image_tokens->ny);
|
||||
case MTMD_POS_TYPE_NORMAL:
|
||||
return image_tokens->n_tokens();
|
||||
case MTMD_POS_TYPE_SENSENOVA_U1:
|
||||
return 1;
|
||||
case MTMD_POS_TYPE_HUNYUANVL:
|
||||
// HunyuanVL: the sequential (dim-0) position advances by the full token count
|
||||
// (includes BOI/EOI and row newline tokens), not by max(nx, ny)
|
||||
|
||||
+4
-1
@@ -23,13 +23,15 @@
|
||||
interface Props {
|
||||
section: AgenticSection;
|
||||
attachments?: DatabaseMessageExtra[];
|
||||
conversationCwd?: string;
|
||||
open: boolean;
|
||||
isStreaming: boolean;
|
||||
isExecuting?: boolean;
|
||||
onToggle?: () => void;
|
||||
}
|
||||
|
||||
let { attachments, isExecuting, isStreaming, onToggle, open, section }: Props = $props();
|
||||
let { attachments, conversationCwd, isExecuting, isStreaming, onToggle, open, section }: Props =
|
||||
$props();
|
||||
|
||||
// Runs for every tool block on mount, before the body renders: the cheap
|
||||
// content prefilter and the tool-name allow-list come first so blobs from
|
||||
@@ -60,6 +62,7 @@
|
||||
{:else if section.toolName === BuiltInTool.SERVER_EXEC_SHELL_COMMAND}
|
||||
<ChatMessageToolCallBlockExecShellCommand
|
||||
{attachments}
|
||||
{conversationCwd}
|
||||
{isExecuting}
|
||||
{isStreaming}
|
||||
{onToggle}
|
||||
|
||||
+26
-10
@@ -2,10 +2,12 @@
|
||||
import { parseEditFileMeta, parseEditFileTitleMeta } from './parsers/edit-file';
|
||||
import ToolCallBlock from './ToolCallBlock.svelte';
|
||||
import { XCircle } from '@lucide/svelte';
|
||||
import { SyntaxHighlightedCode } from '$lib/components/app';
|
||||
import { MAX_HEIGHT_CODE_BLOCK, RESULT_STAT_SEPARATOR } from '$lib/constants';
|
||||
import { FileTypeText } from '$lib/enums';
|
||||
import { toolsStore } from '$lib/stores';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { abbreviateHome, computeLineDiff, prefixFor } from '$lib/utils';
|
||||
import { abbreviateHome, computeLineDiff, formatJsonPretty, prefixFor } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
section: AgenticSection;
|
||||
@@ -44,16 +46,16 @@
|
||||
</span>
|
||||
{/snippet}
|
||||
|
||||
{#snippet children(meta, _ctx)}
|
||||
{#if meta?.errorMessage}
|
||||
{#snippet children(_meta, ctx)}
|
||||
{#if editFileBody?.errorMessage}
|
||||
<div
|
||||
class="flex items-start gap-2 rounded bg-red-500/10 p-2 text-xs text-red-600 italic dark:text-red-400"
|
||||
>
|
||||
<XCircle class="mt-0.5 h-3 w-3 shrink-0" />
|
||||
|
||||
<span>{meta.errorMessage}</span>
|
||||
<span>{editFileBody.errorMessage}</span>
|
||||
</div>
|
||||
{:else if meta && editFileBody && editFileBody.edits.length > 0}
|
||||
{:else if editFileBody?.edits.length}
|
||||
{#each editDiffs as diffLines, ei (ei)}
|
||||
<div class={ei === 0 ? '' : 'mt-3'}>
|
||||
<div class="mb-1.5 text-xs text-muted-foreground/70 italic">
|
||||
@@ -79,14 +81,28 @@
|
||||
{/each}
|
||||
|
||||
<div class="mt-1.5 text-xs text-muted-foreground/70 italic">
|
||||
{#if meta.resultMessage}
|
||||
{meta.resultMessage}{meta.editsApplied != null ? RESULT_STAT_SEPARATOR : ''}{/if}
|
||||
{#if editFileBody.resultMessage}
|
||||
{editFileBody.resultMessage}{editFileBody.editsApplied != null
|
||||
? RESULT_STAT_SEPARATOR
|
||||
: ''}{/if}
|
||||
|
||||
{#if meta.editsApplied != null}
|
||||
<span class="font-mono">{meta.editsApplied}</span>
|
||||
{meta.editsApplied === 1 ? 'edit' : 'edits'} applied
|
||||
{#if editFileBody.editsApplied != null}
|
||||
<span class="font-mono">{editFileBody.editsApplied}</span>
|
||||
{editFileBody.editsApplied === 1 ? 'edit' : 'edits'} applied
|
||||
{/if}
|
||||
</div>
|
||||
{:else if section.toolArgs}
|
||||
<!-- no usable edits: not streamed yet, or a same-named tool with other arg names -->
|
||||
<SyntaxHighlightedCode
|
||||
code={formatJsonPretty(section.toolArgs)}
|
||||
language={FileTypeText.JSON}
|
||||
maxHeight={MAX_HEIGHT_CODE_BLOCK}
|
||||
streaming={ctx.isCodeStreaming}
|
||||
/>
|
||||
{:else if ctx.isStreaming}
|
||||
<div class="rounded bg-muted/20 p-2 text-xs text-muted-foreground/70 italic">
|
||||
Receiving arguments...
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded bg-muted/20 p-2 text-xs text-muted-foreground/70 italic">No edits</div>
|
||||
{/if}
|
||||
|
||||
+64
-7
@@ -11,7 +11,7 @@
|
||||
import { AlertTriangle, Check, Loader2, XCircle } from '@lucide/svelte';
|
||||
import { CollapsibleTerminalBlock } from '$lib/components/app';
|
||||
import { SETTINGS_KEYS, TOOL_RUNTIME_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants';
|
||||
import { AttachmentType } from '$lib/enums';
|
||||
import { AgenticSectionType, AttachmentType } from '$lib/enums';
|
||||
import { settingsStore, toolsStore } from '$lib/stores';
|
||||
import type { AgenticSection, DatabaseMessageExtra, ToolResultLine } from '$lib/types';
|
||||
import {
|
||||
@@ -32,17 +32,40 @@
|
||||
* tool call. Drives max-height + auto-scroll while true; releases
|
||||
* them when the loop reports this call as done. */
|
||||
isExecuting?: boolean;
|
||||
/** Working directory of the conversation this call belongs to; stands
|
||||
* in for `section.toolCwd` until the result message persists the
|
||||
* real one. */
|
||||
conversationCwd?: string;
|
||||
attachments?: DatabaseMessageExtra[];
|
||||
onToggle?: () => void;
|
||||
}
|
||||
|
||||
let { attachments, isExecuting = false, isStreaming, onToggle, open, section }: Props = $props();
|
||||
let {
|
||||
attachments,
|
||||
conversationCwd,
|
||||
isExecuting = false,
|
||||
isStreaming,
|
||||
onToggle,
|
||||
open,
|
||||
section
|
||||
}: Props = $props();
|
||||
|
||||
// `isLive` covers all in-flight phases: pre-chunk spinner and
|
||||
// streaming itself. Frozen output (tool done while agent continues)
|
||||
// is not live.
|
||||
const isLive = $derived(isExecuting);
|
||||
|
||||
const isPreResult = $derived(
|
||||
section.type === AgenticSectionType.TOOL_CALL_PENDING ||
|
||||
section.type === AgenticSectionType.TOOL_CALL_STREAMING
|
||||
);
|
||||
|
||||
// The model is still typing the call while the args JSON doesn't parse;
|
||||
// the title keeps a terminal cursor until it does.
|
||||
const commandStreaming = $derived(
|
||||
isStreaming && isPreResult && !isParsableJson(section.toolArgs)
|
||||
);
|
||||
|
||||
const execShellMeta = $derived(parseExecShellCommandMeta(section));
|
||||
const execShellError = $derived(parseExecShellCommandError(section.toolResult));
|
||||
const execShellExitStatus: ExecShellExitStatus | undefined = $derived(
|
||||
@@ -76,9 +99,11 @@
|
||||
|
||||
// The working directory the command ran with, persisted per call on the
|
||||
// tool result message (it travels via the x-tool-cwd header, not the tool
|
||||
// args). Reading it from the section keeps it accurate even if the
|
||||
// conversation cwd changes later.
|
||||
const cwd = $derived(section.toolCwd);
|
||||
// args). Until that message exists, the conversation cwd is the best known
|
||||
// value - it is what the agentic loop passes when executing the call.
|
||||
const cwd = $derived(
|
||||
section.toolCwd ?? (isStreaming && isPreResult ? conversationCwd : undefined)
|
||||
);
|
||||
const home = $derived(toolsStore.serverHome);
|
||||
const wdDisplay = $derived(abbreviateHome(cwd ?? '', home));
|
||||
|
||||
@@ -112,6 +137,18 @@
|
||||
);
|
||||
}
|
||||
|
||||
function isParsableJson(value: string | undefined): boolean {
|
||||
if (!value) return false;
|
||||
|
||||
try {
|
||||
JSON.parse(value);
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottomOnFrame() {
|
||||
if (pendingFrame !== null || !scrollEl || userScrolledUp) return;
|
||||
|
||||
@@ -185,6 +222,10 @@
|
||||
{:else}
|
||||
<span class="font-mono">{execShellMeta?.command}</span>
|
||||
{/if}
|
||||
|
||||
{#if commandStreaming}
|
||||
<span class="exec-cursor"></span>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<ToolCallBlock
|
||||
@@ -202,10 +243,10 @@
|
||||
{/snippet}
|
||||
|
||||
{#snippet children(_meta, ctx)}
|
||||
{#if ctx.isPending}
|
||||
{#if (ctx.isPending && !commandStreaming) || (isLive && !section.toolResult)}
|
||||
<div class="flex items-start gap-2 text-xs text-muted-foreground/70">
|
||||
<Loader2 class="h-3 w-3 animate-spin" />
|
||||
Running...
|
||||
Executing command...
|
||||
</div>
|
||||
{:else if execShellError}
|
||||
<div class="flex items-start gap-2 text-xs text-red-600 italic dark:text-red-400">
|
||||
@@ -277,6 +318,22 @@
|
||||
margin-right: var(--exec-wd-margin);
|
||||
}
|
||||
|
||||
.exec-cursor {
|
||||
display: inline-block;
|
||||
width: 0.5em;
|
||||
height: 1.05em;
|
||||
margin-left: 0.15rem;
|
||||
vertical-align: text-bottom;
|
||||
background: var(--foreground);
|
||||
animation: exec-cursor-blink 1.1s steps(1) infinite;
|
||||
}
|
||||
|
||||
@keyframes exec-cursor-blink {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.terminal-output {
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
+28
-11
@@ -4,9 +4,10 @@
|
||||
import { XCircle } from '@lucide/svelte';
|
||||
import { SyntaxHighlightedCode } from '$lib/components/app';
|
||||
import { MAX_HEIGHT_CODE_BLOCK, RESULT_STAT_SEPARATOR } from '$lib/constants';
|
||||
import { FileTypeText } from '$lib/enums';
|
||||
import { toolsStore } from '$lib/stores';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { abbreviateHome } from '$lib/utils';
|
||||
import { abbreviateHome, formatJsonPretty } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
section: AgenticSection;
|
||||
@@ -42,32 +43,48 @@
|
||||
</span>
|
||||
{/snippet}
|
||||
|
||||
{#snippet children(meta, ctx)}
|
||||
{#if meta?.errorMessage}
|
||||
{#snippet children(_meta, ctx)}
|
||||
{#if writeFileBody?.errorMessage}
|
||||
<div
|
||||
class="flex items-start gap-2 rounded bg-red-500/10 p-2 text-xs text-red-600 italic dark:text-red-400"
|
||||
>
|
||||
<XCircle class="mt-0.5 h-3 w-3 shrink-0" />
|
||||
|
||||
<span>{meta.errorMessage}</span>
|
||||
<span>{writeFileBody.errorMessage}</span>
|
||||
</div>
|
||||
{:else if meta}
|
||||
{:else if writeFileBody?.content !== undefined}
|
||||
<SyntaxHighlightedCode
|
||||
code={writeFileBody?.content ?? ''}
|
||||
language={meta.language}
|
||||
code={writeFileBody.content}
|
||||
language={writeFileBody.language}
|
||||
maxHeight={MAX_HEIGHT_CODE_BLOCK}
|
||||
streaming={ctx.isCodeStreaming}
|
||||
/>
|
||||
|
||||
<div class="mt-1.5 text-xs text-muted-foreground/70 italic">
|
||||
{#if meta.resultMessage}
|
||||
{meta.resultMessage}{meta.bytesWritten != null ? RESULT_STAT_SEPARATOR : ''}{/if}
|
||||
{#if writeFileBody.resultMessage}
|
||||
{writeFileBody.resultMessage}{writeFileBody.bytesWritten != null
|
||||
? RESULT_STAT_SEPARATOR
|
||||
: ''}{/if}
|
||||
|
||||
{#if meta.bytesWritten != null}
|
||||
<span class="font-mono">{meta.bytesWritten}</span>
|
||||
{#if writeFileBody.bytesWritten != null}
|
||||
<span class="font-mono">{writeFileBody.bytesWritten}</span>
|
||||
bytes
|
||||
{/if}
|
||||
</div>
|
||||
{:else if section.toolArgs}
|
||||
<!-- no content arg: not streamed yet, or a same-named tool with other arg names -->
|
||||
<SyntaxHighlightedCode
|
||||
code={formatJsonPretty(section.toolArgs)}
|
||||
language={FileTypeText.JSON}
|
||||
maxHeight={MAX_HEIGHT_CODE_BLOCK}
|
||||
streaming={ctx.isCodeStreaming}
|
||||
/>
|
||||
{:else if ctx.isStreaming}
|
||||
<div class="rounded bg-muted/20 p-2 text-xs text-muted-foreground/70 italic">
|
||||
Receiving arguments...
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded bg-muted/20 p-2 text-xs text-muted-foreground/70 italic">No content</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ToolCallBlock>
|
||||
|
||||
+35
-3
@@ -1,10 +1,14 @@
|
||||
// Helpers shared by the per-tool meta parsers under
|
||||
// `src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/`.
|
||||
// Each tool needs the same first three steps (tool-name check,
|
||||
// args-present check, JSON parse) - keeping them here lets each parser
|
||||
// stay focused on its own format quirks.
|
||||
// args-present check, JSON parse), plus the same path aliases - keeping
|
||||
// them here lets each parser stay focused on its own format quirks.
|
||||
|
||||
import { TOOL_ARG_STRING_FIELD_PATTERN_TEMPLATE } from '$lib/constants';
|
||||
import {
|
||||
FILE_PATH_SEPARATOR_REGEX,
|
||||
TOOL_ARG_PATH_KEYS,
|
||||
TOOL_ARG_STRING_FIELD_PATTERN_TEMPLATE
|
||||
} from '$lib/constants';
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import type { AgenticSection } from '$lib/types/agentic';
|
||||
import { parsePartialJsonArgs } from '$lib/utils/parse-partial-json-args';
|
||||
@@ -68,6 +72,34 @@ export function extractToolArgString(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* File name shown in a block title, from a tool-call path. Undefined when
|
||||
* the call carries no path; the title then shows the tool label alone.
|
||||
*/
|
||||
export function fileNameFromPath(filePath: string | undefined): string | undefined {
|
||||
if (!filePath) return undefined;
|
||||
|
||||
return filePath.split(FILE_PATH_SEPARATOR_REGEX).pop() || filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the path argument out of an already parsed args object, checking
|
||||
* the aliases in `TOOL_ARG_PATH_KEYS`. Returns undefined when the call
|
||||
* carries no usable path - the path is display-only, so callers must
|
||||
* still render whatever else the call streamed.
|
||||
*/
|
||||
export function extractToolArgPath(args?: Record<string, unknown> | null): string | undefined {
|
||||
if (!args) return undefined;
|
||||
|
||||
for (const key of TOOL_ARG_PATH_KEYS) {
|
||||
const value = args[key];
|
||||
|
||||
if (typeof value === 'string' && value) return value;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a section's toolArgs against an expected tool name. Returns
|
||||
* `null` when:
|
||||
|
||||
+19
-19
@@ -1,10 +1,16 @@
|
||||
// Meta parser for `edit_file` tool calls. Reads the file path and the
|
||||
// array of edits from the streamed args (partial JSON for incremental
|
||||
// rendering), plus the result blob for `result` / `edits_applied` /
|
||||
// `error` fields.
|
||||
// `error` fields. The path is display-only (block title), so a call
|
||||
// that has not streamed one still shows its diffs.
|
||||
|
||||
import { extractToolArgString, parseToolArgs } from './_shared';
|
||||
import { FILE_PATH_SEPARATOR_REGEX, TOOL_ARG_PATH_KEYS } from '$lib/constants';
|
||||
import {
|
||||
extractToolArgPath,
|
||||
extractToolArgString,
|
||||
fileNameFromPath,
|
||||
parseToolArgs
|
||||
} from './_shared';
|
||||
import { TOOL_ARG_PATH_KEYS } from '$lib/constants';
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import type { AgenticSection, EditFileEdit, EditFileMeta, EditFileTitleMeta } from '$lib/types';
|
||||
import { tryParseToolResultObject } from '$lib/utils';
|
||||
@@ -14,11 +20,8 @@ export function parseEditFileMeta(section: AgenticSection): EditFileMeta | null
|
||||
|
||||
if (!args) return null;
|
||||
|
||||
const rawPath = args.path ?? args.file_path ?? args.filePath;
|
||||
|
||||
if (typeof rawPath !== 'string' || !rawPath) return null;
|
||||
|
||||
const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
|
||||
const filePath = extractToolArgPath(args);
|
||||
const fileName = fileNameFromPath(filePath);
|
||||
// Filter the streamed edits array strictly: each entry must be an
|
||||
// object with a non-empty `old_text`. Edits without an old_text
|
||||
// would diff against empty and render as a full re-write.
|
||||
@@ -61,7 +64,7 @@ export function parseEditFileMeta(section: AgenticSection): EditFileMeta | null
|
||||
editsApplied,
|
||||
errorMessage,
|
||||
fileName,
|
||||
filePath: rawPath,
|
||||
filePath,
|
||||
resultMessage
|
||||
};
|
||||
}
|
||||
@@ -75,18 +78,15 @@ export function parseEditFileMeta(section: AgenticSection): EditFileMeta | null
|
||||
export function parseEditFileTitleMeta(section: AgenticSection): EditFileTitleMeta | null {
|
||||
if (section.toolName !== BuiltInTool.SERVER_EDIT_FILE || !section.toolArgs) return null;
|
||||
|
||||
let rawPath: string | undefined = extractToolArgString(section.toolArgs, TOOL_ARG_PATH_KEYS);
|
||||
let filePath = extractToolArgString(section.toolArgs, TOOL_ARG_PATH_KEYS);
|
||||
|
||||
if (!rawPath) {
|
||||
const args = parseToolArgs(BuiltInTool.SERVER_EDIT_FILE, section, { partial: true });
|
||||
const fallbackPath = args?.path ?? args?.file_path ?? args?.filePath;
|
||||
|
||||
if (typeof fallbackPath === 'string' && fallbackPath) rawPath = fallbackPath;
|
||||
if (!filePath) {
|
||||
filePath = extractToolArgPath(
|
||||
parseToolArgs(BuiltInTool.SERVER_EDIT_FILE, section, { partial: true })
|
||||
);
|
||||
}
|
||||
|
||||
if (!rawPath) return null;
|
||||
|
||||
const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
|
||||
const fileName = fileNameFromPath(filePath);
|
||||
const resultObj = tryParseToolResultObject(section.toolResult);
|
||||
|
||||
let resultMessage: string | undefined;
|
||||
@@ -105,5 +105,5 @@ export function parseEditFileTitleMeta(section: AgenticSection): EditFileTitleMe
|
||||
}
|
||||
}
|
||||
|
||||
return { editsApplied, errorMessage, fileName, filePath: rawPath, resultMessage };
|
||||
return { editsApplied, errorMessage, fileName, filePath, resultMessage };
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
// Meta parser for `exec_shell_command` tool calls. Surfaces the
|
||||
// command text from args `command` / `cmd` / `shell_command` aliases.
|
||||
// Args are parsed partially so the title can render the command while
|
||||
// it is still streaming.
|
||||
// The exit-status and error parsing live in their own utilities
|
||||
// (`parse-exec-shell-status.ts` / `parse-exec-shell-error.ts`) - this
|
||||
// file only deals with what's strictly about *calling* the tool, since
|
||||
@@ -14,7 +16,7 @@ export type ExecShellCommandMeta = {
|
||||
};
|
||||
|
||||
export function parseExecShellCommandMeta(section: AgenticSection): ExecShellCommandMeta | null {
|
||||
const args = parseToolArgs(BuiltInTool.SERVER_EXEC_SHELL_COMMAND, section);
|
||||
const args = parseToolArgs(BuiltInTool.SERVER_EXEC_SHELL_COMMAND, section, { partial: true });
|
||||
|
||||
if (!args) return null;
|
||||
|
||||
|
||||
+33
-30
@@ -1,30 +1,40 @@
|
||||
// Meta parser for `write_file` tool calls. Reads the path/content from
|
||||
// the streamed args (partial JSON so we can render before the call
|
||||
// finishes) and surfaces `bytes`, `result`, and `error` from the
|
||||
// result blob.
|
||||
// result blob. The path is display-only (title + highlight language),
|
||||
// so a call that has not streamed one still shows its content.
|
||||
|
||||
import { extractToolArgString, parseToolArgs } from './_shared';
|
||||
import { CODE_BLOCK, FILE_PATH_SEPARATOR_REGEX, TOOL_ARG_PATH_KEYS } from '$lib/constants';
|
||||
import {
|
||||
extractToolArgPath,
|
||||
extractToolArgString,
|
||||
fileNameFromPath,
|
||||
parseToolArgs
|
||||
} from './_shared';
|
||||
import { CODE_BLOCK, TOOL_ARG_PATH_KEYS } from '$lib/constants';
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import type { AgenticSection, WriteFileMeta, WriteFileTitleMeta } from '$lib/types';
|
||||
import { getFileTypeByExtension, tryParseToolResultObject } from '$lib/utils';
|
||||
|
||||
// Plain text when the call carries no path, or one with no known extension.
|
||||
function languageForPath(filePath: string | undefined): string {
|
||||
if (!filePath) return CODE_BLOCK.DEFAULT_LANGUAGE;
|
||||
|
||||
return (
|
||||
getFileTypeByExtension(filePath)?.replace(CODE_BLOCK.TEXT_LANGUAGE_PREFIX_REGEX, '') ??
|
||||
CODE_BLOCK.DEFAULT_LANGUAGE
|
||||
);
|
||||
}
|
||||
|
||||
export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | null {
|
||||
const args = parseToolArgs(BuiltInTool.SERVER_WRITE_FILE, section, { partial: true });
|
||||
|
||||
if (!args) return null;
|
||||
|
||||
// Tool contracts drifted over time: some models emit `path`,
|
||||
// others `file_path` / `filePath`. Accept all three.
|
||||
const rawPath = args.path ?? args.file_path ?? args.filePath;
|
||||
|
||||
if (typeof rawPath !== 'string' || !rawPath) return null;
|
||||
|
||||
const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
|
||||
const content = typeof args.content === 'string' ? args.content : '';
|
||||
const language =
|
||||
getFileTypeByExtension(rawPath)?.replace(CODE_BLOCK.TEXT_LANGUAGE_PREFIX_REGEX, '') ??
|
||||
CODE_BLOCK.DEFAULT_LANGUAGE;
|
||||
const filePath = extractToolArgPath(args);
|
||||
const fileName = fileNameFromPath(filePath);
|
||||
// undefined, not '', so an intentionally empty write still renders
|
||||
const content = typeof args.content === 'string' ? args.content : undefined;
|
||||
const language = languageForPath(filePath);
|
||||
const resultObj = tryParseToolResultObject(section.toolResult);
|
||||
const bytesWritten =
|
||||
resultObj && Number.isFinite(Number(resultObj.bytes)) ? Number(resultObj.bytes) : undefined;
|
||||
@@ -36,7 +46,7 @@ export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | nul
|
||||
content,
|
||||
errorMessage,
|
||||
fileName,
|
||||
filePath: rawPath,
|
||||
filePath,
|
||||
language,
|
||||
resultMessage
|
||||
};
|
||||
@@ -51,21 +61,14 @@ export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | nul
|
||||
export function parseWriteFileTitleMeta(section: AgenticSection): WriteFileTitleMeta | null {
|
||||
if (section.toolName !== BuiltInTool.SERVER_WRITE_FILE || !section.toolArgs) return null;
|
||||
|
||||
let rawPath: string | undefined = extractToolArgString(section.toolArgs, TOOL_ARG_PATH_KEYS);
|
||||
let filePath = extractToolArgString(section.toolArgs, TOOL_ARG_PATH_KEYS);
|
||||
|
||||
if (!rawPath) {
|
||||
const args = parseToolArgs(BuiltInTool.SERVER_WRITE_FILE, section, { partial: true });
|
||||
const fallbackPath = args?.path ?? args?.file_path ?? args?.filePath;
|
||||
|
||||
if (typeof fallbackPath === 'string' && fallbackPath) rawPath = fallbackPath;
|
||||
if (!filePath) {
|
||||
filePath = extractToolArgPath(
|
||||
parseToolArgs(BuiltInTool.SERVER_WRITE_FILE, section, { partial: true })
|
||||
);
|
||||
}
|
||||
|
||||
if (!rawPath) return null;
|
||||
|
||||
const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
|
||||
const language =
|
||||
getFileTypeByExtension(rawPath)?.replace(CODE_BLOCK.TEXT_LANGUAGE_PREFIX_REGEX, '') ??
|
||||
CODE_BLOCK.DEFAULT_LANGUAGE;
|
||||
const resultObj = tryParseToolResultObject(section.toolResult);
|
||||
const bytesWritten =
|
||||
resultObj && Number.isFinite(Number(resultObj.bytes)) ? Number(resultObj.bytes) : undefined;
|
||||
@@ -75,9 +78,9 @@ export function parseWriteFileTitleMeta(section: AgenticSection): WriteFileTitle
|
||||
return {
|
||||
bytesWritten,
|
||||
errorMessage,
|
||||
fileName,
|
||||
filePath: rawPath,
|
||||
language,
|
||||
fileName: fileNameFromPath(filePath),
|
||||
filePath,
|
||||
language: languageForPath(filePath),
|
||||
resultMessage
|
||||
};
|
||||
}
|
||||
|
||||
+18
-19
@@ -8,14 +8,14 @@
|
||||
MarkdownContent
|
||||
} from '$lib/components/app';
|
||||
import { AgenticSectionType, ChatMessageStatsView, ToolPermissionDecision } from '$lib/enums';
|
||||
import { agenticStore, settingsStore } from '$lib/stores';
|
||||
import { agenticStore, conversationsStore, settingsStore } from '$lib/stores';
|
||||
import type {
|
||||
AgenticSection,
|
||||
ChatMessageAgenticTimings,
|
||||
ChatMessageAgenticTurnStats,
|
||||
DatabaseMessage
|
||||
} from '$lib/types';
|
||||
import { deriveAgenticSections } from '$lib/utils';
|
||||
import { deriveAgenticSections, isSectionExpandedByDefault } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
message: DatabaseMessage;
|
||||
@@ -34,6 +34,7 @@
|
||||
let expandedStates: Record<number, boolean> = $state({});
|
||||
|
||||
const showThoughtInProgress = $derived(Boolean(settingsStore.config.showThoughtInProgress));
|
||||
const showToolCallInProgress = $derived(Boolean(settingsStore.config.showToolCallInProgress));
|
||||
const alwaysShowToolCallContent = $derived(
|
||||
Boolean(settingsStore.config.alwaysShowToolCallContent)
|
||||
);
|
||||
@@ -95,6 +96,14 @@
|
||||
isStreaming ? agenticStore.getExecutingToolCallId(message.convId) : null
|
||||
);
|
||||
|
||||
// Cwd the agentic loop passes to tool calls in this conversation; the exec
|
||||
// block shows it until the result message persists the real one.
|
||||
const conversationCwd = $derived(
|
||||
conversationsStore.activeConversation?.id === message.convId
|
||||
? conversationsStore.activeConversation?.cwd
|
||||
: undefined
|
||||
);
|
||||
|
||||
type TurnGroup = {
|
||||
sections: AgenticSection[];
|
||||
flatIndices: number[];
|
||||
@@ -132,28 +141,17 @@
|
||||
return groups;
|
||||
});
|
||||
|
||||
function getDefaultExpanded(section: AgenticSection): boolean {
|
||||
if (
|
||||
section.type === AgenticSectionType.TOOL_CALL ||
|
||||
section.type === AgenticSectionType.TOOL_CALL_PENDING ||
|
||||
section.type === AgenticSectionType.TOOL_CALL_STREAMING
|
||||
) {
|
||||
return alwaysShowToolCallContent;
|
||||
}
|
||||
|
||||
if (section.type === AgenticSectionType.REASONING_PENDING) {
|
||||
return showThoughtInProgress;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isExpanded(index: number, section: AgenticSection): boolean {
|
||||
if (expandedStates[index] !== undefined) {
|
||||
return expandedStates[index];
|
||||
}
|
||||
|
||||
return getDefaultExpanded(section);
|
||||
return isSectionExpandedByDefault(section, {
|
||||
alwaysShowToolCallContent,
|
||||
executingToolCallId: currentlyExecutingToolCallId,
|
||||
showThoughtInProgress,
|
||||
showToolCallInProgress
|
||||
});
|
||||
}
|
||||
|
||||
function toggleExpanded(index: number, section: AgenticSection) {
|
||||
@@ -190,6 +188,7 @@
|
||||
{:else if section.type === AgenticSectionType.TOOL_CALL || section.type === AgenticSectionType.TOOL_CALL_PENDING || section.type === AgenticSectionType.TOOL_CALL_STREAMING}
|
||||
<ChatMessageToolCallBlock
|
||||
attachments={section.toolResultExtras}
|
||||
{conversationCwd}
|
||||
isExecuting={section.toolCallId !== undefined &&
|
||||
section.toolCallId === currentlyExecutingToolCallId}
|
||||
{isStreaming}
|
||||
|
||||
@@ -98,9 +98,17 @@
|
||||
<!-- See CollapsibleContentBlock: bits-ui keeps closed content mounted, which
|
||||
makes a collapsed tool result re-render on every streamed token. -->
|
||||
{#if open}
|
||||
<div class="p-3 pt-1">
|
||||
<div class="terminal-body p-3 pt-1">
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
|
||||
<style>
|
||||
/* A body that renders no element (exec block while its args stream)
|
||||
must not leave its padding as a gap under the title. */
|
||||
.terminal-body:not(:has(*)) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -65,6 +65,7 @@ export const SETTINGS_KEYS = {
|
||||
SHOW_RAW_OUTPUT_SWITCH: 'showRawOutputSwitch',
|
||||
SHOW_SYSTEM_MESSAGE: 'showSystemMessage',
|
||||
SHOW_THOUGHT_IN_PROGRESS: 'showThoughtInProgress',
|
||||
SHOW_TOOL_CALL_IN_PROGRESS: 'showToolCallInProgress',
|
||||
SYMBOLIC_MATH_ENABLED: 'symbolicMathEnabled',
|
||||
SYSTEM_MESSAGE: 'systemMessage',
|
||||
// Sampling
|
||||
|
||||
@@ -227,9 +227,16 @@ export const SETTINGS_REGISTRY: SettingsSectionEntry[] = [
|
||||
label: 'Show thought in progress',
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
},
|
||||
{
|
||||
defaultValue: true,
|
||||
help: 'Expand tool call details while the call is being generated and executed, and collapse them again once its result is in. Mirrors "Show thought in progress". Tool calls you expand or collapse by hand keep your choice.',
|
||||
key: SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS,
|
||||
label: 'Show tool call in progress',
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
},
|
||||
{
|
||||
defaultValue: false,
|
||||
help: 'Automatically expand tool call details while executing and keep them expanded after completion.',
|
||||
help: 'Keep tool call details expanded after completion. Takes precedence over "Show tool call in progress", which only covers the in-flight phases.',
|
||||
key: SETTINGS_KEYS.ALWAYS_SHOW_TOOL_CALL_CONTENT,
|
||||
label: 'Always show tool call content',
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
|
||||
@@ -578,7 +578,6 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
const displayNameSource =
|
||||
details?.name && details.name.trim().length > 0 ? details.name : item.id;
|
||||
const modelId = details?.model || item.id;
|
||||
const repo = repoOf(modelId);
|
||||
|
||||
return {
|
||||
aliases: item.aliases ?? [],
|
||||
@@ -594,7 +593,7 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
: undefined),
|
||||
description: details?.description,
|
||||
details: details?.details,
|
||||
draftSidecars: this._status.getDraftSidecars(repo),
|
||||
draftSidecars: this._status.getDraftSidecars(modelId),
|
||||
id: item.id,
|
||||
meta: item.meta ?? null,
|
||||
modalities: this.props.buildArchitectureModalities(item.architecture),
|
||||
|
||||
@@ -97,10 +97,11 @@ export class ModelStatusManager {
|
||||
/** Sidecar kinds each in-flight or paused download pulls with its model. */
|
||||
private downloadSidecars = new SvelteMap<string, ModelSidecar[]>();
|
||||
/**
|
||||
* Draft sidecars the registered models pull with them, keyed by the repo of the model
|
||||
* that pulls them: the router records them in the model's `--model-draft` args.
|
||||
* Draft sidecars the registered models pull with them, keyed by the model that pulls
|
||||
* them: the router records them in the model's `--model-draft` args, and each quant
|
||||
* of a repo can pull a different draft.
|
||||
*/
|
||||
private draftSidecarsByRepo = $derived.by(() => {
|
||||
private draftSidecarsByModel = $derived.by(() => {
|
||||
const result = new SvelteMap<string, ModelSidecarBadge[]>();
|
||||
|
||||
for (const m of this.host.routerModels) {
|
||||
@@ -108,8 +109,6 @@ export class ModelStatusManager {
|
||||
|
||||
if (!args) continue;
|
||||
|
||||
const baseRepo = repoOf(m.id);
|
||||
|
||||
for (let i = 0; i < args.length - 1; i++) {
|
||||
if (args[i] !== CLI_FLAGS.MODEL_DRAFT && args[i] !== CLI_FLAGS.MODEL_DRAFT_SHORT) {
|
||||
continue;
|
||||
@@ -128,7 +127,7 @@ export class ModelStatusManager {
|
||||
quant: meta.quant,
|
||||
repo: parsed.repo
|
||||
};
|
||||
const kinds = result.get(baseRepo);
|
||||
const kinds = result.get(m.id);
|
||||
|
||||
if (kinds) {
|
||||
if (
|
||||
@@ -139,7 +138,7 @@ export class ModelStatusManager {
|
||||
kinds.push(badge);
|
||||
}
|
||||
} else {
|
||||
result.set(baseRepo, [badge]);
|
||||
result.set(m.id, [badge]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,17 +403,19 @@ export class ModelStatusManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Draft sidecars a repo pulls with its model, e.g. `mtp` at `Q8_0`: the
|
||||
* router records loaded models' sidecars in their `--model-draft` args, and
|
||||
* lists the sidecars each cached model has on disk.
|
||||
* Draft sidecars a model pulls with it, e.g. `mtp` at `Q8_0`: the router
|
||||
* records each model's sidecars in its `--model-draft` args, and lists the
|
||||
* sidecars each cached repo has on disk.
|
||||
*/
|
||||
getDraftSidecars(repoId: string): ModelSidecarBadge[] {
|
||||
// one badge per kind: the args know the quant, the listing carries kinds only,
|
||||
// and a draft still coming down is pending rather than on disk
|
||||
const merged = [...(this.draftSidecarsByRepo.get(repoId) ?? [])];
|
||||
getDraftSidecars(modelId: string): ModelSidecarBadge[] {
|
||||
const repo = repoOf(modelId);
|
||||
// one badge per kind: the args know the quant of this model's draft, the
|
||||
// listing carries repo-wide kinds only, and a draft still coming down is
|
||||
// pending rather than on disk
|
||||
const merged = [...(this.draftSidecarsByModel.get(modelId) ?? [])];
|
||||
|
||||
for (const source of [this.listedSidecarsByRepo, this.pendingSidecarsByRepo]) {
|
||||
for (const badge of source.get(repoId) ?? []) {
|
||||
for (const badge of source.get(repo) ?? []) {
|
||||
if (!merged.some((b) => b.kind === badge.kind && b.repo === badge.repo)) {
|
||||
merged.push(badge);
|
||||
}
|
||||
|
||||
Vendored
+12
-9
@@ -33,10 +33,12 @@ export interface ToolGroup {
|
||||
}
|
||||
|
||||
export interface WriteFileMeta {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
/** Undefined until the call streams a path; content renders without it. */
|
||||
fileName?: string;
|
||||
filePath?: string;
|
||||
language: string;
|
||||
content: string;
|
||||
/** Undefined until the args carry a content string; an empty write is ''. */
|
||||
content?: string;
|
||||
bytesWritten?: number;
|
||||
resultMessage?: string;
|
||||
errorMessage?: string;
|
||||
@@ -46,8 +48,8 @@ export interface WriteFileMeta {
|
||||
* ( with the embedded file content ) stays body-only so collapsed blocks
|
||||
* never parse the content blob. */
|
||||
export interface WriteFileTitleMeta {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
fileName?: string;
|
||||
filePath?: string;
|
||||
language: string;
|
||||
bytesWritten?: number;
|
||||
resultMessage?: string;
|
||||
@@ -60,8 +62,9 @@ export interface EditFileEdit {
|
||||
}
|
||||
|
||||
export interface EditFileMeta {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
/** Undefined until the call streams a path; the edits render without it. */
|
||||
fileName?: string;
|
||||
filePath?: string;
|
||||
edits: EditFileEdit[];
|
||||
resultMessage?: string;
|
||||
editsApplied?: number;
|
||||
@@ -72,8 +75,8 @@ export interface EditFileMeta {
|
||||
* ( with the embedded edit strings ) stays body-only so collapsed blocks
|
||||
* never parse the args blob. */
|
||||
export interface EditFileTitleMeta {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
fileName?: string;
|
||||
filePath?: string;
|
||||
resultMessage?: string;
|
||||
editsApplied?: number;
|
||||
errorMessage?: string;
|
||||
|
||||
@@ -540,6 +540,49 @@ export function hasAgenticContent(
|
||||
return toolMessages.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default expansion of a section, used until the user toggles it by hand.
|
||||
*
|
||||
* @param section - The section to judge
|
||||
* @param flags - Display settings plus the tool call the agentic loop is running now.
|
||||
* A running call outranks its section type: `exec_shell_command` persists an empty
|
||||
* result before reading output, so a live call can already be TOOL_CALL.
|
||||
*/
|
||||
export function isSectionExpandedByDefault(
|
||||
section: AgenticSection,
|
||||
{
|
||||
alwaysShowToolCallContent,
|
||||
executingToolCallId,
|
||||
showThoughtInProgress,
|
||||
showToolCallInProgress
|
||||
}: {
|
||||
alwaysShowToolCallContent: boolean;
|
||||
executingToolCallId?: string | null;
|
||||
showThoughtInProgress: boolean;
|
||||
showToolCallInProgress: boolean;
|
||||
}
|
||||
): boolean {
|
||||
const isRunning = section.toolCallId !== undefined && section.toolCallId === executingToolCallId;
|
||||
|
||||
if (
|
||||
isRunning ||
|
||||
section.type === AgenticSectionType.TOOL_CALL_PENDING ||
|
||||
section.type === AgenticSectionType.TOOL_CALL_STREAMING
|
||||
) {
|
||||
return showToolCallInProgress || alwaysShowToolCallContent;
|
||||
}
|
||||
|
||||
if (section.type === AgenticSectionType.TOOL_CALL) {
|
||||
return alwaysShowToolCallContent;
|
||||
}
|
||||
|
||||
if (section.type === AgenticSectionType.REASONING_PENDING) {
|
||||
return showThoughtInProgress;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide how a Continue click on messages[idx] should resume generation.
|
||||
* Pure function over the persisted history snapshot.
|
||||
|
||||
@@ -282,6 +282,7 @@ export {
|
||||
parseToolResultWithMedia,
|
||||
splitSearchSummaryList,
|
||||
hasAgenticContent,
|
||||
isSectionExpandedByDefault,
|
||||
classifyToolResult,
|
||||
classifyContinueIntent
|
||||
} from './agentic';
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
// Guards the exec_shell_command title while the call is still being
|
||||
// generated: the command must stream into the terminal title from the
|
||||
// first character (args arrive as partial JSON), with a blinking cursor
|
||||
// while the args are incomplete, and the conversation cwd standing in
|
||||
// for the x-tool-cwd value that only arrives with the result message.
|
||||
|
||||
import ChatMessageToolCallBlockExecShellCommand from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockExecShellCommand.svelte';
|
||||
import ChatMessageAgenticContent from '$lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte';
|
||||
import { SETTINGS_KEYS } from '$lib/constants';
|
||||
import { AgenticSectionType, BuiltInTool, MessageRole, MessageType } from '$lib/enums';
|
||||
import { conversationsStore, settingsStore } from '$lib/stores';
|
||||
import type { AgenticSection, DatabaseMessage } from '$lib/types';
|
||||
import { tick } from 'svelte';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { render } from 'vitest-browser-svelte';
|
||||
|
||||
const CONV_ID = 'conv-1';
|
||||
const TOOL_CALL_ID = 'call-1';
|
||||
const TOOL_NAME = BuiltInTool.SERVER_EXEC_SHELL_COMMAND;
|
||||
const CWD = '/home/user/project';
|
||||
|
||||
function messageWithArgs(args: string): DatabaseMessage {
|
||||
return {
|
||||
children: [],
|
||||
content: '',
|
||||
convId: CONV_ID,
|
||||
id: 'assistant-1',
|
||||
parent: null,
|
||||
role: MessageRole.ASSISTANT,
|
||||
timestamp: 0,
|
||||
toolCalls: JSON.stringify([
|
||||
{ function: { arguments: args, name: TOOL_NAME }, id: TOOL_CALL_ID }
|
||||
]),
|
||||
type: MessageType.TEXT
|
||||
};
|
||||
}
|
||||
|
||||
function toolResultMessage(content: string, toolCwd?: string): DatabaseMessage {
|
||||
return {
|
||||
children: [],
|
||||
content,
|
||||
convId: CONV_ID,
|
||||
id: 'tool-1',
|
||||
parent: null,
|
||||
role: MessageRole.TOOL,
|
||||
timestamp: 0,
|
||||
toolCallId: TOOL_CALL_ID,
|
||||
toolCalls: '',
|
||||
toolCwd,
|
||||
type: MessageType.TEXT
|
||||
};
|
||||
}
|
||||
|
||||
function triggerText(container: Element): string {
|
||||
return container.querySelector('[data-slot="collapsible-trigger"]')?.textContent ?? '';
|
||||
}
|
||||
|
||||
function hasCursor(container: Element): boolean {
|
||||
return container.querySelector('.exec-cursor') !== null;
|
||||
}
|
||||
|
||||
describe('exec_shell_command streaming title', () => {
|
||||
beforeEach(() => {
|
||||
settingsStore.updateMultipleConfig({
|
||||
[SETTINGS_KEYS.ALWAYS_SHOW_TOOL_CALL_CONTENT]: false,
|
||||
[SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS]: true
|
||||
});
|
||||
conversationsStore.activeConversation = {
|
||||
currNode: null,
|
||||
cwd: CWD,
|
||||
id: CONV_ID,
|
||||
lastModified: 0,
|
||||
name: 'test'
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
conversationsStore.activeConversation = null;
|
||||
conversationsStore.activeMessages = [];
|
||||
});
|
||||
|
||||
it('streams the partial command into the title with a cursor', async () => {
|
||||
const { container } = render(ChatMessageAgenticContent, {
|
||||
isStreaming: true,
|
||||
message: messageWithArgs('{"command":"git sta'),
|
||||
toolMessages: []
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
const title = triggerText(container);
|
||||
|
||||
expect(title).toContain('git sta');
|
||||
expect(title).toContain('$');
|
||||
expect(title).toContain(CWD);
|
||||
expect(hasCursor(container)).toBe(true);
|
||||
});
|
||||
|
||||
it('drops the cursor once the args JSON is complete', async () => {
|
||||
const screen = render(ChatMessageAgenticContent, {
|
||||
isStreaming: true,
|
||||
message: messageWithArgs('{"command":"git sta'),
|
||||
toolMessages: []
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
expect(hasCursor(screen.container)).toBe(true);
|
||||
|
||||
screen.rerender({
|
||||
isStreaming: true,
|
||||
message: messageWithArgs('{"command":"git status"}'),
|
||||
toolMessages: []
|
||||
});
|
||||
await tick();
|
||||
|
||||
expect(triggerText(screen.container)).toContain('git status');
|
||||
expect(hasCursor(screen.container)).toBe(false);
|
||||
});
|
||||
|
||||
it('switches the prompt from the conversation cwd to the persisted one', async () => {
|
||||
const screen = render(ChatMessageAgenticContent, {
|
||||
isStreaming: true,
|
||||
message: messageWithArgs('{"command":"git status"}'),
|
||||
toolMessages: []
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
expect(triggerText(screen.container)).toContain(CWD);
|
||||
|
||||
// Result message lands: its x-tool-cwd takes over the prompt.
|
||||
screen.rerender({
|
||||
isStreaming: true,
|
||||
message: messageWithArgs('{"command":"git status"}'),
|
||||
toolMessages: [toolResultMessage('', '/other/dir')]
|
||||
});
|
||||
await tick();
|
||||
|
||||
expect(triggerText(screen.container)).toContain('/other/dir');
|
||||
expect(triggerText(screen.container)).not.toContain(CWD);
|
||||
});
|
||||
|
||||
it('shows the executing label only after the command finishes streaming', async () => {
|
||||
const screen = render(ChatMessageAgenticContent, {
|
||||
isStreaming: true,
|
||||
message: messageWithArgs('{"command":"git sta'),
|
||||
toolMessages: []
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
// While the args stream, the title cursor alone signals activity.
|
||||
expect(screen.container.textContent).not.toContain('Executing command...');
|
||||
|
||||
screen.rerender({
|
||||
isStreaming: true,
|
||||
message: messageWithArgs('{"command":"git status"}'),
|
||||
toolMessages: []
|
||||
});
|
||||
await tick();
|
||||
|
||||
expect(screen.container.textContent).toContain('Executing command...');
|
||||
});
|
||||
|
||||
it('shows Executing command... while executing before the first output chunk', async () => {
|
||||
// The result message is created empty right before execution, so
|
||||
// the block must not go blank while the process produces nothing.
|
||||
const section: AgenticSection = {
|
||||
content: '',
|
||||
toolArgs: '{"command":"sleep 1"}',
|
||||
toolCallId: TOOL_CALL_ID,
|
||||
toolName: TOOL_NAME,
|
||||
toolResult: '',
|
||||
type: AgenticSectionType.TOOL_CALL
|
||||
};
|
||||
const { container } = render(ChatMessageToolCallBlockExecShellCommand, {
|
||||
isExecuting: true,
|
||||
isStreaming: true,
|
||||
open: true,
|
||||
section
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
expect(container.textContent).toContain('Executing command...');
|
||||
});
|
||||
|
||||
it('updates the title when the store mutates toolCalls in place, as the stream does', async () => {
|
||||
// The live flow never swaps the message object: onToolCallsStreaming
|
||||
// writes growing toolCalls strings into the same store row per delta.
|
||||
conversationsStore.activeMessages = [messageWithArgs('')];
|
||||
|
||||
const screen = render(ChatMessageAgenticContent, {
|
||||
isStreaming: true,
|
||||
message: conversationsStore.activeMessages[0]!,
|
||||
toolMessages: []
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
const idx = conversationsStore.findMessageIndex('assistant-1');
|
||||
|
||||
for (const args of ['{"command', '{"command":"git', '{"command":"git sta']) {
|
||||
conversationsStore.updateMessageAtIndex(idx, {
|
||||
toolCalls: JSON.stringify([
|
||||
{ function: { arguments: args, name: TOOL_NAME }, id: TOOL_CALL_ID }
|
||||
])
|
||||
});
|
||||
await tick();
|
||||
|
||||
expect(triggerText(screen.container)).toContain(args.slice('{"command":"'.length));
|
||||
expect(hasCursor(screen.container)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
// Guards the draft badges a repo row shows: a loaded model records its draft in
|
||||
// the `--model-draft` args, which know the file's quant, and the listing reports
|
||||
// the kinds of every cached model, which do not. The row shows each kind once.
|
||||
// Guards the draft badges a model row shows: a registered model records its draft
|
||||
// in its `--model-draft` args, which know the file's quant, and the listing reports
|
||||
// the kinds every cached model of the repo has on disk, which do not. The row shows
|
||||
// each kind once, and two quants of one repo keep their own drafts apart.
|
||||
|
||||
import { ModelAuxSidecar, ModelDraftSidecar, ServerModelStatus } from '$lib/enums';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
@@ -9,6 +10,11 @@ import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
const REPO = 'ggml-org/Qwen3.8-Flash-Next-GGUF';
|
||||
|
||||
/** Draft file of a cached model, as a preset records it in `--model-draft`. */
|
||||
function draftPath(quant: string): string {
|
||||
return `/cache/models--ggml-org--Qwen3.8-Flash-Next-GGUF/snapshots/${'a'.repeat(40)}/mtp-Qwen3.8-Flash-Next-${quant}.gguf`;
|
||||
}
|
||||
|
||||
/** A router entry; `args` stand in for the args of a loaded instance. */
|
||||
function entry(
|
||||
id: string,
|
||||
@@ -36,18 +42,28 @@ describe('draft sidecar badges', () => {
|
||||
it('shows one badge for a draft the args and the listing both report', () => {
|
||||
modelsStore.routerModels = [
|
||||
entry(
|
||||
`${REPO}:IQ4_NL`,
|
||||
[],
|
||||
[
|
||||
'--model-draft',
|
||||
`/cache/models--ggml-org--Qwen3.8-Flash-Next-GGUF/snapshots/${'a'.repeat(40)}/mtp-Qwen3.8-Flash-Next-Q8_0.gguf`
|
||||
]
|
||||
),
|
||||
entry(`${REPO}:Q8_0`, [ModelAuxSidecar.MMPROJ, ModelDraftSidecar.MTP])
|
||||
`${REPO}:Q8_0`,
|
||||
[ModelAuxSidecar.MMPROJ, ModelDraftSidecar.MTP],
|
||||
['--model-draft', draftPath('Q8_0')]
|
||||
)
|
||||
];
|
||||
|
||||
// the args badge wins: it knows the quant the listing does not
|
||||
expect(modelsStore.status.getDraftSidecars(REPO)).toEqual([
|
||||
expect(modelsStore.status.getDraftSidecars(`${REPO}:Q8_0`)).toEqual([
|
||||
{ kind: ModelDraftSidecar.MTP, quant: 'Q8_0', repo: REPO }
|
||||
]);
|
||||
});
|
||||
|
||||
it('gives each quant of a repo the draft it pulls itself', () => {
|
||||
modelsStore.routerModels = [
|
||||
entry(`${REPO}:IQ4_NL`, [], ['--model-draft', draftPath('Q4_0')]),
|
||||
entry(`${REPO}:Q8_0`, [], ['--model-draft', draftPath('Q8_0')])
|
||||
];
|
||||
|
||||
expect(modelsStore.status.getDraftSidecars(`${REPO}:IQ4_NL`)).toEqual([
|
||||
{ kind: ModelDraftSidecar.MTP, quant: 'Q4_0', repo: REPO }
|
||||
]);
|
||||
expect(modelsStore.status.getDraftSidecars(`${REPO}:Q8_0`)).toEqual([
|
||||
{ kind: ModelDraftSidecar.MTP, quant: 'Q8_0', repo: REPO }
|
||||
]);
|
||||
});
|
||||
@@ -58,7 +74,7 @@ describe('draft sidecar badges', () => {
|
||||
entry(`${REPO}:Q8_0`, [ModelDraftSidecar.MTP])
|
||||
];
|
||||
|
||||
expect(modelsStore.status.getDraftSidecars(REPO)).toEqual([
|
||||
expect(modelsStore.status.getDraftSidecars(`${REPO}:IQ4_NL`)).toEqual([
|
||||
{ kind: ModelDraftSidecar.MTP, quant: null, repo: REPO }
|
||||
]);
|
||||
});
|
||||
@@ -74,7 +90,7 @@ describe('draft sidecar badges', () => {
|
||||
];
|
||||
|
||||
// the kinds are not on disk yet, so the row reads them as in flight
|
||||
expect(modelsStore.status.getDraftSidecars(REPO)).toEqual([
|
||||
expect(modelsStore.status.getDraftSidecars(`${REPO}:Q4_0`)).toEqual([
|
||||
{ kind: ModelDraftSidecar.MTP, pending: true, quant: null, repo: REPO }
|
||||
]);
|
||||
});
|
||||
@@ -82,17 +98,10 @@ describe('draft sidecar badges', () => {
|
||||
it('keeps the on-disk badge of a kind a download also pulls', () => {
|
||||
modelsStore.routerModels = [
|
||||
entry(`${REPO}:IQ4_NL`, [ModelDraftSidecar.MTP], [], ServerModelStatus.INCOMPLETE),
|
||||
entry(
|
||||
`${REPO}:Q8_0`,
|
||||
[],
|
||||
[
|
||||
'--model-draft',
|
||||
`/cache/models--ggml-org--Qwen3.8-Flash-Next-GGUF/snapshots/${'a'.repeat(40)}/mtp-Qwen3.8-Flash-Next-Q8_0.gguf`
|
||||
]
|
||||
)
|
||||
entry(`${REPO}:Q8_0`, [], ['--model-draft', draftPath('Q8_0')])
|
||||
];
|
||||
|
||||
expect(modelsStore.status.getDraftSidecars(REPO)).toEqual([
|
||||
expect(modelsStore.status.getDraftSidecars(`${REPO}:Q8_0`)).toEqual([
|
||||
{ kind: ModelDraftSidecar.MTP, quant: 'Q8_0', repo: REPO }
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// A file tool call must show what it streamed even when it carries no
|
||||
// path: the path only names the file in the block title, so a call that
|
||||
// streams its body first (or a same-named tool with other arg names) used
|
||||
// to render an empty block.
|
||||
|
||||
import ChatMessageToolCallBlockEditFile from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockEditFile.svelte';
|
||||
import ChatMessageToolCallBlockWriteFile from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockWriteFile.svelte';
|
||||
import { AgenticSectionType, BuiltInTool } from '$lib/enums';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { tick } from 'svelte';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { render } from 'vitest-browser-svelte';
|
||||
|
||||
const WRITE_FILE_BODY = 'body streamed without a path';
|
||||
const EDIT_FILE_NEW_TEXT = 'replacement text';
|
||||
|
||||
function makeSection(toolName: BuiltInTool, toolArgs: string): AgenticSection {
|
||||
return {
|
||||
content: '',
|
||||
toolArgs,
|
||||
toolName,
|
||||
toolResult: undefined,
|
||||
type: AgenticSectionType.TOOL_CALL
|
||||
};
|
||||
}
|
||||
|
||||
describe('file tool calls render their body without a path', () => {
|
||||
it('write_file shows the content it streamed', async () => {
|
||||
render(ChatMessageToolCallBlockWriteFile, {
|
||||
isStreaming: true,
|
||||
open: true,
|
||||
section: makeSection(BuiltInTool.SERVER_WRITE_FILE, `{"content":"${WRITE_FILE_BODY}"`)
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
expect(document.body.textContent).toContain(WRITE_FILE_BODY);
|
||||
});
|
||||
|
||||
it('edit_file shows the diff it streamed', async () => {
|
||||
render(ChatMessageToolCallBlockEditFile, {
|
||||
isStreaming: true,
|
||||
open: true,
|
||||
section: makeSection(
|
||||
BuiltInTool.SERVER_EDIT_FILE,
|
||||
`{"edits":[{"old_text":"stale text","new_text":"${EDIT_FILE_NEW_TEXT}"}]}`
|
||||
)
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
expect(document.body.textContent).toContain(EDIT_FILE_NEW_TEXT);
|
||||
expect(document.body.textContent).toContain('stale text');
|
||||
});
|
||||
|
||||
it('write_file falls back to the raw args when no known content key is present', async () => {
|
||||
render(ChatMessageToolCallBlockWriteFile, {
|
||||
isStreaming: false,
|
||||
open: true,
|
||||
section: makeSection(BuiltInTool.SERVER_WRITE_FILE, '{"contents":"x"}')
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
expect(document.body.textContent).toContain('contents');
|
||||
});
|
||||
|
||||
it('write_file renders an empty code block for a finished empty write', async () => {
|
||||
// content of '' used to fall through to the raw-args fallback
|
||||
const section = makeSection(BuiltInTool.SERVER_WRITE_FILE, '{"content":""}');
|
||||
|
||||
section.toolResult = '{"result":"File written","bytes":0}';
|
||||
|
||||
render(ChatMessageToolCallBlockWriteFile, { isStreaming: false, open: true, section });
|
||||
|
||||
await tick();
|
||||
|
||||
expect(document.querySelector('pre code.hljs')).toBeTruthy();
|
||||
expect(document.body.textContent).not.toContain('"content"');
|
||||
expect(document.body.textContent).toMatch(/0\s*bytes/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,212 @@
|
||||
// Guards the two Settings > Display toggles that drive the default expansion of
|
||||
// a tool call block.
|
||||
//
|
||||
// "Show tool call in progress" mirrors "Show thought in progress": a call is
|
||||
// expanded by default only while it is in flight and folds back on its own once
|
||||
// the result lands. "Always show tool call content" is the one that keeps
|
||||
// finished calls open and wins on its own. A manual toggle always overrides the
|
||||
// default, in both directions.
|
||||
|
||||
import ChatMessageAgenticContent from '$lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte';
|
||||
import { SETTINGS_KEYS } from '$lib/constants';
|
||||
import { MessageRole, MessageType } from '$lib/enums';
|
||||
import { settingsStore } from '$lib/stores';
|
||||
import type { DatabaseMessage } from '$lib/types';
|
||||
import { tick } from 'svelte';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { userEvent } from 'vitest/browser';
|
||||
import { render } from 'vitest-browser-svelte';
|
||||
|
||||
const TOOL_NAME = 'custom_tool';
|
||||
const TOOL_CALL_ID = 'call-1';
|
||||
// Both markers sit inside the collapsible body, which CollapsibleContentBlock
|
||||
// only mounts while open - so their presence is exactly "the block is expanded".
|
||||
const PENDING_BODY = 'Waiting for result...';
|
||||
const RESULT_BODY = 'marker-tool-output';
|
||||
const TRIGGER_SELECTOR = '[data-slot="collapsible-trigger"]';
|
||||
|
||||
function assistantMessage(): DatabaseMessage {
|
||||
return {
|
||||
children: [],
|
||||
content: '',
|
||||
convId: 'conv-1',
|
||||
id: 'assistant-1',
|
||||
parent: null,
|
||||
role: MessageRole.ASSISTANT,
|
||||
timestamp: 0,
|
||||
toolCalls: JSON.stringify([
|
||||
{ function: { arguments: '{"path":"a.txt"}', name: TOOL_NAME }, id: TOOL_CALL_ID }
|
||||
]),
|
||||
type: MessageType.TEXT
|
||||
};
|
||||
}
|
||||
|
||||
function toolResultMessage(): DatabaseMessage {
|
||||
return {
|
||||
...assistantMessage(),
|
||||
content: RESULT_BODY,
|
||||
id: 'tool-1',
|
||||
role: MessageRole.TOOL,
|
||||
toolCallId: TOOL_CALL_ID
|
||||
};
|
||||
}
|
||||
|
||||
function setSettings({
|
||||
alwaysShow,
|
||||
inProgress
|
||||
}: {
|
||||
alwaysShow: boolean;
|
||||
inProgress: boolean;
|
||||
}): void {
|
||||
settingsStore.updateMultipleConfig({
|
||||
[SETTINGS_KEYS.ALWAYS_SHOW_TOOL_CALL_CONTENT]: alwaysShow,
|
||||
[SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS]: inProgress
|
||||
});
|
||||
}
|
||||
|
||||
function textOf(container: Element): string {
|
||||
return container.textContent ?? '';
|
||||
}
|
||||
|
||||
async function collapseBlock(screen: { container: Element }): Promise<void> {
|
||||
const trigger = screen.container.querySelector(TRIGGER_SELECTOR);
|
||||
|
||||
if (!(trigger instanceof HTMLButtonElement)) throw new Error('trigger not found');
|
||||
|
||||
await userEvent.click(trigger);
|
||||
await tick();
|
||||
}
|
||||
|
||||
describe('Show tool call in progress', () => {
|
||||
beforeEach(() => {
|
||||
setSettings({ alwaysShow: false, inProgress: false });
|
||||
});
|
||||
|
||||
it('expands a call that is still executing when the setting is on', async () => {
|
||||
setSettings({ alwaysShow: false, inProgress: true });
|
||||
|
||||
const { container } = render(ChatMessageAgenticContent, {
|
||||
isStreaming: true,
|
||||
message: assistantMessage(),
|
||||
toolMessages: []
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
expect(textOf(container)).toContain(PENDING_BODY);
|
||||
});
|
||||
|
||||
it('keeps the call collapsed while executing when the setting is off', async () => {
|
||||
const { container } = render(ChatMessageAgenticContent, {
|
||||
isStreaming: true,
|
||||
message: assistantMessage(),
|
||||
toolMessages: []
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
// The header is still there, only the body stays hidden.
|
||||
expect(textOf(container)).toContain(TOOL_NAME);
|
||||
expect(textOf(container)).not.toContain(PENDING_BODY);
|
||||
});
|
||||
|
||||
it('folds the call back when its result arrives', async () => {
|
||||
setSettings({ alwaysShow: false, inProgress: true });
|
||||
|
||||
const screen = render(ChatMessageAgenticContent, {
|
||||
isStreaming: true,
|
||||
message: assistantMessage(),
|
||||
toolMessages: []
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
// Open while it waits for the result.
|
||||
expect(textOf(screen.container)).toContain(PENDING_BODY);
|
||||
|
||||
// Same block, now finished: it must close on its own, no toggle given.
|
||||
screen.rerender({
|
||||
isStreaming: true,
|
||||
message: assistantMessage(),
|
||||
toolMessages: [toolResultMessage()]
|
||||
});
|
||||
await tick();
|
||||
|
||||
expect(textOf(screen.container)).toContain(TOOL_NAME);
|
||||
expect(textOf(screen.container)).not.toContain(PENDING_BODY);
|
||||
expect(textOf(screen.container)).not.toContain(RESULT_BODY);
|
||||
});
|
||||
|
||||
it('keeps the manual choice once the result arrives', async () => {
|
||||
setSettings({ alwaysShow: false, inProgress: true });
|
||||
|
||||
const screen = render(ChatMessageAgenticContent, {
|
||||
isStreaming: true,
|
||||
message: assistantMessage(),
|
||||
toolMessages: []
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
// Collapsed by hand while running, so the result must not reopen it.
|
||||
await collapseBlock(screen);
|
||||
|
||||
screen.rerender({
|
||||
isStreaming: true,
|
||||
message: assistantMessage(),
|
||||
toolMessages: [toolResultMessage()]
|
||||
});
|
||||
await tick();
|
||||
|
||||
expect(textOf(screen.container)).toContain(TOOL_NAME);
|
||||
expect(textOf(screen.container)).not.toContain(RESULT_BODY);
|
||||
});
|
||||
|
||||
it('still expands a finished call when "always show" is on', async () => {
|
||||
setSettings({ alwaysShow: true, inProgress: false });
|
||||
|
||||
const { container } = render(ChatMessageAgenticContent, {
|
||||
isStreaming: true,
|
||||
message: assistantMessage(),
|
||||
toolMessages: [toolResultMessage()]
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
expect(textOf(container)).toContain(RESULT_BODY);
|
||||
});
|
||||
|
||||
it('lets a manual collapse win over the in-progress default', async () => {
|
||||
setSettings({ alwaysShow: false, inProgress: true });
|
||||
|
||||
const screen = render(ChatMessageAgenticContent, {
|
||||
isStreaming: true,
|
||||
message: assistantMessage(),
|
||||
toolMessages: []
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
expect(textOf(screen.container)).toContain(PENDING_BODY);
|
||||
|
||||
await collapseBlock(screen);
|
||||
|
||||
expect(textOf(screen.container)).not.toContain(PENDING_BODY);
|
||||
});
|
||||
|
||||
it('lets a manual expand win over the collapsed default', async () => {
|
||||
const screen = render(ChatMessageAgenticContent, {
|
||||
isStreaming: true,
|
||||
message: assistantMessage(),
|
||||
toolMessages: []
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
expect(textOf(screen.container)).not.toContain(PENDING_BODY);
|
||||
|
||||
await collapseBlock(screen);
|
||||
|
||||
expect(textOf(screen.container)).toContain(PENDING_BODY);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
// Truth table for the default expansion of agentic sections - what a tool call
|
||||
// block does before the user touches it.
|
||||
//
|
||||
// The interesting row is a call the agentic loop reports as running while its
|
||||
// section is already TOOL_CALL: exec_shell_command persists an empty result
|
||||
// before any output is read, so the section type alone cannot tell a live call
|
||||
// from a finished one.
|
||||
|
||||
import { AgenticSectionType } from '$lib/enums';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { isSectionExpandedByDefault } from '$lib/utils';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const CALL_ID = 'call-1';
|
||||
|
||||
function section(overrides: Partial<AgenticSection> = {}): AgenticSection {
|
||||
return { content: '', type: AgenticSectionType.TOOL_CALL, ...overrides };
|
||||
}
|
||||
|
||||
function expanded(section: AgenticSection, flags: Record<string, unknown> = {}): boolean {
|
||||
return isSectionExpandedByDefault(section, {
|
||||
alwaysShowToolCallContent: false,
|
||||
executingToolCallId: null,
|
||||
showThoughtInProgress: false,
|
||||
showToolCallInProgress: false,
|
||||
...flags
|
||||
});
|
||||
}
|
||||
|
||||
describe('isSectionExpandedByDefault', () => {
|
||||
it('expands a call whose arguments are streaming when the setting is on', () => {
|
||||
const streaming = section({
|
||||
toolCallId: CALL_ID,
|
||||
toolName: 'read_file',
|
||||
type: AgenticSectionType.TOOL_CALL_STREAMING
|
||||
});
|
||||
|
||||
expect(expanded(streaming, { showToolCallInProgress: true })).toBe(true);
|
||||
expect(expanded(streaming)).toBe(false);
|
||||
});
|
||||
|
||||
it('expands a call waiting for its result when the setting is on', () => {
|
||||
const pending = section({ toolCallId: CALL_ID, type: AgenticSectionType.TOOL_CALL_PENDING });
|
||||
|
||||
expect(expanded(pending, { showToolCallInProgress: true })).toBe(true);
|
||||
expect(expanded(pending)).toBe(false);
|
||||
});
|
||||
|
||||
it('collapses a finished call unless "always show" is on', () => {
|
||||
const finished = section({ toolCallId: CALL_ID, toolResult: 'output' });
|
||||
|
||||
expect(expanded(finished, { showToolCallInProgress: true })).toBe(false);
|
||||
expect(expanded(finished, { alwaysShowToolCallContent: true })).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the running call expanded even when its result is already persisted', () => {
|
||||
// exec_shell_command: empty result created, output still streaming
|
||||
const live = section({ toolCallId: CALL_ID, toolResult: '' });
|
||||
|
||||
expect(expanded(live, { executingToolCallId: CALL_ID, showToolCallInProgress: true })).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('does not treat another call finishing as the running one', () => {
|
||||
const other = section({ toolCallId: 'call-2', toolResult: 'output' });
|
||||
|
||||
expect(expanded(other, { executingToolCallId: CALL_ID, showToolCallInProgress: true })).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('still leaves the running call collapsed when both settings are off', () => {
|
||||
const live = section({ toolCallId: CALL_ID, toolResult: '' });
|
||||
|
||||
expect(expanded(live, { executingToolCallId: CALL_ID })).toBe(false);
|
||||
});
|
||||
|
||||
it('never reads a missing executing id as a running call', () => {
|
||||
expect(expanded(section({ toolCallId: CALL_ID }), { executingToolCallId: null })).toBe(false);
|
||||
expect(expanded(section({ type: AgenticSectionType.TEXT }), { executingToolCallId: '' })).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('expands a running call when "always show" is on and the in-progress one is off', () => {
|
||||
const pending = section({ toolCallId: CALL_ID, type: AgenticSectionType.TOOL_CALL_PENDING });
|
||||
|
||||
expect(expanded(pending, { alwaysShowToolCallContent: true })).toBe(true);
|
||||
});
|
||||
|
||||
it('follows the thought setting for reasoning and never for finished thoughts', () => {
|
||||
expect(
|
||||
expanded(
|
||||
{ content: 'think', type: AgenticSectionType.REASONING_PENDING },
|
||||
{ showThoughtInProgress: true }
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
expanded(
|
||||
{ content: 'think', type: AgenticSectionType.REASONING },
|
||||
{
|
||||
showThoughtInProgress: true
|
||||
}
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('never expands plain text', () => {
|
||||
expect(
|
||||
expanded(
|
||||
{ content: 'hello', type: AgenticSectionType.TEXT },
|
||||
{
|
||||
alwaysShowToolCallContent: true,
|
||||
executingToolCallId: CALL_ID,
|
||||
showThoughtInProgress: true,
|
||||
showToolCallInProgress: true
|
||||
}
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
// Guards the Settings > Display wiring of "Show tool call in progress": the
|
||||
// flat config default and its presence as a checkbox in the Display section
|
||||
// come from the settings registry, so the key/label/type/section pairing is the
|
||||
// contract the dialog renders.
|
||||
|
||||
import {
|
||||
SETTING_CONFIG_DEFAULT,
|
||||
SETTINGS_CHAT_SECTIONS,
|
||||
SETTINGS_KEYS,
|
||||
SETTINGS_SECTION_SLUGS
|
||||
} from '$lib/constants';
|
||||
import { SettingsFieldType } from '$lib/enums';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
function displayField(key: string) {
|
||||
const section = SETTINGS_CHAT_SECTIONS.find((s) => s.slug === SETTINGS_SECTION_SLUGS.DISPLAY);
|
||||
|
||||
return section?.fields?.find((field) => field?.key === key);
|
||||
}
|
||||
|
||||
describe('showToolCallInProgress setting', () => {
|
||||
it('is a checkbox in the Display section', () => {
|
||||
const field = displayField(SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS);
|
||||
|
||||
expect(field).toBeDefined();
|
||||
expect(field?.type).toBe(SettingsFieldType.CHECKBOX);
|
||||
expect(field?.label).toBe('Show tool call in progress');
|
||||
});
|
||||
|
||||
it('ships enabled, like "Show thought in progress"', () => {
|
||||
expect(SETTING_CONFIG_DEFAULT[SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS]).toBe(true);
|
||||
expect(SETTING_CONFIG_DEFAULT[SETTINGS_KEYS.SHOW_THOUGHT_IN_PROGRESS]).toBe(true);
|
||||
});
|
||||
|
||||
it('is listed next to its "always show" counterpart', () => {
|
||||
const section = SETTINGS_CHAT_SECTIONS.find(
|
||||
(s) => s.slug === SETTINGS_SECTION_SLUGS.DISPLAY
|
||||
) as (typeof SETTINGS_CHAT_SECTIONS)[number];
|
||||
const keys = section.fields?.map((field) => field?.key) ?? [];
|
||||
const inProgressIndex = keys.indexOf(SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS);
|
||||
|
||||
expect(inProgressIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(keys[inProgressIndex + 1]).toBe(SETTINGS_KEYS.ALWAYS_SHOW_TOOL_CALL_CONTENT);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,7 @@
|
||||
import { parseToolArgs } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/_shared';
|
||||
import {
|
||||
extractToolArgPath,
|
||||
parseToolArgs
|
||||
} from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/_shared';
|
||||
import {
|
||||
parseEditFileMeta,
|
||||
parseEditFileTitleMeta
|
||||
@@ -12,6 +15,7 @@ import {
|
||||
parseWriteFileMeta,
|
||||
parseWriteFileTitleMeta
|
||||
} from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file';
|
||||
import { CODE_BLOCK } from '$lib/constants';
|
||||
import { AgenticSectionType, BuiltInTool } from '$lib/enums';
|
||||
import type { AgenticSection, WriteFileMeta } from '$lib/types';
|
||||
import { abbreviateHome, formatCwdMessage, lastPathSegment, parseCwdMessage } from '$lib/utils';
|
||||
@@ -164,6 +168,22 @@ describe('parseToolArgs (shared)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractToolArgPath', () => {
|
||||
it('reads every path alias', () => {
|
||||
expect(extractToolArgPath({ path: '/a' })).toBe('/a');
|
||||
expect(extractToolArgPath({ file_path: '/b' })).toBe('/b');
|
||||
expect(extractToolArgPath({ filePath: '/c' })).toBe('/c');
|
||||
});
|
||||
|
||||
it('returns undefined when no path alias is a usable string', () => {
|
||||
expect(extractToolArgPath({ content: 'x' })).toBeUndefined();
|
||||
expect(extractToolArgPath({ path: '' })).toBeUndefined();
|
||||
expect(extractToolArgPath({ path: 7 })).toBeUndefined();
|
||||
expect(extractToolArgPath(null)).toBeUndefined();
|
||||
expect(extractToolArgPath(undefined)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseWriteFileMeta', () => {
|
||||
it('returns null for sections with a different tool name', () => {
|
||||
expect(
|
||||
@@ -176,12 +196,23 @@ describe('parseWriteFileMeta', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when args have no path-like field', () => {
|
||||
expect(
|
||||
parseWriteFileMeta(
|
||||
makeSection({ toolArgs: '{"content":"x"}', toolName: BuiltInTool.SERVER_WRITE_FILE })
|
||||
)
|
||||
).toBeNull();
|
||||
it('returns content without a path when args have no path-like field', () => {
|
||||
const meta = parseWriteFileMeta(
|
||||
makeSection({ toolArgs: '{"content":"x"}', toolName: BuiltInTool.SERVER_WRITE_FILE })
|
||||
);
|
||||
|
||||
expect(meta?.content).toBe('x');
|
||||
expect(meta?.filePath).toBeUndefined();
|
||||
expect(meta?.fileName).toBeUndefined();
|
||||
expect(meta?.language).toBe(CODE_BLOCK.DEFAULT_LANGUAGE);
|
||||
});
|
||||
|
||||
it('returns content streamed before the path has arrived', () => {
|
||||
const meta = parseWriteFileMeta(
|
||||
makeSection({ toolArgs: '{"content":"hello ', toolName: BuiltInTool.SERVER_WRITE_FILE })
|
||||
);
|
||||
|
||||
expect(meta?.content).toBe('hello ');
|
||||
});
|
||||
|
||||
it('accepts partial args (renders incrementally as content streams in)', () => {
|
||||
@@ -279,6 +310,16 @@ describe('parseWriteFileTitleMeta', () => {
|
||||
expect(parseWriteFileTitleMeta(section)?.filePath).toBe('/foo.t');
|
||||
});
|
||||
|
||||
it('returns meta without a path when args have no path-like field', () => {
|
||||
const title = parseWriteFileTitleMeta(
|
||||
makeSection({ toolArgs: '{"content":"x"}', toolName: BuiltInTool.SERVER_WRITE_FILE })
|
||||
);
|
||||
|
||||
expect(title?.filePath).toBeUndefined();
|
||||
expect(title?.fileName).toBeUndefined();
|
||||
expect(title?.language).toBe(CODE_BLOCK.DEFAULT_LANGUAGE);
|
||||
});
|
||||
|
||||
it('returns null for sections with a different tool name', () => {
|
||||
expect(
|
||||
parseWriteFileTitleMeta(
|
||||
@@ -324,12 +365,13 @@ describe('parseEditFileTitleMeta', () => {
|
||||
expect(parseEditFileTitleMeta(section)?.errorMessage).toBe('permission denied');
|
||||
});
|
||||
|
||||
it('returns null when args have no path-like field', () => {
|
||||
expect(
|
||||
parseEditFileTitleMeta(
|
||||
makeSection({ toolArgs: '{"edits":[]}', toolName: BuiltInTool.SERVER_EDIT_FILE })
|
||||
)
|
||||
).toBeNull();
|
||||
it('returns meta without a path when args have no path-like field', () => {
|
||||
const title = parseEditFileTitleMeta(
|
||||
makeSection({ toolArgs: '{"edits":[]}', toolName: BuiltInTool.SERVER_EDIT_FILE })
|
||||
);
|
||||
|
||||
expect(title?.filePath).toBeUndefined();
|
||||
expect(title?.fileName).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -383,6 +425,33 @@ describe('parseEditFileMeta', () => {
|
||||
expect(meta?.errorMessage).toBe('bad path');
|
||||
expect(meta?.resultMessage).toBeUndefined();
|
||||
});
|
||||
|
||||
it('still parses edits when args have no path-like field', () => {
|
||||
const meta = parseEditFileMeta(
|
||||
makeSection({
|
||||
toolArgs: '{"edits":[{"old_text":"a","new_text":"b"}]}',
|
||||
toolName: BuiltInTool.SERVER_EDIT_FILE
|
||||
})
|
||||
);
|
||||
|
||||
expect(meta?.edits).toEqual([{ newText: 'b', oldText: 'a' }]);
|
||||
expect(meta?.filePath).toBeUndefined();
|
||||
});
|
||||
|
||||
it('parses edits streamed before the path has arrived', () => {
|
||||
const meta = parseEditFileMeta(
|
||||
makeSection({
|
||||
// second edit is still mid-stream: only its old_text has landed
|
||||
toolArgs: '{"edits":[{"old_text":"a","new_text":"b"},{"old_text":"c',
|
||||
toolName: BuiltInTool.SERVER_EDIT_FILE
|
||||
})
|
||||
);
|
||||
|
||||
expect(meta?.edits).toEqual([
|
||||
{ newText: 'b', oldText: 'a' },
|
||||
{ newText: '', oldText: 'c' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseReadFileMeta', () => {
|
||||
@@ -647,4 +716,37 @@ describe('parseExecShellCommandMeta', () => {
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('surfaces a partially streamed command before the JSON closes', () => {
|
||||
const meta = parseExecShellCommandMeta(
|
||||
makeSection(
|
||||
{ toolArgs: '{"command":"git sta', toolName: BuiltInTool.SERVER_EXEC_SHELL_COMMAND },
|
||||
BuiltInTool.SERVER_EXEC_SHELL_COMMAND
|
||||
)
|
||||
);
|
||||
|
||||
expect(meta?.command).toBe('git sta');
|
||||
});
|
||||
|
||||
it('keeps a dangling escape from breaking the partial parse', () => {
|
||||
const meta = parseExecShellCommandMeta(
|
||||
makeSection(
|
||||
{ toolArgs: '{"command":"echo \\"he', toolName: BuiltInTool.SERVER_EXEC_SHELL_COMMAND },
|
||||
BuiltInTool.SERVER_EXEC_SHELL_COMMAND
|
||||
)
|
||||
);
|
||||
|
||||
expect(meta?.command).toBe('echo "he');
|
||||
});
|
||||
|
||||
it('returns null while the command value is still empty', () => {
|
||||
expect(
|
||||
parseExecShellCommandMeta(
|
||||
makeSection(
|
||||
{ toolArgs: '{"command":"', toolName: BuiltInTool.SERVER_EXEC_SHELL_COMMAND },
|
||||
BuiltInTool.SERVER_EXEC_SHELL_COMMAND
|
||||
)
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user