diff --git a/css/styles.css b/css/styles.css index 51589c7..dff0085 100644 --- a/css/styles.css +++ b/css/styles.css @@ -300,6 +300,49 @@ html[lang="en"] .tooltip-icon:hover::after { text-align: left; } +.model-name { + text-align: center; + font-weight: bold; + margin-bottom: 15px; + color: #0000FF; +} + +.error-icon { + display: none; + font-size: 14px; + font-weight: bold; + line-height: 1; + cursor: help; + user-select: none; +} + +.error-icon:hover::after { + content: attr(data-tooltip); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background-color: #FF0000; + color: #FFFFFF; + padding: 8px 12px; + font-size: 11px; + white-space: pre-wrap; + max-width: 300px; + z-index: 1000; + line-height: 1.4; + text-align: center; +} + +#reset-btn { + background-color: #333333; + width: auto; + padding: 8px 16px; +} + +#reset-btn:hover { + background-color: #555555; +} + @media (max-width: 600px) { .container { padding: 15px; diff --git a/index.html b/index.html index ad884dc..97aa3b4 100644 --- a/index.html +++ b/index.html @@ -13,6 +13,9 @@ + +
+
@@ -79,6 +82,14 @@
+
+ + + +
+ + +
diff --git a/js/app.js b/js/app.js index ec642c3..5418a99 100644 --- a/js/app.js +++ b/js/app.js @@ -29,6 +29,10 @@ 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 кеш:', @@ -64,6 +68,10 @@ 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', 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:', @@ -80,6 +88,55 @@ } }; + // 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 + if (config.num_hidden_layers) { + result.fields['num-layers'] = config.num_hidden_layers; + } + + if (config.num_key_value_heads) { + result.fields['kv-heads'] = config.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; + } + + 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: { @@ -121,6 +178,9 @@ 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'); // Initialize function init() { @@ -140,6 +200,16 @@ // Language toggle langToggleBtn.addEventListener('click', toggleLanguage); + + // Config file upload + if (configFileInput) { + configFileInput.addEventListener('change', handleConfigUpload); + } + + // Reset button + if (resetBtn) { + resetBtn.addEventListener('click', handleReset); + } } // Toggle language @@ -181,6 +251,23 @@ 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); + } + + // Re-evaluate errors + if (window.configErrors && Object.keys(window.configErrors).length > 0) { + showConfigErrors(); + } } // Update tooltips @@ -326,6 +413,148 @@ }; } + // 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'; + + // 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) { + showConfigError('parallel', currentLang === 'ru' + ? 'Не найден параметр parallel. Использовано значение по умолчанию: 1.' + : 'parallel not found. Default value used: 1.'); + } + if (config.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) { + 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) { + showConfigError('kv-heads', currentLang === 'ru' + ? 'Не найден параметр num_key_value_heads. Важно для расчёта голов внимания.' + : 'num_key_value_heads not found. Important for attention head calculation.'); + } + const textConfig = config.text_config || config; + 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 = ''; + + // 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(); + } + + // 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 => { + const 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, @@ -334,7 +563,13 @@ updateExampleText, toggleLanguage, updateText, - updateTooltips + updateTooltips, + handleConfigUpload, + populateFromConfig, + handleReset, + showConfigError, + clearAllTooltips, + showConfigErrors }; // Initialize when DOM is ready