利用Burstiness优化LLM Serving调度:从概念到验证方法 这次我们来看一个 LLM serving 方向的新思路Burstiness is all you need for LLM serving。这个标题的提法很直接——它想说的是LLM 服务流量天然就不是均匀的而是突发性的与其试图把流量抹平不如让调度策略直接感知并利用这种突发性把它变成提升吞吐和 SLO 达标率的抓手。如果你负责过在线推理服务、做过大模型 API 网关或者正在用 vLLM、TGI 这类框架部署开源模型一定遇到过这样的场景平时请求量很低某一瞬间用户全部涌进来队列瞬间拉长超时率上升GPU 显存利用率却不升反降。这就是 burstiness——请求到达率在短时间尺度内剧烈波动。本文会从概念、调度思路、部署验证、接口测试、性能观测、常见坑位这六个层面把这个主题拆开讲清楚。文章适合三类读者正在做生产级 LLM 服务压测和调优的后端工程师在做推理平台、推理网关的架构师以及准备在业务里接入自建大模型 API、想搞清楚服务瓶颈在哪里的开发同学。下面先给一个整体速览再逐步展开验证流程。1. 核心能力速览先把这篇内容的关键点列出来方便快速判断是否和你的场景相关。能力项说明项目/方法性质关于 LLM serving 调度优化的研究思路与工程方法论不是开箱即用的一键软件包核心概念Burstiness即请求到达时间分布中的突发性特征关注指标TTFT首 token 延迟、TPOT单个输出 token 间隔、端到端延迟、P95/P99 SLO 达标率、吞吐量主要受益场景高并发在线推理、多租户服务、Agent 高频调用、可变负载 API 网关典型依赖框架vLLM、TGI、SGLang、Ray Serve 等支持连续批处理的 serving 框架建议硬件NVIDIA GPU 服务器显存大小按模型规模而定推理阶段常见 16G/24G/48G/80G 不等是否支持 API是通常暴露 OpenAI 兼容接口是否支持批量任务是压测和业务侧都可以用异步批量方式提交是否需要修改模型不需要调度优化发生在 serving 层模型权重不变适用读者后端研发、推理平台负责人、SRE、对 LLM 服务性能优化感兴趣的技术人员这里要强调一点本文在讲「思路」和「验证方法」时不会虚构一份已经发布的开源工具。如果标题所指的研究工作还没有对应可跑代码那就按方法论去理解并在自己的 serving 环境里完成验证。这样更稳妥也不会被无效承诺误导。2. 什么是 Burstiness为什么它对 LLM serving 这么关键Burstiness 描述的是请求到达过程在时间上不均匀的程度。数学上经常用到达间隔的变异系数、自相关函数或者多重分形参数来衡量工程上更直观的观察方式是把一分钟内的请求按秒切分你会发现有些秒有几十个请求有些秒几乎是零。传统 Web 服务也讲突发流量但 LLM serving 的 burstiness 影响更大原因在于请求成本和资源占用高度不均匀。一个普通 HTTP 请求通常毫秒级返回而一个 LLM 推理请求可能持续几秒到几分钟期间需要持续占用 GPU 显存和算力。如果同一秒内有大量请求同时到达服务端不仅要处理排队还要考虑每请求的 prefill预填充阶段和 decode解码阶段如何交错。prefill 阶段需要大量并行计算模型要一次性处理输入 prompt 的所有 tokendecode 阶段则是一次生成一个 token计算密度明显偏低。突发流量一来prefill 任务集中堆积GPU 的算力可能在极短时间内被吃满随后又因为 decode 阶段占满显存而进入低利用率状态。传统调度器面对这种情况往往表现不好。FCFS先到先服务在突发流量下会让短请求排在长请求后面导致尾延迟失控基于固定并发数的信号量限流虽然保护了系统但会直接丢弃流量用户体验很差。而连续批处理continuous batching虽然可以在同一个 batch 里动态增减请求但它的效率上限取决于调度器能否在正确的时间把正确的请求放进 prefill 和解码槽位。如果没有感知 burstiness批大小会剧烈波动GPU 利用率在过饱和和空转之间来回跳跃。Burstiness is all you need 这类思路的出发点就是不要假装流量是均匀的而是把突发性直接建模进调度策略。比如基于到达模式的预测来预先保留 batch 槽位或者在突发窗口内调整 prefill 和 decode 的比例把请求按到达间隔分组让计算密集的 prefill 阶段在流量峰值处合并执行从而提升 GPU 整体的吞吐同时控制 SLO 超标比例。从工程实践看理解 burstiness 至少有四个直接收益排障更快当 P95 延迟突然升高时不再盲目加机器而是先看到达率曲线和队列深度是否同步上涨。压测更真实压测脚本不再使用匀速发请求而是根据业务特征模拟突发到达才能暴露真实环境中的问题。容量规划更准给系统留多少冗余应该取决于突发峰值的规模和持续时间而不是平均 QPS。调度策略可验证在加载不同调度策略时可以量化对比 SLO 达标率、尾部延迟和吞吐量而不是靠感觉判断。3. 适用场景与使用边界先回答「这个东西适合谁、能解决什么问题」。最典型的是四类场景企业级 LLM API 网关多个业务方共用同一个推理集群每个业务方的用户行为不同请求到达叠加后会出现明显的高峰期和低谷期需要感知突发性来做配额和调度。Agent 应用后端Agent 在一次任务里会连续调用模型多次且调用之间有时间间隔这会让单个用户维度出现天然的脉冲式请求流。高并发在线服务比如智能客服、代码补全、实时翻译用户输入行为受工作时间、热点事件影响突发性非常显著。多租户共享推理集群不同租户的负载会互相干扰如果把 burstiness 纳入调度可以避免某个租户的高峰把整个集群的延迟拖垮。同时也要说清楚不适合什么场景离线批量推理离线任务通常预先排队、匀速运行目标是最大化整体吞吐burstiness 的影响很小。单用户低并发场景本地调试、个人开发环境里请求量太小突发性特征不明显不需要专门优化。对延迟不敏感的异步处理链路比如日志分析、内容审核的异步队列只要能扛住增量突发性不会直接伤害业务。使用边界和安全提醒LLM serving 的调度优化不能用来绕过系统的并发限制或资源配额。合理使用方式是在配额范围内让调度更高效而不是突破安全边界。生产环境里的突发流量可能来自爬虫、刷接口或其他异常来源必须和业务限流、认证鉴权配合使用。部署和压测时也只在自有测试环境里对授权模型和自有数据进行验证避免把未授权数据灌入公共模型服务。4. 环境准备与前置条件在验证 burstiness 调度思路之前先搭一套可重复运行的 LLM serving 环境。下面是一个通用检查清单具体版本以实际使用的框架和模型为准。4.1 硬件要求GPUNVIDIA GPU建议至少拥有 16G 显存测试 7B 级别的开源模型比较方便如果测试更大的模型显存需求按模型量化精度和上下文长度相应提升。内存建议 32G 以上。模型权重加载、tokenizer、batch 数据都会占用内存。磁盘模型仓库加依赖环境预留 50G 以上比较稳妥。网络如果使用 Docker 拉取镜像和模型需要稳定网络。4.2 软件环境操作系统Linux 发行版例如 Ubuntu 22.04。NVIDIA 驱动与 CUDA驱动版本要适配本机 GPUPyTorch 和 vLLM 对 CUDA 版本都有要求建议先用nvidia-smi确认驱动支持的 CUDA 版本。Python3.10 或更高版本。Python 包管理建议使用独立的虚拟环境避免污染系统 Python。推理框架以 vLLM 为例它是目前验证连续批处理和调度策略最方便的框架之一。4.3 端口和防火墙Serving 服务默认会监听一个端口。启动前先检查端口是否被占用ss -lntp | grep 8000如果端口已被占用启动参数里显式指定新端口或者关闭占用进程。生产环境还要在防火墙里放行对应端口并限制来源 IP。5. 安装部署与启动方式这里以 vLLM 为例演示从零启动一个 LLM 服务。注意如果是其他框架命令和参数会有所不同但验证思路一致。5.1 创建虚拟环境并安装依赖python3 -m venv llm-serve-env source llm-serve-env/bin/activate pip install --upgrade pip pip install vllm如果你的网络环境里 pip 下载较慢可以换国内镜像源pip install vllm -i https://pypi.tuna.tsinghua.edu.cn/simple5.2 启动 OpenAI 兼容服务vLLM 支持一条命令启动一个兼容 OpenAI 接口的服务。下面是常见用法模型路径和参数需要按实际环境替换python -m vllm.entrypoints.openai.api_server \ --model /path/to/your/model \ --served-model-name my-llm \ --host 0.0.0.0 \ --port 8000 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9参数含义--model模型权重的本地路径或者 Hugging Face 上的模型 ID。--served-model-name对外暴露的模型名称调用 API 时需要保持一致。--host监听地址。本地调试用127.0.0.1容器或局域网访问用0.0.0.0。--port服务端口。--max-model-len最大上下文长度。--gpu-memory-utilization允许框架使用的显存比例。启动后如果看到日志输出类似Uvicorn running on http://0.0.0.0:8000说明服务已经可访问。这时可以用一个简单的请求验证curl http://127.0.0.1:8000/v1/models正常会返回模型列表说明接口已经通了。5.3 Docker 方式启动可选如果你希望减少环境依赖也可以用 Dockerdocker run --gpus all \ -v /path/to/models:/models \ -p 8000:8000 \ vllm/vllm-openai \ --model /models/your-model \ --served-model-name my-llmDocker 方式的好处是环境隔离更干净但需要提前拉取镜像和模型文件。具体镜像 tag 请参考 vLLM 官方文档这里不写死版本号以免误导。6. 突发流量模拟与功能验证服务启动后下一步是验证它在不同到达模式下的表现。这是整个主题的关键只有用突发流量压测你才能看到 burstiness 的影响。6.1 先做功能连通性验证在压测之前先用单请求确认模型可以正常推理from openai import OpenAI client OpenAI( base_urlhttp://127.0.0.1:8000/v1, api_keyEMPTY ) chat_completion client.chat.completions.create( modelmy-llm, messages[ {role: user, content: 用一句话介绍什么是突发流量} ], max_tokens128, temperature0.7 ) print(chat_completion.choices[0].message.content)这段代码能跑通说明 OpenAI 兼容接口正常。接下来再进入压测和调度观察。6.2 构建均匀流量基线均匀流量是基线。使用 Python 脚本每隔固定秒数发送一个请求import time import threading from openai import OpenAI from datetime import datetime client OpenAI( base_urlhttp://127.0.0.1:8000/v1, api_keyEMPTY ) def send_request(idx: int): start time.time() try: resp client.chat.completions.create( modelmy-llm, messages[{role: user, content: f这是第 {idx} 个请求请写一段关于 LLM serving 的简单说明。}], max_tokens200, temperature0.7 ) end time.time() print(f{datetime.now().isoformat()} request{idx} latency{end - start:.2f}s) except Exception as e: print(f{datetime.now().isoformat()} request{idx} error{e}) for i in range(100): threading.Thread(targetsend_request, args(i,)).start() time.sleep(0.5)这个脚本模拟每秒 2 个请求的平稳负载。记录下每个请求的响应时间尤其是 P95 和 P99。这是后续对比的基准。6.3 构建突发流量场景突发流量的特征是在短时间内并发大量请求。模拟方式有很多常见的是脉冲式到达先静默 5 秒然后瞬间抛出 50 个请求再静默 10 秒再抛出 80 个请求。import time import threading from openai import OpenAI client OpenAI( base_urlhttp://127.0.0.1:8000/v1, api_keyEMPTY ) def burst_send(start_idx: int, count: int): threads [] for i in range(start_idx, start_idx count): t threading.Thread( targetcall_once, args(i,) ) threads.append(t) for t in threads: t.start() for t in threads: t.join() def call_once(idx: int): start time.time() try: resp client.chat.completions.create( modelmy-llm, messages[{role: user, content: f突发请求 {idx}帮我写一段技术总结。}], max_tokens150, temperature0.7 ) end time.time() print(frequest{idx} ok latency{end - start:.2f}s) except Exception as e: print(frequest{idx} error{e}) # 静默 3 秒后突发 30 个请求 time.sleep(3) burst_send(0, 30) # 静默 5 秒后突发 60 个请求 time.sleep(5) burst_send(30, 60)你可以观察几个现象突发请求的响应时间是否明显高于均匀流量。突发请求是否出现连接超时或 429 限流错误。在突发请求结束后普通请求的响应时间是否还在持续受到影响也就是恢复时间有多长。6.4 判断验证是否成功判断一套 serving 系统在突发流量下表现是否合格不能只看平均延迟要看三件事SLO 达标率比如定义「200 token 以下请求 P95 延迟低于 3 秒」作为 SLO统计突发压测中的达标比例。队列阻塞恢复时间突发请求结束后服务是否能在几秒内恢复正常延迟。失败率是否存在因为排队时间过长导致的超时错误。如果在突发流量下失败率明显上升且恢复时间过长就说明当前的调度策略没有很好的 burstiness 应对能力需要调整 batch 策略、并发上限或调度策略。6.5 对比实验设计为了验证「burstiness-aware 调度优于普通调度」建议做一组对比实验场景 A均匀流量压测 5 分钟。场景 B突发流量压测 5 分钟其中每 20 秒产生一次脉冲峰值并发是均值的 5 倍。场景 C混合流量80% 时间低负载20% 时间突发高峰。分别记录每组的吞吐量、P95 延迟、P99 延迟、错误率。如果后续有可对比的调度版本只要切换服务端参数或部署版本重复相同场景即可。这里要注意压力测试必须控制请求总量和模型 token 上限避免把测试环境打成不可恢复的状态。7. 接口 API 与批量任务LLM serving 的调度优化最终要落到接口和业务上。OpenAI 兼容接口是事实标准vLLM、TGI、SGLang 都支持接下来看看 API 能提供哪些信息。7.1 查看模型列表curl http://127.0.0.1:8000/v1/models返回结果中包含模型名称、元数据等信息。确认服务已注册模型后再开始调用。7.2 流式响应与指标观测在线服务推荐使用流式输出这样客户端可以拿到 TTFT首 token 时间。Python 侧的调用方式from openai import OpenAI client OpenAI( base_urlhttp://127.0.0.1:8000/v1, api_keyEMPTY ) response client.chat.completions.create( modelmy-llm, messages[{role: user, content: 介绍一下 LLM serving 中的连续批处理}], max_tokens256, temperature0.7, streamTrue ) first_token_time None for chunk in response: if not chunk.choices: continue delta chunk.choices[0].delta.content if delta and first_token_time is None: first_token_time time.time() print(fTTFT: {first_token_time - start_time:.2f}s) if delta: print(delta, end, flushTrue)在代码里记录请求发出时间、首 token 到达时间、完整响应结束时间就能近似估算 TTFT 和 TPOT。7.3 批量提交任务与队列观察如果你要验证服务在大批量请求下的表现可以用异步客户端批量发送而不是一个个串行等待。比如import asyncio from openai import AsyncOpenAI async def main(): client AsyncOpenAI( base_urlhttp://127.0.0.1:8000/v1, api_keyEMPTY ) tasks [] for i in range(50): tasks.append( client.chat.completions.create( modelmy-llm, messages[{role: user, content: f批量任务 {i} 的内容}], max_tokens100 ) ) results await asyncio.gather(*tasks, return_exceptionsTrue) for idx, r in enumerate(results): if isinstance(r, Exception): print(ftask {idx} failed: {r}) else: print(ftask {idx} ok: {r.choices[0].message.content[:20]}) asyncio.run(main())批量提交时可以观察服务端日志里的排队情况和批大小变化。如果框架暴露了 metrics 接口可以直接拉取指标。vLLM 默认暴露了 Prometheus 指标端点路径一般是/metrics。可以通过 Prometheus 采集器抓取如果没有也可以直接访问这个地址看原始指标。curl http://127.0.0.1:8000/metrics | grep -E vllm:|queue|running不同类型的指标名称以实际框架版本为准。重点观察三个量当前排队的请求数量。当前正在运行的批次数。GPU 显存使用率和算力利用率。7.4 接口压力测试工具选择除了自己写 Python 脚本也可以用现成压测工具简单并发测试hey或ab但不推荐直接用于 LLM因为它们不理解 token 流式特性。更接近真实负载locust或k6可以自定义用户行为模拟思考时间和突发高峰。LLM 专项压测工具部分项目提供 LLM serving 压测功能使用前确认是否和你的服务端框架兼容。压测时要在客户端记录请求时间戳和响应时间方便后续计算分位数指标。最好把压测结果导出为 JSON 或 CSV再做后续分析。8. 资源占用与性能观察突发流量对 GPU 和 CPU 的冲击是动态的观察资源占用需要多个工具配合。8.1 显存占用观察nvidia-smi可以看到即时显存利用率和 GPU 利用率。nvidia-smi dmon可以周期性采样便于记录变化曲线。显存占用不能只看一个静态值。在 prefill 集中发生的时刻显存会明显上涨decode 阶段显存释放较慢可能导致显存碎片化。实际占用多少取决于模型规模、batch 大小、上下文长度和框架的显存管理策略。不要在文章里写死某个数字要以你自己的压测记录为准。8.2 帧率和延迟指标LLM serving 的重要延迟指标TTFT首 token 延迟反映 prefill 和排队情况。TPOT每个输出 token 的时间间隔反映 decode 阶段的稳定程度。端到端延迟完整响应耗时是用户感知最明显的指标。Normalized Latency可以进一步按输入 token 数和输出 token 数归一化用于跨场景对比。排障时可以先看 TTFT 是不是过高。如果 TTFT 高大概率是排队时间长或者 prefill 计算量太大如果 TTFT 正常但 TPOT 高问题多半在 decode 阶段的批处理效率不足。8.3 进程和日志排查服务崩了不一定是显存不足。排查顺序看进程还在不在ps aux | grep vllm。看日志最后几行journalctl -u your-service -n 100或直接看控制台输出。看 GPU 状态nvidia-smi -l 1持续输出观察是否出现 Xid 错误。看系统内存和 swapfree -h。如果是本地用 nohup 启动的日志会写在指定文件里如果是 systemd 管理用 journalctl。9. 常见问题与排查方法这一节把实际部署中容易遇到的问题整理成排查表方便对照处理。问题现象可能原因排查方式解决方案启动时提示 CUDA 不可用驱动版本与 PyTorch/CUDA 不匹配nvidia-smi查看驱动支持的 CUDA 版本python -c import torch; print(torch.cuda.is_available())更新驱动或重装对应 CUDA 版本的 PyTorch启动时 OOM 退出了模型权重 KV Cache 超出显存检查日志中的显存估算降低--max-model-len降低--gpu-memory-utilization或换用量化模型接口请求超时队列过长或并发过高看服务端日志中排队数压测日志看错误码增大服务并发上限调高客户端超时限制请求 max_tokens突发流量下大量 429触发了框架限流策略对比限流配置和 QPS调整限流阈值或在网关层增加排队缓冲P95 延迟忽然升高prefill 集中到达batch 剧烈波动观察 TTFT 和 TPOT 分开分析调整调度策略错峰 prefill对长 prompt 做长度限制模型输出质量不稳定上下文长度过长或采样参数不合适检查输入 prompt 长度和生成 seed调整max_tokens、temperature、top_p必要时做 prompt 清洗批量任务中途卡死某个请求 hang 住阻塞整个 batch看服务端请求生命周期日志设置服务端和客户端超时对异常请求做熔断端口被占用上一次服务未完全退出ss -lntp或lsof -i:8000kill 残留进程或换新端口容器内无法访问 GPUDocker 未启用 GPU runtimedocker run --gpus all报错后查驱动路径安装 NVIDIA Container Toolkit重新启动容器这里特别提醒如果你用的是多个请求共享同一个服务的方案某个超长输出可能长时间占住显存资源导致其他请求排队。建议在服务端或网关层对max_tokens做统一上限避免单请求把资源吃光。10. 最佳实践与使用建议最后给一套可以落地的工程建议帮你把 burstiness 思路融入日常的 LLM serving 工作流。10.1 从最小可运行配置开始第一次测试不要追求大模型大并发。选一个 7B 级别的开源模型限制上下文长度在 4096 左右并发数从 1 慢慢加到 10、20、50。每次都记录延迟指标而不是一次性把所有配置推到极限。这样出问题时能快速判断是框架问题、模型问题还是流量模型问题。10.2 保留一套稳定的压测脚本把均匀流量脚本、突发流量脚本、混合流量脚本放到代码仓库里做成可复用的压测工具。每次升级 serving 框架、换模型、改调度参数后都跑同一套脚本对比指标变化。这是验证 burstiness 优化是否有效的可靠方式。10.3 监控要分两层系统层GPU 利用率、显存占用、CPU 内存。服务层排队请求数、批大小、TTFT、TPOT、P95/P99 延迟、错误码比例。两层指标必须关联分析。单独看 GPU 利用率看不出问题单独看 P95 延迟也定位不了瓶颈。推荐把指标统一打到 Prometheus然后用 Grafana 展示。10.4 设计限流和熔断机制生产环境不能依赖调度器硬扛突发流量。建议在入口加两层保护网关层基于 IP、Key、用户维度的 QPS 限流保护后端服务不被打垮。服务层设置最大并发数和超时时间超出部分排队或快速失败。限流的阈值不能拍脑袋。根据压测得到的瓶颈值设置比如服务稳定运行的并发上限是 20那就把限流阈值设为 25留出缓冲。10.5 数据合规与安全提醒部署 LLM serving 服务时务必遵守数据合规要求。压测数据、模型输入输出都可能在日志中留存不要使用未授权的人脸、声纹、隐私文档或敏感业务数据做测试。如果服务开放给外部调用必须做认证鉴权不能裸奔在公网。对模型生成的输出要设置审核机制避免有害内容直接流出。11. 总结与下一步回到标题Burstiness is all you need for LLM serving。这句话的真正含义不是「只要研究突发性就万事大吉」而是说如果把请求到达的突发性理解透彻你的 LLM 服务在吞吐、延迟和稳定性上都会有明显改善。最值得优先尝试的事情是按照本文第 6 节的方法先用均匀流量和突发流量各压测一轮记录 P95 延迟、TTFT、TPOT 和错误率。这是投入最小、收益最直接的验证路径。最容易踩的坑有两个。第一直接用匀速压测工具测 LLM 服务得出的结论会和真实负载差别很大真实用户行为是脉冲式的而且 prompt 长度各不相同。第二只看 GPU 利用率不看排队指标结果就是 GPU 显示很忙但接口延迟已经高到不可用。后续可以继续做的事情包括把 burstiness 指标纳入容量规划模型结合历史请求曲线做自动扩缩容在调度层试验基于到达率预测的批处理策略如果团队自研 serving 框架还可以把 burstiness 作为调度器的在线输入特征做更激进的负载感知优化。这篇文章里的思路和脚本可以直接作为你自建 LLM 服务的第一步验证材料。建议收藏备用下次压测时照着跑一遍再结合你自己的业务流量特征调整参数。