lxcxjxhx's picture
Upload hos_optimizer/quantize.py with huggingface_hub
eed9ef6 verified
Raw
History Blame Contribute Delete
23.3 kB
#!/usr/bin/env python3
"""
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
# 8GB VRAM 优化配置
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}")
# 检查 llama-quantize 工具
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"
)
# 转换模型为 GGUF 格式
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:
# 保存为 GGUF 兼容格式
temp_model_path = os.path.join(tmpdir, "model")
model.save_pretrained(temp_model_path)
tokenizer.save_pretrained(temp_model_path)
# 使用 llama.cpp 转换工具
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
# 检查 VRAM 并优化配置
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
# GPTQ 量化配置
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="", # PPL 计算不需要数据集文件
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")
# 使用参考文本计算 PPL
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("数据集中没有有效的文本样本")
# 计算 PPL
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 格式"""
# 这里需要实现具体的转换逻辑
# 通常需要使用 llama.cpp 的转换工具
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 信息
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()