ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从KV Cache到RadixCache:sglang生产级推理服务部署实战

从KV Cache到RadixCache:sglang生产级推理服务部署实战 最近一直在折腾AI后端手头这个助手项目内部代号小智要从单机脚本升级成对外服务的正式后端。模型本身没让我头疼头疼的全在推理层多轮对话重复计算历史KV长系统提示词把显存吃满并发一上去响应时间直接崩。为了把这条链路理顺我把sglang从原理到部署完整摸了一遍包括sglang serve启动推理服务、和vllm的选型对比、内部缓存机制、生产参数调优。这篇文章就把这轮实操完整沉淀下来适合正在做AI助手、RAG问答、智能客服、内容生成这类后端服务的工程师参考。sglang是一个专为LLM推理设计的开源服务框架核心卖点三块RadixCache带来的请求级前缀缓存复用、连续批处理带来的高吞吐、以及原生支持的受约束解码。和vllm这类框架放在一起比它不是玩具而是能在生产环境扛真实流量的后端引擎。全文没有讲稿式的概念堆砌能直接抄的部署脚本、参数解读和排坑实录都在后面。1. 这套后端架构到底在解决什么问题1.1 我是怎么拆解这个需求的先说需求。小智这个后端最初长这样一个FastAPI进程把prompt拼好后发给模型模型同步返回。业务简单时没问题但一旦会话变长、并发上来问题全暴露了。第一是重复计算。用户和助手聊到第10轮每轮都会把前面所有历史消息重新过一遍模型前面算过的KV Cache全部白费。第二是并发调度粗糙。请求之间没有精细的批次编排GPU要么空闲要么过载。第三是格式不稳定。业务需要模型返回固定JSON字段模型心情好给markdown心情不好给注释。我这边做过一个粗略估算。假设每轮对话300 token聊到第5轮新请求要处理的前缀大约是1500 token其中至少1200 token在上几轮已经算过。没有缓存时这1200 token的计算量每轮都在重复消费有缓存时只需要算新增的300 token。一个用户省这点看不出来一万个用户同时多轮对话差距就是数量级的。所以我的结论很直接AI后端的第一优先级不是换更好的模型而是把推理层的复用能力做出来。1.2 为什么sglang能成为这类后端的核心sglang的底层思路并不神秘一句话总结把KV Cache组织成一棵可复用的树新请求进入时做最长前缀匹配命中的部分直接复用不命中才做增量计算。这个思路具体到工程实现涉及三块调度器决定请求什么时候可以被合并进同一个批次运行时负责管理KV Cache块和显存缓存管理模块负责前缀的插入、查找和淘汰。三块合在一起就是一个具备记忆能力的推理引擎。这里可以给一个生活化类比RadixCache就像浏览器的缓存。你打开同一个网站浏览器不需要重新下载图片和脚本因为本地有副本sglang处理带共享前缀的请求也是如此——系统提示词、Few-shot示例、历史对话这些都是“静态资源”第二次遇到就不用重新计算。我翻sglang源码时重点就看三块请求如何拆解、批次如何调度、缓存树如何管理。把这三点吃透后面调参才有依据而不是别人说调哪就调哪。1.3 架构的最终形态最终落地的形态没有整花活就是一个标准的三层结构业务层拼prompt路由层做模型分发推理层由sglang实例组成。小智这个项目里跑了一个对话模型和一个轻量抽取模型分别在30000和30001两个端口启动对外全部暴露OpenAI兼容接口。业务侧只需要把base_url指向对应端口剩下的事全交给sglang。我刻意没有在这一层做太重的封装原因有两个。一是OpenAI兼容协议已经是事实标准任何语言的SDK都认识它没必要再造一套协议二是sglang自身已经提供了批处理、缓存、并发控制业务层再做一遍等于重复造轮子。整个架构的核心收益是业务线不再关心模型怎么跑得快只需要关心prompt怎么拼得稳。2. sglang和vllm怎么选一份长期踩坑后的对照表2.1 七个维度的硬核对比只要提到sglang必然绕不开vllm。我自己的习惯是不站队把两边都部署起来做相同压测用数据说话。下面这份对照参考来自我近两个月的实际使用不涉及任何benchmark营销数据更多是工程感受。对比维度sglangvllm连续批处理支持调度粒度较细支持成熟稳定前缀复用RadixCache全局树状结构细粒度复用Prefix caching简单场景够用结构化输出原生支持受约束解码依赖guided decoding或Outlines多模态支持常见视觉模型支持覆盖也广生态成熟度社区活跃、接口迭代快生态更成熟、跨框架集成更多源码可读性缓存与调度逻辑集中易学习代码库更庞大生产稳定性新版本偶有破坏性变更版本演进更稳表格里最值得注意的是前缀复用那一行。vllm也支持prefix caching但在复杂共享前缀场景下sglang的RadixCache对公共前缀的复用粒度更细因为它在树结构上做最长前缀匹配而不是简单的hash命中。这个差别在小流量下不明显在高并发多轮对话场景会拉开差距。2.2 什么场景选sglang什么场景选vllm我实际在项目里的做法是在线Chat类业务全部走sglang离线批量生成和固定模型服务走vllm路由层把两边隔离开互不影响。拿小智这个项目来测固定系统提示词加多轮历史的场景sglang相比无缓存基线吞吐提升大约在四成到六成之间具体数值和批次大小、模型结构关系很大。我不建议你直接照搬任何人的数字但方向可以确认前缀稳定性越高sglang的优势越明显。简单给个选型建议多轮对话为主、带共享系统提示词、用户量大 → sglang更合适只要一个稳定OpenAI兼容服务、不想频繁升级 → vllm更省心输出格式要求严格需要稳定JSON/表格 → sglang的约束解码更顺手团队要源码级二次开发 → sglang的缓存和调度逻辑相对集中好上手3. 核心机制拆解RadixCache、连续批处理与结构化输出3.1 RadixCache到底怎么省算力先聊RadixCache因为它是sglang对比其他框架最核心的差异点。要理解它为什么能省算力先得算一笔账。Transformer推理过程中每个token都会产生一组KV状态供后续token做attention。这部分状态的大小可以粗略估算KV字节数约等于2乘hidden_size乘层数乘每个元素字节数。以一个hidden大约4096、32层的7B级模型为例fp16精度下单个token的KV大约在0.5MB上下。一次请求如果带1000 token的公共前缀无缓存时需要完整算这1000个token的KV有缓存时这部分直接跳过。这个节省量放大到生产环境是什么概念假设系统提示词有500 token一天处理10万次请求每次都在重复计算这500 tokenRadixCache命中后可以少算整整5000万token的KV。这还不包括多轮对话里的历史上下文。很多团队觉得sglang快本质不是算子优化多神奇而是把这类重复计算直接砍掉了。3.2 连续批处理与显存管理RadixCache解决的是“重复计算”连续批处理解决的是“GPU利用率”。传统做法是请求来了就占一块显存独自推理请求多就排队sglang的连续批处理则是按照迭代级别调度每一轮transformer前向尽可能把多个请求的不同token段拼成一个批次。已经完成的请求立刻退出新请求随时插入。打个比方这就像高速收费站不是整条车队一次性全部通过而是每条车道时刻有进有出、不停闸。这个机制直接决定显存怎么分配因为sglang会预分配一个KV Cache池池子大小由mem-fraction-static这类静态显存比例决定。你把这个参数理解成“给缓存池上的保险额度”就好太高可能装不下太低浪费空闲显存。3.3 结构化输出、SGL语法与源码级理解第三个让我真正选定sglang的点是结构化输出。生产环境里模型输出最怕两件事格式跑飞和字段缺失。sglang可以在生成阶段做受约束解码限制候选token的合法性让输出稳定落在你想要的结构里。一个典型例子是用正则或者JSON schema约束输出示意代码如下import sglang as sgl sgl.function def extract(s, text): s 请从下面文本中抽取关键信息输出JSON\n s text s \n结果\n s sgl.gen(result, max_tokens128, regexr\{.*\})注意这段正则只是示意生产环境建议用更完整的schema。重点是原理约束解码不是生成完再校验而是在解码每一步就排除不合法token几乎不会出现格式跑飞。读源码时我还有一个收获调度器会把请求拆成两类基本原语一类负责把prompt前缀追加到会话里另一类负责在已有状态上继续生成token。理解了这两条路径你就明白为什么有些请求处理得快、有些请求要排队也就能对应上启动参数里和缓存、并发相关的配置项。4. 从零跑通服务部署sglang后端的完整实操4.1 环境准备与安装硬件、软件、模型权重先说硬件。如果你只是想要一个能跑通的服务8B级别模型单卡24G以上就可以70B级别建议至少4卡80G再往下真的不建议硬撑。显存不是越多越好而是要为KV Cache预留足够余量这一点后面参数部分会反复强调。软件层面我建议Ubuntu 20.04/22.04Python 3.10以上CUDA 12.xPyTorch版本与CUDA匹配即可。sglang的安装推荐用官方二进制一条命令能装完大部分依赖pip install sglang[all]需要做二次开发的话源码编译更合适方便你随时改调度或缓存逻辑。模型权重同样建议提前下到本地。搞一个/data/models目录用HuggingFace的下载工具或国内镜像都行关键是避免每次服务启动都去拉权重。启动时网络波动和磁盘IO都可能造成加载失败本地目录会省心很多。4.2 启动推理服务sglang serve 的命令与参数解读启动服务本身不复杂核心命令是这样的python -m sglang.launch_server \ --model-path /data/models/Qwen2.5-7B-Instruct \ --host 0.0.0.0 \ --port 30000 \ --trust-remote-code \ --mem-fraction-static 0.85 \ --max-running-requests 128逐项拆一下。--model-path指向本地权重目录--host 0.0.0.0允许外部访问--port定义服务端口--trust-remote-code在加载需要远程代码配置的模型时建议加上--mem-fraction-static是静态显存预留比例直接影响KV Cache池大小--max-running-requests限制同时处理的请求数防止队列无限积压。多卡场景用--tp-size做张量并行。比如4卡一起推理一个模型python -m sglang.launch_server \ --model-path /data/models/Qwen2.5-7B-Instruct \ --host 0.0.0.0 \ --port 30000 \ --tp-size 4 \ --mem-fraction-static 0.9如果模型本身是量化权重比如AWQ版本加上--quantization awq即可python -m sglang.launch_server \ --model-path /data/models/Qwen2.5-7B-Instruct-AWQ \ --quantization awq \ --host 0.0.0.0 \ --port 30000下面这几个参数是生产环境里我最常调整的参数作用我的经验值--mem-fraction-static静态显存预留比例影响KV Cache池大小单卡0.8~0.85TP多卡可到0.9--max-running-requests最多同时处理的请求数64~256按业务并发定--max-total-num-tokens总token上限防止超长请求打爆显存结合context-length设置--context-length单请求最长上下文不超过模型支持长度--radix-cache-sizeRadixCache缓存上限默认够用命中率低时再调大--tp-size张量并行卡数按卡数和显存定我的建议是先用默认参数把服务跑通再逐步加这些配置。一次给太多参数出了问题你很难定位是哪个环节造成的。4.3 用OpenAI兼容接口快速接入服务起来以后直接用OpenAI的协议就能调用不需要额外写SDK。最直观的方式是先curl一下curl http://127.0.0.1:30000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: /data/models/Qwen2.5-7B-Instruct, messages: [ {role: system, content: 你是一个严谨的AI助手回答尽量简洁。}, {role: user, content: 什么是KV Cache} ], max_tokens: 256, temperature: 0.7 }业务代码里用Python的话直接把openai库的base_url指过去就能复用from openai import OpenAI client OpenAI( base_urlhttp://127.0.0.1:30000/v1, api_keyEMPTY ) resp client.chat.completions.create( modeldefault, messages[{role: user, content: 介绍下你自己}], max_tokens256 ) print(resp.choices[0].message.content)这里的model字段在实际请求里不会造成混乱服务端以启动时的模型为准。调试阶段可以访问/get_model_info查看模型信息和显存占用访问/health这类端点做健康检查比翻日志快得多。4.4 多模型多端口的生产化管理实际业务不会只有一个模型。小智这套后端现在跑两个模型一个7B对话模型负责日常问答一个轻量抽取模型负责信息抽取。我的做法是各开一个sglang实例分别占用不同端口和GPU用一个统一的管理脚本拉起#!/bin/bash python -m sglang.launch_server --model-path /data/models/Qwen2.5-7B-Instruct --port 30000 --gpu-id 0 python -m sglang.launch_server --model-path /data/models/Qwen2.5-3B-Instruct --port 30001 --gpu-id 1 wait多实例管理有几个坑。第一端口别冲突启动前先检查。第二每个实例的显存预留比例要按各自所在GPU单独算不要套同一个数值。第三如果两个模型结构差异大不建议强行合并到同一个进程中物理隔离最省心。理由很简单推理服务的稳定性优先级永远高于显存利用率。5. 生产化要过的几道坎压测、监控与参数调优5.1 自己写一个并发压测脚本压测我不推荐一上来就上重型工具先用一个几十行的脚本把sglang打热观察有没有OOM、延迟曲线平不平滑。下面这个脚本用asyncio并发发请求统计每轮TTFT、总耗时和完成数import asyncio import time from openai import AsyncOpenAI client AsyncOpenAI(base_urlhttp://127.0.0.1:30000/v1, api_keyEMPTY) async def one_call(idx, prompt): t0 time.perf_counter() resp await client.chat.completions.create( modeldefault, messages[{role: user, content: prompt}], max_tokens64 ) ttft time.perf_counter() - t0 return idx, ttft, resp.choices[0].message.content async def main(): tasks [one_call(i, 请用两句话解释什么是连续批处理) for i in range(50)] results await asyncio.gather(*tasks) ttfts [r[1] for r in results] print(完成:, len(results), 平均TTFT:, sum(ttfts) / len(ttfts)) asyncio.run(main())跑完以后重点看三件事有没有请求报错平均TTFT是否随并发涨显存曲线是否平稳。如果前两项有问题基本可以推断是参数配比不对而不是模型本身的问题。5.2 从压测数据反推参数压测数据不会直接告诉你该调哪个参数但能暴露倾向。如果TTFT整体走高说明请求在排队先看max-running-requests是不是设得太小如果中途出现CUDA OOM说明mem-fraction-static或者上下文长度挤占了缓存池如果RadixCache命中带来的吞吐提升不明显优先确认prompt前缀是否稳定再考虑radix-cache-size是否够大。我的调参顺序比较固定。先把默认参数跑通然后在固定并发数下逐步提高负载观察显存和延迟。接着调整mem-fraction-static至少保留15%显存余量给模型权重和激活值。再调整max-running-requests找到延迟曲线的拐点范围。最后才动radix-cache-size因为它和缓存淘汰策略相关调不好反而会影响命中率。5.3 上生产前的几个注意事项上生产之前有四个细节值得单独拎出来。第一健康检查要接上轮询/health这类端点而不是只看进程是否活着。第二日志级别和保留期提前规划线上事故排查时debug日志就是救命稻草。第三模型更新不要覆盖式发布新权重先用一小部分流量验证再逐步切全量推理引擎最怕权重文件被意外替换。第四客户端连接要复用不要每次请求都新建连接。还有一点容易被忽略稳定性永远优先于性能。我宁可把mem-fraction-static降到0.8让缓存池小一点也要保证模型权重和激活值不会把显存撑爆。一个OOM导致的大面积超时抵掉你省下的10%吞吐这笔账怎么算都不划算。6. 常见问题与排查技巧实录6.1 高频问题速查表这阵子踩过的坑和帮同事排查过的问题整理成一张速查表遇到同类问题可以先对着查现象常见原因排查和解决办法CUDA out of memorymem-fraction-static过高或单请求上下文太长降比例、限制context-length/max-total-num-tokens、加大TP模型加载失败权重目录不对、网络没拉到模型、依赖远程代码检查模型路径加--trust-remote-code提前本地化权重首token特别慢prompt太长且缓存未命中检查prompt前缀稳定性确认RadixCache状态客户端大量超时max-running-requests过小或上游并发暴增提高上限、扩容实例、压测找拐点多卡服务性能不升反降卡间通信成为瓶颈检查NVLink/PCIe带宽换更小的并行粒度或增加批大小缓存命中率几乎为零system prompt和few-shot拼得不固定把常见prompt设计成固定前缀模块这个表里最容易被忽略的是最后一类。很多团队把sglang性能调不上去不是框架本身慢而是prompt拼得越来越随机导致RadixCache根本无从命中。你换个角度想每次请求都带一个随机时间戳、随机顺序的few-shot缓存树永远匹配不上。这跟你用浏览器浏览每次都更换URL一样缓存形同虚设。6.2 一个值得长期坚持的排查习惯我最后想说一个排查习惯监控RadixCache的状态。用日志或者管理接口观察缓存命中信息确认它对吞吐的影响。调试时我会做一组对比实验先固定同一份多轮历史打100个请求再随机打100个请求对比命中前后的TTFT和吞吐。这组数据比任何参数表都更能说明你当前部署的瓶颈在哪。另外建议建一个prompt模板仓库。把系统提示词、few-shot示例、工具定义都做成固定前缀模块业务方共用这套模板而不是每个人自己拼一套。这不仅是给模型看的内容规范更是让RadixCache发挥价值的工程手段。前缀越稳定缓存命中越高服务性能越稳。最后分享一个我这段时间最强烈的体会先稳定prompt前缀再谈调参数。我在小智这个项目上第一次压测结果很差一度以为是sglang参数没配好后来发现是业务侧每个人拼的prompt前缀都不一样RadixCache根本来不及复用。把模板仓库建起来之后同样的并发下吞吐直接上了一个台阶。你先别急着把mem-fraction-static调到0.95先把system prompt固定下来再回头看监控数据。这套思路用在sglang上比什么参数玄学都管用。
返回列表