同一个大模型,使用 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、容器或编排平台中的实际服务名。“INT8 模型”“FP8 模型”和“INT4 模型”只是简写,不能完整描述计算路径。上线评审时至少要回答以下问题:
只有这些信息明确后,显存和性能对比才有意义。
INT8 使用 8 位整数表示量化后的值,通常配合 scale,有些格式还使用 zero-point。常见路径包括:
INT8 数值范围较大,工程生态成熟,适合没有可靠 FP8 路径的硬件或框架,也适合质量门槛较高但 BF16 显存不足的场景。它与 FP8 同为 8 bit,原始权重字节数相近,但动态范围、精度分布、校准方法和硬件内核完全不同,不能只按位宽认为两者等价。
FP8 是 8 位浮点格式,常见编码包括 E4M3 和 E5M2。前者通常提供更多尾数精度,后者提供更大指数范围;实际权重、激活和 KV Cache采用哪种格式,由模型、硬件和推理框架共同决定。
FP8 的主要优势是:在支持原生 FP8 Tensor Core 和成熟内核的 GPU 上,可以同时降低权重/激活带宽和提高计算吞吐。相比 INT8,它对大模型异常值和动态范围的处理路径不同,很多现代数据中心 GPU 的推理栈会把 FP8 作为高性能方案。
但“GPU 支持 FP8”不代表当前模型一定能高效运行。还要检查:
INT4 通常指 4 位权重量化,例如 W4A16 或 W4A8。权重会以 packed 形式存储,计算时由专用 kernel 解包并结合 scale/zero-point 反量化或直接进行低精度矩阵计算。常见部署格式包括 AWQ、GPTQ 等,但同名格式也可能因实现、group size、对称性和版本而不兼容。
INT4 的显存节省最明显,适合显存受限、需要提高单卡并发或合并多实例的场景。代价是:
还要区分 NF4。NF4 是 bitsandbytes 中常用于 QLoRA 的 4 位浮点式量化编码,不等同于通用“INT4 推理格式”。一个模型能用 NF4 做低显存加载或训练,不代表生产推理框架能用同一格式获得最佳吞吐。
对于 N 个参数的稠密模型,未考虑元数据时:
text
BF16/FP16 权重约为 N × 2 字节 FP8/INT8 权重约为 N × 1 字节 INT4 权重约为 N × 0.5 字节 例如 27B 参数模型的原始权重下限约为:
这是十进制 GB 的理论估算,不是实际 nvidia-smi 占用。实际部署还会增加 scale、zero-point、量化分组元数据、未量化层、CUDA context、临时 workspace 和内存碎片。
对于 MoE,权重显存按全部已加载专家参数估算,不能只按每个 Token 激活的参数量计算。一个总参数 122B、激活参数远小于 122B 的 MoE,计算量可能接近较小模型,但存储和加载仍需容纳所有专家权重,除非框架采用专家卸载或分层加载。
自回归生成会为每层保存 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,长度翻倍或并发翻倍仍会近似线性增加缓存需求。
Prefill 长提示词、较大 batch、CUDA Graph、MoE 路由、all-reduce 和量化 kernel 都可能申请临时空间。某些框架启动时会预留大块显存作为 KV Cache 池,所以 nvidia-smi 看起来几乎占满并不一定是泄漏。判断要结合框架日志中的可用 KV block、最大并发估算和请求期间的峰值。
Tensor Parallel 会把权重切到多卡,但每张卡仍有 CUDA context、NCCL buffer、激活和部分复制张量。多卡总显存不能简单除以卡数;跨卡通信还可能成为吞吐瓶颈。量化后计算变快,通信占比可能反而更高。
量化 checkpoint 加载时可能先创建高精度临时张量,离线转换更可能同时保留原权重和量化权重。必须检查 RAM、共享内存、临时目录和磁盘空间。模型最终能放进 80 GB 显存,不代表 64 GB 系统内存就能稳定完成加载。
量化选型前先保存可复现的环境基线:
bash
nvidia-smi --query-gpu=index,name,uuid,driver_version,memory.total --format=csv,noheader nvidia-smi topo -m python3 --version 再检查 Python 推理环境:
bash
python3 - <<'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 框架版本也要固定:
bash
python3 - <<'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 名称跳过兼容性测试。
不要相信目录名里的 FP8、INT4 字样。先读模型配置和量化元数据:
bash
jq '{ model_type, architectures, torch_dtype, hidden_size, num_hidden_layers, num_attention_heads, num_key_value_heads, quantization_config }' <模型目录>/config.json 再列出文件大小:
bash
du -sh <模型目录> find <模型目录> -maxdepth 1 -type f -printf '%f %s bytes ' | sort
如果使用 safetensors,可以只读取 header/元数据而不加载全部权重。安装了 safetensors 时执行:
bash
python3 - <<'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 清单:
bash
cd <模型目录> 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 的场景:
Transformers 与 bitsandbytes 可以用于快速验证 8 bit 权重加载。以下代码是真实 API 示例,但它更适合功能验证和单机推理,不代表生产吞吐最优:
python
import 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 的场景:
以 vLLM 的 OpenAI 兼容服务为例,当前安装版本若在 vllm serve --help 中列出 fp8,可以在隔离环境验证动态 FP8:
bash
CUDA_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。强制指定错误量化方法可能导致加载失败或重复处理。正确流程是先检查配置,再看启动日志是否明确识别了量化格式。
在当前 vLLM 版本的 --help 明确支持时,可以单独测试:
bash
vllm 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 的场景:
以 vLLM 加载 AWQ checkpoint 为例,先确认模型 config.json 中有正确量化配置,再启动隔离测试实例:
bash
CUDA_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 模型在当前版本明确支持时,可使用对应方法:
bash
CUDA_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 必须先运行:
bash
vllm serve --help | less
确认本机版本支持 awq 或 gptq。模型文件虽然能被某个库加载,也不代表 vLLM 的当前 kernel 支持其 group size、bits、zero-point、对称方式和模型架构。
bitsandbytes 的 NF4 路径可用于低显存实验,但要明确它不是 AWQ/GPTQ INT4 服务的等价替代:
python
import 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 类方法的实现和参数会随工具版本变化,本文不提供一个看似通用但可能不兼容的转换命令。无论使用哪个经过批准的量化工具,流程都应具备以下约束:
group size 较小通常可以提供更细粒度 scale,但会增加元数据和计算开销;较大 group size 更省元数据,却可能放大量化误差。不能仅根据社区常用值选择,必须在目标模型和目标硬件上比较。
比较 BF16、INT8、FP8 和 INT4 时,必须固定:
否则“FP8 比 INT4 快”可能只是 batch、并发或 cache 设置不同。
下面脚本每秒记录一次显存、利用率、功率和温度,不修改 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,以免终止其他人的监控任务。
bash
curl --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 和进程参数。生产测试应从受控环境变量或密钥管理系统读取,示例中的占位符只用于说明请求结构。
下面脚本读取本地 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) - 1, int((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() 运行示例:
bash
python3 <压测脚本>.py --api-url '' --api-key '' --model '<模型名称>' --prompts '<提示词文件>' --concurrency 8 --timeout 120 这段脚本的吞吐按整个测试墙钟时间计算,适合做同条件相对比较。生产容量评估还应使用流式请求统计 TTFT、TPOT/ITL、排队时间、prefill/decode 吞吐,并执行稳定性 soak test。
不能只测并发 1。建议在同一提示词分布下依次测 1、2、4、8、16 等阶梯,直到达到以下任一边界:
每个阶梯先预热,再记录固定时长;量化 kernel 首次编译、CUDA Graph 捕获和模型冷启动不能混入稳态结果。
同一套测试至少对比:
采样参数要固定。评测确定性任务时使用 temperature = 0,并保持 chat template、system prompt、stop tokens、max tokens 和后处理一致。
量化误差可能集中在特定能力。测试集应包含:
通用 benchmark 宏平均分相近,不代表生产无回退。比如 INT4 可能只在数学和代码上下降,而普通对话差异很小;业务若恰好依赖这两项,就不能用平均值掩盖。
至少按短、中、长输入分档,例如目标最大上下文的 10%、50%、90%。每档都记录:
如果启用 FP8 KV Cache,长上下文评测是独立准入项,不能只复用权重量化的短题结果。
在单卡约 280 GB 显存的 B300 环境中,27B 模型的 BF16 原始权重约 54 GB,通常没有“为了能加载而必须量化”的压力。此时应先把 BF16 作为质量和性能基线:
对于总参数约 122B 的 MoE 模型,BF16 原始权重约 244 GB,叠加运行时、未量化层和 KV Cache 后,单卡余量可能很紧。FP8 原始权重约 122 GB,往往更适合作为 B300 单卡高性能候选。INT4 原始权重约 61 GB,可释放更多并发空间,但必须用数学、代码、工具调用和长上下文评测证明质量可接受。
这里的数字只是参数位宽推算,不是承诺的实际显存,也不是实测吞吐。模型的量化元数据、专家实现、attention、KV Cache 和框架预留都会改变结果。
模型能加载,不代表使用了优化 kernel。某些层可能反量化到 FP16/BF16,再调用普通 GEMM;频繁的 dtype 转换和解包会抵消收益。启动日志、框架 profiler 和 Nsight Systems 才能证明实际路径。
小 batch、短输出时,调度、Python、网络和反量化开销占比高,INT4 不一定比 FP8 快。大 batch 下又可能受到 KV Cache 或算力限制,所以必须做阶梯并发。
量化降低计算时间后,all-reduce 占比上升。若模型量化后能单卡运行,单卡可能比双卡 tensor parallel 延迟更低;但是否成立必须实测,不能只按卡数判断。
Tokenizer、JSON 序列化、网关、日志、TLS 或 Python 调度都可能限制吞吐。观察 GPU 利用率低时,应同时采集 CPU、内存、网络和进程 profile,而不是继续降低精度。
一个方案最大上下文为 8K,另一个为 32K,显存和并发没有可比性。gpu_memory_utilization、KV dtype、max model len 和并发上限必须统一。
对已量化 checkpoint 再传动态量化参数,可能导致不支持、重复量化或错误 kernel。必须先查看 quantization_config 和张量元数据。
切换模型服务通常涉及重启、显存重新分配和请求中断,属于高风险变更。建议保留现有 BF16/FP8 实例作为蓝环境,新量化实例作为绿环境。
只读环境检查脚本示例:
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}" bash
curl --fail-with-body --silent --show-error -H "Authorization: Bearer " "/v1/models" | jq . 同时验证:
量化服务异常时,回滚目标是路由,不是现场重新量化:
不要在故障中删除原模型文件、镜像或服务清单。停止服务和释放 GPU 会影响在途请求,执行前要确认路由已切走,并在执行后验证没有流量仍指向该实例。
大模型服务的容量至少是输入长度、输出长度、并发和 SLO 的函数。建议按业务流量构建矩阵:
| 场景 | 输入 Token | 输出 Token | 并发 | 重点指标 |
|---|---|---|---|---|
| 短问答 | 短 | 中 | 高 | 排队、TPOT、吞吐 |
| 长文总结 | 长 | 中 | 中 | TTFT、prefill、KV Cache |
| 代码生成 | 中 | 长 | 中 | decode 吞吐、质量 |
| Agent 工具调用 | 中 | 短到中 | 波动 | JSON 正确率、尾延迟 |
| 超长上下文 | 很长 | 短 | 低 | OOM、召回、TTFT |
每个量化方案都应输出:
没有输入输出长度分布的“每秒多少请求”很难用于生产规划。
不同推理框架和 exporter 的 Prometheus 指标名可能变化,以下监控维度以实际 exporter 暴露的指标为准:
GPU 指标可由 DCGM exporter 等组件采集,但指标名以实际部署版本为准。告警应同时关联服务层和 GPU 层:GPU 利用率 100% 不一定是故障,如果吞吐和延迟仍在 SLO 内;GPU 利用率很低但队列很长,则可能是 CPU、调度、锁或网络瓶颈。
可以按以下顺序做决定:
先部署 BF16 基线。如果质量优先且并发满足需求,不必为了“先进”而量化。若需要更多吞吐或 KV Cache,再测试 FP8;INT4 只有在密度收益明确且质量通过时才采用。
新数据中心 GPU优先验证 FP8,旧硬件或特定框架则比较 INT8。两者都要看实际 kernel 和吞吐,不能按理论位宽决定。若质量差异很小,选择稳定性、可观测性和升级路径更清晰的方案。
选择经过校准的 INT4/AWQ/GPTQ,或增加 tensor parallel。INT4 单卡避免通信可能更快,但质量风险更高;多卡 8 bit 质量更稳,却增加通信、成本和故障面。应按总体 SLO 与成本比较,而不是只比显存。
先确认瓶颈是 KV Cache,而不是权重。可尝试降低最大上下文、实施请求配额、优化并发调度、启用受支持的 FP8 KV Cache,或分离长短请求池。单纯把权重从 FP8 改成 INT4,未必能解决持续增长的 KV Cache。
一次 10 分钟压测只能说明短时间可运行,不能证明服务适合生产。量化 kernel 编译、CUDA Graph 捕获、模型权重映射、NUMA 跨节点访问和显存碎片,常常在冷启动或长时间请求波动中才暴露。
冷启动至少拆成四段记录:
如果模型制品位于网络文件系统,首次加载还受缓存和存储吞吐影响。比较格式时应分别测冷缓存和热缓存,不能让 BF16 走冷盘、INT4 走页缓存后宣称 INT4 启动更快。生产自动拉起还要确认健康检查的 startupProbe 或进程管理超时时间足以覆盖最慢冷启动,否则编排系统会在模型加载过程中反复杀进程。
长稳测试建议覆盖业务实际昼夜周期,至少包含:
过程中持续观察显存是否阶梯式增长、KV block 是否能回收、请求取消后缓存是否释放、进程是否出现 OOM/崩溃、NCCL 是否超时,以及输出是否逐渐出现重复或异常。显存高位稳定不等于泄漏;只有在相同负载周期下基线持续抬升、可用 block 下降且无法恢复,才有泄漏或碎片证据。
发生 OOM 时不要只调低 gpu_memory_utilization。应保存失败请求的输入/输出 Token、并发、框架调度日志、GPU 显存曲线和当时的 KV Cache 状态,判断是单个超长请求、并发尖峰、模型加载余量不足、临时 workspace 峰值还是内存未释放。没有这条证据链,参数调整只是在移动故障阈值。
量化评审最终应沉淀为一份可复现的决策记录,而不是聊天中的“FP8 看起来最好”。每个候选方案记录:
建议预先定义“一票否决项”。例如工具调用 JSON 正确率低于基线门槛、核心数学集下降超过允许值、长上下文出现数据错引、P99 超过 SLO、长稳测试出现进程重启,任何一项触发就不进入下一阶段。这样可以避免平均吞吐提升掩盖关键能力退化。
量化评测也不要求输出与 BF16 逐字节一致。低精度计算、并行归约和 sampling 都可能引入细微数值差异,贪心解码也可能在接近的 logits 上走向不同分支。应以任务正确率、结构约束、语义质量和稳定性判断,而不是简单做全文字符串比较;对于 JSON、SQL、代码等可执行产物,则应使用解析器、单元测试和 schema 验证给出客观证据。
当模型、驱动、框架或 GPU 发生升级,原记录只能作为历史基线,不能直接继承结论。kernel 路径和默认参数可能变化,应重新完成兼容性、性能、质量和长稳回归。
quantization_config 和权重元数据。部署前:
上线中:
上线后:
INT8、FP8 与 INT4 没有脱离硬件和业务的绝对优劣。INT8 往往是稳健的 8 bit 路径,FP8 在现代数据中心 GPU 和成熟 kernel 上更有吞吐潜力,INT4 则用更高的质量与兼容风险换取最大的权重压缩。
可靠的选择方法始终相同:先用 BF16/FP16 建基线,确认显存真正消耗在哪一部分,再在同一硬件、同一请求分布、同一并发条件下比较候选方案;质量上不仅看平均分,还要验证业务最敏感的能力。上线时保留蓝环境、逐级灰度、定义回滚阈值。做到这些,量化才是可控的容量与性能工程,而不是一次只看“模型成功加载”的冒险。
全部0条评论
快来发表一下你的评论吧 !