Spaces:
Running
Running
File size: 14,592 Bytes
3e2ce32 afcfd33 3e2ce32 afcfd33 3e2ce32 afcfd33 3e2ce32 afcfd33 3e2ce32 afcfd33 3e2ce32 afcfd33 3e2ce32 afcfd33 3e2ce32 afcfd33 3e2ce32 afcfd33 3e2ce32 afcfd33 3e2ce32 | 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 | /* 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);
|