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

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 这一关,延续"步步分拆、步步可控、不黑箱操作"的老规矩,每一步都先验证再往下走。

环境基线

技术图1(流程/架构)

项目版本
OS / GPUWindows 11, RTX 3090(sm_86)
VS2022 Professional
CUDA(系统 nvcc)13.1
torch(venv 现状)2.10.0+cu130
sglangwin-build 分支,基于 tag v0.5.13.post1
FlashInferSystemPanic/flashinfer-windows,本地 version.txt = 0.6.11.post3

决策点:为什么是 v0.5.13.post1,不是更老的 tag

技术图2(对比/要点)

最初按文档示例打算用 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 的需求点上。

依赖版本链路梳理

技术图3(速查/清单)

读本地代码拿到的真值(没有用网上查到的、可能对不上具体 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.0torchaudio==2.11.0torchao==0.17.0torchcodec==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 里写清楚理由——这是有意的版本偏离,不是疏忽。

编译过程与踩坑记录

#现象根因处理
1git checkout v0.5.6.post2 # 注释 报一串 pathspec did not matchcmd.exe 不像 bash/PowerShell 会吃掉 # 后面的内容,整行都被当成参数拆开了命令里不再带行内 # 注释;这个坑后面还踩了第二次,确认是我反复的疏漏
2fix_cuda_13_align.pyPermissionError,改的是 C:\Program Files\...\cuda.h在普通权限的 cmd 窗口里敲命令不会提权,必须从一开始就用提权方式打开新窗口Win → 输入 cmdCtrl+Shift+Enter 触发 UAC,新开管理员窗口
3补丁脚本提权后跑完,完全没有任何输出脚本本身没写任何 print,无论改成功、条件不满足跳过,表现都是静默退出——不能凭"没报错"就认定生效直接读脚本源码确认它在找什么字符串,再用 findstrcuda.h 里查证:确认 TENSOR_MAP_ALIGN 定义和两处 alignas/_Alignas 替换都已经写入(行号 3662~3671),文件修改时间也对得上,这才算闭环验证
4python -m build --no-isolation --wheelMissing 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-cacheflashinfer-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.allcloseTrue,确认编出来的 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 模型。


参考资料与延伸阅读

以下为本文涉及的官方仓库、文档与规格站,建议发布前点一遍确认可达:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值