Files
lmcache--lmcache/docs/source/_static/kv_cache_calculator.html
T
2026-07-13 12:24:33 +08:00

875 lines
49 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KV Cache Size Calculator</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
margin: 0;
padding: 0;
background: transparent;
overflow: hidden;
}
.calculator-container {
max-width: 100%;
margin: 0;
background: transparent;
padding: 0;
}
h1 {
display: none;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #333;
font-size: 14px;
}
select, input {
width: 100%;
padding: 12px;
font-size: 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
background: white;
transition: border-color 0.2s;
}
select:focus, input:focus {
outline: none;
border-color: #3898ec;
}
button {
width: 100%;
padding: 14px;
font-size: 16px;
font-weight: 600;
background: #3898ec;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #2578cc;
}
button:active {
transform: scale(0.98);
}
.result {
margin-top: 24px;
padding: 20px;
background: #f0f9ff;
border-left: 4px solid #3898ec;
border-radius: 8px;
display: none;
}
.result.show {
display: block;
}
.result-value {
font-size: 32px;
font-weight: 700;
color: #1a1a1a;
margin: 8px 0;
}
.result-label {
font-size: 14px;
color: #666;
margin-bottom: 4px;
}
.details {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #d0e8f7;
font-size: 13px;
color: #555;
line-height: 1.6;
}
.details strong {
color: #333;
}
.error {
background: #fee;
border-left-color: #f44;
}
.loading {
text-align: center;
color: #666;
padding: 20px;
font-size: 14px;
}
</style>
</head>
<body>
<div class="calculator-container">
<h1>KV Cache Size Calculator</h1>
<div class="loading" id="loading">Loading model configurations...</div>
<div id="calculator-form" style="display: none;">
<div class="form-group">
<label for="model">Model:</label>
<select id="model" onchange="updatePrecisionVisibility('')">
<!-- Options will be populated dynamically -->
</select>
</div>
<div class="form-group">
<label for="dtype">Data Type:</label>
<select id="dtype">
<option value="float16">float16 (FP16)</option>
<option value="bfloat16" selected>bfloat16 (BF16)</option>
<option value="float32">float32 (FP32)</option>
<option value="int8">int8 (INT8)</option>
</select>
</div>
<div class="form-group" id="precision-group" style="display: none;">
<label for="precision-mode">DeepSeek V4 KV Precision (bytes / dimension):</label>
<select id="precision-mode" onchange="togglePrecisionInputs('')">
<option value="default" selected>Paper default — FP8 NoPE / BF16 RoPE / FP4 indexer</option>
<option value="custom">Custom…</option>
</select>
<div id="precision-custom" style="display: none; margin-top: 10px;">
<label for="nope-bytes">NoPE bytes/dim:</label>
<input type="number" id="nope-bytes" min="0" step="0.1" value="1">
<label for="rope-bytes">RoPE bytes/dim:</label>
<input type="number" id="rope-bytes" min="0" step="0.1" value="2">
<label for="indexer-bytes">Indexer bytes/dim:</label>
<input type="number" id="indexer-bytes" min="0" step="0.1" value="0.5">
</div>
</div>
<div class="form-group">
<label for="tokens">Number of Tokens:</label>
<input type="number" id="tokens" placeholder="e.g., 1000" min="1" value="1000">
</div>
<button onclick="calculateKVCache()">Calculate KV Cache Size</button>
<div id="result" class="result"></div>
<hr style="margin: 40px 0; border: none; border-top: 1px solid #e0e0e0;">
<h2 style="font-size: 18px; margin-bottom: 20px; color: #1a1a1a;">Reverse Calculator: Find Maximum Tokens</h2>
<div class="form-group">
<label for="model-reverse">Model:</label>
<select id="model-reverse" onchange="updatePrecisionVisibility('-reverse')">
<!-- Options will be populated dynamically -->
</select>
</div>
<div class="form-group">
<label for="dtype-reverse">Data Type:</label>
<select id="dtype-reverse">
<option value="float16">float16 (FP16)</option>
<option value="bfloat16" selected>bfloat16 (BF16)</option>
<option value="float32">float32 (FP32)</option>
<option value="int8">int8 (INT8)</option>
</select>
</div>
<div class="form-group" id="precision-group-reverse" style="display: none;">
<label for="precision-mode-reverse">DeepSeek V4 KV Precision (bytes / dimension):</label>
<select id="precision-mode-reverse" onchange="togglePrecisionInputs('-reverse')">
<option value="default" selected>Paper default — FP8 NoPE / BF16 RoPE / FP4 indexer</option>
<option value="custom">Custom…</option>
</select>
<div id="precision-custom-reverse" style="display: none; margin-top: 10px;">
<label for="nope-bytes-reverse">NoPE bytes/dim:</label>
<input type="number" id="nope-bytes-reverse" min="0" step="0.1" value="1">
<label for="rope-bytes-reverse">RoPE bytes/dim:</label>
<input type="number" id="rope-bytes-reverse" min="0" step="0.1" value="2">
<label for="indexer-bytes-reverse">Indexer bytes/dim:</label>
<input type="number" id="indexer-bytes-reverse" min="0" step="0.1" value="0.5">
</div>
</div>
<div class="form-group">
<label for="gpu-ram">GPU RAM Size (GB):</label>
<input type="number" id="gpu-ram" placeholder="e.g., 20" min="0.1" step="0.1" value="20">
</div>
<button onclick="calculateMaxTokens()">Calculate Maximum Tokens</button>
<div id="result-reverse" class="result"></div>
</div>
</div>
<script>
let modelConfigs = {};
// Load model configurations
async function loadModelConfigs() {
const urls = [
'modelconfig.json',
'_static/modelconfig.json',
'../_static/modelconfig.json',
'https://raw.githubusercontent.com/LMCache/LMCache/refs/heads/dev/examples/kv_cache_calculator/modelconfig.json'
];
for (const url of urls) {
try {
const response = await fetch(url);
if (response.ok) {
modelConfigs = await response.json();
populateModelDropdown();
document.getElementById('loading').style.display = 'none';
document.getElementById('calculator-form').style.display = 'block';
return;
}
} catch (error) {
console.log('Failed to load from:', url);
}
}
document.getElementById('loading').innerHTML =
'<div class="error">Failed to load model configurations. Please check your connection.</div>';
}
// Populate model dropdown
function populateModelDropdown() {
const select = document.getElementById('model');
const selectReverse = document.getElementById('model-reverse');
select.innerHTML = '';
selectReverse.innerHTML = '';
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
const sortedModels = Object.keys(modelConfigs).sort(collator.compare);
const defaultModel = 'Qwen/Qwen3-8B';
for (const modelName of sortedModels) {
const option = document.createElement('option');
option.value = modelName;
option.textContent = modelName;
if (modelName === defaultModel) {
option.selected = true;
}
select.appendChild(option);
const optionReverse = document.createElement('option');
optionReverse.value = modelName;
optionReverse.textContent = modelName;
if (modelName === defaultModel) {
optionReverse.selected = true;
}
selectReverse.appendChild(optionReverse);
}
updatePrecisionVisibility('');
updatePrecisionVisibility('-reverse');
}
// Show the V4 precision controls only for DSA (compress_ratios) models, and
// seed the custom inputs with the selected model's paper-default byte widths.
function updatePrecisionVisibility(suffix) {
const model = document.getElementById('model' + suffix).value;
const config = modelConfigs[model] || {};
const isDSA = Array.isArray(config.compress_ratios)
&& Number.isFinite(config.head_dim);
document.getElementById('precision-group' + suffix).style.display =
isDSA ? 'block' : 'none';
if (isDSA) {
document.getElementById('nope-bytes' + suffix).value =
Number.isFinite(config.nope_bytes) ? config.nope_bytes : 1;
document.getElementById('rope-bytes' + suffix).value =
Number.isFinite(config.rope_bytes) ? config.rope_bytes : 2;
document.getElementById('indexer-bytes' + suffix).value =
Number.isFinite(config.indexer_bytes) ? config.indexer_bytes : 0.5;
}
}
// Reveal the custom byte inputs only when "Custom" is selected.
function togglePrecisionInputs(suffix) {
const mode = document.getElementById('precision-mode' + suffix).value;
document.getElementById('precision-custom' + suffix).style.display =
mode === 'custom' ? 'block' : 'none';
}
// Resolve the per-dimension byte widths for a DSA model: paper defaults from
// the config (nope/rope/indexer_bytes), overridden by the custom inputs when
// the precision mode is "custom". Returns bytes/dim for each component + mode.
function getDSAPrecision(suffix, config) {
const def = {
nope: Number.isFinite(config.nope_bytes) ? config.nope_bytes : 1,
rope: Number.isFinite(config.rope_bytes) ? config.rope_bytes : 2,
indexer: Number.isFinite(config.indexer_bytes) ? config.indexer_bytes : 0.5,
mode: 'default',
};
if (document.getElementById('precision-mode' + suffix).value !== 'custom') {
return def;
}
const nope = parseFloat(document.getElementById('nope-bytes' + suffix).value);
const rope = parseFloat(document.getElementById('rope-bytes' + suffix).value);
const indexer = parseFloat(document.getElementById('indexer-bytes' + suffix).value);
return {
nope: Number.isFinite(nope) ? nope : def.nope,
rope: Number.isFinite(rope) ? rope : def.rope,
indexer: Number.isFinite(indexer) ? indexer : def.indexer,
mode: 'custom',
};
}
// Calculate KV cache size
function calculateKVCache() {
const model = document.getElementById('model').value;
const tokens = parseInt(document.getElementById('tokens').value);
const dtype = document.getElementById('dtype').value;
const resultDiv = document.getElementById('result');
// Validation
if (isNaN(tokens) || tokens <= 0) {
resultDiv.className = 'result show error';
resultDiv.innerHTML = '<div class="result-label">Error</div><div>Please enter a valid number of tokens (greater than 0).</div>';
return;
}
const config = modelConfigs[model];
if (!config) {
resultDiv.className = 'result show error';
resultDiv.innerHTML = '<div class="result-label">Error</div><div>Model configuration not found.</div>';
return;
}
// Get dtype size
const dtypeSizes = {
'float32': 4,
'float16': 2,
'bfloat16': 2,
'int8': 1
};
const dtypeSize = dtypeSizes[dtype];
// Calculate based on model architecture
// DeepSeek V4 sparse attention: per-layer compression schedule.
const isDSA = Array.isArray(config.compress_ratios)
&& Number.isFinite(config.head_dim);
// Multi-head Latent Attention (DeepSeek V3/R1, GLM-5.x DSA): a single
// latent KV vector per layer per token. Feature-gated on kv_lora_rank
// rather than the model name, so non-DeepSeek MLA models are caught too.
const isMLA = Number.isFinite(config.kv_lora_rank)
&& Number.isFinite(config.qk_rope_head_dim);
// Interleaved sliding-window / full attention (Gemma 2/3/4): the sliding
// layers cap their KV cache at the window, only the full layers grow.
const isHybridSWA = Number.isFinite(config.full_attention_layers)
&& Number.isFinite(config.sliding_attention_layers)
&& Number.isFinite(config.sliding_window)
&& Number.isFinite(config.num_key_value_heads)
&& Number.isFinite(config.head_dim);
// Linear/full hybrid (Qwen3-Next / Qwen3.5): Gated DeltaNet layers keep a
// fixed-size recurrent state (no per-token KV); only full layers grow.
const isHybridLinear = Number.isFinite(config.full_attention_layers)
&& Number.isFinite(config.linear_attention_layers)
&& Number.isFinite(config.num_key_value_heads)
&& Number.isFinite(config.head_dim);
const hasHeadDim = Number.isFinite(config.head_dim)
&& Number.isFinite(config.num_key_value_heads);
let totalElements;
let totalBytes;
let sizeGB;
let details = '';
if (isDSA) {
// DeepSeek V4 hybrid attention: Compressed Sparse Attention (CSA)
// + Heavily Compressed Attention (HCA). This is NOT MLA — V4 has no
// kv_lora_rank; head_dim (512) is the width of one shared compressed
// KV entry (per the paper's CSA/HCA shared-KV MQA). Each layer
// compresses every `r` tokens into one entry, so the amortized
// per-token KV cost is head_dim × Σ(1/r) over layers with r > 0.
// (r == 0 layers are pure sliding-window; no compressed entries.)
//
// This branch MUST come before the head_dim/GQA branch below:
// V4 configs carry num_key_value_heads == 1 and head_dim == 512,
// which would otherwise (incorrectly) match the standard
// 2 × layers × kv_heads × head_dim formula and overestimate wildly.
//
// Lightning-indexer keys: the paper compresses them at the SAME
// ratio as the KV entries (K_IComp ∈ R^{(n/m)×c_I}), so the indexer
// key cache is index_head_dim × Σ(1/r) — NOT full-rate. It is a
// ~25% addition to the compressed KV term, not ~2×. (Only CSA
// layers run the indexer; HCA keeps dense attention, but HCA's
// large r contributes negligibly to Σ(1/r), so summing over all
// r > 0 overcounts by <1%.) Net per-token: (head_dim + c_I) × Σ(1/r).
//
// Plus a constant sliding-window floor (numLayers × sliding_window
// × head_dim): ~50% of the total below ~1K tokens, <3% above ~32K.
//
// V4 uses its NATIVE mixed precision (paper §2.3.4), so this branch
// computes bytes directly and the dtype dropdown does NOT apply. The
// per-dimension byte widths come from getDSAPrecision(): the model's
// nope_bytes / rope_bytes / indexer_bytes config defaults (the paper
// values below), overridable live via the "Custom" precision control.
// - KV entry (head_dim dims): RoPE dims (qk_rope_head_dim) in BF16
// (2 B), the rest in FP8 (1 B) -> bytes/entry = (head_dim - rope)
// ·nope + rope·rope_bytes. Paper V4: (512-64)·1 + 64·2 = 576 B
// ("nearly half" of the 1024 B a pure-BF16 entry would take).
// - Indexer keys: FP4 (0.5 B/dim), compressed at the same ratio.
// - Sliding-window entries are uncompressed KV -> same 576 B/entry.
// Only remaining simplification: amortized Σ(1/r), not Σ ceil(tokens/r)
// (<1% at long context). Absolute check: V4-Pro ≈ 4.8 GB @ 1M ctx,
// ~10-12% of a same-precision V3.2 estimate — matching the paper's
// "~10% of V3.2 KV cache". (The V4/V3.2 ratio is ~8.7× / ~12.4× for
// Pro/Flash; precision cancels in the ratio, so the small gap to the
// paper's 9.5× / 13.7× is V3.2-side modeling detail, not V4.)
const activeRatios = config.compress_ratios.filter(r => r > 0);
const compressFactor = activeRatios.reduce((sum, r) => sum + 1 / r, 0);
const ropeDim = Number.isFinite(config.qk_rope_head_dim) ? config.qk_rope_head_dim : 0;
const indexerDim = Number.isFinite(config.index_head_dim) ? config.index_head_dim : 0;
const prec = getDSAPrecision('', config);
const kvBytesPerEntry = (config.head_dim - ropeDim) * prec.nope + ropeDim * prec.rope;
const indexerBytesPerEntry = indexerDim * prec.indexer;
const bytesPerToken = (kvBytesPerEntry + indexerBytesPerEntry) * compressFactor;
const numLayers = config.compress_ratios.length;
const windowBytes = numLayers * config.sliding_window * kvBytesPerEntry;
totalBytes = bytesPerToken * tokens + windowBytes;
totalElements = totalBytes; // unused downstream; bytes are authoritative for V4
sizeGB = totalBytes / (1024 ** 3);
const precLabel = prec.mode === 'custom'
? `custom (NoPE ${prec.nope} / RoPE ${prec.rope} / indexer ${prec.indexer} B/dim)`
: `paper default (NoPE ${prec.nope}=FP8 / RoPE ${prec.rope}=BF16 / indexer ${prec.indexer}=FP4); dtype selector not applied`;
const indexerLine = indexerDim
? `<strong>Indexer Key (CSA, compressed):</strong> ${indexerDim} × ${prec.indexer} × Σ(1/r) = ${(indexerBytesPerEntry * compressFactor).toFixed(1)} B/token<br>`
: '';
details = `
<strong>Selected Model:</strong> ${model}<br>
<strong>Number of Hidden Layers:</strong> ${config.num_hidden_layers}<br>
<strong>Head Dimension:</strong> ${config.head_dim} (RoPE ${ropeDim} @ ${prec.rope} B, NoPE ${config.head_dim - ropeDim} @ ${prec.nope} B)<br>
<strong>Compressed Layers (ratio &gt; 0):</strong> ${activeRatios.length} of ${numLayers}<br>
<strong>Sliding Window:</strong> ${config.sliding_window} (per layer)<br>
<strong>KV Bytes / Entry:</strong> (${config.head_dim} ${ropeDim}) × ${prec.nope} + ${ropeDim} × ${prec.rope} = ${kvBytesPerEntry} B<br>
${indexerLine}<strong>Precision:</strong> ${precLabel}<br>
<strong>Bytes per Token:</strong> (${kvBytesPerEntry} + ${indexerBytesPerEntry}) × Σ(1/r) = ${bytesPerToken.toFixed(1)} B<br>
<strong>Sliding-Window Floor:</strong> ${numLayers} × ${config.sliding_window} × ${kvBytesPerEntry} = ${windowBytes} B<br>
<strong>Total Bytes:</strong> ${bytesPerToken.toFixed(1)} × ${tokens} + ${windowBytes} = ${totalBytes.toFixed(0)} bytes<br>
<strong>KV Cache Size:</strong> ${totalBytes.toFixed(0)} / (1024³) ≈ ${sizeGB.toFixed(4)} GB
`;
} else if (isMLA) {
// Multi-head Latent Attention (DeepSeek V3/R1, GLM-5.x). Each layer
// caches one latent KV vector of (kv_lora_rank + qk_rope_head_dim)
// per token. DSA models (e.g. GLM-5.x) additionally cache a
// lightning-indexer key of index_head_dim per token; LMCache stores
// that indexer group in the same KV pool, so we fold it in whenever
// index_head_dim is present. The indexer key is often kept at lower
// precision than the latent cache, so at one uniform dtype this is a
// slight over-estimate (the safe direction for capacity planning).
const latentDim = config.kv_lora_rank + config.qk_rope_head_dim;
const indexerDim = Number.isFinite(config.index_head_dim) ? config.index_head_dim : 0;
const elementsPerToken = config.num_hidden_layers * (latentDim + indexerDim);
totalElements = elementsPerToken * tokens;
totalBytes = totalElements * dtypeSize;
sizeGB = totalBytes / (1024 ** 3);
const indexerLine = indexerDim
? `<strong>Indexer Head Dim (DSA):</strong> ${indexerDim}<br>`
: '';
const latentExpr = indexerDim
? `(${config.kv_lora_rank} + ${config.qk_rope_head_dim} + ${indexerDim})`
: `(${config.kv_lora_rank} + ${config.qk_rope_head_dim})`;
details = `
<strong>Selected Model:</strong> ${model}<br>
<strong>Number of Hidden Layers:</strong> ${config.num_hidden_layers}<br>
<strong>KV LoRA Rank:</strong> ${config.kv_lora_rank}<br>
<strong>QK RoPE Head Dim:</strong> ${config.qk_rope_head_dim}<br>
${indexerLine}<strong>Data Type Size:</strong> ${dtypeSize} bytes<br>
<strong>Total Elements:</strong> ${config.num_hidden_layers} × ${tokens} × ${latentExpr} = ${totalElements}<br>
<strong>Total Bytes:</strong> ${totalElements} × ${dtypeSize} = ${totalBytes} bytes<br>
<strong>KV Cache Size:</strong> ${totalBytes} / (1024³) ≈ ${sizeGB.toFixed(4)} GB
`;
} else if (isHybridLinear) {
// Attention/recurrent hybrid (Qwen3-Next / Qwen3.5 Gated DeltaNet;
// Nemotron-H Mamba2 SSM). The recurrent (linear) layers keep a
// fixed-size state (no per-token KV growth); only the full-attention
// layers grow with the sequence:
// 2 × kv_heads × head_dim × n_full × tokens (growing KV)
//
// TODO(SSM): the recurrent layers also hold a constant SSM/conv state.
// We are holding off on counting it until LMCache decides how it stores
// linear-attention layers (whether the state lives in the KV-cache pool
// at all, and at what precision / layout). The model's linear_* dims are
// kept in the config so we can wire in the exact constant once that's
// settled. For now we report only the full-attention KV cache.
//
// linear_attention_layers counts only the recurrent layers; any
// remaining layers (e.g. Nemotron's pure-MLP/MoE layers) hold no cache,
// so n_full + n_linear need not equal num_hidden_layers.
const linType = typeof config.linear_attention_type === 'string'
? config.linear_attention_type : 'Gated DeltaNet';
const perLayer = 2 * config.num_key_value_heads * config.head_dim;
const nFull = config.full_attention_layers;
const nLin = config.linear_attention_layers;
totalElements = perLayer * nFull * tokens;
totalBytes = totalElements * dtypeSize;
sizeGB = totalBytes / (1024 ** 3);
details = `
<strong>Selected Model:</strong> ${model}<br>
<strong>Number of Hidden Layers:</strong> ${config.num_hidden_layers}<br>
<strong>Number of Key-Value Heads:</strong> ${config.num_key_value_heads}<br>
<strong>Head Dimension:</strong> ${config.head_dim}<br>
<strong>Full-Attention Layers:</strong> ${nFull} (growing KV)<br>
<strong>Linear (${linType}) Layers:</strong> ${nLin} (recurrent state — not yet counted, TODO pending LMCache storage decision)<br>
<strong>Data Type Size:</strong> ${dtypeSize} bytes<br>
<strong>Elements / Token / Layer:</strong> 2 × ${config.num_key_value_heads} × ${config.head_dim} = ${perLayer}<br>
<strong>Total Elements:</strong> ${perLayer} × ${nFull} × ${tokens} = ${totalElements}<br>
<strong>Total Bytes:</strong> ${totalElements} × ${dtypeSize} = ${totalBytes} bytes<br>
<strong>KV Cache Size (full-attention layers only):</strong> ${totalBytes} / (1024³) ≈ ${sizeGB.toFixed(4)} GB
`;
} else if (isHybridSWA) {
// Gemma-style interleaved attention: full-attention layers cache the
// whole sequence; sliding-attention layers cap their KV at the window.
// So the cache grows only with the full layers once tokens > window:
// 2 × kv_heads × head_dim × (n_full × tokens + n_sliding × min(tokens, window)).
const perLayer = 2 * config.num_key_value_heads * config.head_dim;
const nFull = config.full_attention_layers;
const nSwa = config.sliding_attention_layers;
const win = config.sliding_window;
const swaTokens = Math.min(tokens, win);
totalElements = perLayer * (nFull * tokens + nSwa * swaTokens);
totalBytes = totalElements * dtypeSize;
sizeGB = totalBytes / (1024 ** 3);
details = `
<strong>Selected Model:</strong> ${model}<br>
<strong>Number of Hidden Layers:</strong> ${config.num_hidden_layers}<br>
<strong>Number of Key-Value Heads:</strong> ${config.num_key_value_heads}<br>
<strong>Head Dimension:</strong> ${config.head_dim}<br>
<strong>Full-Attention Layers:</strong> ${nFull} (cache full sequence)<br>
<strong>Sliding-Attention Layers:</strong> ${nSwa} (cap at window ${win}; using ${swaTokens} tokens)<br>
<strong>Data Type Size:</strong> ${dtypeSize} bytes<br>
<strong>Elements / Token / Layer:</strong> 2 × ${config.num_key_value_heads} × ${config.head_dim} = ${perLayer}<br>
<strong>Total Elements:</strong> ${perLayer} × (${nFull} × ${tokens} + ${nSwa} × ${swaTokens}) = ${totalElements}<br>
<strong>Total Bytes:</strong> ${totalElements} × ${dtypeSize} = ${totalBytes} bytes<br>
<strong>KV Cache Size:</strong> ${totalBytes} / (1024³) ≈ ${sizeGB.toFixed(4)} GB
`;
} else if (hasHeadDim) {
// Models with explicit head_dim (e.g., GQA architectures)
totalElements = 2 * config.num_hidden_layers * tokens * config.num_key_value_heads * config.head_dim;
totalBytes = totalElements * dtypeSize;
sizeGB = totalBytes / (1024 ** 3);
details = `
<strong>Selected Model:</strong> ${model}<br>
<strong>Hidden Size:</strong> ${config.hidden_size}<br>
<strong>Number of Attention Heads:</strong> ${config.num_attention_heads}<br>
<strong>Number of Hidden Layers:</strong> ${config.num_hidden_layers}<br>
<strong>Number of Key-Value Heads:</strong> ${config.num_key_value_heads}<br>
<strong>Head Dimension:</strong> ${config.head_dim}<br>
<strong>Data Type Size:</strong> ${dtypeSize} bytes<br>
<strong>Total Elements:</strong> 2 × ${config.num_hidden_layers} × ${tokens} × ${config.num_key_value_heads} × ${config.head_dim} = ${totalElements}<br>
<strong>Total Bytes:</strong> ${totalElements} × ${dtypeSize} = ${totalBytes} bytes<br>
<strong>KV Cache Size:</strong> ${totalBytes} / (1024³) ≈ ${sizeGB.toFixed(4)} GB
`;
} else {
// Standard transformer architecture
const headSize = config.hidden_size / config.num_attention_heads;
totalElements = 2 * config.num_hidden_layers * tokens * config.num_key_value_heads * headSize;
totalBytes = totalElements * dtypeSize;
sizeGB = totalBytes / (1024 ** 3);
details = `
<strong>Selected Model:</strong> ${model}<br>
<strong>Hidden Size:</strong> ${config.hidden_size}<br>
<strong>Number of Attention Heads:</strong> ${config.num_attention_heads}<br>
<strong>Number of Hidden Layers:</strong> ${config.num_hidden_layers}<br>
<strong>Number of Key-Value Heads:</strong> ${config.num_key_value_heads}<br>
<strong>Head Size:</strong> ${headSize} (Hidden Size / Attention Heads)<br>
<strong>Data Type Size:</strong> ${dtypeSize} bytes<br>
<strong>Total Elements:</strong> 2 × ${config.num_hidden_layers} × ${tokens} × ${config.num_key_value_heads} × ${headSize} = ${totalElements}<br>
<strong>Total Bytes:</strong> ${totalElements} × ${dtypeSize} = ${totalBytes} bytes<br>
<strong>KV Cache Size:</strong> ${totalBytes} / (1024³) ≈ ${sizeGB.toFixed(4)} GB
`;
}
resultDiv.className = 'result show';
resultDiv.innerHTML = `
<div class="result-value">KV Cache Size: ${sizeGB.toFixed(4)} GB</div>
<div class="result-label">Calculation Details:</div>
<div class="details">${details}</div>
`;
}
// Calculate maximum tokens from GPU RAM
function calculateMaxTokens() {
const model = document.getElementById('model-reverse').value;
const gpuRamGB = parseFloat(document.getElementById('gpu-ram').value);
const dtype = document.getElementById('dtype-reverse').value;
const resultDiv = document.getElementById('result-reverse');
// Validation
if (isNaN(gpuRamGB) || gpuRamGB <= 0) {
resultDiv.className = 'result show error';
resultDiv.innerHTML = '<div class="result-label">Error</div><div>Please enter a valid GPU RAM size (greater than 0).</div>';
return;
}
const config = modelConfigs[model];
if (!config) {
resultDiv.className = 'result show error';
resultDiv.innerHTML = '<div class="result-label">Error</div><div>Model configuration not found.</div>';
return;
}
// Get dtype size
const dtypeSizes = {
'float32': 4,
'float16': 2,
'bfloat16': 2,
'int8': 1
};
const dtypeSize = dtypeSizes[dtype];
// Convert GPU RAM to bytes
const totalBytes = gpuRamGB * (1024 ** 3);
// Calculate based on model architecture
// DeepSeek V4 sparse attention: per-layer compression schedule.
const isDSA = Array.isArray(config.compress_ratios)
&& Number.isFinite(config.head_dim);
// Multi-head Latent Attention (DeepSeek V3/R1, GLM-5.x DSA): feature-gated
// on kv_lora_rank rather than the model name.
const isMLA = Number.isFinite(config.kv_lora_rank)
&& Number.isFinite(config.qk_rope_head_dim);
// Interleaved sliding-window / full attention (Gemma 2/3/4): the sliding
// layers cap their KV cache at the window, only the full layers grow.
const isHybridSWA = Number.isFinite(config.full_attention_layers)
&& Number.isFinite(config.sliding_attention_layers)
&& Number.isFinite(config.sliding_window)
&& Number.isFinite(config.num_key_value_heads)
&& Number.isFinite(config.head_dim);
// Linear/full hybrid (Qwen3-Next / Qwen3.5): Gated DeltaNet layers keep a
// fixed-size recurrent state (no per-token KV); only full layers grow.
const isHybridLinear = Number.isFinite(config.full_attention_layers)
&& Number.isFinite(config.linear_attention_layers)
&& Number.isFinite(config.num_key_value_heads)
&& Number.isFinite(config.head_dim);
const hasHeadDim = Number.isFinite(config.head_dim)
&& Number.isFinite(config.num_key_value_heads);
let maxTokens;
let details = '';
if (isDSA) {
// DeepSeek V4 (CSA + HCA hybrid; NOT MLA). Bytes are computed in V4's
// native mixed precision (paper §2.3.4) — the dtype dropdown does NOT
// apply. KV entry = (head_dim - rope) FP8 + rope BF16; indexer keys
// FP4; both compressed at Σ(1/r). Sliding-window floor uses the same
// KV byte width and must be covered by the available RAM first.
const activeRatios = config.compress_ratios.filter(r => r > 0);
const compressFactor = activeRatios.reduce((sum, r) => sum + 1 / r, 0);
const ropeDim = Number.isFinite(config.qk_rope_head_dim) ? config.qk_rope_head_dim : 0;
const indexerDim = Number.isFinite(config.index_head_dim) ? config.index_head_dim : 0;
const prec = getDSAPrecision('-reverse', config);
const kvBytesPerEntry = (config.head_dim - ropeDim) * prec.nope + ropeDim * prec.rope;
const indexerBytesPerEntry = indexerDim * prec.indexer;
const bytesPerToken = (kvBytesPerEntry + indexerBytesPerEntry) * compressFactor;
const numLayers = config.compress_ratios.length;
const windowBytes = numLayers * config.sliding_window * kvBytesPerEntry;
maxTokens = Math.max(0, Math.floor((totalBytes - windowBytes) / bytesPerToken));
const precLabel = prec.mode === 'custom'
? `custom (NoPE ${prec.nope} / RoPE ${prec.rope} / indexer ${prec.indexer} B/dim)`
: `paper default (NoPE ${prec.nope}=FP8 / RoPE ${prec.rope}=BF16 / indexer ${prec.indexer}=FP4); dtype selector not applied`;
const indexerLine = indexerDim
? `<strong>Indexer Key (CSA, compressed):</strong> ${indexerDim} × ${prec.indexer} × Σ(1/r) = ${(indexerBytesPerEntry * compressFactor).toFixed(1)} B/token<br>`
: '';
details = `
<strong>Selected Model:</strong> ${model}<br>
<strong>GPU RAM Size:</strong> ${gpuRamGB} GB<br>
<strong>Number of Hidden Layers:</strong> ${config.num_hidden_layers}<br>
<strong>Head Dimension:</strong> ${config.head_dim} (RoPE ${ropeDim} @ ${prec.rope} B, NoPE ${config.head_dim - ropeDim} @ ${prec.nope} B)<br>
<strong>Compressed Layers (ratio &gt; 0):</strong> ${activeRatios.length} of ${numLayers}<br>
<strong>Sliding Window:</strong> ${config.sliding_window} (per layer)<br>
<strong>KV Bytes / Entry:</strong> (${config.head_dim} ${ropeDim}) × ${prec.nope} + ${ropeDim} × ${prec.rope} = ${kvBytesPerEntry} B<br>
${indexerLine}<strong>Precision:</strong> ${precLabel}<br>
<strong>Bytes per Token:</strong> (${kvBytesPerEntry} + ${indexerBytesPerEntry}) × Σ(1/r) = ${bytesPerToken.toFixed(1)} B<br>
<strong>Sliding-Window Floor:</strong> ${numLayers} × ${config.sliding_window} × ${kvBytesPerEntry} = ${windowBytes} B<br>
<strong>Maximum Tokens:</strong> (${totalBytes} ${windowBytes}) / ${bytesPerToken.toFixed(1)} = ${maxTokens} tokens
`;
} else if (isMLA) {
// Multi-head Latent Attention (DeepSeek V3/R1, GLM-5.x): per-token
// cost is layers × (kv_lora_rank + qk_rope_head_dim), plus a DSA
// lightning-indexer key of index_head_dim per layer per token when
// present (LMCache stores the indexer group in the same KV pool).
const latentDim = config.kv_lora_rank + config.qk_rope_head_dim;
const indexerDim = Number.isFinite(config.index_head_dim) ? config.index_head_dim : 0;
const elementsPerToken = config.num_hidden_layers * (latentDim + indexerDim);
const bytesPerToken = elementsPerToken * dtypeSize;
maxTokens = Math.floor(totalBytes / bytesPerToken);
const indexerLine = indexerDim
? `<strong>Indexer Head Dim (DSA):</strong> ${indexerDim}<br>`
: '';
const latentExpr = indexerDim
? `(${config.kv_lora_rank} + ${config.qk_rope_head_dim} + ${indexerDim})`
: `(${config.kv_lora_rank} + ${config.qk_rope_head_dim})`;
details = `
<strong>Selected Model:</strong> ${model}<br>
<strong>GPU RAM Size:</strong> ${gpuRamGB} GB<br>
<strong>Number of Hidden Layers:</strong> ${config.num_hidden_layers}<br>
<strong>KV LoRA Rank:</strong> ${config.kv_lora_rank}<br>
<strong>QK RoPE Head Dim:</strong> ${config.qk_rope_head_dim}<br>
${indexerLine}<strong>Data Type Size:</strong> ${dtypeSize} bytes<br>
<strong>Elements per Token:</strong> ${config.num_hidden_layers} × ${latentExpr} = ${elementsPerToken}<br>
<strong>Bytes per Token:</strong> ${elementsPerToken} × ${dtypeSize} = ${bytesPerToken} bytes<br>
<strong>Maximum Tokens:</strong> ${totalBytes} / ${bytesPerToken} = ${maxTokens} tokens
`;
} else if (isHybridLinear) {
// Attention/recurrent hybrid: only the full-attention layers grow.
// TODO(SSM): the recurrent (Gated DeltaNet / Mamba2 SSM) layers' constant
// state is NOT subtracted yet — holding off until LMCache decides how it
// stores linear-attention layers. For now max tokens is computed from the
// full-attention KV cache alone.
const linType = typeof config.linear_attention_type === 'string'
? config.linear_attention_type : 'Gated DeltaNet';
const perLayer = 2 * config.num_key_value_heads * config.head_dim;
const nFull = config.full_attention_layers;
const nLin = config.linear_attention_layers;
const availElements = totalBytes / dtypeSize;
maxTokens = Math.max(0, Math.floor(availElements / (perLayer * nFull)));
details = `
<strong>Selected Model:</strong> ${model}<br>
<strong>GPU RAM Size:</strong> ${gpuRamGB} GB<br>
<strong>Number of Hidden Layers:</strong> ${config.num_hidden_layers}<br>
<strong>Number of Key-Value Heads:</strong> ${config.num_key_value_heads}<br>
<strong>Head Dimension:</strong> ${config.head_dim}<br>
<strong>Full-Attention Layers:</strong> ${nFull} (growing KV)<br>
<strong>Linear (${linType}) Layers:</strong> ${nLin} (recurrent state — not yet counted, TODO pending LMCache storage decision)<br>
<strong>Data Type Size:</strong> ${dtypeSize} bytes<br>
<strong>Elements / Token / Layer:</strong> 2 × ${config.num_key_value_heads} × ${config.head_dim} = ${perLayer}<br>
<strong>Maximum Tokens (full-attention layers only):</strong> ${availElements} / (${perLayer} × ${nFull}) = ${maxTokens} tokens
`;
} else if (isHybridSWA) {
// Gemma-style interleaved attention. Sliding layers cap at the window,
// so the token count is piecewise: below the window all layers count
// as full; above it only the full layers keep growing.
const perLayer = 2 * config.num_key_value_heads * config.head_dim;
const nFull = config.full_attention_layers;
const nSwa = config.sliding_attention_layers;
const win = config.sliding_window;
const availElements = totalBytes / dtypeSize;
// First assume tokens <= window (every layer caches the full sequence).
let n = Math.floor(availElements / (perLayer * (nFull + nSwa)));
let regime = 'tokens ≤ window (all layers full)';
if (n > win) {
// tokens > window: sliding layers are pinned at `win` tokens.
n = Math.floor((availElements / perLayer - nSwa * win) / nFull);
regime = 'tokens &gt; window (sliding layers capped)';
}
maxTokens = Math.max(0, n);
details = `
<strong>Selected Model:</strong> ${model}<br>
<strong>GPU RAM Size:</strong> ${gpuRamGB} GB<br>
<strong>Number of Hidden Layers:</strong> ${config.num_hidden_layers}<br>
<strong>Number of Key-Value Heads:</strong> ${config.num_key_value_heads}<br>
<strong>Head Dimension:</strong> ${config.head_dim}<br>
<strong>Full-Attention Layers:</strong> ${nFull}<br>
<strong>Sliding-Attention Layers:</strong> ${nSwa} (window ${win})<br>
<strong>Data Type Size:</strong> ${dtypeSize} bytes<br>
<strong>Elements / Token / Layer:</strong> 2 × ${config.num_key_value_heads} × ${config.head_dim} = ${perLayer}<br>
<strong>Regime:</strong> ${regime}<br>
<strong>Maximum Tokens:</strong> ${maxTokens} tokens
`;
} else if (hasHeadDim) {
// Explicit head_dim: tokens = total_bytes / (2 × layers × kv_heads × head_dim × dtype_size)
const elementsPerToken = 2 * config.num_hidden_layers * config.num_key_value_heads * config.head_dim;
const bytesPerToken = elementsPerToken * dtypeSize;
maxTokens = Math.floor(totalBytes / bytesPerToken);
details = `
<strong>Selected Model:</strong> ${model}<br>
<strong>GPU RAM Size:</strong> ${gpuRamGB} GB<br>
<strong>Hidden Size:</strong> ${config.hidden_size}<br>
<strong>Number of Attention Heads:</strong> ${config.num_attention_heads}<br>
<strong>Number of Hidden Layers:</strong> ${config.num_hidden_layers}<br>
<strong>Number of Key-Value Heads:</strong> ${config.num_key_value_heads}<br>
<strong>Head Dimension:</strong> ${config.head_dim}<br>
<strong>Data Type Size:</strong> ${dtypeSize} bytes<br>
<strong>Elements per Token:</strong> 2 × ${config.num_hidden_layers} × ${config.num_key_value_heads} × ${config.head_dim} = ${elementsPerToken}<br>
<strong>Bytes per Token:</strong> ${elementsPerToken} × ${dtypeSize} = ${bytesPerToken} bytes<br>
<strong>Maximum Tokens:</strong> ${totalBytes} / ${bytesPerToken} = ${maxTokens} tokens
`;
} else {
// Standard transformer: tokens = total_bytes / (2 × layers × kv_heads × head_size × dtype_size)
const headSize = config.hidden_size / config.num_attention_heads;
const elementsPerToken = 2 * config.num_hidden_layers * config.num_key_value_heads * headSize;
const bytesPerToken = elementsPerToken * dtypeSize;
maxTokens = Math.floor(totalBytes / bytesPerToken);
details = `
<strong>Selected Model:</strong> ${model}<br>
<strong>GPU RAM Size:</strong> ${gpuRamGB} GB<br>
<strong>Hidden Size:</strong> ${config.hidden_size}<br>
<strong>Number of Attention Heads:</strong> ${config.num_attention_heads}<br>
<strong>Number of Hidden Layers:</strong> ${config.num_hidden_layers}<br>
<strong>Number of Key-Value Heads:</strong> ${config.num_key_value_heads}<br>
<strong>Head Size:</strong> ${headSize} (Hidden Size / Attention Heads)<br>
<strong>Data Type Size:</strong> ${dtypeSize} bytes<br>
<strong>Elements per Token:</strong> 2 × ${config.num_hidden_layers} × ${config.num_key_value_heads} × ${headSize} = ${elementsPerToken}<br>
<strong>Bytes per Token:</strong> ${elementsPerToken} × ${dtypeSize} = ${bytesPerToken} bytes<br>
<strong>Maximum Tokens:</strong> ${totalBytes} / ${bytesPerToken} = ${maxTokens} tokens
`;
}
resultDiv.className = 'result show';
resultDiv.innerHTML = `
<div class="result-value">Maximum Tokens: ${maxTokens.toLocaleString()}</div>
<div class="result-label">Calculation Details:</div>
<div class="details">${details}</div>
`;
}
// Enter key handler
document.addEventListener('DOMContentLoaded', () => {
const tokensInput = document.getElementById('tokens');
if (tokensInput) {
tokensInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
calculateKVCache();
}
});
}
const gpuRamInput = document.getElementById('gpu-ram');
if (gpuRamInput) {
gpuRamInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
calculateMaxTokens();
}
});
}
});
// Load configurations on page load
window.addEventListener('load', loadModelConfigs);
</script>
</body>
</html>