Add config.json support with auto-populate and reset features
- Added ConfigParser module for parsing Hugging Face config files - Added model name display above form - Added file upload for config.json (accepts only .json files) - Added reset button to clear all fields - Added error indicators (!) with language-aware messages for missing fields - Auto-populates fields: num_hidden_layers, num_key_value_heads, head_dim, num_attention_heads, max_position_embeddings, full_attention_interval - Sets defaults for optional fields: parallel=1, model_size=0 - Auto-calculates after successful config upload - Default quantization set to f16
This commit is contained in:
parent
c471e1d0a9
commit
b8352ebd17
3 changed files with 290 additions and 1 deletions
|
|
@ -300,6 +300,49 @@ html[lang="en"] .tooltip-icon:hover::after {
|
||||||
text-align: left;
|
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) {
|
@media (max-width: 600px) {
|
||||||
.container {
|
.container {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
|
|
||||||
11
index.html
11
index.html
|
|
@ -13,6 +13,9 @@
|
||||||
<button id="lang-toggle" class="lang-btn">EN</button>
|
<button id="lang-toggle" class="lang-btn">EN</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Model name display -->
|
||||||
|
<div id="model-name-display" class="model-name"></div>
|
||||||
|
|
||||||
<form id="calculator-form">
|
<form id="calculator-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="context-length" data-key="context_length_label">Длина Контекста (токены)</label>
|
<label for="context-length" data-key="context_length_label">Длина Контекста (токены)</label>
|
||||||
|
|
@ -79,6 +82,14 @@
|
||||||
<span class="tooltip-icon" data-tooltip-ru="Интервал слоев для полного внимания. Слои считают полный KV кеш каждые N слоев. Уменьшает эффективное количество слоев" data-tooltip-en="Interval for full attention layers. Layers compute full KV cache every N layers. Reduces effective layer count"></span>
|
<span class="tooltip-icon" data-tooltip-ru="Интервал слоев для полного внимания. Слои считают полный KV кеш каждые N слоев. Уменьшает эффективное количество слоев" data-tooltip-en="Interval for full attention layers. Layers compute full KV cache every N layers. Reduces effective layer count"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<label for="config-file" data-key="config_file_label" style="margin-bottom: 0; white-space: nowrap;">Config.json</label>
|
||||||
|
<input type="file" id="config-file" accept=".json" style="flex: 1; margin-bottom: 0;">
|
||||||
|
<button type="button" id="reset-btn" data-key="reset_btn" style="margin-bottom: 0;">Сбросить</button>
|
||||||
|
</div>
|
||||||
|
<span class="tooltip-icon" data-tooltip-ru="Загрузите config.json из модели. Автоматически заполнит поля и запустит расчет" data-tooltip-en="Upload model config.json. Auto-fills fields and runs calculation"></span>
|
||||||
|
<span class="error-icon" style="display:none; color: #FF0000; cursor: help; margin-left: 5px;">!</span>
|
||||||
|
|
||||||
<div class="checkbox-group">
|
<div class="checkbox-group">
|
||||||
<input type="checkbox" id="asymmetric">
|
<input type="checkbox" id="asymmetric">
|
||||||
<label for="asymmetric" data-key="asymmetric_label">Асимметричный Контекст</label>
|
<label for="asymmetric" data-key="asymmetric_label">Асимметричный Контекст</label>
|
||||||
|
|
|
||||||
237
js/app.js
237
js/app.js
|
|
@ -29,6 +29,10 @@
|
||||||
asymmetric_label: 'Асимметричный Контекст',
|
asymmetric_label: 'Асимметричный Контекст',
|
||||||
v_type_label: 'Квантование V кеша',
|
v_type_label: 'Квантование V кеша',
|
||||||
calculate_btn: 'Рассчитать',
|
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',
|
example_text: 'Пример: context=8192, layers=32, kv_heads=32, head_size=128, model_size=7 GB, parallel=1',
|
||||||
results_title: 'Результаты',
|
results_title: 'Результаты',
|
||||||
k_cache: 'K кеш:',
|
k_cache: 'K кеш:',
|
||||||
|
|
@ -64,6 +68,10 @@
|
||||||
asymmetric_label: 'Asymmetric Context',
|
asymmetric_label: 'Asymmetric Context',
|
||||||
v_type_label: 'V cache quantization',
|
v_type_label: 'V cache quantization',
|
||||||
calculate_btn: 'Calculate',
|
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',
|
example_text: 'Example: context=8192, layers=32, kv_heads=32, head_size=128, model_size=7 GB, parallel=1',
|
||||||
results_title: 'Results',
|
results_title: 'Results',
|
||||||
k_cache: 'K cache:',
|
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
|
// Tooltip translations
|
||||||
const tooltips = {
|
const tooltips = {
|
||||||
ru: {
|
ru: {
|
||||||
|
|
@ -121,6 +178,9 @@
|
||||||
const resultsContainer = document.getElementById('results');
|
const resultsContainer = document.getElementById('results');
|
||||||
const exampleText = document.getElementById('example-text');
|
const exampleText = document.getElementById('example-text');
|
||||||
const langToggleBtn = document.getElementById('lang-toggle');
|
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
|
// Initialize
|
||||||
function init() {
|
function init() {
|
||||||
|
|
@ -140,6 +200,16 @@
|
||||||
|
|
||||||
// Language toggle
|
// Language toggle
|
||||||
langToggleBtn.addEventListener('click', toggleLanguage);
|
langToggleBtn.addEventListener('click', toggleLanguage);
|
||||||
|
|
||||||
|
// Config file upload
|
||||||
|
if (configFileInput) {
|
||||||
|
configFileInput.addEventListener('change', handleConfigUpload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset button
|
||||||
|
if (resetBtn) {
|
||||||
|
resetBtn.addEventListener('click', handleReset);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle language
|
// Toggle language
|
||||||
|
|
@ -181,6 +251,23 @@
|
||||||
currentLang === 'ru' ? 'Опционально, для общего объема памяти' : 'Optional, for total memory';
|
currentLang === 'ru' ? 'Опционально, для общего объема памяти' : 'Optional, for total memory';
|
||||||
document.getElementById('full-attention').placeholder =
|
document.getElementById('full-attention').placeholder =
|
||||||
currentLang === 'ru' ? 'Опционально' : 'Optional';
|
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
|
// 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
|
// Expose functions globally for testing
|
||||||
window.CalculatorApp = {
|
window.CalculatorApp = {
|
||||||
collectParams,
|
collectParams,
|
||||||
|
|
@ -334,7 +563,13 @@
|
||||||
updateExampleText,
|
updateExampleText,
|
||||||
toggleLanguage,
|
toggleLanguage,
|
||||||
updateText,
|
updateText,
|
||||||
updateTooltips
|
updateTooltips,
|
||||||
|
handleConfigUpload,
|
||||||
|
populateFromConfig,
|
||||||
|
handleReset,
|
||||||
|
showConfigError,
|
||||||
|
clearAllTooltips,
|
||||||
|
showConfigErrors
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize when DOM is ready
|
// Initialize when DOM is ready
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue