commit c471e1d0a97efe45014f4840c0fd6d01276306a9 Author: romenskiy2012 Date: Sat Apr 11 23:54:55 2026 +0300 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 - Co-authored-by: Qwen3.5-35B-A3B-Claude-4.6-Opus-Reasoning-Distilled diff --git a/css/styles.css b/css/styles.css new file mode 100644 index 0000000..51589c7 --- /dev/null +++ b/css/styles.css @@ -0,0 +1,326 @@ +/* + * LLM Context Memory Calculator - Retro Style CSS + * White background, sharp borders, high contrast + */ + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: system-ui, -apple-system, sans-serif; + background-color: #FFFFFF; + color: #000000; + line-height: 1.6; + padding: 20px; +} + +.container { + max-width: 600px; + margin: 0 auto; + border: 3px solid #000000; + padding: 20px; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +h1 { + font-family: 'Courier New', monospace; + font-size: 28px; + font-weight: bold; + color: #0000FF; + border-bottom: 2px solid #0000FF; + padding-bottom: 10px; + flex-grow: 1; +} + +.lang-btn { + background-color: #000000; + color: #FFFFFF; + border: 2px solid #000000; + padding: 5px 10px; + font-family: 'Courier New', monospace; + font-size: 14px; + font-weight: bold; + cursor: pointer; +} + +.lang-btn:hover { + background-color: #333333; +} + +.form-group { + margin-bottom: 15px; + position: relative; +} + +label { + display: block; + font-weight: bold; + margin-bottom: 5px; + font-size: 14px; +} + +input[type="number"], +select { + width: 100%; + padding: 8px; + border: 2px solid #000000; + background-color: #FFFFFF; + color: #000000; + font-family: 'Courier New', monospace; + font-size: 16px; + border-radius: 0; +} + +input[type="number"]:focus, +select:focus { + outline: none; + border-color: #0000FF; + box-shadow: 0 0 5px rgba(0, 0, 255, 0.5); +} + +input[type="number"]::placeholder { + color: #666666; +} + +.checkbox-group { + display: flex; + align-items: center; + margin-bottom: 15px; +} + +.checkbox-group input[type="checkbox"] { + width: auto; + margin-right: 10px; + cursor: pointer; +} + +.checkbox-group label { + margin-bottom: 0; + cursor: pointer; +} + +.asymmetric-controls { + display: none; + border: 2px solid #000000; + padding: 15px; + margin-top: 10px; + background-color: #F5F5F5; +} + +.asymmetric-controls.visible { + display: block; +} + +.asymmetric-controls .form-group { + margin-bottom: 10px; +} + +button { + width: 100%; + padding: 12px; + background-color: #000000; + color: #FFFFFF; + border: none; + font-family: 'Courier New', monospace; + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: background-color 0.2s; +} + +button:hover { + background-color: #333333; +} + +.results { + margin-top: 20px; + border: 2px solid #000000; + padding: 15px; +} + +.results h2 { + font-family: 'Courier New', monospace; + font-size: 18px; + margin-bottom: 10px; + color: #000000; + border-bottom: 1px solid #000000; + padding-bottom: 5px; +} + +.result-item { + display: flex; + justify-content: space-between; + padding: 5px 0; + font-family: 'Courier New', monospace; + font-size: 14px; +} + +.result-item::before { + content: '├─'; + margin-right: 5px; + color: #000000; +} + +.result-item:last-child::before { + content: '└─'; +} + +.result-item .label { + color: #333333; +} + +.result-item .value { + font-weight: bold; +} + +.result-item.total::before { + content: '└─'; + color: #0000FF; +} + +.result-item.total .value { + color: #0000FF; +} + +.example { + margin-top: 20px; + padding: 10px; + border: 1px solid #CCCCCC; + background-color: #FAFAFA; +} + +.example p { + font-family: 'Courier New', monospace; + font-size: 12px; + color: #666666; +} + +.error-message { + background-color: #FFEEEE; + border: 2px solid #FF0000; + color: #FF0000; + padding: 10px; + margin-top: 10px; + font-family: 'Courier New', monospace; + font-size: 14px; +} + +.error-message ul { + margin-left: 20px; +} + +.error-message li { + margin-bottom: 5px; +} + +/* Tooltip Styles */ +.tooltip-icon { + display: inline-block; + width: 16px; + height: 16px; + background-color: #000000; + color: #FFFFFF; + text-align: center; + line-height: 16px; + font-family: 'Courier New', monospace; + font-size: 12px; + font-weight: bold; + margin-left: 5px; + cursor: help; + vertical-align: middle; + position: relative; +} + +.tooltip-icon::before { + content: "?"; +} + +/* Hide both languages by default */ +.tooltip-icon[data-tooltip-ru], +.tooltip-icon[data-tooltip-en] { + display: none; +} + +/* Show Russian when html lang is ru */ +html[lang="ru"] .tooltip-icon[data-tooltip-ru] { + display: inline-block; +} + +/* Show English when html lang is en */ +html[lang="en"] .tooltip-icon[data-tooltip-en] { + display: inline-block; +} + +.tooltip-icon:hover { + background-color: #0000FF; +} + +/* Tooltip popup for Russian */ +html[lang="ru"] .tooltip-icon:hover::after { + content: attr(data-tooltip-ru); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background-color: #000000; + color: #FFFFFF; + padding: 12px 16px; + font-size: 11px; + white-space: pre-wrap; + width: 375px; + z-index: 1000; + line-height: 1.4; + text-align: left; +} + +/* Tooltip popup for English */ +html[lang="en"] .tooltip-icon:hover::after { + content: attr(data-tooltip-en); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background-color: #000000; + color: #FFFFFF; + padding: 12px 16px; + font-size: 11px; + white-space: pre-wrap; + width: 375px; + z-index: 1000; + line-height: 1.4; + text-align: left; +} + +@media (max-width: 600px) { + .container { + padding: 15px; + } + + h1 { + font-size: 22px; + } + + input[type="number"], + select { + font-size: 14px; + } + + .header { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .lang-btn { + align-self: flex-end; + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..ad884dc --- /dev/null +++ b/index.html @@ -0,0 +1,118 @@ + + + + + + Калькулятор Памяти Контекста LLM + + + +
+
+

Калькулятор Памяти Контекста LLM

+ +
+ +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+
+ + + +
+
+ + +
+ +
+

Пример: context=8192, layers=32, kv_heads=32, head_size=128, model_size=7 GB, parallel=1

+
+ +
+
+ + + + + diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000..ec642c3 --- /dev/null +++ b/js/app.js @@ -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 = '

' + 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 + '
    '; + errors.forEach(error => { + html += `
  • ${error}
  • `; + }); + html += '
'; + 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(); + } +})(); diff --git a/js/calculation.js b/js/calculation.js new file mode 100644 index 0000000..46454b6 --- /dev/null +++ b/js/calculation.js @@ -0,0 +1,217 @@ +/** + * LLM Context Memory Calculator - Core Calculation Logic + * Pure functions with no DOM dependencies + */ + +const QUANT_SIZES = { + f32: 4.0, + f16: 2.0, + bf16: 2.0, + q8_0: 34/32, // 1.0625 + q4_0: 18/32, // 0.5625 + q4_1: 20/32, // 0.625 + iq4_nl: 18/32, // 0.5625 + q5_0: 22/32, // 0.6875 + q5_1: 24/32 // 0.75 +}; + +/** + * Normalize quantization type (handle 'K' alias for 'KV') + * @param {string} type - Quantization type + * @returns {string} Normalized type + */ +function normalizeQuantType(type) { + if (type === 'K') return 'KV'; + return type; +} + +/** + * Get quantization size in bytes per tensor element + * @param {string} type - Quantization type + * @returns {number} Size in bytes + */ +function getQuantizationSize(type) { + const normalizedType = normalizeQuantType(type); + return QUANT_SIZES[normalizedType] || QUANT_SIZES.f32; +} + +/** + * Format bytes to human-readable string + * @param {number} bytes - Number of bytes + * @returns {string} Formatted string (e.g., "1.23 MB") + */ +function formatBytes(bytes) { + if (bytes === 0) return '0 B'; + + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +/** + * Calculate memory breakdown + * @param {Object} params - Calculation parameters + * @returns {Object} Memory breakdown object + */ +function calculateMemory(params) { + const { + contextLength, + kType, + vType, + kvHeads, + headSize, + numLayers, + modelSizeGB, + parallel, + fullAttentionInterval + } = params; + + // Calculate effective layers (account for full attention interval) + const effectiveLayers = fullAttentionInterval + ? Math.ceil(numLayers / fullAttentionInterval) + : numLayers; + + // Determine parallel multiplier (default 1) + const parallelMultiplier = parallel || 1; + + // Get quantization sizes + const bK = getQuantizationSize(kType); + const bV = vType ? getQuantizationSize(vType) : bK; + + // Memory per token for all layers + // Formula: ctx × layers × kvheads × headdim × (bK + bV) + const memoryPerToken = contextLength * effectiveLayers * kvHeads * headSize * (bK + bV); + + // Total KV cache memory + const totalKVCache = memoryPerToken * parallelMultiplier; + + // Total memory including model weights (if provided) + const totalMemory = modelSizeGB + ? totalKVCache + (modelSizeGB * 1024 * 1024 * 1024) + : null; + + // Calculate individual cache sizes + const kCacheSize = contextLength * effectiveLayers * kvHeads * headSize * bK * parallelMultiplier; + const vCacheSize = contextLength * effectiveLayers * kvHeads * headSize * bV * parallelMultiplier; + + return { + kCache: { + size: kCacheSize, + formatted: formatBytes(kCacheSize) + }, + vCache: { + size: vCacheSize, + formatted: formatBytes(vCacheSize) + }, + totalKVCache: { + size: totalKVCache, + formatted: formatBytes(totalKVCache) + }, + totalMemory: totalMemory ? { + size: totalMemory, + formatted: formatBytes(totalMemory) + } : null, + effectiveLayers, + parallelMultiplier + }; +} + +/** + * Validate input parameters + * @param {Object} params - Parameters to validate + * @param {string} lang - Language code ('ru' or 'en') + * @returns {Object} Validation result + */ +function validateParams(params, lang) { + lang = lang || 'ru'; + const errors = []; + + if (!params.contextLength || params.contextLength <= 0) { + if (lang === 'ru') { + errors.push('Длина контекста должна быть положительным числом'); + } else { + errors.push('Context length must be a positive number'); + } + } + + if (!params.kvHeads || params.kvHeads <= 0) { + if (lang === 'ru') { + errors.push('Количество KV головок должно быть положительным числом'); + } else { + errors.push('KV heads must be a positive number'); + } + } + + if (!params.headSize || params.headSize <= 0) { + if (lang === 'ru') { + errors.push('Размер головы должен быть положительным числом'); + } else { + errors.push('Head size must be a positive number'); + } + } + + if (!params.numLayers || params.numLayers <= 0) { + if (lang === 'ru') { + errors.push('Количество слоев должно быть положительным числом'); + } else { + errors.push('Number of layers must be a positive number'); + } + } + + if (params.fullAttentionInterval && params.fullAttentionInterval <= 0) { + if (lang === 'ru') { + errors.push('Интервал полного внимания должен быть положительным числом'); + } else { + errors.push('Full attention interval must be a positive number'); + } + } + + if (params.modelSizeGB && params.modelSizeGB <= 0) { + if (lang === 'ru') { + errors.push('Размер модели должен быть положительным числом'); + } else { + errors.push('Model size must be a positive number'); + } + } + + return { + valid: errors.length === 0, + errors + }; +} + +/** + * Generate example parameters + * @returns {Object} Example parameters object + */ +function getExampleParams() { + return { + contextLength: 8192, + kType: 'f16', + vType: 'f16', + kvHeads: 32, + headSize: 128, + numLayers: 32, + modelSizeGB: 7, + parallel: 1, + fullAttentionInterval: null + }; +} + +// Export for use in other files +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + getQuantizationSize, + formatBytes, + calculateMemory, + validateParams, + getExampleParams, + QUANT_SIZES + }; +} + +// Global export for browser +window.calculateMemory = calculateMemory; +window.validateParams = validateParams;