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:
commit
c471e1d0a9
4 changed files with 1007 additions and 0 deletions
346
js/app.js
Normal file
346
js/app.js
Normal 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();
|
||||
}
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue