Commit fecdc81ee for llama.cpp
commit fecdc81eec0922b229e7e6f4069cea603e10d6a7
Author: Nik Bogatyrev <50734723+notforu777@users.noreply.github.com>
Date: Thu Nov 5 21:55:15 2026 +0300
model : add Ripple (ricursiveai) (#31692)
* feat : added ricursiveai ripple support
* fix : mandatiry SWA, scale through helper, comments back
* fix : metal
* model : make ricursive RoPE/NoPE explicit and validate the layer pattern
Write attention.rope_pattern and read it as optional, falling back to the
sliding window pattern so existing GGUFs keep loading. Refuse a
sliding_window_pattern that does not cover every layer, a model with no
sliding layer, and a gate or value residual present in only some layers.
The converter derives layer_types when the config omits it, ignores a stale
hybrid_layer_pattern and drops tensors of features the config disables.
Assisted-by: Claude Opus 5.5
* model : make ricursive swiglu_clamp per-layer
Store swiglu_clamp as a per-layer array, read it with get_key_or_arr so the
scalar in existing GGUFs still applies to every layer, and refuse a negative
or non-finite value. Add a test-llama-archs fixture with mixed per-layer
clamps and no rope_pattern, so the saver roundtrip checks both.
Assisted-by: Claude Opus 5.5
* model : use the ricursive per-tensor scales and RoPE parameters
Pass the attn_gate and output weight scales to build_lora_mm, take
ext_factor and attn_factor from the graph instead of hardcoding them, and
copy rope_freq_scale_train to the SWA layers instead of pinning it to 1.
Cite the reference lines for the value residual and the NoPE layers.
Assisted-by: Claude Opus 5.5
* convert : drop the ricursive chat_template.jinja override
The precedence between tokenizer_config.json and chat_template.jinja is to
be changed in SpecialVocab for all models instead. Until then a new
conversion stores the short tokenizer_config.json template; GGUFs that
already carry the tool-aware template are not affected.
Assisted-by: Claude Opus 5.5
* fix : remove NVFP4 checkpoint quantization block
* match #31733 refactor
---------
Co-authored-by: Piotr Wilkin <ilintar@gmail.com>
Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co>
diff --git a/conversion/__init__.py b/conversion/__init__.py
index a4359f21f..fcbfb6a3a 100644
--- a/conversion/__init__.py
+++ b/conversion/__init__.py
@@ -20,6 +20,7 @@ TEXT_MODEL_MAP: dict[str, str] = {
"NEOChatModel": "sensenova_u1",
"AfmoeForCausalLM": "afmoe",
"LagunaForCausalLM": "laguna",
+ "RicursiveForCausalLM": "ricursive",
"FlintForCausalLM": "flint",
"ApertusForCausalLM": "llama",
"ArceeForCausalLM": "llama",
diff --git a/conversion/ricursive.py b/conversion/ricursive.py
new file mode 100644
index 000000000..13cb77367
--- /dev/null
+++ b/conversion/ricursive.py
@@ -0,0 +1,92 @@
+from __future__ import annotations
+
+import math
+
+from typing import Iterable, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from torch import Tensor
+
+from .base import ModelBase, TextModel, gguf, logger
+
+
+@ModelBase.register("RicursiveForCausalLM")
+@ModelBase.example("ricursiveAI/Ripple-65B-Preview")
+class RicursiveModel(TextModel):
+ model_arch = gguf.MODEL_ARCH.RICURSIVE
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ hparams = self.hparams
+
+ if (act := hparams.get("hidden_act", "silu")) != "silu":
+ raise ValueError(f"Ricursive: unsupported hidden_act {act!r}")
+
+ # RicursiveConfig derives the layer pattern from layer_types alone, by default full attention iff i % 5 == 4
+ if (layer_types := hparams.get("layer_types")) is None:
+ layer_types = ["full_attention" if i % 5 == 4 else "sliding_attention" for i in range(self.block_count)]
+ if len(layer_types) != self.block_count:
+ raise ValueError(f"Ricursive: layer_types has {len(layer_types)} entries, expected {self.block_count}")
+ if unknown := set(layer_types) - {"sliding_attention", "full_attention"}:
+ raise ValueError(f"Ricursive: unsupported layer_types {sorted(unknown)}")
+ self.is_swa = [t == "sliding_attention" for t in layer_types]
+ # only sliding layers use RoPE, an all-full-attention model would have no positional encoding
+ if not any(self.is_swa):
+ raise ValueError("Ricursive: layer_types has no sliding_attention layer, all-full-attention models are not supported")
+ # RicursiveConfig pops a stored hybrid_layer_pattern and recomputes it from layer_types
+ if (pattern := hparams.get("hybrid_layer_pattern")) is not None and list(pattern) != [int(swa) for swa in self.is_swa]:
+ logger.warning("Ricursive: hybrid_layer_pattern does not match layer_types, ignoring it")
+
+ if not hparams.get("gate_full_precision", True):
+ logger.warning("Ricursive: gate_full_precision is false, llama.cpp computes the attention gate in f32")
+
+ # RicursiveConfig defaults; the reference ignores the tensors of a disabled feature
+ self.use_valres = hparams.get("use_valres", False)
+ self.use_gate = hparams.get("use_gated_attention", True)
+ names = list(self.model_tensors)
+ if bias := [name for name in names if name.endswith("bias")]:
+ raise ValueError(f"Ricursive: bias tensors are not supported, found {bias[0]!r}")
+ for enabled, suffix in ((self.use_valres, ".self_attn.value_res_lambda"), (self.use_gate, ".self_attn.g_proj.weight")):
+ if enabled and sum(name.endswith(suffix) for name in names) != self.block_count:
+ raise ValueError(f"Ricursive: expected one {suffix!r} tensor per layer")
+
+ def set_vocab(self):
+ self._set_vocab_gpt2()
+ # eos_token_id is [<|im_end|>, <|endoftext|>]: register the turn end as EOT so generation stops on it
+ eos = self.hparams.get("eos_token_id")
+ if isinstance(eos, list):
+ extra = [e for e in eos if e != self.hparams.get("bos_token_id")]
+ if extra:
+ self.gguf_writer.add_eot_token_id(extra[0])
+ logger.info(f"gguf: eot_token_id = {extra[0]}")
+
+ def set_gguf_parameters(self):
+ super().set_gguf_parameters()
+ hparams = self.hparams
+ self.gguf_writer.add_vocab_size(hparams["vocab_size"])
+ self.gguf_writer.add_rope_dimension_count(hparams["head_dim"])
+
+ self.gguf_writer.add_sliding_window(hparams["sliding_window"])
+ self.gguf_writer.add_sliding_window_pattern(self.is_swa)
+ # sliding layers use RoPE, full attention layers use no positional encoding
+ self.gguf_writer.add_rope_pattern(self.is_swa)
+
+ if (embed_scale := hparams.get("embed_scale")) is not None:
+ if embed_scale == 0:
+ raise ValueError(f"Ricursive: unsupported embed_scale {embed_scale!r}")
+ self.gguf_writer.add_embedding_scale(float(embed_scale))
+ # absent and 0 both mean no clamp (the reference tests `if clamp_threshold:`)
+ if (clamp := hparams.get("ffn_clamp_threshold")) is not None:
+ if clamp < 0 or not math.isfinite(clamp):
+ raise ValueError(f"Ricursive: unsupported ffn_clamp_threshold {clamp!r}")
+ self.gguf_writer.add_swiglu_clamp([float(clamp)] * self.block_count)
+
+ def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
+ # value_res_lambda has no .weight suffix in the checkpoint
+ if name.endswith(".value_res_lambda"):
+ if not self.use_valres:
+ return
+ name += ".weight"
+ elif name.endswith(".g_proj.weight") and not self.use_gate:
+ return
+ yield from super().modify_tensors(data_torch, name, bid)
diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py
index 9fe16a6fa..170df4e6f 100644
--- a/gguf-py/gguf/constants.py
+++ b/gguf-py/gguf/constants.py
@@ -162,6 +162,7 @@ class Keys:
EMBD_LENGTH_PER_LAYER_INP = "{arch}.embedding_length_per_layer_input"
SWIGLU_CLAMP_EXP = "{arch}.swiglu_clamp_exp"
SWIGLU_CLAMP_SHEXP = "{arch}.swiglu_clamp_shexp"
+ SWIGLU_CLAMP = "{arch}.swiglu_clamp"
HIDDEN_ACT = "{arch}.hidden_activation"
DENSE_FEAT_IN_SIZE = "{arch}.{dense}_feat_in"
DENSE_FEAT_OUT_SIZE = "{arch}.{dense}_feat_out"
@@ -710,6 +711,7 @@ class MODEL_ARCH(IntEnum):
ARCEE = auto()
AFMOE = auto()
LAGUNA = auto()
+ RICURSIVE = auto()
ERNIE4_5 = auto()
ERNIE4_5_MOE = auto()
HUNYUAN_MOE = auto()
@@ -878,6 +880,7 @@ class MODEL_TENSOR(IntEnum):
ATTN_K_NORM = auto()
LAYER_OUT_NORM = auto()
LAYER_OUT_SCALE = auto()
+ ATTN_VRES_LAMBDA = auto()
PER_LAYER_TOKEN_EMBD = auto() # gemma3n
PER_LAYER_MODEL_PROJ = auto() # gemma3n
PER_LAYER_INP_GATE = auto() # gemma3n
@@ -1624,6 +1627,7 @@ MODEL_ARCH_NAMES: dict[MODEL_ARCH, str] = {
MODEL_ARCH.ARCEE: "arcee",
MODEL_ARCH.AFMOE: "afmoe",
MODEL_ARCH.LAGUNA: "laguna",
+ MODEL_ARCH.RICURSIVE: "ricursive",
MODEL_ARCH.ERNIE4_5: "ernie4_5",
MODEL_ARCH.ERNIE4_5_MOE: "ernie4_5-moe",
MODEL_ARCH.FALCON_H1: "falcon-h1",
@@ -1791,6 +1795,7 @@ TENSOR_NAMES: dict[MODEL_TENSOR, str] = {
MODEL_TENSOR.FFN_POST_EXP_NORM: "blk.{bid}.ffn_post_exp_norm",
MODEL_TENSOR.LAYER_OUT_NORM: "blk.{bid}.layer_output_norm",
MODEL_TENSOR.LAYER_OUT_SCALE: "blk.{bid}.layer_output_scale",
+ MODEL_TENSOR.ATTN_VRES_LAMBDA: "blk.{bid}.attn_vres_lambda",
MODEL_TENSOR.PER_LAYER_TOKEN_EMBD: "per_layer_token_embd", # gemma3n
MODEL_TENSOR.PER_LAYER_MODEL_PROJ: "per_layer_model_proj", # gemma3n
MODEL_TENSOR.PER_LAYER_PROJ_NORM: "per_layer_proj_norm", # gemma3n
@@ -5580,6 +5585,26 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
MODEL_TENSOR.FFN_POST_NORM,
MODEL_TENSOR.FFN_EXP_PROBS_B,
],
+ MODEL_ARCH.RICURSIVE: [
+ 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.ATTN_VRES_LAMBDA,
+ MODEL_TENSOR.ATTN_POST_NORM,
+ MODEL_TENSOR.FFN_PRE_NORM,
+ MODEL_TENSOR.FFN_GATE,
+ MODEL_TENSOR.FFN_DOWN,
+ MODEL_TENSOR.FFN_UP,
+ MODEL_TENSOR.FFN_POST_NORM,
+ ],
MODEL_ARCH.LAGUNA: [
MODEL_TENSOR.TOKEN_EMBD,
MODEL_TENSOR.OUTPUT_NORM,
diff --git a/gguf-py/gguf/gguf_writer.py b/gguf-py/gguf/gguf_writer.py
index 0e7aa5012..f6b98a57d 100644
--- a/gguf-py/gguf/gguf_writer.py
+++ b/gguf-py/gguf/gguf_writer.py
@@ -912,6 +912,9 @@ class GGUFWriter:
def add_swiglu_clamp_exp(self, values: Sequence[float]) -> None:
self.add_array(Keys.LLM.SWIGLU_CLAMP_EXP.format(arch=self.arch), values)
+ def add_swiglu_clamp(self, values: Sequence[float]) -> None:
+ self.add_array(Keys.LLM.SWIGLU_CLAMP.format(arch=self.arch), values)
+
def add_swiglu_clamp_shexp(self, values: Sequence[float]) -> None:
self.add_array(Keys.LLM.SWIGLU_CLAMP_SHEXP.format(arch=self.arch), values)
diff --git a/gguf-py/gguf/tensor_mapping.py b/gguf-py/gguf/tensor_mapping.py
index ff22a60bc..eeadd6a9a 100644
--- a/gguf-py/gguf/tensor_mapping.py
+++ b/gguf-py/gguf/tensor_mapping.py
@@ -451,6 +451,10 @@ class TensorNameMap:
"model.layers.{bid}.self_attn.attn_gate_proj", # k2-horizon
),
+ MODEL_TENSOR.ATTN_VRES_LAMBDA: (
+ "model.layers.{bid}.self_attn.value_res_lambda", # ricursive
+ ),
+
# Feed-forward norm
MODEL_TENSOR.FFN_NORM: (
"layers.{bid}.moe_connection.norm", # metis16
diff --git a/src/llama-arch.cpp b/src/llama-arch.cpp
index 6bb200f84..265c2ddeb 100644
--- a/src/llama-arch.cpp
+++ b/src/llama-arch.cpp
@@ -177,6 +177,7 @@ static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = {
{ LLM_ARCH_K2_HORIZON, "k2-horizon" },
{ LLM_ARCH_BEAM, "beam" },
{ LLM_ARCH_KOLIBRI1, "kolibri1" },
+ { LLM_ARCH_RICURSIVE, "ricursive" },
{ LLM_ARCH_UNKNOWN, "(unknown)" },
};
@@ -226,6 +227,7 @@ static const std::map<llm_kv, const char *> LLM_KV_NAMES = {
{ LLM_KV_EXPERT_CHUNK_FEED_FORWARD_LENGTH, "%s.expert_chunk_feed_forward_length" },
{ LLM_KV_SWIGLU_CLAMP_EXP, "%s.swiglu_clamp_exp" },
{ LLM_KV_SWIGLU_CLAMP_SHEXP, "%s.swiglu_clamp_shexp" },
+ { LLM_KV_SWIGLU_CLAMP, "%s.swiglu_clamp" },
{ LLM_KV_USE_PARALLEL_RESIDUAL, "%s.use_parallel_residual" },
{ LLM_KV_TENSOR_DATA_LAYOUT, "%s.tensor_data_layout" },
{ LLM_KV_EXPERT_COUNT, "%s.expert_count" },
@@ -594,6 +596,7 @@ static const std::map<llm_tensor, const char *> LLM_TENSOR_NAMES = {
{ LLM_TENSOR_ATTN_QKV, "blk.%d.attn_qkv" },
{ LLM_TENSOR_LAYER_OUT_NORM, "blk.%d.layer_output_norm" },
{ LLM_TENSOR_LAYER_OUT_SCALE, "blk.%d.layer_output_scale" },
+ { LLM_TENSOR_ATTN_VRES_LAMBDA, "blk.%d.attn_vres_lambda" },
{ LLM_TENSOR_ATTN_OUT_NORM, "blk.%d.attn_output_norm" },
{ LLM_TENSOR_POS_EMBD, "position_embd" },
{ LLM_TENSOR_FFN_ACT, "blk.%d.ffn.act" },
@@ -1099,6 +1102,7 @@ static const std::map<llm_tensor, llm_tensor_info> LLM_TENSOR_INFOS = {
{LLM_TENSOR_ATTN_K_NORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
{LLM_TENSOR_LAYER_OUT_NORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
{LLM_TENSOR_LAYER_OUT_SCALE, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
+ {LLM_TENSOR_ATTN_VRES_LAMBDA, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
{LLM_TENSOR_ATTN_Q_A_NORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
{LLM_TENSOR_ATTN_KV_A_NORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
{LLM_TENSOR_ATTN_SUB_NORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}},
diff --git a/src/llama-arch.h b/src/llama-arch.h
index 5dfad29ad..7bf0d3ae5 100644
--- a/src/llama-arch.h
+++ b/src/llama-arch.h
@@ -181,6 +181,7 @@ enum llm_arch {
LLM_ARCH_BEAM,
LLM_ARCH_KOLIBRI1,
LLM_ARCH_LIMITE,
+ LLM_ARCH_RICURSIVE,
LLM_ARCH_UNKNOWN,
};
@@ -230,6 +231,7 @@ enum llm_kv {
LLM_KV_EXPERT_CHUNK_FEED_FORWARD_LENGTH,
LLM_KV_SWIGLU_CLAMP_EXP,
LLM_KV_SWIGLU_CLAMP_SHEXP,
+ LLM_KV_SWIGLU_CLAMP,
LLM_KV_USE_PARALLEL_RESIDUAL,
LLM_KV_TENSOR_DATA_LAYOUT,
LLM_KV_EXPERT_COUNT,
@@ -612,6 +614,7 @@ enum llm_tensor {
LLM_TENSOR_ATTN_K_NORM,
LLM_TENSOR_LAYER_OUT_NORM,
LLM_TENSOR_LAYER_OUT_SCALE,
+ LLM_TENSOR_ATTN_VRES_LAMBDA,
LLM_TENSOR_POST_ATTN_NORM,
LLM_TENSOR_POST_MLP_NORM,
LLM_TENSOR_PER_LAYER_TOKEN_EMBD, // gemma3n
diff --git a/src/llama-hparams.h b/src/llama-hparams.h
index 95daf482d..2f7b97823 100644
--- a/src/llama-hparams.h
+++ b/src/llama-hparams.h
@@ -431,6 +431,7 @@ struct llama_hparams {
// Step35: optional per-layer clamps for (Swi)GLU
std::array<float, LLAMA_MAX_LAYERS> swiglu_clamp_exp; // clamping for expert FFN
std::array<float, LLAMA_MAX_LAYERS> swiglu_clamp_shexp; // shared expert
+ std::array<float, LLAMA_MAX_LAYERS> swiglu_clamp; // dense FFN
// this value n_pattern means that every nth layer is dense (i.e. non-SWA)
// dense_first means whether the pattern is start with a dense layer
diff --git a/src/llama-model-saver.cpp b/src/llama-model-saver.cpp
index b8a8f77df..b667bc49f 100644
--- a/src/llama-model-saver.cpp
+++ b/src/llama-model-saver.cpp
@@ -210,6 +210,8 @@ void llama_model_saver::add_kv_from_model() {
add_kv(LLM_KV_MOE_LATENT_SIZE, hparams.moe_latent_size);
add_kv(LLM_KV_EXPERT_SHARED_FEED_FORWARD_LENGTH, hparams.n_ff_shexp);
add_kv(LLM_KV_EXPERT_CHUNK_FEED_FORWARD_LENGTH, hparams.n_ff_chexp);
+ add_kv(LLM_KV_SWIGLU_CLAMP, std::vector<float>(
+ hparams.swiglu_clamp.begin(), hparams.swiglu_clamp.begin() + hparams.n_layer_all));
add_kv(LLM_KV_SWIGLU_CLAMP_EXP, std::vector<float>(
hparams.swiglu_clamp_exp.begin(), hparams.swiglu_clamp_exp.begin() + hparams.n_layer_all));
add_kv(LLM_KV_SWIGLU_CLAMP_SHEXP, std::vector<float>(
diff --git a/src/llama-model.cpp b/src/llama-model.cpp
index 03aab320f..27809ccad 100644
--- a/src/llama-model.cpp
+++ b/src/llama-model.cpp
@@ -294,6 +294,8 @@ static llama_model * llama_model_mapping(llm_arch arch, const llama_model_params
return new llama_model_afmoe(params);
case LLM_ARCH_LAGUNA:
return new llama_model_laguna(params);
+ case LLM_ARCH_RICURSIVE:
+ return new llama_model_ricursive(params);
case LLM_ARCH_ERNIE4_5:
return new llama_model_ernie4_5(params);
case LLM_ARCH_ERNIE4_5_MOE:
@@ -1548,6 +1550,7 @@ void llama_model_base::load_hparams(llama_model_loader & ml) {
std::fill(hparams.swiglu_clamp_exp.begin(), hparams.swiglu_clamp_exp.end(), 0.0f);
std::fill(hparams.swiglu_clamp_shexp.begin(), hparams.swiglu_clamp_shexp.end(), 0.0f);
+ std::fill(hparams.swiglu_clamp.begin(), hparams.swiglu_clamp.end(), 0.0f);
ml.get_key_or_arr(LLM_KV_FEED_FORWARD_LENGTH, hparams.n_ff_arr, hparams.n_layer_all, false);
ml.get_key_or_arr(LLM_KV_ATTENTION_HEAD_COUNT, hparams.n_head_arr, hparams.n_layer_all, false);
@@ -3709,6 +3712,7 @@ llama_rope_type llama_model_rope_type(const llama_model * model) {
case LLM_ARCH_QWEN3MOE:
case LLM_ARCH_LLADA_MOE:
case LLM_ARCH_RND1:
+ case LLM_ARCH_RICURSIVE:
case LLM_ARCH_OLMO2:
case LLM_ARCH_OLMOE:
case LLM_ARCH_PHI2:
diff --git a/src/llama-model.h b/src/llama-model.h
index 45d2b8666..f8cffb346 100644
--- a/src/llama-model.h
+++ b/src/llama-model.h
@@ -377,6 +377,9 @@ struct llama_layer {
struct ggml_tensor * ffn_resid_alpha = nullptr;
struct ggml_tensor * ffn_resid_beta = nullptr;
+ // value residual mix
+ struct ggml_tensor * attn_vres_lambda = nullptr;
+
// ff shared expert (shexp)
struct ggml_tensor * ffn_gate_inp_shexp = nullptr;
struct ggml_tensor * ffn_gate_shexp = nullptr;
diff --git a/src/models/models.h b/src/models/models.h
index 267468525..628275289 100644
--- a/src/models/models.h
+++ b/src/models/models.h
@@ -2269,6 +2269,18 @@ struct llama_model_beam : public llama_model_base {
};
+struct llama_model_ricursive : public llama_model_base {
+ llama_model_ricursive(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_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;
diff --git a/src/models/ricursive.cpp b/src/models/ricursive.cpp
new file mode 100644
index 000000000..cd5dc3547
--- /dev/null
+++ b/src/models/ricursive.cpp
@@ -0,0 +1,236 @@
+#include "models.h"
+
+void llama_model_ricursive::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_EMBEDDING_SCALE, hparams.f_embedding_scale, false);
+ // optional: absent or 0 means no clamp (the reference tests `if clamp_threshold:`); a scalar applies to every layer
+ ml.get_key_or_arr(LLM_KV_SWIGLU_CLAMP, hparams.swiglu_clamp, hparams.n_layer_all, false);
+ for (uint32_t il = 0; il < hparams.n_layer_all; ++il) {
+ if (!std::isfinite(hparams.swiglu_clamp[il]) || hparams.swiglu_clamp[il] < 0.0f) {
+ throw std::runtime_error(format("ricursive: swiglu_clamp for layer %u must be finite and non-negative, got %g", il, hparams.swiglu_clamp[il]));
+ }
+ }
+
+ if (hparams.n_embd_head_k() != hparams.n_embd_head_v()) {
+ throw std::runtime_error("ricursive: key_length and value_length must be equal");
+ }
+
+ // RoPE is only applied on the SWA layers, so a model without SWA would have no positional encoding
+ ml.get_key(LLM_KV_ATTENTION_SLIDING_WINDOW, hparams.n_swa);
+ if (hparams.n_swa == 0) {
+ throw std::runtime_error("ricursive: sliding_window must be > 0");
+ }
+ hparams.swa_type = LLAMA_SWA_TYPE_STANDARD;
+ if (ml.get_arr(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, hparams.is_swa_impl, false)) {
+ // load_swa_pattern only bounds the array by LLAMA_MAX_LAYERS: a short one would leave the last layers dense and without RoPE
+ uint32_t n_entries = 0;
+ ml.get_arr_n(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, n_entries);
+ if (n_entries != hparams.n_layer()) {
+ throw std::runtime_error(format("ricursive: sliding_window_pattern has %u entries, expected %u", n_entries, hparams.n_layer()));
+ }
+ } else {
+ // default: every 5th layer is global (RicursiveConfig layer_types: full attention iff i % 5 == 4)
+ load_swa_pattern(ml, 5);
+ }
+ if (!hparams.is_swa_any()) {
+ throw std::runtime_error("ricursive: no sliding window layer, the model would have no positional encoding");
+ }
+
+ // RoPE exactly on the SWA layers; rope_pattern is optional because the first published GGUFs do not carry it
+ hparams.rope_pattern = hparams.is_swa_impl;
+ ml.get_key_or_arr(LLM_KV_ATTENTION_ROPE_PATTERN, hparams.rope_pattern, hparams.n_layer(), false);
+ for (uint32_t il = 0; il < hparams.n_layer(); ++il) {
+ if (hparams.has_rope(il) != hparams.is_swa(il)) {
+ throw std::runtime_error(format("ricursive: layer %u has %s but %s", il,
+ hparams.has_rope(il) ? "RoPE" : "no RoPE", hparams.is_swa(il) ? "a sliding window" : "no sliding window"));
+ }
+ }
+
+ // the SWA layers are the only RoPE layers, they use the model RoPE parameters
+ hparams.rope_freq_base_train_swa = hparams.rope_freq_base_train;
+ hparams.rope_freq_scale_train_swa = hparams.rope_freq_scale_train;
+ ml.get_key(LLM_KV_ROPE_FREQ_BASE_SWA, hparams.rope_freq_base_train_swa, false);
+
+ switch (hparams.n_layer()) {
+ case 80: type = LLM_TYPE_65B; break;
+ default: type = LLM_TYPE_UNKNOWN;
+ }
+}
+
+void llama_model_ricursive::load_arch_tensors(llama_model_loader &) {
+ LLAMA_LOAD_LOCALS;
+
+ tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0);
+
+ output_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);
+ }
+
+ const int64_t n_embd_q = n_embd_head_k * n_head;
+
+ for (int i = 0; i < n_layer; ++i) {
+ auto & layer = layers[i];
+
+ layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0);
+
+ 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_k_gqa}, 0);
+ layer.wv = create_tensor(tn(LLM_TENSOR_ATTN_V, "weight", i), {n_embd, n_embd_v_gqa}, 0);
+ layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd_q, 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);
+
+ layer.wqkv_gate = create_tensor(tn(LLM_TENSOR_ATTN_GATE, "weight", i), {n_embd, n_embd_q}, TENSOR_NOT_REQUIRED);
+ layer.attn_vres_lambda = create_tensor(tn(LLM_TENSOR_ATTN_VRES_LAMBDA, "weight", i), {1}, TENSOR_NOT_REQUIRED);
+
+ layer.attn_post_norm = create_tensor(tn(LLM_TENSOR_ATTN_POST_NORM, "weight", i), {n_embd}, 0);
+
+ layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, 0);
+ layer.ffn_gate = create_tensor(tn(LLM_TENSOR_FFN_GATE, "weight", i), {n_embd, n_ff}, 0);
+ layer.ffn_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);
+ layer.ffn_post_norm = create_tensor(tn(LLM_TENSOR_FFN_POST_NORM, "weight", i), {n_embd}, 0);
+ }
+
+ // the gate and the value residual are per-model switches (use_gated_attention, use_valres)
+ for (int i = 1; i < n_layer; ++i) {
+ if ((layers[i].wqkv_gate == nullptr) != (layers[0].wqkv_gate == nullptr)) {
+ throw std::runtime_error(format("ricursive: attn_gate must be present in all layers or in none (layers 0 and %d differ)", i));
+ }
+ if ((layers[i].attn_vres_lambda == nullptr) != (layers[0].attn_vres_lambda == nullptr)) {
+ throw std::runtime_error(format("ricursive: attn_vres_lambda must be present in all layers or in none (layers 0 and %d differ)", i));
+ }
+ }
+}
+
+std::unique_ptr<llm_graph_context> llama_model_ricursive::build_arch_graph(const llm_graph_params & params) const {
+ return std::make_unique<graph>(params);
+}
+
+llama_model_ricursive::graph::graph(const llm_graph_params & params) : llm_graph_context(params) {
+ const int64_t n_embd_head = hparams.n_embd_head_v();
+
+ ggml_tensor * cur;
+ ggml_tensor * inpL;
+
+ inpL = build_inp_embd(model.tok_embd);
+
+ // value residual: x0 = token embeddings * embedding_scale, the same tensor for all layers (modeling_ricursive.py:380-381,405)
+ ggml_tensor * inp_vres = inpL;
+
+ ggml_tensor * inp_pos = build_inp_pos();
+
+ auto * inp_attn = build_attn_inp_kv_iswa();
+
+ 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 auto & layer = model.layers[il];
+
+ ggml_tensor * inpSA = inpL;
+
+ cur = build_norm(inpL, layer.attn_norm, NULL, LLM_NORM_RMS, il);
+ cb(cur, "attn_norm", il);
+
+ ggml_tensor * attn_in = cur;
+
+ {
+ ggml_tensor * Qcur = build_lora_mm(layer.wq, attn_in, layer.wq_s);
+ ggml_tensor * Kcur = build_lora_mm(layer.wk, attn_in, layer.wk_s);
+
+ // v_in = lerp(x0, attn_norm(x), sigmoid(lambda)), lambda is learned per layer (modeling_ricursive.py:182-185)
+ ggml_tensor * v_in = attn_in;
+ if (layer.attn_vres_lambda) {
+ ggml_tensor * mix = ggml_sigmoid(ctx0, layer.attn_vres_lambda);
+ v_in = ggml_add(ctx0, inp_vres, ggml_mul(ctx0, ggml_sub(ctx0, attn_in, inp_vres), mix));
+ cb(v_in, "vres_in", il);
+ }
+ ggml_tensor * Vcur = build_lora_mm(layer.wv, v_in, layer.wv_s);
+
+ Qcur = ggml_reshape_3d(ctx0, Qcur, n_embd_head, n_head, n_tokens);
+ Kcur = ggml_reshape_3d(ctx0, Kcur, n_embd_head, n_head_kv, n_tokens);
+ Vcur = ggml_reshape_3d(ctx0, Vcur, n_embd_head, n_head_kv, n_tokens);
+
+ Qcur = build_norm(Qcur, layer.attn_q_norm, NULL, LLM_NORM_RMS, il);
+ Kcur = build_norm(Kcur, layer.attn_k_norm, NULL, LLM_NORM_RMS, il);
+ cb(Qcur, "Qcur_normed", il);
+ cb(Kcur, "Kcur_normed", il);
+
+ // global layers use no RoPE (NoPE): the reference rotates only sliding_attention layers (modeling_ricursive.py:127-129,203-205)
+ if (hparams.has_rope(il)) {
+ const float freq_base_l = model.get_rope_freq_base (cparams, il);
+ const float freq_scale_l = model.get_rope_freq_scale(cparams, il);
+
+ Qcur = ggml_rope_ext(ctx0, Qcur, inp_pos, nullptr,
+ n_rot, rope_type, n_ctx_orig, freq_base_l, freq_scale_l,
+ ext_factor, attn_factor, beta_fast, beta_slow);
+ Kcur = ggml_rope_ext(ctx0, Kcur, inp_pos, nullptr,
+ n_rot, rope_type, n_ctx_orig, freq_base_l, freq_scale_l,
+ ext_factor, attn_factor, beta_fast, beta_slow);
+ }
+ cb(Qcur, "Qcur", il);
+ cb(Kcur, "Kcur", il);
+ cb(Vcur, "Vcur", il);
+
+ cur = build_attn(inp_attn, NULL, NULL, NULL, Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, kq_scale, il);
+ cb(cur, "attn_out", il);
+
+ if (layer.wqkv_gate) {
+ ggml_tensor * gate = build_lora_mm(layer.wqkv_gate, attn_in, layer.wqkv_gate_s);
+ gate = ggml_sigmoid(ctx0, gate);
+ cb(gate, "attn_gate", il);
+ cur = ggml_mul(ctx0, cur, gate);
+ cb(cur, "attn_gated", il);
+ }
+
+ cur = build_lora_mm(layer.wo, cur, layer.wo_s);
+ }
+
+ 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);
+ }
+
+ cur = build_norm(cur, layer.attn_post_norm, NULL, LLM_NORM_RMS, il);
+ cb(cur, "attn_post_norm", il);
+
+ ggml_tensor * ffn_inp = ggml_add(ctx0, cur, inpSA);
+ cb(ffn_inp, "ffn_inp", il);
+
+ cur = build_norm(ffn_inp, layer.ffn_norm, NULL, LLM_NORM_RMS, il);
+ cb(cur, "ffn_norm", il);
+
+ {
+ ggml_tensor * gate = build_lora_mm(layer.ffn_gate, cur, layer.ffn_gate_s);
+ ggml_tensor * up = build_lora_mm(layer.ffn_up, cur, layer.ffn_up_s);
+ cur = hparams.swiglu_clamp[il] > 0.0f
+ ? ggml_swiglu_clamp(ctx0, gate, up, hparams.swiglu_clamp[il])
+ : ggml_swiglu_split(ctx0, gate, up);
+ cb(cur, "ffn_swiglu", il);
+ cur = build_lora_mm(layer.ffn_down, cur, layer.ffn_down_s);
+ cb(cur, "ffn_out", il);
+ }
+
+ cur = build_norm(cur, layer.ffn_post_norm, NULL, LLM_NORM_RMS, il);
+ cb(cur, "ffn_post_norm", il);
+
+ cur = ggml_add(ctx0, cur, ffn_inp);
+ cur = build_cvec(cur, il);
+ cb(cur, "l_out", il);
+
+ inpL = cur;
+ }
+
+ cur = build_norm(inpL, 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;
+}
diff --git a/tests/fusion/MTL.csv b/tests/fusion/MTL.csv
index cae85fc21..46aec1076 100644
--- a/tests/fusion/MTL.csv
+++ b/tests/fusion/MTL.csv
@@ -318,6 +318,8 @@ qwen4exp ,0 ,any ,SOFT_MAX+ARGSORT+GET_ROWS+SUM_ROWS+CLAMP+DIV,
qwen4exp ,0 ,any ,SSM_CONV+UNARY , 1
refact ,0 ,any ,RMS_NORM+MUL , 5
refact ,0 ,any ,RMS_NORM+MUL , 5
+ricursive ,0 ,any ,RMS_NORM+MUL , 9
+ricursive ,0 ,any ,RMS_NORM+MUL+ADD , 4
rnd1 ,0 ,any ,MUL+ADD , 4
rnd1 ,0 ,any ,RMS_NORM+MUL , 9
rnd1 ,0 ,any ,SOFT_MAX+ARGSORT+GET_ROWS+SUM_ROWS+CLAMP+DIV, 2
diff --git a/tests/test-llama-archs.cpp b/tests/test-llama-archs.cpp
index c42831d0e..39255c341 100644
--- a/tests/test-llama-archs.cpp
+++ b/tests/test-llama-archs.cpp
@@ -267,7 +267,7 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
n_layer = 6;
} else if (arch == LLM_ARCH_STEP35 || arch == LLM_ARCH_LAGUNA) {
n_embd = 160; // exercise per-head tensor split granularity with head size 80
- } else if (arch == LLM_ARCH_QWEN3 || arch == LLM_ARCH_MUSE_GLIMMER || arch == LLM_ARCH_AFMOE) {
+ } else if (arch == LLM_ARCH_QWEN3 || arch == LLM_ARCH_MUSE_GLIMMER || arch == LLM_ARCH_AFMOE || arch == LLM_ARCH_RICURSIVE) {
n_head = 4;
} else if (arch == LLM_ARCH_LIMITE) {
n_head = 4;
@@ -311,7 +311,7 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
if (arch == LLM_ARCH_QWEN3) {
n_head_kv = 1; // MQA coverage
} else if (arch == LLM_ARCH_MUSE_GLIMMER || arch == LLM_ARCH_MUSE_SPARK || arch == LLM_ARCH_AFMOE || arch == LLM_ARCH_MAION_CODER ||
- arch == LLM_ARCH_KOLIBRI1 || arch == LLM_ARCH_LIMITE || arch == LLM_ARCH_GEMMA4) {
+ arch == LLM_ARCH_KOLIBRI1 || arch == LLM_ARCH_LIMITE || arch == LLM_ARCH_GEMMA4 || arch == LLM_ARCH_RICURSIVE) {
n_head_kv = 2; // GQA coverage
}
if (arch == LLM_ARCH_BEAM) {
@@ -466,7 +466,7 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
ms.add_kv(LLM_KV_ATTENTION_SLIDING_WINDOW_PATTERN, pattern);
} else if (arch == LLM_ARCH_COHERE2MOE || arch == LLM_ARCH_MIMO2 || arch == LLM_ARCH_STEP35 || arch == LLM_ARCH_SPARK2_5 ||
arch == LLM_ARCH_MUSE_GLIMMER || arch == LLM_ARCH_MUSE_SPARK || arch == LLM_ARCH_GRANITE_SWA || arch == LLM_ARCH_DOTS3NOTE ||
- arch == LLM_ARCH_KOLIBRI1 || arch == LLM_ARCH_MAPLE || arch == LLM_ARCH_FLINT || arch == LLM_ARCH_BEAM) {
+ arch == LLM_ARCH_KOLIBRI1 || arch == LLM_ARCH_MAPLE || arch == LLM_ARCH_FLINT || arch == LLM_ARCH_BEAM || arch == LLM_ARCH_RICURSIVE) {
std::vector<uint32_t> pattern;
pattern.reserve(n_layer);
for (uint32_t il = 0; il < n_layer; il++) {
@@ -636,6 +636,17 @@ static gguf_context_ptr get_gguf_ctx(const llm_arch arch, const bool moe) {
ms.add_kv(LLM_KV_SWIGLU_CLAMP_EXP, 7.0f);
}
+ if (arch == LLM_ARCH_RICURSIVE) {
+ std::vector<float> swiglu_clamp;
+ swiglu_clamp.reserve(n_layer);
+ for (uint32_t il = 0; il < n_layer; il++) {
+ // the random weight scales keep the FFN activations small, a small limit still clamps so the roundtrip checks the per-layer values
+ swiglu_clamp.push_back(il % 3 == 1 ? 0.0f : 1e-3f);
+ }
+ ms.add_kv(LLM_KV_EMBEDDING_SCALE, 16.0f);
+ ms.add_kv(LLM_KV_SWIGLU_CLAMP, swiglu_clamp);
+ }
+
if (arch == LLM_ARCH_METIS16) {
ms.add_kv(LLM_KV_METIS_PASS_COUNT, uint32_t(2));
ms.add_kv(LLM_KV_HYPER_CONNECTION_COUNT, uint32_t(4));