显存隔离必须靠CUDA上下文隔离,而非进程或线程;多个模型共用同一CUDA上下文时显存共享,OOM风险高;真正隔离需为每个模型绑定独立上下文并严格管理生命周期。

显存隔离必须靠 CUDA 上下文隔离,不是靠进程或线程
多个模型共用一个 CUDA 上下文(比如默认 cudaSetDevice(0) 后直接加载)时,显存分配是共享的,torch::jit::load 或 trt::ICudaEngine::createExecutionContext() 都不会自动隔离——哪怕模型对象是独立的。显存“看起来”没冲突,是因为底层驱动把所有分配都塞进同一个地址空间,一旦某个模型推理爆显存,整个服务可能被 OOM killer 杀掉。
真正隔离的方式是为每个模型实例绑定独立的 CUDA 上下文:
- 调用
cudaSetDevice(device_id)后,立即调用cudaStreamCreate(&stream)+cudaCtxCreate(&ctx, 0, device_id)(注意:CUDA 11.0+ 推荐用cudaStreamCreateWithFlags(..., cudaStreamNonBlocking)配合显式上下文管理) - 每个模型加载、推理前,必须先
cudaCtxSetCurrent(ctx);推理完可cudaCtxSynchronize()确保完成,但不要过早cudaCtxDestroy()(否则后续推理会 crash) - PyTorch 模型需在对应上下文中调用
model->to(torch::kCUDA),且所有输入 tensor 必须在该上下文里创建(不能跨 ctx memcpy)
负载均衡不能只看 GPU 利用率,得盯住显存碎片和 kernel 启动延迟
用 nvidia-smi 看到 GPU-Util 30% 就以为有余量?错。显存可能已碎片化到无法分配 512MB 连续块,而新请求一来就失败。更关键的是,不同模型的 kernel 启动开销差异极大:ResNet50 单次推理 kernel launch 可能 ViT-L/16 因大量 small GEMM 和 transpose,常达 100–300μs——这直接影响吞吐拐点。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 用
cudaMemGetInfo(&free, &total)每次请求前查真实空闲显存,而非依赖历史利用率均值 - 给每个模型实例维护一个轻量队列(如
std::deque<:shared_ptr>></:shared_ptr>),按「显存预留量 + 预估 kernel 数」加权打分,不单纯轮询 - 对小 batch 请求(batch=1),优先调度到显存充足且最近未执行 kernel 的实例——避免因 context switch 把 latency 推高 2–3ms
TensorRT 引擎加载时显存暴涨,是因为 deserialization 默认占用 host 内存 + device 显存双份
trt::IRuntime::deserializeCudaEngine() 在反序列化时,会先把 engine blob 全部解压到 host 内存,再拷贝到 device 显存。一个 1.2GB 的 .plan 文件,峰值内存占用可能突破 2.5GB(host)+ 1.2GB(device),极易触发系统 swap 或 OOM。
缓解方式只有两个硬招:
- 用
trt::IRuntime::setDLACore(0)(如果 DLA 可用)把部分层卸载,降低显存峰值;但注意 DLA 不支持所有 op,需提前用trtexec --allowGPUFallback测试 - 改用 streaming 加载:把
.plan文件 mmap 到内存,用trt::IHostMemory分块读取 +cudaMallocAsync配合cudaMemPool_t预分配显存池,避免 runtime 自己 malloc - 绝对不要在主线程加载多个引擎——必须异步线程 +
cudaStreamSynchronize()等待完成,否则主线程卡死超 1s,健康检查直接失败
PyTorch JIT 模型热更新时显存泄漏,根源是旧模型的 CUDA tensor 没被正确销毁
常见写法:model = torch::jit::load("new.pt") 直接赋值,以为旧 model 会被 RAII 自动释放。错。C++ 中 torch::jit::script::Module 内部持有 std::shared_ptr<:jit::graphexecutorimpl></:jit::graphexecutorimpl>,而 GraphExecutorImpl 里的 CUDA kernel 和 memory allocator 可能被其他 tensor 引用,导致 cudaFree 延迟执行甚至永不触发。
安全做法:
- 每次热更新前,显式调用
model->clear() + model.reset(),再手动cudaDeviceSynchronize() - 用
cudaMallocFromPoolAsync统一管理显存池,更新时cudaMemPoolTrimTo()归还未用块 - 上线前必做压力测试:连续热更新 50 次,用
nvidia-smi -q -d MEMORY观察“Reserved Memory”是否持续增长——涨了就是泄漏
cudaCtxSetCurrent() 会引入不可忽视的同步开销,而用 pthread affinity 把每个模型实例绑死到独立 CPU core,配合 CUDA_MPS_PIPE_DIRECTORY 开启 MPS,才能稳定压测出单卡 8+ 实例的吞吐。


















