做AI模型推理服务的性能调优时,我第一优先看的往往不是模型结构,而是多线程调度——因为很多线上问题根本不是算力不够,而是并发模型设计不合理。AI模型推理链路本来就包含数据预处理、张量计算、结果后处理等多个阶段,单靠一个大循环串行处理请求,CPU利用率和吞吐量都很难上去。这篇文章就把我在推理服务多线程性能调优方面积累的方案、参数计算方法和踩坑经验整理出来,给正在做推理服务化、模型部署或者准备性能优化的同学做个参考。
很多人一说调优就想着换GPU、加机器,实际上大部分推理服务在单机单卡上还有一大截性能没榨干。问题往往出在“任务怎么切、线程怎么安排、队列怎么控”这三件事上。多线程不是简单地让处理更快,而是让整条处理流水线的每个阶段都有活干、不空转。下面我从线程模型设计、落地实现、参数计算到问题排查,把整套思路完整过一遍。
1. 为什么AI模型推理要聊多线程
1.1 推理瓶颈到底在哪里
模型推理不是只有“模型计算”这一件事。一个典型的推理请求从进来到最后返回,至少要经过输入解析、数据预处理、张量计算、后处理、结果序列化这几个环节。拿图像模型举例,前处理要做解码、缩放、归一化;拿语言模型举例,前处理要做token化、padding。很多人以为推理耗时全在模型计算上,实际在CPU推理或小batch场景里,前处理和内存拷贝能占到总耗时的30%到50%。
我见过不少服务,模型单次推理只要10毫秒,但整体链路却要30毫秒。差的这20毫秒全花在数据处理和线程切换上。这还没算上排队等待的时间。CPU在等内存加载、等I/O返回的时候,如果线程没有别的事可做,CPU就在空转。多线程的核心目的,就是让一个线程在等待的时候,其他线程能继续推进计算。
1.2 多线程带来的不是“单请求变快”
很多新手有个误解:开了多线程,单个请求应该变快。实际上对于单次模型推理,它内部的算子并行才是关键,业务层的多线程更多是解决“多请求并发”和“多阶段重叠”的问题。
多线程真正带来的是两个东西:吞吐量和资源利用率。当一个请求在做后处理时,另一个请求可以同时在做前处理,第三个请求正在被模型计算占用。这样单请求时延不一定下降,但单位时间内完成的请求数会明显上涨。对线上服务而言,QPS和P99时延往往比单请求时延更重要,这也是为什么并发模型设计会直接决定服务能不能扛住流量。
1.3 多线程还是多进程:模型推理的特殊性
模型推理有个特点是“权重只读、状态可变”。同一个模型文件加载到内存后,多个线程可以共享这份权重,大大节省内存。但如果用多进程,每个进程都要单独加载一份模型,内存占用直接翻好几倍。对动辄几个GB的大模型来说,多进程方案很多时候根本跑不起来。
那什么时候才考虑多进程?一种情况是使用了非线程安全的推理组件,比如某些Python封装不允许同一模型实例被并发调用;另一种情况是前处理里有大量纯NumPy计算,被GIL卡住了。这种时候可以把前处理放到ProcessPoolExecutor里做,把真正的模型推理留在线程池里。多进程负责并行处理CPU密集型预处理,多线程负责承载模型请求调度,两者配合比单用任何一种都稳。
1.4 从“for循环内多线程”这个高频场景说起
很多开发者最初接触并发都是从“for循环里开线程”开始的。比如有一批请求要分别做推理,就写一个for循环,每个请求启动一个线程去处理。这种做法在请求量小的时候没问题,但并发一上来就非常危险。
for循环内每轮都创建新线程,意味着线程的生命周期完全失控。线程创建、销毁本身有开销,而且线程数量不受限制时,操作系统光做线程上下文切换就忙不过来。更可怕的是,如果每个推理任务里还有阻塞等待,线程会越积越多,最终把CPU和内存全部耗尽。这个场景在Java里尤其常见,很多人用CompletableFuture.runAsync却完全不指定线程池,导致所有任务都涌进公共ForkJoinPool,性能反而比串行还差。后面我会专门讲如何把这个“朴素写法”重构成正规的线程池方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 推理服务中的线程模型设计
2.1 先分清两类并行
推理服务里存在两类“并行”,它们的目标完全不同。
第一类是单请求内部并行。一个模型推理算子可以拆成多个计算单元并行执行,这通常由推理框架底层完成,比如PyTorch的intra-op线程、TensorRT的kernel并行。这类并行的目的是降低单次推理时延。
第二类是请求间并行。多个请求同时进入服务,由多个线程分别处理,或者被聚合到一个batch里一起推理。这类并行的目的是提升系统吞吐和硬件利用率。
调优的时候必须先分清你现在调的是哪一层。经常有人把外层请求线程数调得巨大,结果内层算
