llm_calculator/js/app.js
romenskiy2012 c471e1d0a9 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>
2026-04-12 00:05:56 +03:00

346 lines
15 KiB
JavaScript

/**
* 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();
}
})();