广告:Codex Token 低价中转站稳定接口 · 快速接入 · 开发者备用通道
Engineering article

推理模型踩坑记录:性能优化 | 每周速递

我之前用推理模型做性能优化的时候,踩过不少坑,最扎心的是那些看起来很简单的调参,结果系统直接崩溃。比如用torchscript导出模型,结果推理速度反而变慢。后来发现是内存管理没做好,没有释放掉中间缓存。用onnxruntime推理的时候,GPU没用起来,后来才发现是模型精度没设置对。还有就是模型压缩,我试过用prune和quantize

推理模型踩坑记录:性能优化 | 每周速递
配图来源于网络和AI生成,仅供参考。
▌ 技术引导
我之前用推理模型做性能优化的时候,踩过不少坑,最扎心的是那些看起来很简单的调参,结果系统直接崩溃。比如用torchscript导出模型,结果推理速度反而变慢。后来发现是内存管理没做好,没有释放掉中间缓存。用onnxruntime推理的时候,GPU没用起来,后来才发现是模型精度没设置对。还有就是模型压缩,我试过用prune和quantize,但没注意激活函数的范围,导致输出全为0或者NaN。这些细节真的容易被忽略,但影响很大。我见过的最有效的优化手段是用混合精度训练,加上内存池优化,效果立竿见影。另外,多线程推理的时候,锁机制没处理好,导致CPU利用率打满。这些经验都值得分享,别再重复造轮子了。

▌ 技术参考

推理模型性能优化是深水区,我亲身经历过一次模型推理速度暴跌,后来发现是模型加载方式不对。通常我们会用torch.load加载模型,但有时候模型太大,会导致内存碎片,影响GPU调度。解决方案是用torch.jit.load,不过要记得设置map_location参数,否则会在CUDA设备上加载失败。另外,使用torch.nn.DataParallel时,模型复制会带来额外的显存占用,导致推理延迟飙升。这时候可以用DistributedDataParallel代替,或者直接用模型自带的并行接口。



模型压缩是关键点,我试过用torch.nn.utils.prune.ln_unstructured来剪枝,结果发现剪枝后推理精度下降明显。原因是剪枝参数没选好,没有考虑激活范围。正确的做法是用torch.nn.utils.prune.random_unstructured,加上keep_ratio参数控制保留率。同时,要记得在剪枝后用torch.quantization.prepare_qat_model启用量化感知训练,否则模型输出可能不准确。还有,量化过程中要设置activation_post_process_type为fake_quantize_per_channel_affine,这样在推理时才能正确模拟量化效果。



onnxruntime推理时,很多人不知道如何开启GPU加速。其实只要在session_options里设置intra_op_num_threads和inter_op_num_threads,就能充分利用多线程。还可以用execution_mode参数,设置为exec_mode_parallel,提升并行处理能力。不过要注意,有些模型在onnxruntime里会遇到动态shape问题,这时候需要在模型导出时加上-1占位符,或者用onnxruntime的initializer_type参数指定权重类型为FP16。另外,onnxruntime的graph_optimization_level参数设置为ORT_{graph_optimization_level}_all_onnx_ops,能显著提升推理速度。



混合精度训练是我见过最有效的优化方法。用apex库或者PyTorch的torch.cuda.amp模块,配合loss_scale参数,能在不损失精度的前提下减少显存占用。我在实际中发现,loss_scale设为动态模式,效果比静态模式更好,尤其在批量大小较大的时候。同时,要确保模型中的激活函数支持FP16,比如用ReLU6代替ReLU,否则会导致精度溢出。混合精度训练后,推理时用torch.cuda.amp.autocast,能节省约30%的显存,同时提升运算效率。



多线程推理时,锁机制是大问题。我之前用Python的threading模块来控制多线程,结果发现线程数越多,CPU利用率反而越低。后来换成使用multiprocessing,用Process类来创建子进程,性能提升明显。但要注意,multiprocessing在Windows上要使用if __name__ == "__main__"来启动,否则会报错。另外,模型加载和推理要分开处理,避免锁争用。如果模型是用torch.jit.script导出的,建议用torchserve进行服务化部署,这样能自动分配线程和缓存,减少手动配置的工作。



模型加载方式直接影响性能,我之前直接用model.eval()加载模型,结果发现推理时显存占用很高。后来改用torch.jit.load,效果好很多,但必须设置map_location参数,比如map_location='cuda:0',否则会加载到CPU上,导致速度下降。另外,onnx模型加载时,用InferenceSession和providers参数指定CUDA作为推理后端,能显著加速。如果模型太大,可以考虑用model.optimize()进行优化,或者用onnxsim工具进行模型简化,减少计算图的冗余。



模型输入预处理是性能瓶颈之一。我之前没有对输入数据进行归一化,导致模型推理速度下降。后来发现,输入数据的类型必须统一,比如都转成float32,否则会触发类型转换,增加计算耗时。另外,padding和truncating也要注意,我之前直接用pad_sequence,结果发现显存占用飙升,后来改用padding的max_length参数控制,避免不必要的操作。还有,用TorchScript导出模型时,要确保所有张量的形状是固定的,否则在推理时会报错。



模型输出后处理同样重要。我之前没有对输出进行softmax处理,直接用argmax,导致推理速度变慢。后来发现,使用torch.nn.Softmax(dim=1)能提升效率,尤其是当batch_size很大时。还有,模型输出的维度要和推理逻辑匹配,否则需要额外的转置操作,增加计算开销。另外,对于分类任务,可以使用torch.topk来获取前K个概率,而不是直接计算所有概率,这样能节省时间。但要注意,topk的K值不能太大,否则会增加内存负担。



模型量化是必须掌握的技巧。我在实际中发现,使用torch.quantization.quantize_dynamic可以快速进行量化,但要注意它只适用于特定层,比如Conv2d和Linear。如果想更精细地控制量化,可以用torch.quantization.prepare_qat_model,然后在训练时加入量化aware训练。量化后的模型需要设置activation_post_process_type为fake_quantize_per_channel_affine,这样在推理时才能正确模拟量化效果。另外,量化模型导出时要使用torch.quantization.convert,否则会触发错误。



模型并行化是提升推理速度的另一个方向。我之前尝试用DataParallel,结果发现显存占用过高,推理速度反而更慢。后来改用DistributedDataParallel,配合torch.distributed.init_process_group,可以更好地利用多GPU资源。但要注意,DistributedDataParallel需要在分布式环境中运行,无法直接在本地测试。如果只是想提升单机多卡性能,可以用torch.nn.parallel.DistributedDataParallel,同时确保模型的forward函数能正确处理并行化。另外,可以使用torch.distributed.barrier来同步各设备的计算,避免数据不一致。



模型缓存优化是关键。我之前没有使用模型缓存,在每次推理时都要重新加载模型,导致时间浪费。后来发现,使用torchscript导出的模型可以通过torch.jit.load来实现缓存加载,这样能减少加载时间。对于onnx模型,可以用onnxruntime的模型缓存功能,保存推理后的模型,避免每次都重新解析。还有,使用model.to('cuda')时,要注意是否开启内存池优化,比如设置torch.backends.cudnn.benchmark=True,能提升卷积运算效率。另外,可以使用torch.utils.checkpoint来节省显存,但会带来一定的延迟。



模型结构优化也是重要一环。我之前用的是ResNet50,但发现推理速度不够。后来改用轻量级模型如MobileNetV3,虽然精度略有下降,但速度大幅提升。还有,使用模型剪枝时,要确保输入数据的范围在剪枝后的模型中仍然适用,否则会导致输出错误。另外,可以使用torchvision的模型结构转换工具,比如将ResNet50转换为更高效的结构,同时保持精度。模型优化时,要结合实际需求,不能一味追求速度而忽视准确性。



模型精度设置是容易被忽视的点。我之前用FP32训练的模型,推理时却用了FP16,导致输出不准确。后来发现,推理时要使用相同的精度设置,如果用FP16,训练也要用FP16,否则会出现精度错误。此外,使用混合精度时,要确保loss_scale参数设置合理,避免梯度消失。模型输出的类型也要统一,比如用float16或float32,不能混用。还有,有些模型在推理时需要设置特定的精度模式,比如用onnxruntime的precision_mode参数,选择FP16可以提升速度。



模型推理时的显存占用是致命问题。我之前用大batch_size推理,结果显存爆掉。后来发现,显存占用和模型大小直接相关,可以使用torch.cuda.memory_allocated()来监控占用情况。另外,可以使用torch.cuda.empty_cache()来清理缓存,但要注意,这可能影响模型的稳定性。还有,模型导出时,可以使用optimize_for_inference=True参数,让导出工具自动优化模型计算图,减少内存消耗。对于onnx模型,可以用onnxoptimizer进行优化,提升效率。



模型部署方式影响很大。我之前用Flask部署模型,结果发现响应时间很长。后来改用FastAPI,配合gRPC接口,速度明显提升。模型部署时,要使用模型缓存和内存池优化,比如设置num_workers参数提升并发能力。另外,使用TensorRT进行模型推理,能显著提升速度,但需要确保模型支持INT8或FP16量化,否则无法加速。部署时还可以用模型的onnxruntime后端,配合CUDA加速,减少延迟。比如,在onnxruntime中设置providers=['CUDAExecutionProvider'],就能享受GPU加速。



模型推理时的线程调度是另一个关键。我之前用多线程推理时,发现CPU利用率低,后来发现是线程之间的竞争导致的。改用multiprocessing替代,性能提升明显。但要注意,multiprocessing在Windows上需要使用if __name__ == "__main__"来启动,否则会报错。模型推理时,可以使用asyncio异步框架来管理线程,减少等待时间。另外,使用模型的预热机制,比如在推理前运行几个空batch,能提升后续推理速度。还有,可以使用torch.jit.script加速模型加载,减少初始化时间。



模型加载顺序会影响性能。我之前在推理时先加载模型,再处理输入,结果发现加载时间过长。后来调整顺序,先处理输入数据,再加载模型,这样能减少等待时间。模型加载时,可以使用torchscript的jit_load来加速,但要确保模型的输入输出类型一致。另外,使用模型的onnxruntime后端时,可以设置execution_mode为ort_{execution_mode}_parallel,提升并行处理能力。还有,可以使用模型的缓存机制,避免重复加载,节省时间。