Windows 原生编译 SGLang(3/8·前置):FlashInfer Windows 源码编译踩坑全记录

背景
在 Unlimited-OCR 项目里需要原生跑 sglang 做 OCR/VLM 模型的 serving。sglang 官方明确不支持 Windows——核心原因是它依赖两个 Linux 绑定很深的本地扩展:sgl-kernel(CMake + scikit-build-core 编译的 CUDA kernel 库)和 FlashInfer(默认 attention backend)。社区目前没有 sgl-kernel 的 Windows 移植先例(上游有一个长期没人回应的 issue 在问这个问题),但 FlashInfer 已经有 SystemPanic/flashinfer-windows 这个维护中的 fork 可用——跟我现在用的 vllm-windows 是同一个作者。
这篇先记录 FlashInfer 这一关,延续"步步分拆、步步可控、不黑箱操作"的老规矩,每一步都先验证再往下走。
环境基线

| 项目 | 版本 |
|---|---|
| OS / GPU | Windows 11, RTX 3090(sm_86) |
| VS | 2022 Professional |
| CUDA(系统 nvcc) | 13.1 |
| torch(venv 现状) | 2.10.0+cu130 |
| sglang | win-build 分支,基于 tag v0.5.13.post1 |
| FlashInfer | SystemPanic/flashinfer-windows,本地 version.txt = 0.6.11.post3 |
决策点:为什么是 v0.5.13.post1,不是更老的 tag

最初按文档示例打算用 v0.5.6.post2,但拉下真实 tag 列表后发现项目早就推进到 v0.5.13.post1。改用这个新 tag 的关键证据:main 分支的 python/pyproject.toml 里已经把 easydict 列为依赖,注释写明"Required by remote model code (e.g. DeepSeek-OCR)"——说明较新版本已经内置了对 DeepSeek-OCR 这类远程代码模型的支持,这正好踩在 Unlimited-OCR 的需求点上。
依赖版本链路梳理

读本地代码拿到的真值(没有用网上查到的、可能对不上具体 tag 的二手信息):
sgl-kernel/pyproject.toml:构建只要求torch>=2.8.0,无精确上限,dependencies = [],wheel.py-api = "cp310"(走稳定 ABI,理论上一次编译可以跨 py3.10~3.13 通用)。- 外层
python/pyproject.toml:对 torch 是精确锁定——torch==2.11.0、torchaudio==2.11.0、torchao==0.17.0、torchcodec==0.11.1(注释说明 0.10 版本对 torch 2.11.x ABI 不兼容)。FlashInfer 锁定为flashinfer_python[cu13]==0.6.12。
而 flashinfer-windows fork 本地版本是 0.6.11.post3,比要求的 0.6.12 差一个 patch 版本。查了一下这一档的上游变更(WAN 扩散模型的 RMSNorm+RoPE 融合、cute-dsl 警告修复、SM120 MoE kernel 替换),都不涉及单卡 Ampere 跑 attention/sampling 的核心路径。决定先用 0.6.11.post3 把 Windows 工具链跑通,装 sglang 本体时再把这一行 pin 本地放宽,在 commit 里写清楚理由——这是有意的版本偏离,不是疏忽。
编译过程与踩坑记录
| # | 现象 | 根因 | 处理 |
|---|---|---|---|
| 1 | git checkout v0.5.6.post2 # 注释 报一串 pathspec did not match | cmd.exe 不像 bash/PowerShell 会吃掉 # 后面的内容,整行都被当成参数拆开了 | 命令里不再带行内 # 注释;这个坑后面还踩了第二次,确认是我反复的疏漏 |
| 2 | fix_cuda_13_align.py 报 PermissionError,改的是 C:\Program Files\...\cuda.h | 在普通权限的 cmd 窗口里敲命令不会提权,必须从一开始就用提权方式打开新窗口 | Win → 输入 cmd → Ctrl+Shift+Enter 触发 UAC,新开管理员窗口 |
| 3 | 补丁脚本提权后跑完,完全没有任何输出 | 脚本本身没写任何 print,无论改成功、条件不满足跳过,表现都是静默退出——不能凭"没报错"就认定生效 | 直接读脚本源码确认它在找什么字符串,再用 findstr 去 cuda.h 里查证:确认 TENSOR_MAP_ALIGN 定义和两处 alignas/_Alignas 替换都已经写入(行号 3662~3671),文件修改时间也对得上,这才算闭环验证 |
| 4 | python -m build --no-isolation --wheel 报 Missing dependencies: apache-tvm-ffi..., setuptools<82,>=77 | --no-isolation 不会自动下载构建期依赖,venv 里没有的就得自己装 | pip install "apache-tvm-ffi!=0.1.8,!=0.1.8.post0,<0.2,>=0.1.6" "setuptools<82,>=77" |
| — | CLAUDE.md 看起来像 Windows 构建指南,实际全是上游给 AI agent 用的通用贡献者文档 | fork 时原样带过来的,跟 SystemPanic 自己写的 Windows 专属步骤是两份不同的文档 | 真正的构建步骤要去 README.md 里"Windows instructions"那一节找 |
关于第 2、3 条额外提一句版本相关的背景:CUDA 13.0~13.2 的 cuda.h 用 128 字节对齐定义 CUtensorMap_st,MSVC 目前还不支持按值传递这种过对齐类型作函数参数,NVIDIA 官方说 13.3 会改回 64 字节对齐。手头能用的 13.0/13.1 都在受影响区间内,这个补丁躲不掉。
实际编译命令(去掉踩坑后的干净版本)
管理员窗口,只跑一次:
python K:\PythonProjects5\Unlimited-OCR\flashinfer-windows\flashinfer-jit-cache\fix_cuda_13_align.py
普通窗口,实际编译:
K:\PythonProjects5\Unlimited-OCR\.venv\Scripts\activate.bat
"C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\vcvarsall.bat" x64
cd K:\PythonProjects5\Unlimited-OCR\flashinfer-windows
set DISTUTILS_USE_SDK=1
set MAX_JOBS=10
set TORCH_CUDA_ARCH_LIST=8.6
set FLASHINFER_CUDA_ARCH_LIST=8.6
pip install "apache-tvm-ffi!=0.1.8,!=0.1.8.post0,<0.2,>=0.1.6" "setuptools<82,>=77"
python -m build --no-isolation --wheel
结果:Successfully built flashinfer_python-0.6.11.post3-py3-none-any.whl。
刻意只编了主 wheel,没有顺手把 flashinfer-jit-cache 和 flashinfer-cubin 也编出来——这两个只是预编译缓存,用来加速冷启动,不是必需品;FlashInfer 真正的 kernel 是运行时按需通过 nvcc/ninja 现场 JIT 编译的,所以下一步要验证的不是"wheel 能不能编出来",而是"装上之后,真实调用一次 kernel,MSVC + nvcc + ninja 这条 JIT 编译链路能不能在 Windows 上真正跑通"。
JIT 编译链路验证
import flashinfer 成功只能证明包结构没问题,不能证明运行时 JIT 编译这条链路真的能在 Windows 上跑通——FlashInfer 默认是用到才编,所以专门写了一个最小测试脚本,调用 flashinfer.norm.rmsnorm 两次并计时,同时打开 FLASHINFER_JIT_VERBOSE=1 把底层 ninja/nvcc/link.exe 的真实调用打印出来。
关键证据:
nvcc命令行里-gencode=arch=compute_86,code=sm_86,确认生成的就是 3090 的目标架构代码;- 第一次调用 22.93s(真实触发了 nvcc 编译 +
link.exe链接),第二次 0.0010s(命中进程内模块缓存)——这个耗时悬崖本身就是"确实走了编译路径"的证据,而不是 fallback 到了别的实现; - 两次调用结果
torch.allclose为True,确认编出来的 kernel 计算结果数值正确。
一个不算坑、但值得记一笔的细节:链接阶段 link.exe 输出里出现了一段乱码(????? ...norm.lib ??? ...norm.exp)。这不是报错,是 link.exe 按中文 Windows 本地化输出的提示文字("正在创建库…和对象…"一类的信息)在当前终端代码页下显示错位,纯粹是显示层面的编码问题,用 dir 确认对应的 .dll/.lib/.exp 文件确实生成即可排除疑虑,不影响实际产物。
写"可复现脚本"反而自己引入的两个新坑
把上面验证过的命令封装成 build_flashinfer_windows.bat 之后,反而暴露出两个新问题,比手动敲命令时更隐蔽,值得单独记一笔:
坑一:vswhere -prerelease 选错了 VS 版本。 为了不硬编码 VS 安装路径,脚本里用 vswhere.exe -latest -prerelease ... 动态查找。但这台机器上同时装了 VS2022 Professional(内部版本 17.x,已验证可用)和 VS “18” Insiders(2026 预览版)。-prerelease 让 vswhere 把预览版也纳入候选,-latest 又总是选版本号最高的——于是脚本悄悄换上了一套完全没验证过的编译器。去掉 -prerelease、再加一条 -version "[17.0,18.0)" 把候选范围显式锁定在 VS2022 这一代,才是真正稳妥的修法(单纯去掉 -prerelease 只能解决"今天",VS18 哪天 GA 了照样会被选中)。
坑二:pip install <wheel> --force-reinstall 没加 --no-deps,牵连重装了 torch。 --force-reinstall 不只重装目标包本身,默认会把 pip 解析到的所有依赖一并强制重装。flashinfer_python 的 wheel 元数据里声明了对 torch 的依赖,这一步触发 pip 重新解析 torch——而默认索引 pypi.org 上裸的 torch 包给的是 CPU-only 构建,直接把验证好的 2.10.0+cu130 换成了不带 CUDA 的 2.12.1,导致 flashinfer.jit 初始化时读 torch.version.cuda 拿到 None 直接崩。修法是给这条 pip install 加上 --no-deps,只重装目标包,不让它连带去碰已经精确匹配好的 CUDA 专用依赖。恢复 torch 用的是当初装它的原始命令:
pip install torch==2.10.0 torchvision==0.25.0 torchaudio==2.10.0 --index-url https://download.pytorch.org/whl/cu130 --force-reinstall
这条本身也值得记一笔通用经验:任何时候对一个已经精确钉死 CUDA 变体版本(+cu130 这种本地版本标签)的包做 --force-reinstall,都该顺手加 --no-deps,否则只要这个包的依赖树里牵到 torch/torchvision 这类有 CUDA/CPU 分裂构建的包,就有被默认 PyPI 索引悄悄换成 CPU 版的风险。
修复后用同一个 test_rmsnorm_jit.py 重新跑了一遍,结果跟最初那次几乎一致(21.25s → 0.0000s 的耗时悬崖,sm_86 codegen,Outputs match: True)——确认这场意外没有在环境里留下任何隐性损伤,这一关正式闭环。
当前进度与下一步
- ✅ FlashInfer 主 wheel 编译成功,安装后完成了一次真实的 nvcc/ninja JIT 编译验证(细节见上)
- ⏳ 把 sglang 里
flashinfer_python[cu13]==0.6.12这一行本地放宽到0.6.11.post3,并在 commit message 里写明理由 - ⏳ torch 从
2.10.0+cu130精确升级到 sglang 要求的2.11.0(FlashInfer 用 TVM-FFI 绑定,这一步不影响 FlashInfer 本身,可以放在后面单独做) - ⏳ 真正的硬骨头:
sgl-kernel的 Windows 编译——目前没有任何社区先例,详见第 3 篇起的实战记录
附:最终版脚本
build_flashinfer_windows.bat(已修复 vswhere 选错版本 + force-reinstall 牵连 torch 两个问题)
@echo off
setlocal
REM ---- 按你的实际路径调整这两项 ----
set "REPO_DIR=K:\PythonProjects5\Unlimited-OCR\flashinfer-windows"
set "VENV_DIR=K:\PythonProjects5\Unlimited-OCR\.venv"
REM ---- 按你的 GPU 调整(RTX 3090 = 8.6) ----
set "TARGET_ARCH=8.6"
echo [1/6] Activating venv...
call "%VENV_DIR%\Scripts\activate.bat"
if errorlevel 1 (
echo ERROR: venv activate failed, check VENV_DIR path: %VENV_DIR%
exit /b 1
)
echo [2/6] Locating Visual Studio installation via vswhere...
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
set "VS_INSTALL_PATH="
REM 注意:这一行故意不用括号包成多行 if 块——vswhere 的结果要在
REM 这条语句执行完之后才算真正落地,放进同一层括号块里读会拿到旧值
REM (cmd.exe 对 %var% 是整块预展开,不是逐行实时展开)。
if exist "%VSWHERE%" for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -products * -version "[17.0,18.0)" -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VS_INSTALL_PATH=%%i"
if defined VS_INSTALL_PATH (
set "VS_VCVARS=%VS_INSTALL_PATH%\VC\Auxiliary\Build\vcvarsall.bat"
) else (
echo WARNING: vswhere detection failed or returned nothing, falling back to hardcoded path.
set "VS_VCVARS=D:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\vcvarsall.bat"
)
echo Using vcvarsall at: "%VS_VCVARS%"
if not exist "%VS_VCVARS%" (
echo ERROR: that path does not exist.
exit /b 1
)
echo [3/6] Initializing VS x64 dev environment...
call "%VS_VCVARS%" x64
if errorlevel 1 (
echo ERROR: vcvarsall.bat ran but returned a non-zero exit code.
exit /b 1
)
set "DISTUTILS_USE_SDK=1"
set "MAX_JOBS=10"
set "TORCH_CUDA_ARCH_LIST=%TARGET_ARCH%"
set "FLASHINFER_CUDA_ARCH_LIST=%TARGET_ARCH%"
cd /d "%REPO_DIR%"
if errorlevel 1 (
echo ERROR: cannot cd into REPO_DIR: %REPO_DIR%
exit /b 1
)
echo [4/6] Installing build-time dependencies...
pip install "apache-tvm-ffi!=0.1.8,!=0.1.8.post0,<0.2,>=0.1.6" "setuptools<82,>=77" build
if errorlevel 1 (
echo ERROR: failed to install build dependencies.
exit /b 1
)
echo [5/6] Building wheel (this can take a while, MSVC + nvcc)...
python -m build --no-isolation --wheel
if errorlevel 1 (
echo ERROR: wheel build failed. Scroll up for the actual cl.exe/nvcc error.
exit /b 1
)
echo [6/6] Installing built wheel...
set "WHEEL_PATH="
for %%f in (dist\flashinfer_python-*.whl) do set "WHEEL_PATH=%%f"
if not defined WHEEL_PATH (
echo ERROR: no wheel found in dist\, build may have silently produced nothing.
exit /b 1
)
pip install "%WHEEL_PATH%" --force-reinstall --no-deps
if errorlevel 1 (
echo ERROR: pip install of built wheel failed.
exit /b 1
)
python -c "import flashinfer; print('flashinfer', flashinfer.__version__, 'installed OK')"
echo Done. Wheel: %WHEEL_PATH%
echo Reminder: this only validates import. Run a real JIT-triggering call
echo to confirm the nvcc/ninja/link.exe pipeline actually works end to end.
endlocal
test_rmsnorm_jit.py(JIT 编译链路触发验证脚本)
import time
import torch
import flashinfer.norm as fn
device = torch.device("cuda:0")
batch_size, hidden_size = 4, 4096
x = torch.randn(batch_size, hidden_size, device=device, dtype=torch.float16)
w = torch.randn(hidden_size, device=device, dtype=torch.float16)
print("First call (expect this to trigger nvcc/ninja JIT compile)...")
t0 = time.time()
out1 = fn.rmsnorm(x, w)
torch.cuda.synchronize()
t1 = time.time()
print(f"First call took {t1 - t0:.2f}s, output shape={out1.shape}, dtype={out1.dtype}")
print("Second call (should hit the in-process module cache, no recompile)...")
t2 = time.time()
out2 = fn.rmsnorm(x, w)
torch.cuda.synchronize()
t3 = time.time()
print(f"Second call took {t3 - t2:.4f}s")
print("Output sanity check, max abs value:", out1.abs().max().item())
print("Outputs match between calls:", torch.allclose(out1, out2))
系列导航(全 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 参考(推理调用)
2268

被折叠的 条评论
为什么被折叠?



