1. 项目概述与设计思路
Model-Optimizer是我去年在负责某个端侧视觉模型落地时,从零攒起来的一套模型优化工具链。最初它只是为了解决一个非常具体的问题:一个跑在服务器上精度达标的分类模型,怎么塞进只有几百MB内存、算力还不到服务器十分之一的嵌入式设备里还能保持可用。后来慢慢把它做成了一整套通用流程,从量化、剪枝、蒸馏到推理侧性能回归,覆盖了模型交付给工程团队之前的几乎所有优化动作。
这套工具想要解决的问题很实在:模型文件太大、推理速度太慢、显存或内存占用过高、端侧推理时发热掉帧。这些问题的根子往往不是某个框架的bug,而是在模型设计阶段对部署环境考虑不足。Model-Optimizer做的事情简单说就是“在不显著损失效果的前提下,尽量把模型压到最快、最小、最容易跑”。它面向的人群很清晰:算法工程师要交付模型给应用端,但又不希望每次都被工程团队因为性能问题打回;以及那些刚开始尝试模型压缩、对着论文里一堆术语不知所措的入门者。
我当时设计这条工具链时给自己定了几条原则,这里直接说结论供参考。第一,所有优化动作必须能回溯,每一步做了什么事、改了哪些层、精度下降了多少都要有记录,否则出了问题根本没法排查。第二,凡是能量化的操作全部量化,包括校准数据集的处理、推理时间统计、每一层输出的数值分布,这些数据后续都是调优的依据。第三,默认方案优先选工程上最成熟、踩坑最少的路径,而不是最新论文里看起来最复杂的方案。这也直接决定了工具链整体的走向:以TensorRT、onnxruntime、NCNN和MNN作为推理后端的承载,以量化感知训练和训练后量化为主线,以结构化剪枝和知识蒸馏为补充手段。
整个工具链分成四个模块:分析模块、优化模块、验证模块、导出模块。分析模块会先跑一遍原始模型,统计每个算子的耗时占比、每层激活值的分布范围、模型对输入噪声的敏感程度,这些数据决定了接下来该用什么优化策略。优化模块则根据分析结果自动选择量化、剪枝、蒸馏或者组合方案。验证模块负责端到端对比优化前后模型的精度、时延、体积,输出一份可读的报告。导出模块最后把模型转换到目标推理框架的格式,同时自动插入一些推理侧的优化配置,比如动态形状、批处理设置、显存复用等。
这套工具的实际效果,用我当时最常跑的ResNet-18分类模型举例:原始模型大小为44.7MB,单张224×224图片在x86 CPU上用OpenCV预处理加onnxruntime推理,耗时大约34ms。经过TensorRT FP16量化加适当剪枝之后,模型体积压到12.3MB,推理耗时降到9.8ms,Top-1精度从原始的71.3%掉到69.8%。这个精度损失对于业务上很多场景来说完全能接受,但性能和体积上的收益非常可观。这类收益并非个例,后面的章节我会一条条拆解每个优化手段的原理、具体操作和踩坑经历。
2. 核心优化手段拆解:量化、剪枝与蒸馏的取舍
2.1 模型量化:精度与速度的最直接交换
量化是Model-Optimizer里最核心也是使用频率最高的优化手段。它的本质其实很简单:神经网络在训练和推理时默认使用FP32精度表示每个权重和激活值,占比特位数多、计算量大。而量化技术可以把这些数值用FP16甚至INT8来表示,在大多数处理器上,INT8的乘法运算速度远快于FP32,算力利用率也高得多。
这里必须分清楚两种量化路径,因为它们在实际使用中的坑完全不同。
训练后量化是最省事、最直接的方式。加载已经训练好的FP32模型,拿一小部分校准数据去统计每一层激活值的动态范围,然后把浮点数值映射到一个有限整数区间上。整个过程不需要重新训练模型,通常几分钟就能完成。我在实际使用时,训练后量化在ResNet-18上能把模型压缩到原来的四分之一左右,速度提升2到3倍,精度损失一般在1%以内。前提是校准数据选得足够有代表性,且模型本身没有特别异常的大数值分布。
量化感知训练则是在训练过程中就模拟量化的效果,让模型在前向推理时用模拟的INT8精度计算,反向传播仍然用FP32梯度更新。因为模型在训练阶段就“适应”了低精度的表达方式,最终量化后精度损失往往更小,尤其对MobileNet这类本身结构紧凑的轻量模型来说几乎必须用这种方法。代价是训练时间变长,超参数需要重新调整,而且对训练框架有要求。
实际操作时参数选择是门学问。拿校准数据集大小举例,太少了统计不准每层的数值范围,太多了校准时间成本完全不值。我在跑ImageNet分类模型时试过从64张到4096张不同规模的校准集,结论是256张已经能覆盖绝大部分情况,再多基本只是线性增加时间而精度不再明显改善。这里有个关键细节:校准数据的标签不重要,重要的是数据分布要与真实场景一致。拿网上随便下载的图片去校准一个医疗影像模型,出来的量化模型性能一定很惨。
2.2 结构化剪枝:剔掉不重要的连接和通道
剪枝的工作原理是寻找模型中冗余的参数并把它们删除掉。非结构化剪枝会把很多个单个权重置为零,这样看起来参数稀疏了,但实际底层的矩阵乘法库并不会因此加速,因为零在计算时照样参与乘法。而结构化剪枝整片整片地删除神经元、通道或卷积核,剪完之后卷积计算量真正变少了,推理速度也随之提升。
这是我个人更推荐在Model-Optimizer里默认采用的剪枝方式:按通道剪枝。具体做法是,先跑一批数据统计每个卷积层输出通道的激活值重要性,或者用L1范数、BN层缩放因子这类指标给通道排序,把贡献低的通道直接剪掉,然后微调几个epoch让模型重新适应。整个过程可以迭代执行:剪一批、微调、再评估、再剪下一批。
我踩过最典型的坑:对第一层卷积进行剪枝。很多模型框架对第一层卷积的通道数有硬性要求,因为它对应输入图片的通道数量,比如RGB输入就是3通道。我第一次剪枝时没注意这个约束,直接按照统计指标把第一层卷积通道从32剪到了8,结果模型输出全部变成了NaN,查找问题花了整整一个下午。后来在工具里加了约束:第一层和最后一层默认不参与剪枝。这也引出一个通用经验——不要为了追求极致压缩而对网络的输入输出层动手,收益不大但风险极高。
还有个经常被忽略的指标是Flops占比。剪枝时应该优先剪那些既占用大量计算又在重要性排序中靠后的层。我常跑的模型里,ResNet的第三、第四个残差阶段的卷积占据了大部分计算量,但如果只看通道的重要性评分而不考虑计算占比,很容易把已经很小的浅层继续剪,最后模型是变小了但速度几乎没变。所以我在分析模块里强制输出每层的计算占比热力图,剪枝规则里也把“耗计算多的层优先剪”作为一条显式逻辑。
2.3 知识蒸馏:把大模型的“经验”浓缩给小模型
知识蒸馏的思路非常直观:大模型参数多、能力强,小模型参数少、能力有限。直接让小模型去拟合大模型的输出,让小模型学到的不只是数据本身的标签,还有大模型对样本的判断置信度和“模糊地带”的表达。这种“软标签”里包含的信息远丰富于原始的one-hot标签。
我在Model-Optimizer里把蒸馏作为辅助手段,因为单独用蒸馏优化速度提升有限,小模型本身的推理速度才是关键,蒸馏更适合用来把小模型的精度往上拉。典型用法是:先把大模型静态量化部署上去跑着,同时训练一个小模型,让小模型通过蒸馏去逼近大模型的特征表达能力。当小模型精度达标后,再对小模型做量化剪枝,实现“复合”压缩。在一组实际的车辆检测场景里,我尝试了ResNet-50蒸馏给ResNet-18,蒸馏之后ResNet-18的mAP从61.3%提升到64.1%,几乎追平了原版ResNet-34的水平,但推理速度却只有ResNet-50的一半不到。
蒸馏训练需要注意温度参数T的设定。软标签的表达式是softmax之后除以温度T,T越大输出分布越平滑,小模型能学到更多大模型的“暗知识”。我在目标检测任务里试过T值范围从1到20,T=5左右效果最好,再大会让所有类别概率趋同,信息被过度平滑反而无效。另外,蒸馏时loss函数一般用两项加权,一项是硬标签的常规交叉熵,另一项是软标签的KL散度,权重比例的调整也很关键,一般软标签权重在0.3到0.7之间表现比较好。
2.4 三种手段如何组合才合理
很多刚接触模型优化的人会问:量化、剪枝、蒸馏到底哪个最好?从我经手的项目看,没有标准答案。合理的组合顺序是:如果时间紧张且模型够鲁棒,直接训练后量化是性价比最高的;如果对精度要求苛刻,先做蒸馏训练出更小但更强的模型,再对这个小模型做量化;如果想压榨到极致,就要走“蒸馏+剪枝+量化”的三步组合路线。
这里给出一份我在实际项目里根据预算紧急程度选型使用的判断表,方便对照:
| 场景类型 | 推荐方案 | 预期收益 | 风险与注意点 |
|---|---|---|---|
| 快速上线、精度要求一般 | 训练后量化(INT8) | 体积减少60%以上,速度提升2-3倍 | 要注意校准集分布;对敏感模型精度可能掉1%以上 |
| 端侧部署、精度要求高 | 量化感知训练 | 体积与速度收益同上但精度损失更小 | 训练时间长,超参数调参成本高 |
| 计算量过大、latency敏感 | 结构化剪枝+微调 | 计算量减少30%-50%,时延明显下降 | 网络结构改变,部分框架可能不支持 |
| 大模型+小模型组合 | 知识蒸馏 | 小模型精度接近大模型,推理速度快 | 蒸馏训练本身需要时间,训练目标需精确设计 |
| 极限压缩 | 蒸馏+剪枝+量化 | 三项叠加收益,体积可降80%+ | 逐环节精度损失累加,需要每步验证 |
这三条优化手段背后共通的逻辑是:尽量在保持模型表达能力的前提下,减小计算量和存储代价,然后通过降低参数冗余和改变数值精度来换取实际速度上的收益。想一次性把模型压缩到极小,并不现实,需要依赖多条路线的迭代组合。
3. 实操细节:从ResNet-18出发完整跑一遍优化流程
这一节我会用一个真实项目里的配置做示范,完整展示一个ResNet-18分类模型从原始FP32到INT8端侧部署的全流程,重点是每个操作背后的参数考量和实际遇到的坑。下面是我在Model-Optimizer里跑这个用例时用到的环境配置、校准集构造、量化选型和导出步骤,你可以直接照着复现。
3.1 环境准备与依赖安装
我最终跑通这套流程的机器环境是Ubuntu 20.04、Python 3.8、PyTorch 1.13,推理侧额外安装了onnxruntime 1.14和TensorRT 8.5。为什么采用PyTorch转ONNX再转后端框架的路线,而不是直接用TensorFlow或PaddlePaddle?因为ONNX作为一种中间表示,可以统一不同框架训练出的模型,且目前主流推理框架对ONNX格式的支持都比较成熟。换句话说,只要训练完了转出ONNX格式,后端选NCNN、MNN、TensorRT还是onnxruntime都方便。
安装时的关键一步是选择与CUDA版本匹配的TensorRT版本,否则会出现算子在初始化时直接报“OP not supported”或者“plugin not found”之类的错误。我建议用conda创建全新虚拟环境,避免系统自带的Python环境被污染。我的依赖清单大致是这样:
conda create -n modelopt python=3.8 conda activate modelopt pip install torch==1.13.1 onnx==1.13.1 onnxruntime tensorrt==8.5.3.1 pip install opencv-python numpy pandas matplotlib实测中有一个非常容易踩的坑:ONNX版本与onnxruntime版本必须兼容。如果你用ONNX 1.14导出的模型,而onnxruntime只支持到1.13,推理时大概率会报“Unsupported model IR version”之类的错误。我的建议是常年固定一套版本组合,不要随便升级,我后来一直用onnx=1.13.1搭配onnxruntime=1.14.0,这套组合在多个模型上都验证过非常稳。
3.2 校准数据的准备与预处理
量化过程中最容易被低估的步骤就是校准数据集的构造。训练后量化需要统计模型中每个激活值的分布,而统计的样本就是校准集。校准集不能直接用训练集全量跑一遍,因为时间太长。但如果随便取几十张图,分布偏差太大,又会影响量化模型的精度。
我在Model-Optimizer里推荐的做法是从验证集中均匀采样256张图片作为校准集。关键点有两个:一是采样时保证类别覆盖均匀,不要全部来自某一类;二是图片预处理必须和训练时保持一致。比如训练ResNet-18时用的是ImageNet的预处理方式,图像resize到256×256后中心裁剪到224×224,再按mean和std做归一化。这一整套流程在量化校准和推理时必须一字不差地复现。
这里放一段我当时构造校准数据集的完整代码,逻辑不算复杂但细节多:
import cv2 import numpy as np import os calib_dir = "calib_imgs" calib_list = load_image_list(calib_dir) # 按类别均匀采样后的图片路径 mean = np.array([0.485, 0.456, 0.406], dtype=np.float32).reshape(1,1,3) std = np.array([0.229, 0.224, 0.225], dtype=np.float32).reshape(1,1,3) def preprocess(img_path): img = cv2.imread(img_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (256, 256)) img = img[16:240, 16:240] # 中心裁剪为224x224 img = img.astype(np.float32) / 255.0 img = (img - mean) / std img = img.transpose(2, 0, 1) # HWC转CHW return img.reshape(1, 3, 224, 224) calib_data = np.zeros((256, 3, 224, 224), dtype=np.float32) for i, p in enumerate(calib_list): calib_data[i] = preprocess(p)这段代码里最值得注意的坑是图片读取通道顺序。OpenCV读进来默认是BGR,而模型训练时用的几乎都是RGB,如果不做convert操作,喂进去的图片颜色通道是反的,量化分析和后续推理的精度都会异常下降。这种错误在模型精度高时很难发现,因为模型够强,通道反了也能蒙对一些样本,但一旦量化后精度就会雪崩,到时候排查半天才发现是预处理的问题,就非常浪费时间。
3.3 导出ONNX与执行量化
原始模型训练好后,第一步是转成ONNX格式并检查网络结构是否完整。PyTorch导出时要把模型切到eval模式再torch.no_grad(),并且用真实的输入尺寸跑一次导出,否则某些算子会因为shape不确定而在转换时报错。
import torch from torchvision.models import resnet18 model = resnet18(pretrained=True).eval() dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, "resnet18.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, opset_version=13 ) print("ONNX export done.")dynamic_axes这里很关键。如果不打开动态轴,导出后的模型输入batch只能固定为1,后面如果服务器端需要多batch推理就会出问题。当然,如果目标是端侧部署且单帧输入居多,固定batch=1反而能加速推理,这里要根据实际场景决定。
接下来我直接跑Model-Optimizer里的量化子命令,它会调用onnxruntime的量化工具,配合之前准备的校准数据,完成INT8动态量化并输出一份报告:
python -m model_optimizer quantize \ --model resnet18.onnx \ --calibrate-data ./calib_data.npy \ --quantize-type int8 \ --calibrate-method entropy \ --output resnet18_int8.onnx量化方式选择上,我极力推荐entropy校准方法。另一类mminmax方法直接取激活值的最小最大值做对称区间,简单粗暴但对抗大离群值的能力很差,最终精度常常偏低。entropy方法会搜索一个合适的边界,让量化前后的信息损失最小,虽然计算量大一些但精度明显更稳。测试结果里,mminmax量化resnet18的Top-1精度比原始低2.1个百分点,而entropy只低0.7个百分点,差异就是这么明显。
3.4 性能回归验证与报告产出
量化完成之后必须做完整验证,包括推理速度对比和精度对比。我记得第一次完整跑这套流程时,光验证环节就踩了一堆坑,其中最典型的:用onnxruntime的CPU推理时,同一模型有时明明量化了却速度没提升。原因在于onnxruntime的INT8优化在某些x86 CPU上必须依赖特定的算子实现,如果某些算子回退到了FP32实现,整体速度就无法得到理想提升。要看清楚每层算子实际使用的是INT8还是FP32内核,最好的方式是用profiling工具逐层查看,而不是只看总耗时。
Model-Optimizer的验证模块会输出这样一份summary:
| 指标 | 原始模型 | INT8量化模型 | 变化 |
|---|---|---|---|
| 模型体积 | 44.7MB | 11.6MB | -74% |
| 单图CPU推理耗时 | 34.2ms | 12.1ms | -64.6% |
| Top-1精度 | 71.3% | 70.4% | -0.9% |
| 最大值内存占用 | 约200MB | 约60MB | -70% |
这份报告是优化工作的最终交付物。我在实际推进项目时,最常被工程团队追问的不是“你怎么做量化的”,而是“量化到底掉了多少精度、快了多少、内存省了多少”。只要能把这份报告清晰地给出来,推动落地的阻力会小很多。报告里我还会把每一层算子的耗时分解做成表格,方便在下一步抽丝剥茧地排查性能瓶颈。
4. 实战中频率最高的疑难问题与排查记录
4.1 量化后精度突然严重下降,该怎么定位
这是Model-Optimizer使用中最高频的问题。我的排查套路是从“数据流”角度逐层查,而不是无头苍蝇式乱试。第一步先把量化模型和原始模型的输出层特征直接对比,算出所有测试样本上的输出向量余弦相似度,看偏差是整体性偏移还是集中在少数样本。如果是整体性偏移,多半是校准阶段的数值分布统计有问题;如果是异常样本导致的,多半是模型本身对某些输入极度敏感,这类样本恰好没被校准集覆盖。
第二步是逐层对比量化前后的激活输出。我在工具里直接集成了一个逐层dump功能,可以对比指定层的输出分布profil。绝大多数情况下问题出在两种层:一是带有残差连接的层,二是直接跟卷积后面的BatchNorm层。量化时如果BN层的均值和方差没有被正确吸收或转换,INT8推理的分布就和训练时对不上。这个问题的快速修复方案是改用量化感知训练,或者只对特定层跳过量化,工具里支持设置“排除层”列表。
第四步是检查校准集有没有脏数据。有次我拿了一组带有水印的图片当校准集,水印区域数值分布占比高,叠加上后续量化边界统计,直接把模型不少层的高响应区域压没了,整个模型退化到几乎随机猜测的程度。检查校准集最快的方式是可视化一下图片和对应的激活值分布,看看有没有明显异常峰值。
4.2 目标框架不支持某些算子的处理方式
把模型量化完后往NCNN或MNN上部署时,经常遇到“量化不支持”或“算子不支持”的错误。这类问题通常出于两个原因:一是你用了较新的运算符,目标框架还没适配;二是某些自定义算子或动态shape操作在转换时无法被框架静态图编译器处理。
我在遇到算子不支持时,第一选择是简化网络结构而非寻找绕过方案。举个实例,模型里有个Gather算子配合动态索引来做注意力矩阵的抽取,NCNN当时一直报不支持。后来我把这部分改成用多个固定索引的Split加Concat实现,笨是笨了点但稳定跑通。第二选择是在导出ONNX时手动替换算子,用一系列标准算子组合去仿写自定义逻辑。如果还是不行,就只能考虑换推理框架。TensorRT对算子的支持度比NCNN更全面,代价是需要GPU运行环境。
这里建议在工具里维护一张“算子兼容性底表”,记录每个目标框架下已验证可用的算子集合,每踩一个坑就补充一条记录,后面再遇到同类问题可以直接查到。
4.3 模型量化后时延不降反升的几种原因
量化后推理变慢,是最打击人信心的现象。我遇到这种情况基本就三类原因。
第一类是推理框架没有真正调用低精度内核,典型现象是模型体积变小了,但耗时和FP32一模一样。排查方式是用profile工具,逐层打印内核类型,确保关键卷积层都匹配到了INT8实现。第二类是某些层存在较多量化反量化操作。量化模型在推理时,需要把输入从FP32转成INT8,输出再转回FP32。如果模型结构层数很浅或算子碎片化严重,那转换的额外开销就已经抵消掉低精度计算带来的收益。这种模型建议直接用FP16或剪枝替代INT8量化。第三类比较隐蔽,是小模型大量使用了大深度单batch算子时,多次kernel launch的开销占比过高。单张图推理在GPU上启动很多个小型kernel,整体耗时反而被启动开销主导,这种场景下把多个算子融合成一个kernel会显著改善,TensorRT的图优化就是为此设计的。
4.4 常见问题速查表
| 表现 | 可能原因 | 推荐排查顺序 |
|---|---|---|
| 量化后精度掉幅超过3% | 校准集分布偏差太大 | 检查校准集是否类别均匀、预处理是否正确;查看激活值直方图;尝试换成entropy校准方法 |
| 结果中出现NaN | 网络中某些中间层数值溢出 | 查看bn层是否被错误剪枝;检查剪枝时是否动了第一层;看归一化方式是否变化 |
| 推理耗时几乎没变 | 未匹配到低精度内核 | 逐层profiling查看实际使用的算子内核;检查onnxruntime算子设置 |
| 模型转换时报错 | 目标框架不支持某些算子 | 用netscope或onnxsim简化模型;替换算子的等价实现 |
| 编译没问题但部署后崩溃 | 动态内存分配或shape不匹配 | 打开框架的debug日志;检查动态轴设置是否正确;跑最小复现用例 |
排查问题有一条总原则:每次只改一个变量。量化、剪枝、蒸馏本身都是高风险改动,如果你同时调了校准集、换了推理后端、改了批大小,出了问题之后,完全无法定位是哪个环节引起的。我在模块里强制记录每一步的输入输出和版本号,就是因为在实践中吃过太多“无法复现”的亏。
5. 项目落地后的经验心得与最佳实践
做到了这个阶段,项目中踩过的坑已经慢慢变成了经验积累。这一节想分享一些我在实际操作中形成的方法论,以及一些对后来者很有用的细节。这些细节在设计工具链之初很难完整预见到,基本都是靠一个项目一个项目喂出来的。
5.1 关于模型精度的判断,永远要量化不要感觉
优化过程里,精度掉了0.5%到底是好是坏,不能靠直觉判断。每个业务场景对精度的容忍度有明显差异,比如相册分类容忍度高一点没关系,但安全检测或者医疗识别就会特别敏感。我通常在项目启动前先跟需求方确认精度指标的下限,“最多只允许掉1%”,还是“和原始模型差异要小于0.5%”,这样做所有优化动作都有明确的停止条件,也就知道什么时候该继续剪、什么时候该收手。这个小习惯帮我节省了将近一半的优化时间,因为不用反复试探“再压一点行不行”。
5.2 建议先做量化,不要一上来就做蒸馏
我在Model-Optimizer里默认推荐的顺序是先做训练后量化,观察精度影响,如果影响在可接受范围内就更不要往后折腾。理由非常务实:训练后量化的成本最低,半天之内就能得到结论。如果连这种低风险优化都没法满足精度要求,直接上蒸馏+剪枝大概率也白搭。如果量化后精度损失确实偏大,这时候再考虑做量化感知训练,还不成再做剪枝或蒸馏。优先级从简到繁、从低风险到高风险,这套顺序能让人少走大量弯路。
5.3 优化目标是整体系统,不是单独某一个指标
很多人优化模型时恨不得把体积压到最小,速度提到最高,但忽略了目标推理环境的真实需求。比如端侧人脸检测,瓶颈常常不在网络推理而在图像预处理的耗时,那这时候花大力气把推理层从4ms优化到3ms就不如优化一下图像缩放算法收益更大。Model-Optimizer发展到后面,已经不光是模型优化工具,还加入了预处理管线耗时分析,因为延迟优化必须端到端看才能看到真实瓶颈。
5.4 后期向更多框架和硬件扩展
这个项目后续其实可以很自然地扩展到更多方向。一是对不同推理框架的支持,目前很多公司内部还有自研推理引擎,Model-Optimizer的ONNX导出和算子改写方式可以快速迁移到这些引擎上。二是GPU推理场景下更细粒度的优化,包括张量并行、算子融合、动态shape适配。三是对Transformer这类结构的支持,对注意力机制的量化敏感度分析和剪枝研究还有不少空间。
当然这只是我个人规划里比较大的几个方向。回到最开始的那个问题:为什么我会花这么多精力去维护这么一套工具链?因为模型优化这项工作,本质上是在算力限制和模型效果之间寻找最优平衡点。没有一把万能钥匙能解决所有场景,只有把量化、剪枝、蒸馏、校准、验证这套基本功练扎实,并且沉淀成一套可复用的流程,才能在遇到新的业务场景时第一时间拿出可靠的优化方案,而不是从零开始踩一遍所有坑。