用一个 uv 编译开关为 vLLM 容器启动省下约 18 秒
Reducing vLLM startup time with one uv flag
vLLM 博客作者 Nils Matteson 实测在镜像构建时设置 ENV UV_COMPILE_BYTECODE=1 预编译字节码,H100 服务器上 Qwen3-8B 和 Qwen3-32B 的启动到首个正确响应中位节省分别为 17.6 秒(17.6%)和 17.8 秒(13.9%)。
作者用配对实测给出 uv 预编译字节码对 vLLM 启动的具体节省和设置方法,结果边界和镜像体积代价也交代清楚。
全新的 vLLM 容器在开始处理任何请求之前,可能会花上几秒钟来编译 Python 源码。在我的 H100 服务器基准测试中,把这份字节码打包进镜像后,每次启动大约节省了 18 秒,只需一个 uv 构建设置:
ENV UV_COMPILE_BYTECODE=1在安装运行时包之前设置该标志,这样 uv 就会在构建期间编译它们的 Python 源码。这避免了在跨模型共享的导入过程中重复编译。Qwen3-8B 和 Qwen3-32B 就是两个例子:配对启动节省的中位数分别为 17.6 秒和 17.8 秒。
对于官方镜像用户,PR #55422 已包含在这个 CUDA nightly 中:
vllm/vllm-openai:nightly-8cbd5d03006c33185f402249ff2b448efd594986该变更将从 v0.32.0 起在官方发布镜像中提供支持。这些镜像的用户无需自行设置构建标志。
H100 上的启动节省
我测量了从进程启动到首次正确响应的时间,镜像和模型文件均已位于本地。每个模型四组相邻配对,比较 nightly 中随镜像发布的包字节码与移除这些缓存后的情况。全部八组配对均有改善。镜像拉取和模型下载不计入。

每个模型四组配对,每种顺序各两组,在一台 Xeon Platinum 8480+ / H100 SXM 80GB 服务器上。软件文件常驻内存;应用/编译器缓存从全新状态开始。连线连接配对的启动。
解释器、已安装软件和路径保持不变;只有包 .pyc 的可用性发生变化。两种条件下均保持正常的缓存写入启用。方法、JSON 和 全部 16 次观测 记录了配置和输出检查。这些是单一部署上的配对中位数,并非全集群范围的估计。
两种模型规模的绝对节省时间相近,而 8B 的百分比降幅更大:17.6% 对 13.9%。这与避免两个模型共享的 Python 导入工作相一致。节省的时间将取决于运行时和主机。
其他启动测量
在我的 i9-13900HX / RTX 4090 Laptop 上,一项单独的匹配镜像研究覆盖了 Qwen3-0.6B、Qwen3-1.7B 和 SmolLM2-1.7B,每个模型六组平衡配对。全部 18 组配对均有利于预编译,配对节省的中位数为 12.8–13.5 秒。图 展示了每一组配对,方法 链接了数据。硬件、运行时和时钟与 H100 研究不同,因此我将结果分开呈现。
早先的 Qwen3-0.6B 测试还包括从本地回环注册表拉取并解包匹配镜像到空的 Docker 存储中。在三组配对中,配对节省的中位数为 15.4 秒。预编译镜像增加了 153 MB 压缩体积,即 1.8%,这在首次拉取较慢时可能有影响。镜像交付测量 保留了该单独的时间边界和镜像大小权衡。
为什么全新容器会再次编译 Python
CPython 在执行源码之前会将其编译为代码对象。有效的 .pyc 文件存储了编译后的代码,以便 Python 下次直接加载。模块初始化仍会运行。这些是 Python 导入;torch.compile 和 GPU 内核有单独的编译和缓存路径。
pip 默认在安装时编译字节码;uv 会跳过这一步。Python 可以在容器运行时写入缺失的缓存,但这些文件位于其可写层中。下一个全新容器从镜像启动,同样的编译工作仍在等着它。uv 的 Docker 指南建议为生产镜像启用编译。
在之前的匹配镜像研究中,我用 vLLM 的 CUDA Dockerfile 针对测试 GPU 的架构,从同一个 vLLM 开发提交构建了两个镜像,一个启用该设置,一个不启用。它们的 Python 解释器、已安装包版本、源文件和原生库均一致。
为了确认 Python 使用了缓存,我在一个全新进程中运行了 import vllm.entrypoints.cli.serve。这会加载 vllm serve 背后的 Python 代码和依赖,但不加载模型,也不处理请求。我将其称为serving-CLI 导入:
| 匹配的 Docker 镜像 | 默认构建 | 预编译构建 |
|---|---|---|
| 已安装包的源文件 | 26,734 | 26,734 |
| 具有有效字节码的源文件 | 407 | 26,734 |
| serving-CLI 导入期间的源编译 | 5,257 | 0 |
用 -X pycache_prefix 指向空目录运行预编译镜像,源编译又回来了。包和源文件没有变化;加载器再也找不到随镜像提供的字节码。
编译器时间解释了大部分导入节省
在 Docker 之外的一个独立 vLLM 安装中,有效字节码将插桩后的 serving-CLI 导入从 12.20 秒缩短到 5.01 秒,节省了 7.19 秒。没有缓存时,Python 在 5,118 次源编译器调用中花费了 6.99 秒;有缓存时,此类调用为零。编译占了这一导入差距的大部分。这个每种条件只运行一次的诊断并未划分完整的服务器启动节省。
另一项独立的重复研究在 Docker 之外的一个已发布 vLLM 0.28.0 安装中测量了不带编译器插桩的导入。每个导入根都有改善:

每种条件三个全新进程,被测的 Python 和原生库文件常驻内存,且无编译器插桩。这些是与上述诊断分开的观察结果。圆点是观察值;短横线是中位数。每次导入都包含其依赖,因此各行不可相加。
在这个 vLLM 版本中,import vllm 已经加载了 Dynamo、Inductor 和 SymPy;单纯的 import torch 则不会加载它们。大部分源代码属于依赖项,但 vLLM 的导入路径决定了何时加载它们。
对于一项单独的仅编译器测试,我使用了追踪得到的 serving-CLI 语料库:5,118 个文件,包含 78.6 MB 源代码。此安装与上面的 Docker 构建不同,后者编译了 5,257 个文件。我在计时 Python 编译器之前先将语料库读入内存:
# Read and verify the sources before starting the clock.
for filename, source_bytes in preloaded_sources:
compile(source_bytes, filename, "exec", dont_inherit=True, optimize=0)编译并丢弃这些代码对象在三次全新进程运行中的中位数为 6.30 秒,当前线程 CPU 时间为 6.30 秒。计时循环内没有发生文件读取、模块执行或字节码写入。这是独立编译器的中位数;上面的 6.99 秒是在实际导入期间通过逐调用计时钩子测得的。
源文件已在 RAM 中时节省仍然存在
当两种条件下相同的 8.85 GB 已安装 Python 和共享库文件都已常驻内存时,预编译字节码仍能在每次服务器启动时节省 9–12 秒。将这些文件保留在操作系统页面缓存中并未消除这一节省。
更快的第二次启动无法确定原因:第一次启动会预热文件系统页面并写入 .pyc 文件。在同一解释器中重复导入也可能复用 sys.modules。
在此测试中,我直接从 Linux Python 安装环境启动 vllm serve,未使用 Docker。每次全新启动前我都确认了驻留状态,然后计时从启动到首次正确 HTTP 响应。不涉及镜像拉取。一种条件以预编译包 .pyc 文件开始;另一种在受测包安装中没有任何此类文件。
从启动到首次正确响应,无预编译包缓存时耗时 54.7–60.6 秒,有缓存时耗时 44.6–48.4 秒。三组配对节省分别为 10.13、9.35 和 12.23 秒。全部六次启动均返回正确响应。
正常的字节码写入和父子缓存复用保持启用,因此这测量的不只是编译器。它还使用了与匹配镜像实验不同的 vLLM 构建、Python 构建、CPU 限制和计时边界。结果不可叠加。这些是单主机观察结果;CPU 频率和热状态未固定。
在镜像中使用
对于你自己的镜像,在安装运行时包的 uv 命令之前设置 UV_COMPILE_BYTECODE=1,然后重新构建。
主要陷阱是已有的基础镜像。后续 uv 命令是否也会编译从 FROM 继承的包,取决于命令和 uv 版本。检查最终镜像,并使用运行时 Python 的 compileall 准备任何缺失的缓存。在阶段之间复制包时保留源码和字节码。添加 -O 或更改运行时缓存路径可能改变 Python 查找的文件。方法补充包含缓存覆盖命令和兼容性注意事项。
在早先的匹配镜像测试中,额外的 153 MB 在镜像拉取缓慢时可能产生影响。已缓存这些层的节点在启动另一个容器时可避免该传输。构建时开销未单独测量。在早先的隔离安装中,缓存的服务 CLI 导入仍耗时约五秒;模块初始化仍是需要调查的工作。
方法与测量给出了确切环境、运行顺序、对照和限制,并附有 JSON 和 CSV 数据。感谢 Simon Mo 推动我控制页面缓存并单独对编译器计时。
来源:vLLM 官方博客 · vllm.ai