日期:2026-08-14
我们接了一批图文混合的推理请求,用户传一张图加一段描述,模型要读懂图再答。最初这套链路是同步的:图片解码、OCR 抽文字、尺寸归一全堵在主推理线程里,一张大图进来,整条请求卡在预处理上,后面的请求排队,GPU 利用率反而上不去,因为卡大部分时间在等人把图处理完。我们当时看监控很困惑,显存占满、算力闲置,吞吐就是上不来,后来才定位到是预处理和推理抢同一根线程。随着图文请求占比从一成涨到近四成,这个问题从偶发变成了日常瓶颈,不解决扩容再多卡也白搭。
系统基于 vLLM 加自研预处理服务的架构。我们单独拉了一条预处理管线,图片解码、缩放、OCR、音视频转写全部异步化,处理完的干净输入(文本加结构化图像特征)才进主推理队列。预处理结果按输入指纹缓存,同一张图被多个请求复用时直接命中,不用重复解码。主推理线程只吃干净输入,不再被大图或异常格式拖累。前端看到的还是一次请求,背后已经拆成两段,用户无感,但链路内部的资源争抢彻底分开了。
第一个难点是预处理和主推理争资源。我们给预处理单独划了 CPU 池和显存预算,不和生产推理抢,避免互相拖累,即便预处理忙也不会把推理挤慢。第二个难点是大图和异常格式兜底,有的图十几兆、有的是损坏文件,同步时直接把线程卡死,异步后我们加了超时和降级,解不出就返回结构化错误让主推理走兜底回答,绝不阻塞整条队列。第三个难点是异步链路的超时与重试,预处理慢了不能无限等,我们设了分级超时,预处理超时就先返回文本部分能答的,别让用户干等。第四个难点是缓存一致性,同一张图内容变了但指纹没变会命中旧结果,我们对内容哈希做指纹,避免脏缓存。我后来觉得,预处理缓存这步最容易被忽略,其实图文场景里同一张商品图被反复问是常态,命中一次缓存省下的算力比多开一个实例划算。
还有一块是预处理失败的可观测,异步之后故障更难排查,我们给每个预处理任务打了链路追踪,解码慢、OCR 超时都能单独看见,不至于和推理混在一起查半天。早期没这层,一次 OCR 服务抖动让我们误判成推理变慢,白查了一晚,之后可观测就成了标配。
另外,预处理和推理之间的背压也要管,预处理快、推理慢时中间队列会堆,我们设了队列上限,堆满了就背压回预处理侧,避免内存被打爆。早期没这层,一次大图洪峰把队列撑爆,整实例 OOM,之后队列上限成了必选项,链路才真正稳。异步不是把活挪走就完事,挪走之后两端怎么互相兜底,才是异步链路真正的难点,我们在这上面交过学费。
改造后,预处理耗时占整条链路的比例从约四成降到约一成二,GPU 利用率从约五成提升到约八成。P99 时延下降约三成五,异常格式(损坏或超大图)的失败率从约百分之五降到约千分之三。预处理缓存命中率约四成,相当于每天省下近四成的重复解码算力。一个季度推理成本下降约两成,而图文请求占比还在涨,如果还走老链路,这批增长会直接把成本拉爆。运维侧最直观的感受是,大图高峰时不再有请求雪崩,队列长度平稳了。
顺带一提,预处理异步之后,单实例能扛的并发比同步时高了近一倍,同样的卡数服务能力直接翻倍,这比单纯堆硬件划算得多,扩容压力小了一大截。
多模态推理最要紧的是把预处理挪出去异步掉,主推理线程腾不出来,GPU 再贵也是空转等人,这个坑我们交过学费才认。异常格式一定要在预处理阶段就兜住,同步链路里一张坏图能卡死整条线程,那种半夜告警的事故不想再来一回。缓存别省,图文场景的复用率比想象高,命中一次顶多开半台实例,指纹用内容哈希而不是路径,脏缓存的雷也顺手排了。说到底,多模态的瓶颈常常不在模型,在模型前面的那堆脏活,谁先把脏活理顺,谁先把吞吐做起来,模型选得再好也救不了被预处理拖死的链路。
缓存这步我们起初也没当回事,后来发现图文场景里同一张商品图被反复问是常态,命中一次省下的算力比多开半台实例划算,这才把缓存当刚需。异步和缓存两件脏活做好,链路才算真正顺,这账我们算过好几遍,结论一直没变。
异步和缓存看着是基础设施的脏活,实则决定了多模态能不能真正上线,我们在这上面花的时间比选模型多得多,也踩得坑最多。
案例片段(已脱敏): 预处理异步队列与缓存配置:
yaml preprocess: async_queue: true dedicated_cpu_pool: 16 vram_budget_gb: 8 timeout: image_decode_ms: 3000 fallback: text_only # 解不出先答文本 cache: key: content_hash # 内容哈希, 防脏缓存 ttl: 3600链路监控日志节选:[2026-08-03] req=R_551209 图文混合 预处理 280ms(缓存命中), 推理 640ms 整链 920ms, 预处理占比 30%(未命中时约 40%) 异常格式当日失败率 0.3%