aleada commited on
Commit
e73d8ef
·
verified ·
1 Parent(s): 1fab703

pack precision map

Browse files
Files changed (2) hide show
  1. index.html +255 -258
  2. precision_map.js +390 -384
index.html CHANGED
@@ -1,258 +1,255 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>How much of this pack is actually 4-bit?</title>
7
- <!-- The Space renders inside an iframe and huggingface.co sends
8
- x-frame-options: DENY, so any link without a target tries to load a
9
- refused page in the frame and reads as broken. -->
10
- <base target="_blank">
11
- <style>
12
- :root {
13
- --bg: #fbfbfa; --panel: #ffffff; --ink: #1a1a19; --muted: #6b6b66;
14
- --line: #e4e3df; --accent: #3d5a80;
15
- --problem: #a03030; --problem-bg: #fdf3f2;
16
- --check: #8a6212; --check-bg: #fdf9ee;
17
- --clear: #2f6b45; --clear-bg: #f1f8f3;
18
- --q: #3d5a80; --m: #9a9790; --f: #b5793a; --bfr: #d9d6d0;
19
- --mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace;
20
- }
21
- @media (prefers-color-scheme: dark) {
22
- :root {
23
- --bg: #16171a; --panel: #1d1f23; --ink: #e8e6e3; --muted: #9a978f;
24
- --line: #2e3137; --accent: #8ab0d9;
25
- --problem: #e08a86; --problem-bg: #2a1e1e;
26
- --check: #d9b76a; --check-bg: #29241a;
27
- --clear: #8fc9a6; --clear-bg: #1b2620;
28
- --q: #7fa3cc; --m: #6f6c66; --f: #d19a5e; --bfr: #3a3d43;
29
- }
30
- }
31
- * { box-sizing: border-box; }
32
- body {
33
- margin: 0; background: var(--bg); color: var(--ink);
34
- font: 16px/1.65 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
35
- -webkit-font-smoothing: antialiased;
36
- }
37
- .wrap { max-width: 50rem; margin: 0 auto; padding: 3.5rem 1.25rem 5rem; }
38
- .brand { display: inline-block; margin-bottom: 1.75rem; }
39
- .brand img { height: 1.5rem; width: auto; display: block; filter: invert(1); opacity: .78; }
40
- @media (prefers-color-scheme: dark) { .brand img { filter: none; opacity: .9; } }
41
-
42
- header h1 {
43
- font-size: clamp(1.6rem, 4.5vw, 2.2rem); line-height: 1.2;
44
- letter-spacing: -0.02em; margin: 0 0 .75rem; font-weight: 620;
45
- }
46
- header p { color: var(--muted); margin: 0 0 .5rem; max-width: 42rem; }
47
-
48
- form { display: flex; gap: .5rem; flex-wrap: wrap; margin: 2rem 0 .75rem; }
49
- input[type=text] {
50
- flex: 1 1 20rem; min-width: 0; padding: .7rem .85rem;
51
- border: 1px solid var(--line); border-radius: .5rem;
52
- background: var(--panel); color: var(--ink);
53
- font: inherit; font-family: var(--mono); font-size: .92rem;
54
- }
55
- input[type=text]:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: transparent; }
56
- button {
57
- padding: .7rem 1.4rem; border: 0; border-radius: .5rem;
58
- background: var(--accent); color: #fff; font: inherit; font-weight: 560; cursor: pointer;
59
- }
60
- button:hover { filter: brightness(1.08); }
61
- button:disabled { opacity: .55; cursor: progress; }
62
-
63
- .examples { font-size: .86rem; color: var(--muted); margin-bottom: 2.5rem; line-height: 2; }
64
- .examples .group { display: inline-block; min-width: 8.5rem; }
65
- .examples button {
66
- background: none; color: var(--accent); padding: 0 .15rem; font-size: .86rem;
67
- font-family: var(--mono); text-decoration: underline; text-underline-offset: 2px; font-weight: 400;
68
- }
69
-
70
- .card {
71
- background: var(--panel); border: 1px solid var(--line);
72
- border-radius: .7rem; padding: 1.1rem 1.3rem; margin: .85rem 0;
73
- }
74
- .card h3 { margin: 0 0 .5rem; font-size: 1.02rem; font-weight: 600; }
75
- .card h4 { margin: 1.4rem 0 .4rem; font-size: .85rem; font-weight: 600;
76
- text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
77
- .card p { margin: .5rem 0; }
78
- .problem { border-left: 3px solid var(--problem); background: var(--problem-bg); }
79
- .problem h3 { color: var(--problem); }
80
- .check { border-left: 3px solid var(--check); background: var(--check-bg); }
81
- .check h3 { color: var(--check); }
82
- .clear { border-left: 3px solid var(--clear); background: var(--clear-bg); }
83
- .clear h3 { color: var(--clear); }
84
- .warn { color: var(--problem); }
85
- .note { color: var(--muted); font-size: .92rem; }
86
-
87
- .bar {
88
- display: flex; height: 1.15rem; border-radius: .3rem; overflow: hidden;
89
- margin: 1rem 0 .3rem; background: var(--line);
90
- }
91
- .seg { display: block; height: 100%; }
92
- .seg + .seg { box-shadow: inset 2px 0 0 var(--panel); }
93
- .seg.q { background: var(--q); }
94
- .seg.m { background: var(--m); }
95
- .seg.f { background: var(--f); }
96
- .seg.b { background: var(--bfr); }
97
- .key { display: flex; flex-wrap: wrap; gap: .35rem 1.1rem; font-size: .82rem;
98
- color: var(--muted); margin: .6rem 0 0; }
99
- .key i { display: inline-block; width: .62rem; height: .62rem; border-radius: .15rem;
100
- margin-right: .35rem; vertical-align: baseline; }
101
-
102
- .meta {
103
- display: flex; flex-wrap: wrap; gap: .35rem 1.5rem; font-size: .86rem;
104
- color: var(--muted); padding-bottom: .9rem; margin-bottom: .3rem;
105
- border-bottom: 1px solid var(--line);
106
- }
107
- .meta code { color: var(--ink); }
108
- code { font-family: var(--mono); font-size: .86em; }
109
- .muted { color: var(--muted); }
110
- .small { font-size: .84rem; color: var(--muted); }
111
-
112
- h2 { font-size: 1.05rem; font-weight: 600; letter-spacing: -0.01em; margin: 3rem 0 .75rem; }
113
- table { border-collapse: collapse; width: 100%; font-size: .87rem; }
114
- th, td { text-align: left; padding: .45rem .7rem .45rem 0; border-bottom: 1px solid var(--line); vertical-align: top; }
115
- th { font-weight: 600; color: var(--muted); font-size: .8rem; text-transform: uppercase; letter-spacing: .04em; }
116
- td.num { text-align: right; font-family: var(--mono); font-size: .85rem; white-space: nowrap; }
117
- table.rows { margin: .5rem 0; }
118
- table.rows td:first-child { width: 45%; }
119
- .scroll { overflow-x: auto; }
120
- footer { margin-top: 3.5rem; padding-top: 1.5rem; border-top: 1px solid var(--line); font-size: .87rem; color: var(--muted); }
121
- footer a { color: var(--accent); }
122
- .spin { color: var(--muted); font-size: .9rem; }
123
- </style>
124
- </head>
125
- <body>
126
- <div class="wrap">
127
-
128
- <header>
129
- <a class="brand" href="https://assert.gr" rel="noopener">
130
- <img src="./assert-logo.png" alt="ASSERT" width="420" height="87">
131
- </a>
132
- <h1>How much of this pack is actually 4-bit?</h1>
133
- <p>
134
- A name like <code>W4A16</code> or <code>-AWQ-4bit</code> reads as though
135
- everything in the file is four bits. It never is. Embeddings, the output
136
- head, the norms and any preserved auxiliary head stay at the source's
137
- precision — commonly a fifth to two fifths of the bytes — and no model page
138
- says which, or how much.
139
- </p>
140
- <p>
141
- <strong>The set matters more than the total.</strong> An <code>lm_head</code>
142
- left whole is a deliberate, defensible cost. An <code>lm_head</code>
143
- quantized is a decision the pack made on your behalf without mentioning it:
144
- measured on our own weights, quantizing it flips the model's chosen token on
145
- about a fifth of positions.
146
- </p>
147
- <p>
148
- This reads <strong>safetensors headers only</strong>, over ranged requests
149
- from your browser — a few hundred kilobytes against packs of many gigabytes.
150
- It never loads the model and never runs it.
151
- </p>
152
- </header>
153
-
154
- <form id="form">
155
- <input type="text" id="repo" value="aleada/Nemotron-3.5-Lightning-30B-A3B-W4A16"
156
- placeholder="owner/model" autocomplete="off" spellcheck="false"
157
- aria-label="Model repo id">
158
- <button type="submit" id="go">Inspect</button>
159
- </form>
160
- <div class="examples">
161
- <span class="group">Different tools:</span>
162
- <button type="button" data-ex="casperhansen/llama-3-8b-instruct-awq">AutoAWQ</button> ·
163
- <button type="button" data-ex="TheBloke/Llama-2-7B-Chat-GPTQ">GPTQ</button> ·
164
- <button type="button" data-ex="unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit">bitsandbytes</button> ·
165
- <button type="button" data-ex="nvidia/Llama-3.3-70B-Instruct-FP4">ModelOpt&nbsp;NVFP4</button> ·
166
- <button type="button" data-ex="openai/gpt-oss-20b">MXFP4</button>
167
- <br>
168
- <span class="group">Worth comparing:</span>
169
- <button type="button" data-ex="cyankiwi/Qwen3-VL-8B-Instruct-AWQ-4bit">a pack whose name says AWQ</button> ·
170
- <button type="button" data-ex="Qwen/Qwen2.5-0.5B-Instruct">an unquantized model</button>
171
- <br>
172
- <span class="group">Our packs:</span>
173
- <button type="button" data-ex="aleada/Nemotron-3.5-Lightning-30B-A3B-W4A16">Nemotron-3.5</button> ·
174
- <button type="button" data-ex="aleada/Qwen3.8-27B-W4A16">Qwen3.8-27B</button>
175
- </div>
176
-
177
- <div id="out"></div>
178
-
179
- <div class="key">
180
- <span><i style="background:var(--q)"></i>quantized payload</span>
181
- <span><i style="background:var(--m)"></i>quantization data</span>
182
- <span><i style="background:var(--f)"></i>full precision</span>
183
- <span><i style="background:var(--bfr)"></i>index buffers</span>
184
- </div>
185
-
186
- <h2>Why this reads dtypes, not tensor names</h2>
187
- <p>
188
- The first version of this tool classified tensors by <em>name</em>, learned
189
- from compressed-tensors packs where the payload is called
190
- <code>weight_packed</code>. Probing nine toolchains showed that convention is
191
- the minority. <strong>bitsandbytes, ModelOpt FP8, ModelOpt NVFP4 and
192
- compressed-tensors' own <code>int-quantized</code> and
193
- <code>naive-quantized</code> formats all store the quantized payload under the
194
- plain name <code>weight</code></strong> — so a name-driven reader calls every
195
- one of those packs 100% full precision, confidently, and about most of what is
196
- published.
197
- </p>
198
- <p>
199
- So the dtype decides: an <code>I32</code>, <code>I8</code>, <code>U8</code> or
200
- <code>F8</code> tensor is not full precision whatever it is called. Names are
201
- used only to separate quantization metadata — scales, zero points, group
202
- indices — from model weights, a distinction that genuinely has no dtype
203
- signature.
204
- </p>
205
- <p>
206
- The declared method is read from <code>config.json</code>, or from
207
- <code>hf_quant_config.json</code> where the tool writes it there — never from
208
- the repo name. <code>cyankiwi/Qwen3-VL-8B-Instruct-<strong>AWQ</strong>-4bit</code>
209
- declares <code>compressed-tensors</code>.
210
- </p>
211
-
212
- <h2>Conventions, and where each was verified</h2>
213
- <p>
214
- None of these naming conventions is a stable public API — they are internal
215
- choices of each tool, and they have moved before. Two versions therefore
216
- appear, answering different questions: the <strong>producer's</strong>, read
217
- out of the pack itself wherever that tool records it, which is the version
218
- that actually wrote those bytes; and <strong>this reader's</strong>, the date
219
- below, which says how stale the table is rather than leaving a wrong answer
220
- looking authoritative.
221
- </p>
222
- <div class="scroll">
223
- <table>
224
- <thead><tr><th>Declared as</th><th>Tool</th><th>Records its version in</th><th>Verified against</th></tr></thead>
225
- <tbody id="formats"></tbody>
226
- </table>
227
- </div>
228
-
229
- <footer>
230
- <p>
231
- <strong>A high full-precision share is not a fault.</strong> It is usually
232
- the pack being careful — an output head or a vision tower left whole costs
233
- size and buys accuracy. This says what was kept and what it cost in bytes;
234
- it does not say whether the trade was a good one.
235
- </p>
236
- <p>
237
- Conventions verified <strong id="measured">—</strong> against one published
238
- pack per tool, listed above. AutoAWQ's <code>version</code> field is
239
- deliberately not reported as a tool version: it holds <code>gemm</code>, the
240
- kernel variant. Packs that ship a <code>.pt</code> or <code>.bin</code>
241
- instead of safetensors — torchao and HQQ commonly do — cannot be read from
242
- metadata and are reported as unreadable rather than as unquantized. Gated
243
- and private repos cannot be read.
244
- </p>
245
- <p>
246
- Built from the tooling behind the
247
- <a href="https://huggingface.co/aleada" rel="noopener">aleada</a> W4A16 packs.
248
- See also the
249
- <a href="https://huggingface.co/spaces/aleada/pack-integrity-check" rel="noopener">pack integrity checker</a>
250
- and the
251
- <a href="https://huggingface.co/spaces/aleada/reasoning-parser-advisor" rel="noopener">reasoning-parser advisor</a>.
252
- </p>
253
- </footer>
254
-
255
- </div>
256
- <script type="module" src="./app.js"></script>
257
- </body>
258
- </html>
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>How much of this pack is actually 4-bit?</title>
7
+ <!-- The Space renders inside an iframe and huggingface.co sends
8
+ x-frame-options: DENY, so any link without a target tries to load a
9
+ refused page in the frame and reads as broken. -->
10
+ <base target="_blank">
11
+ <style>
12
+ :root {
13
+ --bg: #fbfbfa; --panel: #ffffff; --ink: #1a1a19; --muted: #6b6b66;
14
+ --line: #e4e3df; --accent: #3d5a80;
15
+ --problem: #a03030; --problem-bg: #fdf3f2;
16
+ --check: #8a6212; --check-bg: #fdf9ee;
17
+ --clear: #2f6b45; --clear-bg: #f1f8f3;
18
+ --q: #3d5a80; --m: #9a9790; --f: #b5793a; --bfr: #d9d6d0;
19
+ --mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace;
20
+ }
21
+ @media (prefers-color-scheme: dark) {
22
+ :root {
23
+ --bg: #16171a; --panel: #1d1f23; --ink: #e8e6e3; --muted: #9a978f;
24
+ --line: #2e3137; --accent: #8ab0d9;
25
+ --problem: #e08a86; --problem-bg: #2a1e1e;
26
+ --check: #d9b76a; --check-bg: #29241a;
27
+ --clear: #8fc9a6; --clear-bg: #1b2620;
28
+ --q: #7fa3cc; --m: #6f6c66; --f: #d19a5e; --bfr: #3a3d43;
29
+ }
30
+ }
31
+ * { box-sizing: border-box; }
32
+ body {
33
+ margin: 0; background: var(--bg); color: var(--ink);
34
+ font: 16px/1.65 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
35
+ -webkit-font-smoothing: antialiased;
36
+ }
37
+ .wrap { max-width: 50rem; margin: 0 auto; padding: 3.5rem 1.25rem 5rem; }
38
+ .brand { display: inline-block; margin-bottom: 1.75rem; }
39
+ .brand img { height: 1.5rem; width: auto; display: block; filter: invert(1); opacity: .78; }
40
+ @media (prefers-color-scheme: dark) { .brand img { filter: none; opacity: .9; } }
41
+
42
+ header h1 {
43
+ font-size: clamp(1.6rem, 4.5vw, 2.2rem); line-height: 1.2;
44
+ letter-spacing: -0.02em; margin: 0 0 .75rem; font-weight: 620;
45
+ }
46
+ header p { color: var(--muted); margin: 0 0 .5rem; max-width: 42rem; }
47
+
48
+ form { display: flex; gap: .5rem; flex-wrap: wrap; margin: 2rem 0 .75rem; }
49
+ input[type=text] {
50
+ flex: 1 1 20rem; min-width: 0; padding: .7rem .85rem;
51
+ border: 1px solid var(--line); border-radius: .5rem;
52
+ background: var(--panel); color: var(--ink);
53
+ font: inherit; font-family: var(--mono); font-size: .92rem;
54
+ }
55
+ input[type=text]:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: transparent; }
56
+ button {
57
+ padding: .7rem 1.4rem; border: 0; border-radius: .5rem;
58
+ background: var(--accent); color: #fff; font: inherit; font-weight: 560; cursor: pointer;
59
+ }
60
+ button:hover { filter: brightness(1.08); }
61
+ button:disabled { opacity: .55; cursor: progress; }
62
+
63
+ .examples { font-size: .86rem; color: var(--muted); margin-bottom: 2.5rem; line-height: 2; }
64
+ .examples .group { display: inline-block; min-width: 8.5rem; }
65
+ .examples button {
66
+ background: none; color: var(--accent); padding: 0 .15rem; font-size: .86rem;
67
+ font-family: var(--mono); text-decoration: underline; text-underline-offset: 2px; font-weight: 400;
68
+ }
69
+
70
+ .card {
71
+ background: var(--panel); border: 1px solid var(--line);
72
+ border-radius: .7rem; padding: 1.1rem 1.3rem; margin: .85rem 0;
73
+ }
74
+ .card h3 { margin: 0 0 .5rem; font-size: 1.02rem; font-weight: 600; }
75
+ .card h4 { margin: 1.4rem 0 .4rem; font-size: .85rem; font-weight: 600;
76
+ text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
77
+ .card p { margin: .5rem 0; }
78
+ .problem { border-left: 3px solid var(--problem); background: var(--problem-bg); }
79
+ .problem h3 { color: var(--problem); }
80
+ .check { border-left: 3px solid var(--check); background: var(--check-bg); }
81
+ .check h3 { color: var(--check); }
82
+ .clear { border-left: 3px solid var(--clear); background: var(--clear-bg); }
83
+ .clear h3 { color: var(--clear); }
84
+ .warn { color: var(--problem); }
85
+ .note { color: var(--muted); font-size: .92rem; }
86
+
87
+ .bar {
88
+ display: flex; height: 1.15rem; border-radius: .3rem; overflow: hidden;
89
+ margin: 1rem 0 .3rem; background: var(--line);
90
+ }
91
+ .seg { display: block; height: 100%; }
92
+ .seg + .seg { box-shadow: inset 2px 0 0 var(--panel); }
93
+ .seg.q { background: var(--q); }
94
+ .seg.m { background: var(--m); }
95
+ .seg.f { background: var(--f); }
96
+ .seg.b { background: var(--bfr); }
97
+ .key { display: flex; flex-wrap: wrap; gap: .35rem 1.1rem; font-size: .82rem;
98
+ color: var(--muted); margin: .6rem 0 0; }
99
+ .key i { display: inline-block; width: .62rem; height: .62rem; border-radius: .15rem;
100
+ margin-right: .35rem; vertical-align: baseline; }
101
+
102
+ .meta {
103
+ display: flex; flex-wrap: wrap; gap: .35rem 1.5rem; font-size: .86rem;
104
+ color: var(--muted); padding-bottom: .9rem; margin-bottom: .3rem;
105
+ border-bottom: 1px solid var(--line);
106
+ }
107
+ .meta code { color: var(--ink); }
108
+ code { font-family: var(--mono); font-size: .86em; }
109
+ .muted { color: var(--muted); }
110
+ .small { font-size: .84rem; color: var(--muted); }
111
+
112
+ h2 { font-size: 1.05rem; font-weight: 600; letter-spacing: -0.01em; margin: 3rem 0 .75rem; }
113
+ table { border-collapse: collapse; width: 100%; font-size: .87rem; }
114
+ th, td { text-align: left; padding: .45rem .7rem .45rem 0; border-bottom: 1px solid var(--line); vertical-align: top; }
115
+ th { font-weight: 600; color: var(--muted); font-size: .8rem; text-transform: uppercase; letter-spacing: .04em; }
116
+ td.num { text-align: right; font-family: var(--mono); font-size: .85rem; white-space: nowrap; }
117
+ table.rows { margin: .5rem 0; }
118
+ table.rows td:first-child { width: 45%; }
119
+ .scroll { overflow-x: auto; }
120
+ footer { margin-top: 3.5rem; padding-top: 1.5rem; border-top: 1px solid var(--line); font-size: .87rem; color: var(--muted); }
121
+ footer a { color: var(--accent); }
122
+ .spin { color: var(--muted); font-size: .9rem; }
123
+ </style>
124
+ </head>
125
+ <body>
126
+ <div class="wrap">
127
+
128
+ <header>
129
+ <a class="brand" href="https://assert.gr" rel="noopener">
130
+ <img src="./assert-logo.png" alt="ASSERT" width="420" height="87">
131
+ </a>
132
+ <h1>How much of this pack is actually 4-bit?</h1>
133
+ <p>
134
+ A name like <code>W4A16</code> or <code>-AWQ-4bit</code> reads as though
135
+ everything in the file is four bits. It never is. Embeddings, the output
136
+ head, the norms and any preserved auxiliary head stay at the source's
137
+ precision — commonly a fifth to two fifths of the bytes — and no model page
138
+ says which, or how much.
139
+ </p>
140
+ <p>
141
+ <strong>The set matters more than the total.</strong> An <code>lm_head</code>
142
+ left whole is a deliberate, defensible cost. An <code>lm_head</code>
143
+ quantized is a decision the pack made on your behalf without mentioning it:
144
+ measured on our own weights, quantizing it flips the model's chosen token on
145
+ about a fifth of positions.
146
+ </p>
147
+ <p>
148
+ This reads <strong>safetensors headers only</strong>, over ranged requests
149
+ from your browser — a few hundred kilobytes against packs of many gigabytes.
150
+ It never loads the model and never runs it.
151
+ </p>
152
+ </header>
153
+
154
+ <form id="form">
155
+ <input type="text" id="repo" value="aleada/Nemotron-3.5-Lightning-30B-A3B-W4A16"
156
+ placeholder="owner/model" autocomplete="off" spellcheck="false"
157
+ aria-label="Model repo id">
158
+ <button type="submit" id="go">Inspect</button>
159
+ </form>
160
+ <div class="examples">
161
+ <span class="group">Different tools:</span>
162
+ <button type="button" data-ex="casperhansen/llama-3-8b-instruct-awq">AutoAWQ</button> ·
163
+ <button type="button" data-ex="TheBloke/Llama-2-7B-Chat-GPTQ">GPTQ</button> ·
164
+ <button type="button" data-ex="unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit">bitsandbytes</button> ·
165
+ <button type="button" data-ex="nvidia/Llama-3.3-70B-Instruct-FP4">ModelOpt&nbsp;NVFP4</button> ·
166
+ <button type="button" data-ex="openai/gpt-oss-20b">MXFP4</button>
167
+ <br>
168
+ <span class="group">Worth comparing:</span>
169
+ <button type="button" data-ex="cyankiwi/Qwen3-VL-8B-Instruct-AWQ-4bit">a pack whose name says AWQ</button> ·
170
+ <button type="button" data-ex="Qwen/Qwen2.5-0.5B-Instruct">an unquantized model</button>
171
+ <br>
172
+ <span class="group">Our packs:</span>
173
+ <button type="button" data-ex="aleada/Nemotron-3.5-Lightning-30B-A3B-W4A16">Nemotron-3.5</button> ·
174
+ <button type="button" data-ex="aleada/Qwen3.8-27B-W4A16">Qwen3.8-27B</button>
175
+ </div>
176
+
177
+ <div id="out"></div>
178
+
179
+ <div class="key">
180
+ <span><i style="background:var(--q)"></i>quantized payload</span>
181
+ <span><i style="background:var(--m)"></i>quantization data</span>
182
+ <span><i style="background:var(--f)"></i>full precision</span>
183
+ <span><i style="background:var(--bfr)"></i>index buffers</span>
184
+ </div>
185
+
186
+ <h2>Why this reads dtypes, not tensor names</h2>
187
+ <p>
188
+ The first version of this tool classified tensors by <em>name</em>, learned
189
+ from compressed-tensors packs where the payload is called
190
+ <code>weight_packed</code>. Probing nine toolchains showed that convention is
191
+ the minority. <strong>bitsandbytes, ModelOpt FP8, ModelOpt NVFP4 and
192
+ compressed-tensors' own <code>int-quantized</code> and
193
+ <code>naive-quantized</code> formats all store the quantized payload under the
194
+ plain name <code>weight</code></strong> — so a name-driven reader calls every
195
+ one of those packs 100% full precision, confidently, and about most of what is
196
+ published.
197
+ </p>
198
+ <p>
199
+ So the dtype decides: an <code>I32</code>, <code>I8</code>, <code>U8</code> or
200
+ <code>F8</code> tensor is not full precision whatever it is called. Names are
201
+ used only to separate quantization metadata — scales, zero points, group
202
+ indices — from model weights, a distinction that genuinely has no dtype
203
+ signature.
204
+ </p>
205
+ <p>
206
+ The declared method is read from <code>config.json</code>, or from
207
+ <code>hf_quant_config.json</code> where the tool writes it there — never from
208
+ the repo name. <code>cyankiwi/Qwen3-VL-8B-Instruct-<strong>AWQ</strong>-4bit</code>
209
+ declares <code>compressed-tensors</code>.
210
+ </p>
211
+
212
+ <h2>Conventions, and where each was verified</h2>
213
+ <p>
214
+ None of these naming conventions is a stable public API — they are internal
215
+ choices of each tool, and they have moved before. Two versions therefore
216
+ appear, answering different questions: the <strong>producer's</strong>, read
217
+ out of the pack itself wherever that tool records it, which is the version
218
+ that actually wrote those bytes; and <strong>this reader's</strong>, the date
219
+ below, which says how stale the table is rather than leaving a wrong answer
220
+ looking authoritative.
221
+ </p>
222
+ <div class="scroll">
223
+ <table>
224
+ <thead><tr><th>Declared as</th><th>Tool</th><th>Records its version in</th><th>Verified against</th></tr></thead>
225
+ <tbody id="formats"></tbody>
226
+ </table>
227
+ </div>
228
+
229
+ <footer>
230
+ <p>
231
+ <strong>A high full-precision share is not a fault.</strong> It is usually
232
+ the pack being careful — an output head or a vision tower left whole costs
233
+ size and buys accuracy. This says what was kept and what it cost in bytes;
234
+ it does not say whether the trade was a good one.
235
+ </p>
236
+ <p>
237
+ Conventions verified <strong id="measured">—</strong> against one published
238
+ pack per tool, listed above. AutoAWQ's <code>version</code> field is
239
+ deliberately not reported as a tool version: it holds <code>gemm</code>, the
240
+ kernel variant. Packs that ship a <code>.pt</code> or <code>.bin</code>
241
+ instead of safetensors — torchao and HQQ commonly do — cannot be read from
242
+ metadata and are reported as unreadable rather than as unquantized. Gated
243
+ and private repos cannot be read.
244
+ </p>
245
+ <p>
246
+ Built from the tooling behind the
247
+ <a href="https://huggingface.co/aleada" rel="noopener">aleada</a> W4A16 packs.
248
+ See also the <a href="https://huggingface.co/spaces/aleada/pack-integrity-check" rel="noopener">pack integrity checker</a>, the <a href="https://huggingface.co/spaces/aleada/reasoning-parser-advisor" rel="noopener">reasoning-parser advisor</a> and the <a href="https://huggingface.co/spaces/aleada/model-search-that-answers" rel="noopener">model search</a>.
249
+ </p>
250
+ </footer>
251
+
252
+ </div>
253
+ <script type="module" src="./app.js"></script>
254
+ </body>
255
+ </html>
 
 
 
precision_map.js CHANGED
@@ -1,384 +1,390 @@
1
- /**
2
- * What precision a pack is actually in — the browser half.
3
- *
4
- * The JavaScript port of `tools/quantize/precision_map.py`. Both are held
5
- * together by `parity.mjs`, which runs real tensor names harvested from
6
- * nine toolchains through each and diffs. The rules took a wrong answer
7
- * to get right, and a port that drifted from them would bring it back
8
- * silently.
9
- *
10
- * The wrong answer is worth stating, because it is the whole reason this
11
- * file classifies the way it does. The first version keyed on tensor
12
- * NAMES, learned from compressed-tensors packs where the payload is
13
- * called `weight_packed`. Probing the ecosystem showed that convention is
14
- * the minority: bitsandbytes, ModelOpt FP8, ModelOpt NVFP4 and
15
- * compressed-tensors' own int-quantized and naive-quantized formats all
16
- * store the quantized payload under the plain name `weight`. A
17
- * name-driven reader reports every one of those packs as 100% full
18
- * precision — confidently, and about most of what is published.
19
- *
20
- * So dtype is the primary evidence here. Names are used only to separate
21
- * quantization metadata from model weights, a distinction that genuinely
22
- * has no dtype signature.
23
- */
24
-
25
- /** Bytes per element, keyed by the safetensors dtype string. */
26
- export const DTYPE_BYTES = {
27
- BF16: 2, F16: 2, F32: 4, F64: 8, F8_E4M3: 1, F8_E5M2: 1, F4: 1,
28
- I8: 1, U8: 1, I16: 2, I32: 4, I64: 8, BOOL: 1,
29
- };
30
-
31
- /**
32
- * A tensor in one of these dtypes is carrying quantized payload. Every
33
- * packing scheme measured lands here: compressed-tensors packs into I32,
34
- * GPTQ/AWQ/AutoRound into I32, bitsandbytes and MXFP4 and NVFP4 into U8,
35
- * FP8 into F8_E4M3, int8 into I8.
36
- */
37
- export const PAYLOAD_DTYPES = new Set(
38
- ['I32', 'I8', 'U8', 'F8_E4M3', 'F8_E5M2', 'F4', 'I16']);
39
-
40
- /**
41
- * Excluded on purpose: index buffers and masks (`position_ids`, causal
42
- * masks, `weight_shape`), never packed weights.
43
- */
44
- export const BUFFER_DTYPES = new Set(['I64', 'BOOL']);
45
-
46
- /**
47
- * Quantization metadata: scales, zero points, group permutations, the
48
- * stored original shape. Real bytes, reported as their own category —
49
- * counting them as full precision would overstate what stayed whole, and
50
- * counting them as payload would hide the overhead.
51
- *
52
- * One union across all toolchains rather than a table per format: these
53
- * names do not collide, so a reader that misidentifies the format still
54
- * gets the metadata right.
55
- */
56
- export const METADATA_SUFFIXES = [
57
- // compressed-tensors
58
- 'weight_scale', 'weight_shape', 'weight_zero_point', 'weight_g_idx',
59
- 'input_scale', 'output_scale',
60
- // gptq / awq / autoround
61
- 'qzeros', 'scales', 'g_idx',
62
- // bitsandbytes — double quantization, so the scales are themselves quantized
63
- 'absmax', 'quant_map', 'nested_absmax', 'nested_quant_map',
64
- 'quant_state.bitsandbytes__nf4', 'quant_state.bitsandbytes__fp4',
65
- // ModelOpt / native fp8 + fp4
66
- 'weight_scale_2', 'k_scale', 'v_scale', 'q_scale', 'prob_scale',
67
- // mxfp4
68
- '_scales',
69
- ];
70
-
71
- /** Formats that give the payload its own name; the rest reuse `weight`. */
72
- export const PAYLOAD_SUFFIXES = ['weight_packed', 'qweight', '_blocks'];
73
-
74
- /**
75
- * Every format here was verified by reading a real published pack of it.
76
- *
77
- * `producer` says where that pack records the version of the tool that
78
- * wrote it, and `null` means the tool records nothing — which is worth
79
- * reporting rather than hiding, since it tells the reader no version
80
- * evidence exists for those bytes.
81
- *
82
- * AutoAWQ is deliberately `null` even though its config has a `version`
83
- * field: that field holds `"gemm"`, the kernel variant. Printing it as a
84
- * release number would be a small invented fact.
85
- */
86
- export const FORMATS = {
87
- 'compressed-tensors': {
88
- tool: 'llm-compressor / compressed-tensors',
89
- producer: ['config', 'version'],
90
- verifiedOn: 'aleada/Qwen3.8-27B-W4A16 (0.17.1), RedHatAI w8a8, RedHatAI FP8',
91
- note: 'pack-quantized names the payload weight_packed; int-quantized and naive-quantized reuse `weight`',
92
- },
93
- gptq: {
94
- tool: 'GPTQModel / AutoGPTQ',
95
- producer: ['config', 'meta.quantizer'],
96
- verifiedOn: 'ModelCloud vortex-v3 (gptqmodel:1.4.4), TheBloke/Llama-2-7B-Chat-GPTQ',
97
- note: 'qweight + qzeros + scales + g_idx',
98
- },
99
- awq: {
100
- tool: 'AutoAWQ',
101
- producer: null,
102
- verifiedOn: 'casperhansen/llama-3-8b-instruct-awq, Qwen/Qwen3-8B-AWQ',
103
- note: 'qweight + qzeros + scales, no g_idx',
104
- },
105
- 'auto-round': {
106
- tool: 'Intel AutoRound',
107
- producer: ['config', 'autoround_version'],
108
- verifiedOn: 'OPEA/Qwen2.5-7B-Instruct-int4-sym-inc (0.4.0.dev)',
109
- note: 'exports in gptq or compressed-tensors layout',
110
- },
111
- 'intel/auto-round': {
112
- tool: 'Intel AutoRound',
113
- producer: ['config', 'autoround_version'],
114
- verifiedOn: 'Intel/Qwen2-7B-int4-inc',
115
- note: 'same layout, older method spelling',
116
- },
117
- bitsandbytes: {
118
- tool: 'bitsandbytes',
119
- producer: null,
120
- verifiedOn: 'unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit',
121
- note: 'payload is U8 under the plain name `weight`; absmax/quant_map are the scales',
122
- },
123
- mxfp4: {
124
- tool: 'MXFP4 (native export)',
125
- producer: null,
126
- verifiedOn: 'openai/gpt-oss-20b',
127
- note: '*_blocks payload + *_scales, per fused expert tensor',
128
- },
129
- modelopt: {
130
- tool: 'NVIDIA TensorRT ModelOpt',
131
- producer: ['hf_quant_config', 'producer.version'],
132
- verifiedOn: 'nvidia/Llama-3.3-70B-Instruct-FP4 (0.23.0), nvidia/Llama-3.1-8B-Instruct-FP8',
133
- note: 'config.json declares nothing the method lives in hf_quant_config.json',
134
- },
135
- };
136
-
137
- export const RULES_MEASURED_ON = '2026-09-07';
138
-
139
- /**
140
- * Which module a full-precision tensor belongs to. Ordered: the first
141
- * match wins, so specific patterns precede general ones.
142
- */
143
- export const FAMILIES = [
144
- [/(^|\.)mtp\.|(^|\.)draft/, 'auxiliary head (MTP / draft)'],
145
- [/(^|\.)(lm_head|output_layer)\b/, 'output head (lm_head)'],
146
- [/embed|embeddings?\b|wte\b/, 'embeddings'],
147
- [/vision_tower|vision_model|visual|image_encoder|patch_embed/, 'vision tower'],
148
- [/multi_modal_projector|mm_projector|merger/, 'multimodal projector'],
149
- [/norm|layernorm|rmsnorm/, 'norms'],
150
- [/A_log|dt_bias|conv1d|\.D$/, 'state-space parameters'],
151
- [/(^|\.)(gate|router)\b|e_score_correction|sinks/, 'routers and sinks'],
152
- [/rotary|inv_freq|position/, 'position buffers'],
153
- [/\.bias$/, 'biases'],
154
- [/experts?\./, 'experts (left unquantized)'],
155
- ];
156
-
157
- const HEAD_RE = /lm_head|output_layer/;
158
-
159
- /**
160
- * quantized | metadata | full | buffer.
161
- *
162
- * Name first, but only to pull metadata out — a scale is a scale whatever
163
- * its dtype. Then dtype, which is what actually decides whether a tensor
164
- * holds full-precision weights.
165
- */
166
- export function classify(name, dtype) {
167
- for (const suf of METADATA_SUFFIXES) if (name.endsWith(suf)) return 'metadata';
168
- for (const suf of PAYLOAD_SUFFIXES) if (name.endsWith(suf)) return 'quantized';
169
- if (BUFFER_DTYPES.has(dtype)) return 'buffer';
170
- if (PAYLOAD_DTYPES.has(dtype)) return 'quantized';
171
- return 'full';
172
- }
173
-
174
- export function family(name) {
175
- for (const [pattern, label] of FAMILIES) if (pattern.test(name)) return label;
176
- return 'other linear weights';
177
- }
178
-
179
- /**
180
- * `meta.quantizer` out of a nested object, tolerating a list value:
181
- * GPTQModel writes that field as a bare string in some releases and a
182
- * one-element array in others, and both are in the wild.
183
- */
184
- export function dig(obj, path) {
185
- let cur = obj;
186
- for (const part of path.split('.')) {
187
- if (cur === null || typeof cur !== 'object' || Array.isArray(cur)) return null;
188
- cur = cur[part];
189
- if (cur === undefined) return null;
190
- }
191
- if (Array.isArray(cur)) return cur.length ? cur.map(String).join(', ') : null;
192
- return cur === undefined ? null : cur;
193
- }
194
-
195
- /**
196
- * The quantization method, from wherever the tool chose to record it.
197
- *
198
- * Never from the repo name: `cyankiwi/Qwen3-VL-8B-Instruct-AWQ-4bit`
199
- * declares `compressed-tensors`, and a reader that trusted the name would
200
- * apply the wrong convention to it.
201
- */
202
- export function declaredMethod(cfg, hfQuant) {
203
- let q = cfg?.quantization_config;
204
- if (!q && cfg?.text_config && typeof cfg.text_config === 'object') {
205
- q = cfg.text_config.quantization_config;
206
- }
207
- const m = q?.quant_method;
208
- if (m) return String(m);
209
- if (hfQuant) return 'modelopt';
210
- return 'none';
211
- }
212
-
213
- /**
214
- * Fold a list of `{name, dtype, shape}` into the report.
215
- *
216
- * Split out from any fetching so the parity gate can run it on fixtures
217
- * and so the same arithmetic serves whichever transport reads the header.
218
- */
219
- export function summarize(tensors, method) {
220
- const spec = FORMATS[method] || null;
221
- const buckets = { quantized: 0, metadata: 0, full: 0, buffer: 0 };
222
- const families = {};
223
- const dtypeBytes = {};
224
- const unknownDtypes = new Set();
225
- let headQuantized = false;
226
-
227
- for (const t of tensors) {
228
- if (!(t.dtype in DTYPE_BYTES)) unknownDtypes.add(t.dtype);
229
- let n = 1;
230
- for (const d of t.shape) n *= d;
231
- const size = n * (DTYPE_BYTES[t.dtype] ?? 2);
232
- const kind = classify(t.name, t.dtype);
233
- buckets[kind] += size;
234
- dtypeBytes[t.dtype] = (dtypeBytes[t.dtype] || 0) + size;
235
- if (kind === 'full') {
236
- const fam = family(t.name);
237
- families[fam] = (families[fam] || 0) + size;
238
- } else if (kind === 'quantized' && HEAD_RE.test(t.name)) {
239
- headQuantized = true;
240
- }
241
- }
242
-
243
- const total = Object.values(buckets).reduce((a, b) => a + b, 0) || 1;
244
- // Largest first, ties broken by name. The tie is not hypothetical: an
245
- // untied model's embeddings and output head are the same tensor
246
- // transposed, so their byte counts are equal exactly. Shards are read
247
- // concurrently, so without the second key those two rows would swap
248
- // between runs of the same pack — and a report that changes when
249
- // nothing changed is a report nobody can quote.
250
- //
251
- // Compared by code point, not localeCompare: Python's `sorted` orders
252
- // by code point, and a locale-aware comparison disagrees with it on
253
- // case and punctuation. The parity gate can only catch that when a
254
- // fixture happens to contain a tie, so the two are made to agree by
255
- // construction instead of by luck.
256
- const byName = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
257
- const sortDesc = (o) => Object.fromEntries(
258
- Object.entries(o).sort((a, b) => b[1] - a[1] || byName(a[0], b[0])));
259
-
260
- return {
261
- method,
262
- tool: spec ? spec.tool : null,
263
- verifiedOn: spec ? spec.verifiedOn : null,
264
- note: spec ? spec.note : null,
265
- totalBytes: total,
266
- buckets,
267
- dtypeBytes: sortDesc(dtypeBytes),
268
- fullByFamily: sortDesc(families),
269
- fullPct: (buckets.full / total) * 100,
270
- headQuantized,
271
- unknownDtypes: [...unknownDtypes].sort(),
272
- // A declared method with no payload found means this reader does not
273
- // understand that tool's layout — not that the pack is unquantized.
274
- // Saying so is the one guard that keeps a future convention change
275
- // from producing a confident wrong answer.
276
- recognised: buckets.quantized > 0 || method === 'none',
277
- knownFormat: spec !== null || method === 'none',
278
- };
279
- }
280
-
281
- // ------------------------------------------------------------- transport
282
-
283
- const HF = 'https://huggingface.co';
284
-
285
- async function json(url) {
286
- const r = await fetch(url);
287
- if (!r.ok) return null;
288
- try { return await r.json(); } catch { return null; }
289
- }
290
-
291
- /**
292
- * Read one safetensors file's header without downloading the file.
293
- *
294
- * The format opens with a little-endian u64 giving the header length,
295
- * then that many bytes of JSON describing every tensor. Two ranged
296
- * requests fetch it — typically a few hundred kilobytes against shards
297
- * of many gigabytes, which is what makes this possible from a browser
298
- * at all.
299
- */
300
- export async function readHeader(repoId, file) {
301
- const url = `${HF}/${repoId}/resolve/main/${file}`;
302
- const head = await fetch(url, { headers: { Range: 'bytes=0-7' } });
303
- if (!head.ok) throw new Error(`cannot read ${file} (HTTP ${head.status})`);
304
- const len = Number(new DataView(await head.arrayBuffer()).getBigUint64(0, true));
305
- if (!Number.isFinite(len) || len <= 0 || len > 200_000_000) {
306
- throw new Error(`${file} does not look like a safetensors file`);
307
- }
308
- const body = await fetch(url, { headers: { Range: `bytes=8-${7 + len}` } });
309
- if (!body.ok) throw new Error(`cannot read ${file} header (HTTP ${body.status})`);
310
- const parsed = JSON.parse(new TextDecoder().decode(await body.arrayBuffer()));
311
-
312
- const tensors = [];
313
- for (const [name, info] of Object.entries(parsed)) {
314
- if (name === '__metadata__') continue;
315
- tensors.push({ name, dtype: info.dtype, shape: info.shape || [] });
316
- }
317
- return tensors;
318
- }
319
-
320
- /** Which safetensors files a repo publishes, sharded or not. */
321
- export async function shardList(repoId) {
322
- const index = await json(`${HF}/${repoId}/resolve/main/model.safetensors.index.json`);
323
- if (index?.weight_map) return [...new Set(Object.values(index.weight_map))];
324
- const head = await fetch(`${HF}/${repoId}/resolve/main/model.safetensors`,
325
- { headers: { Range: 'bytes=0-7' } });
326
- if (head.ok) return ['model.safetensors'];
327
- return [];
328
- }
329
-
330
- /** Everything the page needs about one repo. */
331
- export async function inspect(repoId, onProgress = () => {}) {
332
- const cfg = await json(`${HF}/${repoId}/resolve/main/config.json`);
333
- if (!cfg) throw new Error('no config.json — gated, private, or not a model repo');
334
- const hfQuant = await json(`${HF}/${repoId}/resolve/main/hf_quant_config.json`);
335
- const method = declaredMethod(cfg, hfQuant);
336
-
337
- let producerVersion = null;
338
- const spec = FORMATS[method];
339
- if (spec?.producer) {
340
- const [where, path] = spec.producer;
341
- const src = where === 'config' ? (cfg.quantization_config || {}) : (hfQuant || {});
342
- const v = dig(src, path);
343
- producerVersion = v === null || v === undefined ? null : String(v);
344
- }
345
-
346
- const files = await shardList(repoId);
347
- if (!files.length) {
348
- // A pack whose weights are a .pt or .bin cannot be read this way, and
349
- // must not be reported as unquantized.
350
- throw new Error('no safetensors weights published — torchao and HQQ '
351
- + 'packs often ship a .pt instead, and their contents cannot be read '
352
- + 'from metadata');
353
- }
354
-
355
- onProgress(0, files.length);
356
-
357
- // Read the shards a few at a time rather than one after another. A
358
- // 70B pack is nine shards and two ranged requests each; sequentially
359
- // that is long enough that the page reads as broken. Four at a time
360
- // is enough to hide most of the latency without opening a burst of
361
- // connections against the Hub for a page that is only reading
362
- // metadata.
363
- const tensors = [];
364
- let next = 0;
365
- let finished = 0;
366
- const worker = async () => {
367
- while (next < files.length) {
368
- const mine = files[next++];
369
- const got = await readHeader(repoId, mine);
370
- tensors.push(...got);
371
- onProgress(++finished, files.length);
372
- }
373
- };
374
- await Promise.all(
375
- Array.from({ length: Math.min(4, files.length) }, worker));
376
-
377
- return {
378
- repo: repoId,
379
- producerVersion,
380
- shards: files.length,
381
- tensorCount: tensors.length,
382
- ...summarize(tensors, method),
383
- };
384
- }
 
 
 
 
 
 
 
1
+ /**
2
+ * What precision a pack is actually in — the browser half.
3
+ *
4
+ * The JavaScript port of `tools/quantize/precision_map.py`. Both are held
5
+ * together by `parity.mjs`, which runs real tensor names harvested from
6
+ * nine toolchains through each and diffs. The rules took a wrong answer
7
+ * to get right, and a port that drifted from them would bring it back
8
+ * silently.
9
+ *
10
+ * The wrong answer is worth stating, because it is the whole reason this
11
+ * file classifies the way it does. The first version keyed on tensor
12
+ * NAMES, learned from compressed-tensors packs where the payload is
13
+ * called `weight_packed`. Probing the ecosystem showed that convention is
14
+ * the minority: bitsandbytes, ModelOpt FP8, ModelOpt NVFP4 and
15
+ * compressed-tensors' own int-quantized and naive-quantized formats all
16
+ * store the quantized payload under the plain name `weight`. A
17
+ * name-driven reader reports every one of those packs as 100% full
18
+ * precision — confidently, and about most of what is published.
19
+ *
20
+ * So dtype is the primary evidence here. Names are used only to separate
21
+ * quantization metadata from model weights, a distinction that genuinely
22
+ * has no dtype signature.
23
+ */
24
+
25
+ /** Bytes per element, keyed by the safetensors dtype string. */
26
+ export const DTYPE_BYTES = {
27
+ BF16: 2, F16: 2, F32: 4, F64: 8, F8_E4M3: 1, F8_E5M2: 1, F4: 1,
28
+ I8: 1, U8: 1, I16: 2, I32: 4, I64: 8, BOOL: 1,
29
+ };
30
+
31
+ /**
32
+ * A tensor in one of these dtypes is carrying quantized payload. Every
33
+ * packing scheme measured lands here: compressed-tensors packs into I32,
34
+ * GPTQ/AWQ/AutoRound into I32, bitsandbytes and MXFP4 and NVFP4 into U8,
35
+ * FP8 into F8_E4M3, int8 into I8.
36
+ */
37
+ export const PAYLOAD_DTYPES = new Set(
38
+ ['I32', 'I8', 'U8', 'F8_E4M3', 'F8_E5M2', 'F4', 'I16']);
39
+
40
+ /**
41
+ * Excluded on purpose: index buffers and masks (`position_ids`, causal
42
+ * masks, `weight_shape`), never packed weights.
43
+ */
44
+ export const BUFFER_DTYPES = new Set(['I64', 'BOOL']);
45
+
46
+ /**
47
+ * Quantization metadata: scales, zero points, group permutations, the
48
+ * stored original shape. Real bytes, reported as their own category —
49
+ * counting them as full precision would overstate what stayed whole, and
50
+ * counting them as payload would hide the overhead.
51
+ *
52
+ * One union across all toolchains rather than a table per format: these
53
+ * names do not collide, so a reader that misidentifies the format still
54
+ * gets the metadata right.
55
+ */
56
+ export const METADATA_SUFFIXES = [
57
+ // compressed-tensors
58
+ 'weight_scale', 'weight_shape', 'weight_zero_point', 'weight_g_idx',
59
+ 'input_scale', 'output_scale',
60
+ // gptq / awq / autoround
61
+ 'qzeros', 'scales', 'g_idx',
62
+ // bitsandbytes — double quantization, so the scales are themselves quantized
63
+ 'absmax', 'quant_map', 'nested_absmax', 'nested_quant_map',
64
+ 'quant_state.bitsandbytes__nf4', 'quant_state.bitsandbytes__fp4',
65
+ // ModelOpt / native fp8 + fp4
66
+ 'weight_scale_2', 'k_scale', 'v_scale', 'q_scale', 'prob_scale',
67
+ // mxfp4
68
+ '_scales',
69
+ ];
70
+
71
+ /** Formats that give the payload its own name; the rest reuse `weight`. */
72
+ export const PAYLOAD_SUFFIXES = ['weight_packed', 'qweight', '_blocks'];
73
+
74
+ /**
75
+ * Every format here was verified by reading a real published pack of it.
76
+ *
77
+ * `producer` says where that pack records the version of the tool that
78
+ * wrote it, and `null` means the tool records nothing — which is worth
79
+ * reporting rather than hiding, since it tells the reader no version
80
+ * evidence exists for those bytes.
81
+ *
82
+ * AutoAWQ is deliberately `null` even though its config has a `version`
83
+ * field: that field holds `"gemm"`, the kernel variant. Printing it as a
84
+ * release number would be a small invented fact.
85
+ */
86
+ export const FORMATS = {
87
+ 'compressed-tensors': {
88
+ tool: 'llm-compressor / compressed-tensors',
89
+ producer: ['config', 'version'],
90
+ verifiedOn: 'aleada/Qwen3.8-27B-W4A16 (0.17.1), RedHatAI w8a8, RedHatAI FP8',
91
+ note: 'pack-quantized names the payload weight_packed; int-quantized and naive-quantized reuse `weight`',
92
+ },
93
+ gptq: {
94
+ tool: 'GPTQModel / AutoGPTQ',
95
+ producer: ['config', 'meta.quantizer'],
96
+ verifiedOn: 'ModelCloud vortex-v3 (gptqmodel:1.4.4), TheBloke/Llama-2-7B-Chat-GPTQ',
97
+ note: 'qweight + qzeros + scales + g_idx',
98
+ },
99
+ awq: {
100
+ tool: 'AutoAWQ',
101
+ producer: null,
102
+ verifiedOn: 'casperhansen/llama-3-8b-instruct-awq, Qwen/Qwen3-8B-AWQ',
103
+ note: 'qweight + qzeros + scales, no g_idx',
104
+ },
105
+ 'auto-round': {
106
+ tool: 'Intel AutoRound',
107
+ producer: ['config', 'autoround_version'],
108
+ verifiedOn: 'OPEA/Qwen2.5-7B-Instruct-int4-sym-inc (0.4.0.dev)',
109
+ note: 'exports in gptq or compressed-tensors layout',
110
+ },
111
+ 'intel/auto-round': {
112
+ tool: 'Intel AutoRound',
113
+ producer: ['config', 'autoround_version'],
114
+ verifiedOn: 'Intel/Qwen2-7B-int4-inc',
115
+ note: 'same layout, older method spelling',
116
+ },
117
+ bitsandbytes: {
118
+ tool: 'bitsandbytes',
119
+ producer: null,
120
+ verifiedOn: 'unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit',
121
+ note: 'payload is U8 under the plain name `weight`; absmax/quant_map are the scales',
122
+ },
123
+ mxfp4: {
124
+ tool: 'MXFP4 (native export)',
125
+ producer: null,
126
+ verifiedOn: 'openai/gpt-oss-20b',
127
+ note: '*_blocks payload + *_scales, per fused expert tensor',
128
+ },
129
+ quark: {
130
+ tool: 'AMD Quark',
131
+ producer: null,
132
+ verifiedOn: 'amd/Qwen3.8-27B-Quark-AWQ-INT4-W4A16',
133
+ note: 'payload is `weight` at I32, with weight_scale and an I32 weight_zero_point',
134
+ },
135
+ modelopt: {
136
+ tool: 'NVIDIA TensorRT ModelOpt',
137
+ producer: ['hf_quant_config', 'producer.version'],
138
+ verifiedOn: 'nvidia/Llama-3.3-70B-Instruct-FP4 (0.23.0), nvidia/Llama-3.1-8B-Instruct-FP8',
139
+ note: 'config.json declares nothing — the method lives in hf_quant_config.json',
140
+ },
141
+ };
142
+
143
+ export const RULES_MEASURED_ON = '2026-09-07';
144
+
145
+ /**
146
+ * Which module a full-precision tensor belongs to. Ordered: the first
147
+ * match wins, so specific patterns precede general ones.
148
+ */
149
+ export const FAMILIES = [
150
+ [/(^|\.)mtp\.|(^|\.)draft/, 'auxiliary head (MTP / draft)'],
151
+ [/(^|\.)(lm_head|output_layer)\b/, 'output head (lm_head)'],
152
+ [/embed|embeddings?\b|wte\b/, 'embeddings'],
153
+ [/vision_tower|vision_model|visual|image_encoder|patch_embed/, 'vision tower'],
154
+ [/multi_modal_projector|mm_projector|merger/, 'multimodal projector'],
155
+ [/norm|layernorm|rmsnorm/, 'norms'],
156
+ [/A_log|dt_bias|conv1d|\.D$/, 'state-space parameters'],
157
+ [/(^|\.)(gate|router)\b|e_score_correction|sinks/, 'routers and sinks'],
158
+ [/rotary|inv_freq|position/, 'position buffers'],
159
+ [/\.bias$/, 'biases'],
160
+ [/experts?\./, 'experts (left unquantized)'],
161
+ ];
162
+
163
+ const HEAD_RE = /lm_head|output_layer/;
164
+
165
+ /**
166
+ * quantized | metadata | full | buffer.
167
+ *
168
+ * Name first, but only to pull metadata out — a scale is a scale whatever
169
+ * its dtype. Then dtype, which is what actually decides whether a tensor
170
+ * holds full-precision weights.
171
+ */
172
+ export function classify(name, dtype) {
173
+ for (const suf of METADATA_SUFFIXES) if (name.endsWith(suf)) return 'metadata';
174
+ for (const suf of PAYLOAD_SUFFIXES) if (name.endsWith(suf)) return 'quantized';
175
+ if (BUFFER_DTYPES.has(dtype)) return 'buffer';
176
+ if (PAYLOAD_DTYPES.has(dtype)) return 'quantized';
177
+ return 'full';
178
+ }
179
+
180
+ export function family(name) {
181
+ for (const [pattern, label] of FAMILIES) if (pattern.test(name)) return label;
182
+ return 'other linear weights';
183
+ }
184
+
185
+ /**
186
+ * `meta.quantizer` out of a nested object, tolerating a list value:
187
+ * GPTQModel writes that field as a bare string in some releases and a
188
+ * one-element array in others, and both are in the wild.
189
+ */
190
+ export function dig(obj, path) {
191
+ let cur = obj;
192
+ for (const part of path.split('.')) {
193
+ if (cur === null || typeof cur !== 'object' || Array.isArray(cur)) return null;
194
+ cur = cur[part];
195
+ if (cur === undefined) return null;
196
+ }
197
+ if (Array.isArray(cur)) return cur.length ? cur.map(String).join(', ') : null;
198
+ return cur === undefined ? null : cur;
199
+ }
200
+
201
+ /**
202
+ * The quantization method, from wherever the tool chose to record it.
203
+ *
204
+ * Never from the repo name: `cyankiwi/Qwen3-VL-8B-Instruct-AWQ-4bit`
205
+ * declares `compressed-tensors`, and a reader that trusted the name would
206
+ * apply the wrong convention to it.
207
+ */
208
+ export function declaredMethod(cfg, hfQuant) {
209
+ let q = cfg?.quantization_config;
210
+ if (!q && cfg?.text_config && typeof cfg.text_config === 'object') {
211
+ q = cfg.text_config.quantization_config;
212
+ }
213
+ const m = q?.quant_method;
214
+ if (m) return String(m);
215
+ if (hfQuant) return 'modelopt';
216
+ return 'none';
217
+ }
218
+
219
+ /**
220
+ * Fold a list of `{name, dtype, shape}` into the report.
221
+ *
222
+ * Split out from any fetching so the parity gate can run it on fixtures
223
+ * and so the same arithmetic serves whichever transport reads the header.
224
+ */
225
+ export function summarize(tensors, method) {
226
+ const spec = FORMATS[method] || null;
227
+ const buckets = { quantized: 0, metadata: 0, full: 0, buffer: 0 };
228
+ const families = {};
229
+ const dtypeBytes = {};
230
+ const unknownDtypes = new Set();
231
+ let headQuantized = false;
232
+
233
+ for (const t of tensors) {
234
+ if (!(t.dtype in DTYPE_BYTES)) unknownDtypes.add(t.dtype);
235
+ let n = 1;
236
+ for (const d of t.shape) n *= d;
237
+ const size = n * (DTYPE_BYTES[t.dtype] ?? 2);
238
+ const kind = classify(t.name, t.dtype);
239
+ buckets[kind] += size;
240
+ dtypeBytes[t.dtype] = (dtypeBytes[t.dtype] || 0) + size;
241
+ if (kind === 'full') {
242
+ const fam = family(t.name);
243
+ families[fam] = (families[fam] || 0) + size;
244
+ } else if (kind === 'quantized' && HEAD_RE.test(t.name)) {
245
+ headQuantized = true;
246
+ }
247
+ }
248
+
249
+ const total = Object.values(buckets).reduce((a, b) => a + b, 0) || 1;
250
+ // Largest first, ties broken by name. The tie is not hypothetical: an
251
+ // untied model's embeddings and output head are the same tensor
252
+ // transposed, so their byte counts are equal exactly. Shards are read
253
+ // concurrently, so without the second key those two rows would swap
254
+ // between runs of the same pack and a report that changes when
255
+ // nothing changed is a report nobody can quote.
256
+ //
257
+ // Compared by code point, not localeCompare: Python's `sorted` orders
258
+ // by code point, and a locale-aware comparison disagrees with it on
259
+ // case and punctuation. The parity gate can only catch that when a
260
+ // fixture happens to contain a tie, so the two are made to agree by
261
+ // construction instead of by luck.
262
+ const byName = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
263
+ const sortDesc = (o) => Object.fromEntries(
264
+ Object.entries(o).sort((a, b) => b[1] - a[1] || byName(a[0], b[0])));
265
+
266
+ return {
267
+ method,
268
+ tool: spec ? spec.tool : null,
269
+ verifiedOn: spec ? spec.verifiedOn : null,
270
+ note: spec ? spec.note : null,
271
+ totalBytes: total,
272
+ buckets,
273
+ dtypeBytes: sortDesc(dtypeBytes),
274
+ fullByFamily: sortDesc(families),
275
+ fullPct: (buckets.full / total) * 100,
276
+ headQuantized,
277
+ unknownDtypes: [...unknownDtypes].sort(),
278
+ // A declared method with no payload found means this reader does not
279
+ // understand that tool's layout — not that the pack is unquantized.
280
+ // Saying so is the one guard that keeps a future convention change
281
+ // from producing a confident wrong answer.
282
+ recognised: buckets.quantized > 0 || method === 'none',
283
+ knownFormat: spec !== null || method === 'none',
284
+ };
285
+ }
286
+
287
+ // ------------------------------------------------------------- transport
288
+
289
+ const HF = 'https://huggingface.co';
290
+
291
+ async function json(url) {
292
+ const r = await fetch(url);
293
+ if (!r.ok) return null;
294
+ try { return await r.json(); } catch { return null; }
295
+ }
296
+
297
+ /**
298
+ * Read one safetensors file's header without downloading the file.
299
+ *
300
+ * The format opens with a little-endian u64 giving the header length,
301
+ * then that many bytes of JSON describing every tensor. Two ranged
302
+ * requests fetch it typically a few hundred kilobytes against shards
303
+ * of many gigabytes, which is what makes this possible from a browser
304
+ * at all.
305
+ */
306
+ export async function readHeader(repoId, file) {
307
+ const url = `${HF}/${repoId}/resolve/main/${file}`;
308
+ const head = await fetch(url, { headers: { Range: 'bytes=0-7' } });
309
+ if (!head.ok) throw new Error(`cannot read ${file} (HTTP ${head.status})`);
310
+ const len = Number(new DataView(await head.arrayBuffer()).getBigUint64(0, true));
311
+ if (!Number.isFinite(len) || len <= 0 || len > 200_000_000) {
312
+ throw new Error(`${file} does not look like a safetensors file`);
313
+ }
314
+ const body = await fetch(url, { headers: { Range: `bytes=8-${7 + len}` } });
315
+ if (!body.ok) throw new Error(`cannot read ${file} header (HTTP ${body.status})`);
316
+ const parsed = JSON.parse(new TextDecoder().decode(await body.arrayBuffer()));
317
+
318
+ const tensors = [];
319
+ for (const [name, info] of Object.entries(parsed)) {
320
+ if (name === '__metadata__') continue;
321
+ tensors.push({ name, dtype: info.dtype, shape: info.shape || [] });
322
+ }
323
+ return tensors;
324
+ }
325
+
326
+ /** Which safetensors files a repo publishes, sharded or not. */
327
+ export async function shardList(repoId) {
328
+ const index = await json(`${HF}/${repoId}/resolve/main/model.safetensors.index.json`);
329
+ if (index?.weight_map) return [...new Set(Object.values(index.weight_map))];
330
+ const head = await fetch(`${HF}/${repoId}/resolve/main/model.safetensors`,
331
+ { headers: { Range: 'bytes=0-7' } });
332
+ if (head.ok) return ['model.safetensors'];
333
+ return [];
334
+ }
335
+
336
+ /** Everything the page needs about one repo. */
337
+ export async function inspect(repoId, onProgress = () => {}) {
338
+ const cfg = await json(`${HF}/${repoId}/resolve/main/config.json`);
339
+ if (!cfg) throw new Error('no config.json — gated, private, or not a model repo');
340
+ const hfQuant = await json(`${HF}/${repoId}/resolve/main/hf_quant_config.json`);
341
+ const method = declaredMethod(cfg, hfQuant);
342
+
343
+ let producerVersion = null;
344
+ const spec = FORMATS[method];
345
+ if (spec?.producer) {
346
+ const [where, path] = spec.producer;
347
+ const src = where === 'config' ? (cfg.quantization_config || {}) : (hfQuant || {});
348
+ const v = dig(src, path);
349
+ producerVersion = v === null || v === undefined ? null : String(v);
350
+ }
351
+
352
+ const files = await shardList(repoId);
353
+ if (!files.length) {
354
+ // A pack whose weights are a .pt or .bin cannot be read this way, and
355
+ // must not be reported as unquantized.
356
+ throw new Error('no safetensors weights published — torchao and HQQ '
357
+ + 'packs often ship a .pt instead, and their contents cannot be read '
358
+ + 'from metadata');
359
+ }
360
+
361
+ onProgress(0, files.length);
362
+
363
+ // Read the shards a few at a time rather than one after another. A
364
+ // 70B pack is nine shards and two ranged requests each; sequentially
365
+ // that is long enough that the page reads as broken. Four at a time
366
+ // is enough to hide most of the latency without opening a burst of
367
+ // connections against the Hub for a page that is only reading
368
+ // metadata.
369
+ const tensors = [];
370
+ let next = 0;
371
+ let finished = 0;
372
+ const worker = async () => {
373
+ while (next < files.length) {
374
+ const mine = files[next++];
375
+ const got = await readHeader(repoId, mine);
376
+ tensors.push(...got);
377
+ onProgress(++finished, files.length);
378
+ }
379
+ };
380
+ await Promise.all(
381
+ Array.from({ length: Math.min(4, files.length) }, worker));
382
+
383
+ return {
384
+ repo: repoId,
385
+ producerVersion,
386
+ shards: files.length,
387
+ tensorCount: tensors.length,
388
+ ...summarize(tensors, method),
389
+ };
390
+ }