File size: 34,471 Bytes
4294de3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5dba7e1
4294de3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5dba7e1
4294de3
 
 
 
 
 
 
 
 
5dba7e1
 
 
 
 
 
 
 
 
 
 
 
4294de3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5dba7e1
 
4294de3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
#!/usr/bin/env python3
"""Render both HF cards from summary.json + judgments.json. Every number comes from the summary;
absent => 'β€”'. Judgment prose lives in nex_judge.py and only ever references computed values.
usage: nex_render.py <summary.json> <outdir> <judgments.json> <SHA256SUMS_std> <SHA256SUMS_imat> <staged_sizes.json>"""
import json, math, os, sys

S = json.load(open(sys.argv[1])); OUT = sys.argv[2]; os.makedirs(OUT, exist_ok=True)
JUDGE = json.load(open(sys.argv[3]))
SUMS = {}
for sf in sys.argv[4:6]:
    if os.path.exists(sf):
        for line in open(sf):
            h, fn = line.split(maxsplit=1); SUMS[fn.strip()] = h
SIZES = json.load(open(sys.argv[6])) if len(sys.argv) > 6 and os.path.exists(sys.argv[6]) else {}
N = S["model"]; STD, IMAT = S["model_repo_std"], S["model_repo_imat"]
T = S.get("tiers") or {}; B = S.get("binary") or {}; SRC = S.get("source") or {}; AR = S.get("arch") or {}
GiB, MiB = 1024 ** 3, 1024 ** 2
UB = S.get("n_ubatch") if S.get("n_ubatch") is not None else 1024
CTX, GEN, REPS = 65536, 256, 3
STD_TAGS, IMAT_TAGS = ("q106", "q102", "q103"), ("q106i", "q102i", "q103i")
NAMES = {"q106": "STRIX_LEAN", "q102": "COHERENT", "q103": "FAST",
         "q106i": "STRIX_LEAN", "q102i": "COHERENT", "q103i": "FAST"}


def g(v, fmt="{:.2f}"):
    return "β€”" if v is None else fmt.format(v)


def gib(b):
    return g(None if b is None else b / GiB, "{:.2f} GiB")


def pm(v, e, fmt="{:.4f}"):
    return "β€”" if v is None else (fmt.format(v) + ("" if e is None else " Β± " + fmt.format(e)))


def bench(label):
    return next((x for x in S.get("bench") or [] if x["label"] == label), None)


def tg(label):
    x = bench(label)
    return None if x is None else x.get("tg_median")


def pp(label):
    x = bench(label)
    return None if x is None else x.get("pp_median")


def gate(label):
    return next((x for x in S.get("gates") or [] if x.get("label") == label), None)


def J(k):
    return JUDGE.get(k, f"**[JUDGMENT PENDING: {k}]**")


def Jopt(k):
    return JUDGE.get(k, "")


def speed_label(tag, dev):
    return f"n-{tag}-{dev}"


def prompt_range(workload="code"):
    """Prompt tokens processed by the timed requests of one workload (each carries a unique nonce)."""
    lo, hi = [], []
    for b in S.get("bench") or []:
        if b.get("workload") != workload:
            continue
        a_ = b.get("prompt_n_min") if b.get("prompt_n_min") is not None else b.get("prompt_n")
        z_ = b.get("prompt_n_max") if b.get("prompt_n_max") is not None else b.get("prompt_n")
        if a_ is not None and z_ is not None:
            lo.append(a_); hi.append(z_)
    if not lo:
        return "β€”"
    return f"{min(lo):,}" if min(lo) == max(hi) else f"{min(lo):,}–{max(hi):,}"


def tier(tag):
    return T.get(tag) or {}


def tier_row(tag):
    t = tier(tag); ratio = t.get("ppl_ratio")
    r = [f"`{t.get('file') or 'β€”'}`", g(t.get("ftype"), "{}"), gib(t.get("size_bytes")), g(t.get("bpw")),
         pm(t.get("kld_mean"), t.get("kld_err")), g(t.get("same_top_p"), "{:.2f} %"),
         pm(t.get("ppl"), t.get("ppl_err")) + ("" if ratio is None else f" (Γ—{ratio:.4f})"),
         g(tg(speed_label(tag, "rocm"))), g(tg(speed_label(tag, "vk"))),
         g(pp(speed_label(tag, "rocm")), "{:.0f}")]
    return "| " + " | ".join(r) + " |"


PN_TXT = prompt_range("code")
PROTOCOL = (
    f"Ryzen AI Max+ 395 (MAX-1), ROCm 7.2.4, unpatched `llama-server` at `d3ca537` (see [Quick start](#quick-start)), "
    f"`-c {CTX}`, one request at a time (`--parallel 1`), greedy (`temp 0`, `top_k 1`), `ignore_eos` so every arm "
    f"generates exactly {GEN} tokens "
    f"after a code prompt of {PN_TXT} tokens (the first 30,000 characters of `convert_hf_to_gguf.py` plus an "
    f"instruction), a unique nonce per request and `cache_prompt: false` (`cache_n = 0` asserted on "
    f"every timed request), 1 warm-up then the median of {REPS}. Decode numbers are the server's own "
    f"`predicted_per_second`. Box iced: no other model loaded.")
TABLE_HEAD = ("| File | ftype | Size | BPW⁴ | KLD vs BF16 ↓² | Same top-1 ↑ | PPL (Γ— BF16) | TG ROCm0 | TG Vulkan0 | PP ROCm0 |\n"
              "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |")
TG_NOTE = f"TG = decode tokens/s after the {PN_TXT}-token code prompt, no draft head. PP = prefill tokens/s on ROCm0."


def quality_blurb():
    b = S.get("bf16") or {}
    ch, nc, st = b.get("chunks"), b.get("n_ctx"), b.get("scored_tokens")
    if ch is not None and nc is not None and st is not None:
        scored = (f"{ch} chunks Γ— {nc // 2 - 1:,} scored tokens each β€” the second half of every window, less its first token β€” = {st:,}")
    else:
        scored = "β€” chunks Γ— β€” scored tokens"
    return (f"Quality is graded against the **BF16 GGUF** (reference logits computed on the CPU) on a **held-out** corpus (wikitext-2 *test*, `-c 2048`, "
            f"{scored}), never on the imatrix calibration text. **KLD** is the per-token KL divergence of each "
            "quant's next-token distribution from BF16's on the same tokens β€” far more sensitive than perplexity.")


def bf16_row():
    b = S.get("bf16") or {}
    return (f"| *BF16 reference* | {g(b.get('ftype'), '{}')} | {gib(b.get('size_bytes'))}Β³ | {g(b.get('bpw_logged'), '{}')} | 0 | 100 % | "
            f"{pm(b.get('ppl_paired'), b.get('ppl_paired_err'))}ΒΉ | β€” | β€” | β€” |")


def footnotes(where="below"):
    b = S.get("bf16") or {}
    return (f"{TG_NOTE}\n"
            f"ΒΉ The BF16 PPL shown is the paired base every \"Γ—\" ratio is computed against (averaged over the same scored tokens "
            f"in the KL-divergence runs). The standalone BF16 run's own summary line reads {pm(b.get('ppl'), b.get('ppl_err'))}.\n"
            f"Β² Quality columns: see *Where the quality numbers come from* {where}.\n"
            f"Β³ BF16 conversion of the checkpoint; not published.\n"
            f"{bpw_note()}")


def bpw_note():
    b, m = S.get("bf16") or {}, S.get("mmproj") or {}
    n, v, p = b.get("elements"), m.get("elements"), SRC.get("params")
    s = f"⁴ BPW as printed by `llama-quantize`: bits per weight over the {g(n, '{:,}')} weights in each GGUF."
    if None not in (n, v, p) and n + v == p:
        s += (f" The {p:,}-parameter count above also includes the {v:,}-weight vision tower, which ships in the "
              f"projector file.")
    return s


YAML = """---
license: apache-2.0
base_model: nex-agi/Nex-N2.5-mini
base_model_relation: quantized
pipeline_tag: image-text-to-text
library_name: gguf
tags:
  - gguf
  - llama.cpp
  - rocm
  - amd
  - rocmfp4
  - rocmfpx
  - strix-halo
  - amd-strix-halo
  - gfx1151
  - ryzen-ai-max
  - ryzen-ai-max-395
  - radeon-8060s
  - moe
  - reasoning
  - multimodal
  - vision
  - nex
  - qwen3.5
  - quantized{extra}
---
"""


def cmake_block():
    commit = B.get("commit") or "d3ca537"
    return f"""```bash
git clone https://github.com/charlie12345/ROCmFPX.git && cd ROCmFPX
git checkout {commit}
HIPCXX="$(hipconfig -l)/clang" HIP_PATH="$(hipconfig -R)" \\
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \\
  -DGGML_HIP=ON -DGGML_VULKAN=ON -DGPU_TARGETS=gfx1151 \\
  -DGGML_HIP_GRAPHS=ON -DGGML_HIP_NO_VMM=ON -DLLAMA_CURL=OFF
cmake --build build --target llama-server -j
```"""


def serve_block(model_file):
    env = ("env LD_LIBRARY_PATH=$PWD/build/bin:/opt/rocm/lib HSA_OVERRIDE_GFX_VERSION=11.5.1 "
           "GGML_HIP_ENABLE_UNIFIED_MEMORY=1 \\\n")
    mm = f"  --mmproj ~/models/nex/mmproj-{N}-BF16.gguf \\\n"
    tpl = ("  --chat-template-file ~/models/nex/chat_template_enable_thinking.jinja --reasoning off \\\n")
    tail = f"  -ngl 999 -fa on -dio --jinja -fit off --parallel 1 -dev ROCm0 \\\n  -c {CTX} --host 127.0.0.1 --port 8080"
    head = f"build/bin/llama-server \\\n  -m ~/models/nex/{model_file} \\\n"
    cmd = f"```bash\n{env}{head}{mm}{tpl}{tail}\n```"
    w = Jopt("vision_quickstart_warning")
    return cmd + ("\n\n" + w if w else "")


def curl_block():
    return """```bash
curl http://127.0.0.1:8080/v1/chat/completions \\
  -H 'Content-Type: application/json' \\
  -d '{
    "messages": [{"role": "user", "content": "Hello"}],
    "temperature": 0.7,
    "top_p": 0.95,
    "top_k": 40,
    "chat_template_kwargs": {"enable_thinking": true}
  }'
```"""


def quick_start(model_file, repo):
    return f"""**1. Download**

```bash
hf download {repo} --local-dir ~/models/nex
```

**2. Build `llama-server`** β€” ROCmFPX at the measured commit (ROCm and Vulkan
prerequisites: the project's [build guide](https://github.com/charlie12345/ROCmFPX/blob/{B.get('commit') or 'd3ca537'}/docs/build.md)). No patch.

{cmake_block()}

(The CMake options of the measured build are listed in [Reproduction](#reproduction).)

`d3ca537` is also in the history of the official [ROCmFPX/ROCmFPX](https://github.com/ROCmFPX/ROCmFPX) repository.

**3. Serve**

{serve_block(model_file)}

(`LD_LIBRARY_PATH` avoids a soname clash on machines that also have a Vulkan-only llama.cpp build.) The exact measured
argv is in [Reproduction](#reproduction).

**4. Call** β€” upstream sampling. Thinking is off unless the request sets `"enable_thinking": true` (as here; drop that
line for a direct answer):

{curl_block()}

| Flag | Why |
| --- | --- |
| `--chat-template-file …/chat_template_enable_thinking.jinja` | The model's own template plus one line (see [Reasoning controls](#reasoning-controls)). Without it llama-server leaves the reasoning in `content` and thinking-on tool calls fail ([measured](#tool-calling)). |
| `--reasoning off` | Thinking stays off unless a request passes `"enable_thinking": true`. |
| `--jinja` | Already on by default in this build; keep it on β€” the reasoning controls (`chat_template_kwargs`) and tool calling rely on the Jinja chat template. |
| `-fit off` | Autofit reads `MemAvailable` on integrated GPUs and can silently shrink context or push tensors to CPU. |
| `-cram <MiB>` | Not set above (default 8 GiB of host RAM for saved prompts). Set it on a shared box β€” see [Known issues](#known-issues-and-limits). |
| `--mmproj` | Loads the {AR.get('vision_layers', 'β€”')}-layer vision tower. Drop the flag for text-only. |

Requires a llama.cpp build with ROCmFP4 / ROCmFPX tensor-type support; stock llama.cpp rejects these tensor types."""


def reasoning_block():
    tf = S.get("template_fix") or {}
    shim = (tf.get("shim") or "").rstrip("\n")
    shim_md = ("```jinja\n" + shim + "\n```") if shim else "**[JUDGMENT PENDING: template shim]**"
    return f"""## Reasoning controls

The model's own chat template switches thinking with `chat_template_kwargs.reasoning_effort` and ignores
`enable_thinking`:

| `reasoning_effort` | What the stock template emits |
| --- | --- |
| `"none"` | empty `<think>\\n\\n</think>` (no thinking) |
| `"high"` | opens `<think>\\n` (always think) |
| `"medium"`, unset, or anything else | opens `<think>` and lets the model decide (adaptive; upstream default is `"medium"`) |

llama-server decides how to split reasoning from the answer by rendering the template with `enable_thinking` on and
off. With this template both renders are the same, so it never extracts the reasoning ([measured](#tool-calling)).
`chat_template_enable_thinking.jinja` in this repo is the model's `chat_template.jinja` (sha256
`{tf.get('source_sha256') or 'β€”'}`) with one line added at the top (file sha256 `{tf.get('sha256') or 'β€”'}`):

{shim_md}

Serve it with `--chat-template-file` and `--reasoning off`.

{J('template_note')}

Upstream serving (SGLang) uses `--reasoning-parser qwen3 --tool-call-parser qwen3_coder`. Recommended sampling:
temperature 0.7, top_p 0.95, top_k 40.

Earlier assistant turns are re-rendered **with** their reasoning (contexts grow faster than with templates that drop
it). With thinking on and a small `max_tokens`, the whole budget can go to reasoning and `content` comes back empty β€”
raise `max_tokens` before concluding the model is broken.

Tool calls use the XML-style `<tool_call><function=…><parameter=…>` format, which llama.cpp parses natively
through the Jinja chat template (on by default)."""


def speed_table():
    rows = ["| File | Backend | Workload | Decode tok/s (min–max) | Prefill tok/s |",
            "| --- | --- | --- | ---: | ---: |"]
    for tag in STD_TAGS + IMAT_TAGS:
        t = tier(tag)
        fn = t.get("file") or "β€”"
        for dev, dn in (("rocm", "ROCm0"), ("vk", "Vulkan0")):
            b = bench(speed_label(tag, dev))
            if b is None or b.get("tg_median") is None:
                rows.append(f"| `{fn}` | {dn} | code | β€” | β€” |")
            else:
                rows.append(f"| `{fn}` | {dn} | code | {b['tg_median']:.2f} ({g(b.get('tg_min'))}–{g(b.get('tg_max'))}) | "
                            f"{g(b.get('pp_median'), '{:.0f}')} |")
    for lab, dn in (("n-q106-rocm-prose", "ROCm0"), ("n-q106-vk-prose", "Vulkan0")):
        b = bench(lab)
        fn = tier("q106").get("file") or "β€”"
        if b is None or b.get("tg_median") is None:
            rows.append(f"| `{fn}` | {dn} | prose | β€” | β€” |")
        else:
            rows.append(f"| `{fn}` | {dn} | prose | {b['tg_median']:.2f} ({g(b.get('tg_min'))}–{g(b.get('tg_max'))}) | "
                        f"{g(b.get('pp_median'), '{:.0f}')} |")
    return "\n".join(rows)


def cache_table():
    x = gate("n-c3-q106")
    rows = ["| server | second-request prompt tokens reused | processed | warm reply = cold reply |",
            "| --- | ---: | ---: | :---: |"]
    if not x:
        rows.append("| d3ca537, unpatched | β€” | β€” | β€” |")
        return "\n".join(rows)
    n, L, ident = x.get("n"), x.get("L"), x.get("identical")
    got = sorted({r_.get("warm_cache_n") for r_ in x.get("rows") or []}, key=lambda v: (v is None, v))
    if n is None or L is None or not got or None in got:
        rows.append("| d3ca537, unpatched | β€” | β€” | β€” |")
        return "\n".join(rows)
    if len(got) == 1:
        reused, proc = f"**{got[0]:,}** of {L:,} (all {n} pairs)", f"{L - got[0]:,}"
    else:
        reused, proc = f"{got[0]:,}–{got[-1]:,} of {L:,} (varies across {n} pairs)", f"{L - got[-1]:,}–{L - got[0]:,}"
    rows.append(f"| d3ca537, unpatched | {reused} | {proc} | {ident if ident is not None else 'β€”'}/{n} |")
    return "\n".join(rows)


def tools_block():
    t = gate("n-tools-q106")
    fx = [gate(l) for l in ("n-tools-q106-roff", "n-tools-q106-roff-r2", "n-tools-q106-roff-r3")]
    if not t and not any(fx):
        return "_Not measured._"
    names = ["multi-arg", "nested-object", "enum", "correct-decline", "multi-turn", "streaming", "parallel"]
    rows = ["| check | quick start, thinking ON | quick start, thinking OFF | stock template, thinking ON | "
            "stock template, thinking OFF |",
            "| --- | :---: | :---: | :---: | :---: |"]
    detail = (t or {}).get("detail") or {}
    mk = lambda x: "β€”" if x is None else ("βœ…" if x else "❌")

    def count(n, think):
        vals = [((x or {}).get("detail") or {}).get(f"{n}|think={think}") for x in fx]
        if any(v is None for v in vals):
            return "β€”"
        return f"{sum(bool(v) for v in vals)}/{len(vals)}"
    for n in names:
        rows.append(f"| {n} | {count(n, True)} | {count(n, False)} | {mk(detail.get(f'{n}|think=True'))} | "
                    f"{mk(detail.get(f'{n}|think=False'))} |")
    fn = os.path.basename(tier("q106").get("file") or "β€”")
    tot = (f"**{sum(x['passed'] for x in fx)}/{sum(x['total'] for x in fx)}** over three passes with the quick-start "
           f"configuration, **{t['passed']}/{t['total']}** with the stock template"
           if all(fx) and t and t.get("passed") is not None else "Tool-calling suite")
    return (f"{tot}, run on `{fn}`. Quick start = the included template file + `--reasoning off`, thinking switched "
            f"with `enable_thinking`; stock = the model's own template, thinking switched with `reasoning_effort` "
            f"(`high` / `none`). A check passes only with a native `tool_calls` entry carrying the right arguments "
            f"and no raw XML or think tags left in `content`.\n\n" + "\n".join(rows))


def vision_block():
    on, off = gate("n-vision-q106-faon"), gate("n-vision-q106-faoff")
    if not on and not off:
        return "_Not measured._"
    vp = S.get("vision_probe") or {}
    exp_row = next((x for x in (on, off) if x and x.get("expected")), None)
    exp_txt = ", ".join(f"`{e.strip()}`" for e in exp_row["expected"].split(",")) if exp_row else "β€”"
    def cell(x):
        if not x:
            return "β€”"
        exp = x.get("expected") or ""
        nexp = len(exp.split(",")) if exp else None
        hits = x.get("hits") or []
        n = f"{len(hits)}/{nexp} terms" if nexp is not None else "β€”"
        if x.get("result") == "PASS":
            return f"βœ… {n}"
        return "❌ " + ("server stopped" if x.get("server_died") else ("request failed" if x.get("error") else n))
    rows = ["| | `-fa on` | `-fa off` |", "| --- | :---: | :---: |",
            f"| STRIX_LEAN + projector | {cell(on)} | {cell(off)} |"]
    txt = (f"Probe: a synthetic {vp.get('width', 'β€”')}Γ—{vp.get('height', 'β€”')} image with a red circle and a blue square "
           f"(a model that ignores the image cannot name both), sent to `{tier('q106').get('file') or 'β€”'}` with "
           f"`--mmproj`, temperature 0. Pass = the reply names every expected term ({exp_txt}).\n\n"
           + "\n".join(rows) + "\n\n" + J("vision_note"))
    ans = next((x for x in (on, off) if x and x.get("result") == "PASS" and x.get("answer")), None)
    if ans:
        a_ = (ans.get("answer") or "").strip()
        which = "`-fa on`" if ans is on else "`-fa off`"
        cut = a_[:300]
        txt += f"\n\nReply ({which}):\n\n> {cut}" + (" …" if len(a_) >= 300 else "")
    return txt


def files_table(names):
    rows = ["| File | Size | sha256 |", "| --- | ---: | --- |"]
    for fn, size in names:
        size = SIZES.get(fn, size)
        sz = ("β€”" if size is None else gib(size) if size >= GiB // 10 else
              f"{size / MiB:.1f} MiB" if size >= MiB else f"{size / 1024:.1f} KiB")
        rows.append(f"| `{fn}` | {sz} | `{SUMS.get(fn, 'β€”')}` |")
    return "\n".join(rows)


def receipts_table(tags, imat=False):
    head = ("| File | `output.weight` | `token_embd.weight` | tensors |"
            + (" imatrix entries | bytes differ from standard |" if imat else ""))
    sep = "| --- | --- | --- | ---: |" + (" ---: | :---: |" if imat else "")
    rows = [head, sep]
    for k in tags:
        t = tier(k)
        r = (f"| `{t.get('file') or 'β€”'}` | {t.get('output_weight') or 'β€”'} | {t.get('token_embd') or 'β€”'} | "
             f"{g(t.get('tensors'), '{}')} |")
        if imat:
            dfs = t.get("differs_from_standard")
            r += f" {t.get('imatrix_entries') if t.get('imatrix_entries') is not None else 'β€”'} | "
            r += f"{'yes' if dfs else ('no' if dfs is False else 'β€”')} |"
        rows.append(r)
    return "\n".join(rows)


def repro(model_file, label):
    b = bench(label)
    sha = B.get("sha256") or {}
    cmd = (b.get("cmd") if b else None) or "β€”"
    return f"""```
server   : {B.get('repo') or 'β€”'} @ {B.get('commit') or 'β€”'}
           unpatched; build dir {os.path.dirname(B['dir']) if B.get('dir') else 'β€”'}, Release, Unix Makefiles, GGML_HIP=ON GGML_VULKAN=ON
           GGML_HIP_GRAPHS=ON GGML_HIP_NO_VMM=ON GGML_NATIVE=ON AMDGPU_TARGETS=gfx1151 LLAMA_CURL=OFF
           CMAKE_HIP_COMPILER=/opt/rocm-7.2.4/lib/llvm/bin/clang
           sha256 llama-quantize   {sha.get('llama-quantize') or 'β€”'}
           sha256 llama-imatrix    {sha.get('llama-imatrix') or 'β€”'}
           sha256 llama-perplexity {sha.get('llama-perplexity') or 'β€”'}
           sha256 llama-server     {sha.get('llama-server') or 'β€”'}
source   : {SRC.get('repo') or 'β€”'}  revision {SRC.get('revision') or 'β€”'}
model    : {model_file} (the argv below; every file was measured the same way)
argv     : {cmd}
template : the quick-start tool-suite and image rows add --chat-template-file chat_template_enable_thinking.jinja
           --reasoning off to this argv (recipe/pipeline/run_tools_roff.sh -> nex_tools_tpl.py; their server logs
           read "chat template, thinking = 0"); the speed rows use the stock template
env      : LD_LIBRARY_PATH=<build>/bin:/opt/rocm-7.2.4/lib
           HSA_OVERRIDE_GFX_VERSION=11.5.1 GGML_HIP_ENABLE_UNIFIED_MEMORY=1
box      : aimax β€” AMD Ryzen AI Max+ 395 / Radeon 8060S (gfx1151), 124 GiB, GTT 131072 MiB,
           kernel 6.17.6-061706-generic, ROCm 7.2.4
protocol : {PN_TXT}-token code prompt, {GEN} generated tokens, temp 0 / top_k 1, ignore_eos, cache_prompt false,
           1 warm-up + median of {REPS}, no co-resident models (box iced)
measured : {' to '.join(S.get('measured_range') or []) or S.get('measured') or 'β€”'}, by the pipeline in recipe/ (every raw number in recipe/results_summary.json and recipe/raw/)
```"""


def methodology_std():
    return f"""```bash
# 1. convert: text model and the vision projector (the checkpoint has no mtp.* tensors)
python convert_hf_to_gguf.py hf --outtype bf16 --model-name {N} --outfile {N}-BF16.gguf
python convert_hf_to_gguf.py hf --outtype bf16 --mmproj --model-name {N} --outfile mmproj-{N}-BF16.gguf

# 2. quantize from BF16 only; the LM head is forced up on every tier and read back by exact tensor name
llama-quantize --output-tensor-type q6_K                            {N}-BF16.gguf OUT Q4_0_ROCMFP4_STRIX_LEAN 16
llama-quantize --output-tensor-type q6_K --token-embedding-type q6_K {N}-BF16.gguf OUT Q4_0_ROCMFP4_COHERENT 16
llama-quantize --output-tensor-type q6_K                            {N}-BF16.gguf OUT Q4_0_ROCMFP4_FAST 16

# 3. BF16 reference logits on the CPU only (this build's ROCm0 path computes the BF16 MoE wrong β€” Known issues)
llama-perplexity -m {N}-BF16.gguf -f wikitext-2-raw/wiki.test.raw -c 2048 -b 2048 --chunks 40 --kl-divergence-base bf16.kld \\
  -dev none -ngl 0 --no-op-offload -t 16
# 4. grade each shipped file against those logits, on each GPU backend
llama-perplexity -m OUT --kl-divergence-base bf16.kld --kl-divergence -c 2048 -b 2048 -ngl 999 -fa on -dio -dev ROCm0
llama-perplexity -m OUT --kl-divergence-base bf16.kld --kl-divergence -c 2048 -b 2048 -ngl 999 -fa on -dio -dev Vulkan0
```

Receipts (the built file is the receipt β€” exact tensor names, never a substring match; `recipe/logs/`):

{receipts_table(STD_TAGS)}"""


def first_rocm_bullet():
    hc = S.get("hub_check")
    others = [h for h in (hc or {}).get("header_checks") or [] if h.get("output_weight_type")]
    if others:
        return "".join(
            f"- Another public ROCmFP4 build of this model exists β€” [{h['repo']}](https://huggingface.co/{h['repo']}): "
            f"its `{h['file']}` stores `output.weight` as `{h['output_weight_type']}`"
            f"{' and carries no imatrix metadata' if h.get('imatrix_keys') == [] else ''}. Every tier here keeps "
            f"`output.weight` at `Q6_K`, and the imatrix builds are a separate repo.\n" for h in others)
    if isinstance(hc, dict) and hc.get("rocm_builds_found") == 0:
        return ("- **First ROCmFP4 build of this model** β€” no ROCm or Strix Halo build of Nex-N2.5-mini was on the Hub "
                "at publication.\n")
    return ""


def intro_arch():
    p = SRC.get("params")
    ptxt = f"{p:,} parameters (BF16)" if p is not None else "β€” parameters"
    return (f"{AR.get('layers', 'β€”')}-layer Qwen3.5 MoE ({AR.get('linear_attn_layers', 'β€”')} Gated DeltaNet linear-attention + "
            f"{AR.get('full_attn_layers', 'β€”')} full-attention layers), {AR.get('num_experts', 'β€”')} routed experts / "
            f"{AR.get('num_experts_per_tok', 'β€”')} active, {AR.get('max_position_embeddings', 'β€”'):,}-token context"
            if AR.get("max_position_embeddings") is not None else
            f"{AR.get('layers', 'β€”')}-layer Qwen3.5 MoE, {ptxt}")


def std_card():
    L = tier("q106")
    bf = S.get("bf16") or {}
    nextn = bf.get("nextn_tensors")
    no_mtp = ("the checkpoint ships no `mtp.*` weights" +
              (f" (the converted BF16 GGUF reads back {nextn} `nextn` tensors)" if nextn is not None else ""))
    return YAML.format(extra="") + f"""
# Nex-N2.5-mini β€” ROCmFP4 for AMD Strix Halo (gfx1151)

ROCmFP4 / ROCmFPX quantizations of **[nex-agi/Nex-N2.5-mini](https://huggingface.co/nex-agi/Nex-N2.5-mini)** β€”
{g(SRC.get('params'), '{:,}')} parameters (BF16), {intro_arch()}, text + image β€” built and measured on an AMD Ryzen AI
Max+ 395 (Radeon 8060S, `gfx1151`). Upstream publishes no GGUF.

{first_rocm_bullet()}- **Vision projector included.**
- **No MTP head.** `mtp_num_hidden_layers: 1` is declared in `config.json`, but {no_mtp}. There is no speculative
  decoding on these files.
- Importance-matrix builds of the same three 4-bit tiers: **[{IMAT}](https://huggingface.co/{IMAT})**.

## Which file should I use?

{PROTOCOL}

{TABLE_HEAD}
{chr(10).join(tier_row(k) for k in STD_TAGS)}
{bf16_row()}

{footnotes()}

{J('std_recommendation')}

{quality_blurb()}

**Where the quality numbers come from.** {J('quality_provenance')}

{J('backend_quality_note')}

## Quick start

{quick_start(JUDGE.get('std_default') or L.get('file') or 'β€”', STD)}

{reasoning_block()}

## Speed

{speed_table()}

{J('speed_note')}

## Prompt caching

Measured: pairs of requests that share a long code prefix and differ only in the closing instruction. The second
request of each pair runs warm (`cache_prompt: true`, resuming from what the first one left) and then cold
(`cache_prompt: false`), and the two replies are compared byte for byte. Every prompt is padded to one token length so
warm and cold see identical chunking.

{cache_table()}

{J('cache_note')}

## Tool calling

The template emits the XML-style `<tool_call><function=…><parameter=…>` format, which llama.cpp parses natively
through the Jinja chat template (on by default). Suite run through `llama-server`, at the checkpoint's recommended
sampling (temperature 0.7, top-p 0.95, top-k 40):

{tools_block()}

{J('tools_note')}

## Vision

`mmproj-{N}-BF16.gguf` is the {AR.get('vision_layers', 'β€”')}-layer vision tower (width {AR.get('vision_width', 'β€”')}),
loaded with `--mmproj`. Its attention follows the server's `-fa` setting, so both settings were checked.

{vision_block()}

## Memory

{J('memory_note')}

## Quantization methodology

{methodology_std()}

`tie_word_embeddings` is false, so the output head is a real tensor and `--output-tensor-type q6_K` does real work.
All three tiers pin `output.weight` to `q6_K`; COHERENT also pins `token_embd.weight` to `q6_K`, while STRIX_LEAN and
FAST keep their tier's own embedding type (shown in the receipts).

## Reproduction

{repro(L.get('file') or 'β€”', 'n-q106-rocm')}

## Files

{files_table([(tier(k).get('file'), tier(k).get('size_bytes')) for k in STD_TAGS] + list((S.get('aux') or {}).items()))}

`SHA256SUMS` covers every model file and the chat template file. `recipe/` holds the measurement pipeline (`recipe/pipeline/`), raw per-run
results (`recipe/raw/`), build and receipt logs (`recipe/logs/`), and `results_summary.json` with every measured value
on this card. Architecture facts (layer counts, vocabulary, vision depth) come from the checkpoint's `config.json` at
revision `{SRC.get('revision') or 'β€”'}`.

## Known issues and limits

{J('std_known_issues')}

## License and attribution

Apache-2.0, inherited from the base model. Weights and architecture: **Nex-AGI**
([nex-agi/Nex-N2.5-mini](https://huggingface.co/nex-agi/Nex-N2.5-mini)). ROCmFP4 / ROCmFPX quantization format and
runtime: the ROCmFPX project. Quantization and measurements: kingjones777.
"""


def imat_effect_table():
    rows = ["| Tier | Build | Size | KLD vs BF16 ↓ | Same top-1 ↑ | PPL (Γ— BF16) | 99th-pct KLD |",
            "| --- | --- | ---: | ---: | ---: | ---: | ---: |"]
    def d(a, b, k, ek):
        x, y = tier(a).get(k), tier(b).get(k)
        ex, ey = (tier(a).get(ek), tier(b).get(ek)) if ek else (None, None)
        if None in (x, y):
            return "β€”"
        s = f"{(y - x) / x * 100:+.1f} %"
        if ex is not None and ey is not None:
            s += f" ({abs(y - x) / math.sqrt(ex * ex + ey * ey):.1f}Οƒ)"
        return s
    for base, imat in (("q106", "q106i"), ("q102", "q102i"), ("q103", "q103i")):
        for tag, lab in ((base, "standard"), (imat, "**imatrix**")):
            x = tier(tag)
            ratio = x.get("ppl_ratio")
            rows.append(f"| {NAMES[tag]} | {lab} | {gib(x.get('size_bytes'))} | {pm(x.get('kld_mean'), x.get('kld_err'))} | "
                        f"{g(x.get('same_top_p'), '{:.2f} %')} | {pm(x.get('ppl'), x.get('ppl_err'))} "
                        f"({'β€”' if ratio is None else 'Γ—%.4f' % ratio}) | {g(x.get('kld_p99'), '{:.4f}')} |")
        dpp = ("β€”" if None in (tier(base).get("same_top_p"), tier(imat).get("same_top_p"))
               else f"{tier(imat)['same_top_p'] - tier(base)['same_top_p']:+.2f} pp")
        rows.append(f"| | *Ξ” imatrix* | | {d(base, imat, 'kld_mean', 'kld_err')} | {dpp} | "
                    f"{d(base, imat, 'ppl', 'ppl_err')} | {d(base, imat, 'kld_p99', None)} |")
    return "\n".join(rows)


def imat_card():
    im = S.get("imatrix") or {}
    Li = tier("q106i")
    entries = [tier(k).get("imatrix_entries") for k in IMAT_TAGS]
    ent = next((e for e in entries if e is not None), None)
    ent_txt = g(ent, "{:,}") if (ent is None or len(set(e for e in entries if e is not None)) <= 1) else \
        " / ".join(g(e, "{:,}") for e in entries)
    return YAML.format(extra="\n  - imatrix") + f"""
# Nex-N2.5-mini β€” ROCmFP4 **imatrix** for AMD Strix Halo (gfx1151)

Importance-matrix-calibrated ROCmFP4 quantizations of
**[nex-agi/Nex-N2.5-mini](https://huggingface.co/nex-agi/Nex-N2.5-mini)** ({g(SRC.get('params'), '{:,}')} parameters,
{intro_arch()}, text + image). Companion to the standard build **[{STD}](https://huggingface.co/{STD})** β€” the same
three 4-bit tiers, same vision projector, same unpatched `d3ca537` server; the only difference in the weights is how
each 4-bit block's scale was chosen. There is no MTP head on either repo.

## What the imatrix changes

ROCmFP4 has an importance-weighted quantizer path: with `--imatrix`, each block's scale is chosen by an exhaustive
search that minimises error **weighted by how strongly the calibration activations use each weight**, instead of the
unweighted default. It changes **which** scales are picked at the **same** bit width and tensor types β€” so it moves
quality, not size, and per-token compute is identical.

| | |
| --- | --- |
| calibration text | {im.get('calibration') or 'β€”'} (the widely used community calibration set) |
| computed on | BF16 GGUF, {g(im.get('chunks'), '{}')} chunks Γ— {g(im.get('n_ctx'), '{}')} tokens, {im.get('device') or 'β€”'} |
| entries loaded | {ent_txt} (from the N3 quantize logs) |
| file | `{im.get('file') or 'β€”'}` (GGUF format), sha256 `{im.get('sha256') or 'β€”'}` |

## Measured effect

{quality_blurb()} The calibration text and the grading text are different corpora.

{imat_effect_table()}

Οƒ = difference divided by the two runs' combined standard error. The two runs score the **same** tokens, so this is
conservative (paired noise is smaller).

{J('imat_verdict')}

**Where the quality numbers come from.** {J('quality_provenance')}

{J('backend_quality_note')}

## Which file should I use?

{J('imat_recommendation')}

{TABLE_HEAD}
{chr(10).join(tier_row(k) for k in IMAT_TAGS)}
{bf16_row()}

{footnotes("above")}

{J('imat_speed_note')}

Full speed tables (both backends, prose vs code), prompt-cache, tool-calling and vision results are on
the [standard card](https://huggingface.co/{STD}).

## Quick start

{quick_start(JUDGE.get('imat_default') or Li.get('file') or 'β€”', IMAT)}

{reasoning_block()}

## Quantization methodology

```bash
llama-imatrix -m {N}-BF16.gguf -f calibration_datav3.txt -o {N}.imatrix \\
  -c 512 -b 512 -dev none -ngl 0 --no-op-offload -t 16
llama-quantize --imatrix {N}.imatrix --output-tensor-type q6_K \\
  {N}-BF16.gguf {N}-imatrix-Q4_0_ROCMFP4_STRIX_LEAN.gguf Q4_0_ROCMFP4_STRIX_LEAN 16
llama-quantize --imatrix {N}.imatrix --output-tensor-type q6_K --token-embedding-type q6_K \\
  {N}-BF16.gguf {N}-imatrix-Q4_0_ROCMFP4_COHERENT.gguf Q4_0_ROCMFP4_COHERENT 16
llama-quantize --imatrix {N}.imatrix --output-tensor-type q6_K \\
  {N}-BF16.gguf {N}-imatrix-Q4_0_ROCMFP4_FAST.gguf Q4_0_ROCMFP4_FAST 16
```

Receipts that the weighted path was actually taken, and that each shipped file differs from its standard twin:

{receipts_table(IMAT_TAGS, imat=True)}

## Reproduction

{repro(Li.get('file') or 'β€”', 'n-q106i-rocm')}

## Files

{files_table([(tier(k).get('file'), tier(k).get('size_bytes')) for k in IMAT_TAGS] + [(im.get('file'), im.get('size_bytes'))] + list((S.get('aux') or {}).items()))}

## Known issues and limits

{J('imat_known_issues')}

## License and attribution

Apache-2.0, inherited from the base model. Weights and architecture: **Nex-AGI**
([nex-agi/Nex-N2.5-mini](https://huggingface.co/nex-agi/Nex-N2.5-mini)). Calibration text: bartowski's
`calibration_datav3`. ROCmFP4 / ROCmFPX: the ROCmFPX project. Imatrix, quantization and measurements: kingjones777.
"""


open(os.path.join(OUT, "README_std.md"), "w").write(std_card())
def fix_anchors(md, other_repo):
    """Links to sections that exist only on the other card point there instead of to a missing anchor."""
    import re
    slugs = {re.sub(r"[^a-z0-9 -]", "", h.strip().lower()).replace(" ", "-")
             for h in re.findall(r"^#{1,6} (.+)$", md, flags=re.M)}
    return re.sub(r"\]\(#([a-z0-9-]+)\)",
                  lambda m: m.group(0) if m.group(1) in slugs else "](https://huggingface.co/%s#%s)" % (
                      other_repo, m.group(1)), md)


open(os.path.join(OUT, "README_imat.md"), "w").write(fix_anchors(imat_card(), STD))
cards = open(os.path.join(OUT, "README_std.md")).read() + open(os.path.join(OUT, "README_imat.md")).read()
pend = sorted(set(x.split("JUDGMENT PENDING: ")[1].split("]")[0] for x in cards.split("**[")[1:] if "JUDGMENT PENDING" in x))
dash_cells = cards.count("| β€” |")
print("rendered | bench rows =", len(S.get("bench") or []), "| gates =", len(S.get("gates") or []),
      "| pending judgments:", pend, "| 'β€”' cells:", dash_cells)