mirror of
https://github.com/ollama/ollama.git
synced 2026-07-23 09:10:53 -05:00
model: align Laguna with upstream llama.cpp (#17335)
Update llama.cpp to pick up upstream Laguna implementation and remove Ollama's local Laguna implementation. Retain a narrow Metal-only scaling workaround for routed-MoE prompt overflow. Translate older Ollama GGUF attention-gate and SWA metadata names so existing models continue to load.
This commit is contained in:
@@ -1 +1 @@
|
|||||||
b10069
|
b10091
|
||||||
|
|||||||
@@ -11,8 +11,29 @@ import (
|
|||||||
"github.com/ollama/ollama/api"
|
"github.com/ollama/ollama/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// runThinkingEnabled verifies that when thinking is requested, the model
|
var rawThinkingProtocolTags = []string{
|
||||||
// produces both thinking and content output without leaking raw channel tags.
|
"<|channel>",
|
||||||
|
"<channel|>",
|
||||||
|
"<think>",
|
||||||
|
"</think>",
|
||||||
|
"<assistant>",
|
||||||
|
"</assistant>",
|
||||||
|
"<tool_call>",
|
||||||
|
"</tool_call>",
|
||||||
|
}
|
||||||
|
|
||||||
|
func rejectRawThinkingProtocolTags(t *testing.T, field, value string) {
|
||||||
|
t.Helper()
|
||||||
|
for _, tag := range rawThinkingProtocolTags {
|
||||||
|
if strings.Contains(value, tag) {
|
||||||
|
t.Errorf("%s contains raw protocol tag %q: %s", field, tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runThinkingEnabled verifies that thinking-capable models honor an explicit
|
||||||
|
// thinking request, complete a reasoning trace, and return the final answer
|
||||||
|
// without leaking raw channel tags.
|
||||||
func runThinkingEnabled(t *testing.T) {
|
func runThinkingEnabled(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -33,12 +54,15 @@ func runThinkingEnabled(t *testing.T) {
|
|||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
Think: &think,
|
Think: &think,
|
||||||
Messages: []api.Message{
|
Messages: []api.Message{
|
||||||
{Role: "user", Content: "What is 12 * 15? Think step by step."},
|
{Role: "user", Content: "What is 12 multiplied by 15? Give the final answer."},
|
||||||
},
|
},
|
||||||
Options: map[string]any{
|
Options: map[string]any{
|
||||||
"temperature": 0,
|
"temperature": 0,
|
||||||
"seed": 42,
|
"seed": 42,
|
||||||
"num_predict": 512,
|
// Deep-thinking models can use several thousand tokens on
|
||||||
|
// simple problems before producing their final answer. Keep
|
||||||
|
// this high enough to verify a natural stop, not truncation.
|
||||||
|
"num_predict": 8192,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,22 +85,17 @@ func runThinkingEnabled(t *testing.T) {
|
|||||||
if thinking == "" {
|
if thinking == "" {
|
||||||
t.Error("expected non-empty thinking output when thinking is enabled")
|
t.Error("expected non-empty thinking output when thinking is enabled")
|
||||||
}
|
}
|
||||||
|
if content == "" {
|
||||||
// The answer (180) should appear in thinking, content, or both.
|
t.Error("expected non-empty final content after thinking")
|
||||||
// Some models put everything in thinking and leave content empty
|
} else if !strings.Contains(content, "180") {
|
||||||
// if they hit the token limit while still thinking.
|
t.Errorf("expected final answer 180, got content=%q", content)
|
||||||
combined := thinking + " " + content
|
}
|
||||||
if !strings.Contains(combined, "180") {
|
if response.DoneReason != "stop" {
|
||||||
t.Errorf("expected '180' in thinking or content, got thinking=%q content=%q", thinking, content)
|
t.Errorf("expected completed response, got done reason %q", response.DoneReason)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Neither thinking nor content should contain raw channel tags
|
rejectRawThinkingProtocolTags(t, "content", content)
|
||||||
if strings.Contains(content, "<|channel>") || strings.Contains(content, "<channel|>") {
|
rejectRawThinkingProtocolTags(t, "thinking", thinking)
|
||||||
t.Errorf("content contains raw channel tags: %s", content)
|
|
||||||
}
|
|
||||||
if strings.Contains(thinking, "<|channel>") || strings.Contains(thinking, "<channel|>") {
|
|
||||||
t.Errorf("thinking contains raw channel tags: %s", thinking)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("thinking (%d chars): %.100s...", len(thinking), thinking)
|
t.Logf("thinking (%d chars): %.100s...", len(thinking), thinking)
|
||||||
t.Logf("content (%d chars): %s", len(content), content)
|
t.Logf("content (%d chars): %s", len(content), content)
|
||||||
@@ -84,7 +103,7 @@ func runThinkingEnabled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// runThinkingSuppressed verifies that when thinking is NOT requested,
|
// runThinkingSuppressed verifies that when thinking is explicitly disabled,
|
||||||
// the model does not leak thinking/channel content into the response.
|
// the model does not leak thinking/channel content into the response.
|
||||||
func runThinkingSuppressed(t *testing.T) {
|
func runThinkingSuppressed(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
@@ -100,10 +119,11 @@ func runThinkingSuppressed(t *testing.T) {
|
|||||||
pullOrSkip(ctx, t, client, modelName)
|
pullOrSkip(ctx, t, client, modelName)
|
||||||
|
|
||||||
stream := false
|
stream := false
|
||||||
|
think := api.ThinkValue{Value: false}
|
||||||
req := api.ChatRequest{
|
req := api.ChatRequest{
|
||||||
Model: modelName,
|
Model: modelName,
|
||||||
Stream: &stream,
|
Stream: &stream,
|
||||||
// Think is nil — thinking not requested
|
Think: &think,
|
||||||
Messages: []api.Message{
|
Messages: []api.Message{
|
||||||
{Role: "user", Content: "What is the capital of Japan? Answer in one word."},
|
{Role: "user", Content: "What is the capital of Japan? Answer in one word."},
|
||||||
},
|
},
|
||||||
@@ -129,24 +149,16 @@ func runThinkingSuppressed(t *testing.T) {
|
|||||||
content := response.Message.Content
|
content := response.Message.Content
|
||||||
thinking := response.Message.Thinking
|
thinking := response.Message.Thinking
|
||||||
|
|
||||||
// The answer should appear in content or thinking
|
// With thinking disabled, the answer must be returned as content.
|
||||||
combined := content + " " + thinking
|
if !strings.Contains(content, "Tokyo") {
|
||||||
if !strings.Contains(combined, "Tokyo") {
|
t.Errorf("expected 'Tokyo' in content, got content=%q thinking=%q", content, thinking)
|
||||||
t.Errorf("expected 'Tokyo' in content or thinking, got content=%q thinking=%q", content, thinking)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content must NOT contain channel/thinking tags
|
rejectRawThinkingProtocolTags(t, "content", content)
|
||||||
if strings.Contains(content, "<|channel>") || strings.Contains(content, "<channel|>") {
|
rejectRawThinkingProtocolTags(t, "thinking", thinking)
|
||||||
t.Errorf("content contains leaked channel tags when thinking not requested: %s", content)
|
|
||||||
}
|
|
||||||
if strings.Contains(content, "thought") && strings.Contains(content, "<channel|>") {
|
|
||||||
t.Errorf("content contains leaked thinking block: %s", content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thinking field should ideally be empty when not requested.
|
|
||||||
// Some small models may still produce thinking output; log but don't fail.
|
|
||||||
if thinking != "" {
|
if thinking != "" {
|
||||||
t.Logf("WARNING: model produced thinking output when not requested (%d chars): %.100s...", len(thinking), thinking)
|
t.Errorf("expected empty thinking when thinking is disabled, got %q", thinking)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("content: %s", content)
|
t.Logf("content: %s", content)
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ This table tracks the dispatch surface. Keep it brief; the handler comments in
|
|||||||
| `deepseekocr` | Maps to `deepseek2-ocr`, injects missing OCR/MoE metadata, and hides embedded SAM/vision/projector tensors. | DeepSeek OCR projector translation. |
|
| `deepseekocr` | Maps to `deepseek2-ocr`, injects missing OCR/MoE metadata, and hides embedded SAM/vision/projector tensors. | DeepSeek OCR projector translation. |
|
||||||
| `glmocr` | Maps GLM OCR metadata/tensors to the llama.cpp-compatible view. | GLM OCR projector translation. |
|
| `glmocr` | Maps GLM OCR metadata/tensors to the llama.cpp-compatible view. | GLM OCR projector translation. |
|
||||||
| `glm4moelite` | Maps GLM-4.7 Flash MLA metadata to the `deepseek2` path and fixes special-token metadata. | n/a |
|
| `glm4moelite` | Maps GLM-4.7 Flash MLA metadata to the `deepseek2` path and fixes special-token metadata. | n/a |
|
||||||
|
| `laguna` | Renames legacy attention-gate tensors and SWA RoPE metadata to current llama.cpp names. | n/a |
|
||||||
| `nemotron_h_moe` | Fixes latent-FFN variants and hides MTP tensors. | n/a |
|
| `nemotron_h_moe` | Fixes latent-FFN variants and hides MTP tensors. | n/a |
|
||||||
| `nemotron_h_omni` | Selects the Nemotron text loader and hides audio/vision/projector tensors from the text loader. | Nemotron V2 VL projector translation; audio remains disabled. |
|
| `nemotron_h_omni` | Selects the Nemotron text loader and hides audio/vision/projector tensors from the text loader. | Nemotron V2 VL projector translation; audio remains disabled. |
|
||||||
| `llama` with Llama 3 markers | Fixes Llama 3 tokenizer metadata. | n/a |
|
| `llama` with Llama 3 markers | Fixes Llama 3 tokenizer metadata. | n/a |
|
||||||
|
|||||||
38
llama/compat/llama-ollama-compat.cpp
vendored
38
llama/compat/llama-ollama-compat.cpp
vendored
@@ -118,6 +118,43 @@ void fix_glm4moelite_eog_token_ids(gguf_context * meta) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// laguna (text side)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
bool detect_ollama_laguna(const gguf_context * meta, const ggml_context * ctx) {
|
||||||
|
const int64_t arch_kid = gguf_find_key(meta, "general.architecture");
|
||||||
|
if (arch_kid < 0 || std::strcmp(gguf_get_val_str(meta, arch_kid), "laguna") != 0) return false;
|
||||||
|
|
||||||
|
return has_key(meta, "laguna.rope.swa.dimension_count")
|
||||||
|
|| has_key(meta, "laguna.rope.swa.freq_base")
|
||||||
|
|| ggml_get_tensor(const_cast<ggml_context *>(ctx), "blk.0.attn_g.weight") != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle_laguna(gguf_context * meta, ggml_context * ctx) {
|
||||||
|
if (!detect_ollama_laguna(meta, ctx)) return;
|
||||||
|
|
||||||
|
OLLAMA_COMPAT_LOG_INFO("%s: detected Ollama-format laguna GGUF; applying compatibility fixes\n", __func__);
|
||||||
|
|
||||||
|
copy_u32_kv(meta, "laguna.rope.swa.dimension_count", "laguna.rope.dimension_count_swa");
|
||||||
|
copy_f32_kv(meta, "laguna.rope.swa.freq_base", "laguna.rope.freq_base_swa");
|
||||||
|
|
||||||
|
std::vector<std::pair<std::string, std::string>> renames;
|
||||||
|
const int64_t n = gguf_get_n_tensors(meta);
|
||||||
|
constexpr const char * old_suffix = ".attn_g.weight";
|
||||||
|
constexpr const char * new_suffix = ".attn_gate.weight";
|
||||||
|
for (int64_t i = 0; i < n; ++i) {
|
||||||
|
const std::string name(gguf_get_tensor_name(meta, i));
|
||||||
|
const size_t pos = name.rfind(old_suffix);
|
||||||
|
if (pos != std::string::npos && pos + std::strlen(old_suffix) == name.size()) {
|
||||||
|
renames.emplace_back(name, name.substr(0, pos) + new_suffix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto & [from, to] : renames) {
|
||||||
|
rename_tensor(meta, ctx, from.c_str(), to.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool get_u32_kv(const gguf_context * meta, const char * key, uint32_t & out) {
|
bool get_u32_kv(const gguf_context * meta, const char * key, uint32_t & out) {
|
||||||
const int64_t kid = gguf_find_key(meta, key);
|
const int64_t kid = gguf_find_key(meta, key);
|
||||||
if (kid < 0) return false;
|
if (kid < 0) return false;
|
||||||
@@ -3221,6 +3258,7 @@ bool translate_metadata(const llama_model_loader * ml,
|
|||||||
if (arch_name == "qwen35moe") handle_qwen35moe(ml, meta, ctx);
|
if (arch_name == "qwen35moe") handle_qwen35moe(ml, meta, ctx);
|
||||||
if (arch_name == "qwen35") handle_qwen35 (ml, meta, ctx);
|
if (arch_name == "qwen35") handle_qwen35 (ml, meta, ctx);
|
||||||
if (arch_name == "qwen3next") handle_qwen3next(meta, ctx);
|
if (arch_name == "qwen3next") handle_qwen3next(meta, ctx);
|
||||||
|
if (arch_name == "laguna") handle_laguna (meta, ctx);
|
||||||
if (arch_name == "gptoss") handle_gptoss (ml, meta, ctx, arch_name);
|
if (arch_name == "gptoss") handle_gptoss (ml, meta, ctx, arch_name);
|
||||||
if (arch_name == "lfm2") handle_lfm2 (ml, meta, ctx);
|
if (arch_name == "lfm2") handle_lfm2 (ml, meta, ctx);
|
||||||
if (arch_name == "olmo3") handle_olmo3 (meta, arch_name);
|
if (arch_name == "olmo3") handle_olmo3 (meta, arch_name);
|
||||||
|
|||||||
42
llama/compat/models/003-llama-cpp-laguna-metal.patch
Normal file
42
llama/compat/models/003-llama-cpp-laguna-metal.patch
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
diff --git a/src/models/laguna.cpp b/src/models/laguna.cpp
|
||||||
|
index 1d705440e..7e708b144 100644
|
||||||
|
--- a/src/models/laguna.cpp
|
||||||
|
+++ b/src/models/laguna.cpp
|
||||||
|
@@ -275,16 +275,35 @@ llama_model_laguna::graph::graph(const llama_model & model, const llm_graph_para
|
||||||
|
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 * up_scale = nullptr;
|
||||||
|
+ float expert_weights_scale = hparams.expert_weights_scale;
|
||||||
|
+
|
||||||
|
+#if defined(GGML_USE_METAL)
|
||||||
|
+ const ggml_type down_type = model.layers[il].ffn_down_exps->type;
|
||||||
|
+ if (n_tokens >= 32 && (ggml_is_quantized(down_type) || down_type == GGML_TYPE_F16)) {
|
||||||
|
+ // At 32 prompt tokens, Metal switches MUL_MAT_ID from its
|
||||||
|
+ // range-safe matrix-vector kernel to FP16 matrix tiles.
|
||||||
|
+ // Laguna's routed SwiGLU activations can overflow those tiles.
|
||||||
|
+ // Generation uses one token and does not need this workaround.
|
||||||
|
+ constexpr float down_input_scale = 1.0f / 256.0f;
|
||||||
|
+ up_scale = ggml_fill(ctx0,
|
||||||
|
+ model.layers[il].ffn_exp_probs_b, down_input_scale);
|
||||||
|
+ expert_weights_scale /= down_input_scale;
|
||||||
|
+ }
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
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,
|
||||||
|
+ expert_weights_scale,
|
||||||
|
(llama_expert_gating_func_type) hparams.expert_gating_func,
|
||||||
|
- il);
|
||||||
|
+ il,
|
||||||
|
+ nullptr, nullptr,
|
||||||
|
+ up_scale);
|
||||||
|
cb(moe_out, "ffn_moe_out", il);
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
diff --git a/src/llama-arch.cpp b/src/llama-arch.cpp
|
|
||||||
--- a/src/llama-arch.cpp
|
|
||||||
+++ b/src/llama-arch.cpp
|
|
||||||
@@ -136,2 +136,3 @@ static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = {
|
|
||||||
{ LLM_ARCH_MELLUM, "mellum" },
|
|
||||||
+ { LLM_ARCH_LAGUNA, "laguna" },
|
|
||||||
{ LLM_ARCH_UNKNOWN, "(unknown)" },
|
|
||||||
@@ -398,2 +399,3 @@ static const std::map<llm_tensor, const char *> LLM_TENSOR_NAMES = {
|
|
||||||
{ LLM_TENSOR_ATTN_GATE, "blk.%d.attn_gate" },
|
|
||||||
+ { LLM_TENSOR_ATTN_GATE_LAGUNA, "blk.%d.attn_g" },
|
|
||||||
{ LLM_TENSOR_FFN_POST_NORM, "blk.%d.post_ffw_norm" },
|
|
||||||
@@ -596,2 +598,3 @@ static const std::map<llm_tensor, llm_tensor_info> LLM_TENSOR_INFOS = {
|
|
||||||
{LLM_TENSOR_ATTN_GATE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
|
||||||
+ {LLM_TENSOR_ATTN_GATE_LAGUNA, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
|
||||||
{LLM_TENSOR_FFN_GATE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL_MAT}},
|
|
||||||
diff --git a/src/llama-arch.h b/src/llama-arch.h
|
|
||||||
--- a/src/llama-arch.h
|
|
||||||
+++ b/src/llama-arch.h
|
|
||||||
@@ -145,2 +145,3 @@ enum llm_arch {
|
|
||||||
LLM_ARCH_DFLASH,
|
|
||||||
+ LLM_ARCH_LAGUNA,
|
|
||||||
LLM_ARCH_UNKNOWN,
|
|
||||||
@@ -382,2 +383,3 @@ enum llm_tensor {
|
|
||||||
LLM_TENSOR_ATTN_GATE,
|
|
||||||
+ LLM_TENSOR_ATTN_GATE_LAGUNA,
|
|
||||||
LLM_TENSOR_FFN_GATE_INP,
|
|
||||||
diff --git a/src/llama-model.cpp b/src/llama-model.cpp
|
|
||||||
--- a/src/llama-model.cpp
|
|
||||||
+++ b/src/llama-model.cpp
|
|
||||||
@@ -48,4 +48,6 @@ static llama_model * llama_model_mapping(llm_arch arch, const llama_model_params
|
|
||||||
case LLM_ARCH_TALKIE:
|
|
||||||
return new llama_model_talkie(params);
|
|
||||||
+ case LLM_ARCH_LAGUNA:
|
|
||||||
+ return new llama_model_laguna(params);
|
|
||||||
case LLM_ARCH_DECI:
|
|
||||||
return new llama_model_deci(params);
|
|
||||||
@@ -2525,2 +2527,3 @@ llama_rope_type llama_model_rope_type(const llama_model * model) {
|
|
||||||
case LLM_ARCH_MELLUM:
|
|
||||||
+ case LLM_ARCH_LAGUNA:
|
|
||||||
case LLM_ARCH_DFLASH:
|
|
||||||
diff --git a/src/llama-model-loader.cpp b/src/llama-model-loader.cpp
|
|
||||||
--- a/src/llama-model-loader.cpp
|
|
||||||
+++ b/src/llama-model-loader.cpp
|
|
||||||
@@ -505,2 +505,3 @@ namespace GGUFMeta {
|
|
||||||
template bool llama_model_loader::get_key_or_arr<std::array<uint32_t, 512>>(enum llm_kv kid, std::array<uint32_t, 512> & result, uint32_t n, bool required);
|
|
||||||
+ template bool llama_model_loader::get_key_or_arr<uint32_t, 512>(const std::string & key, std::array<uint32_t, 512> & result, uint32_t n, bool required);
|
|
||||||
template bool llama_model_loader::get_key_or_arr<std::array<float, 512>>(enum llm_kv kid, std::array<float, 512> & result, uint32_t n, bool required);
|
|
||||||
diff --git a/src/llama-vocab.cpp b/src/llama-vocab.cpp
|
|
||||||
--- a/src/llama-vocab.cpp
|
|
||||||
+++ b/src/llama-vocab.cpp
|
|
||||||
@@ -359,2 +359,8 @@ struct llm_tokenizer_bpe : llm_tokenizer {
|
|
||||||
break;
|
|
||||||
+ case LLAMA_VOCAB_PRE_TYPE_LAGUNA:
|
|
||||||
+ regex_exprs = {
|
|
||||||
+ "(?:\\r?\\n)+(?!\\r?\\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_GPT2:
|
|
||||||
@@ -2100,2 +2106,4 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
|
|
||||||
ignore_merges = true;
|
|
||||||
+ } else if (tokenizer_pre == "laguna") {
|
|
||||||
+ pre_type = LLAMA_VOCAB_PRE_TYPE_LAGUNA;
|
|
||||||
} else if (
|
|
||||||
@@ -2773,2 +2781,3 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
|
|
||||||
|| t.first == "<|end▁of▁sentence|>" // deepseek-ocr
|
|
||||||
+ || t.first == "</assistant>" // poolside Laguna (eos_token_ids)
|
|
||||||
) {
|
|
||||||
diff --git a/src/llama-vocab.h b/src/llama-vocab.h
|
|
||||||
--- a/src/llama-vocab.h
|
|
||||||
+++ b/src/llama-vocab.h
|
|
||||||
@@ -65,2 +65,3 @@ enum llama_vocab_pre_type {
|
|
||||||
LLAMA_VOCAB_PRE_TYPE_MELLUM2 = 55,
|
|
||||||
+ LLAMA_VOCAB_PRE_TYPE_LAGUNA = 56,
|
|
||||||
};
|
|
||||||
diff --git a/src/models/models.h b/src/models/models.h
|
|
||||||
--- a/src/models/models.h
|
|
||||||
+++ b/src/models/models.h
|
|
||||||
@@ -1657,1 +1657,14 @@ struct llama_model_arcee : 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_arcee : public llama_model_base {
|
|
||||||
253
llama/compat/models/laguna.cpp
vendored
253
llama/compat/models/laguna.cpp
vendored
@@ -1,253 +0,0 @@
|
|||||||
#include "models/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);
|
|
||||||
|
|
||||||
// MoE
|
|
||||||
ml.get_key(LLM_KV_LEADING_DENSE_BLOCK_COUNT, hparams.n_layer_dense_lead, false);
|
|
||||||
ml.get_key(LLM_KV_EXPERT_FEED_FORWARD_LENGTH, hparams.n_ff_exp);
|
|
||||||
ml.get_key(LLM_KV_EXPERT_SHARED_FEED_FORWARD_LENGTH, hparams.n_ff_shexp, false);
|
|
||||||
ml.get_key(LLM_KV_EXPERT_SHARED_COUNT, hparams.n_expert_shared, 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);
|
|
||||||
ml.get_key(LLM_KV_EXPERT_GATING_FUNC, hparams.expert_gating_func, false);
|
|
||||||
|
|
||||||
ml.get_key(LLM_KV_ATTENTION_SLIDING_WINDOW, hparams.n_swa, false);
|
|
||||||
hparams.swa_type = LLAMA_SWA_TYPE_STANDARD;
|
|
||||||
ml.get_key_or_arr("laguna.attention.layer_types", hparams.is_swa_impl, hparams.n_layer(), false);
|
|
||||||
|
|
||||||
ml.get_key("laguna.rope.swa.dimension_count", hparams.n_rot_swa, false);
|
|
||||||
ml.get_key("laguna.rope.swa.freq_base", hparams.rope_freq_base_train_swa, false);
|
|
||||||
ml.get_key("laguna.rope.scaling.beta_fast", hparams.yarn_beta_fast, false);
|
|
||||||
ml.get_key("laguna.rope.scaling.beta_slow", hparams.yarn_beta_slow, false);
|
|
||||||
|
|
||||||
type = LLM_TYPE_UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
void llama_model_laguna::load_arch_tensors(llama_model_loader &) {
|
|
||||||
LLAMA_LOAD_LOCALS;
|
|
||||||
|
|
||||||
const int64_t n_ff_exp = hparams.n_ff_exp;
|
|
||||||
const int64_t n_ff_shexp = hparams.n_ff_shexp;
|
|
||||||
|
|
||||||
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) {
|
|
||||||
output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, TENSOR_DUPLICATED);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < n_layer; ++i) {
|
|
||||||
auto & layer = layers[i];
|
|
||||||
|
|
||||||
const int64_t n_head_i = hparams.n_head(i);
|
|
||||||
const int64_t n_head_kv_i = hparams.n_head_kv(i);
|
|
||||||
const int64_t n_embd_q = n_embd_head_k * n_head_i;
|
|
||||||
const int64_t n_embd_kv = n_embd_head_k * n_head_kv_i;
|
|
||||||
|
|
||||||
layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0);
|
|
||||||
|
|
||||||
layer.wq = create_tensor(tn(LLM_TENSOR_ATTN_Q, "weight", i), {n_embd, n_embd_q}, 0);
|
|
||||||
layer.wk = create_tensor(tn(LLM_TENSOR_ATTN_K, "weight", i), {n_embd, n_embd_kv}, 0);
|
|
||||||
layer.wv = create_tensor(tn(LLM_TENSOR_ATTN_V, "weight", i), {n_embd, n_embd_kv}, 0);
|
|
||||||
layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd_q, n_embd}, 0);
|
|
||||||
|
|
||||||
layer.wqkv_gate = create_tensor(tn(LLM_TENSOR_ATTN_GATE_LAGUNA, "weight", i), {n_embd, n_head_i}, 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);
|
|
||||||
|
|
||||||
layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, 0);
|
|
||||||
|
|
||||||
if (i < (int) hparams.n_layer_dense_lead) {
|
|
||||||
layer.ffn_gate = create_tensor(tn(LLM_TENSOR_FFN_GATE, "weight", i), {n_embd, n_ff}, 0);
|
|
||||||
layer.ffn_down = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "weight", i), { n_ff, n_embd}, 0);
|
|
||||||
layer.ffn_up = create_tensor(tn(LLM_TENSOR_FFN_UP, "weight", i), {n_embd, n_ff}, 0);
|
|
||||||
} else {
|
|
||||||
layer.ffn_gate_inp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP, "weight", i), {n_embd, n_expert}, 0);
|
|
||||||
layer.ffn_exp_probs_b = create_tensor(tn(LLM_TENSOR_FFN_EXP_PROBS_B, "bias", i), {n_expert}, TENSOR_NOT_REQUIRED);
|
|
||||||
|
|
||||||
layer.ffn_gate_exps = create_tensor(tn(LLM_TENSOR_FFN_GATE_EXPS, "weight", i), { n_embd, n_ff_exp, n_expert}, 0);
|
|
||||||
layer.ffn_down_exps = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXPS, "weight", i), {n_ff_exp, n_embd, n_expert}, 0);
|
|
||||||
layer.ffn_up_exps = create_tensor(tn(LLM_TENSOR_FFN_UP_EXPS, "weight", i), { n_embd, n_ff_exp, n_expert}, 0);
|
|
||||||
|
|
||||||
layer.ffn_gate_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_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);
|
|
||||||
layer.ffn_up_shexp = create_tensor(tn(LLM_TENSOR_FFN_UP_SHEXP, "weight", i), {n_embd, n_ff_shexp}, 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());
|
|
||||||
|
|
||||||
const float kq_scale = 1.0f / sqrtf(float(n_embd_head));
|
|
||||||
|
|
||||||
ggml_tensor * cur;
|
|
||||||
ggml_tensor * inpL;
|
|
||||||
|
|
||||||
inpL = build_inp_embd(model.tok_embd);
|
|
||||||
|
|
||||||
ggml_tensor * inp_pos = build_inp_pos();
|
|
||||||
|
|
||||||
auto * inp_attn = build_attn_inp_kv_iswa();
|
|
||||||
|
|
||||||
ggml_tensor * inp_out_ids = build_inp_out_ids();
|
|
||||||
|
|
||||||
for (int il = 0; il < n_layer; ++il) {
|
|
||||||
ggml_tensor * inpSA = inpL;
|
|
||||||
|
|
||||||
const int64_t n_head_il = hparams.n_head(il);
|
|
||||||
const int64_t n_head_kv_il = hparams.n_head_kv(il);
|
|
||||||
const bool is_swa = hparams.is_swa(il);
|
|
||||||
|
|
||||||
const int rope_n_dims = hparams.n_rot(il);
|
|
||||||
const float rope_base = is_swa ? hparams.rope_freq_base_train_swa : hparams.rope_freq_base_train;
|
|
||||||
const float rope_scale = is_swa ? hparams.rope_freq_scale_train_swa : hparams.rope_freq_scale_train;
|
|
||||||
const float rope_ext = is_swa ? 0.0f : 1.0f;
|
|
||||||
const float rope_bfast = hparams.yarn_beta_fast;
|
|
||||||
const float rope_bslow = hparams.yarn_beta_slow;
|
|
||||||
|
|
||||||
cur = build_norm(inpL, model.layers[il].attn_norm, NULL, LLM_NORM_RMS, il);
|
|
||||||
cb(cur, "attn_norm", il);
|
|
||||||
|
|
||||||
// self-attention
|
|
||||||
{
|
|
||||||
ggml_tensor * Qcur = build_lora_mm(model.layers[il].wq, cur);
|
|
||||||
cb(Qcur, "Qcur", il);
|
|
||||||
ggml_tensor * Kcur = build_lora_mm(model.layers[il].wk, cur);
|
|
||||||
cb(Kcur, "Kcur", il);
|
|
||||||
ggml_tensor * Vcur = build_lora_mm(model.layers[il].wv, cur);
|
|
||||||
cb(Vcur, "Vcur", il);
|
|
||||||
|
|
||||||
ggml_tensor * gate = build_lora_mm(model.layers[il].wqkv_gate, cur);
|
|
||||||
cb(gate, "gate", il);
|
|
||||||
|
|
||||||
Qcur = ggml_reshape_3d(ctx0, Qcur, n_embd_head, n_head_il, n_tokens);
|
|
||||||
Kcur = ggml_reshape_3d(ctx0, Kcur, n_embd_head, n_head_kv_il, n_tokens);
|
|
||||||
Vcur = ggml_reshape_3d(ctx0, Vcur, n_embd_head, n_head_kv_il, n_tokens);
|
|
||||||
|
|
||||||
Qcur = build_norm(Qcur, model.layers[il].attn_q_norm, NULL, LLM_NORM_RMS, il);
|
|
||||||
cb(Qcur, "Qcur_normed", il);
|
|
||||||
Kcur = build_norm(Kcur, model.layers[il].attn_k_norm, NULL, LLM_NORM_RMS, il);
|
|
||||||
cb(Kcur, "Kcur_normed", il);
|
|
||||||
|
|
||||||
Qcur = ggml_rope_ext(ctx0, Qcur, inp_pos, nullptr,
|
|
||||||
rope_n_dims, rope_type, hparams.n_ctx_orig_yarn, rope_base, rope_scale,
|
|
||||||
rope_ext, hparams.rope_attn_factor, rope_bfast, rope_bslow);
|
|
||||||
Kcur = ggml_rope_ext(ctx0, Kcur, inp_pos, nullptr,
|
|
||||||
rope_n_dims, rope_type, hparams.n_ctx_orig_yarn, rope_base, rope_scale,
|
|
||||||
rope_ext, hparams.rope_attn_factor, rope_bfast, rope_bslow);
|
|
||||||
|
|
||||||
cb(Qcur, "Qcur", il);
|
|
||||||
cb(Kcur, "Kcur", il);
|
|
||||||
cb(Vcur, "Vcur", il);
|
|
||||||
|
|
||||||
cur = build_attn(inp_attn,
|
|
||||||
nullptr, nullptr, nullptr,
|
|
||||||
Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, kq_scale, il);
|
|
||||||
cb(cur, "attn_pregate", il);
|
|
||||||
|
|
||||||
gate = ggml_softplus(ctx0, gate);
|
|
||||||
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);
|
|
||||||
cb(cur, "attn_gated", il);
|
|
||||||
|
|
||||||
cur = build_lora_mm(model.layers[il].wo, cur, model.layers[il].wo_s);
|
|
||||||
cb(cur, "attn_out", il);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (il == n_layer - 1 && inp_out_ids) {
|
|
||||||
cur = ggml_get_rows(ctx0, cur, inp_out_ids);
|
|
||||||
inpSA = ggml_get_rows(ctx0, inpSA, inp_out_ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
ggml_tensor * ffn_inp = ggml_add(ctx0, cur, inpSA);
|
|
||||||
cb(ffn_inp, "ffn_inp", il);
|
|
||||||
|
|
||||||
// feed-forward
|
|
||||||
cur = build_norm(ffn_inp, model.layers[il].ffn_norm, NULL, LLM_NORM_RMS, il);
|
|
||||||
cb(cur, "ffn_norm", il);
|
|
||||||
|
|
||||||
if ((uint32_t) il < hparams.n_layer_dense_lead) {
|
|
||||||
cur = build_ffn(cur,
|
|
||||||
model.layers[il].ffn_up, NULL, 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);
|
|
||||||
} else {
|
|
||||||
ggml_tensor * up_scale = nullptr;
|
|
||||||
float expert_weights_scale = hparams.expert_weights_scale;
|
|
||||||
|
|
||||||
#if defined(GGML_USE_METAL)
|
|
||||||
if (n_tokens >= 32 && ggml_is_quantized(model.layers[il].ffn_down_exps->type)) {
|
|
||||||
// ggml-metal switches MUL_MAT_ID from its range-safe
|
|
||||||
// matrix-vector kernel to FP16 matrix tiles at 32 tokens
|
|
||||||
// (ne21_mm_id_min in ggml_metal_op_mul_mat_id). Laguna's routed
|
|
||||||
// SwiGLU activations can overflow those tiles. Scale the linear
|
|
||||||
// up branch and fold the inverse power-of-two factor into the
|
|
||||||
// existing routing-weight scale. Revisit this guard if the
|
|
||||||
// Metal dispatch threshold changes.
|
|
||||||
constexpr float down_input_scale = 1.0f / 256.0f;
|
|
||||||
up_scale = ggml_fill(ctx0,
|
|
||||||
model.layers[il].ffn_exp_probs_b, down_input_scale);
|
|
||||||
expert_weights_scale /= down_input_scale;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
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,
|
|
||||||
expert_weights_scale,
|
|
||||||
(llama_expert_gating_func_type) hparams.expert_gating_func,
|
|
||||||
il,
|
|
||||||
nullptr, nullptr,
|
|
||||||
up_scale);
|
|
||||||
cb(moe_out, "ffn_moe_out", il);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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, model.output_s);
|
|
||||||
cb(cur, "result_output", -1);
|
|
||||||
res->t_logits = cur;
|
|
||||||
|
|
||||||
ggml_build_forward_expand(gf, cur);
|
|
||||||
}
|
|
||||||
@@ -535,12 +535,12 @@ func TestLagunaParserNonAssistantLastMessageStillPrimesThinking(t *testing.T) {
|
|||||||
func TestLagunaV8ParserAssistantHistoryStillPrimesThinking(t *testing.T) {
|
func TestLagunaV8ParserAssistantHistoryStillPrimesThinking(t *testing.T) {
|
||||||
// Laguna v8 closes assistant history and emits a fresh generation prompt,
|
// Laguna v8 closes assistant history and emits a fresh generation prompt,
|
||||||
// so an assistant tail message must not switch the parser into prefill mode.
|
// so an assistant tail message must not switch the parser into prefill mode.
|
||||||
parser := ParserForName("laguna-v8")
|
parser := ParserForName("poolside-v1")
|
||||||
if parser == nil {
|
if parser == nil {
|
||||||
t.Fatal("expected laguna-v8 parser")
|
t.Fatal("expected poolside-v1 parser")
|
||||||
}
|
}
|
||||||
if !parser.HasToolSupport() || !parser.HasThinkingSupport() {
|
if !parser.HasToolSupport() || !parser.HasThinkingSupport() {
|
||||||
t.Fatal("laguna-v8 parser should advertise tools and thinking")
|
t.Fatal("poolside-v1 parser should advertise tools and thinking")
|
||||||
}
|
}
|
||||||
|
|
||||||
parser.Init(nil, &api.Message{Role: "assistant", Content: "Previous."}, &api.ThinkValue{Value: true})
|
parser.Init(nil, &api.Message{Role: "assistant", Content: "Previous."}, &api.ThinkValue{Value: true})
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ func ParserForName(name string) Parser {
|
|||||||
return &LFM2Parser{hasThinkingSupport: true}
|
return &LFM2Parser{hasThinkingSupport: true}
|
||||||
case "laguna":
|
case "laguna":
|
||||||
return &LagunaParser{}
|
return &LagunaParser{}
|
||||||
case "laguna-v8":
|
case "poolside-v1":
|
||||||
return &LagunaV8Parser{}
|
return &LagunaV8Parser{}
|
||||||
case "cohere":
|
case "cohere":
|
||||||
return &CohereParser{}
|
return &CohereParser{}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ func rendererForName(name string) Renderer {
|
|||||||
return &LFM2Renderer{IsThinking: true, useImgTags: RenderImgTags}
|
return &LFM2Renderer{IsThinking: true, useImgTags: RenderImgTags}
|
||||||
case "laguna":
|
case "laguna":
|
||||||
return &LagunaRenderer{}
|
return &LagunaRenderer{}
|
||||||
case "laguna-v8":
|
case "poolside-v1":
|
||||||
return &LagunaV8Renderer{}
|
return &LagunaV8Renderer{}
|
||||||
case "cohere":
|
case "cohere":
|
||||||
return &CohereRenderer{}
|
return &CohereRenderer{}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ func TestLeadingBOSForRenderer(t *testing.T) {
|
|||||||
{name: "lfm2", want: "<|startoftext|>"},
|
{name: "lfm2", want: "<|startoftext|>"},
|
||||||
{name: "lfm2-thinking", want: "<|startoftext|>"},
|
{name: "lfm2-thinking", want: "<|startoftext|>"},
|
||||||
{name: "laguna", want: "〈|EOS|〉"},
|
{name: "laguna", want: "〈|EOS|〉"},
|
||||||
{name: "laguna-v8", want: "〈|EOS|〉"},
|
{name: "poolside-v1", want: "〈|EOS|〉"},
|
||||||
{name: "deepseek3.1", want: "<|begin▁of▁sentence|>"},
|
{name: "deepseek3.1", want: "<|begin▁of▁sentence|>"},
|
||||||
{name: "cogito", want: "<|begin▁of▁sentence|>"},
|
{name: "cogito", want: "<|begin▁of▁sentence|>"},
|
||||||
{name: "qwen3-coder", want: ""},
|
{name: "qwen3-coder", want: ""},
|
||||||
|
|||||||
Reference in New Issue
Block a user