File size: 9,102 Bytes
a436a4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
// Browser front end. Every request goes straight from the visitor to
// huggingface.co — this page has no backend and holds no token, so it
// reads exactly what the visitor could read themselves.

import {
  KNOWN_METHODS, EXCLUSION_FIELD, covers, moduleTree, modulePaths,
  resolveMethod, sameModule, unmatchedExclusions,
} from './pack_check.js';

const HF = 'https://huggingface.co';
const $ = (id) => document.getElementById(id);

// Tensor tails that mark a converted weight, used to compare a pack
// against its source by module rather than by tensor name.
const EXPECTED_UNQUANTIZED = new Set([
  'embed_tokens', 'lm_head', 'embed_out', 'A_log', 'dt_bias', 'conv1d',
]);

function expectedUnquantized(module) {
  const tail = module.split('.').pop();
  return EXPECTED_UNQUANTIZED.has(tail)
    || tail.endsWith('norm') || module.includes('.norm')
    || module.includes('embed')   // embeddings are not LinearBase
    || tail.includes('conv');     // convolutions are not either
}

const esc = (s) => String(s).replace(/[&<>]/g,
  (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));

async function json(url) {
  const r = await fetch(url);
  if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
  return r.json();
}

/** Tensor name -> shape, from safetensors headers only.

 *

 *  Two ranged requests per shard: eight bytes for the header length,

 *  then the header itself. A file of any size costs kilobytes. */
async function safetensorNames(repo) {
  const info = await json(`${HF}/api/models/${repo}`);
  const files = (info.siblings ?? [])
    .map((s) => s.rfilename)
    .filter((f) => f.endsWith('.safetensors'));

  const out = new Map();
  await Promise.all(files.map(async (file) => {
    const url = `${HF}/${repo}/resolve/main/${file}`;
    const head = await fetch(url, { headers: { Range: 'bytes=0-7' } });
    if (!head.ok) throw new Error(`${file}: ${head.status}`);
    const len = Number(new DataView(await head.arrayBuffer())
      .getBigUint64(0, true));
    const body = await fetch(url, {
      headers: { Range: `bytes=8-${8 + len - 1}` },
    });
    if (!body.ok) throw new Error(`${file}: ${body.status}`);
    for (const [name, entry] of Object.entries(JSON.parse(await body.text()))) {
      if (name !== '__metadata__') out.set(name, entry.shape ?? []);
    }
  }));
  return { names: out, info };
}

function declared(quant, method, module, entries) {
  if (covers(quant, method, module)) return true;
  return entries.some((e) => {
    const t = String(e);
    return !t.startsWith('re:') && !t.startsWith('-:') && !t.startsWith('+:')
      && sameModule(t, module);
  });
}

function card(kind, title, html) {
  return `<div class="card ${kind}"><h3>${esc(title)}</h3>${html}</div>`;
}

const list = (items, cap = 14) =>
  '<ul>' + items.slice(0, cap).map((i) => `<li>${esc(i)}</li>`).join('')
  + (items.length > cap ? `<li>… and ${items.length - cap} more</li>` : '')
  + '</ul>';

async function check(repo) {
  const { names, info } = await safetensorNames(repo);
  if (names.size === 0) {
    return card('problem', 'Nothing to read',
      '<p>This repo has no <code>.safetensors</code> files.</p>');
  }

  const config = await json(`${HF}/${repo}/resolve/main/config.json`);
  const quant = config.quantization_config ?? {};
  const method = resolveMethod(quant);
  const known = KNOWN_METHODS.has(method);
  const base = [].concat(info.cardData?.base_model ?? [])[0];
  const tied = Boolean(config.tie_word_embeddings
    ?? config.text_config?.tie_word_embeddings);

  const meta = `<div class="meta">

    <span><code>${esc(repo)}</code></span>

    <span>quantization: <code>${esc(method)}</code></span>

    <span>tensors: <code>${names.size.toLocaleString()}</code></span>

    ${base ? `<span>source: <code>${esc(base)}</code></span>` : ''}

  </div>`;

  if (Object.keys(quant).length === 0) {
    return meta + card('clear', 'Not a quantized pack',
      '<p>No <code>quantization_config</code>, so nothing here could have '
      + 'been quantized away.</p>');
  }
  if (!known) {
    return meta + card('check', `Format “${method}” is not implemented here`,
      '<p>The checks reproduce the exclusion rules of compressed-tensors, '
      + 'awq, gptq, bitsandbytes, modelopt and auto-round from vLLM\'s '
      + 'source. Yours is not among them, so they are skipped rather than '
      + 'guessed at.</p>');
  }

  const out = [];
  const tree = moduleTree(names.keys());

  // 1 — entries that name nothing
  const stray = unmatchedExclusions(quant, method, tree, tied);
  if (stray.length) {
    out.push(card('problem',
      `${stray.length} exclusion ${stray.length === 1 ? 'entry names' : 'entries name'} no module in this model`,
      '<p>They protect nothing, so whatever they were meant to keep was '
      + 'quantized:</p>' + list(stray)
      + '<p>A common cause is a vision tower: Llama and Pixtral call it '
      + '<code>vision_tower</code>, Qwen calls it <code>visual</code>.</p>'));
  }

  // 2 — modules that did not survive from the source
  if (base && base !== repo) {
    try {
      const src = await safetensorNames(base);
      const have = modulePaths(names.keys());
      const dropped = [...modulePaths(src.names.keys())]
        .filter((m) => !have.has(m)).sort();
      if (dropped.length) {
        const groups = {};
        for (const m of dropped) {
          const head = m.split('.')[0];
          groups[head] = (groups[head] ?? 0) + 1;
        }
        out.push(card('problem',
          `${dropped.length} modules of the source are absent here`,
          '<p><code>from_pretrained</code> does not materialise tensors the '
          + 'AutoModel class has no slot for, so they never reach the '
          + 'quantizer and never reach the artifact. For a '
          + 'multi-token-prediction head the symptom is 0% draft acceptance, '
          + 'with nothing in the logs.</p>'
          + list(Object.entries(groups)
            .sort((a, b) => b[1] - a[1])
            .map(([k, v]) => `${k}${v} modules`), 10)));
      }
    } catch (e) {
      out.push(card('check', 'Could not read the source model',
        `<p><code>${esc(base)}</code> is named as the base but could not be `
        + `read (${esc(e.message)}), so that check was skipped.</p>`));
    }
  }

  // 3 — present, unquantized, undeclared
  const entries = Object.keys(quant).length
    ? [].concat(quant[EXCLUSION_FIELD[method]] ?? []).flatMap(
        (v) => (typeof v === 'object' && v !== null ? Object.keys(v) : v))
    : [];
  const raw = quant[EXCLUSION_FIELD[method]];
  const entryList = Array.isArray(raw) ? raw
    : (raw && typeof raw === 'object' ? Object.keys(raw) : entries);
  const undeclared = [...new Set([...names.entries()]
    .filter(([n, shape]) => n.endsWith('.weight') && shape.length === 2)
    .map(([n]) => n.slice(0, -'.weight'.length))
    .filter((m) => !expectedUnquantized(m)
      && !declared(quant, method, m, entryList)))].sort();

  if (undeclared.length) {
    out.push(card('check',
      `${undeclared.length} modules are at source precision but not declared`,
      '<p>A runtime builds these quantized, looks for a packed weight, finds '
      + 'a plain one, and skips it — leaving the module randomly initialised '
      + 'while the model still answers correctly.</p>' + list(undeclared, 12)
      + '<p>This is a <strong>prediction</strong>, not a measurement. Confirm '
      + 'it by serving the pack and grepping the startup log for '
      + '<code>not found in params_dict</code>.</p>'));
  }

  if (!out.length) {
    out.push(card('clear', 'Nothing found',
      '<p>Every exclusion entry names a real module, no source module is '
      + 'missing, and nothing is left at source precision without being '
      + 'declared.</p>'));
  }
  return meta + out.join('');
}

async function submit(event) {
  event?.preventDefault();
  let repo = $('repo').value.trim().replace(/\/+$/, '');
  if (!repo) return;
  if (repo.startsWith('http')) repo = repo.split('huggingface.co/').pop();

  const out = $('out');
  const button = $('go');
  button.disabled = true;
  out.innerHTML = '<p class="spin">Reading headers…</p>';
  try {
    out.innerHTML = await check(repo);
  } catch (e) {
    out.innerHTML = card('problem', `Could not check ${repo}`,
      `<p><code>${esc(e.message)}</code></p><p>If the repo is gated or `
      + `private, this page cannot read it — it has no token.</p>`);
  } finally {
    button.disabled = false;
  }
}

$('form').addEventListener('submit', submit);
// Run once on load against the pre-filled example. A checker whose
// first screen is empty asks the visitor to trust it before it has
// shown them anything.
submit();
for (const b of document.querySelectorAll('[data-ex]')) {
  b.addEventListener('click', () => {
    $('repo').value = b.dataset.ex;
    submit();
  });
}