commit a407032b4d811a68b6122614a6ca6b532588b164 Author: hz4th_coder Date: Sun Jul 19 18:08:36 2026 +0800 feat: llama.cpp 命令生成器 v1.0.0 - 支持多版本 llama.cpp 参数 (b6310, b10068) - GPU 模式 / GPU+CPU 模式 - 多 GPU 支持 (最多4张) - 实时显存/内存估算 - 自然语言解析生成命令 - 参数分级显示 (重要/隐藏) - 仅输出非默认值参数 - 后台管理 (GPU/版本/参数 CRUD) - 模型预设 (7B-70B) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e83a417 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +data.db +logs/ +*.log +.env diff --git a/app.py b/app.py new file mode 100644 index 0000000..106dff8 --- /dev/null +++ b/app.py @@ -0,0 +1,741 @@ +#!/usr/bin/env python3 +"""llama.cpp command generator - main Flask application.""" + +import os +import sys +import json +import re +import math +from flask import Flask, request, jsonify, send_from_directory +from flask_cors import CORS +from db import get_db, init_db, DB_PATH + +app = Flask(__name__, static_folder='static', static_url_path='') +CORS(app) + + +# ==================== 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': + try: + if '.' in str(value): + return float(value) + return int(value) + except (ValueError, TypeError): + return value + elif param_type == 'boolean': + return str(value).lower() in ('true', '1', 'yes', 'on') + return value + + +def format_param_value(param_type, value): + """Format parameter value for display.""" + if param_type == 'number': + try: + f = float(value) + if f == int(f): + return str(int(f)) + return str(f) + except (ValueError, TypeError): + return str(value) + return str(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, + } + 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) + model_heads = int(params_dict.get('_model_heads', 0) or 0) + 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 + if ngl_raw == '0': + ngl = 0 + else: + try: + ngl = int(ngl_raw) + 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 + + # 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 + + # 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 { + 'total_mb': round(total_estimate, 1), + 'total_gb': round(total_estimate / 1024, 2), + 'weights_mb': round(weights_vram_mb, 1), + 'weights_gb': round(weights_vram_mb / 1024, 2), + 'kv_cache_mb': round(kv_vram_mb, 1), + 'kv_cache_gb': round(kv_vram_mb / 1024, 2), + 'compute_mb': round(compute_mb, 1), + 'cuda_overhead_mb': round(cuda_overhead_mb, 1), + 'total_vram_available_mb': total_vram, + 'total_vram_available_gb': round(total_vram / 1024, 2) if total_vram else 0, + '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'), + (r'(?:rtx\s*)?4080', 'RTX 4080'), + (r'(?:rtx\s*)?3080', 'RTX 3080'), + (r'(?:rtx\s*)?5090', 'RTX 5090'), + (r'(?:rtx\s*)?4070\s*ti', 'RTX 4070 Ti'), + (r'(?:rtx\s*)?4060', 'RTX 4060'), + (r'a100\s*80', 'A100 80GB'), + (r'a100\s*40', 'A100 40GB'), + (r'h100', 'H100 80GB'), + (r'v100', 'V100 32GB'), + (r'a6000', 'RTX A6000'), + (r'a5000', 'RTX A5000'), + (r'7900\s*xtx', 'RX 7900 XTX'), + ] + for pattern, gpu_name in gpu_patterns: + if re.search(pattern, text_lower): + 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)', + ] + for pattern in ctx_patterns: + m = re.search(pattern, text_lower) + if m: + val = int(m.group(1)) + if val < 100: # like "8k context" + 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)', + r'全部(?:offload|卸载|gpu)|all\s*(?:gpu|layers?)', + ] + for pattern in ngl_patterns: + m = re.search(pattern, text_lower) + if m: + val = m.group(1) if m.lastindex else '-1' + 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) + + return result + + +# ==================== API Routes ==================== + +@app.route('/') +def index(): + return send_from_directory('static', 'index.html') + + +@app.route('/admin') +def admin(): + return send_from_directory('static', 'admin.html') + + +# ----- Versions ----- +@app.route('/api/versions') +def get_versions(): + db = get_db() + versions = db.execute('SELECT * FROM llama_versions WHERE is_active = 1 ORDER BY sort_order').fetchall() + result = [dict(v) for v in versions] + db.close() + return jsonify(result) + + +@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() + result = [] + for p in params: + d = dict(p) + if d.get('options'): + try: + d['options'] = json.loads(d['options']) + except (json.JSONDecodeError, TypeError): + pass + result.append(d) + db.close() + return jsonify(result) + + +# ----- GPUs ----- +@app.route('/api/gpus') +def get_gpus(): + db = get_db() + gpus = db.execute('SELECT * FROM gpus ORDER BY sort_order, name').fetchall() + result = [dict(g) for g in gpus] + db.close() + return jsonify(result) + + +# ----- Generate Command ----- +@app.route('/api/generate', methods=['POST']) +def generate_command(): + data = request.json + version_id = data.get('version_id') + params = data.get('params', {}) + mode = data.get('mode', 'gpu') + gpu_selections = data.get('gpu_selections', []) + binary = data.get('binary', 'llama-server') + 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_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('_'): + continue + if key in skip_params: + continue + + p_def = param_def_map.get(key) + if not p_def: + 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 + + flag = p_def['short_flag'] or p_def['long_flag'] + cmd_parts.append(f'{flag} {value}') + + command = ' '.join(cmd_parts) + return jsonify({'command': command, 'mode': mode, 'binary': binary}) + + +# ----- Estimate VRAM ----- +@app.route('/api/estimate', methods=['POST']) +def estimate(): + data = request.json + params = data.get('params', {}) + gpu_selections = data.get('gpu_selections', []) + 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) + ngl_raw = params.get('n_gpu_layers', 'auto') + + if ngl_raw in ('auto', 'all', '-1'): + ngl = model_layers if model_layers > 0 else 32 + else: + try: + ngl = int(ngl_raw) + 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 + parallel = int(params.get('parallel', 1) or 1) + 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) + + if model_layers > 0 and ngl < model_layers: + remaining_layers = model_layers - ngl + if model_kv_heads > 0 and model_head_dim > 0: + kv_cpu_bytes = 2 * model_kv_heads * model_head_dim * ctx_size * remaining_layers * parallel + elif model_embd > 0: + 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 + else: + kv_cpu_mb = 0 + + total_cpu_mb = cpu_weights_gb * 1024 + kv_cpu_mb + 500 # overhead + 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) + result['cpu_kv_cache_mb'] = round(kv_cpu_mb, 1) + result['system_memory_gb'] = system_memory_gb + if system_memory_gb > 0: + result['cpu_usage_percent'] = round(total_cpu_mb / (system_memory_gb * 1024) * 100, 1) + + return jsonify(result) + + +# ----- Parse Natural Language ----- +@app.route('/api/parse-nl', methods=['POST']) +def parse_nl(): + data = request.json + text = data.get('text', '') + result = parse_natural_language(text) + return jsonify(result) + + +# ==================== Admin API ==================== + +@app.route('/api/admin/gpus', methods=['GET', 'POST']) +def admin_gpus(): + db = get_db() + if request.method == 'GET': + gpus = db.execute('SELECT * FROM gpus ORDER BY sort_order, name').fetchall() + result = [dict(g) for g in gpus] + db.close() + return jsonify(result) + + elif request.method == 'POST': + data = request.json + db.execute( + 'INSERT INTO gpus (name, vram_mb, compute_capability, description, sort_order) VALUES (?, ?, ?, ?, ?)', + (data['name'], data['vram_mb'], data.get('compute_capability', ''), + data.get('description', ''), data.get('sort_order', 0)) + ) + db.commit() + db.close() + return jsonify({'status': 'ok'}) + + +@app.route('/api/admin/gpus/', methods=['PUT', 'DELETE']) +def admin_gpu_edit(gid): + db = get_db() + if request.method == 'PUT': + data = request.json + db.execute( + 'UPDATE gpus SET name=?, vram_mb=?, compute_capability=?, description=?, sort_order=? WHERE id=?', + (data['name'], data['vram_mb'], data.get('compute_capability', ''), + data.get('description', ''), data.get('sort_order', 0), gid) + ) + db.commit() + db.close() + return jsonify({'status': 'ok'}) + elif request.method == 'DELETE': + db.execute('DELETE FROM gpus WHERE id=?', (gid,)) + db.commit() + db.close() + return jsonify({'status': 'ok'}) + + +@app.route('/api/admin/versions', methods=['GET', 'POST']) +def admin_versions(): + db = get_db() + if request.method == 'GET': + versions = db.execute('SELECT * FROM llama_versions ORDER BY sort_order').fetchall() + result = [dict(v) for v in versions] + db.close() + return jsonify(result) + elif request.method == 'POST': + data = request.json + db.execute( + 'INSERT INTO llama_versions (version_tag, description, release_date, is_active, sort_order) VALUES (?, ?, ?, ?, ?)', + (data['version_tag'], data.get('description', ''), data.get('release_date', ''), + data.get('is_active', 1), data.get('sort_order', 0)) + ) + db.commit() + db.close() + return jsonify({'status': 'ok'}) + + +@app.route('/api/admin/versions/', methods=['PUT', 'DELETE']) +def admin_version_edit(vid): + db = get_db() + if request.method == 'PUT': + data = request.json + db.execute( + 'UPDATE llama_versions SET version_tag=?, description=?, release_date=?, is_active=?, sort_order=? WHERE id=?', + (data['version_tag'], data.get('description', ''), data.get('release_date', ''), + data.get('is_active', 1), data.get('sort_order', 0), vid) + ) + db.commit() + db.close() + return jsonify({'status': 'ok'}) + elif request.method == 'DELETE': + db.execute('DELETE FROM llama_versions WHERE id=?', (vid,)) + db.commit() + db.close() + return jsonify({'status': 'ok'}) + + +@app.route('/api/admin/versions//params', methods=['GET', 'POST']) +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() + result = [] + for p in params: + d = dict(p) + if d.get('options'): + try: + d['options'] = json.loads(d['options']) + except (json.JSONDecodeError, TypeError): + pass + result.append(d) + db.close() + return jsonify(result) + elif request.method == 'POST': + data = request.json + options = data.get('options') + if isinstance(options, list): + options = json.dumps(options) + db.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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', + (vid, 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'), + data.get('step'), data.get('unit'), data.get('is_important', 0), + data.get('affects_vram', 0), data.get('sort_order', 0)) + ) + db.commit() + db.close() + return jsonify({'status': 'ok'}) + + +@app.route('/api/admin/params/', methods=['PUT', 'DELETE']) +def admin_param_edit(pid): + db = get_db() + if request.method == 'PUT': + data = request.json + options = data.get('options') + if isinstance(options, list): + options = json.dumps(options) + db.execute( + '''UPDATE params SET + 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'], + 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'), + data.get('step'), data.get('unit'), data.get('is_important', 0), + data.get('affects_vram', 0), data.get('sort_order', 0), pid) + ) + db.commit() + db.close() + return jsonify({'status': 'ok'}) + elif request.method == 'DELETE': + db.execute('DELETE FROM params WHERE id=?', (pid,)) + db.commit() + db.close() + return jsonify({'status': 'ok'}) + + +@app.route('/api/admin/settings', methods=['GET', 'PUT']) +def admin_settings(): + db = get_db() + if request.method == 'GET': + settings = db.execute('SELECT * FROM settings').fetchall() + result = {s['key']: s['value'] for s in settings} + db.close() + return jsonify(result) + elif request.method == 'PUT': + data = request.json + for key, value in data.items(): + db.execute( + 'INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)', + (key, str(value)) + ) + 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 new file mode 100644 index 0000000..7ac4181 --- /dev/null +++ b/db.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +"""Database management for llama.cpp command generator.""" + +import sqlite3 +import json +import os + +DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data.db') + + +def get_db(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA foreign_keys = ON") + return conn + + +def init_db(): + conn = get_db() + c = conn.cursor() + + # GPU table + c.execute(''' + CREATE TABLE IF NOT EXISTS gpus ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + vram_mb INTEGER NOT NULL, + compute_capability TEXT, + description TEXT, + sort_order INTEGER DEFAULT 0 + ) + ''') + + # llama.cpp versions table + c.execute(''' + CREATE TABLE IF NOT EXISTS llama_versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + version_tag TEXT NOT NULL UNIQUE, + description TEXT, + release_date TEXT, + is_active INTEGER DEFAULT 1, + sort_order INTEGER DEFAULT 0 + ) + ''') + + # Parameters table (per version) + c.execute(''' + CREATE TABLE IF NOT EXISTS params ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + version_id INTEGER NOT 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, + UNIQUE(version_id, param_key) + ) + ''') + + # Settings table (key-value for app config) + c.execute(''' + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT + ) + ''') + + conn.commit() + + # Insert default data + insert_default_data(conn) + + conn.close() + + +def insert_default_data(conn): + c = conn.cursor() + + # Check if data already exists + c.execute("SELECT COUNT(*) as cnt FROM gpus") + if c.fetchone()['cnt'] > 0: + return + + # ===== Default GPUs ===== + default_gpus = [ + ("RTX 3090", 24576, "8.6", "NVIDIA GeForce RTX 3090, 24GB VRAM", 1), + ("RTX 4090", 24576, "8.9", "NVIDIA GeForce RTX 4090, 24GB VRAM", 2), + ("RTX 4080", 16384, "8.9", "NVIDIA GeForce RTX 4080, 16GB VRAM", 3), + ("RTX 3080", 10240, "8.6", "NVIDIA GeForce RTX 3080, 10GB VRAM", 4), + ("RTX 3090 Ti", 24576, "8.6", "NVIDIA GeForce RTX 3090 Ti, 24GB VRAM", 5), + ("RTX 5090", 32768, "12.0", "NVIDIA GeForce RTX 5090, 32GB VRAM", 6), + ("RTX 4070 Ti", 12288, "8.9", "NVIDIA GeForce RTX 4070 Ti, 12GB VRAM", 7), + ("RTX 4060", 8192, "8.9", "NVIDIA GeForce RTX 4060, 8GB VRAM", 8), + ("A100 80GB", 81920, "8.0", "NVIDIA A100 80GB", 9), + ("A100 40GB", 40960, "8.0", "NVIDIA A100 40GB", 10), + ("H100 80GB", 81920, "9.0", "NVIDIA H100 80GB", 11), + ("V100 32GB", 32768, "7.0", "NVIDIA V100 32GB", 12), + ("RTX A6000", 49152, "8.6", "NVIDIA RTX A6000, 48GB VRAM", 13), + ("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) + + # ===== 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 (?, ?, ?, ?, ?)''', + ('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 (?, ?, ?, ?, ?)''', + ('b10068', 'llama.cpp build 10068 (2026年最新)', '2026-07-01', 1, 2)) + v2_id = c.lastrowid + + # ===== 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), + ("threads", "-t", "--threads", "生成期间使用的CPU线程数", "common", "number", "-1", None, -1, 128, 1, "threads", 1, 0, 4), + ("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), + ("cache_type_v", "-ctv", "--cache-type-v", "KV缓存V的数据类型", "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, 13), + ("split_mode", "-sm", "--split-mode", "跨多个GPU的分割模式", "common", "select", "layer", json.dumps(["none", "layer", "row"]), None, None, None, None, 0, 0, 14), + ("tensor_split", "-ts", "--tensor-split", "每个GPU卸载的模型比例, 逗号分隔", "common", "string", "", None, None, None, None, None, 0, 0, 15), + ("main_gpu", "-mg", "--main-gpu", "用于模型的主GPU索引", "common", "number", "0", None, 0, 7, 1, None, 0, 0, 16), + ("mlock", "", "--mlock", "强制系统将模型保留在RAM中", "common", "boolean", "false", None, None, None, None, None, 0, 0, 17), + ("mmap", "", "--mmap", "内存映射模型文件", "common", "boolean", "true", None, None, None, None, None, 0, 0, 18), + ("numa", "", "--numa", "NUMA优化", "common", "select", "", json.dumps(["", "distribute", "isolate", "numactl"]), None, None, None, None, 0, 0, 19), + ("cpu_moe", "-cmoe", "--cpu-moe", "将所有MoE权重保留在CPU", "common", "boolean", "false", None, None, None, None, None, 0, 0, 20), + ("rope_scaling", "", "--rope-scaling", "RoPE频率缩放方法", "common", "select", "linear", json.dumps(["none", "linear", "yarn"]), None, None, None, None, 0, 0, 21), + ("rope_freq_base", "", "--rope-freq-base", "RoPE基础频率", "common", "number", "10000", None, 1, 1000000, 1, None, 0, 0, 22), + ("rope_freq_scale", "", "--rope-freq-scale", "RoPE频率缩放因子", "common", "number", "1.0", None, 0.01, 10, 0.01, None, 0, 0, 23), + ("yarn_orig_ctx", "", "--yarn-orig-ctx", "YaRN原始上下文大小", "common", "number", "0", None, 0, 131072, 512, "tokens", 0, 0, 24), + ("yarn_ext_factor", "", "--yarn-ext-factor", "YaRN外推混合因子", "common", "number", "-1.0", None, -1, 1, 0.1, None, 0, 0, 25), + ("yarn_attn_factor", "", "--yarn-attn-factor", "YaRN注意力缩放因子", "common", "number", "-1.0", None, -1, 10, 0.1, None, 0, 0, 26), + ("yarn_beta_slow", "", "--yarn-beta-slow", "YaRN高修正维度", "common", "number", "-1.0", None, -1, 10, 0.1, None, 0, 0, 27), + ("yarn_beta_fast", "", "--yarn-beta-fast", "YaRN低修正维度", "common", "number", "-1.0", None, -1, 10, 0.1, None, 0, 0, 28), + ("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), + ("top_k", "", "--top-k", "Top-K采样", "sampling", "number", "40", None, 0, 200, 1, None, 1, 0, 3), + ("top_p", "", "--top-p", "Top-P (核) 采样", "sampling", "number", "0.95", None, 0.0, 1.0, 0.05, None, 1, 0, 4), + ("min_p", "", "--min-p", "Min-P采样", "sampling", "number", "0.05", None, 0.0, 1.0, 0.01, None, 0, 0, 5), + ("typical", "", "--typical", "局部典型采样", "sampling", "number", "1.0", None, 0.0, 1.0, 0.05, None, 0, 0, 6), + ("repeat_penalty", "", "--repeat-penalty", "重复惩罚", "sampling", "number", "1.0", None, 0.5, 2.0, 0.05, None, 0, 0, 7), + ("repeat_last_n", "", "--repeat-last-n", "惩罚考虑的最后n个token", "sampling", "number", "64", None, 0, 999999, 1, "tokens", 0, 0, 8), + ("presence_penalty", "", "--presence-penalty", "存在惩罚", "sampling", "number", "0.0", None, -2.0, 2.0, 0.1, None, 0, 0, 9), + ("frequency_penalty", "", "--frequency-penalty", "频率惩罚", "sampling", "number", "0.0", None, -2.0, 2.0, 0.1, None, 0, 0, 10), + ("mirostat", "", "--mirostat", "Mirostat采样模式", "sampling", "select", "0", json.dumps(["0", "1", "2"]), None, None, None, None, 0, 0, 11), + ("mirostat_lr", "", "--mirostat-lr", "Mirostat学习率", "sampling", "number", "0.1", None, 0.01, 1.0, 0.01, None, 0, 0, 12), + ("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), + ("parallel", "-np", "--parallel", "服务器槽位数", "server", "number", "-1", None, -1, 64, 1, "slots", 0, 1, 3), + ("cont_batching", "-cb", "--cont-batching", "连续批处理", "server", "boolean", "true", None, None, None, None, None, 0, 0, 4), + ("context_shift", "", "--context-shift", "上下文移位", "server", "boolean", "false", None, None, None, None, None, 0, 0, 5), + ("special", "-sp", "--special", "特殊token输出", "server", "boolean", "false", None, None, None, None, None, 0, 0, 6), + ("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), + ("cpu_strict", "", "--cpu-strict", "严格CPU放置", "advanced", "boolean", "false", None, None, None, None, None, 0, 0, 3), + ("prio", "", "--prio", "进程/线程优先级", "advanced", "number", "0", None, -1, 3, 1, None, 0, 0, 4), + ("poll", "", "--poll", "轮询级别", "advanced", "number", "50", None, 0, 100, 1, None, 0, 0, 5), + ("threads_batch", "-tb", "--threads-batch", "批处理和提示处理线程数", "advanced", "number", "-1", None, -1, 128, 1, None, 0, 0, 6), + ("rope_scale", "", "--rope-scale", "RoPE上下文缩放因子", "advanced", "number", "1.0", None, 0.1, 10, 0.1, None, 0, 0, 7), + ("op_offload", "", "--op-offload", "卸载主机张量操作到设备", "advanced", "boolean", "true", None, None, None, None, None, 0, 0, 8), + ("check_tensors", "", "--check-tensors", "检查模型张量数据", "advanced", "boolean", "false", None, None, None, None, None, 0, 0, 9), + ("override_kv", "", "--override-kv", "覆盖模型元数据", "advanced", "string", "", None, None, None, None, None, 0, 0, 10), + ] + + 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', + (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 = [(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), + ("cache_ram", "-cram", "--cache-ram", "最大缓存大小(MiB)", "server", "number", "8192", None, -1, 999999, 512, "MiB", 0, 1, 10), + ("kv_unified", "-kvu", "--kv-unified", "使用统一KV缓冲区", "server", "boolean", "false", None, None, None, None, None, 0, 1, 11), + ("cache_idle_slots", "", "--cache-idle-slots", "缓存空闲槽位", "server", "boolean", "true", None, None, None, None, None, 0, 0, 12), + ("ctx_checkpoints", "-ctxcp", "--ctx-checkpoints", "每槽最大上下文检查点数", "server", "number", "32", None, 1, 999, 1, None, 0, 0, 13), + ("checkpoint_min_step", "-cms", "--checkpoint-min-step", "检查点最小间距", "server", "number", "8192", None, 0, 999999, 512, "tokens", 0, 0, 14), + ("swa_full", "", "--swa-full", "使用全尺寸SWA缓存", "common", "boolean", "false", None, None, None, None, None, 0, 1, 32), + ("perf", "", "--perf", "启用性能计时", "common", "boolean", "false", None, None, None, None, None, 0, 0, 33), + ("repack", "", "--repack", "权重重打包", "common", "boolean", "true", None, None, None, None, None, 0, 0, 34), + ("no_host", "", "--no-host", "绕过主机缓冲区", "common", "boolean", "false", None, None, None, None, None, 0, 0, 35), + ("n_cpu_moe", "-ncmoe", "--n-cpu-moe", "前N层MoE权重保留在CPU", "common", "number", "0", None, 0, 999, 1, "layers", 0, 0, 36), + ("direct_io", "-dio", "--direct-io", "使用DirectIO", "common", "boolean", "false", None, None, None, None, None, 0, 0, 37), + ("offline", "", "--offline", "离线模式", "common", "boolean", "false", None, None, None, None, None, 0, 0, 38), + ("spec_draft_cache_type_k", "-ctkd", "--cache-type-k-draft", "草稿模型KV缓存K类型", "advanced", "select", "f16", json.dumps(["f32", "f16", "bf16", "q8_0", "q4_0", "q4_1", "iq4_nl", "q5_0", "q5_1"]), None, None, None, None, 0, 0, 11), + ("spec_draft_cache_type_v", "-ctvd", "--cache-type-v-draft", "草稿模型KV缓存V类型", "advanced", "select", "f16", json.dumps(["f32", "f16", "bf16", "q8_0", "q4_0", "q4_1", "iq4_nl", "q5_0", "q5_1"]), None, None, None, None, 0, 0, 12), + ("adaptive_target", "", "--adaptive-target", "adaptive-p目标概率", "sampling", "number", "-1.0", None, -1.0, 1.0, 0.05, None, 0, 0, 16), + ("adaptive_decay", "", "--adaptive-decay", "adaptive-p衰减率", "sampling", "number", "0.9", None, 0.0, 0.99, 0.01, None, 0, 0, 17), + ("dynatemp_range", "", "--dynatemp-range", "动态温度范围", "sampling", "number", "0.0", None, 0.0, 2.0, 0.05, None, 0, 0, 18), + ("dynatemp_exp", "", "--dynatemp-exp", "动态温度指数", "sampling", "number", "1.0", None, 0.1, 5.0, 0.1, None, 0, 0, 19), + ("top_n_sigma", "--top-nsigma", "--top-n-sigma", "Top-n-sigma采样", "sampling", "number", "-1.0", None, -1.0, 10.0, 0.1, None, 0, 0, 20), + ("xtc_probability", "", "--xtc-probability", "XTC概率", "sampling", "number", "0.0", None, 0.0, 1.0, 0.01, None, 0, 0, 21), + ("xtc_threshold", "", "--xtc-threshold", "XTC阈值", "sampling", "number", "0.1", None, 0.0, 1.0, 0.01, None, 0, 0, 22), + ("dry_multiplier", "", "--dry-multiplier", "DRY采样乘数", "sampling", "number", "0.0", None, 0.0, 5.0, 0.1, None, 0, 0, 23), + ("dry_base", "", "--dry-base", "DRY采样基础值", "sampling", "number", "1.75", None, 1.0, 3.0, 0.05, None, 0, 0, 24), + ("dry_allowed_length", "", "--dry-allowed-length", "DRY允许长度", "sampling", "number", "2", None, 1, 20, 1, None, 0, 0, 25), + ("dry_penalty_last_n", "", "--dry-penalty-last-n", "DRY惩罚最后n个token", "sampling", "number", "-1", None, -1, 999999, 1, "tokens", 0, 0, 26), + ("sampler_seq", "", "--sampler-seq", "简化采样器序列", "sampling", "string", "edskypmxt", None, None, None, None, None, 0, 0, 27), + ("backend_sampling", "-bs", "--backend-sampling", "后端采样(实验性)", "sampling", "boolean", "false", None, None, None, None, None, 0, 0, 28), + ("hf_repo_v", "-hfv", "--hf-repo-v", "vocoder模型HF仓库", "model_source", "string", "", None, None, None, None, None, 0, 0, 6), + ("hf_file_v", "-hffv", "--hf-file-v", "vocoder模型HF文件", "model_source", "string", "", None, None, None, None, None, 0, 0, 7), + ] + + 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', + (v2_id,) + p) + + # ===== Default settings ===== + c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("admin_password", "admin123")) + c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", ("default_gpu", "RTX 3090")) + 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")) + + conn.commit() + + +if __name__ == '__main__': + init_db() + print(f"Database initialized at {DB_PATH}") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..01e9b57 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask==3.1.0 +flask-cors==5.0.1 diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..b470a06 --- /dev/null +++ b/start.sh @@ -0,0 +1,11 @@ +#!/bin/bash +cd "$(dirname "$0")" + +# Install dependencies +pip install flask flask-cors -q 2>/dev/null + +# Initialize database +python3 db.py + +# Start server +exec python3 app.py diff --git a/static/admin.html b/static/admin.html new file mode 100644 index 0000000..2dbec41 --- /dev/null +++ b/static/admin.html @@ -0,0 +1,155 @@ + + + + + + llama.cpp 命令生成器 - 后台管理 + + + +
+
+

⚙️ 后台管理

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

GPU 管理

+
+

添加新 GPU

+
+ + + + + + +
+
+ + + + + + + + + + + + + +
ID名称显存(MB)计算能力描述排序操作
+
+ + +
+

llama.cpp 版本管理

+
+

添加新版本

+
+ + + + + +
+
+ + + + + + + + + + + + + +
ID版本描述发布日期活跃排序操作
+
+ + +
+

参数管理

+
+ + +
+
+

添加新参数

+
+ + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + +
ID标志描述分类类型默认值重要影响显存操作
+
+ + +
+

系统设置

+
+ +
+
+ + + + diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..e9468d0 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,774 @@ +/* ===== Base Styles ===== */ +:root { + --bg: #1a1a2e; + --bg-panel: #16213e; + --bg-input: #0f3460; + --bg-hover: #1a3a6b; + --text: #e0e0e0; + --text-dim: #8892b0; + --accent: #e94560; + --accent-hover: #ff6b6b; + --accent2: #4ecca3; + --border: #2a3a5c; + --shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + --radius: 8px; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.6; + min-height: 100vh; +} + +#app, #admin-app { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +/* ===== Header ===== */ +header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 0; + border-bottom: 1px solid var(--border); + margin-bottom: 20px; +} + +header h1 { + font-size: 1.8em; + background: linear-gradient(135deg, var(--accent), var(--accent2)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.header-right { + display: flex; + gap: 12px; +} + +.admin-link { + color: var(--accent2); + text-decoration: none; + padding: 8px 16px; + border: 1px solid var(--accent2); + border-radius: var(--radius); + transition: all 0.2s; + font-size: 0.9em; +} + +.admin-link:hover { + background: var(--accent2); + color: var(--bg); +} + +/* ===== Top Controls ===== */ +.top-controls { + display: flex; + gap: 20px; + flex-wrap: wrap; + margin-bottom: 20px; +} + +.control-group { + display: flex; + flex-direction: column; + gap: 6px; + flex: 1; + min-width: 200px; +} + +.control-group label { + font-size: 0.85em; + color: var(--text-dim); + font-weight: 500; +} + +.control-group input, +.control-group select { + background: var(--bg-input); + border: 1px solid var(--border); + color: var(--text); + padding: 8px 12px; + border-radius: var(--radius); + font-size: 0.95em; + transition: border-color 0.2s; +} + +.control-group input:focus, +.control-group select:focus { + outline: none; + border-color: var(--accent); +} + +/* ===== Mode Switch ===== */ +.mode-switch { + display: flex; + gap: 0; + flex: 1; +} + +.mode-btn { + flex: 1; + padding: 8px 16px; + border: 1px solid var(--border); + background: var(--bg-input); + color: var(--text-dim); + cursor: pointer; + transition: all 0.2s; + font-size: 0.9em; +} + +.mode-btn:first-child { + border-radius: var(--radius) 0 0 var(--radius); +} + +.mode-btn:last-child { + border-radius: 0 var(--radius) var(--radius) 0; + border-left: none; +} + +.mode-btn.active { + background: var(--accent); + color: white; + border-color: var(--accent); +} + +/* ===== Panels ===== */ +.panel { + background: var(--bg-panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 20px; + margin-bottom: 20px; + box-shadow: var(--shadow); +} + +.panel h2 { + font-size: 1.2em; + margin-bottom: 16px; + color: var(--accent2); + border-bottom: 1px solid var(--border); + padding-bottom: 8px; +} + +.hidden { + display: none !important; +} + +/* ===== GPU Slots ===== */ +.gpu-slot { + display: flex; + gap: 12px; + align-items: center; + margin-bottom: 10px; + padding: 10px; + background: var(--bg-input); + border-radius: var(--radius); + border: 1px solid var(--border); +} + +.gpu-slot .gpu-index { + font-weight: bold; + color: var(--accent); + min-width: 30px; +} + +.gpu-slot select { + flex: 1; + background: var(--bg); + border: 1px solid var(--border); + color: var(--text); + padding: 6px 10px; + border-radius: 4px; +} + +.gpu-slot .vram-info { + color: var(--accent2); + font-size: 0.9em; + min-width: 120px; +} + +.gpu-slot .btn-remove { + background: var(--accent); + color: white; + border: none; + width: 28px; + height: 28px; + border-radius: 4px; + cursor: pointer; + font-size: 1.1em; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s; +} + +.gpu-slot .btn-remove:hover { + background: var(--accent-hover); +} + +.btn-add { + background: transparent; + border: 1px dashed var(--border); + color: var(--text-dim); + padding: 8px 16px; + border-radius: var(--radius); + cursor: pointer; + transition: all 0.2s; + width: 100%; + margin-top: 8px; +} + +.btn-add:hover { + border-color: var(--accent2); + color: var(--accent2); +} + +/* ===== VRAM Display ===== */ +.vram-display { + margin-top: 16px; + padding: 12px; + background: var(--bg-input); + border-radius: var(--radius); +} + +.vram-bar-container { + position: relative; + height: 30px; + background: var(--bg); + border-radius: 4px; + overflow: hidden; + border: 1px solid var(--border); +} + +.vram-bar { + height: 100%; + background: linear-gradient(90deg, #4ecca3, #e9c46a); + border-radius: 3px; + transition: width 0.3s; + width: 0%; +} + +.vram-bar.warning { + background: linear-gradient(90deg, #e9c46a, #f4a261); +} + +.vram-bar.danger { + background: linear-gradient(90deg, #f4a261, #e94560); +} + +.ram-bar { + background: linear-gradient(90deg, #4e9cca, #4ecca3); +} + +.vram-label { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 0.85em; + font-weight: 600; + color: white; + text-shadow: 1px 1px 2px rgba(0,0,0,0.8); + white-space: nowrap; +} + +.vram-breakdown { + margin-top: 10px; + font-size: 0.85em; + color: var(--text-dim); + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 6px; +} + +.vram-breakdown .breakdown-item { + display: flex; + justify-content: space-between; + padding: 4px 8px; + background: var(--bg); + border-radius: 4px; +} + +.vram-breakdown .breakdown-item .label { + color: var(--text-dim); +} + +.vram-breakdown .breakdown-item .value { + color: var(--text); + font-weight: 600; +} + +.vram-breakdown .breakdown-item.warning .value { + color: var(--accent); +} + +/* ===== Model Info ===== */ +.model-info-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 12px; +} + +.model-presets { + margin-top: 12px; + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; +} + +.preset-label { + font-size: 0.85em; + color: var(--text-dim); +} + +.preset-btn { + padding: 4px 12px; + background: var(--bg-input); + border: 1px solid var(--border); + color: var(--text); + border-radius: 4px; + cursor: pointer; + font-size: 0.85em; + transition: all 0.2s; +} + +.preset-btn:hover { + border-color: var(--accent2); + color: var(--accent2); +} + +/* ===== Natural Language ===== */ +.nl-input-container { + display: flex; + gap: 8px; +} + +.nl-input-container input { + flex: 1; + background: var(--bg-input); + border: 1px solid var(--border); + color: var(--text); + padding: 10px 14px; + border-radius: var(--radius); + font-size: 0.95em; +} + +.nl-input-container button { + padding: 10px 24px; + background: var(--accent2); + color: var(--bg); + border: none; + border-radius: var(--radius); + cursor: pointer; + font-weight: 600; + transition: background 0.2s; +} + +.nl-input-container button:hover { + opacity: 0.85; +} + +.nl-result { + margin-top: 10px; + font-size: 0.9em; + color: var(--text-dim); +} + +.nl-result .parsed-param { + display: inline-block; + margin: 2px 4px; + padding: 2px 8px; + background: var(--bg-input); + border-radius: 4px; + border: 1px solid var(--border); +} + +.nl-result .parsed-param .key { + color: var(--accent2); +} + +.nl-result .parsed-param .value { + color: var(--accent); +} + +/* ===== Parameter Tabs ===== */ +.param-tabs { + display: flex; + gap: 4px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.tab-btn { + padding: 6px 14px; + background: var(--bg-input); + border: 1px solid var(--border); + color: var(--text-dim); + border-radius: var(--radius); + cursor: pointer; + font-size: 0.85em; + transition: all 0.2s; +} + +.tab-btn.active { + background: var(--accent); + color: white; + border-color: var(--accent); +} + +/* ===== Parameter Items ===== */ +#param-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 12px; +} + +.param-item { + display: flex; + flex-direction: column; + gap: 4px; + padding: 10px; + background: var(--bg-input); + border-radius: var(--radius); + border: 1px solid var(--border); + transition: border-color 0.2s; +} + +.param-item:hover { + border-color: var(--accent2); +} + +.param-item.modified { + border-color: var(--accent); + background: rgba(233, 69, 96, 0.05); +} + +.param-item.hidden-param { + display: none; +} + +.param-item .param-label { + font-size: 0.85em; + color: var(--text); + font-weight: 500; + display: flex; + align-items: center; + gap: 6px; +} + +.param-item .param-flag { + font-family: monospace; + font-size: 0.8em; + color: var(--accent2); + background: var(--bg); + padding: 1px 4px; + border-radius: 3px; +} + +.param-item .param-desc { + font-size: 0.75em; + color: var(--text-dim); + margin-bottom: 4px; +} + +.param-item input, +.param-item select { + background: var(--bg); + border: 1px solid var(--border); + color: var(--text); + padding: 6px 8px; + border-radius: 4px; + font-size: 0.9em; + width: 100%; +} + +.param-item input:focus, +.param-item select:focus { + outline: none; + border-color: var(--accent); +} + +.param-item .param-checkbox { + display: flex; + align-items: center; + gap: 6px; +} + +.param-item .param-checkbox input { + width: auto; + cursor: pointer; +} + +/* ===== Toggle Advanced ===== */ +.btn-toggle-advanced { + margin-top: 12px; + padding: 8px 16px; + background: transparent; + border: 1px solid var(--border); + color: var(--text-dim); + border-radius: var(--radius); + cursor: pointer; + font-size: 0.85em; + transition: all 0.2s; + width: 100%; +} + +.btn-toggle-advanced:hover { + border-color: var(--accent); + color: var(--accent); +} + +/* ===== Command Output ===== */ +.command-panel { + position: sticky; + bottom: 20px; +} + +.command-output { + background: #0d1117; + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px; + font-family: 'Cascadia Code', 'Fira Code', monospace; + font-size: 0.9em; + color: #4ecca3; + word-break: break-all; + min-height: 60px; + white-space: pre-wrap; + line-height: 1.5; +} + +.command-actions { + margin-top: 12px; + display: flex; + gap: 8px; +} + +.btn-copy, .btn-gen { + padding: 8px 20px; + border: none; + border-radius: var(--radius); + cursor: pointer; + font-size: 0.9em; + font-weight: 500; + transition: all 0.2s; +} + +.btn-copy { + background: var(--accent2); + color: var(--bg); +} + +.btn-copy:hover { + opacity: 0.85; +} + +.btn-gen { + background: var(--bg-input); + color: var(--text); + border: 1px solid var(--border); +} + +.btn-gen:hover { + border-color: var(--accent); +} + +/* ===== Admin ===== */ +.admin-tabs { + display: flex; + gap: 4px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.admin-tab { + padding: 8px 20px; + background: var(--bg-input); + border: 1px solid var(--border); + color: var(--text-dim); + border-radius: var(--radius); + cursor: pointer; + transition: all 0.2s; +} + +.admin-tab.active { + background: var(--accent); + color: white; + border-color: var(--accent); +} + +.admin-section { + display: none; +} + +.admin-section.active { + display: block; +} + +.admin-add-form { + background: var(--bg-input); + border-radius: var(--radius); + padding: 16px; + margin-bottom: 20px; + border: 1px solid var(--border); +} + +.admin-add-form h3 { + margin-bottom: 12px; + color: var(--accent2); + font-size: 1em; +} + +.form-row { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.form-row input { + flex: 1; + min-width: 120px; + padding: 6px 10px; + background: var(--bg); + border: 1px solid var(--border); + color: var(--text); + border-radius: 4px; +} + +.form-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 8px; +} + +.form-grid input, +.form-grid select { + padding: 6px 10px; + background: var(--bg); + border: 1px solid var(--border); + color: var(--text); + border-radius: 4px; +} + +.btn-primary { + padding: 8px 20px; + background: var(--accent); + color: white; + border: none; + border-radius: var(--radius); + cursor: pointer; + font-weight: 500; + transition: all 0.2s; + margin-top: 8px; +} + +.btn-primary:hover { + background: var(--accent-hover); +} + +.admin-table { + width: 100%; + border-collapse: collapse; + font-size: 0.85em; +} + +.admin-table th, +.admin-table td { + padding: 8px 10px; + text-align: left; + border-bottom: 1px solid var(--border); +} + +.admin-table th { + color: var(--accent2); + font-weight: 600; +} + +.admin-table tr:hover { + background: var(--bg-input); +} + +.admin-table .btn-action { + padding: 4px 10px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.85em; + margin-right: 4px; +} + +.btn-edit { + background: var(--accent2); + color: var(--bg); +} + +.btn-delete { + background: var(--accent); + color: white; +} + +/* Editable inputs in tables */ +.admin-table input, +.admin-table select { + background: var(--bg); + border: 1px solid var(--border); + color: var(--text); + padding: 4px 6px; + border-radius: 3px; + font-size: 0.95em; + width: 100%; +} + +.settings-form { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 12px; + margin-bottom: 16px; +} + +.settings-form .setting-item { + display: flex; + flex-direction: column; + gap: 4px; +} + +.settings-form .setting-item label { + font-size: 0.85em; + color: var(--text-dim); +} + +.settings-form .setting-item input { + background: var(--bg-input); + border: 1px solid var(--border); + color: var(--text); + padding: 8px 12px; + border-radius: var(--radius); +} + +/* ===== Responsive ===== */ +@media (max-width: 768px) { + .top-controls { + flex-direction: column; + } + + #param-container { + grid-template-columns: 1fr; + } + + .form-row { + flex-direction: column; + } +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..8fc1e1a --- /dev/null +++ b/static/index.html @@ -0,0 +1,156 @@ + + + + + + llama.cpp 命令生成器 + + + +
+
+

🦙 llama.cpp 命令生成器

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

🖥️ GPU 配置

+
+ + + +
+
+
+
VRAM: 等待配置...
+
+
+
+
+ + + + + +
+

📦 模型信息 (用于显存估算)

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ 快速预设: + + + + + + +
+
+ + +
+

💬 自然语言生成

+
+ + +
+
+
+ + +
+

⚙️ 参数配置

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

📋 生成命令

+
请在上方配置参数...
+
+ + +
+
+
+ + + + diff --git a/static/js/admin.js b/static/js/admin.js new file mode 100644 index 0000000..a7341c5 --- /dev/null +++ b/static/js/admin.js @@ -0,0 +1,394 @@ +// ===== Admin State ===== +let adminState = { + gpus: [], + versions: [], + params: [], + currentVersionId: null, + settings: {}, +}; + +// ===== Init ===== +async function adminInit() { + await loadAdminGpus(); + await loadAdminVersions(); + await loadAdminSettings(); +} + +// ===== Tab Switching ===== +function adminSwitchTab(tab) { + document.querySelectorAll('.admin-tab').forEach(t => { + t.classList.toggle('active', t.dataset.tab === tab); + }); + document.querySelectorAll('.admin-section').forEach(s => { + s.classList.toggle('active', s.id === 'admin-' + tab); + }); +} + +// ===== GPU Management ===== +async function loadAdminGpus() { + const res = await fetch('/api/admin/gpus'); + adminState.gpus = await res.json(); + renderGpuTable(); +} + +function renderGpuTable() { + const tbody = document.getElementById('gpu-table-body'); + tbody.innerHTML = adminState.gpus.map(g => ` + + ${g.id} + + + + + + + + `).join(''); +} + +async function addGpu() { + const data = { + name: document.getElementById('gpu-name').value, + vram_mb: parseInt(document.getElementById('gpu-vram').value) || 0, + compute_capability: document.getElementById('gpu-cc').value, + description: document.getElementById('gpu-desc').value, + sort_order: parseInt(document.getElementById('gpu-order').value) || 0, + }; + if (!data.name || !data.vram_mb) { + alert('请填写GPU名称和显存大小'); + return; + } + await fetch('/api/admin/gpus', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + // Clear inputs + document.getElementById('gpu-name').value = ''; + document.getElementById('gpu-vram').value = ''; + document.getElementById('gpu-cc').value = ''; + document.getElementById('gpu-desc').value = ''; + document.getElementById('gpu-order').value = '0'; + await loadAdminGpus(); +} + +async function updateGpu(id, field, value) { + const gpu = adminState.gpus.find(g => g.id === id); + if (!gpu) return; + const data = { + name: gpu.name, + vram_mb: gpu.vram_mb, + compute_capability: gpu.compute_capability, + description: gpu.description, + sort_order: gpu.sort_order, + }; + if (field === 'vram_mb' || field === 'sort_order') { + data[field] = parseInt(value) || 0; + } else { + data[field] = value; + } + await fetch(`/api/admin/gpus/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + await loadAdminGpus(); +} + +async function deleteGpu(id) { + if (!confirm('确定删除这个GPU吗?')) return; + await fetch(`/api/admin/gpus/${id}`, { method: 'DELETE' }); + await loadAdminGpus(); +} + +// ===== Version Management ===== +async function loadAdminVersions() { + const res = await fetch('/api/admin/versions'); + adminState.versions = await res.json(); + renderVersionTable(); + // Populate version select in params tab + const sel = document.getElementById('admin-param-version'); + sel.innerHTML = adminState.versions.map(v => ``).join(''); + if (adminState.versions.length > 0) { + adminState.currentVersionId = adminState.versions[0].id; + sel.value = adminState.currentVersionId; + await loadAdminParams(); + } +} + +function renderVersionTable() { + const tbody = document.getElementById('version-table-body'); + tbody.innerHTML = adminState.versions.map(v => ` + + ${v.id} + + + + + + + + + + `).join(''); +} + +async function addVersion() { + const data = { + version_tag: document.getElementById('ver-tag').value, + description: document.getElementById('ver-desc').value, + release_date: document.getElementById('ver-date').value, + sort_order: parseInt(document.getElementById('ver-order').value) || 0, + }; + if (!data.version_tag) { + alert('请填写版本标签'); + return; + } + await fetch('/api/admin/versions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + document.getElementById('ver-tag').value = ''; + document.getElementById('ver-desc').value = ''; + document.getElementById('ver-date').value = ''; + document.getElementById('ver-order').value = '0'; + await loadAdminVersions(); +} + +async function updateVersion(id, field, value) { + const ver = adminState.versions.find(v => v.id === id); + if (!ver) return; + const data = { + version_tag: ver.version_tag, + description: ver.description, + release_date: ver.release_date, + is_active: ver.is_active, + sort_order: ver.sort_order, + }; + if (field === 'is_active' || field === 'sort_order') { + data[field] = parseInt(value); + } else { + data[field] = value; + } + await fetch(`/api/admin/versions/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + await loadAdminVersions(); +} + +async function deleteVersion(id) { + 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'); + adminState.currentVersionId = parseInt(sel.value); + if (!adminState.currentVersionId) return; + const res = await fetch(`/api/admin/versions/${adminState.currentVersionId}/params`); + adminState.params = await res.json(); + renderParamTable(); +} + +function renderParamTable() { + const tbody = document.getElementById('param-table-body'); + tbody.innerHTML = adminState.params.map(p => { + const options = Array.isArray(p.options) ? p.options.join(', ') : (p.options || ''); + const flag = p.short_flag ? `${p.short_flag}/${p.long_flag}` : p.long_flag; + return ` + + ${p.id} + + + + + + + + + + + + + + + + + + + `; + }).join(''); +} + +async function addParam() { + const optionsStr = document.getElementById('param-options').value; + const options = optionsStr ? optionsStr.split(',').map(s => s.trim()).filter(s => s) : null; + + 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, + category: document.getElementById('param-category').value, + param_type: document.getElementById('param-type').value, + default_value: document.getElementById('param-default').value, + options: options, + 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, + 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) + }); + + // Clear inputs + ['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'; + + 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, + }; + + if (field === 'is_important' || field === 'affects_vram' || field === 'sort_order') { + data[field] = parseInt(value); + } else { + data[field] = value; + } + + await fetch(`/api/admin/params/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + await loadAdminParams(); +} + +async function updateParamFlag(id, value) { + const p = adminState.params.find(p => p.id === id); + if (!p) return; + // Parse "short/long" format + 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, + }; + await fetch(`/api/admin/params/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + await loadAdminParams(); +} + +async function deleteParam(id) { + if (!confirm('确定删除这个参数吗?')) return; + await fetch(`/api/admin/params/${id}`, { method: 'DELETE' }); + await loadAdminParams(); +} + +// ===== Settings ===== +async function loadAdminSettings() { + const res = await fetch('/api/admin/settings'); + adminState.settings = await res.json(); + renderSettings(); +} + +function renderSettings() { + const container = document.getElementById('settings-form'); + container.innerHTML = Object.entries(adminState.settings).map(([key, value]) => ` +
+ + +
+ `).join(''); +} + +async function saveSettings() { + await fetch('/api/admin/settings', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(adminState.settings) + }); + alert('设置已保存'); +} + +// ===== Init ===== +window.addEventListener('DOMContentLoaded', adminInit); diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..976e732 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,501 @@ +// ===== State ===== +let state = { + versions: [], + gpus: [], + currentVersionId: null, + params: [], // param definitions for current version + paramValues: {}, // user-selected values + mode: 'gpu', + gpuSlots: [], // [{name, vram_mb, ...}] + showHidden: false, + currentTab: 'common', + binary: 'llama-server', +}; + +// ===== Init ===== +async function init() { + await loadVersions(); + await loadGpus(); + // Set default version + const defaultVer = state.versions.find(v => v.version_tag === 'b10068') || state.versions[0]; + if (defaultVer) { + state.currentVersionId = defaultVer.id; + document.getElementById('version-select').value = defaultVer.id; + await loadParams(defaultVer.id); + } + // Default GPU: RTX 3090 + const defaultGpu = state.gpus.find(g => g.name === 'RTX 3090') || state.gpus[0]; + if (defaultGpu) { + state.gpuSlots = [{ ...defaultGpu }]; + renderGpuSlots(); + } + renderParams(); + generateCommand(); + updateEstimate(); +} + +// ===== Load Data ===== +async function loadVersions() { + const res = await fetch('/api/versions'); + state.versions = await res.json(); + const sel = document.getElementById('version-select'); + sel.innerHTML = state.versions.map(v => ``).join(''); +} + +async function loadGpus() { + const res = await fetch('/api/gpus'); + state.gpus = await res.json(); +} + +async function loadParams(versionId) { + const res = await fetch(`/api/versions/${versionId}/params`); + state.params = await res.json(); + // Initialize param values with defaults + state.paramValues = {}; + state.params.forEach(p => { + if (p.param_type === 'number') { + state.paramValues[p.param_key] = p.default_value; + } else if (p.param_type === 'boolean') { + state.paramValues[p.param_key] = p.default_value === 'true'; + } else { + state.paramValues[p.param_key] = p.default_value || ''; + } + }); + renderParams(); + generateCommand(); + updateEstimate(); +} + +// ===== Version Change ===== +async function onVersionChange() { + const sel = document.getElementById('version-select'); + state.currentVersionId = parseInt(sel.value); + await loadParams(state.currentVersionId); +} + +// ===== Mode Switch ===== +function switchMode(mode) { + state.mode = mode; + document.querySelectorAll('.mode-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.mode === mode); + }); + + const memPanel = document.getElementById('memory-panel'); + const gpuPanel = document.getElementById('gpu-panel'); + + if (mode === 'gpu_cpu') { + memPanel.classList.remove('hidden'); + } else { + memPanel.classList.add('hidden'); + } + + generateCommand(); + updateEstimate(); +} + +// ===== GPU Slots ===== +function renderGpuSlots() { + const container = document.getElementById('gpu-slots'); + container.innerHTML = state.gpuSlots.map((gpu, i) => { + const options = state.gpus.map(g => + `` + ).join(''); + return ` +
+ GPU ${i + 1} + + 显存: ${(gpu.vram_mb / 1024).toFixed(1)} GB + ${state.gpuSlots.length > 1 ? `` : ''} +
+ `; + }).join(''); + + const addBtn = document.getElementById('btn-add-gpu'); + addBtn.style.display = state.gpuSlots.length >= 4 ? 'none' : 'block'; + + updateEstimate(); +} + +function addGpuSlot() { + if (state.gpuSlots.length >= 4) return; + const defaultGpu = state.gpus.find(g => g.name === 'RTX 3090') || state.gpus[0]; + state.gpuSlots.push({ ...defaultGpu }); + renderGpuSlots(); + generateCommand(); +} + +function removeGpuSlot(index) { + state.gpuSlots.splice(index, 1); + renderGpuSlots(); + generateCommand(); +} + +function updateGpuSlot(index, name) { + const gpu = state.gpus.find(g => g.name === name); + if (gpu) { + state.gpuSlots[index] = { ...gpu }; + } + renderGpuSlots(); + generateCommand(); +} + +// ===== Parameter Rendering ===== +function switchTab(cat) { + state.currentTab = cat; + document.querySelectorAll('.tab-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.cat === cat); + }); + renderParams(); +} + +function renderParams() { + const container = document.getElementById('param-container'); + const params = state.params.filter(p => p.category === state.currentTab); + + const importantParams = params.filter(p => p.is_important === 1); + const otherParams = params.filter(p => p.is_important === 0); + + let html = ''; + + // Important params always visible + importantParams.forEach(p => html += renderParamItem(p, false)); + + // Other params + otherParams.forEach(p => { + const hidden = !state.showHidden; + html += renderParamItem(p, hidden); + }); + + container.innerHTML = html; + + // Show/hide toggle button + const toggleBtn = document.getElementById('toggle-advanced-btn'); + if (otherParams.length > 0) { + toggleBtn.style.display = 'block'; + toggleBtn.textContent = state.showHidden ? '▲ 收起更多参数' : '▼ 显示更多参数'; + } else { + toggleBtn.style.display = 'none'; + } +} + +function renderParamItem(p, hidden) { + const val = state.paramValues[p.param_key]; + const defaultVal = p.default_value; + const isModified = isParamModified(p, val); + const hiddenClass = hidden ? 'hidden-param' : ''; + const modifiedClass = isModified ? 'modified' : ''; + + let inputHtml = ''; + if (p.param_type === 'boolean') { + inputHtml = ` +
+ + ${val ? '开启' : '关闭'} +
+ `; + } else if (p.param_type === 'select') { + const options = Array.isArray(p.options) ? p.options : []; + inputHtml = ` + + `; + } else if (p.param_type === 'number') { + const step = p.step || 'any'; + const min = p.min_value !== null ? `min="${p.min_value}"` : ''; + const max = p.max_value !== null ? `max="${p.max_value}"` : ''; + inputHtml = ` + + `; + } else { + inputHtml = ` + + `; + } + + const unitHtml = p.unit ? `${p.unit}` : ''; + const vramBadge = p.affects_vram ? '⚡显存' : ''; + + return ` +
+
+ ${p.short_flag || p.long_flag} + ${vramBadge} + ${p.description} +
+
${p.description}
+ ${inputHtml} + ${unitHtml} +
+ `; +} + +function isParamModified(p, val) { + const defaultVal = p.default_value; + if (p.param_type === 'boolean') { + return (val === true || val === 'true') !== (defaultVal === 'true'); + } + return String(val) !== String(defaultVal); +} + +function setParam(key, value) { + state.paramValues[key] = value; + renderParams(); + generateCommand(); + updateEstimate(); +} + +let inputTimer = null; +function onParamInput(key, value) { + state.paramValues[key] = value; + // Debounce update + if (inputTimer) clearTimeout(inputTimer); + inputTimer = setTimeout(() => { + renderParams(); + generateCommand(); + updateEstimate(); + }, 300); +} + +function toggleHiddenParams() { + state.showHidden = !state.showHidden; + renderParams(); +} + +// ===== Model Presets ===== +function setModelPreset(type) { + const presets = { + '7b': { size: 4.1, layers: 32, embd: 4096, kv_heads: 32, head_dim: 128, heads: 32 }, + '13b': { size: 7.4, layers: 40, embd: 5120, kv_heads: 40, head_dim: 128, heads: 40 }, + '34b': { size: 19.5, layers: 48, embd: 8192, kv_heads: 8, head_dim: 128, heads: 64 }, + '70b': { size: 38.5, layers: 80, embd: 8192, kv_heads: 8, head_dim: 128, heads: 64 }, + '70b-q8': { size: 74.0, layers: 80, embd: 8192, kv_heads: 8, head_dim: 128, heads: 64 }, + '70b-fp16': { size: 138.0, layers: 80, embd: 8192, kv_heads: 8, head_dim: 128, heads: 64 }, + }; + const p = presets[type]; + if (p) { + document.getElementById('model-size-gb').value = p.size; + document.getElementById('model-layers').value = p.layers; + document.getElementById('model-embd').value = p.embd; + document.getElementById('model-kv-heads').value = p.kv_heads; + document.getElementById('model-head-dim').value = p.head_dim; + document.getElementById('model-heads').value = p.heads; + updateEstimate(); + } +} + +// ===== VRAM Estimation ===== +async function updateEstimate() { + const params = collectParamsForEstimate(); + const gpuSelections = state.gpuSlots.map(s => ({ name: s.name, vram_mb: s.vram_mb })); + + const sysMemory = parseFloat(document.getElementById('sys-memory').value) || 0; + const sysMemUnit = document.getElementById('sys-memory-unit').value; + let sysMemoryGb = 0; + if (sysMemUnit === '1') sysMemoryGb = sysMemory; + else if (sysMemUnit === '2') sysMemoryGb = sysMemory / 1024; + + const res = await fetch('/api/estimate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + params, + gpu_selections: gpuSelections, + mode: state.mode, + system_memory_gb: sysMemoryGb, + }) + }); + const data = await res.json(); + + renderVramDisplay(data); + if (state.mode === 'gpu_cpu') { + renderRamDisplay(data, sysMemoryGb); + } +} + +function collectParamsForEstimate() { + const params = { ...state.paramValues }; + params._model_size_gb = parseFloat(document.getElementById('model-size-gb').value) || 0; + params._model_layers = parseInt(document.getElementById('model-layers').value) || 0; + params._model_embd = parseInt(document.getElementById('model-embd').value) || 0; + params._model_kv_heads = parseInt(document.getElementById('model-kv-heads').value) || 0; + params._model_head_dim = parseInt(document.getElementById('model-head-dim').value) || 0; + params._model_heads = parseInt(document.getElementById('model-heads').value) || 0; + return params; +} + +function renderVramDisplay(data) { + const bar = document.getElementById('vram-bar'); + const label = document.getElementById('vram-label'); + const breakdown = document.getElementById('vram-breakdown'); + + if (!data.total_vram_available_mb || data.total_vram_available_mb === 0) { + bar.style.width = '0%'; + label.textContent = '请选择GPU'; + breakdown.innerHTML = ''; + return; + } + + const percent = data.usage_percent || 0; + bar.style.width = Math.min(percent, 100) + '%'; + + if (percent > 90) { + bar.className = 'vram-bar danger'; + } else if (percent > 75) { + bar.className = 'vram-bar warning'; + } else { + bar.className = 'vram-bar'; + } + + label.textContent = `VRAM: ${data.total_gb}GB / ${data.total_vram_available_gb}GB (${percent}%)`; + + breakdown.innerHTML = ` +
+ 模型权重 + ${data.weights_gb}GB +
+
+ KV缓存 + ${data.kv_cache_gb}GB +
+
+ 计算/开销 + ${(data.compute_mb / 1024).toFixed(2)}GB +
+
+ CUDA开销 + ${(data.cuda_overhead_mb / 1024).toFixed(2)}GB +
+ `; +} + +function renderRamDisplay(data, sysMemoryGb) { + const bar = document.getElementById('ram-bar'); + const label = document.getElementById('ram-label'); + const breakdown = document.getElementById('ram-breakdown'); + + if (sysMemoryGb === 0) { + bar.style.width = '0%'; + label.textContent = `内存: ${data.cpu_total_gb}GB (无上限)`; + } else { + const percent = data.cpu_usage_percent || 0; + bar.style.width = Math.min(percent, 100) + '%'; + if (percent > 90) { + bar.className = 'vram-bar ram-bar danger'; + } else if (percent > 75) { + bar.className = 'vram-bar ram-bar warning'; + } else { + bar.className = 'vram-bar ram-bar'; + } + label.textContent = `内存: ${data.cpu_total_gb}GB / ${sysMemoryGb}GB (${percent}%)`; + } + + breakdown.innerHTML = ` +
+ CPU模型权重 + ${data.cpu_weights_gb}GB +
+
+ CPU KV缓存 + ${(data.cpu_kv_cache_mb / 1024).toFixed(2)}GB +
+
+ 开销 + ~0.49GB +
+ `; +} + +// ===== Natural Language ===== +async function parseNaturalLanguage() { + const text = document.getElementById('nl-input').value; + if (!text.trim()) return; + + const res = await fetch('/api/parse-nl', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text }) + }); + const data = await res.json(); + + // Apply parsed values + let resultHtml = '
解析结果:
'; + + // Handle GPU + if (data._gpu_name) { + const gpu = state.gpus.find(g => g.name === data._gpu_name); + if (gpu) { + if (data._gpu_count && data._gpu_count > 1) { + state.gpuSlots = []; + for (let i = 0; i < Math.min(data._gpu_count, 4); i++) { + state.gpuSlots.push({ ...gpu }); + } + } else { + state.gpuSlots = [{ ...gpu }]; + } + renderGpuSlots(); + resultHtml += `GPU: ${data._gpu_name}${data._gpu_count > 1 ? ' x' + data._gpu_count : ''}`; + } + } + + // Handle mode + if (data._mode) { + switchMode(data._mode); + resultHtml += `模式: ${data._mode}`; + } + + // Apply other params + for (const [key, value] of Object.entries(data)) { + if (key.startsWith('_')) continue; + state.paramValues[key] = value; + resultHtml += `${key}: ${value}`; + } + + document.getElementById('nl-result').innerHTML = resultHtml; + renderParams(); + generateCommand(); + updateEstimate(); +} + +// ===== Generate Command ===== +async function generateCommand() { + const params = { ...state.paramValues }; + // Remove model info params + Object.keys(params).forEach(k => { if (k.startsWith('_')) delete params[k]; }); + + const res = await fetch('/api/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + version_id: state.currentVersionId, + params, + mode: state.mode, + gpu_selections: state.gpuSlots, + binary: document.getElementById('binary-select').value, + }) + }); + const data = await res.json(); + document.getElementById('command-output').textContent = data.command; +} + +// ===== Copy ===== +function copyCommand() { + const text = document.getElementById('command-output').textContent; + navigator.clipboard.writeText(text).then(() => { + const btn = event.target; + const originalText = btn.textContent; + btn.textContent = '✅ 已复制!'; + setTimeout(() => btn.textContent = originalText, 2000); + }); +} + +// ===== Init on Load ===== +window.addEventListener('DOMContentLoaded', init);