Windows 原生编译 SGLang(4/8):环境关------VS 版本、venv 顺序、CUDA 多版本、生成器缓存

这是攻关系列的第一篇实战。很多人以为编译的难点全在源码,其实最容易被低估、却最容易让人卡在第一步的,是环境。本篇要讲的四个坑,没有一个跟 sglang 本身有关------它们是所有人在 Windows 上编译 CUDA 扩展都可能撞上的共性问题。把这一关过了,才谈得上碰源码。
本篇所有解法都来自实战中真实踩过、并逐一验证过的过程。每个坑都按"现象 → 成因 → 解法 → 验证"来讲,方便你照着自查。
坑一:明明装了 VS2022,CMake 却报"No CUDA toolset found"
现象
配置阶段直接失败,报错很唬人:
-- Building for: Visual Studio 18 2026
CMake Error: No CUDA toolset found.
成因
这台机器上同时装着两个 Visual Studio:正式的 VS2022 ,以及一个 VS18 Insiders(2026 预览版)。CMake 在自动探测生成器时,默认会挑它认为"最新"的那个------于是选中了 VS18。
但问题在于:CUDA 13.1 只与 VS2022 做了 MSBuild 集成,没有为 VS18 提供集成文件。CMake 用 VS18 去找 CUDA 工具集,自然找不到,于是报"No CUDA toolset found"。报错信息本身有误导性------它看起来像"CUDA 没装好",实际是"用错了编译器版本去找 CUDA"。
解法
有两条路,本系列两条都用上了,各自解决一半问题。
解法 A(根上绕开):强制走 Ninja 生成器。 最干净的办法不是去修 VS 版本问题,而是改用 Ninja------Ninja 直接调用 nvcc,不依赖 Visual Studio 的 MSBuild CUDA 集成,从根上绕开"哪个 VS 认 CUDA"这件事:
cmd
set CMAKE_GENERATOR=Ninja
设好之后,配置阶段开头应显示 -- Building for: Ninja,而不再是 Visual Studio 18 2026。本系列全程用 Ninja,它也比 MSBuild 更快、更省心。
解法 B(显式锁定):用 vswhere 把版本钉死在 VS2022。 如果你确实要用 VS 工程而非 Ninja,就不能让 CMake / vswhere 自动挑,必须加版本区间约束,把范围限定在 17.x(VS2022 的主版本号是 17),排除掉 18.x:
cmd
vswhere -version "[17.0,18.0)" -property installationPath
顺带一个易错点:如果用了
vswhere -prerelease,它会优先选中预览版(VS18 Insiders),正好踩中这个坑。去掉-prerelease,并用上面的版本区间约束,才能稳定锁到 VS2022。
一个相关的历史残留
排查这个问题时还发现一个隐藏因素:之前有一次中途中止的 CUDA 13.2 升级,在两个 VS 安装目录里都留下了孤儿的 MSBuild 集成文件。这些残留会进一步干扰 CMake 对 CUDA 版本的正确探测。把这些孤儿文件清掉之后,CMake 才能干净地识别到 v13.1。如果你也经历过 CUDA 的反复升降级,这一点值得检查。
坑二:vcvarsall 显示成功,cl.exe 却凭空消失

这是本系列最隐蔽、也最反直觉的一个环境坑,值得细讲。
现象
在一个新开的命令行窗口里,按"常规直觉"的顺序搭环境:
cmd
:: 先跑 vcvarsall 初始化 MSVC 环境
"D:\...\VC\Auxiliary\Build\vcvarsall.bat" x64
:: 再激活项目的 venv
K:\...\.venv\Scripts\activate.bat
vcvarsall 明明打印了成功提示:
[vcvarsall.bat] Environment initialized for: 'x64'
但紧接着检查编译器,却什么都找不到:
cmd
where cl
INFO: Could not find files for the given pattern(s).
cl.exe(MSVC 编译器本体)凭空消失了------这跟"环境初始化成功"那句话直接矛盾。
成因
关键在 venv 的创建方式 。这个项目的 venv 是用 uv 创建的,而 uv 生成的 activate.bat 在处理 PATH 时,是"重置"而非"前置追加" :它会把 PATH 恢复到某个缓存下来的旧值,再把 venv 自己的 Scripts 目录加进去。
于是,如果先 跑 vcvarsall(它往 PATH 里塞进了 MSVC 工具链、Windows SDK、MSBuild 等一整批路径),再 激活 venv------激活动作会把刚才 vcvarsall 辛苦塞进去的那一整段路径整个冲掉 。vcvarsall 自己确实成功了,但它的成果被随后的 venv 激活覆盖了,表现就是"报成功、但 cl.exe 找不到"。
验证这个推断很简单:打印完整
PATH,会发现里面完全没有VC\Tools\MSVC\<版本号>\bin\Hostx64\x64这一段------vcvarsall该加的全没了,只剩 venv 激活前的原始 PATH 加上 venv 的 Scripts。
解法
把顺序反过来:先激活 venv,最后再跑 vcvarsall。 让 vcvarsall 的修改是最后一个生效的,就不会被任何后续步骤覆盖:
cmd
:: 1. 先激活 venv
K:\PythonProjects5\Unlimited-OCR\.venv\Scripts\activate.bat
:: 2. 再跑 vcvarsall(它的 PATH 修改最后生效,不会被冲掉)
"D:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\vcvarsall.bat" x64
:: 3. 其余环境变量
set CMAKE_GENERATOR=Ninja
set DISTUTILS_USE_SDK=1
set MAX_JOBS=4
cd /d K:\PythonProjects5\Unlimited-OCR\sglang\sgl-kernel
验证
cmd
where cl
正确时应指向 VS2022 的 MSVC 工具链:
D:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Tools\MSVC\14.42.34433\bin\Hostx64\x64\cl.exe
只要 where cl 能找到这个路径,就说明环境搭对了。这一步务必显式验证 ,不要因为 vcvarsall 报了成功就默认环境就绪------这个坑的全部杀伤力,就在于"报成功"和"实际可用"之间的那道裂缝。
坑三:生成器选择被缓存写死,改对环境也不生效

现象
环境明明已经按坑二的方法搭对了(where cl 能找到、CMAKE_GENERATOR=Ninja 也设了),重新配置却还是 报"No CUDA toolset found",而且这次的输出里少了 Building for: ... 那一行------直接从"加载初始缓存文件"跳到报错。
成因
这是 CMake 一个经典且代价高昂的陷阱:生成器的选择,一旦写进某个构建目录的缓存(CMakeCache.txt),后续即便环境变量改对了,CMake 也不会主动重新选择,只会沿用第一次缓存下来的那个。
也就是说:某次环境还没搭对时跑过一次配置,在构建目录里留下了一份记着"用 VS18"的缓存。之后哪怕你把环境彻底修对,只要还在同一个构建目录里继续,CMake 读到的仍是那份带毒的旧缓存------它根本不重新探测,所以"少了 Building for 那一行"正是它跳过探测、直接读缓存的标志。
解法
改动任何与生成器相关的东西后,必须物理删除整个构建目录,强制 CMake 从零重新配置:
cmd
rmdir /s /q Z:\b
删除后,务必再确认一次它真的没了,不要因为"提示找不到文件"就默认已清空:
cmd
dir Z:\b
:: 期望看到 "File Not Found",才算真清干净
确认彻底清空后,在搭好环境的窗口里重新配置,CMake 会从零开始、正确选用 Ninja。配置成功的标志是开头出现:
-- Building for: Ninja
而不再是 Visual Studio 18 2026。
一个容易混淆的点:并不是每次都要清构建目录。只改源码、只改编译选项(不涉及生成器)时,保留缓存增量编译反而更快。只有当你改动了生成器、或改动了
CMAKE_CUDA_FLAGS这类 Configure 阶段的全局变量时,才必须清缓存重配。 判断标准是:这次改的东西,CMake 是在"配置阶段"读它,还是在"编译阶段"读它------前者必须清。
坑四:CUDA 多版本共存,与 subst 短路径

这两个不算"故障",但属于会反复绊到人的环境特性,一并说清。
CUDA 多版本
这台机器上同时装了 5 个版本的 CUDA(12.6 / 12.8 / 12.9 / 13.0 / 13.1),where nvcc 会一口气列出全部:
cmd
where nvcc
C:\...\CUDA\v13.1\bin\nvcc.exe
C:\...\CUDA\v12.6\bin\nvcc.exe
... (其余版本)
这通常是 NVIDIA 安装程序每装一个版本就往系统 PATH 追加一条、长年累积的结果。它不致命 ------只要 CMake 配置时已明确指向 v13.1(本项目通过环境与 CMake 设置锁定),它就会用对版本。但它是一个有用的旁证:PATH 里塞着多个 nvcc,说明这台机器的环境层比较"拥挤",排查其他问题时要意识到这一点。
如果你也维护多版本 CUDA,建议配合一套显式的版本切换机制(例如把目标版本的
bin目录前置到PATH),让"当前生效版本"始终唯一、可控,而不是依赖PATH里谁排前面这种隐式顺序。
subst 短路径不跨会话
为了缩短构建路径(CUDA + CUTLASS 的深层模板路径很容易触及 Windows 路径长度限制),我们用 subst 把构建目录映射成一个短盘符:
cmd
subst Z: K:\sgk_build
要注意:subst 的映射不跨重启、不跨重新登录 。每开一个新的工作会话,都可能需要重新挂一次。如果某次构建突然报找不到 Z:\...,先检查这个映射是不是失效了------这跟下一个要提的"环境变量只在当前窗口有效"是同一类"会话级状态"问题。
贯穿四坑的一条主线:警惕"会话级状态"
回头看这四个坑,坑二、坑三、坑四其实共享同一个底层教训:很多关键状态是"会话级"或"目录级"的,不会自动跨越窗口、重启或目录留存下来。
set CMAKE_GENERATOR=Ninja只在当前命令行窗口有效------新开一个窗口,它就没了,CMake 退回默认探测、又选中 VS18(这正是坑一与坑三联动复发的根源);subst Z:只在当前登录会话有效------重启或重新登录就失效;- 生成器选择被写进构建目录的缓存------换了环境也不重选,除非物理删目录;
- venv 激活会重置当前窗口的 PATH------顺序错了就冲掉 vcvarsall 的成果。
所以,本系列后续每次"新开窗口续编",都遵循一套固定的、顺序严格的环境搭建流程(见下),把这些会话级状态一次性、按正确顺序重新建立起来。漏掉其中任何一步,或顺序错了,都可能让你莫名其妙地回到第一步的报错。
标准环境搭建流程(每个新窗口都照此执行)
cmd
:: 顺序严格,不可调换 ------ venv 必须在 vcvarsall 之前
K:\PythonProjects5\Unlimited-OCR\.venv\Scripts\activate.bat
"D:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\vcvarsall.bat" x64
set CMAKE_GENERATOR=Ninja
set DISTUTILS_USE_SDK=1
set MAX_JOBS=4
subst Z: K:\sgk_build
cd /d K:\PythonProjects5\Unlimited-OCR\sglang\sgl-kernel
:: 三项必查,缺一不可:
where cl :: 应指向 VS2022 的 cl.exe
where nvcc :: 应能找到 v13.1 的 nvcc
python -c "import torch; print(torch.__version__, torch.version.cuda)" :: 应是 cu130
小结与下一篇
本篇的四个坑,全部与 sglang 源码无关,却足以让人在真正开始编译之前就反复受挫:
- VS 版本:多版本共存时显式锁定 VS2022,别让 CMake 自动选到预览版;
- vcvarsall 与 venv 顺序:uv 的 venv 激活会重置 PATH,必须"先 venv,后 vcvarsall";
- 生成器缓存:改生成器/Configure 级变量后必须物理清空构建目录;
- 会话级状态 :环境变量、
subst、缓存都不会自动跨窗口/重启留存,新窗口要按固定流程重建。
把环境这一关稳住之后,我们才真正开始碰源码。第 4 篇 进入源码移植的上半场:那些"GCC/Clang 能过、MSVC 不认"的方言问题,以及 MSVC 预处理器的严格性------从 __builtin_clz 到 __asm__,从 __attribute__ 到宏参数里的裸 # 指令。
系列导航(全 14 篇)
编译移植篇(怎么把 sglang 从源码编出来)
- 00 · 系列总览
- 01 · EPGF 环境地基与岔路口
- 02 · 结论与可行性:三铁证 + --no-deps
- 03 · 编译篇·前置:FlashInfer Windows 源码编译
- 04 · 编译篇·环境关:VS 版本、venv 顺序、CUDA 多版本、生成器缓存
- 05 · 移植篇(上):GCC 方言与 MSVC 预处理器严格性
- 06 · 移植篇(下):常量求值、重载决议与编译器崩溃
- 07 · 编译篇·收尾:架构裁剪与 LNK2019 链接收尾
- 08 · 方法论:台账、幂等补丁脚本与多 AI 协作
部署运行篇(怎么跑起来并排障)
- 09 · 正确启动 SGLang + Unlimited-OCR
- 10 · 排障①:推理输出乱码/数值错误根因定位
- 11 · 排障②:环境变量块超限导致 spawn 子进程崩溃
- 12 · 性能调优:RTX 3090 MoE triton autotune config
- 13 · 长文档验证 + 代理/端口冲突坑 + 使用指南
编译移植篇讲"能不能编出来、怎么编";部署运行篇讲"编出来之后怎么跑通、怎么排障、怎么调优"。
两篇之间最关键的交叉点:本机实际编译用的是 第 07 篇 产物
sglang_kernel-0.4.3-cp310-abi3-win_amd64.whl,而 第 09 篇 的启动命令正是加载它 + Unlimited-OCR 模型。
参考资料与延伸阅读
以下为本文涉及的官方仓库、文档与规格站,建议发布前点一遍确认可达:
- Unlimited-OCR 官方仓库(模型与项目源码)
- SGLang 官方仓库
- SGLang 官方文档(启动参数 / OpenAI 兼容 API)
- flashinfer-windows(Windows 兼容 fork,编译前置)
- vllm-windows(同作者,可对照的 Windows 移植思路)
- PyTorch Windows CUDA 预编译索引(cu130)
- NVIDIA CUDA Toolkit 下载
- uv 官方文档(Python 环境治理)
- MSVC /Zc:preprocessor 标准预处理器
- MSVC 致命错误 C1001(编译器内部错误)
- nvcc -Xcompiler 转发 host 编译器选项
- CMake 生成器(Visual Studio / Ninja)
- RTX 3090 规格(GA102 / sm_86,共享内存 100KB)
- CUDA 共享内存上限与 dynamic_shared_memory 限制
- Windows 子进程环境变量块限制(CreateProcess / ~32KB)
- OpenAI 兼容 API 参考(推理调用)