☰
ViT/DeiT/SwinT模型后训练量化(PTQ)工程实践详解
2026/9/28 12:35:18 网站建设 项目流程

简介:面向深度学习部署与推理优化场景,这份资源围绕VisionTransformer系列模型(ViT、DeiT、SwinT)提供了完整的PTQ后训练量化加速方案,适合需要将视觉Transformer模型落地到资源受限环境的算法工程师与研究者,也适合对模型压缩感兴趣的初学者系统学习量化流程。压缩包共15个文件,以14个Python脚本和1个Markdown说明文档为主,整体约41KB;脚本覆盖模型定义、整数化处理、量化校准、量化层封装、测试评估等关键模块,Markdown文档则给出配套流程指引,目录结构便于按模块对照学习。已有196人学习/下载,可作为量化入门与部署优化的实战参考。资源同时提供经过PTQ量化的ViT/DeiT/SwinT模型、逐步复现的流程教程以及可直接运行的源码,有助于从原理到实现完整掌握量化加速过程;测试脚本支持全模型测试、消融实验和分模型验证等多种方式,便于对比量化前后的性能变化,是一份轻量但实用的优质项目资料。

1. 量化加速:这份 PTQ 源码包到底能干什么

做模型部署的人应该都有这种体验:ViT 在 GPU 上跑得挺欢,一旦挪到边缘盒子或者用 ONNX Runtime 跑 CPU 推理,延迟直接让人怀疑人生。Transformer 架构的视觉模型——ViT、DeiT、SwinT——参数和计算量都摆在那里,不做压缩根本没法落地。这份资源包做的就是后训练量化(PTQ),把 FP32 的模型变成 INT8,而且不是只给一个量化脚本,是连模型带流程教程带完整工程代码一起给。

我拆完这个包之后最大的感受是:它不是那种只能跑通 demo 的教学代码,而是真的考虑了实际部署场景——支持 ViT、DeiT、SwinT 三个系列,提供了校准(Calibration)流程、量化层实现、验证脚本,甚至对 LayerNorm 这类敏感层做了特殊处理。适合谁?正在做视觉 Transformer 模型部署的工程师,或者想搞懂 PTQ 原理但不想只停留在理论层面的算法工程师。这篇笔记我把整个工程拆开来讲,从原理到跑通,再到参数怎么调、坑在哪,一次性说清楚。

2. 为什么选 PTQ 而不是 QAT:先搞清楚量化方案的取舍

2.1 PTQ 和 QAT 的本质区别,以及为什么这个项目选 PTQ

量化就是把 FP32 的权重和激活值映射到低比特整数,比如 INT8。但怎么确定映射的缩放因子,有两种路线。QAT(Quantization-Aware Training)需要在训练过程中插入伪量化节点,让模型在训练时就适应量化误差,效果好但成本高——你得有完整的训练环境和数据,还得重新训练模型。PTQ 则完全不一样,它不需要训练,只需要拿一批校准数据跑一遍前向推理,统计激活值的分布,然后算出缩放因子,就可以完成量化。

这个项目选 PTQ 是有道理的。ViT、DeiT、SwinT 这类模型动辄几十上百兆参数,重新训练成本太高,而且很多场景下我们手里只有预训练权重,没有原始训练数据。PTQ 只需要少量校准数据——一般几百到一千张图片就够——跑一次前向,整个过程可能就几分钟到十几分钟。代价是精度会有一定损失,但通过合理的校准策略和敏感层处理,可以把这个损失控制在可接受范围内。

从部署角度讲,PTQ 是性价比最高的方案。它不依赖训练框架,量化完的模型可以直接转成 ONNX 或者 TensorRT 能用的格式。QAT 虽然精度更好,但你要保住 QAT 的优势,推理框架也得配套支持伪量化节点的解析,这在很多实际部署链路上是做不到的。

2.2 校准过程的核心逻辑:MinMax 还是 Percentile

PTQ 的关键在校准阶段。简单说,你拿一批代表真实场景的数据,跑一遍 FP32 的模型,记录每一层激活值的分布范围,然后根据这个范围决定量化参数。常见做法有两种:MinMax 就是直接取统计到的最大值和最小值作为量化范围,实现简单,但容易受离群点影响——某个极端激活值会把整个量化范围拉大,导致普通值的量化精度变差。

这个项目里我看到对激活值采用了类似 Percentile 的思路,就是取分布中某个百分位作为最大值,比如 99.9%,把极端离群值排除掉。对于视觉 Transformer 来说,激活值分布其实比 CNN 更复杂。attention map 经过 Softmax 之后值集中在 0 到 1 之间,但中间层的特征值分布会有长尾,直接用 MinMax 很容易翻车。项目里utils/integer.py文件处理的就是这部分逻辑,后面我会拆开细看。

权重量化则相对简单一些,通常直接用 MinMax 或者 MSE 最小化准则找到最优缩放因子。MSE 的做法是遍历不同的缩放因子,选一个让量化前后误差最小的。这个项目里权重用的是对称量化,因为权重分布大致是正负对称的,用对称量化可以简化计算。

注意:选校准数据集很重要。如果你拿 ImageNet 的验证集图片做校准,但实际部署时处理的是医学影像或者工业检测图,分布差异过大会导致量化后的精度崩掉。我习惯是拿实际业务数据的一小部分做校准,哪怕只有几百张都行。

2.3 哪些层不能随便量化:Transformer 的敏感层分析

Vision Transformer 里有几类层在量化时需要特别注意。第一个是 LayerNorm。它的作用是标准化特征,计算过程中有除法、开方这些操作,直接量化会产生较大的数值误差。这个项目里把 LayerNorm 的权重和偏置保留为 FP32,只量化它前后的线性层,这是标准做法。

第二个是 Softmax。Softmax 的输出分布是 0 到 1 之间的概率值,且通常集中在某个区间,暴力量化会丢掉很多信息。常见做法是保留 FP32,或者用分段线性近似。这个项目里默认保留 FP32,但如果你要部署到纯 INT8 算子上,就得考虑用近似实现。

第三个是 GELU 激活函数。ViT 用的 GELU 不是简单的 ReLU,它涉及到误差函数,结构上是非线性中的非线性。这个项目里对 GELU 采用的是输出量化,就是让 GELU 保持 FP32 计算,但它的输出会被量化成 INT8 传给下一层。这么做是为了平衡精度和速度——毕竟 GELU 本身的计算量不大,但它的输出分布确实不太好提前预估。

embedding 层也比较特殊。Patch Embedding 是把图片切块后做线性投影,这部分如果量化不好,错误会沿着整个网络传播放大,属于第一层放大效应。我看到的做法是 embedding 层也做量化,但校准数据要足够充分,最好把这个层单独拎出来观察量化前后的输出差异。

3. 工程结构拆解:这份源码包的文件布局和核心模块

3.1 从 configs 到 quant_layers:每个文件是干什么的

这份资源的代码结构是照着工程化标准来组织的,不是随手写的实验脚本。我把关键文件逐个过一遍。

configs/PTQ4ViT.py是总配置文件。量化相关的所有超参数都在这里,包括校准数据数量、batch size、量化比特数、是量化权重还是量化激活、以及哪些层需要特殊处理。这个文件是整个工程的入口,你跑任何脚本之前都会先加载它。

BasePTQ.py是量化流程的主控模块。它负责调度整个 PTQ 流程:读取配置文件、加载预训练模型、准备校准数据、逐层完成量化、最后输出量化后的模型。它把 PTQ 的流程做了一个抽象,换模型、换配置都不用改主流程代码。

models/目录下面有三个模型定义文件,分别对应 ViT、DeiT、SwinT。这里的关键点不是模型定义本身——这些模型在 timm 里都有现成的——而是它们如何与量化层结合。项目里没有直接替换模型里的 Linear 层,而是通过net_wrap.py对模型做了一层包装,把需要量化的层替换成量化版本,同时保留不需要量化的层不动。

quant_layers/目录是量化层的具体实现,包含matmul.py、linear.py、conv.py。这三个文件实现了量化的 Linear 层、Conv 层和矩阵乘法。在 ViT 里 Linear 占绝大多数,但 SwinT 里有 Conv 层(Patch Embedding 和 Patch Merging),所以 Conv 的量化实现也必不可少。MatMul 的量化则主要用在 attention 里的 Q 和 K 的矩阵乘法。

utils/integer.py这个文件实现的是整数运算的辅助函数。它做的事情是把 FP32 的张量转换成 INT8 的表示,同时记录缩放因子和零点。这里有个细节:对于对称量化,零点强制为 0,对于非对称量化,零点是一个整数。ViT 的权重用对称量化,激活值可以用非对称——这个文件里两种情况都有实现。

quant_calib.py是校准逻辑的核心。它负责在模型前向推理过程中,钩住每一层的激活值输出,统计它们的分布特征,然后计算出量化参数。这个文件里能看到它使用了类似 runner 的模式,在 forward 过程中动态注册 hook,处理完一个模块就立刻对权重和激活做量化,属于按层量化的策略。

3.2 数据流水线:从原始图片到量化参数

datasets.py做的事情是准备校准数据。它不依赖完整的 ImageNet,只需要一个包含图片和标签的目录结构,按类分文件夹就行。代码里做了 resize 和 crop,尺寸默认是 224×224——ViT 和 DeiT 都要求这个输入尺寸,SwinT 根据变体不同可能是 224 或 384。

校准数据量的设置我一般取 1024 张左右。太少了统计不准激活分布,太多了校准时间成倍增加。这个项目的默认值我看了下是 1024,属于经验值里的黄金档位。

数据加载部分用的是 DataLoader,batch size 在配置文件里设。校准过程中模型处于 eval 模式,不更新梯度,只做前向推理。这里有个大多数人容易忽略的点:校准数据经过预处理后,归一化参数必须保持不变。如果训练时用的是 mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225],那校准数据也必须用完全相同的参数,否则前面几层的输入分布直接偏差,量化参数全错。

3.3 替换层之后的模型长什么样

量化完成之后,模型内部的结构发生了变化。原来的nn.Linear被替换成了QuantLinear,这个新的层在前向推理时会把 FP32 的输入量化成 INT8,然后做整数矩阵乘法,最后再用缩放因子反量化回 FP32。之所以要反量化,是因为相邻层之间如果有 LayerNorm 或 GELU 这类保留 FP32 的算子,它们需要 FP32 输入。

这里涉及到一个关键的工程决策:是层融合(Layer Fusion)还是保持逐层原样。层融合的意思是把 Conv 后的 BN 或者 Linear 后的 Bias 加法融合进前一层,减少量化边界。这个项目里做了部分融合,但要注意融合之后缩放因子的计算方式要对应调整。net_wrap.py里能看见这方面的处理逻辑。

SwinT 的情况比 ViT 复杂一些,因为 SwinT 有 window-based attention,需要在窗口内部做 attention 计算。这导致它的张量形状变化比 ViT 多,量化层的替换也要跟着适配。项目里对 SwinT 是逐层替换的方式,没有对整个 window attention 的输入输出做特殊量化处理,而是在每个 Linear 和 MatMul 处单独量化,这样实现更简单,也符合大多数部署框架的实际能力。

4. 跑通 PTQ 量化全流程:从配置文件到验证脚本

4.1 配置文件的关键参数解读

动手跑之前,先把configs/PTQ4ViT.py里的参数搞清楚。这个文件是工程的指令中心,改错一个参数量化结果可能就废了。

# configs/PTQ4ViT.py 关键配置项 num_calib_batch = 16 # 校准 batch 数量 calib_batch_size = 64 # 每个 batch 的图片数量 num_workers = 8 # 数据加载线程数 weight_bit = 8 # 权重量化比特数 activation_bit = 8 # 激活量化比特数 quant_mode = "percentile" # 校准模式: percentile / minmax / mse percentile = 99.9 # percentile 模式的百分位 calib_data_path = "/path/to/calib_images" # 校准数据路径 model_type = "ViT" # ViT / DeiT / SwinT model_name = "vit_base_patch16_224" # 具体模型名称

num_calib_batch乘以calib_batch_size等于实际的校准图片数量。16×64 就是 1024 张。如果你数据量少,可以调整这两个值的组合。quant_mode决定了激活值量化范围的确定方式。percentile模式是我最推荐的,它对长尾分布更友好。如果你发现量化后精度异常,可以先试minmax对比定位是不是离群点的问题。

model_type和model_name决定了加载哪个模型定义。ViT 系列在 timm 里有现成的权重,比如vit_base_patch16_224、vit_large_patch16_384;DeiT 系列对应的是deit_base_patch16_224;SwinT 对应swin_base_patch4_window7_224等。项目代码里对这几个系列做了适配,换模型的时候把这两个参数对齐到 timm 里的名字就行。

注意:如果你用的是 384 分辨率的模型变体,校准数据预处理阶段裁剪尺寸也要改成 384。这个参数不在配置里,在datasets.py里写死了,需要手动改。

4.2 跑完整个量化流程的命令和步骤

整个流程可以拆成三步:准备数据、执行量化、验证精度。项目提供的核心运行脚本在example/目录里。

第一步,准备校准数据目录。目录结构应该是每个类别一个子文件夹,图片放里面。

# 目录结构示例 calib_images/ ├── class_001/ │ ├── img_001.jpg │ ├── img_002.jpg │ └── ... ├── class_002/ │ ├── img_001.jpg │ └── ... └── ...

第二步,执行量化脚本。以 ViT 为例:

cd example python test_vit.py \ --config ../configs/PTQ4ViT.py \ --model vit_base_patch16_224 \ --data /path/to/calib_images \ --output ../outputs/vit_int8.pt

test_vit.py会加载配置、准备校准数据、执行 PTQ 量化、然后把量化后的模型保存到outputs/目录。中间会打印每一层的量化信息——权重和激活值的缩放因子、量化范围、以及该层输出的统计指标。

第三步,验证精度。项目里提供了test_all.py,它会同时加载原始 FP32 模型和量化后的 INT8 模型,在验证集上分别跑 Top-1 和 Top-5 准确率,并给出对比。

python test_all.py \ --model vit_base_patch16_224 \ --quantized ../outputs/vit_int8.pt \ --data /path/to/val_data \ --batch_size 128

正常来说,ViT-Base 经过 PTQ 量化后 Top-1 准确率下降应该在 1% 以内。这个项目的教程里提到通常能控制在 0.5% 到 1% 之间。如果超过 2% 就要检查上面说的那几个敏感层是不是处理有问题。

4.3 如何把量化后的模型转成 ONNX 部署

量化完成只是第一步,落地到具体推理框架还需要格式转换。项目本身没有直接提供 ONNX 导出脚本,但量化后的模型基于 PyTorch 的torch.jit.save或者torch.save保存,可以自行导出。

# 导出 ONNX(需要适配部署框架的量化算子支持) import torch import torch.onnx model = torch.load("../outputs/vit_int8.pt", map_location="cpu") model.eval() dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, "../outputs/vit_int8.onnx", opset_version=13, input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}} )

这里有个现实问题:PyTorch 导出的 ONNX 里 QuantLinear 层是自定义算子,标准的 ONNX Runtime CPU 版本不认识它,除非你用的部署框架本身就支持这种量化方式。实际落地场景,我一般会建议直接拿这个量化参数导入 TensorRT 或者用 ONNX Runtime 的 QDQ 格式重新做一遍。项目源码的价值在于把量化参数算好,这个信息比具体的部署格式更有用。

5. 避坑指南:ViT 量化实测中踩过的五个真实问题

5.1 校准数据与部署数据分布不一致,精度直接崩

现象:量化后在验证集上精度下降高达 3% 到 5%,远超过预期的 1%。

原因:校准数据是从 ImageNet 验证集里随便抽的,但实际部署场景是自采集的工业图片,光照、目标分布差太多。激活值的统计范围根本不是实际部署时的分布,量化参数自然就是错的。

解决:从实际部署场景里抽几百张图作为校准集。不需要打标签——PTQ 校准只需要图片输入,跑前向拿到激活分布就行。我通常的做法是跑一个在线采样脚本,从生产环境的图片流里抽 500 到 1000 张存下来,定期用来刷新量化参数。从那以后我每次做 PTQ 的第一件事就是确认校准数据来源,而不是代码有没有 bug。

5.2 LayerNorm 被量化后整体精度掉 2 个点

现象:第一次跑量化,发现精度掉了 2.2%,看每层误差时发现 LayerNorm 的输出偏差特别大。

原因:LayerNorm 里存在除法、平方根、减法等操作,数值范围跨度大,对量化误差极其敏感。直接对 LayerNorm 的输入输出做 INT8 量化,误差被非线性放大。

解决:把 LayerNorm 保留为 FP32。这个项目在net_wrap.py里已经默认对 LayerNorm 做了保护,跑之前先确认保护逻辑是开启的。具体做法是在net_wrap.py里检查isinstance(module, nn.LayerNorm)就跳过量化的分支。如果你用的是自己改过的模型代码,也要确保这个逻辑没有被破坏。

5.3 GELU 的输出分布长尾严重,MinMax 校准后 attention 模块异常

现象:用 MinMax 校准模式,量化结果里 attention 部分的输出值出现明显的截断现象,分类头损失很大。

原因:GELU 的输出分布有一个很长的右尾,MinMax 会把量化范围拉到最大值,导致中间密集分布的值量化步长过大,精度丢失严重。

解决:换成percentile校准模式,把percentile = 99.9。这样会忽略掉最极端的 0.1% 离群值,让量化范围更贴近主体分布。我验证过,对于 ViT-Base,percentile 模式比 minmax 模式平均能挽回 0.5 到 1 个点的精度。

5.4 SwinT 的 window-based attention 在量化后张量形状出错

现象:SwinT 量化过程中报RuntimeError: shape mismatch,定位到window_size相关的位置。

原因:SwinT 的 attention 计算是在窗口内进行的,张量需要经过window_partition把 H 和 W 维度拆成窗口。量化层替换时如果改变了输入形状假设——比如把量化前的view操作和transpose操作顺序搞乱——窗口内的形状就对不上了。

解决:在替换 SwinT 的层时不要动 attention 内部的reshape和transpose逻辑,只替换线性和矩阵乘法。这个项目在models/目录下对 SwinT 的适配已经处理了这个问题,但如果你换 SwinT 的变体——比如swin_large_patch4_window14_384——窗口大小变了,需要确认量化层的形状推断和window_size=14是匹配的。

5.5 量化后 CPU 推理反而变慢,问题出在算子不支持

现象:量化完成后模型大小确实减小了,但在 CPU 上的推理延迟不但没降,反而比 FP32 还慢。

原因:QuantLinear层在 PyTorch CPU 推理时走的是模拟量化的路径——每次前向都要做量化、整数运算、反量化三件事。在没有底层 INT8 算子加速的框架里,这个流程是纯 Python 级别的开销叠加,比直接用 FP32 的 BLAS 库还慢。

解决:PTQ 量化本身不直接等同于推理加速。真正的加速需要配合支持 INT8 的底层算子库——比如 TensorRT 的 INT8 engine、ONNX Runtime 的 QDQ 模式、或者特定硬件平台的 NPU/GPU 加速库。项目产出的量化模型只是确定了量化参数,落地加速还得靠部署框架。我一般在拿到量化模型后,会用它的每层量化范围做参考,在部署框架里重新搭一遍量化计算图。

6. 验证量化模型是否合格的三个关键技巧

量化完之后不能只看一个 Top-1 准确率就草草收工,那样很容易漏掉某些层的隐性失真。我习惯做三件额外的事。

第一件是逐层对比量化前后的激活值分布差异。项目里的test_ablation.py可以输出每一层量化前和量化后的输出相似度,通常是计算余弦相似度或者 MSE。我一般会把相似度低于 0.99 的层单独列出来,看是不是敏感层漏保护了。有一次我发现某个模型的第 6 层 attention 输出的余弦相似度只有 0.96,排查后发现是 GELU 层在那一层正好有长尾离群值,percentile 设到 99.99 才恢复。这个逐层体检的习惯帮我发现了很多整体指标看不出来的隐患。

第二件是拿量化模型跑一次你实际业务里的硬样本。精度指标是统计意义上的,但业务场景里往往有风险样本——比如医疗影像里的罕见病灶、工业质检里的罕见缺陷。我会单独准备一个几十到上百张的风险样本集,量化模型在这些样本上的表现要比验证集准确率更值得关注。具体做法是在test_all.py里把验证数据目录换成风险样本目录,跑出来的 Top-1 能和 FP32 保持在一个量级就算合格。

第三件是验证量化参数是否真的被部署框架正确解析。这个坑我踩过:PyTorch 里导出的缩放因子是 tensor 格式,导入 TensorRT 时需要转成标量,一旦 shape 不一致,整个量化范围错位但不会报错。验证方法很简单——拿同一张输入图片分别在 PyTorch 量化模型和部署框架上跑,对比最后输出的 logits 差异,差异阈值设为 0.05 以内。如果超过这个范围,说明部署侧对量化参数的解释有问题,需要检查缩放因子和零点的对齐。

关于量化参数,我提供一个实用技巧:把关键层的量化范围导出成文件,方便在部署侧独立核对。

# 导出每层量化参数 import json def export_quant_params(model, output_path): params = {} for name, module in model.named_modules(): if hasattr(module, "weight_scale"): params[name] = { "weight_scale": float(module.weight_scale), "activation_scale": float(module.activation_scale) } with open(output_path, "w") as f: json.dump(params, f, indent=2) export_quant_params(model, "../outputs/quant_params.json")

导出之后,打开 JSON 文件检查每一层的weight_scale是否在一个合理的范围内——通常应该在 0.001 到 0.1 之间。如果某层的weight_scale突然到 1 以上,说明这层的权重分布有问题,大概率是加载权重不对或者层替换逻辑有误。这个习惯救了我好几次,有一次就是第四层的 scale 异常,排查了半天发现是模型权重文件混入了预训练里没用到的分类头参数。

从那以后我每次做 ViT 的 PTQ,都会强制走一遍这个流程:先用 percentile 跑量化,再做逐层相似度体检,最后在风险样本集和部署框架里做双重验证。这套流程走完,量化模型的质量我心里才算有底。希望你拿到这份资源后,也能用这套方法把量化做扎实。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询