最近在把一个基于LangChain的RAG服务部署到生产环境,用的vLLM跑Qwen2.5-7B,向量库是Milvus。本地测试时感觉还行,但上线后并发一上来就发现首token延迟飙到3-5秒,用户反馈很卡。自己看了下链路,感觉是检索和LLM生成串行导致的,但不确定瓶颈到底在embedding、向量检索还是prompt拼接上。也试过调vLLM的prefill参数,但效果不明显。想问下大佬们,这种场景一般怎么优化?是走rerank加缓存,还是把检索和生成改成异步,或者直接上更小的模型?希望有实战经验的朋友能指点一下,先谢过了。
RAG部署后首token特别慢,有什么优化思路吗?
全部回复
共 19 条并发上来首token飙到3-5秒,大概率不是vLLM的问题,我遇到过类似情况,瓶颈往往在embedding和向量检索的排队上。你可以先给Milvus加个连接池,然后把embedding单独拆个服务,这样至少能隔离掉一部分延迟。另外,如果业务允许,把top-k调小点或者加个简单的关键词预筛,比直接上rerank性价比高。缓存倒是建议先做,尤其是高频query,能省不少事。你测过纯LLM首token在空跑时的基准是多少吗?这能帮你定位到底哪段链路吃掉了时间。
我之前也踩过这个坑,实测下来瓶颈往往不在vLLM本身,而是LangChain默认的检索和生成完全串行,建议先给Milvus的检索加个并发,或者把embedding丢到独立进程里,能快不少。另外rerank别急着上,先试试给高频query加个简单的语义缓存,命中率上来后首token能压到1秒内。你这个延迟如果是偶发性的,大概率是prompt拼接里塞了太多历史消息,把模板精简下可能立竿见影。最后想问下,你的vLLM是开的continuous batching吗,并发高的时候prefill和decode混排会影响首token,试试调下max-num-seqs。
并发一上来首token慢,大概率不是模型本身的问题,而是检索链路串行拖的。你可以先打个时间戳埋点,把embedding、Milvus查询、prompt拼接这三段分别测一下,基本就能定位了。我这边经验是Milvus在并发高的时候查询抖动挺明显,加个本地LRU缓存命中率高的话能省不少。另外vLLM的prefix caching如果prompt前缀固定记得开,对首token帮助比调prefill参数直接。
先查向量检索耗时吧,Milvus并发下经常是它拖后腿,不一定是LLM的锅。
先查下检索耗时占比,如果Milvus查询就占了大头,那调vLLM纯属白费劲。异步化确实值得搞,但别忘了prompt拼太长也会拖慢prefill。
我之前也踩过这个坑,首token慢大概率是检索那段卡住了,尤其Milvus并发一高、没建好索引的话延迟很吓人。建议先把embedding和检索的耗时打点看清楚,别急着换模型。另外prompt拼接如果太长,prefill确实会拖慢,但3-5秒更像是检索侧的问题。可以试试加个语义缓存,命中率高的话首token能降不少。
首token慢到3-5秒,大概率不是单纯prefill参数能救回来的,得先把链路上每一段的耗时拆开看。你现在检索和生成串行,embedding加Milvus查询如果没做预热和索引调优,光这一跳就可能吃掉一两秒。建议先用LangSmith或者自己打点,把query embedding、向量召回、rerank、prompt拼装、vLLM首token这几段分别记一下,不然全靠猜很容易调错方向。如果确认是检索侧重,可以考虑把rerank挪到流式输出之后做,或者对高频query做语义缓存,别每次都全量走一遍。vLLM那边如果并发高,留意是不是排队等调度,而不是模型本身慢,开continuous batching和调gpu_memory_utilization往往比改prefill更直接。异步化检索和生成听起来美好,但RAG里生成依赖检索结果,真正能并行的是多路召回或者缓存命中路径,不是简单把两步拆开。模型换小确实能降延迟,但7B换3B对首token帮助有限,反而可能牺牲答案质量,优先级可以放后面。
首token慢八成是prefill阶段被拖住了,检索和embedding这块其实可以提前做。我们之前也是Milvus加vLLM,后来把检索结果做了缓存,热门query直接命中,首token从4秒降到1秒出头。你可以先打个埋点看下各阶段耗时,别急着换小模型,7B在prefill上本来就不算快,把prompt长度压一压可能比换模型更实在。
首token慢大概率不在embedding,Milvus检索一般也就几十毫秒。你可以先打个日志把检索、prompt拼装、LLM首token分段计时,看看时间到底花在哪。我之前遇到类似情况,瓶颈其实是prompt太长导致prefill慢,压一下上下文和rerank数量就下来了。并发上来后vLLM排队也会拉高首token,建议开chunked prefill配合流式输出,体感会好很多。
首token慢大概率不是单点问题,得先拆开看。你可以在链路里加点耗时打点,把embedding、Milvus检索、prompt拼接、vLLM prefill分别计时,这样瓶颈一目了然。我这边类似架构下,Milvus的nprobe调太大加上没开缓存,检索就吃掉一秒多。另外首token对prefill长度特别敏感,检索回来的chunk别一股脑全塞进去,截断或rerank压一压往往比换模型管用。
首token慢大概率是prefill阶段在等着检索结果,串行链路确实很致命。你可以先打个时间戳看下embedding和Milvus检索各占多少,一般向量检索几十毫秒就回来了,反倒是prompt拼太长导致prefill计算量大。试试把检索结果做个语义缓存,命中直接跳过embedding和检索,另外vLLM开chunked prefill对长prompt首token改善挺明显的。
首token慢到3-5秒,我第一反应是vLLM那边其实背了不小的锅。Qwen2.5-7B在并发上来之后,如果没开prefix caching,每个请求的prompt都要重新prefill一遍,RAG的prompt又长,光这一块就能吃掉大半时间。你调prefill参数没用,大概率是因为batching策略和调度才是关键,建议看下gpu_cache_util和max_num_batched_tokens这两个值,别让长prompt把batch挤爆了。
检索这块我倒觉得不一定是主因。Milvus单次查询一般也就几十毫秒,除非你collection没建好索引或者在跑暴力搜索。真正容易踩的是embedding,如果每次query都同步去调一个远程embedding接口,那延迟就说不准了,可以本地起个小的embedding模型或者加个LRU缓存。
串行确实是问题,但改成异步不一定能降首token,用户感知的首token还是得等检索完才能开始生成。更实际的做法是把检索结果缓存起来,相似query直接命中,再配合rerank只对top结果精排,别对全量做。
模型方面我倒不建议直接换小的,7B已经不算大了,除非业务对质量要求不高。先拿profiling把链路每段耗时打出来,别猜,很多时候瓶颈跟你以为的完全不一样。
首token慢大概率是prefill阶段被检索卡住了,你可以在每个环节打点看下耗时分布,别只盯着vLLM。我之前遇到过Milvus并发查询时连接池不够,检索直接拖到2秒多,加了连接池和topk限制就好很多。另外prompt拼接太长也会明显拖慢prefill,可以考虑把检索结果压缩或做摘要再塞进去。异步化确实有用,但得先把真正瓶颈找出来,不然改了一圈还是卡。
先把检索和生成链路打点看下耗时分布,别急着换模型,很多时候瓶颈在embedding那步。
先别急着换小模型,7B首token 3-5秒大概率不是模型本身的问题。你可以在链路里打点看下embedding和Milvus检索各占多久,很多时候是查询向量化和检索那一步在并发下排队了。另外LangChain默认的prompt拼接挺重的,模板渲染和上下文塞太长也会拖prefill。建议先把耗时拆开看,再决定是加缓存还是换异步,不然容易白折腾。
先查查embedding和Milvus检索各占多久,很多时候瓶颈在向量查询那。串行改并行加个缓存,首token能降不少。
学到了,感谢分享!
刚入门,这个对我帮助很大。
我之前也遇到过类似问题,后来换了方案。