- 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>
346 lines
15 KiB
JavaScript
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();
|
|
}
|
|
})();
|