CyberMax commited on
Commit
f3fd622
·
verified ·
1 Parent(s): bde37c3

Deploy fitcheck to the hub

Browse files
.gitattributes CHANGED
@@ -34,3 +34,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  honeyfold/og.png filter=lfs diff=lfs merge=lfs -text
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  honeyfold/og.png filter=lfs diff=lfs merge=lfs -text
37
+ fitcheck/og.png filter=lfs diff=lfs merge=lfs -text
fitcheck/app.js ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Fitcheck UI. © 2026 CyberMax. All rights reserved. */
2
+ (function () {
3
+ "use strict";
4
+ var F = window.Fitcheck;
5
+ var HF = "https://huggingface.co";
6
+ var $ = function (id) { return document.getElementById(id); };
7
+ var PRESETS = [
8
+ ["Qwen3 8B", "Qwen/Qwen3-8B"], ["Llama 3.1 8B", "meta-llama/Llama-3.1-8B-Instruct"], ["gpt-oss 20B", "openai/gpt-oss-20b"],
9
+ ["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"],
10
+ ["Qwen3 32B", "Qwen/Qwen3-32B"], ["Llama 3.3 70B", "meta-llama/Llama-3.3-70B-Instruct"], ["gpt-oss 120B", "openai/gpt-oss-120b"],
11
+ ["DeepSeek V3", "deepseek-ai/DeepSeek-V3"]
12
+ ];
13
+ var CTX = [2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144];
14
+ var model = null; // describe() output, possibly edited
15
+ var kind = "gpu";
16
+ var lastRows = [];
17
+ var loadSeq = 0;
18
+
19
+ function toast(msg) { var t = $("toast"); t.textContent = msg; t.classList.add("show"); setTimeout(function () { t.classList.remove("show"); }, 1800); }
20
+ function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]; }); }
21
+ function opt(v, t, sel) { var o = document.createElement("option"); o.value = v; o.textContent = t; if (sel) o.selected = true; return o; }
22
+ function save() { try { localStorage.setItem("fitcheck", JSON.stringify(state())); } catch (e) {} }
23
+ function load() { try { return JSON.parse(localStorage.getItem("fitcheck") || "{}"); } catch (e) { return {}; } }
24
+
25
+ // ---------- hardware controls ----------
26
+ F.GPUS.forEach(function (g) { $("gpu").appendChild(opt(g.name, g.name, g.name === "RTX 4090 24GB")); });
27
+ F.APPLE.forEach(function (a) { $("chip").appendChild(opt(a.name, a.name, a.name === "M4 Pro")); });
28
+ F.UNIFIED.forEach(function (u) { $("unified").appendChild(opt(u.name, u.name)); });
29
+ CTX.forEach(function (c) { $("ctx").appendChild(opt(c, F.fmtCtx(c) + " tokens", c === 8192)); });
30
+ function fillMacMem(pref) {
31
+ var chip = F.APPLE.filter(function (a) { return a.name === $("chip").value; })[0];
32
+ var sel = $("mac-mem"); sel.innerHTML = "";
33
+ chip.mem.forEach(function (m) { sel.appendChild(opt(m, m + " GB", String(m) === String(pref))); });
34
+ if (!sel.value || sel.selectedIndex < 0) sel.selectedIndex = sel.options.length - 1;
35
+ }
36
+ fillMacMem(48);
37
+
38
+ function setKind(k) {
39
+ kind = k;
40
+ document.querySelectorAll(".seg button").forEach(function (b) { b.setAttribute("aria-checked", String(b.dataset.kind === k)); });
41
+ document.querySelectorAll("[data-hw]").forEach(function (d) { d.hidden = d.dataset.hw !== k; });
42
+ render();
43
+ }
44
+ document.querySelectorAll(".seg button").forEach(function (b) { b.addEventListener("click", function () { setKind(b.dataset.kind); }); });
45
+
46
+ function hardware() {
47
+ if (kind === "mac") return F.appleHardware($("chip").value, Number($("mac-mem").value), $("mac-raised").checked);
48
+ if (kind === "unified") return F.unifiedHardware($("unified").value);
49
+ if (kind === "custom") return { label: "Custom " + $("c-mem").value + " GB", usable: Number($("c-mem").value) || 0, bw: Number($("c-bw").value) || 1, ramGB: 0 };
50
+ return F.gpuHardware($("gpu").value, $("gpu-count").value, $("ram").value, $("ram-bw").value);
51
+ }
52
+
53
+ // ---------- model loading ----------
54
+ function getJSON(url, retried) {
55
+ return fetch(url).then(function (r) {
56
+ if (r.status === 429 && !retried) return new Promise(function (ok) { setTimeout(ok, 2500); }).then(function () { return getJSON(url, true); });
57
+ if (!r.ok) { var e = new Error("HTTP " + r.status); e.status = r.status; throw e; }
58
+ return r.json();
59
+ });
60
+ }
61
+ function configFor(ids) {
62
+ // Try each repo's config.json in turn (gated repos return 401).
63
+ var i = 0;
64
+ function next() {
65
+ if (i >= ids.length) return Promise.resolve(null);
66
+ var id = ids[i++];
67
+ return getJSON(HF + "/" + id + "/resolve/main/config.json").then(function (c) { c.__from = id; return c; }, next);
68
+ }
69
+ return next();
70
+ }
71
+ function baseModels(meta) {
72
+ var b = meta.cardData && meta.cardData.base_model;
73
+ return (Array.isArray(b) ? b : b ? [b] : []).filter(function (x) { return typeof x === "string" && x.indexOf("/") > 0; });
74
+ }
75
+
76
+ function loadModel(raw) {
77
+ var id = F.parseModelId(raw);
78
+ 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(); }
79
+ $("model").value = id;
80
+ var seq = ++loadSeq;
81
+ info("Reading <b>" + esc(id) + "</b> from Hugging Face…");
82
+ return getJSON(HF + "/api/models/" + id + "?expand[]=safetensors&expand[]=gguf&expand[]=config&expand[]=gated&expand[]=cardData")
83
+ .then(function (meta) {
84
+ var name = id.split("/")[1], org = id.split("/")[0];
85
+ // Gated repos (Llama, Gemma) refuse config.json without a login: use a public mirror of the same weights.
86
+ var tries = meta.gated ? [] : [id];
87
+ baseModels(meta).forEach(function (b) { if (!(meta.gated && b.split("/")[0] === org)) tries.push(b); });
88
+ if (meta.gated || !tries.length || !(meta.safetensors || meta.config)) tries.push("unsloth/" + name);
89
+ return configFor(tries).then(function (cfg) { return { meta: meta, cfg: cfg }; });
90
+ })
91
+ .then(function (x) {
92
+ if (seq !== loadSeq) return;
93
+ if (!x.meta.id) x.meta.id = id;
94
+ model = F.describe(x.meta, x.cfg);
95
+ model.configFrom = x.cfg && x.cfg.__from;
96
+ fillAdv();
97
+ showInfo(x.meta);
98
+ render();
99
+ save();
100
+ })
101
+ .catch(function (e) {
102
+ if (seq !== loadSeq) return;
103
+ 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>");
104
+ $("adv").open = true;
105
+ });
106
+ }
107
+ function info(html) { $("model-info").innerHTML = html; }
108
+ function showInfo(meta) {
109
+ var m = model, tags = [];
110
+ tags.push(F.fmtParams(m.params) + " parameters");
111
+ if (m.moe) tags.push("MoE · " + F.fmtParams(m.activeParams) + " active" + (m.experts ? " · " + m.topK + " of " + m.experts + " experts" : ""));
112
+ if (m.layers) tags.push(m.layers + " layers");
113
+ if (m.mla) tags.push("MLA cache"); else if (m.kvHeads) tags.push(m.kvHeads + " KV heads");
114
+ if (m.window) tags.push(m.slidingLayers + " sliding-window layers");
115
+ if (m.maxContext) tags.push(F.fmtCtx(m.maxContext) + " max context");
116
+ if (m.nativeLabel) tags.push("files: " + m.nativeLabel);
117
+ var note = "";
118
+ 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>";
119
+ 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>';
120
+ if (!m.params) note += '<div class="hint err">No parameter count published for this repo. Enter it under Model details.</div>';
121
+ info('<a href="' + HF + "/" + esc(m.id) + '" target="_blank" rel="noopener"><b>' + esc(m.id) + '</b></a><div class="tags">' +
122
+ tags.map(function (t) { return '<span class="tag">' + esc(t) + "</span>"; }).join("") + "</div>" + note);
123
+ }
124
+ function fillAdv() {
125
+ $("m-params").value = model.params ? +(model.params / 1e9).toFixed(2) : "";
126
+ $("m-active").value = model.activeParams ? +(model.activeParams / 1e9).toFixed(2) : "";
127
+ $("m-layers").value = model.layers || "";
128
+ $("m-kvheads").value = model.mla ? "" : model.kvHeads || "";
129
+ $("m-headdim").value = model.mla ? "" : model.headDim || "";
130
+ $("m-maxctx").value = model.maxContext || "";
131
+ $("m-kvheads").disabled = $("m-headdim").disabled = !!model.mla;
132
+ }
133
+ function readAdv() {
134
+ if (!model) model = F.describe({ id: "custom" }, {});
135
+ var p = Number($("m-params").value) * 1e9, a = Number($("m-active").value) * 1e9;
136
+ if (p) { if (model.nativeBytes && model.params) model.nativeBytes *= p / model.params; model.params = p; }
137
+ model.activeParams = a && a <= model.params ? a : model.params;
138
+ model.moe = model.activeParams < model.params * 0.97;
139
+ model.layers = Number($("m-layers").value) || 0;
140
+ if (!model.mla) { model.kvHeads = Number($("m-kvheads").value) || 0; model.headDim = Number($("m-headdim").value) || 0; }
141
+ model.fullLayers = model.layers - (model.slidingLayers || 0);
142
+ if (model.fullLayers < 0) { model.fullLayers = model.layers; model.slidingLayers = 0; }
143
+ model.maxContext = Number($("m-maxctx").value) || 0;
144
+ model.hasShape = !!(model.layers && (model.mla || (model.kvHeads && model.headDim)));
145
+ render(); save();
146
+ }
147
+ ["m-params", "m-active", "m-layers", "m-kvheads", "m-headdim", "m-maxctx"].forEach(function (id) { $(id).addEventListener("change", readAdv); });
148
+
149
+ // ---------- results ----------
150
+ function render() {
151
+ var tb = document.querySelector("#table tbody");
152
+ if (!model || !model.params) { tb.innerHTML = ""; $("verdict").textContent = "Pick a model to see if it fits."; lastRows = []; return; }
153
+ var hw = hardware(), ctx = Number($("ctx").value), kvBits = F.KV_TYPES[$("kv").value];
154
+ var rows = F.quantRows(model).map(function (q) { return { q: q, r: F.estimate(model, hw, q, ctx, kvBits) }; });
155
+ lastRows = rows;
156
+ var best = rows.filter(function (x) { return x.r.fit === "yes"; })[0] || rows.filter(function (x) { return x.r.fit === "tight"; })[0];
157
+ var off = rows.filter(function (x) { return x.r.fit === "offload"; })[0];
158
+ var name = model.id.split("/").pop();
159
+ var v;
160
+ if (best) v = "Yes: " + esc(name) + " runs on " + esc(hw.label) + " at <u>" + esc(best.q.label.split(" /")[0]) + "</u>" +
161
+ "<small>" + F.fmtGB(best.r.total) + " GB of " + F.fmtGB(best.r.usable) + " GB usable at " + F.fmtCtx(ctx) + " context · about " + Math.round(best.r.tpsLo) + "–" + Math.round(best.r.tpsHi) + " tokens/s</small>";
162
+ 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 at roughly " + off.r.tpsLo.toFixed(1) + "–" + off.r.tpsHi.toFixed(1) + " tokens/s.</small>";
163
+ 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>";
164
+ $("verdict").innerHTML = v;
165
+ tb.innerHTML = rows.map(function (x) {
166
+ var r = x.r, pct = Math.min(100, r.total / r.usable * 100);
167
+ var fitTxt = { yes: "Fits", tight: "Tight", offload: "Offload", no: "No" }[r.fit];
168
+ var speed = r.fit === "no" ? "—" : r.tpsHi >= 10 ? Math.round(r.tpsLo) + "–" + Math.round(r.tpsHi) : r.tpsLo.toFixed(1) + "–" + r.tpsHi.toFixed(1);
169
+ 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>" +
170
+ '<td class="num">' + F.fmtGB(r.weights) + ' GB</td><td class="num">' + F.fmtGB(r.kv) + ' GB</td>' +
171
+ '<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>' +
172
+ '<td><span class="pill ' + r.fit + '">' + fitTxt + "</span></td>" +
173
+ '<td class="num">' + (r.maxCtx ? F.fmtCtx(r.maxCtx) : "—") + '</td><td class="num">' + speed + "</td></tr>";
174
+ }).join("");
175
+ $("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." : "");
176
+ syncUrl();
177
+ }
178
+ ["gpu", "gpu-count", "ram", "ram-bw", "mac-mem", "mac-raised", "unified", "c-mem", "c-bw", "ctx", "kv"].forEach(function (id) {
179
+ $(id).addEventListener("change", function () { render(); save(); });
180
+ $(id).addEventListener("input", function () { if ($(id).type === "number") render(); });
181
+ });
182
+ $("chip").addEventListener("change", function () { fillMacMem($("mac-mem").value); render(); save(); });
183
+
184
+ // ---------- state, share, CSV ----------
185
+ function state() {
186
+ return { model: model ? model.id : $("model").value, kind: kind, gpu: $("gpu").value, n: $("gpu-count").value, ram: $("ram").value, rbw: $("ram-bw").value,
187
+ chip: $("chip").value, mem: $("mac-mem").value, raised: $("mac-raised").checked ? 1 : 0, uni: $("unified").value, cm: $("c-mem").value, cbw: $("c-bw").value,
188
+ ctx: $("ctx").value, kv: $("kv").value };
189
+ }
190
+ function applyState(s) {
191
+ if (s.kind) kind = s.kind;
192
+ [["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) {
193
+ if (s[p[0]] !== undefined && s[p[0]] !== null && s[p[0]] !== "") $(p[1]).value = s[p[0]];
194
+ });
195
+ fillMacMem(s.mem || 48);
196
+ $("mac-raised").checked = String(s.raised) === "1";
197
+ }
198
+ function syncUrl() {
199
+ var s = state(), q = new URLSearchParams();
200
+ Object.keys(s).forEach(function (k) { if (s[k] !== "" && s[k] !== undefined) q.set(k, s[k]); });
201
+ history.replaceState(null, "", "?" + q.toString());
202
+ }
203
+ $("btn-share").addEventListener("click", function () {
204
+ var url = location.href;
205
+ (navigator.clipboard ? navigator.clipboard.writeText(url) : Promise.reject()).then(function () { toast("Link copied"); }, function () { prompt("Copy this link", url); });
206
+ });
207
+ $("btn-csv").addEventListener("click", function () {
208
+ if (!lastRows.length) return;
209
+ var hw = hardware();
210
+ 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"].join(",")];
211
+ lastRows.forEach(function (x) {
212
+ var r = x.r;
213
+ 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, r.tpsLo.toFixed(1), r.tpsHi.toFixed(1)].join(","));
214
+ });
215
+ var a = document.createElement("a");
216
+ a.href = URL.createObjectURL(new Blob([lines.join("\n") + "\n"], { type: "text/csv" }));
217
+ a.download = "fitcheck-" + model.id.replace(/[^\w.-]+/g, "_") + ".csv";
218
+ document.body.appendChild(a); a.click(); a.remove();
219
+ });
220
+
221
+ // ---------- model input ----------
222
+ PRESETS.forEach(function (p) {
223
+ var b = document.createElement("button"); b.className = "chip"; b.textContent = p[0]; b.type = "button";
224
+ b.addEventListener("click", function () { loadModel(p[1]); });
225
+ $("presets").appendChild(b);
226
+ });
227
+ $("btn-load").addEventListener("click", function () { loadModel($("model").value); });
228
+ $("model").addEventListener("keydown", function (e) { if (e.key === "Enter") loadModel($("model").value); });
229
+ var suggestTimer;
230
+ $("model").addEventListener("input", function () {
231
+ clearTimeout(suggestTimer);
232
+ var q = $("model").value.trim();
233
+ if (q.length < 3 || /\s/.test(q)) return;
234
+ suggestTimer = setTimeout(function () {
235
+ getJSON(HF + "/api/models?search=" + encodeURIComponent(q) + "&pipeline_tag=text-generation&sort=downloads&limit=8").then(function (list) {
236
+ var dl = $("model-suggest"); dl.innerHTML = "";
237
+ list.forEach(function (m) { dl.appendChild(opt(m.id, m.id)); });
238
+ }, function () {});
239
+ }, 300);
240
+ });
241
+
242
+ // ---------- start ----------
243
+ var params = new URLSearchParams(location.search);
244
+ var init = {};
245
+ params.forEach(function (v, k) { init[k] = v; });
246
+ if (!init.model) init = load();
247
+ applyState(init);
248
+ setKind(kind);
249
+ loadModel(init.model || "Qwen/Qwen3-8B");
250
+ window.fitcheckApp = { loadModel: loadModel, rows: function () { return lastRows; }, model: function () { return model; } };
251
+ if ("serviceWorker" in navigator && location.protocol === "https:") navigator.serviceWorker.register("sw.js").catch(function () {});
252
+ })();
fitcheck/fit.css ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Fitcheck extras. © 2026 CyberMax. All rights reserved. */
2
+ :root{--accent:#0f7b6c;--accent-soft:#e2f4f1;--bad:#b3261e;--bad-soft:#fdecea}
3
+ @media (prefers-color-scheme:dark){:root{--accent:#4fd1bd;--accent-soft:#16312d;--bad:#ff8a80;--bad-soft:#3a1c1a}}
4
+ .inline{display:flex;gap:8px}
5
+ .inline input{flex:1}
6
+ .chips{margin-top:10px}
7
+ .model-info{margin-top:12px;font-size:14px;min-height:22px}
8
+ .model-info .err{color:var(--bad)}
9
+ .model-info .tags{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
10
+ .model-info .tag{background:var(--code);border-radius:6px;padding:2px 8px;font-size:12px;font-weight:600}
11
+ .adv{margin-top:12px;font-size:14px}
12
+ .adv summary{cursor:pointer;font-weight:700;color:var(--muted)}
13
+ .seg{display:flex;background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:3px;gap:3px;margin-bottom:6px}
14
+ .seg button{flex:1;border:0;background:none;padding:8px 6px;border-radius:8px;font-weight:700;color:var(--muted);cursor:pointer}
15
+ .seg button[aria-checked="true"]{background:var(--surface);color:var(--accent);box-shadow:0 1px 3px rgba(0,0,0,.12)}
16
+ [data-hw][hidden]{display:none}
17
+ .result{margin-top:16px}
18
+ .verdict{font-size:clamp(18px,3.4vw,24px);font-weight:800;margin-bottom:12px;line-height:1.25}
19
+ .verdict small{display:block;font-size:14px;font-weight:500;color:var(--muted);margin-top:4px}
20
+ .pill{display:inline-block;border-radius:999px;padding:2px 10px;font-size:12px;font-weight:800;white-space:nowrap}
21
+ .pill.yes{background:var(--accent-soft);color:var(--good)}
22
+ .pill.tight{background:var(--warn-soft);color:var(--warn)}
23
+ .pill.offload{background:var(--warn-soft);color:var(--warn)}
24
+ .pill.no{background:var(--bad-soft);color:var(--bad)}
25
+ #table td{white-space:nowrap}
26
+ #table td .q{font-weight:700}
27
+ #table td .note{display:block;font-size:11px;color:var(--muted);font-weight:500}
28
+ #table tr.best td{background:var(--accent-soft)}
29
+ .meter{height:6px;border-radius:3px;background:var(--line);margin-top:4px;overflow:hidden;min-width:60px}
30
+ .meter i{display:block;height:100%;background:var(--accent)}
31
+ .meter i.over{background:var(--bad)}
32
+ @media (max-width:560px){#table th:nth-child(2),#table td:nth-child(2),#table th:nth-child(3),#table td:nth-child(3){display:none}}
33
+ @media (max-width:480px){.top .btn{padding:8px 10px;font-size:13px;white-space:nowrap}.brand small{display:none}}
fitcheck/fitcheck.js ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Fitcheck core (pure, no DOM): LLM memory + speed estimates. © 2026 CyberMax. All rights reserved. */
2
+ (function (root) {
3
+ "use strict";
4
+
5
+ var GB = 1e9;
6
+
7
+ // Average bits per weight. GGUF values are llama.cpp's published averages for its k-quant/i-quant mixes (approximate:
8
+ // they vary a little by architecture). MLX 4-bit = 4 bits + fp16 scale and bias per group of 64.
9
+ var QUANTS = [
10
+ { id: "bf16", label: "BF16 / FP16", bits: 16, note: "full precision" },
11
+ { id: "q8_0", label: "Q8_0 / FP8 / MLX 8-bit", bits: 8.5, note: "near-lossless" },
12
+ { id: "q6_k", label: "Q6_K", bits: 6.56, note: "excellent" },
13
+ { id: "q5_k_m", label: "Q5_K_M", bits: 5.69, note: "very good" },
14
+ { id: "q4_k_m", label: "Q4_K_M / MLX 4-bit", bits: 4.85, note: "the usual sweet spot" },
15
+ { id: "iq4_xs", label: "IQ4_XS / AWQ / GPTQ 4-bit", bits: 4.25, note: "good" },
16
+ { id: "q3_k_m", label: "Q3_K_M", bits: 3.91, note: "noticeable quality loss" },
17
+ { id: "q2_k", label: "Q2_K", bits: 2.96, note: "large quality loss" },
18
+ { id: "iq2_xxs", label: "IQ2_XXS", bits: 2.06, note: "last resort" }
19
+ ];
20
+
21
+ var KV_TYPES = { f16: 16, q8_0: 8.5, q4_0: 4.5 };
22
+
23
+ // Spec-sheet memory size (GB) and bandwidth (GB/s).
24
+ var GPUS = [
25
+ ["RTX 3060 12GB", 12, 360], ["RTX 3070 8GB", 8, 448], ["RTX 3080 10GB", 10, 760], ["RTX 3090 24GB", 24, 936],
26
+ ["RTX 4060 8GB", 8, 272], ["RTX 4060 Ti 16GB", 16, 288], ["RTX 4070 12GB", 12, 504], ["RTX 4070 Ti Super 16GB", 16, 672],
27
+ ["RTX 4080 16GB", 16, 717], ["RTX 4090 24GB", 24, 1008], ["RTX 5060 Ti 16GB", 16, 448], ["RTX 5070 12GB", 12, 672],
28
+ ["RTX 5070 Ti 16GB", 16, 896], ["RTX 5080 16GB", 16, 960], ["RTX 5090 32GB", 32, 1792],
29
+ ["RX 7900 XT 20GB", 20, 800], ["RX 7900 XTX 24GB", 24, 960], ["RX 9070 XT 16GB", 16, 640],
30
+ ["Tesla T4 16GB", 16, 320], ["L4 24GB", 24, 300], ["A10G 24GB", 24, 600], ["L40S 48GB", 48, 864],
31
+ ["RTX 6000 Ada 48GB", 48, 960], ["RTX PRO 6000 96GB", 96, 1792], ["A100 40GB", 40, 1555], ["A100 80GB", 80, 2039],
32
+ ["H100 80GB (SXM)", 80, 3350], ["H200 141GB", 141, 4800]
33
+ ].map(function (g) { return { name: g[0], vram: g[1], bw: g[2] }; });
34
+
35
+ // Apple silicon: memory sizes offered and bandwidth. Binned M3 Max / M4 Max chips have lower bandwidth at their smallest size.
36
+ var APPLE = [
37
+ { name: "M1", mem: [8, 16], bw: 68 }, { name: "M1 Pro", mem: [16, 32], bw: 200 }, { name: "M1 Max", mem: [32, 64], bw: 400 },
38
+ { name: "M1 Ultra", mem: [64, 128], bw: 800 }, { name: "M2", mem: [8, 16, 24], bw: 100 }, { name: "M2 Pro", mem: [16, 32], bw: 200 },
39
+ { name: "M2 Max", mem: [32, 64, 96], bw: 400 }, { name: "M2 Ultra", mem: [64, 128, 192], bw: 800 },
40
+ { name: "M3", mem: [8, 16, 24], bw: 100 }, { name: "M3 Pro", mem: [18, 36], bw: 150 },
41
+ { name: "M3 Max", mem: [36, 48, 64, 96, 128], bw: 400, bwByMem: { 36: 300, 96: 300 } },
42
+ { name: "M3 Ultra", mem: [96, 256, 512], bw: 819 }, { name: "M4", mem: [16, 24, 32], bw: 120 },
43
+ { name: "M4 Pro", mem: [24, 48, 64], bw: 273 }, { name: "M4 Max", mem: [36, 48, 64, 128], bw: 546, bwByMem: { 36: 410 } }
44
+ ];
45
+
46
+ // Other unified-memory machines: total memory, how much the GPU may use by default, bandwidth.
47
+ var UNIFIED = [
48
+ { name: "NVIDIA DGX Spark 128GB", mem: 128, usable: 120, bw: 273 },
49
+ { name: "AMD Ryzen AI Max+ 395 128GB", mem: 128, usable: 96, bw: 256 }
50
+ ];
51
+
52
+ function num(v) { var n = Number(v); return isFinite(n) ? n : 0; }
53
+
54
+ // Flatten nested configs (Gemma 3, Llama 4, Mistral 3 put the LM under text_config).
55
+ function flatConfig(cfg) {
56
+ cfg = cfg || {};
57
+ var out = {};
58
+ Object.keys(cfg).forEach(function (k) { out[k] = cfg[k]; });
59
+ var t = cfg.text_config || cfg.llm_config || cfg.language_config;
60
+ if (t && typeof t === "object") Object.keys(t).forEach(function (k) { if (t[k] !== undefined && t[k] !== null) out[k] = t[k]; });
61
+ return out;
62
+ }
63
+
64
+ // Build a model description from HF API metadata (/api/models/{id}) and config.json.
65
+ function describe(meta, config) {
66
+ meta = meta || {};
67
+ var c = flatConfig(config || meta.config);
68
+ var st = meta.safetensors || {};
69
+ var params = num(st.total) || num(meta.gguf && meta.gguf.total) || num(c.num_parameters);
70
+ var p = st.parameters || {};
71
+ var quant = (c.quantization_config && c.quantization_config.quant_method) || "";
72
+ var nativeBytes = 0;
73
+ Object.keys(p).forEach(function (k) {
74
+ var n = num(p[k]);
75
+ var b = /^(F64|I64)$/.test(k) ? 8 : /^(F32|I32)$/.test(k) ? 4 : /^(BF16|F16|I16)$/.test(k) ? 2 : 1;
76
+ if (k === "U8" && quant === "mxfp4") b = 17 / 32; // 4-bit values + one 8-bit scale per 32
77
+ nativeBytes += n * b;
78
+ });
79
+ var layers = num(c.num_hidden_layers || c.n_layer || c.num_layers);
80
+ var heads = num(c.num_attention_heads || c.n_head);
81
+ var kvHeads = num(c.num_key_value_heads || c.num_kv_heads || c.multi_query_group_num) || heads;
82
+ var hidden = num(c.hidden_size || c.n_embd || c.d_model);
83
+ var headDim = num(c.head_dim) || (heads ? hidden / heads : 0);
84
+ var nExperts = num(c.num_local_experts || c.num_experts || c.n_routed_experts);
85
+ var topK = num(c.num_experts_per_tok || c.experts_per_token || c.moe_topk);
86
+ var active = params;
87
+ if (nExperts > 1 && topK > 0 && hidden) {
88
+ var moeInter = num(c.moe_intermediate_size) || num(c.intermediate_size);
89
+ var moeLayers = layers - num(c.first_k_dense_replace);
90
+ var inactive = (nExperts - topK) * 3 * hidden * moeInter * moeLayers;
91
+ active = Math.max(params * 0.03, params - inactive);
92
+ }
93
+ // Sliding-window layers only keep `window` tokens of cache.
94
+ var win = num(c.sliding_window);
95
+ var fullLayers = layers;
96
+ if (win && Array.isArray(c.layer_types)) {
97
+ fullLayers = c.layer_types.filter(function (t) { return !/sliding/.test(t); }).length;
98
+ } else if (win && num(c.sliding_window_pattern) > 1) {
99
+ fullLayers = Math.floor(layers / num(c.sliding_window_pattern));
100
+ } else {
101
+ win = 0;
102
+ }
103
+ var mla = num(c.kv_lora_rank) ? num(c.kv_lora_rank) + num(c.qk_rope_head_dim) : 0;
104
+ return {
105
+ id: meta.id || meta.modelId || "",
106
+ params: params,
107
+ activeParams: active,
108
+ moe: active < params * 0.97,
109
+ experts: nExperts, topK: topK,
110
+ nativeBytes: nativeBytes,
111
+ nativeLabel: quant ? quant.toUpperCase() : Object.keys(p).sort(function (a, b) { return num(p[b]) - num(p[a]); })[0] || "",
112
+ layers: layers, kvHeads: kvHeads, headDim: headDim,
113
+ fullLayers: fullLayers, slidingLayers: layers - fullLayers, window: win,
114
+ mla: mla,
115
+ maxContext: num(c.max_position_embeddings) || num(meta.gguf && meta.gguf.context_length) || 0,
116
+ gated: !!meta.gated,
117
+ hasShape: !!(layers && (mla || (kvHeads && headDim)))
118
+ };
119
+ }
120
+
121
+ // KV cache bytes for `ctx` tokens (batch 1) at `bits` per element.
122
+ function kvBytes(m, ctx, bits) {
123
+ if (!m.hasShape) return 0;
124
+ var bytes = (bits || 16) / 8;
125
+ if (m.mla) return m.layers * m.mla * bytes * ctx;
126
+ var perLayerTok = 2 * m.kvHeads * m.headDim * bytes;
127
+ var slidingCtx = m.window ? Math.min(ctx, m.window) : ctx;
128
+ return perLayerTok * (m.fullLayers * ctx + m.slidingLayers * slidingCtx);
129
+ }
130
+
131
+ function weightBytes(m, bits) { return m.params * bits / 8; }
132
+
133
+ var OVERHEAD = 1.0 * GB; // runtime context + compute buffers (rough)
134
+
135
+ // hw: { usable: GB the GPU can use, bw: GB/s, ramGB: extra system RAM for offload (0 = none), ramBw: GB/s }
136
+ function estimate(m, hw, q, ctx, kvBits) {
137
+ var w = q.bytes !== undefined ? q.bytes : weightBytes(m, q.bits);
138
+ var kv = kvBytes(m, ctx, kvBits);
139
+ var total = w + kv + OVERHEAD;
140
+ var usable = hw.usable * GB;
141
+ var activeW = m.params ? w * (m.activeParams / m.params) : w;
142
+ var perToken = activeW + kv; // bytes read per generated token (batch 1)
143
+ var r = { weights: w, kv: kv, total: total, usable: usable, fit: "no", tpsLo: 0, tpsHi: 0 };
144
+ if (total <= usable) {
145
+ r.fit = total <= usable * 0.9 ? "yes" : "tight";
146
+ var ceil = hw.bw * GB / perToken;
147
+ r.tpsLo = ceil * 0.5; r.tpsHi = ceil * 0.8;
148
+ } else if (hw.ramGB > 0 && total <= usable + hw.ramGB * GB * 0.85) {
149
+ r.fit = "offload";
150
+ var onGpu = Math.max(0, usable - kv - OVERHEAD);
151
+ var share = w ? Math.min(1, onGpu / w) : 1;
152
+ var t = (activeW * share + kv) / (hw.bw * GB) + (activeW * (1 - share)) / ((hw.ramBw || 50) * GB);
153
+ r.gpuShare = share;
154
+ r.tpsLo = 0.5 / t; r.tpsHi = 0.8 / t;
155
+ }
156
+ // Longest context that still fits entirely in usable memory.
157
+ var free = usable - w - OVERHEAD;
158
+ var per = kvBytes(m, 1, kvBits);
159
+ if (free <= 0) r.maxCtx = 0;
160
+ else if (!per) r.maxCtx = m.maxContext || 0;
161
+ else {
162
+ var lo = 0, hi = Math.max(m.maxContext || 131072, 1024);
163
+ if (kvBytes(m, hi, kvBits) <= free) lo = hi;
164
+ else while (hi - lo > 256) { var mid = Math.floor((lo + hi) / 2); if (kvBytes(m, mid, kvBits) <= free) lo = mid; else hi = mid; }
165
+ r.maxCtx = Math.floor(lo / 256) * 256;
166
+ }
167
+ return r;
168
+ }
169
+
170
+ function quantRows(m) {
171
+ var rows = QUANTS.slice();
172
+ if (m.nativeBytes && m.nativeLabel && !/^(BF16|F16)$/.test(m.nativeLabel)) {
173
+ rows.unshift({ id: "native", label: "As published (" + m.nativeLabel + ")", bytes: m.nativeBytes, bits: m.params ? m.nativeBytes * 8 / m.params : 16, note: "the original files" });
174
+ }
175
+ return rows;
176
+ }
177
+
178
+ // Apple: macOS lets the GPU wire about 2/3 of RAM up to 36 GB and about 3/4 above (raisable with iogpu.wired_limit_mb).
179
+ // raised = the user lifted the limit, leaving ~8 GB for macOS.
180
+ function appleHardware(chip, memGB, raised) {
181
+ var c = APPLE.filter(function (a) { return a.name === chip; })[0] || APPLE[0];
182
+ var bw = (c.bwByMem && c.bwByMem[memGB]) || c.bw;
183
+ var usable = raised ? Math.max(memGB - 8, memGB * 0.75) : memGB * (memGB <= 36 ? 2 / 3 : 0.75);
184
+ return { label: c.name + " " + memGB + "GB", usable: usable, bw: bw, ramGB: 0, unified: true };
185
+ }
186
+
187
+ function gpuHardware(name, count, ramGB, ramBw) {
188
+ var g = GPUS.filter(function (x) { return x.name === name; })[0] || GPUS[0];
189
+ count = Math.max(1, Math.floor(num(count)) || 1);
190
+ // Split across cards, per-token speed is closer to one card's bandwidth than the sum (conservative).
191
+ return { label: (count > 1 ? count + "× " : "") + g.name, usable: g.vram * count, bw: g.bw, ramGB: num(ramGB), ramBw: num(ramBw) || 50 };
192
+ }
193
+
194
+ function unifiedHardware(name) {
195
+ var u = UNIFIED.filter(function (x) { return x.name === name; })[0] || UNIFIED[0];
196
+ return { label: u.name, usable: u.usable, bw: u.bw, ramGB: 0, unified: true };
197
+ }
198
+
199
+ function fmtGB(bytes) { var g = bytes / GB; return g >= 100 ? g.toFixed(0) : g >= 10 ? g.toFixed(1) : g.toFixed(2); }
200
+ function fmtParams(n) { return n >= 1e12 ? (n / 1e12).toFixed(2) + "T" : n >= 1e9 ? (n / 1e9).toFixed(n >= 1e10 ? 0 : 1) + "B" : (n / 1e6).toFixed(0) + "M"; }
201
+ function fmtCtx(n) { return n >= 1024 ? Math.round(n / 1024) + "K" : String(n); }
202
+
203
+ // "owner/name" from a pasted URL or id.
204
+ function parseModelId(s) {
205
+ s = String(s || "").trim();
206
+ var m = s.match(/huggingface\.co\/(?:models\/)?([\w.-]+\/[\w.-]+)/);
207
+ if (m) return m[1];
208
+ m = s.match(/^([\w.-]+\/[\w.-]+)$/);
209
+ return m ? m[1] : "";
210
+ }
211
+
212
+ var api = { QUANTS: QUANTS, KV_TYPES: KV_TYPES, GPUS: GPUS, APPLE: APPLE, UNIFIED: UNIFIED, GB: GB,
213
+ describe: describe, kvBytes: kvBytes, weightBytes: weightBytes, estimate: estimate, quantRows: quantRows,
214
+ appleHardware: appleHardware, gpuHardware: gpuHardware, unifiedHardware: unifiedHardware,
215
+ fmtGB: fmtGB, fmtParams: fmtParams, fmtCtx: fmtCtx, parseModelId: parseModelId, flatConfig: flatConfig };
216
+ root.Fitcheck = api;
217
+ if (typeof module !== "undefined") module.exports = api;
218
+ })(typeof window !== "undefined" ? window : globalThis);
fitcheck/icon-192.png ADDED
fitcheck/icon-512.png ADDED
fitcheck/icon.svg ADDED
fitcheck/index.html ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Fitcheck — Can I Run This LLM? VRAM &amp; Speed Calculator</title>
7
+ <meta name="description" content="Will this LLM run on my GPU or Mac? Paste any Hugging Face model and see VRAM needed per quantization (Q4_K_M, Q8, FP16), max context and tokens/sec.">
8
+ <meta name="keywords" content="can i run this llm, llm vram calculator, gpu memory calculator, gguf vram, q4_k_m vram, kv cache calculator, local llm hardware, apple silicon llm, mac llm memory, tokens per second estimate">
9
+ <meta name="theme-color" content="#0f7b6c">
10
+ <link rel="canonical" href="https://shaw276-cybermax.static.hf.space/fitcheck/">
11
+ <link rel="manifest" href="manifest.webmanifest">
12
+ <link rel="icon" href="icon.svg" type="image/svg+xml">
13
+ <link rel="apple-touch-icon" href="icon-192.png">
14
+ <meta property="og:type" content="website">
15
+ <meta property="og:title" content="Fitcheck — will this LLM run on my GPU or Mac?">
16
+ <meta property="og:description" content="VRAM per quantization, KV cache, max context and a tokens/sec estimate for any Hugging Face model. Free, no sign-up.">
17
+ <meta property="og:url" content="https://shaw276-cybermax.static.hf.space/fitcheck/">
18
+ <meta property="og:image" content="https://shaw276-cybermax.static.hf.space/fitcheck/og.png">
19
+ <meta name="twitter:card" content="summary_large_image">
20
+ <script type="application/ld+json">
21
+ {"@context":"https://schema.org","@graph":[
22
+ {"@type":"WebApplication","name":"Fitcheck by CyberMax","url":"https://shaw276-cybermax.static.hf.space/fitcheck/","applicationCategory":"DeveloperApplication","operatingSystem":"Any","description":"LLM VRAM and speed calculator: reads any Hugging Face model's config live and shows memory per quantization, KV cache, max context and a tokens-per-second estimate for your GPU or Mac.","offers":{"@type":"Offer","price":"0","priceCurrency":"USD"},"publisher":{"@type":"Organization","name":"CyberMax"}},
23
+ {"@type":"FAQPage","mainEntity":[
24
+ {"@type":"Question","name":"How much VRAM does an LLM need?","acceptedAnswer":{"@type":"Answer","text":"Roughly parameters × bits per weight ÷ 8 for the weights, plus the KV cache for your context length, plus about 1 GB of runtime overhead. An 8B model at Q4_K_M needs about 5 GB of weights; at 32K context its KV cache can add another 4–5 GB."}},
25
+ {"@type":"Question","name":"What is the KV cache?","acceptedAnswer":{"@type":"Answer","text":"The KV cache stores attention keys and values for every token in the context. It grows linearly with context length: 2 × layers × KV heads × head size × bytes per value per token. Quantizing it to 8-bit roughly halves it."}},
26
+ {"@type":"Question","name":"How much of a Mac's memory can the GPU use?","acceptedAnswer":{"@type":"Answer","text":"By default macOS lets the GPU use about two-thirds of unified memory on machines with 36 GB or less and about three-quarters above that. The limit can be raised with the iogpu.wired_limit_mb setting."}},
27
+ {"@type":"Question","name":"How fast will it generate?","acceptedAnswer":{"@type":"Answer","text":"Single-user generation speed is limited mainly by memory bandwidth: every token reads the active weights once. Fitcheck divides your hardware's bandwidth by the bytes read per token and shows 50–80% of that ceiling as a rough range."}}]}
28
+ ]}
29
+ </script>
30
+ <link rel="stylesheet" href="style.css">
31
+ <link rel="stylesheet" href="fit.css">
32
+ </head>
33
+ <body>
34
+ <header class="top"><div class="wrap">
35
+ <div class="brand"><span class="dot">✓</span> Fitcheck <small>by CyberMax</small></div>
36
+ <a class="btn ghost" href="#faq">How it works</a>
37
+ </div></header>
38
+
39
+ <main class="wrap">
40
+ <h1>Can I run this LLM? VRAM &amp; speed calculator</h1>
41
+ <p class="lede">Paste any Hugging Face model. Fitcheck reads its config live and shows the memory each quantization needs, how much context fits, and a tokens-per-second estimate on your GPU or Mac.</p>
42
+
43
+ <div class="grid">
44
+ <section class="card" aria-labelledby="h-model">
45
+ <h2 id="h-model">1 · Model</h2>
46
+ <label for="model">Hugging Face model id or URL</label>
47
+ <div class="inline">
48
+ <input type="text" id="model" list="model-suggest" placeholder="e.g. Qwen/Qwen3-8B" autocomplete="off" spellcheck="false">
49
+ <button class="btn" id="btn-load">Check</button>
50
+ </div>
51
+ <datalist id="model-suggest"></datalist>
52
+ <div class="chips" id="presets" aria-label="Popular models"></div>
53
+ <div class="model-info" id="model-info" aria-live="polite"></div>
54
+ <details class="adv" id="adv">
55
+ <summary>Model details (edit if a value is missing)</summary>
56
+ <div class="row3">
57
+ <div><label for="m-params">Parameters (B)</label><input type="number" id="m-params" step="0.1" min="0"></div>
58
+ <div><label for="m-active">Active per token (B)</label><input type="number" id="m-active" step="0.1" min="0"></div>
59
+ <div><label for="m-layers">Layers</label><input type="number" id="m-layers" min="0"></div>
60
+ <div><label for="m-kvheads">KV heads</label><input type="number" id="m-kvheads" min="0"></div>
61
+ <div><label for="m-headdim">Head size</label><input type="number" id="m-headdim" min="0"></div>
62
+ <div><label for="m-maxctx">Max context</label><input type="number" id="m-maxctx" min="0"></div>
63
+ </div>
64
+ </details>
65
+ </section>
66
+
67
+ <section class="card" aria-labelledby="h-hw">
68
+ <h2 id="h-hw">2 · Hardware</h2>
69
+ <div class="seg" role="radiogroup" aria-label="Hardware type">
70
+ <button role="radio" aria-checked="true" data-kind="gpu">GPU</button>
71
+ <button role="radio" aria-checked="false" data-kind="mac">Mac</button>
72
+ <button role="radio" aria-checked="false" data-kind="unified">Mini PC</button>
73
+ <button role="radio" aria-checked="false" data-kind="custom">Custom</button>
74
+ </div>
75
+ <div data-hw="gpu">
76
+ <div class="row2">
77
+ <div><label for="gpu">Graphics card</label><select id="gpu"></select></div>
78
+ <div><label for="gpu-count">How many</label><select id="gpu-count"><option>1</option><option>2</option><option>3</option><option>4</option><option>8</option></select></div>
79
+ </div>
80
+ <div class="row2">
81
+ <div><label for="ram">System RAM for offload <span class="opt">(GB)</span></label><input type="number" id="ram" value="32" min="0"></div>
82
+ <div><label for="ram-bw">RAM speed <span class="opt">(GB/s)</span></label><input type="number" id="ram-bw" value="60" min="1"><div class="hint">DDR4 dual ≈ 40–50, DDR5 dual ≈ 60–90</div></div>
83
+ </div>
84
+ </div>
85
+ <div data-hw="mac" hidden>
86
+ <div class="row2">
87
+ <div><label for="chip">Chip</label><select id="chip"></select></div>
88
+ <div><label for="mac-mem">Memory</label><select id="mac-mem"></select></div>
89
+ </div>
90
+ <label class="check"><input type="checkbox" id="mac-raised"> I raised the GPU memory limit (iogpu.wired_limit_mb)</label>
91
+ </div>
92
+ <div data-hw="unified" hidden>
93
+ <label for="unified">Machine</label><select id="unified"></select>
94
+ </div>
95
+ <div data-hw="custom" hidden>
96
+ <div class="row2">
97
+ <div><label for="c-mem">Memory the GPU can use (GB)</label><input type="number" id="c-mem" value="24" min="1"></div>
98
+ <div><label for="c-bw">Memory bandwidth (GB/s)</label><input type="number" id="c-bw" value="500" min="1"></div>
99
+ </div>
100
+ </div>
101
+ <div class="row2">
102
+ <div><label for="ctx">Context length</label><select id="ctx"></select></div>
103
+ <div><label for="kv">KV cache type</label><select id="kv"><option value="f16">FP16 (default)</option><option value="q8_0">Q8_0 (half the size)</option><option value="q4_0">Q4_0 (quarter)</option></select></div>
104
+ </div>
105
+ </section>
106
+ </div>
107
+
108
+ <section class="card result" id="result" aria-live="polite">
109
+ <div class="verdict" id="verdict">Pick a model to see if it fits.</div>
110
+ <div class="table-wrap">
111
+ <table id="table">
112
+ <thead><tr><th>Quantization</th><th class="num">Weights</th><th class="num">KV cache</th><th class="num">Total</th><th>Fits?</th><th class="num">Max context</th><th class="num">Speed (tok/s)</th></tr></thead>
113
+ <tbody></tbody>
114
+ </table>
115
+ </div>
116
+ <div class="bar-legend" id="legend"></div>
117
+ <div class="btns"><button class="btn ghost" id="btn-share">Copy share link</button><button class="btn ghost" id="btn-csv">Download CSV</button></div>
118
+ <p class="hint">Estimates, not guarantees: real use differs by runtime (llama.cpp, Ollama, LM Studio, MLX, vLLM), batch size and drivers. Total includes ~1 GB runtime overhead. Speed is 50–80% of the memory-bandwidth limit for one user.</p>
119
+ </section>
120
+
121
+ <div class="related">
122
+ <h2>Building with LLMs?</h2>
123
+ <p>Give your agents real web data: CyberMax's pay-per-result tools run on the <a href="https://apify.com/cybermax" target="_blank" rel="noopener">Apify Store</a> and plug into any MCP client via the <a href="https://huggingface.co/spaces/shaw276/cybermax-dev-kit" target="_blank" rel="noopener">CyberMax Developer Kit</a>. Transcribe audio privately with <a href="https://huggingface.co/spaces/shaw276/hushscribe" target="_blank" rel="noopener">Hushscribe</a>.</p>
124
+ </div>
125
+
126
+ <section class="seo" id="faq">
127
+ <h2>How Fitcheck calculates memory</h2>
128
+ <p><b>Weights</b> = parameters × bits per weight ÷ 8. Parameter counts come straight from the model's safetensors or GGUF metadata on Hugging Face, so new models work the day they are published. Bits per weight are llama.cpp's averages for each quant mix (Q4_K_M ≈ 4.85, Q8_0 = 8.5).</p>
129
+ <p><b>KV cache</b> = 2 × layers × KV heads × head size × bytes × context tokens. Fitcheck reads layers, KV heads and head size from <code>config.json</code>, handles grouped-query attention, sliding-window layers (Gemma 3, gpt-oss) and DeepSeek's compressed MLA cache.</p>
130
+ <p><b>Mixture-of-experts</b> models (Qwen3-30B-A3B, gpt-oss, DeepSeek) must fit all experts in memory, but each token only reads the active ones, so they generate much faster than a dense model of the same size.</p>
131
+ <h3>How much of a Mac's memory can the GPU use?</h3>
132
+ <p>By default macOS lets the GPU wire about two-thirds of unified memory up to 36 GB and about three-quarters above that. You can raise it (for example <code>sudo sysctl iogpu.wired_limit_mb=57344</code> on a 64 GB Mac) at your own risk; tick the box to see what fits then.</p>
133
+ <h3>What if it doesn't fit?</h3>
134
+ <p>Try a smaller quantization (Q4_K_M is the usual sweet spot), a shorter context, or a quantized KV cache. On a PC, llama.cpp can keep some layers in system RAM ("offload"): it works, but generation drops to a few tokens per second.</p>
135
+ <h3>Gated models</h3>
136
+ <p>Meta Llama and Google Gemma configs need a login to download. Fitcheck then reads the same architecture from a public mirror or asks you to fill in the model details.</p>
137
+ <h3>Is anything uploaded?</h3>
138
+ <p>No. Your browser asks the public Hugging Face API for the model's metadata; your hardware choices stay on your device.</p>
139
+ </section>
140
+
141
+ <footer class="more">
142
+ <div class="more-title">More from CyberMax</div>
143
+ <div class="more-links">
144
+ <a href="https://huggingface.co/spaces/shaw276/hushscribe" target="_blank" rel="noopener">Hushscribe · private transcription</a>
145
+ <a href="https://huggingface.co/spaces/shaw276/cybermax-dev-kit" target="_blank" rel="noopener">Developer Kit · MCP tools</a>
146
+ <a href="https://shaw276-cybermax.static.hf.space/utm-builder/" target="_blank" rel="noopener">UTM Builder</a>
147
+ <a href="https://shaw276-cybermax.static.hf.space/sumbloom/" target="_blank" rel="noopener">Sumbloom · daily puzzle</a>
148
+ <a href="https://huggingface.co/spaces/shaw276/cybermax" target="_blank" rel="noopener">All CyberMax tools</a>
149
+ </div>
150
+ <div class="copy">© 2026 CyberMax. All rights reserved. Model metadata from the public Hugging Face API. Not affiliated with Hugging Face, NVIDIA, AMD or Apple.</div>
151
+ </footer>
152
+ </main>
153
+ <div class="toast" id="toast" role="status" aria-live="polite"></div>
154
+ <script src="fitcheck.js"></script>
155
+ <script src="app.js"></script>
156
+ </body>
157
+ </html>
fitcheck/manifest.webmanifest ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Fitcheck: LLM VRAM & speed calculator by CyberMax",
3
+ "short_name": "Fitcheck",
4
+ "description": "Will this LLM run on my GPU or Mac? Memory per quantization, KV cache, max context and speed.",
5
+ "start_url": "./",
6
+ "scope": "./",
7
+ "display": "standalone",
8
+ "background_color": "#f6f7fb",
9
+ "theme_color": "#0f7b6c",
10
+ "icons": [
11
+ {"src": "icon.svg", "sizes": "any", "type": "image/svg+xml"},
12
+ {"src": "icon-192.png", "sizes": "192x192", "type": "image/png"},
13
+ {"src": "icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable"}
14
+ ]
15
+ }
fitcheck/og.png ADDED

Git LFS Details

  • SHA256: 06029d82b953f16cef156308a62e8da61df3eac845e09032ce4b290a96b2d0e9
  • Pointer size: 131 Bytes
  • Size of remote file: 110 kB
fitcheck/robots.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ User-agent: *
2
+ Allow: /
3
+ Sitemap: https://shaw276-cybermax.static.hf.space/fitcheck/sitemap.xml
fitcheck/sitemap.xml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>https://shaw276-cybermax.static.hf.space/fitcheck/</loc><lastmod>2026-09-24</lastmod></url></urlset>
fitcheck/style.css ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Fitcheck styles (CyberMax web app base). © 2026 CyberMax. All rights reserved. */
2
+ :root{
3
+ --bg:#f6f7fb;--surface:#fff;--ink:#1d2433;--muted:#5d6678;--line:#e1e5ee;--accent:#3056d3;--accent-ink:#fff;
4
+ --accent-soft:#e8edfc;--good:#1f8a4c;--warn:#b25e00;--warn-soft:#fff3e0;--code:#f0f2f7;color-scheme:light;
5
+ }
6
+ @media (prefers-color-scheme:dark){
7
+ :root{--bg:#12151c;--surface:#1b2029;--ink:#e8ecf4;--muted:#9aa4b6;--line:#2d3440;--accent:#7c9cff;--accent-ink:#0d1220;
8
+ --accent-soft:#232c45;--good:#5ccf8b;--warn:#ffb25c;--warn-soft:#3a2b18;--code:#232833;color-scheme:dark}
9
+ }
10
+ *{box-sizing:border-box}
11
+ html,body{margin:0}
12
+ body{background:var(--bg);color:var(--ink);font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
13
+ a{color:var(--accent)}
14
+ button,input,select,textarea{font:inherit;color:inherit}
15
+ .wrap{max-width:1040px;margin:0 auto;padding:0 16px}
16
+ .top{background:var(--surface);border-bottom:1px solid var(--line)}
17
+ .top .wrap{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-top:12px;padding-bottom:12px}
18
+ .brand{font-weight:800;font-size:18px;display:flex;align-items:center;gap:8px}
19
+ .brand .dot{width:26px;height:26px;border-radius:8px;background:var(--accent);color:var(--accent-ink);display:inline-flex;align-items:center;justify-content:center;font-size:14px}
20
+ .brand small{font-weight:600;color:var(--muted);font-size:13px}
21
+ h1{font-size:clamp(24px,4.5vw,34px);line-height:1.15;margin:22px 0 6px}
22
+ .lede{color:var(--muted);margin:0 0 16px;max-width:720px}
23
+ .tabs{display:flex;gap:4px;border-bottom:1px solid var(--line);margin-bottom:16px;overflow-x:auto}
24
+ .tab{border:0;background:none;padding:10px 14px;font-weight:700;color:var(--muted);border-bottom:3px solid transparent;cursor:pointer;white-space:nowrap}
25
+ .tab[aria-selected="true"]{color:var(--accent);border-bottom-color:var(--accent)}
26
+ .panel[hidden]{display:none}
27
+ .grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px}
28
+ .card{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:16px}
29
+ .card h2{font-size:16px;margin:0 0 12px}
30
+ label{display:block;font-size:13px;font-weight:700;margin:10px 0 4px}
31
+ label .opt{font-weight:500;color:var(--muted)}
32
+ .hint{font-size:12px;color:var(--muted);margin-top:3px}
33
+ input[type=text],input[type=url],input[type=number],select,textarea{width:100%;padding:10px 12px;border:1px solid var(--line);border-radius:10px;background:var(--bg);outline:none;min-width:0}
34
+ input:focus,select:focus,textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
35
+ textarea{min-height:110px;resize:vertical;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px}
36
+ .row2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
37
+ .row3{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
38
+ .chips{display:flex;flex-wrap:wrap;gap:6px}
39
+ .chip{border:1px solid var(--line);background:var(--bg);border-radius:999px;padding:5px 11px;font-size:13px;font-weight:600;cursor:pointer}
40
+ .chip.on,.chip:hover{border-color:var(--accent);color:var(--accent)}
41
+ .btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;border:0;border-radius:10px;background:var(--accent);color:var(--accent-ink);font-weight:700;padding:10px 16px;cursor:pointer;text-decoration:none}
42
+ .btn.ghost{background:var(--surface);color:var(--ink);border:1px solid var(--line)}
43
+ .btn:disabled{opacity:.5;cursor:default}
44
+ .btns{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
45
+ .out{background:var(--code);border-radius:10px;padding:12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;word-break:break-all;min-height:46px}
46
+ .warn{background:var(--warn-soft);color:var(--warn);border-radius:10px;padding:8px 12px;font-size:13px;margin-top:10px}
47
+ .warn:empty{display:none}
48
+ .check{display:flex;align-items:center;gap:8px;font-weight:600;font-size:13px;margin:8px 0}
49
+ .check input{width:18px;height:18px}
50
+ table{width:100%;border-collapse:collapse;font-size:13px}
51
+ th,td{text-align:left;padding:7px 8px;border-bottom:1px solid var(--line);vertical-align:top}
52
+ th{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
53
+ td.num,th.num{text-align:right;font-variant-numeric:tabular-nums}
54
+ .table-wrap{overflow-x:auto}
55
+ .big{font-size:clamp(26px,5vw,38px);font-weight:800;font-variant-numeric:tabular-nums;line-height:1.1}
56
+ .stat{background:var(--bg);border-radius:12px;padding:12px}
57
+ .stat .k{font-size:12px;color:var(--muted);font-weight:700;text-transform:uppercase;letter-spacing:.5px}
58
+ .stat .v{font-size:22px;font-weight:800;font-variant-numeric:tabular-nums}
59
+ .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px}
60
+ .related{border:1px dashed var(--accent);background:var(--accent-soft);border-radius:14px;padding:14px 16px;margin:18px 0}
61
+ .related h2{font-size:15px;margin:0 0 6px}
62
+ .related p{margin:4px 0;font-size:14px}
63
+ .seo{color:var(--muted);font-size:14px;margin-top:10px}
64
+ .seo h2{color:var(--ink);font-size:18px;margin:22px 0 6px}
65
+ .seo h3{color:var(--ink);font-size:15px;margin:14px 0 4px}
66
+ .more{border-top:1px solid var(--line);margin-top:28px;padding:18px 0 22px;text-align:center;font-size:14px}
67
+ .more-title{font-weight:700;margin-bottom:8px}
68
+ .more-links{display:flex;flex-wrap:wrap;gap:8px;justify-content:center}
69
+ .more-links a{color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:999px;padding:6px 12px;text-decoration:none}
70
+ .more-links a:hover{border-color:var(--accent)}
71
+ .copy{margin-top:12px;color:var(--muted);font-size:12px}
72
+ .toast{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);background:var(--ink);color:var(--bg);padding:10px 16px;border-radius:10px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .2s;z-index:20}
73
+ .toast.show{opacity:1}
74
+ @media (max-width:760px){.grid,.row3{grid-template-columns:minmax(0,1fr)}}
75
+ @media (max-width:420px){.row2{grid-template-columns:minmax(0,1fr)}}
fitcheck/sw.js ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Fitcheck offline cache. © 2026 CyberMax. All rights reserved. */
2
+ var CACHE = "fitcheck-v1";
3
+ var FILES = ["./", "index.html", "style.css", "fit.css", "fitcheck.js", "app.js", "manifest.webmanifest", "icon.svg", "icon-192.png", "icon-512.png"];
4
+ self.addEventListener("install", function (e) {
5
+ e.waitUntil(caches.open(CACHE).then(function (c) { return c.addAll(FILES); }).then(function () { return self.skipWaiting(); }));
6
+ });
7
+ self.addEventListener("activate", function (e) {
8
+ e.waitUntil(caches.keys().then(function (keys) {
9
+ return Promise.all(keys.filter(function (k) { return k !== CACHE; }).map(function (k) { return caches.delete(k); }));
10
+ }).then(function () { return self.clients.claim(); }));
11
+ });
12
+ self.addEventListener("fetch", function (e) {
13
+ if (e.request.method !== "GET" || new URL(e.request.url).origin !== location.origin) return;
14
+ // Network first so updates arrive quickly; cache as offline fallback.
15
+ e.respondWith(fetch(e.request).then(function (r) {
16
+ var copy = r.clone();
17
+ caches.open(CACHE).then(function (c) { c.put(e.request, copy); });
18
+ return r;
19
+ }).catch(function () { return caches.match(e.request, { ignoreSearch: true }); }));
20
+ });