#!/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 """ 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 ` | 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 `\\n\\n` (no thinking) | | `"high"` | opens `\\n` (always think) | | `"medium"`, unset, or anything else | opens `` 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 `` 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=/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 `` 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)