大模型推理流式输出与背压控制落地

日期:2026-07-25

一、项目背景

我们把自托管的大模型推理服务接到业务系统后,第一个被投诉的就是"慢"。不是真的慢,而是首字要等很久——模型在内部把整段答案生成完才一次性返回,前端卡在加载圈,用户以为断了。更严重的在并发侧:某次大批量作业同时发起上千个长文本生成请求,GPU 显存被瞬间打满,队列里堆了几千个请求,后到的正常业务请求排了十几分钟才响应,长连接一个个超时断开,触发客户端重试,又把压力叠上去,形成雪崩。

我们当时判断,核心矛盾是"推理服务把'生成'和'返回'绑死了,且对并发毫无缓冲"。这个项目里,我们采用私有化推理底座(vLLM 类服务化框架,连续批处理 + KV 缓存)承载模型,并在服务外围做流式与背压治理。底座确定后,真正的难点是怎么在不改模型本身的前提下,把"首字时延、流式体验、并发上限、背压"四件事一起管好。

二、落地场景

第一类场景是流式输出与首字优化。业务侧不再等整段答案,而是边生成边接收。我们把推理服务的 streaming 接口透传到前端,用户看到的是逐字出现的回答,感知时延从"整段生成完"降到"第一个 token 出来"。

第二类场景是客户端背压控制。前端不是无脑接收,而是按自身渲染能力做消费限速——后端推太快、前端来不及渲染时,通过流控信号让后端暂缓推送,避免浏览器内存暴涨或卡死。

第三类场景是并发上限与队列。推理服务前置一层网关,对并发做硬上限(如单卡并发不超过 N),超出部分进入有界队列而非无限制堆积,队列满则快速失败而非无限等待。

第四类场景是超时与降级。长请求超过阈值时,网关主动中断并降级返回"已生成部分 + 提示",而不是让连接挂死占用 GPU。

落地过程中我们还补了一块常被忽略的能力:KV 缓存占用看板。连续批处理依赖 KV 缓存,长请求会长期占用缓存块挤占他人,我们把单请求的缓存占用纳入调度权重,长请求自动降优先级。

三、关键技术挑战与解决思路

第一个挑战是流式输出与首字时延。关键在于让推理服务吐出第一个 token 就立刻推给前端,而不是攒完再发。我们在网关层透传 SSE 流,仅做最小化封装。下面是流式与背压的脱敏配置片段。

# 推理流式与背压配置(已脱敏)
streaming:
  enabled: true
  first_token_timeout_ms: 3000   # 首字超时则降级
  sse_flush_interval: 20ms       # 推送粒度
backpressure:
  client_window: 8               # 客户端未确认窗口上限(token 块)
  pause_when_full: true          # 窗口满则服务端暂停推送

第二个挑战是并发上限与队列。我们用有界队列替代无限堆积,配置单卡并发上限与全局排队上限,超出即快速失败,把"慢死"变成"早失败"。下面是限流与队列的脱敏片段。

# 并发与队列治理(已脱敏)
gate:
  max_concurrency_per_gpu: 16
  queue:
    max_waiting: 200             # 等待队列上限
    strategy: "fair"             # 公平调度,避免长请求饿死短请求
  on_queue_full: "reject_429"    # 队列满快速失败

第三个挑战是超时与降级兜底。长请求超过首字或整体阈值,网关中断并返回已生成内容,释放 GPU 给后续请求,避免单请求长期霸占 KV 缓存。

四、效果数据

改造前后,我们拉通了四组口径一致的脱敏示意指标:

指标改造前改造后说明
首字时延约 4.5s降至约 0.8s首个 token 返回时延
流式中断率约 7%降至约 0.5%连接中断占比
队列积压峰值约 3200降至约 180并发洪峰时排队数
P99 时延约 48s降至约 9s长尾请求时延

上表为脱敏示意值,用于说明趋势而非审计口径;不同输入长度差异大,此处取中等长度请求均值。

五、可复用经验总结

第一,流式先降首字再谈体验。用户感知的"快"来自第一个字出来的时间,而非整段完成时间。把首字时延压下来,体验立竿见影,远比优化后端总耗时容易见效。

第二,背压优于硬扛。客户端不是无限接收,服务端也不是无限推送。用有界窗口做背压,浏览器和后端都不会被突发流冲垮,稳定性远高于"能推多快推多快"。

第三,队列要有界,失败要早。无限堆积只会把"慢"升级成"雪崩"。给队列设上限、满则快速失败,调用方能立刻感知并退避,比挂死后再超时友好得多。

第四,长请求要主动降级。长文本生成长期占用 KV 缓存挤占他人,网关按阈值中断并返回已生成部分,既释放算力又保住大部分产出,比硬等更划算。

案例片段(已脱敏):一次营销文案批量作业误发起约 1500 个长文本请求,旧架构下 GPU 显存打满、队列堆到 3200、后到业务请求平均等 12 分钟。新治理上线后同样洪峰被有界队列吸收(满 200 即 429 快速失败),前端作业端收到 429 后按指数退避重投,峰值并发稳定在单卡 16、全局排队不超过 180,业务侧正常请求 P99 控制在 9 秒内,无雪崩。

案例片段(已脱敏):一个超长报告生成请求因输入过长,首字后持续占用 KV 缓存约 40 秒。网关在整体超时阈值触发后主动中断,返回已生成的约 1200 字摘要并附"内容截断提示",前端正常展示,该请求释放的缓存块立即被后续 3 个短请求复用,未造成连锁阻塞。