/* * VRAM & GPU cost calculator (FastGPU's Hugging Face Space). * * Runs entirely in the visitor's browser. Two public, keyless reads: * 1. the Hugging Face Hub API, for the model's own metadata (parameter count from its safetensors or GGUF * header, its weight format, its task); * 2. FastGPU's match API (https://fastgpu.co/api/v1/match), which sizes the job and ranks the GPU setups * that hold it at live prices. All sizing is FastGPU's, so this page and fastgpu.co never disagree. * The model list in catalog.js only powers search and quick picks; every selection is re-read live. */ (function () { "use strict"; const SITE = "https://fastgpu.co"; const API = SITE + "/api/v1/match"; const GPUS_API = SITE + "/api/v1/gpus"; const HUB = "https://huggingface.co"; const UTM = { utm_source: "huggingface", utm_medium: "space", utm_campaign: "gpu-cost-calculator" }; const DEFAULT_MODEL = "Qwen/Qwen3-32B"; const PICKS = window.FASTGPU_PICKS || []; const CATALOG = (window.FASTGPU_CATALOG || []).map(function (r) { return { id: r[0], p: r[1], task: r[2], dl: r[3], q: r[4], low: r[0].toLowerCase() }; }); const BY_ID = new Map(CATALOG.map(function (m) { return [m.low, m]; })); const TASKS = { inference: "Run / serve it", "finetune-lora": "Fine-tune with LoRA", "finetune-full": "Full fine-tune", generate: "Generate images or video", transcribe: "Transcribe speech", embed: "Embed text", }; const PIPELINE_TASK = { "text-generation": "inference", "image-text-to-text": "inference", "any-to-any": "inference", "text2text-generation": "inference", "visual-question-answering": "inference", "text-to-speech": "inference", "text-to-image": "generate", "image-to-image": "generate", "text-to-video": "generate", "image-to-video": "generate", "automatic-speech-recognition": "transcribe", "feature-extraction": "embed", "sentence-similarity": "embed", "text-ranking": "embed", }; const PRECISION_TEXT = { fp16: "16-bit", int8: "8-bit", int4: "4-bit" }; const BYTES = { fp16: 2, int8: 1, int4: 0.5 }; const $ = function (id) { return document.getElementById(id); }; const el = { form: $("calc"), input: $("model"), list: $("suggestions"), picks: $("picks"), task: $("task"), prec: $("precision"), region: $("region"), spot: $("spot"), out: $("result"), status: $("status"), }; const state = { sel: null, // { id, paramsB, paramsSource, bits, format, pipeline, gated } or { size: N } result: null, // last match API answer sort: "price", gpuMemory: null, // { "H100 SXM": 80, ... } from FastGPU's GPU list seq: 0, // request counter, so a slow answer never overwrites a newer one active: -1, // highlighted suggestion options: [], }; // ---------------------------------------------------------------- helpers function link(url) { const u = new URL(url, SITE); if (u.hostname.endsWith("fastgpu.co")) Object.keys(UTM).forEach(function (k) { u.searchParams.set(k, UTM[k]); }); return u.toString(); } function esc(s) { return String(s).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); } function usd(n) { if (n == null || !isFinite(n)) return "n/a"; const digits = n < 100 ? 2 : 0; return "$" + n.toLocaleString("en-US", { minimumFractionDigits: digits, maximumFractionDigits: digits }); } function gb(n) { if (n >= 100) return Math.round(n).toLocaleString("en-US"); if (n >= 1) return n.toFixed(1).replace(/\.0$/, ""); return n.toFixed(2).replace(/0$/, ""); } // The parameter count as shown (32.76 -> 32.8), so every figure derived on screen uses the number on screen. function shownParams(p) { if (p >= 1000) return Math.round(p / 10) * 10; if (p >= 10) return Math.round(p * 10) / 10; return Math.round(p * 100) / 100; } function sizeText(p) { const v = shownParams(p); return v >= 1000 ? (v / 1000).toString() + "T" : v.toString() + "B"; } function ago(iso) { const t = Date.parse(iso); if (!isFinite(t)) return null; const min = Math.max(0, Math.round((Date.now() - t) / 60000)); if (min < 1) return "just now"; if (min < 60) return min + " min ago"; const h = Math.round(min / 60); return h + (h === 1 ? " hour ago" : " hours ago"); } function timeout(ms) { const c = new AbortController(); setTimeout(function () { c.abort(); }, ms); return c.signal; } async function getJson(url, ms) { const r = await fetch(url, { signal: timeout(ms) }); if (!r.ok) { const e = new Error("HTTP " + r.status); e.status = r.status; throw e; } return r.json(); } // The total size a repo name states ("Qwen3-30B-A3B" -> 30), skipping the active ("A3B") and expert ("8x7B") forms. function nameSizeB(id) { const name = (id.split("/").pop() || "").replace(/_/g, "-"); const m = name.match(/(?= 0) return (qc.bits || qc.w_bit || qc.nbits) === 8 ? 8 : 4; if (method === "bitsandbytes") return qc.load_in_4bit ? 4 : qc.load_in_8bit ? 8 : null; if (method === "mxfp4" || method === "nvfp4") return 4; if (["fp8", "fbgemm_fp8", "eetq"].indexOf(method) >= 0) return 8; if (method === "compressed-tensors") { const bits = Object.values(qc.config_groups || {}).map(function (g) { return g && g.weights && g.weights.num_bits; }).filter(Boolean); if (bits.length) { const b = Math.min.apply(null, bits); if (b === 4 || b === 8) return b; } } const top = dominantDtype(meta); if (top && top.indexOf("F8") === 0) return 8; if (meta.gguf && !meta.safetensors) return 4; const n = id.toLowerCase(); if (/(^|[-_./])(4bit|4-bit|int4|w4a16|awq|gptq|nf4|mxfp4|nvfp4|q4)/.test(n)) return 4; if (/(^|[-_./])(8bit|8-bit|int8|w8a8|w8a16|fp8)/.test(n)) return 8; return null; } function dominantDtype(meta) { const p = meta.safetensors && meta.safetensors.parameters; if (!p) return null; let best = null; Object.keys(p).forEach(function (k) { if (!best || p[k] > p[best]) best = k; }); return best; } function formatName(meta, bits) { const qc = (meta.config && meta.config.quantization_config) || {}; const method = String(qc.quant_method || "").toUpperCase(); if (meta.gguf && !meta.safetensors) return "GGUF"; if (method && method !== "COMPRESSED-TENSORS" && method !== "BITSANDBYTES") return method + (bits ? " " + bits + "-bit" : ""); if (bits) return bits + "-bit"; const top = dominantDtype(meta); return top || null; } // ---------------------------------------------------------------- model metadata async function readModel(id) { const path = id.split("/").map(encodeURIComponent).join("/"); const q = ["safetensors", "gguf", "config", "pipeline_tag", "library_name", "gated", "downloads", "likes"] .map(function (k) { return "expand[]=" + k; }).join("&"); const meta = await getJson(HUB + "/api/models/" + path + "?" + q, 8000); const total = ((meta.safetensors && meta.safetensors.total) || (meta.gguf && meta.gguf.total) || 0) / 1e9; const stated = nameSizeB(meta.id || id); let paramsB = null, source = null; if (total && stated && total < 0.6 * stated) { paramsB = stated; source = "name"; } else if (total) { paramsB = total; source = meta.safetensors && meta.safetensors.total ? "safetensors" : "gguf"; } else if (stated) { paramsB = stated; source = "name"; } const bits = weightBits(meta, meta.id || id); return { id: meta.id || id, paramsB: paramsB, paramsSource: source, bits: bits, format: formatName(meta, bits), pipeline: meta.pipeline_tag || null, library: meta.library_name || null, gated: !!meta.gated, }; } // ---------------------------------------------------------------- search box function sizeQuery(text) { const m = text.trim().match(/^~?(\d+(?:\.\d+)?)\s*([bt])?(?:\s*(?:params?|parameters?))?$/i); if (!m) return null; const n = parseFloat(m[1]) * (m[2] && m[2].toLowerCase() === "t" ? 1000 : 1); return n >= 0.05 && n <= 5000 ? n : null; } function suggest(text) { const t = text.trim().toLowerCase(); const out = []; if (!t) { PICKS.forEach(function (id) { const m = BY_ID.get(id.toLowerCase()); if (m) out.push({ kind: "model", m: m }); }); return out.slice(0, 8); } const size = sizeQuery(t); if (size) out.push({ kind: "size", size: size }); const words = t.split(/[\s/]+/).filter(Boolean); const scored = []; for (let i = 0; i < CATALOG.length; i++) { const m = CATALOG[i]; if (!words.every(function (w) { return m.low.indexOf(w) >= 0; })) continue; const name = m.low.split("/").pop(); const score = (m.low === t ? 4 : 0) + (name.indexOf(t) === 0 ? 2 : 0) + (m.low.indexOf(t) >= 0 ? 1 : 0); scored.push({ m: m, score: score }); } scored.sort(function (a, b) { return b.score - a.score || b.m.dl - a.m.dl; }); scored.slice(0, 8).forEach(function (s) { out.push({ kind: "model", m: s.m }); }); if (/^[\w.-]+\/[\w.-]+$/.test(text.trim()) && !BY_ID.has(t)) out.unshift({ kind: "hub", id: text.trim() }); return out.slice(0, 9); } function renderSuggestions() { const opts = state.options; if (!opts.length) { closeSuggestions(); return; } el.list.innerHTML = opts.map(function (o, i) { const sel = i === state.active ? ' aria-selected="true"' : ""; if (o.kind === "size") return '
' + facts.join(" · ") + "
" + 'Parameter count from ' + src + "." + (s.library === "mlx" ? " MLX builds run on Apple silicon; sized here as the same model on a rented GPU." : "") + "
~' + esc(Math.round(need).toLocaleString("en-US")) + " GB
" + '' + esc(needSentence(task, precText)) + "
" + '' + esc(derivation(task, paramsB, precision, need)) + ' How FastGPU sizes and ranks
| Setup | Per hour | Per month | Provider |
|---|---|---|---|
| ' + esc(setup) + '' + esc(sub) + " | " + '' + esc(usd(r.effective_usd_hr)) + "" + (r.effective_usd_hr === cheapest ? 'cheapest' : "") + " | " + '' + esc(usd(r.monthly_usd)) + " | " + "" + esc(r.provider_label) + (r.partner ? ' ★' : "") + '' + esc(kind) + " |
' + (fresh ? "Prices from FastGPU's live feed, refreshed " + esc(fresh) + ". " : "") + "Each price is the whole setup per hour (per-GPU rate × GPUs), per month at 730 hours; it links to every live offer for that GPU. " + "Best match orders by FastGPU's score, led by price and weighing reliability and availability; providers marked ★ pay FastGPU a referral fee and can only win a near-tie there, within a few percent of the cheapest. " + "Some providers bill CPU, RAM or disk on top of the GPU rate; the GPU page says which.
" + ''; el.out.innerHTML = head + needHtml + table + foot; el.out.querySelectorAll("button[data-sort]").forEach(function (b) { b.addEventListener("click", function () { state.sort = b.getAttribute("data-sort"); render(); }); }); } // ---------------------------------------------------------------- shareable state function saveHash() { const s = state.sel; if (!s) return; const h = new URLSearchParams(); if (s.id) h.set("model", s.id); else h.set("size", String(s.size)); if (el.task.value !== "inference") h.set("task", el.task.value); if (el.prec.value !== "auto") h.set("precision", el.prec.value); if (el.region.value) h.set("region", el.region.value); if (el.spot.checked) h.set("spot", "1"); try { history.replaceState(null, "", "#" + h.toString()); } catch (e) { /* sandboxed frame */ } } function start() { const raw = (location.hash || "").replace(/^#/, "") || (location.search || "").replace(/^\?/, ""); const h = new URLSearchParams(raw); const task = h.get("task"); if (task && TASKS[task]) el.task.value = task; const prec = h.get("precision"); if (prec && ["fp16", "int8", "int4"].indexOf(prec) >= 0) el.prec.value = prec; const region = h.get("region"); if (region && ["US", "EU", "ASIA"].indexOf(region) >= 0) el.region.value = region; el.spot.checked = h.get("spot") === "1"; const size = h.get("size") ? parseFloat(h.get("size")) : NaN; if (isFinite(size) && size > 0) { el.input.value = sizeText(size) + " model"; selectSize(size); return; } const model = h.get("model") || DEFAULT_MODEL; el.input.value = model; selectModel(model, !task); } start(); })();