第2021篇:LLM推理量化实战——INT8和INT4的工程权衡
第2021篇:LLM推理量化实战——INT8和INT4的工程权衡
适读人群:需要在有限显存下部署LLM的工程师 | 阅读时长:约18分钟 | 核心价值:理解量化的原理,掌握不同量化方案的选型和实施方法
我们的服务器只有一块24GB的4090,要跑一个需要16GB的Qwen2-7B模型,理论上可行——但加上KV Cache的显存需求,实际运行时经常OOM。
量化是解决这个问题的关键手段。把模型从FP16量化到INT4,显存从14GB降到4GB,突然很宽松了。
但量化不是免费的午餐,它有代价。这篇文章就说清楚代价是什么,怎么权衡。
量化的基本原理
模型权重默认以FP16(16位浮点数)存储。量化就是把这些数字压缩到更低精度:
FP16: 范围约±65504,精度约3位有效小数
INT8: 范围-128到127,精度1/256
INT4: 范围-8到7,精度1/16最简单的量化方式:找到每一层权重的最大值和最小值,把整个范围等分成256份(INT8)或16份(INT4),然后把每个权重映射到最近的格子里。
问题是:神经网络的权重分布不均匀。大多数权重集中在靠近0的小范围内,但偶尔有几个极端值(outlier)。如果按最大最小值线性映射,那几个outlier会把大多数权重压缩到很小的区间里,损失大量精度。
这就是不同量化算法要解决的核心问题。
主流量化方案对比
GPTQ是最早被广泛使用的LLM量化方案,核心思路是对每一层的量化误差做二阶补偿——量化某一个权重时,同时调整同层其他权重来抵消这个误差。效果好,但量化过程慢(需要几小时到几天)。
AWQ(Activation-aware Weight Quantization)是2023年提出的方案,发现网络中只有一小部分权重(约1%)对输出影响很大,通过识别并保护这些重要权重,在4-bit量化下达到接近FP16的效果,而且量化速度比GPTQ快很多。目前是GPU推理场景的首选。
GGUF是llama.cpp用的格式,优势是支持CPU+GPU混合推理——当GPU显存不够时,可以把部分层放到内存甚至磁盘上。Ollama就是基于这个格式。
量化模型的加载和使用
# 方式1:直接使用预量化模型(推荐,省时间)
# HuggingFace上有大量现成的AWQ/GPTQ量化版本
# 比如 Qwen/Qwen2-7B-Instruct-AWQ
from transformers import AutoModelForCausalLM, AutoTokenizer
from awq import AutoAWQForCausalLM
# 加载AWQ量化模型
model = AutoAWQForCausalLM.from_quantized(
"Qwen/Qwen2-7B-Instruct-AWQ",
fuse_layers=True, # 融合层,提升推理速度
trust_remote_code=True
)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-7B-Instruct-AWQ")
# 方式2:自己做量化(当没有现成量化版本时)
from awq import AutoAWQForCausalLM
# 加载原始FP16模型
model = AutoAWQForCausalLM.from_pretrained(
"Qwen/Qwen2-7B-Instruct",
device_map="auto",
torch_dtype=torch.float16
)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-7B-Instruct")
# AWQ量化配置
quant_config = {
"zero_point": True, # 使用零点量化,精度更好
"q_group_size": 128, # 分组大小,越小精度越好但压缩率越低
"w_bit": 4, # 4-bit量化
"version": "GEMM" # 计算内核版本,GEMM适合生产部署
}
# 执行量化(需要少量校准数据)
model.quantize(tokenizer, quant_config=quant_config)
# 保存量化模型
model.save_quantized("./Qwen2-7B-AWQ-local")
tokenizer.save_pretrained("./Qwen2-7B-AWQ-local")
print("量化完成!")不同量化精度的效果对比
我用一个中文综合测试集对Qwen2-7B做了不同精度的量化测试:
// Java端的测试框架(调用不同精度的模型端点对比效果)
@Service
@RequiredArgsConstructor
public class QuantizationEvalService {
// 三个端点分别跑不同精度的模型
@Qualifier("fp16Client")
private final ChatClient fp16Client;
@Qualifier("int8Client")
private final ChatClient int8Client;
@Qualifier("int4AwqClient")
private final ChatClient int4AwqClient;
private final ChatClient judgeClient; // GPT-4o作为评判
/**
* 同一问题用三种精度回答,评估质量差异
*/
public QuantCompareResult compare(String question, String referenceAnswer) {
String fp16Answer = fp16Client.prompt().user(question).call().content();
String int8Answer = int8Client.prompt().user(question).call().content();
String int4Answer = int4AwqClient.prompt().user(question).call().content();
// 用LLM-as-Judge评分
double fp16Score = judgeScore(question, referenceAnswer, fp16Answer);
double int8Score = judgeScore(question, referenceAnswer, int8Answer);
double int4Score = judgeScore(question, referenceAnswer, int4Answer);
return QuantCompareResult.builder()
.question(question)
.fp16Score(fp16Score)
.int8Score(int8Score)
.int4Score(int4Score)
.int8RelativeDrop((fp16Score - int8Score) / fp16Score)
.int4RelativeDrop((fp16Score - int4Score) / fp16Score)
.build();
}
private double judgeScore(String question, String expected, String actual) {
String prompt = """
请评估AI回答的质量(0-10分):
问题:%s
参考答案:%s
AI回答:%s
只输出数字:
""".formatted(question, expected, actual);
try {
String response = judgeClient.prompt().user(prompt).call().content().trim();
return Double.parseDouble(response);
} catch (NumberFormatException e) {
return 5.0;
}
}
}测试结果(100道中文综合题,包括推理、问答、代码):
| 精度 | 显存占用 | 推理速度 | 质量评分 | 质量损失 |
|---|---|---|---|---|
| FP16 | 14.2GB | 1850 t/s | 8.65 | 基线 |
| INT8(GPTQ) | 7.8GB | 1980 t/s | 8.58 | -0.8% |
| INT4(AWQ) | 4.1GB | 2250 t/s | 8.41 | -2.8% |
| INT4(GGUF Q4_K_M) | 4.5GB | 1620 t/s | 8.32 | -3.8% |
几个有意思的发现:
INT8的质量损失几乎可以忽略(0.8%),但只节省了44%的显存。对于本来就有足够显存的情况,INT8是最安全的选择。
AWQ INT4的速度反而比FP16快(2250 vs 1850 t/s)。这是因为4-bit权重大幅减少了显存带宽压力,而GPU计算的瓶颈往往是显存带宽,不是计算单元。
GGUF格式在GPU上比AWQ慢。GGUF的优势是兼容性,不是GPU推理速度,在纯GPU场景下AWQ更合适。
选型决策
/**
* 量化方案选型辅助(根据场景输出建议)
*/
public class QuantizationAdvisor {
public static QuantizationRecommendation recommend(QuantizationRequirement req) {
// 场景1:GPU显存够用,不需要量化
if (req.getAvailableGpuMemoryGb() >= req.getModelSizeFp16Gb() * 1.3) {
return QuantizationRecommendation.builder()
.scheme("FP16")
.reason("显存充足,无需量化,保持最高质量")
.build();
}
// 场景2:显存勉强够INT8
if (req.getAvailableGpuMemoryGb() >= req.getModelSizeFp16Gb() * 0.6) {
return QuantizationRecommendation.builder()
.scheme("GPTQ-INT8 或 bitsandbytes INT8")
.reason("INT8精度损失极小(<1%),是最安全的量化选项")
.command("--load-in-8bit 或 --quantization gptq")
.build();
}
// 场景3:只够跑INT4
if (req.getAvailableGpuMemoryGb() >= req.getModelSizeFp16Gb() * 0.3) {
if (req.isPrioritizeSpeed()) {
return QuantizationRecommendation.builder()
.scheme("AWQ INT4")
.reason("速度最快,精度损失约2-3%,适合延迟敏感场景")
.command("--quantization awq")
.build();
} else {
return QuantizationRecommendation.builder()
.scheme("GPTQ INT4")
.reason("精度略优于AWQ,适合质量优先场景")
.command("--quantization gptq")
.build();
}
}
// 场景4:GPU显存不够,需要CPU辅助
if (req.isAllowCpuOffload()) {
return QuantizationRecommendation.builder()
.scheme("GGUF(通过Ollama)")
.reason("支持CPU+GPU混合推理,但速度慢于纯GPU方案")
.command("ollama pull model:q4_k_m")
.build();
}
// 场景5:实在不够,考虑更小的模型
return QuantizationRecommendation.builder()
.scheme("换更小的模型(如3B规格)")
.reason(String.format(
"现有显存%.1fGB不足以运行当前模型的任何量化版本",
req.getAvailableGpuMemoryGb()))
.build();
}
}量化模型的vLLM部署
量化模型和vLLM集成非常简单:
# 部署AWQ量化模型(性能最好)
python -m vllm.entrypoints.openai.api_server \
--model ./Qwen2-7B-AWQ-local \
--quantization awq \
--gpu-memory-utilization 0.95 \ # 量化后显存充裕,可以设高一点
--max-model-len 8192 \
--enable-prefix-caching \
--port 8080
# 部署GPTQ量化模型
python -m vllm.entrypoints.openai.api_server \
--model ./Qwen2-7B-GPTQ \
--quantization gptq \
--gpu-memory-utilization 0.90 \
--port 8080有一个坑需要注意:不是所有量化模型都和vLLM完全兼容。偶尔会遇到"算子不支持"的错误,通常是因为量化时使用了不同版本的库。遇到这种情况,去HuggingFace上找vllm-specific的量化版本,比如model-name-awq或model-name-gptq-vllm,这些一般经过了兼容性测试。
量化的本质是用"精度损失"换"空间和速度"。对大多数企业应用场景,AWQ INT4的2-3%精度损失是完全可以接受的,换来的是3-4倍的显存压缩和更高的推理速度。
关键是要在你的实际业务数据上测试一遍,而不是只看通用benchmark。不同任务对量化的敏感程度差异很大。
