Windows 原生编译 SGLang(2/8):三铁证判定可行 + --no-deps 外科手术式安装
Windows 原生编译 SGLang(2/8):三铁证判定可行 + --no-deps 外科手术式安装
上一篇(第 0 篇)我们用 EPGF 架构把 Unlimited-OCR 部署到了能跑通推理的状态,并停在了一个岔路口:高性能后端 sglang 在 Windows 上"装不了",主流建议是退回 Transformers 或转投 WSL2 / Docker。
本篇先把结论摆在最前面——我们没有退,而是在原生 Windows 上从源码把它编出来了——然后讲清楚这件事到底难在哪、我们怎么组织这场攻关,以及为什么"官方没有 Windows 包"和"Windows 编译不出来"根本是两回事。
一、先看结果:铁证三连
技术复盘最怕"我觉得我成功了"。所以先把三条最硬的证据摆出来,每一条都可独立复验。
证据一:产出了 Windows 平台的win_amd64wheel。
K:\PythonProjects5\Unlimited-OCR\sglang\sgl-kernel\dist\sglang_kernel-0.4.3-cp310-abi3-win_amd64.whl注意结尾的win_amd64——这正是官方"没有提供"的那个平台标签。abi3表示它使用 Python 稳定 ABI(Limited API),一次编译可跨多个 Python 版本通用,cp310是其声明的最低兼容版本,实测在 Python 3.12 上安装、导入均正常。
证据二:编译出来的 C++/CUDA 扩展能被 Python 真正加载。
python -c "import sgl_kernel; print('Kernel OK')"Kernel OK这一条比"wheel 文件存在"更关键:编译成功和运行可用是两件事。import sgl_kernel通过,说明我们逐行抠出来的那个二进制扩展,不只是"编出来了",而是"能加载进来"。
证据三:它正确填进了 sglang 主包的依赖链。
pip show sglang sglang-kernel关键输出:sglang-kernel 0.4.3的Required-by: sglang,而sglang 0.0.0.dev11416+g92e8bb79e主包的Requires:列表里明确列着sglang-kernel。也就是说,我们编出来的子包不是一个孤立的产物,而是严丝合缝地补进了主包本来就需要、但 Windows 上一直缺失的那个位置。
python -c "import sglang; print('sglang', sglang.__version__)"sglang 0.0.0.dev11416+g92e8bb79e三条证据合起来,坐实一件事:sglang 高性能后端,在原生 Windows 上,跑起来了。
二、“官方没有 Windows 包” ≠ “Windows 编译不出来”
这是整个系列最想先纠正的一个认知。
当你在 Windows 上尝试安装 sglang(无论是项目自带的主包 wheel,还是从公网拉官方包)失败时,真实发生的是这样一条链:
- sglang 主包本身是纯 Python 的
py3-none-any,看起来跟平台无关; - 但它在依赖里钉死了一个 C++/CUDA 扩展子包
sglang-kernel; - 官方只为这个子包发布了 Linux 预编译包(
manylinux2014_x86_64),没有发布win_amd64; - 于是 pip 解析依赖时,在 Windows 上找不到任何能装的
sglang-kernel,整条安装链断在这里。
主流结论由此而来:“Windows 装不了 sglang。” 这个结论的前半句是事实(直接装确实会失败),但后半句的引申是错的——它把"官方没有发布 Windows 预编译包"偷换成了"Windows 无法运行 sglang",再进一步引申成"只能退 WSL2 / Docker"。
真相是:sglang-kernel是开源的,源码就在 sglang 仓库的sgl-kernel/目录下。官方没编 Windows 版,不代表它编不出来,只代表"没有人(在官方流程里)去编它"。缺的不是可能性,而是一份win_amd64的 wheel——那就自己把它编出来。
这就是本系列的全部主张:不退 WSL、不上 Docker,在原生 Windows + MSVC + CUDA 工具链下,从源码编译出缺失的sglang-kernel,再让主包正常用上它。
三、关键转折:为什么必须--no-deps
把子包编出来之后,还有一个容易被忽略、但极具代表性的转折,值得在总览里先讲清楚——因为它精确地解释了"为什么常规安装必然失败,而我们的路线能成"。
先说清楚这里的主包从哪来:Unlimited-OCR 项目仓库自带了一个wheel/目录,里面就放着 sglang 主包的 wheel(sglang-0.0.0.dev11416+g92e8bb79e-py3-none-any.whl)。我们要装的就是这个项目自带的主包,而不是从公网 PyPI 拉官方包。但光装它不算完整——它依赖sglang-kernel子包,而这个子包正是 Windows 上一直缺、需要我们自己编出来的那个。
于是正确的安装顺序是:先装我们编好的sglang-kernel 0.4.3(win_amd64),再装项目自带的主包 wheel。可即便子包已经装好,直接装主包仍然会报错:
ERROR: Could not find a version that satisfies the requirement sglang-kernel==0.4.1 (from sglang) ERROR: No matching distribution found for sglang-kernel==0.4.1原因是:项目自带的这个主包 wheel,其元数据里把依赖精确钉死在了sglang-kernel==0.4.1,而我们从当前源码编出来的是0.4.3。pip 按这个元数据严格解析时,会去公网找0.4.1的win_amd64包——它当然不存在,于是失败。
这恰恰印证了"按它声明的依赖正常装必然失败"这个判断本身没错。但结论不该是"装不了",而该是:绕过这次依赖解析,用已经在本地、已经装好的子包去满足它。
pip install K:\...\wheel\sglang-0.0.0.dev11416+g92e8bb79e-py3-none-any.whl --no-deps--no-deps告诉 pip:“别去重新解析依赖,我自己已经把该装的都装好了。” 主包随即安装成功。这一步是整条路线的"临门一脚":前面所有的源码攻关,都是为了让这一步有底气说出"我自己装好了"。
一个工程提醒:
0.4.1与0.4.3的版本号差异,源于项目自带的主包 wheel 元数据里钉的是0.4.1,而当前源码 tag 编出来的子包是0.4.3,两者对不上。在自己复现时,以实际编出来的子包版本为准,用--no-deps装主包即可,不必纠结这个号差。
完整依赖安装顺序:把
--no-deps说得底气十足的前提,是真的把所有依赖都提前装好。这场攻关实际上有四个包需要严格按序安装:
- flashinfer-windows(
flashinfer_python-0.6.11.post3-py3-none-any.whl):sglang 的 attention 计算后端依赖,Windows 官方不提供,需使用单独维护的 Windows 兼容 fork 手动编译出 wheel。py3-none-any后缀意味着它的 CUDA kernel 代码是 JIT 模式(运行时即时编译),wheel 本身是纯 Python 外壳——但它必须先于 sglang 主包安装,否则 sglang 运行时找不到这个后端。- sgl-kernel(
sglang_kernel-0.4.3-cp310-abi3-win_amd64.whl):本系列整场攻关的产物,紧接着装。- kernels==0.11.7:sglang 依赖链里一个未被自动拉取的缺失包。PyPI 上有预编译版本,
uv pip install --link-mode=copy kernels==0.11.7装它只需几秒钟。- sglang 主包(
--no-deps):前三步都就位后,用--no-deps跳过依赖解析,让已装好的包顶上。步骤①②③必须在步骤④之前完成。flashinfer-windows 的编译过程本身也是一段独立的攻关(另见仓库
flashinfer-windows/目录);kernels 包虽然装起来一瞬间,但它是 sglang 运行时不可缺少的一环——少了它,主包导入会在运行时才报缺依赖。
四、这场攻关到底难在哪:全局地图
把一个为 Linux + GCC 精心打磨的 CUDA 扩展,搬到 Windows + MSVC 上原生编译,难点不是某一个 bug,而是成体系的、跨越多个层次的不兼容。整个系列后续四篇,正是按这张地图的层次来组织的:
前置·FlashInfer 依赖编译(第 2 篇)。在碰 sgl-kernel 之前,还有一个独立的前置关卡:sglang 的 attention backendFlashInfer官方不支持 Windows,必须手动编译SystemPanic/flashinfer-windowsfork 产出 wheel。这一关涉及 CUDA 13.x 头文件的对齐补丁(需管理员权限)、vswhere多版本 VS 选错、--force-reinstall悄悄把 CUDA torch 换成 CPU-only 版等独立踩坑。
第一层·环境(第 3 篇)。还没碰到一行源码,光是把"正确的编译器 + 正确的 CUDA + 正确的生成器"凑齐并稳定下来,就有一连串坑:机器上同时装着 VS2022 和 VS18 预览版、五个版本的 CUDA 并存、uv 创建的 venv 会重置 PATH、CMake 的生成器选择会被缓存写死……这些与 sglang 本身无关,却是所有人在 Windows 上编 CUDA 扩展都会撞上的共性问题。
第二层·源码方言(第 4 篇)。数十个.cu/.cuh/.h源文件里,散布着大量"GCC/Clang 能过、MSVC 不认"的写法:编译器内建函数(__builtin_clz)、GNU 内联汇编拼法(__asm__/__volatile__)、GCC 属性(__attribute__((always_inline/weak)))、类型简写(uint/ushort)、非标准数学宏(M_LOG2E/M_LN2),以及 MSVC 预处理器的严格性(宏参数里的裸#指令、__VA_ARGS__的跨层展开)。
第三层·MSVC 的语义严格性(第 5 篇)。比方言更深的一类:MSVC 在常量求值、重载决议上比 GCC 严格得多。std::numeric_limits<>::max()在设备端不被当作编译期常量、const被误当constexpr、嵌套 lambda 里的常量推导链断裂、half/bf16 构造函数的重载歧义、乃至 Windows SDK 一个古董宏#define small char对正常变量名的污染——直到 MSVC 自己在某个 CUTLASS 模板上内部崩溃(C1001)。
第四层·架构裁剪与链接收尾(第 6 篇)。这一层是贯穿全程的主线决策,而非逐个 bug:sgl-kernel为 Hopper/Blackwell(SM90/SM100)等数据中心级架构硬编码了大量代码路径,而我们的目标是消费级的 RTX 3090(sm_86)——用不上的,就一律不编。裁剪干净之后,还要处理"算子注册与实现不同步"导致的链接期未解析符号(LNK2019),才能走完最后的链接、产出 wheel。
这四层叠加,构成了一条此前几乎没有公开成功先例的路径。它确实很长——但每一层的每一个坎,都有明确的成因和可复现的解法。这正是后续各篇要逐层拆解的内容。
五、本系列的工程底色:不是运气,是方法
提前点明一件事,因为它贯穿后面所有篇章:这场攻关能走通,靠的不是"运气好碰对了",而是一套刻意维持的工程纪律——
- 全程台账驱动:每一处"已验证生效"的修改都即时记录(改了什么、为什么、怎么验证的),而不是事后凭记忆回溯;
- 幂等补丁脚本:每一处源码修改都写成可重复运行、带"已打补丁/未找到"双重保护的脚本,而不是手改——这样在反复清理重建构建目录时,补丁能稳定复现;
- 改完即验证:每打一个补丁,紧跟着用实际文件内容核实一遍,绝不仅凭"脚本报告成功"就相信(这条规矩是吃过亏才立下的,后面会讲到那次教训)。
这套方法本身,会在系列最后一篇(机动篇)单独复盘。现在你只需要知道:后面看到的每一个解法,背后都有一条"发现 → 定位 → 修改 → 验证"的完整证据链,而不是试错试出来的玄学。
六、小结与下一篇
本篇把整个系列的"为什么"和"做成了没有"立住了:
- sglang 高性能后端在原生 Windows 上确实编译并运行成功,有可复验的三条铁证;
- "官方没有 Windows 包"是事实,但"Windows 编译不出来 / 只能 WSL2"是站不住的引申;
- 真正的解法是从源码编出缺失的
win_amd64子包,再用--no-deps让主包用上它; - 整场攻关按"环境 → 源码方言 → MSVC 语义 → 架构裁剪与链接"四层展开,是后续四篇的主线。
从下一篇开始,我们正式进入实战。第 2 篇先处理 sgl-kernel 的前置依赖:手动编译 FlashInfer 的 Windows 兼容 fork——包括 CUDA 13.x 对齐补丁、多版本 VS 探测陷阱、--force-reinstall悄悄换掉 CUDA torch 等独立踩坑。第 3 篇再啃最容易被低估、却让无数人卡在第一步的"环境关"——为什么明明装了 VS2022,CMake 却报"找不到 CUDA 工具集";为什么vcvarsall明明显示成功,cl.exe却凭空消失;以及那个反复让人栽跟头的"生成器缓存"。
系列导航(全 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 参考(推理调用)
