大模型INT8、FP8与INT4量化部署方案选择

描述

 

同一个大模型,使用 BF16、FP8、INT8 或 INT4 部署,显存占用、吞吐、延迟和回答质量可能完全不同。实际运维中最常见的错误,是只看模型文件大小或“4 bit 更省显存”就做决定,却没有区分权重量化、激活量化、KV Cache 精度、计算内核和硬件支持。结果可能是模型能加载但速度更慢,显存省下来了却被 KV Cache 吃满,或者常规问答看不出问题,代码、数学、工具调用和长上下文能力已经明显回退。

量化方案选择不是单纯的模型问题,而是一个完整的部署决策:模型结构决定权重和 KV Cache 规模,硬件决定哪些低精度算子真正加速,推理框架决定量化格式是否受支持,业务流量决定更看重首 Token 延迟还是批量吞吐,质量门槛决定能够接受多大损失。

本文面向使用 NVIDIA GPU 和常见开源推理框架的部署环境,重点说明 INT8、FP8 与 INT4 的工程差异、检查方法、验证脚本、灰度和回滚。框架参数会随版本变化,执行前必须在当前环境运行 --help 并核对本地版本;不能把其他版本的示例参数直接用于生产。

文中占位符说明:

  • <模型目录> :本地模型目录或经过批准的模型标识。
  • <模型名称> :对外服务使用的模型名。
  • :目标 GPU,例如 0 或 0,1
  • :OpenAI 兼容接口根地址,例如内网服务地址,不包含结尾 /v1 时需按示例拼接。
  • :服务要求的访问凭证;不要写入脚本仓库或 shell history。
  • <提示词文件> :经过脱敏的本地 JSONL 测试集。
  • <输出目录> :容量充足、受访问控制的测试结果目录。
  • <服务名> :systemd、容器或编排平台中的实际服务名。

一、先把“几 bit”说清楚

“INT8 模型”“FP8 模型”和“INT4 模型”只是简写,不能完整描述计算路径。上线评审时至少要回答以下问题:

  1. 权重是什么精度,例如 W8、W4 或 FP8。
  2. 激活是什么精度,例如 A16、A8 或 FP8。
  3. 矩阵乘法是否使用硬件原生低精度 Tensor Core,还是运行时先反量化到 FP16/BF16。
  4. 累加精度是什么,通常不会直接用 4 bit 累加。
  5. KV Cache 是 FP16/BF16、FP8 还是其他格式。
  6. 量化是离线校准得到的静态模型,还是加载时动态量化。
  7. 哪些层保持高精度,例如 embedding、lm_head、归一化层或异常通道。

只有这些信息明确后,显存和性能对比才有意义。

1. INT8

INT8 使用 8 位整数表示量化后的值,通常配合 scale,有些格式还使用 zero-point。常见路径包括:

  • W8A16 :权重 8 bit,激活仍为 FP16/BF16。主要节省权重显存,计算是否加速取决于内核。
  • W8A8 :权重和激活都以 8 bit 参与低精度计算,通常需要校准或平滑异常通道。
  • 仅部分线性层量化 :敏感层保留更高精度,以控制质量损失。

INT8 数值范围较大,工程生态成熟,适合没有可靠 FP8 路径的硬件或框架,也适合质量门槛较高但 BF16 显存不足的场景。它与 FP8 同为 8 bit,原始权重字节数相近,但动态范围、精度分布、校准方法和硬件内核完全不同,不能只按位宽认为两者等价。

2. FP8

FP8 是 8 位浮点格式,常见编码包括 E4M3 和 E5M2。前者通常提供更多尾数精度,后者提供更大指数范围;实际权重、激活和 KV Cache采用哪种格式,由模型、硬件和推理框架共同决定。

FP8 的主要优势是:在支持原生 FP8 Tensor Core 和成熟内核的 GPU 上,可以同时降低权重/激活带宽和提高计算吞吐。相比 INT8,它对大模型异常值和动态范围的处理路径不同,很多现代数据中心 GPU 的推理栈会把 FP8 作为高性能方案。

但“GPU 支持 FP8”不代表当前模型一定能高效运行。还要检查:

  • 驱动、CUDA、PyTorch 和推理框架组合是否受支持。
  • 模型架构是否有对应 FP8 kernel,特别是 MoE、GQA、特殊 attention 或自定义算子。
  • 模型是原生/离线 FP8 checkpoint,还是运行时动态量化。
  • scale 粒度是 per-tensor、per-channel 还是 per-block。
  • 是否存在回退算子,导致某些层频繁转换精度。

3. INT4

INT4 通常指 4 位权重量化,例如 W4A16 或 W4A8。权重会以 packed 形式存储,计算时由专用 kernel 解包并结合 scale/zero-point 反量化或直接进行低精度矩阵计算。常见部署格式包括 AWQ、GPTQ 等,但同名格式也可能因实现、group size、对称性和版本而不兼容。

INT4 的显存节省最明显,适合显存受限、需要提高单卡并发或合并多实例的场景。代价是:

  • 对量化算法、校准数据和 group size 更敏感。
  • 小 batch 下解包与反量化开销可能抵消带宽收益。
  • 某些硬件缺少高效 W4 内核,模型反而比 FP8/INT8 慢。
  • 数学、代码、长上下文、罕见语言和结构化输出更容易暴露质量损失。
  • 不同框架对 AWQ/GPTQ 的支持范围并不一致。

还要区分 NF4。NF4 是 bitsandbytes 中常用于 QLoRA 的 4 位浮点式量化编码,不等同于通用“INT4 推理格式”。一个模型能用 NF4 做低显存加载或训练,不代表生产推理框架能用同一格式获得最佳吞吐。

二、不要只算权重:显存由五部分组成

1. 权重显存的粗略下限

对于 N 个参数的稠密模型,未考虑元数据时:


			   textBF16/FP16 权重约为 N × 2 字节 FP8/INT8 权重约为 N × 1 字节 INT4 权重约为 N × 0.5 字节 

例如 27B 参数模型的原始权重下限约为:

  • BF16:54 GB。
  • FP8/INT8:27 GB。
  • INT4:13.5 GB。

这是十进制 GB 的理论估算,不是实际 nvidia-smi 占用。实际部署还会增加 scale、zero-point、量化分组元数据、未量化层、CUDA context、临时 workspace 和内存碎片。

对于 MoE,权重显存按全部已加载专家参数估算,不能只按每个 Token 激活的参数量计算。一个总参数 122B、激活参数远小于 122B 的 MoE,计算量可能接近较小模型,但存储和加载仍需容纳所有专家权重,除非框架采用专家卸载或分层加载。

2. KV Cache

自回归生成会为每层保存 Key 和 Value。对使用 GQA/MQA 的模型,可用下面的近似式估算单 Token KV Cache:


			   text每 Token KV 字节数 ≈ 2 × 层数 × KV Head 数 × Head Dim × KV 元素字节数 

总 KV Cache 还要乘以并发序列和实际缓存 Token 数。这里的 2 代表 Key 与 Value。Paged Attention 会按 block 管理缓存,实际分配还受块大小、碎片、前缀缓存和调度策略影响。

这解释了为什么一个 FP8 权重模型在短请求下很省显存,长上下文高并发时仍然 OOM。权重量化只压缩静态权重;如果 KV Cache 保持 BF16,长度翻倍或并发翻倍仍会近似线性增加缓存需求。

3. 激活与临时 workspace

Prefill 长提示词、较大 batch、CUDA Graph、MoE 路由、all-reduce 和量化 kernel 都可能申请临时空间。某些框架启动时会预留大块显存作为 KV Cache 池,所以 nvidia-smi 看起来几乎占满并不一定是泄漏。判断要结合框架日志中的可用 KV block、最大并发估算和请求期间的峰值。

4. 运行时与通信开销

Tensor Parallel 会把权重切到多卡,但每张卡仍有 CUDA context、NCCL buffer、激活和部分复制张量。多卡总显存不能简单除以卡数;跨卡通信还可能成为吞吐瓶颈。量化后计算变快,通信占比可能反而更高。

5. 宿主机内存和模型加载峰值

量化 checkpoint 加载时可能先创建高精度临时张量,离线转换更可能同时保留原权重和量化权重。必须检查 RAM、共享内存、临时目录和磁盘空间。模型最终能放进 80 GB 显存,不代表 64 GB 系统内存就能稳定完成加载。

三、先盘点硬件和软件环境

量化选型前先保存可复现的环境基线:


			   bashnvidia-smi --query-gpu=index,name,uuid,driver_version,memory.total    --format=csv,noheader nvidia-smi topo -m python3 --version 

再检查 Python 推理环境:


			   bashpython3 - <<'PY' import json import platform import torch data = {     "python": platform.python_version(),     "torch": torch.__version__,     "torch_cuda": torch.version.cuda,     "cuda_available": torch.cuda.is_available(),     "device_count": torch.cuda.device_count(),     "devices": [], } for index in range(torch.cuda.device_count()):     props = torch.cuda.get_device_properties(index)     data["devices"].append(         {             "index": index,             "name": props.name,             "compute_capability": [props.major, props.minor],             "memory_bytes": props.total_memory,         }     ) print(json.dumps(data, ensure_ascii=False, indent=2)) PY 

框架版本也要固定:


			   bashpython3 - <<'PY' from importlib.metadata import PackageNotFoundError, version for package in (     "transformers",     "accelerate",     "bitsandbytes",     "vllm",     "torchao", ):     try:         print(f"{package}=={version(package)}")     except PackageNotFoundError:         print(f"{package}: not installed") PY 

“not installed” 只是环境事实,不应为了试验在生产环境直接 pip install。应在独立虚拟环境或版本化容器镜像中安装,锁定依赖并经过安全扫描。

对于 NVIDIA B200/B300 等 Blackwell 数据中心 GPU,FP8 是值得优先验证的路径;但是否加速仍取决于当前框架和具体模型 kernel。不能仅凭 GPU 名称跳过兼容性测试。

四、检查模型到底是什么格式

不要相信目录名里的 FP8INT4 字样。先读模型配置和量化元数据:


			   bashjq '{   model_type,   architectures,   torch_dtype,   hidden_size,   num_hidden_layers,   num_attention_heads,   num_key_value_heads,   quantization_config }' <模型目录>/config.json 

再列出文件大小:


			   bashdu -sh <模型目录> find <模型目录> -maxdepth 1 -type f    -printf '%f %s bytes '    | sort 

如果使用 safetensors,可以只读取 header/元数据而不加载全部权重。安装了 safetensors 时执行:


			   bashpython3 - <<'PY' from pathlib import Path from safetensors import safe_open model_dir = Path("<模型目录>") files = sorted(model_dir.glob("*.safetensors")) if not files:     raise SystemExit("no safetensors files found") dtype_counts = {} tensor_count = 0 for path in files:     with safe_open(path, framework="pt", device="cpu") as handle:         for key in handle.keys():             tensor = handle.get_slice(key)             dtype = str(tensor.get_dtype())             dtype_counts[dtype] = dtype_counts.get(dtype, 0) + 1             tensor_count += 1 print("tensor_count:", tensor_count) print("dtype_counts:", dtype_counts) PY 

即使权重张量存储为 FP8 或 INT8,也可能需要额外 scale tensor;有些 AWQ/GPTQ 文件会以 INT32 保存打包后的 4 bit 值。因此不能仅根据张量 dtype 判定实际计算精度,仍要结合 quantization_config 和框架加载日志。

还应校验模型文件完整性。若制品库提供 SHA-256 清单:


			   bashcd <模型目录> sha256sum -c <校验和文件> 

校验失败时必须停止部署。量化模型来源应与原模型许可证、供应链审批和安全扫描保持一致,不能从未知网盘下载所谓“已量化极速版”。

五、三类方案的工程对比

维度 INT8 FP8 INT4
原始权重位宽 8 bit 8 bit 4 bit
常见路径 W8A16、W8A8 W8A8/FP8 动态或 checkpoint W4A16、W4A8
显存节省 中等 中等 最大
质量风险 通常较低到中等 通常较低到中等 中等到较高,依模型而定
校准依赖 W8A8 常需要 路径不同,动态/静态均可能 AWQ/GPTQ 通常依赖校准
硬件依赖 INT8 内核成熟度 原生 FP8 硬件与框架 高效 W4 kernel 与格式支持
小 batch 延迟 需实测 支持良好时有优势 可能受解包开销影响
大 batch 吞吐 常有优势 支持良好时通常有优势 受 kernel、内存带宽和调度影响
格式兼容风险 中等 中等 较高
首选场景 稳健压缩、兼容旧路径 新数据中心 GPU 高性能服务 显存刚性受限、单卡部署或高密度实例

表中的“质量风险”不是保证。任何方案都必须和 BF16/FP16 基线做相同数据、相同采样参数的评测。

六、INT8 什么时候更合适

优先考虑 INT8 的场景:

  • GPU/推理框架的 INT8 路径比 FP8 更成熟。
  • 业务对质量敏感,希望先从 8 bit 压缩开始。
  • 已有经过验证的 W8A8 校准流程。
  • 模型或特定算子不支持 FP8,但支持 INT8 kernel。
  • 需要在 CPU 或不同硬件后端之间保持相对统一的量化策略。

Transformers 与 bitsandbytes 可以用于快速验证 8 bit 权重加载。以下代码是真实 API 示例,但它更适合功能验证和单机推理,不代表生产吞吐最优:


			   pythonimport torch from transformers import (     AutoModelForCausalLM,     AutoTokenizer,     BitsAndBytesConfig, ) model_path = "<模型目录>" quantization_config = BitsAndBytesConfig(load_in_8bit=True) tokenizer = AutoTokenizer.from_pretrained(     model_path,     trust_remote_code=False, ) model = AutoModelForCausalLM.from_pretrained(     model_path,     device_map="auto",     torch_dtype=torch.float16,     quantization_config=quantization_config,     trust_remote_code=False, ) inputs = tokenizer("请简要说明 TCP 三次握手。", return_tensors="pt") inputs = {key: value.to(model.device) for key, value in inputs.items()} with torch.inference_mode():     output = model.generate(         **inputs,         max_new_tokens=128,         do_sample=False,     ) print(tokenizer.decode(output[0], skip_special_tokens=True)) 

trust_remote_code=False 是更保守的默认值;如果模型架构必须运行仓库自定义代码,应先进行代码审计并固定 revision,不能直接改成 True 绕过错误。

bitsandbytes 的 load_in_8bit=True 主要是权重量化加载路径,不应直接写成“W8A8 原生 INT8 计算”。要从框架日志、profiler 或文档确认实际 kernel。

七、FP8 什么时候更合适

优先考虑 FP8 的场景:

  • 使用原生支持 FP8 的数据中心 GPU。
  • 当前推理框架对模型架构有成熟 FP8 kernel。
  • 希望在接近 8 bit 权重显存的同时获得高吞吐。
  • 模型供应方提供经过验证的 FP8 checkpoint 和量化元数据。
  • BF16 模型能运行,但需要为 KV Cache 和并发释放更多空间。

以 vLLM 的 OpenAI 兼容服务为例,当前安装版本若在 vllm serve --help 中列出 fp8,可以在隔离环境验证动态 FP8:


			   bashCUDA_VISIBLE_DEVICES=  vllm serve <模型目录>    --host 127.0.0.1    --port 8000    --served-model-name <模型名称>    --dtype auto    --quantization fp8    --max-model-len 8192    --gpu-memory-utilization 0.85 

这不是可直接用于生产的完整安全配置:示例只监听本机,尚未配置认证、TLS、进程管理、日志轮转和健康探测。--gpu-memory-utilization 是当前实例可使用的显存比例提示,不是模型精度或系统全局限额。值过高会降低峰值余量,值过低则减少 KV Cache 和并发。

如果模型本身已经是框架支持的 FP8 checkpoint,框架可能从 quantization_config 自动识别,不需要重复传 --quantization fp8。强制指定错误量化方法可能导致加载失败或重复处理。正确流程是先检查配置,再看启动日志是否明确识别了量化格式。

FP8 KV Cache 与 FP8 权重是两件事

在当前 vLLM 版本的 --help 明确支持时,可以单独测试:


			   bashvllm serve <模型目录>    --host 127.0.0.1    --port 8000    --served-model-name <模型名称>    --dtype auto    --kv-cache-dtype fp8    --max-model-len 8192    --gpu-memory-utilization 0.85 

这可能显著降低长上下文高并发的 KV Cache 占用,但需要独立验证长文本召回、注意力稳定性、工具调用参数和生成质量。不要因为权重使用 FP8 就自动假设 KV Cache 也是 FP8。

八、INT4 什么时候值得用

优先考虑 INT4 的场景:

  • BF16/FP8/INT8 权重确实放不进目标 GPU。
  • 需要在单卡上部署更大模型,避免跨卡通信。
  • 业务愿意用系统化评测换取更高部署密度。
  • 已确认推理框架支持该 AWQ/GPTQ 模型架构和量化配置。
  • 请求以中小 batch 为主,并已证明目标 kernel 不会因解包开销变慢。

以 vLLM 加载 AWQ checkpoint 为例,先确认模型 config.json 中有正确量化配置,再启动隔离测试实例:


			   bashCUDA_VISIBLE_DEVICES=  vllm serve <模型目录>    --host 127.0.0.1    --port 8000    --served-model-name <模型名称>    --quantization awq    --dtype half    --max-model-len 8192    --gpu-memory-utilization 0.85 

GPTQ 模型在当前版本明确支持时,可使用对应方法:


			   bashCUDA_VISIBLE_DEVICES=  vllm serve <模型目录>    --host 127.0.0.1    --port 8000    --served-model-name <模型名称>    --quantization gptq    --dtype half    --max-model-len 8192    --gpu-memory-utilization 0.85 

必须先运行:


			   bashvllm serve --help | less 

确认本机版本支持 awq 或 gptq。模型文件虽然能被某个库加载,也不代表 vLLM 的当前 kernel 支持其 group size、bits、zero-point、对称方式和模型架构。

4 bit Transformers 功能验证

bitsandbytes 的 NF4 路径可用于低显存实验,但要明确它不是 AWQ/GPTQ INT4 服务的等价替代:


			   pythonimport torch from transformers import AutoModelForCausalLM, BitsAndBytesConfig model_path = "<模型目录>" quantization_config = BitsAndBytesConfig(     load_in_4bit=True,     bnb_4bit_quant_type="nf4",     bnb_4bit_compute_dtype=torch.bfloat16,     bnb_4bit_use_double_quant=True, ) model = AutoModelForCausalLM.from_pretrained(     model_path,     device_map="auto",     quantization_config=quantization_config,     trust_remote_code=False, ) 

该示例没有声称会得到某个吞吐数字。是否适合生产,应由服务框架、连续批处理、并发稳定性和真实 kernel 测试决定。

九、离线量化:校准数据比“跑通命令”更重要

AWQ、GPTQ、SmoothQuant 类方法的实现和参数会随工具版本变化,本文不提供一个看似通用但可能不兼容的转换命令。无论使用哪个经过批准的量化工具,流程都应具备以下约束:

  1. 固定原始模型版本、tokenizer、量化工具和依赖版本。
  2. 保存完整量化配置,包括 bits、group size、是否对称、zero-point、跳过层和校准序列长度。
  3. 校准数据覆盖真实语言、指令长度、代码、数学、结构化输出和领域文本。
  4. 删除账号、密钥、个人信息和生产机密,禁止将原始生产请求直接交给外部量化服务。
  5. 校准样本不与最终评测集重合,避免结果失真。
  6. 输出到新目录,不覆盖 BF16 原模型。
  7. 生成 checksum、模型卡和质量报告。
  8. 在隔离环境先加载和推理,再进入性能与质量评测。

group size 较小通常可以提供更细粒度 scale,但会增加元数据和计算开销;较大 group size 更省元数据,却可能放大量化误差。不能仅根据社区常用值选择,必须在目标模型和目标硬件上比较。

十、建立可比较的性能基线

1. 固定测试条件

比较 BF16、INT8、FP8 和 INT4 时,必须固定:

  • 相同模型版本与 tokenizer。
  • 相同 GPU、功率模式、驱动和框架版本。
  • 相同 tensor parallel、pipeline parallel 和实例数量。
  • 相同输入 Token 分布、输出上限和采样参数。
  • 相同并发阶梯和预热次数。
  • 相同最大上下文与 KV Cache 配置。
  • 相同前缀缓存、chunked prefill 和 CUDA Graph 设置。

否则“FP8 比 INT4 快”可能只是 batch、并发或 cache 设置不同。

2. 记录 GPU 指标

下面脚本每秒记录一次显存、利用率、功率和温度,不修改 GPU 配置:


			   bash#!/usr/bin/env bash set -euo pipefail OUTPUT_DIR="<输出目录>" GPU_QUERY_FILE="${OUTPUT_DIR}/gpu-metrics.csv" install -d -m 0750 "${OUTPUT_DIR}" nvidia-smi    --query-gpu=timestamp,index,name,memory.used,memory.total,utilization.gpu,power.draw,temperature.gpu    --format=csv    --loop=1    > "${GPU_QUERY_FILE}" 

脚本会持续运行,需要在另一个终端用正常的 SIGINT 停止。不要对 nvidia-smi 进程使用不受控的批量 pkill,以免终止其他人的监控任务。

3. 验证 OpenAI 兼容接口


			   bashcurl --fail-with-body --silent --show-error    -H "Authorization: Bearer "    -H 'Content-Type: application/json'    "/v1/chat/completions"    -d '{     "model": "<模型名称>",     "messages": [       {"role": "user", "content": "请说明如何确认 Linux 文件系统是否只读挂载。"}     ],     "temperature": 0,     "max_tokens": 256   }'    | jq . 

不要在命令行直接放真实长期 API 密钥,因为它会进入 shell history 和进程参数。生产测试应从受控环境变量或密钥管理系统读取,示例中的占位符只用于说明请求结构。

4. 并发延迟与吞吐测试脚本

下面脚本读取本地 JSONL,每行格式为 {"prompt":"..."},使用非流式接口统计端到端延迟、成功率和服务返回的完成 Token。它不统计 TTFT,因此不能替代流式性能工具。


			   python#!/usr/bin/env python3 import argparse import concurrent.futures import json import statistics import time import urllib.error import urllib.request defpercentile(values, fraction):     ifnot values:         returnNone     ordered = sorted(values)     index = min(len(ordered) - 1int((len(ordered) - 1) * fraction))     return ordered[index] defrequest_once(api_url, api_key, model, prompt, timeout):     body = json.dumps(         {             "model": model,             "messages": [{"role""user""content": prompt}],             "temperature"0,             "max_tokens"256,         },         ensure_ascii=False,     ).encode("utf-8")     request = urllib.request.Request(         f"{api_url.rstrip('/')}/v1/chat/completions",         data=body,         headers={             "Authorization"f"Bearer {api_key}",             "Content-Type""application/json",         },         method="POST",     )     started = time.perf_counter()     try:         with urllib.request.urlopen(request, timeout=timeout) as response:             payload = json.loads(response.read())         elapsed = time.perf_counter() - started         usage = payload.get("usage", {})         return {             "ok"True,             "latency": elapsed,             "completion_tokens"int(usage.get("completion_tokens"0)),         }     except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as error:         return {             "ok"False,             "latency": time.perf_counter() - started,             "error"repr(error),         } defmain():     parser = argparse.ArgumentParser()     parser.add_argument("--api-url", required=True)     parser.add_argument("--api-key", required=True)     parser.add_argument("--model", required=True)     parser.add_argument("--prompts", required=True)     parser.add_argument("--concurrency"type=int, default=4)     parser.add_argument("--timeout"type=float, default=120.0)     args = parser.parse_args()     prompts = []     withopen(args.prompts, "r", encoding="utf-8"as handle:         for line_number, line inenumerate(handle, start=1):             ifnot line.strip():                 continue             item = json.loads(line)             ifnotisinstance(item.get("prompt"), str):                 raise ValueError(f"line {line_number}: missing string prompt")             prompts.append(item["prompt"])     ifnot prompts:         raise ValueError("no prompts loaded")     wall_start = time.perf_counter()     with concurrent.futures.ThreadPoolExecutor(         max_workers=args.concurrency     ) as executor:         futures = [             executor.submit(                 request_once,                 args.api_url,                 args.api_key,                 args.model,                 prompt,                 args.timeout,             )             for prompt in prompts         ]         results = [future.result() for future in futures]     wall_time = time.perf_counter() - wall_start     succeeded = [item for item in results if item["ok"]]     failed = [item for item in results ifnot item["ok"]]     latencies = [item["latency"for item in succeeded]     completion_tokens = sum(         item["completion_tokens"for item in succeeded     )     summary = {         "requests"len(results),         "succeeded"len(succeeded),         "failed"len(failed),         "wall_time_seconds": wall_time,         "requests_per_second"len(succeeded) / wall_time,         "completion_tokens": completion_tokens,         "completion_tokens_per_second": completion_tokens / wall_time,         "latency_mean_seconds": statistics.mean(latencies) if latencies elseNone,         "latency_p50_seconds": percentile(latencies, 0.50),         "latency_p95_seconds": percentile(latencies, 0.95),         "errors": [item.get("error"for item in failed[:10]],     }     print(json.dumps(summary, ensure_ascii=False, indent=2)) if __name__ == "__main__":     main() 

运行示例:


			   bashpython3 <压测脚本>.py    --api-url ''    --api-key ''    --model '<模型名称>'    --prompts '<提示词文件>'    --concurrency 8    --timeout 120 

这段脚本的吞吐按整个测试墙钟时间计算,适合做同条件相对比较。生产容量评估还应使用流式请求统计 TTFT、TPOT/ITL、排队时间、prefill/decode 吞吐,并执行稳定性 soak test。

5. 至少做阶梯并发

不能只测并发 1。建议在同一提示词分布下依次测 1、2、4、8、16 等阶梯,直到达到以下任一边界:

  • P95/P99 延迟超过 SLO。
  • 错误率或超时率上升。
  • KV Cache 不足,调度抢占明显。
  • GPU 利用率饱和但吞吐不再增长。
  • 功率、温度或主机 CPU/内存达到运行上限。

每个阶梯先预热,再记录固定时长;量化 kernel 首次编译、CUDA Graph 捕获和模型冷启动不能混入稳态结果。

十一、质量评测必须覆盖“量化敏感项”

1. 使用 BF16/FP16 作为基线

同一套测试至少对比:

  • BF16/FP16 基线。
  • 候选 INT8 或 FP8。
  • 候选 INT4。
  • 若使用 FP8 KV Cache,再单独作为一个变量。

采样参数要固定。评测确定性任务时使用 temperature = 0,并保持 chat template、system prompt、stop tokens、max tokens 和后处理一致。

2. 不只看通用平均分

量化误差可能集中在特定能力。测试集应包含:

  • 数学推理:最终答案、推理稳定性和长链条错误。
  • 代码:可执行率、单元测试通过率和语法完整性。
  • 多语言:中文、英文及业务实际语言。
  • 工具调用:JSON schema、参数类型、必填字段和重复调用率。
  • 结构化输出:JSON、YAML、SQL 和固定格式遵循。
  • 长上下文:不同位置的事实召回、跨段落推理和提示注入边界。
  • 领域任务:业务术语、检索增强回答和安全拒答。
  • 生成稳定性:重复、乱码、异常截断和 EOS 行为。

通用 benchmark 宏平均分相近,不代表生产无回退。比如 INT4 可能只在数学和代码上下降,而普通对话差异很小;业务若恰好依赖这两项,就不能用平均值掩盖。

3. 长上下文要分档测试

至少按短、中、长输入分档,例如目标最大上下文的 10%、50%、90%。每档都记录:

  • 是否成功完成请求。
  • TTFT、端到端延迟和输出速度。
  • GPU 峰值显存。
  • 目标事实召回率。
  • 是否出现注意力退化、重复或越界。

如果启用 FP8 KV Cache,长上下文评测是独立准入项,不能只复用权重量化的短题结果。

十二、以 B300 大显存场景为例的选择思路

在单卡约 280 GB 显存的 B300 环境中,27B 模型的 BF16 原始权重约 54 GB,通常没有“为了能加载而必须量化”的压力。此时应先把 BF16 作为质量和性能基线:

  • 若并发不高、质量最重要,优先 BF16。
  • 若需要更多 KV Cache 或更高吞吐,验证 FP8。
  • 只有在实例密度、功耗或多模型共卡有明确收益时,才进一步评估 INT4。

对于总参数约 122B 的 MoE 模型,BF16 原始权重约 244 GB,叠加运行时、未量化层和 KV Cache 后,单卡余量可能很紧。FP8 原始权重约 122 GB,往往更适合作为 B300 单卡高性能候选。INT4 原始权重约 61 GB,可释放更多并发空间,但必须用数学、代码、工具调用和长上下文评测证明质量可接受。

这里的数字只是参数位宽推算,不是承诺的实际显存,也不是实测吞吐。模型的量化元数据、专家实现、attention、KV Cache 和框架预留都会改变结果。

十三、常见“省显存但没变快”的原因

1. 内核发生回退

模型能加载,不代表使用了优化 kernel。某些层可能反量化到 FP16/BF16,再调用普通 GEMM;频繁的 dtype 转换和解包会抵消收益。启动日志、框架 profiler 和 Nsight Systems 才能证明实际路径。

2. batch 太小

小 batch、短输出时,调度、Python、网络和反量化开销占比高,INT4 不一定比 FP8 快。大 batch 下又可能受到 KV Cache 或算力限制,所以必须做阶梯并发。

3. 跨卡通信成为瓶颈

量化降低计算时间后,all-reduce 占比上升。若模型量化后能单卡运行,单卡可能比双卡 tensor parallel 延迟更低;但是否成立必须实测,不能只按卡数判断。

4. CPU 和请求处理成为瓶颈

Tokenizer、JSON 序列化、网关、日志、TLS 或 Python 调度都可能限制吞吐。观察 GPU 利用率低时,应同时采集 CPU、内存、网络和进程 profile,而不是继续降低精度。

5. KV Cache 或上下文配置不同

一个方案最大上下文为 8K,另一个为 32K,显存和并发没有可比性。gpu_memory_utilization、KV dtype、max model len 和并发上限必须统一。

6. 模型本身已经量化

对已量化 checkpoint 再传动态量化参数,可能导致不支持、重复量化或错误 kernel。必须先查看 quantization_config 和张量元数据。

十四、上线采用蓝绿方式,不原地覆盖

切换模型服务通常涉及重启、显存重新分配和请求中断,属于高风险变更。建议保留现有 BF16/FP8 实例作为蓝环境,新量化实例作为绿环境。

1. 影响范围

  • 模型加载期间 GPU 资源被占用。
  • 首次请求可能有冷启动延迟。
  • 输出质量和格式可能变化。
  • 网关切流可能影响长连接和流式请求。
  • OOM 或进程退出会造成 5xx、超时和重试放大。

2. 执行前检查

  • 固定模型 checksum、镜像 digest、配置和依赖版本。
  • 保留原服务清单和路由权重。
  • 验证 GPU 无其他任务,确认显存与功率余量。
  • 用脱敏测试集完成质量和负载评测。
  • 定义错误率、P95/P99、TTFT、吞吐和质量回滚阈值。
  • 确认 API schema、模型名、tokenizer 和 chat template 不变。
  • 准备现有实例健康检查和快速切回路径。

只读环境检查脚本示例:


			   bash#!/usr/bin/env bash set -euo pipefail MODEL_DIR="<模型目录>" test -d "${MODEL_DIR}" test -r "${MODEL_DIR}/config.json" jq '.model_type, .architectures, .torch_dtype, .quantization_config'  "${MODEL_DIR}/config.json" nvidia-smi --query-compute-apps=gpu_uuid,pid,process_name,used_memory    --format=csv du -sh "${MODEL_DIR}" df -h "${MODEL_DIR}" 

3. 灰度步骤

  1. 在独立端口或独立节点启动绿环境,不接生产流量。
  2. 完成健康检查、固定样本回归和阶梯并发。
  3. 导入极小比例的影子流量;若涉及用户内容,必须符合隐私和数据治理要求。
  4. 先切 1% 或内部账号,再逐级提升 5%、10%、25%、50%、100%。
  5. 每一档至少覆盖业务高峰和长请求,检查技术指标与质量抽检。
  6. 切换完成后保留蓝环境一段约定时间,不立即删除模型或镜像。

4. 执行后验证


			   bashcurl --fail-with-body --silent --show-error    -H "Authorization: Bearer "    "/v1/models"    | jq . 

同时验证:

  • 健康接口和模型列表正常。
  • 固定提示词输出没有乱码、空回复或异常重复。
  • HTTP 4xx/5xx、超时和取消率未超阈值。
  • TTFT、TPOT/ITL 和 P95/P99 满足 SLO。
  • GPU OOM、ECC、Xid、进程重启和 KV Cache 告警正常。
  • 工具调用 JSON、stop token 和最大输出长度与基线一致。

5. 明确回滚

量化服务异常时,回滚目标是路由,不是现场重新量化:

  1. 停止继续扩大绿环境流量。
  2. 将网关权重切回仍健康的蓝环境。
  3. 等待在途流式请求结束或按超时策略终止。
  4. 验证蓝环境错误率、延迟和质量恢复。
  5. 保留绿环境日志、配置和模型 checksum 用于复盘。
  6. 在确认无排查价值后,再按变更流程停止绿实例释放 GPU。

不要在故障中删除原模型文件、镜像或服务清单。停止服务和释放 GPU 会影响在途请求,执行前要确认路由已切走,并在执行后验证没有流量仍指向该实例。

十五、容量规划不能只给一个 QPS 数字

大模型服务的容量至少是输入长度、输出长度、并发和 SLO 的函数。建议按业务流量构建矩阵:

场景 输入 Token 输出 Token 并发 重点指标
短问答 排队、TPOT、吞吐
长文总结 TTFT、prefill、KV Cache
代码生成 decode 吞吐、质量
Agent 工具调用 短到中 波动 JSON 正确率、尾延迟
超长上下文 很长 OOM、召回、TTFT

每个量化方案都应输出:

  • 单卡/多卡拓扑。
  • 模型加载显存和稳态保留显存。
  • 各并发档的 P50/P95/P99。
  • TTFT、TPOT/ITL、输入与输出 Token 吞吐。
  • 成功率、OOM、超时和重启次数。
  • 功率与温度。
  • 质量评测差值和置信区间。
  • 可支持的最大上下文与并发组合。

没有输入输出长度分布的“每秒多少请求”很难用于生产规划。

十六、监控量化服务

不同推理框架和 exporter 的 Prometheus 指标名可能变化,以下监控维度以实际 exporter 暴露的指标为准:

  • 请求总数、成功数、4xx/5xx、取消和超时。
  • 排队请求数、运行请求数、抢占数。
  • TTFT、每输出 Token 延迟、端到端延迟。
  • prompt tokens/s、generation tokens/s。
  • KV Cache 使用率和可用 block。
  • GPU 显存、利用率、功率、温度和 ECC/Xid。
  • 模型进程重启、加载失败和 kernel fallback 日志。
  • 业务侧 JSON schema 失败、工具调用失败和答案抽检。

GPU 指标可由 DCGM exporter 等组件采集,但指标名以实际部署版本为准。告警应同时关联服务层和 GPU 层:GPU 利用率 100% 不一定是故障,如果吞吐和延迟仍在 SLO 内;GPU 利用率很低但队列很长,则可能是 CPU、调度、锁或网络瓶颈。

十七、选型决策建议

可以按以下顺序做决定:

路径 A:模型 BF16 已能放入显存

先部署 BF16 基线。如果质量优先且并发满足需求,不必为了“先进”而量化。若需要更多吞吐或 KV Cache,再测试 FP8;INT4 只有在密度收益明确且质量通过时才采用。

路径 B:BF16 放不下,但 8 bit 可以

新数据中心 GPU优先验证 FP8,旧硬件或特定框架则比较 INT8。两者都要看实际 kernel 和吞吐,不能按理论位宽决定。若质量差异很小,选择稳定性、可观测性和升级路径更清晰的方案。

路径 C:8 bit 仍放不下

选择经过校准的 INT4/AWQ/GPTQ,或增加 tensor parallel。INT4 单卡避免通信可能更快,但质量风险更高;多卡 8 bit 质量更稳,却增加通信、成本和故障面。应按总体 SLO 与成本比较,而不是只比显存。

路径 D:长上下文并发导致显存不足

先确认瓶颈是 KV Cache,而不是权重。可尝试降低最大上下文、实施请求配额、优化并发调度、启用受支持的 FP8 KV Cache,或分离长短请求池。单纯把权重从 FP8 改成 INT4,未必能解决持续增长的 KV Cache。

十八、不要忽略冷启动和长稳测试

一次 10 分钟压测只能说明短时间可运行,不能证明服务适合生产。量化 kernel 编译、CUDA Graph 捕获、模型权重映射、NUMA 跨节点访问和显存碎片,常常在冷启动或长时间请求波动中才暴露。

冷启动至少拆成四段记录:

  1. 进程启动到开始读取模型。
  2. 权重读取、反序列化和分片加载。
  3. KV Cache 分配、kernel 初始化和 CUDA Graph 捕获。
  4. 服务健康到第一个真实请求完成。

如果模型制品位于网络文件系统,首次加载还受缓存和存储吞吐影响。比较格式时应分别测冷缓存和热缓存,不能让 BF16 走冷盘、INT4 走页缓存后宣称 INT4 启动更快。生产自动拉起还要确认健康检查的 startupProbe 或进程管理超时时间足以覆盖最慢冷启动,否则编排系统会在模型加载过程中反复杀进程。

长稳测试建议覆盖业务实际昼夜周期,至少包含:

  • 长短请求混合,而不是固定 Token 长度。
  • 并发突增、回落和取消请求。
  • 达到最大上下文附近的请求。
  • 工具调用、流式响应和客户端断开。
  • 模型空闲后再次升压。
  • 日志轮转、监控采集和健康检查并行运行。

过程中持续观察显存是否阶梯式增长、KV block 是否能回收、请求取消后缓存是否释放、进程是否出现 OOM/崩溃、NCCL 是否超时,以及输出是否逐渐出现重复或异常。显存高位稳定不等于泄漏;只有在相同负载周期下基线持续抬升、可用 block 下降且无法恢复,才有泄漏或碎片证据。

发生 OOM 时不要只调低 gpu_memory_utilization。应保存失败请求的输入/输出 Token、并发、框架调度日志、GPU 显存曲线和当时的 KV Cache 状态,判断是单个超长请求、并发尖峰、模型加载余量不足、临时 workspace 峰值还是内存未释放。没有这条证据链,参数调整只是在移动故障阈值。

十九、形成可审计的选型记录

量化评审最终应沉淀为一份可复现的决策记录,而不是聊天中的“FP8 看起来最好”。每个候选方案记录:

  • 原始模型、tokenizer、量化 checkpoint 与 checksum。
  • bits、group size、scale 粒度、对称性、跳过层和 KV dtype。
  • GPU 型号与数量、拓扑、驱动、CUDA、框架和镜像 digest。
  • 最大上下文、并发、batch 调度、显存比例和并行方式。
  • 性能原始结果、质量逐项结果、失败样本和日志位置。
  • 通过阈值、未通过项、已知限制和批准人。
  • 上线灰度比例、观察时间、回滚条件和保留期限。

建议预先定义“一票否决项”。例如工具调用 JSON 正确率低于基线门槛、核心数学集下降超过允许值、长上下文出现数据错引、P99 超过 SLO、长稳测试出现进程重启,任何一项触发就不进入下一阶段。这样可以避免平均吞吐提升掩盖关键能力退化。

量化评测也不要求输出与 BF16 逐字节一致。低精度计算、并行归约和 sampling 都可能引入细微数值差异,贪心解码也可能在接近的 logits 上走向不同分支。应以任务正确率、结构约束、语义质量和稳定性判断,而不是简单做全文字符串比较;对于 JSON、SQL、代码等可执行产物,则应使用解析器、单元测试和 schema 验证给出客观证据。

当模型、驱动、框架或 GPU 发生升级,原记录只能作为历史基线,不能直接继承结论。kernel 路径和默认参数可能变化,应重新完成兼容性、性能、质量和长稳回归。

二十、容易踩的十个坑

  1. 把文件大小当显存占用 :忽略 scale、未量化层、KV Cache 和 workspace。
  2. 把 FP8 权重当成 FP8 KV Cache :两者需要独立配置和验证。
  3. 只测能否回答 :没有质量数据和性能阶梯。
  4. 只测并发 1 :无法反映连续批处理和 KV Cache 压力。
  5. 比较条件不一致 :上下文、batch、TP 和采样参数不同。
  6. 模型目录名即事实 :没有检查 quantization_config 和权重元数据。
  7. 忽略 kernel fallback :模型能跑,但大量算子回到高精度。
  8. 用通用平均分掩盖业务回退 :结构化输出、代码或数学已经下降。
  9. 直接覆盖旧服务 :发生问题时没有秒级路由回滚。
  10. 复制其他版本参数 :框架升级后量化方法、参数或模型支持范围变化。

二十一、最终验收清单

部署前:

  • 已记录 GPU、驱动、CUDA、PyTorch、推理框架和镜像版本。
  • 已核对模型 config、量化配置、checksum 和许可证。
  • 已计算权重、KV Cache、workspace 和并发显存预算。
  • 已用 BF16/FP16 建立质量和性能基线。
  • 已验证当前硬件存在目标量化 kernel,不只是“能够加载”。
  • 校准数据经过脱敏,覆盖真实业务分布。
  • INT8、FP8、INT4 候选在相同条件完成阶梯压测。
  • 质量评测覆盖数学、代码、工具调用、多语言和长上下文。

上线中:

  • 新实例使用独立端口或节点。
  • 先健康检查和固定样本,再逐级灰度。
  • 监控错误率、TTFT、TPOT/ITL、吞吐、KV Cache 和 GPU。
  • 长连接和流式请求有明确切流策略。
  • 蓝环境仍然可用,路由可快速切回。

上线后:

  • 结果、配置、模型 checksum 和测试数据版本已归档。
  • 实际高峰满足 SLO,没有持续 OOM 或进程重启。
  • 业务质量抽检和结构化输出错误率达标。
  • 保留期结束前未删除原模型和回滚镜像。
  • 框架或驱动升级后会重新执行兼容性和回归测试。

结语

INT8、FP8 与 INT4 没有脱离硬件和业务的绝对优劣。INT8 往往是稳健的 8 bit 路径,FP8 在现代数据中心 GPU 和成熟 kernel 上更有吞吐潜力,INT4 则用更高的质量与兼容风险换取最大的权重压缩。

可靠的选择方法始终相同:先用 BF16/FP16 建基线,确认显存真正消耗在哪一部分,再在同一硬件、同一请求分布、同一并发条件下比较候选方案;质量上不仅看平均分,还要验证业务最敏感的能力。上线时保留蓝环境、逐级灰度、定义回滚阈值。做到这些,量化才是可控的容量与性能工程,而不是一次只看“模型成功加载”的冒险。

 


打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分