CoolFace
Apppublic

aleada/pack-integrity-check

sourceHugging Faceapache-2.0updated 19d agoView on Hugging Face
0likes
app.js274 linesDownload Raw Back to root
1// Browser front end. Every request goes straight from the visitor to
2// huggingface.co — this page has no backend and holds no token, so it
3// reads exactly what the visitor could read themselves.
4
5import {
6  KNOWN_METHODS, EXCLUSION_FIELD, covers, moduleTree, modulePaths,
7  indexGaps, resolveMethod, sameModule, unmatchedExclusions,
8} from './pack_check.js';
9import { progress } from './progress.js';
10import { ensureNextStepStyles, nextSteps, repoFromUrl } from './tools.js';
11
12const HF = 'https://huggingface.co';
13const $ = (id) => document.getElementById(id);
14
15// A link from another tool carries the model over; arriving at an empty
16// form is what makes five tools feel like five tools instead of one.
17ensureNextStepStyles();
18const carried = repoFromUrl();
19if (carried) { $('repo').value = carried; }
20
21
22// Tensor tails that mark a converted weight, used to compare a pack
23// against its source by module rather than by tensor name.
24const EXPECTED_UNQUANTIZED = new Set([
25  'embed_tokens', 'lm_head', 'embed_out', 'A_log', 'dt_bias', 'conv1d',
26]);
27
28function expectedUnquantized(module) {
29  const tail = module.split('.').pop();
30  return EXPECTED_UNQUANTIZED.has(tail)
31    || tail.endsWith('norm') || module.includes('.norm')
32    || module.includes('embed')   // embeddings are not LinearBase
33    || tail.includes('conv');     // convolutions are not either
34}
35
36const esc = (s) => String(s).replace(/[&<>]/g,
37  (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
38
39async function json(url) {
40  const r = await fetch(url);
41  if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
42  return r.json();
43}
44
45/** Tensor name -> shape, from safetensors headers only.
46 *
47 *  Two ranged requests per shard: eight bytes for the header length,
48 *  then the header itself. A file of any size costs kilobytes. */
49async function safetensorNames(repo, pg = null) {
50  const info = await json(`${HF}/api/models/${repo}`);
51  const files = (info.siblings ?? [])
52    .map((s) => s.rfilename)
53    .filter((f) => f.endsWith('.safetensors'));
54
55  // The shard count is only knowable here, after the file listing — so
56  // this is where an indeterminate bar can become a real proportion.
57  if (pg) pg.total(files.length).start(`reading ${files.length} `
58    + `safetensors header${files.length === 1 ? '' : 's'} from ${repo}`);
59
60  const out = new Map();
61  await Promise.all(files.map(async (file) => {
62    const url = `${HF}/${repo}/resolve/main/${file}`;
63    const head = await fetch(url, { headers: { Range: 'bytes=0-7' } });
64    if (!head.ok) throw new Error(`${file}: ${head.status}`);
65    const len = Number(new DataView(await head.arrayBuffer())
66      .getBigUint64(0, true));
67    const body = await fetch(url, {
68      headers: { Range: `bytes=8-${8 + len - 1}` },
69    });
70    if (!body.ok) throw new Error(`${file}: ${body.status}`);
71    for (const [name, entry] of Object.entries(JSON.parse(await body.text()))) {
72      if (name !== '__metadata__') out.set(name, entry.shape ?? []);
73    }
74    if (pg) pg.step();
75  }));
76  return { names: out, info, files: (info.siblings ?? []).map((s) => s.rfilename) };
77}
78
79function declared(quant, method, module, entries) {
80  if (covers(quant, method, module)) return true;
81  return entries.some((e) => {
82    const t = String(e);
83    return !t.startsWith('re:') && !t.startsWith('-:') && !t.startsWith('+:')
84      && sameModule(t, module);
85  });
86}
87
88function card(kind, title, html) {
89  return `<div class="card ${kind}"><h3>${esc(title)}</h3>${html}</div>`;
90}
91
92const list = (items, cap = 14) =>
93  '<ul>' + items.slice(0, cap).map((i) => `<li>${esc(i)}</li>`).join('')
94  + (items.length > cap ? `<li>… and ${items.length - cap} more</li>` : '')
95  + '</ul>';
96
97async function check(repo, pg = null) {
98  const { names, info, files } = await safetensorNames(repo, pg);
99  if (names.size === 0) {
100    return card('problem', 'Nothing to read',
101      '<p>This repo has no <code>.safetensors</code> files.</p>');
102  }
103
104  const config = await json(`${HF}/${repo}/resolve/main/config.json`);
105  const quant = config.quantization_config ?? {};
106  const method = resolveMethod(quant);
107  const known = KNOWN_METHODS.has(method);
108  const base = [].concat(info.cardData?.base_model ?? [])[0];
109  const tied = Boolean(config.tie_word_embeddings
110    ?? config.text_config?.tie_word_embeddings);
111
112  const meta = `<div class="meta">
113    <span><code>${esc(repo)}</code></span>
114    <span>quantization: <code>${esc(method)}</code></span>
115    <span>tensors: <code>${names.size.toLocaleString()}</code></span>
116    ${base ? `<span>source: <code>${esc(base)}</code></span>` : ''}
117  </div>`;
118
119  if (Object.keys(quant).length === 0) {
120    return meta + card('clear', 'Not a quantized pack',
121      '<p>No <code>quantization_config</code>, so nothing here could have '
122      + 'been quantized away.</p>');
123  }
124  if (!known) {
125    return meta + card('check', `Format “${method}” is not implemented here`,
126      '<p>The checks reproduce the exclusion rules of compressed-tensors, '
127      + 'awq, gptq, bitsandbytes, modelopt and auto-round from vLLM\'s '
128      + 'source. Yours is not among them, so they are skipped rather than '
129      + 'guessed at.</p>');
130  }
131
132  const out = [];
133  const tree = moduleTree(names.keys());
134
135  // 0 — the index against the files. The only check here that holds
136  // for any model made by any tool: an index naming a shard the repo
137  // lacks is broken everywhere, because the loader resolves weights
138  // through it. Ours had exactly this before it was repaired.
139  if (files.includes('model.safetensors.index.json')) {
140    try {
141      const wm = (await json(`${HF}/${repo}/resolve/main/model.safetensors.index.json`)).weight_map ?? {};
142      const gaps = indexGaps(wm, names.keys(), files);
143      if (gaps.missing_tensors.length || gaps.missing_shards.length) {
144        out.push(card('problem', 'The index promises weights the repo does not have',
145          '<p>`model.safetensors.index.json` maps tensors to shards, and a '
146          + 'loader resolves every weight through it. These entries point at '
147          + 'nothing, so the pack cannot load:</p>'
148          + (gaps.missing_shards.length
149              ? '<p><strong>Missing shards:</strong></p>' + list(gaps.missing_shards, 6) : '')
150          + (gaps.missing_tensors.length
151              ? `<p><strong>${gaps.missing_tensors.length} tensors with no file:</strong></p>`
152                + list(gaps.missing_tensors, 8) : '')));
153      }
154    } catch { /* an unreadable index is reported by the checks below */ }
155  }
156
157  // 1 — entries that name nothing
158  const stray = unmatchedExclusions(quant, method, tree, tied);
159  if (stray.length) {
160    out.push(card('problem',
161      `${stray.length} exclusion ${stray.length === 1 ? 'entry names' : 'entries name'} no module in this model`,
162      '<p>They protect nothing, so whatever they were meant to keep was '
163      + 'quantized:</p>' + list(stray)
164      + '<p>A common cause is a vision tower: Llama and Pixtral call it '
165      + '<code>vision_tower</code>, Qwen calls it <code>visual</code>.</p>'));
166  }
167
168  // 2 — modules that did not survive from the source
169  if (base && base !== repo) {
170    try {
171      // The source model is a second read of the same size, and the
172      // reason this page can sit silent for twice as long as it looks
173      // like it should. Say so rather than letting the bar restart
174      // without explanation.
175      if (pg) pg.start(`reading the source model ${base}`);
176      const src = await safetensorNames(base, pg);
177      const have = modulePaths(names.keys());
178      const dropped = [...modulePaths(src.names.keys())]
179        .filter((m) => !have.has(m)).sort();
180      if (dropped.length) {
181        const groups = {};
182        for (const m of dropped) {
183          const head = m.split('.')[0];
184          groups[head] = (groups[head] ?? 0) + 1;
185        }
186        out.push(card('problem',
187          `${dropped.length} modules of the source are absent here`,
188          '<p><code>from_pretrained</code> does not materialise tensors the '
189          + 'AutoModel class has no slot for, so they never reach the '
190          + 'quantizer and never reach the artifact. For a '
191          + 'multi-token-prediction head the symptom is 0% draft acceptance, '
192          + 'with nothing in the logs.</p>'
193          + list(Object.entries(groups)
194            .sort((a, b) => b[1] - a[1])
195            .map(([k, v]) => `${k} — ${v} modules`), 10)));
196      }
197    } catch (e) {
198      out.push(card('check', 'Could not read the source model',
199        `<p><code>${esc(base)}</code> is named as the base but could not be `
200        + `read (${esc(e.message)}), so that check was skipped.</p>`));
201    }
202  }
203
204  // 3 — present, unquantized, undeclared
205  const entries = Object.keys(quant).length
206    ? [].concat(quant[EXCLUSION_FIELD[method]] ?? []).flatMap(
207        (v) => (typeof v === 'object' && v !== null ? Object.keys(v) : v))
208    : [];
209  const raw = quant[EXCLUSION_FIELD[method]];
210  const entryList = Array.isArray(raw) ? raw
211    : (raw && typeof raw === 'object' ? Object.keys(raw) : entries);
212  const undeclared = [...new Set([...names.entries()]
213    .filter(([n, shape]) => n.endsWith('.weight') && shape.length === 2)
214    .map(([n]) => n.slice(0, -'.weight'.length))
215    .filter((m) => !expectedUnquantized(m)
216      && !declared(quant, method, m, entryList)))].sort();
217
218  if (undeclared.length) {
219    out.push(card('check',
220      `${undeclared.length} modules are at source precision but not declared`,
221      '<p>A runtime builds these quantized, looks for a packed weight, finds '
222      + 'a plain one, and skips it — leaving the module randomly initialised '
223      + 'while the model still answers correctly.</p>' + list(undeclared, 12)
224      + '<p>This is a <strong>prediction</strong>, not a measurement. Confirm '
225      + 'it by serving the pack and grepping the startup log for '
226      + '<code>not found in params_dict</code>.</p>'));
227  }
228
229  if (!out.length) {
230    out.push(card('clear', 'Nothing found',
231      '<p>Every exclusion entry names a real module, no source module is '
232      + 'missing, and nothing is left at source precision without being '
233      + 'declared.</p>'));
234  }
235  return meta + out.join('');
236}
237
238async function submit(event) {
239  event?.preventDefault();
240  let repo = $('repo').value.trim().replace(/\/+$/, '');
241  if (!repo) return;
242  if (repo.startsWith('http')) repo = repo.split('huggingface.co/').pop();
243
244  const out = $('out');
245  const button = $('go');
246  button.disabled = true;
247  // Indeterminate until the file listing arrives: nothing before it says
248  // how many shards there are to read.
249  const pg = progress(out).start(`reading ${repo}`);
250  try {
251    const html = await check(repo, pg);
252    pg.done();
253    out.innerHTML = html + nextSteps('integrity', repo);
254  } catch (e) {
255    out.innerHTML = card('problem', `Could not check ${repo}`,
256      `<p><code>${esc(e.message)}</code></p><p>If the repo is gated or `
257      + `private, this page cannot read it — it has no token.</p>`);
258  } finally {
259    button.disabled = false;
260  }
261}
262
263$('form').addEventListener('submit', submit);
264// Run once on load against the pre-filled example. A checker whose
265// first screen is empty asks the visitor to trust it before it has
266// shown them anything.
267submit();
268for (const b of document.querySelectorAll('[data-ex]')) {
269  b.addEventListener('click', () => {
270    $('repo').value = b.dataset.ex;
271    submit();
272  });
273}
274