| |
| """ |
| HOS Model Optimizer - 量化模块 |
| |
| 提供多种量化方法支持: |
| - GGUF 量化:使用 llama.cpp 工具链 |
| - AWQ 4-bit 量化:激活感知量化,精度损失最小 |
| - GPTQ 4/8-bit 量化:基于 GPU 的 Post-Training 量化 |
| - 量化质量评估:PPL (Perplexity) 计算 |
| - 格式转换工具:支持不同量化格式间的转换 |
| |
| 针对 8GB VRAM 场景进行了优化配置。 |
| |
| 使用方法: |
| # GGUF 量化 |
| python quantize.py --method gguf --model ./model_path --output ./output_path |
| |
| # AWQ 4-bit 量化 |
| python quantize.py --method awq --bits 4 --model ./model_path |
| |
| # GPTQ 量化 |
| python quantize.py --method gptq --bits 4 --model ./model_path |
| |
| # 评估量化质量 |
| python quantize.py --method evaluate --model ./quantized_model_path |
| |
| # 格式转换 |
| python quantize.py --method convert --from gguf --to awq --model ./model_path |
| """ |
|
|
| import argparse |
| import os |
| import sys |
| import json |
| import subprocess |
| import tempfile |
| from pathlib import Path |
| from typing import Optional, Dict, Any, List |
| import torch |
| import time |
|
|
|
|
| |
| VRAM_8GB_CONFIG = { |
| "max_batch_size": 1, |
| "max_seq_length": 512, |
| "gradient_checkpointing": True, |
| "offload_to_cpu": True, |
| "calibration_samples": 128, |
| } |
|
|
|
|
| class QuantizationError(Exception): |
| """量化过程中的异常""" |
| pass |
|
|
|
|
| def check_vram_availability() -> Dict[str, Any]: |
| """检查 GPU VRAM 可用性 |
| |
| Returns: |
| 包含 VRAM 信息的字典 |
| """ |
| if not torch.cuda.is_available(): |
| return { |
| "available": False, |
| "total_vram_gb": 0, |
| "free_vram_gb": 0, |
| "device": "cpu" |
| } |
| |
| device = torch.cuda.current_device() |
| total_vram = torch.cuda.get_device_properties(device).total_memory |
| free_vram = total_vram - torch.cuda.memory_allocated(device) |
| |
| return { |
| "available": True, |
| "total_vram_gb": total_vram / (1024 ** 3), |
| "free_vram_gb": free_vram / (1024 ** 3), |
| "device": torch.cuda.get_device_name(device) |
| } |
|
|
|
|
| def optimize_for_low_vram(config: Dict[str, Any]) -> Dict[str, Any]: |
| """根据 VRAM 限制优化配置 |
| |
| Args: |
| config: 原始配置 |
| |
| Returns: |
| 优化后的配置 |
| """ |
| vram_info = check_vram_availability() |
| |
| if not vram_info["available"] or vram_info["free_vram_gb"] <= 8: |
| print(f"⚠️ 检测到 VRAM <= 8GB ({vram_info['free_vram_gb']:.2f}GB 可用)") |
| print(" 启用低 VRAM 优化配置...") |
| |
| |
| optimized = config.copy() |
| optimized.update(VRAM_8GB_CONFIG) |
| |
| return optimized |
| |
| return config |
|
|
|
|
| def quantize_gguf( |
| model_path: str, |
| output_path: str, |
| quant_type: str = "Q4_K_M", |
| llama_cpp_path: Optional[str] = None |
| ) -> str: |
| """GGUF 量化 - 使用 llama.cpp 工具链 |
| |
| Args: |
| model_path: 输入模型路径(HuggingFace 格式) |
| output_path: 输出 GGUF 文件路径 |
| quant_type: 量化类型,如 Q4_K_M, Q5_K_M, Q8_0 等 |
| llama_cpp_path: llama.cpp 安装路径,如果为 None 则从 PATH 查找 |
| |
| Returns: |
| 输出文件路径 |
| |
| Raises: |
| QuantizationError: 量化失败时抛出 |
| """ |
| print(f"=== GGUF {quant_type} 量化 ===") |
| print(f"输入模型: {model_path}") |
| print(f"输出路径: {output_path}") |
| |
| |
| quantize_tool = "llama-quantize" |
| if llama_cpp_path: |
| quantize_tool = os.path.join(llama_cpp_path, "llama-quantize") |
| |
| try: |
| |
| result = subprocess.run( |
| [quantize_tool, "--help"], |
| capture_output=True, |
| text=True, |
| timeout=5 |
| ) |
| except (FileNotFoundError, subprocess.TimeoutExpired): |
| raise QuantizationError( |
| "找不到 llama-quantize 工具。请确保已安装 llama.cpp 并将其添加到 PATH," |
| "或通过 --llama-cpp-path 指定安装路径。\n" |
| "安装指南: https://github.com/ggerganov/llama.cpp" |
| ) |
| |
| |
| print("步骤 1/2: 转换模型为 GGUF 格式...") |
| try: |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
| |
| |
| model = AutoModelForCausalLM.from_pretrained( |
| model_path, |
| torch_dtype=torch.float16, |
| trust_remote_code=True |
| ) |
| tokenizer = AutoTokenizer.from_pretrained( |
| model_path, |
| trust_remote_code=True |
| ) |
| |
| |
| with tempfile.TemporaryDirectory() as tmpdir: |
| |
| temp_model_path = os.path.join(tmpdir, "model") |
| model.save_pretrained(temp_model_path) |
| tokenizer.save_pretrained(temp_model_path) |
| |
| |
| convert_script = "convert.py" |
| if llama_cpp_path: |
| convert_script = os.path.join(llama_cpp_path, "convert.py") |
| |
| f16_path = os.path.join(tmpdir, "model-f16.gguf") |
| |
| print(" 转换为 FP16 GGUF...") |
| subprocess.run( |
| [sys.executable, convert_script, temp_model_path, "--outfile", f16_path], |
| check=True, |
| capture_output=True, |
| text=True |
| ) |
| |
| |
| print(f"步骤 2/2: 量化为 {quant_type}...") |
| subprocess.run( |
| [quantize_tool, f16_path, output_path, quant_type], |
| check=True, |
| capture_output=True, |
| text=True |
| ) |
| |
| except subprocess.CalledProcessError as e: |
| raise QuantizationError(f"GGUF 量化失败: {e.stderr}") |
| except Exception as e: |
| raise QuantizationError(f"GGUF 量化过程中发生错误: {str(e)}") |
| |
| print("✓ GGUF 量化完成!") |
| return output_path |
|
|
|
|
| def quantize_awq( |
| model_path: str, |
| output_path: str, |
| bits: int = 4, |
| group_size: int = 128 |
| ) -> str: |
| """AWQ 4-bit 量化 |
| |
| AWQ (Activation-aware Weight Quantization) 通过保护显著权重通道 |
| 实现高精度 4-bit 量化,适合小模型。 |
| |
| Args: |
| model_path: 输入模型路径 |
| output_path: 输出模型路径 |
| bits: 量化位数,默认 4 |
| group_size: 分组大小,默认 128 |
| |
| Returns: |
| 输出模型路径 |
| |
| Raises: |
| QuantizationError: 量化失败时抛出 |
| """ |
| print(f"=== AWQ {bits}-bit 量化 ===") |
| print(f"输入模型: {model_path}") |
| print(f"输出路径: {output_path}") |
| |
| try: |
| from awq import AutoAWQForCausalLM |
| from transformers import AutoTokenizer |
| |
| |
| config = { |
| "zero_point": True, |
| "q_group_size": group_size, |
| "w_bit": bits, |
| "version": "GEMM", |
| } |
| config = optimize_for_low_vram(config) |
| |
| |
| print("加载模型...") |
| model = AutoAWQForCausalLM.from_pretrained( |
| model_path, |
| trust_remote_code=True, |
| ) |
| tokenizer = AutoTokenizer.from_pretrained( |
| model_path, |
| trust_remote_code=True, |
| ) |
| |
| print(f"开始 AWQ {bits}-bit 量化...") |
| print(f" 配置: zero_point={config['zero_point']}, " |
| f"group_size={config['q_group_size']}, version={config['version']}") |
| |
| model.quantize(tokenizer, quant_config=config) |
| |
| |
| print(f"保存量化模型到: {output_path}") |
| model.save_quantized(output_path) |
| tokenizer.save_pretrained(output_path) |
| |
| print("✓ AWQ 量化完成!") |
| return output_path |
| |
| except ImportError as e: |
| raise QuantizationError( |
| f"缺少依赖: {e.name}\n" |
| f"请安装: pip install autoawq" |
| ) |
| except Exception as e: |
| raise QuantizationError(f"AWQ 量化失败: {str(e)}") |
|
|
|
|
| def quantize_gptq( |
| model_path: str, |
| output_path: str, |
| bits: int = 4, |
| group_size: int = 128, |
| desc_act: bool = False |
| ) -> str: |
| """GPTQ 量化 |
| |
| GPTQ 基于 Optimal Brain Quantization 框架,通过逐层量化 |
| 和误差补偿实现高精度量化。 |
| |
| Args: |
| model_path: 输入模型路径 |
| output_path: 输出模型路径 |
| bits: 量化位数,4 或 8 |
| group_size: 分组大小,默认 128 |
| desc_act: 是否按激活值排序,默认 False |
| |
| Returns: |
| 输出模型路径 |
| |
| Raises: |
| QuantizationError: 量化失败时抛出 |
| """ |
| if bits not in [4, 8]: |
| raise ValueError(f"GPTQ 仅支持 4-bit 或 8-bit 量化,收到: {bits}") |
| |
| print(f"=== GPTQ {bits}-bit 量化 ===") |
| print(f"输入模型: {model_path}") |
| print(f"输出路径: {output_path}") |
| |
| try: |
| from transformers import AutoTokenizer |
| from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig |
| |
| |
| quantize_config = BaseQuantizeConfig( |
| bits=bits, |
| group_size=group_size, |
| desc_act=desc_act, |
| damp_percent=0.1, |
| ) |
| |
| print("加载模型...") |
| model = AutoGPTQForCausalLM.from_pretrained( |
| model_path, |
| quantize_config, |
| trust_remote_code=True, |
| ) |
| tokenizer = AutoTokenizer.from_pretrained( |
| model_path, |
| trust_remote_code=True, |
| ) |
| |
| |
| print("准备校准数据...") |
| calibration_texts = [ |
| "信息安全是保护计算机系统和网络免受未经授权的访问、攻击或破坏的实践。", |
| "SQL注入是一种常见的Web安全漏洞,攻击者通过注入恶意SQL代码来操纵数据库。", |
| "渗透测试是一种模拟真实攻击者的方法来评估系统安全性的技术。", |
| "防火墙是网络安全的第一道防线,用于监控和控制进出网络的流量。", |
| "加密技术用于保护数据在传输和存储过程中的机密性和完整性。", |
| "身份验证是确认用户身份的过程,通常使用密码、生物特征或多因素认证。", |
| "网络监控用于检测和分析网络流量,以识别潜在的安全威胁。", |
| "数据备份是防止数据丢失的重要措施,应定期进行并存储在安全位置。", |
| ] |
| |
| calibration_data = [ |
| tokenizer(text, return_tensors="pt", max_length=512, truncation=True) |
| for text in calibration_texts |
| ] |
| |
| print(f"开始 GPTQ {bits}-bit 量化...") |
| print(f" 配置: bits={bits}, group_size={group_size}, desc_act={desc_act}") |
| |
| model.quantize(calibration_data) |
| |
| print(f"保存量化模型到: {output_path}") |
| model.save_quantized(output_path) |
| tokenizer.save_pretrained(output_path) |
| |
| print("✓ GPTQ 量化完成!") |
| return output_path |
| |
| except ImportError as e: |
| raise QuantizationError( |
| f"缺少依赖: {e.name}\n" |
| f"请安装: pip install auto-gptq" |
| ) |
| except Exception as e: |
| raise QuantizationError(f"GPTQ 量化失败: {str(e)}") |
|
|
|
|
| def evaluate_perplexity( |
| model_path: str, |
| dataset: str = "wikitext", |
| max_samples: int = 100, |
| stride: int = 512 |
| ) -> float: |
| """评估量化模型的 PPL (Perplexity) |
| |
| Args: |
| model_path: 模型路径 |
| dataset: 评估数据集名称,默认 wikitext |
| max_samples: 最大评估样本数 |
| stride: 滑动窗口步长 |
| |
| Returns: |
| PPL 值 |
| |
| Raises: |
| QuantizationError: 评估失败时抛出 |
| """ |
| print(f"=== 评估模型 PPL ===") |
| print(f"模型: {model_path}") |
| print(f"数据集: {dataset}") |
| |
| try: |
| |
| from hos_optimizer.evaluate import EvaluationEngine, EvaluationConfig, MetricLoader |
| |
| |
| config = EvaluationConfig( |
| model_path=model_path, |
| dataset_path="", |
| metrics=["ppl"], |
| task_type="perplexity", |
| max_samples=max_samples, |
| batch_size=1, |
| device_map="auto", |
| ) |
| |
| |
| engine = EvaluationEngine(config) |
| |
| |
| print("加载模型...") |
| engine.load_model() |
| |
| |
| print(f"加载评估数据: {dataset}...") |
| from datasets import load_dataset |
| |
| test_data = load_dataset(dataset, "wikitext-2-raw-v1", split="test") |
| |
| |
| references = [] |
| for i, text in enumerate(test_data["text"]): |
| if text and text.strip(): |
| references.append(text) |
| if len(references) >= max_samples: |
| break |
| |
| if not references: |
| raise QuantizationError("数据集中没有有效的文本样本") |
| |
| |
| print("计算 PPL...") |
| metrics_result = engine.compute_metrics( |
| predictions=references, |
| references=references |
| ) |
| |
| ppl_value = metrics_result.get("ppl", 0.0) |
| |
| |
| engine.shutdown() |
| |
| print(f"✓ PPL 评估完成: {ppl_value:.2f}") |
| return ppl_value |
| |
| except ImportError as e: |
| raise QuantizationError( |
| f"缺少依赖: {e.name}\n" |
| f"请安装: pip install datasets evaluate" |
| ) |
| except Exception as e: |
| raise QuantizationError(f"PPL 评估失败: {str(e)}") |
|
|
|
|
| def convert_format( |
| model_path: str, |
| output_path: str, |
| from_format: str, |
| to_format: str, |
| **kwargs |
| ) -> str: |
| """量化格式转换工具 |
| |
| Args: |
| model_path: 输入模型路径 |
| output_path: 输出模型路径 |
| from_format: 源格式 (gguf, awq, gptq, hf) |
| to_format: 目标格式 (gguf, awq, gptq, hf) |
| **kwargs: 其他参数 |
| |
| Returns: |
| 输出模型路径 |
| |
| Raises: |
| QuantizationError: 转换失败时抛出 |
| """ |
| print(f"=== 格式转换: {from_format} -> {to_format} ===") |
| print(f"输入: {model_path}") |
| print(f"输出: {output_path}") |
| |
| |
| conversion_map = { |
| ("hf", "gguf"): lambda: quantize_gguf(model_path, output_path, **kwargs), |
| ("hf", "awq"): lambda: quantize_awq(model_path, output_path, **kwargs), |
| ("hf", "gptq"): lambda: quantize_gptq(model_path, output_path, **kwargs), |
| ("gguf", "hf"): lambda: _convert_gguf_to_hf(model_path, output_path), |
| ("awq", "hf"): lambda: _convert_awq_to_hf(model_path, output_path), |
| ("gptq", "hf"): lambda: _convert_gptq_to_hf(model_path, output_path), |
| } |
| |
| conversion_key = (from_format.lower(), to_format.lower()) |
| |
| if conversion_key not in conversion_map: |
| raise QuantizationError( |
| f"不支持的转换路径: {from_format} -> {to_format}\n" |
| f"支持的转换: {', '.join([f'{k[0]}->{k[1]}' for k in conversion_map.keys()])}" |
| ) |
| |
| try: |
| result = conversion_map[conversion_key]() |
| print(f"✓ 格式转换完成: {from_format} -> {to_format}") |
| return result |
| except Exception as e: |
| raise QuantizationError(f"格式转换失败: {str(e)}") |
|
|
|
|
| def _convert_gguf_to_hf(model_path: str, output_path: str) -> str: |
| """GGUF 转 HuggingFace 格式""" |
| |
| |
| raise NotImplementedError("GGUF -> HF 转换尚未实现") |
|
|
|
|
| def _convert_awq_to_hf(model_path: str, output_path: str) -> str: |
| """AWQ 转 HuggingFace 格式""" |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
| |
| print("加载 AWQ 模型...") |
| model = AutoModelForCausalLM.from_pretrained( |
| model_path, |
| trust_remote_code=True, |
| torch_dtype=torch.float16 |
| ) |
| tokenizer = AutoTokenizer.from_pretrained( |
| model_path, |
| trust_remote_code=True |
| ) |
| |
| print(f"保存到: {output_path}") |
| model.save_pretrained(output_path) |
| tokenizer.save_pretrained(output_path) |
| |
| return output_path |
|
|
|
|
| def _convert_gptq_to_hf(model_path: str, output_path: str) -> str: |
| """GPTQ 转 HuggingFace 格式""" |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
| |
| print("加载 GPTQ 模型...") |
| model = AutoModelForCausalLM.from_pretrained( |
| model_path, |
| trust_remote_code=True, |
| torch_dtype=torch.float16 |
| ) |
| tokenizer = AutoTokenizer.from_pretrained( |
| model_path, |
| trust_remote_code=True |
| ) |
| |
| print(f"保存到: {output_path}") |
| model.save_pretrained(output_path) |
| tokenizer.save_pretrained(output_path) |
| |
| return output_path |
|
|
|
|
| def get_model_size(model_path: str) -> float: |
| """获取模型文件大小 (GB) |
| |
| Args: |
| model_path: 模型路径 |
| |
| Returns: |
| 模型大小 (GB) |
| """ |
| total_size = 0 |
| for dirpath, _, filenames in os.walk(model_path): |
| for f in filenames: |
| if f.endswith((".safetensors", ".bin", ".pt", ".gguf")): |
| fp = os.path.join(dirpath, f) |
| total_size += os.path.getsize(fp) |
| return total_size / (1024 ** 3) |
|
|
|
|
| def main(): |
| """命令行入口""" |
| parser = argparse.ArgumentParser( |
| description="HOS Model Optimizer - 量化工具", |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| epilog=""" |
| 示例: |
| # GGUF 量化 |
| python quantize.py --method gguf --model ./model --output ./model.gguf --quant-type Q4_K_M |
| |
| # AWQ 4-bit 量化 |
| python quantize.py --method awq --bits 4 --model ./model --output ./model-awq |
| |
| # GPTQ 8-bit 量化 |
| python quantize.py --method gptq --bits 8 --model ./model --output ./model-gptq |
| |
| # 评估 PPL |
| python quantize.py --method evaluate --model ./quantized-model |
| |
| # 格式转换 |
| python quantize.py --method convert --from hf --to awq --model ./model --output ./model-awq |
| """ |
| ) |
| |
| parser.add_argument( |
| "--method", |
| type=str, |
| required=True, |
| choices=["gguf", "awq", "gptq", "evaluate", "convert"], |
| help="量化方法" |
| ) |
| parser.add_argument( |
| "--model", |
| type=str, |
| required=True, |
| help="输入模型路径" |
| ) |
| parser.add_argument( |
| "--output", |
| type=str, |
| default=None, |
| help="输出路径(默认自动生成)" |
| ) |
| parser.add_argument( |
| "--bits", |
| type=int, |
| default=4, |
| choices=[4, 8], |
| help="量化位数(默认: 4)" |
| ) |
| parser.add_argument( |
| "--quant-type", |
| type=str, |
| default="Q4_K_M", |
| help="GGUF 量化类型(默认: Q4_K_M)" |
| ) |
| parser.add_argument( |
| "--group-size", |
| type=int, |
| default=128, |
| help="量化分组大小(默认: 128)" |
| ) |
| parser.add_argument( |
| "--llama-cpp-path", |
| type=str, |
| default=None, |
| help="llama.cpp 安装路径(用于 GGUF 量化)" |
| ) |
| parser.add_argument( |
| "--from", |
| dest="from_format", |
| type=str, |
| choices=["gguf", "awq", "gptq", "hf"], |
| help="源格式(用于格式转换)" |
| ) |
| parser.add_argument( |
| "--to", |
| dest="to_format", |
| type=str, |
| choices=["gguf", "awq", "gptq", "hf"], |
| help="目标格式(用于格式转换)" |
| ) |
| |
| args = parser.parse_args() |
| |
| |
| if args.output is None: |
| model_name = os.path.basename(args.model) |
| if args.method == "gguf": |
| args.output = f"{model_name}-{args.quant_type}.gguf" |
| elif args.method == "convert": |
| args.output = f"{model_name}-{args.to_format}" |
| elif args.method == "evaluate": |
| print("错误: 评估模式需要指定 --output 参数") |
| sys.exit(1) |
| else: |
| args.output = f"{model_name}-{args.method}-{args.bits}bit" |
| |
| |
| vram_info = check_vram_availability() |
| if vram_info["available"]: |
| print(f"GPU: {vram_info['device']}") |
| print(f"VRAM: {vram_info['free_vram_gb']:.2f}GB / {vram_info['total_vram_gb']:.2f}GB") |
| else: |
| print("⚠️ 未检测到 GPU,将使用 CPU(速度较慢)") |
| |
| |
| if os.path.exists(args.model): |
| original_size = get_model_size(args.model) |
| print(f"原始模型大小: {original_size:.2f} GB") |
| |
| |
| try: |
| if args.method == "gguf": |
| result = quantize_gguf( |
| args.model, |
| args.output, |
| quant_type=args.quant_type, |
| llama_cpp_path=args.llama_cpp_path |
| ) |
| elif args.method == "awq": |
| result = quantize_awq( |
| args.model, |
| args.output, |
| bits=args.bits, |
| group_size=args.group_size |
| ) |
| elif args.method == "gptq": |
| result = quantize_gptq( |
| args.model, |
| args.output, |
| bits=args.bits, |
| group_size=args.group_size |
| ) |
| elif args.method == "evaluate": |
| ppl = evaluate_perplexity(args.model) |
| print(f"\n最终 PPL: {ppl:.2f}") |
| result = None |
| elif args.method == "convert": |
| if not args.from_format or not args.to_format: |
| print("错误: 格式转换需要指定 --from 和 --to 参数") |
| sys.exit(1) |
| result = convert_format( |
| args.model, |
| args.output, |
| args.from_format, |
| args.to_format, |
| bits=args.bits, |
| quant_type=args.quant_type |
| ) |
| |
| |
| if result and os.path.exists(result): |
| quantized_size = get_model_size(result) |
| print(f"\n量化后模型大小: {quantized_size:.2f} GB") |
| if os.path.exists(args.model): |
| compression = (1 - quantized_size / original_size) * 100 |
| print(f"压缩率: {compression:.1f}%") |
| |
| except QuantizationError as e: |
| print(f"\n❌ 量化失败: {e}", file=sys.stderr) |
| sys.exit(1) |
| except Exception as e: |
| print(f"\n❌ 发生未预期的错误: {e}", file=sys.stderr) |
| import traceback |
| traceback.print_exc() |
| sys.exit(1) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|