SGLang 一加并发就出问题,先查显存还是队列
场景:SGLang 接进应用后多人同时提问,有人等很久、有人直接失败,团队准备先降并发。结论:92% 显存占用不等于 OOM;要先按"是否在排队/哪个阶段失败"分诊,再考虑
--chunked-prefill-size/--max-running-requests/--mem-fraction-static等单项参数。产出:5 步分诊法 + 教学 20 请求对照表 + 16 行错误速查卡。 适用版本:SGLang v0.5.19(2026-09-05),资料复核于 2026-09-12;教学数据非 GPU 实测。
TL;DR
- 场景:本地正常、接入应用后多人并发出现长时间等待和请求失败,团队首选"先降并发"。
- 结论:显存 92% 不能直接判 OOM;应先按"是否在排队 / 哪个阶段失败 / 哪类资源耗尽"分诊,再在 FAQ 给出的三个方向(prefill 改
--chunked-prefill-size、decode 改--max-running-requests、改--mem-fraction-static)中按阶段单选一项。 - 产出:5 步分诊法(接前/排队/prefill/decode/取消)+ 教学 20 请求对照表(8192→4096 单变量对照,60%→80%,仍未达 95%)+ 16 行错误速查卡。
版本矩阵
| 项目 | 状态 | 说明 |
|---|---|---|
| SGLang Production Metrics 文档可访问 | ✅ 已验证 | https://docs.sglang.io/docs/references/production_metrics 返回 HTTP 200 |
--enable-metrics 开启 Prometheus 指标 | ✅ 已验证 | 沿用前几轮核查 |
指标 sglang:num_running_reqs / num_queue_reqs 含义 | ✅ 已验证 | 官方页明确:分别是"the number of running requests"与"the number of requests in the waiting queue" |
指标 sglang:time_to_first_token_seconds | ✅ 已验证 | 官方页含 generate_request 标签的 latency histogram |
| SGLang FAQ 可访问 | ✅ 已验证 | https://docs.sglang.io/docs/references/faq 返回 HTTP 200 |
| FAQ OOM 条目给出三个参数方向 | ✅ 已验证 | 贴图忠实摘录:prefill 改 --chunked-prefill-size、decode 改 --max-running-requests、通用改 --mem-fraction-static |
| SGLang Server Arguments 文档可访问 | ✅ 已验证 | https://docs.sglang.io/docs/advanced_features/server_arguments 返回 HTTP 200 |
| SGLang v0.5.19(2026-09-05) | ✅ 已验证 | 沿用前几轮核查 |
| 教学 20 请求对照表(8192 vs 4096) | ✅ 已设计 | 由作者构造的对照数据,非 GPU 实测 |
| 资料复核日期 2026-09-12 | ✅ 已标注 | 文中明确 |
| 5 张配图 URL 全部可访问 | ✅ 已验证 | i-blog.csdnimg.cn/img_convert 5 个 URL 全部 200 |
nvidia-smi 92% 占用直接判定 OOM | ❌ 已驳斥 | 显存可预先划给缓存;不替代内存分配失败证据 |
| 看到排队就调小并发 | ❌ 已驳斥 | 调小可能只是把工作推到更长队列 |
| 显存 92% + 请求失败 = 一定是显存问题 | ❌ 已驳斥 | 错误可能发生在 prefill、decode、其他阶段 |
| 启动失败用"降低运行请求数"通用答案 | ❌ 已驳斥 | 启动失败要先查初始化日志、模型版本、硬件 |
| 三个 FAQ 参数一起调 | ❌ 已驳斥 | 一次只改一项;可能多变量折叠 |
| 用 19/20 一次结果就声称稳定 95% | ❌ 已驳斥 | 20 样本不足,需更多样本和重复验证 |
| 浏览器总耗时减服务端平均当网络等待 | ❌ 已驳斥 | 跨机未校准;先同进程内时间间隔 |
| 用户点停止即服务端旧计算结束 | ❌ 已驳斥 | 沿请求 ID 看取消是否被接收、任务是否终结 |
| 客户端立即重试 + 旧工作仍在 = "并发忽然变大" | ❌ 已驳斥 | 重试放大了负载,不是真实并发变化 |
| 同一长输入 OOM 后原样重发 | ❌ 已驳斥 | 原样重发通常不改变原因 |
| 取消后沿用允许迟到完成的统计 | ❌ 已驳斥 | 取消策略下工作量和终止方式都变了 |
| 写业务接口的 Agent 因生成超时直接重跑 | ❌ 已驳斥 | 外部动作可能已发生,需要先确认 |
文章正文
本地问一句、答一句,服务一直正常。接进应用以后,几个人同时提问,有人等很久,有人的请求直接失败。GPU 显存显示已经用了 92%,于是团队准备先把并发调小。
这可能减少失败,也可能把同一批工作推到更长的队列里。为了看清区别,下面跟随一次教学排障:先辨认"显存高但仍在正常处理"的现场,再找到长请求的输入阶段 OOM,只改一个参数,最后算出为什么错误消失了,服务仍未达标。
全文数值、请求和结果均为作者设计的教学数据,不是 SGLang GPU 实测,也不是通用参数推荐。 我们假设只有一个推理副本,采用同一批 20 个请求复测;业务要求从客户端实际提交开始,8 秒内得到内容完整、质量合格的回答,并希望至少 95% 请求达标。先定这个标准,才能知道改动究竟改善了什么。
显存接近满,为什么还不能直接判定故障
推理服务会把显存分给权重、KV Cache 等用途,并为计算留空间。预先划给缓存的空间也会体现在占用里。因此,nvidia-smi 的 92% 只能说明看到了较高占用,不能替代内存分配失败的证据。
先把同一副本、同一时间段的记录放在一起。假设第一次观察时,运行请求数为 8,排队数为 12;随后运行数维持在 8 左右,队列逐渐回落。对应请求最终结束,进程没有重启,保存的该时间窗日志中也没有匹配的 OOM。此时更合理的描述是"存在等待,尚无内存分配失败证据",而不是"显存已经爆了"。
SGLang 生产指标文档说明,可以用 --enable-metrics 开启指标,其中包含 sglang:num_running_reqs、sglang:num_queue_reqs 与 sglang:time_to_first_token_seconds。前两项帮助看运行和排队,后一项观察首 token 等待分布。它们是定位入口,不是完整逐请求诊断。
这一步有几个很容易把结论弄错的细节。运行数为 8,不单独证明运行上限配置就是 8;需要核对实际启动参数和调度约束。队列必须来自这个模型副本,不能拿网关总队列或多个副本的合计去解释单卡现象。服务端统计的 TTFT 也不自动等于浏览器显示第一段字的等待,前面还可能有网关和客户端开销。
如果你只有一张显存截图,缺少请求结果和完整日志,那么正确状态是证据还不够。没有收集到错误不等于没有错误。先补观测,比根据 92% 连降三个参数更有用。
服务还没起来,先别把它当成并发问题
还有一种现场需要先从这条排障路径分出去:业务请求尚未发出,服务就在模型加载或初始化时失败。此时不存在可供解释这次失败的业务请求队列。保存启动日志、模型版本、硬件与完整启动参数,先区分明确的 OOM、通信错误和其他初始化异常。
官方 FAQ把初始化或运行中卡住单独讨论,可能涉及内存、网络通信或其他问题。不能用"降低运行请求数"作为启动失败的通用答案。我们下面的教学记录,前提都是服务已完成启动,并能接收请求;启动耗时与重启损失单独保留。
长文一进来就失败,先看 prefill
继续收集证据后,假设我们获得了另一轮可复现记录:20 条请求里有 16 个短输入、4 个长输入,长请求都没有产生首 token,错误栈和阶段记录共同指向 prefill 内存分配失败。Prefill 是处理输入 token 的阶段;这次真正支持调参方向的是请求关联和错误位置,不是"用户没等到第一句话"。没有首 token 也可能来自路由、网络或初始化问题,不能单独定位 OOM。
先打开这四条请求的实际输入。如果应用把同一检索段落拼了三遍,优先修正拼接。修复输入会改变工作负载,要保存新样本并重新建立基线,不能把收益冒充为引擎参数调优。若内容确实需要那么长,再考虑输入处理的分块大小。
SGLang FAQ 的 OOM 条目给出的方向是:prefill OOM 可尝试减小 --chunked-prefill-size,代价可能是长输入处理更慢。本例假设当前安装版本接受原配置 8192,本轮只把它改成 4096。两个数只是说明如何设计单变量对照,不表示任何模型都应该采用 4096。
在改动之前,把待复放的工作保存完整。每条请求保留应用 ID、模板后的输入 token 数、脱敏请求体摘要、计划到达偏移、输出上限和答案合格条件。本轮仍然采用 16 短、4 长的相同顺序,不等前一条完成才发下一条,也不自动重试。客户端另记实际发送时刻;若连接池使发送比计划晚,不能把客户端等待混算为服务端排队。
同时保存模型与分词器 revision、精度、硬件、引擎版本、完整启动参数和配置摘要。停止旧进程后,确认新进程实际加载了 4096,再开始复放;配置文件已经改了而接流量的进程没变,后面的比较就失去意义。实验在隔离环境完成,不把重启损失混进稳定运行期的耗时。
缓存状态也要控制。两轮都从相同的业务缓存起点进入,预热请求单独标记并等它结束;冷起点与预热后的结果分别保存。不能先用原配置跑一遍长文,再让新配置直接利用上一轮留下的缓存,并把变快全部归功于减小分块。
完成这些准备后,得到以下教学结果。这里不是 SGLang 输出的原始日志格式,而是从逐请求记录归类后用于手算的表。
| 同一批 20 个请求,逐条跟踪到终结 | 原配置 8192 | 只改分块为 4096 |
|---|---|---|
| 正常结束且内容、质量合格 | 16 | 20 |
| 明确的 prefill OOM 失败 | 4 | 0 |
| 8 秒内完整合格结束 | 12 | 16 |
| 正常合格结束但超过 8 秒 | 4 | 4 |
| 失败或未达标合计 | 8 | 4 |
先读清楚各行关系:原配置的 16 条正常结果由 12 条及时结束、4 条迟到组成,另有 4 条 OOM;修改后的 20 条都正常,但仍有 4 条迟到。不能把"正常结束"与"8 秒内结束"相加,它们是包含关系。
这轮观察器允许超过 8 秒的请求继续运行到结束,用于区分迟到与失败。8 秒是验收截止线,本轮不是在第 8 秒强制取消。如果实际应用会取消,就另跑一轮启用相同取消策略的对照,不能借上表宣称真实超时用户后来都获得了答案。若真实 OOM 导致进程退出,还必须计入连带失败、未决请求和重启损失;本表不保证引擎遇到 OOM 后只损失那四条。
按全部 20 次提交作分母,达标率从 12/20=60% 提高到 16/20=80%;OOM 从四次变成零次。但 80% 仍低于事先定的 95% 目标。这里消除的是一种错误现象,没有完成容量与等待验收。对 20 个样本而言,95% 至少意味着 19 条达标;即使下一轮碰巧达到 19 条,也需要更多样本和重复验证才能主张稳定服务水平。
多个人回答到一半失败,再看 decode 压力
不要因为本例剩下四条迟到,就顺手再降低运行请求上限。先看每条迟到在哪一段:客户端提交到得到结果的总时长是一层,服务端接纳、开始计算、首 token、生成结束又是另一层。通过请求 ID 关联这些记录,才能判断是在等资源、处理长输入,还是生成太久。
如果另一个现场的错误发生在多个请求已经开始生成之后,而且分配失败位置明确指向 decode,才转入解码阶段的排查。生成阶段会延长序列,多条长回复一起运行可能形成不同于 prefill 的压力。官方 FAQ 对这种 OOM 的建议之一是降低 --max-running-requests,限制同时运行的请求数。
它不会消除原来的工作量。假设降低后中断减少,队列却更长,就需要把完整回答数、等待和超时一起放回同一批请求;不能只截取"错误为零"那一列。对于刚才的 4096 实验,若要试这个参数,应当另建一轮,明确保留还是回退分块改动,不把多个变化折叠成一次"调低并发"。
输出长度同样需要保留证据。查询订单状态只需要一段短回答,却允许无限展开,会让个别请求占用很久;但把所有输出强行截短也可能丢掉任务结果。限制输出前先写清所需内容,重测后同时记录实际 token 数、结束原因和答案完整性。更短的回答如果缺了用户要的信息,不能算作本例的合格结束。
降低静态显存比例,有什么代价
官方 FAQ 还给出减小 --mem-fraction-static 的方向,让运行时计算获得更多空间;相应地,KV Cache 内存池预算会缩小,最大并发与峰值吞吐也可能受影响。因此"显存曲线变低"不是独立的成功标准,仍要回到同一批请求的结果。
在我们的 prefill 案例里,已有明确的阶段证据,所以第一轮选择分块参数。若再考虑静态比例,需要说明新证据:是否仍发生内存分配失败,缓存预算是否已经成为容量约束,哪些请求因此等待。不要因为三个参数都出现在 FAQ 的 OOM 条目里,就认为应该一起调低。
错误类型也不能省略。非法内存访问或进程异常退出可能涉及内核、版本或内存等原因,先保留完整错误与复现条件。把它们全部归为"参数太激进",会让后面的每一轮实验都围绕未经确认的根因旋转。
请求只是排队时,应用也要做一点事
回到表中修改后的四条迟到请求。继续观察到终结的方式帮助我们定位,但实际客服不一定愿意一直等。应用需要明确等待多久后停止、如何告知用户,以及是否取消后端任务。这是另一组需要验证的行为,不能用一次 GPU 参数实验顺便宣布已经完成。
例如,用户在第 8 秒点击停止,前端不再显示,连接也关闭了,但这些事件本身不证明服务端的旧计算已经结束。沿同一请求 ID 查看取消是否被接收、任务是否终结,以及资源和请求计数是否回落。若客户端立即重试,而旧工作仍在执行,两次计算可能重叠;看起来是"并发忽然变大",实际却是应用重试放大了负载。
重试策略要按错误分类。瞬时连接故障可以有受限重试;同一个超长输入反复触发同一 OOM,原样重发通常不会改变原因。对已经调用写业务接口的 Agent,还要先确认外部动作是否发生,不能因为生成答案超时就重跑整条业务操作。
验证取消时,仍保存原来的 20 个请求,明确哪些会到达截止线、哪些会被取消、有没有重试,并单列该策略下的有效完成数和资源终结证据。取消后的计数不能直接套用允许迟到完成的那张表,因为工作量与终止方式已经变了。
下一次改配置前,留下能复现这次问题的材料
补齐四条迟到请求的时间线时,要小心时钟。客户端和服务端没有校准,不能拿客户端提交时间直接减服务端接纳时间来算网络等待。分别计算各自时钟域内的时长,再按请求 ID 关联;需要跨机器分段时,先建立可用的时钟或 tracing 条件,并保留误差说明。
服务端 TTFT histogram 可以展示同窗口的分布变化,却不能替代 R17 这一条请求的完整经历。应用 ID、输入长度和错误阶段需要日志或 tracing 提供,也不能宣称默认 /metrics 已经包含逐请求记录。只有 20 个样本时,先看逐条明细和计数,不用一个看似精确的 p99 掩盖样本不足。
如果迟到主要发生在接纳前或排队中,下一轮才有理由验证接纳、分流或容量方案;若接纳很快、输入处理段长,继续检验长短输入的干扰;若首 token 很快但结束迟,回到输出长度与解码段。这些方向都需要阶段记录支持,不能凭用户说"卡"替他选好参数。
本轮最后应当留下的结论是:"相同 20 请求、相同缓存起点下,只把分块从 8192 改为 4096,教学记录中的 prefill OOM 从 4 次降到 0,8 秒达标率从 60% 升到 80%,仍未达到 95%。保留原配置与新配置、请求明细和错误证据,接着查四条迟到;取消策略尚未在本轮验证。"
同时写清回退条件:目标错误未改善、同批达标完成数减少,或者短请求出现不可接受的退化,就恢复保存的原配置。更新模型或运行时后重新复放。一次调整的价值在于让问题范围缩小,并留下下一步能继续核查的证据;"进程不再报错"还不是多人服务已经可用的证明。
参考资料
资料复核于 2026-09-12。官方链接支撑按阶段排查的方向与指标定义;20 请求、92% 显存、8192→4096 对照及所有结果属于教学构造,未执行 GPU 压测。具体选项与可用值仍需核对安装版本。
配图保持方法示意与官方原文的区别;图上 2026-09-06 来源检查日期保留,相关说明于 2026-09-12 复核。20 请求对照与验收计算由正文教学表承担,不是图中展示的实测结果。
错误速查卡
| 症状 | 根因 | 定位 | 修复 |
|---|---|---|---|
nvidia-smi 92% 直接判定 OOM | 显存可预先划给缓存;不等于内存分配失败 | 同时看请求结果、错误栈、运行/排队指标 | 补观测后再决定改参数;不要凭显存截图连降三个参数 |
| 看到排队就调小并发 | 调小只是把工作推到更长队列 | 区分"在排队"与"哪个阶段失败" | 先分诊再决定调哪一项 |
| 启动失败用"降低运行请求数"通用答案 | 启动失败不存在业务请求队列 | 看启动日志、模型版本、硬件、完整启动参数 | 先区分 OOM / 通信错误 / 其他初始化异常 |
prefill OOM 后顺手调 --max-running-requests | 错误阶段不在 decode | 看错误栈 + 阶段记录 | prefill 改 --chunked-prefill-size;decode 才改 --max-running-requests |
| 三个 FAQ 参数一起调 | 多变量折叠,无法定位收益 | 一次只改一项 + 保留原配置 | 拆轮次:每个改动独立对照 |
| 19/20 一次结果就声称稳定 95% | 样本不足 | 20 样本规模 + 单次 | 扩样本 + 重复验证 |
| 浏览器总耗时减服务端平均当网络等待 | 跨机未校准 | 同进程内时间间隔 | 用同进程时钟 + request_id 串联 |
| 用户点停止即服务端旧计算结束 | 停止显示/连接关闭不等于服务端终结 | 沿请求 ID 查取消是否被接收、任务状态、资源计数 | 单独验证取消策略 |
| 客户端立即重试 + 旧工作仍在 = "并发忽然变大" | 重试放大负载 | 看请求 ID 重复、重叠执行 | 按错误分类重试;先确认旧工作终结 |
| 同一长输入 OOM 后原样重发 | OOM 原因未变 | 看错误阶段与输入长度 | 先处理输入或资源根因,不原样重发 |
| 取消后沿用允许迟到完成的统计 | 工作量与终止方式已变 | 取消策略下计数条件不同 | 取消策略单独跑一轮对照 |
| 写业务接口的 Agent 因生成超时直接重跑 | 外部动作可能已发生 | 沿请求 ID 查业务接口是否已执行 | 重跑前先确认业务动作是否完成 |
| 应用层把"队列增长"全归因于服务端并发 | 网关/客户端/服务端混算 | 各自时钟域内时间间隔 | 用 request_id 关联各观察点 |
| 觉得 FAQ 都提到就该一起调 | 多变量 | 一次只改一项 | 拆轮次,保留可恢复的原配置 |
| 凭"显存曲线变低"宣布成功 | 曲线不替代容量与等待 | 同一批请求的达标率 | 仍要回到业务验收 |
| 凭"用户说卡"替他选参数 | 缺乏阶段证据 | 阶段记录 + 错误日志 | 先分诊,按阶段选参数方向 |
作者:武子康的个人博客
发布日期:2026-09-14(资料复核 2026-09-12)
核查依据:SGLang Production Metrics 页(HTTP 200,含 sglang:num_running_reqs / num_queue_reqs 指标定义)、FAQ 页(HTTP 200,含 CUDA OOM 三个参数方向)、Server Arguments 页(HTTP 200);SGLang v0.5.19(2026-09-05)。本文 5 张配图 URL 全部 HTTP 200;20 请求对照与所有数值为作者教学构造,未执行 GPU 压测;具体选项与可用值仍需核对安装版本。