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:
Daniel Hiltgen
2026-07-22 17:09:18 -07:00
committed by GitHub
parent efb7e3c55e
commit 1fd1ccf7ad
11 changed files with 134 additions and 387 deletions

View File

@@ -1 +1 @@
b10069 b10091

View File

@@ -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)

View File

@@ -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 |

View File

@@ -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);

View 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);

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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})

View File

@@ -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{}

View File

@@ -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{}

View File

@@ -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: ""},