From 77fa757959e9e9702dbce4d8a0e4f5455e622bcd Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Mon, 20 Jul 2026 13:07:33 +0800 Subject: [PATCH] =?UTF-8?q?v2.0.0:=20=E9=87=8D=E6=9E=84=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E7=AE=A1=E7=90=86(dense/MoE+=E9=87=8F=E5=8C=96=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=88=86=E7=A6=BB),=20=E7=89=88=E6=9C=AC=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=A2=9E=E5=8A=A0=E6=89=A7=E8=A1=8C=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E7=AE=A1=E7=90=86,=20=E4=BF=AE=E5=A4=8D=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E9=A1=B5GPU=E4=B8=8B=E6=8B=89=E4=B8=BA=E7=A9=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 版本管理增加执行程序(binaries)管理: 每个版本可管理llama-server/llama-cli/llama-bench等程序 - 参数管理中增加执行程序选择器, 可按程序筛选参数 - 参数可绑定到特定执行程序或适用所有程序 2. 模型管理重构: 一个模型统一参数(区分dense/MoE), 量化版本作为子表管理 - models表增加model_type(dense/moe)和num_experts字段 - 新增model_quants表, 每个模型可有多个量化版本 - 前端模型选择改为: 选模型 -> 选量化版本 3. 修复系统设置中默认GPU下拉为空: 异步加载GPU后再渲染设置 --- .gitignore | 1 + app.py | 371 +++++++++++++++++++++++++-------------------- db.py | 330 ++++++++++++++++++++++++++++------------ static/admin.html | 64 +++++++- static/index.html | 5 +- static/js/admin.js | 267 ++++++++++++++++++++++++++------ static/js/main.js | 197 ++++++++++++------------ 7 files changed, 807 insertions(+), 428 deletions(-) diff --git a/.gitignore b/.gitignore index e83a417..7b6e4ec 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ data.db logs/ *.log .env +data.db.bak.* diff --git a/app.py b/app.py index f4737ab..cb570ef 100644 --- a/app.py +++ b/app.py @@ -31,7 +31,6 @@ def admin_login_required(f): # ==================== Helper Functions ==================== def parse_param_value(param_type, value): - """Parse parameter value based on its type.""" if value is None or value == '': return None if param_type == 'number': @@ -47,7 +46,6 @@ def parse_param_value(param_type, value): def format_param_value(param_type, value): - """Format parameter value for display.""" if param_type == 'number': try: f = float(value) @@ -60,34 +58,21 @@ def format_param_value(param_type, value): def get_flag_for_param(param): - """Get the flag string for a parameter (prefers short flag).""" if param['short_flag']: return param['short_flag'] return param['long_flag'] def get_kv_cache_bytes_per_element(cache_type): - """Get bytes per element for KV cache type.""" cache_type_map = { - 'f32': 4, - 'f16': 2, - 'bf16': 2, - 'q8_0': 1, - 'q4_0': 0.5, - 'q4_1': 0.5625, - 'iq4_nl': 0.5, - 'q5_0': 0.625, - 'q5_1': 0.6875, + 'f32': 4, 'f16': 2, 'bf16': 2, 'q8_0': 1, + 'q4_0': 0.5, 'q4_1': 0.5625, 'iq4_nl': 0.5, + 'q5_0': 0.625, 'q5_1': 0.6875, } return cache_type_map.get(cache_type, 2) def estimate_vram(params_dict, gpus, version_params): - """ - Estimate VRAM usage based on parameters. - Returns dict with breakdown. - """ - # Model parameters (user configurable hints) model_size_gb = float(params_dict.get('_model_size_gb', 0) or 0) model_layers = int(params_dict.get('_model_layers', 0) or 0) model_embd = int(params_dict.get('_model_embd', 0) or 0) @@ -95,12 +80,9 @@ def estimate_vram(params_dict, gpus, version_params): model_kv_heads = int(params_dict.get('_model_kv_heads', 0) or 0) model_head_dim = int(params_dict.get('_model_head_dim', 0) or 0) - # If model_layers not specified, try to estimate from model size if model_layers == 0 and model_size_gb > 0: - # Rough estimate: ~0.3GB per layer for 7B, scale accordingly model_layers = max(1, int(model_size_gb / 0.4)) - # n_gpu_layers ngl_raw = params_dict.get('n_gpu_layers', 'auto') if ngl_raw in ('auto', 'all', '-1'): ngl = model_layers if model_layers > 0 else 32 @@ -112,74 +94,56 @@ def estimate_vram(params_dict, gpus, version_params): except (ValueError, TypeError): ngl = 0 - # Context size ctx_size = int(params_dict.get('ctx_size', 0) or 0) if ctx_size == 0: - ctx_size = 4096 # default assumption + ctx_size = 4096 - # Batch size batch_size = int(params_dict.get('batch_size', 2048) or 2048) ubatch_size = int(params_dict.get('ubatch_size', 512) or 512) - # KV cache types ctk = params_dict.get('cache_type_k', 'f16') ctv = params_dict.get('cache_type_v', 'f16') k_bytes = get_kv_cache_bytes_per_element(ctk) v_bytes = get_kv_cache_bytes_per_element(ctv) - # Parallel slots parallel = int(params_dict.get('parallel', 1) or 1) if parallel < 1: parallel = 1 - # Flash attention fa = params_dict.get('flash_attn', 'auto') fa_enabled = fa in ('on', 'auto') - # === Calculate VRAM components === total_vram = sum(g['vram_mb'] for g in gpus) if gpus else 0 max_single_vram = max((g['vram_mb'] for g in gpus), default=0) - # 1. Model weights in VRAM if model_size_gb > 0 and model_layers > 0: ratio = min(1.0, ngl / model_layers) if ngl > 0 else 0 weights_vram_mb = model_size_gb * 1024 * ratio else: - # Fallback estimate: ~1GB per 1B params at Q4 weights_vram_mb = 0 - # 2. KV cache - # KV cache per layer = 2 (K and V) * n_kv_heads * head_dim * ctx_size * bytes_per_element if model_kv_heads > 0 and model_head_dim > 0: kv_per_layer_bytes = 2 * model_kv_heads * model_head_dim * ctx_size kv_total_bytes = kv_per_layer_bytes * model_layers * parallel elif model_embd > 0: - # Fallback: use embedding dim kv_per_layer_bytes = 2 * model_embd * ctx_size kv_total_bytes = kv_per_layer_bytes * model_layers * parallel else: - # Very rough estimate: ~0.5MB per layer per 1K context at f16 kv_total_bytes = 0 if model_layers > 0: kv_total_bytes = int(0.5 * 1024 * 1024 * model_layers * (ctx_size / 1024) * parallel) kv_vram_mb = (kv_total_bytes * (k_bytes + v_bytes) / 2) / (1024 * 1024) - # If ngl < model_layers, only ngl layers' KV is on GPU if model_layers > 0 and ngl < model_layers: kv_vram_mb = kv_vram_mb * (ngl / model_layers) - # 3. Compute buffer / overhead - # Rough: batch_size * model_embd * 4 bytes * some factor compute_mb = 0 if model_embd > 0: - compute_mb = (batch_size * model_embd * 4 * 2) / (1024 * 1024) # logits buffer - compute_mb = max(compute_mb, 100) # minimum overhead + compute_mb = (batch_size * model_embd * 4 * 2) / (1024 * 1024) + compute_mb = max(compute_mb, 100) - # CUDA context overhead (~300-500MB per GPU) cuda_overhead_mb = len(gpus) * 400 if gpus else 0 - - # Total total_estimate = weights_vram_mb + kv_vram_mb + compute_mb + cuda_overhead_mb return { @@ -196,20 +160,14 @@ def estimate_vram(params_dict, gpus, version_params): 'max_single_vram_mb': max_single_vram, 'fits': total_estimate <= total_vram if total_vram > 0 else None, 'usage_percent': round(total_estimate / total_vram * 100, 1) if total_vram > 0 else None, - # For CPU+GPU mode 'cpu_weights_mb': round(model_size_gb * 1024 * max(0, 1 - (ngl / model_layers if model_layers > 0 else 0)), 1) if model_size_gb > 0 else 0, } def parse_natural_language(text): - """ - Parse natural language description into parameter values. - Supports Chinese and English keywords. - """ result = {} text_lower = text.lower() - # GPU selection gpu_patterns = [ (r'(?:rtx\s*)?3090', 'RTX 3090'), (r'(?:rtx\s*)?4090', 'RTX 4090'), @@ -231,12 +189,10 @@ def parse_natural_language(text): result['_gpu_name'] = gpu_name break - # GPU count gpu_count_match = re.search(r'(\d+)\s*(?:张|块|个)?\s*(?:gpu|显卡|卡)', text_lower) if gpu_count_match: result['_gpu_count'] = int(gpu_count_match.group(1)) - # Context size ctx_patterns = [ r'(?:上下文|context|ctx)[\s大小为]*[::\s]*(\d+)', r'(\d+)\s*(?:k|K)\s*(?:上下文|context|ctx)', @@ -245,12 +201,11 @@ def parse_natural_language(text): m = re.search(pattern, text_lower) if m: val = int(m.group(1)) - if val < 100: # like "8k context" + if val < 100: val = val * 1024 result['ctx_size'] = str(val) break - # GPU layers ngl_patterns = [ r'(?:gpu\s*层|gpu\s*layers?|ngl|offload)[\s::]*(\d+)', r'(\d+)\s*(?:层|layers?)\s*(?:gpu|offload)', @@ -263,77 +218,62 @@ def parse_natural_language(text): result['n_gpu_layers'] = val break - # Temperature temp_match = re.search(r'(?:温度|temp|temperature)[\s::]*(\d+(?:\.\d+)?)', text_lower) if temp_match: result['temperature'] = temp_match.group(1) - # Top-k topk_match = re.search(r'(?:top[\s-]*k)[\s::]*(\d+)', text_lower) if topk_match: result['top_k'] = topk_match.group(1) - # Top-p topp_match = re.search(r'(?:top[\s-]*p)[\s::]*(\d+(?:\.\d+)?)', text_lower) if topp_match: result['top_p'] = topp_match.group(1) - # Threads threads_match = re.search(r'(?:线程|threads?|cpu\s*线程)[\s::]*(\d+)', text_lower) if threads_match: result['threads'] = threads_match.group(1) - # Batch size batch_match = re.search(r'(?:batch|批处理|批次)[\s大小::]*(\d+)', text_lower) if batch_match: result['batch_size'] = batch_match.group(1) - # Port port_match = re.search(r'(?:端口|port)[\s::]*(\d+)', text_lower) if port_match: result['port'] = port_match.group(1) - # Model path model_match = re.search(r'(?:模型|model)[\s路径]*[::\s]+([^\s,,]+\.gguf)', text_lower) if model_match: result['model'] = model_match.group(1) - # HF repo hf_match = re.search(r'(?:hf|hugging\s*face|仓库)[\s::]+([^\s,,]+)', text_lower) if hf_match and not model_match: result['hf_repo'] = hf_match.group(1) - # Flash attention if re.search(r'flash\s*atten|flash\s*attn|fa', text_lower): result['flash_attn'] = 'on' - # Mode if re.search(r'cpu\s*\+\s*gpu|gpu\s*\+\s*cpu|混合', text_lower): result['_mode'] = 'gpu_cpu' elif re.search(r'纯\s*gpu|gpu\s*only|仅\s*gpu', text_lower): result['_mode'] = 'gpu' - # Parallel parallel_match = re.search(r'(?:并行|parallel|slots?)[\s::]*(\d+)', text_lower) if parallel_match: result['parallel'] = parallel_match.group(1) - # Split mode if re.search(r'row\s*split|行分割|按行分割', text_lower): result['split_mode'] = 'row' elif re.search(r'tensor\s*split|张量分割|按张量分割', text_lower): result['split_mode'] = 'tensor' - # mlock if re.search(r'mlock|锁[\s定]*内存|内存锁', text_lower): result['mlock'] = 'true' - # numa numa_match = re.search(r'numa[\s::]*(distribute|isolate|numactl)', text_lower) if numa_match: result['numa'] = numa_match.group(1) - # Reasoning budget reasoning_match = re.search(r'(?:reasoning|推理|thinking)[\s预算budget]*[::\s]*(\d+)', text_lower) if reasoning_match: result['reasoning_budget'] = reasoning_match.group(1) @@ -341,6 +281,47 @@ def parse_natural_language(text): return result +def call_llm_for_parsing(url, key, model, system_prompt, user_text): + headers = {'Content-Type': 'application/json'} + if key: + headers['Authorization'] = f'Bearer {key}' + body = { + 'model': model, + 'messages': [ + {'role': 'system', 'content': system_prompt}, + {'role': 'user', 'content': user_text} + ], + 'temperature': 0.1, + 'max_tokens': 2000, + } + req = urllib.request.Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method='POST') + with urllib.request.urlopen(req, timeout=30) as resp: + data = json.loads(resp.read().decode('utf-8')) + content = data['choices'][0]['message']['content'] + content = content.strip() + if content.startswith('```'): + content = re.sub(r'^```\w*\n?', '', content) + content = re.sub(r'\n?```$', '', content) + result = json.loads(content) + return result + + +def get_model_quants_dict(db, model_id): + """Get quants for a model as a list of dicts.""" + quants = db.execute('SELECT * FROM model_quants WHERE model_id = ? ORDER BY sort_order', (model_id,)).fetchall() + return [dict(q) for q in quants] + + +def attach_quants_to_models(db, models): + """Attach quants array to each model in the list.""" + result = [] + for m in models: + d = dict(m) + d['quants'] = get_model_quants_dict(db, d['id']) + result.append(d) + return result + + # ==================== API Routes ==================== @app.route('/') @@ -350,7 +331,6 @@ def index(): @app.route('/admin') def admin(): - # Serve admin page; JS handles login check return send_from_directory('static', 'admin.html') @@ -391,10 +371,17 @@ def get_versions(): @app.route('/api/versions//params') def get_version_params(vid): db = get_db() - params = db.execute( - 'SELECT * FROM params WHERE version_id = ? ORDER BY is_important DESC, sort_order', - (vid,) - ).fetchall() + binary_id = request.args.get('binary_id', type=int) + if binary_id is not None: + params = db.execute( + 'SELECT * FROM params WHERE version_id = ? AND (binary_id = ? OR binary_id IS NULL) ORDER BY is_important DESC, sort_order', + (vid, binary_id) + ).fetchall() + else: + params = db.execute( + 'SELECT * FROM params WHERE version_id = ? ORDER BY is_important DESC, sort_order', + (vid,) + ).fetchall() result = [] for p in params: d = dict(p) @@ -408,6 +395,18 @@ def get_version_params(vid): return jsonify(result) +# ----- Version Binaries (public) ----- +@app.route('/api/versions//binaries') +def get_version_binaries(vid): + db = get_db() + binaries = db.execute( + 'SELECT * FROM version_binaries WHERE version_id = ? ORDER BY sort_order', (vid,) + ).fetchall() + result = [dict(b) for b in binaries] + db.close() + return jsonify(result) + + # ----- GPUs ----- @app.route('/api/gpus') def get_gpus(): @@ -430,54 +429,37 @@ def generate_command(): system_memory_gb = data.get('system_memory_gb', 0) db = get_db() - - # Get param definitions - param_defs = db.execute( - 'SELECT * FROM params WHERE version_id = ?', - (version_id,) - ).fetchall() + param_defs = db.execute('SELECT * FROM params WHERE version_id = ?', (version_id,)).fetchall() param_def_map = {p['param_key']: dict(p) for p in param_defs} - db.close() - # Build command cmd_parts = [binary] - # GPU layers if mode == 'gpu': - # Add GPU-related args if gpu_selections: - gpu_names = [g.get('name', '') for g in gpu_selections] - device_str = ','.join(str(i) for i in range(len(gpu_selections))) - - # n_gpu_layers ngl = params.get('n_gpu_layers', 'auto') if ngl and ngl != param_def_map.get('n_gpu_layers', {}).get('default_value', 'auto'): flag = param_def_map.get('n_gpu_layers', {}).get('short_flag') or param_def_map.get('n_gpu_layers', {}).get('long_flag', '-ngl') cmd_parts.append(f'{flag} {ngl}') - # tensor_split for multiple GPUs if len(gpu_selections) > 1: ts = params.get('tensor_split', '') if ts and ts != param_def_map.get('tensor_split', {}).get('default_value', ''): flag = param_def_map.get('tensor_split', {}).get('short_flag', '-ts') cmd_parts.append(f'{flag} {ts}') elif not ts: - # Auto-generate tensor split based on VRAM ratio total_vram = sum(g.get('vram_mb', 0) for g in gpu_selections) if total_vram > 0: ratios = [str(round(g.get('vram_mb', 0) / total_vram, 2)) for g in gpu_selections] cmd_parts.append(f'-ts {",".join(ratios)}') elif mode == 'gpu_cpu': - # GPU+CPU mode if gpu_selections: ngl = params.get('n_gpu_layers', 'auto') if ngl and ngl != param_def_map.get('n_gpu_layers', {}).get('default_value', 'auto'): flag = param_def_map.get('n_gpu_layers', {}).get('short_flag') or param_def_map.get('n_gpu_layers', {}).get('long_flag', '-ngl') cmd_parts.append(f'{flag} {ngl}') - # Iterate through params and add non-default ones skip_params = {'n_gpu_layers', 'tensor_split'} for key, value in params.items(): if key.startswith('_'): @@ -490,24 +472,20 @@ def generate_command(): continue default_val = p_def['default_value'] - # Skip if value equals default if str(value) == str(default_val): continue - # Skip empty values if value is None or value == '' or value == 'false': if str(default_val).lower() == 'false' and str(value).lower() == 'false': continue if value == '' or value is None: continue - # Boolean params: only add if true (and default is false) if p_def['param_type'] == 'boolean': if str(value).lower() == 'true' and str(default_val).lower() != 'true': flag = p_def['short_flag'] or p_def['long_flag'] cmd_parts.append(flag) elif str(value).lower() == 'false' and str(default_val).lower() == 'true': - # Add --no- variant flag = p_def['long_flag'] cmd_parts.append(f'--no-{flag.lstrip("--")}') continue @@ -528,14 +506,12 @@ def estimate(): mode = data.get('mode', 'gpu') system_memory_gb = data.get('system_memory_gb', 0) - # Prepare GPU list with VRAM gpus = [] for gs in gpu_selections: gpus.append({'vram_mb': gs.get('vram_mb', 0), 'name': gs.get('name', '')}) result = estimate_vram(params, gpus, []) - # Add system memory estimation for GPU+CPU mode if mode == 'gpu_cpu': model_size_gb = float(params.get('_model_size_gb', 0) or 0) model_layers = int(params.get('_model_layers', 0) or 0) @@ -549,13 +525,11 @@ def estimate(): except (ValueError, TypeError): ngl = 0 - # CPU portion of weights if model_layers > 0 and ngl < model_layers: cpu_weights_gb = model_size_gb * (1 - ngl / model_layers) else: cpu_weights_gb = 0 - # KV cache on CPU ctx_size = int(params.get('ctx_size', 0) or 0) if ctx_size == 0: ctx_size = 4096 @@ -563,7 +537,6 @@ def estimate(): if parallel < 1: parallel = 1 - # Estimate KV cache on CPU (layers not on GPU) model_kv_heads = int(params.get('_model_kv_heads', 0) or 0) model_head_dim = int(params.get('_model_head_dim', 0) or 0) model_embd = int(params.get('_model_embd', 0) or 0) @@ -576,11 +549,11 @@ def estimate(): kv_cpu_bytes = 2 * model_embd * ctx_size * remaining_layers * parallel else: kv_cpu_bytes = int(0.5 * 1024 * 1024 * remaining_layers * (ctx_size / 1024) * parallel) * 2 - kv_cpu_mb = kv_cpu_bytes * 2 / (1024 * 1024) # Assuming f16 + kv_cpu_mb = kv_cpu_bytes * 2 / (1024 * 1024) else: kv_cpu_mb = 0 - total_cpu_mb = cpu_weights_gb * 1024 + kv_cpu_mb + 500 # overhead + total_cpu_mb = cpu_weights_gb * 1024 + kv_cpu_mb + 500 result['cpu_total_mb'] = round(total_cpu_mb, 1) result['cpu_total_gb'] = round(total_cpu_mb / 1024, 2) result['cpu_weights_gb'] = round(cpu_weights_gb, 2) @@ -597,31 +570,28 @@ def estimate(): def get_models(): db = get_db() models = db.execute('SELECT * FROM models ORDER BY sort_order, name').fetchall() - result = [dict(m) for m in models] + result = attach_quants_to_models(db, models) db.close() return jsonify(result) + @app.route('/api/models/grouped') def get_models_grouped(): db = get_db() models = db.execute('SELECT * FROM models ORDER BY base_model, sort_order, name').fetchall() + result = attach_quants_to_models(db, models) + dq = db.execute("SELECT value FROM settings WHERE key = 'default_quant'").fetchone() db.close() grouped = {} - for m in models: - d = dict(m) - base = d['base_model'] + for m in result: + base = m['base_model'] if base not in grouped: - grouped[base] = [] - grouped[base].append(d) - # Get default quant from settings - db2 = get_db() - dq = db2.execute("SELECT value FROM settings WHERE key = 'default_quant'").fetchone() - db2.close() + grouped[base] = m # Store the full model (with quants) default_quant = dq['value'] if dq else 'Q4_K_M' return jsonify({'models': grouped, 'default_quant': default_quant}) -# ----- Public Settings (only nl_default_text) ----- +# ----- Public Settings ----- @app.route('/api/settings/public') def get_public_settings(): db = get_db() @@ -639,7 +609,6 @@ def get_public_settings(): def parse_nl(): data = request.json text = data.get('text', '') - # Try LLM API first if enabled db = get_db() llm_enabled = db.execute("SELECT value FROM settings WHERE key = 'llm_enabled'").fetchone() if llm_enabled and llm_enabled['value'] == 'true': @@ -661,43 +630,13 @@ def parse_nl(): print(f'LLM parse failed: {e}', file=sys.stderr) else: db.close() - # Fallback to regex parsing result = parse_natural_language(text) return jsonify(result) -def call_llm_for_parsing(url, key, model, system_prompt, user_text): - """Call LLM API to parse natural language into params.""" - headers = {'Content-Type': 'application/json'} - if key: - headers['Authorization'] = f'Bearer {key}' - body = { - 'model': model, - 'messages': [ - {'role': 'system', 'content': system_prompt}, - {'role': 'user', 'content': user_text} - ], - 'temperature': 0.1, - 'max_tokens': 2000, - } - req = urllib.request.Request(url, data=json.dumps(body).encode('utf-8'), headers=headers, method='POST') - with urllib.request.urlopen(req, timeout=30) as resp: - data = json.loads(resp.read().decode('utf-8')) - # OpenAI-compatible response - content = data['choices'][0]['message']['content'] - # Try to extract JSON from the response - content = content.strip() - if content.startswith('```'): - content = re.sub(r'^```\w*\n?', '', content) - content = re.sub(r'\n?```$', '', content) - result = json.loads(content) - return result - - # ==================== Admin API ==================== - # All admin routes below require login - +# ----- Admin GPUs ----- @app.route('/api/admin/gpus', methods=['GET', 'POST']) @admin_login_required def admin_gpus(): @@ -707,7 +646,6 @@ def admin_gpus(): result = [dict(g) for g in gpus] db.close() return jsonify(result) - elif request.method == 'POST': data = request.json db.execute( @@ -741,6 +679,7 @@ def admin_gpu_edit(gid): return jsonify({'status': 'ok'}) +# ----- Admin Versions ----- @app.route('/api/admin/versions', methods=['GET', 'POST']) @admin_login_required def admin_versions(): @@ -783,15 +722,66 @@ def admin_version_edit(vid): return jsonify({'status': 'ok'}) +# ----- Admin Version Binaries ----- +@app.route('/api/admin/versions//binaries', methods=['GET', 'POST']) +@admin_login_required +def admin_binaries(vid): + db = get_db() + if request.method == 'GET': + binaries = db.execute( + 'SELECT * FROM version_binaries WHERE version_id = ? ORDER BY sort_order', (vid,) + ).fetchall() + result = [dict(b) for b in binaries] + db.close() + return jsonify(result) + elif request.method == 'POST': + data = request.json + db.execute( + 'INSERT INTO version_binaries (version_id, name, description, sort_order) VALUES (?, ?, ?, ?)', + (vid, data['name'], data.get('description', ''), data.get('sort_order', 0)) + ) + db.commit() + db.close() + return jsonify({'status': 'ok'}) + + +@app.route('/api/admin/binaries/', methods=['PUT', 'DELETE']) +@admin_login_required +def admin_binary_edit(bid): + db = get_db() + if request.method == 'PUT': + data = request.json + db.execute( + 'UPDATE version_binaries SET name=?, description=?, sort_order=? WHERE id=?', + (data['name'], data.get('description', ''), data.get('sort_order', 0), bid) + ) + db.commit() + db.close() + return jsonify({'status': 'ok'}) + elif request.method == 'DELETE': + db.execute('DELETE FROM version_binaries WHERE id=?', (bid,)) + db.commit() + db.close() + return jsonify({'status': 'ok'}) + + +# ----- Admin Params ----- @app.route('/api/admin/versions//params', methods=['GET', 'POST']) @admin_login_required def admin_params(vid): db = get_db() if request.method == 'GET': - params = db.execute( - 'SELECT * FROM params WHERE version_id = ? ORDER BY is_important DESC, sort_order', - (vid,) - ).fetchall() + binary_id = request.args.get('binary_id', type=int) + if binary_id is not None: + params = db.execute( + 'SELECT * FROM params WHERE version_id = ? AND (binary_id = ? OR binary_id IS NULL) ORDER BY is_important DESC, sort_order', + (vid, binary_id) + ).fetchall() + else: + params = db.execute( + 'SELECT * FROM params WHERE version_id = ? ORDER BY is_important DESC, sort_order', + (vid,) + ).fetchall() result = [] for p in params: d = dict(p) @@ -808,12 +798,13 @@ def admin_params(vid): options = data.get('options') if isinstance(options, list): options = json.dumps(options) + binary_id = data.get('binary_id') or None db.execute( '''INSERT INTO params - (version_id, param_key, short_flag, long_flag, description, category, param_type, + (version_id, binary_id, param_key, short_flag, long_flag, description, category, param_type, default_value, options, min_value, max_value, step, unit, is_important, affects_vram, sort_order) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', - (vid, data['param_key'], data.get('short_flag', ''), data['long_flag'], + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', + (vid, binary_id, data['param_key'], data.get('short_flag', ''), data['long_flag'], data.get('description', ''), data.get('category', 'common'), data.get('param_type', 'string'), data.get('default_value', ''), options, data.get('min_value'), data.get('max_value'), @@ -834,12 +825,13 @@ def admin_param_edit(pid): options = data.get('options') if isinstance(options, list): options = json.dumps(options) + binary_id = data.get('binary_id') or None db.execute( '''UPDATE params SET - param_key=?, short_flag=?, long_flag=?, description=?, category=?, param_type=?, + binary_id=?, param_key=?, short_flag=?, long_flag=?, description=?, category=?, param_type=?, default_value=?, options=?, min_value=?, max_value=?, step=?, unit=?, is_important=?, affects_vram=?, sort_order=? WHERE id=?''', - (data['param_key'], data.get('short_flag', ''), data['long_flag'], + (binary_id, data['param_key'], data.get('short_flag', ''), data['long_flag'], data.get('description', ''), data.get('category', 'common'), data.get('param_type', 'string'), data.get('default_value', ''), options, data.get('min_value'), data.get('max_value'), @@ -856,6 +848,7 @@ def admin_param_edit(pid): return jsonify({'status': 'ok'}) +# ----- Admin Settings ----- @app.route('/api/admin/settings', methods=['GET', 'PUT']) @admin_login_required def admin_settings(): @@ -877,18 +870,20 @@ def admin_settings(): return jsonify({'status': 'ok'}) -# ----- Admin Models CRUD ----- +# ----- Admin Models ----- @app.route('/api/admin/models', methods=['POST']) @admin_login_required def admin_add_model(): data = request.json db = get_db() db.execute( - '''INSERT INTO models (base_model, name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, default_ctx, quant, description, sort_order) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', - (data['base_model'], data['name'], data['size_gb'], data['layers'], data['embd'], - data['kv_heads'], data['head_dim'], data['attention_heads'], - data.get('default_ctx', 0), data.get('quant', ''), data.get('description', ''), data.get('sort_order', 0)) + '''INSERT INTO models (base_model, name, model_type, num_experts, size_gb, quant, layers, embd, kv_heads, head_dim, attention_heads, default_ctx, description, sort_order) + VALUES (?, ?, ?, ?, 0, '', ?, ?, ?, ?, ?, ?, ?, ?)''', + (data['base_model'], data.get('name', data['base_model']), + data.get('model_type', 'dense'), data.get('num_experts', 0), + data['layers'], data['embd'], data['kv_heads'], data['head_dim'], + data['attention_heads'], data.get('default_ctx', 0), + data.get('description', ''), data.get('sort_order', 0)) ) db.commit() db.close() @@ -902,11 +897,14 @@ def admin_model_edit(mid): if request.method == 'PUT': data = request.json db.execute( - '''UPDATE models SET base_model=?, name=?, size_gb=?, layers=?, embd=?, kv_heads=?, - head_dim=?, attention_heads=?, default_ctx=?, quant=?, description=?, sort_order=? WHERE id=?''', - (data['base_model'], data['name'], data['size_gb'], data['layers'], data['embd'], - data['kv_heads'], data['head_dim'], data['attention_heads'], - data.get('default_ctx', 0), data.get('quant', ''), data.get('description', ''), data.get('sort_order', 0), mid) + '''UPDATE models SET base_model=?, name=?, model_type=?, num_experts=?, + layers=?, embd=?, kv_heads=?, head_dim=?, attention_heads=?, + default_ctx=?, description=?, sort_order=? WHERE id=?''', + (data['base_model'], data.get('name', data['base_model']), + data.get('model_type', 'dense'), data.get('num_experts', 0), + data['layers'], data['embd'], data['kv_heads'], data['head_dim'], + data['attention_heads'], data.get('default_ctx', 0), + data.get('description', ''), data.get('sort_order', 0), mid) ) db.commit() db.close() @@ -918,6 +916,49 @@ def admin_model_edit(mid): return jsonify({'status': 'ok'}) +# ----- Admin Model Quants ----- +@app.route('/api/admin/models//quants', methods=['GET', 'POST']) +@admin_login_required +def admin_model_quants(mid): + db = get_db() + if request.method == 'GET': + quants = db.execute( + 'SELECT * FROM model_quants WHERE model_id = ? ORDER BY sort_order', (mid,) + ).fetchall() + result = [dict(q) for q in quants] + db.close() + return jsonify(result) + elif request.method == 'POST': + data = request.json + db.execute( + 'INSERT INTO model_quants (model_id, quant_type, size_gb, sort_order) VALUES (?, ?, ?, ?)', + (mid, data['quant_type'], data['size_gb'], data.get('sort_order', 0)) + ) + db.commit() + db.close() + return jsonify({'status': 'ok'}) + + +@app.route('/api/admin/model_quants/', methods=['PUT', 'DELETE']) +@admin_login_required +def admin_model_quant_edit(qid): + db = get_db() + if request.method == 'PUT': + data = request.json + db.execute( + 'UPDATE model_quants SET quant_type=?, size_gb=?, sort_order=? WHERE id=?', + (data['quant_type'], data['size_gb'], data.get('sort_order', 0), qid) + ) + db.commit() + db.close() + return jsonify({'status': 'ok'}) + elif request.method == 'DELETE': + db.execute('DELETE FROM model_quants WHERE id=?', (qid,)) + db.commit() + db.close() + return jsonify({'status': 'ok'}) + + if __name__ == '__main__': init_db() app.run(host='0.0.0.0', port=16052, debug=False) diff --git a/db.py b/db.py index 8e1813f..c096e15 100644 --- a/db.py +++ b/db.py @@ -43,11 +43,26 @@ def init_db(): ) ''') - # Parameters table (per version) + # Version binaries table (llama-server, llama-cli, etc.) + c.execute(''' + CREATE TABLE IF NOT EXISTS version_binaries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + version_id INTEGER NOT NULL, + name TEXT NOT NULL, + description TEXT, + sort_order INTEGER DEFAULT 0, + FOREIGN KEY (version_id) REFERENCES llama_versions(id) ON DELETE CASCADE, + UNIQUE(version_id, name) + ) + ''') + + # Parameters table (per version, optionally per binary) + # binary_id = NULL means the param applies to all binaries c.execute(''' CREATE TABLE IF NOT EXISTS params ( id INTEGER PRIMARY KEY AUTOINCREMENT, version_id INTEGER NOT NULL, + binary_id INTEGER DEFAULT NULL, param_key TEXT NOT NULL, short_flag TEXT, long_flag TEXT NOT NULL, @@ -63,8 +78,7 @@ def init_db(): is_important INTEGER DEFAULT 0, affects_vram INTEGER DEFAULT 0, sort_order INTEGER DEFAULT 0, - FOREIGN KEY (version_id) REFERENCES llama_versions(id) ON DELETE CASCADE, - UNIQUE(version_id, param_key) + FOREIGN KEY (version_id) REFERENCES llama_versions(id) ON DELETE CASCADE ) ''') @@ -76,33 +90,170 @@ def init_db(): ) ''') - # Models table + # Models table (base model info, one row per base model) c.execute(''' CREATE TABLE IF NOT EXISTS models ( id INTEGER PRIMARY KEY AUTOINCREMENT, base_model TEXT NOT NULL, name TEXT NOT NULL, - size_gb REAL NOT NULL, + model_type TEXT DEFAULT 'dense', + num_experts INTEGER DEFAULT 0, + size_gb REAL DEFAULT 0, + quant TEXT DEFAULT '', layers INTEGER NOT NULL, embd INTEGER NOT NULL, kv_heads INTEGER NOT NULL, head_dim INTEGER NOT NULL, attention_heads INTEGER NOT NULL, default_ctx INTEGER DEFAULT 0, - quant TEXT DEFAULT '', description TEXT DEFAULT '', sort_order INTEGER DEFAULT 0 ) ''') + # Model quantization variants table + c.execute(''' + CREATE TABLE IF NOT EXISTS model_quants ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + model_id INTEGER NOT NULL, + quant_type TEXT NOT NULL, + size_gb REAL NOT NULL, + sort_order INTEGER DEFAULT 0, + FOREIGN KEY (model_id) REFERENCES models(id) ON DELETE CASCADE + ) + ''') + conn.commit() - # Insert default data + # Run migrations for existing databases + migrate_db(conn) + + # Insert default data if empty insert_default_data(conn) conn.close() +def migrate_db(conn): + """Migrate existing database to new schema.""" + c = conn.cursor() + + # 1. Migrate params table: add binary_id column + try: + c.execute("SELECT binary_id FROM params LIMIT 1") + except sqlite3.OperationalError: + # Recreate params table with binary_id + c.execute("ALTER TABLE params RENAME TO params_old") + c.execute(''' + CREATE TABLE params ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + version_id INTEGER NOT NULL, + binary_id INTEGER DEFAULT NULL, + param_key TEXT NOT NULL, + short_flag TEXT, + long_flag TEXT NOT NULL, + description TEXT, + category TEXT NOT NULL DEFAULT 'common', + param_type TEXT NOT NULL DEFAULT 'string', + default_value TEXT, + options TEXT, + min_value REAL, + max_value REAL, + step REAL, + unit TEXT, + is_important INTEGER DEFAULT 0, + affects_vram INTEGER DEFAULT 0, + sort_order INTEGER DEFAULT 0, + FOREIGN KEY (version_id) REFERENCES llama_versions(id) ON DELETE CASCADE + ) + ''') + c.execute('''INSERT INTO params (id, version_id, binary_id, param_key, short_flag, long_flag, + description, category, param_type, default_value, options, min_value, max_value, + step, unit, is_important, affects_vram, sort_order) + SELECT id, version_id, NULL, param_key, short_flag, long_flag, + description, category, param_type, default_value, options, min_value, max_value, + step, unit, is_important, affects_vram, sort_order FROM params_old''') + c.execute("DROP TABLE params_old") + + # 2. Add model_type to models table + try: + c.execute("SELECT model_type FROM models LIMIT 1") + except sqlite3.OperationalError: + c.execute("ALTER TABLE models ADD COLUMN model_type TEXT DEFAULT 'dense'") + + # 3. Add num_experts to models table + try: + c.execute("SELECT num_experts FROM models LIMIT 1") + except sqlite3.OperationalError: + c.execute("ALTER TABLE models ADD COLUMN num_experts INTEGER DEFAULT 0") + + conn.commit() + + # 4. Insert default binaries for existing versions + c.execute("SELECT COUNT(*) as cnt FROM version_binaries") + if c.fetchone()['cnt'] == 0: + c.execute("SELECT id FROM llama_versions") + for v in c.fetchall(): + vid = v['id'] + c.execute("INSERT OR IGNORE INTO version_binaries (version_id, name, description, sort_order) VALUES (?, ?, ?, ?)", + (vid, 'llama-server', 'HTTP API 服务器', 1)) + c.execute("INSERT OR IGNORE INTO version_binaries (version_id, name, description, sort_order) VALUES (?, ?, ?, ?)", + (vid, 'llama-cli', '命令行交互', 2)) + c.execute("INSERT OR IGNORE INTO version_binaries (version_id, name, description, sort_order) VALUES (?, ?, ?, ?)", + (vid, 'llama-bench', '性能基准测试', 3)) + conn.commit() + + # 5. Migrate existing model data to model_quants table + c.execute("SELECT COUNT(*) as cnt FROM model_quants") + if c.fetchone()['cnt'] == 0: + c.execute("PRAGMA table_info(models)") + columns = [col['name'] for col in c.fetchall()] + + if 'quant' in columns: + c.execute("SELECT DISTINCT base_model FROM models") + base_models = [r['base_model'] for r in c.fetchall()] + + moe_models = { + 'Mixtral-8x7B-Instruct': 8, + 'DeepSeek-V2-Chat': 160, + 'DeepSeek-V2.5-Chat': 160, + } + + for base in base_models: + c.execute("SELECT * FROM models WHERE base_model = ? ORDER BY sort_order", (base,)) + variants = c.fetchall() + + if not variants: + continue + + first = variants[0] + model_id = first['id'] + + # Insert all quant variants into model_quants + for v in variants: + quant = v['quant'] if v['quant'] else 'FP16' + c.execute( + "INSERT INTO model_quants (model_id, quant_type, size_gb, sort_order) VALUES (?, ?, ?, ?)", + (model_id, quant, v['size_gb'], v['sort_order']) + ) + + # Set model_type and num_experts + model_type = 'dense' + num_experts = 0 + if base in moe_models: + model_type = 'moe' + num_experts = moe_models[base] + + c.execute("UPDATE models SET model_type = ?, num_experts = ?, name = ?, size_gb = 0, quant = '' WHERE id = ?", + (model_type, num_experts, base, model_id)) + + # Delete duplicate variants + for v in variants[1:]: + c.execute("DELETE FROM models WHERE id = ?", (v['id'],)) + + conn.commit() + + def insert_default_data(conn): c = conn.cursor() @@ -129,28 +280,30 @@ def insert_default_data(conn): ("RTX A5000", 24576, "8.6", "NVIDIA RTX A5000, 24GB VRAM", 14), ("RX 7900 XTX", 24576, "N/A", "AMD Radeon RX 7900 XTX, 24GB VRAM", 15), ] - for g in default_gpus: - c.execute('''INSERT INTO gpus (name, vram_mb, compute_capability, description, sort_order) - VALUES (?, ?, ?, ?, ?)''', g) + c.execute('INSERT INTO gpus (name, vram_mb, compute_capability, description, sort_order) VALUES (?, ?, ?, ?, ?)', g) # ===== Default llama.cpp versions ===== - # Version 1: b6310 (mid 2025) - c.execute('''INSERT INTO llama_versions (version_tag, description, release_date, is_active, sort_order) - VALUES (?, ?, ?, ?, ?)''', + c.execute('INSERT INTO llama_versions (version_tag, description, release_date, is_active, sort_order) VALUES (?, ?, ?, ?, ?)', ('b6310', 'llama.cpp build 6310 (2025年中)', '2025-06-01', 1, 1)) v1_id = c.lastrowid - # Version 2: b10068 (latest 2026) - c.execute('''INSERT INTO llama_versions (version_tag, description, release_date, is_active, sort_order) - VALUES (?, ?, ?, ?, ?)''', + c.execute('INSERT INTO llama_versions (version_tag, description, release_date, is_active, sort_order) VALUES (?, ?, ?, ?, ?)', ('b10068', 'llama.cpp build 10068 (2026年最新)', '2026-07-01', 1, 2)) v2_id = c.lastrowid + # ===== Default binaries for each version ===== + for vid in [v1_id, v2_id]: + c.execute('INSERT OR IGNORE INTO version_binaries (version_id, name, description, sort_order) VALUES (?, ?, ?, ?)', + (vid, 'llama-server', 'HTTP API 服务器', 1)) + c.execute('INSERT OR IGNORE INTO version_binaries (version_id, name, description, sort_order) VALUES (?, ?, ?, ?)', + (vid, 'llama-cli', '命令行交互', 2)) + c.execute('INSERT OR IGNORE INTO version_binaries (version_id, name, description, sort_order) VALUES (?, ?, ?, ?)', + (vid, 'llama-bench', '性能基准测试', 3)) + # ===== Parameters for b6310 ===== params_b6310 = [ # === Common params === - # Important ("model", "-m", "--model", "模型文件路径", "common", "string", "", None, None, None, None, None, 1, 0, 1), ("ctx_size", "-c", "--ctx-size", "上下文窗口大小 (0=从模型加载)", "common", "number", "0", None, 0, 131072, 512, "tokens", 1, 1, 2), ("n_gpu_layers", "-ngl", "--n-gpu-layers", "存储在VRAM中的最大层数 (0=不卸载, -1=全部)", "common", "number", "0", None, -1, 999, 1, "layers", 1, 1, 3), @@ -158,8 +311,6 @@ def insert_default_data(conn): ("batch_size", "-b", "--batch-size", "逻辑最大批处理大小", "common", "number", "2048", None, 1, 8192, 128, "tokens", 1, 1, 5), ("ubatch_size", "-ub", "--ubatch-size", "物理最大批处理大小", "common", "number", "512", None, 1, 4096, 64, "tokens", 0, 1, 6), ("flash_attn", "-fa", "--flash-attn", "Flash Attention (on/off)", "common", "select", "off", json.dumps(["on", "off"]), None, None, None, None, 1, 1, 7), - - # Less important ("predict", "-n", "--predict", "要预测的token数量 (-1=无限)", "common", "number", "-1", None, -1, 999999, 1, "tokens", 0, 0, 10), ("keep", "", "--keep", "从初始提示中保留的token数", "common", "number", "0", None, -1, 999999, 1, "tokens", 0, 0, 11), ("cache_type_k", "-ctk", "--cache-type-k", "KV缓存K的数据类型", "common", "select", "f16", json.dumps(["f32", "f16", "bf16", "q8_0", "q4_0", "q4_1", "iq4_nl", "q5_0", "q5_1"]), None, None, None, None, 0, 1, 12), @@ -182,7 +333,6 @@ def insert_default_data(conn): ("kv_offload", "-kvo", "--kv-offload", "启用KV缓存卸载", "common", "boolean", "true", None, None, None, None, None, 0, 0, 29), ("device", "-dev", "--device", "用于卸载的设备列表", "common", "string", "", None, None, None, None, None, 0, 0, 30), ("override_tensor", "-ot", "--override-tensor", "覆盖张量缓冲区类型", "common", "string", "", None, None, None, None, None, 0, 0, 31), - # === Sampling params === ("temperature", "", "--temp", "温度 (创造力)", "sampling", "number", "0.8", None, 0.01, 2.0, 0.05, None, 1, 0, 1), ("seed", "-s", "--seed", "随机种子 (-1=随机)", "sampling", "number", "-1", None, -1, 999999, 1, None, 0, 0, 2), @@ -199,7 +349,6 @@ def insert_default_data(conn): ("mirostat_ent", "", "--mirostat-ent", "Mirostat目标熵", "sampling", "number", "5.0", None, 1.0, 10.0, 0.1, None, 0, 0, 13), ("ignore_eos", "", "--ignore-eos", "忽略结束流token", "sampling", "boolean", "false", None, None, None, None, None, 0, 0, 14), ("samplers", "", "--samplers", "采样器序列", "sampling", "string", "penalties;dry;top_n_sigma;top_k;typ_p;top_p;min_p;xtc;temperature", None, None, None, None, None, 0, 0, 15), - # === Server params === ("port", "", "--port", "服务器监听端口", "server", "number", "8080", None, 1, 65535, 1, None, 1, 0, 1), ("host", "", "--host", "服务器监听地址", "server", "string", "0.0.0.0", None, None, None, None, None, 1, 0, 2), @@ -210,24 +359,20 @@ def insert_default_data(conn): ("warmup", "", "--warmup", "预热运行", "server", "boolean", "true", None, None, None, None, None, 0, 0, 7), ("pooling", "", "--pooling", "嵌入池化类型", "server", "select", "", json.dumps(["", "none", "mean", "cls", "last", "rank"]), None, None, None, None, 0, 0, 8), ("mmproj", "-mm", "--mmproj", "多模态投影文件路径", "server", "string", "", None, None, None, None, None, 0, 0, 9), - # === Model download === ("hf_repo", "-hf", "--hf-repo", "Hugging Face模型仓库", "model_source", "string", "", None, None, None, None, None, 0, 0, 1), ("hf_file", "-hff", "--hf-file", "Hugging Face模型文件", "model_source", "string", "", None, None, None, None, None, 0, 0, 2), ("hf_token", "-hft", "--hf-token", "Hugging Face访问令牌", "model_source", "string", "", None, None, None, None, None, 0, 0, 3), ("model_url", "-mu", "--model-url", "模型下载URL", "model_source", "string", "", None, None, None, None, None, 0, 0, 4), ("docker_repo", "-dr", "--docker-repo", "Docker Hub模型仓库", "model_source", "string", "", None, None, None, None, None, 0, 0, 5), - # === Logging === ("verbose", "-v", "--verbose", "详细日志输出", "logging", "boolean", "false", None, None, None, None, None, 0, 0, 1), ("log_file", "", "--log-file", "日志文件路径", "logging", "string", "", None, None, None, None, None, 0, 0, 2), ("log_colors", "", "--log-colors", "彩色日志", "logging", "select", "auto", json.dumps(["on", "off", "auto"]), None, None, None, None, 0, 0, 3), ("log_verbosity", "-lv", "--verbosity", "日志详细级别", "logging", "select", "3", json.dumps(["0", "1", "2", "3", "4", "5"]), None, None, None, None, 0, 0, 4), - # === LoRA === ("lora", "", "--lora", "LoRA适配器路径", "lora", "string", "", None, None, None, None, None, 0, 0, 1), ("lora_scaled", "", "--lora-scaled", "带缩放的LoRA适配器", "lora", "string", "", None, None, None, None, None, 0, 0, 2), - # === Advanced === ("cpu_mask", "-C", "--cpu-mask", "CPU亲和性掩码", "advanced", "string", "", None, None, None, None, None, 0, 0, 1), ("cpu_range", "-Cr", "--cpu-range", "CPU亲和性范围", "advanced", "string", "", None, None, None, None, None, 0, 0, 2), @@ -243,32 +388,24 @@ def insert_default_data(conn): for p in params_b6310: c.execute('''INSERT INTO params - (version_id, param_key, short_flag, long_flag, description, category, param_type, - default_value, options, min_value, max_value, step, unit, is_important, - affects_vram, sort_order) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', + (version_id, binary_id, param_key, short_flag, long_flag, description, category, param_type, + default_value, options, min_value, max_value, step, unit, is_important, affects_vram, sort_order) + VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', (v1_id,) + p) # ===== Parameters for b10068 (latest) ===== - params_b10068 = list(params_b6310) # Copy all from b6310 - - # Modify defaults that changed - # n_gpu_layers default changed from "0" to "auto" + params_b10068 = list(params_b6310) params_b10068 = [(p[0], p[1], p[2], p[3], p[4], p[5], "auto" if p[0] == "n_gpu_layers" else p[6], p[7], p[8], p[9], p[10], p[11], p[12], p[13], p[14]) if p[0] == "n_gpu_layers" else p for p in params_b10068] - - # flash_attn: default changed from "off" to "auto", and options include "auto" params_b10068 = [(p[0], p[1], p[2], p[3], p[4], p[5], "auto" if p[0] == "flash_attn" else p[6], '["on", "off", "auto"]' if p[0] == "flash_attn" else p[7], p[8], p[9], p[10], p[11], p[12], p[13], p[14]) if p[0] == "flash_attn" else p for p in params_b10068] - # Add new parameters for b10068 new_params_b10068 = [ - # New in b10068 ("fit", "-fit", "--fit", "自动调整参数以适应设备内存", "common", "select", "on", json.dumps(["on", "off"]), None, None, None, None, 1, 1, 8), ("fit_target", "-fitt", "--fit-target", "每个设备的目标余量(MiB)", "common", "string", "1024", None, None, None, None, "MiB", 0, 1, 9), ("fit_ctx", "-fitc", "--fit-ctx", "fit选项可设置的最小ctx大小", "common", "number", "4096", None, 1024, 131072, 512, "tokens", 0, 1, 10), @@ -304,76 +441,70 @@ def insert_default_data(conn): ] all_params_b10068 = list(params_b10068) + list(new_params_b10068) - for p in all_params_b10068: c.execute('''INSERT INTO params - (version_id, param_key, short_flag, long_flag, description, category, param_type, - default_value, options, min_value, max_value, step, unit, is_important, - affects_vram, sort_order) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', + (version_id, binary_id, param_key, short_flag, long_flag, description, category, param_type, + default_value, options, min_value, max_value, step, unit, is_important, affects_vram, sort_order) + VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', (v2_id,) + p) - # ===== Default models ===== - # (base_model, name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, default_ctx, quant, description, sort_order) + # ===== Default models (one entry per base_model) ===== + # (base_model, name, model_type, num_experts, layers, embd, kv_heads, head_dim, attention_heads, default_ctx, description, sort_order) default_models = [ - # Llama-3-8B-Instruct (ctx 8192) - ("Llama-3-8B-Instruct", "Llama-3-8B-Instruct (Q4_K_M)", 4.9, 32, 4096, 8, 128, 32, 8192, "Q4_K_M", "Meta Llama 3 8B Instruct, Q4_K_M 量化", 1), - ("Llama-3-8B-Instruct", "Llama-3-8B-Instruct (Q8_0)", 8.5, 32, 4096, 8, 128, 32, 8192, "Q8_0", "Meta Llama 3 8B Instruct, Q8_0 量化", 2), - ("Llama-3-8B-Instruct", "Llama-3-8B-Instruct (FP16)", 15.5, 32, 4096, 8, 128, 32, 8192, "FP16", "Meta Llama 3 8B Instruct, FP16", 3), - # Llama-3-70B-Instruct (ctx 8192) - ("Llama-3-70B-Instruct", "Llama-3-70B-Instruct (Q4_K_M)", 38.5, 80, 8192, 8, 128, 64, 8192, "Q4_K_M", "Meta Llama 3 70B Instruct, Q4_K_M 量化", 4), - ("Llama-3-70B-Instruct", "Llama-3-70B-Instruct (Q8_0)", 74.0, 80, 8192, 8, 128, 64, 8192, "Q8_0", "Meta Llama 3 70B Instruct, Q8_0 量化", 5), - ("Llama-3-70B-Instruct", "Llama-3-70B-Instruct (FP16)", 138.0, 80, 8192, 8, 128, 64, 8192, "FP16", "Meta Llama 3 70B Instruct, FP16", 6), - # Llama-3.1-8B-Instruct (ctx 131072) - ("Llama-3.1-8B-Instruct", "Llama-3.1-8B-Instruct (Q4_K_M)", 4.9, 32, 4096, 8, 128, 32, 131072, "Q4_K_M", "Meta Llama 3.1 8B Instruct, Q4_K_M 量化", 7), - ("Llama-3.1-8B-Instruct", "Llama-3.1-8B-Instruct (Q8_0)", 8.5, 32, 4096, 8, 128, 32, 131072, "Q8_0", "Meta Llama 3.1 8B Instruct, Q8_0 量化", 8), - # Llama-3.1-70B-Instruct (ctx 131072) - ("Llama-3.1-70B-Instruct", "Llama-3.1-70B-Instruct (Q4_K_M)", 38.5, 80, 8192, 8, 128, 64, 131072, "Q4_K_M", "Meta Llama 3.1 70B Instruct, Q4_K_M 量化", 9), - ("Llama-3.1-70B-Instruct", "Llama-3.1-70B-Instruct (Q8_0)", 74.0, 80, 8192, 8, 128, 64, 131072, "Q8_0", "Meta Llama 3.1 70B Instruct, Q8_0 量化", 10), - # Llama-3.1-405B-Instruct (ctx 131072) - ("Llama-3.1-405B-Instruct", "Llama-3.1-405B-Instruct (Q4_K_M)", 226.0, 126, 16384, 8, 128, 128, 131072, "Q4_K_M", "Meta Llama 3.1 405B Instruct, Q4_K_M 量化", 11), - # Qwen2.5-7B-Instruct (ctx 32768) - ("Qwen2.5-7B-Instruct", "Qwen2.5-7B-Instruct (Q4_K_M)", 4.7, 28, 3584, 4, 128, 28, 32768, "Q4_K_M", "Qwen2.5 7B Instruct, Q4_K_M 量化", 12), - ("Qwen2.5-7B-Instruct", "Qwen2.5-7B-Instruct (Q8_0)", 7.6, 28, 3584, 4, 128, 28, 32768, "Q8_0", "Qwen2.5 7B Instruct, Q8_0 量化", 13), - # Qwen2.5-14B-Instruct (ctx 32768) - ("Qwen2.5-14B-Instruct", "Qwen2.5-14B-Instruct (Q4_K_M)", 8.7, 40, 5120, 8, 128, 40, 32768, "Q4_K_M", "Qwen2.5 14B Instruct, Q4_K_M 量化", 14), - # Qwen2.5-32B-Instruct (ctx 32768) - ("Qwen2.5-32B-Instruct", "Qwen2.5-32B-Instruct (Q4_K_M)", 19.5, 64, 5120, 8, 128, 64, 32768, "Q4_K_M", "Qwen2.5 32B Instruct, Q4_K_M 量化", 15), - ("Qwen2.5-32B-Instruct", "Qwen2.5-32B-Instruct (Q8_0)", 32.0, 64, 5120, 8, 128, 64, 32768, "Q8_0", "Qwen2.5 32B Instruct, Q8_0 量化", 16), - # Qwen2.5-72B-Instruct (ctx 32768) - ("Qwen2.5-72B-Instruct", "Qwen2.5-72B-Instruct (Q4_K_M)", 42.0, 80, 8192, 8, 128, 64, 32768, "Q4_K_M", "Qwen2.5 72B Instruct, Q4_K_M 量化", 17), - ("Qwen2.5-72B-Instruct", "Qwen2.5-72B-Instruct (Q8_0)", 75.0, 80, 8192, 8, 128, 64, 32768, "Q8_0", "Qwen2.5 72B Instruct, Q8_0 量化", 18), - # DeepSeek-V2-Chat (ctx 4096) - ("DeepSeek-V2-Chat", "DeepSeek-V2-Chat (Q4_K_M)", 23.0, 60, 5120, 8, 128, 60, 4096, "Q4_K_M", "DeepSeek V2 Chat, Q4_K_M 量化", 19), - # DeepSeek-V2.5-Chat (ctx 4096) - ("DeepSeek-V2.5-Chat", "DeepSeek-V2.5-Chat (Q4_K_M)", 23.0, 60, 5120, 8, 128, 60, 4096, "Q4_K_M", "DeepSeek V2.5 Chat, Q4_K_M 量化", 20), - # DeepSeek-R1-Distill-Qwen-32B (ctx 131072) - ("DeepSeek-R1-Distill-Qwen-32B", "DeepSeek-R1-Distill-Qwen-32B (Q4_K_M)", 19.5, 64, 5120, 8, 128, 64, 131072, "Q4_K_M", "DeepSeek R1 Distill Qwen 32B, Q4_K_M 量化", 21), - ("DeepSeek-R1-Distill-Qwen-32B", "DeepSeek-R1-Distill-Qwen-32B (Q8_0)", 32.0, 64, 5120, 8, 128, 64, 131072, "Q8_0", "DeepSeek R1 Distill Qwen 32B, Q8_0 量化", 22), - # DeepSeek-R1-Distill-Llama-70B (ctx 131072) - ("DeepSeek-R1-Distill-Llama-70B", "DeepSeek-R1-Distill-Llama-70B (Q4_K_M)", 42.0, 80, 8192, 8, 128, 64, 131072, "Q4_K_M", "DeepSeek R1 Distill Llama 70B, Q4_K_M 量化", 23), - ("DeepSeek-R1-Distill-Llama-70B", "DeepSeek-R1-Distill-Llama-70B (Q8_0)", 75.0, 80, 8192, 8, 128, 64, 131072, "Q8_0", "DeepSeek R1 Distill Llama 70B, Q8_0 量化", 24), - # Mistral-7B-Instruct-v0.3 (ctx 32768) - ("Mistral-7B-Instruct-v0.3", "Mistral-7B-Instruct-v0.3 (Q4_K_M)", 4.4, 32, 4096, 8, 128, 32, 32768, "Q4_K_M", "Mistral 7B Instruct v0.3, Q4_K_M 量化", 25), - ("Mistral-7B-Instruct-v0.3", "Mistral-7B-Instruct-v0.3 (Q8_0)", 7.5, 32, 4096, 8, 128, 32, 32768, "Q8_0", "Mistral 7B Instruct v0.3, Q8_0 量化", 26), - # Mixtral-8x7B-Instruct (ctx 32768) - ("Mixtral-8x7B-Instruct", "Mixtral-8x7B-Instruct (Q4_K_M)", 26.0, 32, 4096, 8, 128, 32, 32768, "Q4_K_M", "Mixtral 8x7B Instruct, Q4_K_M 量化", 27), - # Gemma-2-9B-It (ctx 8192) - ("Gemma-2-9B-It", "Gemma-2-9B-It (Q4_K_M)", 5.4, 42, 3584, 4, 256, 14, 8192, "Q4_K_M", "Google Gemma 2 9B It, Q4_K_M 量化", 28), - # Gemma-2-27B-It (ctx 8192) - ("Gemma-2-27B-It", "Gemma-2-27B-It (Q4_K_M)", 16.5, 46, 4608, 4, 128, 36, 8192, "Q4_K_M", "Google Gemma 2 27B It, Q4_K_M 量化", 29), - # Phi-3-Mini-4K-Instruct (ctx 4096) - ("Phi-3-Mini-4K-Instruct", "Phi-3-Mini-4K-Instruct (Q4_K_M)", 2.5, 32, 3072, 32, 96, 32, 4096, "Q4_K_M", "Microsoft Phi-3 Mini 4K Instruct, Q4_K_M 量化", 30), - # Phi-3-Medium-14B-Instruct (ctx 14336) - ("Phi-3-Medium-14B-Instruct", "Phi-3-Medium-14B-Instruct (Q4_K_M)", 8.4, 40, 5120, 10, 128, 40, 14336, "Q4_K_M", "Microsoft Phi-3 Medium 14B Instruct, Q4_K_M 量化", 31), - # GLM-4-9B-Chat (ctx 131072) - ("GLM-4-9B-Chat", "GLM-4-9B-Chat (Q4_K_M)", 5.5, 40, 4096, 4, 128, 40, 131072, "Q4_K_M", "Zhipu GLM-4 9B Chat, Q4_K_M 量化", 32), - ("GLM-4-9B-Chat", "GLM-4-9B-Chat (Q8_0)", 9.0, 40, 4096, 4, 128, 40, 131072, "Q8_0", "Zhipu GLM-4 9B Chat, Q8_0 量化", 33), + ("Llama-3-8B-Instruct", "Llama-3-8B-Instruct", "dense", 0, 32, 4096, 8, 128, 32, 8192, "Meta Llama 3 8B Instruct", 1), + ("Llama-3-70B-Instruct", "Llama-3-70B-Instruct", "dense", 0, 80, 8192, 8, 128, 64, 8192, "Meta Llama 3 70B Instruct", 2), + ("Llama-3.1-8B-Instruct", "Llama-3.1-8B-Instruct", "dense", 0, 32, 4096, 8, 128, 32, 131072, "Meta Llama 3.1 8B Instruct", 3), + ("Llama-3.1-70B-Instruct", "Llama-3.1-70B-Instruct", "dense", 0, 80, 8192, 8, 128, 64, 131072, "Meta Llama 3.1 70B Instruct", 4), + ("Llama-3.1-405B-Instruct", "Llama-3.1-405B-Instruct", "dense", 0, 126, 16384, 8, 128, 128, 131072, "Meta Llama 3.1 405B Instruct", 5), + ("Qwen2.5-7B-Instruct", "Qwen2.5-7B-Instruct", "dense", 0, 28, 3584, 4, 128, 28, 32768, "Qwen2.5 7B Instruct", 6), + ("Qwen2.5-14B-Instruct", "Qwen2.5-14B-Instruct", "dense", 0, 40, 5120, 8, 128, 40, 32768, "Qwen2.5 14B Instruct", 7), + ("Qwen2.5-32B-Instruct", "Qwen2.5-32B-Instruct", "dense", 0, 64, 5120, 8, 128, 64, 32768, "Qwen2.5 32B Instruct", 8), + ("Qwen2.5-72B-Instruct", "Qwen2.5-72B-Instruct", "dense", 0, 80, 8192, 8, 128, 64, 32768, "Qwen2.5 72B Instruct", 9), + ("DeepSeek-V2-Chat", "DeepSeek-V2-Chat", "moe", 160, 60, 5120, 8, 128, 60, 4096, "DeepSeek V2 Chat (MoE, 160 experts)", 10), + ("DeepSeek-V2.5-Chat", "DeepSeek-V2.5-Chat", "moe", 160, 60, 5120, 8, 128, 60, 4096, "DeepSeek V2.5 Chat (MoE, 160 experts)", 11), + ("DeepSeek-R1-Distill-Qwen-32B", "DeepSeek-R1-Distill-Qwen-32B", "dense", 0, 64, 5120, 8, 128, 64, 131072, "DeepSeek R1 Distill Qwen 32B", 12), + ("DeepSeek-R1-Distill-Llama-70B", "DeepSeek-R1-Distill-Llama-70B", "dense", 0, 80, 8192, 8, 128, 64, 131072, "DeepSeek R1 Distill Llama 70B", 13), + ("Mistral-7B-Instruct-v0.3", "Mistral-7B-Instruct-v0.3", "dense", 0, 32, 4096, 8, 128, 32, 32768, "Mistral 7B Instruct v0.3", 14), + ("Mixtral-8x7B-Instruct", "Mixtral-8x7B-Instruct", "moe", 8, 32, 4096, 8, 128, 32, 32768, "Mixtral 8x7B Instruct (MoE, 8 experts)", 15), + ("Gemma-2-9B-It", "Gemma-2-9B-It", "dense", 0, 42, 3584, 4, 256, 14, 8192, "Google Gemma 2 9B It", 16), + ("Gemma-2-27B-It", "Gemma-2-27B-It", "dense", 0, 46, 4608, 4, 128, 36, 8192, "Google Gemma 2 27B It", 17), + ("Phi-3-Mini-4K-Instruct", "Phi-3-Mini-4K-Instruct", "dense", 0, 32, 3072, 32, 96, 32, 4096, "Microsoft Phi-3 Mini 4K Instruct", 18), + ("Phi-3-Medium-14B-Instruct", "Phi-3-Medium-14B-Instruct", "dense", 0, 40, 5120, 10, 128, 40, 14336, "Microsoft Phi-3 Medium 14B Instruct", 19), + ("GLM-4-9B-Chat", "GLM-4-9B-Chat", "dense", 0, 40, 4096, 4, 128, 40, 131072, "Zhipu GLM-4 9B Chat", 20), ] + # Quant variants: { base_model: [(quant_type, size_gb, sort_order), ...] } + default_model_quants = { + "Llama-3-8B-Instruct": [("Q4_K_M", 4.9, 1), ("Q8_0", 8.5, 2), ("FP16", 15.5, 3)], + "Llama-3-70B-Instruct": [("Q4_K_M", 38.5, 1), ("Q8_0", 74.0, 2), ("FP16", 138.0, 3)], + "Llama-3.1-8B-Instruct": [("Q4_K_M", 4.9, 1), ("Q8_0", 8.5, 2)], + "Llama-3.1-70B-Instruct": [("Q4_K_M", 38.5, 1), ("Q8_0", 74.0, 2)], + "Llama-3.1-405B-Instruct": [("Q4_K_M", 226.0, 1)], + "Qwen2.5-7B-Instruct": [("Q4_K_M", 4.7, 1), ("Q8_0", 7.6, 2)], + "Qwen2.5-14B-Instruct": [("Q4_K_M", 8.7, 1)], + "Qwen2.5-32B-Instruct": [("Q4_K_M", 19.5, 1), ("Q8_0", 32.0, 2)], + "Qwen2.5-72B-Instruct": [("Q4_K_M", 42.0, 1), ("Q8_0", 75.0, 2)], + "DeepSeek-V2-Chat": [("Q4_K_M", 23.0, 1)], + "DeepSeek-V2.5-Chat": [("Q4_K_M", 23.0, 1)], + "DeepSeek-R1-Distill-Qwen-32B": [("Q4_K_M", 19.5, 1), ("Q8_0", 32.0, 2)], + "DeepSeek-R1-Distill-Llama-70B": [("Q4_K_M", 42.0, 1), ("Q8_0", 75.0, 2)], + "Mistral-7B-Instruct-v0.3": [("Q4_K_M", 4.4, 1), ("Q8_0", 7.5, 2)], + "Mixtral-8x7B-Instruct": [("Q4_K_M", 26.0, 1)], + "Gemma-2-9B-It": [("Q4_K_M", 5.4, 1)], + "Gemma-2-27B-It": [("Q4_K_M", 16.5, 1)], + "Phi-3-Mini-4K-Instruct": [("Q4_K_M", 2.5, 1)], + "Phi-3-Medium-14B-Instruct": [("Q4_K_M", 8.4, 1)], + "GLM-4-9B-Chat": [("Q4_K_M", 5.5, 1), ("Q8_0", 9.0, 2)], + } + for m in default_models: - c.execute('''INSERT INTO models (base_model, name, size_gb, layers, embd, kv_heads, head_dim, attention_heads, default_ctx, quant, description, sort_order) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', m) + c.execute('''INSERT INTO models (base_model, name, model_type, num_experts, size_gb, quant, layers, embd, kv_heads, head_dim, attention_heads, default_ctx, description, sort_order) + VALUES (?, ?, ?, ?, 0, '', ?, ?, ?, ?, ?, ?, ?, ?)''', m) + model_id = c.lastrowid + base = m[0] + for q in default_model_quants.get(base, []): + c.execute('INSERT INTO model_quants (model_id, quant_type, size_gb, sort_order) VALUES (?, ?, ?, ?)', + (model_id, q[0], q[1], q[2])) # ===== Default settings ===== c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("admin_password", "admin123")) @@ -381,7 +512,6 @@ def insert_default_data(conn): c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("default_version", "b10068")) c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("default_mode", "gpu")) c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("default_quant", "Q4_K_M")) - # LLM API settings for natural language parsing c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("llm_enabled", "false")) c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("llm_api_url", "")) c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("llm_api_key", "")) diff --git a/static/admin.html b/static/admin.html index 080ae9e..1361f07 100644 --- a/static/admin.html +++ b/static/admin.html @@ -73,14 +73,39 @@ ID版本描述发布日期活跃排序操作 + + +
+

执行程序管理

+
+ + +
+
+ + + + +
+ + + +
ID程序名称描述排序操作
+

参数管理

-
+
- + +
+
+ +

添加新参数

@@ -105,6 +130,9 @@ + @@ -112,7 +140,7 @@
- +
ID标志描述分类类型默认值重要影响显存操作
ID标志描述分类类型默认值程序重要影响显存操作
@@ -124,24 +152,46 @@

添加新模型

- - + + + -
- +
ID基模型名称大小层数EMBDKVHDHeads默认CTX量化排序操作
ID基模型名称类型专家数层数EMBDKVHDHeads默认CTX排序操作
+ + +
+

量化版本管理

+
+ + +
+
+ + + + +
+ + + +
ID量化类型大小(GB)排序操作
+
diff --git a/static/index.html b/static/index.html index 4a9a16b..bfee97d 100644 --- a/static/index.html +++ b/static/index.html @@ -44,10 +44,9 @@
- - -
diff --git a/static/js/admin.js b/static/js/admin.js index 7b6a3cf..9e89256 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -1,12 +1,11 @@ // ===== Admin State ===== let adminState = { - gpus: [], versions: [], params: [], models: [], - currentVersionId: null, settings: {}, + gpus: [], versions: [], params: [], models: [], binaries: [], quants: [], + currentVersionId: null, currentBinaryId: null, currentModelId: null, settings: {}, }; // ===== Init ===== async function adminInit() { - // Check login status const res = await fetch('/api/admin/check'); const data = await res.json(); if (data.logged_in) { @@ -14,28 +13,25 @@ async function adminInit() { } } -function showAdminContent() { +async function showAdminContent() { document.getElementById('login-screen').classList.add('hidden'); document.getElementById('admin-content').classList.remove('hidden'); - loadAdminGpus(); - loadAdminVersions(); - loadAdminModels(); - loadAdminSettings(); + // IMPORTANT: Load GPUs first, then settings (settings dropdown depends on GPU list) + await loadAdminGpus(); + await loadAdminVersions(); + await loadAdminModels(); + await loadAdminSettings(); } // ===== Login ===== async function doLogin() { const password = document.getElementById('login-password').value; const res = await fetch('/api/admin/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) }); - if (res.ok) { - showAdminContent(); - } else { - document.getElementById('login-error').textContent = '密码错误,请重试'; - } + if (res.ok) { showAdminContent(); } + else { document.getElementById('login-error').textContent = '密码错误,请重试'; } } async function doLogout() { @@ -108,12 +104,18 @@ async function loadAdminVersions() { if (!res.ok) return; adminState.versions = await res.json(); renderVersionTable(); - const sel = document.getElementById('admin-param-version'); - sel.innerHTML = adminState.versions.map(v => ``).join(''); + // Populate version selectors + const selParam = document.getElementById('admin-param-version'); + const selBinary = document.getElementById('admin-binary-version'); + const opts = adminState.versions.map(v => ``).join(''); + selParam.innerHTML = opts; + selBinary.innerHTML = opts; if (adminState.versions.length > 0) { adminState.currentVersionId = adminState.versions[0].id; - sel.value = adminState.currentVersionId; - await loadAdminParams(); + selParam.value = adminState.currentVersionId; + selBinary.value = adminState.currentVersionId; + await loadAdminBinaries(); + await onParamVersionChange(); } } @@ -148,17 +150,97 @@ async function updateVersion(id, field, value) { } async function deleteVersion(id) { - if (!confirm('删除版本将同时删除该版本的所有参数,确定继续吗?')) return; + if (!confirm('删除版本将同时删除该版本的所有参数和执行程序,确定继续吗?')) return; await fetch(`/api/admin/versions/${id}`, { method: 'DELETE' }); await loadAdminVersions(); } -// ===== Parameter Management ===== -async function loadAdminParams() { - const sel = document.getElementById('admin-param-version'); +// ===== Binary Management ===== +async function loadAdminBinaries() { + const sel = document.getElementById('admin-binary-version'); adminState.currentVersionId = parseInt(sel.value); if (!adminState.currentVersionId) return; - const res = await fetch(`/api/admin/versions/${adminState.currentVersionId}/params`); + const res = await fetch(`/api/admin/versions/${adminState.currentVersionId}/binaries`); + if (!res.ok) return; + adminState.binaries = await res.json(); + renderBinaryTable(); + // Also update the param binary dropdown + updateParamBinaryDropdown(); +} + +function renderBinaryTable() { + document.getElementById('binary-table-body').innerHTML = adminState.binaries.map(b => ` + + ${b.id} + + + + + `).join(''); +} + +function updateParamBinaryDropdown() { + // Update the binary selector in param management + const sel = document.getElementById('admin-param-binary'); + const currentVal = sel.value; + sel.innerHTML = '' + adminState.binaries.map(b => + ``).join(''); + sel.value = currentVal; + // Also update the binary bind dropdown in add param form + const bindSel = document.getElementById('param-binary-bind'); + bindSel.innerHTML = '' + adminState.binaries.map(b => + ``).join(''); +} + +async function addBinary() { + const data = { + name: document.getElementById('binary-name').value, + description: document.getElementById('binary-desc').value, + sort_order: parseInt(document.getElementById('binary-order').value) || 0, + }; + if (!data.name) { alert('请填写程序名称'); return; } + await fetch(`/api/admin/versions/${adminState.currentVersionId}/binaries`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) + }); + ['binary-name','binary-desc','binary-order'].forEach(id => document.getElementById(id).value = id === 'binary-order' ? '0' : ''); + await loadAdminBinaries(); +} + +async function updateBinary(id, field, value) { + const b = adminState.binaries.find(b => b.id === id); + if (!b) return; + const data = { name: b.name, description: b.description, sort_order: b.sort_order }; + data[field] = (field === 'sort_order') ? (parseInt(value) || 0) : value; + await fetch(`/api/admin/binaries/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); + await loadAdminBinaries(); +} + +async function deleteBinary(id) { + if (!confirm('确定删除这个执行程序吗?')) return; + await fetch(`/api/admin/binaries/${id}`, { method: 'DELETE' }); + await loadAdminBinaries(); +} + +// ===== Parameter Management ===== +async function onParamVersionChange() { + const sel = document.getElementById('admin-param-version'); + adminState.currentVersionId = parseInt(sel.value); + // Load binaries for this version + const res = await fetch(`/api/admin/versions/${adminState.currentVersionId}/binaries`); + if (res.ok) { + adminState.binaries = await res.json(); + updateParamBinaryDropdown(); + } + await loadAdminParams(); +} + +async function loadAdminParams() { + if (!adminState.currentVersionId) return; + const binarySel = document.getElementById('admin-param-binary'); + const binaryId = binarySel.value; + let url = `/api/admin/versions/${adminState.currentVersionId}/params`; + if (binaryId) url += `?binary_id=${binaryId}`; + const res = await fetch(url); if (!res.ok) return; adminState.params = await res.json(); renderParamTable(); @@ -167,6 +249,12 @@ async function loadAdminParams() { function renderParamTable() { document.getElementById('param-table-body').innerHTML = adminState.params.map(p => { const flag = p.short_flag ? `${p.short_flag}/${p.long_flag}` : p.long_flag; + // Find binary name + let binaryName = '全部'; + if (p.binary_id) { + const b = adminState.binaries.find(b => b.id === p.binary_id); + binaryName = b ? b.name : '?'; + } return ` ${p.id} @@ -176,6 +264,7 @@ function renderParamTable() { + ${binaryName} @@ -186,6 +275,7 @@ function renderParamTable() { async function addParam() { const optionsStr = document.getElementById('param-options').value; const options = optionsStr ? optionsStr.split(',').map(s => s.trim()).filter(s => s) : null; + const binaryBind = document.getElementById('param-binary-bind').value; const data = { param_key: document.getElementById('param-key').value, short_flag: document.getElementById('param-short').value, long_flag: document.getElementById('param-long').value, description: document.getElementById('param-desc').value, @@ -194,20 +284,29 @@ async function addParam() { min_value: document.getElementById('param-min').value ? parseFloat(document.getElementById('param-min').value) : null, max_value: document.getElementById('param-max').value ? parseFloat(document.getElementById('param-max').value) : null, step: document.getElementById('param-step').value ? parseFloat(document.getElementById('param-step').value) : null, - unit: document.getElementById('param-unit').value, is_important: parseInt(document.getElementById('param-important').value) || 0, + unit: document.getElementById('param-unit').value, + binary_id: binaryBind ? parseInt(binaryBind) : null, + is_important: parseInt(document.getElementById('param-important').value) || 0, affects_vram: parseInt(document.getElementById('param-vram').value) || 0, sort_order: parseInt(document.getElementById('param-order').value) || 0, }; if (!data.param_key || !data.long_flag) { alert('请填写参数键和长标志'); return; } await fetch(`/api/admin/versions/${adminState.currentVersionId}/params`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); ['param-key','param-short','param-long','param-desc','param-default','param-options','param-min','param-max','param-step','param-unit'].forEach(id => document.getElementById(id).value = ''); document.getElementById('param-important').value = '0'; document.getElementById('param-vram').value = '0'; document.getElementById('param-order').value = '0'; + document.getElementById('param-binary-bind').value = ''; await loadAdminParams(); } async function updateParam(id, field, value) { const p = adminState.params.find(p => p.id === id); if (!p) return; - const data = { param_key: p.param_key, short_flag: p.short_flag, long_flag: p.long_flag, description: p.description, category: p.category, param_type: p.param_type, default_value: p.default_value, options: p.options, min_value: p.min_value, max_value: p.max_value, step: p.step, unit: p.unit, is_important: p.is_important, affects_vram: p.affects_vram, sort_order: p.sort_order }; + const data = { + param_key: p.param_key, short_flag: p.short_flag, long_flag: p.long_flag, description: p.description, + category: p.category, param_type: p.param_type, default_value: p.default_value, + options: p.options, min_value: p.min_value, max_value: p.max_value, step: p.step, + unit: p.unit, is_important: p.is_important, affects_vram: p.affects_vram, sort_order: p.sort_order, + binary_id: p.binary_id, + }; data[field] = (field === 'is_important' || field === 'affects_vram' || field === 'sort_order') ? parseInt(value) : value; await fetch(`/api/admin/params/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); await loadAdminParams(); @@ -217,7 +316,13 @@ async function updateParamFlag(id, value) { const p = adminState.params.find(p => p.id === id); if (!p) return; const parts = value.split('/').map(s => s.trim()); - const data = { param_key: p.param_key, short_flag: parts[0] || '', long_flag: parts[1] || parts[0] || '', description: p.description, category: p.category, param_type: p.param_type, default_value: p.default_value, options: p.options, min_value: p.min_value, max_value: p.max_value, step: p.step, unit: p.unit, is_important: p.is_important, affects_vram: p.affects_vram, sort_order: p.sort_order }; + const data = { + param_key: p.param_key, short_flag: parts[0] || '', long_flag: parts[1] || parts[0] || '', + description: p.description, category: p.category, param_type: p.param_type, default_value: p.default_value, + options: p.options, min_value: p.min_value, max_value: p.max_value, step: p.step, + unit: p.unit, is_important: p.is_important, affects_vram: p.affects_vram, sort_order: p.sort_order, + binary_id: p.binary_id, + }; await fetch(`/api/admin/params/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); await loadAdminParams(); } @@ -233,65 +338,135 @@ async function loadAdminModels() { const res = await fetch('/api/models'); adminState.models = await res.json(); renderModelTable(); + // Populate model selector for quants + const sel = document.getElementById('admin-quant-model'); + sel.innerHTML = adminState.models.map(m => ``).join(''); + if (adminState.models.length > 0) { + adminState.currentModelId = adminState.models[0].id; + await loadAdminQuants(); + } } function renderModelTable() { - document.getElementById('model-table-body').innerHTML = adminState.models.map(m => ` + document.getElementById('model-table-body').innerHTML = adminState.models.map(m => { + const typeBadge = m.model_type === 'moe' + ? 'MoE' + : 'Dense'; + return ` ${m.id} - - + + + - - `).join(''); + `; + }).join(''); } async function addModel() { const data = { base_model: document.getElementById('model-basemodel').value, - name: document.getElementById('model-name').value, - size_gb: parseFloat(document.getElementById('model-size').value) || 0, + name: document.getElementById('model-name').value || document.getElementById('model-basemodel').value, + model_type: document.getElementById('model-type').value, + num_experts: parseInt(document.getElementById('model-experts').value) || 0, layers: parseInt(document.getElementById('model-layers').value) || 0, embd: parseInt(document.getElementById('model-embd').value) || 0, kv_heads: parseInt(document.getElementById('model-kv').value) || 0, head_dim: parseInt(document.getElementById('model-hdim').value) || 0, attention_heads: parseInt(document.getElementById('model-heads').value) || 0, default_ctx: parseInt(document.getElementById('model-ctx').value) || 0, - quant: document.getElementById('model-quant').value, description: document.getElementById('model-desc').value, sort_order: parseInt(document.getElementById('model-order').value) || 0, }; - if (!data.base_model || !data.name || !data.size_gb || !data.layers) { alert('请填写基模型、名称、大小和层数'); return; } + if (!data.base_model || !data.layers) { alert('请填写基模型和层数'); return; } await fetch('/api/admin/models', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); - ['model-basemodel','model-name','model-size','model-layers','model-embd','model-kv','model-hdim','model-heads','model-ctx','model-quant','model-desc','model-order'].forEach(id => document.getElementById(id).value = id === 'model-order' ? '0' : ''); + ['model-basemodel','model-name','model-layers','model-embd','model-kv','model-hdim','model-heads','model-ctx','model-desc','model-order','model-experts'].forEach(id => document.getElementById(id).value = id === 'model-order' ? '0' : id === 'model-experts' ? '0' : ''); + document.getElementById('model-type').value = 'dense'; await loadAdminModels(); } async function updateModel(id, field, value) { const m = adminState.models.find(m => m.id === id); if (!m) return; - const data = { base_model: m.base_model, name: m.name, size_gb: m.size_gb, layers: m.layers, embd: m.embd, kv_heads: m.kv_heads, head_dim: m.head_dim, attention_heads: m.attention_heads, default_ctx: m.default_ctx || 0, quant: m.quant, description: m.description, sort_order: m.sort_order }; - if (['size_gb'].includes(field)) data[field] = parseFloat(value) || 0; - else if (['layers','embd','kv_heads','head_dim','attention_heads','default_ctx','sort_order'].includes(field)) data[field] = parseInt(value) || 0; + const data = { + base_model: m.base_model, name: m.name, model_type: m.model_type, num_experts: m.num_experts, + layers: m.layers, embd: m.embd, kv_heads: m.kv_heads, head_dim: m.head_dim, + attention_heads: m.attention_heads, default_ctx: m.default_ctx || 0, + description: m.description, sort_order: m.sort_order, + }; + if (['num_experts','layers','embd','kv_heads','head_dim','attention_heads','default_ctx','sort_order'].includes(field)) data[field] = parseInt(value) || 0; else data[field] = value; await fetch(`/api/admin/models/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); await loadAdminModels(); } async function deleteModel(id) { - if (!confirm('确定删除这个模型吗?')) return; + if (!confirm('删除模型将同时删除其所有量化版本,确定继续吗?')) return; await fetch(`/api/admin/models/${id}`, { method: 'DELETE' }); await loadAdminModels(); } +// ===== Quant Management ===== +async function loadAdminQuants() { + const sel = document.getElementById('admin-quant-model'); + adminState.currentModelId = parseInt(sel.value); + if (!adminState.currentModelId) return; + const res = await fetch(`/api/admin/models/${adminState.currentModelId}/quants`); + if (!res.ok) return; + adminState.quants = await res.json(); + renderQuantTable(); +} + +function renderQuantTable() { + document.getElementById('quant-table-body').innerHTML = adminState.quants.map(q => ` + + ${q.id} + + + + + `).join(''); +} + +async function addQuant() { + const data = { + quant_type: document.getElementById('quant-type').value, + size_gb: parseFloat(document.getElementById('quant-size').value) || 0, + sort_order: parseInt(document.getElementById('quant-order').value) || 0, + }; + if (!data.quant_type || !data.size_gb) { alert('请填写量化类型和大小'); return; } + await fetch(`/api/admin/models/${adminState.currentModelId}/quants`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) + }); + ['quant-type','quant-size','quant-order'].forEach(id => document.getElementById(id).value = id === 'quant-order' ? '0' : ''); + await loadAdminQuants(); +} + +async function updateQuant(id, field, value) { + const q = adminState.quants.find(q => q.id === id); + if (!q) return; + const data = { quant_type: q.quant_type, size_gb: q.size_gb, sort_order: q.sort_order }; + if (field === 'size_gb') data[field] = parseFloat(value) || 0; + else if (field === 'sort_order') data[field] = parseInt(value) || 0; + else data[field] = value; + await fetch(`/api/admin/model_quants/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); + await loadAdminQuants(); +} + +async function deleteQuant(id) { + if (!confirm('确定删除这个量化版本吗?')) return; + await fetch(`/api/admin/model_quants/${id}`, { method: 'DELETE' }); + await loadAdminQuants(); +} + // ===== Settings ===== async function loadAdminSettings() { const res = await fetch('/api/admin/settings'); @@ -315,40 +490,32 @@ function renderSettings() { 'llm_api_model': 'LLM 模型名', 'llm_system_prompt': 'LLM 系统提示词', }; - // Keys that use multi-line textarea const textareaKeys = ['nl_default_text', 'llm_system_prompt']; - // Keys that use select with live data const gpuSelectKeys = ['default_gpu']; const versionSelectKeys = ['default_version']; const modeSelectKeys = ['default_mode']; - // Keys that are boolean-like const boolKeys = ['llm_enabled', 'show_nl_section']; document.getElementById('settings-form').innerHTML = Object.entries(adminState.settings).map(([key, value]) => { const label = labels[key] || key; - // Textarea if (textareaKeys.includes(key)) { return `
`; } - // Select with GPU list + // GPU select with live data from adminState.gpus if (gpuSelectKeys.includes(key)) { const opts = adminState.gpus.map(g => ``).join(''); return `
`; } - // Select with version list if (versionSelectKeys.includes(key)) { const opts = adminState.versions.map(v => ``).join(''); return `
`; } - // Select with mode if (modeSelectKeys.includes(key)) { return `
`; } - // Boolean if (boolKeys.includes(key)) { return `
`; } - // Password or text const type = key === 'llm_api_key' ? 'password' : 'text'; return `
`; }).join(''); diff --git a/static/js/main.js b/static/js/main.js index 11a9d80..9db052c 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,12 +1,11 @@ // ===== State ===== let state = { - versions: [], gpus: [], models: [], modelsGrouped: {}, - baseModelList: [], filteredBaseModels: [], + versions: [], gpus: [], models: {}, modelList: [], binaries: [], currentVersionId: null, params: [], paramValues: {}, mode: 'gpu', gpuSlots: [], showHidden: false, currentTab: 'common', paramSearchText: '', - selectedModel: null, defaultQuant: 'Q4_K_M', - lastEstimate: null, + selectedModel: null, selectedQuant: null, defaultQuant: 'Q4_K_M', + lastEstimate: null, currentBinary: 'llama-server', }; // ===== Init ===== @@ -16,7 +15,12 @@ async function init() { await loadModelsGrouped(); await loadNlDefaultText(); const dv = state.versions.find(v => v.version_tag === 'b10068') || state.versions[0]; - if (dv) { state.currentVersionId = dv.id; document.getElementById('version-select').value = dv.id; await loadParams(dv.id); } + if (dv) { + state.currentVersionId = dv.id; + document.getElementById('version-select').value = dv.id; + await loadBinaries(dv.id); + await loadParams(dv.id); + } const dg = state.gpus.find(g => g.name === 'RTX 3090') || state.gpus[0]; if (dg) { state.gpuSlots = [{ ...dg }]; renderGpuSlots(); } renderParams(); generateCommand(); updateEstimate(); @@ -26,14 +30,9 @@ async function loadNlDefaultText() { const res = await fetch('/api/settings/public'); const data = await res.json(); const ta = document.getElementById('nl-input'); - if (data.nl_default_text) { - ta.placeholder = data.nl_default_text; - } - // Show/hide natural language section + if (data.nl_default_text) { ta.placeholder = data.nl_default_text; } const nlSection = document.getElementById('nl-section'); - if (nlSection) { - nlSection.classList.toggle('hidden', data.show_nl_section !== 'true'); - } + if (nlSection) { nlSection.classList.toggle('hidden', data.show_nl_section !== 'true'); } } // ===== Load Data ===== @@ -41,16 +40,29 @@ async function loadVersions() { const res = await fetch('/api/versions'); state.versions = await res.json(); document.getElementById('version-select').innerHTML = state.versions.map(v => ``).join(''); } + async function loadGpus() { const res = await fetch('/api/gpus'); state.gpus = await res.json(); } +async function loadBinaries(versionId) { + const res = await fetch(`/api/versions/${versionId}/binaries`); + const binaries = await res.json(); + state.binaries = binaries; + const select = document.getElementById('binary-select'); + const prevVal = state.currentBinary; + select.innerHTML = binaries.map(b => ``).join(''); + if (prevVal && binaries.some(b => b.name === prevVal)) { + select.value = prevVal; + } else if (binaries.length > 0) { + state.currentBinary = binaries[0].name; + } +} + async function loadModelsGrouped() { const res = await fetch('/api/models/grouped'); const data = await res.json(); - state.modelsGrouped = data.models || {}; + state.models = data.models || {}; state.defaultQuant = data.default_quant || 'Q4_K_M'; - state.baseModelList = Object.keys(state.modelsGrouped).sort(); - state.filteredBaseModels = state.baseModelList; - state.models = Object.values(state.modelsGrouped).flat(); + state.modelList = Object.values(state.models); renderBaseModelDropdown(); } @@ -64,7 +76,16 @@ async function loadParams(versionId) { renderParams(); generateCommand(); updateEstimate(); } -async function onVersionChange() { state.currentVersionId = parseInt(document.getElementById('version-select').value); await loadParams(state.currentVersionId); } +async function onVersionChange() { + state.currentVersionId = parseInt(document.getElementById('version-select').value); + await loadBinaries(state.currentVersionId); + await loadParams(state.currentVersionId); +} + +function onBinaryChange() { + state.currentBinary = document.getElementById('binary-select').value; + generateCommand(); +} function switchMode(mode) { state.mode = mode; @@ -91,83 +112,89 @@ function updateGpuSlot(i, name) { const g = state.gpus.find(g => g.name === name // ===== Model Selection ===== function filterBaseModels() { const text = document.getElementById('model-search').value.toLowerCase(); + const allNames = Object.keys(state.models).sort(); if (!text) { - // Show top 5 when input is empty (on focus) - state.filteredBaseModels = state.baseModelList.slice(0, 5); + state.filteredBaseModels = allNames.slice(0, 5); } else { - state.filteredBaseModels = state.baseModelList.filter(n => n.toLowerCase().includes(text)); + state.filteredBaseModels = allNames.filter(n => n.toLowerCase().includes(text)); } renderBaseModelDropdown(); document.getElementById('model-dropdown').style.display = state.filteredBaseModels.length > 0 ? 'block' : 'none'; } function onModelSearchFocus() { if (!document.getElementById('model-search').value) { - state.filteredBaseModels = state.baseModelList.slice(0, 5); + state.filteredBaseModels = Object.keys(state.models).sort().slice(0, 5); renderBaseModelDropdown(); document.getElementById('model-dropdown').style.display = 'block'; } } function onModelSearchBlur() { - // Delay to allow click on option setTimeout(() => { document.getElementById('model-dropdown').style.display = 'none'; }, 200); } function renderBaseModelDropdown() { - document.getElementById('model-dropdown').innerHTML = state.filteredBaseModels.map(n => { - const quants = state.modelsGrouped[n] || []; - const qs = quants.map(q => q.quant).filter(Boolean).join(', '); - return `
${n}${qs}
`; + document.getElementById('model-dropdown').innerHTML = (state.filteredBaseModels || []).map(n => { + const model = state.models[n]; + if (!model) return ''; + const quants = model.quants || []; + const qs = quants.map(q => q.quant_type).filter(Boolean).join(', '); + const typeBadge = model.model_type === 'moe' ? ' [MoE]' : ''; + return `
${n}${typeBadge}${qs}
`; }).join(''); } function selectBaseModel(baseName) { document.getElementById('model-search').value = baseName; document.getElementById('model-dropdown').style.display = 'none'; - const variants = state.modelsGrouped[baseName] || []; - if (variants.length === 0) return; + const model = state.models[baseName]; + if (!model) return; document.getElementById('quant-step').classList.remove('hidden'); - const dv = variants.find(v => v.quant === state.defaultQuant) || variants[0]; - renderQuantOptions(baseName, variants, dv.id); - selectModel(dv.id); + // Find default quant + const quants = model.quants || []; + const dq = quants.find(q => q.quant_type === state.defaultQuant) || quants[0]; + if (dq) { + renderQuantOptions(baseName, quants, dq.id); + selectQuant(baseName, dq.id); + } } -function renderQuantOptions(baseName, variants, selectedId) { - document.getElementById('quant-options').innerHTML = variants.map(v => ` - +function renderQuantOptions(baseName, quants, selectedId) { + document.getElementById('quant-options').innerHTML = quants.map(q => ` + `).join(''); } -function selectModel(id) { - const m = state.models.find(m => m.id === id); - if (!m) return; - state.selectedModel = m; - renderQuantOptions(m.base_model, state.modelsGrouped[m.base_model] || [], id); +function selectQuant(baseName, quantId) { + const model = state.models[baseName]; + if (!model) return; + const quant = (model.quants || []).find(q => q.id === quantId); + if (!quant) return; + state.selectedModel = model; + state.selectedQuant = quant; + renderQuantOptions(baseName, model.quants || [], quantId); + const typeInfo = model.model_type === 'moe' ? ` | MoE (${model.num_experts} experts)` : ' | Dense'; document.getElementById('model-selected-info').innerHTML = `
- 模型: ${m.name} - 大小: ${m.size_gb} GB - 层数: ${m.layers} - 嵌入: ${m.embd} - KV Heads: ${m.kv_heads} - Head Dim: ${m.head_dim} - 量化: ${m.quant || 'N/A'} + 模型: ${model.name} + 类型: ${model.model_type}${typeInfo} + 量化: ${quant.quant_type} + 大小: ${quant.size_gb} GB + 层数: ${model.layers} + 嵌入: ${model.embd} + KV Heads: ${model.kv_heads} + Head Dim: ${model.head_dim}
`; - // Auto-adjust params based on selected model - applyModelDefaults(m); + applyModelDefaults(model); updateEstimate(); } function applyModelDefaults(m) { - // Set ctx_size to model's default context if available if (m.default_ctx && m.default_ctx > 0) { state.paramValues['ctx_size'] = String(m.default_ctx); } - // Set n_gpu_layers to model's layer count (not 'all', use actual number) if (m.layers && m.layers > 0) { state.paramValues['n_gpu_layers'] = String(m.layers); } - // Multi-GPU: set split_mode to tensor, compute tensor_split by VRAM ratio if (state.gpuSlots.length > 1) { state.paramValues['split_mode'] = 'tensor'; updateTensorSplit(); } - // Update param UI if currently visible renderParams(); generateCommand(); } @@ -212,7 +239,6 @@ function onParamSearch() { } function getFilteredParams() { - // 'modified' is a special tab showing all modified params if (state.currentTab === 'modified') { return state.params.filter(p => isParamModified(p, state.paramValues[p.param_key])); } @@ -231,7 +257,6 @@ function getFilteredParams() { function renderParams() { const container = document.getElementById('param-container'); const all = getFilteredParams(); - // For 'modified' tab, show all (no important/hidden distinction) if (state.currentTab === 'modified' || state.paramSearchText) { container.innerHTML = all.map(p => renderParamItem(p)).join(''); document.getElementById('toggle-advanced-btn').style.display = 'none'; @@ -249,15 +274,10 @@ function renderParams() { function renderParamItem(p) { const val = state.paramValues[p.param_key]; - // Determine item class: empty-required (red), modified (orange), or normal let itemClass = ''; - if (p.param_key === 'model' && (!val || val.trim() === '')) { - itemClass = 'param-empty'; - } else if (isParamModified(p, val)) { - itemClass = 'param-changed'; - } + if (p.param_key === 'model' && (!val || val.trim() === '')) { itemClass = 'param-empty'; } + else if (isParamModified(p, val)) { itemClass = 'param-changed'; } const vb = p.affects_vram ? '⚡显存' : ''; - // Show both short and long flag const shortFlag = p.short_flag || ''; const longFlag = p.long_flag || ''; let flagHtml; @@ -292,20 +312,15 @@ function isParamModified(p, val) { function setParam(key, value) { state.paramValues[key] = value; - // Update this item's visual state without full re-render (preserves focus) const item = document.querySelector(`.param-item[data-key="${key}"]`); if (item) { const p = state.params.find(p => p.param_key === key); if (p) { item.classList.remove('param-empty', 'param-changed'); - if (p.param_key === 'model' && (!value || String(value).trim() === '')) { - item.classList.add('param-empty'); - } else if (isParamModified(p, value)) { - item.classList.add('param-changed'); - } + if (p.param_key === 'model' && (!value || String(value).trim() === '')) { item.classList.add('param-empty'); } + else if (isParamModified(p, value)) { item.classList.add('param-changed'); } } } - // If on 'modified' tab, re-render to update the list if (state.currentTab === 'modified') renderParams(); generateCommand(); updateEstimate(); } @@ -317,46 +332,40 @@ async function updateEstimate() { const params = collectParamsForEstimate(); const gpuSel = state.gpuSlots.map(s => ({ name: s.name, vram_mb: s.vram_mb })); const sm = parseFloat(document.getElementById('sys-memory').value) || 0; - // No unit select anymore - always GB, 0 = unlimited const sysGb = sm > 0 ? sm : 0; const isUnlimited = sm === 0; const res = await fetch('/api/estimate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ params, gpu_selections: gpuSel, mode: state.mode, system_memory_gb: sysGb }) }); const data = await res.json(); state.lastEstimate = data; renderVramDisplay(data); - if (state.mode === 'gpu_cpu') renderRamDisplay(data, sysGb, isUnlimited); updateStickyBar(data, sysGb, isUnlimited); generateCommandHint(data, sysGb, isUnlimited); } function collectParamsForEstimate() { const params = { ...state.paramValues }; - if (state.selectedModel) { - params._model_size_gb = state.selectedModel.size_gb; + if (state.selectedModel && state.selectedQuant) { + params._model_size_gb = state.selectedQuant.size_gb; params._model_layers = state.selectedModel.layers; params._model_embd = state.selectedModel.embd; params._model_kv_heads = state.selectedModel.kv_heads; params._model_head_dim = state.selectedModel.head_dim; params._model_heads = state.selectedModel.attention_heads; - } else { params._model_size_gb = 0; params._model_layers = 0; params._model_embd = 0; params._model_kv_heads = 0; params._model_head_dim = 0; params._model_heads = 0; } + } else { + params._model_size_gb = 0; params._model_layers = 0; params._model_embd = 0; + params._model_kv_heads = 0; params._model_head_dim = 0; params._model_heads = 0; + } return params; } function renderVramDisplay(data) { - // Update sticky bar instead of inline display updateStickyBar(data, parseFloat(document.getElementById('sys-memory').value) || 0, (parseFloat(document.getElementById('sys-memory').value) || 0) === 0); } -function renderRamDisplay(data, sysGb, isUnlimited) { - // Update sticky ram bar instead of inline display - // (sticky bar is already updated in updateStickyBar) -} - // ===== Sticky top bar ===== function updateStickyBar(data, sysGb, isUnlimited) { const vr = document.getElementById('sticky-vram-row'); const rr = document.getElementById('sticky-ram-row'); - // VRAM if (data.total_vram_available_mb) { const pct = data.usage_percent || 0; const bar = document.getElementById('sticky-vram-bar'); @@ -366,7 +375,6 @@ function updateStickyBar(data, sysGb, isUnlimited) { } else { document.getElementById('sticky-vram-text').textContent = 'VRAM: 请选择GPU'; } - // RAM if (state.mode === 'gpu_cpu') { rr.classList.remove('hidden'); if (isUnlimited) { @@ -388,8 +396,6 @@ function updateStickyBar(data, sysGb, isUnlimited) { function generateCommandHint(data, sysGb, isUnlimited) { const hint = document.getElementById('command-hint'); let hints = []; - - // Check VRAM if (data.usage_percent && data.usage_percent > 100) { hints.push({ type: 'error', text: `⚠️ 显存不足!预计需要 ${data.total_gb}GB,但仅有 ${data.total_vram_available_gb}GB。建议:减少 GPU 层数(n_gpu_layers)、减小上下文(ctx_size)、使用更低量化版本,或切换到 GPU+CPU 模式。` }); } else if (data.usage_percent && data.usage_percent > 90) { @@ -399,30 +405,16 @@ function generateCommandHint(data, sysGb, isUnlimited) { } else if (state.selectedModel && data.usage_percent && data.usage_percent <= 75) { hints.push({ type: 'ok', text: `✅ 显存充足,预计占用 ${data.usage_percent}%。` }); } - - // Check RAM (GPU+CPU mode) if (state.mode === 'gpu_cpu' && !isUnlimited) { if (data.cpu_usage_percent && data.cpu_usage_percent > 100) { - hints.push({ type: 'error', text: `⚠️ 系统内存不足!预计需要 ${data.cpu_total_gb}GB,但上限仅 ${sysGb}GB。建议:增加内存上限、减少 GPU 层数让更多权重留在 GPU、或减小上下文。` }); + hints.push({ type: 'error', text: `⚠️ 系统内存不足!预计需要 ${data.cpu_total_gb}GB,但上限仅 ${sysGb}GB。` }); } else if (data.cpu_usage_percent && data.cpu_usage_percent > 90) { hints.push({ type: 'warning', text: `⚡ 内存使用率 ${data.cpu_usage_percent}%,接近上限。` }); } } - - // No model selected - if (!state.selectedModel) { - hints.push({ type: 'info', text: '💡 请在上方选择模型,以便进行准确的显存估算。' }); - } - - // No GPU selected - if (state.gpuSlots.length === 0 || !state.gpuSlots[0].name) { - hints.push({ type: 'info', text: '💡 请选择 GPU 显卡。' }); - } - - if (hints.length === 0) { - hints.push({ type: 'ok', text: '✅ 配置看起来没问题,可以复制使用。' }); - } - + if (!state.selectedModel) { hints.push({ type: 'info', text: '💡 请在上方选择模型,以便进行准确的显存估算。' }); } + if (state.gpuSlots.length === 0 || !state.gpuSlots[0].name) { hints.push({ type: 'info', text: '💡 请选择 GPU 显卡。' }); } + if (hints.length === 0) { hints.push({ type: 'ok', text: '✅ 配置看起来没问题,可以复制使用。' }); } hint.innerHTML = hints.map(h => `
${h.text}
`).join(''); } @@ -461,8 +453,7 @@ function copyCommand() { try { document.execCommand('copy'); const btn = document.getElementById('copy-btn'); - btn.textContent = '✅ 已复制'; - btn.classList.add('copied'); + btn.textContent = '✅ 已复制'; btn.classList.add('copied'); setTimeout(() => { btn.textContent = '📋 复制'; btn.classList.remove('copied'); }, 2000); } catch(e) { alert('复制失败,请手动选择文本复制'); } document.body.removeChild(ta);