INT4 与 FP16 部署比较:显存、精度和吞吐怎么测
INT4 与 FP16 部署比较:显存、精度和吞吐怎么测
INT4 与 FP16 的选择不能只看显存。相同模型、数据集和并发下,记录精度变化、吞吐、尾延迟与显存,再决定量化方案。CUDA 版本与并行拓扑也要随结果保存。
1. INT4 仍可能触发 CUDA OOM,先拆显存构成
量化降低的是权重存储,并不会等比例减少 KV Cache、Activation、CUDA Context 和通信缓冲。出现CUDA out of memory时,应保存请求长度、Batch、各卡占用与分配器统计,再判断是哪一项越过预算。
torch.cuda.OutOfMemoryError: CUDA out of memory候选原因包括 CUDA Context 与 NCCL 初始化占用、各卡负载不均、KV Cache 增长和 Activation 峰值。需要用torch.cuda.memory_summary()、NVML 和请求轨迹逐项排除,不能从 OOM 文本直接认定根因。
2. 量化权重加载机制与 CUDA 内存分配器的冲突
要从根本上规避 OOM 崩溃,需要深入理解 PyTorch CUDA Caching Allocator、INT4 权重反量化与多卡 NCCL 通信的底层交互机制。
排查时重点检查以下因素:
- INT4 算子的 Activation 反量化开销:INT4 量化压缩的是模型的 Weights(权重),但在 Transformer 层的 Gemm 矩阵乘法计算时,CUDA Kernel 需要将 INT4 权重实时反量化(Dequantize)转换为 FP16/BF16 的 Activation 进行 Floating Point 计算。在长 Prompt 上下文(如 8K Token)下,Activation 占用的显存随 Batch Size 呈现线性增长。
- NCCL 共享内存与 P2P 拓扑配置不匹配:多卡并行(Tensor Parallelism)依赖于 NCCL 库进行 All-Reduce 节点间通信。如果没有配置
NCCL_P2P_DISABLE与NCCL_IB_DISABLE适配 PCIe/NVLink 拓扑,NCCL 会默认预分配明显的 Host-to-Device 共享内存 Ring Buffer,直接挤爆本就紧张的显存边界。
下面是 INT4 量化模型显存分配与并发 Activation 暴涨的物理逻辑图:
要打破这个瓶颈,需要在服务启动前实现拓扑自检、动态显存预留以及环境变量的严格收口。
3. 示例 LLM 推理引擎环境自检与显存配额保护代码
在工程落地中,下面给出了一个 Python 示例 LLM 环境自检与显存配额保护组件。
代码结合了torch.cuda与pynvml,实现了 GPU 显存碎片检测、Tensor 并行拓扑自检、量化模型分块加载保护以及 OOM 熔断控制:
import os import sys import torch import pynvml from typing import Dict, List, Tuple class CUDAGPUGovernance: """示例 GPU 显存与 CUDA 拓扑治理组件""" def __init__(self, target_gpu_ids: List[int], min_free_ratio: float, memory_reserve_ratio: float, min_kv_cache_gb: float): self.target_gpu_ids = target_gpu_ids self.min_free_ratio = min_free_ratio self.memory_reserve_ratio = memory_reserve_ratio self.min_kv_cache_gb = min_kv_cache_gb pynvml.nvmlInit() def perform_environment_check(self) -> bool: """启动阶段硬性自检环境与环境变量收口""" print("=== [Step 1] 执行 CUDA 环境变量收口校验 ===") # 1. 校验 CUDA_VISIBLE_DEVICES 避免主卡显存污染 cuda_vis = os.environ.get("CUDA_VISIBLE_DEVICES", "") if not cuda_vis: print("【警告】未检测到 CUDA_VISIBLE_DEVICES 环境变量!强烈建议显式指定绑定的 GPU 卡!") else: print(f"CUDA_VISIBLE_DEVICES 绑定正确: {cuda_vis}") # 2. 检查 PyTorch CUDA 内存分配器策略 alloc_conf = os.environ.get("PYTORCH_CUDA_ALLOC_CONF", "") if not alloc_conf: print("【提示】未显式配置 PyTorch CUDA 分配器;是否需要调整应由碎片率对照实验决定。") print("\n=== [Step 2] 检查物理 GPU 显存与 NVLink 拓扑 ===") device_count = pynvml.nvmlDeviceGetCount() print(f"检测到物理 GPU 总数: {device_count}") for gpu_id in self.target_gpu_ids: if gpu_id >= device_count: print(f"错误: 目标 GPU ID {gpu_id} 超出物理设备上限 {device_count}!") return False handle = pynvml.nvmlDeviceGetHandleByIndex(gpu_id) mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle) total_gb = mem_info.total / (1024 ** 3) free_gb = mem_info.free / (1024 ** 3) print(f"GPU {gpu_id}: 总显存 {total_gb:.2f} GB | 当前剩余可用 {free_gb:.2f} GB") # 阈值由模型加载峰值和预留空间测试得出,不在脚本中写死 if free_gb / total_gb < self.min_free_ratio: print(f"GPU {gpu_id} 可用显存低于启动阈值,当前占用 {total_gb - free_gb:.2f} GB") return False return True def calculate_safe_kv_cache_budget(self, model_weight_gb: float, tp_size: int) -> int: """根据 INT4 权重大小计算安全的 KV Cache 显存预算 (MB)""" # 以第一张 GPU (GPU 0) 的显存为基准进行安全估算 handle = pynvml.nvmlDeviceGetHandleByIndex(self.target_gpu_ids[0]) mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle) total_mem_gb = mem_info.total / (1024 ** 3) # 单卡实际分摊的模型权重显存 per_card_weight_gb = model_weight_gb / tp_size # 预留比例由 CUDA Context、NCCL 缓冲和 Activation 峰值测试得出 usable_memory_gb = total_mem_gb * self.memory_reserve_ratio remaining_for_kv_gb = usable_memory_gb - per_card_weight_gb if remaining_for_kv_gb <= self.min_kv_cache_gb: raise MemoryError(f"显存预算不足!单卡余下空间仅 {remaining_for_kv_gb:.2f} GB,无法分配安全的 KV Cache") kv_cache_mb = int(remaining_for_kv_gb * 1024) print(f"\n=== [Step 3] 显存预算精算完成 ===") print(f"单卡模型权重: {per_card_weight_gb:.2f} GB | 可用安全 KV Cache 空间: {kv_cache_mb} MB") return kv_cache_mb def shutdown(self): pynvml.nvmlShutdown() # 验证保护逻辑 if __name__ == "__main__": # 这些值应由部署配置和容量测试提供 gpu_ids = load_gpu_ids() governance = CUDAGPUGovernance( target_gpu_ids=gpu_ids, min_free_ratio=load_min_free_ratio(), memory_reserve_ratio=load_memory_reserve_ratio(), min_kv_cache_gb=load_min_kv_cache_gb(), ) if not governance.perform_environment_check(): print("\n环境检查未通过,服务拒绝启动。") sys.exit(1) try: kv_budget_mb = governance.calculate_safe_kv_cache_budget( model_weight_gb=load_model_weight_gb(), tp_size=len(gpu_ids), ) print(f"推理引擎准备就绪,分配 KV Cache 空间上限: {kv_budget_mb} MB") except MemoryError as e: print(f"\n启动拦截: {e}") finally: governance.shutdown()4. 在同一硬件上比较 TensorRT-LLM 与 vLLM
选择量化模型的候选引擎时,应在同一台服务器、相同 GPU 数量和驱动版本下,对 TensorRT-LLM 与 vLLM 运行同一组 INT4 (AWQ) 请求。硬件型号、显存、输入输出长度和并发档位都作为实验输入记录,不把某台固定机器写成通用结论。
请求并发、输入 Token 与输出 Token 使用业务分布的多个档位,并在两种引擎间保持一致。
收集的核心指标数据如下表所示:
| 推理引擎与配置 | 显存碎片 | 各卡显存分布 | 容量边界 | P99 延迟 | 失败类型 |
|---|---|---|---|---|---|
| vLLM 当前配置 | 采集 allocated/reserved | 逐卡采集 | 逐级加压 | 保存分位值 | 按 OOM、超时等分类 |
| vLLM 候选配置 | 同一口径采集 | 逐卡采集 | 逐级加压 | 保存分位值 | 按类型分类 |
| TensorRT-LLM 候选配置 | 同一口径采集 | 逐卡采集 | 逐级加压 | 保存分位值 | 按类型分类 |
分析时应同时检查各卡是否失衡、碎片是否随时间累积,以及 OOM 发生在哪个阶段;只比较总显存不足以确定引擎优劣。
5. 模型量化部署上线前需要收口的 5 个环境变量
部署前应把 CUDA 与通信相关变量显式纳入配置和版本审计,但取值必须匹配实际 GPU 拓扑与驱动:
CUDA_VISIBLE_DEVICES:显式列出调度器分配给进程的 GPU ID,并与容器设备挂载核对。NCCL_P2P_DISABLE:先用nvidia-smi topo -m与 NCCL 测试确认 P2P 能力,再决定是否禁用;不能仅凭“有无 NVLink”二分配置。
