Spaces:
Running
Running
Vramwise: decision models (MATILDA-jev, JEV, Decider), linear-attention + packed FP4 support
afcfd33 verified Download fitcheck/vramwise.js from CyberMax-tools/cybermax: direct link, hf CLI and curl.
- Browser
- Download file 14.6 kB
-
https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/fitcheck/vramwise.js
- Command line
-
hf download hf://spaces/CyberMax-tools/cybermax/fitcheck/vramwise.js
-
curl -L -o vramwise.js https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/fitcheck/vramwise.js
14.6 kB
| /* Vramwise core (pure, no DOM): LLM memory + speed estimates. © 2026 CyberMax. All rights reserved. */ | |
| (function (root) { | |
| "use strict"; | |
| var GB = 1e9; | |
| // Average bits per weight. GGUF values are llama.cpp's published averages for its k-quant/i-quant mixes (approximate: | |
| // they vary a little by architecture). MLX 4-bit = 4 bits + fp16 scale and bias per group of 64. | |
| var QUANTS = [ | |
| { id: "bf16", label: "BF16 / FP16", bits: 16, note: "full precision" }, | |
| { id: "q8_0", label: "Q8_0 / FP8 / MLX 8-bit", bits: 8.5, note: "near-lossless" }, | |
| { id: "q6_k", label: "Q6_K", bits: 6.56, note: "excellent" }, | |
| { id: "q5_k_m", label: "Q5_K_M", bits: 5.69, note: "very good" }, | |
| { id: "q4_k_m", label: "Q4_K_M / MLX 4-bit", bits: 4.85, note: "the usual sweet spot" }, | |
| { id: "iq4_xs", label: "IQ4_XS / AWQ / GPTQ 4-bit", bits: 4.25, note: "good" }, | |
| { id: "q3_k_m", label: "Q3_K_M", bits: 3.91, note: "noticeable quality loss" }, | |
| { id: "q2_k", label: "Q2_K", bits: 2.96, note: "large quality loss" }, | |
| { id: "iq2_xxs", label: "IQ2_XXS", bits: 2.06, note: "last resort" } | |
| ]; | |
| var KV_TYPES = { f16: 16, q8_0: 8.5, q4_0: 4.5 }; | |
| // Spec-sheet memory size (GB) and bandwidth (GB/s). | |
| var GPUS = [ | |
| ["RTX 3060 12GB", 12, 360], ["RTX 3070 8GB", 8, 448], ["RTX 3080 10GB", 10, 760], ["RTX 3090 24GB", 24, 936], | |
| ["RTX 4060 8GB", 8, 272], ["RTX 4060 Ti 16GB", 16, 288], ["RTX 4070 12GB", 12, 504], ["RTX 4070 Ti Super 16GB", 16, 672], | |
| ["RTX 4080 16GB", 16, 717], ["RTX 4090 24GB", 24, 1008], ["RTX 5060 Ti 16GB", 16, 448], ["RTX 5070 12GB", 12, 672], | |
| ["RTX 5070 Ti 16GB", 16, 896], ["RTX 5080 16GB", 16, 960], ["RTX 5090 32GB", 32, 1792], | |
| ["RX 7900 XT 20GB", 20, 800], ["RX 7900 XTX 24GB", 24, 960], ["RX 9070 XT 16GB", 16, 640], | |
| ["Tesla T4 16GB", 16, 320], ["L4 24GB", 24, 300], ["A10G 24GB", 24, 600], ["L40S 48GB", 48, 864], | |
| ["RTX 6000 Ada 48GB", 48, 960], ["RTX PRO 6000 96GB", 96, 1792], ["A100 40GB", 40, 1555], ["A100 80GB", 80, 2039], | |
| ["H100 80GB (SXM)", 80, 3350], ["H200 141GB", 141, 4800] | |
| ].map(function (g) { return { name: g[0], vram: g[1], bw: g[2] }; }); | |
| // Apple silicon: memory sizes offered and bandwidth. Binned M3 Max / M4 Max chips have lower bandwidth at their smallest size. | |
| var APPLE = [ | |
| { name: "M1", mem: [8, 16], bw: 68 }, { name: "M1 Pro", mem: [16, 32], bw: 200 }, { name: "M1 Max", mem: [32, 64], bw: 400 }, | |
| { name: "M1 Ultra", mem: [64, 128], bw: 800 }, { name: "M2", mem: [8, 16, 24], bw: 100 }, { name: "M2 Pro", mem: [16, 32], bw: 200 }, | |
| { name: "M2 Max", mem: [32, 64, 96], bw: 400 }, { name: "M2 Ultra", mem: [64, 128, 192], bw: 800 }, | |
| { name: "M3", mem: [8, 16, 24], bw: 100 }, { name: "M3 Pro", mem: [18, 36], bw: 150 }, | |
| { name: "M3 Max", mem: [36, 48, 64, 96, 128], bw: 400, bwByMem: { 36: 300, 96: 300 } }, | |
| { name: "M3 Ultra", mem: [96, 256, 512], bw: 819 }, { name: "M4", mem: [16, 24, 32], bw: 120 }, | |
| { name: "M4 Pro", mem: [24, 48, 64], bw: 273 }, { name: "M4 Max", mem: [36, 48, 64, 128], bw: 546, bwByMem: { 36: 410 } } | |
| ]; | |
| // Other unified-memory machines: total memory, how much the GPU may use by default, bandwidth. | |
| var UNIFIED = [ | |
| { name: "NVIDIA DGX Spark 128GB", mem: 128, usable: 120, bw: 273 }, | |
| { name: "AMD Ryzen AI Max+ 395 128GB", mem: 128, usable: 96, bw: 256 } | |
| ]; | |
| function num(v) { var n = Number(v); return isFinite(n) ? n : 0; } | |
| // Flatten nested configs (Gemma 3, Llama 4, Mistral 3 put the LM under text_config). | |
| function flatConfig(cfg) { | |
| cfg = cfg || {}; | |
| var out = {}; | |
| Object.keys(cfg).forEach(function (k) { out[k] = cfg[k]; }); | |
| var t = cfg.text_config || cfg.llm_config || cfg.language_config; | |
| if (t && typeof t === "object") Object.keys(t).forEach(function (k) { if (t[k] !== undefined && t[k] !== null) out[k] = t[k]; }); | |
| return out; | |
| } | |
| // Build a model description from HF API metadata (/api/models/{id}) and config.json. | |
| function describe(meta, config) { | |
| meta = meta || {}; | |
| var c = flatConfig(config || meta.config); | |
| var st = meta.safetensors || {}; | |
| var params = num(st.total) || num(meta.gguf && meta.gguf.total) || num(c.num_parameters); | |
| var p = st.parameters || {}; | |
| var qc = c.quantization_config || {}; | |
| var quant = qc.quant_method || ""; | |
| // Packed FP4 (NVFP4 / E2M1 with FP8 block scales per 16): the Hub counts each U8 byte, which holds two weights, as one | |
| // parameter and the FP8 scales as parameters. Count the real weights instead. | |
| var u8 = num(p.U8), f8 = num(p.F8_E4M3); | |
| var fp4 = u8 > 0 && (/nvfp4/i.test(qc.quant_algo || "") || (f8 > 0 && Math.abs(f8 - u8 * 2 / 16) <= u8 * 0.01)); | |
| if (fp4 && num(st.total)) { | |
| params = num(st.total) + u8 - (/nvfp4/i.test(qc.quant_algo || "") ? 0 : f8); | |
| quant = /nvfp4/i.test(qc.quant_algo || "") ? "nvfp4" : quant && quant !== "modelopt" ? quant : "fp4"; | |
| } | |
| var nativeBytes = 0; | |
| Object.keys(p).forEach(function (k) { | |
| var n = num(p[k]); | |
| var b = /^(F64|I64)$/.test(k) ? 8 : /^(F32|I32)$/.test(k) ? 4 : /^(BF16|F16|I16)$/.test(k) ? 2 : 1; | |
| if (k === "U8" && quant === "mxfp4") b = 17 / 32; // 4-bit values + one 8-bit scale per 32 | |
| nativeBytes += n * b; | |
| }); | |
| var layers = num(c.num_hidden_layers || c.n_layer || c.num_layers); | |
| var heads = num(c.num_attention_heads || c.n_head); | |
| var kvHeads = num(c.num_key_value_heads || c.num_kv_heads || c.multi_query_group_num) || heads; | |
| var hidden = num(c.hidden_size || c.n_embd || c.d_model); | |
| var headDim = num(c.head_dim) || (heads ? hidden / heads : 0); | |
| var nExperts = num(c.num_local_experts || c.num_experts || c.n_routed_experts); | |
| var topK = num(c.num_experts_per_tok || c.experts_per_token || c.moe_topk || c.top_k_experts); | |
| var active = params; | |
| if (nExperts > 1 && topK > 0 && hidden) { | |
| var moeInter = num(c.moe_intermediate_size) || num(c.intermediate_size); | |
| var moeLayers = layers - num(c.first_k_dense_replace); | |
| var inactive = (nExperts - topK) * 3 * hidden * moeInter * moeLayers; | |
| active = Math.max(params * 0.03, params - inactive); | |
| } | |
| // Sliding-window layers only keep `window` tokens of cache. Linear-attention layers (Qwen3.5 / Qwen3-Next style | |
| // gated delta-net) keep a fixed-size state that does not grow with context. | |
| var win = num(c.sliding_window); | |
| var fullLayers = layers, linearLayers = 0; | |
| var types = Array.isArray(c.layer_types) && c.layer_types.length === layers ? c.layer_types : null; | |
| if (types) linearLayers = types.filter(function (t) { return /linear|mamba|recurrent|ssm/.test(t); }).length; | |
| if (win && types) { | |
| fullLayers = types.filter(function (t) { return !/sliding|linear|mamba|recurrent|ssm/.test(t); }).length; | |
| } else if (win && num(c.sliding_window_pattern) > 1) { | |
| fullLayers = Math.floor(layers / num(c.sliding_window_pattern)); | |
| } else { | |
| win = 0; | |
| fullLayers = layers - linearLayers; | |
| } | |
| var slidingLayers = layers - fullLayers - linearLayers; | |
| // Fixed recurrent state per linear layer: value heads x key dim x value dim (state dtype) + a short conv cache. | |
| var stateBytes = 0; | |
| if (linearLayers) { | |
| var lvh = num(c.linear_num_value_heads), lkh = num(c.linear_num_key_heads), lkd = num(c.linear_key_head_dim), lvd = num(c.linear_value_head_dim); | |
| var sb = /float32|fp32/.test(c.mamba_ssm_dtype || "float32") ? 4 : 2; | |
| stateBytes = linearLayers * (lvh * lkd * lvd * sb + num(c.linear_conv_kernel_dim || 4) * (2 * lkh * lkd + lvh * lvd) * 2); | |
| } | |
| // Gemma 4 style: full-attention layers use their own (fewer, wider) KV heads, optionally with K and V shared. | |
| var gKv = num(c.num_global_key_value_heads), gHd = num(c.global_head_dim); | |
| var fullKvPerTok = gKv && gHd ? (c.attention_k_eq_v ? 1 : 2) * gKv * gHd : 0; | |
| // One-pass models (classifiers, decision/scoring models) read the prompt once and generate nothing. | |
| var tags = Array.isArray(meta.tags) ? meta.tags : []; | |
| var pipe = meta.pipeline_tag || ""; | |
| var decision = tags.indexOf("decision-model") >= 0 || | |
| /^(text-classification|zero-shot-classification|token-classification|feature-extraction|sentence-similarity)$/.test(pipe); | |
| var mla = num(c.kv_lora_rank) ? num(c.kv_lora_rank) + num(c.qk_rope_head_dim) : 0; | |
| return { | |
| id: meta.id || meta.modelId || "", | |
| params: params, | |
| activeParams: active, | |
| moe: active < params * 0.97, | |
| experts: nExperts, topK: topK, | |
| nativeBytes: nativeBytes, | |
| nativeLabel: quant ? quant.toUpperCase() : Object.keys(p).sort(function (a, b) { return num(p[b]) - num(p[a]); })[0] || "", | |
| layers: layers, kvHeads: kvHeads, headDim: headDim, | |
| fullLayers: fullLayers, slidingLayers: slidingLayers, window: win, | |
| linearLayers: linearLayers, stateBytes: stateBytes, fullKvPerTok: fullKvPerTok, | |
| decision: decision, pipeline: pipe, | |
| mla: mla, | |
| maxContext: num(c.max_position_embeddings) || num(meta.gguf && meta.gguf.context_length) || 0, | |
| gated: !!meta.gated, | |
| hasShape: !!(layers && (mla || (kvHeads && headDim))) | |
| }; | |
| } | |
| // KV cache bytes for `ctx` tokens (batch 1) at `bits` per element. | |
| function kvBytes(m, ctx, bits) { | |
| if (!m.hasShape) return 0; | |
| var bytes = (bits || 16) / 8; | |
| if (m.mla) return m.layers * m.mla * bytes * ctx; | |
| var perLayerTok = 2 * m.kvHeads * m.headDim * bytes; | |
| var fullTok = m.fullKvPerTok ? m.fullKvPerTok * bytes : perLayerTok; | |
| var slidingCtx = m.window ? Math.min(ctx, m.window) : ctx; | |
| var sliding = m.slidingLayers || 0; | |
| var full = m.fullLayers !== undefined ? m.fullLayers : m.layers - sliding; | |
| return fullTok * full * ctx + perLayerTok * sliding * slidingCtx + (m.stateBytes || 0); | |
| } | |
| function weightBytes(m, bits) { return m.params * bits / 8; } | |
| var OVERHEAD = 1.0 * GB; // runtime context + compute buffers (rough) | |
| // hw: { usable: GB the GPU can use, bw: GB/s, ramGB: extra system RAM for offload (0 = none), ramBw: GB/s } | |
| function estimate(m, hw, q, ctx, kvBits) { | |
| var w = q.bytes !== undefined ? q.bytes : weightBytes(m, q.bits); | |
| var kv = kvBytes(m, ctx, kvBits); | |
| var total = w + kv + OVERHEAD; | |
| var usable = hw.usable * GB; | |
| var activeW = m.params ? w * (m.activeParams / m.params) : w; | |
| var perToken = activeW + kv; // bytes read per generated token (batch 1) | |
| var r = { weights: w, kv: kv, total: total, usable: usable, fit: "no", tpsLo: 0, tpsHi: 0 }; | |
| if (total <= usable) { | |
| r.fit = total <= usable * 0.9 ? "yes" : "tight"; | |
| var ceil = hw.bw * GB / perToken; | |
| r.tpsLo = ceil * 0.5; r.tpsHi = ceil * 0.8; | |
| // One-pass models: each call reads the weights once, so weights / bandwidth is the shortest possible call (batch 1). | |
| // Long prompts add compute time on top; this is a floor, not a prediction. | |
| r.minMs = activeW / (hw.bw * GB) * 1000; | |
| } else if (hw.ramGB > 0 && total <= usable + hw.ramGB * GB * 0.85) { | |
| r.fit = "offload"; | |
| var onGpu = Math.max(0, usable - kv - OVERHEAD); | |
| var share = w ? Math.min(1, onGpu / w) : 1; | |
| var t = (activeW * share + kv) / (hw.bw * GB) + (activeW * (1 - share)) / ((hw.ramBw || 50) * GB); | |
| r.gpuShare = share; | |
| r.tpsLo = 0.5 / t; r.tpsHi = 0.8 / t; | |
| } | |
| // Longest context that still fits entirely in usable memory. | |
| var free = usable - w - OVERHEAD; | |
| var per = kvBytes(m, 1, kvBits); | |
| if (free <= 0) r.maxCtx = 0; | |
| else if (!per) r.maxCtx = m.maxContext || 0; | |
| else { | |
| var lo = 0, hi = Math.max(m.maxContext || 131072, 1024); | |
| if (kvBytes(m, hi, kvBits) <= free) lo = hi; | |
| else while (hi - lo > 256) { var mid = Math.floor((lo + hi) / 2); if (kvBytes(m, mid, kvBits) <= free) lo = mid; else hi = mid; } | |
| r.maxCtx = Math.floor(lo / 256) * 256; | |
| } | |
| return r; | |
| } | |
| function quantRows(m) { | |
| var rows = QUANTS.slice(); | |
| if (m.nativeBytes && m.nativeLabel && !/^(BF16|F16)$/.test(m.nativeLabel)) { | |
| 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" }); | |
| } | |
| return rows; | |
| } | |
| // 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). | |
| // raised = the user lifted the limit, leaving ~8 GB for macOS. | |
| function appleHardware(chip, memGB, raised) { | |
| var c = APPLE.filter(function (a) { return a.name === chip; })[0] || APPLE[0]; | |
| var bw = (c.bwByMem && c.bwByMem[memGB]) || c.bw; | |
| var usable = raised ? Math.max(memGB - 8, memGB * 0.75) : memGB * (memGB <= 36 ? 2 / 3 : 0.75); | |
| return { label: c.name + " " + memGB + "GB", usable: usable, bw: bw, ramGB: 0, unified: true }; | |
| } | |
| function gpuHardware(name, count, ramGB, ramBw) { | |
| var g = GPUS.filter(function (x) { return x.name === name; })[0] || GPUS[0]; | |
| count = Math.max(1, Math.floor(num(count)) || 1); | |
| // Split across cards, per-token speed is closer to one card's bandwidth than the sum (conservative). | |
| return { label: (count > 1 ? count + "× " : "") + g.name, usable: g.vram * count, bw: g.bw, ramGB: num(ramGB), ramBw: num(ramBw) || 50 }; | |
| } | |
| function unifiedHardware(name) { | |
| var u = UNIFIED.filter(function (x) { return x.name === name; })[0] || UNIFIED[0]; | |
| return { label: u.name, usable: u.usable, bw: u.bw, ramGB: 0, unified: true }; | |
| } | |
| function fmtGB(bytes) { var g = bytes / GB; return g >= 100 ? g.toFixed(0) : g >= 10 ? g.toFixed(1) : g.toFixed(2); } | |
| 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"; } | |
| function fmtCtx(n) { return n >= 1024 ? Math.round(n / 1024) + "K" : String(n); } | |
| // "owner/name" from a pasted URL or id. | |
| function parseModelId(s) { | |
| s = String(s || "").trim(); | |
| var m = s.match(/huggingface\.co\/(?:models\/)?([\w.-]+\/[\w.-]+)/); | |
| if (m) return m[1]; | |
| m = s.match(/^([\w.-]+\/[\w.-]+)$/); | |
| return m ? m[1] : ""; | |
| } | |
| var api = { QUANTS: QUANTS, KV_TYPES: KV_TYPES, GPUS: GPUS, APPLE: APPLE, UNIFIED: UNIFIED, GB: GB, | |
| describe: describe, kvBytes: kvBytes, weightBytes: weightBytes, estimate: estimate, quantRows: quantRows, | |
| appleHardware: appleHardware, gpuHardware: gpuHardware, unifiedHardware: unifiedHardware, | |
| fmtGB: fmtGB, fmtParams: fmtParams, fmtCtx: fmtCtx, parseModelId: parseModelId, flatConfig: flatConfig }; | |
| root.Vramwise = api; | |
| if (typeof module !== "undefined") module.exports = api; | |
| })(typeof window !== "undefined" ? window : globalThis); | |