llm_calculator/js/app.js
Arseniy Romenskiy fd6208a376 Fix tooltips and error icons after config parsing
- Call updateTooltips() after clearAllTooltips() to restore tooltip attributes
- Add error icons to all input fields (context-length, kv-heads, head-size,
  num-heads, num-layers, parallel, full-attention, hf-model)
- Update showConfigErrors() to handle hf-model field correctly
2026-04-12 02:19:03 +03:00

671 lines
27 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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