Skip to content

Commit c7afa33

Browse files
Fix Windows GPU build: use Ninja generator + MSVC env setup
Root cause: pyproject-pypi.toml.j2 hardcoded '-G Visual Studio 17 2022' for ALL Windows builds. scikit-build-core passed this directly to CMake, overriding the CMAKE_GENERATOR env var. The Visual Studio generator uses MSBuild which spawns nvcc in subprocesses where pixi's conda-modified PATH lacks cl.exe. Fix: 1. Split cmake_windows_args() into cpu (Visual Studio) and gpu (Ninja) macros 2. Add setup_msvc_env.py helper that runs vcvarsall.bat inside the pixi env, capturing MSVC environment (PATH, INCLUDE, LIB) for the build subprocess 3. Pass CMAKE_CUDA_HOST_COMPILER via cmake.define to tell nvcc where cl.exe is 4. Find cl.exe path in CI workflow using vswhere.exe before the build step
1 parent 4898d68 commit c7afa33

File tree

4 files changed

+207
-9
lines changed

4 files changed

+207
-9
lines changed

.github/workflows/publish_to_pypi.yml

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,42 @@ jobs:
283283

284284
# Note: No separate CUDA toolkit install needed for Windows.
285285
# conda-forge's cuda-toolkit (via pixi) provides nvcc and CUDA headers/libs.
286-
# We use Ninja generator to avoid MSBuild CUDA .targets PATH issues.
286+
# We use Ninja generator (set in pyproject-pypi.toml.j2) to avoid MSBuild CUDA
287+
# .targets PATH issues. CMAKE_CUDA_HOST_COMPILER points nvcc to cl.exe directly.
288+
289+
- name: Find MSVC cl.exe path (Windows GPU)
290+
if: steps.should_run.outputs.run == 'true' && runner.os == 'Windows'
291+
id: msvc
292+
shell: bash
293+
run: |
294+
# Find vswhere.exe (installed by default on GitHub-hosted Windows runners)
295+
VSWHERE="/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe"
296+
if [ ! -f "$VSWHERE" ]; then
297+
echo "::error::vswhere.exe not found"
298+
exit 1
299+
fi
300+
301+
# Get the Visual Studio installation path
302+
VS_PATH=$("$VSWHERE" -latest -property installationPath)
303+
echo "Visual Studio path: $VS_PATH"
304+
305+
# Find cl.exe for the host architecture (x64)
306+
CL_PATH=$(find "$VS_PATH/VC/Tools/MSVC" -name "cl.exe" -path "*/Hostx64/x64/*" | head -1)
307+
if [ -z "$CL_PATH" ]; then
308+
echo "::error::cl.exe not found in Visual Studio installation"
309+
exit 1
310+
fi
311+
echo "cl.exe found: $CL_PATH"
312+
313+
# Convert to Windows path format for CMake
314+
CL_PATH_WIN=$(cygpath -w "$CL_PATH")
315+
echo "cl.exe (Windows path): $CL_PATH_WIN"
316+
echo "cl_path=$CL_PATH_WIN" >> $GITHUB_OUTPUT
317+
318+
# Also export the directory containing cl.exe for PATH
319+
CL_DIR=$(dirname "$CL_PATH")
320+
CL_DIR_WIN=$(cygpath -w "$CL_DIR")
321+
echo "cl_dir=$CL_DIR_WIN" >> $GITHUB_OUTPUT
287322
288323
- name: Set up pixi
289324
if: steps.should_run.outputs.run == 'true'
@@ -344,7 +379,7 @@ jobs:
344379
CMAKE_C_COMPILER_LAUNCHER: sccache
345380
CMAKE_CXX_COMPILER_LAUNCHER: sccache
346381
SCCACHE_GHA_ENABLED: "true"
347-
CMAKE_GENERATOR: Ninja
382+
CMAKE_CUDA_HOST_COMPILER: ${{ steps.msvc.outputs.cl_path }}
348383
run: pixi run -e ${{ matrix.pixi-environment }} wheel_build
349384

350385
- name: Repair GPU wheel (Linux)

pixi.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -589,11 +589,13 @@ pytorch-gpu = ">=2.8.0,<3"
589589

590590
[feature.gpu-wheel-build-win.target.win-64.tasks]
591591
# Windows GPU wheel build using pip wheel + delvewheel
592-
# Uses CUDA from conda-forge for GPU support
592+
# Uses Ninja generator with CUDA from conda-forge
593+
# setup_msvc_env.py injects MSVC environment (cl.exe, INCLUDE, LIB) into the pixi env
594+
# so that nvcc can find cl.exe for host compilation
593595
wheel_build = { cmd = """
594596
python scripts/generate_pyproject.py && \
595597
python -c "import os, shutil; os.makedirs('.tmp', exist_ok=True); shutil.copy('pyproject.toml', '.tmp/pyproject-backup.toml'); shutil.copy('pyproject-pypi-gpu.toml', 'pyproject.toml')" && \
596-
pip wheel . --no-deps --no-build-isolation --wheel-dir=dist && \
598+
python scripts/setup_msvc_env.py pip wheel . --no-deps --no-build-isolation --wheel-dir=dist && \
597599
python -c "import shutil; shutil.move('.tmp/pyproject-backup.toml', 'pyproject.toml')"
598600
""", env = { MOMENTUM_ENABLE_CUDA = "ON", TORCH_CUDA_ARCH_LIST = "5.0;6.0;6.1;7.0;7.5;8.0;8.6;8.9;9.0+PTX" }, description = "Build GPU wheel for Windows (uses pip wheel + delvewheel + CUDA from conda-forge)" }
599601

pyproject-pypi.toml.j2

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
"-DMOMENTUM_USE_SYSTEM_PYBIND11=ON",
3434
{%- endmacro %}
3535

36-
{#- Windows-specific args (maintains original order with -G after SHARED_LIBS) -#}
37-
{%- macro cmake_windows_args() %}
36+
{#- Windows CPU-specific args (Visual Studio generator for CPU builds) -#}
37+
{%- macro cmake_windows_cpu_args() %}
3838
"-DBUILD_SHARED_LIBS=OFF",
3939
"-G Visual Studio 17 2022",
4040
"-DMOMENTUM_BUILD_PYMOMENTUM=ON",
@@ -44,9 +44,23 @@
4444
"-DMOMENTUM_ENABLE_SIMD=OFF",
4545
"-DMOMENTUM_USE_SYSTEM_RERUN_CPP_SDK=ON",
4646
"-DMOMENTUM_USE_SYSTEM_PYBIND11=ON",
47-
{% if variant == "cpu" %}
4847
"-DCMAKE_CXX_SCAN_FOR_MODULES=OFF",
49-
{% endif %}
48+
{%- endmacro %}
49+
50+
{#- Windows GPU-specific args (Ninja generator to avoid MSBuild/CUDA PATH issues) -#}
51+
{#- Using Ninja avoids the Visual Studio CUDA .targets integration which spawns -#}
52+
{#- nvcc in subprocesses where pixi's conda-modified PATH lacks cl.exe. -#}
53+
{#- CMAKE_CUDA_HOST_COMPILER must be set to the full cl.exe path via env var. -#}
54+
{%- macro cmake_windows_gpu_args() %}
55+
"-DBUILD_SHARED_LIBS=OFF",
56+
"-G Ninja",
57+
"-DMOMENTUM_BUILD_PYMOMENTUM=ON",
58+
"-DMOMENTUM_BUILD_EXAMPLES=OFF",
59+
"-DMOMENTUM_BUILD_TESTING=OFF",
60+
"-DMOMENTUM_BUILD_RENDERER=OFF",
61+
"-DMOMENTUM_ENABLE_SIMD=OFF",
62+
"-DMOMENTUM_USE_SYSTEM_RERUN_CPP_SDK=ON",
63+
"-DMOMENTUM_USE_SYSTEM_PYBIND11=ON",
5064
{%- endmacro %}
5165

5266
{#- Common cibuildwheel before-all script for Linux (CPU builds) -#}
@@ -136,6 +150,7 @@ classifiers = [
136150
"License :: OSI Approved :: MIT License",
137151
{% if variant == "gpu" %}
138152
"Operating System :: POSIX :: Linux",
153+
"Operating System :: Microsoft :: Windows",
139154
{% else %}
140155
"Operating System :: POSIX :: Linux",
141156
"Operating System :: MacOS",
@@ -226,12 +241,24 @@ cmake.args = [
226241
cmake.define.CMAKE_PREFIX_PATH = {env = "CMAKE_PREFIX_PATH"}
227242
{% endif %}
228243

244+
{% if variant == "cpu" %}
229245
[[tool.scikit-build.overrides]]
230246
if.platform-system = "^win32"
231247
cmake.args = [
232-
{{ cmake_windows_args() }}
248+
{{ cmake_windows_cpu_args() }}
233249
]
234250
cmake.define.CMAKE_PREFIX_PATH = {env = "CMAKE_PREFIX_PATH"}
251+
{% endif %}
252+
253+
{% if variant == "gpu" %}
254+
[[tool.scikit-build.overrides]]
255+
if.platform-system = "^win32"
256+
cmake.args = [
257+
{{ cmake_windows_gpu_args() }}
258+
]
259+
cmake.define.CMAKE_PREFIX_PATH = {env = "CMAKE_PREFIX_PATH"}
260+
cmake.define.CMAKE_CUDA_HOST_COMPILER = {env = "CMAKE_CUDA_HOST_COMPILER", default = "cl.exe"}
261+
{% endif %}
235262

236263
[tool.setuptools_scm]
237264
# Automatically determine version from git tags

scripts/setup_msvc_env.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) Meta Platforms, Inc. and affiliates.
3+
#
4+
# This source code is licensed under the MIT license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""Set up MSVC environment variables for Ninja + CUDA builds on Windows.
8+
9+
When using the Ninja CMake generator with CUDA on Windows inside a pixi/conda
10+
environment, the PATH may not include MSVC tools (cl.exe, link.exe, etc.).
11+
This script finds vcvarsall.bat, runs it, captures the environment changes,
12+
and applies them to the current process before executing the given command.
13+
14+
Usage:
15+
python scripts/setup_msvc_env.py <command> [args...]
16+
17+
Example:
18+
python scripts/setup_msvc_env.py pip wheel . --no-deps --wheel-dir=dist
19+
"""
20+
21+
import os
22+
import subprocess
23+
import sys
24+
from pathlib import Path
25+
26+
27+
def find_vcvarsall() -> Path:
28+
"""Find vcvarsall.bat using vswhere.exe."""
29+
vswhere = Path(
30+
r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
31+
)
32+
if not vswhere.exists():
33+
raise FileNotFoundError(f"vswhere.exe not found at {vswhere}")
34+
35+
result = subprocess.run(
36+
[str(vswhere), "-latest", "-property", "installationPath"],
37+
capture_output=True,
38+
text=True,
39+
check=True,
40+
)
41+
vs_path = Path(result.stdout.strip())
42+
vcvarsall = vs_path / "VC" / "Auxiliary" / "Build" / "vcvarsall.bat"
43+
if not vcvarsall.exists():
44+
raise FileNotFoundError(f"vcvarsall.bat not found at {vcvarsall}")
45+
return vcvarsall
46+
47+
48+
def get_msvc_env(vcvarsall: Path, arch: str = "amd64") -> dict:
49+
"""Run vcvarsall.bat and capture the resulting environment variables."""
50+
# Run vcvarsall.bat and print the environment
51+
cmd = f'"{vcvarsall}" {arch} && set'
52+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, check=True)
53+
54+
env = {}
55+
for line in result.stdout.splitlines():
56+
if "=" in line:
57+
key, _, value = line.partition("=")
58+
env[key] = value
59+
return env
60+
61+
62+
def main():
63+
if len(sys.argv) < 2:
64+
print(f"Usage: {sys.argv[0]} <command> [args...]", file=sys.stderr)
65+
sys.exit(1)
66+
67+
if sys.platform != "win32":
68+
# On non-Windows, just exec the command directly
69+
os.execvp(sys.argv[1], sys.argv[1:])
70+
71+
print("=== Setting up MSVC environment for Ninja + CUDA build ===")
72+
73+
# Find vcvarsall.bat
74+
vcvarsall = find_vcvarsall()
75+
print(f"Found vcvarsall.bat: {vcvarsall}")
76+
77+
# Get MSVC environment
78+
msvc_env = get_msvc_env(vcvarsall)
79+
80+
# Merge MSVC environment into current environment
81+
# Key variables to merge: PATH, INCLUDE, LIB, LIBPATH, etc.
82+
merge_vars = [
83+
"PATH",
84+
"INCLUDE",
85+
"LIB",
86+
"LIBPATH",
87+
"WindowsSdkDir",
88+
"WindowsSdkVersion",
89+
"VCToolsInstallDir",
90+
"VCToolsVersion",
91+
"VSCMD_ARG_TGT_ARCH",
92+
]
93+
94+
for var in merge_vars:
95+
if var in msvc_env:
96+
if var == "PATH":
97+
# Prepend MSVC paths to existing PATH (preserve conda paths too)
98+
current_path = os.environ.get("PATH", "")
99+
msvc_path = msvc_env["PATH"]
100+
# Extract paths from MSVC env that aren't already in current PATH
101+
current_paths = set(current_path.lower().split(os.pathsep))
102+
new_paths = [
103+
p
104+
for p in msvc_path.split(os.pathsep)
105+
if p.lower() not in current_paths
106+
]
107+
if new_paths:
108+
os.environ["PATH"] = (
109+
os.pathsep.join(new_paths) + os.pathsep + current_path
110+
)
111+
print(f"Added {len(new_paths)} MSVC paths to PATH")
112+
else:
113+
os.environ[var] = msvc_env[var]
114+
print(f"Set {var}")
115+
116+
# Verify cl.exe is accessible
117+
try:
118+
result = subprocess.run(
119+
["where", "cl.exe"], capture_output=True, text=True, check=True
120+
)
121+
print(f"cl.exe found: {result.stdout.strip().splitlines()[0]}")
122+
except (subprocess.CalledProcessError, FileNotFoundError):
123+
print("WARNING: cl.exe not found on PATH after MSVC setup", file=sys.stderr)
124+
125+
print("=== MSVC environment ready ===")
126+
print(f"Executing: {' '.join(sys.argv[1:])}")
127+
128+
# Execute the requested command
129+
result = subprocess.run(sys.argv[1:])
130+
sys.exit(result.returncode)
131+
132+
133+
if __name__ == "__main__":
134+
main()

0 commit comments

Comments
 (0)