File size: 23,251 Bytes
eed9ef6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 | #!/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()
|