一、什么是TensorRT推理的显存泄漏?
在生产环境用TensorRT做深度学习推理(比如图像分类、目标检测)时,常会遇到服务跑了几天,显卡显存从几百MB涨到几个GB,最后报显存不足(OOM),重启后才回落——这就是典型的TensorRT显存泄漏。简单说,就是程序用了显存但没还给系统,一直占着导致后续请求没法分配资源。
1.1 生产中常见的泄漏表现
最明显的是显存持续上涨,增长速度和请求量正相关,重启才会下降。比如每秒处理100张图的服务,正常显存稳定在1GB,跑一周后涨到8GB,且不会自动回落,这种情况不是模型太大,而是资源没释放。
二、从引擎序列化到资源释放的全流程踩坑点
TensorRT推理流程是:构建网络→创建引擎→序列化引擎(存文件,下次直接加载)→加载引擎→创建上下文→执行推理→释放资源,每个环节都可能留下未回收的显存,下面用代码示例说明。
2.1 引擎序列化时的资源残留
序列化引擎是为了加速启动,但很多人忽略构建网络的中间对象(比如builder、network、config)。这些是TensorRT的C++核心对象,Python垃圾回收不会自动释放,必须手动删除。
技术栈:TensorRT 8.6 + Python 3.10 + PyCUDA
错误示例:
import tensorrt as trt
# 错误:序列化后中间对象没释放,占着显存
def serialize_engine(model_path):
logger = trt.Logger(trt.Logger.WARNING)
builder = trt.Builder(logger)
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
config = builder.create_builder_config()
# (省略)填充network,比如解析ONNX模型
serialized_engine = builder.build_serialized_network(network, config)
with open("model.trt", "wb") as f:
f.write(serialized_engine)
# 忘记删除builder、network、config,它们的显存没释放
serialize_engine("model.onnx")
正确示例:
import tensorrt as trt
# 正确:序列化后手动删除所有中间TensorRT对象
def serialize_engine(model_path):
logger = trt.Logger(trt.Logger.WARNING)
builder = trt.Builder(logger)
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
config = builder.create_builder_config()
# (省略)填充network
serialized_engine = builder.build_serialized_network(network, config)
with open("model.trt", "wb") as f:
f.write(serialized_engine)
# 手动删除触发C++层析构,释放显存
del config, network, builder, serialized_engine
serialize_engine("model.onnx")
关键:所有TensorRT核心对象必须手动del,因为GC管不到C++对象的内存。
2.2 推理时的上下文和显存泄漏
引擎加载后,要创建IExecutionContext执行推理,还要给输入输出分配GPU显存。很多服务每次请求都新建资源,用完不释放,导致显存持续上涨。
技术栈同上
错误示例:
import tensorrt as trt
import pycuda.driver as cuda
import numpy as np
class BadService:
def __init__(self):
# 加载引擎
with open("model.trt", "rb") as f:
engine_data = f.read()
self.engine = trt.Runtime(trt.Logger(trt.Logger.WARNING)).deserialize_cuda_engine(engine_data)
def infer(self, img):
# 错误1:每次请求新建上下文,用完不删
context = self.engine.create_execution_context()
# 错误2:每次分配显存,用完不释放
input_gpu = cuda.mem_alloc(img.nbytes)
output_gpu = cuda.mem_alloc(1000*4)
cuda.memcpy_htod(input_gpu, img)
context.execute_v2(bindings=[int(input_gpu), int(output_gpu)])
output = np.empty(1000, dtype=np.float32)
cuda.memcpy_dtoh(output, output_gpu)
# 完全没释放资源,每次请求都占显存
return output
# 跑1000次请求,显存爆炸
bad_service = BadService()
for _ in range(1000):
img = np.random.randn(3,224,224).astype(np.float32)
bad_service.infer(img)
正确示例:
import tensorrt as trt
import pycuda.driver as cuda
import numpy as np
class GoodService:
def __init__(self):
# 引擎只加载1次,复用
with open("model.trt", "rb") as f:
engine_data = f.read()
self.engine = trt.Runtime(trt.Logger(trt.Logger.WARNING)).deserialize_cuda_engine(engine_data)
# 上下文只创建1次,复用
self.context = self.engine.create_execution_context()
# 预分配固定大小的显存,不用每次分配
self.input_shape = (3,224,224)
self.output_size = 1000
self.input_gpu = cuda.mem_alloc(np.prod(self.input_shape)*4)
self.output_gpu = cuda.mem_alloc(self.output_size*4)
def infer(self, img):
cuda.memcpy_htod(self.input_gpu, img)
self.context.execute_v2(bindings=[int(self.input_gpu), int(self.output_gpu)])
output = np.empty(self.output_size, dtype=np.float32)
cuda.memcpy_dtoh(output, self.output_gpu)
return output
# 服务停止时强制释放所有资源
def release(self):
cuda.free(self.input_gpu)
cuda.free(self.output_gpu)
del self.context, self.engine
# 用try-finally确保资源一定释放
good_service = GoodService()
try:
for _ in range(1000):
img = np.random.randn(3,224,224).astype(np.float32)
res = good_service.infer(img)
finally:
good_service.release()
核心:复用上下文和显存,服务退出时必须释放资源,用try-finally确保逻辑执行。
2.3 临时显存的泄漏
TensorRT构建引擎或推理时会分配临时显存(比如workspace),如果是手动申请的临时buffer,必须手动cuda.free,否则也会泄漏。大部分情况下,删除核心对象会自动释放临时显存,但自定义层的临时内存要注意。
三、泄漏定位的实用方法
找到泄漏点是解决问题的关键,下面几个方法可快速定位:
3.1 监控显存实时变化
用PyCUDA的mem_get_info()查看已用显存,每次推理前后调用,看是否上涨:
import pycuda.driver as cuda
def print_gpu_usage():
free, total = cuda.mem_get_info()
used = (total - free)/(1024**2) # 转MB
print(f"已用:{used:.2f} MB,空闲:{free/(1024**2):.2f} MB")
# 测试时的打印
print("启动前:")
print_gpu_usage()
service = GoodService()
print("预分配后:")
print_gpu_usage()
for i in range(5):
service.infer(np.random.randn(3,224,224).astype(np.float32))
print(f"第{i+1}次推理后:")
print_gpu_usage()
service.release()
print("释放后:")
print_gpu_usage()
如果每次推理后显存涨,说明infer里的资源没释放;服务释放后显存回落到初始值,说明资源管理正确。
3.2 拆分流程缩小范围
把代码拆成小部分测试:先测序列化引擎,再测加载,最后测推理。比如序列化时显存没释放,就是2.1的问题;推理时涨就是2.2的问题,快速缩小范围。
3.3 查看TensorRT详细日志
把日志级别设为VERBOSE,会打印资源分配和释放记录,找只出现“Allocated”没“Released”的buffer,就是泄漏点:
# 把logger级别设为VERBOSE,替换之前的WARNING
logger = trt.Logger(trt.Logger.VERBOSE)
四、技术优缺点和注意事项
4.1 资源管理方式的优缺点
- 手动删除对象+try-finally:优点是可控,确保资源释放;缺点是需要写额外代码,生产环境必须。
- 复用资源:优点是减少内存开销,提升性能;缺点是要固定输入形状,动态形状需调整。
4.2 生产环境核心注意事项
- 不要每次请求新建引擎/上下文:引擎加载慢,复用可减少泄漏和性能损耗。
- 服务退出强制释放资源:用
try-finally确保,避免异常退出残留资源。 - 监控显存:用NVIDIA Exporter+Prometheus+Grafana设阈值,超过告警,及时重启。
- 版本选择:用稳定版本(如8.4+),旧版本有已知泄漏问题。
4.3 常见误区
- 误区1:Python GC会释放TensorRT对象→不会,必须手动
del。 - 误区2:序列化后中间对象不用管→中间对象的显存和引擎独立,必须释放。
- 误区3:显存涨是模型大→模型显存固定,持续涨的肯定是泄漏。
五、文章总结
生产环境TensorRT显存泄漏,本质是C++核心对象和GPU显存的生命周期没正确管理。从序列化到推理的每个环节,都要检查资源是否释放,核心是:手动删除所有TensorRT核心对象,复用上下文和显存,服务退出时强制释放资源。定位时通过监控显存、拆分流程、查看日志,可快速找到问题,解决后大幅提升服务稳定性,避免OOM故障。
Comments