最近在折腾MCP Server,想把一个用PyTorch训练的文本分类模型封装成工具,让LLM能直接调用。本地跑推理没问题,但一接到MCP请求,推理几次后显存就爆了。我用了torch.no_grad(),也试了del model和torch.cuda.empty_cache(),但好像没完全释放。是不是MCP每次调用都会重新加载模型?还是需要自己搞个模型池管理?有没有踩过坑的大佬指点一下,或者推荐个轻量级的推理框架结合MCP用?谢了!
MCP Server接入PyTorch模型,推理时总报显存泄漏怎么排查?
全部回复
共 179 条八成是MCP每次请求都新加载模型没卸载干净,试试把模型初始化放全局变量里,或者用FastAPI套一层搞个常驻进程。
大概率是每次请求都新加载了模型,建议用模型池或者全局加载一次,别反复初始化。
大概率是模型没复用,每次请求都新加载一份到显存,试试把模型实例化成全局变量或者搞个LRU缓存池。
你这个问题我太有同感了,之前搞过类似的MCP封装,第一反应也是怀疑模型重复加载。但你看下日志里每次请求的进程PID是不是同一个,MCP默认是常驻服务的话,其实模型不会反复load,真正坑的是PyTorch的缓存分配器——它占着显存不还给驱动,empty_cache只是清空未用块,不是释放给其他进程。我后来是直接给推理包了个独立子进程,每个请求走一次队列,完事儿直接杀进程,显存立刻归零,虽然麻烦但绝对干净。要是嫌重,可以试试用vLLM或者Triton这类带显存池的服务框架,把模型常驻在那边,MCP只做HTTP转发,这样就算崩了也能自动重启。还有个细节,检查下是不是输入tensor的batch维度没固定,MCP请求里如果文本长度变化大会触发动态shape,导致缓存碎片化,最好pad到固定长度。再就是确认下有没有把模型放进eval模式并且禁掉grad,有时候某个模块没切干净,反向传播的缓存会偷偷累积。实在不行就上nvidia-smi监控一下每次请求前后显存变化的曲线,别只看总量,看是稳步上涨还是突然跳变,能帮你定位是泄漏还是缓存碎片。
我之前也踩过一模一样的坑,MCP默认的调用生命周期确实容易让人误以为模型会复用,但实际上每次请求进来,如果你是在函数内部初始化模型的话,那它确实会被反复加载和销毁,显存碎片就是这么攒出来的。你试的那几个方法都治标不治本,del和empty_cache在PyTorch里对已分配缓存块的回收作用很有限,尤其当CUDA context被MCP的worker线程反复触发时。我建议你把模型加载挪到模块顶层或者用一个全局的单例类持有,这样MCP每次调用只是走inference函数,模型权重和CUDA context都能保住。另外可以看看是不是输入tensor没有显式detach或者转移到CPU,有些情况下计算图虽然不保留梯度,但中间变量依然会挂在显存上。如果你不想自己搞模型池,可以试试vLLM或者Triton Inference Server,它们对显存管理更成熟,跟MCP对接也就一个HTTP封装的事。不过最轻量的方案其实是写个简单的LRU缓存,把模型实例和当前线程绑定,配合max_split_size_mb调一下PyTorch的缓存分配器,基本就能解决。
大概率不是MCP的问题,而是MCP进程和推理进程没隔离,模型被反复加载进显存了。建议把PyTorch模型单独起一个常驻服务(比如FastAPI或Triton),MCP那边只发HTTP请求,这样显存只分配一次。另外别手动del和empty_cache,容易干扰PyTorch的缓存策略,直接让进程活着就行。轻量方案可以看看vLLM或者CTranslate2,不过你这模型小的话,其实搞个简单的单例模式就够了。
这问题我熟,之前搞过类似的MCP封装,八成不是模型本身泄漏,而是MCP的请求生命周期没处理好。你试的那几个方法其实都没错,但torch.cuda.empty_cache()只是清缓存,如果模型或者中间变量还被引用着,显存根本不会还回去。我猜你是在server的handler里直接加载模型然后推理,每次请求都新建一个model实例,旧的在请求结束后没有被python的gc立刻回收,多来几次自然爆。建议你把模型做成模块级单例,在server启动时load一次,之后所有请求共用,推理完只清中间tensor,别del model。要是真需要多模型切换,就搞个简单的dict缓存,key是模型名,value是模型对象,超过两个就LRU淘汰。另外MCP如果用的starlette或fastapi这类异步框架,记得推理逻辑丢到线程池里跑,别在event loop里阻塞,不然上下文管理会乱。轻量级方案的话,可以试试把PyTorch转成ONNX或者TorchScript,用ONNX Runtime跑,显存占用小很多,而且能直接控制session释放,配合MCP更稳。
大概率不是MCP的问题,你每次请求进来模型重新加载这个猜测方向是对的,但更关键的是PyTorch的缓存机制,empty_cache只是把未使用的块标记为可复用,显存占用数字不会立刻降。建议你把模型加载和推理拆成独立进程,用gunicorn或者FastAPI起个常驻服务,MCP只做HTTP转发,这样显存生命周期可控得多。另外试试torch.inference_mode替代no_grad,能省不少峰值内存,推理框架的话vLLM太重了,可以看看CTranslate2,对文本分类这种小模型特别友好。
我之前也踩过一模一样的坑,最后发现根本不是模型没释放,是MCP的worker进程没退,显存一直被那个进程占着。你试试在每次推理完强制把CUDA context销毁掉,torch.cuda.reset_peak_memory_stats可能都不够,得用multiprocessing配合,让每个请求走独立子进程,跑完直接杀掉,这样最干净。另外不建议在MCP里反复load模型,加载一次的耗时和显存碎片化反而更严重,搞个简单的模型池,比如用队列存两个实例轮询,比每次重建靠谱得多。至于轻量级框架,你可以看看vLLM或者CTranslate2,但如果是小模型,其实自己写个FastAPI服务包一层,再用MCP去调HTTP,比直接在MCP进程里跑PyTorch稳很多,也方便横向扩容。还有个小技巧,用torch.cuda.memory_summary()看下到底是哪块内存没释放,有时候是缓存分配器的问题,不是你的代码漏了引用。我之前还试过在MCP的tool函数里显式加上gc.collect(),配合empty_cache确实有点用,但治标不治本。你要是想省事,直接上Ray Serve或者BentoML,它们自带生命周期管理和显存隔离,跟MCP对接也容易,就是稍微重了点。
之前踩过类似的坑,MCP每次请求默认都是独立上下文,模型确实会被反复加载,但显存没释放的核心问题往往不在模型本身,而是PyTorch的缓存分配器没把显存还给驱动,empty_cache只是清空缓存块,不意味着归还给系统。你试过加载模型时加个max_split_size_mb参数没,或者干脆用torch.set_per_process_memory_fraction限制上限,这样哪怕缓存不释放也不会爆。另外del model只是删引用,如果MCP内部有全局变量或者请求队列持有模型输出,那照样泄漏,建议你把每次推理的输入输出都显式赋成None然后强制gc.collect()。模型池是个方向,但更轻量的做法是用vLLM或者TGI这种自带连续批处理和显存管理的推理框架,它们对MCP这种短生命周期请求友好很多,PyTorch原生接口在并发场景下确实不够用。还有一个隐藏点,MCP工具定义里如果用了pydantic模型做请求体,那个数据在GPU上没拷贝回CPU,也会一直占着显存,检查下是不是这个原因。
你这个问题我踩过,核心不是no_grad,而是MCP的请求生命周期和模型的生命周期绑在一起了。每次调用都重复加载权重,显存肯定扛不住,建议把模型初始化放到全局作用域,用单例模式或者lru_cache缓存住,推理完只释放中间tensor。另外torch.cuda.empty_cache()最好在请求结束后异步等一帧再调,不然有时候显存碎片化还是会被占着。轻量框架的话可以看看vLLM或者CTranslate2,不过文本分类这种小模型其实自己写个池子就够了,别过度设计。
大概率是MCP进程常驻导致模型没被回收,试着把推理丢到子进程里跑完就杀,或者用vLLM这类带连续批处理的框架顶一下。
大概率不是MCP重新加载模型,而是每次请求都新建了tensor图或者推理输出没解引用,试试把模型和tokenizer都定义成模块级单例,再在推理函数里显式清理中间变量。另外torch.cuda.empty_cache()要放在del之后且等几毫秒才生效,建议直接看nvidia-smi确认是哪个进程在涨。如果实在嫌麻烦,可以试试用vLLM或者FastAPI包个独立服务,MCP只做转发,这样显存和进程隔离都干净很多。
大概率是MCP请求生命周期没管好,试试把模型加载扔到全局单例里,别每次请求都初始化。
大概率是MCP每个请求都新起进程加载模型,旧进程没回收,试试用vLLM或TGI起个常驻服务,MCP只发HTTP请求。
这问题大概率不是模型没释放,而是MCP的请求处理线程里每次都会新建计算图,即使有no_grad,PyTorch的缓存分配器也不会立刻把显存还给驱动。你可以试试在推理函数入口固定用同一个model实例,别在请求里初始化,再加个torch.cuda.set_per_process_memory_fraction限制上限,比empty_cache管用。我之前用vLLM或者Triton Inference Server包一层,再让MCP去调HTTP接口,反而省心很多,模型池都不用自己写。
这问题我遇到过,大概率不是MCP重复加载模型,而是每次请求都新建了tensor又没及时释放,尤其attention的中间变量特别占显存。你可以试试在推理函数里把输入输出都转成CPU再返回,GPU上只留模型权重,或者用torch.inference_mode替代no_grad,能省不少内存。另外建议别手动管缓存,直接用vLLM或者TGI这类推理框架,内置了连续批处理和显存优化,接MCP只做转发,省心很多。
大概率不是MCP每次重新加载模型,而是请求进来时模型还在显存里,但PyTorch的缓存机制把碎片空间占住了,你手动empty_cache其实治标不治本。我之前遇到类似情况,最后是把模型推理包成一个常驻进程,用FastAPI或者gRPC暴露接口,MCP只做转发,这样显存生命周期完全可控。你不如先监控一下每次请求前后的显存占用曲线,确认是线性增长还是固定峰值,如果是前者那多半是你代码里某个张量没释放。另外可以试试把输入batch强制设成1,有时候MCP并发请求会隐式堆叠。
大概率是模型加载逻辑写在请求处理里了,换个常驻进程或vLLM这类服务化框架试试。
这问题大概率不是MCP每次重载模型,而是PyTorch的缓存分配器在作祟,显存碎块没还给驱动。可以试试在请求结束时调一下torch.cuda.synchronize()再配合empty_cache,或者干脆用vLLM或者TGI这类自带显存管理的服务把模型包一层,MCP只做转发,省心很多。另外如果并发量不大的话,用mlflow或者自己写个简单的LRU模型池也行,别频繁load/unload,那样反而容易触发碎片问题。