vonjack commited on
Commit
57462bd
·
verified ·
1 Parent(s): a2971c1

Upload 12 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ gguf/HRM-Text-1B-Code-Tools-SFT-BF16.gguf filter=lfs diff=lfs merge=lfs -text
37
+ gguf/HRM-Text-1B-Code-Tools-SFT-Q8_0.gguf filter=lfs diff=lfs merge=lfs -text
LICENSE ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License 2.0
2
+
3
+ Copyright 2026
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
README.md CHANGED
@@ -1,3 +1,335 @@
1
  ---
 
 
2
  license: apache-2.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ language:
3
+ - en
4
  license: apache-2.0
5
+ library_name: transformers
6
+ pipeline_tag: text-generation
7
+ base_model: sapientinc/HRM-Text-1B
8
+ base_model_relation: finetune
9
+ datasets:
10
+ - pzarzycki/hrm-text-code-tools-sft
11
+ inference: false
12
+ tags:
13
+ - hrm
14
+ - hierarchical-reasoning
15
+ - prefix-lm
16
+ - code
17
+ - tool-use
18
+ - sft
19
+ - transformers
20
+ - safetensors
21
+ - gguf
22
+ - bf16
23
+ - q8_0
24
+ - llama.cpp
25
  ---
26
+
27
+ # HRM-Text-1B Code and Tool-Use SFT
28
+
29
+ This repository is a Transformers BF16 conversion of
30
+ [`pzarzycki/hrm-text-1b-code-tools-sft`](https://huggingface.co/pzarzycki/hrm-text-1b-code-tools-sft),
31
+ a full-parameter Stage A fine-tune of
32
+ [`sapientinc/HRM-Text-1B`](https://huggingface.co/sapientinc/HRM-Text-1B).
33
+ It also contains canonical BF16 and directly derived Q8_0 GGUF files.
34
+
35
+ The source checkpoint is a research pilot trained for code generation and a
36
+ fixed tool-transcript protocol. It has not undergone downstream benchmark or
37
+ production-agent evaluation.
38
+
39
+ ## Model details
40
+
41
+ | Field | Value |
42
+ | --- | --- |
43
+ | Architecture | HRM-Text PrefixLM |
44
+ | Parameters | 1,182,795,264 |
45
+ | Stored dtype | BF16 |
46
+ | Hidden size | 1,536 |
47
+ | H/L stack depth | 16 layers each |
48
+ | Recurrence | `H_cycles=2`, `L_cycles=3` |
49
+ | Attention | 12 heads, head dimension 128, gated MHA |
50
+ | Intermediate size | 4,096, SwiGLU |
51
+ | Context length | 4,096 tokens |
52
+ | Vocabulary | 65,536 |
53
+ | Position encoding | RoPE, theta 10,000 |
54
+ | Normalization | Parameterless pre-RMSNorm |
55
+ | Training stage | Stage A pilot, seed 17 |
56
+ | Training budget | 10,000,147 response tokens |
57
+ | Primary language | English |
58
+
59
+ Stage B was not trained or published as part of the source revision used here.
60
+
61
+ ## Files
62
+
63
+ | File | Format | Size | SHA256 |
64
+ | --- | --- | ---: | --- |
65
+ | `model.safetensors` | Transformers BF16 | 2,365,606,568 bytes | `2bc954894ab677dcdc66331863cf12aa4da8aa719fa56538a816632a3935546b` |
66
+ | `gguf/HRM-Text-1B-Code-Tools-SFT-BF16.gguf` | GGUF BF16 | 2,367,996,448 bytes | `4c59b7f0187315ba942088c07e7aeee38339bbfdcc1da05f4e83d3efd3c22cf7` |
67
+ | `gguf/HRM-Text-1B-Code-Tools-SFT-Q8_0.gguf` | GGUF Q8_0 | 1,259,127,360 bytes | `b0faf21c398ca4935d458d60decf6d3e60e9bc5d8dbfb1bf92d370b37bc9252e` |
68
+ | `gguf/runtime/llama.cpp-hrm_text.patch` | llama.cpp runtime patch | - | See compatibility section |
69
+
70
+ BF16 is the canonical storage format. Q8_0 was quantized directly from the
71
+ BF16 GGUF. No F16 derivative is provided because converting BF16 to F16 would
72
+ change 17,119 finite stored values and underflow 87 values to zero.
73
+
74
+ ## Requirements
75
+
76
+ Use `transformers>=5.9.0`, which includes native `hrm_text` model support.
77
+ The conversion and validation environment used Transformers 5.16.1 and
78
+ PyTorch 2.13.0.
79
+
80
+ ```bash
81
+ pip install --upgrade "transformers>=5.9.0" torch
82
+ ```
83
+
84
+ Hosted inference is disabled in the model-card metadata because generic text
85
+ generation endpoints do not provide the required PrefixLM `token_type_ids`.
86
+
87
+ ## Transformers usage
88
+
89
+ The included Jinja template must be applied. It serializes the learned
90
+ `direct` condition and the SFT transcript markup; this is not a Qwen/ChatML
91
+ prompt despite using a Qwen-compatible tokenizer.
92
+
93
+ ```python
94
+ import torch
95
+ from transformers import AutoModelForCausalLM, AutoTokenizer
96
+
97
+ model_id = "YOUR_NAMESPACE/HRM-Text-1B-Code-Tools-SFT"
98
+ device = torch.device(
99
+ "cuda" if torch.cuda.is_available()
100
+ else "mps" if torch.backends.mps.is_available()
101
+ else "cpu"
102
+ )
103
+
104
+ tokenizer = AutoTokenizer.from_pretrained(model_id)
105
+ model = AutoModelForCausalLM.from_pretrained(
106
+ model_id,
107
+ dtype=torch.bfloat16,
108
+ ).to(device).eval()
109
+
110
+ messages = [
111
+ {
112
+ "role": "user",
113
+ "content": "Write a Python function that returns the larger of two integers.",
114
+ }
115
+ ]
116
+ inputs = tokenizer.apply_chat_template(
117
+ messages,
118
+ tokenize=True,
119
+ add_generation_prompt=True,
120
+ return_tensors="pt",
121
+ return_dict=True,
122
+ ).to(device)
123
+
124
+ # HRM-Text was trained with a bidirectional prompt prefix.
125
+ inputs["token_type_ids"] = torch.ones_like(inputs["input_ids"])
126
+
127
+ with torch.inference_mode():
128
+ output_ids = model.generate(
129
+ **inputs,
130
+ max_new_tokens=256,
131
+ do_sample=False,
132
+ )
133
+
134
+ new_ids = output_ids[0, inputs["input_ids"].shape[1]:]
135
+ print(tokenizer.decode(new_ids, skip_special_tokens=False))
136
+ ```
137
+
138
+ The rendered prompt starts with the following exact envelope:
139
+
140
+ ```text
141
+ <|im_start|><|object_ref_start|><user>
142
+ Write a Python function that returns the larger of two integers.
143
+ </user>
144
+ <assistant>
145
+ <|im_end|>
146
+ ```
147
+
148
+ Do not omit `token_type_ids` when using Transformers. A value of 1 marks a
149
+ prompt position as part of the bidirectional prefix block. Omitting it falls
150
+ back to pure-causal attention and does not match the training-time objective.
151
+
152
+ ### Tool schemas
153
+
154
+ Pass OpenAI-style function schemas through the `tools` argument. The template
155
+ places them inside the learned `<tools>...</tools>` transcript markup.
156
+
157
+ ```python
158
+ tools = [
159
+ {
160
+ "type": "function",
161
+ "function": {
162
+ "name": "read_file",
163
+ "description": "Read a UTF-8 file relative to the task root.",
164
+ "parameters": {
165
+ "type": "object",
166
+ "properties": {"path": {"type": "string"}},
167
+ "required": ["path"],
168
+ "additionalProperties": False,
169
+ },
170
+ },
171
+ }
172
+ ]
173
+
174
+ prompt = tokenizer.apply_chat_template(
175
+ [{"role": "user", "content": "Read README.md and summarize it."}],
176
+ tools=tools,
177
+ tokenize=False,
178
+ add_generation_prompt=True,
179
+ )
180
+ ```
181
+
182
+ `<user>`, `<assistant>`, `<tools>`, `<tool_call>`, and `<tool_result>` are
183
+ ordinary learned text markup, not pretrained HRM control tokens. The model
184
+ does not execute tools, validate arguments, or sandbox generated code. A
185
+ `system` message is intentionally serialized with the same `<user>` markup;
186
+ there is no separately trained system role.
187
+
188
+ ## GGUF compatibility
189
+
190
+ The GGUF files use `general.architecture = hrm_text` and embed the exact Jinja
191
+ template under `tokenizer.chat_template`. Standard unpatched llama.cpp,
192
+ Ollama, LM Studio, and llama-cpp-python builds do not support this custom
193
+ runtime graph at the time of this release.
194
+
195
+ Apply the included patch to this exact llama.cpp commit:
196
+
197
+ ```text
198
+ 6a257d44633d4a752183ed778b88d2924d0a6b9d
199
+ ```
200
+
201
+ ```bash
202
+ git clone https://github.com/ggml-org/llama.cpp
203
+ cd llama.cpp
204
+ git checkout 6a257d44633d4a752183ed778b88d2924d0a6b9d
205
+ git apply /path/to/model/gguf/runtime/llama.cpp-hrm_text.patch
206
+ cmake -B build -DGGML_METAL=ON -DGGML_NATIVE=OFF -DLLAMA_BUILD_UI=OFF
207
+ cmake --build build --config Release --target llama-cli llama-server llama-quantize -j
208
+ ```
209
+
210
+ Ninja is optional. The documented CMake flow works with the default Unix
211
+ Makefiles generator, and Metal support is independent of the generator.
212
+
213
+ Start the server with Jinja explicitly enabled. PrefixLM prefill must process
214
+ the complete prompt in one physical batch, so set `--batch-size` and
215
+ `--ubatch-size` to at least the maximum prompt length you intend to use. The
216
+ example below supports prompts up to 512 tokens. Use `-ngl all` for Metal or
217
+ `-ngl 0` for CPU-only inference.
218
+
219
+ ```bash
220
+ ./build/bin/llama-server \
221
+ -m /path/to/model/gguf/HRM-Text-1B-Code-Tools-SFT-Q8_0.gguf \
222
+ --alias HRM-Text-1B-Code-Tools-SFT \
223
+ --jinja --ctx-size 512 --batch-size 512 --ubatch-size 512 \
224
+ --cache-ram 0 --parallel 1 \
225
+ -ngl all --host 127.0.0.1 --port 8080
226
+ ```
227
+
228
+ Requests to the OpenAI-compatible chat endpoint apply the embedded template:
229
+
230
+ ```bash
231
+ curl http://127.0.0.1:8080/v1/chat/completions \
232
+ -H "Content-Type: application/json" \
233
+ -d '{
234
+ "model": "HRM-Text-1B-Code-Tools-SFT",
235
+ "messages": [{"role": "user", "content": "Write a Python max function."}],
236
+ "temperature": 0,
237
+ "max_tokens": 128,
238
+ "cache_prompt": false
239
+ }'
240
+ ```
241
+
242
+ The patch maps `hrm_text.prefix_lm=true` to llama.cpp's non-causal attention
243
+ mask. The complete initial prompt is therefore one bidirectional prefix block.
244
+ Autoregressive token-by-token decoding remains causal in effect because the KV
245
+ cache contains no future generated positions.
246
+
247
+ This is deliberately narrower than arbitrary Transformers `token_type_ids`.
248
+ Do not split one prefix across multiple physical batches, reuse a KV cache from
249
+ a shorter prompt, or enable speculative multi-token decoding. Prompt-cache
250
+ reuse is disabled in the command and request above. For the full 4,096-token
251
+ context, set `--ctx-size`, `--batch-size`, and `--ubatch-size` to 4096 if the
252
+ available memory permits it.
253
+
254
+ ## Conversion validation
255
+
256
+ | Check | Result |
257
+ | --- | --- |
258
+ | Source Keras H5 to Transformers BF16 values | 1,182,795,264 checked, 0 bit mismatches |
259
+ | Tensor mapping | 259 Keras tensors mapped to 131 fused Safetensors entries |
260
+ | Keras vs Transformers FP32, pure causal | Maximum absolute logit difference `7.2718e-06` |
261
+ | Keras vs Transformers FP32, direct PrefixLM | Maximum absolute logit difference `2.0981e-05` |
262
+ | Author conversion tolerance | Passed at `atol=2e-4`, `rtol=2e-4` |
263
+ | BF16 MPS top-1 | All tested positions matched |
264
+ | BF16 MPS final-position top-10 | 10/10 overlap |
265
+ | BF16 MPS two-step greedy generation | Matched for causal and PrefixLM cases |
266
+ | Chat template | Plain and tool-schema cases passed |
267
+ | GGUF structure | 259 tensors and embedded Jinja verified for BF16 and Q8_0 |
268
+ | BF16 GGUF, Metal | 129/129 layers on MTL0; both two-token continuations matched Transformers PrefixLM baseline |
269
+ | BF16 GGUF, CPU | 0/129 layers offloaded; both two-token continuations matched |
270
+ | Q8_0 GGUF, Metal | 129/129 layers on MTL0; both two-token continuations matched |
271
+ | llama.cpp PrefixLM mask | `prefix_lm=true`, `causal_attn=0`, complete-prompt prefill, prompt cache disabled |
272
+ | llama.cpp Jinja | `/apply-template` strings and `/tokenize` IDs matched for plain and tool-schema prompts |
273
+ | llama.cpp token ranking | Top-1 matched at all 12 checked steps; top-10 overlap was 9/10 or 10/10 |
274
+ | OpenAI-compatible chat route | `/v1/chat/completions` passed with embedded Jinja enabled for all three runtime targets |
275
+
276
+ BF16 logits are not bit-identical across Keras and Transformers because their
277
+ RMSNorm, softmax, and backend arithmetic paths differ. Stored weights are
278
+ bit-identical after the audited tensor mapping, FP32 outputs pass the source
279
+ author's tolerance, and the tested BF16 token rankings and greedy outputs
280
+ match.
281
+
282
+ The llama.cpp comparisons use the same rendered token IDs and PrefixLM mask on
283
+ both sides: every initial prompt token is bidirectional and generated tokens
284
+ are causal. The validated two-step continuations were `[26763, 2336]` for the
285
+ plain prompt and `[58, 19975]` for the tool-schema prompt on BF16 CPU, BF16
286
+ Metal, and Q8_0 Metal.
287
+
288
+ ## Training provenance
289
+
290
+ | Item | Value |
291
+ | --- | --- |
292
+ | SFT source repository | `pzarzycki/hrm-text-1b-code-tools-sft` |
293
+ | SFT source revision | `ab083a772a29d4999251a437fe4e8b6007f828c3` |
294
+ | Source Keras H5 SHA256 | `8ddb10cea110edff99380b360ec569e5dbde0d4a233d1c058b0e292700a42b84` |
295
+ | Base model | `sapientinc/HRM-Text-1B` |
296
+ | Training dataset | `pzarzycki/hrm-text-code-tools-sft`, canonical v2 Stage A |
297
+ | Underlying data source | `nvidia/OpenCodeInstruct` revision `8f3ba5bafe4d6e8db46082cf7ae6741bc370604d` |
298
+ | KerasHub reference implementation | `pzarzycki/keras-hub` revision `8e9207acfae1833c25ba6813932b4234b6b84bf9` |
299
+
300
+ The Stage A selection contains 38,248 rows from the sealed training split.
301
+ Training used full-parameter BF16 optimization with a 4,096-token context cap.
302
+ See the source model card for the full optimizer setup and telemetry.
303
+
304
+ ## Intended use and limitations
305
+
306
+ This checkpoint is intended for research on HRM-Text code adaptation,
307
+ tool-call transcript generation, conversion fidelity, and local inference.
308
+
309
+ - No downstream coding or agent benchmark has been reported for this pilot.
310
+ - Training loss is not evidence of production coding-agent performance.
311
+ - Generated code and tool calls may be incorrect, unsafe, or fabricated.
312
+ - Tool execution, argument validation, permissions, and sandboxing must be
313
+ implemented by the host application.
314
+ - The model is predominantly English and is limited to 4,096 tokens.
315
+ - Evaluate task quality and safety independently before deployment.
316
+
317
+ ## License and citation
318
+
319
+ The model is released under the Apache License 2.0. The Stage A dataset traces
320
+ to CC-BY-4.0 data; consult the linked dataset card for its attribution and
321
+ usage terms.
322
+
323
+ Please cite the base HRM-Text work:
324
+
325
+ ```bibtex
326
+ @misc{wang2026hrmtextefficientpretrainingscaling,
327
+ title={HRM-Text: Efficient Pretraining Beyond Scaling},
328
+ author={Guan Wang and Changling Liu and Chenyu Wang and Cai Zhou and Yuhao Sun and Yifei Wu and Shuai Zhen and Luca Scimeca and Yasin Abbasi Yadkori},
329
+ year={2026},
330
+ eprint={2605.20613},
331
+ archivePrefix={arXiv},
332
+ primaryClass={cs.CL},
333
+ url={https://arxiv.org/abs/2605.20613}
334
+ }
335
+ ```
chat_template.jinja ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{- '<|im_start|><|object_ref_start|>' -}}
2
+ {%- if tools -%}
3
+ {{- '<tools>' -}}
4
+ {{- tools | tojson -}}
5
+ {{- '</tools>\n' -}}
6
+ {%- endif -%}
7
+ {%- for message in messages -%}
8
+ {%- if message['role'] == 'system' -%}
9
+ {{- '<user>\n' + message['content'] + '\n</user>\n' -}}
10
+ {%- elif message['role'] == 'user' -%}
11
+ {{- '<user>\n' + message['content'] + '\n</user>\n' -}}
12
+ {%- elif message['role'] == 'assistant' -%}
13
+ {{- '<assistant>\n' -}}
14
+ {%- if message.get('content') -%}
15
+ {{- message['content'] -}}
16
+ {%- endif -%}
17
+ {%- for tool_call in message.get('tool_calls', []) -%}
18
+ {{- '<tool_call>' -}}
19
+ {{- {'arguments': tool_call['function']['arguments'], 'name': tool_call['function']['name']} | tojson -}}
20
+ {{- '</tool_call>' -}}
21
+ {%- endfor -%}
22
+ {{- '\n' -}}
23
+ {%- elif message['role'] == 'tool' -%}
24
+ {{- '<tool_result>' + message['content'] + '</tool_result>\n' -}}
25
+ {%- else -%}
26
+ {{- raise_exception('Unsupported message role: ' + message['role']) -}}
27
+ {%- endif -%}
28
+ {%- endfor -%}
29
+ {%- if add_generation_prompt -%}
30
+ {{- '<assistant>\n' -}}
31
+ {%- endif -%}
32
+ {{- '<|im_end|>' -}}
config.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "hrm_text",
3
+ "architectures": [
4
+ "HrmTextForCausalLM"
5
+ ],
6
+ "vocab_size": 65536,
7
+ "hidden_size": 1536,
8
+ "intermediate_size": 4096,
9
+ "num_hidden_layers": 16,
10
+ "num_attention_heads": 12,
11
+ "num_key_value_heads": 12,
12
+ "head_dim": 128,
13
+ "H_cycles": 2,
14
+ "L_cycles": 3,
15
+ "L_bp_cycles": [
16
+ 0,
17
+ 3
18
+ ],
19
+ "max_position_embeddings": 4096,
20
+ "rms_norm_eps": 1e-06,
21
+ "rope_theta": 10000.0,
22
+ "tie_word_embeddings": false,
23
+ "initializer_range": 0.025515518153991442,
24
+ "embedding_scale": 39.191835884530846,
25
+ "prefix_lm": true,
26
+ "pad_token_id": 5,
27
+ "bos_token_id": 6,
28
+ "eos_token_id": 11,
29
+ "dtype": "bfloat16",
30
+ "transformers_version": "5.16.1"
31
+ }
generation_config.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "bos_token_id": 6,
4
+ "eos_token_id": 11,
5
+ "pad_token_id": 5,
6
+ "transformers_version": "5.16.1"
7
+ }
gguf/HRM-Text-1B-Code-Tools-SFT-BF16.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4c59b7f0187315ba942088c07e7aeee38339bbfdcc1da05f4e83d3efd3c22cf7
3
+ size 2367996448
gguf/HRM-Text-1B-Code-Tools-SFT-Q8_0.gguf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b0faf21c398ca4935d458d60decf6d3e60e9bc5d8dbfb1bf92d370b37bc9252e
3
+ size 1259127360
gguf/runtime/llama.cpp-hrm_text.patch ADDED
@@ -0,0 +1,572 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/conversion/__init__.py b/conversion/__init__.py
2
+ index 2c38123df..ecf1be2db 100644
3
+ --- a/conversion/__init__.py
4
+ +++ b/conversion/__init__.py
5
+ @@ -95,6 +95,7 @@ TEXT_MODEL_MAP: dict[str, str] = {
6
+ "HunYuanDenseV1ForCausalLM": "hunyuan",
7
+ "HunYuanMoEV1ForCausalLM": "hunyuan",
8
+ "HunYuanVLForConditionalGeneration": "hunyuan",
9
+ + "HrmTextForCausalLM": "hrm_text",
10
+ "IQuestCoderForCausalLM": "llama",
11
+ "InternLM2ForCausalLM": "internlm",
12
+ "InternLM3ForCausalLM": "internlm",
13
+ diff --git a/conversion/hrm_text.py b/conversion/hrm_text.py
14
+ new file mode 100644
15
+ index 000000000..1f29ab55e
16
+ --- /dev/null
17
+ +++ b/conversion/hrm_text.py
18
+ @@ -0,0 +1,120 @@
19
+ +from __future__ import annotations
20
+ +
21
+ +import re
22
+ +import json
23
+ +
24
+ +from typing import Iterable, TYPE_CHECKING
25
+ +
26
+ +import torch
27
+ +
28
+ +if TYPE_CHECKING:
29
+ + from torch import Tensor
30
+ +
31
+ +from .base import ModelBase, TextModel, gguf, logger
32
+ +
33
+ +
34
+ +@ModelBase.register("HrmTextForCausalLM")
35
+ +class HrmTextModel(TextModel):
36
+ + model_arch = gguf.MODEL_ARCH.HRM_TEXT
37
+ +
38
+ + def __init__(self, *args, **kwargs):
39
+ + super().__init__(*args, **kwargs)
40
+ +
41
+ + with open(self.dir_model / "config.json", "r", encoding="utf-8") as f:
42
+ + self.raw_hparams = json.load(f)
43
+ +
44
+ + self.layers_per_stack = self.raw_hparams["num_hidden_layers"]
45
+ + self.h_cycles = self.raw_hparams["H_cycles"]
46
+ + self.l_cycles = self.raw_hparams["L_cycles"]
47
+ + self.physical_block_count = self.layers_per_stack * 2
48
+ + self.cache_block_count = self.layers_per_stack * self.h_cycles * (self.l_cycles + 1)
49
+ +
50
+ + # GGUF tensors store one physical L stack followed by one physical H stack.
51
+ + # The runtime expands these 32 physical layers across 128 KV-cache slots.
52
+ + self.block_count = self.physical_block_count
53
+ + self.tensor_map = gguf.get_tensor_name_map(self.model_arch, self.block_count)
54
+ +
55
+ + def set_vocab(self):
56
+ + # HRM-Text ships a Qwen2-style tokenizer.json. Keep it as a plain tokenizer;
57
+ + # do not add a chat template for validation GGUFs.
58
+ + self._set_vocab_gpt2()
59
+ +
60
+ + def get_vocab_base_pre(self, tokenizer) -> str:
61
+ + del tokenizer
62
+ + return "qwen2"
63
+ +
64
+ + def set_gguf_parameters(self):
65
+ + hp = self.raw_hparams
66
+ + head_dim = hp["head_dim"]
67
+ +
68
+ + self.gguf_writer.add_context_length(hp["max_position_embeddings"])
69
+ + self.gguf_writer.add_embedding_length(hp["hidden_size"])
70
+ + self.gguf_writer.add_block_count(self.cache_block_count)
71
+ + self.gguf_writer.add_feed_forward_length(hp["intermediate_size"])
72
+ + self.gguf_writer.add_head_count(hp["num_attention_heads"])
73
+ + self.gguf_writer.add_head_count_kv(hp["num_key_value_heads"])
74
+ + self.gguf_writer.add_key_length(head_dim)
75
+ + self.gguf_writer.add_value_length(head_dim)
76
+ + self.gguf_writer.add_rope_dimension_count(head_dim)
77
+ + self.gguf_writer.add_rope_freq_base(hp.get("rope_theta", 10000.0))
78
+ + self.gguf_writer.add_layer_norm_rms_eps(hp["rms_norm_eps"])
79
+ + self.gguf_writer.add_embedding_scale(hp["embedding_scale"])
80
+ +
81
+ + arch = self.gguf_writer.arch
82
+ + self.gguf_writer.add_uint32(gguf.Keys.LLM.HRM_LAYERS_PER_STACK.format(arch=arch), self.layers_per_stack)
83
+ + self.gguf_writer.add_uint32(gguf.Keys.LLM.HRM_H_CYCLES.format(arch=arch), self.h_cycles)
84
+ + self.gguf_writer.add_uint32(gguf.Keys.LLM.HRM_L_CYCLES.format(arch=arch), self.l_cycles)
85
+ + self.gguf_writer.add_bool(gguf.Keys.LLM.HRM_PREFIX_LM.format(arch=arch), bool(hp.get("prefix_lm", False)))
86
+ +
87
+ + def _format(self, key: gguf.MODEL_TENSOR, bid: int | None = None, suffix: str = ".weight") -> str:
88
+ + return self.format_tensor_name(key, bid=bid, suffix=suffix)
89
+ +
90
+ + def modify_tensors(self, data_torch: Tensor, name: str, bid: int | None) -> Iterable[tuple[str, Tensor]]:
91
+ + if name == "model.embed_tokens.weight":
92
+ + yield self._format(gguf.MODEL_TENSOR.TOKEN_EMBD), data_torch
93
+ + return
94
+ +
95
+ + if name == "lm_head.weight":
96
+ + yield self._format(gguf.MODEL_TENSOR.OUTPUT), data_torch
97
+ + return
98
+ +
99
+ + if name == "model.z_L_init":
100
+ + yield self._format(gguf.MODEL_TENSOR.HRM_Z_L_INIT, suffix=""), data_torch
101
+ + return
102
+ +
103
+ + match = re.fullmatch(r"model\.([LH])_module\.layers\.(\d+)\.(.+)", name)
104
+ + if match is None:
105
+ + raise ValueError(f"Can not map tensor {name!r}")
106
+ +
107
+ + stack, layer_s, tensor_name = match.groups()
108
+ + layer_idx = int(layer_s)
109
+ + if layer_idx >= self.layers_per_stack:
110
+ + raise ValueError(f"Layer index {layer_idx} outside HRM stack size {self.layers_per_stack}")
111
+ +
112
+ + physical_bid = layer_idx + (self.layers_per_stack if stack == "H" else 0)
113
+ +
114
+ + if tensor_name == "attn.gqkv_proj.weight":
115
+ + gate, q, k, v = torch.chunk(data_torch, 4, dim=0)
116
+ + logger.debug("Split %s as gate, q, k, v", name)
117
+ + yield self._format(gguf.MODEL_TENSOR.ATTN_GATE, physical_bid), gate.contiguous()
118
+ + yield self._format(gguf.MODEL_TENSOR.ATTN_Q, physical_bid), q.contiguous()
119
+ + yield self._format(gguf.MODEL_TENSOR.ATTN_K, physical_bid), k.contiguous()
120
+ + yield self._format(gguf.MODEL_TENSOR.ATTN_V, physical_bid), v.contiguous()
121
+ + return
122
+ +
123
+ + if tensor_name == "attn.o_proj.weight":
124
+ + yield self._format(gguf.MODEL_TENSOR.ATTN_OUT, physical_bid), data_torch
125
+ + return
126
+ +
127
+ + if tensor_name == "mlp.gate_up_proj.weight":
128
+ + gate, up = torch.chunk(data_torch, 2, dim=0)
129
+ + logger.debug("Split %s as gate, up", name)
130
+ + yield self._format(gguf.MODEL_TENSOR.FFN_GATE, physical_bid), gate.contiguous()
131
+ + yield self._format(gguf.MODEL_TENSOR.FFN_UP, physical_bid), up.contiguous()
132
+ + return
133
+ +
134
+ + if tensor_name == "mlp.down_proj.weight":
135
+ + yield self._format(gguf.MODEL_TENSOR.FFN_DOWN, physical_bid), data_torch
136
+ + return
137
+ +
138
+ + raise ValueError(f"Can not map tensor {name!r}")
139
+ diff --git a/gguf-py/gguf/constants.py b/gguf-py/gguf/constants.py
140
+ index 7fdcf03d7..b84cc8827 100644
141
+ --- a/gguf-py/gguf/constants.py
142
+ +++ b/gguf-py/gguf/constants.py
143
+ @@ -144,6 +144,10 @@ class Keys:
144
+ TOKEN_SHIFT_COUNT = "{arch}.token_shift_count"
145
+ INTERLEAVE_MOE_LAYER_STEP = "{arch}.interleave_moe_layer_step"
146
+ FULL_ATTENTION_INTERVAL = "{arch}.full_attention_interval"
147
+ + HRM_LAYERS_PER_STACK = "{arch}.layers_per_stack"
148
+ + HRM_H_CYCLES = "{arch}.h_cycles"
149
+ + HRM_L_CYCLES = "{arch}.l_cycles"
150
+ + HRM_PREFIX_LM = "{arch}.prefix_lm"
151
+ ACTIVATION_SPARSITY_SCALE = "{arch}.activation_sparsity_scale"
152
+ ALTUP_ACTIVE_IDX = "{arch}.altup.active_idx"
153
+ ALTUP_NUM_INPUTS = "{arch}.altup.num_inputs"
154
+ @@ -410,6 +414,7 @@ class MODEL_ARCH(IntEnum):
155
+ QWEN3 = auto()
156
+ QWEN3MOE = auto()
157
+ QWEN3NEXT = auto()
158
+ + HRM_TEXT = auto()
159
+ QWEN3VL = auto()
160
+ QWEN3VLMOE = auto()
161
+ QWEN35 = auto()
162
+ @@ -527,6 +532,7 @@ class MODEL_TENSOR(IntEnum):
163
+ TOKEN_TYPES = auto()
164
+ POS_EMBD = auto()
165
+ OUTPUT = auto()
166
+ + HRM_Z_L_INIT = auto()
167
+ DENSE_2_OUT = auto() # embeddinggemma 2_Dense
168
+ DENSE_3_OUT = auto() # embeddinggemma 3_Dense
169
+ OUTPUT_NORM = auto()
170
+ @@ -925,6 +931,7 @@ MODEL_ARCH_NAMES: dict[MODEL_ARCH, str] = {
171
+ MODEL_ARCH.QWEN3: "qwen3",
172
+ MODEL_ARCH.QWEN3MOE: "qwen3moe",
173
+ MODEL_ARCH.QWEN3NEXT: "qwen3next",
174
+ + MODEL_ARCH.HRM_TEXT: "hrm_text",
175
+ MODEL_ARCH.QWEN3VL: "qwen3vl",
176
+ MODEL_ARCH.QWEN3VLMOE: "qwen3vlmoe",
177
+ MODEL_ARCH.QWEN35: "qwen35",
178
+ @@ -1042,6 +1049,7 @@ TENSOR_NAMES: dict[MODEL_TENSOR, str] = {
179
+ MODEL_TENSOR.POS_EMBD: "position_embd",
180
+ MODEL_TENSOR.OUTPUT_NORM: "output_norm",
181
+ MODEL_TENSOR.OUTPUT: "output",
182
+ + MODEL_TENSOR.HRM_Z_L_INIT: "hrm.z_l_init",
183
+ MODEL_TENSOR.DENSE_2_OUT: "dense_2", # embeddinggemma 2_Dense
184
+ MODEL_TENSOR.DENSE_3_OUT: "dense_3", # embeddinggemma 2_Dense
185
+ MODEL_TENSOR.ROPE_FREQS: "rope_freqs",
186
+ @@ -2057,6 +2065,19 @@ MODEL_TENSORS: dict[MODEL_ARCH, list[MODEL_TENSOR]] = {
187
+ MODEL_TENSOR.SSM_BETA_ALPHA,
188
+ MODEL_TENSOR.SSM_OUT
189
+ ],
190
+ + MODEL_ARCH.HRM_TEXT: [
191
+ + MODEL_TENSOR.TOKEN_EMBD,
192
+ + MODEL_TENSOR.OUTPUT,
193
+ + MODEL_TENSOR.HRM_Z_L_INIT,
194
+ + MODEL_TENSOR.ATTN_Q,
195
+ + MODEL_TENSOR.ATTN_K,
196
+ + MODEL_TENSOR.ATTN_V,
197
+ + MODEL_TENSOR.ATTN_GATE,
198
+ + MODEL_TENSOR.ATTN_OUT,
199
+ + MODEL_TENSOR.FFN_GATE,
200
+ + MODEL_TENSOR.FFN_DOWN,
201
+ + MODEL_TENSOR.FFN_UP,
202
+ + ],
203
+ MODEL_ARCH.QWEN3VL: [
204
+ MODEL_TENSOR.TOKEN_EMBD,
205
+ MODEL_TENSOR.OUTPUT_NORM,
206
+ diff --git a/src/llama-arch.cpp b/src/llama-arch.cpp
207
+ index c9eead18a..5b8ee3781 100644
208
+ --- a/src/llama-arch.cpp
209
+ +++ b/src/llama-arch.cpp
210
+ @@ -37,6 +37,7 @@ static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = {
211
+ { LLM_ARCH_QWEN3, "qwen3" },
212
+ { LLM_ARCH_QWEN3MOE, "qwen3moe" },
213
+ { LLM_ARCH_QWEN3NEXT, "qwen3next" },
214
+ + { LLM_ARCH_HRM_TEXT, "hrm_text" },
215
+ { LLM_ARCH_QWEN3VL, "qwen3vl" },
216
+ { LLM_ARCH_QWEN3VLMOE, "qwen3vlmoe" },
217
+ { LLM_ARCH_QWEN35, "qwen35" },
218
+ @@ -209,6 +210,10 @@ static const std::map<llm_kv, const char *> LLM_KV_NAMES = {
219
+ { LLM_KV_TOKEN_SHIFT_COUNT, "%s.token_shift_count" },
220
+ { LLM_KV_INTERLEAVE_MOE_LAYER_STEP, "%s.interleave_moe_layer_step" },
221
+ { LLM_KV_FULL_ATTENTION_INTERVAL, "%s.full_attention_interval" },
222
+ + { LLM_KV_HRM_LAYERS_PER_STACK, "%s.layers_per_stack" },
223
+ + { LLM_KV_HRM_H_CYCLES, "%s.h_cycles" },
224
+ + { LLM_KV_HRM_L_CYCLES, "%s.l_cycles" },
225
+ + { LLM_KV_HRM_PREFIX_LM, "%s.prefix_lm" },
226
+
227
+ { LLM_KV_ATTENTION_HEAD_COUNT, "%s.attention.head_count" },
228
+ { LLM_KV_ATTENTION_HEAD_COUNT_KV, "%s.attention.head_count_kv" },
229
+ @@ -346,6 +351,7 @@ static const std::map<llm_tensor, const char *> LLM_TENSOR_NAMES = {
230
+ { LLM_TENSOR_OUTPUT_NORM, "output_norm" },
231
+ { LLM_TENSOR_OUTPUT_NORM_LFM2, "token_embd_norm" }, // fix for wrong tensor name
232
+ { LLM_TENSOR_OUTPUT, "output" },
233
+ + { LLM_TENSOR_HRM_Z_L_INIT, "hrm.z_l_init" },
234
+ { LLM_TENSOR_ROPE_FREQS, "rope_freqs" },
235
+ { LLM_TENSOR_ATTN_NORM, "blk.%d.attn_norm" },
236
+ { LLM_TENSOR_ATTN_Q, "blk.%d.attn_q" },
237
+ @@ -565,6 +571,7 @@ static const std::map<llm_tensor, llm_tensor_info> LLM_TENSOR_INFOS = {
238
+ {LLM_TENSOR_POS_EMBD, {LLM_TENSOR_LAYER_INPUT, GGML_OP_GET_ROWS}},
239
+ {LLM_TENSOR_TOKEN_TYPES, {LLM_TENSOR_LAYER_INPUT, GGML_OP_GET_ROWS}},
240
+ {LLM_TENSOR_TOKEN_EMBD_NORM, {LLM_TENSOR_LAYER_REPEATING, GGML_OP_MUL}}, // do the norms on the first layer (not the input layer)
241
+ + {LLM_TENSOR_HRM_Z_L_INIT, {LLM_TENSOR_LAYER_INPUT, GGML_OP_MUL}},
242
+ {LLM_TENSOR_OUTPUT, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL_MAT}},
243
+ {LLM_TENSOR_CLS, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL_MAT}},
244
+ {LLM_TENSOR_CLS_OUT, {LLM_TENSOR_LAYER_OUTPUT, GGML_OP_MUL_MAT}},
245
+ diff --git a/src/llama-arch.h b/src/llama-arch.h
246
+ index 89cf16cc3..fa04b684b 100644
247
+ --- a/src/llama-arch.h
248
+ +++ b/src/llama-arch.h
249
+ @@ -41,6 +41,7 @@ enum llm_arch {
250
+ LLM_ARCH_QWEN3,
251
+ LLM_ARCH_QWEN3MOE,
252
+ LLM_ARCH_QWEN3NEXT,
253
+ + LLM_ARCH_HRM_TEXT,
254
+ LLM_ARCH_QWEN3VL,
255
+ LLM_ARCH_QWEN3VLMOE,
256
+ LLM_ARCH_QWEN35,
257
+ @@ -213,6 +214,10 @@ enum llm_kv {
258
+ LLM_KV_TOKEN_SHIFT_COUNT,
259
+ LLM_KV_INTERLEAVE_MOE_LAYER_STEP,
260
+ LLM_KV_FULL_ATTENTION_INTERVAL,
261
+ + LLM_KV_HRM_LAYERS_PER_STACK,
262
+ + LLM_KV_HRM_H_CYCLES,
263
+ + LLM_KV_HRM_L_CYCLES,
264
+ + LLM_KV_HRM_PREFIX_LM,
265
+
266
+ LLM_KV_ATTENTION_HEAD_COUNT,
267
+ LLM_KV_ATTENTION_HEAD_COUNT_KV,
268
+ @@ -354,6 +359,7 @@ enum llm_tensor {
269
+ LLM_TENSOR_DENSE_2_OUT,
270
+ LLM_TENSOR_DENSE_3_OUT,
271
+ LLM_TENSOR_OUTPUT,
272
+ + LLM_TENSOR_HRM_Z_L_INIT,
273
+ LLM_TENSOR_OUTPUT_NORM,
274
+ LLM_TENSOR_OUTPUT_NORM_LFM2, // fix for wrong tensor name
275
+ LLM_TENSOR_ROPE_FREQS,
276
+ diff --git a/src/llama-context.cpp b/src/llama-context.cpp
277
+ index ad36c0666..fa80f4260 100644
278
+ --- a/src/llama-context.cpp
279
+ +++ b/src/llama-context.cpp
280
+ @@ -2208,6 +2208,9 @@ uint32_t llama_context::graph_max_nodes(uint32_t n_tokens) const {
281
+ if (model.arch == LLM_ARCH_QWEN3NEXT || model.arch == LLM_ARCH_KIMI_LINEAR || model.arch == LLM_ARCH_QWEN35 || model.arch == LLM_ARCH_QWEN35MOE) {
282
+ return std::max<uint32_t>(n_tokens * 40, 32u * model.n_tensors());
283
+ }
284
+ + if (model.arch == LLM_ARCH_HRM_TEXT) {
285
+ + return std::max<uint32_t>(n_tokens * 80, 64u * model.n_tensors());
286
+ + }
287
+ uint32_t res = std::max<uint32_t>(1024u, 8u*model.n_tensors());
288
+ for (const auto & lora : model.loras) {
289
+ res += lora->get_n_nodes();
290
+ diff --git a/src/llama-hparams.h b/src/llama-hparams.h
291
+ index e2d051edc..812598f69 100644
292
+ --- a/src/llama-hparams.h
293
+ +++ b/src/llama-hparams.h
294
+ @@ -164,6 +164,12 @@ struct llama_hparams {
295
+ float f_embedding_scale = 0.0f;
296
+ float f_attention_scale = 0.0f;
297
+
298
+ + // HRM-Text recurrence metadata. n_layer remains the expanded KV-cache slot count.
299
+ + uint32_t n_hrm_layer_per_stack = 0;
300
+ + uint32_t n_hrm_h_cycles = 0;
301
+ + uint32_t n_hrm_l_cycles = 0;
302
+ + bool hrm_prefix_lm = false;
303
+ +
304
+ // grok-2
305
+ float f_attn_out_scale = 0.0f;
306
+ uint32_t attn_temp_length = 0;
307
+ diff --git a/src/llama-model-saver.cpp b/src/llama-model-saver.cpp
308
+ index 528e4c9c0..8a6e009c6 100644
309
+ --- a/src/llama-model-saver.cpp
310
+ +++ b/src/llama-model-saver.cpp
311
+ @@ -245,6 +245,10 @@ void llama_model_saver::add_kv_from_model() {
312
+ add_kv(LLM_KV_TOKEN_SHIFT_COUNT, hparams.token_shift_count);
313
+ add_kv(LLM_KV_INTERLEAVE_MOE_LAYER_STEP, hparams.n_moe_layer_step);
314
+ // add_kv(LLM_KV_FULL_ATTENTION_INTERVAL, ???);
315
+ + add_kv(LLM_KV_HRM_LAYERS_PER_STACK, hparams.n_hrm_layer_per_stack);
316
+ + add_kv(LLM_KV_HRM_H_CYCLES, hparams.n_hrm_h_cycles);
317
+ + add_kv(LLM_KV_HRM_L_CYCLES, hparams.n_hrm_l_cycles);
318
+ + add_kv(LLM_KV_HRM_PREFIX_LM, hparams.hrm_prefix_lm);
319
+
320
+ add_kv(LLM_KV_ATTENTION_HEAD_COUNT, hparams.n_head_arr, true);
321
+ add_kv(LLM_KV_ATTENTION_HEAD_COUNT_KV, hparams.n_head_kv_arr, true);
322
+ diff --git a/src/llama-model.cpp b/src/llama-model.cpp
323
+ index 8bf20a716..a3cc996aa 100644
324
+ --- a/src/llama-model.cpp
325
+ +++ b/src/llama-model.cpp
326
+ @@ -96,6 +96,8 @@ static llama_model * llama_model_mapping(llm_arch arch, const llama_model_params
327
+ return new llama_model_qwen2moe(params);
328
+ case LLM_ARCH_QWEN3:
329
+ return new llama_model_qwen3(params);
330
+ + case LLM_ARCH_HRM_TEXT:
331
+ + return new llama_model_hrm_text(params);
332
+ case LLM_ARCH_QWEN3MOE:
333
+ return new llama_model_qwen3moe(params);
334
+ case LLM_ARCH_QWEN3VL:
335
+ @@ -2339,6 +2341,7 @@ llama_rope_type llama_model_rope_type(const llama_model * model) {
336
+ case LLM_ARCH_PANGU_EMBED:
337
+ case LLM_ARCH_AFMOE:
338
+ case LLM_ARCH_QWEN3NEXT:
339
+ + case LLM_ARCH_HRM_TEXT:
340
+ case LLM_ARCH_MIMO2:
341
+ case LLM_ARCH_STEP35:
342
+ return LLAMA_ROPE_TYPE_NEOX;
343
+ diff --git a/src/models/hrm-text.cpp b/src/models/hrm-text.cpp
344
+ new file mode 100644
345
+ index 000000000..e0a3e9f59
346
+ --- /dev/null
347
+ +++ b/src/models/hrm-text.cpp
348
+ @@ -0,0 +1,184 @@
349
+ +#include "models.h"
350
+ +
351
+ +#include <cmath>
352
+ +#include <vector>
353
+ +
354
+ +void llama_model_hrm_text::load_arch_hparams(llama_model_loader & ml) {
355
+ + ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps);
356
+ + ml.get_key(LLM_KV_EMBEDDING_SCALE, hparams.f_embedding_scale);
357
+ + ml.get_key(LLM_KV_HRM_LAYERS_PER_STACK, hparams.n_hrm_layer_per_stack);
358
+ + ml.get_key(LLM_KV_HRM_H_CYCLES, hparams.n_hrm_h_cycles);
359
+ + ml.get_key(LLM_KV_HRM_L_CYCLES, hparams.n_hrm_l_cycles);
360
+ + ml.get_key(LLM_KV_HRM_PREFIX_LM, hparams.hrm_prefix_lm, false);
361
+ + hparams.causal_attn = !hparams.hrm_prefix_lm;
362
+ +
363
+ + switch (hparams.n_embd) {
364
+ + case 1536: type = LLM_TYPE_1B; break;
365
+ + default: type = LLM_TYPE_UNKNOWN;
366
+ + }
367
+ +}
368
+ +
369
+ +void llama_model_hrm_text::load_arch_tensors(llama_model_loader &) {
370
+ + LLAMA_LOAD_LOCALS;
371
+ +
372
+ + const int64_t n_stack = hparams.n_hrm_layer_per_stack;
373
+ + const int64_t n_cycle_slots = n_stack * (hparams.n_hrm_l_cycles + 1);
374
+ +
375
+ + tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0);
376
+ + output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), {n_embd, n_vocab}, 0);
377
+ +
378
+ + hrm_z_l_init = create_tensor(tn(LLM_TENSOR_HRM_Z_L_INIT), {n_embd}, 0);
379
+ +
380
+ + std::vector<bool> loaded_physical(2 * n_stack, false);
381
+ +
382
+ + for (int il = 0; il < n_layer; ++il) {
383
+ + auto & layer = layers[il];
384
+ +
385
+ + const int64_t layer_in_stack = il % n_stack;
386
+ + const int64_t phase = (il % n_cycle_slots) / n_stack;
387
+ + const bool is_h_stack = phase == int64_t(hparams.n_hrm_l_cycles);
388
+ + const int physical_bid = int((is_h_stack ? n_stack : 0) + layer_in_stack);
389
+ +
390
+ + const int flags = loaded_physical[physical_bid] ? TENSOR_DUPLICATED : 0;
391
+ + loaded_physical[physical_bid] = true;
392
+ +
393
+ + create_tensor_qkv(layer, physical_bid,
394
+ + n_embd,
395
+ + n_embd_head_k * n_head,
396
+ + n_embd_k_gqa,
397
+ + n_embd_v_gqa,
398
+ + flags);
399
+ +
400
+ + layer.wqkv_gate = create_tensor(tn(LLM_TENSOR_ATTN_GATE, "weight", physical_bid), {n_embd, n_embd_head_k * n_head}, flags);
401
+ + layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", physical_bid), {n_embd_head_k * n_head, n_embd}, flags);
402
+ +
403
+ + layer.ffn_gate = create_tensor(tn(LLM_TENSOR_FFN_GATE, "weight", physical_bid), {n_embd, n_ff}, flags);
404
+ + layer.ffn_down = create_tensor(tn(LLM_TENSOR_FFN_DOWN, "weight", physical_bid), {n_ff, n_embd}, flags);
405
+ + layer.ffn_up = create_tensor(tn(LLM_TENSOR_FFN_UP, "weight", physical_bid), {n_embd, n_ff}, flags);
406
+ + }
407
+ +}
408
+ +
409
+ +std::unique_ptr<llm_graph_context> llama_model_hrm_text::build_arch_graph(const llm_graph_params & params) const {
410
+ + return std::make_unique<graph>(*this, params);
411
+ +}
412
+ +
413
+ +llama_model_hrm_text::graph::graph(const llama_model & model_, const llm_graph_params & params) : llm_graph_context(params) {
414
+ + const auto & model = static_cast<const llama_model_hrm_text &>(model_);
415
+ +
416
+ + GGML_ASSERT(model.tok_embd != nullptr);
417
+ + GGML_ASSERT(model.output != nullptr);
418
+ + GGML_ASSERT(model.hrm_z_l_init != nullptr);
419
+ +
420
+ + const int64_t n_embd_head = hparams.n_embd_head_v();
421
+ + GGML_ASSERT(n_embd_head == hparams.n_embd_head_k());
422
+ + GGML_ASSERT(n_embd_head == n_rot);
423
+ +
424
+ + const int64_t n_stack = hparams.n_hrm_layer_per_stack;
425
+ + const int64_t h_cycles = hparams.n_hrm_h_cycles;
426
+ + const int64_t l_cycles = hparams.n_hrm_l_cycles;
427
+ +
428
+ + ggml_tensor * inp_pos = build_inp_pos();
429
+ + auto * inp_attn = build_attn_inp_kv();
430
+ + ggml_tensor * inp_out_ids = build_inp_out_ids();
431
+ +
432
+ + ggml_tensor * hidden_high = build_inp_embd(model.tok_embd);
433
+ + ggml_tensor * hidden_low = ggml_repeat(ctx0, model.hrm_z_l_init, hidden_high);
434
+ + cb(hidden_low, "hrm_z_l_init", -1);
435
+ +
436
+ + const float kq_scale = 1.0f / std::sqrt(float(n_embd_head));
437
+ +
438
+ + auto build_stack = [&](ggml_tensor * stack_inp, int slot_offset) -> ggml_tensor * {
439
+ + ggml_tensor * stack_cur = stack_inp;
440
+ +
441
+ + for (int layer_idx = 0; layer_idx < n_stack; ++layer_idx) {
442
+ + const int il = slot_offset + layer_idx;
443
+ + const auto & layer = model.layers[il];
444
+ +
445
+ + ggml_tensor * inpSA = stack_cur;
446
+ + ggml_tensor * cur = build_norm(stack_cur, nullptr, nullptr, LLM_NORM_RMS, il);
447
+ + cb(cur, "attn_norm", il);
448
+ +
449
+ + {
450
+ + ggml_tensor * attn_inp = cur;
451
+ + auto [Qcur, Kcur, Vcur] = build_qkv(layer, cur, n_embd_head, n_head, n_head_kv, il);
452
+ +
453
+ + ggml_tensor * gate = build_lora_mm(layer.wqkv_gate, attn_inp, layer.wqkv_gate_s);
454
+ + cb(gate, "attn_gate_proj", il);
455
+ +
456
+ + Qcur = ggml_rope_ext(
457
+ + ctx0, Qcur, inp_pos, nullptr,
458
+ + n_rot, rope_type, n_ctx_orig, freq_base, freq_scale,
459
+ + ext_factor, attn_factor, beta_fast, beta_slow);
460
+ + cb(Qcur, "Qcur_rope", il);
461
+ +
462
+ + Kcur = ggml_rope_ext(
463
+ + ctx0, Kcur, inp_pos, nullptr,
464
+ + n_rot, rope_type, n_ctx_orig, freq_base, freq_scale,
465
+ + ext_factor, attn_factor, beta_fast, beta_slow);
466
+ + cb(Kcur, "Kcur_rope", il);
467
+ +
468
+ + cur = build_attn(inp_attn,
469
+ + nullptr, nullptr, nullptr,
470
+ + Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, kq_scale, il);
471
+ + cb(cur, "attn_out", il);
472
+ +
473
+ + gate = ggml_sigmoid(ctx0, gate);
474
+ + cb(gate, "attn_gate_sig", il);
475
+ +
476
+ + cur = ggml_mul(ctx0, cur, gate);
477
+ + cb(cur, "attn_gated", il);
478
+ +
479
+ + cur = build_lora_mm(layer.wo, cur, layer.wo_s);
480
+ + cb(cur, "attn_o_proj", il);
481
+ + }
482
+ +
483
+ + ggml_tensor * ffn_inp = ggml_add(ctx0, cur, inpSA);
484
+ + cb(ffn_inp, "ffn_inp", il);
485
+ +
486
+ + cur = build_norm(ffn_inp, nullptr, nullptr, LLM_NORM_RMS, il);
487
+ + cb(cur, "ffn_norm", il);
488
+ +
489
+ + cur = build_ffn(cur,
490
+ + layer.ffn_up, nullptr, layer.ffn_up_s,
491
+ + layer.ffn_gate, nullptr, layer.ffn_gate_s,
492
+ + layer.ffn_down, nullptr, layer.ffn_down_s,
493
+ + nullptr,
494
+ + LLM_FFN_SILU, LLM_FFN_PAR, il);
495
+ + cb(cur, "ffn_out", il);
496
+ +
497
+ + cur = ggml_add(ctx0, cur, ffn_inp);
498
+ + cur = build_cvec(cur, il);
499
+ + cb(cur, "hrm_layer_out", il);
500
+ +
501
+ + stack_cur = cur;
502
+ + }
503
+ +
504
+ + stack_cur = build_norm(stack_cur, nullptr, nullptr, LLM_NORM_RMS, slot_offset);
505
+ + cb(stack_cur, "stack_final_norm", slot_offset);
506
+ + return stack_cur;
507
+ + };
508
+ +
509
+ + for (int h = 0; h < h_cycles; ++h) {
510
+ + for (int l = 0; l < l_cycles; ++l) {
511
+ + const int slot_offset = int((h * (l_cycles + 1) + l) * n_stack);
512
+ + hidden_low = build_stack(ggml_add(ctx0, hidden_low, hidden_high), slot_offset);
513
+ + }
514
+ +
515
+ + const int slot_offset = int((h * (l_cycles + 1) + l_cycles) * n_stack);
516
+ + hidden_high = build_stack(ggml_add(ctx0, hidden_high, hidden_low), slot_offset);
517
+ + }
518
+ +
519
+ + ggml_tensor * cur = hidden_high;
520
+ +
521
+ + if (inp_out_ids) {
522
+ + cur = ggml_get_rows(ctx0, cur, inp_out_ids);
523
+ + }
524
+ +
525
+ + res->t_embd = cur;
526
+ +
527
+ + cur = build_lora_mm(model.output, cur, model.output_s);
528
+ + cb(cur, "result_output", -1);
529
+ +
530
+ + res->t_logits = cur;
531
+ + ggml_build_forward_expand(gf, cur);
532
+ +}
533
+ diff --git a/src/models/models.h b/src/models/models.h
534
+ index 7e551eb96..7da6b7f7f 100644
535
+ --- a/src/models/models.h
536
+ +++ b/src/models/models.h
537
+ @@ -515,6 +515,20 @@ struct llama_model_qwen3 : public llama_model_base {
538
+ std::unique_ptr<llm_graph_context> build_arch_graph(const llm_graph_params & params) const override;
539
+ };
540
+
541
+ +struct llama_model_hrm_text : public llama_model_base {
542
+ + llama_model_hrm_text(const struct llama_model_params & params) : llama_model_base(params) {}
543
+ + void load_arch_hparams(llama_model_loader & ml) override;
544
+ + void load_arch_tensors(llama_model_loader & ml) override;
545
+ +
546
+ + ggml_tensor * hrm_z_l_init = nullptr;
547
+ +
548
+ + struct graph : public llm_graph_context {
549
+ + graph(const llama_model & model, const llm_graph_params & params);
550
+ + };
551
+ +
552
+ + std::unique_ptr<llm_graph_context> build_arch_graph(const llm_graph_params & params) const override;
553
+ +};
554
+ +
555
+
556
+ struct llama_model_qwen3moe : public llama_model_base {
557
+ llama_model_qwen3moe(const struct llama_model_params & params) : llama_model_base(params) {}
558
+ diff --git a/tools/server/server-http.h b/tools/server/server-http.h
559
+ --- a/tools/server/server-http.h
560
+ +++ b/tools/server/server-http.h
561
+ @@ -2,9 +2,11 @@
562
+
563
+ #include <atomic>
564
+ #include <functional>
565
+ #include <map>
566
+ +#include <memory>
567
+ #include <string>
568
+ #include <thread>
569
+ +#include <unordered_map>
570
+ #include <vector>
571
+ #include <cstdint>
572
+
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2bc954894ab677dcdc66331863cf12aa4da8aa719fa56538a816632a3935546b
3
+ size 2365606568
special_tokens_map.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token": "<|im_start|>",
3
+ "eos_token": "<|box_end|>",
4
+ "pad_token": "<|endoftext|>",
5
+ "unk_token": "<|endoftext|>"
6
+ }
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": null,
3
+ "backend": "tokenizers",
4
+ "bos_token": "<|im_start|>",
5
+ "eos_token": "<|box_end|>",
6
+ "is_local": true,
7
+ "local_files_only": false,
8
+ "model_max_length": 4096,
9
+ "pad_token": "<|endoftext|>",
10
+ "tokenizer_class": "Qwen2Tokenizer",
11
+ "unk_token": "<|endoftext|>",
12
+ "chat_template": "{{- '<|im_start|><|object_ref_start|>' -}}\n{%- if tools -%}\n{{- '<tools>' -}}\n{{- tools | tojson -}}\n{{- '</tools>\\n' -}}\n{%- endif -%}\n{%- for message in messages -%}\n{%- if message['role'] == 'system' -%}\n{{- '<user>\\n' + message['content'] + '\\n</user>\\n' -}}\n{%- elif message['role'] == 'user' -%}\n{{- '<user>\\n' + message['content'] + '\\n</user>\\n' -}}\n{%- elif message['role'] == 'assistant' -%}\n{{- '<assistant>\\n' -}}\n{%- if message.get('content') -%}\n{{- message['content'] -}}\n{%- endif -%}\n{%- for tool_call in message.get('tool_calls', []) -%}\n{{- '<tool_call>' -}}\n{{- {'arguments': tool_call['function']['arguments'], 'name': tool_call['function']['name']} | tojson -}}\n{{- '</tool_call>' -}}\n{%- endfor -%}\n{{- '\\n' -}}\n{%- elif message['role'] == 'tool' -%}\n{{- '<tool_result>' + message['content'] + '</tool_result>\\n' -}}\n{%- else -%}\n{{- raise_exception('Unsupported message role: ' + message['role']) -}}\n{%- endif -%}\n{%- endfor -%}\n{%- if add_generation_prompt -%}\n{{- '<assistant>\\n' -}}\n{%- endif -%}\n{{- '<|im_end|>' -}}",
13
+ "clean_up_tokenization_spaces": false
14
+ }