File size: 18,144 Bytes
19a0974
f3fd622
 
19a0974
f3fd622
 
 
 
 
 
 
 
afcfd33
 
 
 
 
 
 
 
 
f3fd622
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
afcfd33
f3fd622
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
afcfd33
f3fd622
 
 
 
afcfd33
f3fd622
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
afcfd33
 
f3fd622
 
afcfd33
 
f3fd622
 
 
 
 
afcfd33
f3fd622
 
 
 
 
 
afcfd33
 
 
f3fd622
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
afcfd33
f3fd622
 
afcfd33
f3fd622
 
 
3e2ce32
f3fd622
 
 
 
afcfd33
 
 
 
 
 
f3fd622
 
 
 
 
 
 
 
 
afcfd33
f3fd622
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
/* Vramwise UI. © 2026 CyberMax. All rights reserved. */
(function () {
  "use strict";
  var F = window.Vramwise;
  var HF = "https://huggingface.co";
  var $ = function (id) { return document.getElementById(id); };
  var PRESETS = [
    ["Qwen3 8B", "Qwen/Qwen3-8B"], ["Llama 3.1 8B", "meta-llama/Llama-3.1-8B-Instruct"], ["gpt-oss 20B", "openai/gpt-oss-20b"],
    ["Qwen3 30B-A3B", "Qwen/Qwen3-30B-A3B"], ["Gemma 3 27B", "google/gemma-3-27b-it"], ["Mistral Small 24B", "mistralai/Mistral-Small-3.2-24B-Instruct-2506"],
    ["Qwen3 32B", "Qwen/Qwen3-32B"], ["Llama 3.3 70B", "meta-llama/Llama-3.3-70B-Instruct"], ["gpt-oss 120B", "openai/gpt-oss-120b"],
    ["DeepSeek V3", "deepseek-ai/DeepSeek-V3"]
  ];
  // One-pass decision / scoring models (no text generation). Ids checked on the Hugging Face API 15 Oct 2026; sizes are
  // always read live from each repo, never stored here.
  var DECISION_PRESETS = [
    ["MATILDA-jev v1", "Maincode/matilda-jev-v1"], ["MATILDA-jev v1.5", "Maincode/matilda-jev-v1.5"],
    ["MATILDA-jev FP4", "Maincode/matilda-jev-fp4"], ["Matilda Decision v1", "Maincode/matilda-decision-v1"],
    ["JEV 9B", "autotrust/JEV-9B"], ["JEV 27B", "autotrust/JEV-27B"], ["JEV 35B (MoE)", "autotrust/JEV-35B"],
    ["JEV 35B NVFP4", "autotrust/JEV-35B-NVFP4"], ["pplx-decider 27B", "perplexity-ai/pplx-decider-v1.1-27b"],
    ["Decider 2B", "Mapika/decider-2b"], ["Decider 4B", "Mapika/decider-4b"]
  ];
  var CTX = [2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144];
  var model = null;   // describe() output, possibly edited
  var kind = "gpu";
  var lastRows = [];
  var loadSeq = 0;

  function toast(msg) { var t = $("toast"); t.textContent = msg; t.classList.add("show"); setTimeout(function () { t.classList.remove("show"); }, 1800); }
  function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]; }); }
  function opt(v, t, sel) { var o = document.createElement("option"); o.value = v; o.textContent = t; if (sel) o.selected = true; return o; }
  function save() { try { localStorage.setItem("fitcheck", JSON.stringify(state())); } catch (e) {} }
  function load() { try { return JSON.parse(localStorage.getItem("fitcheck") || "{}"); } catch (e) { return {}; } }

  // ---------- hardware controls ----------
  F.GPUS.forEach(function (g) { $("gpu").appendChild(opt(g.name, g.name, g.name === "RTX 4090 24GB")); });
  F.APPLE.forEach(function (a) { $("chip").appendChild(opt(a.name, a.name, a.name === "M4 Pro")); });
  F.UNIFIED.forEach(function (u) { $("unified").appendChild(opt(u.name, u.name)); });
  CTX.forEach(function (c) { $("ctx").appendChild(opt(c, F.fmtCtx(c) + " tokens", c === 8192)); });
  function fillMacMem(pref) {
    var chip = F.APPLE.filter(function (a) { return a.name === $("chip").value; })[0];
    var sel = $("mac-mem"); sel.innerHTML = "";
    chip.mem.forEach(function (m) { sel.appendChild(opt(m, m + " GB", String(m) === String(pref))); });
    if (!sel.value || sel.selectedIndex < 0) sel.selectedIndex = sel.options.length - 1;
  }
  fillMacMem(48);

  function setKind(k) {
    kind = k;
    document.querySelectorAll(".seg button").forEach(function (b) { b.setAttribute("aria-checked", String(b.dataset.kind === k)); });
    document.querySelectorAll("[data-hw]").forEach(function (d) { d.hidden = d.dataset.hw !== k; });
    render();
  }
  document.querySelectorAll(".seg button").forEach(function (b) { b.addEventListener("click", function () { setKind(b.dataset.kind); }); });

  function hardware() {
    if (kind === "mac") return F.appleHardware($("chip").value, Number($("mac-mem").value), $("mac-raised").checked);
    if (kind === "unified") return F.unifiedHardware($("unified").value);
    if (kind === "custom") return { label: "Custom " + $("c-mem").value + " GB", usable: Number($("c-mem").value) || 0, bw: Number($("c-bw").value) || 1, ramGB: 0 };
    return F.gpuHardware($("gpu").value, $("gpu-count").value, $("ram").value, $("ram-bw").value);
  }

  // ---------- model loading ----------
  function getJSON(url, retried) {
    return fetch(url).then(function (r) {
      if (r.status === 429 && !retried) return new Promise(function (ok) { setTimeout(ok, 2500); }).then(function () { return getJSON(url, true); });
      if (!r.ok) { var e = new Error("HTTP " + r.status); e.status = r.status; throw e; }
      return r.json();
    });
  }
  function configFor(ids) {
    // Try each repo's config.json in turn (gated repos return 401).
    var i = 0;
    function next() {
      if (i >= ids.length) return Promise.resolve(null);
      var id = ids[i++];
      return getJSON(HF + "/" + id + "/resolve/main/config.json").then(function (c) { c.__from = id; return c; }, next);
    }
    return next();
  }
  function baseModels(meta) {
    var b = meta.cardData && meta.cardData.base_model;
    return (Array.isArray(b) ? b : b ? [b] : []).filter(function (x) { return typeof x === "string" && x.indexOf("/") > 0; });
  }

  function loadModel(raw) {
    var id = F.parseModelId(raw);
    if (!id) { info('<span class="err">Enter a model id like <b>Qwen/Qwen3-8B</b> or paste its Hugging Face URL.</span>'); return Promise.resolve(); }
    $("model").value = id;
    var seq = ++loadSeq;
    info("Reading <b>" + esc(id) + "</b> from Hugging Face…");
    return getJSON(HF + "/api/models/" + id + "?expand[]=safetensors&expand[]=gguf&expand[]=config&expand[]=gated&expand[]=cardData&expand[]=pipeline_tag&expand[]=tags")
      .then(function (meta) {
        var name = id.split("/")[1], org = id.split("/")[0];
        // Gated repos (Llama, Gemma) refuse config.json without a login: use a public mirror of the same weights.
        var tries = meta.gated ? [] : [id];
        baseModels(meta).forEach(function (b) { if (!(meta.gated && b.split("/")[0] === org)) tries.push(b); });
        if (meta.gated || !tries.length || !(meta.safetensors || meta.config)) tries.push("unsloth/" + name);
        return configFor(tries).then(function (cfg) { return { meta: meta, cfg: cfg }; });
      })
      .then(function (x) {
        if (seq !== loadSeq) return;
        if (!x.meta.id) x.meta.id = id;
        model = F.describe(x.meta, x.cfg);
        model.configFrom = x.cfg && x.cfg.__from;
        fillAdv();
        showInfo(x.meta);
        render();
        save();
      })
      .catch(function (e) {
        if (seq !== loadSeq) return;
        info('<span class="err">' + (e.status === 404 || e.status === 401 ? "Model not found (or private): <b>" + esc(id) + "</b>. Check the spelling, or fill in the model details below." : "Couldn't reach Hugging Face (" + esc(e.message) + "). Try again, or fill in the model details below.") + "</span>");
        $("adv").open = true;
      });
  }
  function info(html) { $("model-info").innerHTML = html; }
  function showInfo(meta) {
    var m = model, tags = [];
    tags.push(F.fmtParams(m.params) + " parameters");
    if (m.decision) tags.push("one-pass model · no text generation");
    if (m.moe) tags.push("MoE · " + F.fmtParams(m.activeParams) + " active" + (m.experts ? " · " + m.topK + " of " + m.experts + " experts" : ""));
    if (m.layers) tags.push(m.layers + " layers");
    if (m.mla) tags.push("MLA cache"); else if (m.kvHeads) tags.push(m.kvHeads + " KV heads");
    if (m.window) tags.push(m.slidingLayers + " sliding-window layers");
    if (m.linearLayers) tags.push(m.linearLayers + " linear-attention layers (fixed state)");
    if (m.maxContext) tags.push(F.fmtCtx(m.maxContext) + " max context");
    if (m.nativeLabel) tags.push("files: " + m.nativeLabel);
    var note = "";
    if (m.configFrom && m.configFrom !== m.id) note = '<div class="hint">Architecture read from <a href="' + HF + "/" + esc(m.configFrom) + '" target="_blank" rel="noopener">' + esc(m.configFrom) + "</a>" + (m.gated ? " because the original repo is gated." : ".") + "</div>";
    if (!m.hasShape) note += '<div class="hint err">Couldn\'t read layers/KV heads, so the KV cache is not counted. Fill them in under Model details.</div>';
    if (!m.params) note += '<div class="hint err">No parameter count published for this repo. Enter it under Model details.</div>';
    info('<a href="' + HF + "/" + esc(m.id) + '" target="_blank" rel="noopener"><b>' + esc(m.id) + '</b></a><div class="tags">' +
      tags.map(function (t) { return '<span class="tag">' + esc(t) + "</span>"; }).join("") + "</div>" + note);
  }
  function fillAdv() {
    $("m-params").value = model.params ? +(model.params / 1e9).toFixed(2) : "";
    $("m-active").value = model.activeParams ? +(model.activeParams / 1e9).toFixed(2) : "";
    $("m-layers").value = model.layers || "";
    $("m-kvheads").value = model.mla ? "" : model.kvHeads || "";
    $("m-headdim").value = model.mla ? "" : model.headDim || "";
    $("m-maxctx").value = model.maxContext || "";
    $("m-kvheads").disabled = $("m-headdim").disabled = !!model.mla;
  }
  function readAdv() {
    if (!model) model = F.describe({ id: "custom" }, {});
    var p = Number($("m-params").value) * 1e9, a = Number($("m-active").value) * 1e9;
    if (p) { if (model.nativeBytes && model.params) model.nativeBytes *= p / model.params; model.params = p; }
    model.activeParams = a && a <= model.params ? a : model.params;
    model.moe = model.activeParams < model.params * 0.97;
    model.layers = Number($("m-layers").value) || 0;
    if (!model.mla) { model.kvHeads = Number($("m-kvheads").value) || 0; model.headDim = Number($("m-headdim").value) || 0; }
    model.fullLayers = model.layers - (model.slidingLayers || 0);
    if (model.fullLayers < 0) { model.fullLayers = model.layers; model.slidingLayers = 0; }
    model.maxContext = Number($("m-maxctx").value) || 0;
    model.hasShape = !!(model.layers && (model.mla || (model.kvHeads && model.headDim)));
    render(); save();
  }
  ["m-params", "m-active", "m-layers", "m-kvheads", "m-headdim", "m-maxctx"].forEach(function (id) { $(id).addEventListener("change", readAdv); });

  // ---------- results ----------
  function render() {
    var tb = document.querySelector("#table tbody");
    if (!model || !model.params) { tb.innerHTML = ""; $("verdict").textContent = "Pick a model to see if it fits."; lastRows = []; return; }
    var hw = hardware(), ctx = Number($("ctx").value), kvBits = F.KV_TYPES[$("kv").value];
    var rows = F.quantRows(model).map(function (q) { return { q: q, r: F.estimate(model, hw, q, ctx, kvBits) }; });
    lastRows = rows;
    var best = rows.filter(function (x) { return x.r.fit === "yes"; })[0] || rows.filter(function (x) { return x.r.fit === "tight"; })[0];
    var off = rows.filter(function (x) { return x.r.fit === "offload"; })[0];
    var name = model.id.split("/").pop();
    var dec = !!model.decision;
    var ms = function (r) { var x = r.minMs || 0; return x >= 10 ? x.toFixed(0) : x.toFixed(1); };
    var v;
    if (best) v = "Yes: " + esc(name) + " runs on " + esc(hw.label) + " at <u>" + esc(best.q.label.split(" /")[0]) + "</u>" +
      "<small>" + F.fmtGB(best.r.total) + " GB of " + F.fmtGB(best.r.usable) + " GB usable at " + F.fmtCtx(ctx) + (dec ? " input tokens · one forward pass per call, at least " + ms(best.r) + " ms each (memory floor)" : " context · about " + Math.round(best.r.tpsLo) + "–" + Math.round(best.r.tpsHi) + " tokens/s") + "</small>";
    else if (off) v = "Only with CPU offload: " + esc(name) + " at " + esc(off.q.label.split(" /")[0]) + " on " + esc(hw.label) + "<small>" + Math.round(off.r.gpuShare * 100) + "% of the weights fit on the GPU; the rest runs from system RAM" + (dec ? ", so every call is slow." : " at roughly " + off.r.tpsLo.toFixed(1) + "–" + off.r.tpsHi.toFixed(1) + " tokens/s.") + "</small>";
    else v = "No: " + esc(name) + " doesn't fit " + esc(hw.label) + "<small>Even " + esc(rows[rows.length - 1].q.label) + " needs " + F.fmtGB(rows[rows.length - 1].r.total) + " GB and " + F.fmtGB(rows[rows.length - 1].r.usable) + " GB is usable. Try a shorter context, a smaller model or more memory.</small>";
    $("verdict").innerHTML = v;
    tb.innerHTML = rows.map(function (x) {
      var r = x.r, pct = Math.min(100, r.total / r.usable * 100);
      var fitTxt = { yes: "Fits", tight: "Tight", offload: "Offload", no: "No" }[r.fit];
      var speed = r.fit === "no" ? "—" : dec ? (r.minMs ? "≥ " + ms(r) + " ms" : "slow") : r.tpsHi >= 10 ? Math.round(r.tpsLo) + "–" + Math.round(r.tpsHi) : r.tpsLo.toFixed(1) + "–" + r.tpsHi.toFixed(1);
      return '<tr class="' + (best && x === best ? "best" : "") + '"><td><span class="q">' + esc(x.q.label) + '</span><span class="note">' + x.q.bits.toFixed(2).replace(/\.?0+$/, "") + " bits · " + esc(x.q.note) + "</span></td>" +
        '<td class="num">' + F.fmtGB(r.weights) + ' GB</td><td class="num">' + F.fmtGB(r.kv) + ' GB</td>' +
        '<td class="num">' + F.fmtGB(r.total) + ' GB<div class="meter"><i class="' + (r.total > r.usable ? "over" : "") + '" style="width:' + pct.toFixed(0) + '%"></i></div></td>' +
        '<td><span class="pill ' + r.fit + '">' + fitTxt + "</span></td>" +
        '<td class="num">' + (r.maxCtx ? F.fmtCtx(r.maxCtx) : "—") + '</td><td class="num">' + speed + "</td></tr>";
    }).join("");
    $("th-speed").textContent = dec ? "Per call" : "Speed (tok/s)";
    $("th-ctx").textContent = dec ? "Max input" : "Max context";
    $("decision-note").hidden = !dec;
    $("legend").textContent = hw.label + ": " + F.fmtGB(hw.usable * F.GB) + " GB usable" + (hw.unified ? " (unified memory)" : "") + ", " + hw.bw + " GB/s." + (kind === "gpu" && hw.ramGB ? " Offload uses up to " + Math.round(hw.ramGB * 0.85) + " GB of system RAM." : "");
    syncUrl();
  }
  ["gpu", "gpu-count", "ram", "ram-bw", "mac-mem", "mac-raised", "unified", "c-mem", "c-bw", "ctx", "kv"].forEach(function (id) {
    $(id).addEventListener("change", function () { render(); save(); });
    $(id).addEventListener("input", function () { if ($(id).type === "number") render(); });
  });
  $("chip").addEventListener("change", function () { fillMacMem($("mac-mem").value); render(); save(); });

  // ---------- state, share, CSV ----------
  function state() {
    return { model: model ? model.id : $("model").value, kind: kind, gpu: $("gpu").value, n: $("gpu-count").value, ram: $("ram").value, rbw: $("ram-bw").value,
      chip: $("chip").value, mem: $("mac-mem").value, raised: $("mac-raised").checked ? 1 : 0, uni: $("unified").value, cm: $("c-mem").value, cbw: $("c-bw").value,
      ctx: $("ctx").value, kv: $("kv").value };
  }
  function applyState(s) {
    if (s.kind) kind = s.kind;
    [["gpu", "gpu"], ["n", "gpu-count"], ["ram", "ram"], ["rbw", "ram-bw"], ["chip", "chip"], ["uni", "unified"], ["cm", "c-mem"], ["cbw", "c-bw"], ["ctx", "ctx"], ["kv", "kv"]].forEach(function (p) {
      if (s[p[0]] !== undefined && s[p[0]] !== null && s[p[0]] !== "") $(p[1]).value = s[p[0]];
    });
    fillMacMem(s.mem || 48);
    $("mac-raised").checked = String(s.raised) === "1";
  }
  function syncUrl() {
    var s = state(), q = new URLSearchParams();
    Object.keys(s).forEach(function (k) { if (s[k] !== "" && s[k] !== undefined) q.set(k, s[k]); });
    history.replaceState(null, "", "?" + q.toString());
  }
  $("btn-share").addEventListener("click", function () {
    var url = location.href;
    (navigator.clipboard ? navigator.clipboard.writeText(url) : Promise.reject()).then(function () { toast("Link copied"); }, function () { prompt("Copy this link", url); });
  });
  $("btn-csv").addEventListener("click", function () {
    if (!lastRows.length) return;
    var hw = hardware();
    var lines = [["model", "hardware", "context", "quantization", "bits_per_weight", "weights_gb", "kv_cache_gb", "total_gb", "usable_gb", "fits", "max_context", "tok_s_low", "tok_s_high", "min_ms_per_call"].join(",")];
    lastRows.forEach(function (x) {
      var r = x.r;
      lines.push([model.id, '"' + hw.label + '"', $("ctx").value, '"' + x.q.label + '"', x.q.bits.toFixed(2), F.fmtGB(r.weights), F.fmtGB(r.kv), F.fmtGB(r.total), F.fmtGB(r.usable), r.fit, r.maxCtx || 0, model.decision ? "" : r.tpsLo.toFixed(1), model.decision ? "" : r.tpsHi.toFixed(1), model.decision && r.minMs ? r.minMs.toFixed(1) : ""].join(","));
    });
    var a = document.createElement("a");
    a.href = URL.createObjectURL(new Blob([lines.join("\n") + "\n"], { type: "text/csv" }));
    a.download = "vramwise-" + model.id.replace(/[^\w.-]+/g, "_") + ".csv";
    document.body.appendChild(a); a.click(); a.remove();
  });

  // ---------- model input ----------
  [[PRESETS, "presets"], [DECISION_PRESETS, "presets-decision"]].forEach(function (g) {
    g[0].forEach(function (p) {
      var b = document.createElement("button"); b.className = "chip"; b.textContent = p[0]; b.type = "button";
      b.addEventListener("click", function () { loadModel(p[1]); });
      $(g[1]).appendChild(b);
    });
  });
  $("btn-load").addEventListener("click", function () { loadModel($("model").value); });
  $("model").addEventListener("keydown", function (e) { if (e.key === "Enter") loadModel($("model").value); });
  var suggestTimer;
  $("model").addEventListener("input", function () {
    clearTimeout(suggestTimer);
    var q = $("model").value.trim();
    if (q.length < 3 || /\s/.test(q)) return;
    suggestTimer = setTimeout(function () {
      getJSON(HF + "/api/models?search=" + encodeURIComponent(q) + "&sort=downloads&limit=8").then(function (list) {
        var dl = $("model-suggest"); dl.innerHTML = "";
        list.forEach(function (m) { dl.appendChild(opt(m.id, m.id)); });
      }, function () {});
    }, 300);
  });

  // ---------- start ----------
  var params = new URLSearchParams(location.search);
  var init = {};
  params.forEach(function (v, k) { init[k] = v; });
  if (!init.model) init = load();
  applyState(init);
  setKind(kind);
  loadModel(init.model || "Qwen/Qwen3-8B");
  window.fitcheckApp = { loadModel: loadModel, rows: function () { return lastRows; }, model: function () { return model; } };
  if ("serviceWorker" in navigator && location.protocol === "https:") navigator.serviceWorker.register("sw.js").catch(function () {});
})();