mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-20 01:31:31 +02:00
model : add support for HrmTextForCausalLM (DFM Mimir 1B) (#27625)
* model : add support for HrmTextForCausalLM (DFM Mimir 1B) HRM-Text runs two transformer stacks (low, high) in an alternating cycle over the same token stream. The low-cycle state z_l starts from a learned [n_embd] tensor and is broadcast over positions. - conversion: new writer for the fused gqkv projection (order gate,q,k,v) remapped to llama.cpp q/k/v plus a separate sigmoid gate tensor - loader: block_count = lps * h_cycles * (l_cycles + 1) cache slots aliasing 2*lps physical blocks via struct copies - graph: looped build with sigmoid-gated attention, SwiGLU FFN and parameterless RMS norms; learned embedding_scale applied in build_inp_embd - saver: pointer-deduplicated layer loop (looped archs alias tensors) - tests: hrm_text fixture (lps 1, h 2, l 3) in test-llama-archs Limitations: causal attention only - the upstream prefix-LM mode is not implemented (the prefix_lm GGUF key round-trips unused). The KV cache holds one entry per pass: 128 layers for Mimir 1B, i.e. 4x a same-width 32-layer model - about 3072 MiB at ctx 4096 in F16 (halves with q8_0 KV + FA). Every token runs all 128 block passes, so decode cost is roughly 4x a dense model of equal width (2.65 t/s BF16, 8-thread desktop CPU). Verified against the HF reference: identical argmax at 334/334 positions across 20 prompts (BF16 GGUF vs FP32 golden). q8_0 requant: 95.8% top-1, all remaining misses inside the HF top-5 (accumulated error over 128 sequential blocks). AI usage disclosure: YES Used GLM-5.3 for the majority of code AI-generated under my direction, all gates verified locally. All in all I could say that I have written less than 20% of the code and most of the heavy lifting has been done by the model. As such, this should be considered experimental. * Update conversion/hrm_text.py Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co> * Update src/llama-arch.cpp Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co> * convert : add gguf_writer methods for hrm_text metadata replace raw add_uint32/add_bool calls with dedicated GGUFWriter methods, following the add_embedding_scale pattern Assisted-by: GLM-5.3 * convert : map regular hrm_text tensors via tensor_mapping delegate unfused checkpoints to the base tensor mapping; training-style attn. names are renamed to self_attn. so the patterns match Assisted-by: GLM-5.3 * model : format hrm-text build_* calls as in other models one argument group per line, matching sibling model files Assisted-by: GLM-5.3 * llama : move hrm z_l_init table entries out of the nemotron group place the name and tensor-info entries with the other global input tensors Assisted-by: GLM-5.3 * convert : slim down hrm_text comments Assisted-by: GLM-5.3 * convert : build hrm_text block tensor names from the {bid} template The tensor map holds concrete per-block names, so format the template with the computed layer index before handing it to super(). * llama : name hrm metadata keys in their own hrm. namespace The four keys are arch-independent, unlike the arch-substituted Keys.LLM entries, so group them under Keys.HRM (like Keys.Split) and rename the llm_kv entries to LLM_KV_HRM_*. Only our own GGUFs carry the old hrm_text.* keys; they are regenerated. * Update src/llama-model-saver.cpp Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co> * llama : keep hrm metadata keys arch-substituted Per review: the GGUF keys stay "{arch}.h_cycles" style, so the Python members drop the LLM_KV_HRM_ prefix and keep arch templates; C++ keeps the LLM_KV_HRM_* enums. GGUF output is unchanged - existing files and HF uploads stay valid. * Update gguf-py/gguf/constants.py Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co> * Update src/llama-arch.cpp Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co> * Update src/llama-arch.cpp Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co> * convert : rename hrm writer methods to add_hrm_* Generic names like add_h_cycles/add_prefix_lm are too broad on the shared GGUFWriter; prefix them with hrm_ like the metadata keys. * model : fix meta-split lookup for archs with aliased cache slots Cache tensors of archs that alias physical blocks across looped slots (hrm_text, nanbeige with num_loops > 1) can reference block indices without weight tensor names. Take the output projection from the layer array instead of asserting; all other lookups are unchanged. * model : replicate hrm_text tensors on meta devices instead of splitting The aliased cache slots rotate split states differently from their physical weights, so the meta-split execution invariants (set_rows requires the cache state to match the token indices) cannot hold for any device count. Replicate all hrm_text tensors on every meta device instead; single-device and non-meta paths are unchanged. Assisted-by: Claude Sonnet --------- Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co>
This commit is contained in:
@@ -123,6 +123,7 @@ TEXT_MODEL_MAP: dict[str, str] = {
|
||||
"HunYuanDenseV1ForCausalLM": "hunyuan",
|
||||
"HunYuanMoEV1ForCausalLM": "hunyuan",
|
||||
"HunYuanVLForConditionalGeneration": "hunyuan",
|
||||
"HrmTextForCausalLM": "hrm_text",
|
||||
"HYV3ForCausalLM": "hunyuan",
|
||||
"HYV4ForCausalLM": "hy_v4",
|
||||
"IQuestCoderForCausalLM": "llama",
|
||||
|
||||
@@ -1633,6 +1633,9 @@ class TextModel(ModelBase):
|
||||
if chkhsh == "9e454714343b69b99b71795c1d27a68c2a1d15dab111f4d353109f966af29da7":
|
||||
# ref: https://huggingface.co/LiquidAI/LFM2.5-8B-A1B
|
||||
res = "lfm2"
|
||||
if chkhsh == "846deafc5b0fa786186fa4ae6c7b49903cf2f1d1895bdb80b9120d60be135252":
|
||||
# ref: https://huggingface.co/danish-foundation-models/DFM-Mimir
|
||||
res = "gemma4"
|
||||
if chkhsh == "0a766d034107bc736a3f2dc4968fd62e54a3570f1454443e0c5a4cc6bd7941ed":
|
||||
# ref: https://huggingface.co/XHToken/Spark-X2.5-1.7B
|
||||
res = "spark2_5"
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from typing import Iterable, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch import Tensor
|
||||
|
||||
from .base import ModelBase, TextModel, gguf
|
||||
|
||||
|
||||
@ModelBase.register("HrmTextForCausalLM")
|
||||
@ModelBase.example("danish-foundation-models/DFM-Mimir")
|
||||
class HrmTextModel(TextModel):
|
||||
model_arch = gguf.MODEL_ARCH.HRM_TEXT
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# training-style configs store the per-stack count in num_hidden_layers,
|
||||
# transformers-style configs keep it in num_layers_per_stack
|
||||
self.layers_per_stack = self.hparams.get("num_layers_per_stack") or self.hparams["num_hidden_layers"]
|
||||
self.h_cycles = self.hparams["H_cycles"]
|
||||
self.l_cycles = self.hparams["L_cycles"]
|
||||
|
||||
# block_count is the expanded cache-slot count; the file only holds
|
||||
# 2 * layers_per_stack physical blocks
|
||||
self.block_count = self.layers_per_stack * self.h_cycles * (self.l_cycles + 1)
|
||||
self.tensor_map = gguf.get_tensor_name_map(self.model_arch, 2 * self.layers_per_stack)
|
||||
|
||||
def set_vocab(self):
|
||||
self._set_vocab_gpt2()
|
||||
|
||||
def set_gguf_parameters(self):
|
||||
super().set_gguf_parameters()
|
||||
|
||||
head_dim = self.hparams.get("head_dim") or self.hparams["hidden_size"] // self.hparams["num_attention_heads"]
|
||||
self.gguf_writer.add_rope_dimension_count(head_dim)
|
||||
self.gguf_writer.add_embedding_scale(self.hparams["embedding_scale"])
|
||||
self.gguf_writer.add_hrm_layers_per_stack(self.layers_per_stack)
|
||||
self.gguf_writer.add_hrm_h_cycles(self.h_cycles)
|
||||
self.gguf_writer.add_hrm_l_cycles(self.l_cycles)
|
||||
self.gguf_writer.add_hrm_prefix_lm(bool(self.hparams.get("prefix_lm", False)))
|
||||
|
||||
def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
|
||||
if name == "model.embed_tokens.weight":
|
||||
yield self.format_tensor_name(gguf.MODEL_TENSOR.TOKEN_EMBD), data_torch
|
||||
return
|
||||
if name == "lm_head.weight":
|
||||
yield self.format_tensor_name(gguf.MODEL_TENSOR.OUTPUT), data_torch
|
||||
return
|
||||
if name == "model.z_L_init":
|
||||
yield self.format_tensor_name(gguf.MODEL_TENSOR.HRM_Z_L_INIT, suffix=""), data_torch
|
||||
return
|
||||
|
||||
match = re.fullmatch(r"model\.([LH])_module\.layers\.(\d+)\.(.+)", name)
|
||||
if match is None:
|
||||
raise ValueError(f"can not map tensor: {name}")
|
||||
|
||||
stack, layer_s, tensor_name = match.groups()
|
||||
# the L stack occupies blocks [0, layers_per_stack), the H stack follows it
|
||||
layer_idx = int(layer_s) + (self.layers_per_stack if stack == "H" else 0)
|
||||
|
||||
if tensor_name == "attn.gqkv_proj.weight":
|
||||
gate, q, k, v = data_torch.chunk(4, dim=0)
|
||||
yield self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_GATE, layer_idx), gate.contiguous()
|
||||
yield self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_Q, layer_idx), q.contiguous()
|
||||
yield self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_K, layer_idx), k.contiguous()
|
||||
yield self.format_tensor_name(gguf.MODEL_TENSOR.ATTN_V, layer_idx), v.contiguous()
|
||||
elif tensor_name == "mlp.gate_up_proj.weight":
|
||||
gate, up = data_torch.chunk(2, dim=0)
|
||||
yield self.format_tensor_name(gguf.MODEL_TENSOR.FFN_GATE, layer_idx), gate.contiguous()
|
||||
yield self.format_tensor_name(gguf.MODEL_TENSOR.FFN_UP, layer_idx), up.contiguous()
|
||||
else:
|
||||
if tensor_name.startswith("attn."):
|
||||
tensor_name = "self_attn." + tensor_name[len("attn."):]
|
||||
tensor_name = "model.layers.{bid}." + tensor_name
|
||||
yield from super().modify_tensors(data_torch, tensor_name.format(bid=layer_idx), layer_idx)
|
||||
Reference in New Issue
Block a user