Create LLM context memory calculator with:

- Accurate memory calculation using ggml quantization formulas
- Support for f32, f16, bf16, q8_0, q4_0, q4_1, iq4_nl, q5_0, q5_1 quantizations
- Asymmetric context support (separate K/V cache quantization)
- Full attention interval support
- Parallel sequences multiplier
- Bilingual interface (Russian/English)
- Retro-style design with tooltips

Signed-off-by: Arseniy Romenskiy <romenskiy@altlinux.org> - Co-authored-by: Qwen3.5-35B-A3B-Claude-4.6-Opus-Reasoning-Distilled <qwen@example.com>
This commit is contained in:
romenskiy2012 2026-04-11 23:54:55 +03:00 committed by Arseniy Romenskiy
commit c471e1d0a9
4 changed files with 1007 additions and 0 deletions

326
css/styles.css Normal file
View file

@ -0,0 +1,326 @@
/*
* LLM Context Memory Calculator - Retro Style CSS
* White background, sharp borders, high contrast
*/
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background-color: #FFFFFF;
color: #000000;
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
border: 3px solid #000000;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
h1 {
font-family: 'Courier New', monospace;
font-size: 28px;
font-weight: bold;
color: #0000FF;
border-bottom: 2px solid #0000FF;
padding-bottom: 10px;
flex-grow: 1;
}
.lang-btn {
background-color: #000000;
color: #FFFFFF;
border: 2px solid #000000;
padding: 5px 10px;
font-family: 'Courier New', monospace;
font-size: 14px;
font-weight: bold;
cursor: pointer;
}
.lang-btn:hover {
background-color: #333333;
}
.form-group {
margin-bottom: 15px;
position: relative;
}
label {
display: block;
font-weight: bold;
margin-bottom: 5px;
font-size: 14px;
}
input[type="number"],
select {
width: 100%;
padding: 8px;
border: 2px solid #000000;
background-color: #FFFFFF;
color: #000000;
font-family: 'Courier New', monospace;
font-size: 16px;
border-radius: 0;
}
input[type="number"]:focus,
select:focus {
outline: none;
border-color: #0000FF;
box-shadow: 0 0 5px rgba(0, 0, 255, 0.5);
}
input[type="number"]::placeholder {
color: #666666;
}
.checkbox-group {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.checkbox-group input[type="checkbox"] {
width: auto;
margin-right: 10px;
cursor: pointer;
}
.checkbox-group label {
margin-bottom: 0;
cursor: pointer;
}
.asymmetric-controls {
display: none;
border: 2px solid #000000;
padding: 15px;
margin-top: 10px;
background-color: #F5F5F5;
}
.asymmetric-controls.visible {
display: block;
}
.asymmetric-controls .form-group {
margin-bottom: 10px;
}
button {
width: 100%;
padding: 12px;
background-color: #000000;
color: #FFFFFF;
border: none;
font-family: 'Courier New', monospace;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: #333333;
}
.results {
margin-top: 20px;
border: 2px solid #000000;
padding: 15px;
}
.results h2 {
font-family: 'Courier New', monospace;
font-size: 18px;
margin-bottom: 10px;
color: #000000;
border-bottom: 1px solid #000000;
padding-bottom: 5px;
}
.result-item {
display: flex;
justify-content: space-between;
padding: 5px 0;
font-family: 'Courier New', monospace;
font-size: 14px;
}
.result-item::before {
content: '├─';
margin-right: 5px;
color: #000000;
}
.result-item:last-child::before {
content: '└─';
}
.result-item .label {
color: #333333;
}
.result-item .value {
font-weight: bold;
}
.result-item.total::before {
content: '└─';
color: #0000FF;
}
.result-item.total .value {
color: #0000FF;
}
.example {
margin-top: 20px;
padding: 10px;
border: 1px solid #CCCCCC;
background-color: #FAFAFA;
}
.example p {
font-family: 'Courier New', monospace;
font-size: 12px;
color: #666666;
}
.error-message {
background-color: #FFEEEE;
border: 2px solid #FF0000;
color: #FF0000;
padding: 10px;
margin-top: 10px;
font-family: 'Courier New', monospace;
font-size: 14px;
}
.error-message ul {
margin-left: 20px;
}
.error-message li {
margin-bottom: 5px;
}
/* Tooltip Styles */
.tooltip-icon {
display: inline-block;
width: 16px;
height: 16px;
background-color: #000000;
color: #FFFFFF;
text-align: center;
line-height: 16px;
font-family: 'Courier New', monospace;
font-size: 12px;
font-weight: bold;
margin-left: 5px;
cursor: help;
vertical-align: middle;
position: relative;
}
.tooltip-icon::before {
content: "?";
}
/* Hide both languages by default */
.tooltip-icon[data-tooltip-ru],
.tooltip-icon[data-tooltip-en] {
display: none;
}
/* Show Russian when html lang is ru */
html[lang="ru"] .tooltip-icon[data-tooltip-ru] {
display: inline-block;
}
/* Show English when html lang is en */
html[lang="en"] .tooltip-icon[data-tooltip-en] {
display: inline-block;
}
.tooltip-icon:hover {
background-color: #0000FF;
}
/* Tooltip popup for Russian */
html[lang="ru"] .tooltip-icon:hover::after {
content: attr(data-tooltip-ru);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background-color: #000000;
color: #FFFFFF;
padding: 12px 16px;
font-size: 11px;
white-space: pre-wrap;
width: 375px;
z-index: 1000;
line-height: 1.4;
text-align: left;
}
/* Tooltip popup for English */
html[lang="en"] .tooltip-icon:hover::after {
content: attr(data-tooltip-en);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background-color: #000000;
color: #FFFFFF;
padding: 12px 16px;
font-size: 11px;
white-space: pre-wrap;
width: 375px;
z-index: 1000;
line-height: 1.4;
text-align: left;
}
@media (max-width: 600px) {
.container {
padding: 15px;
}
h1 {
font-size: 22px;
}
input[type="number"],
select {
font-size: 14px;
}
.header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.lang-btn {
align-self: flex-end;
}
}

118
index.html Normal file
View file

@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-key="title">Калькулятор Памяти Контекста LLM</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div class="container">
<div class="header">
<h1 data-key="title">Калькулятор Памяти Контекста LLM</h1>
<button id="lang-toggle" class="lang-btn">EN</button>
</div>
<form id="calculator-form">
<div class="form-group">
<label for="context-length" data-key="context_length_label">Длина Контекста (токены)</label>
<input type="number" id="context-length" placeholder="Введите длину контекста" required min="1">
<span class="tooltip-icon" data-tooltip-ru="Количество токенов в контексте. Влияет линейно на размер KV кеша" data-tooltip-en="Number of tokens in context. Affects KV cache size linearly"></span>
</div>
<div class="form-group">
<label for="k-type" data-key="k_type_label">Квантование K кеша</label>
<select id="k-type" required>
<option value="KV" selected data-key="kv_cache">KV кеш</option>
<option value="f32">f32</option>
<option value="f16">f16</option>
<option value="bf16">bf16</option>
<option value="q8_0" data-key="q8_0">q8_0</option>
<option value="q4_0" data-key="q4_0">q4_0</option>
<option value="q4_1" data-key="q4_1">q4_1</option>
<option value="iq4_nl" data-key="iq4_nl">iq4_nl</option>
<option value="q5_0" data-key="q5_0">q5_0</option>
<option value="q5_1" data-key="q5_1">q5_1</option>
</select>
<span class="tooltip-icon" data-tooltip-ru="Тип квантования для тензоров ключей внимания. Меньшее значение = меньше памяти, но потенциально выше ошибка квантования" data-tooltip-en="Quantization type for attention key tensors. Lower value = less memory, but potentially higher quantization error"></span>
</div>
<div class="form-group">
<label for="kv-heads" data-key="kv_heads_label">Головок KV</label>
<input type="number" id="kv-heads" placeholder="Введите количество головок KV" required min="1">
<span class="tooltip-icon" data-tooltip-ru="Количество голов внимания для K и V тензоров. Обычно num_key_value_heads в конфигурации модели" data-tooltip-en="Number of attention heads for K and V tensors. Usually num_key_value_heads in model config"></span>
</div>
<div class="form-group">
<label for="head-size" data-key="head_size_label">Размер Головки</label>
<input type="number" id="head-size" placeholder="Введите размер головы" required min="1">
<span class="tooltip-icon" data-tooltip-ru="Размер каждой головы внимания (head_dim). Влияет линейно на размер KV кеша" data-tooltip-en="Size of each attention head (head_dim). Affects KV cache size linearly"></span>
</div>
<div class="form-group">
<label for="num-heads" data-key="num_heads_label">Количество Головок</label>
<input type="number" id="num-heads" placeholder="Введите количество головок" required min="1">
<span class="tooltip-icon" data-tooltip-ru="Общее количество голов внимания. Используется только для информации и примеров" data-tooltip-en="Total number of attention heads. Used for display and examples only"></span>
</div>
<div class="form-group">
<label for="num-layers" data-key="num_layers_label">Количество Слов</label>
<input type="number" id="num-layers" placeholder="Введите количество слоев" required min="1">
<span class="tooltip-icon" data-tooltip-ru="Количество слоев модели. Каждый слой имеет свой KV кеш" data-tooltip-en="Number of model layers. Each layer has its own KV cache"></span>
</div>
<div class="form-group">
<label for="model-size" data-key="model_size_label">Размер Модели (GB)</label>
<input type="number" id="model-size" placeholder="Опционально, для общего объема памяти" min="0" step="0.1">
<span class="tooltip-icon" data-tooltip-ru="Размер весов модели в гигабайтах. Используется для расчета общего объема памяти (KV кеш + веса)" data-tooltip-en="Model weights size in gigabytes. Used to calculate total memory (KV cache + weights)"></span>
</div>
<div class="form-group">
<label for="parallel" data-key="parallel_label">Параллелизм (np)</label>
<input type="number" id="parallel" value="1" min="1" title="">
<span class="tooltip-icon" data-tooltip-ru="Количество параллельных последовательностей для декодирования (-np/--parallel в llama.cpp). Увеличивает KV кеш пропорционально" data-tooltip-en="Number of parallel sequences for decoding (-np/--parallel in llama.cpp). Increases KV cache proportionally"></span>
</div>
<div class="form-group">
<label for="full-attention" data-key="full_attention_label">Интервал Полного Внимания</label>
<input type="number" id="full-attention" placeholder="Опционально" min="1">
<span class="tooltip-icon" data-tooltip-ru="Интервал слоев для полного внимания. Слои считают полный KV кеш каждые N слоев. Уменьшает эффективное количество слоев" data-tooltip-en="Interval for full attention layers. Layers compute full KV cache every N layers. Reduces effective layer count"></span>
</div>
<div class="checkbox-group">
<input type="checkbox" id="asymmetric">
<label for="asymmetric" data-key="asymmetric_label">Асимметричный Контекст</label>
</div>
<div id="asymmetric-controls" class="asymmetric-controls">
<div class="form-group">
<label for="v-type" data-key="v_type_label">Квантование V кеша</label>
<select id="v-type">
<option value="f32">f32</option>
<option value="f16">f16</option>
<option value="bf16">bf16</option>
<option value="q8_0" data-key="q8_0">q8_0</option>
<option value="q4_0" data-key="q4_0">q4_0</option>
<option value="q4_1" data-key="q4_1">q4_1</option>
<option value="iq4_nl" data-key="iq4_nl">iq4_nl</option>
<option value="q5_0" data-key="q5_0">q5_0</option>
<option value="q5_1" data-key="q5_1">q5_1</option>
</select>
<span class="tooltip-icon" data-tooltip-ru="Тип квантования для тензоров значений внимания. Можно выбрать отдельно от K кеша" data-tooltip-en="Quantization type for attention value tensors. Can be selected separately from K cache"></span>
</div>
</div>
<button type="button" id="calculate-btn" data-key="calculate_btn">Рассчитать</button>
</form>
<div id="example-text" class="example">
<p data-key="example_text">Пример: context=8192, layers=32, kv_heads=32, head_size=128, model_size=7 GB, parallel=1</p>
</div>
<div id="results"></div>
</div>
<script src="js/calculation.js"></script>
<script src="js/app.js"></script>
</body>
</html>

346
js/app.js Normal file
View file

@ -0,0 +1,346 @@
/**
* LLM Context Memory Calculator - Main Application
* DOM manipulation and event handling
*/
(function() {
'use strict';
// Translation dictionary
const translations = {
ru: {
title: 'Калькулятор Памяти Контекста LLM',
context_length_label: 'Длина Контекста (токены)',
k_type_label: 'Квантование K кеша',
kv_cache: 'KV кеш',
q8_0: 'q8_0',
q4_0: 'q4_0',
q4_1: 'q4_1',
iq4_nl: 'iq4_nl',
q5_0: 'q5_0',
q5_1: 'q5_1',
kv_heads_label: 'Головок KV',
head_size_label: 'Размер Головки',
num_heads_label: 'Количество Головок',
num_layers_label: 'Количество Слов',
model_size_label: 'Размер Модели (GB)',
parallel_label: 'Параллелизм (np)',
full_attention_label: 'Интервал Полного Внимания',
asymmetric_label: 'Асимметричный Контекст',
v_type_label: 'Квантование V кеша',
calculate_btn: 'Рассчитать',
example_text: 'Пример: context=8192, layers=32, kv_heads=32, head_size=128, model_size=7 GB, parallel=1',
results_title: 'Результаты',
k_cache: 'K кеш:',
v_cache: 'V кеш:',
total_kv: 'Total KV:',
total: 'Total:',
errors_header: 'Ошибки:',
error_context_length: 'Длина контекста должна быть положительным числом',
error_kv_heads: 'Количество KV головок должно быть положительным числом',
error_head_size: 'Размер головы должен быть положительным числом',
error_num_layers: 'Количество слоев должно быть положительным числом',
error_full_attention: 'Интервал полного внимания должен быть положительным числом',
error_model_size: 'Размер модели должен быть положительным числом'
},
en: {
title: 'LLM Context Memory Calculator',
context_length_label: 'Context Length (tokens)',
k_type_label: 'K cache quantization',
kv_cache: 'KV cache',
q8_0: 'q8_0',
q4_0: 'q4_0',
q4_1: 'q4_1',
iq4_nl: 'iq4_nl',
q5_0: 'q5_0',
q5_1: 'q5_1',
kv_heads_label: 'KV Heads',
head_size_label: 'Head Size',
num_heads_label: 'Number of Heads',
num_layers_label: 'Number of Layers',
model_size_label: 'Model Size (GB)',
parallel_label: 'Parallel (np)',
full_attention_label: 'Full Attention Interval',
asymmetric_label: 'Asymmetric Context',
v_type_label: 'V cache quantization',
calculate_btn: 'Calculate',
example_text: 'Example: context=8192, layers=32, kv_heads=32, head_size=128, model_size=7 GB, parallel=1',
results_title: 'Results',
k_cache: 'K cache:',
v_cache: 'V cache:',
total_kv: 'Total KV:',
total: 'Total:',
errors_header: 'Errors:',
error_context_length: 'Context length must be a positive number',
error_kv_heads: 'KV heads must be a positive number',
error_head_size: 'Head size must be a positive number',
error_num_layers: 'Number of layers must be a positive number',
error_full_attention: 'Full attention interval must be a positive number',
error_model_size: 'Model size must be a positive number'
}
};
// Tooltip translations
const tooltips = {
ru: {
context_length: 'Количество токенов в контексте. Влияет линейно на размер KV кеша',
k_type: 'Тип квантования для тензоров ключей внимания. Меньшее значение = меньше памяти, но потенциально выше ошибка квантования',
kv_heads: 'Количество голов внимания для K и V тензоров. Обычно num_key_value_heads в конфигурации модели',
head_size: 'Размер каждой головы внимания (head_dim). Влияет линейно на размер KV кеша',
num_heads: 'Общее количество голов внимания. Используется только для информации и примеров',
num_layers: 'Количество слоев модели. Каждый слой имеет свой KV кеш',
model_size: 'Размер весов модели в гигабайтах. Используется для расчета общего объема памяти (KV кеш + веса)',
parallel: 'Количество параллельных последовательностей для декодирования (-np/--parallel в llama.cpp). Увеличивает KV кеш пропорционально',
full_attention: 'Интервал слоев для полного внимания. Слои считают полный KV кеш каждые N слоев. Уменьшает эффективное количество слоев',
v_type: 'Тип квантования для тензоров значений внимания. Можно выбрать отдельно от K кеша'
},
en: {
context_length: 'Number of tokens in context. Affects KV cache size linearly',
k_type: 'Quantization type for attention key tensors. Lower value = less memory, but potentially higher quantization error',
kv_heads: 'Number of attention heads for K and V tensors. Usually num_key_value_heads in model config',
head_size: 'Size of each attention head (head_dim). Affects KV cache size linearly',
num_heads: 'Total number of attention heads. Used for display and examples only',
num_layers: 'Number of model layers. Each layer has its own KV cache',
model_size: 'Model weights size in gigabytes. Used to calculate total memory (KV cache + weights)',
parallel: 'Number of parallel sequences for decoding (-np/--parallel in llama.cpp). Increases KV cache proportionally',
full_attention: 'Interval for full attention layers. Layers compute full KV cache every N layers. Reduces effective layer count',
v_type: 'Quantization type for attention value tensors. Can be selected separately from K cache'
}
};
// Current language
let currentLang = 'ru';
// DOM Elements
const form = document.getElementById('calculator-form');
const kTypeSelect = document.getElementById('k-type');
const vTypeSelect = document.getElementById('v-type');
const asymmetricCheckbox = document.getElementById('asymmetric');
const asymmetricControls = document.getElementById('asymmetric-controls');
const calculateBtn = document.getElementById('calculate-btn');
const resultsContainer = document.getElementById('results');
const exampleText = document.getElementById('example-text');
const langToggleBtn = document.getElementById('lang-toggle');
// Initialize
function init() {
setupEventListeners();
updateText();
updateTooltips();
updateExampleText();
}
// Set up event listeners
function setupEventListeners() {
asymmetricCheckbox.addEventListener('change', toggleAsymmetric);
calculateBtn.addEventListener('click', handleCalculate);
// Auto-calculate on input change
form.addEventListener('input', debounce(handleCalculate, 500));
// Language toggle
langToggleBtn.addEventListener('click', toggleLanguage);
}
// Toggle language
function toggleLanguage() {
currentLang = currentLang === 'ru' ? 'en' : 'ru';
langToggleBtn.textContent = currentLang === 'ru' ? 'EN' : 'RU';
document.documentElement.lang = currentLang;
updateText();
updateTooltips();
updateExampleText();
}
// Update all translatable text
function updateText() {
// Translate elements with data-key attribute
document.querySelectorAll('[data-key]').forEach(el => {
const key = el.getAttribute('data-key');
if (translations[currentLang][key]) {
if (el.tagName === 'TITLE') {
document.title = translations[currentLang][key];
} else {
el.textContent = translations[currentLang][key];
}
}
});
// Update placeholder text
document.getElementById('context-length').placeholder =
currentLang === 'ru' ? 'Введите длину контекста' : 'Enter context length';
document.getElementById('kv-heads').placeholder =
currentLang === 'ru' ? 'Введите количество головок KV' : 'Enter KV heads';
document.getElementById('head-size').placeholder =
currentLang === 'ru' ? 'Введите размер головы' : 'Enter head size';
document.getElementById('num-heads').placeholder =
currentLang === 'ru' ? 'Введите количество головок' : 'Enter number of heads';
document.getElementById('num-layers').placeholder =
currentLang === 'ru' ? 'Введите количество слоев' : 'Enter number of layers';
document.getElementById('model-size').placeholder =
currentLang === 'ru' ? 'Опционально, для общего объема памяти' : 'Optional, for total memory';
document.getElementById('full-attention').placeholder =
currentLang === 'ru' ? 'Опционально' : 'Optional';
}
// Update tooltips
function updateTooltips() {
const tooltipMap = {
'context-length': 'context_length',
'k-type': 'k_type',
'kv-heads': 'kv_heads',
'head-size': 'head_size',
'num-heads': 'num_heads',
'num-layers': 'num_layers',
'model-size': 'model_size',
'parallel': 'parallel',
'full-attention': 'full_attention',
'v-type': 'v_type'
};
Object.keys(tooltipMap).forEach(fieldId => {
const tooltipIcon = document.querySelector(`#${fieldId} + .tooltip-icon`);
if (tooltipIcon) {
const key = tooltipMap[fieldId];
const ruText = tooltips.ru[key];
const enText = tooltips.en[key];
if (currentLang === 'ru') {
tooltipIcon.setAttribute('data-tooltip-ru', ruText);
tooltipIcon.removeAttribute('data-tooltip-en');
} else {
tooltipIcon.setAttribute('data-tooltip-en', enText);
tooltipIcon.removeAttribute('data-tooltip-ru');
}
}
});
}
// Update example text
function updateExampleText() {
const kvHeads = parseInt(document.getElementById('kv-heads').value) || 32;
const headSize = parseInt(document.getElementById('head-size').value) || 128;
const layers = parseInt(document.getElementById('num-layers').value) || 32;
const modelSize = document.getElementById('model-size').value || 7;
const context = parseInt(document.getElementById('context-length').value) || 8192;
const parallel = parseInt(document.getElementById('parallel').value) || 1;
exampleText.textContent = translations[currentLang].example_text
.replace('8192', context)
.replace('32', layers)
.replace('kv_heads=32', `kv_heads=${kvHeads}`)
.replace('head_size=128', `head_size=${headSize}`)
.replace('7 GB', `${modelSize} GB`)
.replace('parallel=1', `parallel=${parallel}`);
}
// Toggle asymmetric context controls
function toggleAsymmetric() {
if (asymmetricCheckbox.checked) {
asymmetricControls.classList.add('visible');
const kvOption = kTypeSelect.querySelector('option[value="KV"]');
if (kvOption) {
kvOption.value = 'K';
kvOption.textContent = currentLang === 'ru' ? 'K кеш' : 'K cache';
}
} else {
asymmetricControls.classList.remove('visible');
const kOption = kTypeSelect.querySelector('option[value="K"]');
if (kOption) {
kOption.value = 'KV';
kOption.textContent = translations[currentLang].kv_cache;
}
}
}
// Handle calculate button click
function handleCalculate() {
const params = collectParams();
const validation = validateParams(params, currentLang);
if (!validation.valid) {
displayErrors(validation.errors);
return;
}
const results = calculateMemory(params);
displayResults(results);
}
// Collect parameters from form
function collectParams() {
return {
contextLength: parseInt(document.getElementById('context-length').value),
kType: asymmetricCheckbox.checked
? kTypeSelect.value === 'K' ? 'KV' : kTypeSelect.value
: 'KV',
vType: asymmetricCheckbox.checked ? vTypeSelect.value : null,
kvHeads: parseInt(document.getElementById('kv-heads').value),
headSize: parseInt(document.getElementById('head-size').value),
numLayers: parseInt(document.getElementById('num-layers').value),
modelSizeGB: parseFloat(document.getElementById('model-size').value) || null,
parallel: parseInt(document.getElementById('parallel').value) || 1,
fullAttentionInterval: document.getElementById('full-attention').value
? parseInt(document.getElementById('full-attention').value)
: null
};
}
// Display calculation results
function displayResults(results) {
let html = '<div class="results"><h2>' + translations[currentLang].results_title + '</h2>';
html += `<div class="result-item"><span class="label">${translations[currentLang].k_cache}</span><span class="value">${results.kCache.formatted}</span></div>`;
html += `<div class="result-item"><span class="label">${translations[currentLang].v_cache}</span><span class="value">${results.vCache.formatted}</span></div>`;
html += `<div class="result-item"><span class="label">${translations[currentLang].total_kv}</span><span class="value">${results.totalKVCache.formatted}</span></div>`;
if (results.totalMemory) {
html += `<div class="result-item total"><span class="label">${translations[currentLang].total}</span><span class="value">${results.totalMemory.formatted}</span></div>`;
}
html += '</div>';
resultsContainer.innerHTML = html;
}
// Display validation errors
function displayErrors(errors) {
let html = '<div class="error-message"><strong>' + translations[currentLang].errors_header + '</strong><ul>';
errors.forEach(error => {
html += `<li>${error}</li>`;
});
html += '</ul></div>';
resultsContainer.innerHTML = html;
}
// Debounce function for performance
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Expose functions globally for testing
window.CalculatorApp = {
collectParams,
handleCalculate,
toggleAsymmetric,
updateExampleText,
toggleLanguage,
updateText,
updateTooltips
};
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

217
js/calculation.js Normal file
View file

@ -0,0 +1,217 @@
/**
* LLM Context Memory Calculator - Core Calculation Logic
* Pure functions with no DOM dependencies
*/
const QUANT_SIZES = {
f32: 4.0,
f16: 2.0,
bf16: 2.0,
q8_0: 34/32, // 1.0625
q4_0: 18/32, // 0.5625
q4_1: 20/32, // 0.625
iq4_nl: 18/32, // 0.5625
q5_0: 22/32, // 0.6875
q5_1: 24/32 // 0.75
};
/**
* Normalize quantization type (handle 'K' alias for 'KV')
* @param {string} type - Quantization type
* @returns {string} Normalized type
*/
function normalizeQuantType(type) {
if (type === 'K') return 'KV';
return type;
}
/**
* Get quantization size in bytes per tensor element
* @param {string} type - Quantization type
* @returns {number} Size in bytes
*/
function getQuantizationSize(type) {
const normalizedType = normalizeQuantType(type);
return QUANT_SIZES[normalizedType] || QUANT_SIZES.f32;
}
/**
* Format bytes to human-readable string
* @param {number} bytes - Number of bytes
* @returns {string} Formatted string (e.g., "1.23 MB")
*/
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Calculate memory breakdown
* @param {Object} params - Calculation parameters
* @returns {Object} Memory breakdown object
*/
function calculateMemory(params) {
const {
contextLength,
kType,
vType,
kvHeads,
headSize,
numLayers,
modelSizeGB,
parallel,
fullAttentionInterval
} = params;
// Calculate effective layers (account for full attention interval)
const effectiveLayers = fullAttentionInterval
? Math.ceil(numLayers / fullAttentionInterval)
: numLayers;
// Determine parallel multiplier (default 1)
const parallelMultiplier = parallel || 1;
// Get quantization sizes
const bK = getQuantizationSize(kType);
const bV = vType ? getQuantizationSize(vType) : bK;
// Memory per token for all layers
// Formula: ctx × layers × kvheads × headdim × (bK + bV)
const memoryPerToken = contextLength * effectiveLayers * kvHeads * headSize * (bK + bV);
// Total KV cache memory
const totalKVCache = memoryPerToken * parallelMultiplier;
// Total memory including model weights (if provided)
const totalMemory = modelSizeGB
? totalKVCache + (modelSizeGB * 1024 * 1024 * 1024)
: null;
// Calculate individual cache sizes
const kCacheSize = contextLength * effectiveLayers * kvHeads * headSize * bK * parallelMultiplier;
const vCacheSize = contextLength * effectiveLayers * kvHeads * headSize * bV * parallelMultiplier;
return {
kCache: {
size: kCacheSize,
formatted: formatBytes(kCacheSize)
},
vCache: {
size: vCacheSize,
formatted: formatBytes(vCacheSize)
},
totalKVCache: {
size: totalKVCache,
formatted: formatBytes(totalKVCache)
},
totalMemory: totalMemory ? {
size: totalMemory,
formatted: formatBytes(totalMemory)
} : null,
effectiveLayers,
parallelMultiplier
};
}
/**
* Validate input parameters
* @param {Object} params - Parameters to validate
* @param {string} lang - Language code ('ru' or 'en')
* @returns {Object} Validation result
*/
function validateParams(params, lang) {
lang = lang || 'ru';
const errors = [];
if (!params.contextLength || params.contextLength <= 0) {
if (lang === 'ru') {
errors.push('Длина контекста должна быть положительным числом');
} else {
errors.push('Context length must be a positive number');
}
}
if (!params.kvHeads || params.kvHeads <= 0) {
if (lang === 'ru') {
errors.push('Количество KV головок должно быть положительным числом');
} else {
errors.push('KV heads must be a positive number');
}
}
if (!params.headSize || params.headSize <= 0) {
if (lang === 'ru') {
errors.push('Размер головы должен быть положительным числом');
} else {
errors.push('Head size must be a positive number');
}
}
if (!params.numLayers || params.numLayers <= 0) {
if (lang === 'ru') {
errors.push('Количество слоев должно быть положительным числом');
} else {
errors.push('Number of layers must be a positive number');
}
}
if (params.fullAttentionInterval && params.fullAttentionInterval <= 0) {
if (lang === 'ru') {
errors.push('Интервал полного внимания должен быть положительным числом');
} else {
errors.push('Full attention interval must be a positive number');
}
}
if (params.modelSizeGB && params.modelSizeGB <= 0) {
if (lang === 'ru') {
errors.push('Размер модели должен быть положительным числом');
} else {
errors.push('Model size must be a positive number');
}
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Generate example parameters
* @returns {Object} Example parameters object
*/
function getExampleParams() {
return {
contextLength: 8192,
kType: 'f16',
vType: 'f16',
kvHeads: 32,
headSize: 128,
numLayers: 32,
modelSizeGB: 7,
parallel: 1,
fullAttentionInterval: null
};
}
// Export for use in other files
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
getQuantizationSize,
formatBytes,
calculateMemory,
validateParams,
getExampleParams,
QUANT_SIZES
};
}
// Global export for browser
window.calculateMemory = calculateMemory;
window.validateParams = validateParams;