/** * 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: 'Рассчитать', reset_btn: 'Сбросить', config_file_label: 'Config.json', config_file_ru: 'Загрузите config.json из модели. Автоматически заполнит поля и запустит расчет', config_file_en: 'Upload model config.json. Auto-fills fields and runs calculation', 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', reset_btn: 'Reset', config_file_label: 'Config.json', config_file_ru: 'Загрузите config.json из модели. Автоматически заполнит поля и запустит расчет', config_file_en: 'Upload model config.json. Auto-fills fields and runs calculation', hf_model_label: 'Model (HuggingFace)', hf_model_ru: 'Введите имя модели', hf_model_en: 'Please enter a model name', hf_fetch_btn: 'Fetch', 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' } }; // ConfigParser module const ConfigParser = { defaults: { parallel: 1, model_size: 0 }, parse(config) { const result = { modelName: config.model_type || 'Unknown', fields: {}, errors: {}, warnings: {} }; const textConfig = config.text_config || config; // Required fields - check both top level and text_config if (config.num_hidden_layers) { result.fields['num-layers'] = config.num_hidden_layers; } else if (textConfig.num_hidden_layers) { result.fields['num-layers'] = textConfig.num_hidden_layers; } if (config.num_key_value_heads) { result.fields['kv-heads'] = config.num_key_value_heads; } else if (textConfig.num_key_value_heads) { result.fields['kv-heads'] = textConfig.num_key_value_heads; } if (textConfig.head_dim) { result.fields['head-size'] = textConfig.head_dim; } else if (textConfig.hidden_size && textConfig.num_attention_heads) { result.fields['head-size'] = Math.round(textConfig.hidden_size / textConfig.num_attention_heads); } if (config.num_attention_heads) { result.fields['num-heads'] = config.num_attention_heads; } else if (textConfig.num_attention_heads) { result.fields['num-heads'] = textConfig.num_attention_heads; } if (textConfig.max_position_embeddings) { result.fields['context-length'] = textConfig.max_position_embeddings; } if (textConfig.full_attention_interval) { result.fields['full-attention'] = textConfig.full_attention_interval; } // Optional fields with defaults result.fields['parallel'] = config.parallel || this.defaults.parallel; result.fields['model-size'] = config.model_size_gb || this.defaults.model_size; return result; } }; // 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'); const configFileInput = document.getElementById('config-file'); const resetBtn = document.getElementById('reset-btn'); const modelNameDisplay = document.getElementById('model-name-display'); const hfModelInput = document.getElementById('hf-model'); const hfFetchBtn = document.getElementById('hf-fetch-btn'); const loadingIndicator = document.getElementById('loading-indicator'); // 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); // Config file upload if (configFileInput) { configFileInput.addEventListener('change', handleConfigUpload); } // Reset button if (resetBtn) { resetBtn.addEventListener('click', handleReset); } // HF fetch button if (hfFetchBtn) { hfFetchBtn.addEventListener('click', handleHfFetch); } } // 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 config field translations const configLabel = document.querySelector('#config-file + label'); if (configLabel) { configLabel.textContent = translations[currentLang].config_file_label; } const configTooltip = document.querySelector('#config-file + .tooltip-icon'); if (configTooltip) { configTooltip.setAttribute('data-tooltip-ru', translations[currentLang].config_file_ru); configTooltip.setAttribute('data-tooltip-en', translations[currentLang].config_file_en); } // Update HF model field translation const hfModelLabel = document.querySelector('#hf-model + label'); if (hfModelLabel) { hfModelLabel.textContent = translations[currentLang].hf_model_label; } // Re-evaluate errors if (window.configErrors && Object.keys(window.configErrors).length > 0) { showConfigErrors(); } } // 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 = '

' + translations[currentLang].results_title + '

'; html += `
${translations[currentLang].k_cache}${results.kCache.formatted}
`; html += `
${translations[currentLang].v_cache}${results.vCache.formatted}
`; html += `
${translations[currentLang].total_kv}${results.totalKVCache.formatted}
`; if (results.totalMemory) { html += `
${translations[currentLang].total}${results.totalMemory.formatted}
`; } html += '
'; resultsContainer.innerHTML = html; } // Display validation errors function displayErrors(errors) { let html = '
' + translations[currentLang].errors_header + '
'; 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); }; } // Config file upload handler function handleConfigUpload(event) { const file = event.target.files[0]; if (!file) return; // Only accept .json files if (!file.name.toLowerCase().endsWith('.json')) { showConfigError('config-file', currentLang === 'ru' ? 'Неверный формат файла. Только .json файлы.' : 'Invalid file format. Only .json files allowed.'); return; } const reader = new FileReader(); reader.onload = function(e) { try { const config = JSON.parse(e.target.result); populateFromConfig(config); } catch (err) { showConfigError('config-file', currentLang === 'ru' ? 'Неверный JSON формат.' : 'Invalid JSON format.'); } }; reader.readAsText(file); } // Populate form from config function populateFromConfig(config) { // Set default quantization document.getElementById('k-type').value = 'f16'; document.getElementById('v-type').value = 'f16'; // Get text_config if available const textConfig = config.text_config || config; // Parse config using ConfigParser const parsed = ConfigParser.parse(config); // Clear previous errors window.configErrors = {}; // Display model name modelNameDisplay.textContent = parsed.modelName; // Set field values Object.keys(parsed.fields).forEach(fieldId => { const input = document.getElementById(fieldId); if (input) { input.value = parsed.fields[fieldId]; } }); // Show warnings for default values if (config.parallel === undefined && (!textConfig || textConfig.parallel === undefined)) { showConfigError('parallel', currentLang === 'ru' ? 'Не найден параметр parallel. Использовано значение по умолчанию: 1.' : 'parallel not found. Default value used: 1.'); } if (config.model_size_gb === undefined && (!textConfig || textConfig.model_size_gb === undefined)) { showConfigError('model-size', currentLang === 'ru' ? 'Не найден параметр model_size_gb. Использовано значение по умолчанию: 0.' : 'model_size_gb not found. Default value used: 0.'); } // Show errors for missing required fields if (!config.num_hidden_layers && (!textConfig || !textConfig.num_hidden_layers)) { showConfigError('num-layers', currentLang === 'ru' ? 'Не найден параметр num_hidden_layers. Важно для расчёта количества слоёв в KV кеше.' : 'num_hidden_layers not found. Important for calculating KV cache layers.'); } if (!config.num_key_value_heads && (!textConfig || !textConfig.num_key_value_heads)) { showConfigError('kv-heads', currentLang === 'ru' ? 'Не найден параметр num_key_value_heads. Важно для расчёта голов внимания.' : 'num_key_value_heads not found. Important for attention head calculation.'); } if (!textConfig.head_dim && !(textConfig.hidden_size && textConfig.num_attention_heads)) { showConfigError('head-size', currentLang === 'ru' ? 'Не найден параметр head_dim. Важно для размера каждой головы.' : 'head_dim not found. Important for head size calculation.'); } // Clear and show errors clearAllTooltips(); showConfigErrors(); // Auto-calculate handleCalculate(); } // Reset button handler function handleReset() { // Clear all form fields document.getElementById('context-length').value = ''; document.getElementById('kv-heads').value = ''; document.getElementById('head-size').value = ''; document.getElementById('num-heads').value = ''; document.getElementById('num-layers').value = ''; document.getElementById('model-size').value = ''; document.getElementById('parallel').value = '1'; document.getElementById('full-attention').value = ''; hfModelInput.value = ''; // Reset quantization document.getElementById('k-type').value = 'KV'; document.getElementById('v-type').value = 'f16'; document.getElementById('asymmetric').checked = false; document.getElementById('asymmetric-controls').classList.remove('visible'); // Clear model name modelNameDisplay.textContent = ''; // Clear errors window.configErrors = {}; clearAllTooltips(); } // HuggingFace fetch function async function handleHfFetch() { const modelName = hfModelInput.value.trim(); if (!modelName) { showConfigError('hf-model', currentLang === 'ru' ? 'Введите имя модели' : 'Please enter a model name'); return; } // Show loading state loadingIndicator.textContent = currentLang === 'ru' ? 'Загрузка config из HuggingFace...' : 'Fetching config from HuggingFace...'; loadingIndicator.style.display = 'block'; const url = `https://huggingface.co/${modelName}/resolve/main/config.json`; try { const response = await fetch(url); if (!response.ok) { if (response.status === 404) { showConfigError('hf-model', currentLang === 'ru' ? 'Модель не найдена' : 'Model not found'); } else { showConfigError('hf-model', currentLang === 'ru' ? `Ошибка загрузки: ${response.status}` : `Error loading: ${response.status}`); } loadingIndicator.style.display = 'none'; return; } const config = await response.json(); // Clear errors window.configErrors = {}; clearAllTooltips(); // Populate from config populateFromConfig(config); // Update tooltips after clearing them updateTooltips(); // Hide loading and set focus loadingIndicator.style.display = 'none'; hfModelInput.focus(); } catch (err) { loadingIndicator.style.display = 'none'; showConfigError('hf-model', currentLang === 'ru' ? `Не удалось загрузить config: ${err.message}` : `Failed to load config: ${err.message}`); } } // Show config error function showConfigError(field, message) { window.configErrors[field] = message; } // Clear all tooltips function clearAllTooltips() { document.querySelectorAll('.tooltip-icon').forEach(icon => { icon.removeAttribute('data-tooltip-ru'); icon.removeAttribute('data-tooltip-en'); }); document.querySelectorAll('.error-icon').forEach(icon => { icon.style.display = 'none'; }); } // Show config errors function showConfigErrors() { Object.keys(window.configErrors).forEach(fieldId => { let errorIcon; if (fieldId === 'hf-model') { errorIcon = document.querySelector('#hf-model + .error-icon'); } else { errorIcon = document.querySelector(`#${fieldId} ~ .error-icon`); } if (errorIcon) { errorIcon.style.display = 'inline-block'; errorIcon.setAttribute('data-tooltip-ru', window.configErrors[fieldId]); errorIcon.setAttribute('data-tooltip-en', window.configErrors[fieldId]); } }); } // Expose functions globally for testing window.CalculatorApp = { collectParams, handleCalculate, toggleAsymmetric, updateExampleText, toggleLanguage, updateText, updateTooltips, handleConfigUpload, populateFromConfig, handleReset, showConfigError, clearAllTooltips, showConfigErrors }; // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();