| |
| """ |
| HOS Model Optimizer - 配置优化模块 |
| |
| 提供配置管理功能: |
| - 配置文件加载和解析(YAML 格式) |
| - 8GB VRAM 场景最优配置生成 |
| - 配置验证和冲突检测 |
| - 配置模板管理 |
| |
| 针对 8GB VRAM 场景进行了优化,支持推理、量化、训练和部署四大配置场景。 |
| |
| 使用方法: |
| # 生成 8GB VRAM 最优配置 |
| python config.py --generate --vram 8 |
| |
| # 加载并验证配置文件 |
| python config.py --validate --config my_config.yaml |
| |
| # 列出所有配置模板 |
| python config.py --list-templates |
| |
| # 导出指定模板 |
| python config.py --export-template inference_8gb --output ./my_config.yaml |
| """ |
|
|
| import argparse |
| import copy |
| import os |
| import sys |
| from pathlib import Path |
| from typing import Optional, Dict, Any, List, Tuple |
|
|
| try: |
| import yaml |
| except ImportError: |
| yaml = None |
|
|
|
|
| |
| |
| |
|
|
| class ConfigError(Exception): |
| """配置相关异常的基类""" |
| pass |
|
|
|
|
| class ConfigValidationError(ConfigError): |
| """配置验证失败时抛出""" |
| pass |
|
|
|
|
| class ConfigConflictError(ConfigError): |
| """配置项之间存在冲突时抛出""" |
| pass |
|
|
|
|
| class TemplateNotFoundError(ConfigError): |
| """请求的模板不存在时抛出""" |
| pass |
|
|
|
|
| |
| |
| |
|
|
| |
| _TEMPLATE_LLAMA_CPP = { |
| "backend": "llama-cpp", |
| "model": { |
| "path": "", |
| "format": "gguf", |
| "quantization": "Q4_K_M", |
| }, |
| "inference": { |
| "n_ctx": 512, |
| "n_batch": 1, |
| "n_threads": 4, |
| "n_gpu_layers": -1, |
| "rope_freq_base": 0.0, |
| "rope_freq_scale": 0.0, |
| }, |
| "sampling": { |
| "temperature": 0.7, |
| "top_p": 0.9, |
| "top_k": 40, |
| "repeat_penalty": 1.1, |
| "max_tokens": 256, |
| }, |
| "memory": { |
| "use_mmap": True, |
| "use_mlock": False, |
| "offload_kqv": True, |
| }, |
| } |
|
|
| |
| _TEMPLATE_VLLM = { |
| "backend": "vllm", |
| "model": { |
| "path": "", |
| "format": "awq", |
| "quantization": "awq", |
| "dtype": "float16", |
| }, |
| "inference": { |
| "max_model_len": 512, |
| "gpu_memory_utilization": 0.90, |
| "max_num_batched_tokens": 512, |
| "max_num_seqs": 16, |
| "tensor_parallel_size": 1, |
| "enable_prefix_caching": True, |
| "enable_chunked_prefill": True, |
| }, |
| "sampling": { |
| "temperature": 0.7, |
| "top_p": 0.9, |
| "top_k": -1, |
| "repeat_penalty": 1.1, |
| "max_tokens": 256, |
| }, |
| "server": { |
| "host": "0.0.0.0", |
| "port": 8000, |
| "api_type": "openai", |
| }, |
| } |
|
|
| |
| _TEMPLATE_SGLANG = { |
| "backend": "sglang", |
| "model": { |
| "path": "", |
| "format": "awq", |
| "quantization": "awq", |
| "dtype": "float16", |
| }, |
| "inference": { |
| "context_length": 512, |
| "mem_fraction_static": 0.88, |
| "max_running_requests": 16, |
| "tp_size": 1, |
| "disable_radix_cache": False, |
| "enable_overlap_schedule": True, |
| }, |
| "sampling": { |
| "temperature": 0.7, |
| "top_p": 0.9, |
| "top_k": -1, |
| "max_new_tokens": 256, |
| }, |
| "server": { |
| "host": "0.0.0.0", |
| "port": 30000, |
| "api_type": "openai", |
| }, |
| } |
|
|
| |
| _TEMPLATE_QUANTIZE = { |
| "method": "gguf", |
| "model": { |
| "path": "", |
| "output_path": "", |
| }, |
| "quantization": { |
| "format": "gguf", |
| "type": "Q4_K_M", |
| "bits": 4, |
| "group_size": 128, |
| "symmetric": False, |
| }, |
| "calibration": { |
| "dataset": "", |
| "num_samples": 128, |
| "max_length": 512, |
| }, |
| "evaluation": { |
| "compute_ppl": True, |
| "perplexity_stride": 512, |
| }, |
| } |
|
|
| |
| _TEMPLATE_TRAINING = { |
| "method": "qlora", |
| "model": { |
| "path": "", |
| "max_seq_length": 512, |
| "load_in_4bit": True, |
| "use_gradient_checkpointing": True, |
| }, |
| "lora": { |
| "r": 16, |
| "lora_alpha": 32, |
| "lora_dropout": 0.05, |
| "target_modules": ["q_proj", "v_proj", "k_proj", "o_proj"], |
| "task_type": "CAUSAL_LM", |
| }, |
| "training": { |
| "output_dir": "./output", |
| "num_train_epochs": 3, |
| "per_device_train_batch_size": 1, |
| "gradient_accumulation_steps": 16, |
| "learning_rate": 2e-4, |
| "weight_decay": 0.01, |
| "warmup_ratio": 0.03, |
| "lr_scheduler_type": "cosine", |
| "fp16": True, |
| "bf16": False, |
| "logging_steps": 10, |
| "save_strategy": "steps", |
| "save_steps": 200, |
| "save_total_limit": 3, |
| "optim": "paged_adamw_32bit", |
| "max_grad_norm": 0.3, |
| "seed": 42, |
| }, |
| "dataset": { |
| "path": "", |
| "format": "alpaca", |
| "test_split_ratio": 0.1, |
| }, |
| } |
|
|
| |
| _TEMPLATE_DEPLOY = { |
| "service_type": "api", |
| "model": { |
| "path": "", |
| "format": "gguf", |
| "quantization": "Q4_K_M", |
| }, |
| "server": { |
| "host": "0.0.0.0", |
| "port": 8000, |
| "workers": 1, |
| "max_concurrent_requests": 32, |
| "timeout": 300, |
| }, |
| "backend": "llama-cpp", |
| "scaling": { |
| "min_replicas": 1, |
| "max_replicas": 1, |
| "target_gpu_utilization": 0.80, |
| }, |
| "logging": { |
| "level": "INFO", |
| "access_log": True, |
| "metrics_enabled": True, |
| }, |
| } |
|
|
|
|
| |
| _BUILTIN_TEMPLATES: Dict[str, Dict[str, Any]] = { |
| "llama_cpp": _TEMPLATE_LLAMA_CPP, |
| "vllm": _TEMPLATE_VLLM, |
| "sglang": _TEMPLATE_SGLANG, |
| "quantize": _TEMPLATE_QUANTIZE, |
| "training": _TEMPLATE_TRAINING, |
| "deploy": _TEMPLATE_DEPLOY, |
| } |
|
|
|
|
| |
| |
| |
|
|
| |
| _8GB_OPTIMAL_CONFIGS = { |
| "inference_0.8b": { |
| "backend": "llama-cpp", |
| "model": { |
| "path": "", |
| "format": "gguf", |
| "quantization": "Q4_K_M", |
| }, |
| "inference": { |
| "n_ctx": 2048, |
| "n_batch": 1, |
| "n_threads": 4, |
| "n_gpu_layers": -1, |
| }, |
| "expected_performance": { |
| "tokens_per_second": "100+", |
| "vram_usage_gb": "1-2", |
| }, |
| }, |
| "inference_7b": { |
| "backend": "llama-cpp", |
| "model": { |
| "path": "", |
| "format": "gguf", |
| "quantization": "Q4_K_M", |
| }, |
| "inference": { |
| "n_ctx": 512, |
| "n_batch": 1, |
| "n_threads": 4, |
| "n_gpu_layers": -1, |
| }, |
| "expected_performance": { |
| "tokens_per_second": "35-45", |
| "vram_usage_gb": "5-6", |
| }, |
| }, |
| "training_0.8b": { |
| "method": "qlora", |
| "model": { |
| "path": "", |
| "max_seq_length": 512, |
| "load_in_4bit": True, |
| "use_gradient_checkpointing": True, |
| }, |
| "lora": { |
| "r": 16, |
| "lora_alpha": 32, |
| "target_modules": ["q_proj", "v_proj", "k_proj", "o_proj"], |
| }, |
| "training": { |
| "per_device_train_batch_size": 1, |
| "gradient_accumulation_steps": 16, |
| "learning_rate": 2e-4, |
| "fp16": True, |
| "optim": "paged_adamw_32bit", |
| }, |
| "expected_performance": { |
| "vram_usage_gb": "2-3", |
| }, |
| }, |
| "training_7b": { |
| "method": "qlora", |
| "model": { |
| "path": "", |
| "max_seq_length": 512, |
| "load_in_4bit": True, |
| "use_gradient_checkpointing": True, |
| }, |
| "lora": { |
| "r": 8, |
| "lora_alpha": 16, |
| "target_modules": ["q_proj", "v_proj"], |
| }, |
| "training": { |
| "per_device_train_batch_size": 1, |
| "gradient_accumulation_steps": 32, |
| "learning_rate": 1e-4, |
| "fp16": True, |
| "optim": "paged_adamw_32bit", |
| }, |
| "expected_performance": { |
| "vram_usage_gb": "6-8", |
| }, |
| }, |
| "high_throughput_serving": { |
| "backend": "vllm", |
| "model": { |
| "path": "", |
| "format": "awq", |
| "quantization": "awq", |
| "dtype": "float16", |
| }, |
| "inference": { |
| "max_model_len": 512, |
| "gpu_memory_utilization": 0.90, |
| "max_num_seqs": 16, |
| "tensor_parallel_size": 1, |
| "enable_prefix_caching": True, |
| }, |
| "expected_performance": { |
| "description": "高吞吐服务场景", |
| "vram_usage_gb": "6-8", |
| }, |
| }, |
| "multi_turn_dialogue": { |
| "backend": "sglang", |
| "model": { |
| "path": "", |
| "format": "awq", |
| "quantization": "awq", |
| "dtype": "float16", |
| }, |
| "inference": { |
| "context_length": 1024, |
| "mem_fraction_static": 0.88, |
| "disable_radix_cache": False, |
| }, |
| "expected_performance": { |
| "description": "多轮对话场景,RadixAttention 缓存命中 40-70%", |
| "vram_usage_gb": "5-7", |
| }, |
| }, |
| } |
|
|
|
|
| |
| |
| |
|
|
| |
| _CONFLICT_RULES: List[Dict[str, Any]] = [ |
| { |
| "description": "fp16 和 bf16 不能同时启用", |
| "keys": [("training.fp16", "training.bf16")], |
| "condition": "both_true", |
| }, |
| { |
| "description": "GGUF 格式不支持 dtype 参数", |
| "keys": [("model.format", "model.dtype")], |
| "condition": "format_gguf_with_dtype", |
| }, |
| { |
| "description": "tensor_parallel_size > 1 需要多 GPU,与 8GB VRAM 场景冲突", |
| "keys": [("inference.tensor_parallel_size",)], |
| "condition": "value_gt", |
| "threshold": 1, |
| }, |
| { |
| "description": "llama-cpp 后端不支持 enable_prefix_caching", |
| "keys": [("backend", "inference.enable_prefix_caching")], |
| "condition": "backend_mismatch", |
| "expected_backend": "vllm", |
| }, |
| { |
| "description": "llama-cpp 后端不支持 disable_radix_cache", |
| "keys": [("backend", "inference.disable_radix_cache")], |
| "condition": "backend_mismatch", |
| "expected_backend": "sglang", |
| }, |
| ] |
|
|
|
|
| |
| |
| |
|
|
| class ConfigManager: |
| """配置管理器,负责加载、生成、验证和管理配置""" |
|
|
| def __init__(self, config_dir: Optional[str] = None): |
| """ |
| 初始化配置管理器 |
| |
| Args: |
| config_dir: 配置文件目录,默认为项目根目录下的 configs/ |
| """ |
| if config_dir is None: |
| self.config_dir = Path(__file__).parent.parent / "configs" |
| else: |
| self.config_dir = Path(config_dir) |
|
|
| |
| self._custom_templates: Dict[str, Dict[str, Any]] = {} |
|
|
| |
| |
| |
|
|
| def load_config(self, config_path: str) -> Dict[str, Any]: |
| """ |
| 从 YAML 文件加载配置 |
| |
| Args: |
| config_path: YAML 配置文件路径 |
| |
| Returns: |
| 解析后的配置字典 |
| |
| Raises: |
| ConfigError: 文件不存在或解析失败 |
| """ |
| if yaml is None: |
| raise ConfigError( |
| "PyYAML 未安装,请运行: pip install pyyaml>=6.0" |
| ) |
|
|
| path = Path(config_path) |
| if not path.exists(): |
| raise ConfigError(f"配置文件不存在: {config_path}") |
| if not path.is_file(): |
| raise ConfigError(f"路径不是文件: {config_path}") |
|
|
| try: |
| with open(path, "r", encoding="utf-8") as f: |
| config = yaml.safe_load(f) |
| except yaml.YAMLError as e: |
| raise ConfigError(f"YAML 解析失败: {e}") |
|
|
| if config is None: |
| config = {} |
| if not isinstance(config, dict): |
| raise ConfigError( |
| f"配置文件顶层结构必须是字典,当前为: {type(config).__name__}" |
| ) |
|
|
| return config |
|
|
| def save_config(self, config: Dict[str, Any], output_path: str) -> None: |
| """ |
| 将配置保存为 YAML 文件 |
| |
| Args: |
| config: 配置字典 |
| output_path: 输出文件路径 |
| |
| Raises: |
| ConfigError: 保存失败 |
| """ |
| if yaml is None: |
| raise ConfigError( |
| "PyYAML 未安装,请运行: pip install pyyaml>=6.0" |
| ) |
|
|
| path = Path(output_path) |
| path.parent.mkdir(parents=True, exist_ok=True) |
|
|
| try: |
| with open(path, "w", encoding="utf-8") as f: |
| yaml.dump( |
| config, |
| f, |
| default_flow_style=False, |
| allow_unicode=True, |
| sort_keys=False, |
| ) |
| except (IOError, yaml.YAMLError) as e: |
| raise ConfigError(f"保存配置文件失败: {e}") |
|
|
| def merge_configs(self, base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]: |
| """ |
| 深度合并两个配置字典,override 中的值覆盖 base 中的值 |
| |
| Args: |
| base: 基础配置 |
| override: 覆盖配置 |
| |
| Returns: |
| 合并后的新配置字典 |
| """ |
| result = copy.deepcopy(base) |
| for key, value in override.items(): |
| if ( |
| key in result |
| and isinstance(result[key], dict) |
| and isinstance(value, dict) |
| ): |
| result[key] = self.merge_configs(result[key], value) |
| else: |
| result[key] = copy.deepcopy(value) |
| return result |
|
|
| |
| |
| |
|
|
| def generate_optimal_config( |
| self, |
| scenario: str, |
| model_path: str = "", |
| vram_gb: float = 8.0, |
| ) -> Dict[str, Any]: |
| """ |
| 根据使用场景和硬件条件生成最优配置 |
| |
| Args: |
| scenario: 场景名称,可选值: |
| inference_0.8b, inference_7b, training_0.8b, |
| training_7b, high_throughput_serving, multi_turn_dialogue |
| model_path: 模型路径 |
| vram_gb: 可用显存大小(GB) |
| |
| Returns: |
| 最优配置字典 |
| |
| Raises: |
| ConfigError: 不支持的场景或显存不足 |
| """ |
| if scenario not in _8GB_OPTIMAL_CONFIGS: |
| available = ", ".join(sorted(_8GB_OPTIMAL_CONFIGS.keys())) |
| raise ConfigError( |
| f"不支持的场景: {scenario},可选: {available}" |
| ) |
|
|
| if vram_gb > 8.0: |
| vram_gb = 8.0 |
|
|
| config = copy.deepcopy(_8GB_OPTIMAL_CONFIGS[scenario]) |
|
|
| |
| self._set_nested(config, "model.path", model_path) |
|
|
| |
| config = self._adjust_for_vram(config, vram_gb) |
|
|
| return config |
|
|
| def auto_select_scenario( |
| self, |
| model_size_b: float, |
| task: str = "inference", |
| vram_gb: float = 8.0, |
| ) -> Dict[str, Any]: |
| """ |
| 根据模型大小和任务类型自动选择最优场景并生成配置 |
| |
| Args: |
| model_size_b: 模型大小(十亿参数) |
| task: 任务类型,可选 inference / training / serving / dialogue |
| vram_gb: 可用显存(GB) |
| |
| Returns: |
| 自动选择的最优配置 |
| """ |
| |
| if model_size_b <= 1.0: |
| size_key = "0.8b" |
| elif model_size_b <= 8.0: |
| size_key = "7b" |
| else: |
| raise ConfigError( |
| f"模型大小 {model_size_b}B 超出 8GB VRAM 场景支持范围(最大 ~7B)" |
| ) |
|
|
| |
| task_scenario_map = { |
| "inference": f"inference_{size_key}", |
| "training": f"training_{size_key}", |
| "serving": "high_throughput_serving", |
| "dialogue": "multi_turn_dialogue", |
| } |
|
|
| if task not in task_scenario_map: |
| available = ", ".join(sorted(task_scenario_map.keys())) |
| raise ConfigError(f"不支持的任务类型: {task},可选: {available}") |
|
|
| scenario = task_scenario_map[task] |
| return self.generate_optimal_config(scenario, vram_gb=vram_gb) |
|
|
| def _adjust_for_vram( |
| self, config: Dict[str, Any], vram_gb: float |
| ) -> Dict[str, Any]: |
| """ |
| 根据实际可用 VRAM 微调配置参数 |
| |
| Args: |
| config: 基础配置 |
| vram_gb: 可用显存(GB) |
| |
| Returns: |
| 调整后的配置 |
| """ |
| |
| n_ctx = self._get_nested(config, "inference.n_ctx") |
| if n_ctx is not None: |
| if vram_gb < 4.0: |
| config["inference"]["n_ctx"] = min(n_ctx, 256) |
| elif vram_gb < 6.0: |
| config["inference"]["n_ctx"] = min(n_ctx, 512) |
| else: |
| config["inference"]["n_ctx"] = min(n_ctx, 2048) |
|
|
| |
| gpu_util = self._get_nested(config, "inference.gpu_memory_utilization") |
| if gpu_util is not None: |
| if vram_gb < 6.0: |
| config["inference"]["gpu_memory_utilization"] = 0.85 |
| else: |
| config["inference"]["gpu_memory_utilization"] = min(gpu_util, 0.90) |
|
|
| |
| lora_r = self._get_nested(config, "lora.r") |
| if lora_r is not None: |
| if vram_gb < 4.0: |
| config["lora"]["r"] = 4 |
| config["lora"]["lora_alpha"] = 8 |
| config["training"]["gradient_accumulation_steps"] = 64 |
| elif vram_gb < 6.0: |
| config["lora"]["r"] = min(lora_r, 8) |
| config["training"]["gradient_accumulation_steps"] = max( |
| self._get_nested(config, "training.gradient_accumulation_steps") or 16, 16 |
| ) |
|
|
| return config |
|
|
| |
| |
| |
|
|
| def validate_config(self, config: Dict[str, Any]) -> List[str]: |
| """ |
| 验证配置并返回所有发现的问题列表 |
| |
| Args: |
| config: 待验证的配置字典 |
| |
| Returns: |
| 问题描述字符串列表,空列表表示验证通过 |
| """ |
| issues: List[str] = [] |
|
|
| |
| issues.extend(self._check_conflicts(config)) |
|
|
| |
| issues.extend(self._check_value_ranges(config)) |
|
|
| |
| issues.extend(self._check_required_fields(config)) |
|
|
| return issues |
|
|
| def _check_conflicts(self, config: Dict[str, Any]) -> List[str]: |
| """检测配置项之间的冲突""" |
| issues: List[str] = [] |
|
|
| for rule in _CONFLICT_RULES: |
| condition = rule["condition"] |
|
|
| if condition == "both_true": |
| for key_pair in rule["keys"]: |
| v1 = self._get_nested(config, key_pair[0]) |
| v2 = self._get_nested(config, key_pair[1]) |
| if v1 is True and v2 is True: |
| issues.append( |
| f"[冲突] {rule['description']}: " |
| f"{key_pair[0]}={v1}, {key_pair[1]}={v2}" |
| ) |
|
|
| elif condition == "format_gguf_with_dtype": |
| fmt = self._get_nested(config, "model.format") |
| dtype = self._get_nested(config, "model.dtype") |
| if fmt == "gguf" and dtype is not None: |
| issues.append( |
| f"[冲突] {rule['description']}: " |
| f"format=gguf 时不应设置 dtype={dtype}" |
| ) |
|
|
| elif condition == "value_gt": |
| for key in rule["keys"]: |
| val = self._get_nested(config, key) |
| threshold = rule.get("threshold", 1) |
| if val is not None and val > threshold: |
| issues.append( |
| f"[冲突] {rule['description']}: {key}={val}" |
| ) |
|
|
| elif condition == "backend_mismatch": |
| backend = self._get_nested(config, "backend") |
| expected = rule.get("expected_backend", "") |
| |
| if len(rule["keys"]) > 1: |
| feature_key = rule["keys"][1] |
| feature_val = self._get_nested(config, feature_key) |
| if backend is not None and backend != expected and feature_val: |
| issues.append( |
| f"[冲突] {rule['description']}: " |
| f"backend={backend} 不支持 {feature_key}={feature_val}" |
| ) |
|
|
| return issues |
|
|
| def _check_value_ranges(self, config: Dict[str, Any]) -> List[str]: |
| """检查数值参数的合理范围""" |
| issues: List[str] = [] |
|
|
| |
| temp = self._get_nested(config, "sampling.temperature") |
| if temp is not None and (temp < 0 or temp > 2.0): |
| issues.append( |
| f"[范围] sampling.temperature={temp} 超出合理范围 [0, 2.0]" |
| ) |
|
|
| |
| top_p = self._get_nested(config, "sampling.top_p") |
| if top_p is not None and (top_p < 0 or top_p > 1.0): |
| issues.append( |
| f"[范围] sampling.top_p={top_p} 超出合理范围 [0, 1.0]" |
| ) |
|
|
| |
| gpu_util = self._get_nested(config, "inference.gpu_memory_utilization") |
| if gpu_util is not None and (gpu_util < 0.5 or gpu_util > 0.95): |
| issues.append( |
| f"[范围] inference.gpu_memory_utilization={gpu_util} " |
| f"超出安全范围 [0.5, 0.95]" |
| ) |
|
|
| |
| lr = self._get_nested(config, "training.learning_rate") |
| if lr is not None and (lr <= 0 or lr > 1e-2): |
| issues.append( |
| f"[范围] training.learning_rate={lr} 超出合理范围 (0, 1e-2]" |
| ) |
|
|
| |
| for key in ["inference.n_ctx", "inference.max_model_len", "inference.context_length"]: |
| val = self._get_nested(config, key) |
| if val is not None and val <= 0: |
| issues.append(f"[范围] {key}={val} 必须为正整数") |
|
|
| |
| for key in ["inference.n_batch", "training.per_device_train_batch_size"]: |
| val = self._get_nested(config, key) |
| if val is not None and val <= 0: |
| issues.append(f"[范围] {key}={val} 必须为正整数") |
|
|
| return issues |
|
|
| def _check_required_fields(self, config: Dict[str, Any]) -> List[str]: |
| """检查必填字段是否存在且非空""" |
| issues: List[str] = [] |
|
|
| |
| model_path = self._get_nested(config, "model.path") |
| if model_path is not None and model_path == "": |
| issues.append("[必填] model.path 未设置,请指定模型路径") |
|
|
| return issues |
|
|
| def validate_and_raise(self, config: Dict[str, Any]) -> None: |
| """ |
| 验证配置,如果发现问题则抛出异常 |
| |
| Args: |
| config: 待验证的配置 |
| |
| Raises: |
| ConfigValidationError: 存在验证问题 |
| ConfigConflictError: 存在配置冲突 |
| """ |
| issues = self.validate_config(config) |
| if not issues: |
| return |
|
|
| conflicts = [i for i in issues if i.startswith("[冲突]")] |
| others = [i for i in issues if not i.startswith("[冲突]")] |
|
|
| if conflicts: |
| raise ConfigConflictError( |
| "配置冲突:\n" + "\n".join(conflicts) |
| ) |
| if others: |
| raise ConfigValidationError( |
| "配置验证失败:\n" + "\n".join(others) |
| ) |
|
|
| |
| |
| |
|
|
| def list_templates(self) -> List[str]: |
| """ |
| 列出所有可用的配置模板名称(内置 + 自定义) |
| |
| Returns: |
| 模板名称列表 |
| """ |
| all_names = sorted(_BUILTIN_TEMPLATES.keys()) |
| all_names.extend(sorted(self._custom_templates.keys())) |
| return all_names |
|
|
| def get_template(self, name: str) -> Dict[str, Any]: |
| """ |
| 获取指定名称的配置模板(深拷贝) |
| |
| Args: |
| name: 模板名称 |
| |
| Returns: |
| 模板配置字典 |
| |
| Raises: |
| TemplateNotFoundError: 模板不存在 |
| """ |
| if name in _BUILTIN_TEMPLATES: |
| return copy.deepcopy(_BUILTIN_TEMPLATES[name]) |
| if name in self._custom_templates: |
| return copy.deepcopy(self._custom_templates[name]) |
| raise TemplateNotFoundError( |
| f"模板 '{name}' 不存在,可用模板: {', '.join(self.list_templates())}" |
| ) |
|
|
| def register_template(self, name: str, template: Dict[str, Any]) -> None: |
| """ |
| 注册自定义配置模板 |
| |
| Args: |
| name: 模板名称 |
| template: 模板配置字典 |
| """ |
| if not isinstance(template, dict): |
| raise ConfigError("模板必须是字典类型") |
| self._custom_templates[name] = copy.deepcopy(template) |
|
|
| def unregister_template(self, name: str) -> None: |
| """ |
| 注销自定义配置模板(不能注销内置模板) |
| |
| Args: |
| name: 模板名称 |
| |
| Raises: |
| ConfigError: 尝试注销内置模板 |
| TemplateNotFoundError: 模板不存在 |
| """ |
| if name in _BUILTIN_TEMPLATES: |
| raise ConfigError(f"不能注销内置模板: {name}") |
| if name not in self._custom_templates: |
| raise TemplateNotFoundError(f"自定义模板 '{name}' 不存在") |
| del self._custom_templates[name] |
|
|
| def export_template(self, name: str, output_path: str) -> None: |
| """ |
| 将模板导出为 YAML 文件 |
| |
| Args: |
| name: 模板名称 |
| output_path: 输出文件路径 |
| """ |
| template = self.get_template(name) |
| self.save_config(template, output_path) |
|
|
| def load_template_from_file(self, name: str, file_path: str) -> None: |
| """ |
| 从 YAML 文件加载并注册为自定义模板 |
| |
| Args: |
| name: 注册时使用的模板名称 |
| file_path: YAML 文件路径 |
| """ |
| config = self.load_config(file_path) |
| self.register_template(name, config) |
|
|
| |
| |
| |
|
|
| @staticmethod |
| def _get_nested(config: Dict[str, Any], dotted_key: str) -> Any: |
| """ |
| 通过点分路径读取嵌套字典的值 |
| |
| Args: |
| config: 配置字典 |
| dotted_key: 点分路径,如 "inference.n_ctx" |
| |
| Returns: |
| 对应值,路径不存在时返回 None |
| """ |
| keys = dotted_key.split(".") |
| current = config |
| for key in keys: |
| if isinstance(current, dict) and key in current: |
| current = current[key] |
| else: |
| return None |
| return current |
|
|
| @staticmethod |
| def _set_nested(config: Dict[str, Any], dotted_key: str, value: Any) -> None: |
| """ |
| 通过点分路径设置嵌套字典的值 |
| |
| Args: |
| config: 配置字典 |
| dotted_key: 点分路径 |
| value: 要设置的值 |
| """ |
| keys = dotted_key.split(".") |
| current = config |
| for key in keys[:-1]: |
| if key not in current or not isinstance(current[key], dict): |
| current[key] = {} |
| current = current[key] |
| current[keys[-1]] = value |
|
|
|
|
| |
| |
| |
|
|
| def _build_parser() -> argparse.ArgumentParser: |
| """构建命令行参数解析器""" |
| parser = argparse.ArgumentParser( |
| description="HOS Model Optimizer - 配置优化模块", |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| epilog=""" |
| 示例: |
| # 生成 8GB VRAM 推理配置(0.8B 模型) |
| python config.py --generate --scenario inference_0.8b --model-path ./my_model |
| |
| # 自动生成最优配置 |
| python config.py --auto --model-size 0.8 --task inference |
| |
| # 验证配置文件 |
| python config.py --validate --config my_config.yaml |
| |
| # 列出所有模板 |
| python config.py --list-templates |
| |
| # 导出模板 |
| python config.py --export-template llama_cpp --output ./llama_cpp.yaml |
| """, |
| ) |
|
|
| |
| group = parser.add_mutually_exclusive_group(required=True) |
| group.add_argument( |
| "--generate", |
| action="store_true", |
| help="生成指定场景的最优配置", |
| ) |
| group.add_argument( |
| "--auto", |
| action="store_true", |
| help="根据模型大小和任务类型自动生成最优配置", |
| ) |
| group.add_argument( |
| "--validate", |
| action="store_true", |
| help="验证配置文件的有效性", |
| ) |
| group.add_argument( |
| "--list-templates", |
| action="store_true", |
| help="列出所有可用的配置模板", |
| ) |
| group.add_argument( |
| "--export-template", |
| type=str, |
| metavar="NAME", |
| help="导出指定名称的配置模板", |
| ) |
|
|
| |
| parser.add_argument( |
| "--config", type=str, help="配置文件路径(用于 --validate)" |
| ) |
| parser.add_argument( |
| "--output", "-o", type=str, help="输出文件路径" |
| ) |
|
|
| |
| parser.add_argument( |
| "--scenario", |
| type=str, |
| help="场景名称(用于 --generate),可选: " |
| + ", ".join(sorted(_8GB_OPTIMAL_CONFIGS.keys())), |
| ) |
| parser.add_argument( |
| "--model-path", type=str, default="", help="模型路径" |
| ) |
| parser.add_argument( |
| "--vram", type=float, default=8.0, help="可用显存大小(GB),默认 8" |
| ) |
|
|
| |
| parser.add_argument( |
| "--model-size", |
| type=float, |
| help="模型大小(十亿参数),用于 --auto", |
| ) |
| parser.add_argument( |
| "--task", |
| type=str, |
| default="inference", |
| choices=["inference", "training", "serving", "dialogue"], |
| help="任务类型(用于 --auto),默认 inference", |
| ) |
|
|
| return parser |
|
|
|
|
| def main(argv: Optional[List[str]] = None) -> int: |
| """ |
| 命令行入口函数 |
| |
| Args: |
| argv: 命令行参数列表,默认使用 sys.argv |
| |
| Returns: |
| 退出码,0 表示成功 |
| """ |
| parser = _build_parser() |
| args = parser.parse_args(argv) |
| manager = ConfigManager() |
|
|
| try: |
| |
| if args.list_templates: |
| templates = manager.list_templates() |
| print("可用的配置模板:") |
| for name in templates: |
| source = "内置" if name in _BUILTIN_TEMPLATES else "自定义" |
| print(f" - {name} [{source}]") |
| return 0 |
|
|
| |
| if args.export_template: |
| if not args.output: |
| print("错误: 导出模板需要指定 --output 参数", file=sys.stderr) |
| return 1 |
| manager.export_template(args.export_template, args.output) |
| print(f"模板 '{args.export_template}' 已导出到: {args.output}") |
| return 0 |
|
|
| |
| if args.validate: |
| if not args.config: |
| print("错误: 验证配置需要指定 --config 参数", file=sys.stderr) |
| return 1 |
| config = manager.load_config(args.config) |
| issues = manager.validate_config(config) |
| if not issues: |
| print("配置验证通过 ✓") |
| return 0 |
| else: |
| print(f"发现 {len(issues)} 个问题:") |
| for issue in issues: |
| print(f" {issue}") |
| return 1 |
|
|
| |
| if args.generate: |
| if not args.scenario: |
| print( |
| "错误: 生成配置需要指定 --scenario 参数", |
| file=sys.stderr, |
| ) |
| print( |
| "可选场景: " + ", ".join(sorted(_8GB_OPTIMAL_CONFIGS.keys())), |
| file=sys.stderr, |
| ) |
| return 1 |
| config = manager.generate_optimal_config( |
| scenario=args.scenario, |
| model_path=args.model_path, |
| vram_gb=args.vram, |
| ) |
| if args.output: |
| manager.save_config(config, args.output) |
| print(f"配置已保存到: {args.output}") |
| else: |
| if yaml is not None: |
| print(yaml.dump(config, default_flow_style=False, allow_unicode=True, sort_keys=False)) |
| else: |
| import json |
| print(json.dumps(config, indent=2, ensure_ascii=False)) |
| return 0 |
|
|
| |
| if args.auto: |
| if args.model_size is None: |
| print("错误: 自动生成需要指定 --model-size 参数", file=sys.stderr) |
| return 1 |
| config = manager.auto_select_scenario( |
| model_size_b=args.model_size, |
| task=args.task, |
| vram_gb=args.vram, |
| ) |
| if args.model_path: |
| ConfigManager._set_nested(config, "model.path", args.model_path) |
| if args.output: |
| manager.save_config(config, args.output) |
| print(f"配置已保存到: {args.output}") |
| else: |
| if yaml is not None: |
| print(yaml.dump(config, default_flow_style=False, allow_unicode=True, sort_keys=False)) |
| else: |
| import json |
| print(json.dumps(config, indent=2, ensure_ascii=False)) |
| return 0 |
|
|
| except ConfigError as e: |
| print(f"配置错误: {e}", file=sys.stderr) |
| return 1 |
| except Exception as e: |
| print(f"未知错误: {e}", file=sys.stderr) |
| return 1 |
|
|
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| sys.exit(main()) |
|
|