部署 ChatGLM 系列模型到 CPU 时动态量化与静态量化的内存占用对比

文章导读
把 ChatGLM 系列模型部署到 CPU,内存占用要先拆开看来源,不能直接拿模型文件大小当成运行时占用。动态量化和静态量化都改动了权重存储,但对激活和中间缓存的处理方式不同,因此对比重点不是文件大小,而是在目标机器上能否获得稳定可用的内存曲线。
📋 目录
  1. 内存对比先拆成三个数
  2. 动态量化与静态量化差异比对
  3. 复现内存对比的观测骨架
  4. 选择顺序和验证清单
A A

把 ChatGLM 系列模型部署到 CPU,内存占用要先拆开看来源,不能直接拿模型文件大小当成运行时占用。动态量化和静态量化都改动了权重存储,但对激活和中间缓存的处理方式不同,因此对比重点不是文件大小,而是在目标机器上能否获得稳定可用的内存曲线。

对 CPU 部署 ChatGLM 这类权重占大头的模型,动态量化能先压掉权重冗余,通常不需要校准数据,适合做可行性确认;静态量化把激活也固定成低精度整数,推理阶段内存曲线更稳定,但需要校准集和精度检查。先做动态量化观察权重和峰值内存,再决定是否有必要做静态量化。具体差值依赖 CPU 指令集、模型版本和部署框架,要现场测量。

内存对比先拆成三个数

一个模型在 CPU 上运行,常见的内存指标有三个:权重内存、峰值内存、常驻内存。权重内存是模型参数和缓冲区的总和;峰值内存包含加载模型时临时文件、初始化中间变量和一次前向计算产生的最大缓冲区;常驻内存是服务稳定后的进程占用。动态量化和静态量化在这三个数上的变化不同步,因此要分别记录。

适用动作:先用 model.get_memory_footprint() 看权重内存,再用进程最大 RSS 看峰值内存,最后在连续推理多次后记录常驻 RSS。验证方式:给待比较的两种部署方式指定相同的输入长度和 batch 大小;在首次推理前后分别记录 RSS,再把服务空转一段时间后的 RSS 单独记录。风险边界:不要用加载模型前的 RSS 增量去推断推理时的内存,因为 PyTorch 的内存分配器会保留缓存,RSS 可能不会立刻下降。

动态量化与静态量化差异比对

维度动态量化静态量化
权重内存权重以低精度存储,加载内存通常下降权重同样低精度存储,下降幅度与动态量化接近
激活内存激活按需即时量化,临时缓冲仍保留浮点形态激活使用预统计的量化参数,中间缓冲更稳定
校准数据不需要专门收集需要覆盖线上输入的校准集
实施动作在模型加载入口挂上量化配置,改动集中需要 prepare、校准、convert 三步,并检查导出算子
适用场景快速确认 CPU 部署可行性想进一步压低推理峰值和长会话内存时再投入

动态量化

  • 适用场景:权重内存压力最大,需要先让模型能加载,或快速验证 CPU 部署可行性。
  • 操作动作:在模型加载入口启用 int8 或 dynamic quantization 配置;不同部署框架参数不统一,启动后先用一小段输入跑通前向。
  • 验证方式:分别记录启用前后的 model.get_memory_footprint() 和首次推理 VmHWM。
  • 风险边界:动态量化不会改变激活存储,当序列变长或 batch 变大时,峰值内存仍会明显爬升。

静态量化

  • 适用场景:推理时激活和临时缓冲成为瓶颈,长上下文或多并发请求导致内存增幅过高。
  • 操作动作:准备能覆盖线上输入分布的校准集,跑一次前向统计激活范围,再执行量化转换;导出模型后仍需用校准集之外的数据复核。
  • 验证方式:固定同一批次输入,对比静态量化前后的常驻 RSS 和峰值 RSS。
  • 风险边界:校准集偏差会放大量化误差,个别层可能需要回退到较高精度;硬件指令集不支持 int8 加速时收益会收缩。

复现内存对比的观测骨架

为了不把框架差异和模型差异混在一起,先用最小可运行骨架记录内存。下面代码是伪代码,量化加载入口用注释占位,重点是对比流程本身。

部署 ChatGLM 系列模型到 CPU 时动态量化与静态量化的内存占用对比
# 观测骨架:保持输入、线程数、模型路径一致
import gc
import psutil, torch

# 加载入口按你的部署方式替换,例如动态量化配置
# 或从静态量化导出目录读取模型
def load_model(qtype):
    print(f"load model, qtype = {qtype}")
    return model   # 替换为实际模型对象

def sample(model, input_ids):
    gc.collect()
    proc = psutil.Process()
    before = proc.memory_info().rss
    with torch.no_grad():
        model(input_ids)
    after = proc.memory_info().rss
    print(f"rss_before_mb={before / 1024**2:.1f}")
    print(f"rss_after_mb={after / 1024**2:.1f}")

# 固定模型路径、输入长度和一个批次大小
input_ids = torch.ones(1, 128, dtype=torch.long)
for qtype in ["dynamic", "static"]:
    model = load_model(qtype)
    sample(model, input_ids)

如果不想侵入模型代码,可以每次单独跑一份推理脚本,用 /usr/bin/time -v 收集 Maximum resident set size。脚本里除模型加载和推理外,不要纳入其他会吃内存的初始化逻辑。

选择顺序和验证清单

第一轮先用动态量化把基线跑出来。第二轮,只有当动态量化后的峰值内存或常驻内存仍不能满足部署目标时,再上静态量化。两轮检查都使用相同输入长度、batch 和线程数,否则对比结果会被环境噪声带偏。

  • 固定 CPU 核数和线程数,例如 torch.set_num_threads(n) 或等价的线程掩码。
  • 固定输入长度与 batch,不要一边跑短文本一边跑长文本。
  • 分别记录加载后 RSS、首次推理峰值 RSS、连续推理后的常驻 RSS。
  • 用同一组问题跑量化前后对比,检查输出质量和语法是否出现明显劣化。

内存对比不是一个静态数字,而是一组过程数据。先让动态量化稳定跑通,再决定静态量化是否值得投入;静态量化适合把部署内存进一步压低,但要为校准确认和环境适配留下时间。