kingjones777's picture
recipe: BPW footnote inputs (GGUF weight counts) + card scripts
d8bf7d6 verified
Raw
History Blame Contribute Delete
34.5 kB
#!/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)