← 返回资讯

SGLang 部署 OpenAI 兼容服务:启动参数与并发调优

2026-09-15

把 SGLang 跑起来是一行命令的事,这件事有点误导性。真正吃时间的是后面那段:并发一上来,日志里开始刷 KV 池满、请求被 retract 的告警,或者干脆在预填充阶段 OOM 崩掉;换个思路把并发限低一点,GPU 利用率又掉下来,队列空转。这时候翻文档会发现启动参数表长得看不到头,而它们彼此之间是互相牵制的——调大一个,另一个的余量就被吃掉。

所以这篇按「起服务 → 确认就绪 → 接客户端 → 拧旋钮 → 装观测」的顺序走一遍,重点放在每个参数在调什么,而不是给一套抄了就能用的配置(那种东西不存在,参数的合理值取决于你的模型、上下文长度和并发曲线)。所有命令与参数名都按 SGLang 官方文档原文,文档没写的地方我会直接说没写。

一、环境门槛和装法

先把不满足就白折腾的前置条件列清楚。文档给的常见 NVIDIA 平台口径是:Python 3.10 或更高,NVIDIA GPU 需要 sm80 及以上(文档举的例子是 A10、A100、L4、L40S、H100 这一档),操作系统推荐 Linux。AMD GPU、Intel Xeon CPU、Google TPU、昇腾 NPU、Jetson 这些平台各有专门的文档页,不走这一页的说明。

装法里推荐的是 uv:

pip install --upgrade pip
pip install uv
uv pip install --prerelease=allow sglang

--prerelease=allow 这个标记不是可选的装饰。安装文档专门解释了原因:SGLang 的部分依赖在 PyPI 上只发预发布版,不带这个标记时较老版本的 uv 会静默装上一个旧版 SGLang——不报错,只是版本不对,等你发现的时候可能已经在排查一个早就修掉的问题。uv 0.12.0 及之后这个标记变成无害的空操作,所以无脑带上就行。

另一条硬约束是 CUDA:文档写的是 SGLang 要求 CUDA 13,CUDA 12(cu129)的 wheel 与镜像已经退役,原因是对应的 PyTorch 版本不再发 CUDA 12.9 构建;带 CUDA 12 lane 的最后一个 SGLang 版本文档里点明了。如果你的机器还锁在 CUDA 12,这一条决定了你能装哪个版本,得先去安装页确认当前说明。

想要还没进稳定版的修复,文档给的是 nightly 装法,要同时加 --extra-index-url 指向 SGLang 的 wheel 索引、--prerelease=allow--index-strategy unsafe-best-match,缺一个 uv 就不会把 nightly 当候选。源码装是 clone 指定的发布分支再 pip install -e "python"

容器部署用 Docker Hub 上的 lmsysorg/sglang。这里有三个细节值得记:生产环境文档建议用 runtime 变体,它去掉了构建工具和开发依赖,体积明显更小;latestdev 都是可变标签,会被覆盖,要可复现就钉一个不可变的版本号标签;容器和 Kubernetes 都需要把共享内存配够(Docker 侧是 --shm-size,K8s 侧是改 /dev/shm 大小),它用于进程间通信,这一条在 manifest 里最容易漏。

两个高频启动故障有现成答案:报 OSError: CUDA_HOME environment variable is not set 就 export CUDA_HOME 指向 CUDA 安装根目录;FlashInfer 是默认的注意力算子后端,在 sm75 及以上设备上如果出问题,启动时加 --attention-backend triton --sampling-backend pytorch 换后端,并且文档希望你顺手去开个 issue。

二、起服务:文档的最小命令,和生产上要补的那几个参数

最小命令就是它:

python3 -m sglang.launch_server --model-path qwen/qwen2.5-0.5b-instruct --host 0.0.0.0 --port 30000

--host 默认值是 127.0.0.1,这是最常见的一个自找麻烦点——在容器里不显式写成 0.0.0.0,外面就是连不上,而现象长得像端口没映射。--port 默认 30000

参数一多,命令行会长到不好维护,文档给了配置文件的走法:写一个 YAML,用 --config 指过去,命令行参数覆盖配置文件里的值。文档里的示例是这样的:

cat > config.yaml << EOF
model-path: meta-llama/Meta-Llama-3-8B-Instruct
host: 0.0.0.0
port: 30000
tensor-parallel-size: 2
enable-metrics: true
log-requests: true
EOF

python -m sglang.launch_server --config config.yaml

注意 YAML 里的键名是去掉前导 -- 的长名形式。这也顺带回答了一个常见疑问:张量并行度那个参数的正式名是 --tensor-parallel-size--tp-size 是它的别名,文档的示例命令里还会用更短的 --tp。数据并行同理,--data-parallel-size--dp-size

对外提供服务时还有几个参数基本是必填的:

  • --api-key 给服务设访问密钥,OpenAI 兼容接口这一侧也走它。默认是不设,也就是任何人能连上就能用。
  • --admin-api-key 是单独的管理密钥,覆盖的是权重更新、清缓存、/server_info 这类管理/控制端点。设了之后这些端点要求请求头带 Authorization: Bearer <admin_api_key>。把推理密钥和管理密钥分开这件事,文档已经替你把接口切好了,别图省事共用一个。
  • --served-model-name 覆盖 v1/models 端点返回的模型名。你在网关或客户端里配的模型标识不想跟着本地权重路径走的时候用它。
  • --chat-template 指定内置对话模板名或模板文件路径,只对 OpenAI 兼容服务生效。默认情况下服务会自动套用 Hugging Face tokenizer 里带的对话模板;如果 tokenizer 里有多个命名模板(文档举的例子是 defaulttool_userag),用 --hf-chat-template-name 选一个,不选就用第一个可用的。这一条在工具调用场景下踩过的人不少——模板选错,工具块的格式就不对。
  • 反向代理后面跑要用 --fastapi-root-path 告诉应用它挂在哪个路径前缀下。要直接终止 TLS 则有 --ssl-keyfile--ssl-certfile--ssl-ca-certs--ssl-keyfile-password,证书文件变化时想热重载再加 --enable-ssl-refresh(它要求前两个参数已设)。
  • --enable-http2 会把 ASGI 服务器从 Uvicorn 换成 Granian,支持 HTTP/1.1 与 HTTP/2 自动协商,需要额外装 sglang[http2]

三、怎么判断服务真的就绪了

这一节我要先说文档没说的部分:在 quickstart、send_request 和服务端参数这几页里,官方没有给出一个明确的 readiness 探测端点的说明。文档给的就绪判断方式有两个,都偏「人看」:一是等终端里出现 The server is fired up and ready to roll! 这行日志;二是在示例代码里用 sglang.utils 提供的 wait_for_server 工具函数轮询。

所以如果你要给 Kubernetes 写探针,别照着别的引擎的习惯猜一个路径填上去。服务起来之后 API 文档挂在 http://localhost:30000/docs(Swagger UI)、/redoc(ReDoc)和 /openapi.json(OpenAPI spec)上,后者就是权威的端点清单——以你装的那个版本的 /openapi.json 实际列出的为准,比任何文章里抄来的路径可靠。

和启动时序有关的几个参数:--skip-server-warmup 跳过预热;--warmups 用逗号分隔的名字指定自定义预热函数,在服务开始监听请求之前跑;如果你是通过 checkpoint-engine 一类机制在服务起来之后才推权重进去,--checkpoint-engine-wait-weights-before-ready 让服务先等初始权重加载完再接推理请求。

另外一个和线上稳定性直接相关的是 --watchdog-timeout:一个前向批次跑得超过这个秒数,服务会主动崩掉而不是挂在那里。这个设计是对的——挂死的进程会让上游探针一直以为它活着,崩掉反而能触发重启和告警。但它意味着你的编排层必须有自动重启策略,否则一次长尾批次就变成一次人工介入。

四、把客户端接过来:OpenAI 兼容端点的细节

接口层是 OpenAI 兼容的,最省事的验证方式就是 curl:

curl http://localhost:30000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen/qwen2.5-0.5b-instruct",
    "messages": [
      {"role": "user", "content": "What is the capital of France?"}
    ]
  }'

Python 侧换个 base_url 就行,流式加 stream=True

import openai

client = openai.Client(base_url="http://127.0.0.1:30000/v1", api_key="None")

response = client.chat.completions.create(
    model="qwen/qwen2.5-0.5b-instruct",
    messages=[
        {"role": "user", "content": "List 3 countries and their capitals."},
    ],
    temperature=0,
    max_tokens=64,
    stream=True,
)

for chunk in response:
    if chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)

这也是从别的引擎迁过来成本最低的一点:如果你现在的服务是用 vLLM 起的 OpenAI 兼容接口,客户端代码基本不用动,改的只有 base_url 和模型名。

除了兼容端点,它还有原生的 /generate,采样参数走 sampling_params 字段(例如 temperaturemax_new_tokens),加 "stream": True 走流式,返回体里取 text。原生端点的灵活性更高,但代价是你的客户端被绑死在这个引擎上,除非确实需要兼容层没暴露的东西,否则没必要用。另外它支持完全不起 HTTP 服务、直接用 Engine 类做离线批量推理,批量刷数据和离线评测不必为了走 HTTP 再包一层。

有四个参数直接影响「接过来之后行为一不一样」,值得单独点出来:

--sampling-defaults,默认值是 model,可选 openaimodel 表示从模型的 generation_config.json 里读推荐采样参数,openai 用 SGLang/OpenAI 那套默认值。这一条解释了一类很难查的现象:同一个模型、同一个 prompt、客户端什么采样参数都没传,换个引擎结果风格就变了——因为默认值的来源本来就不一样。要对齐行为,要么显式传全部采样参数,要么把这个开关摆正。

--enable-cache-report,打开后每个 OpenAI 请求的 usage.prompt_tokens_details 里会返回命中缓存的 token 数。这是观测前缀KV cache命中率最直接的一条通路,而且是按请求粒度的,比只看聚合指标更容易定位「哪类请求没吃到缓存」。默认关。

--reasoning-parser--tool-call-parser,分别处理推理模型的思考段和工具调用的解析,两个都支持 auto,也就是从模型的对话模板里自动检测该用哪个解析器;也可以按模型系列显式指定(文档给了很长一张可选值清单,涵盖主流的几个系列)。不设的后果通常不是报错,而是思考内容或工具调用块以原始文本形式漏进 content 里,下游解析器一脸懵。

--allow-auto-truncate,默认关,也就是超过最大输入长度的请求直接报错;打开则自动截断。这个默认值是保守且正确的,自动截断会静默丢掉你 prompt 的一部分,通常是最前面的系统指令。真要打开,务必先确认截断发生在哪一端。配套的还有 --context-length,不设时用模型 config.json 里的值。

五、并发与显存:每个旋钮到底在调什么

这一节是本文的重点。先把显存账本摆出来,文档给的构成是:

总显存占用 = 模型权重 + KV 缓存池 + CUDA graph 缓冲 + 激活

--mem-fraction-static 控制的是前两项占 GPU 容量的比例,也就是 (模型权重 + KV 缓存池) / GPU 显存容量。不设时它会按 (GPU 显存 − 预留显存) / GPU 显存 自动算,检测不到 GPU 容量时落到文档里写明的一个默认分数(默认值以你装的版本的 --help 为准)。

理解这个公式,后面所有的 OOM 都有方向了:想扛更高并发,就是想把 KV 池做大,也就是把 --mem-fraction-static 顶高;顶得越高,留给激活和 CUDA graph 的余量越少,OOM 的风险就越靠前。 这不是一个能”优化”掉的矛盾,只能找位置。

文档给了两个找位置的办法。一是看启动日志里那行汇总,字段包括 max_total_num_tokenschunked_prefill_sizemax_prefill_tokensmax_running_requestscontext_lenavailable_gpu_mem——重点看 available_gpu_mem:偏高说明白留了太多显存,把 --mem-fraction-static 往上调;偏低则后面有 OOM 风险,往下调。文档给了一个留给激活的经验余量区间,具体到哪一档看 hyperparameter tuning 页。二是更糙但有效的办法:以 0.01 为步长往上加,直到你的负载开始 OOM,然后退回来一档。

然后是并发侧的几个上限:

  • --max-running-requests:同时在跑的请求数上限。这是解码阶段并发的闸门。
  • --max-queued-requests:排队请求数上限。注意文档明确说了,用 disaggregation 模式时这个参数被忽略。
  • --max-prefill-tokens:一个预填充批次里的 token 上限,有默认值(以 --help 为准)。这里有个容易误读的点:真实的上界是这个值与模型最大上下文长度中的较大者,所以把它调得比上下文长度还小并不会生效。
  • --chunked-prefill-size:分块预填充里单块的 token 上限,设成 -1 表示关闭分块预填充。长 prompt 场景下这是预填充侧最主要的显存旋钮。
  • --prefill-max-requests:预填充批次里的请求数上限,不设则不限。

调度策略这块,--schedule-policy 默认 fcfs,文档列的可选值有 lpmrandomfcfsdfs-weightlofpriorityrouting-key。其中 lpm 是 longest prefix match,它会重排请求顺序来提高前缀缓存命中率,代价是调度开销上升——只有你的负载确实有大量共享前缀时它才划算,负载之间毫无重叠时它是净开销。要判断自己属于哪一类,SGLang 的 RadixAttention 机制那篇里有一份「哪些负载吃得到前缀缓存」的清单,先对着过一遍再决定动不动这个参数。

--schedule-conservativeness 默认 1.0,含义是调度策略有多保守,值越大越保守。文档给的用法是双向的:如果经常看到 token usage 偏低(KV 池没用满)而队列里又有请求在等,说明服务端收新请求太保守,往下调(文档举的值是 0.3 这一档);反过来频繁看到 KV 池满、请求被 retract 的告警,就往上调(文档举 1.3 这一档)。偶尔出现 retract(一分钟一次这种频率)文档认为是正常的,不用管。服务端过于保守这个情况文档还解释了成因:客户端发了很多 max_new_tokens 很大的请求,但实际都因为 EOS 或停止词很早就结束了,调度器按最坏情况留了额。

KV 池满了要退掉谁,由 --retraction-policy 决定,默认 length(保持原有行为,先退掉「输出短、输入长」的请求),另一个可选值是 priority,按请求优先级退,方向与优先级调度一致。优先级调度本身是 --enable-priority-scheduling,默认关;开了之后还有一串配套项控制方向和抢占门限。

剩下几个跟吞吐直接相关的:

  • --page-size,一页里的 token 数,默认 1。
  • --cuda-graph-max-bs-decode:默认只对小批次启用 CUDA graph,但文档说某些模型、尤其在较大张量并行度下,把它调到更大的批次也有收益。代价明确写了——CUDA graph 吃显存,调它的同时可能要把 --mem-fraction-static 往回调。这就是前面说的互相牵制。
  • --num-continuous-decode-steps,默认 1,连续跑多步解码来摊薄调度开销。文档把利弊都写清了:可能提高吞吐,但也可能抬高首 token 延迟。典型的吞吐换延迟。
  • --enable-mixed-chunk:分块预填充时允许在一个批次里混合预填充与解码。
  • 并行度:文档的口径是显存够的时候为吞吐优先用数据并行,并且建议用 SGLang Model Gateway(原 Router)来做数据并行而不是直接用 dp_size 参数。张量并行开 --tp,如果报 “peer access is not supported between these two devices”,加 --enable-p2p-check
  • --enable-torch-compile:文档标注为实验性,对小模型小批次有加速,而且另一处说明里写了这个特性已经处于失维护状态、可能报错。这类参数摆在最后一档试。
  • 量化侧有 --quantization(例如 fp8)和 --kv-cache-dtype(fp8 的两种 recipe 都要求对应的 CUDA 与 PyTorch 版本),它们能把 KV 池的有效容量拉大,但会和精度、以及可用的算子后端互相制约。

最后把 OOM 的分流路径单独列一遍,因为这是最常用到的:预填充阶段 OOM,先调小 --chunked-prefill-size(文档举的是 4096、2048 这两档),代价是长 prompt 的预填充变慢;解码阶段 OOM,调小 --max-running-requests;两头都想压,就降 --mem-fraction-static(文档举 0.8、0.7),但它同时限制了最大并发和峰值吞吐。这三条的共同点是都在用吞吐换稳定,所以别在没看指标的情况下一次性全调。想系统地看吞吐这一侧还有哪些手段可用,可以接着看吞吐优化那篇,它和这里的参数是叠加生效的。

六、可观测性怎么开

没有观测就没有调优,这句话在推理服务上格外硬——上面那些参数里有一半,你不看指标根本不知道该往哪个方向拧。

最基础的是运行时日志。 稳定满载时看解码批次那行统计,字段包括在跑请求数、token 数、token usage、CUDA graph 是否启用、生成吞吐和 #queue-req。文档给的两条判据:token usage 是 KV 缓存的显存利用率,超过 0.9 算用得好;#queue-req 是队列深度,长期为 0 说明客户端提交太慢喂不饱服务端(这是客户端问题,别去调服务端),健康区间在几百到两千这个量级,但也不宜太大,否则调度开销上升。这行日志的打印间隔由 --decode-log-interval 控制。

Prometheus 指标--enable-metrics 打开。两个配套项容易漏:--enable-metrics-for-all-schedulers 让所有 TP rank 上的调度器各自记录请求指标,开启 DP attention 时尤其需要,否则所有指标看起来都来自 TP 0;--enable-mfu-metrics 额外暴露 MFU 估算相关指标。直方图的分桶都能自己定:--bucket-time-to-first-token--bucket-inter-token-latency--bucket-e2e-request-latency 收一组浮点数;--prompt-tokens-buckets--generation-tokens-buckets 支持三种规则(default 用预定义桶、tse 生成双侧指数分布桶、custom 直接给值)。默认桶边界不合你的延迟分布时,分位数会失真得很厉害,这块值得花十分钟配一次。还有 --extra-metric-labels 给指标挂自定义标签,以及一组 tokenizer 指标的自定义标签机制(通过 HTTP 头传,且必须在 --tokenizer-metrics-allowed-custom-labels 里白名单过)。注意如果你跑的是 --grpc-mode,指标和 profiling 端点在 HTTP sidecar 上,端口由 --grpc-http-sidecar-port 控制,默认是主端口加一。

请求级日志--log-requests 打开,粒度由 --log-requests-level 分四档:只记元数据、元数据加采样参数、再加部分输入输出、记全部输入输出。格式 --log-requests-format 可选人读的 text 或结构化 json,输出目标 --log-requests-target 可以同时给 stdout 和目录路径。这里有个现实的取舍:最高档会把完整 prompt 和输出落盘,对排查是金矿,对合规和磁盘是负担,生产上一般只在排查窗口临时开。另有 --enable-request-time-stats-logging 记录每请求的时间统计、--show-time-cost 显示自定义打点耗时、--export-metrics-to-file(配合 --export-metrics-to-file-dir)把每请求的性能指标写到本地文件供外部系统消费。

链路追踪--enable-trace 打开 OpenTelemetry,--trace-modules 选要追的组件,--otlp-traces-endpoint 配采集器地址。崩溃诊断则是 --crash-dump-folder:它会保存崩溃缓冲区里已完成的请求加在途请求,在 NVIDIA CUDA 上还会把 device coredump 配到这个目录下。这个参数不设的话,请求级崩溃转储和 coredump 默认都不开——出过一次不可复现的崩溃之后再想起它就晚了。

七、调优的正确顺序

参数表是平的,动手顺序不是。按文档的逻辑串下来,合理的顺序是这样:

  1. 先装观测,再拧旋钮。 --enable-metrics 加上日志级别配好,至少要能看到 token usage#queue-req 和延迟分位。没有这几个数,后面每一步都是盲调。
  2. 先确认瓶颈在预填充还是解码。 两边的参数几乎不重叠,方向错了怎么调都没用。长 prompt、短输出、#queue-req 堆积通常指向预填充;反过来在跑请求数上不去、频繁 retract 指向解码与 KV 池。
  3. 先排除「不是服务端的问题」。 #queue-req 长期为 0 就是客户端喂得慢,这时候调服务端参数纯属浪费时间,该改的是压测脚本或上游并发。
  4. 再榨利用率。--schedule-conservativeness 双向找位置,目标是把 token usage 顶上去同时把 retract 频率压到可接受。这一步的收益通常比换调度策略大。
  5. 然后调显存分配。 --mem-fraction-static 按启动日志里的余量往上顶,顶到 OOM 再退一档。注意它和 --cuda-graph-max-bs-decode 是抢同一块显存的。
  6. 再考虑改调度策略和并行度。 lpm 只在前缀重叠明显时才值得付调度开销;并行度调整意味着重新压测和重算显存账本,放在这一步之后。
  7. 最后才是实验性开关。 torch.compile、量化格式、KV cache 数据类型这一档,收益不确定、副作用面大,而且要重新验证输出质量。

贯穿全程有一条纪律:一次只动一个参数,每次都记下前后的指标。 这些旋钮互相牵制,同时动两个之后你分不清是哪个起的作用,甚至会把一个负收益和一个正收益抵消掉,得出「都没用」的错误结论。

八、这些参数解决不了什么

说几句反过来的话。

服务端参数救不了 prompt 结构问题。 如果你的请求之间前缀重叠比例本来就低,lpm、缓存策略这些旋钮都是空转,该改的是 prompt 的字段顺序——把固定内容放前面、易变字段放后面。这是客户端的活,跟启动命令没关系。

这些参数大多在拿延迟换吞吐。 如果你的场景是低并发、对首 token 延迟敏感(比如交互式补全),上面一半参数的方向是反的:--num-continuous-decode-steps 会抬 TTFT,把并发上限顶高对单请求延迟没好处。按吞吐优化出来的配置直接搬到延迟敏感场景,通常更差。

默认值和可选值会随版本变。 这篇里提到的默认值都以文档当前说明为准,真正权威的是你装的那个版本的 python3 -m sglang.launch_server --help 输出。参数别名、可选值清单、乃至某个参数是否被标记为废弃(--disable-cuda-graph 就已经被标为废弃、改用新的 CUDA graph 后端参数),这些都在动。写部署脚本时把 --help 的输出存一份进仓库,升级时 diff 一下,比事后查故障便宜得多。

最后一条得自己量。 我没法告诉你 --mem-fraction-static 该设多少、--max-running-requests 该开多大,任何一篇文章给出具体数值都是不负责任的——这些值取决于你的模型大小、上下文长度、输入输出长度分布和并发曲线,换个模型就得重来一遍。能确定的只有机制和顺序。真要给一个起点,就是拿你线上抽样的真实请求去压,而不是拿合成的等长 prompt——合成负载压出来的配置在真实的长尾分布下经常第一天就 OOM。如果你连该不该自建这一层都还没定,先回到推理框架选型那篇把候选池和判据过一遍,再回来调参数。

算完账发现自建推理不划算?

先用托管端点把业务跑起来,量上来了再回头算自建的平衡点。

去试用

这个页面有问题?

提交时会附带当前页面地址和浏览器信息,帮助我们定位问题。不填联系方式即为匿名。