mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-07-24 21:35:54 +02:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ce7da2c85 | |||
| b4d6c7d8ff | |||
| 7347430f44 | |||
| c5a4a0bb83 | |||
| 67b9b0e7f6 | |||
| 1f66c3ce1c | |||
| 66e4bf7e59 | |||
| b4aa7dd477 | |||
| 71102a73f2 | |||
| 846e991ec3 | |||
| fb0e6b6219 | |||
| 60f6a17704 | |||
| fd41bf65a2 | |||
| 40b740ad05 | |||
| f048010180 | |||
| 5735e10c49 | |||
| 305ba519ab | |||
| 76f46ad29d | |||
| 2beefef688 | |||
| 91d2fc3875 | |||
| 4ee6a9af71 | |||
| 43b5e63589 | |||
| 1521a9ac31 | |||
| 178a6c4493 |
@@ -1109,6 +1109,8 @@ jobs:
|
||||
-DGGML_SYCL=ON \
|
||||
-DCMAKE_C_COMPILER=icx \
|
||||
-DCMAKE_CXX_COMPILER=icpx \
|
||||
-DCMAKE_INSTALL_RPATH='$ORIGIN' \
|
||||
-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \
|
||||
-DLLAMA_OPENSSL=OFF \
|
||||
-DGGML_NATIVE=OFF \
|
||||
-DGGML_SYCL_F16=${{ matrix.fp16 }}
|
||||
|
||||
+36
-1
@@ -527,8 +527,43 @@ void common_models_handler_apply(common_models_handler & handler, common_params
|
||||
}
|
||||
};
|
||||
|
||||
// when a sidecar type is requested, the draft repo resolves to its sidecar instead of a full model
|
||||
const bool spec_sidecar_found = !plan_spec.mtp.local_path.empty() ||
|
||||
!plan_spec.dflash.local_path.empty() ||
|
||||
!plan_spec.eagle3.local_path.empty();
|
||||
if (!plan_spec.mtp.local_path.empty() && !had_spec_url) {
|
||||
tasks.emplace_back(plan_spec.mtp, opts, [&]() {
|
||||
// only use the discovered MTP head when no draft path is set yet
|
||||
if (params.speculative.draft.mparams.path.empty()) {
|
||||
params.speculative.draft.mparams.path = hf_cache::finalize_file(plan_spec.mtp);
|
||||
} else {
|
||||
hf_cache::finalize_file(plan_spec.mtp);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!plan_spec.dflash.local_path.empty() && !had_spec_url) {
|
||||
tasks.emplace_back(plan_spec.dflash, opts, [&]() {
|
||||
// only use the discovered DFlash sidecar when no draft path is set yet
|
||||
if (params.speculative.draft.mparams.path.empty()) {
|
||||
params.speculative.draft.mparams.path = hf_cache::finalize_file(plan_spec.dflash);
|
||||
} else {
|
||||
hf_cache::finalize_file(plan_spec.dflash);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!plan_spec.eagle3.local_path.empty() && !had_spec_url) {
|
||||
tasks.emplace_back(plan_spec.eagle3, opts, [&]() {
|
||||
// only use the discovered Eagle3 sidecar when no draft path is set yet
|
||||
if (params.speculative.draft.mparams.path.empty()) {
|
||||
params.speculative.draft.mparams.path = hf_cache::finalize_file(plan_spec.eagle3);
|
||||
} else {
|
||||
hf_cache::finalize_file(plan_spec.eagle3);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// handle plan_spec (e.g. --spec-draft-hf)
|
||||
if (!plan_spec.model_files.empty() && !had_spec_url) {
|
||||
if (!plan_spec.model_files.empty() && !had_spec_url && !spec_sidecar_found) {
|
||||
add_tasks(plan_spec.model_files, plan_spec.primary, params.speculative.draft.mparams);
|
||||
had_spec_url = true;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ common_chat_params peg_generator::generate_parser(const common_chat_template &
|
||||
data.generation_prompt = common_chat_template_generation_prompt(tmpl, inputs);
|
||||
data.format = COMMON_CHAT_FORMAT_PEG_NATIVE;
|
||||
data.preserved_tokens = autoparser.preserved_tokens;
|
||||
data.additional_stops.insert(data.additional_stops.end(),
|
||||
autoparser.additional_stops.begin(), autoparser.additional_stops.end());
|
||||
|
||||
std::string parser_generation_prompt = data.generation_prompt;
|
||||
|
||||
@@ -286,7 +288,13 @@ common_peg_parser analyze_tools::build_func_parser(common_chat_peg_builder & p,
|
||||
// we only emit tool_close when we can actually see the closing marker. This prevents
|
||||
// premature closing during partial parsing when we've seen e.g. "</" which could be
|
||||
// either "</tool_call>" (end) or "<arg_key>" prefix that failed to match.
|
||||
func_parser = func_parser + p.tool_close(p.peek(p.literal(format.per_call_end)));
|
||||
// Laguna (v4): the model may emit whitespace between the last </arg_value> and
|
||||
// </tool_call> even though the template renders them tight. Tolerate optional
|
||||
// leading space in the close lookahead so the tool call still closes.
|
||||
auto close_peek = arguments.tolerate_intertag_whitespace
|
||||
? p.peek(p.space() + p.literal(format.per_call_end))
|
||||
: p.peek(p.literal(format.per_call_end));
|
||||
func_parser = func_parser + p.tool_close(close_peek);
|
||||
} else {
|
||||
func_parser = func_parser + p.tool_close(p.space()); // force this to process tool closing callbacks in mapper
|
||||
}
|
||||
|
||||
@@ -206,6 +206,7 @@ struct tool_arguments_analysis {
|
||||
std::string value_prefix; // e.g., "", "<arg_value>", ""
|
||||
std::string value_suffix; // e.g., "</param>", "</arg_value>", ""
|
||||
std::string separator; // e.g., "", "\n", ","
|
||||
bool tolerate_intertag_whitespace = false; // Laguna: accept optional whitespace between arg tags
|
||||
};
|
||||
|
||||
struct tool_id_analysis {
|
||||
@@ -388,6 +389,7 @@ struct autoparser {
|
||||
|
||||
// Preserved tokens for tokenizer (union of all non-empty markers)
|
||||
std::vector<std::string> preserved_tokens;
|
||||
std::vector<std::string> additional_stops; // literal stop strings (e.g. Laguna </assistant>) caught however tokenized
|
||||
|
||||
autoparser() = default;
|
||||
|
||||
|
||||
@@ -173,6 +173,26 @@ static std::vector<std::function<void(const common_chat_template & tmpl, autopar
|
||||
LOG_DBG(ANSI_ORANGE "[Patch: JSON name/parameters tool instruction]\n" ANSI_RESET);
|
||||
}
|
||||
},
|
||||
// Laguna (poolside) - the v4 chat template renders reasoning and tool-arg
|
||||
// delimiters with formatting whitespace ("<think>\n", "</arg_value>\n") that
|
||||
// the model does not emit, so the inferred delimiters carry a spurious
|
||||
// newline and never match the model output. Trim to the bare tag. (v8
|
||||
// renders without the whitespace, so this is a no-op there.)
|
||||
[](const common_chat_template & tmpl, autoparser & analysis) -> void {
|
||||
if (tmpl.src.find("laguna_glm_thinking") != std::string::npos) {
|
||||
analysis.reasoning.start = trim_whitespace(analysis.reasoning.start);
|
||||
analysis.reasoning.end = trim_whitespace(analysis.reasoning.end);
|
||||
analysis.tools.arguments.value_prefix = trim_whitespace(analysis.tools.arguments.value_prefix);
|
||||
analysis.tools.arguments.value_suffix = trim_whitespace(analysis.tools.arguments.value_suffix);
|
||||
analysis.tools.arguments.separator = trim_whitespace(analysis.tools.arguments.separator);
|
||||
analysis.tools.arguments.tolerate_intertag_whitespace = true;
|
||||
// The CONTROL/eot </assistant> token only halts generation when emitted as the
|
||||
// single token; after tool calls the model can spell it out as text tokens.
|
||||
// A literal stop string catches it either way.
|
||||
analysis.additional_stops.push_back("</assistant>");
|
||||
LOG_DBG(ANSI_ORANGE "[Patch: Laguna]\n" ANSI_RESET);
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ __all__ = [
|
||||
|
||||
TEXT_MODEL_MAP: dict[str, str] = {
|
||||
"AfmoeForCausalLM": "afmoe",
|
||||
"LagunaForCausalLM": "laguna",
|
||||
"ApertusForCausalLM": "llama",
|
||||
"ArceeForCausalLM": "llama",
|
||||
"ArcticForCausalLM": "arctic",
|
||||
|
||||
@@ -1682,6 +1682,9 @@ class TextModel(ModelBase):
|
||||
if chkhsh == "9dcf830ee9990cdbf78cc523a5f7bd9ad8f3f9890c2d3581d2785ad10f07049d":
|
||||
# ref: https://huggingface.co/JetBrains/Mellum2-12B-A2.5B-Base
|
||||
res = "mellum2"
|
||||
if chkhsh == "972da7b59cec44d1f0a490a86c96df53859e486e481563e5dddac155013d87ac":
|
||||
# ref: https://huggingface.co/poolside/Laguna-XS.2
|
||||
res = "laguna"
|
||||
|
||||
if res is None:
|
||||
logger.warning("\n")
|
||||
|
||||
@@ -338,6 +338,12 @@ class HunyuanVLTextModel(HunYuanModel):
|
||||
|
||||
def __init__(self, dir_model: Path, *args, **kwargs):
|
||||
super().__init__(dir_model, *args, **kwargs)
|
||||
# transformers 5.13.0 encodes HunyuanVL XD-RoPE as dynamic + mrope_section.
|
||||
# Normalize it to avoid the HunYuan dynamic-RoPE context assertion.
|
||||
if self.rope_parameters.get("rope_type") == "dynamic" and "mrope_section" in self.rope_parameters:
|
||||
self.rope_parameters["rope_type"] = "xdrope"
|
||||
self.rope_parameters["type"] = "xdrope"
|
||||
self.rope_parameters["xdrope_section"] = list(self.rope_parameters["mrope_section"])
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch import Tensor
|
||||
|
||||
from .base import ModelBase, TextModel, gguf, logger
|
||||
|
||||
|
||||
@ModelBase.register("LagunaForCausalLM")
|
||||
class LagunaModel(TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.LAGUNA
|
||||
_experts: list[dict] | None = None
|
||||
_gate_types: list[str] | None = None
|
||||
|
||||
# --- vocab ---------------------------------------------------------------
|
||||
|
||||
def set_vocab(self) -> None:
|
||||
self._set_vocab_gpt2()
|
||||
|
||||
# Some Laguna releases wrap the chat template in tokenizer_config.json as
|
||||
# "{% include 'chat_template.jinja' %}", which SpecialVocab embeds verbatim
|
||||
# and llama.cpp's jinja engine cannot process. Prefer the resolved template
|
||||
# from the chat_template.jinja file so the GGUF is self-contained.
|
||||
tmpl_file = self.dir_model / "chat_template.jinja"
|
||||
if tmpl_file.is_file():
|
||||
self.gguf_writer.add_chat_template(tmpl_file.read_text(encoding="utf-8"))
|
||||
logger.info("gguf: embedded resolved chat_template.jinja (overriding include directive)")
|
||||
|
||||
# eos_token_id is a list [2, 24]: token 2 (EOS, also BOS) and token 24
|
||||
# (</assistant>, the turn-end). _set_vocab_gpt2 only records the scalar
|
||||
# eos, so register the extra id as eot; llama.cpp folds eot into its EOG
|
||||
# set, so the model halts on </assistant> natively.
|
||||
eos_ids = self.hparams.get("eos_token_id")
|
||||
if isinstance(eos_ids, list):
|
||||
bos_id = self.hparams.get("bos_token_id")
|
||||
extra = [e for e in eos_ids if e != bos_id]
|
||||
if extra:
|
||||
self.gguf_writer.add_eot_token_id(extra[0])
|
||||
logger.info(f"gguf: registered eot_token_id={extra[0]} from eos list {eos_ids}")
|
||||
|
||||
def get_vocab_base(self) -> tuple[list[str], list[int], str]:
|
||||
# </assistant> is the assistant turn-end (registered as eot below). The
|
||||
# HF tokenizer flags it special=false, so the base classifies it as
|
||||
# USER_DEFINED and llama.cpp renders its text into generated content,
|
||||
# leaking "</assistant>" and breaking response parsing. It is a control
|
||||
# marker, so promote it to CONTROL: llama.cpp then treats it as
|
||||
# end-of-generation and suppresses its text.
|
||||
tokens, toktypes, tokpre = super().get_vocab_base()
|
||||
for i, tok in enumerate(tokens):
|
||||
if tok == "</assistant>":
|
||||
toktypes[i] = gguf.TokenType.CONTROL
|
||||
logger.info(f"gguf: marked </assistant> (id {i}) as CONTROL token")
|
||||
return tokens, toktypes, tokpre
|
||||
|
||||
# --- hparams -------------------------------------------------------------
|
||||
|
||||
def set_gguf_parameters(self) -> None:
|
||||
super().set_gguf_parameters()
|
||||
hparams = self.hparams
|
||||
|
||||
# super() does not emit vocab_size for the gpt2 vocab path; head_count is
|
||||
# overridden with a per-layer array (XS.2 varies heads per layer via
|
||||
# num_attention_heads_per_layer; M.1 is uniform and omits it).
|
||||
self.gguf_writer.add_vocab_size(hparams["vocab_size"])
|
||||
|
||||
per_layer_heads = hparams.get("num_attention_heads_per_layer")
|
||||
if not per_layer_heads:
|
||||
per_layer_heads = [hparams["num_attention_heads"]] * hparams["num_hidden_layers"]
|
||||
assert len(per_layer_heads) == hparams["num_hidden_layers"], (
|
||||
f"num_attention_heads_per_layer length {len(per_layer_heads)} != "
|
||||
f"num_hidden_layers {hparams['num_hidden_layers']}"
|
||||
)
|
||||
self.gguf_writer.add_head_count(per_layer_heads)
|
||||
|
||||
# Resolve + validate the attention gate type now so an inconsistent
|
||||
# `gating` field fails at conversion time. See _attn_gate_types.
|
||||
self._attn_gate_types()
|
||||
|
||||
# SWA window size (M.1 has none -> key omitted, swa_type stays NONE).
|
||||
sliding_window = hparams.get("sliding_window") or 0
|
||||
if sliding_window > 0:
|
||||
self.gguf_writer.add_sliding_window(sliding_window)
|
||||
|
||||
# MoE (expert_count / expert_used_count come from super().set_gguf_parameters())
|
||||
self.gguf_writer.add_expert_feed_forward_length(hparams["moe_intermediate_size"])
|
||||
self.gguf_writer.add_expert_shared_feed_forward_length(hparams["shared_expert_intermediate_size"])
|
||||
self.gguf_writer.add_expert_weights_norm(True) # HF reference always sum-normalises after top-k
|
||||
self.gguf_writer.add_expert_weights_scale(float(hparams["moe_routed_scaling_factor"]))
|
||||
self.gguf_writer.add_expert_gating_func(gguf.ExpertGatingFuncType.SIGMOID)
|
||||
|
||||
# Leading dense layers (XS.2 has 1, M.1 has 3) before the MoE layers.
|
||||
mlp_layer_types: list[str] = hparams["mlp_layer_types"]
|
||||
leading_dense = 0
|
||||
for t in mlp_layer_types:
|
||||
if t == "dense":
|
||||
leading_dense += 1
|
||||
else:
|
||||
break
|
||||
self.gguf_writer.add_leading_dense_block_count(leading_dense)
|
||||
|
||||
# Per-layer-type RoPE dimension count (partial rotary). base emits
|
||||
# rope_freq_base(_swa) and the YaRN params from self.rope_parameters.
|
||||
head_dim = hparams["head_dim"]
|
||||
full_rope = self.rope_parameters["full_attention"]
|
||||
self.gguf_writer.add_rope_dimension_count(
|
||||
int(head_dim * float(full_rope.get("partial_rotary_factor", 1.0))))
|
||||
swa_rope = self.rope_parameters.get("sliding_attention")
|
||||
if swa_rope is not None:
|
||||
self.gguf_writer.add_rope_dimension_count_swa(
|
||||
int(head_dim * float(swa_rope.get("partial_rotary_factor", 1.0))))
|
||||
|
||||
def _attn_gate_types(self) -> list[str]:
|
||||
"""Per-layer attention output gate type: "per_head" or "per_element".
|
||||
|
||||
`gating_types` (per layer) is authoritative when present; otherwise the
|
||||
scalar `gating` field is used (the "per-element"/"per-head" string, or
|
||||
the legacy boolean True == per-head, as in Laguna-XS.2).
|
||||
|
||||
Fails loudly when the model is per-element but the `gating` field does
|
||||
not declare that as a string: runtimes that key off `gating` (vLLM,
|
||||
transformers) ignore gating_types and read a bare boolean True as
|
||||
per-head, silently corrupting the model. Surfacing it here keeps a
|
||||
broken checkpoint from being packaged as if it were fine.
|
||||
"""
|
||||
if self._gate_types is not None:
|
||||
return self._gate_types
|
||||
hparams = self.hparams
|
||||
n_layer = hparams["num_hidden_layers"]
|
||||
gating = hparams.get("gating")
|
||||
gating_types = hparams.get("gating_types")
|
||||
|
||||
def _norm(t: object) -> str:
|
||||
sval = str(t).replace("-", "_")
|
||||
if sval in ("per_element", "per_head"):
|
||||
return sval
|
||||
raise ValueError(f"Laguna: unrecognised attention gate type {t!r}")
|
||||
|
||||
if gating_types:
|
||||
assert len(gating_types) == n_layer, (
|
||||
f"gating_types length {len(gating_types)} != num_hidden_layers {n_layer}")
|
||||
types = [_norm(t) for t in gating_types]
|
||||
elif isinstance(gating, str):
|
||||
types = [_norm(gating)] * n_layer
|
||||
elif gating is True:
|
||||
types = ["per_head"] * n_layer
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Laguna: cannot determine attention gate type "
|
||||
f"(gating={gating!r}, gating_types={gating_types!r})")
|
||||
|
||||
if any(t == "per_element" for t in types) and not (
|
||||
isinstance(gating, str) and _norm(gating) == "per_element"):
|
||||
raise ValueError(
|
||||
f"Laguna config declares a per-element attention gate but "
|
||||
f"`gating`={gating!r} is not the string \"per-element\". Runtimes that "
|
||||
f"read `gating` (vLLM, transformers) will mis-handle this checkpoint as "
|
||||
f"per-head. Set gating=\"per-element\" in the source config.")
|
||||
|
||||
self._gate_types = types
|
||||
return types
|
||||
|
||||
# --- tensor handling -----------------------------------------------------
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
# Per-expert MoE weights: model.layers.{bid}.mlp.experts.{xid}.{w}.weight.
|
||||
# Only the NUMBERED per-expert weights are stacked; the router bias
|
||||
# (mlp.experts.e_score_correction_bias) takes the normal mapping path.
|
||||
if re.search(r"mlp\.experts\.\d+\.", name):
|
||||
n_experts = self.find_hparam(["num_local_experts", "num_experts"])
|
||||
assert bid is not None
|
||||
if self._experts is None:
|
||||
self._experts = [{} for _ in range(self.block_count)]
|
||||
self._experts[bid][name] = data_torch
|
||||
needed = [f"model.layers.{bid}.mlp.experts.{x}.{w}.weight"
|
||||
for x in range(n_experts) for w in ("gate_proj", "up_proj", "down_proj")]
|
||||
if all(e in self._experts[bid] for e in needed):
|
||||
for w_name in ["gate_proj", "up_proj", "down_proj"]:
|
||||
datas = [self._experts[bid][f"model.layers.{bid}.mlp.experts.{x}.{w_name}.weight"]
|
||||
for x in range(n_experts)]
|
||||
stacked = torch.stack(datas, dim=0)
|
||||
merged = f"model.layers.{bid}.mlp.experts.{w_name}.weight"
|
||||
yield from TextModel.modify_tensors(self, stacked, merged, bid)
|
||||
self._experts[bid].clear()
|
||||
return
|
||||
return
|
||||
# Cross-check the gate projection width against the declared gate type;
|
||||
# a mismatch means the weights and config disagree -> fail, do not guess.
|
||||
if bid is not None and name.endswith("self_attn.g_proj.weight"):
|
||||
heads = (self.hparams.get("num_attention_heads_per_layer")
|
||||
or [self.hparams["num_attention_heads"]] * self.hparams["num_hidden_layers"])
|
||||
n_head = heads[bid]
|
||||
head_dim = self.hparams["head_dim"]
|
||||
gate_type = self._attn_gate_types()[bid]
|
||||
expected = n_head * head_dim if gate_type == "per_element" else n_head
|
||||
out_features = int(data_torch.shape[0])
|
||||
if out_features != expected:
|
||||
raise ValueError(
|
||||
f"Laguna layer {bid}: g_proj output width {out_features} contradicts the "
|
||||
f"declared {gate_type} gate (expected {expected}); weights and config disagree.")
|
||||
|
||||
yield from TextModel.modify_tensors(self, data_torch, name, bid)
|
||||
@@ -162,6 +162,7 @@ models = [
|
||||
{"name": "granite-embed-multi-97m", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/ibm-granite/granite-embedding-97m-multilingual-r2", },
|
||||
{"name": "granite-embed-multi-311m", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/ibm-granite/granite-embedding-311m-multilingual-r2", },
|
||||
{"name": "mellum2", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/JetBrains/Mellum2-12B-A2.5B-Base"},
|
||||
{"name": "laguna", "tokt": TOKENIZER_TYPE.BPE, "repo": "https://huggingface.co/poolside/Laguna-XS.2", },
|
||||
]
|
||||
|
||||
# some models are known to be broken upstream, so we will skip them as exceptions
|
||||
|
||||
+10
-6
@@ -25,10 +25,10 @@ Legend:
|
||||
| CEIL | ❌ | ❌ | ✅ | 🟡 | 🟡 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| CLAMP | ❌ | ✅ | ✅ | ✅ | 🟡 | ✅ | 🟡 | ✅ | 🟡 | ✅ | ❌ | ❌ |
|
||||
| COL2IM_1D | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| CONCAT | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | 🟡 | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| CONCAT | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | 🟡 | ✅ | ✅ | 🟡 | ❌ | ❌ |
|
||||
| CONT | ❌ | 🟡 | ✅ | ✅ | 🟡 | ✅ | 🟡 | ✅ | ✅ | 🟡 | ❌ | ❌ |
|
||||
| CONV_2D | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| CONV_2D_DW | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| CONV_2D_DW | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| CONV_3D | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| CONV_TRANSPOSE_1D | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| CONV_TRANSPOSE_2D | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
@@ -41,6 +41,9 @@ Legend:
|
||||
| DIAG | ❌ | ❌ | ✅ | ✅ | 🟡 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| DIAG_MASK_INF | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | 🟡 | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| DIV | ❌ | ✅ | ✅ | ✅ | ❌ | 🟡 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| DSV4_HC_COMB | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| DSV4_HC_POST | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| DSV4_HC_PRE | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| DUP | ❌ | ✅ | ✅ | 🟡 | ❌ | 🟡 | 🟡 | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| ELU | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| EXP | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
@@ -63,16 +66,17 @@ Legend:
|
||||
| HARDSWISH | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| IM2COL | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| IM2COL_3D | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| L2_NORM | ❌ | ✅ | ✅ | ✅ | 🟡 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| L2_NORM | ❌ | ✅ | ✅ | ✅ | 🟡 | ✅ | ❌ | ✅ | ✅ | 🟡 | ❌ | ❌ |
|
||||
| LEAKY_RELU | ❌ | ✅ | ✅ | ✅ | ❌ | 🟡 | ❌ | ✅ | 🟡 | ❌ | ❌ | ❌ |
|
||||
| LIGHTNING_INDEXER | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| LOG | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| MEAN | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| MUL | ❌ | ✅ | ✅ | ✅ | 🟡 | 🟡 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| MUL_MAT | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 | 🟡 |
|
||||
| MUL_MAT_HADAMARD | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| MUL_MAT_HADAMARD | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| MUL_MAT_ID | ❌ | 🟡 | ✅ | ✅ | 🟡 | 🟡 | 🟡 | ✅ | ✅ | 🟡 | 🟡 | ❌ |
|
||||
| NEG | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| NORM | ❌ | ✅ | ✅ | ✅ | 🟡 | ✅ | ✅ | ✅ | 🟡 | ✅ | ❌ | ❌ |
|
||||
| NORM | ❌ | ✅ | ✅ | ✅ | 🟡 | ✅ | ✅ | ✅ | 🟡 | 🟡 | ❌ | ❌ |
|
||||
| OPT_STEP_ADAMW | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| OPT_STEP_SGD | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| OUT_PROD | 🟡 | 🟡 | 🟡 | 🟡 | ❌ | ❌ | ❌ | 🟡 | ❌ | ❌ | ❌ | 🟡 |
|
||||
@@ -82,7 +86,7 @@ Legend:
|
||||
| POOL_2D | ❌ | 🟡 | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| REGLU | ❌ | ✅ | ✅ | ✅ | 🟡 | 🟡 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| RELU | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | 🟡 | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| REPEAT | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | 🟡 | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| REPEAT | ❌ | ✅ | ✅ | 🟡 | 🟡 | ✅ | 🟡 | ✅ | ✅ | 🟡 | ❌ | ❌ |
|
||||
| REPEAT_BACK | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| RMS_NORM | ❌ | ✅ | ✅ | ✅ | 🟡 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
| RMS_NORM_BACK | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
|
||||
+2629
-952
File diff suppressed because it is too large
Load Diff
@@ -430,7 +430,7 @@ if (GGML_CPU_ALL_VARIANTS)
|
||||
message(FATAL_ERROR "Unsupported ARM target OS: ${CMAKE_SYSTEM_NAME}")
|
||||
endif()
|
||||
elseif (GGML_SYSTEM_ARCH STREQUAL "PowerPC")
|
||||
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
|
||||
if (CMAKE_SYSTEM_NAME MATCHES "Linux|AIX")
|
||||
ggml_add_cpu_backend_variant(power0)
|
||||
ggml_add_cpu_backend_variant(power7_1 POWER7)
|
||||
ggml_add_cpu_backend_variant(power7_2 POWER7 VSX)
|
||||
|
||||
@@ -1719,6 +1719,7 @@ class extra_buffer_type : ggml::cpu::extra_buffer_type {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1727,6 +1728,20 @@ class extra_buffer_type : ggml::cpu::extra_buffer_type {
|
||||
if (op->src[0]->buffer && op->src[0]->buffer->buft == ggml_backend_cpu_kleidiai_buffer_type()) {
|
||||
return (ggml::cpu::tensor_traits *) op->src[0]->extra;
|
||||
} else {
|
||||
// KleidiAI only has kernels for Q4_0 and Q8_0. For a quantized weight of any
|
||||
// other type (K-quants, IQ) it declines the op and returns nullptr below, so
|
||||
// KleidiAI does not accelerate it. Another CPU backend may still take the op,
|
||||
// and this can run during graph planning, so the message says what KleidiAI
|
||||
// did rather than what ends up executing. Warn once per process.
|
||||
if (ggml_is_quantized(op->src[0]->type) &&
|
||||
op->src[0]->type != GGML_TYPE_Q4_0 && op->src[0]->type != GGML_TYPE_Q8_0) {
|
||||
static std::atomic<bool> warned(false);
|
||||
if (!warned.exchange(true)) {
|
||||
GGML_LOG_WARN("kleidiai: no kernel for tensor type %s, not accelerated by KleidiAI "
|
||||
"(kernels available for Q4_0 and Q8_0)\n",
|
||||
ggml_type_name(op->src[0]->type));
|
||||
}
|
||||
}
|
||||
if (op->src[0]->type != GGML_TYPE_F16) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -2329,7 +2329,7 @@ class tinyBLAS_Q0_PPC {
|
||||
mc = 32;
|
||||
nc = 32;
|
||||
kc = 32;
|
||||
n_chunk = 32
|
||||
n_chunk = 32;
|
||||
#endif
|
||||
int64_t n_aligned = 0;
|
||||
if (n % n_chunk == 0) {
|
||||
|
||||
@@ -937,6 +937,9 @@ static __device__ __forceinline__ uint2 fast_div_modulo(uint32_t n, const uint3
|
||||
|
||||
typedef void (*dequantize_kernel_t)(const void * vx, const int64_t ib, const int iqs, float2 & v);
|
||||
|
||||
template<typename dst_t>
|
||||
using dequantize_kq_t = void (*)(const void * vx, const int64_t ib, dst_t * y, const int tid);
|
||||
|
||||
static __device__ __forceinline__ float get_alibi_slope(
|
||||
const float max_bias, const uint32_t h, const uint32_t n_head_log2, const float m0, const float m1
|
||||
) {
|
||||
|
||||
+26
-277
@@ -140,358 +140,107 @@ static __global__ void dequantize_block_q4_1(const void * __restrict__ vx, dst_t
|
||||
|
||||
template<typename dst_t>
|
||||
static __global__ void dequantize_block_q2_K(const void * __restrict__ vx, dst_t * __restrict__ yy) {
|
||||
const int64_t i = blockIdx.x;
|
||||
|
||||
const int64_t i = blockIdx.x;
|
||||
const block_q2_K * x = (const block_q2_K *) vx;
|
||||
|
||||
const int64_t tid = threadIdx.x;
|
||||
const int64_t n = tid/32;
|
||||
const int64_t l = tid - 32*n;
|
||||
const int64_t is = 8*n + l/16;
|
||||
|
||||
const uint8_t q = x[i].qs[32*n + l];
|
||||
dst_t * y = yy + i*QK_K + 128*n;
|
||||
|
||||
float dall = __low2half(x[i].dm);
|
||||
float dmin = __high2half(x[i].dm);
|
||||
y[l+ 0] = ggml_cuda_cast<dst_t>(dall * (x[i].scales[is+0] & 0xF) * ((q >> 0) & 3) - dmin * (x[i].scales[is+0] >> 4));
|
||||
y[l+32] = ggml_cuda_cast<dst_t>(dall * (x[i].scales[is+2] & 0xF) * ((q >> 2) & 3) - dmin * (x[i].scales[is+2] >> 4));
|
||||
y[l+64] = ggml_cuda_cast<dst_t>(dall * (x[i].scales[is+4] & 0xF) * ((q >> 4) & 3) - dmin * (x[i].scales[is+4] >> 4));
|
||||
y[l+96] = ggml_cuda_cast<dst_t>(dall * (x[i].scales[is+6] & 0xF) * ((q >> 6) & 3) - dmin * (x[i].scales[is+6] >> 4));
|
||||
dequantize_q2_K(vx, i, yy + i*QK_K, threadIdx.x);
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __global__ void dequantize_block_q3_K(const void * __restrict__ vx, dst_t * __restrict__ yy) {
|
||||
|
||||
const int64_t i = blockIdx.x;
|
||||
const block_q3_K * x = (const block_q3_K *) vx;
|
||||
|
||||
const int64_t r = threadIdx.x/4;
|
||||
const int64_t tid = r/2;
|
||||
const int64_t is0 = r%2;
|
||||
const int64_t l0 = 16*is0 + 4*(threadIdx.x%4);
|
||||
const int64_t n = tid / 4;
|
||||
const int64_t j = tid - 4*n;
|
||||
|
||||
uint8_t m = 1 << (4*n + j);
|
||||
int64_t is = 8*n + 2*j + is0;
|
||||
int shift = 2*j;
|
||||
|
||||
int8_t us = is < 4 ? (x[i].scales[is-0] & 0xF) | (((x[i].scales[is+8] >> 0) & 3) << 4) :
|
||||
is < 8 ? (x[i].scales[is-0] & 0xF) | (((x[i].scales[is+4] >> 2) & 3) << 4) :
|
||||
is < 12 ? (x[i].scales[is-8] >> 4) | (((x[i].scales[is+0] >> 4) & 3) << 4) :
|
||||
(x[i].scales[is-8] >> 4) | (((x[i].scales[is-4] >> 6) & 3) << 4);
|
||||
float d_all = x[i].d;
|
||||
float dl = d_all * (us - 32);
|
||||
|
||||
dst_t * y = yy + i*QK_K + 128*n + 32*j;
|
||||
const uint8_t * q = x[i].qs + 32*n;
|
||||
const uint8_t * hm = x[i].hmask;
|
||||
|
||||
for (int l = l0; l < l0+4; ++l) {
|
||||
y[l] = ggml_cuda_cast<dst_t>(dl * ((int8_t)((q[l] >> shift) & 3) - ((hm[l] & m) ? 0 : 4)));
|
||||
}
|
||||
}
|
||||
|
||||
static inline __device__ void get_scale_min_k4(int j, const uint8_t * q, uint8_t & d, uint8_t & m) {
|
||||
if (j < 4) {
|
||||
d = q[j] & 63; m = q[j + 4] & 63;
|
||||
} else {
|
||||
d = (q[j+4] & 0xF) | ((q[j-4] >> 6) << 4);
|
||||
m = (q[j+4] >> 4) | ((q[j-0] >> 6) << 4);
|
||||
}
|
||||
dequantize_q3_K(vx, i, yy + i*QK_K, threadIdx.x);
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __global__ void dequantize_block_q4_K(const void * __restrict__ vx, dst_t * __restrict__ yy) {
|
||||
const block_q4_K * x = (const block_q4_K *) vx;
|
||||
|
||||
const int64_t i = blockIdx.x;
|
||||
|
||||
// assume 32 threads
|
||||
const int64_t tid = threadIdx.x;
|
||||
const int64_t il = tid/8;
|
||||
const int64_t ir = tid%8;
|
||||
const int64_t is = 2*il;
|
||||
const int64_t n = 4;
|
||||
|
||||
dst_t * y = yy + i*QK_K + 64*il + n*ir;
|
||||
|
||||
const float dall = __low2half(x[i].dm);
|
||||
const float dmin = __high2half(x[i].dm);
|
||||
|
||||
const uint8_t * q = x[i].qs + 32*il + n*ir;
|
||||
|
||||
uint8_t sc, m;
|
||||
get_scale_min_k4(is + 0, x[i].scales, sc, m);
|
||||
const float d1 = dall * sc; const float m1 = dmin * m;
|
||||
get_scale_min_k4(is + 1, x[i].scales, sc, m);
|
||||
const float d2 = dall * sc; const float m2 = dmin * m;
|
||||
for (int l = 0; l < n; ++l) {
|
||||
y[l + 0] = ggml_cuda_cast<dst_t>(d1 * (q[l] & 0xF) - m1);
|
||||
y[l +32] = ggml_cuda_cast<dst_t>(d2 * (q[l] >> 4) - m2);
|
||||
}
|
||||
dequantize_q4_K(vx, i, yy + i*QK_K, threadIdx.x);
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __global__ void dequantize_block_q5_K(const void * __restrict__ vx, dst_t * __restrict__ yy) {
|
||||
const block_q5_K * x = (const block_q5_K *) vx;
|
||||
|
||||
const int64_t i = blockIdx.x;
|
||||
|
||||
// assume 64 threads - this is very slightly better than the one below
|
||||
const int64_t tid = threadIdx.x;
|
||||
const int64_t il = tid/16; // il is in 0...3
|
||||
const int64_t ir = tid%16; // ir is in 0...15
|
||||
const int64_t is = 2*il; // is is in 0...6
|
||||
|
||||
dst_t * y = yy + i*QK_K + 64*il + 2*ir;
|
||||
|
||||
const float dall = __low2half(x[i].dm);
|
||||
const float dmin = __high2half(x[i].dm);
|
||||
|
||||
const uint8_t * ql = x[i].qs + 32*il + 2*ir;
|
||||
const uint8_t * qh = x[i].qh + 2*ir;
|
||||
|
||||
uint8_t sc, m;
|
||||
get_scale_min_k4(is + 0, x[i].scales, sc, m);
|
||||
const float d1 = dall * sc; const float m1 = dmin * m;
|
||||
get_scale_min_k4(is + 1, x[i].scales, sc, m);
|
||||
const float d2 = dall * sc; const float m2 = dmin * m;
|
||||
|
||||
uint8_t hm = 1 << (2*il);
|
||||
y[ 0] = ggml_cuda_cast<dst_t>(d1 * ((ql[ 0] & 0xF) + (qh[ 0] & hm ? 16 : 0)) - m1);
|
||||
y[ 1] = ggml_cuda_cast<dst_t>(d1 * ((ql[ 1] & 0xF) + (qh[ 1] & hm ? 16 : 0)) - m1);
|
||||
hm <<= 1;
|
||||
y[32] = ggml_cuda_cast<dst_t>(d2 * ((ql[ 0] >> 4) + (qh[ 0] & hm ? 16 : 0)) - m2);
|
||||
y[33] = ggml_cuda_cast<dst_t>(d2 * ((ql[ 1] >> 4) + (qh[ 1] & hm ? 16 : 0)) - m2);
|
||||
dequantize_q5_K(vx, i, yy + i*QK_K, threadIdx.x);
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __global__ void dequantize_block_q6_K(const void * __restrict__ vx, dst_t * __restrict__ yy) {
|
||||
const block_q6_K * x = (const block_q6_K *) vx;
|
||||
|
||||
const int64_t i = blockIdx.x;
|
||||
|
||||
// assume 64 threads - this is very slightly better than the one below
|
||||
const int64_t tid = threadIdx.x;
|
||||
const int64_t ip = tid/32; // ip is 0 or 1
|
||||
const int64_t il = tid - 32*ip; // 0...32
|
||||
const int64_t is = 8*ip + il/16;
|
||||
|
||||
dst_t * y = yy + i*QK_K + 128*ip + il;
|
||||
|
||||
const float d = x[i].d;
|
||||
|
||||
const uint8_t * ql = x[i].ql + 64*ip + il;
|
||||
const uint8_t qh = x[i].qh[32*ip + il];
|
||||
const int8_t * sc = x[i].scales + is;
|
||||
|
||||
y[ 0] = ggml_cuda_cast<dst_t>(d * sc[0] * ((int8_t)((ql[ 0] & 0xF) | (((qh >> 0) & 3) << 4)) - 32));
|
||||
y[32] = ggml_cuda_cast<dst_t>(d * sc[2] * ((int8_t)((ql[32] & 0xF) | (((qh >> 2) & 3) << 4)) - 32));
|
||||
y[64] = ggml_cuda_cast<dst_t>(d * sc[4] * ((int8_t)((ql[ 0] >> 4) | (((qh >> 4) & 3) << 4)) - 32));
|
||||
y[96] = ggml_cuda_cast<dst_t>(d * sc[6] * ((int8_t)((ql[32] >> 4) | (((qh >> 6) & 3) << 4)) - 32));
|
||||
dequantize_q6_K(vx, i, yy + i*QK_K, threadIdx.x);
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __global__ void dequantize_block_iq2_xxs(const void * __restrict__ vx, dst_t * __restrict__ yy) {
|
||||
const int64_t i = blockIdx.x;
|
||||
|
||||
const int64_t i = blockIdx.x;
|
||||
const block_iq2_xxs * x = (const block_iq2_xxs *) vx;
|
||||
|
||||
const int64_t tid = threadIdx.x;
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + i*QK_K + 32*ib + 8*il;
|
||||
const uint16_t * q2 = x[i].qs + 4*ib;
|
||||
const uint8_t * aux8 = (const uint8_t *)q2;
|
||||
const uint8_t * grid = (const uint8_t *)(iq2xxs_grid + aux8[il]);
|
||||
const uint32_t aux32 = q2[2] | (q2[3] << 16);
|
||||
const float d = (float)x[i].d * (0.5f + (aux32 >> 28)) * 0.25f;
|
||||
const uint8_t signs = ksigns_iq2xs[(aux32 >> 7*il) & 127];
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
y[j] = ggml_cuda_cast<dst_t>(d * grid[j] * (signs & kmask_iq2xs[j] ? -1.f : 1.f));
|
||||
}
|
||||
dequantize_iq2_xxs(vx, i, yy + i*QK_K, threadIdx.x);
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __global__ void dequantize_block_iq2_xs(const void * __restrict__ vx, dst_t * __restrict__ yy) {
|
||||
const int64_t i = blockIdx.x;
|
||||
|
||||
const int64_t i = blockIdx.x;
|
||||
const block_iq2_xs * x = (const block_iq2_xs *) vx;
|
||||
|
||||
const int64_t tid = threadIdx.x;
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + i*QK_K + 32*ib + 8*il;
|
||||
const uint16_t * q2 = x[i].qs + 4*ib;
|
||||
const uint8_t * grid = (const uint8_t *)(iq2xs_grid + (q2[il] & 511));
|
||||
const float d = (float)x[i].d * (0.5f + ((x[i].scales[ib] >> 4*(il/2)) & 0xf)) * 0.25f;
|
||||
const uint8_t signs = ksigns_iq2xs[q2[il] >> 9];
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
y[j] = ggml_cuda_cast<dst_t>(d * grid[j] * (signs & kmask_iq2xs[j] ? -1.f : 1.f));
|
||||
}
|
||||
dequantize_iq2_xs(vx, i, yy + i*QK_K, threadIdx.x);
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __global__ void dequantize_block_iq2_s(const void * __restrict__ vx, dst_t * __restrict__ yy) {
|
||||
const int64_t i = blockIdx.x;
|
||||
|
||||
const int64_t i = blockIdx.x;
|
||||
const block_iq2_s * x = (const block_iq2_s *) vx;
|
||||
|
||||
const int64_t tid = threadIdx.x;
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + i*QK_K + 32*ib + 8*il;
|
||||
const uint8_t * grid = (const uint8_t *)(iq2s_grid + (x[i].qs[4*ib+il] | ((x[i].qh[ib] << (8-2*il)) & 0x300)));
|
||||
const float d = (float)x[i].d * (0.5f + ((x[i].scales[ib] >> 4*(il/2)) & 0xf)) * 0.25f;
|
||||
const uint8_t signs = x[i].qs[QK_K/8+4*ib+il];
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
y[j] = ggml_cuda_cast<dst_t>(d * grid[j] * (signs & kmask_iq2xs[j] ? -1.f : 1.f));
|
||||
}
|
||||
dequantize_iq2_s(vx, i, yy + i*QK_K, threadIdx.x);
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __global__ void dequantize_block_iq3_xxs(const void * __restrict__ vx, dst_t * __restrict__ yy) {
|
||||
const int64_t i = blockIdx.x;
|
||||
|
||||
const int64_t i = blockIdx.x;
|
||||
const block_iq3_xxs * x = (const block_iq3_xxs *) vx;
|
||||
|
||||
const int64_t tid = threadIdx.x;
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + i*QK_K + 32*ib + 8*il;
|
||||
const uint8_t * q3 = x[i].qs + 8*ib;
|
||||
const uint16_t * gas = (const uint16_t *)(x[i].qs + QK_K/4) + 2*ib;
|
||||
const uint8_t * grid1 = (const uint8_t *)(iq3xxs_grid + q3[2*il+0]);
|
||||
const uint8_t * grid2 = (const uint8_t *)(iq3xxs_grid + q3[2*il+1]);
|
||||
const uint32_t aux32 = gas[0] | (gas[1] << 16);
|
||||
const float d = (float)x[i].d * (0.5f + (aux32 >> 28)) * 0.5f;
|
||||
const uint8_t signs = ksigns_iq2xs[(aux32 >> 7*il) & 127];
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
y[j+0] = ggml_cuda_cast<dst_t>(d * grid1[j] * (signs & kmask_iq2xs[j+0] ? -1.f : 1.f));
|
||||
y[j+4] = ggml_cuda_cast<dst_t>(d * grid2[j] * (signs & kmask_iq2xs[j+4] ? -1.f : 1.f));
|
||||
}
|
||||
dequantize_iq3_xxs(vx, i, yy + i*QK_K, threadIdx.x);
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __global__ void dequantize_block_iq3_s(const void * __restrict__ vx, dst_t * __restrict__ yy) {
|
||||
const int64_t i = blockIdx.x;
|
||||
|
||||
const int64_t i = blockIdx.x;
|
||||
const block_iq3_s * x = (const block_iq3_s *) vx;
|
||||
|
||||
const int64_t tid = threadIdx.x;
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + i*QK_K + 32*ib + 8*il;
|
||||
const uint8_t * qs = x[i].qs + 8*ib;
|
||||
const uint8_t * grid1 = (const uint8_t *)(iq3s_grid + (qs[2*il+0] | ((x[i].qh[ib] << (8-2*il)) & 256)));
|
||||
const uint8_t * grid2 = (const uint8_t *)(iq3s_grid + (qs[2*il+1] | ((x[i].qh[ib] << (7-2*il)) & 256)));
|
||||
const float d = (float)x[i].d * (1 + 2*((x[i].scales[ib/2] >> 4*(ib%2)) & 0xf));
|
||||
const uint8_t signs = x[i].signs[4*ib + il];
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
y[j+0] = ggml_cuda_cast<dst_t>(d * grid1[j] * (signs & kmask_iq2xs[j+0] ? -1.f : 1.f));
|
||||
y[j+4] = ggml_cuda_cast<dst_t>(d * grid2[j] * (signs & kmask_iq2xs[j+4] ? -1.f : 1.f));
|
||||
}
|
||||
dequantize_iq3_s(vx, i, yy + i*QK_K, threadIdx.x);
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __global__ void dequantize_block_iq1_s(const void * __restrict__ vx, dst_t * __restrict__ yy) {
|
||||
const int64_t i = blockIdx.x;
|
||||
|
||||
const int64_t i = blockIdx.x;
|
||||
const block_iq1_s * x = (const block_iq1_s *) vx;
|
||||
|
||||
const int64_t tid = threadIdx.x;
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + i*QK_K + 32*ib + 8*il;
|
||||
const float delta = x[i].qh[ib] & 0x8000 ? -1 - IQ1S_DELTA : -1 + IQ1S_DELTA;
|
||||
const float d = (float)x[i].d * (2*((x[i].qh[ib] >> 12) & 7) + 1);
|
||||
uint32_t grid32[2]; const int8_t * q = (const int8_t *)grid32;
|
||||
grid32[0] = iq1s_grid_gpu[x[i].qs[4*ib+il] | (((x[i].qh[ib] >> 3*il) & 7) << 8)];
|
||||
grid32[1] = (grid32[0] >> 4) & 0x0f0f0f0f;
|
||||
grid32[0] &= 0x0f0f0f0f;
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
y[j] = ggml_cuda_cast<dst_t>(d * (q[j] + delta));
|
||||
}
|
||||
dequantize_iq1_s(vx, i, yy + i*QK_K, threadIdx.x);
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __global__ void dequantize_block_iq1_m(const void * __restrict__ vx, dst_t * __restrict__ yy) {
|
||||
const int64_t i = blockIdx.x;
|
||||
|
||||
const int64_t i = blockIdx.x;
|
||||
const block_iq1_m * x = (const block_iq1_m *) vx;
|
||||
|
||||
const int64_t tid = threadIdx.x;
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + i*QK_K + 32*ib + 8*il;
|
||||
const uint16_t * sc = (const uint16_t *)x[i].scales;
|
||||
iq1m_scale_t scale;
|
||||
scale.u16 = (sc[0] >> 12) | ((sc[1] >> 8) & 0x00f0) | ((sc[2] >> 4) & 0x0f00) | (sc[3] & 0xf000);
|
||||
const int64_t ib16 = 2*ib + il/2; // sc[ib16/4] >> 3*(ib16%4) -> sc[ib/2] >> 3*((2*ib+il/2)%4);
|
||||
const float d = (float)scale.f16 * (2*((sc[ib16/4] >> 3*(ib16%4)) & 0x7) + 1);
|
||||
const float delta = x[i].qh[2*ib+il/2] & (0x08 << 4*(il%2)) ? -1 - IQ1M_DELTA : -1 + IQ1M_DELTA;
|
||||
uint32_t grid32[2]; const int8_t * q = (const int8_t *)grid32;
|
||||
grid32[0] = iq1s_grid_gpu[x[i].qs[4*ib+il] | (((x[i].qh[2*ib+il/2] >> 4*(il%2)) & 7) << 8)];
|
||||
grid32[1] = (grid32[0] >> 4) & 0x0f0f0f0f;
|
||||
grid32[0] &= 0x0f0f0f0f;
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
y[j] = ggml_cuda_cast<dst_t>(d * (q[j] + delta));
|
||||
}
|
||||
dequantize_iq1_m(vx, i, yy + i*QK_K, threadIdx.x);
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __global__ void dequantize_block_iq4_nl(const void * __restrict__ vx, dst_t * __restrict__ yy) {
|
||||
const int64_t i = blockIdx.x;
|
||||
|
||||
const int64_t i = blockIdx.x;
|
||||
const block_iq4_nl * x = (const block_iq4_nl *) vx + i*(QK_K/QK4_NL);
|
||||
|
||||
const int64_t tid = threadIdx.x;
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + i*QK_K + 32*ib + 4*il;
|
||||
const uint8_t * q4 = x[ib].qs + 4*il;
|
||||
const float d = (float)x[ib].d;
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
y[j+ 0] = ggml_cuda_cast<dst_t>(d * kvalues_iq4nl[q4[j] & 0xf]);
|
||||
y[j+16] = ggml_cuda_cast<dst_t>(d * kvalues_iq4nl[q4[j] >> 4]);
|
||||
}
|
||||
dequantize_iq4_nl(vx, i, yy + i*QK_K, threadIdx.x);
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __global__ void dequantize_block_iq4_xs(const void * __restrict__ vx, dst_t * __restrict__ yy) {
|
||||
const int64_t i = blockIdx.x;
|
||||
const block_iq4_xs * x = (const block_iq4_xs *)vx;
|
||||
const int64_t i = blockIdx.x;
|
||||
|
||||
const int64_t tid = threadIdx.x;
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + i*QK_K + 32*ib + 4*il;
|
||||
const uint8_t * q4 = x[i].qs + 16*ib + 4*il;
|
||||
const float d = (float)x[i].d * ((((x[i].scales_l[ib/2] >> 4*(ib%2)) & 0xf) | (((x[i].scales_h >> 2*ib) & 3) << 4)) - 32);
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
y[j+ 0] = ggml_cuda_cast<dst_t>(d * kvalues_iq4nl[q4[j] & 0xf]);
|
||||
y[j+16] = ggml_cuda_cast<dst_t>(d * kvalues_iq4nl[q4[j] >> 4]);
|
||||
}
|
||||
dequantize_iq4_xs(vx, i, yy + i*QK_K, threadIdx.x);
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __global__ void dequantize_block_mxfp4(const void * __restrict__ vx, dst_t * __restrict__ yy) {
|
||||
const int64_t i = blockIdx.x;
|
||||
|
||||
const int64_t i = blockIdx.x;
|
||||
const block_mxfp4 * x = (const block_mxfp4 *) vx + i*(QK_K/QK_MXFP4);
|
||||
|
||||
const int64_t tid = threadIdx.x;
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + i*QK_K + 32*ib + 4*il;
|
||||
const uint8_t * q4 = x[ib].qs + 4*il;
|
||||
const float d = ggml_cuda_e8m0_to_fp32(x[ib].e);
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
y[j+ 0] = ggml_cuda_cast<dst_t>(d * kvalues_mxfp4[q4[j] & 0xf]*0.5f);
|
||||
y[j+16] = ggml_cuda_cast<dst_t>(d * kvalues_mxfp4[q4[j] >> 4]*0.5f);
|
||||
}
|
||||
dequantize_mxfp4(vx, i, yy + i*QK_K, threadIdx.x);
|
||||
}
|
||||
|
||||
template <int qk, int qr, dequantize_kernel_t dequantize_kernel, typename dst_t>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "common.cuh"
|
||||
#include "convert.cuh"
|
||||
|
||||
static __device__ __forceinline__ void dequantize_q1_0(const void * vx, const int64_t ib, const int iqs, float2 & v){
|
||||
const block_q1_0 * x = (const block_q1_0 *) vx;
|
||||
@@ -97,3 +98,335 @@ static __device__ __forceinline__ void dequantize_q8_0(const void * vx, const in
|
||||
v.x *= d;
|
||||
v.y *= d;
|
||||
}
|
||||
|
||||
//================================== k-quants
|
||||
|
||||
// Each call dequantizes one super-block of QK_K values into y using the
|
||||
// thread layout of the caller: 32 threads for q4_K, 64 threads otherwise.
|
||||
|
||||
template<typename dst_t>
|
||||
static __device__ __forceinline__ void dequantize_q2_K(const void * vx, const int64_t ib, dst_t * yy, const int tid) {
|
||||
const block_q2_K * x = (const block_q2_K *) vx;
|
||||
|
||||
const int64_t n = tid/32;
|
||||
const int64_t l = tid - 32*n;
|
||||
const int64_t is = 8*n + l/16;
|
||||
|
||||
const uint8_t q = x[ib].qs[32*n + l];
|
||||
dst_t * y = yy + 128*n;
|
||||
|
||||
float dall = __low2half(x[ib].dm);
|
||||
float dmin = __high2half(x[ib].dm);
|
||||
y[l+ 0] = ggml_cuda_cast<dst_t>(dall * (x[ib].scales[is+0] & 0xF) * ((q >> 0) & 3) - dmin * (x[ib].scales[is+0] >> 4));
|
||||
y[l+32] = ggml_cuda_cast<dst_t>(dall * (x[ib].scales[is+2] & 0xF) * ((q >> 2) & 3) - dmin * (x[ib].scales[is+2] >> 4));
|
||||
y[l+64] = ggml_cuda_cast<dst_t>(dall * (x[ib].scales[is+4] & 0xF) * ((q >> 4) & 3) - dmin * (x[ib].scales[is+4] >> 4));
|
||||
y[l+96] = ggml_cuda_cast<dst_t>(dall * (x[ib].scales[is+6] & 0xF) * ((q >> 6) & 3) - dmin * (x[ib].scales[is+6] >> 4));
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __device__ __forceinline__ void dequantize_q3_K(const void * vx, const int64_t ib, dst_t * yy, const int tid) {
|
||||
const block_q3_K * x = (const block_q3_K *) vx;
|
||||
|
||||
const int64_t r = tid/4;
|
||||
const int64_t t = r/2;
|
||||
const int64_t is0 = r%2;
|
||||
const int64_t l0 = 16*is0 + 4*(tid%4);
|
||||
const int64_t n = t / 4;
|
||||
const int64_t j = t - 4*n;
|
||||
|
||||
uint8_t m = 1 << (4*n + j);
|
||||
int64_t is = 8*n + 2*j + is0;
|
||||
int shift = 2*j;
|
||||
|
||||
int8_t us = is < 4 ? (x[ib].scales[is-0] & 0xF) | (((x[ib].scales[is+8] >> 0) & 3) << 4) :
|
||||
is < 8 ? (x[ib].scales[is-0] & 0xF) | (((x[ib].scales[is+4] >> 2) & 3) << 4) :
|
||||
is < 12 ? (x[ib].scales[is-8] >> 4) | (((x[ib].scales[is+0] >> 4) & 3) << 4) :
|
||||
(x[ib].scales[is-8] >> 4) | (((x[ib].scales[is-4] >> 6) & 3) << 4);
|
||||
float d_all = x[ib].d;
|
||||
float dl = d_all * (us - 32);
|
||||
|
||||
dst_t * y = yy + 128*n + 32*j;
|
||||
const uint8_t * q = x[ib].qs + 32*n;
|
||||
const uint8_t * hm = x[ib].hmask;
|
||||
|
||||
for (int l = l0; l < l0+4; ++l) {
|
||||
y[l] = ggml_cuda_cast<dst_t>(dl * ((int8_t)((q[l] >> shift) & 3) - ((hm[l] & m) ? 0 : 4)));
|
||||
}
|
||||
}
|
||||
|
||||
static inline __device__ void get_scale_min_k4(int j, const uint8_t * q, uint8_t & d, uint8_t & m) {
|
||||
if (j < 4) {
|
||||
d = q[j] & 63; m = q[j + 4] & 63;
|
||||
} else {
|
||||
d = (q[j+4] & 0xF) | ((q[j-4] >> 6) << 4);
|
||||
m = (q[j+4] >> 4) | ((q[j-0] >> 6) << 4);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __device__ __forceinline__ void dequantize_q4_K(const void * vx, const int64_t ib, dst_t * yy, const int tid) {
|
||||
const block_q4_K * x = (const block_q4_K *) vx;
|
||||
|
||||
// assume 32 threads
|
||||
const int64_t il = tid/8;
|
||||
const int64_t ir = tid%8;
|
||||
const int64_t is = 2*il;
|
||||
const int64_t n = 4;
|
||||
|
||||
dst_t * y = yy + 64*il + n*ir;
|
||||
|
||||
const float dall = __low2half(x[ib].dm);
|
||||
const float dmin = __high2half(x[ib].dm);
|
||||
|
||||
const uint8_t * q = x[ib].qs + 32*il + n*ir;
|
||||
|
||||
uint8_t sc, m;
|
||||
get_scale_min_k4(is + 0, x[ib].scales, sc, m);
|
||||
const float d1 = dall * sc; const float m1 = dmin * m;
|
||||
get_scale_min_k4(is + 1, x[ib].scales, sc, m);
|
||||
const float d2 = dall * sc; const float m2 = dmin * m;
|
||||
for (int l = 0; l < n; ++l) {
|
||||
y[l + 0] = ggml_cuda_cast<dst_t>(d1 * (q[l] & 0xF) - m1);
|
||||
y[l +32] = ggml_cuda_cast<dst_t>(d2 * (q[l] >> 4) - m2);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __device__ __forceinline__ void dequantize_q5_K(const void * vx, const int64_t ib, dst_t * yy, const int tid) {
|
||||
const block_q5_K * x = (const block_q5_K *) vx;
|
||||
|
||||
// assume 64 threads - this is very slightly better than the one below
|
||||
const int64_t il = tid/16; // il is in 0...3
|
||||
const int64_t ir = tid%16; // ir is in 0...15
|
||||
const int64_t is = 2*il; // is is in 0...6
|
||||
|
||||
dst_t * y = yy + 64*il + 2*ir;
|
||||
|
||||
const float dall = __low2half(x[ib].dm);
|
||||
const float dmin = __high2half(x[ib].dm);
|
||||
|
||||
const uint8_t * ql = x[ib].qs + 32*il + 2*ir;
|
||||
const uint8_t * qh = x[ib].qh + 2*ir;
|
||||
|
||||
uint8_t sc, m;
|
||||
get_scale_min_k4(is + 0, x[ib].scales, sc, m);
|
||||
const float d1 = dall * sc; const float m1 = dmin * m;
|
||||
get_scale_min_k4(is + 1, x[ib].scales, sc, m);
|
||||
const float d2 = dall * sc; const float m2 = dmin * m;
|
||||
|
||||
uint8_t hm = 1 << (2*il);
|
||||
y[ 0] = ggml_cuda_cast<dst_t>(d1 * ((ql[ 0] & 0xF) + (qh[ 0] & hm ? 16 : 0)) - m1);
|
||||
y[ 1] = ggml_cuda_cast<dst_t>(d1 * ((ql[ 1] & 0xF) + (qh[ 1] & hm ? 16 : 0)) - m1);
|
||||
hm <<= 1;
|
||||
y[32] = ggml_cuda_cast<dst_t>(d2 * ((ql[ 0] >> 4) + (qh[ 0] & hm ? 16 : 0)) - m2);
|
||||
y[33] = ggml_cuda_cast<dst_t>(d2 * ((ql[ 1] >> 4) + (qh[ 1] & hm ? 16 : 0)) - m2);
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __device__ __forceinline__ void dequantize_q6_K(const void * vx, const int64_t ib, dst_t * yy, const int tid) {
|
||||
const block_q6_K * x = (const block_q6_K *) vx;
|
||||
|
||||
// assume 64 threads - this is very slightly better than the one below
|
||||
const int64_t ip = tid/32; // ip is 0 or 1
|
||||
const int64_t il = tid - 32*ip; // 0...32
|
||||
const int64_t is = 8*ip + il/16;
|
||||
|
||||
dst_t * y = yy + 128*ip + il;
|
||||
|
||||
const float d = x[ib].d;
|
||||
|
||||
const uint8_t * ql = x[ib].ql + 64*ip + il;
|
||||
const uint8_t qh = x[ib].qh[32*ip + il];
|
||||
const int8_t * sc = x[ib].scales + is;
|
||||
|
||||
y[ 0] = ggml_cuda_cast<dst_t>(d * sc[0] * ((int8_t)((ql[ 0] & 0xF) | (((qh >> 0) & 3) << 4)) - 32));
|
||||
y[32] = ggml_cuda_cast<dst_t>(d * sc[2] * ((int8_t)((ql[32] & 0xF) | (((qh >> 2) & 3) << 4)) - 32));
|
||||
y[64] = ggml_cuda_cast<dst_t>(d * sc[4] * ((int8_t)((ql[ 0] >> 4) | (((qh >> 4) & 3) << 4)) - 32));
|
||||
y[96] = ggml_cuda_cast<dst_t>(d * sc[6] * ((int8_t)((ql[32] >> 4) | (((qh >> 6) & 3) << 4)) - 32));
|
||||
}
|
||||
|
||||
//================================== i-quants
|
||||
|
||||
// Each call dequantizes one super-block of QK_K values into y with 32
|
||||
// threads; iq4_nl packs QK_K/QK4_NL sub-blocks per super-block.
|
||||
|
||||
template<typename dst_t>
|
||||
static __device__ __forceinline__ void dequantize_iq2_xxs(const void * vx, const int64_t ibs, dst_t * yy, const int tid) {
|
||||
|
||||
const block_iq2_xxs * x = (const block_iq2_xxs *) vx;
|
||||
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + 32*ib + 8*il;
|
||||
const uint16_t * q2 = x[ibs].qs + 4*ib;
|
||||
const uint8_t * aux8 = (const uint8_t *)q2;
|
||||
const uint8_t * grid = (const uint8_t *)(iq2xxs_grid + aux8[il]);
|
||||
const uint32_t aux32 = q2[2] | (q2[3] << 16);
|
||||
const float d = (float)x[ibs].d * (0.5f + (aux32 >> 28)) * 0.25f;
|
||||
const uint8_t signs = ksigns_iq2xs[(aux32 >> 7*il) & 127];
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
y[j] = ggml_cuda_cast<dst_t>(d * grid[j] * (signs & kmask_iq2xs[j] ? -1.f : 1.f));
|
||||
}
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __device__ __forceinline__ void dequantize_iq2_xs(const void * vx, const int64_t ibs, dst_t * yy, const int tid) {
|
||||
|
||||
const block_iq2_xs * x = (const block_iq2_xs *) vx;
|
||||
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + 32*ib + 8*il;
|
||||
const uint16_t * q2 = x[ibs].qs + 4*ib;
|
||||
const uint8_t * grid = (const uint8_t *)(iq2xs_grid + (q2[il] & 511));
|
||||
const float d = (float)x[ibs].d * (0.5f + ((x[ibs].scales[ib] >> 4*(il/2)) & 0xf)) * 0.25f;
|
||||
const uint8_t signs = ksigns_iq2xs[q2[il] >> 9];
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
y[j] = ggml_cuda_cast<dst_t>(d * grid[j] * (signs & kmask_iq2xs[j] ? -1.f : 1.f));
|
||||
}
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __device__ __forceinline__ void dequantize_iq2_s(const void * vx, const int64_t ibs, dst_t * yy, const int tid) {
|
||||
|
||||
const block_iq2_s * x = (const block_iq2_s *) vx;
|
||||
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + 32*ib + 8*il;
|
||||
const uint8_t * grid = (const uint8_t *)(iq2s_grid + (x[ibs].qs[4*ib+il] | ((x[ibs].qh[ib] << (8-2*il)) & 0x300)));
|
||||
const float d = (float)x[ibs].d * (0.5f + ((x[ibs].scales[ib] >> 4*(il/2)) & 0xf)) * 0.25f;
|
||||
const uint8_t signs = x[ibs].qs[QK_K/8+4*ib+il];
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
y[j] = ggml_cuda_cast<dst_t>(d * grid[j] * (signs & kmask_iq2xs[j] ? -1.f : 1.f));
|
||||
}
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __device__ __forceinline__ void dequantize_iq3_xxs(const void * vx, const int64_t ibs, dst_t * yy, const int tid) {
|
||||
|
||||
const block_iq3_xxs * x = (const block_iq3_xxs *) vx;
|
||||
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + 32*ib + 8*il;
|
||||
const uint8_t * q3 = x[ibs].qs + 8*ib;
|
||||
const uint16_t * gas = (const uint16_t *)(x[ibs].qs + QK_K/4) + 2*ib;
|
||||
const uint8_t * grid1 = (const uint8_t *)(iq3xxs_grid + q3[2*il+0]);
|
||||
const uint8_t * grid2 = (const uint8_t *)(iq3xxs_grid + q3[2*il+1]);
|
||||
const uint32_t aux32 = gas[0] | (gas[1] << 16);
|
||||
const float d = (float)x[ibs].d * (0.5f + (aux32 >> 28)) * 0.5f;
|
||||
const uint8_t signs = ksigns_iq2xs[(aux32 >> 7*il) & 127];
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
y[j+0] = ggml_cuda_cast<dst_t>(d * grid1[j] * (signs & kmask_iq2xs[j+0] ? -1.f : 1.f));
|
||||
y[j+4] = ggml_cuda_cast<dst_t>(d * grid2[j] * (signs & kmask_iq2xs[j+4] ? -1.f : 1.f));
|
||||
}
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __device__ __forceinline__ void dequantize_iq3_s(const void * vx, const int64_t ibs, dst_t * yy, const int tid) {
|
||||
|
||||
const block_iq3_s * x = (const block_iq3_s *) vx;
|
||||
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + 32*ib + 8*il;
|
||||
const uint8_t * qs = x[ibs].qs + 8*ib;
|
||||
const uint8_t * grid1 = (const uint8_t *)(iq3s_grid + (qs[2*il+0] | ((x[ibs].qh[ib] << (8-2*il)) & 256)));
|
||||
const uint8_t * grid2 = (const uint8_t *)(iq3s_grid + (qs[2*il+1] | ((x[ibs].qh[ib] << (7-2*il)) & 256)));
|
||||
const float d = (float)x[ibs].d * (1 + 2*((x[ibs].scales[ib/2] >> 4*(ib%2)) & 0xf));
|
||||
const uint8_t signs = x[ibs].signs[4*ib + il];
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
y[j+0] = ggml_cuda_cast<dst_t>(d * grid1[j] * (signs & kmask_iq2xs[j+0] ? -1.f : 1.f));
|
||||
y[j+4] = ggml_cuda_cast<dst_t>(d * grid2[j] * (signs & kmask_iq2xs[j+4] ? -1.f : 1.f));
|
||||
}
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __device__ __forceinline__ void dequantize_iq1_s(const void * vx, const int64_t ibs, dst_t * yy, const int tid) {
|
||||
|
||||
const block_iq1_s * x = (const block_iq1_s *) vx;
|
||||
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + 32*ib + 8*il;
|
||||
const float delta = x[ibs].qh[ib] & 0x8000 ? -1 - IQ1S_DELTA : -1 + IQ1S_DELTA;
|
||||
const float d = (float)x[ibs].d * (2*((x[ibs].qh[ib] >> 12) & 7) + 1);
|
||||
uint32_t grid32[2]; const int8_t * q = (const int8_t *)grid32;
|
||||
grid32[0] = iq1s_grid_gpu[x[ibs].qs[4*ib+il] | (((x[ibs].qh[ib] >> 3*il) & 7) << 8)];
|
||||
grid32[1] = (grid32[0] >> 4) & 0x0f0f0f0f;
|
||||
grid32[0] &= 0x0f0f0f0f;
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
y[j] = ggml_cuda_cast<dst_t>(d * (q[j] + delta));
|
||||
}
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __device__ __forceinline__ void dequantize_iq1_m(const void * vx, const int64_t ibs, dst_t * yy, const int tid) {
|
||||
|
||||
const block_iq1_m * x = (const block_iq1_m *) vx;
|
||||
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + 32*ib + 8*il;
|
||||
const uint16_t * sc = (const uint16_t *)x[ibs].scales;
|
||||
iq1m_scale_t scale;
|
||||
scale.u16 = (sc[0] >> 12) | ((sc[1] >> 8) & 0x00f0) | ((sc[2] >> 4) & 0x0f00) | (sc[3] & 0xf000);
|
||||
const int64_t ib16 = 2*ib + il/2; // sc[ib16/4] >> 3*(ib16%4) -> sc[ib/2] >> 3*((2*ib+il/2)%4);
|
||||
const float d = (float)scale.f16 * (2*((sc[ib16/4] >> 3*(ib16%4)) & 0x7) + 1);
|
||||
const float delta = x[ibs].qh[2*ib+il/2] & (0x08 << 4*(il%2)) ? -1 - IQ1M_DELTA : -1 + IQ1M_DELTA;
|
||||
uint32_t grid32[2]; const int8_t * q = (const int8_t *)grid32;
|
||||
grid32[0] = iq1s_grid_gpu[x[ibs].qs[4*ib+il] | (((x[ibs].qh[2*ib+il/2] >> 4*(il%2)) & 7) << 8)];
|
||||
grid32[1] = (grid32[0] >> 4) & 0x0f0f0f0f;
|
||||
grid32[0] &= 0x0f0f0f0f;
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
y[j] = ggml_cuda_cast<dst_t>(d * (q[j] + delta));
|
||||
}
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __device__ __forceinline__ void dequantize_iq4_nl(const void * vx, const int64_t ibs, dst_t * yy, const int tid) {
|
||||
|
||||
const block_iq4_nl * x = (const block_iq4_nl *) vx + ibs*(QK_K/QK4_NL);
|
||||
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + 32*ib + 4*il;
|
||||
const uint8_t * q4 = x[ib].qs + 4*il;
|
||||
const float d = (float)x[ib].d;
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
y[j+ 0] = ggml_cuda_cast<dst_t>(d * kvalues_iq4nl[q4[j] & 0xf]);
|
||||
y[j+16] = ggml_cuda_cast<dst_t>(d * kvalues_iq4nl[q4[j] >> 4]);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __device__ __forceinline__ void dequantize_iq4_xs(const void * vx, const int64_t ibs, dst_t * yy, const int tid) {
|
||||
const block_iq4_xs * x = (const block_iq4_xs *)vx;
|
||||
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + 32*ib + 4*il;
|
||||
const uint8_t * q4 = x[ibs].qs + 16*ib + 4*il;
|
||||
const float d = (float)x[ibs].d * ((((x[ibs].scales_l[ib/2] >> 4*(ib%2)) & 0xf) | (((x[ibs].scales_h >> 2*ib) & 3) << 4)) - 32);
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
y[j+ 0] = ggml_cuda_cast<dst_t>(d * kvalues_iq4nl[q4[j] & 0xf]);
|
||||
y[j+16] = ggml_cuda_cast<dst_t>(d * kvalues_iq4nl[q4[j] >> 4]);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __device__ __forceinline__ void dequantize_mxfp4(const void * vx, const int64_t ibs, dst_t * yy, const int tid) {
|
||||
|
||||
const block_mxfp4 * x = (const block_mxfp4 *) vx + ibs*(QK_K/QK_MXFP4);
|
||||
|
||||
const int64_t il = tid/8; // 0...3
|
||||
const int64_t ib = tid%8; // 0...7
|
||||
dst_t * y = yy + 32*ib + 4*il;
|
||||
const uint8_t * q4 = x[ib].qs + 4*il;
|
||||
const float d = ggml_cuda_e8m0_to_fp32(x[ib].e);
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
y[j+ 0] = ggml_cuda_cast<dst_t>(d * kvalues_mxfp4[q4[j] & 0xf]*0.5f);
|
||||
y[j+16] = ggml_cuda_cast<dst_t>(d * kvalues_mxfp4[q4[j] >> 4]*0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
+197
-22
@@ -40,6 +40,35 @@ static __global__ void k_get_rows(
|
||||
}
|
||||
}
|
||||
|
||||
template<typename dst_t, dequantize_kq_t<dst_t> dequantize_kq>
|
||||
static __global__ void k_get_rows_kq(
|
||||
const void * __restrict__ src0, const int32_t * __restrict__ src1, dst_t * __restrict__ dst,
|
||||
const int64_t ne00, /*const int64_t ne01, const int64_t ne02, const int64_t ne03,*/
|
||||
/*const int64_t ne10,*/ const int64_t ne11, const uint3 ne12_fdv, /*const int64_t ne13,*/
|
||||
/*const size_t s0,*/ const size_t s1, const size_t s2, const size_t s3,
|
||||
/*const size_t nb00,*/ const size_t nb01, const size_t nb02, const size_t nb03,
|
||||
const size_t s10, const size_t s11, const size_t s12/*, const size_t s13*/) {
|
||||
|
||||
ggml_cuda_pdl_sync();
|
||||
const int64_t nsb = ne00/QK_K; // super-blocks per row
|
||||
for (int64_t z = blockIdx.z; z < ne11*(int64_t)ne12_fdv.z; z += gridDim.z) {
|
||||
// The x and y dimensions of the grid are swapped because the maximum allowed grid size for x is higher.
|
||||
const int i10 = blockIdx.x;
|
||||
const uint2 dm = fast_div_modulo((uint32_t)z, ne12_fdv);
|
||||
const int i11 = dm.x;
|
||||
const int i12 = dm.y;
|
||||
|
||||
const int i01 = src1[i10*s10 + i11*s11 + i12*s12];
|
||||
|
||||
dst_t * dst_row = dst + i10*s1 + i11*s2 + i12*s3;
|
||||
const void * src0_row = (const char *) src0 + i01*nb01 + i11*nb02 + i12*nb03;
|
||||
|
||||
for (int64_t ib = blockIdx.y; ib < nsb; ib += gridDim.y) {
|
||||
dequantize_kq(src0_row, ib, dst_row + ib*QK_K, threadIdx.x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<typename src0_t, typename dst_t>
|
||||
static __global__ void k_get_rows_float(
|
||||
const src0_t * src0_ptr, const int32_t * src1_ptr, dst_t * dst_ptr,
|
||||
@@ -55,27 +84,51 @@ static __global__ void k_get_rows_float(
|
||||
dst_t * GGML_CUDA_RESTRICT dst = dst_ptr;
|
||||
ggml_cuda_pdl_sync();
|
||||
for (int64_t z = blockIdx.z; z < ne11*(int64_t)ne12_fdv.z; z += gridDim.z) {
|
||||
// The x and y dimensions of the grid are swapped because the maximum allowed grid size for x is higher.
|
||||
const int i10 = blockIdx.x;
|
||||
const uint2 dm = fast_div_modulo((uint32_t)z, ne12_fdv);
|
||||
const int i11 = dm.x;
|
||||
const int i12 = dm.y;
|
||||
|
||||
const int i01 = src1[i10*s10 + i11*s11 + i12*s12];
|
||||
|
||||
dst_t * GGML_CUDA_RESTRICT dst_row = dst + i10*s1 + i11*s2 + i12*s3;
|
||||
const src0_t * GGML_CUDA_RESTRICT src0_row = (const src0_t *)((const char *) src0 + i01*nb01 + i11*nb02 + i12*nb03);
|
||||
|
||||
for (int64_t i00 = blockIdx.y*blockDim.x + threadIdx.x; i00 < ne00; i00 += gridDim.y*blockDim.x) {
|
||||
// The x and y dimensions of the grid are swapped because the maximum allowed grid size for x is higher.
|
||||
const int i10 = blockIdx.x;
|
||||
const uint2 dm = fast_div_modulo((uint32_t)z, ne12_fdv);
|
||||
const int i11 = dm.x;
|
||||
const int i12 = dm.y;
|
||||
|
||||
if (i00 >= ne00) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int i01 = src1[i10*s10 + i11*s11 + i12*s12];
|
||||
|
||||
dst_t * dst_row = dst + i10*s1 + i11*s2 + i12*s3;
|
||||
const src0_t * src0_row = (const src0_t *)((const char *) src0 + i01*nb01 + i11*nb02 + i12*nb03);
|
||||
|
||||
dst_row[i00] = ggml_cuda_cast<dst_t>(src0_row[i00]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<typename dst_t>
|
||||
static __global__ void k_get_rows_float_vec(
|
||||
const dst_t * src0_ptr, const int32_t * src1_ptr, dst_t * dst_ptr,
|
||||
const int64_t ne00v,
|
||||
const int64_t ne11, const uint3 ne12_fdv,
|
||||
const size_t s1, const size_t s2, const size_t s3,
|
||||
const size_t nb01, const size_t nb02, const size_t nb03,
|
||||
const size_t s10, const size_t s11, const size_t s12) {
|
||||
|
||||
ggml_cuda_pdl_lc();
|
||||
ggml_cuda_pdl_sync();
|
||||
for (int64_t z = blockIdx.z; z < ne11*(int64_t)ne12_fdv.z; z += gridDim.z) {
|
||||
const int i10 = blockIdx.x;
|
||||
const uint2 dm = fast_div_modulo((uint32_t)z, ne12_fdv);
|
||||
const int i11 = dm.x;
|
||||
const int i12 = dm.y;
|
||||
|
||||
const int i01 = src1_ptr[i10*s10 + i11*s11 + i12*s12];
|
||||
|
||||
int4 * GGML_CUDA_RESTRICT dst_row = (int4 *) (dst_ptr + i10*s1 + i11*s2 + i12*s3);
|
||||
const int4 * GGML_CUDA_RESTRICT src0_row = (const int4 *)((const char *) src0_ptr + i01*nb01 + i11*nb02 + i12*nb03);
|
||||
|
||||
for (int64_t i = blockIdx.y*blockDim.x + threadIdx.x; i < ne00v; i += gridDim.y*blockDim.x) {
|
||||
dst_row[i] = src0_row[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<typename grad_t, typename dst_t>
|
||||
static __global__ void k_get_rows_back_float(
|
||||
const grad_t * __restrict__ grad, const int32_t * __restrict__ rows, dst_t * __restrict__ dst,
|
||||
@@ -140,16 +193,18 @@ static void get_rows_cuda_q(
|
||||
s10, s11, s12/*, s13*/);
|
||||
}
|
||||
|
||||
template<typename src0_t, typename dst_t>
|
||||
static void get_rows_cuda_float(
|
||||
const src0_t * src0_d, const int32_t * src1_d, dst_t * dst_d,
|
||||
template<int block_dim, typename dst_t, dequantize_kq_t<dst_t> dequantize_kq>
|
||||
static void get_rows_cuda_kq(
|
||||
const void * src0_d, const int32_t * src1_d, dst_t * dst_d,
|
||||
const int64_t ne00, const size_t nb01, const size_t nb02, const size_t nb03,
|
||||
const int64_t ne10, const int64_t ne11, const int64_t ne12, const size_t nb10, const size_t nb11, const size_t nb12,
|
||||
const size_t nb1, const size_t nb2, const size_t nb3,
|
||||
cudaStream_t stream) {
|
||||
const dim3 block_dims(CUDA_GET_ROWS_BLOCK_SIZE, 1, 1);
|
||||
const int block_num_y = (ne00 + CUDA_GET_ROWS_BLOCK_SIZE - 1) / CUDA_GET_ROWS_BLOCK_SIZE;
|
||||
const dim3 block_nums(ne10, MIN(block_num_y, UINT16_MAX), MIN(ne11*ne12, UINT16_MAX));
|
||||
GGML_ASSERT(ne00 % QK_K == 0);
|
||||
const int64_t nsb = ne00/QK_K;
|
||||
|
||||
const dim3 block_dims(block_dim, 1, 1);
|
||||
const dim3 block_nums(ne10, MIN(nsb, UINT16_MAX), MIN(ne11*ne12, UINT16_MAX));
|
||||
|
||||
// strides in elements
|
||||
// const size_t s0 = nb0 / sizeof(dst_t);
|
||||
@@ -166,6 +221,67 @@ static void get_rows_cuda_float(
|
||||
GGML_ASSERT(ne11 <= std::numeric_limits<uint32_t>::max() / ne12);
|
||||
const uint3 ne12_fdv = init_fastdiv_values(ne12);
|
||||
|
||||
k_get_rows_kq<dst_t, dequantize_kq><<<block_nums, block_dims, 0, stream>>>(
|
||||
src0_d, src1_d, dst_d,
|
||||
ne00, /*ne01, ne02, ne03,*/
|
||||
/*ne10,*/ ne11, ne12_fdv, /*ne13,*/
|
||||
/* s0,*/ s1, s2, s3,
|
||||
/* nb00,*/ nb01, nb02, nb03,
|
||||
s10, s11, s12/*, s13*/);
|
||||
}
|
||||
|
||||
template<typename src0_t, typename dst_t>
|
||||
static void get_rows_cuda_float(
|
||||
const src0_t * src0_d, const int32_t * src1_d, dst_t * dst_d,
|
||||
const int64_t ne00, const size_t nb01, const size_t nb02, const size_t nb03,
|
||||
const int64_t ne10, const int64_t ne11, const int64_t ne12, const size_t nb10, const size_t nb11, const size_t nb12,
|
||||
const size_t nb1, const size_t nb2, const size_t nb3,
|
||||
cudaStream_t stream) {
|
||||
const dim3 block_dims(CUDA_GET_ROWS_BLOCK_SIZE, 1, 1);
|
||||
|
||||
// strides in elements
|
||||
// const size_t s0 = nb0 / sizeof(dst_t);
|
||||
const size_t s1 = nb1 / sizeof(dst_t);
|
||||
const size_t s2 = nb2 / sizeof(dst_t);
|
||||
const size_t s3 = nb3 / sizeof(dst_t);
|
||||
|
||||
const size_t s10 = nb10 / sizeof(int32_t);
|
||||
const size_t s11 = nb11 / sizeof(int32_t);
|
||||
const size_t s12 = nb12 / sizeof(int32_t);
|
||||
// const size_t s13 = nb13 / sizeof(int32_t);
|
||||
|
||||
GGML_ASSERT(ne12 > 0);
|
||||
GGML_ASSERT(ne11 <= std::numeric_limits<uint32_t>::max() / ne12);
|
||||
const uint3 ne12_fdv = init_fastdiv_values(ne12);
|
||||
|
||||
if constexpr (std::is_same<src0_t, dst_t>::value) {
|
||||
constexpr int VEC = 16 / sizeof(dst_t);
|
||||
const int64_t ne00v = ne00 / VEC;
|
||||
const int64_t vec_block_num_y = (ne00v + CUDA_GET_ROWS_BLOCK_SIZE - 1) / CUDA_GET_ROWS_BLOCK_SIZE;
|
||||
const bool enough_blocks = vec_block_num_y * ne10 * ne11 * ne12 >= 128;
|
||||
const bool can_vec = VEC > 1 && enough_blocks &&
|
||||
(ne00 % VEC == 0) &&
|
||||
(nb01 % 16 == 0) && (nb02 % 16 == 0) && (nb03 % 16 == 0) &&
|
||||
(nb1 % 16 == 0) && (nb2 % 16 == 0) && (nb3 % 16 == 0) &&
|
||||
(((uintptr_t) src0_d) % 16 == 0) && (((uintptr_t) dst_d) % 16 == 0);
|
||||
|
||||
if (can_vec) {
|
||||
const int block_num_y = vec_block_num_y;
|
||||
const dim3 block_nums(ne10, MIN(block_num_y, UINT16_MAX), MIN(ne11*ne12, UINT16_MAX));
|
||||
const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params{block_nums, block_dims, 0, stream};
|
||||
ggml_cuda_kernel_launch(k_get_rows_float_vec<dst_t>, launch_params,
|
||||
(const dst_t *) src0_d, src1_d, dst_d,
|
||||
ne00v, ne11, ne12_fdv,
|
||||
s1, s2, s3,
|
||||
nb01, nb02, nb03,
|
||||
s10, s11, s12);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const int block_num_y = (ne00 + CUDA_GET_ROWS_BLOCK_SIZE - 1) / CUDA_GET_ROWS_BLOCK_SIZE;
|
||||
const dim3 block_nums(ne10, MIN(block_num_y, UINT16_MAX), MIN(ne11*ne12, UINT16_MAX));
|
||||
|
||||
const ggml_cuda_kernel_launch_params launch_params = ggml_cuda_kernel_launch_params{block_nums, block_dims, 0, stream};
|
||||
ggml_cuda_kernel_launch(k_get_rows_float<src0_t, dst_t>, launch_params,
|
||||
src0_d, src1_d, dst_d,
|
||||
@@ -224,8 +340,67 @@ static void ggml_cuda_get_rows_switch_src0_type(
|
||||
get_rows_cuda_q<QK8_0, QR8_0, dequantize_q8_0>(src0_d, src1_d, dst_d,
|
||||
ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream);
|
||||
break;
|
||||
case GGML_TYPE_Q2_K:
|
||||
get_rows_cuda_kq<64, dst_t, dequantize_q2_K<dst_t>>(src0_d, src1_d, dst_d,
|
||||
ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream);
|
||||
break;
|
||||
case GGML_TYPE_Q3_K:
|
||||
get_rows_cuda_kq<64, dst_t, dequantize_q3_K<dst_t>>(src0_d, src1_d, dst_d,
|
||||
ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream);
|
||||
break;
|
||||
case GGML_TYPE_Q4_K:
|
||||
get_rows_cuda_kq<32, dst_t, dequantize_q4_K<dst_t>>(src0_d, src1_d, dst_d,
|
||||
ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream);
|
||||
break;
|
||||
case GGML_TYPE_Q5_K:
|
||||
get_rows_cuda_kq<64, dst_t, dequantize_q5_K<dst_t>>(src0_d, src1_d, dst_d,
|
||||
ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream);
|
||||
break;
|
||||
case GGML_TYPE_Q6_K:
|
||||
get_rows_cuda_kq<64, dst_t, dequantize_q6_K<dst_t>>(src0_d, src1_d, dst_d,
|
||||
ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream);
|
||||
break;
|
||||
case GGML_TYPE_IQ2_XXS:
|
||||
get_rows_cuda_kq<32, dst_t, dequantize_iq2_xxs<dst_t>>(src0_d, src1_d, dst_d,
|
||||
ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream);
|
||||
break;
|
||||
case GGML_TYPE_IQ2_XS:
|
||||
get_rows_cuda_kq<32, dst_t, dequantize_iq2_xs<dst_t>>(src0_d, src1_d, dst_d,
|
||||
ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream);
|
||||
break;
|
||||
case GGML_TYPE_IQ2_S:
|
||||
get_rows_cuda_kq<32, dst_t, dequantize_iq2_s<dst_t>>(src0_d, src1_d, dst_d,
|
||||
ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream);
|
||||
break;
|
||||
case GGML_TYPE_IQ3_XXS:
|
||||
get_rows_cuda_kq<32, dst_t, dequantize_iq3_xxs<dst_t>>(src0_d, src1_d, dst_d,
|
||||
ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream);
|
||||
break;
|
||||
case GGML_TYPE_IQ3_S:
|
||||
get_rows_cuda_kq<32, dst_t, dequantize_iq3_s<dst_t>>(src0_d, src1_d, dst_d,
|
||||
ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream);
|
||||
break;
|
||||
case GGML_TYPE_IQ1_S:
|
||||
get_rows_cuda_kq<32, dst_t, dequantize_iq1_s<dst_t>>(src0_d, src1_d, dst_d,
|
||||
ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream);
|
||||
break;
|
||||
case GGML_TYPE_IQ1_M:
|
||||
get_rows_cuda_kq<32, dst_t, dequantize_iq1_m<dst_t>>(src0_d, src1_d, dst_d,
|
||||
ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream);
|
||||
break;
|
||||
case GGML_TYPE_IQ4_NL:
|
||||
get_rows_cuda_kq<32, dst_t, dequantize_iq4_nl<dst_t>>(src0_d, src1_d, dst_d,
|
||||
ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream);
|
||||
break;
|
||||
case GGML_TYPE_IQ4_XS:
|
||||
get_rows_cuda_kq<32, dst_t, dequantize_iq4_xs<dst_t>>(src0_d, src1_d, dst_d,
|
||||
ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream);
|
||||
break;
|
||||
case GGML_TYPE_MXFP4:
|
||||
get_rows_cuda_kq<32, dst_t, dequantize_mxfp4<dst_t>>(src0_d, src1_d, dst_d,
|
||||
ne00, nb01, nb02, nb03, ne10, ne11, ne12, nb10, nb11, nb12, nb1, nb2, nb3, stream);
|
||||
break;
|
||||
default:
|
||||
// TODO: k-quants
|
||||
GGML_ABORT("%s: unsupported src0 type: %s\n", __func__, ggml_type_name(src0_type));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2703,6 +2703,7 @@ static int ggml_cuda_try_gdn_cache_fusion(
|
||||
|
||||
static bool ggml_cuda_topk_moe_fusion(const struct ggml_cgraph * cgraph, int node_idx, ggml_cuda_topk_moe_args & args) {
|
||||
args.sigmoid = false;
|
||||
args.sqrt_softplus = false;
|
||||
args.softmax = false;
|
||||
args.delayed_softmax = false;
|
||||
args.prob_bias = false;
|
||||
@@ -2716,10 +2717,17 @@ static bool ggml_cuda_topk_moe_fusion(const struct ggml_cgraph * cgraph, int nod
|
||||
}
|
||||
|
||||
if (nodes[node_idx]->op == GGML_OP_UNARY) {
|
||||
if (ggml_get_unary_op(nodes[node_idx]) != GGML_UNARY_OP_SIGMOID) {
|
||||
const ggml_unary_op unary_op = ggml_get_unary_op(nodes[node_idx]);
|
||||
if (unary_op == GGML_UNARY_OP_SIGMOID) {
|
||||
args.sigmoid = true;
|
||||
} else if (unary_op == GGML_UNARY_OP_SOFTPLUS && node_idx + 1 < n_nodes &&
|
||||
nodes[node_idx + 1]->op == GGML_OP_SQRT && nodes[node_idx + 1]->src[0] == nodes[node_idx]) {
|
||||
// sqrt(softplus(x)) scoring (DeepSeek-V4)
|
||||
args.sqrt_softplus = true;
|
||||
node_idx++;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
args.sigmoid = true;
|
||||
}
|
||||
|
||||
if (nodes[node_idx]->op == GGML_OP_ARGSORT) {
|
||||
@@ -2728,7 +2736,7 @@ static bool ggml_cuda_topk_moe_fusion(const struct ggml_cgraph * cgraph, int nod
|
||||
|
||||
node_idx++;
|
||||
|
||||
if (args.sigmoid || args.softmax) {
|
||||
if (args.sigmoid || args.sqrt_softplus || args.softmax) {
|
||||
// SOFTMAX -> RESHAPE
|
||||
if (node_idx >= n_nodes || nodes[node_idx]->op != GGML_OP_RESHAPE ||
|
||||
nodes[node_idx]->src[0] != nodes[node_idx - 1]) {
|
||||
@@ -3172,21 +3180,27 @@ static int ggml_cuda_try_fuse(ggml_backend_cuda_context * cuda_ctx, ggml_cgraph
|
||||
const ggml_tensor * scale = nullptr;
|
||||
|
||||
if (!args.delayed_softmax) {
|
||||
ggml_op gating_op = args.sigmoid ? GGML_OP_UNARY : GGML_OP_SOFT_MAX;
|
||||
int out_nodes[2]; // nodes which can't be elided
|
||||
int out_nodes[2]; // nodes which can't be elided
|
||||
|
||||
if (args.sigmoid) {
|
||||
ops.insert(ops.end(), { GGML_OP_UNARY });
|
||||
} else if (args.sqrt_softplus) {
|
||||
ops.insert(ops.end(), { GGML_OP_UNARY, GGML_OP_SQRT });
|
||||
} else {
|
||||
ops.insert(ops.end(), { GGML_OP_SOFT_MAX });
|
||||
}
|
||||
const int i_probs = i + (int) ops.size() - 1; // last node of the gating activation
|
||||
|
||||
if (args.prob_bias) {
|
||||
bias = cgraph->nodes[i + 2]->src[1];
|
||||
ops.insert(ops.end(), { gating_op, GGML_OP_RESHAPE, GGML_OP_ADD, GGML_OP_ARGSORT, GGML_OP_VIEW,
|
||||
bias = cgraph->nodes[i_probs + 2]->src[1];
|
||||
ops.insert(ops.end(), { GGML_OP_RESHAPE, GGML_OP_ADD, GGML_OP_ARGSORT, GGML_OP_VIEW,
|
||||
GGML_OP_GET_ROWS });
|
||||
out_nodes[0] = i + 4;
|
||||
ids = cgraph->nodes[i + 4];
|
||||
out_nodes[0] = i_probs + 4;
|
||||
} else {
|
||||
ops.insert(ops.end(),
|
||||
{ gating_op, GGML_OP_RESHAPE, GGML_OP_ARGSORT, GGML_OP_VIEW, GGML_OP_GET_ROWS });
|
||||
out_nodes[0] = i + 3;
|
||||
ids = cgraph->nodes[i + 3];
|
||||
ops.insert(ops.end(), { GGML_OP_RESHAPE, GGML_OP_ARGSORT, GGML_OP_VIEW, GGML_OP_GET_ROWS });
|
||||
out_nodes[0] = i_probs + 3;
|
||||
}
|
||||
ids = cgraph->nodes[out_nodes[0]];
|
||||
|
||||
if (args.norm) {
|
||||
ops.insert(ops.end(),
|
||||
@@ -4831,7 +4845,25 @@ static bool ggml_backend_cuda_device_supports_op(ggml_backend_dev_t dev, const g
|
||||
case GGML_TYPE_Q5_0:
|
||||
case GGML_TYPE_Q5_1:
|
||||
case GGML_TYPE_Q8_0:
|
||||
case GGML_TYPE_Q2_K:
|
||||
case GGML_TYPE_Q3_K:
|
||||
case GGML_TYPE_Q4_K:
|
||||
case GGML_TYPE_Q5_K:
|
||||
case GGML_TYPE_Q6_K:
|
||||
case GGML_TYPE_IQ2_XXS:
|
||||
case GGML_TYPE_IQ2_XS:
|
||||
case GGML_TYPE_IQ2_S:
|
||||
case GGML_TYPE_IQ3_XXS:
|
||||
case GGML_TYPE_IQ3_S:
|
||||
case GGML_TYPE_IQ1_S:
|
||||
case GGML_TYPE_IQ1_M:
|
||||
case GGML_TYPE_IQ4_XS:
|
||||
return true;
|
||||
case GGML_TYPE_IQ4_NL:
|
||||
case GGML_TYPE_MXFP4:
|
||||
// 32-value sub-blocks, the row size does not guarantee
|
||||
// the QK_K super-blocks the get_rows kernel iterates on
|
||||
return op->src[0]->ne[0] % QK_K == 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
// Kernel config struct - passed by value to CUDA kernel
|
||||
struct topk_moe_config {
|
||||
bool use_sigmoid;
|
||||
bool use_sqrt_softplus;
|
||||
bool with_norm;
|
||||
bool delayed_softmax;
|
||||
};
|
||||
@@ -67,6 +68,16 @@ __device__ void sigmoid_warp_inplace(float (&vals)[experts_per_thread], const in
|
||||
}
|
||||
}
|
||||
|
||||
template <int experts_per_thread, bool use_limit>
|
||||
__device__ void sqrt_softplus_warp_inplace(float (&vals)[experts_per_thread], const int limit, const int lane) {
|
||||
#pragma unroll
|
||||
for (int i = 0; i < experts_per_thread; i++) {
|
||||
const int idx = lane + i * WARP_SIZE;
|
||||
const bool active = !use_limit || (idx < limit);
|
||||
vals[i] = active ? sqrtf(vals[i] > 20.0f ? vals[i] : logf(1.0f + expf(vals[i]))) : -INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
This kernel does the following:
|
||||
1. optionally softmax over the logits per token [n_experts, n_tokens]
|
||||
@@ -115,6 +126,8 @@ __launch_bounds__(4 * WARP_SIZE, 1) __global__ void topk_moe_cuda(const float *
|
||||
if (!config.delayed_softmax) {
|
||||
if (config.use_sigmoid) {
|
||||
sigmoid_warp_inplace<experts_per_thread, false>(wt, n_experts, threadIdx.x);
|
||||
} else if (config.use_sqrt_softplus) {
|
||||
sqrt_softplus_warp_inplace<experts_per_thread, false>(wt, n_experts, threadIdx.x);
|
||||
} else {
|
||||
softmax_warp_inplace<experts_per_thread, false>(wt, n_experts, threadIdx.x);
|
||||
}
|
||||
@@ -364,9 +377,10 @@ void ggml_cuda_op_topk_moe(ggml_backend_cuda_context & ctx,
|
||||
}
|
||||
|
||||
topk_moe_config config;
|
||||
config.use_sigmoid = args.sigmoid;
|
||||
config.with_norm = with_norm;
|
||||
config.delayed_softmax = args.delayed_softmax;
|
||||
config.use_sigmoid = args.sigmoid;
|
||||
config.use_sqrt_softplus = args.sqrt_softplus;
|
||||
config.with_norm = with_norm;
|
||||
config.delayed_softmax = args.delayed_softmax;
|
||||
|
||||
if (bias) {
|
||||
launch_topk_moe_cuda<true>(ctx, logits_d, weights_d, ids_d, bias_d, n_rows, n_experts, n_expert_used, clamp_val,
|
||||
@@ -415,7 +429,7 @@ bool ggml_cuda_should_use_topk_moe(const ggml_tensor * gating_op,
|
||||
} else if (gating_op->op == GGML_OP_UNARY) {
|
||||
ggml_unary_op op = ggml_get_unary_op(gating_op);
|
||||
|
||||
if (op != GGML_UNARY_OP_SIGMOID) {
|
||||
if (op != GGML_UNARY_OP_SIGMOID && op != GGML_UNARY_OP_SOFTPLUS) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
struct ggml_cuda_topk_moe_args {
|
||||
bool sigmoid{};
|
||||
bool sqrt_softplus{};
|
||||
bool softmax{};
|
||||
bool delayed_softmax{};
|
||||
bool prob_bias{};
|
||||
|
||||
@@ -1286,7 +1286,8 @@ struct ggml_hexagon_opbatch {
|
||||
int64_t nb2 = is_repack ? nb1 * ne1 : t->nb[2];
|
||||
int64_t nb3 = is_repack ? nb2 * t->ne[2] : t->nb[3];
|
||||
|
||||
return (h->ne[0] == ne0) && (h->ne[1] == ne1) && (h->ne[2] == t->ne[2]) && (h->ne[3] == t->ne[3]) &&
|
||||
return (h->type == t->type) &&
|
||||
(h->ne[0] == ne0) && (h->ne[1] == ne1) && (h->ne[2] == t->ne[2]) && (h->ne[3] == t->ne[3]) &&
|
||||
(h->nb[0] == t->nb[0]) && (h->nb[1] == nb1) && (h->nb[2] == nb2) && (h->nb[3] == nb3);
|
||||
}
|
||||
|
||||
@@ -3476,6 +3477,7 @@ static htp_op_code op_remap_to_htp(const ggml_tensor * t) {
|
||||
case GGML_OP_RMS_NORM: return HTP_OP_RMS_NORM;
|
||||
case GGML_OP_CONCAT: return HTP_OP_CONCAT;
|
||||
case GGML_OP_SCALE: return HTP_OP_SCALE;
|
||||
case GGML_OP_CLAMP: return HTP_OP_CLAMP;
|
||||
case GGML_OP_SQR: return HTP_OP_SQR;
|
||||
case GGML_OP_SQRT: return HTP_OP_SQRT;
|
||||
case GGML_OP_SOFT_MAX: return HTP_OP_SOFTMAX;
|
||||
@@ -4126,6 +4128,7 @@ static bool ggml_backend_hexagon_device_supports_op(ggml_backend_dev_t dev, cons
|
||||
case GGML_OP_L2_NORM:
|
||||
case GGML_OP_RMS_NORM:
|
||||
case GGML_OP_SCALE:
|
||||
case GGML_OP_CLAMP:
|
||||
supp = ggml_hexagon_supported_unary(sess, op);
|
||||
break;
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ enum htp_op_code {
|
||||
HTP_OP_PAD,
|
||||
HTP_OP_NORM,
|
||||
HTP_OP_CONCAT,
|
||||
HTP_OP_CLAMP,
|
||||
|
||||
HTP_OP_INVALID
|
||||
};
|
||||
|
||||
@@ -718,6 +718,7 @@ static int execute_op(struct htp_ops_context * octx) {
|
||||
case HTP_OP_RMS_NORM:
|
||||
case HTP_OP_RMS_NORM_MUL:
|
||||
case HTP_OP_SCALE:
|
||||
case HTP_OP_CLAMP:
|
||||
case HTP_OP_SQR:
|
||||
case HTP_OP_SQRT:
|
||||
case HTP_OP_UNARY_SOFTPLUS:
|
||||
|
||||
@@ -138,6 +138,24 @@ static void scale_f32(const float * restrict src,
|
||||
}
|
||||
}
|
||||
|
||||
static void clamp_f32(const float * restrict src,
|
||||
float * restrict dst,
|
||||
const uint32_t num_rows,
|
||||
const struct htp_unary_context * uctx) {
|
||||
htp_unary_op_preamble;
|
||||
float min = 0.f;
|
||||
float max = 0.f;
|
||||
memcpy(&min, &op_params[0], sizeof(float));
|
||||
memcpy(&max, &op_params[1], sizeof(float));
|
||||
|
||||
for (uint32_t ir = 0; ir < num_rows; ir++) {
|
||||
const uint8_t * restrict src_local = (const uint8_t *)src + (ir * src0_row_size_aligned);
|
||||
uint8_t * restrict dst_local = (uint8_t *)dst + (ir * dst_row_size_aligned);
|
||||
|
||||
hvx_clamp_scalar_f32(dst_local, src_local, min, max, ne0);
|
||||
}
|
||||
}
|
||||
|
||||
static void rms_norm_f32(const float * restrict src,
|
||||
float * restrict dst,
|
||||
const uint32_t num_rows,
|
||||
@@ -542,6 +560,7 @@ DEFINE_UNARY_TASK(norm, false, false, norm_f32(src0_vtcm, dst_vtcm, bl
|
||||
DEFINE_UNARY_TASK(rms_norm, false, false, rms_norm_f32(src0_vtcm, dst_vtcm, block_size, uctx))
|
||||
DEFINE_UNARY_TASK(rms_norm_mul, true, false, rms_norm_mul_f32(src0_vtcm, uctx->broadcast_weight ? (const float *) src1_vtcm_data : src1_vtcm, dst_vtcm, block_size, uctx))
|
||||
DEFINE_UNARY_TASK(scale, false, false, scale_f32(src0_vtcm, dst_vtcm, block_size, uctx))
|
||||
DEFINE_UNARY_TASK(clamp, false, false, clamp_f32(src0_vtcm, dst_vtcm, block_size, uctx))
|
||||
DEFINE_UNARY_TASK(sqr, false, false, sqr_f32(src0_vtcm, dst_vtcm, block_size, uctx))
|
||||
DEFINE_UNARY_TASK(sqrt, false, false, sqrt_f32(src0_vtcm, dst_vtcm, block_size, uctx))
|
||||
DEFINE_UNARY_TASK(unary_neg, false, false, neg_f32(src0_vtcm, dst_vtcm, block_size, uctx))
|
||||
@@ -681,6 +700,14 @@ static inline void tile_scale_f32(uint8_t * dst_vtcm, const uint8_t * src_vtcm,
|
||||
hvx_scale_offset_f32_aa(dst_vtcm, src_vtcm, tw, scale, bias);
|
||||
}
|
||||
|
||||
static inline void tile_clamp_f32(uint8_t * dst_vtcm, const uint8_t * src_vtcm, uint32_t tw, const int32_t * op_params) {
|
||||
float min = 0.f;
|
||||
float max = 0.f;
|
||||
memcpy(&min, &op_params[0], sizeof(float));
|
||||
memcpy(&max, &op_params[1], sizeof(float));
|
||||
hvx_clamp_scalar_f32(dst_vtcm, src_vtcm, min, max, tw);
|
||||
}
|
||||
|
||||
static inline void tile_unary_softplus_f32(uint8_t * dst_vtcm, const uint8_t * src_vtcm, uint32_t tw) {
|
||||
const float * restrict sf = (const float *) src_vtcm;
|
||||
float * restrict df = (float *) dst_vtcm;
|
||||
@@ -765,6 +792,7 @@ static inline void tri_apply_tile_f32(const uint8_t * restrict src, uint8_t * re
|
||||
}
|
||||
|
||||
DEFINE_UNARY_TILED_TASK(scale, false, tile_scale_f32(dst_vtcm, src_vtcm, tw, op_params))
|
||||
DEFINE_UNARY_TILED_TASK(clamp, false, tile_clamp_f32(dst_vtcm, src_vtcm, tw, op_params))
|
||||
DEFINE_UNARY_TILED_TASK(sqr, false, hvx_sqr_f32_aa(dst_vtcm, src_vtcm, tw))
|
||||
DEFINE_UNARY_TILED_TASK(sqrt, false, hvx_sqrt_f32_aa(dst_vtcm, src_vtcm, tw))
|
||||
DEFINE_UNARY_TILED_TASK(unary_neg, false, hvx_scale_f32_aa(dst_vtcm, src_vtcm, tw, -1.0f))
|
||||
@@ -787,6 +815,7 @@ static int execute_op_unary_f32(struct htp_ops_context * octx) {
|
||||
case HTP_OP_RMS_NORM: op_type = "rmsnorm-f32"; break;
|
||||
case HTP_OP_RMS_NORM_MUL: op_type = "rmsnorm-mul-f32"; break;
|
||||
case HTP_OP_SCALE: op_type = "scale-f32"; break;
|
||||
case HTP_OP_CLAMP: op_type = "clamp-f32"; break;
|
||||
case HTP_OP_SQR: op_type = "sqr-f32"; break;
|
||||
case HTP_OP_SQRT: op_type = "sqrt-f32"; break;
|
||||
case HTP_OP_UNARY_NEG: op_type = "neg-f32"; break;
|
||||
@@ -882,6 +911,7 @@ static int execute_op_unary_f32(struct htp_ops_context * octx) {
|
||||
if (col_tile) {
|
||||
switch (octx->op) {
|
||||
case HTP_OP_SCALE: task_func = unary_task_f32_tiled_scale; break;
|
||||
case HTP_OP_CLAMP: task_func = unary_task_f32_tiled_clamp; break;
|
||||
case HTP_OP_SQR: task_func = unary_task_f32_tiled_sqr; break;
|
||||
case HTP_OP_SQRT: task_func = unary_task_f32_tiled_sqrt; break;
|
||||
case HTP_OP_UNARY_NEG: task_func = unary_task_f32_tiled_unary_neg; break;
|
||||
@@ -898,6 +928,7 @@ static int execute_op_unary_f32(struct htp_ops_context * octx) {
|
||||
case HTP_OP_RMS_NORM: task_func = unary_task_f32_rms_norm; break;
|
||||
case HTP_OP_RMS_NORM_MUL: task_func = unary_task_f32_rms_norm_mul; break;
|
||||
case HTP_OP_SCALE: task_func = unary_task_f32_scale; break;
|
||||
case HTP_OP_CLAMP: task_func = unary_task_f32_clamp; break;
|
||||
case HTP_OP_SQR: task_func = unary_task_f32_sqr; break;
|
||||
case HTP_OP_SQRT: task_func = unary_task_f32_sqrt; break;
|
||||
case HTP_OP_UNARY_NEG: task_func = unary_task_f32_unary_neg; break;
|
||||
|
||||
@@ -41,6 +41,7 @@ _Static_assert(sizeof(struct htp_unary_kernel_params) <= 128, "htp_unary_kernel_
|
||||
|
||||
static inline bool htp_op_is_unary(uint32_t opcode) {
|
||||
switch (opcode) {
|
||||
case HTP_OP_CLAMP:
|
||||
case HTP_OP_NORM:
|
||||
case HTP_OP_RMS_NORM:
|
||||
case HTP_OP_RMS_NORM_MUL:
|
||||
|
||||
@@ -17085,9 +17085,6 @@ static void ggml_cl_mul_mat_q8_0_f32_adreno(ggml_backend_t backend, const ggml_t
|
||||
cl_ulong offset1 = extra1->offset + src1->view_offs;
|
||||
cl_ulong offsetd = extrad->offset + dst->view_offs;
|
||||
|
||||
GGML_ASSERT(src1->view_offs == 0);
|
||||
GGML_ASSERT(dst->view_offs == 0);
|
||||
|
||||
const int ne00 = src0->ne[0];
|
||||
const int ne01 = src0->ne[1];
|
||||
const int ne02 = src0->ne[2];
|
||||
@@ -17148,9 +17145,9 @@ static void ggml_cl_mul_mat_q8_0_f32_adreno(ggml_backend_t backend, const ggml_t
|
||||
CL_CHECK(clSetKernelArg(kernel, 0, sizeof(cl_mem), &q_img));
|
||||
CL_CHECK(clSetKernelArg(kernel, 1, sizeof(cl_mem), &extra0_q8_0->d));
|
||||
CL_CHECK(clSetKernelArg(kernel, 2, sizeof(cl_mem), &b_img));
|
||||
CL_CHECK(clSetKernelArg(kernel, 3, sizeof(cl_ulong), &extra1->offset));
|
||||
CL_CHECK(clSetKernelArg(kernel, 3, sizeof(cl_ulong), &offset1));
|
||||
CL_CHECK(clSetKernelArg(kernel, 4, sizeof(cl_mem), &extrad->data_device));
|
||||
CL_CHECK(clSetKernelArg(kernel, 5, sizeof(cl_ulong), &extrad->offset));
|
||||
CL_CHECK(clSetKernelArg(kernel, 5, sizeof(cl_ulong), &offsetd));
|
||||
CL_CHECK(clSetKernelArg(kernel, 6, sizeof(int), &ne00));
|
||||
CL_CHECK(clSetKernelArg(kernel, 7, sizeof(int), &ne01));
|
||||
CL_CHECK(clSetKernelArg(kernel, 8, sizeof(int), &ne02));
|
||||
@@ -18545,6 +18542,26 @@ static void ggml_cl_mul_mat(ggml_backend_t backend, const ggml_tensor * src0, co
|
||||
|
||||
GGML_ASSERT(ne00 == ne10);
|
||||
|
||||
#ifdef GGML_OPENCL_USE_ADRENO_KERNELS
|
||||
// adreno GEMM/GEMV kernels do not support broadcast, assuming ne2 and ne3 are 1 for src1
|
||||
// so we handle broadcast here
|
||||
if ((ne12 > 1 || ne13 > 1) && ne02 == 1 && ne03 == 1 &&
|
||||
src0t != GGML_TYPE_F16 && src0t != GGML_TYPE_F32) {
|
||||
for (int i13 = 0; i13 < ne13; ++i13) {
|
||||
for (int i12 = 0; i12 < ne12; ++i12) {
|
||||
ggml_tensor s1 = *src1;
|
||||
s1.ne[2] = 1; s1.ne[3] = 1;
|
||||
s1.view_offs = src1->view_offs + (size_t)i12*nb12 + (size_t)i13*nb13;
|
||||
ggml_tensor d = *dst;
|
||||
d.ne[2] = 1; d.ne[3] = 1;
|
||||
d.view_offs = dst->view_offs + (size_t)i12*nb2 + (size_t)i13*nb3;
|
||||
ggml_cl_mul_mat(backend, src0, &s1, &d);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
int nth0 = 32;
|
||||
int nth1 = 1;
|
||||
int nrows = 1;
|
||||
|
||||
@@ -1378,3 +1378,5 @@ GGML_BACKEND_API ggml_backend_reg_t ggml_backend_openvino_reg(void) {
|
||||
|
||||
return ®
|
||||
}
|
||||
|
||||
GGML_BACKEND_DL_IMPL(ggml_backend_openvino_reg)
|
||||
|
||||
@@ -156,6 +156,24 @@ typedef struct VkPhysicalDeviceShaderFloat8FeaturesEXT {
|
||||
} VkPhysicalDeviceShaderFloat8FeaturesEXT;
|
||||
#endif
|
||||
|
||||
#ifndef VK_KHR_INTERNALLY_SYNCHRONIZED_QUEUES_EXTENSION_NAME
|
||||
#define VK_KHR_INTERNALLY_SYNCHRONIZED_QUEUES_EXTENSION_NAME "VK_KHR_internally_synchronized_queues"
|
||||
#define VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_INTERNALLY_SYNCHRONIZED_QUEUES_FEATURES_KHR ((VkStructureType)1000504000)
|
||||
#define VK_DEVICE_QUEUE_CREATE_INTERNALLY_SYNCHRONIZED_BIT_KHR ((VkDeviceQueueCreateFlagBits)0x00000004)
|
||||
|
||||
// Compile-time constant guaranteed; no runtime initialization overhead
|
||||
static constexpr vk::DeviceQueueCreateFlagBits eInternallySynchronizedKHR =
|
||||
static_cast<vk::DeviceQueueCreateFlagBits>(0x00000004);
|
||||
|
||||
typedef struct VkPhysicalDeviceInternallySynchronizedQueuesFeaturesKHR {
|
||||
VkStructureType sType;
|
||||
void* pNext;
|
||||
VkBool32 internallySynchronizedQueues;
|
||||
} VkPhysicalDeviceInternallySynchronizedQueuesFeaturesKHR;
|
||||
#else
|
||||
static constexpr vk::DeviceQueueCreateFlagBits eInternallySynchronizedKHR = vk::DeviceQueueCreateFlagBits::eInternallySynchronizedKHR;
|
||||
#endif
|
||||
|
||||
#define ROUNDUP_POW2(M, N) (((M) + (N) - 1) & ~((N) - 1))
|
||||
#define CEIL_DIV(M, N) (((M) / (N)) + (((M) % (N)) != 0))
|
||||
static bool is_pow2(uint32_t x) { return x > 1 && (x & (x-1)) == 0; }
|
||||
@@ -285,27 +303,41 @@ struct vk_command_pool {
|
||||
};
|
||||
|
||||
// Prevent simultaneous submissions to the same queue.
|
||||
// This could be per vk_queue if we stopped having two vk_queue structures
|
||||
// sharing the same vk::Queue.
|
||||
static std::mutex queue_mutex;
|
||||
struct vk_queue_handle {
|
||||
vk::Queue queue;
|
||||
virtual void submit(vk::ArrayProxy<const vk::SubmitInfo> submits, vk::Fence fence) = 0;
|
||||
virtual void lock() {} // no-op by default (internally synchronized case)
|
||||
virtual void unlock() {}
|
||||
virtual ~vk_queue_handle() = default;
|
||||
};
|
||||
|
||||
struct vk_queue_handle_synchronized : vk_queue_handle {
|
||||
std::mutex mutex;
|
||||
void submit(vk::ArrayProxy<const vk::SubmitInfo> submits, vk::Fence fence) override {
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
queue.submit(submits, fence);
|
||||
}
|
||||
void lock() override { mutex.lock(); }
|
||||
void unlock() override { mutex.unlock(); }
|
||||
};
|
||||
|
||||
struct vk_queue_handle_unsynchronized : vk_queue_handle {
|
||||
void submit(vk::ArrayProxy<const vk::SubmitInfo> submits, vk::Fence fence) override {
|
||||
// Driver guarantees internal synchronization via VK_KHR_internally_synchronized_queues
|
||||
queue.submit(submits, fence);
|
||||
}
|
||||
// lock()/unlock() inherited no-ops
|
||||
};
|
||||
|
||||
struct vk_queue {
|
||||
uint32_t queue_family_index;
|
||||
vk::Queue queue;
|
||||
std::shared_ptr<vk_queue_handle> handle;
|
||||
|
||||
vk_command_pool cmd_pool;
|
||||
|
||||
vk::PipelineStageFlags stage_flags;
|
||||
|
||||
bool transfer_only;
|
||||
|
||||
// copy everything except the cmd_pool
|
||||
void copyFrom(vk_queue &other) {
|
||||
queue_family_index = other.queue_family_index;
|
||||
queue = other.queue;
|
||||
stage_flags = other.stage_flags;
|
||||
transfer_only = other.transfer_only;
|
||||
}
|
||||
};
|
||||
|
||||
static const char * ggml_backend_vk_buffer_type_name(ggml_backend_buffer_type_t buft);
|
||||
@@ -712,11 +744,12 @@ struct vk_device_struct {
|
||||
uint32_t vendor_id;
|
||||
vk::DriverId driver_id;
|
||||
vk_device_architecture architecture;
|
||||
vk_queue compute_queue;
|
||||
vk_queue transfer_queue;
|
||||
std::unique_ptr<vk_queue> compute_queue;
|
||||
std::unique_ptr<vk_queue> transfer_queue;
|
||||
bool single_queue;
|
||||
bool support_async;
|
||||
bool async_use_transfer_queue;
|
||||
bool has_internally_synchronized_queues = false;
|
||||
uint32_t subgroup_size;
|
||||
uint32_t subgroup_size_log2;
|
||||
uint32_t shader_core_count;
|
||||
@@ -1019,8 +1052,13 @@ struct vk_device_struct {
|
||||
|
||||
ggml_vk_destroy_buffer(sync_staging);
|
||||
|
||||
compute_queue.cmd_pool.destroy(device);
|
||||
transfer_queue.cmd_pool.destroy(device);
|
||||
if (compute_queue) compute_queue->cmd_pool.destroy(device);
|
||||
if (transfer_queue) transfer_queue->cmd_pool.destroy(device);
|
||||
|
||||
// Explicitly clear to ensure queues drop their shared_ptrs to handles
|
||||
// before the Vulkan logical device instance is destroyed
|
||||
compute_queue.reset();
|
||||
transfer_queue.reset();
|
||||
|
||||
for (auto& pipeline : all_pipelines) {
|
||||
if (pipeline.expired()) {
|
||||
@@ -2909,8 +2947,7 @@ static vk_command_buffer* ggml_vk_create_cmd_buffer(vk_device& device, vk_comman
|
||||
static void ggml_vk_submit(vk_context& ctx, vk::Fence fence) {
|
||||
if (ctx->seqs.empty()) {
|
||||
if (fence) {
|
||||
std::lock_guard<std::mutex> guard(queue_mutex);
|
||||
ctx->p->q->queue.submit({}, fence);
|
||||
ctx->p->q->handle->submit({}, fence);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -2979,8 +3016,7 @@ static void ggml_vk_submit(vk_context& ctx, vk::Fence fence) {
|
||||
}
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> guard(queue_mutex);
|
||||
ctx->p->q->queue.submit(submit_infos, fence);
|
||||
ctx->p->q->handle->submit(submit_infos, fence);
|
||||
|
||||
ctx->seqs.clear();
|
||||
}
|
||||
@@ -3031,18 +3067,44 @@ static uint32_t ggml_vk_find_queue_family_index(std::vector<vk::QueueFamilyPrope
|
||||
abort();
|
||||
}
|
||||
|
||||
static void ggml_vk_create_queue(vk_device& device, vk_queue& q, uint32_t queue_family_index, uint32_t queue_index, vk::PipelineStageFlags&& stage_flags, bool transfer_only) {
|
||||
static std::unique_ptr<vk_queue> ggml_vk_create_queue(vk_device& device, uint32_t queue_family_index, uint32_t queue_index, vk::PipelineStageFlags&& stage_flags, bool transfer_only) {
|
||||
VK_LOG_DEBUG("ggml_vk_create_queue()");
|
||||
std::lock_guard<std::recursive_mutex> guard(device->mutex);
|
||||
|
||||
q.queue_family_index = queue_family_index;
|
||||
q.transfer_only = transfer_only;
|
||||
auto q = std::make_unique<vk_queue>();
|
||||
q->queue_family_index = queue_family_index;
|
||||
q->transfer_only = transfer_only;
|
||||
|
||||
q.cmd_pool.init(device, &q);
|
||||
std::shared_ptr<vk_queue_handle> h;
|
||||
vk::DeviceQueueInfo2 queue_info2{};
|
||||
queue_info2.queueFamilyIndex = queue_family_index;
|
||||
queue_info2.queueIndex = queue_index;
|
||||
|
||||
q.queue = device->device.getQueue(queue_family_index, queue_index);
|
||||
if (device->has_internally_synchronized_queues) {
|
||||
h = std::make_shared<vk_queue_handle_unsynchronized>();
|
||||
queue_info2.flags = eInternallySynchronizedKHR;
|
||||
} else {
|
||||
h = std::make_shared<vk_queue_handle_synchronized>();
|
||||
}
|
||||
|
||||
q.stage_flags = stage_flags;
|
||||
h->queue = device->device.getQueue2(queue_info2);
|
||||
q->handle = h;
|
||||
|
||||
q->cmd_pool.init(device, q.get());
|
||||
|
||||
q->stage_flags = stage_flags;
|
||||
return q;
|
||||
}
|
||||
|
||||
static std::unique_ptr<vk_queue> ggml_vk_create_aliased_queue(vk_device& device, const std::unique_ptr<vk_queue>& source) {
|
||||
std::lock_guard<std::recursive_mutex> guard(device->mutex);
|
||||
auto q = std::make_unique<vk_queue>();
|
||||
q->handle = source->handle;
|
||||
q->queue_family_index = source->queue_family_index;
|
||||
q->stage_flags = source->stage_flags;
|
||||
q->transfer_only = source->transfer_only;
|
||||
q->cmd_pool.init(device, q.get());
|
||||
return q;
|
||||
}
|
||||
|
||||
static vk_context ggml_vk_create_context(ggml_backend_vk_context * ctx, vk_command_pool& p) {
|
||||
@@ -3107,11 +3169,11 @@ static void ggml_vk_queue_command_pools_cleanup(vk_device& device) {
|
||||
// Arbitrary frequency to cleanup/reuse command buffers
|
||||
static constexpr uint32_t cleanup_frequency = 10;
|
||||
|
||||
if (device->compute_queue.cmd_pool.buffers_in_use() >= cleanup_frequency) {
|
||||
ggml_vk_command_pool_cleanup(device, device->compute_queue.cmd_pool);
|
||||
if (device->compute_queue->cmd_pool.buffers_in_use() >= cleanup_frequency) {
|
||||
ggml_vk_command_pool_cleanup(device, device->compute_queue->cmd_pool);
|
||||
}
|
||||
if (device->transfer_queue.cmd_pool.buffers_in_use() >= cleanup_frequency) {
|
||||
ggml_vk_command_pool_cleanup(device, device->transfer_queue.cmd_pool);
|
||||
if (device->transfer_queue->cmd_pool.buffers_in_use() >= cleanup_frequency) {
|
||||
ggml_vk_command_pool_cleanup(device, device->transfer_queue->cmd_pool);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5886,6 +5948,7 @@ static vk_device ggml_vk_get_device(size_t idx) {
|
||||
bool coopmat2_support = false;
|
||||
bool coopmat2_decode_vector_support = false;
|
||||
bool pipeline_executable_properties_support = false;
|
||||
bool internally_sync_support = false;
|
||||
device->coopmat_support = false;
|
||||
device->integer_dot_product = false;
|
||||
device->shader_64b_indexing = false;
|
||||
@@ -5957,6 +6020,8 @@ static vk_device ggml_vk_get_device(size_t idx) {
|
||||
} else if (strcmp("VK_EXT_shader_64bit_indexing", properties.extensionName) == 0) {
|
||||
device->shader_64b_indexing = true;
|
||||
#endif
|
||||
} else if (strcmp(VK_KHR_INTERNALLY_SYNCHRONIZED_QUEUES_EXTENSION_NAME, properties.extensionName) == 0) {
|
||||
internally_sync_support = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6143,14 +6208,6 @@ static vk_device ggml_vk_get_device(size_t idx) {
|
||||
device->single_queue = compute_queue_family_index == transfer_queue_family_index && queue_family_props[compute_queue_family_index].queueCount == 1;
|
||||
|
||||
std::vector<vk::DeviceQueueCreateInfo> device_queue_create_infos;
|
||||
if (compute_queue_family_index != transfer_queue_family_index) {
|
||||
device_queue_create_infos.push_back({vk::DeviceQueueCreateFlags(), compute_queue_family_index, 1, priorities});
|
||||
device_queue_create_infos.push_back({vk::DeviceQueueCreateFlags(), transfer_queue_family_index, 1, priorities + 1});
|
||||
} else if(!device->single_queue) {
|
||||
device_queue_create_infos.push_back({vk::DeviceQueueCreateFlags(), compute_queue_family_index, 2, priorities});
|
||||
} else {
|
||||
device_queue_create_infos.push_back({vk::DeviceQueueCreateFlags(), compute_queue_family_index, 1, priorities});
|
||||
}
|
||||
vk::DeviceCreateInfo device_create_info{};
|
||||
std::vector<const char *> device_extensions;
|
||||
vk::PhysicalDeviceFeatures device_features = device->physical_device.getFeatures();
|
||||
@@ -6172,6 +6229,17 @@ static vk_device ggml_vk_get_device(size_t idx) {
|
||||
|
||||
last_struct = (VkBaseOutStructure *)&vk12_features;
|
||||
|
||||
VkPhysicalDeviceInternallySynchronizedQueuesFeaturesKHR internally_synchronized_queues_features{};
|
||||
internally_synchronized_queues_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_INTERNALLY_SYNCHRONIZED_QUEUES_FEATURES_KHR;
|
||||
internally_synchronized_queues_features.pNext = nullptr;
|
||||
internally_synchronized_queues_features.internallySynchronizedQueues = VK_FALSE;
|
||||
|
||||
if (internally_sync_support) {
|
||||
last_struct->pNext = (VkBaseOutStructure *)&internally_synchronized_queues_features;
|
||||
last_struct = (VkBaseOutStructure *)&internally_synchronized_queues_features;
|
||||
device_extensions.push_back(VK_KHR_INTERNALLY_SYNCHRONIZED_QUEUES_EXTENSION_NAME);
|
||||
}
|
||||
|
||||
VkPhysicalDevicePipelineRobustnessFeaturesEXT pl_robustness_features;
|
||||
pl_robustness_features.pNext = nullptr;
|
||||
pl_robustness_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_ROBUSTNESS_FEATURES_EXT;
|
||||
@@ -6310,6 +6378,23 @@ static vk_device ggml_vk_get_device(size_t idx) {
|
||||
|
||||
vkGetPhysicalDeviceFeatures2(device->physical_device, &device_features2);
|
||||
|
||||
device->has_internally_synchronized_queues = internally_synchronized_queues_features.internallySynchronizedQueues;
|
||||
|
||||
// Build queue create infos only after querying whether internally synchronized queues are enabled.
|
||||
// getQueue2() later uses the same flag, so creation/retrieval must stay consistent.
|
||||
vk::DeviceQueueCreateFlags queue_flags = device->has_internally_synchronized_queues ?
|
||||
eInternallySynchronizedKHR :
|
||||
vk::DeviceQueueCreateFlags();
|
||||
|
||||
if (compute_queue_family_index != transfer_queue_family_index) {
|
||||
device_queue_create_infos.push_back({queue_flags, compute_queue_family_index, 1, priorities});
|
||||
device_queue_create_infos.push_back({queue_flags, transfer_queue_family_index, 1, priorities + 1});
|
||||
} else if(!device->single_queue) {
|
||||
device_queue_create_infos.push_back({queue_flags, compute_queue_family_index, 2, priorities});
|
||||
} else {
|
||||
device_queue_create_infos.push_back({queue_flags, compute_queue_family_index, 1, priorities});
|
||||
}
|
||||
|
||||
device->pipeline_executable_properties_support = pipeline_executable_properties_support;
|
||||
|
||||
device->fp16 = device->fp16 && vk12_features.shaderFloat16;
|
||||
@@ -6592,7 +6677,7 @@ static vk_device ggml_vk_get_device(size_t idx) {
|
||||
device->device = device->physical_device.createDevice(device_create_info);
|
||||
|
||||
// Queues
|
||||
ggml_vk_create_queue(device, device->compute_queue, compute_queue_family_index, 0, { vk::PipelineStageFlagBits::eComputeShader | vk::PipelineStageFlagBits::eTransfer }, false);
|
||||
device->compute_queue = ggml_vk_create_queue(device, compute_queue_family_index, 0, { vk::PipelineStageFlagBits::eComputeShader | vk::PipelineStageFlagBits::eTransfer }, false);
|
||||
|
||||
// Shaders
|
||||
// Disable matmul tile sizes early if performance low or not supported
|
||||
@@ -6694,13 +6779,11 @@ static vk_device ggml_vk_get_device(size_t idx) {
|
||||
|
||||
if (!device->single_queue) {
|
||||
const uint32_t transfer_queue_index = compute_queue_family_index == transfer_queue_family_index ? 1 : 0;
|
||||
ggml_vk_create_queue(device, device->transfer_queue, transfer_queue_family_index, transfer_queue_index, { vk::PipelineStageFlagBits::eTransfer }, true);
|
||||
device->transfer_queue = ggml_vk_create_queue(device, transfer_queue_family_index, transfer_queue_index, { vk::PipelineStageFlagBits::eTransfer }, true);
|
||||
|
||||
device->async_use_transfer_queue = prefers_transfer_queue || (getenv("GGML_VK_ASYNC_USE_TRANSFER_QUEUE") != nullptr);
|
||||
} else {
|
||||
// TODO: Use pointer or reference to avoid copy
|
||||
device->transfer_queue.copyFrom(device->compute_queue);
|
||||
device->transfer_queue.cmd_pool.init(device, &device->transfer_queue);
|
||||
device->transfer_queue = ggml_vk_create_aliased_queue(device, device->compute_queue);
|
||||
|
||||
device->async_use_transfer_queue = false;
|
||||
}
|
||||
@@ -7263,7 +7346,7 @@ static void ggml_vk_init(ggml_backend_vk_context * ctx, size_t idx) {
|
||||
ctx->fence = ctx->device->device.createFence({});
|
||||
ctx->almost_ready_fence = ctx->device->device.createFence({});
|
||||
|
||||
ctx->compute_cmd_pool.init(ctx->device, &ctx->device->compute_queue);
|
||||
ctx->compute_cmd_pool.init(ctx->device, ctx->device->compute_queue.get());
|
||||
if (ctx->device->async_use_transfer_queue) {
|
||||
vk::SemaphoreTypeCreateInfo tci{ vk::SemaphoreType::eTimeline, 0 };
|
||||
vk::SemaphoreCreateInfo ci{};
|
||||
@@ -7271,7 +7354,7 @@ static void ggml_vk_init(ggml_backend_vk_context * ctx, size_t idx) {
|
||||
ctx->transfer_semaphore.s = ctx->device->device.createSemaphore(ci);
|
||||
ctx->transfer_semaphore.value = 0;
|
||||
|
||||
ctx->transfer_cmd_pool.init(ctx->device, &ctx->device->transfer_queue);
|
||||
ctx->transfer_cmd_pool.init(ctx->device, ctx->device->transfer_queue.get());
|
||||
}
|
||||
|
||||
if (vk_perf_logger_enabled) {
|
||||
@@ -8126,7 +8209,7 @@ static void ggml_vk_buffer_write_2d(vk_buffer& dst, size_t offset, const void *
|
||||
} else {
|
||||
std::lock_guard<std::recursive_mutex> guard(dst->device->mutex);
|
||||
|
||||
vk_context subctx = ggml_vk_create_temporary_context(dst->device->transfer_queue.cmd_pool);
|
||||
vk_context subctx = ggml_vk_create_temporary_context(dst->device->transfer_queue->cmd_pool);
|
||||
ggml_vk_ctx_begin(dst->device, subctx);
|
||||
bool ret = ggml_vk_buffer_write_2d_async(subctx, dst, offset, src, spitch, dpitch, width, height, true);
|
||||
GGML_ASSERT(ret);
|
||||
@@ -8241,7 +8324,7 @@ static void ggml_vk_buffer_read_2d(vk_buffer& src, size_t offset, void * dst, si
|
||||
GGML_ASSERT(src->memory_property_flags & vk::MemoryPropertyFlagBits::eHostCoherent);
|
||||
|
||||
std::lock_guard<std::recursive_mutex> guard(src->device->mutex);
|
||||
vk_context subctx = ggml_vk_create_temporary_context(src->device->compute_queue.cmd_pool);
|
||||
vk_context subctx = ggml_vk_create_temporary_context(src->device->compute_queue->cmd_pool);
|
||||
ggml_vk_ctx_begin(src->device, subctx);
|
||||
subctx->s->buffer->buf.pipelineBarrier(
|
||||
vk::PipelineStageFlagBits::eComputeShader | vk::PipelineStageFlagBits::eTransfer,
|
||||
@@ -8267,7 +8350,7 @@ static void ggml_vk_buffer_read_2d(vk_buffer& src, size_t offset, void * dst, si
|
||||
} else {
|
||||
std::lock_guard<std::recursive_mutex> guard(src->device->mutex);
|
||||
|
||||
vk_context subctx = ggml_vk_create_temporary_context(src->device->transfer_queue.cmd_pool);
|
||||
vk_context subctx = ggml_vk_create_temporary_context(src->device->transfer_queue->cmd_pool);
|
||||
ggml_vk_ctx_begin(src->device, subctx);
|
||||
bool ret = ggml_vk_buffer_read_2d_async(subctx, src, offset, dst, spitch, dpitch, width, height, true);
|
||||
GGML_ASSERT(ret);
|
||||
@@ -8304,7 +8387,7 @@ static void ggml_vk_buffer_copy(vk_buffer& dst, size_t dst_offset, vk_buffer& sr
|
||||
std::lock_guard<std::recursive_mutex> guard(src->device->mutex);
|
||||
VK_LOG_DEBUG("ggml_vk_buffer_copy(SINGLE_DEVICE, " << size << ")");
|
||||
// Copy within the device
|
||||
vk_context subctx = ggml_vk_create_temporary_context(src->device->transfer_queue.cmd_pool);
|
||||
vk_context subctx = ggml_vk_create_temporary_context(src->device->transfer_queue->cmd_pool);
|
||||
ggml_vk_ctx_begin(src->device, subctx);
|
||||
ggml_vk_buffer_copy_async(subctx, dst, dst_offset, src, src_offset, size);
|
||||
ggml_vk_ctx_end(subctx);
|
||||
@@ -8347,7 +8430,7 @@ static void ggml_vk_buffer_memset(vk_buffer& dst, size_t offset, uint32_t c, siz
|
||||
}
|
||||
|
||||
std::lock_guard<std::recursive_mutex> guard(dst->device->mutex);
|
||||
vk_context subctx = ggml_vk_create_temporary_context(dst->device->transfer_queue.cmd_pool);
|
||||
vk_context subctx = ggml_vk_create_temporary_context(dst->device->transfer_queue->cmd_pool);
|
||||
ggml_vk_ctx_begin(dst->device, subctx);
|
||||
subctx->s->buffer->buf.fillBuffer(dst->buffer, offset, size, c);
|
||||
ggml_vk_ctx_end(subctx);
|
||||
@@ -15875,19 +15958,17 @@ static void ggml_vk_synchronize(ggml_backend_vk_context * ctx) {
|
||||
1, &ctx->transfer_semaphore.value,
|
||||
0, nullptr,
|
||||
};
|
||||
vk::PipelineStageFlags stage = ctx->device->transfer_queue.stage_flags;
|
||||
vk::PipelineStageFlags stage = ctx->device->transfer_queue->stage_flags;
|
||||
vk::SubmitInfo si{
|
||||
1, &ctx->transfer_semaphore.s, &stage,
|
||||
0, nullptr,
|
||||
0, nullptr,
|
||||
};
|
||||
si.setPNext(&tl_info);
|
||||
std::lock_guard<std::mutex> guard(queue_mutex);
|
||||
ctx->device->compute_queue.queue.submit({ si }, ctx->fence);
|
||||
ctx->device->compute_queue->handle->submit({ si }, ctx->fence);
|
||||
ctx->transfer_semaphore_last_submitted = ctx->transfer_semaphore.value;
|
||||
} else {
|
||||
std::lock_guard<std::mutex> guard(queue_mutex);
|
||||
ctx->device->compute_queue.queue.submit({}, ctx->fence);
|
||||
ctx->device->compute_queue->handle->submit({}, ctx->fence);
|
||||
}
|
||||
ggml_vk_wait_for_fence(ctx);
|
||||
ctx->submit_pending = false;
|
||||
@@ -16449,7 +16530,9 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg
|
||||
vk::DebugUtilsLabelEXT dul = {};
|
||||
dul.pLabelName = "ggml_backend_vk_graph_compute";
|
||||
dul.color = std::array<float,4>{1.0f, 1.0f, 1.0f, 1.0f};
|
||||
vk_instance.pfn_vkQueueBeginDebugUtilsLabelEXT(ctx->device->compute_queue.queue, reinterpret_cast<VkDebugUtilsLabelEXT*>(&dul));
|
||||
|
||||
std::lock_guard<vk_queue_handle> guard(*ctx->device->compute_queue->handle);
|
||||
vk_instance.pfn_vkQueueBeginDebugUtilsLabelEXT(ctx->device->compute_queue->handle->queue, reinterpret_cast<VkDebugUtilsLabelEXT*>(&dul));
|
||||
}
|
||||
|
||||
ctx->prealloc_size_add_rms_partials_offset = 0;
|
||||
|
||||
@@ -355,6 +355,30 @@ struct ggml_webgpu_conv2d_pipeline_key_hash {
|
||||
}
|
||||
};
|
||||
|
||||
// Same type fields as conv2d plus the input layout (WHCN vs CWHN).
|
||||
struct ggml_webgpu_conv2d_dw_pipeline_key {
|
||||
ggml_type weight_type;
|
||||
ggml_type input_type;
|
||||
ggml_type output_type;
|
||||
bool whcn;
|
||||
|
||||
bool operator==(const ggml_webgpu_conv2d_dw_pipeline_key & other) const {
|
||||
return weight_type == other.weight_type && input_type == other.input_type && output_type == other.output_type &&
|
||||
whcn == other.whcn;
|
||||
}
|
||||
};
|
||||
|
||||
struct ggml_webgpu_conv2d_dw_pipeline_key_hash {
|
||||
size_t operator()(const ggml_webgpu_conv2d_dw_pipeline_key & key) const {
|
||||
size_t seed = 0;
|
||||
ggml_webgpu_hash_combine(seed, key.weight_type);
|
||||
ggml_webgpu_hash_combine(seed, key.input_type);
|
||||
ggml_webgpu_hash_combine(seed, key.output_type);
|
||||
ggml_webgpu_hash_combine(seed, key.whcn);
|
||||
return seed;
|
||||
}
|
||||
};
|
||||
|
||||
/** Im2Col **/
|
||||
struct ggml_webgpu_im2col_pipeline_key {
|
||||
ggml_type input_type;
|
||||
@@ -1210,6 +1234,8 @@ class ggml_webgpu_shader_lib {
|
||||
soft_max_pipelines;
|
||||
std::unordered_map<ggml_webgpu_conv2d_pipeline_key, webgpu_pipeline, ggml_webgpu_conv2d_pipeline_key_hash>
|
||||
conv2d_pipelines;
|
||||
std::unordered_map<ggml_webgpu_conv2d_dw_pipeline_key, webgpu_pipeline, ggml_webgpu_conv2d_dw_pipeline_key_hash>
|
||||
conv2d_dw_pipelines;
|
||||
std::unordered_map<ggml_webgpu_im2col_pipeline_key, webgpu_pipeline, ggml_webgpu_im2col_pipeline_key_hash>
|
||||
im2col_pipelines;
|
||||
|
||||
@@ -3172,6 +3198,50 @@ class ggml_webgpu_shader_lib {
|
||||
return conv2d_pipelines[key];
|
||||
}
|
||||
|
||||
// whcn selects the input layout: contiguous WHCN vs contiguous-channels CWHN
|
||||
webgpu_pipeline get_conv2d_dw_pipeline(const ggml_webgpu_shader_lib_context & context, bool whcn) {
|
||||
ggml_webgpu_conv2d_dw_pipeline_key key = {};
|
||||
key.weight_type = context.src0->type;
|
||||
key.input_type = context.src1->type;
|
||||
key.output_type = context.dst->type;
|
||||
key.whcn = whcn;
|
||||
|
||||
auto it = conv2d_dw_pipelines.find(key);
|
||||
if (it != conv2d_dw_pipelines.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::vector<std::string> defines;
|
||||
std::string variant = whcn ? "conv_2d_dw_whcn" : "conv_2d_dw_cwhn";
|
||||
|
||||
auto push_type_defines = [&](const char * prefix, ggml_type type) {
|
||||
std::string s_prefix = prefix;
|
||||
if (type == GGML_TYPE_F32) {
|
||||
defines.push_back(s_prefix + "_F32");
|
||||
} else if (type == GGML_TYPE_F16) {
|
||||
defines.push_back(s_prefix + "_F16");
|
||||
} else {
|
||||
GGML_ABORT("Unsupported type for CONV_2D_DW shader");
|
||||
}
|
||||
};
|
||||
|
||||
push_type_defines("WEIGHT", key.weight_type);
|
||||
push_type_defines("INPUT", key.input_type);
|
||||
push_type_defines("OUTPUT", key.output_type);
|
||||
if (whcn) {
|
||||
defines.push_back("WHCN");
|
||||
}
|
||||
defines.push_back(std::string("WG_SIZE=") + std::to_string(context.max_wg_size));
|
||||
|
||||
auto processed = preprocessor.preprocess(wgsl_conv2d_dw, defines);
|
||||
auto decisions = std::make_shared<ggml_webgpu_generic_shader_decisions>();
|
||||
decisions->wg_size = context.max_wg_size;
|
||||
webgpu_pipeline pipeline = ggml_webgpu_create_pipeline(device, processed, variant);
|
||||
pipeline.context = decisions;
|
||||
conv2d_dw_pipelines[key] = pipeline;
|
||||
return conv2d_dw_pipelines[key];
|
||||
}
|
||||
|
||||
webgpu_pipeline get_im2col_pipeline(const ggml_webgpu_shader_lib_context & context) {
|
||||
ggml_webgpu_im2col_pipeline_key key = {};
|
||||
key.input_type = context.src1->type;
|
||||
|
||||
@@ -978,6 +978,67 @@ static webgpu_encoded_op ggml_webgpu_conv_2d(webgpu_context & ctx,
|
||||
return ggml_backend_webgpu_build(ctx, pipeline, params, entries, wg_x, wg_y);
|
||||
}
|
||||
|
||||
// Same param/binding layout as conv_2d; the shader differs
|
||||
static webgpu_encoded_op ggml_webgpu_conv_2d_dw(webgpu_context & ctx,
|
||||
ggml_tensor * src0,
|
||||
ggml_tensor * src1,
|
||||
ggml_tensor * dst) {
|
||||
const int32_t s0 = ggml_get_op_params_i32(dst, 0);
|
||||
const int32_t s1 = ggml_get_op_params_i32(dst, 1);
|
||||
const int32_t p0 = ggml_get_op_params_i32(dst, 2);
|
||||
const int32_t p1 = ggml_get_op_params_i32(dst, 3);
|
||||
const int32_t d0 = ggml_get_op_params_i32(dst, 4);
|
||||
const int32_t d1 = ggml_get_op_params_i32(dst, 5);
|
||||
|
||||
// Scalar params matching conv2d_dw.wgsl (weight src0 [KW,KH,1,C], input src1, output dst).
|
||||
std::vector<uint32_t> params = {
|
||||
(uint32_t) (ggml_webgpu_tensor_misalignment(ctx, src0) / ggml_type_size(src0->type)),
|
||||
(uint32_t) (ggml_webgpu_tensor_misalignment(ctx, src1) / ggml_type_size(src1->type)),
|
||||
(uint32_t) (ggml_webgpu_tensor_misalignment(ctx, dst) / ggml_type_size(dst->type)),
|
||||
|
||||
(uint32_t) ggml_nelements(dst),
|
||||
(uint32_t) dst->ne[2],
|
||||
(uint32_t) dst->ne[3],
|
||||
(uint32_t) dst->ne[0],
|
||||
(uint32_t) dst->ne[1],
|
||||
(uint32_t) src1->ne[0],
|
||||
(uint32_t) src1->ne[1],
|
||||
(uint32_t) src0->ne[0],
|
||||
(uint32_t) src0->ne[1],
|
||||
|
||||
(uint32_t) s0,
|
||||
(uint32_t) s1,
|
||||
(uint32_t) p0,
|
||||
(uint32_t) p1,
|
||||
(uint32_t) d0,
|
||||
(uint32_t) d1,
|
||||
};
|
||||
|
||||
std::vector<wgpu::BindGroupEntry> entries = {
|
||||
ggml_webgpu_make_tensor_bind_group_entry(ctx, 0, src0),
|
||||
ggml_webgpu_make_tensor_bind_group_entry(ctx, 1, src1),
|
||||
ggml_webgpu_make_tensor_bind_group_entry(ctx, 2, dst),
|
||||
};
|
||||
|
||||
ggml_webgpu_shader_lib_context shader_lib_ctx = {};
|
||||
shader_lib_ctx.src0 = src0;
|
||||
shader_lib_ctx.src1 = src1;
|
||||
shader_lib_ctx.dst = dst;
|
||||
shader_lib_ctx.max_wg_size = ctx->global_ctx->capabilities.limits.maxComputeInvocationsPerWorkgroup;
|
||||
|
||||
// Input layout: contiguous -> WHCN, contiguous-channels -> CWHN
|
||||
const bool whcn = ggml_is_contiguous(src1);
|
||||
webgpu_pipeline pipeline = ctx->shader_lib->get_conv2d_dw_pipeline(shader_lib_ctx, whcn);
|
||||
auto * decisions = static_cast<ggml_webgpu_generic_shader_decisions *>(pipeline.context.get());
|
||||
|
||||
uint32_t wg_x;
|
||||
uint32_t wg_y;
|
||||
uint32_t total_wg = CEIL_DIV((uint32_t) ggml_nelements(dst), decisions->wg_size);
|
||||
compute_2d_workgroups(total_wg, ctx->global_ctx->capabilities.limits.maxComputeWorkgroupsPerDimension, wg_x, wg_y);
|
||||
|
||||
return ggml_backend_webgpu_build(ctx, pipeline, params, entries, wg_x, wg_y);
|
||||
}
|
||||
|
||||
static webgpu_encoded_op ggml_webgpu_im2col(webgpu_context & ctx,
|
||||
ggml_tensor * src0,
|
||||
ggml_tensor * src1,
|
||||
@@ -3164,6 +3225,8 @@ static std::optional<webgpu_encoded_op> ggml_webgpu_encode(webgpu_context ctx,
|
||||
return ggml_webgpu_sum_rows(ctx, src0, node);
|
||||
case GGML_OP_CONV_2D:
|
||||
return ggml_webgpu_conv_2d(ctx, src0, src1, node);
|
||||
case GGML_OP_CONV_2D_DW:
|
||||
return ggml_webgpu_conv_2d_dw(ctx, src0, src1, node);
|
||||
case GGML_OP_IM2COL:
|
||||
return ggml_webgpu_im2col(ctx, src0, src1, node);
|
||||
case GGML_OP_UPSCALE:
|
||||
@@ -4349,6 +4412,12 @@ static bool ggml_backend_webgpu_device_supports_op(ggml_backend_dev_t dev, const
|
||||
(src0->type == GGML_TYPE_F32 || src0->type == GGML_TYPE_F16) &&
|
||||
(src1->type == GGML_TYPE_F32 || src1->type == GGML_TYPE_F16);
|
||||
break;
|
||||
case GGML_OP_CONV_2D_DW:
|
||||
supports_op = (op->type == GGML_TYPE_F32 || op->type == GGML_TYPE_F16) &&
|
||||
(src0->type == GGML_TYPE_F32 || src0->type == GGML_TYPE_F16) &&
|
||||
(src1->type == GGML_TYPE_F32 || src1->type == GGML_TYPE_F16) &&
|
||||
(ggml_is_contiguous(src1) || ggml_is_contiguous_channels(src1));
|
||||
break;
|
||||
case GGML_OP_IM2COL:
|
||||
supports_op = (op->type == GGML_TYPE_F32 || op->type == GGML_TYPE_F16) &&
|
||||
(src0->type == GGML_TYPE_F32 || src0->type == GGML_TYPE_F16);
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
#include "common_decls.tmpl"
|
||||
enable f16;
|
||||
|
||||
// Ported from the Vulkan backend's conv2d_dw.comp. Two variants (based on WHCN)
|
||||
// selected by the input (src1) layout: contiguous -> WHCN, else CWHN.
|
||||
// weight (src0) is [KW,KH,1,C]; output matches the input layout.
|
||||
|
||||
@group(0) @binding(0)
|
||||
#if defined(WEIGHT_F32)
|
||||
var<storage, read_write> weights: array<f32>;
|
||||
#elif defined(WEIGHT_F16)
|
||||
var<storage, read_write> weights: array<f16>;
|
||||
#endif
|
||||
|
||||
@group(0) @binding(1)
|
||||
#if defined(INPUT_F32)
|
||||
var<storage, read_write> input: array<f32>;
|
||||
#elif defined(INPUT_F16)
|
||||
var<storage, read_write> input: array<f16>;
|
||||
#endif
|
||||
|
||||
@group(0) @binding(2)
|
||||
#if defined(OUTPUT_F32)
|
||||
var<storage, read_write> output: array<f32>;
|
||||
#elif defined(OUTPUT_F16)
|
||||
var<storage, read_write> output: array<f16>;
|
||||
#endif
|
||||
|
||||
struct Params {
|
||||
offset_w: u32,
|
||||
offset_i: u32,
|
||||
offset_o: u32,
|
||||
|
||||
ne: u32,
|
||||
channels: u32,
|
||||
batches: u32,
|
||||
dst_w: u32, dst_h: u32,
|
||||
src_w: u32, src_h: u32,
|
||||
knl_w: u32, knl_h: u32,
|
||||
|
||||
stride_x: i32, stride_y: i32,
|
||||
pad_x: i32, pad_y: i32,
|
||||
dilation_x: i32, dilation_y: i32,
|
||||
};
|
||||
|
||||
@group(0) @binding(3)
|
||||
var<uniform> params: Params;
|
||||
|
||||
fn load_weight(idx: u32) -> f32 {
|
||||
#if defined(WEIGHT_F32)
|
||||
return weights[idx];
|
||||
#elif defined(WEIGHT_F16)
|
||||
return f32(weights[idx]);
|
||||
#endif
|
||||
}
|
||||
fn load_input(idx: u32) -> f32 {
|
||||
#if defined(INPUT_F32)
|
||||
return input[idx];
|
||||
#elif defined(INPUT_F16)
|
||||
return f32(input[idx]);
|
||||
#endif
|
||||
}
|
||||
fn store_output(idx: u32, val: f32) {
|
||||
#if defined(OUTPUT_F32)
|
||||
output[idx] = val;
|
||||
#elif defined(OUTPUT_F16)
|
||||
output[idx] = f16(val);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(WHCN)
|
||||
// Input/output/kernel contiguous in [W, H, C, N] order (kernel [KW,KH,C]).
|
||||
fn conv_2d_dw(idx: u32) -> f32 {
|
||||
let i0 = idx / params.dst_w;
|
||||
let dst_x = idx - i0 * params.dst_w;
|
||||
let i1 = i0 / params.dst_h;
|
||||
let dst_y = i0 - i1 * params.dst_h;
|
||||
let n = i1 / params.channels;
|
||||
let c = i1 - n * params.channels;
|
||||
|
||||
let src_i = params.offset_i + n * params.channels * params.src_h * params.src_w
|
||||
+ c * params.src_h * params.src_w;
|
||||
let knl_i = params.offset_w + c * params.knl_h * params.knl_w;
|
||||
|
||||
var sum: f32 = 0.0;
|
||||
for (var ky: u32 = 0u; ky < params.knl_h; ky += 1u) {
|
||||
let src_y = i32(dst_y) * params.stride_y + i32(ky) * params.dilation_y - params.pad_y;
|
||||
if (src_y < 0 || src_y >= i32(params.src_h)) { continue; }
|
||||
for (var kx: u32 = 0u; kx < params.knl_w; kx += 1u) {
|
||||
let src_x = i32(dst_x) * params.stride_x + i32(kx) * params.dilation_x - params.pad_x;
|
||||
if (src_x < 0 || src_x >= i32(params.src_w)) { continue; }
|
||||
let v = load_input(src_i + u32(src_y) * params.src_w + u32(src_x));
|
||||
let k = load_weight(knl_i + ky * params.knl_w + kx);
|
||||
sum += v * k;
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
#else
|
||||
// Channels contiguous (CWHN): channel is the innermost axis.
|
||||
fn conv_2d_dw(idx: u32) -> f32 {
|
||||
let i0 = idx / params.channels;
|
||||
let c = idx - i0 * params.channels;
|
||||
let i1 = i0 / params.dst_w;
|
||||
let dst_x = i0 - i1 * params.dst_w;
|
||||
let n = i1 / params.dst_h;
|
||||
let dst_y = i1 - n * params.dst_h;
|
||||
|
||||
let src_i = params.offset_i + n * params.channels * params.src_h * params.src_w;
|
||||
let src_row = params.src_w * params.channels;
|
||||
let knl_row = params.knl_w * params.channels;
|
||||
|
||||
var sum: f32 = 0.0;
|
||||
for (var ky: u32 = 0u; ky < params.knl_h; ky += 1u) {
|
||||
let src_y = i32(dst_y) * params.stride_y + i32(ky) * params.dilation_y - params.pad_y;
|
||||
if (src_y < 0 || src_y >= i32(params.src_h)) { continue; }
|
||||
for (var kx: u32 = 0u; kx < params.knl_w; kx += 1u) {
|
||||
let src_x = i32(dst_x) * params.stride_x + i32(kx) * params.dilation_x - params.pad_x;
|
||||
if (src_x < 0 || src_x >= i32(params.src_w)) { continue; }
|
||||
let v = load_input(src_i + u32(src_y) * src_row + u32(src_x) * params.channels + c);
|
||||
let k = load_weight(params.offset_w + ky * knl_row + kx * params.channels + c);
|
||||
sum += v * k;
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
#endif
|
||||
|
||||
@compute @workgroup_size(WG_SIZE)
|
||||
fn main(
|
||||
@builtin(global_invocation_id) gid: vec3<u32>,
|
||||
@builtin(num_workgroups) num_wg: vec3<u32>
|
||||
) {
|
||||
let idx = gid.x + (num_wg.x * u32(WG_SIZE)) * gid.y;
|
||||
if (idx >= params.ne) { return; }
|
||||
store_output(params.offset_o + idx, conv_2d_dw(idx));
|
||||
}
|
||||
@@ -507,6 +507,7 @@ class MODEL_ARCH(IntEnum):
|
||||
DOTS1 = auto()
|
||||
ARCEE = auto()
|
||||
AFMOE = auto()
|
||||
LAGUNA = auto()
|
||||
ERNIE4_5 = auto()
|
||||
ERNIE4_5_MOE = auto()
|
||||
HUNYUAN_MOE = auto()
|
||||
@@ -1088,6 +1089,7 @@ MODEL_ARCH_NAMES: dict[MODEL_ARCH, str] = {
|
||||
MODEL_ARCH.DOTS1: "dots1",
|
||||
MODEL_ARCH.ARCEE: "arcee",
|
||||
MODEL_ARCH.AFMOE: "afmoe",
|
||||
MODEL_ARCH.LAGUNA: "laguna",
|
||||
MODEL_ARCH.ERNIE4_5: "ernie4_5",
|
||||
MODEL_ARCH.ERNIE4_5_MOE: "ernie4_5-moe",
|
||||
MODEL_ARCH.FALCON_H1: "falcon-h1",
|
||||
@@ -3823,6 +3825,31 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
|
||||
MODEL_TENSOR.FFN_POST_NORM,
|
||||
MODEL_TENSOR.FFN_EXP_PROBS_B,
|
||||
],
|
||||
MODEL_ARCH.LAGUNA: [
|
||||
MODEL_TENSOR.TOKEN_EMBD,
|
||||
MODEL_TENSOR.OUTPUT_NORM,
|
||||
MODEL_TENSOR.OUTPUT,
|
||||
MODEL_TENSOR.ATTN_NORM,
|
||||
MODEL_TENSOR.ATTN_Q,
|
||||
MODEL_TENSOR.ATTN_K,
|
||||
MODEL_TENSOR.ATTN_V,
|
||||
MODEL_TENSOR.ATTN_OUT,
|
||||
MODEL_TENSOR.ATTN_Q_NORM,
|
||||
MODEL_TENSOR.ATTN_K_NORM,
|
||||
MODEL_TENSOR.ATTN_GATE,
|
||||
MODEL_TENSOR.FFN_NORM,
|
||||
MODEL_TENSOR.FFN_GATE,
|
||||
MODEL_TENSOR.FFN_DOWN,
|
||||
MODEL_TENSOR.FFN_UP,
|
||||
MODEL_TENSOR.FFN_GATE_INP,
|
||||
MODEL_TENSOR.FFN_EXP_PROBS_B,
|
||||
MODEL_TENSOR.FFN_GATE_EXP,
|
||||
MODEL_TENSOR.FFN_DOWN_EXP,
|
||||
MODEL_TENSOR.FFN_UP_EXP,
|
||||
MODEL_TENSOR.FFN_GATE_SHEXP,
|
||||
MODEL_TENSOR.FFN_UP_SHEXP,
|
||||
MODEL_TENSOR.FFN_DOWN_SHEXP,
|
||||
],
|
||||
MODEL_ARCH.ERNIE4_5: [
|
||||
MODEL_TENSOR.TOKEN_EMBD,
|
||||
MODEL_TENSOR.OUTPUT_NORM,
|
||||
|
||||
@@ -479,6 +479,7 @@ class TensorNameMap:
|
||||
"model.layers.{bid}.mlp.e_score_correction", # exaone-moe
|
||||
"model.layers.{bid}.block_sparse_moe.gate.e_score_correction", # kimi
|
||||
"model.layers.{bid}.moe.router_bias", # step3.5 expert selection bias
|
||||
"model.layers.{bid}.mlp.experts.e_score_correction", # laguna
|
||||
),
|
||||
|
||||
# Feed-forward up
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
{#- Iteration on laguna_glm_thinking_v8/chat_template.jinja -#}
|
||||
{#- No formatting instructions -#}
|
||||
{{- "〈|EOS|〉" -}}
|
||||
{%- set enable_thinking = enable_thinking | default(false) -%}
|
||||
{%- set add_generation_prompt = add_generation_prompt | default(false) -%}
|
||||
|
||||
{#- ───── header (system message) ───── -#}
|
||||
{#- A caller-supplied system message with empty content opts out of the default below, producing no <system> block — used to train without a system message. -#}
|
||||
{%- set system_message = "You are a helpful, conversationally-fluent assistant made by Poolside. You are here to be helpful to users through natural language conversations." -%}
|
||||
{%- if messages and messages[0].role == "system" -%}
|
||||
{%- set system_message = messages[0].content -%}
|
||||
{%- set messages = messages[1:] -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- set has_sys = system_message and system_message.strip() -%}
|
||||
{%- if has_sys or tools or enable_thinking -%}
|
||||
{{- "<system>" -}}
|
||||
|
||||
{%- if has_sys -%}
|
||||
{{- system_message.rstrip() -}}
|
||||
{%- if tools -%}{{- "\n\n" -}}{%- endif -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if tools -%}
|
||||
{{- "### Tools\n\n" -}}
|
||||
{{- "You may call functions to assist with the user query.\n" -}}
|
||||
{{- "All available function signatures are listed below:\n" -}}
|
||||
{{- "<available_tools>\n" -}}
|
||||
{%- for tool in tools -%}
|
||||
{{- (tool | tojson) ~ "\n" -}}
|
||||
{%- endfor -%}
|
||||
{{- "</available_tools>" -}}
|
||||
{%- endif -%}
|
||||
|
||||
{{- "</system>\n" -}}
|
||||
{%- endif -%}
|
||||
|
||||
{#- ───── main loop ───── -#}
|
||||
{%- for message in messages -%}
|
||||
{%- set content = message.content if message.content is string else "" -%}
|
||||
{%- if message.role == "user" -%}
|
||||
{{- "<user>" + content + "</user>\n" -}}
|
||||
{%- elif message.role == "assistant" -%}
|
||||
{%- generation -%}
|
||||
{{- "<assistant>" -}}
|
||||
{#- Extract reasoning content from message.reasoning (vLLM field name) or message.reasoning_content -#}
|
||||
{%- set reasoning_content = '' -%}
|
||||
{%- if message.reasoning is string -%}
|
||||
{%- set reasoning_content = message.reasoning -%}
|
||||
{%- elif message.reasoning_content is string -%}
|
||||
{%- set reasoning_content = message.reasoning_content -%}
|
||||
{%- endif -%}
|
||||
{#- Display reasoning content for all messages if enable_thinking -#}
|
||||
{%- if enable_thinking -%}
|
||||
{{- '<think>' + reasoning_content + '</think>' -}}
|
||||
{%- else -%}
|
||||
{{- '</think>' -}}
|
||||
{%- endif -%}
|
||||
{#- Display main content (trailing newline only when no tool_calls follow) -#}
|
||||
{%- if content -%}
|
||||
{{- content -}}
|
||||
{%- endif -%}
|
||||
{%- if message.tool_calls -%}
|
||||
{%- for tool_call in message.tool_calls -%}
|
||||
{%- set function_data = tool_call.function -%}
|
||||
{{- '<tool_call>' + function_data.name -}}
|
||||
{%- set _args = function_data.arguments -%}
|
||||
{%- for k, v in _args.items() -%}
|
||||
{{- "<arg_key>" ~ k ~ "</arg_key>" -}}
|
||||
{{- "<arg_value>" -}}{{- v | tojson(ensure_ascii=False) if v is not string else v -}}{{- "</arg_value>" -}}
|
||||
{%- endfor -%}
|
||||
{{- "</tool_call>" -}}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{{- "</assistant>\n" -}}
|
||||
{%- endgeneration -%}
|
||||
{%- elif message.role == "tool" -%}
|
||||
{{- "<tool_response>" + content + "</tool_response>\n" -}}
|
||||
{%- elif message.role == "system" -%}
|
||||
{#- Render additional system messages (the first one, if any, is handled separately in the header and was sliced off above) -#}
|
||||
{{- "<system>" + content + "</system>\n" -}}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{#- ───── generation prompt ───── -#}
|
||||
{%- if add_generation_prompt -%}
|
||||
{{- "<assistant>" -}}
|
||||
{#- ───── Include reasoning mode directive ───── -#}
|
||||
{%- if enable_thinking -%}
|
||||
{{- '<think>' -}}
|
||||
{%- else -%}
|
||||
{{- '</think>' -}}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
@@ -0,0 +1,132 @@
|
||||
{#- Copied from laguna_glm_thinking_v4/chat_template.jinja -#}
|
||||
{#- Removes prefix that references <think> token, and replaces message.reasoning_content reference with message.reasoning -#}
|
||||
{{- "〈|EOS|〉" -}}
|
||||
{%- set enable_thinking = enable_thinking | default(false) -%}
|
||||
{%- set render_assistant_messages_raw = render_assistant_messages_raw | default(false) -%}
|
||||
{%- set add_generation_prompt = add_generation_prompt | default(false) -%}
|
||||
|
||||
{#- ───── header (system message) ───── -#}
|
||||
{%- set system_message = "" -%}
|
||||
{%- if messages and messages[0].role == "system" -%}
|
||||
{%- set system_message = messages[0].content -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if (system_message and system_message.strip()) or tools -%}
|
||||
{{- "<system>\n" -}}
|
||||
|
||||
{%- if system_message and system_message.strip() -%}
|
||||
{{- "\n" -}}
|
||||
{{- system_message.rstrip() -}}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if tools -%}
|
||||
{{- "\n\n### Tools\n\n" -}}
|
||||
{%- set ns = namespace(tool_string="You may call functions to assist with the user query.\n"
|
||||
~ "All available function signatures are listed below:\n"
|
||||
~ "<available_tools>\n") -%}
|
||||
{%- for tool in tools -%}
|
||||
{%- set ns.tool_string = ns.tool_string ~ (tool | tojson) ~ "\n" -%}
|
||||
{%- endfor -%}
|
||||
{%- if enable_thinking -%}
|
||||
{%- set tool_string = ns.tool_string + "</available_tools>\n\n" ~
|
||||
"Wrap your thinking in '<think>', '</think>' tags, followed by a function call. For each function call, return an unescaped XML-like object with function name and arguments within '<tool_call>' and '</tool_call>' tags, like here:\n" ~
|
||||
"<think> your thoughts here </think>\n" ~
|
||||
"<tool_call>function-name\n<arg_key>argument-key</arg_key>\n<arg_value>value-of-argument-key</arg_value>\n" ~
|
||||
"</tool_call>" -%}
|
||||
{%- else -%}
|
||||
{%- set tool_string = ns.tool_string + "</available_tools>\n\n" ~
|
||||
"For each function call, return an unescaped XML-like object " ~
|
||||
"with function name and arguments within '<tool_call>' and '</tool_call>' tags, like here:\n" ~
|
||||
"<tool_call>function-name\n<arg_key>argument-key</arg_key>\n<arg_value>value-of-argument-key</arg_value>\n" ~
|
||||
"</tool_call>" -%}
|
||||
{%- endif -%}
|
||||
{{- tool_string -}}
|
||||
{%- endif -%}
|
||||
|
||||
{{- "\n</system>\n" -}}
|
||||
{%- endif -%}
|
||||
|
||||
{#- ───── main loop ───── -#}
|
||||
{%- for message in messages -%}
|
||||
{%- set content = message.content if message.content is string else "" -%}
|
||||
{%- if message.role == "user" -%}
|
||||
{{- "<user>\n" + content + "\n</user>\n" -}}
|
||||
{%- elif message.role == "assistant" -%}
|
||||
{%- generation -%}
|
||||
{{- "<assistant>\n" -}}
|
||||
{%- if render_assistant_messages_raw -%}
|
||||
{#- Raw mode: prepend the generation prompt token, then dump content verbatim. -#}
|
||||
{#- The generation prompt is <think> when enable_thinking, </think> otherwise. -#}
|
||||
{#- Only prepend if content doesn't already start with it. -#}
|
||||
{%- if enable_thinking -%}
|
||||
{%- if not content.startswith('<think>') -%}
|
||||
{{- '<think>' -}}
|
||||
{%- endif -%}
|
||||
{%- else -%}
|
||||
{%- if not content.startswith('</think>') -%}
|
||||
{{- '</think>' -}}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{{- content -}}
|
||||
{#- Append closing tag if content doesn't already end with it. -#}
|
||||
{%- if not content.endswith('</assistant>\n') and not content.endswith('</assistant>') -%}
|
||||
{{- '\n</assistant>' -}}
|
||||
{%- endif -%}
|
||||
{{- "\n" -}}
|
||||
{%- else -%}
|
||||
{#- Extract reasoning content from message.reasoning (vLLM field name) or message.reasoning_content, or from <think> tags -#}
|
||||
{%- set reasoning_content = '' %}
|
||||
{%- if message.reasoning is string %}
|
||||
{%- set reasoning_content = message.reasoning %}
|
||||
{%- elif message.reasoning_content is string %}
|
||||
{%- set reasoning_content = message.reasoning_content %}
|
||||
{%- endif %}
|
||||
{#- Always strip <think> tags from content if present to avoid duplication -#}
|
||||
{%- if '</think>' in content %}
|
||||
{%- if not reasoning_content %}
|
||||
{%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
|
||||
{%- endif %}
|
||||
{%- set content = content.split('</think>')[-1].lstrip('\n') %}
|
||||
{%- endif %}
|
||||
{#- Display reasoning content for all messages -#}
|
||||
{%- if reasoning_content -%}
|
||||
{{- '<think>\n' + reasoning_content.strip() + '\n</think>\n' -}}
|
||||
{%- else -%}
|
||||
{{- '</think>\n' -}}
|
||||
{%- endif -%}
|
||||
{#- Display main content -#}
|
||||
{%- if content.strip() -%}
|
||||
{{- content.strip() ~ "\n" -}}
|
||||
{%- endif -%}
|
||||
{%- if message.tool_calls -%}
|
||||
{%- for tool_call in message.tool_calls -%}
|
||||
{%- set function_data = tool_call.function -%}
|
||||
{{- '<tool_call>' + function_data.name }}
|
||||
{% set _args = function_data.arguments %}
|
||||
{%- for k, v in _args.items() -%}
|
||||
{{- "<arg_key>" ~ k ~ "</arg_key>\n" -}}
|
||||
{{- "<arg_value>"}}{{ v | tojson(ensure_ascii=False) if v is not string else v }}{{ "</arg_value>\n" -}}
|
||||
{%- endfor -%}
|
||||
{{- "</tool_call>\n" -}}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{{- "</assistant>\n" -}}
|
||||
{%- endif -%}
|
||||
{%- endgeneration -%}
|
||||
{%- elif message.role == "tool" -%}
|
||||
{{- "<tool_response>\n" + content + "\n</tool_response>\n" -}}
|
||||
{%- elif message.role == "system" and loop.index0 != 0 -%}
|
||||
{#- Render additional system messages (skip the first one which is handled separately in the header) -#}
|
||||
{{- "<system>\n" + content + "\n</system>\n" -}}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{#- ───── generation prompt ───── -#}
|
||||
{%- if add_generation_prompt -%}
|
||||
{{- "<assistant>\n" -}}
|
||||
{#- ───── Include reasoning mode directive ───── -#}
|
||||
{%- if not enable_thinking %}
|
||||
{{- '</think>' -}}
|
||||
{%- else %}
|
||||
{{- '<think>' -}}
|
||||
{%- endif %}
|
||||
{%- endif -%}
|
||||
@@ -0,0 +1,132 @@
|
||||
{#- Iteration on laguna_glm_thinking_v5/chat_template.jinja -#}
|
||||
{#- Adds a default system message (used when no system message is provided in `messages`). -#}
|
||||
{{- "〈|EOS|〉" -}}
|
||||
{%- set enable_thinking = enable_thinking | default(false) -%}
|
||||
{%- set render_assistant_messages_raw = render_assistant_messages_raw | default(false) -%}
|
||||
{%- set add_generation_prompt = add_generation_prompt | default(false) -%}
|
||||
|
||||
{#- ───── header (system message) ───── -#}
|
||||
{%- set system_message = "You are a helpful, conversationally-fluent assistant made by Poolside. You are here to be helpful to users through natural language conversations." -%}
|
||||
{%- if messages and messages[0].role == "system" -%}
|
||||
{%- set system_message = messages[0].content -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if (system_message and system_message.strip()) or tools -%}
|
||||
{{- "<system>\n" -}}
|
||||
|
||||
{%- if system_message and system_message.strip() -%}
|
||||
{{- "\n" -}}
|
||||
{{- system_message.rstrip() -}}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if tools -%}
|
||||
{{- "\n\n### Tools\n\n" -}}
|
||||
{%- set ns = namespace(tool_string="You may call functions to assist with the user query.\n"
|
||||
~ "All available function signatures are listed below:\n"
|
||||
~ "<available_tools>\n") -%}
|
||||
{%- for tool in tools -%}
|
||||
{%- set ns.tool_string = ns.tool_string ~ (tool | tojson) ~ "\n" -%}
|
||||
{%- endfor -%}
|
||||
{%- if enable_thinking -%}
|
||||
{%- set tool_string = ns.tool_string + "</available_tools>\n\n" ~
|
||||
"Wrap your thinking in '<think>', '</think>' tags, followed by a function call. For each function call, return an unescaped XML-like object with function name and arguments within '<tool_call>' and '</tool_call>' tags, like here:\n" ~
|
||||
"<think> your thoughts here </think>\n" ~
|
||||
"<tool_call>function-name\n<arg_key>argument-key</arg_key>\n<arg_value>value-of-argument-key</arg_value>\n" ~
|
||||
"</tool_call>" -%}
|
||||
{%- else -%}
|
||||
{%- set tool_string = ns.tool_string + "</available_tools>\n\n" ~
|
||||
"For each function call, return an unescaped XML-like object " ~
|
||||
"with function name and arguments within '<tool_call>' and '</tool_call>' tags, like here:\n" ~
|
||||
"<tool_call>function-name\n<arg_key>argument-key</arg_key>\n<arg_value>value-of-argument-key</arg_value>\n" ~
|
||||
"</tool_call>" -%}
|
||||
{%- endif -%}
|
||||
{{- tool_string -}}
|
||||
{%- endif -%}
|
||||
|
||||
{{- "\n</system>\n" -}}
|
||||
{%- endif -%}
|
||||
|
||||
{#- ───── main loop ───── -#}
|
||||
{%- for message in messages -%}
|
||||
{%- set content = message.content if message.content is string else "" -%}
|
||||
{%- if message.role == "user" -%}
|
||||
{{- "<user>\n" + content + "\n</user>\n" -}}
|
||||
{%- elif message.role == "assistant" -%}
|
||||
{%- generation -%}
|
||||
{{- "<assistant>\n" -}}
|
||||
{%- if render_assistant_messages_raw -%}
|
||||
{#- Raw mode: prepend the generation prompt token, then dump content verbatim. -#}
|
||||
{#- The generation prompt is <think> when enable_thinking, </think> otherwise. -#}
|
||||
{#- Only prepend if content doesn't already start with it. -#}
|
||||
{%- if enable_thinking -%}
|
||||
{%- if not content.startswith('<think>') -%}
|
||||
{{- '<think>' -}}
|
||||
{%- endif -%}
|
||||
{%- else -%}
|
||||
{%- if not content.startswith('</think>') -%}
|
||||
{{- '</think>' -}}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{{- content -}}
|
||||
{#- Append closing tag if content doesn't already end with it. -#}
|
||||
{%- if not content.endswith('</assistant>\n') and not content.endswith('</assistant>') -%}
|
||||
{{- '\n</assistant>' -}}
|
||||
{%- endif -%}
|
||||
{{- "\n" -}}
|
||||
{%- else -%}
|
||||
{#- Extract reasoning content from message.reasoning (vLLM field name) or message.reasoning_content, or from <think> tags -#}
|
||||
{%- set reasoning_content = '' %}
|
||||
{%- if message.reasoning is string %}
|
||||
{%- set reasoning_content = message.reasoning %}
|
||||
{%- elif message.reasoning_content is string %}
|
||||
{%- set reasoning_content = message.reasoning_content %}
|
||||
{%- endif %}
|
||||
{#- Always strip <think> tags from content if present to avoid duplication -#}
|
||||
{%- if '</think>' in content %}
|
||||
{%- if not reasoning_content %}
|
||||
{%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
|
||||
{%- endif %}
|
||||
{%- set content = content.split('</think>')[-1].lstrip('\n') %}
|
||||
{%- endif %}
|
||||
{#- Display reasoning content for all messages -#}
|
||||
{%- if reasoning_content -%}
|
||||
{{- '<think>\n' + reasoning_content.strip() + '\n</think>\n' -}}
|
||||
{%- else -%}
|
||||
{{- '</think>\n' -}}
|
||||
{%- endif -%}
|
||||
{#- Display main content -#}
|
||||
{%- if content.strip() -%}
|
||||
{{- content.strip() ~ "\n" -}}
|
||||
{%- endif -%}
|
||||
{%- if message.tool_calls -%}
|
||||
{%- for tool_call in message.tool_calls -%}
|
||||
{%- set function_data = tool_call.function -%}
|
||||
{{- '<tool_call>' + function_data.name }}
|
||||
{% set _args = function_data.arguments %}
|
||||
{%- for k, v in _args.items() -%}
|
||||
{{- "<arg_key>" ~ k ~ "</arg_key>\n" -}}
|
||||
{{- "<arg_value>"}}{{ v | tojson(ensure_ascii=False) if v is not string else v }}{{ "</arg_value>\n" -}}
|
||||
{%- endfor -%}
|
||||
{{- "</tool_call>\n" -}}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{{- "</assistant>\n" -}}
|
||||
{%- endif -%}
|
||||
{%- endgeneration -%}
|
||||
{%- elif message.role == "tool" -%}
|
||||
{{- "<tool_response>\n" + content + "\n</tool_response>\n" -}}
|
||||
{%- elif message.role == "system" and loop.index0 != 0 -%}
|
||||
{#- Render additional system messages (skip the first one which is handled separately in the header) -#}
|
||||
{{- "<system>\n" + content + "\n</system>\n" -}}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{#- ───── generation prompt ───── -#}
|
||||
{%- if add_generation_prompt -%}
|
||||
{{- "<assistant>\n" -}}
|
||||
{#- ───── Include reasoning mode directive ───── -#}
|
||||
{%- if not enable_thinking %}
|
||||
{{- '</think>' -}}
|
||||
{%- else %}
|
||||
{{- '<think>' -}}
|
||||
{%- endif %}
|
||||
{%- endif -%}
|
||||
+3
-2
@@ -108,6 +108,7 @@ static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = {
|
||||
{ LLM_ARCH_DOTS1, "dots1" },
|
||||
{ LLM_ARCH_ARCEE, "arcee" },
|
||||
{ LLM_ARCH_AFMOE, "afmoe" },
|
||||
{ LLM_ARCH_LAGUNA, "laguna" },
|
||||
{ LLM_ARCH_ERNIE4_5, "ernie4_5" },
|
||||
{ LLM_ARCH_ERNIE4_5_MOE, "ernie4_5-moe" },
|
||||
{ LLM_ARCH_HUNYUAN_MOE, "hunyuan-moe" },
|
||||
@@ -665,7 +666,7 @@ static const std::map<llm_tensor, llm_tensor_info> LLM_TENSOR_INFOS = {
|
||||
{LLM_TENSOR_HC_FFN_SCALE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
|
||||
{LLM_TENSOR_ATTN_COMPRESSOR_WKV, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
||||
{LLM_TENSOR_ATTN_COMPRESSOR_WGATE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
||||
{LLM_TENSOR_ATTN_COMPRESSOR_APE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_ADD}},
|
||||
{LLM_TENSOR_ATTN_COMPRESSOR_APE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_GET_ROWS}},
|
||||
{LLM_TENSOR_ATTN_COMPRESSOR_NORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
|
||||
{LLM_TENSOR_ATTN_K_B, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
||||
{LLM_TENSOR_ATTN_V_B, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
||||
@@ -832,7 +833,7 @@ static const std::map<llm_tensor, llm_tensor_info> LLM_TENSOR_INFOS = {
|
||||
{LLM_TENSOR_INDEXER_ATTN_Q_B, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
||||
{LLM_TENSOR_INDEXER_COMPRESSOR_WKV, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
||||
{LLM_TENSOR_INDEXER_COMPRESSOR_WGATE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
||||
{LLM_TENSOR_INDEXER_COMPRESSOR_APE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_ADD}},
|
||||
{LLM_TENSOR_INDEXER_COMPRESSOR_APE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_GET_ROWS}},
|
||||
{LLM_TENSOR_INDEXER_COMPRESSOR_NORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
|
||||
{LLM_TENSOR_FFN_GATE_TID2EID, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_GET_ROWS}},
|
||||
{LLM_TENSOR_NEXTN_PROJ_PRE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
||||
|
||||
@@ -113,6 +113,7 @@ enum llm_arch {
|
||||
LLM_ARCH_DOTS1,
|
||||
LLM_ARCH_ARCEE,
|
||||
LLM_ARCH_AFMOE,
|
||||
LLM_ARCH_LAGUNA,
|
||||
LLM_ARCH_ERNIE4_5,
|
||||
LLM_ARCH_ERNIE4_5_MOE,
|
||||
LLM_ARCH_HUNYUAN_MOE,
|
||||
|
||||
+79
-22
@@ -22,7 +22,7 @@ static constexpr uint32_t DSV4_STATE_MAGIC = 0x34565344; // DSV4
|
||||
static constexpr uint32_t DSV4_STATE_VERSION = 1;
|
||||
static constexpr uint32_t DSV4_STATE_MODE_FULL = 0;
|
||||
static constexpr uint32_t DSV4_STATE_MODE_PARTIAL = 1;
|
||||
static constexpr uint32_t DSV4_K_CACHE_STATE_VER = 1;
|
||||
static constexpr uint32_t DSV4_K_CACHE_STATE_VER = 2;
|
||||
static constexpr uint32_t DSV4_COMP_STATE_VER = 1;
|
||||
|
||||
static uint32_t dsv4_comp_size(uint32_t kv_size, uint32_t ratio) {
|
||||
@@ -38,6 +38,16 @@ static void dsv4_clear_tensor_stream(ggml_tensor * tensor, uint32_t stream) {
|
||||
ggml_backend_tensor_memset(tensor, 0, stream*stream_size, stream_size);
|
||||
}
|
||||
|
||||
static uint32_t dsv4_state_n_used_k_rows(llama_pos pos_max, uint32_t ratio, uint32_t kv_size) {
|
||||
if (pos_max < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint64_t n_rows = ((uint64_t) pos_max + 1)/ratio;
|
||||
|
||||
return (uint32_t) std::min<uint64_t>(kv_size, n_rows);
|
||||
}
|
||||
|
||||
static int64_t dsv4_stream_offset(uint32_t n_stream, llama_seq_id seq_id, uint32_t size) {
|
||||
if (n_stream <= 1) {
|
||||
return 0;
|
||||
@@ -239,6 +249,7 @@ static void dsv4_state_dst_stream_range(
|
||||
static void dsv4_state_write_tensor_streams(
|
||||
llama_io_write_i & io,
|
||||
ggml_tensor * tensor,
|
||||
uint32_t tensor_rows,
|
||||
uint32_t n_rows,
|
||||
uint32_t s0,
|
||||
uint32_t ns) {
|
||||
@@ -247,20 +258,31 @@ static void dsv4_state_write_tensor_streams(
|
||||
const uint64_t rows = n_rows;
|
||||
const uint64_t row_size = ggml_row_size(tensor->type, tensor->ne[0]);
|
||||
|
||||
if (n_rows > tensor_rows) {
|
||||
throw std::runtime_error("DSV4 state tensor row count exceeds storage");
|
||||
}
|
||||
|
||||
io.write(&type_i, sizeof(type_i));
|
||||
io.write(&ne0, sizeof(ne0));
|
||||
io.write(&rows, sizeof(rows));
|
||||
io.write(&row_size, sizeof(row_size));
|
||||
|
||||
const size_t offset = (size_t) s0*n_rows*row_size;
|
||||
const size_t size = (size_t) ns*n_rows*row_size;
|
||||
const size_t stream_stride = (size_t) tensor_rows*row_size;
|
||||
const size_t size = (size_t) n_rows*row_size;
|
||||
if (size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
io.write_tensor(tensor, offset, size);
|
||||
for (uint32_t s = 0; s < ns; ++s) {
|
||||
const size_t offset = (size_t) (s0 + s)*stream_stride;
|
||||
io.write_tensor(tensor, offset, size);
|
||||
}
|
||||
}
|
||||
|
||||
static void dsv4_state_read_tensor_streams(
|
||||
llama_io_read_i & io,
|
||||
ggml_tensor * tensor,
|
||||
uint32_t tensor_rows,
|
||||
uint32_t n_rows,
|
||||
uint32_t s0,
|
||||
uint32_t ns) {
|
||||
@@ -282,18 +304,28 @@ static void dsv4_state_read_tensor_streams(
|
||||
if (type_i != type_i_ref || ne0 != ne0_ref || rows != rows_ref || row_size != row_size_ref) {
|
||||
throw std::runtime_error("DSV4 state tensor metadata mismatch");
|
||||
}
|
||||
if (n_rows > tensor_rows) {
|
||||
throw std::runtime_error("DSV4 state tensor row count exceeds storage");
|
||||
}
|
||||
|
||||
const size_t offset = (size_t) s0*n_rows*row_size;
|
||||
const size_t size = (size_t) ns*n_rows*row_size;
|
||||
const size_t stream_stride = (size_t) tensor_rows*row_size;
|
||||
const size_t size = (size_t) n_rows*row_size;
|
||||
if (size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
io.read_tensor(tensor, offset, size);
|
||||
for (uint32_t s = 0; s < ns; ++s) {
|
||||
const size_t offset = (size_t) (s0 + s)*stream_stride;
|
||||
io.read_tensor(tensor, offset, size);
|
||||
}
|
||||
}
|
||||
|
||||
static void dsv4_state_write_k_cache(
|
||||
llama_io_write_i & io,
|
||||
const llama_kv_cache * kv,
|
||||
llama_seq_id seq_id,
|
||||
llama_state_seq_flags flags) {
|
||||
llama_state_seq_flags flags,
|
||||
uint32_t n_rows) {
|
||||
GGML_UNUSED(flags);
|
||||
|
||||
uint32_t s0;
|
||||
@@ -305,14 +337,18 @@ static void dsv4_state_write_k_cache(
|
||||
const auto layer_ids = kv->get_layer_ids();
|
||||
const uint32_t n_layer = layer_ids.size();
|
||||
|
||||
if (n_rows > kv_size) {
|
||||
throw std::runtime_error("DSV4 K-cache state row count exceeds cache size");
|
||||
}
|
||||
|
||||
io.write(&version, sizeof(version));
|
||||
io.write(&kv_size, sizeof(kv_size));
|
||||
io.write(&n_rows, sizeof(n_rows));
|
||||
io.write(&ns, sizeof(ns));
|
||||
io.write(&n_layer, sizeof(n_layer));
|
||||
|
||||
for (uint32_t il : layer_ids) {
|
||||
io.write(&il, sizeof(il));
|
||||
dsv4_state_write_tensor_streams(io, kv->get_k_storage(il), kv_size, s0, ns);
|
||||
dsv4_state_write_tensor_streams(io, kv->get_k_storage(il), kv_size, n_rows, s0, ns);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,19 +360,26 @@ static void dsv4_state_read_k_cache(
|
||||
GGML_UNUSED(flags);
|
||||
|
||||
uint32_t version;
|
||||
uint32_t kv_size_ref;
|
||||
uint32_t n_rows_ref;
|
||||
uint32_t ns;
|
||||
uint32_t n_layer_ref;
|
||||
|
||||
io.read(&version, sizeof(version));
|
||||
io.read(&kv_size_ref, sizeof(kv_size_ref));
|
||||
io.read(&n_rows_ref, sizeof(n_rows_ref));
|
||||
io.read(&ns, sizeof(ns));
|
||||
io.read(&n_layer_ref, sizeof(n_layer_ref));
|
||||
|
||||
if (version != DSV4_K_CACHE_STATE_VER) {
|
||||
if (version != 1 && version != DSV4_K_CACHE_STATE_VER) {
|
||||
throw std::runtime_error("DSV4 K-cache state version mismatch");
|
||||
}
|
||||
if (kv_size_ref != kv->get_size()) {
|
||||
|
||||
const uint32_t kv_size = kv->get_size();
|
||||
if (version == 1 && n_rows_ref != kv_size) {
|
||||
LLAMA_LOG_INFO("kv size ref %d kv %d\n", n_rows_ref, kv_size);
|
||||
throw std::runtime_error("DSV4 K-cache state size mismatch");
|
||||
}
|
||||
if (n_rows_ref > kv_size) {
|
||||
LLAMA_LOG_INFO("kv rows ref %d kv %d\n", n_rows_ref, kv_size);
|
||||
throw std::runtime_error("DSV4 K-cache state size mismatch");
|
||||
}
|
||||
|
||||
@@ -355,7 +398,7 @@ static void dsv4_state_read_k_cache(
|
||||
throw std::runtime_error("DSV4 K-cache layer id mismatch");
|
||||
}
|
||||
|
||||
dsv4_state_read_tensor_streams(io, kv->get_k_storage(il), kv->get_size(), s0, ns);
|
||||
dsv4_state_read_tensor_streams(io, kv->get_k_storage(il), kv_size, n_rows_ref, s0, ns);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -882,8 +925,8 @@ void llama_dsv4_comp_state::state_write(llama_io_write_i & io, llama_seq_id seq_
|
||||
for (const auto & layer : layers) {
|
||||
io.write(&layer.il, sizeof(layer.il));
|
||||
|
||||
dsv4_state_write_tensor_streams(io, layer.kv, state_size, s0, ns);
|
||||
dsv4_state_write_tensor_streams(io, layer.score, state_size, s0, ns);
|
||||
dsv4_state_write_tensor_streams(io, layer.kv, state_size, state_size, s0, ns);
|
||||
dsv4_state_write_tensor_streams(io, layer.score, state_size, state_size, s0, ns);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -924,8 +967,8 @@ void llama_dsv4_comp_state::state_read(llama_io_read_i & io, llama_seq_id seq_id
|
||||
throw std::runtime_error("DSV4 compressor state layer id mismatch");
|
||||
}
|
||||
|
||||
dsv4_state_read_tensor_streams(io, layer.kv, state_size, s0, ns);
|
||||
dsv4_state_read_tensor_streams(io, layer.score, state_size, s0, ns);
|
||||
dsv4_state_read_tensor_streams(io, layer.kv, state_size, state_size, s0, ns);
|
||||
dsv4_state_read_tensor_streams(io, layer.score, state_size, state_size, s0, ns);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1328,9 +1371,19 @@ void llama_kv_cache_dsv4::state_write(llama_io_write_i & io, llama_seq_id seq_id
|
||||
kv_raw->state_write(io, seq_id, flags);
|
||||
|
||||
if (!partial_only) {
|
||||
dsv4_state_write_k_cache(io, kv_csa.get(), seq_id, flags);
|
||||
dsv4_state_write_k_cache(io, kv_hca.get(), seq_id, flags);
|
||||
dsv4_state_write_k_cache(io, kv_lid.get(), seq_id, flags);
|
||||
const llama_pos pos_max = seq_id >= 0 ? kv_raw->seq_pos_max(seq_id) : -1;
|
||||
|
||||
//FIXME : note that we conflate token positions with rows, which is not true for multi-modal case.
|
||||
const uint32_t n_rows_csa = seq_id >= 0 ?
|
||||
dsv4_state_n_used_k_rows(pos_max, DSV4_CSA_RATIO, kv_csa->get_size()) : kv_csa->get_size();
|
||||
const uint32_t n_rows_hca = seq_id >= 0 ?
|
||||
dsv4_state_n_used_k_rows(pos_max, DSV4_HCA_RATIO, kv_hca->get_size()) : kv_hca->get_size();
|
||||
const uint32_t n_rows_lid = seq_id >= 0 ?
|
||||
dsv4_state_n_used_k_rows(pos_max, DSV4_CSA_RATIO, kv_lid->get_size()) : kv_lid->get_size();
|
||||
|
||||
dsv4_state_write_k_cache(io, kv_csa.get(), seq_id, flags, n_rows_csa);
|
||||
dsv4_state_write_k_cache(io, kv_hca.get(), seq_id, flags, n_rows_hca);
|
||||
dsv4_state_write_k_cache(io, kv_lid.get(), seq_id, flags, n_rows_lid);
|
||||
}
|
||||
|
||||
csa_state->state_write(io, seq_id, flags);
|
||||
@@ -1366,6 +1419,10 @@ void llama_kv_cache_dsv4::state_read(llama_io_read_i & io, llama_seq_id seq_id,
|
||||
kv_raw->state_read(io, seq_id, flags);
|
||||
|
||||
if (!partial_only) {
|
||||
kv_csa->clear(true);
|
||||
kv_hca->clear(true);
|
||||
kv_lid->clear(true);
|
||||
|
||||
dsv4_state_read_k_cache(io, kv_csa.get(), seq_id, flags);
|
||||
dsv4_state_read_k_cache(io, kv_hca.get(), seq_id, flags);
|
||||
dsv4_state_read_k_cache(io, kv_lid.get(), seq_id, flags);
|
||||
|
||||
@@ -28,6 +28,7 @@ bool llama_model_saver_supports_arch(llm_arch arch) {
|
||||
case LLM_ARCH_MIMO2:
|
||||
case LLM_ARCH_STEP35:
|
||||
case LLM_ARCH_MELLUM:
|
||||
case LLM_ARCH_LAGUNA:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
|
||||
@@ -250,6 +250,8 @@ static llama_model * llama_model_mapping(llm_arch arch, const llama_model_params
|
||||
return new llama_model_arcee(params);
|
||||
case LLM_ARCH_AFMOE:
|
||||
return new llama_model_afmoe(params);
|
||||
case LLM_ARCH_LAGUNA:
|
||||
return new llama_model_laguna(params);
|
||||
case LLM_ARCH_ERNIE4_5:
|
||||
return new llama_model_ernie4_5(params);
|
||||
case LLM_ARCH_ERNIE4_5_MOE:
|
||||
@@ -2549,6 +2551,7 @@ llama_rope_type llama_model_rope_type(const llama_model * model) {
|
||||
case LLM_ARCH_COGVLM:
|
||||
case LLM_ARCH_PANGU_EMBED:
|
||||
case LLM_ARCH_AFMOE:
|
||||
case LLM_ARCH_LAGUNA:
|
||||
case LLM_ARCH_QWEN3NEXT:
|
||||
case LLM_ARCH_MIMO2:
|
||||
case LLM_ARCH_STEP35:
|
||||
|
||||
@@ -496,6 +496,12 @@ struct llm_tokenizer_bpe : llm_tokenizer {
|
||||
"[!\"#$%&'()*+,\\-./:;<=>?@\\[\\\\\\]^_`{|}~][A-Za-z]+|[^\\r\\n\\p{L}\\p{P}\\p{S}]?[\\p{L}\\p{M}]+| ?[\\p{P}\\p{S}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
|
||||
};
|
||||
break;
|
||||
case LLAMA_VOCAB_PRE_TYPE_LAGUNA:
|
||||
regex_exprs = {
|
||||
"[^\\n]+|[\\n]+",
|
||||
"(?:'[sS]|'[tT]|'[rR][eE]|'[vV][eE]|'[mM]|'[lL][lL]|'[dD])|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
|
||||
};
|
||||
break;
|
||||
case LLAMA_VOCAB_PRE_TYPE_EXAONE_MOE:
|
||||
regex_exprs = {
|
||||
// original regex from tokenizer.json
|
||||
@@ -2342,6 +2348,10 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
|
||||
tokenizer_pre == "afmoe") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_AFMOE;
|
||||
clean_spaces = false;
|
||||
} else if (
|
||||
tokenizer_pre == "laguna") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_LAGUNA;
|
||||
clean_spaces = false;
|
||||
} else if (
|
||||
tokenizer_pre == "minimax-m2") {
|
||||
pre_type = LLAMA_VOCAB_PRE_TYPE_MINIMAX_M2;
|
||||
|
||||
@@ -64,6 +64,7 @@ enum llama_vocab_pre_type {
|
||||
LLAMA_VOCAB_PRE_TYPE_WHITESPACE = 53,
|
||||
LLAMA_VOCAB_PRE_TYPE_GRANITE_EMB_MULTI = 54,
|
||||
LLAMA_VOCAB_PRE_TYPE_MELLUM2 = 55,
|
||||
LLAMA_VOCAB_PRE_TYPE_LAGUNA = 56,
|
||||
};
|
||||
|
||||
struct LLM_KV;
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
// Laguna (poolside): sigmoid-routed MoE with a score-correction bias, one shared
|
||||
// expert, a softplus attention output gate, QK-norm, and per-layer-type RoPE
|
||||
// (YaRN on full-attention layers, plain RoPE on sliding-window layers). XS.2 is
|
||||
// hybrid full/SWA with a per-head gate; M.1 is full-attention with a per-element
|
||||
// gate. Shares the MoE/gate structure with afmoe.
|
||||
|
||||
#include "models.h"
|
||||
|
||||
void llama_model_laguna::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(LLM_KV_EXPERT_FEED_FORWARD_LENGTH, hparams.n_ff_exp);
|
||||
ml.get_key(LLM_KV_EXPERT_GATING_FUNC, hparams.expert_gating_func, false);
|
||||
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, false);
|
||||
|
||||
// Laguna ships one shared expert and stores its size directly (routed and
|
||||
// shared experts may differ), so read the size from expert_shared_feed_forward_length.
|
||||
// The count is not in the config; default to 1 but read the key if present.
|
||||
hparams.n_expert_shared = 1;
|
||||
ml.get_key(LLM_KV_EXPERT_SHARED_COUNT, hparams.n_expert_shared, false);
|
||||
ml.get_key(LLM_KV_EXPERT_SHARED_FEED_FORWARD_LENGTH, hparams.n_ff_shexp, false);
|
||||
if (hparams.n_ff_shexp == 0) {
|
||||
// Weightless fixtures (test-llama-archs) omit this key; derive a nonzero
|
||||
// size so the shared expert is still built. Real GGUFs always carry the
|
||||
// exact value (routed and shared FF lengths may differ).
|
||||
hparams.n_ff_shexp = hparams.n_ff_exp * hparams.n_expert_shared;
|
||||
}
|
||||
|
||||
// Sliding-window attention is OPTIONAL. XS.2 is hybrid (full / SWA / SWA /
|
||||
// SWA repeating, period 4 starting with full); M.1 has no sliding window
|
||||
// (all layers full attention). When sliding_window is absent or zero we
|
||||
// leave swa_type = NONE and skip the SWA-specific per-layer-type RoPE.
|
||||
hparams.n_swa = 0;
|
||||
ml.get_key(LLM_KV_ATTENTION_SLIDING_WINDOW, hparams.n_swa, false);
|
||||
if (hparams.n_swa > 0) {
|
||||
hparams.swa_type = LLAMA_SWA_TYPE_STANDARD;
|
||||
|
||||
uint32_t swa_period = 4;
|
||||
ml.get_key_or_arr(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, swa_period, false);
|
||||
hparams.set_swa_pattern(swa_period, /*dense_first=*/true); // XS.2: FULL at il%4==0
|
||||
|
||||
// Per-layer-type RoPE: full layers use YaRN θ=500000 over 64 dims;
|
||||
// SWA layers use default RoPE θ=10000 over 128 dims. Base load_hparams
|
||||
// already reads ROPE_FREQ_BASE and ROPE_DIMENSION_COUNT into the
|
||||
// non-SWA fields; we explicitly pull the SWA mirrors here.
|
||||
hparams.rope_freq_base_train_swa = hparams.rope_freq_base_train;
|
||||
hparams.rope_freq_scale_train_swa = 1.0f; // SWA uses plain RoPE (no YaRN scaling); do NOT inherit full layers 1/factor
|
||||
ml.get_key(LLM_KV_ROPE_FREQ_BASE_SWA, hparams.rope_freq_base_train_swa, false);
|
||||
ml.get_key(LLM_KV_ROPE_DIMENSION_COUNT_SWA, hparams.n_rot_swa, false);
|
||||
}
|
||||
|
||||
// Default the expert gating function to SIGMOID when the key is absent
|
||||
// (matches the HF reference).
|
||||
if (hparams.expert_gating_func == LLAMA_EXPERT_GATING_FUNC_TYPE_NONE) {
|
||||
hparams.expert_gating_func = LLAMA_EXPERT_GATING_FUNC_TYPE_SIGMOID;
|
||||
}
|
||||
|
||||
switch (hparams.n_layer()) {
|
||||
case 40: type = LLM_TYPE_30B_A3B; break; // Laguna-XS.2
|
||||
case 70: type = LLM_TYPE_230B_A10B; break; // Laguna-M.1
|
||||
default: type = LLM_TYPE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
void llama_model_laguna::load_arch_tensors(llama_model_loader & ml) {
|
||||
LLAMA_LOAD_LOCALS;
|
||||
|
||||
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}, TENSOR_NOT_REQUIRED);
|
||||
if (output == NULL) {
|
||||
// tied embeddings fallback
|
||||
output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, TENSOR_DUPLICATED);
|
||||
}
|
||||
|
||||
const int64_t n_ff_exp = hparams.n_ff_exp;
|
||||
const int64_t n_ff_shexp = hparams.n_ff_shexp;
|
||||
|
||||
for (int i = 0; i < n_layer; ++i) {
|
||||
auto & layer = layers[i];
|
||||
|
||||
// Per-layer head count — Laguna varies n_head between full and SWA
|
||||
// layers (48 vs 64 in XS.2). KV head count is uniform.
|
||||
const int64_t n_head_il = hparams.n_head(i);
|
||||
const int64_t n_head_kv_il = hparams.n_head_kv(i);
|
||||
const int64_t n_embd_q_il = n_embd_head_k * n_head_il;
|
||||
const int64_t n_embd_k_il = n_embd_head_k * n_head_kv_il;
|
||||
const int64_t n_embd_v_il = n_embd_head_v * n_head_kv_il;
|
||||
|
||||
layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0);
|
||||
|
||||
create_tensor_qkv(layer, i, n_embd, n_embd_q_il, n_embd_k_il, n_embd_v_il, 0);
|
||||
layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd_q_il, n_embd}, 0);
|
||||
|
||||
layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", i), {n_embd_head_k}, 0);
|
||||
layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", i), {n_embd_head_k}, 0);
|
||||
|
||||
// Attention output gate. XS.2 is per-head (g_proj -> n_head, one scalar
|
||||
// per head broadcast over head_dim at multiply time); M.1 is per-element
|
||||
// (g_proj -> n_head*head_dim, like afmoe). Detect from the stored tensor
|
||||
// shape so a single arch handles both; the graph mirrors this check.
|
||||
// Gate width selects per-head vs per-element. Real GGUFs always carry the
|
||||
// gate tensor, so read the width from it and require EXACTLY one of the two
|
||||
// valid widths -- never guess between them. Weightless fixtures
|
||||
// (test-llama-archs) have no gate tensor; fall back to the per-head layout so
|
||||
// the per-head reshape path is still exercised.
|
||||
const int64_t n_gate_per_head = n_head_il;
|
||||
const int64_t n_gate_per_elem = n_embd_head_k * n_head_il;
|
||||
const ggml_tensor * gate_meta = ml.get_tensor_meta(tn(LLM_TENSOR_ATTN_GATE, "weight", i).str().c_str());
|
||||
int64_t n_gate_out;
|
||||
if (gate_meta != nullptr) {
|
||||
n_gate_out = gate_meta->ne[1];
|
||||
if (n_gate_out != n_gate_per_head && n_gate_out != n_gate_per_elem) {
|
||||
GGML_ABORT("Laguna: unexpected attention gate width %lld at layer %d "
|
||||
"(expected %lld per-head or %lld per-element)",
|
||||
(long long) n_gate_out, i, (long long) n_gate_per_head, (long long) n_gate_per_elem);
|
||||
}
|
||||
} else {
|
||||
n_gate_out = n_gate_per_head;
|
||||
}
|
||||
layer.wqkv_gate = create_tensor(tn(LLM_TENSOR_ATTN_GATE, "weight", i), {n_embd, n_gate_out}, 0);
|
||||
|
||||
layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, 0);
|
||||
|
||||
if ((uint32_t)i >= hparams.n_layer_dense_lead) {
|
||||
// MoE layer
|
||||
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}, 0);
|
||||
|
||||
layer.ffn_gate_exps = create_tensor(tn(LLM_TENSOR_FFN_GATE_EXPS, "weight", i), {n_embd, n_ff_exp, 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);
|
||||
layer.ffn_down_exps = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXPS, "weight", i), {n_ff_exp, n_embd, n_expert}, 0);
|
||||
|
||||
// Always-on shared expert.
|
||||
layer.ffn_gate_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_SHEXP, "weight", i), {n_embd, n_ff_shexp}, 0);
|
||||
layer.ffn_up_shexp = create_tensor(tn(LLM_TENSOR_FFN_UP_SHEXP, "weight", i), {n_embd, n_ff_shexp}, 0);
|
||||
layer.ffn_down_shexp = create_tensor(tn(LLM_TENSOR_FFN_DOWN_SHEXP, "weight", i), {n_ff_shexp, n_embd}, 0);
|
||||
} else {
|
||||
// Dense layer (the leading n_layer_dense_lead layers)
|
||||
layer.ffn_gate = create_tensor(tn(LLM_TENSOR_FFN_GATE, "weight", i), {n_embd, n_ff}, 0);
|
||||
layer.ffn_up = create_tensor(tn(LLM_TENSOR_FFN_UP, "weight", i), {n_embd, n_ff}, 0);
|
||||
layer.ffn_down = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "weight", i), {n_ff, n_embd}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<llm_graph_context> llama_model_laguna::build_arch_graph(const llm_graph_params & params) const {
|
||||
return std::make_unique<graph>(*this, params);
|
||||
}
|
||||
|
||||
llama_model_laguna::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_tensor * cur;
|
||||
ggml_tensor * inpL;
|
||||
|
||||
inpL = build_inp_embd(model.tok_embd);
|
||||
// No MuP embedding scale (laguna omits this; afmoe scales by sqrt(hidden)).
|
||||
|
||||
ggml_tensor * inp_pos = build_inp_pos();
|
||||
// XS.2 is hybrid SWA -> interleaved-SWA KV input; M.1 is all-full -> plain
|
||||
// KV input. Pick the matching input (and build_attn overload) per swa_type.
|
||||
const bool has_swa = hparams.swa_type != LLAMA_SWA_TYPE_NONE;
|
||||
llm_graph_input_attn_kv * inp_attn_kv = has_swa ? nullptr : build_attn_inp_kv();
|
||||
llm_graph_input_attn_kv_iswa * inp_attn_iswa = has_swa ? build_attn_inp_kv_iswa() : nullptr;
|
||||
ggml_tensor * inp_out_ids = build_inp_out_ids();
|
||||
|
||||
const float kq_scale = 1.0f / sqrtf(float(n_embd_head));
|
||||
|
||||
for (int il = 0; il < n_layer; ++il) {
|
||||
const bool is_swa_il = hparams.is_swa(il);
|
||||
const int64_t n_head_il = hparams.n_head(il);
|
||||
const int64_t n_head_kv_il = hparams.n_head_kv(il);
|
||||
|
||||
// Per-layer-type RoPE config. SWA layers run plain rope (no YaRN),
|
||||
// achieved by zeroing the YaRN ext/beta params for those layers.
|
||||
const int n_rot_l = is_swa_il ? hparams.n_rot_swa : n_rot;
|
||||
const float freq_base_l = is_swa_il ? hparams.rope_freq_base_train_swa : freq_base;
|
||||
const float freq_scale_l = is_swa_il ? hparams.rope_freq_scale_train_swa : freq_scale;
|
||||
const float ext_factor_l = is_swa_il ? 0.0f : ext_factor;
|
||||
// YaRN magnitude scaling (mscale) is already handled by the framework:
|
||||
// llama_context pre-divides cparams.yarn_attn_factor by (1 + 0.1*ln(factor))
|
||||
// to cancel ggml rope_yarn's internal mscale *= 1 + 0.1*ln(1/freq_scale).
|
||||
// Pass attn_factor straight through (like every other arch); SWA layers run
|
||||
// plain RoPE (ext_factor 0, no mscale) so force 1.0 there.
|
||||
const float attn_factor_l = is_swa_il ? 1.0f : attn_factor;
|
||||
const float beta_fast_l = is_swa_il ? 0.0f : beta_fast;
|
||||
const float beta_slow_l = is_swa_il ? 0.0f : beta_slow;
|
||||
const int n_ctx_orig_l = is_swa_il ? hparams.n_ctx_train : n_ctx_orig;
|
||||
|
||||
ggml_tensor * inpSA = inpL;
|
||||
|
||||
// Pre-norm
|
||||
cur = build_norm(inpL, model.layers[il].attn_norm, NULL, LLM_NORM_RMS, il);
|
||||
cb(cur, "attn_norm", il);
|
||||
|
||||
// Self-attention
|
||||
{
|
||||
ggml_tensor * attn_inp = cur; // saved for the gate projection
|
||||
|
||||
auto [Qcur, Kcur, Vcur] = build_qkv(model.layers[il], cur,
|
||||
n_embd_head, n_head_il, n_head_kv_il, il);
|
||||
|
||||
// g_proj on the *pre-attention* hidden state (matches HF
|
||||
// reference: gate is computed from the same `hidden_states`
|
||||
// input as q/k/v, not from the attn output).
|
||||
ggml_tensor * gate = build_lora_mm(model.layers[il].wqkv_gate, attn_inp);
|
||||
cb(gate, "attn_gate_proj", il);
|
||||
|
||||
// QK RMSNorm at head_dim level (Qwen3 style)
|
||||
Qcur = build_norm(Qcur, model.layers[il].attn_q_norm, NULL, LLM_NORM_RMS, il);
|
||||
Kcur = build_norm(Kcur, model.layers[il].attn_k_norm, NULL, LLM_NORM_RMS, il);
|
||||
cb(Qcur, "Qcur_normed", il);
|
||||
cb(Kcur, "Kcur_normed", il);
|
||||
|
||||
Qcur = ggml_rope_ext(ctx0, Qcur, inp_pos, nullptr,
|
||||
n_rot_l, rope_type, n_ctx_orig_l, freq_base_l, freq_scale_l,
|
||||
ext_factor_l, attn_factor_l, beta_fast_l, beta_slow_l);
|
||||
Kcur = ggml_rope_ext(ctx0, Kcur, inp_pos, nullptr,
|
||||
n_rot_l, rope_type, n_ctx_orig_l, freq_base_l, freq_scale_l,
|
||||
ext_factor_l, attn_factor_l, beta_fast_l, beta_slow_l);
|
||||
cb(Qcur, "Qcur_rope", il);
|
||||
cb(Kcur, "Kcur_rope", il);
|
||||
|
||||
cur = has_swa
|
||||
? build_attn(inp_attn_iswa,
|
||||
NULL, NULL, NULL, // o_proj deferred until after gating
|
||||
Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, kq_scale, il)
|
||||
: build_attn(inp_attn_kv,
|
||||
NULL, NULL, NULL,
|
||||
Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, kq_scale, il);
|
||||
cb(cur, "attn_out", il);
|
||||
|
||||
// Softplus output gate (the unary kernel computes softplus in fp32
|
||||
// and casts back). Two shapes, distinguished by the g_proj output
|
||||
// dim (matching the load-time detection):
|
||||
// XS.2 per-head : gate [n_head_il, n_tokens] -> reshape to
|
||||
// [1, n_head_il, n_tokens] and broadcast over
|
||||
// head_dim against cur [head_dim, n_head, T].
|
||||
// M.1 per-element : gate [n_head_il*head_dim, n_tokens] spans the
|
||||
// full attention output -> direct ggml_mul.
|
||||
gate = ggml_softplus(ctx0, gate);
|
||||
cb(gate, "attn_gate_softplus", il);
|
||||
|
||||
const int64_t n_tokens = cur->ne[1];
|
||||
if (model.layers[il].wqkv_gate->ne[1] == n_head_il) {
|
||||
cur = ggml_reshape_3d(ctx0, cur, n_embd_head, n_head_il, n_tokens);
|
||||
gate = ggml_reshape_3d(ctx0, gate, 1, n_head_il, n_tokens);
|
||||
cur = ggml_mul(ctx0, cur, gate);
|
||||
cur = ggml_reshape_2d(ctx0, cur, n_embd_head * n_head_il, n_tokens);
|
||||
} else {
|
||||
cur = ggml_mul(ctx0, cur, gate);
|
||||
}
|
||||
cb(cur, "attn_gated", il);
|
||||
|
||||
cur = build_lora_mm(model.layers[il].wo, cur, model.layers[il].wo_s);
|
||||
cb(cur, "attn_o_proj", 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);
|
||||
|
||||
// Pre-norm only (no post-attn norm)
|
||||
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) {
|
||||
// MoE: sigmoid routing + score-correction bias + sum-norm +
|
||||
// routed_scaling_factor (all handled by build_moe_ffn).
|
||||
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);
|
||||
cb(moe_out, "ffn_moe_out", il);
|
||||
|
||||
// Always-on shared expert, summed in parallel.
|
||||
ggml_tensor * ffn_shexp = build_ffn(cur,
|
||||
model.layers[il].ffn_up_shexp, NULL, NULL,
|
||||
model.layers[il].ffn_gate_shexp, NULL, NULL,
|
||||
model.layers[il].ffn_down_shexp, NULL, NULL,
|
||||
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);
|
||||
} else {
|
||||
// Dense FFN for the leading n_layer_dense_lead layers (XS.2: 1, M.1: 3)
|
||||
cur = build_ffn(cur,
|
||||
model.layers[il].ffn_up, NULL, NULL,
|
||||
model.layers[il].ffn_gate, NULL, NULL,
|
||||
model.layers[il].ffn_down, NULL, NULL,
|
||||
NULL,
|
||||
LLM_FFN_SILU, LLM_FFN_PAR, il);
|
||||
cb(cur, "ffn_out", il);
|
||||
}
|
||||
|
||||
// No post-ffn norm
|
||||
cur = ggml_add(ctx0, cur, ffn_inp);
|
||||
cur = build_cvec(cur, il);
|
||||
cb(cur, "l_out", il);
|
||||
|
||||
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;
|
||||
|
||||
cur = build_lora_mm(model.output, cur);
|
||||
cb(cur, "result_output", -1);
|
||||
res->t_logits = cur;
|
||||
|
||||
ggml_build_forward_expand(gf, cur);
|
||||
}
|
||||
@@ -1681,6 +1681,19 @@ struct llama_model_afmoe : public llama_model_base {
|
||||
};
|
||||
|
||||
|
||||
struct llama_model_laguna : public llama_model_base {
|
||||
llama_model_laguna(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_ernie4_5 : public llama_model_base {
|
||||
llama_model_ernie4_5(const struct llama_model_params & params) : llama_model_base(params) {}
|
||||
void load_arch_hparams(llama_model_loader & ml) override;
|
||||
|
||||
@@ -5960,6 +5960,7 @@ enum MoeGatingFunc {
|
||||
GATING_FUNC_SOFTMAX,
|
||||
GATING_FUNC_SIGMOID,
|
||||
GATING_FUNC_SOFTMAX_WEIGHT,
|
||||
GATING_FUNC_SQRT_SOFTPLUS,
|
||||
};
|
||||
|
||||
struct test_topk_moe : public test_case {
|
||||
@@ -6003,7 +6004,8 @@ struct test_topk_moe : public test_case {
|
||||
ggml_tensor * logits = ggml_new_tensor(ctx, GGML_TYPE_F32, 4, ne.data());
|
||||
ggml_tensor * probs =
|
||||
(gating_func == GATING_FUNC_SOFTMAX) ? ggml_soft_max(ctx, logits) :
|
||||
(gating_func == GATING_FUNC_SIGMOID) ? ggml_sigmoid(ctx, logits) : logits;
|
||||
(gating_func == GATING_FUNC_SIGMOID) ? ggml_sigmoid(ctx, logits) :
|
||||
(gating_func == GATING_FUNC_SQRT_SOFTPLUS) ? ggml_sqrt(ctx, ggml_softplus(ctx, logits)) : logits;
|
||||
ggml_set_name(probs, "probs");
|
||||
|
||||
ggml_tensor * selection_probs = probs;
|
||||
@@ -9584,7 +9586,7 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() {
|
||||
}
|
||||
}
|
||||
|
||||
for (auto gate : {GATING_FUNC_SOFTMAX, GATING_FUNC_SIGMOID, GATING_FUNC_SOFTMAX_WEIGHT}) {
|
||||
for (auto gate : {GATING_FUNC_SOFTMAX, GATING_FUNC_SIGMOID, GATING_FUNC_SOFTMAX_WEIGHT, GATING_FUNC_SQRT_SOFTPLUS}) {
|
||||
for (bool with_norm : {false, true}) {
|
||||
for (bool bias_probs : {false, true}) {
|
||||
for (float scale_w : {0.0f, 2.0f}) {
|
||||
@@ -9596,6 +9598,7 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() {
|
||||
test_cases.emplace_back(new test_topk_moe({128, 1, 1, 1}, 128, with_norm, bias_probs, gate, scale_w));
|
||||
test_cases.emplace_back(new test_topk_moe({129, 1, 1, 1}, 128, with_norm, bias_probs, gate, scale_w));
|
||||
test_cases.emplace_back(new test_topk_moe({160, 4, 1, 1}, 160, with_norm, bias_probs, gate, scale_w));
|
||||
test_cases.emplace_back(new test_topk_moe({256, 22, 1, 1}, 6, with_norm, bias_probs, gate, scale_w)); // Used by DeepSeek-V4
|
||||
test_cases.emplace_back(new test_topk_moe({288, 22, 1, 1}, 8, with_norm, bias_probs, gate, scale_w)); // Used by StepFun 3.7
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,15 @@ static void test_seed_oss_tool_with_reasoning(testing & t);
|
||||
static void test_nemotron_analysis(testing & t);
|
||||
static void test_nemotron_reasoning_detection(testing & t);
|
||||
static void test_nemotron_tool_format(testing & t);
|
||||
static void test_laguna_analysis(testing & t);
|
||||
static void test_laguna_reasoning_detection(testing & t);
|
||||
static void test_laguna_tool_format(testing & t);
|
||||
static void test_laguna_s_analysis(testing & t);
|
||||
static void test_laguna_s_reasoning_detection(testing & t);
|
||||
static void test_laguna_s_tool_format(testing & t);
|
||||
static void test_laguna_xs2_analysis(testing & t);
|
||||
static void test_laguna_xs2_reasoning_detection(testing & t);
|
||||
static void test_laguna_xs2_tool_format(testing & t);
|
||||
|
||||
// CohereForAI template analysis tests
|
||||
static void test_cohere_reasoning_detection(testing & t);
|
||||
@@ -101,6 +110,9 @@ int main(int argc, char * argv[]) {
|
||||
t.test("seed_oss_diffs", test_seed_oss_tool_analysis);
|
||||
t.test("cohere", test_cohere_analysis);
|
||||
t.test("nemotron", test_nemotron_analysis);
|
||||
t.test("laguna", test_laguna_analysis);
|
||||
t.test("laguna-s", test_laguna_s_analysis);
|
||||
t.test("laguna-xs2", test_laguna_xs2_analysis);
|
||||
t.test("smollm3", test_smollm3_analysis);
|
||||
t.test("standard_json_tools", test_standard_json_tools_formats);
|
||||
t.test("normalize_quotes_to_json", test_normalize_quotes_to_json);
|
||||
@@ -1378,6 +1390,94 @@ static void test_nemotron_tool_format(testing & t) {
|
||||
t.assert_true("should support tools", analysis.jinja_caps.supports_tools);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Laguna Template Analysis Tests
|
||||
// ============================================================================
|
||||
static common_chat_template load_laguna_template(testing & t) {
|
||||
return load_template(t, "models/templates/poolside-Laguna-XS-2.1.jinja");
|
||||
}
|
||||
|
||||
static void test_laguna_reasoning_detection(testing & t) {
|
||||
common_chat_template tmpl = load_laguna_template(t);
|
||||
struct autoparser analysis;
|
||||
analysis.analyze_template(tmpl);
|
||||
// Laguna's template renders reasoning delimiters with formatting whitespace
|
||||
// ("<think>\n") that the model does not emit; the Laguna patch trims them.
|
||||
t.assert_equal("reasoning_start should be '<think>'", "<think>", analysis.reasoning.start);
|
||||
t.assert_equal("reasoning_end should be '</think>'", "</think>", analysis.reasoning.end);
|
||||
t.assert_equal("reasoning should be TAG_BASED", reasoning_mode::TAG_BASED, analysis.reasoning.mode);
|
||||
}
|
||||
|
||||
static void test_laguna_tool_format(testing & t) {
|
||||
common_chat_template tmpl = load_laguna_template(t);
|
||||
struct autoparser analysis;
|
||||
analysis.analyze_template(tmpl);
|
||||
t.assert_equal("arg_value_suffix should be '</arg_value>'", "</arg_value>", analysis.tools.arguments.value_suffix);
|
||||
}
|
||||
|
||||
static void test_laguna_stop_string(testing & t) {
|
||||
// The </assistant> turn terminator can be emitted as ordinary text tokens
|
||||
// (not the single eot token), so it must also be a literal stop string.
|
||||
common_chat_template tmpl = load_laguna_template(t);
|
||||
struct autoparser analysis;
|
||||
analysis.analyze_template(tmpl);
|
||||
bool has_stop = false;
|
||||
for (const auto & stop : analysis.additional_stops) {
|
||||
if (stop == "</assistant>") { has_stop = true; break; }
|
||||
}
|
||||
t.assert_true("Laguna additional_stops contains </assistant>", has_stop);
|
||||
}
|
||||
|
||||
static void test_laguna_analysis(testing & t) {
|
||||
t.test("Laguna reasoning detection", test_laguna_reasoning_detection);
|
||||
t.test("Laguna tool format", test_laguna_tool_format);
|
||||
t.test("Laguna stop string", test_laguna_stop_string);
|
||||
}
|
||||
|
||||
static common_chat_template load_laguna_s_template(testing & t) {
|
||||
return load_template(t, "models/templates/poolside-Laguna-S-2.1.jinja");
|
||||
}
|
||||
static void test_laguna_s_reasoning_detection(testing & t) {
|
||||
common_chat_template tmpl = load_laguna_s_template(t);
|
||||
struct autoparser analysis;
|
||||
analysis.analyze_template(tmpl);
|
||||
t.assert_equal("Laguna-S(v8) reasoning_start should be '<think>'", "<think>", analysis.reasoning.start);
|
||||
t.assert_equal("Laguna-S(v8) reasoning_end should be '</think>'", "</think>", analysis.reasoning.end);
|
||||
t.assert_equal("Laguna-S(v8) reasoning should be TAG_BASED", reasoning_mode::TAG_BASED, analysis.reasoning.mode);
|
||||
}
|
||||
static void test_laguna_s_tool_format(testing & t) {
|
||||
common_chat_template tmpl = load_laguna_s_template(t);
|
||||
struct autoparser analysis;
|
||||
analysis.analyze_template(tmpl);
|
||||
t.assert_equal("Laguna-S(v8) arg_value_suffix should be '</arg_value>'", "</arg_value>", analysis.tools.arguments.value_suffix);
|
||||
}
|
||||
static void test_laguna_s_analysis(testing & t) {
|
||||
t.test("Laguna-S(v8) reasoning detection", test_laguna_s_reasoning_detection);
|
||||
t.test("Laguna-S(v8) tool format", test_laguna_s_tool_format);
|
||||
}
|
||||
|
||||
static common_chat_template load_laguna_xs2_template(testing & t) {
|
||||
return load_template(t, "models/templates/poolside-Laguna-XS.2.jinja");
|
||||
}
|
||||
static void test_laguna_xs2_reasoning_detection(testing & t) {
|
||||
common_chat_template tmpl = load_laguna_xs2_template(t);
|
||||
struct autoparser analysis;
|
||||
analysis.analyze_template(tmpl);
|
||||
t.assert_equal("Laguna-XS.2(v5) reasoning_start should be '<think>'", "<think>", analysis.reasoning.start);
|
||||
t.assert_equal("Laguna-XS.2(v5) reasoning_end should be '</think>'", "</think>", analysis.reasoning.end);
|
||||
t.assert_equal("Laguna-XS.2(v5) reasoning should be TAG_BASED", reasoning_mode::TAG_BASED, analysis.reasoning.mode);
|
||||
}
|
||||
static void test_laguna_xs2_tool_format(testing & t) {
|
||||
common_chat_template tmpl = load_laguna_xs2_template(t);
|
||||
struct autoparser analysis;
|
||||
analysis.analyze_template(tmpl);
|
||||
t.assert_equal("Laguna-XS.2(v5) arg_value_suffix should be '</arg_value>'", "</arg_value>", analysis.tools.arguments.value_suffix);
|
||||
}
|
||||
static void test_laguna_xs2_analysis(testing & t) {
|
||||
t.test("Laguna-XS.2(v5) reasoning detection", test_laguna_xs2_reasoning_detection);
|
||||
t.test("Laguna-XS.2(v5) tool format", test_laguna_xs2_tool_format);
|
||||
}
|
||||
|
||||
static common_chat_template load_cohere_template(testing & t) {
|
||||
return load_template(t, "models/templates/CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja");
|
||||
}
|
||||
|
||||
@@ -362,6 +362,7 @@ static bool moe_mandatory(const llm_arch arch) {
|
||||
case LLM_ARCH_STEP35:
|
||||
case LLM_ARCH_MISTRAL4:
|
||||
case LLM_ARCH_MELLUM:
|
||||
case LLM_ARCH_LAGUNA:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
||||
@@ -37,7 +37,7 @@ ggml_cgraph * clip_graph_qwen3vl::build() {
|
||||
}
|
||||
|
||||
// calculate absolute position embedding and apply
|
||||
ggml_tensor * learned_pos_embd = resize_position_embeddings();
|
||||
ggml_tensor * learned_pos_embd = resize_position_embeddings(GGML_SCALE_MODE_BILINEAR | GGML_SCALE_FLAG_ALIGN_CORNERS);
|
||||
learned_pos_embd = ggml_cont_4d(
|
||||
ctx0, learned_pos_embd,
|
||||
n_embd * 2, n_patches_x / 2, n_patches_y, batch_size);
|
||||
|
||||
@@ -1152,6 +1152,11 @@ private:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ctx_tgt == nullptr) {
|
||||
SRV_ERR("failed to create_context with model '%s'\n", params_base.model.path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
vocab = llama_model_get_vocab(model_tgt);
|
||||
|
||||
n_ctx = llama_n_ctx(ctx_tgt);
|
||||
|
||||
@@ -632,6 +632,13 @@ void server_res_spipe::on_complete() {
|
||||
if (!spipe || next_finished) {
|
||||
return;
|
||||
}
|
||||
// an empty next_orig means set_next() never ran: the request failed before streaming
|
||||
// started, typically a params validation throw. evict the session installed by set_req()
|
||||
// so the failed request leaves nothing behind for discovery or replay
|
||||
if (!next_orig) {
|
||||
g_stream_sessions.evict(server_stream_conv_id_from_headers(req->headers));
|
||||
return;
|
||||
}
|
||||
std::string chunk;
|
||||
while (!spipe->is_cancelled()) {
|
||||
chunk.clear();
|
||||
|
||||
Generated
+4
-4
@@ -12,7 +12,7 @@
|
||||
"@eslint/compat": "1.4.1",
|
||||
"@eslint/js": "9.39.2",
|
||||
"@internationalized/date": "3.12.2",
|
||||
"@lucide/svelte": "0.515.0",
|
||||
"@lucide/svelte": "1.25.0",
|
||||
"@modelcontextprotocol/sdk": "1.26.0",
|
||||
"@playwright/test": "1.56.1",
|
||||
"@storybook/addon-a11y": "10.2.4",
|
||||
@@ -3065,9 +3065,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@lucide/svelte": {
|
||||
"version": "0.515.0",
|
||||
"resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.515.0.tgz",
|
||||
"integrity": "sha512-CEAyqcZmNBfYzVgaRmK2RFJP5tnbXxekRyDk0XX/eZQRfsJmkDvmQwXNX8C869BgNeryzmrRyjHhUL6g9ZOHNA==",
|
||||
"version": "1.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.25.0.tgz",
|
||||
"integrity": "sha512-v9m+dD68jxVnqkU3K59mG/RSRFlPGzmKCGSyMfnXcaGv9jODDQMyQkcp1CGvk3Y/cUj9v7f8rw1n//K0B53xGQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"@eslint/compat": "1.4.1",
|
||||
"@eslint/js": "9.39.2",
|
||||
"@internationalized/date": "3.12.2",
|
||||
"@lucide/svelte": "0.515.0",
|
||||
"@lucide/svelte": "1.25.0",
|
||||
"@modelcontextprotocol/sdk": "1.26.0",
|
||||
"@playwright/test": "1.56.1",
|
||||
"@storybook/addon-a11y": "10.2.4",
|
||||
|
||||
@@ -66,7 +66,14 @@
|
||||
<Tooltip.Trigger>
|
||||
<!-- prevent another nested button element -->
|
||||
{#snippet child({ props })}
|
||||
{@render button(props)}
|
||||
{#if disabled}
|
||||
<!-- disabled buttons have pointer-events:none; wrap in a span so the tooltip hover surface stays alive -->
|
||||
<span {...props}>
|
||||
{@render button({})}
|
||||
</span>
|
||||
{:else}
|
||||
{@render button(props)}
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
|
||||
|
||||
+1
-4
@@ -157,10 +157,7 @@
|
||||
>
|
||||
{#if currentConfig.renderUserContentAsMarkdown}
|
||||
<div bind:this={messageElement} class={isExpanded ? 'cursor-text' : ''}>
|
||||
<MarkdownContent
|
||||
class="markdown-system-content -my-4"
|
||||
content={message.content}
|
||||
/>
|
||||
<MarkdownContent class="markdown-system-content" content={message.content} />
|
||||
</div>
|
||||
{:else}
|
||||
<span
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@
|
||||
>
|
||||
{#if renderMarkdown && currentConfig.renderUserContentAsMarkdown}
|
||||
<div bind:this={messageElement}>
|
||||
<MarkdownContent class="markdown-user-content -my-4" {content} />
|
||||
<MarkdownContent class="markdown-user-content" {content} />
|
||||
</div>
|
||||
{:else}
|
||||
<span bind:this={messageElement} class="text-md whitespace-pre-wrap">
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
|
||||
const renderThinkingAsMarkdown = $derived(config().renderThinkingAsMarkdown as boolean);
|
||||
const showThoughtInProgress = $derived(Boolean(config().showThoughtInProgress));
|
||||
const alwaysShowToolCallContent = $derived(Boolean(config().alwaysShowToolCallContent));
|
||||
const showMessageStats = $derived(Boolean(config().showMessageStats));
|
||||
const showAgenticTurnStats = $derived(showMessageStats && Boolean(config().showAgenticTurnStats));
|
||||
|
||||
@@ -98,19 +99,6 @@
|
||||
isStreaming ? agenticExecutingToolCallId(message.convId) : null
|
||||
);
|
||||
|
||||
// Skip sections the user manually collapsed - we never override an explicit false.
|
||||
let lastSeenExecutingToolCallId: string | null = null;
|
||||
$effect(() => {
|
||||
const current = currentlyExecutingToolCallId;
|
||||
const previous = lastSeenExecutingToolCallId;
|
||||
lastSeenExecutingToolCallId = current;
|
||||
if (!current || current === previous) return;
|
||||
const idx = sections.findIndex((s) => s.toolCallId === current);
|
||||
if (idx >= 0 && expandedStates[idx] === undefined) {
|
||||
expandedStates[idx] = true;
|
||||
}
|
||||
});
|
||||
|
||||
type TurnGroup = {
|
||||
sections: AgenticSection[];
|
||||
flatIndices: number[];
|
||||
@@ -149,10 +137,11 @@
|
||||
|
||||
function getDefaultExpanded(section: AgenticSection): boolean {
|
||||
if (
|
||||
section.type === AgenticSectionType.TOOL_CALL ||
|
||||
section.type === AgenticSectionType.TOOL_CALL_PENDING ||
|
||||
section.type === AgenticSectionType.TOOL_CALL_STREAMING
|
||||
) {
|
||||
return false;
|
||||
return alwaysShowToolCallContent;
|
||||
}
|
||||
|
||||
if (section.type === AgenticSectionType.REASONING_PENDING) {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Pencil } from '@lucide/svelte';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
currentTitle: string;
|
||||
value: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
currentTitle,
|
||||
value = $bindable(''),
|
||||
onConfirm,
|
||||
onCancel
|
||||
}: Props = $props();
|
||||
|
||||
let inputRef = $state<HTMLInputElement | null>(null);
|
||||
|
||||
const canSubmit = $derived(value.trim().length > 0 && value.trim() !== currentTitle.trim());
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
value = currentTitle;
|
||||
queueMicrotask(() => {
|
||||
inputRef?.focus();
|
||||
inputRef?.select();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function handleOpenChange(newOpen: boolean) {
|
||||
if (!newOpen) {
|
||||
onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit(event: Event) {
|
||||
event.preventDefault();
|
||||
if (!canSubmit) return;
|
||||
value = value.trim();
|
||||
onConfirm();
|
||||
}
|
||||
</script>
|
||||
|
||||
<AlertDialog.Root bind:open onOpenChange={handleOpenChange}>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title class="flex items-center gap-2">
|
||||
<Pencil class="h-5 w-5" />
|
||||
Rename conversation
|
||||
</AlertDialog.Title>
|
||||
|
||||
<AlertDialog.Description>Choose a new title for this conversation.</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
|
||||
<form onsubmit={handleSubmit} class="space-y-2 pt-2 pb-4">
|
||||
<label for="conversation-rename-input" class="text-sm font-medium text-muted-foreground">
|
||||
Conversation title
|
||||
</label>
|
||||
|
||||
<Input
|
||||
id="conversation-rename-input"
|
||||
bind:ref={inputRef}
|
||||
bind:value
|
||||
placeholder="Conversation title"
|
||||
maxlength={200}
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck={false}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<AlertDialog.Footer>
|
||||
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
|
||||
|
||||
<Button type="button" onclick={handleSubmit} disabled={!canSubmit}>Save</Button>
|
||||
</AlertDialog.Footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
@@ -37,9 +37,9 @@
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="z-[1000000]" />
|
||||
<Dialog.Overlay class="z-1000000" />
|
||||
|
||||
<Dialog.Content class="z-[1000001] max-w-2xl">
|
||||
<Dialog.Content class="z-1000001 max-w-2xl">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>
|
||||
Select Conversations to {mode === 'export' ? 'Export' : 'Import'}
|
||||
@@ -58,6 +58,7 @@
|
||||
|
||||
<ConversationSelection
|
||||
bind:this={conversationSelectionRef}
|
||||
isOpen={open}
|
||||
{conversations}
|
||||
{messageCountMap}
|
||||
{mode}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<script lang="ts">
|
||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
currentTitle: string;
|
||||
newTitle: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
let { open = $bindable(), currentTitle, newTitle, onConfirm, onCancel }: Props = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialog.Root bind:open>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>Update Conversation Title?</AlertDialog.Title>
|
||||
|
||||
<AlertDialog.Description>
|
||||
Do you want to update the conversation title to match the first message content?
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
|
||||
<div class="space-y-4 pt-2 pb-6">
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-medium text-muted-foreground">Current title:</p>
|
||||
|
||||
<p class="rounded-md bg-muted/50 p-3 text-sm font-medium">{currentTitle}</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-medium text-muted-foreground">New title would be:</p>
|
||||
|
||||
<p class="rounded-md bg-muted/50 p-3 text-sm font-medium">{newTitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AlertDialog.Footer>
|
||||
<Button variant="outline" onclick={onCancel}>Keep Current Title</Button>
|
||||
|
||||
<Button onclick={onConfirm}>Update Title</Button>
|
||||
</AlertDialog.Footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
@@ -68,6 +68,7 @@
|
||||
|
||||
<AlertDialog.Footer>
|
||||
<AlertDialog.Cancel onclick={onCancel}>Cancel</AlertDialog.Cancel>
|
||||
|
||||
<AlertDialog.Action
|
||||
onclick={onConfirm}
|
||||
class="bg-destructive text-white hover:bg-destructive/80"
|
||||
|
||||
@@ -92,33 +92,36 @@ export { default as DialogExportSettings } from './DialogExportSettings.svelte';
|
||||
export { default as DialogConfirmation } from './DialogConfirmation.svelte';
|
||||
|
||||
/**
|
||||
* **DialogConversationTitleUpdate** - Conversation rename confirmation
|
||||
* **DialogConversationRename** - Rename a conversation
|
||||
*
|
||||
* Confirmation dialog shown when editing the first user message in a conversation.
|
||||
* Asks user whether to update the conversation title to match the new message content.
|
||||
* Modal dialog for renaming a conversation. Replaces the prior
|
||||
* `window.prompt()`-based flow with a styled, accessible AlertDialog
|
||||
* containing an editable input. Triggered from the sidebar conversation
|
||||
* item's "Edit" action.
|
||||
*
|
||||
* **Architecture:**
|
||||
* - Uses ShadCN AlertDialog
|
||||
* - Shows current vs proposed title comparison
|
||||
* - Triggered by ChatMessages when first message is edited
|
||||
* - Bindable `value` keeps the new title in sync with parent state
|
||||
* - Submit is gated on a non-empty trimmed value that differs from the current title
|
||||
*
|
||||
* **Features:**
|
||||
* - Side-by-side display of current and new title
|
||||
* - "Keep Current Title" and "Update Title" action buttons
|
||||
* - Styled title previews in muted background boxes
|
||||
* - Autofocus on open with text selected for quick overwrite
|
||||
* - Disabled Save button when value is empty or unchanged
|
||||
* - Trim-on-submit normalization
|
||||
* - Cancel via AlertDialog.Cancel or `onOpenChange(false)`
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <DialogConversationTitleUpdate
|
||||
* bind:open={showTitleUpdate}
|
||||
* <DialogConversationRename
|
||||
* bind:open={showRename}
|
||||
* currentTitle={conversation.name}
|
||||
* newTitle={truncatedMessageContent}
|
||||
* onConfirm={updateTitle}
|
||||
* onCancel={() => showTitleUpdate = false}
|
||||
* bind:value={renameDraft}
|
||||
* onConfirm={handleRenameConfirm}
|
||||
* onCancel={() => (showRename = false)}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export { default as DialogConversationTitleUpdate } from './DialogConversationTitleUpdate.svelte';
|
||||
export { default as DialogConversationRename } from './DialogConversationRename.svelte';
|
||||
|
||||
/**
|
||||
*
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import SearchInput from '$lib/components/app/forms/SearchInput.svelte';
|
||||
import { ScrollArea } from '$lib/components/ui/scroll-area';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import { useMarqueeSelection } from '$lib/hooks/use-marquee-selection.svelte';
|
||||
|
||||
interface Props {
|
||||
conversations: DatabaseConversation[];
|
||||
@@ -11,13 +12,20 @@
|
||||
mode: 'export' | 'import';
|
||||
onCancel: () => void;
|
||||
onConfirm: (selectedConversations: DatabaseConversation[]) => void;
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
let { conversations, messageCountMap = new Map(), mode, onCancel, onConfirm }: Props = $props();
|
||||
let {
|
||||
conversations,
|
||||
messageCountMap = new Map(),
|
||||
mode,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
isOpen = true
|
||||
}: Props = $props();
|
||||
|
||||
let searchQuery = $state('');
|
||||
let selectedIds = $state.raw<SvelteSet<string>>(getInitialSelectedIds());
|
||||
let lastClickedId = $state<string | null>(null);
|
||||
|
||||
function getInitialSelectedIds(): SvelteSet<string> {
|
||||
return new SvelteSet(conversations.map((c) => c.id));
|
||||
@@ -30,6 +38,8 @@
|
||||
})
|
||||
);
|
||||
|
||||
let orderedIds = $derived(filteredConversations.map((c) => c.id));
|
||||
|
||||
let allSelected = $derived(
|
||||
filteredConversations.length > 0 &&
|
||||
filteredConversations.every((conv) => selectedIds.has(conv.id))
|
||||
@@ -39,54 +49,20 @@
|
||||
filteredConversations.some((conv) => selectedIds.has(conv.id)) && !allSelected
|
||||
);
|
||||
|
||||
function toggleConversation(id: string, shiftKey: boolean = false) {
|
||||
const newSet = new SvelteSet(selectedIds);
|
||||
|
||||
if (shiftKey && lastClickedId !== null) {
|
||||
const lastIndex = filteredConversations.findIndex((c) => c.id === lastClickedId);
|
||||
const currentIndex = filteredConversations.findIndex((c) => c.id === id);
|
||||
|
||||
if (lastIndex !== -1 && currentIndex !== -1) {
|
||||
const start = Math.min(lastIndex, currentIndex);
|
||||
const end = Math.max(lastIndex, currentIndex);
|
||||
|
||||
const shouldSelect = !newSet.has(id);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
if (shouldSelect) {
|
||||
newSet.add(filteredConversations[i].id);
|
||||
} else {
|
||||
newSet.delete(filteredConversations[i].id);
|
||||
}
|
||||
}
|
||||
|
||||
selectedIds = newSet;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (newSet.has(id)) {
|
||||
newSet.delete(id);
|
||||
} else {
|
||||
newSet.add(id);
|
||||
}
|
||||
|
||||
selectedIds = newSet;
|
||||
lastClickedId = id;
|
||||
}
|
||||
const marquee = useMarqueeSelection({
|
||||
selectedIds: () => selectedIds,
|
||||
orderedIds: () => orderedIds,
|
||||
enabled: () => isOpen
|
||||
});
|
||||
|
||||
function toggleAll() {
|
||||
const newSet = new SvelteSet(selectedIds);
|
||||
if (allSelected) {
|
||||
const newSet = new SvelteSet(selectedIds);
|
||||
|
||||
filteredConversations.forEach((conv) => newSet.delete(conv.id));
|
||||
selectedIds = newSet;
|
||||
} else {
|
||||
const newSet = new SvelteSet(selectedIds);
|
||||
|
||||
filteredConversations.forEach((conv) => newSet.add(conv.id));
|
||||
selectedIds = newSet;
|
||||
}
|
||||
selectedIds = newSet;
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
@@ -97,7 +73,7 @@
|
||||
function handleCancel() {
|
||||
selectedIds = getInitialSelectedIds();
|
||||
searchQuery = '';
|
||||
lastClickedId = null;
|
||||
marquee.reset();
|
||||
|
||||
onCancel();
|
||||
}
|
||||
@@ -105,7 +81,7 @@
|
||||
export function reset() {
|
||||
selectedIds = getInitialSelectedIds();
|
||||
searchQuery = '';
|
||||
lastClickedId = null;
|
||||
marquee.reset();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -122,7 +98,7 @@
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-md border">
|
||||
<ScrollArea class="h-[400px]">
|
||||
<ScrollArea class="h-100">
|
||||
<table class="w-full">
|
||||
<thead class="sticky top-0 z-10 bg-muted">
|
||||
<tr class="border-b">
|
||||
@@ -139,6 +115,7 @@
|
||||
<th class="w-32 p-3 text-left text-sm font-medium">Messages</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{#if filteredConversations.length === 0}
|
||||
<tr>
|
||||
@@ -152,23 +129,28 @@
|
||||
</tr>
|
||||
{:else}
|
||||
{#each filteredConversations as conv (conv.id)}
|
||||
{@const checked = selectedIds.has(conv.id)}
|
||||
<tr
|
||||
class="cursor-pointer border-b transition-colors hover:bg-muted/50"
|
||||
onclick={(event) => toggleConversation(conv.id, event.shiftKey)}
|
||||
class="cursor-pointer border-b transition-colors hover:bg-muted/50 {checked
|
||||
? 'bg-muted/75'
|
||||
: ''}"
|
||||
data-conversation-row={conv.id}
|
||||
onmousedown={(event) => marquee.rowMouseDown(conv.id, event)}
|
||||
onclick={(event) => marquee.rowClick(conv.id, event.shiftKey)}
|
||||
>
|
||||
<td class="p-3">
|
||||
<Checkbox
|
||||
checked={selectedIds.has(conv.id)}
|
||||
{checked}
|
||||
onclick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
toggleConversation(conv.id, event.shiftKey);
|
||||
marquee.rowClick(conv.id, event.shiftKey);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td class="p-3 text-sm">
|
||||
<div class="max-w-[17rem] truncate" title={conv.name || 'Untitled conversation'}>
|
||||
<div class="max-w-68 truncate" title={conv.name || 'Untitled conversation'}>
|
||||
{conv.name || 'Untitled conversation'}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
+204
-47
@@ -4,15 +4,22 @@
|
||||
import { PanelLeftClose, PanelLeftOpen, X } from '@lucide/svelte';
|
||||
import {
|
||||
ActionIcon,
|
||||
DialogConversationRename,
|
||||
Logo,
|
||||
SidebarNavigationConversationList,
|
||||
SidebarNavigationActions
|
||||
} from '$lib/components/app';
|
||||
import { ROUTES } from '$lib/constants';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import { useMarqueeSelection } from '$lib/hooks/use-marquee-selection.svelte';
|
||||
|
||||
import { useKeyboardShortcuts } from '$lib/hooks/use-keyboard-shortcuts.svelte';
|
||||
import { conversationsStore, conversations } from '$lib/stores/conversations.svelte';
|
||||
import {
|
||||
buildConversationTree,
|
||||
conversationsStore,
|
||||
conversations
|
||||
} from '$lib/stores/conversations.svelte';
|
||||
import { chatStore } from '$lib/stores/chat.svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { RouterService } from '$lib/services/router.service';
|
||||
@@ -40,7 +47,6 @@
|
||||
const isOnMobile = $derived(isMobile.current);
|
||||
const alwaysShowOnDesktop = $derived(config().alwaysShowSidebarOnDesktop as boolean);
|
||||
|
||||
// Keep the sidebar expanded on desktop when the user pins it open
|
||||
$effect(() => {
|
||||
if (alwaysShowOnDesktop && !isOnMobile) {
|
||||
isExpandedMode = true;
|
||||
@@ -58,13 +64,11 @@
|
||||
if (!isExpandedMode) {
|
||||
isSearchModeActive = false;
|
||||
searchQuery = '';
|
||||
if (isSelectionMode) exitSelectionMode();
|
||||
cancelMobileCollapse();
|
||||
}
|
||||
});
|
||||
|
||||
// On mobile the dedicated /search route hides the sidebar (see the aside
|
||||
// render guard below). Collapse it as we enter /search so it doesn't
|
||||
// reappear expanded when the user navigates back via the back button.
|
||||
$effect(() => {
|
||||
if (isMobile.current && page.url.hash.includes(ROUTES.SEARCH)) {
|
||||
isExpandedMode = false;
|
||||
@@ -89,6 +93,121 @@
|
||||
return conversations();
|
||||
});
|
||||
|
||||
let isSelectionMode = $state(false);
|
||||
let selectedIds = new SvelteSet<string>();
|
||||
|
||||
let renameDialogOpen = $state(false);
|
||||
let renameTargetConversationId = $state<string | null>(null);
|
||||
let renameDraft = $state('');
|
||||
let renameOriginalTitle = $state('');
|
||||
|
||||
const renderedOrderIds = $derived(
|
||||
buildConversationTree(filteredConversations).map((t) => t.conversation.id)
|
||||
);
|
||||
|
||||
const allSelectedArePinned = $derived.by(() => {
|
||||
if (selectedIds.size === 0) return false;
|
||||
const convs = conversations();
|
||||
for (const id of selectedIds) {
|
||||
const c = convs.find((conv) => conv.id === id);
|
||||
if (c && !c.pinned) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const pinStateIsMixed = $derived.by(() => {
|
||||
if (selectedIds.size === 0) return false;
|
||||
const convs = conversations();
|
||||
let anyPinned = false;
|
||||
let anyUnpinned = false;
|
||||
for (const id of selectedIds) {
|
||||
const c = convs.find((conv) => conv.id === id);
|
||||
if (!c) continue;
|
||||
if (c.pinned) anyPinned = true;
|
||||
else anyUnpinned = true;
|
||||
if (anyPinned && anyUnpinned) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const visibleSelectionStats = $derived.by(() => {
|
||||
const visibleIds = filteredConversations.map((c) => c.id);
|
||||
let selectedVisible = 0;
|
||||
for (const id of visibleIds) {
|
||||
if (selectedIds.has(id)) selectedVisible++;
|
||||
}
|
||||
return {
|
||||
visibleCount: visibleIds.length,
|
||||
selectedVisibleCount: selectedVisible
|
||||
};
|
||||
});
|
||||
|
||||
function enterSelectionMode(id?: string) {
|
||||
isSelectionMode = true;
|
||||
if (id !== undefined) {
|
||||
selectedIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
function exitSelectionMode() {
|
||||
isSelectionMode = false;
|
||||
selectedIds.clear();
|
||||
}
|
||||
|
||||
function toggleSelected(id: string) {
|
||||
if (selectedIds.has(id)) {
|
||||
selectedIds.delete(id);
|
||||
} else {
|
||||
selectedIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelectAllVisible() {
|
||||
const visibleIds = filteredConversations.map((c) => c.id);
|
||||
const allSelected = visibleIds.length > 0 && visibleIds.every((id) => selectedIds.has(id));
|
||||
|
||||
if (allSelected) {
|
||||
for (const id of visibleIds) selectedIds.delete(id);
|
||||
} else {
|
||||
for (const id of visibleIds) selectedIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBulkDelete() {
|
||||
const ids = Array.from(selectedIds);
|
||||
if (ids.length === 0) return;
|
||||
await conversationsStore.bulkDeleteConversations(ids);
|
||||
exitSelectionMode();
|
||||
}
|
||||
|
||||
async function handleBulkPinToggle() {
|
||||
const ids = Array.from(selectedIds);
|
||||
if (ids.length === 0) return;
|
||||
await conversationsStore.bulkToggleConversationPin(ids);
|
||||
}
|
||||
|
||||
async function handleBulkExport() {
|
||||
const ids = Array.from(selectedIds);
|
||||
if (ids.length === 0) return;
|
||||
await conversationsStore.bulkExportConversations(ids);
|
||||
}
|
||||
|
||||
const marquee = useMarqueeSelection({
|
||||
selectedIds: () => selectedIds,
|
||||
orderedIds: () => renderedOrderIds,
|
||||
enabled: () => isSelectionMode
|
||||
});
|
||||
|
||||
function handleRowMouseDown(id: string, event: MouseEvent) {
|
||||
if (!isSelectionMode) return;
|
||||
marquee.rowMouseDown(id, event);
|
||||
}
|
||||
|
||||
function handleSelectionClick(id: string, options: { shiftKey: boolean }): void {
|
||||
if (!isSelectionMode) return;
|
||||
marquee.rowClick(id, options.shiftKey);
|
||||
}
|
||||
|
||||
async function selectConversation(id: string) {
|
||||
if (isMobile.current) {
|
||||
scheduleMobileCollapse();
|
||||
@@ -100,10 +219,30 @@
|
||||
const conversation = conversations().find((conv) => conv.id === id);
|
||||
if (!conversation) return;
|
||||
|
||||
const newName = window.prompt('Rename conversation', conversation.name);
|
||||
if (newName && newName.trim()) {
|
||||
await conversationsStore.updateConversationName(id, newName.trim());
|
||||
}
|
||||
renameTargetConversationId = id;
|
||||
renameOriginalTitle = conversation.name;
|
||||
renameDraft = conversation.name;
|
||||
renameDialogOpen = true;
|
||||
}
|
||||
|
||||
async function handleRenameConfirm() {
|
||||
const id = renameTargetConversationId;
|
||||
if (!id) return;
|
||||
|
||||
const nextName = renameDraft.trim();
|
||||
if (!nextName || nextName === renameOriginalTitle.trim()) return;
|
||||
|
||||
await conversationsStore.updateConversationName(id, nextName);
|
||||
|
||||
renameDialogOpen = false;
|
||||
renameTargetConversationId = null;
|
||||
}
|
||||
|
||||
function handleRenameCancel() {
|
||||
renameDialogOpen = false;
|
||||
renameTargetConversationId = null;
|
||||
renameDraft = '';
|
||||
renameOriginalTitle = '';
|
||||
}
|
||||
|
||||
async function handleDeleteConversation(id: string) {
|
||||
@@ -148,9 +287,7 @@
|
||||
{#if innerWidth > 768 || (!page.url.hash.includes(ROUTES.SETTINGS) && !page.url.hash.includes(ROUTES.MCP_SERVERS) && !page.url.hash.includes(ROUTES.SEARCH))}
|
||||
<aside
|
||||
class={[
|
||||
// Layout & positioning
|
||||
'fixed md:sticky top-2 left-2 md:left-0 md:ml-2 md:mt-2 pt-2 z-10 w-[calc(100dvw-1rem)]',
|
||||
// Dimensions & overflow
|
||||
'md:h-[calc(100dvh-1.125rem)]',
|
||||
isExpandedMode &&
|
||||
(device.isStandalone
|
||||
@@ -158,17 +295,11 @@
|
||||
: device.isIOSDevice
|
||||
? 'h-[calc(100dvh-0.5rem)]'
|
||||
: 'h-[calc(100dvh-1rem)]'),
|
||||
// Shape & depth
|
||||
'rounded-3xl md:rounded-2xl',
|
||||
// Flex layout
|
||||
'flex flex-col justify-between',
|
||||
// Transition
|
||||
'md:transition-[width,padding] duration-200 ease-out',
|
||||
// Expanded state: width, surface, depth
|
||||
isStripExpanded && 'md:w-72 md:bg-muted/60 md:backdrop-blur-xl border-border shadow-md',
|
||||
// Collapsed state
|
||||
!isStripExpanded && 'md:w-12',
|
||||
// Expanded mode flag (for mobile ::before overlay)
|
||||
isExpandedMode && 'is-expanded'
|
||||
]}
|
||||
>
|
||||
@@ -218,52 +349,78 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex min-h-0 flex-1 flex-col gap-4 md:gap-1 overflow-y-auto">
|
||||
<div
|
||||
class="flex min-h-0 flex-1 flex-col gap-4 md:gap-1 {isMobile.current
|
||||
? 'transition-[opacity,height] duration-200 ease-out'
|
||||
: ''} {isMobile.current && !isExpandedMode ? 'opacity-0 !h-0' : ''}"
|
||||
in:fade={{ duration: 200 }}
|
||||
out:fade={{ duration: 200 }}
|
||||
>
|
||||
<SidebarNavigationActions
|
||||
isExpandedMode={innerWidth > 768 ? isExpandedMode : true}
|
||||
class="px-2"
|
||||
bind:isSearchModeActive
|
||||
bind:searchQuery
|
||||
onSearchDeactivated={() => {
|
||||
isSearchModeActive = false;
|
||||
searchQuery = '';
|
||||
}}
|
||||
onSearchClick={() => {
|
||||
isExpandedMode = true;
|
||||
isSearchModeActive = true;
|
||||
}}
|
||||
onNewChat={() => {
|
||||
if (isMobile.current) {
|
||||
scheduleMobileCollapse();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="mt-2 flex min-h-0 flex-1 flex-col gap-4 md:gap-1 {isMobile.current
|
||||
? 'transition-[opacity,height] duration-200 ease-out'
|
||||
: ''} {isMobile.current && !isExpandedMode ? 'opacity-0 !h-0' : ''}"
|
||||
in:fade={{ duration: 200 }}
|
||||
out:fade={{ duration: 200 }}
|
||||
>
|
||||
<SidebarNavigationActions
|
||||
isExpandedMode={innerWidth > 768 ? isExpandedMode : true}
|
||||
class="px-2"
|
||||
bind:isSearchModeActive
|
||||
bind:searchQuery
|
||||
onSearchDeactivated={() => {
|
||||
isSearchModeActive = false;
|
||||
searchQuery = '';
|
||||
}}
|
||||
onSearchClick={() => {
|
||||
isExpandedMode = true;
|
||||
isSearchModeActive = true;
|
||||
}}
|
||||
onNewChat={() => {
|
||||
if (isMobile.current) {
|
||||
scheduleMobileCollapse();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if isExpandedMode || isOnMobile}
|
||||
{#if isExpandedMode || isOnMobile}
|
||||
<div class="flex min-h-0 flex-1 flex-col overflow-y-auto">
|
||||
<SidebarNavigationConversationList
|
||||
class="px-2"
|
||||
{filteredConversations}
|
||||
{currentChatId}
|
||||
{isSearchModeActive}
|
||||
{searchQuery}
|
||||
{isSelectionMode}
|
||||
{selectedIds}
|
||||
onSelect={selectConversation}
|
||||
onEdit={handleEditConversation}
|
||||
onDelete={handleDeleteConversation}
|
||||
onStop={handleStopGeneration}
|
||||
onToggleSelect={toggleSelected}
|
||||
onEnterSelectionMode={enterSelectionMode}
|
||||
onSelectionClick={handleSelectionClick}
|
||||
onRowMouseDown={handleRowMouseDown}
|
||||
visibleCount={visibleSelectionStats.visibleCount}
|
||||
allVisibleSelected={visibleSelectionStats.visibleCount > 0 &&
|
||||
visibleSelectionStats.selectedVisibleCount === visibleSelectionStats.visibleCount}
|
||||
someVisibleSelected={visibleSelectionStats.selectedVisibleCount > 0 &&
|
||||
visibleSelectionStats.selectedVisibleCount < visibleSelectionStats.visibleCount}
|
||||
{allSelectedArePinned}
|
||||
{pinStateIsMixed}
|
||||
onSelectAllToggle={toggleSelectAllVisible}
|
||||
onBulkPinToggle={handleBulkPinToggle}
|
||||
onBulkExport={handleBulkExport}
|
||||
onBulkDelete={handleBulkDelete}
|
||||
onCloseSelection={exitSelectionMode}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<DialogConversationRename
|
||||
bind:open={renameDialogOpen}
|
||||
currentTitle={renameOriginalTitle}
|
||||
bind:value={renameDraft}
|
||||
onConfirm={handleRenameConfirm}
|
||||
onCancel={handleRenameCancel}
|
||||
/>
|
||||
|
||||
<style>
|
||||
aside {
|
||||
@media (max-width: 768px) {
|
||||
|
||||
+4
-10
@@ -119,9 +119,7 @@
|
||||
: onSearchClick}
|
||||
{@const itemTransition = {
|
||||
duration: ICON_STRIP_TRANSITION_DURATION,
|
||||
delay: !initialized
|
||||
? ICON_STRIP_TRANSITION_DELAY_MULTIPLIER + i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER
|
||||
: 0,
|
||||
delay: !initialized ? i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER : 0,
|
||||
easing: circIn
|
||||
}}
|
||||
|
||||
@@ -140,10 +138,8 @@
|
||||
{@render itemIcon(item.icon)}
|
||||
|
||||
{#if showIcons}
|
||||
<span
|
||||
in:fade={{ duration: 150, easing: circIn, delay: 50 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
class="min-w-0 truncate">{item.tooltip}</span
|
||||
<span in:fade={itemTransition} out:fade={itemTransition} class="min-w-0 truncate"
|
||||
>{item.tooltip}</span
|
||||
>
|
||||
{/if}
|
||||
</span>
|
||||
@@ -171,9 +167,7 @@
|
||||
: onSearchClick}
|
||||
{@const itemTransition = {
|
||||
duration: ICON_STRIP_TRANSITION_DURATION,
|
||||
delay: !initialized
|
||||
? ICON_STRIP_TRANSITION_DELAY_MULTIPLIER + i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER
|
||||
: 0,
|
||||
delay: !initialized ? i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER : 0,
|
||||
easing: circIn
|
||||
}}
|
||||
|
||||
|
||||
+84
-6
@@ -9,10 +9,12 @@
|
||||
Square,
|
||||
GitBranch,
|
||||
Pin,
|
||||
PinOff
|
||||
PinOff,
|
||||
ListChecks
|
||||
} from '@lucide/svelte';
|
||||
import { DropdownMenuActions } from '$lib/components/app';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import { FORK_TREE_DEPTH_PADDING } from '$lib/constants';
|
||||
import { RouterService } from '$lib/services/router.service';
|
||||
import { getAllLoadingChats } from '$lib/stores/chat.svelte';
|
||||
@@ -24,10 +26,16 @@
|
||||
isActive?: boolean;
|
||||
depth?: number;
|
||||
conversation: DatabaseConversation;
|
||||
isSelectionMode?: boolean;
|
||||
isSelected?: boolean;
|
||||
onDelete?: (id: string) => void;
|
||||
onEdit?: (id: string) => void;
|
||||
onSelect?: (id: string) => void;
|
||||
onStop?: (id: string) => void;
|
||||
onToggleSelect?: (id: string) => void;
|
||||
onEnterSelectionMode?: (id: string) => void;
|
||||
onSelectionClick?: (id: string, options: { shiftKey: boolean }) => void;
|
||||
onRowMouseDown?: (id: string, event: MouseEvent) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -36,7 +44,13 @@
|
||||
onEdit,
|
||||
onSelect,
|
||||
onStop,
|
||||
onToggleSelect,
|
||||
onEnterSelectionMode,
|
||||
onSelectionClick,
|
||||
onRowMouseDown,
|
||||
isActive = false,
|
||||
isSelectionMode = false,
|
||||
isSelected = false,
|
||||
depth = 0
|
||||
}: Props = $props();
|
||||
|
||||
@@ -64,6 +78,11 @@
|
||||
conversationsStore.toggleConversationPin(conversation.id);
|
||||
}
|
||||
|
||||
function handleEnterSelectionMode(event: Event) {
|
||||
event.stopPropagation();
|
||||
onEnterSelectionMode?.(conversation.id);
|
||||
}
|
||||
|
||||
function handleGlobalEditEvent(event: Event) {
|
||||
const customEvent = event as CustomEvent<{ conversationId: string }>;
|
||||
|
||||
@@ -79,11 +98,40 @@
|
||||
}
|
||||
|
||||
function handleMouseOver() {
|
||||
if (isSelectionMode) return;
|
||||
renderActionsDropdown = true;
|
||||
}
|
||||
|
||||
function handleSelect() {
|
||||
onSelect?.(conversation.id);
|
||||
function handleSelect(event: MouseEvent) {
|
||||
if (isSelectionMode) {
|
||||
onSelectionClick?.(conversation.id, { shiftKey: event.shiftKey });
|
||||
} else {
|
||||
onSelect?.(conversation.id);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCheckboxClick(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
if (isSelectionMode) {
|
||||
onSelectionClick?.(conversation.id, { shiftKey: event.shiftKey });
|
||||
} else {
|
||||
onToggleSelect?.(conversation.id);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRowMouseDown(event: MouseEvent) {
|
||||
onRowMouseDown?.(conversation.id, event);
|
||||
}
|
||||
|
||||
function handleCheckboxKeydown(event: KeyboardEvent) {
|
||||
if (event.key !== ' ' && event.key !== 'Enter') return;
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (isSelectionMode) {
|
||||
onSelectionClick?.(conversation.id, { shiftKey: event.shiftKey });
|
||||
} else {
|
||||
onToggleSelect?.(conversation.id);
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
@@ -108,10 +156,14 @@
|
||||
<button
|
||||
class="group flex min-h-9 w-full cursor-pointer items-center justify-between space-x-3 rounded-lg py-1.5 text-left transition-colors hover:bg-foreground/10 {isActive
|
||||
? 'bg-foreground/5 text-accent-foreground'
|
||||
: ''} px-3"
|
||||
onclick={handleSelect}
|
||||
: ''} {isSelected ? 'bg-primary/10 hover:bg-primary/15' : ''} {isSelectionMode
|
||||
? 'is-selection-mode'
|
||||
: ''} px-2"
|
||||
data-conversation-row={conversation.id}
|
||||
onclick={(e) => handleSelect(e)}
|
||||
onmouseover={handleMouseOver}
|
||||
onmouseleave={handleMouseLeave}
|
||||
onmousedown={(e) => handleRowMouseDown(e)}
|
||||
onfocusin={handleMouseOver}
|
||||
onfocusout={(e) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {
|
||||
@@ -123,6 +175,23 @@
|
||||
class="flex min-w-0 flex-1 items-center gap-2"
|
||||
style:padding-left="{depth * FORK_TREE_DEPTH_PADDING}px"
|
||||
>
|
||||
{#if isSelectionMode}
|
||||
<div
|
||||
class="shrink-0"
|
||||
onclick={(e) => handleCheckboxClick(e)}
|
||||
onkeydown={handleCheckboxKeydown}
|
||||
role="checkbox"
|
||||
aria-checked={isSelected}
|
||||
aria-label={isSelected ? `Deselect ${conversation.name}` : `Select ${conversation.name}`}
|
||||
tabindex="-1"
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
aria-label={isSelected ? `Deselect ${conversation.name}` : `Select ${conversation.name}`}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if depth > 0}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
@@ -170,7 +239,7 @@
|
||||
<TruncatedText text={conversation.name} class="text-sm font-medium" showTooltip={false} />
|
||||
</div>
|
||||
|
||||
{#if renderActionsDropdown}
|
||||
{#if !isSelectionMode && renderActionsDropdown}
|
||||
<div class="actions flex items-center">
|
||||
<DropdownMenuActions
|
||||
triggerIcon={MoreHorizontal}
|
||||
@@ -200,6 +269,11 @@
|
||||
},
|
||||
shortcut: ['shift', 'cmd', 's']
|
||||
},
|
||||
{
|
||||
icon: ListChecks,
|
||||
label: 'Select',
|
||||
onclick: handleEnterSelectionMode
|
||||
},
|
||||
{
|
||||
icon: Trash2,
|
||||
label: 'Delete',
|
||||
@@ -230,6 +304,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.is-selection-mode :global([data-slot='dropdown-menu-trigger']) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.stop-button {
|
||||
:global(.stop-icon) {
|
||||
display: none;
|
||||
|
||||
+137
-67
@@ -3,6 +3,7 @@
|
||||
import { buildConversationTree } from '$lib/stores/conversations.svelte';
|
||||
import SidebarNavigationConversationItem from './SidebarNavigationConversationItem.svelte';
|
||||
import SidebarNavigationSearchResults from './SidebarNavigationSearchResults.svelte';
|
||||
import SidebarNavigationSelectionBar from './SidebarNavigationSelectionBar.svelte';
|
||||
|
||||
interface Props {
|
||||
class: string;
|
||||
@@ -10,10 +11,26 @@
|
||||
currentChatId: string | undefined;
|
||||
isSearchModeActive: boolean;
|
||||
searchQuery: string;
|
||||
isSelectionMode?: boolean;
|
||||
selectedIds?: Set<string>;
|
||||
onSelect: (id: string) => void;
|
||||
onEdit: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onStop: (id: string) => void;
|
||||
onToggleSelect?: (id: string) => void;
|
||||
onEnterSelectionMode?: (id: string) => void;
|
||||
onSelectionClick?: (id: string, options: { shiftKey: boolean }) => void;
|
||||
onRowMouseDown?: (id: string, event: MouseEvent) => void;
|
||||
visibleCount: number;
|
||||
allVisibleSelected: boolean;
|
||||
someVisibleSelected: boolean;
|
||||
allSelectedArePinned: boolean;
|
||||
pinStateIsMixed: boolean;
|
||||
onSelectAllToggle: () => void;
|
||||
onBulkPinToggle: () => void;
|
||||
onBulkExport: () => void;
|
||||
onBulkDelete: () => void;
|
||||
onCloseSelection: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -22,10 +39,26 @@
|
||||
currentChatId,
|
||||
isSearchModeActive,
|
||||
searchQuery,
|
||||
isSelectionMode = false,
|
||||
selectedIds = new Set<string>(),
|
||||
onSelect,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onStop
|
||||
onStop,
|
||||
onToggleSelect,
|
||||
onEnterSelectionMode,
|
||||
onSelectionClick,
|
||||
onRowMouseDown,
|
||||
visibleCount,
|
||||
allVisibleSelected,
|
||||
someVisibleSelected,
|
||||
allSelectedArePinned,
|
||||
pinStateIsMixed,
|
||||
onSelectAllToggle,
|
||||
onBulkPinToggle,
|
||||
onBulkExport,
|
||||
onBulkDelete,
|
||||
onCloseSelection
|
||||
}: Props = $props();
|
||||
|
||||
let conversationTree = $derived(buildConversationTree(filteredConversations));
|
||||
@@ -43,65 +76,38 @@
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if isSearchModeActive}
|
||||
<SidebarNavigationSearchResults
|
||||
class={className}
|
||||
{searchQuery}
|
||||
{filteredConversations}
|
||||
{currentChatId}
|
||||
{onSelect}
|
||||
{onEdit}
|
||||
{onDelete}
|
||||
{onStop}
|
||||
/>
|
||||
{:else}
|
||||
{#if pinnedConversations.length > 0}
|
||||
<div class="py-2 flex whitespace-nowrap {className}">
|
||||
<div
|
||||
class="text-muted-foreground inline-flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium gap-1"
|
||||
>
|
||||
<Pin class="h-3.5 w-3.5" />
|
||||
<div class="flex min-h-0 flex-1 flex-col">
|
||||
{#if isSearchModeActive}
|
||||
<SidebarNavigationSearchResults
|
||||
class={className}
|
||||
{searchQuery}
|
||||
{filteredConversations}
|
||||
{currentChatId}
|
||||
{onSelect}
|
||||
{onEdit}
|
||||
{onDelete}
|
||||
{onStop}
|
||||
{isSelectionMode}
|
||||
{selectedIds}
|
||||
{onToggleSelect}
|
||||
{onEnterSelectionMode}
|
||||
{onSelectionClick}
|
||||
{onRowMouseDown}
|
||||
/>
|
||||
{:else}
|
||||
{#if pinnedConversations.length > 0}
|
||||
<div class="py-2 flex whitespace-nowrap {className}">
|
||||
<div
|
||||
class="text-muted-foreground inline-flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium gap-1"
|
||||
>
|
||||
<Pin class="h-3.5 w-3.5" />
|
||||
|
||||
<span>Pinned</span>
|
||||
<span>Pinned</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="flex w-full min-w-0 flex-col gap-4 md:gap-1 {className}">
|
||||
{#each pinnedConversations as { conversation, depth } (conversation.id)}
|
||||
<li class="group/item relative mb-1 p-0">
|
||||
<SidebarNavigationConversationItem
|
||||
conversation={{
|
||||
id: conversation.id,
|
||||
name: conversation.name,
|
||||
lastModified: conversation.lastModified,
|
||||
currNode: conversation.currNode,
|
||||
forkedFromConversationId: conversation.forkedFromConversationId,
|
||||
pinned: conversation.pinned
|
||||
}}
|
||||
{depth}
|
||||
isActive={currentChatId === conversation.id}
|
||||
{onSelect}
|
||||
{onEdit}
|
||||
{onDelete}
|
||||
{onStop}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<div class="mt-2 flex min-h-0 flex-1 flex-col gap-4 md:gap-2 whitespace-nowrap {className}">
|
||||
{#if filteredConversations.length > 0}
|
||||
<div
|
||||
class="text-muted-foreground flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium"
|
||||
>
|
||||
Recent conversations
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="min-h-0 flex-1 md:overflow-y-auto">
|
||||
<ul class="flex w-full min-w-0 flex-col gap-4 md:gap-1">
|
||||
{#each unpinnedConversations as { conversation, depth } (conversation.id)}
|
||||
<ul class="flex w-full min-w-0 flex-col gap-4 md:gap-1 {className}">
|
||||
{#each pinnedConversations as { conversation, depth } (conversation.id)}
|
||||
<li class="group/item relative mb-1 p-0">
|
||||
<SidebarNavigationConversationItem
|
||||
conversation={{
|
||||
@@ -114,22 +120,86 @@
|
||||
}}
|
||||
{depth}
|
||||
isActive={currentChatId === conversation.id}
|
||||
{isSelectionMode}
|
||||
isSelected={selectedIds.has(conversation.id)}
|
||||
{onSelect}
|
||||
{onEdit}
|
||||
{onDelete}
|
||||
{onStop}
|
||||
{onToggleSelect}
|
||||
{onEnterSelectionMode}
|
||||
{onSelectionClick}
|
||||
{onRowMouseDown}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
{#if unpinnedConversations.length === 0}
|
||||
<li class="px-2 py-4 text-center">
|
||||
<p class="mb-4 p-4 text-sm text-muted-foreground">
|
||||
{recentEmptyMessage}
|
||||
</p>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<div class="mt-2 flex min-h-0 flex-1 flex-col gap-4 md:gap-0 whitespace-nowrap {className}">
|
||||
{#if filteredConversations.length > 0}
|
||||
<div
|
||||
class="text-muted-foreground flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium"
|
||||
>
|
||||
Recent conversations
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="min-h-0 flex-1 md:overflow-y-auto">
|
||||
<ul class="flex w-full min-w-0 flex-col gap-4 md:gap-0">
|
||||
{#each unpinnedConversations as { conversation, depth } (conversation.id)}
|
||||
<li class="group/item relative mb-1 p-0">
|
||||
<SidebarNavigationConversationItem
|
||||
conversation={{
|
||||
id: conversation.id,
|
||||
name: conversation.name,
|
||||
lastModified: conversation.lastModified,
|
||||
currNode: conversation.currNode,
|
||||
forkedFromConversationId: conversation.forkedFromConversationId,
|
||||
pinned: conversation.pinned
|
||||
}}
|
||||
{depth}
|
||||
isActive={currentChatId === conversation.id}
|
||||
{isSelectionMode}
|
||||
isSelected={selectedIds.has(conversation.id)}
|
||||
{onSelect}
|
||||
{onEdit}
|
||||
{onDelete}
|
||||
{onStop}
|
||||
{onToggleSelect}
|
||||
{onEnterSelectionMode}
|
||||
{onSelectionClick}
|
||||
{onRowMouseDown}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
{#if unpinnedConversations.length === 0}
|
||||
<li class="px-2 py-4 text-center">
|
||||
<p class="mb-4 p-4 text-sm text-muted-foreground">
|
||||
{recentEmptyMessage}
|
||||
</p>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isSelectionMode}
|
||||
<SidebarNavigationSelectionBar
|
||||
class="sticky top-0 z-10 m-2 mt-0"
|
||||
selectedCount={selectedIds.size}
|
||||
{visibleCount}
|
||||
{allVisibleSelected}
|
||||
{someVisibleSelected}
|
||||
someSelectedPinned={allSelectedArePinned}
|
||||
{pinStateIsMixed}
|
||||
{onSelectAllToggle}
|
||||
{onBulkPinToggle}
|
||||
{onBulkExport}
|
||||
{onBulkDelete}
|
||||
onClose={onCloseSelection}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
+19
-1
@@ -7,10 +7,16 @@
|
||||
searchQuery: string;
|
||||
filteredConversations: DatabaseConversation[];
|
||||
currentChatId: string | undefined;
|
||||
isSelectionMode?: boolean;
|
||||
selectedIds?: Set<string>;
|
||||
onSelect: (id: string) => void;
|
||||
onEdit: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onStop: (id: string) => void;
|
||||
onToggleSelect?: (id: string) => void;
|
||||
onEnterSelectionMode?: (id: string) => void;
|
||||
onSelectionClick?: (id: string, options: { shiftKey: boolean }) => void;
|
||||
onRowMouseDown?: (id: string, event: MouseEvent) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -18,10 +24,16 @@
|
||||
searchQuery,
|
||||
filteredConversations,
|
||||
currentChatId,
|
||||
isSelectionMode = false,
|
||||
selectedIds = new Set<string>(),
|
||||
onSelect,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onStop
|
||||
onStop,
|
||||
onToggleSelect,
|
||||
onEnterSelectionMode,
|
||||
onSelectionClick,
|
||||
onRowMouseDown
|
||||
}: Props = $props();
|
||||
|
||||
let tree = $derived(buildConversationTree(filteredConversations));
|
||||
@@ -56,10 +68,16 @@
|
||||
}}
|
||||
{depth}
|
||||
isActive={currentChatId === conversation.id}
|
||||
{isSelectionMode}
|
||||
isSelected={selectedIds.has(conversation.id)}
|
||||
{onSelect}
|
||||
{onEdit}
|
||||
{onDelete}
|
||||
{onStop}
|
||||
{onToggleSelect}
|
||||
{onEnterSelectionMode}
|
||||
{onSelectionClick}
|
||||
{onRowMouseDown}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
<script lang="ts">
|
||||
import { Download, Pin, PinOff, Trash2, X } from '@lucide/svelte';
|
||||
import { ActionIcon, DialogConfirmation } from '$lib/components/app';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import { TooltipSide } from '$lib/enums';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
selectedCount: number;
|
||||
visibleCount: number;
|
||||
allVisibleSelected: boolean;
|
||||
someVisibleSelected: boolean;
|
||||
someSelectedPinned: boolean;
|
||||
pinStateIsMixed: boolean;
|
||||
onSelectAllToggle: () => void;
|
||||
onBulkPinToggle: () => void;
|
||||
onBulkExport: () => void;
|
||||
onBulkDelete: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
class: className = '',
|
||||
selectedCount,
|
||||
visibleCount,
|
||||
allVisibleSelected,
|
||||
someVisibleSelected,
|
||||
someSelectedPinned,
|
||||
pinStateIsMixed,
|
||||
onSelectAllToggle,
|
||||
onBulkPinToggle,
|
||||
onBulkExport,
|
||||
onBulkDelete,
|
||||
onClose
|
||||
}: Props = $props();
|
||||
|
||||
let showDeleteDialog = $state(false);
|
||||
|
||||
function handleDeleteClick() {
|
||||
showDeleteDialog = true;
|
||||
}
|
||||
|
||||
function handleDeleteConfirm() {
|
||||
showDeleteDialog = false;
|
||||
onBulkDelete();
|
||||
}
|
||||
|
||||
function handleDeleteCancel() {
|
||||
showDeleteDialog = false;
|
||||
}
|
||||
|
||||
const hasSelection = $derived(selectedCount > 0);
|
||||
const isMasterChecked = $derived(allVisibleSelected);
|
||||
const isMasterIndeterminate = $derived(!allVisibleSelected && someVisibleSelected);
|
||||
|
||||
const pinTooltip = $derived(
|
||||
hasSelection
|
||||
? pinStateIsMixed
|
||||
? 'Unavailable for mixed state selection'
|
||||
: someSelectedPinned
|
||||
? selectedCount === 1
|
||||
? 'Unpin'
|
||||
: 'Unpin all'
|
||||
: selectedCount === 1
|
||||
? 'Pin'
|
||||
: 'Pin all'
|
||||
: 'Pin'
|
||||
);
|
||||
|
||||
const pinDisabled = $derived(!hasSelection || pinStateIsMixed);
|
||||
</script>
|
||||
|
||||
<div
|
||||
role="toolbar"
|
||||
aria-label="Bulk actions for selected conversations"
|
||||
class="flex items-center gap-1.5 rounded-xl border border-border/50 bg-background/50 px-2 py-1.5 shadow-sm backdrop-blur-xl {className}"
|
||||
>
|
||||
<label class="flex min-w-0 cursor-pointer items-center gap-2">
|
||||
<Checkbox
|
||||
checked={isMasterChecked}
|
||||
indeterminate={isMasterIndeterminate}
|
||||
onCheckedChange={onSelectAllToggle}
|
||||
aria-label={isMasterChecked ? 'Deselect all' : 'Select all'}
|
||||
/>
|
||||
|
||||
<span class="truncate text-xs font-medium text-muted-foreground">
|
||||
{selectedCount} / {visibleCount} selected
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="ml-auto flex items-center gap-0.75">
|
||||
<ActionIcon
|
||||
icon={someSelectedPinned ? PinOff : Pin}
|
||||
tooltip={pinTooltip}
|
||||
tooltipSide={TooltipSide.TOP}
|
||||
disabled={pinDisabled}
|
||||
ariaLabel={pinTooltip}
|
||||
size="sm"
|
||||
iconSize="h-3.5 w-3.5"
|
||||
class="h-7 w-7 rounded-md bg-transparent backdrop-blur-none hover:bg-accent! {pinDisabled
|
||||
? 'cursor-not-allowed'
|
||||
: ''} {!pinDisabled ? 'opacity-100' : 'opacity-40'}"
|
||||
onclick={onBulkPinToggle}
|
||||
/>
|
||||
|
||||
<ActionIcon
|
||||
icon={Download}
|
||||
tooltip={hasSelection ? 'Export' : 'Export'}
|
||||
tooltipSide={TooltipSide.TOP}
|
||||
disabled={!hasSelection}
|
||||
ariaLabel="Export selected"
|
||||
size="sm"
|
||||
iconSize="h-3.5 w-3.5"
|
||||
class="h-7 w-7 rounded-md bg-transparent backdrop-blur-none hover:bg-accent! {hasSelection
|
||||
? 'opacity-100'
|
||||
: 'opacity-40'}"
|
||||
onclick={onBulkExport}
|
||||
/>
|
||||
|
||||
<ActionIcon
|
||||
icon={Trash2}
|
||||
tooltip="Delete selected"
|
||||
tooltipSide={TooltipSide.TOP}
|
||||
disabled={!hasSelection}
|
||||
ariaLabel="Delete selected"
|
||||
size="sm"
|
||||
iconSize="h-3.5 w-3.5 text-destructive"
|
||||
class="h-7 w-7 rounded-md bg-transparent backdrop-blur-none hover:bg-destructive/10! dark:hover:bg-destructive/20! disabled:hover:bg-transparent {hasSelection
|
||||
? 'opacity-100'
|
||||
: 'opacity-40'}"
|
||||
onclick={handleDeleteClick}
|
||||
/>
|
||||
|
||||
<div class="mx-1 h-4 w-px bg-border" aria-hidden="true"></div>
|
||||
|
||||
<ActionIcon
|
||||
icon={X}
|
||||
tooltip="Exit bulk selection mode"
|
||||
tooltipSide={TooltipSide.TOP}
|
||||
ariaLabel="Exit bulk selection mode"
|
||||
size="sm"
|
||||
iconSize="h-3.5 w-3.5"
|
||||
class="h-7 w-7 rounded-md bg-transparent backdrop-blur-none hover:bg-accent!"
|
||||
onclick={onClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogConfirmation
|
||||
bind:open={showDeleteDialog}
|
||||
title="Delete {selectedCount} conversation{selectedCount === 1 ? '' : 's'}"
|
||||
description="This action cannot be undone. The selected conversation{selectedCount === 1
|
||||
? ''
|
||||
: 's'} and {selectedCount === 1
|
||||
? 'its'
|
||||
: 'their'} messages will be permanently removed, including any forks."
|
||||
confirmText={selectedCount === 1 ? 'Delete' : `Delete ${selectedCount}`}
|
||||
cancelText="Cancel"
|
||||
variant="destructive"
|
||||
icon={Trash2}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
onCancel={handleDeleteCancel}
|
||||
/>
|
||||
@@ -114,6 +114,36 @@ export { default as SidebarNavigation } from './SidebarNavigation/SidebarNavigat
|
||||
*/
|
||||
export { default as SidebarNavigationConversationItem } from './SidebarNavigation/SidebarNavigationConversationItem.svelte';
|
||||
|
||||
/**
|
||||
* **SidebarNavigationSelectionBar** - Bulk action toolbar for selection mode
|
||||
*
|
||||
* Rendered above the conversation list when the sidebar enters selection mode.
|
||||
* Hosts a master checkbox (with select-all / clear-all semantics over the
|
||||
* currently-visible items), a selected-count caption, and bulk actions for
|
||||
* pin/unpin, export, and delete. Delete uses
|
||||
* {@link DialogConfirmation} before invoking the bulk store method.
|
||||
*
|
||||
* Pure-presentational; all operations are delegated via callbacks so the
|
||||
* sidebar owns selection state and persistence.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <SidebarNavigationSelectionBar
|
||||
* selectedCount={selectedIds.size}
|
||||
* visibleCount={visibleConversations.length}
|
||||
* allVisibleSelected={...}
|
||||
* someVisibleSelected={...}
|
||||
* someSelectedPinned={...}
|
||||
* onSelectAllToggle={toggleSelectAll}
|
||||
* onBulkPinToggle={handleBulkPin}
|
||||
* onBulkExport={handleBulkExport}
|
||||
* onBulkDelete={handleBulkDelete}
|
||||
* onClose={exitSelectionMode}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export { default as SidebarNavigationSelectionBar } from './SidebarNavigation/SidebarNavigationSelectionBar.svelte';
|
||||
|
||||
/**
|
||||
* **SidebarNavigationConversationList** - Grouped conversation list
|
||||
*
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import Label from '$lib/components/ui/label/label.svelte';
|
||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { Textarea } from '$lib/components/ui/textarea';
|
||||
import { SETTING_CONFIG_INFO, SETTINGS_KEYS } from '$lib/constants';
|
||||
@@ -84,10 +85,7 @@
|
||||
type={field.isPositiveInteger ? 'number' : 'text'}
|
||||
{...field.isPositiveInteger ? { min: '1', step: '1' } : {}}
|
||||
value={currentValue}
|
||||
oninput={(e) => {
|
||||
// Update local config immediately for real-time badge feedback
|
||||
onConfigChange(field.key, e.currentTarget.value);
|
||||
}}
|
||||
oninput={(e) => onConfigChange(field.key, e.currentTarget.value)}
|
||||
placeholder={currentModelParams[field.key] != null
|
||||
? `Default: ${normalizeFloatingPoint(currentModelParams[field.key])}`
|
||||
: ''}
|
||||
@@ -236,6 +234,52 @@
|
||||
{field.help || SETTING_CONFIG_INFO[field.key]}
|
||||
</p>
|
||||
{/if}
|
||||
{:else if field.type === SettingsFieldType.RADIO && field.radioOptions}
|
||||
{@const radioOptions = field.radioOptions}
|
||||
{@const currentMode =
|
||||
radioOptions.find((o: { key: string }) => Boolean(localConfig[o.key]))?.value ??
|
||||
radioOptions[0].value}
|
||||
|
||||
<Label class="flex items-center gap-1.5 text-sm font-medium mb-4">
|
||||
{field.label}
|
||||
|
||||
{#if field.isExperimental}
|
||||
<FlaskConical class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
{/if}
|
||||
</Label>
|
||||
|
||||
<RadioGroup.Root
|
||||
class="gap-4"
|
||||
value={currentMode}
|
||||
onValueChange={(value) => {
|
||||
for (const opt of radioOptions) {
|
||||
onConfigChange(opt.key, opt.value === value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#each radioOptions as opt (opt.value)}
|
||||
{@const itemId = `${field.key}-${opt.value}`}
|
||||
<div class="flex items-center gap-2">
|
||||
<RadioGroup.Item value={opt.value} id={itemId} />
|
||||
<Label
|
||||
for={itemId}
|
||||
class="flex cursor-pointer items-center gap-1.5 text-sm font-normal"
|
||||
>
|
||||
{opt.label}
|
||||
|
||||
{#if opt.isExperimental}
|
||||
<FlaskConical class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
{/if}
|
||||
</Label>
|
||||
</div>
|
||||
{/each}
|
||||
</RadioGroup.Root>
|
||||
|
||||
{#if field.help || SETTING_CONFIG_INFO[field.key]}
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{field.help || SETTING_CONFIG_INFO[field.key]}
|
||||
</p>
|
||||
{/if}
|
||||
{:else if field.type === SettingsFieldType.CHECKBOX}
|
||||
<div class="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||
import { permissionsStore } from '$lib/stores/permissions.svelte';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
|
||||
import { ToolSource } from '$lib/enums/tools.enums';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
let expandedGroups = new SvelteSet<string>();
|
||||
@@ -69,12 +71,23 @@
|
||||
|
||||
{#each group.tools as entry (entry.key)}
|
||||
{@const toolName = entry.definition.function.name}
|
||||
{@const builtinUi =
|
||||
entry.source === ToolSource.BUILTIN || entry.source === ToolSource.FRONTEND
|
||||
? getBuiltinToolUi(toolName)
|
||||
: null}
|
||||
{@const displayLabel = builtinUi?.label ?? toolName}
|
||||
{@const IconComponent = builtinUi?.icon ?? null}
|
||||
{@const isEnabled = toolsStore.isToolEnabled(entry.key)}
|
||||
{@const permissionKey = entry.key}
|
||||
{@const isAlwaysAllowed = permissionsStore.hasTool(permissionKey)}
|
||||
|
||||
<div class="flex items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-muted/50">
|
||||
<TruncatedText text={toolName} class="flex-1" showTooltip={true} />
|
||||
<span class="flex min-w-0 flex-1 items-center gap-1.5">
|
||||
{#if IconComponent}
|
||||
<IconComponent class={ICON_CLASS_DEFAULT} />
|
||||
{/if}
|
||||
<TruncatedText text={displayLabel} class="min-w-0" showTooltip={true} />
|
||||
</span>
|
||||
|
||||
<div class="flex w-16 shrink-0 justify-center">
|
||||
<Checkbox
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
bind:ref
|
||||
data-slot="checkbox"
|
||||
class={cn(
|
||||
'peer flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary',
|
||||
'peer flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input bg-background shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary',
|
||||
className
|
||||
)}
|
||||
bind:checked
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import Root from './radio-group.svelte';
|
||||
import Item from './radio-group-item.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Item,
|
||||
//
|
||||
Root as RadioGroup,
|
||||
Item as RadioGroupItem
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
|
||||
import CircleIcon from '@lucide/svelte/icons/circle';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/components/ui/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<RadioGroupPrimitive.ItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<RadioGroupPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="radio-group-item"
|
||||
class={cn(
|
||||
'border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 dark:aria-invalid:border-destructive/50 flex size-4 rounded-full focus-visible:ring-3 aria-invalid:ring-3 group/radio-group-item peer relative aspect-square shrink-0 border outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked })}
|
||||
<div data-slot="radio-group-indicator" class="flex size-4 items-center justify-center">
|
||||
{#if checked}
|
||||
<CircleIcon
|
||||
class="bg-primary-foreground absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</RadioGroupPrimitive.Item>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/components/ui/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value = $bindable(''),
|
||||
...restProps
|
||||
}: RadioGroupPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<RadioGroupPrimitive.Root
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="radio-group"
|
||||
class={cn('grid gap-2 w-full', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -14,7 +14,6 @@ export const SETTINGS_KEYS = {
|
||||
SEND_ON_ENTER: 'sendOnEnter',
|
||||
ENABLE_CONTINUE_GENERATION: 'enableContinueGeneration',
|
||||
PDF_AS_IMAGE: 'pdfAsImage',
|
||||
ASK_FOR_TITLE_CONFIRMATION: 'askForTitleConfirmation',
|
||||
TITLE_GENERATION_USE_FIRST_LINE: 'titleGenerationUseFirstLine',
|
||||
TITLE_GENERATION_USE_LLM: 'titleGenerationUseLLM',
|
||||
TITLE_GENERATION_PROMPT: 'titleGenerationPrompt',
|
||||
@@ -60,15 +59,15 @@ export const SETTINGS_KEYS = {
|
||||
MCP_SERVERS: 'mcpServers',
|
||||
MCP_REQUEST_TIMEOUT_SECONDS: 'mcpRequestTimeoutSeconds',
|
||||
AGENTIC_MAX_TURNS: 'agenticMaxTurns',
|
||||
SHOW_TOOL_CALL_IN_PROGRESS: 'showToolCallInProgress',
|
||||
ALWAYS_SHOW_TOOL_CALL_CONTENT: 'alwaysShowToolCallContent',
|
||||
// Performance
|
||||
PRE_ENCODE_CONVERSATION: 'preEncodeConversation',
|
||||
// Developer
|
||||
DISABLE_REASONING_PARSING: 'disableReasoningParsing',
|
||||
EXCLUDE_REASONING_FROM_CONTEXT: 'excludeReasoningFromContext',
|
||||
SHOW_RAW_OUTPUT_SWITCH: 'showRawOutputSwitch',
|
||||
// PY_INTERPRETER_ENABLED: 'pyInterpreterEnabled',
|
||||
JS_SANDBOX_ENABLED: 'jsSandboxEnabled',
|
||||
// PY_INTERPRETER_ENABLED: 'pyInterpreterEnabled',
|
||||
CUSTOM_JSON: 'customJson',
|
||||
CUSTOM_CSS: 'customCss'
|
||||
} as const;
|
||||
|
||||
@@ -54,6 +54,34 @@ const COLOR_MODE_OPTIONS: Array<{ value: string; label: string; icon: Component
|
||||
{ value: ColorMode.DARK, label: 'Dark', icon: Moon }
|
||||
];
|
||||
|
||||
// Shared options for the title-generation radio group. Both paired registry entries
|
||||
// (USE_FIRST_LINE, USE_LLM) reference this list so labels stay in lockstep.
|
||||
const TITLE_GENERATION_RADIO_OPTIONS: Array<{
|
||||
value: string;
|
||||
label: string;
|
||||
key: string;
|
||||
isExperimental?: boolean;
|
||||
}> = [
|
||||
{
|
||||
value: 'firstLine',
|
||||
label: 'Use first non-empty line for the conversation title',
|
||||
key: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE
|
||||
},
|
||||
{
|
||||
value: 'llm',
|
||||
label: 'Generate title with LLM',
|
||||
key: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
|
||||
isExperimental: true
|
||||
}
|
||||
];
|
||||
|
||||
// Common shape for the conversation title radio entry.
|
||||
const TITLE_GENERATION_BASE = {
|
||||
type: SettingsFieldType.RADIO,
|
||||
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||
radioOptions: TITLE_GENERATION_RADIO_OPTIONS
|
||||
} as const;
|
||||
|
||||
const SETTINGS_REGISTRY: Record<string, SettingsSectionEntry> = {
|
||||
[SETTINGS_SECTION_SLUGS.GENERAL]: {
|
||||
title: SETTINGS_SECTION_TITLES.GENERAL,
|
||||
@@ -115,14 +143,15 @@ const SETTINGS_REGISTRY: Record<string, SettingsSectionEntry> = {
|
||||
}
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT,
|
||||
label: 'Copy text attachments as plain text',
|
||||
help: 'When copying a message with text attachments, combine them into a single plain text string instead of a special format that can be pasted back as attachments.',
|
||||
key: SETTINGS_KEYS.AUTO_MIC_ON_EMPTY,
|
||||
label: 'Show microphone on empty input',
|
||||
help: 'Automatically show microphone button instead of send button when textarea is empty for models with audio modality support.',
|
||||
defaultValue: false,
|
||||
type: SettingsFieldType.CHECKBOX,
|
||||
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||
isExperimental: true,
|
||||
sync: {
|
||||
serverKey: SETTINGS_KEYS.COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT,
|
||||
serverKey: SETTINGS_KEYS.AUTO_MIC_ON_EMPTY,
|
||||
paramType: SyncableParameterType.BOOLEAN
|
||||
}
|
||||
},
|
||||
@@ -140,54 +169,16 @@ const SETTINGS_REGISTRY: Record<string, SettingsSectionEntry> = {
|
||||
}
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.PDF_AS_IMAGE,
|
||||
label: 'Parse PDF as image',
|
||||
help: 'Parse PDF as image instead of text. Automatically falls back to text processing for non-vision models.',
|
||||
defaultValue: false,
|
||||
type: SettingsFieldType.CHECKBOX,
|
||||
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||
sync: {
|
||||
serverKey: SETTINGS_KEYS.PDF_AS_IMAGE,
|
||||
paramType: SyncableParameterType.BOOLEAN
|
||||
}
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.ASK_FOR_TITLE_CONFIRMATION,
|
||||
label: 'Ask for confirmation before changing conversation title',
|
||||
help: 'Ask for confirmation before automatically changing conversation title when editing the first message.',
|
||||
defaultValue: false,
|
||||
type: SettingsFieldType.CHECKBOX,
|
||||
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||
sync: {
|
||||
serverKey: SETTINGS_KEYS.ASK_FOR_TITLE_CONFIRMATION,
|
||||
paramType: SyncableParameterType.BOOLEAN
|
||||
}
|
||||
},
|
||||
{
|
||||
...TITLE_GENERATION_BASE,
|
||||
key: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE,
|
||||
label: 'Use first non-empty line for conversation title',
|
||||
help: 'Use only the first non-empty line of the prompt to generate the conversation title.',
|
||||
defaultValue: false,
|
||||
type: SettingsFieldType.CHECKBOX,
|
||||
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||
label: 'Conversation title',
|
||||
help: 'Choose how conversation titles are generated. The first non-empty line uses a fast deterministic rule; the LLM option uses a model-generated title from the first message exchange.',
|
||||
defaultValue: true,
|
||||
sync: {
|
||||
serverKey: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE,
|
||||
paramType: SyncableParameterType.BOOLEAN
|
||||
}
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
|
||||
label: 'Use LLM to generate conversation title',
|
||||
help: 'Use the LLM to automatically generate conversation titles based on the first message exchange.',
|
||||
defaultValue: false,
|
||||
type: SettingsFieldType.CHECKBOX,
|
||||
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||
isExperimental: true,
|
||||
sync: {
|
||||
serverKey: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
|
||||
paramType: SyncableParameterType.BOOLEAN
|
||||
}
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.TITLE_GENERATION_PROMPT,
|
||||
label: 'LLM title generation prompt',
|
||||
@@ -195,11 +186,36 @@ const SETTINGS_REGISTRY: Record<string, SettingsSectionEntry> = {
|
||||
defaultValue: TITLE_GENERATION.DEFAULT_PROMPT,
|
||||
type: SettingsFieldType.TEXTAREA,
|
||||
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||
dependsOn: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
|
||||
sync: {
|
||||
serverKey: SETTINGS_KEYS.TITLE_GENERATION_PROMPT,
|
||||
paramType: SyncableParameterType.STRING
|
||||
}
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT,
|
||||
label: 'Copy text attachments as plain text',
|
||||
help: 'When copying a message with text attachments, combine them into a single plain text string instead of a special format that can be pasted back as attachments.',
|
||||
defaultValue: false,
|
||||
type: SettingsFieldType.CHECKBOX,
|
||||
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||
sync: {
|
||||
serverKey: SETTINGS_KEYS.COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT,
|
||||
paramType: SyncableParameterType.BOOLEAN
|
||||
}
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.PDF_AS_IMAGE,
|
||||
label: 'Parse PDF as image',
|
||||
help: 'Parse PDF as image instead of text. Automatically falls back to text processing for non-vision models.',
|
||||
defaultValue: false,
|
||||
type: SettingsFieldType.CHECKBOX,
|
||||
section: SETTINGS_SECTION_SLUGS.GENERAL,
|
||||
sync: {
|
||||
serverKey: SETTINGS_KEYS.PDF_AS_IMAGE,
|
||||
paramType: SyncableParameterType.BOOLEAN
|
||||
}
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.MAX_IMAGE_RESOLUTION,
|
||||
label: 'Maximum image resolution (megapixels)',
|
||||
@@ -253,27 +269,14 @@ const SETTINGS_REGISTRY: Record<string, SettingsSectionEntry> = {
|
||||
}
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS,
|
||||
label: 'Show tool call in progress',
|
||||
key: SETTINGS_KEYS.ALWAYS_SHOW_TOOL_CALL_CONTENT,
|
||||
label: 'Always show tool call content',
|
||||
help: 'Automatically expand tool call details while executing and keep them expanded after completion.',
|
||||
defaultValue: false,
|
||||
type: SettingsFieldType.CHECKBOX,
|
||||
section: SETTINGS_SECTION_SLUGS.DISPLAY,
|
||||
sync: {
|
||||
serverKey: SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS,
|
||||
paramType: SyncableParameterType.BOOLEAN
|
||||
}
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.AUTO_MIC_ON_EMPTY,
|
||||
label: 'Show microphone on empty input',
|
||||
help: 'Automatically show microphone button instead of send button when textarea is empty for models with audio modality support.',
|
||||
defaultValue: false,
|
||||
type: SettingsFieldType.CHECKBOX,
|
||||
section: SETTINGS_SECTION_SLUGS.DISPLAY,
|
||||
isExperimental: true,
|
||||
sync: {
|
||||
serverKey: SETTINGS_KEYS.AUTO_MIC_ON_EMPTY,
|
||||
serverKey: SETTINGS_KEYS.ALWAYS_SHOW_TOOL_CALL_CONTENT,
|
||||
paramType: SyncableParameterType.BOOLEAN
|
||||
}
|
||||
},
|
||||
@@ -764,6 +767,17 @@ const NON_UI_SETTINGS: SettingsEntry[] = [
|
||||
defaultValue: '[]',
|
||||
type: SettingsFieldType.INPUT,
|
||||
sync: { serverKey: SETTINGS_KEYS.MCP_SERVERS, paramType: SyncableParameterType.STRING }
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
|
||||
label: 'Generate title with LLM',
|
||||
help: 'Counterpart of the conversation title radio; stored and synced without a dedicated UI field.',
|
||||
defaultValue: false,
|
||||
type: SettingsFieldType.CHECKBOX,
|
||||
sync: {
|
||||
serverKey: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
|
||||
paramType: SyncableParameterType.BOOLEAN
|
||||
}
|
||||
}
|
||||
// {
|
||||
// key: SETTINGS_KEYS.PY_INTERPRETER_ENABLED,
|
||||
@@ -812,7 +826,8 @@ export const SETTINGS_CHAT_SECTIONS: SettingsSection[] = [
|
||||
isPositiveInteger: s.isPositiveInteger,
|
||||
dependsOn: s.dependsOn,
|
||||
help: s.help,
|
||||
options: s.options
|
||||
options: s.options,
|
||||
radioOptions: s.radioOptions
|
||||
}))
|
||||
})),
|
||||
...STANDALONE_SECTIONS
|
||||
|
||||
@@ -22,5 +22,6 @@ export enum SettingsFieldType {
|
||||
INPUT = 'input',
|
||||
TEXTAREA = 'textarea',
|
||||
CHECKBOX = 'checkbox',
|
||||
SELECT = 'select'
|
||||
SELECT = 'select',
|
||||
RADIO = 'radio'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Reusable selection state-machine: shift+click/shift+drag range select plus
|
||||
* rubber-band marquee drag, anchored on the last clicked row. Both the sidebar
|
||||
* conversation list and the dialog conversations table share this.
|
||||
*
|
||||
* The hook mutates the consumer's SvelteSet<string> directly; the consumer
|
||||
* owns the source of truth and reads it like any other $state. orderedIds
|
||||
* must reflect the current visual order of selectable rows so the range
|
||||
* matches what the user sees on screen.
|
||||
*/
|
||||
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
interface UseMarqueeSelectionOptions {
|
||||
/** Latest selected-IDs set. Re-read per selection event so consumer-side reassignment works. */
|
||||
selectedIds: () => SvelteSet<string>;
|
||||
/** IDs in the current rendered order; used to compute shift+click ranges and gate marquee visibility. */
|
||||
orderedIds: () => string[];
|
||||
/** Document listeners attach only while the getter returns true. */
|
||||
enabled: () => boolean;
|
||||
/** DOM attribute key (after the `data-` prefix) that marks selectable rows. */
|
||||
attributeName?: () => string;
|
||||
/** Minimum pixel distance before a press becomes a marquee drag. */
|
||||
dragThresholdPx?: number;
|
||||
}
|
||||
|
||||
export function useMarqueeSelection(options: UseMarqueeSelectionOptions) {
|
||||
const dragThresholdPx = options.dragThresholdPx ?? 5;
|
||||
|
||||
let dragAnchorId = $state<string | null>(null);
|
||||
let isMarqueeDragging = $state(false);
|
||||
let mouseDownActive = false;
|
||||
let dragStartX = 0;
|
||||
let dragStartY = 0;
|
||||
let mousedownRowId: string | null = null;
|
||||
let dragMode: 'add' | 'remove' | null = null;
|
||||
let suppressNextClick = false;
|
||||
|
||||
function resolveAttributeName(): string {
|
||||
return options.attributeName?.() ?? 'conversation-row';
|
||||
}
|
||||
|
||||
/**
|
||||
* `dataset` keys are camelCased. `data-conversation-row` -> `conversationRow`.
|
||||
* We resolve the attribute name once per call and read via the camelCase key.
|
||||
*/
|
||||
function datasetKey(key: string = resolveAttributeName()): string {
|
||||
return key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
function decideDragMode(startingRowId: string | null, currentlySelected: ReadonlySet<string>) {
|
||||
return startingRowId !== null && currentlySelected.has(startingRowId) ? 'remove' : 'add';
|
||||
}
|
||||
|
||||
/**
|
||||
* Range-select uses Finder-style toggle-by-target semantics: if the target
|
||||
* row is currently selected the range becomes deselected, otherwise it
|
||||
* becomes selected. Anchor moves to `toId` so chained shift+clicks keep
|
||||
* extending from the previous endpoint.
|
||||
*/
|
||||
function rangeSelect(fromId: string, toId: string) {
|
||||
const selected = options.selectedIds();
|
||||
const order = options.orderedIds();
|
||||
const fromIdx = order.indexOf(fromId);
|
||||
const toIdx = order.indexOf(toId);
|
||||
if (fromIdx === -1 || toIdx === -1) return;
|
||||
const [lo, hi] = fromIdx < toIdx ? [fromIdx, toIdx] : [toIdx, fromIdx];
|
||||
const shouldSelect = !selected.has(toId);
|
||||
for (let i = lo; i <= hi; i++) {
|
||||
const id = order[i];
|
||||
if (shouldSelect) selected.add(id);
|
||||
else selected.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
function findRowAtPoint(x: number, y: number): string | null {
|
||||
const attr = resolveAttributeName();
|
||||
const selector = `[data-${attr}]`;
|
||||
const key = datasetKey(attr);
|
||||
let bestMatch: HTMLElement | null = null;
|
||||
let bestCenterDistance = Infinity;
|
||||
|
||||
for (const row of document.querySelectorAll<HTMLElement>(selector)) {
|
||||
const rect = row.getBoundingClientRect();
|
||||
if (y >= rect.top && y <= rect.bottom && x >= rect.left && x <= rect.right) {
|
||||
return row.dataset[key] ?? null;
|
||||
}
|
||||
if (x >= rect.left && x <= rect.right) {
|
||||
const centerDistance = Math.abs(y - (rect.top + rect.height / 2));
|
||||
if (centerDistance < bestCenterDistance) {
|
||||
bestCenterDistance = centerDistance;
|
||||
bestMatch = row;
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestMatch ? (bestMatch.dataset[key] ?? null) : null;
|
||||
}
|
||||
|
||||
function updateMarqueeRect(currentX: number, currentY: number) {
|
||||
const attr = resolveAttributeName();
|
||||
const selector = `[data-${attr}]`;
|
||||
const key = datasetKey(attr);
|
||||
const selected = options.selectedIds();
|
||||
const left = Math.min(dragStartX, currentX);
|
||||
const top = Math.min(dragStartY, currentY);
|
||||
const right = Math.max(dragStartX, currentX);
|
||||
const bottom = Math.max(dragStartY, currentY);
|
||||
const visibleIds = new SvelteSet(options.orderedIds());
|
||||
|
||||
for (const row of document.querySelectorAll<HTMLElement>(selector)) {
|
||||
const id = row.dataset[key];
|
||||
if (!id || !visibleIds.has(id)) continue;
|
||||
|
||||
const rect = row.getBoundingClientRect();
|
||||
const intersects = !(
|
||||
rect.right < left ||
|
||||
rect.left > right ||
|
||||
rect.bottom < top ||
|
||||
rect.top > bottom
|
||||
);
|
||||
|
||||
if (dragMode === 'add') {
|
||||
if (intersects) selected.add(id);
|
||||
} else if (dragMode === 'remove') {
|
||||
if (intersects && selected.has(id)) selected.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDocumentMouseMove(event: MouseEvent) {
|
||||
if (!mouseDownActive) return;
|
||||
|
||||
if (event.shiftKey && dragAnchorId !== null) {
|
||||
const target = findRowAtPoint(event.clientX, event.clientY);
|
||||
if (target && target !== mousedownRowId) rangeSelect(dragAnchorId, target);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isMarqueeDragging) {
|
||||
const dx = event.clientX - dragStartX;
|
||||
const dy = event.clientY - dragStartY;
|
||||
if (Math.hypot(dx, dy) < dragThresholdPx) return;
|
||||
isMarqueeDragging = true;
|
||||
dragMode = decideDragMode(mousedownRowId, options.selectedIds());
|
||||
}
|
||||
updateMarqueeRect(event.clientX, event.clientY);
|
||||
}
|
||||
|
||||
function handleDocumentMouseUp(event: MouseEvent) {
|
||||
if (isMarqueeDragging) {
|
||||
suppressNextClick = true;
|
||||
const target = findRowAtPoint(event.clientX, event.clientY);
|
||||
if (target) dragAnchorId = target;
|
||||
}
|
||||
isMarqueeDragging = false;
|
||||
mouseDownActive = false;
|
||||
mousedownRowId = null;
|
||||
dragMode = null;
|
||||
dragStartX = 0;
|
||||
dragStartY = 0;
|
||||
}
|
||||
|
||||
function handleClickCapture(event: MouseEvent) {
|
||||
if (suppressNextClick) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
suppressNextClick = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!options.enabled()) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
document.addEventListener('mousemove', handleDocumentMouseMove);
|
||||
document.addEventListener('mouseup', handleDocumentMouseUp);
|
||||
document.addEventListener('click', handleClickCapture, { capture: true });
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleDocumentMouseMove);
|
||||
document.removeEventListener('mouseup', handleDocumentMouseUp);
|
||||
document.removeEventListener('click', handleClickCapture, { capture: true });
|
||||
};
|
||||
});
|
||||
|
||||
function rowMouseDown(id: string, event: MouseEvent) {
|
||||
if (!options.enabled()) return;
|
||||
if (event.button !== 0) return;
|
||||
event.preventDefault();
|
||||
mouseDownActive = true;
|
||||
mousedownRowId = id;
|
||||
dragStartX = event.clientX;
|
||||
dragStartY = event.clientY;
|
||||
isMarqueeDragging = false;
|
||||
dragMode = null;
|
||||
}
|
||||
|
||||
function rowClick(id: string, shiftKey: boolean) {
|
||||
if (!options.enabled()) return;
|
||||
const selected = options.selectedIds();
|
||||
|
||||
if (shiftKey) {
|
||||
const anchor = dragAnchorId;
|
||||
if (anchor !== null && anchor !== id) {
|
||||
rangeSelect(anchor, id);
|
||||
} else if (selected.has(id)) {
|
||||
selected.delete(id);
|
||||
} else {
|
||||
selected.add(id);
|
||||
}
|
||||
dragAnchorId = id;
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected.has(id)) selected.delete(id);
|
||||
else selected.add(id);
|
||||
dragAnchorId = id;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
dragAnchorId = null;
|
||||
isMarqueeDragging = false;
|
||||
mouseDownActive = false;
|
||||
suppressNextClick = false;
|
||||
mousedownRowId = null;
|
||||
dragMode = null;
|
||||
dragStartX = 0;
|
||||
dragStartY = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
rowMouseDown,
|
||||
rowClick,
|
||||
reset,
|
||||
get dragAnchorId() {
|
||||
return dragAnchorId;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { findDescendantMessages, uuid, filterByLeafNodeId } from '$lib/utils';
|
||||
import { IDXDB_TABLES, IDXDB_STORES, STORAGE_APP_NAME } from '$lib/constants';
|
||||
import { MessageRole } from '$lib/enums';
|
||||
import type { McpServerOverride } from '$lib/types/database';
|
||||
import type { ExportedConversation } from '$lib/types/database';
|
||||
|
||||
class LlamaUiDatabase extends Dexie {
|
||||
[IDXDB_TABLES.conversations]!: EntityTable<DatabaseConversation, string>;
|
||||
@@ -206,18 +207,7 @@ export class DatabaseService {
|
||||
await db[IDXDB_TABLES.messages].where('convId').equals(forkId).delete();
|
||||
}
|
||||
} else {
|
||||
// Reparent direct children to deleted conv's parent
|
||||
const conv = await db[IDXDB_TABLES.conversations].get(id);
|
||||
const newParent = conv?.forkedFromConversationId;
|
||||
const directChildren = await db[IDXDB_TABLES.conversations]
|
||||
.filter((c) => c.forkedFromConversationId === id)
|
||||
.toArray();
|
||||
|
||||
for (const child of directChildren) {
|
||||
await db[IDXDB_TABLES.conversations].update(child.id, {
|
||||
forkedFromConversationId: newParent ?? undefined
|
||||
});
|
||||
}
|
||||
await this.reparentDirectChildren(id);
|
||||
}
|
||||
|
||||
await db[IDXDB_TABLES.conversations].delete(id);
|
||||
@@ -226,6 +216,101 @@ export class DatabaseService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reparents direct children of `parentId` to the nearest surviving
|
||||
* ancestor (or promotes them to top-level when the immediate parent was
|
||||
* top-level). Walking skips any ancestor listed in `excludeIds`, since
|
||||
* those will be deleted in the same batch — leaving a grandchild pointing
|
||||
* at an `excludeIds` entry would orphan it. Children whose own id is in
|
||||
* `excludeIds` are dropped from the updates (the bulk-delete pass will
|
||||
* remove them). `prefetched` may carry a pre-fetched ancestor map to
|
||||
* avoid repeat reads inside a bulk transaction.
|
||||
*/
|
||||
private static async reparentDirectChildren(
|
||||
parentId: string,
|
||||
excludeIds: ReadonlySet<string> = new Set(),
|
||||
prefetched?: ReadonlyMap<string, DatabaseConversation>
|
||||
): Promise<void> {
|
||||
const conv = prefetched?.get(parentId) ?? (await db[IDXDB_TABLES.conversations].get(parentId));
|
||||
if (!conv) return;
|
||||
|
||||
let newParent = conv.forkedFromConversationId;
|
||||
const visited = new Set<string>([parentId]);
|
||||
while (newParent && excludeIds.has(newParent)) {
|
||||
if (visited.has(newParent)) {
|
||||
newParent = undefined;
|
||||
break;
|
||||
}
|
||||
visited.add(newParent);
|
||||
const next =
|
||||
prefetched?.get(newParent) ?? (await db[IDXDB_TABLES.conversations].get(newParent));
|
||||
if (!next) {
|
||||
newParent = undefined;
|
||||
break;
|
||||
}
|
||||
newParent = next.forkedFromConversationId;
|
||||
}
|
||||
|
||||
const directChildren = await db[IDXDB_TABLES.conversations]
|
||||
.filter((c) => c.forkedFromConversationId === parentId)
|
||||
.toArray();
|
||||
|
||||
const updates: DatabaseConversation[] = [];
|
||||
for (const child of directChildren) {
|
||||
if (excludeIds.has(child.id)) continue;
|
||||
updates.push({ ...child, forkedFromConversationId: newParent });
|
||||
}
|
||||
if (updates.length === 0) return;
|
||||
await db[IDXDB_TABLES.conversations].bulkPut(updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes multiple conversations in a single transaction. Each deleted
|
||||
* conversation has its direct children reparented to the nearest surviving
|
||||
* ancestor (or promoted to top-level). Children also in `ids` are dropped
|
||||
* entirely rather than reparented.
|
||||
*
|
||||
* @param ids - Conversation IDs to delete
|
||||
*/
|
||||
static async bulkDeleteConversations(ids: string[]): Promise<void> {
|
||||
const cleanIds = ids.filter((id): id is string => typeof id === 'string' && id.length > 0);
|
||||
if (cleanIds.length === 0) return;
|
||||
const idSet = new Set(cleanIds);
|
||||
|
||||
await db.transaction(
|
||||
'rw',
|
||||
[db[IDXDB_TABLES.conversations], db[IDXDB_TABLES.messages]],
|
||||
async () => {
|
||||
// Pre-load each to-delete conversation so the per-id reparent
|
||||
// walk-up doesn't ping-pong the same ancestry chain.
|
||||
const prefetched = new Map<string, DatabaseConversation>();
|
||||
let frontier = [...cleanIds];
|
||||
const requested = new Set<string>(frontier);
|
||||
while (frontier.length > 0) {
|
||||
const fetched = await db[IDXDB_TABLES.conversations].bulkGet(frontier);
|
||||
frontier = [];
|
||||
for (let i = 0; i < fetched.length; i++) {
|
||||
const conv = fetched[i];
|
||||
if (!conv || !conv.id) continue;
|
||||
prefetched.set(conv.id, conv);
|
||||
const ancestor = conv.forkedFromConversationId;
|
||||
if (ancestor && !prefetched.has(ancestor) && !requested.has(ancestor)) {
|
||||
frontier.push(ancestor);
|
||||
requested.add(ancestor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of cleanIds) {
|
||||
await this.reparentDirectChildren(id, idSet, prefetched);
|
||||
}
|
||||
|
||||
await db[IDXDB_TABLES.conversations].bulkDelete(cleanIds);
|
||||
await db[IDXDB_TABLES.messages].where('convId').anyOf(cleanIds).delete();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a message and removes it from its parent's children array.
|
||||
*
|
||||
@@ -319,6 +404,43 @@ export class DatabaseService {
|
||||
return await db[IDXDB_TABLES.messages].where('convId').equals(convId).sortBy('timestamp');
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads multiple conversations with all of their messages in two bulk
|
||||
* reads. Missing conversations are silently omitted from the result.
|
||||
*
|
||||
* @param convIds - Conversation IDs to load
|
||||
* @returns Map of id -> { conv, messages }. Messages are sorted ascending by timestamp.
|
||||
*/
|
||||
static async getConversationsWithMessages(
|
||||
convIds: string[]
|
||||
): Promise<Map<string, ExportedConversation>> {
|
||||
const result = new Map<string, ExportedConversation>();
|
||||
const cleanIds = convIds.filter((id): id is string => typeof id === 'string' && id.length > 0);
|
||||
if (cleanIds.length === 0) return result;
|
||||
|
||||
const [convs, allMessages] = await Promise.all([
|
||||
db[IDXDB_TABLES.conversations].bulkGet(cleanIds),
|
||||
db[IDXDB_TABLES.messages].where('convId').anyOf(cleanIds).toArray()
|
||||
]);
|
||||
|
||||
const messagesByConv = new Map<string, DatabaseMessage[]>();
|
||||
for (const msg of allMessages) {
|
||||
const bucket = messagesByConv.get(msg.convId);
|
||||
if (bucket) bucket.push(msg);
|
||||
else messagesByConv.set(msg.convId, [msg]);
|
||||
}
|
||||
|
||||
for (let i = 0; i < cleanIds.length; i++) {
|
||||
const conv = convs[i];
|
||||
if (!conv) continue;
|
||||
const messages = (messagesByConv.get(conv.id) ?? []).sort(
|
||||
(a, b) => a.timestamp - b.timestamp
|
||||
);
|
||||
result.set(conv.id, { conv, messages });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a conversation.
|
||||
*
|
||||
@@ -360,6 +482,38 @@ export class DatabaseService {
|
||||
return newPinnedState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the pinned status of each conversation in `ids` inside a single
|
||||
* transaction. Treats `pinned === undefined` as `false`, matching the
|
||||
* semantics of {@link toggleConversationPin} where `!undefined` evaluates
|
||||
* to `true`. Returns the resulting pinned state for every id that was
|
||||
* updated; missing ids are omitted from the map.
|
||||
*
|
||||
* @param ids - Conversation IDs to toggle
|
||||
* @returns Map of id -> new pinned state
|
||||
*/
|
||||
static async bulkToggleConversationPins(ids: string[]): Promise<Map<string, boolean>> {
|
||||
const cleanIds = ids.filter((id): id is string => typeof id === 'string' && id.length > 0);
|
||||
const result = new Map<string, boolean>();
|
||||
if (cleanIds.length === 0) return result;
|
||||
|
||||
const now = Date.now();
|
||||
await db.transaction('rw', db[IDXDB_TABLES.conversations], async () => {
|
||||
const convs = await db[IDXDB_TABLES.conversations].bulkGet(cleanIds);
|
||||
const updates: DatabaseConversation[] = [];
|
||||
for (let i = 0; i < cleanIds.length; i++) {
|
||||
const conv = convs[i];
|
||||
if (!conv) continue;
|
||||
const newPinned = !conv.pinned;
|
||||
updates.push({ ...conv, pinned: newPinned, lastModified: now });
|
||||
result.set(cleanIds[i], newPinned);
|
||||
}
|
||||
if (updates.length === 0) return;
|
||||
await db[IDXDB_TABLES.conversations].bulkPut(updates);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the conversation's current node (active branch).
|
||||
* This determines which conversation path is currently being viewed.
|
||||
|
||||
@@ -288,6 +288,7 @@ class AgenticStore {
|
||||
const hasTools =
|
||||
mcpStore.hasEnabledServers(perChatOverrides) ||
|
||||
toolsStore.builtinTools.length > 0 ||
|
||||
toolsStore.frontendTools.length > 0 ||
|
||||
toolsStore.customTools.length > 0;
|
||||
return {
|
||||
enabled: hasTools && DEFAULT_AGENTIC_CONFIG.enabled,
|
||||
|
||||
@@ -1596,7 +1596,7 @@ class ChatStore {
|
||||
conversationsStore.updateMessageAtIndex(messageIndex, { content: newContent });
|
||||
await DatabaseService.updateMessage(messageId, { content: newContent });
|
||||
if (isFirstUserMessage && newContent.trim())
|
||||
await conversationsStore.updateConversationTitleWithConfirmation(
|
||||
await conversationsStore.updateConversationName(
|
||||
activeConv.id,
|
||||
generateConversationTitle(newContent, Boolean(config().titleGenerationUseFirstLine))
|
||||
);
|
||||
@@ -2113,7 +2113,7 @@ class ChatStore {
|
||||
const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
|
||||
|
||||
if (rootMessage && msg.parent === rootMessage.id && newContent.trim()) {
|
||||
await conversationsStore.updateConversationTitleWithConfirmation(
|
||||
await conversationsStore.updateConversationName(
|
||||
activeConv.id,
|
||||
generateConversationTitle(newContent, Boolean(config().titleGenerationUseFirstLine))
|
||||
);
|
||||
@@ -2187,7 +2187,7 @@ class ChatStore {
|
||||
|
||||
conversationsStore.updateConversationTimestamp();
|
||||
if (isFirstUserMessage && newContent.trim())
|
||||
await conversationsStore.updateConversationTitleWithConfirmation(
|
||||
await conversationsStore.updateConversationName(
|
||||
activeConv.id,
|
||||
generateConversationTitle(newContent, Boolean(config().titleGenerationUseFirstLine))
|
||||
);
|
||||
@@ -2428,7 +2428,8 @@ class ChatStore {
|
||||
|
||||
if (currentConfig.samplers) apiOptions.samplers = currentConfig.samplers;
|
||||
|
||||
apiOptions.backend_sampling = currentConfig.backend_sampling;
|
||||
if (hasValue(currentConfig.backend_sampling))
|
||||
apiOptions.backend_sampling = currentConfig.backend_sampling;
|
||||
|
||||
if (currentConfig.customJson) apiOptions.custom = currentConfig.customJson;
|
||||
|
||||
|
||||
@@ -108,9 +108,6 @@ class ConversationsStore {
|
||||
localStorage.setItem(REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY, this.pendingReasoningEffort);
|
||||
}
|
||||
|
||||
/** Callback for title update confirmation dialog */
|
||||
titleUpdateConfirmationCallback?: (currentTitle: string, newTitle: string) => Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Callback for updating message content in chatStore.
|
||||
* Registered by chatStore to enable cross-store updates without circular dependency.
|
||||
@@ -209,15 +206,6 @@ class ConversationsStore {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the callback function for title update confirmations
|
||||
*/
|
||||
setTitleUpdateConfirmationCallback(
|
||||
callback: (currentTitle: string, newTitle: string) => Promise<boolean>
|
||||
): void {
|
||||
this.titleUpdateConfirmationCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
@@ -386,6 +374,123 @@ class ConversationsStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes multiple conversations in sequence.
|
||||
* Mirrors deleteConversation() per-id; navigates to NEW_CHAT only if the
|
||||
* currently-open chat was among the deleted ones.
|
||||
* @param convIds - Conversation IDs to delete
|
||||
*/
|
||||
async bulkDeleteConversations(convIds: string[]): Promise<void> {
|
||||
if (convIds.length === 0) return;
|
||||
|
||||
try {
|
||||
const idsToRemove = new SvelteSet(convIds);
|
||||
// Collect all descendants recursively so the local cache stays consistent
|
||||
// even when deleteWithForks is omitted.
|
||||
const queue = [...convIds];
|
||||
while (queue.length > 0) {
|
||||
const parentId = queue.pop()!;
|
||||
for (const c of this.conversations) {
|
||||
if (c.forkedFromConversationId === parentId && !idsToRemove.has(c.id)) {
|
||||
idsToRemove.add(c.id);
|
||||
queue.push(c.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const activeWasDeleted =
|
||||
this.activeConversation !== null && idsToRemove.has(this.activeConversation.id);
|
||||
|
||||
await DatabaseService.bulkDeleteConversations([...idsToRemove]);
|
||||
|
||||
this.conversations = this.conversations.filter((c) => !idsToRemove.has(c.id));
|
||||
|
||||
if (activeWasDeleted) {
|
||||
this.clearActiveConversation();
|
||||
await goto(ROUTES.NEW_CHAT);
|
||||
}
|
||||
|
||||
toast.success(
|
||||
convIds.length === 1 ? 'Conversation deleted' : `${convIds.length} conversations deleted`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to bulk delete conversations:', error);
|
||||
toast.error('Failed to delete conversations');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the pinned state of each conversation individually.
|
||||
* Mixed-pin selections are intentionally not normalised here; the bulk
|
||||
* action UI surfaces them as a disabled mixed-state instead.
|
||||
* @param convIds - Conversation IDs to toggle
|
||||
*/
|
||||
async bulkToggleConversationPin(convIds: string[]): Promise<void> {
|
||||
if (convIds.length === 0) return;
|
||||
|
||||
try {
|
||||
const updates = await DatabaseService.bulkToggleConversationPins(convIds);
|
||||
|
||||
const activeId = this.activeConversation?.id;
|
||||
if (activeId && updates.has(activeId)) {
|
||||
this.activeConversation = {
|
||||
...this.activeConversation!,
|
||||
pinned: updates.get(activeId)!
|
||||
};
|
||||
}
|
||||
for (let i = 0; i < this.conversations.length; i++) {
|
||||
const newPinned = updates.get(this.conversations[i].id);
|
||||
if (newPinned !== undefined) this.conversations[i].pinned = newPinned;
|
||||
}
|
||||
this.conversations = [...this.conversations];
|
||||
|
||||
toast.success(
|
||||
convIds.length === 1
|
||||
? 'Conversation pin toggled'
|
||||
: `Updated pin state for ${convIds.length} conversations`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to bulk toggle pin:', error);
|
||||
toast.error('Failed to update pin state');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundles the given conversations into a single zip archive and triggers a
|
||||
* browser download (one JSONL file per conversation).
|
||||
* @param convIds - Conversation IDs to export
|
||||
*/
|
||||
async bulkExportConversations(convIds: string[]): Promise<void> {
|
||||
if (convIds.length === 0) return;
|
||||
|
||||
try {
|
||||
const fetched = await DatabaseService.getConversationsWithMessages(convIds);
|
||||
|
||||
const activeId = this.activeConversation?.id;
|
||||
const overridden = fetched.get(activeId ?? '');
|
||||
if (overridden && activeId) {
|
||||
overridden.conv = { ...this.activeConversation! };
|
||||
}
|
||||
|
||||
const exported = [...fetched.values()];
|
||||
if (exported.length === 0) {
|
||||
toast.error('No conversations to export');
|
||||
return;
|
||||
}
|
||||
|
||||
this.downloadConversationsArchive(exported);
|
||||
|
||||
toast.success(
|
||||
exported.length === 1
|
||||
? 'Conversation exported'
|
||||
: `${exported.length} conversations exported`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to bulk export conversations:', error);
|
||||
toast.error('Failed to export conversations');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
@@ -484,38 +589,6 @@ class ConversationsStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates conversation title with optional confirmation dialog based on settings
|
||||
* @param convId - The conversation ID to update
|
||||
* @param newTitle - The new title content
|
||||
* @returns True if title was updated, false if cancelled
|
||||
*/
|
||||
async updateConversationTitleWithConfirmation(
|
||||
convId: string,
|
||||
newTitle: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const currentConfig = config();
|
||||
|
||||
if (currentConfig.askForTitleConfirmation && this.titleUpdateConfirmationCallback) {
|
||||
const conversation = await DatabaseService.getConversation(convId);
|
||||
if (!conversation) return false;
|
||||
|
||||
const shouldUpdate = await this.titleUpdateConfirmationCallback(
|
||||
conversation.name,
|
||||
newTitle
|
||||
);
|
||||
if (!shouldUpdate) return false;
|
||||
}
|
||||
|
||||
await this.updateConversationName(convId, newTitle);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to update conversation title with confirmation:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates conversation lastModified timestamp and moves it to top of list
|
||||
*/
|
||||
@@ -581,7 +654,7 @@ class ConversationsStore {
|
||||
newFirstUserMessage.id !== currentFirstUserMessage.id ||
|
||||
newFirstUserMessage.content.trim() !== currentFirstUserMessage.content.trim())
|
||||
) {
|
||||
await this.updateConversationTitleWithConfirmation(
|
||||
await this.updateConversationName(
|
||||
this.activeConversation.id,
|
||||
generateConversationTitle(
|
||||
newFirstUserMessage.content,
|
||||
@@ -1162,6 +1235,10 @@ export const isConversationsInitialized = () => conversationsStore.isInitialized
|
||||
/**
|
||||
* Builds a flat tree of conversations with depth levels for nested forks.
|
||||
* Accepts a pre-filtered list so search filtering stays in the component.
|
||||
*
|
||||
* Output order matches the sidebar render exactly: pinned first, then
|
||||
* unpinned by lastModified desc, with forks interleaved under their parents.
|
||||
* Range-select / marquee in the sidebar rely on this alignment.
|
||||
*/
|
||||
|
||||
// Pinned conversations first, then by lastModified descending
|
||||
|
||||
Vendored
+4
@@ -27,6 +27,8 @@ export interface SettingsEntry {
|
||||
type: SettingsFieldType;
|
||||
section?: string;
|
||||
options?: Array<{ value: string; label: string; icon: Component }>;
|
||||
/** Options rendered for RADIO fields. Each entry maps a `value` (the radio's selected value) to the underlying config `key` whose boolean state mirrors it. */
|
||||
radioOptions?: Array<{ value: string; label: string; key: string; isExperimental?: boolean }>;
|
||||
isExperimental?: boolean;
|
||||
isPositiveInteger?: boolean;
|
||||
dependsOn?: string;
|
||||
@@ -53,6 +55,8 @@ export interface SettingsFieldConfig {
|
||||
dependsOn?: string;
|
||||
help?: string;
|
||||
options?: Array<{ value: string; label: string; icon?: typeof Icon }>;
|
||||
/** Options rendered for RADIO fields. Each entry maps a `value` (the radio's selected value) to the underlying config `key` whose boolean state mirrors it. */
|
||||
radioOptions?: Array<{ value: string; label: string; key: string; isExperimental?: boolean }>;
|
||||
}
|
||||
|
||||
/** Re-exported for backward compatibility. */
|
||||
|
||||
@@ -7,12 +7,11 @@
|
||||
import { untrack } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { SidebarNavigation, DialogConversationTitleUpdate } from '$lib/components/app';
|
||||
import { SidebarNavigation } from '$lib/components/app';
|
||||
import { PwaMetaTags, PwaRefreshAlert } from '$lib/components/pwa';
|
||||
import { pwaAssetsHead } from 'virtual:pwa-assets/head';
|
||||
|
||||
import { chatStore } from '$lib/stores/chat.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
|
||||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||
@@ -46,11 +45,6 @@
|
||||
|
||||
let showBuildVersion = $derived(config()[SETTINGS_KEYS.SHOW_BUILD_VERSION] as boolean);
|
||||
|
||||
let titleUpdateDialogOpen = $state(false);
|
||||
let titleUpdateCurrentTitle = $state('');
|
||||
let titleUpdateNewTitle = $state('');
|
||||
let titleUpdateResolve: ((value: boolean) => void) | null = null;
|
||||
|
||||
// Keep the hook object intact: destructuring needRefreshByStorage reads the getter once and freezes it
|
||||
const pwa = usePwa();
|
||||
const { needRefresh, updateServiceWorker } = pwa;
|
||||
@@ -135,24 +129,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
function handleTitleUpdateCancel() {
|
||||
titleUpdateDialogOpen = false;
|
||||
|
||||
if (titleUpdateResolve) {
|
||||
titleUpdateResolve(false);
|
||||
titleUpdateResolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleTitleUpdateConfirm() {
|
||||
titleUpdateDialogOpen = false;
|
||||
|
||||
if (titleUpdateResolve) {
|
||||
titleUpdateResolve(true);
|
||||
titleUpdateResolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
updateFavicon();
|
||||
// snapshot of every backend running stream on first load, populates the sidebar spinners
|
||||
@@ -264,20 +240,6 @@
|
||||
$effect(() => {
|
||||
checkApiKey();
|
||||
});
|
||||
|
||||
// Set up title update confirmation callback
|
||||
$effect(() => {
|
||||
conversationsStore.setTitleUpdateConfirmationCallback(
|
||||
async (currentTitle: string, newTitle: string) => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
titleUpdateCurrentTitle = currentTitle;
|
||||
titleUpdateNewTitle = newTitle;
|
||||
titleUpdateResolve = resolve;
|
||||
titleUpdateDialogOpen = true;
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -319,14 +281,6 @@
|
||||
<ModeWatcher />
|
||||
|
||||
<Toaster richColors />
|
||||
|
||||
<DialogConversationTitleUpdate
|
||||
bind:open={titleUpdateDialogOpen}
|
||||
currentTitle={titleUpdateCurrentTitle}
|
||||
newTitle={titleUpdateNewTitle}
|
||||
onConfirm={handleTitleUpdateConfirm}
|
||||
onCancel={handleTitleUpdateCancel}
|
||||
/>
|
||||
</Tooltip.Provider>
|
||||
|
||||
<!-- PWA update prompt + version -->
|
||||
|
||||
Reference in New Issue
Block a user