witwin 0.3.0__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,288 @@
1
+ """JIT build of the native CUDA mesh-SDF extension.
2
+
3
+ Self-contained ``torch.utils.cpp_extension.load`` wrapper with the Windows
4
+ MSVC + CUDA toolchain discovery the platform needs. ``witwin.core`` must not
5
+ depend on ``witwin.maxwell``, so the toolchain helpers are duplicated here
6
+ rather than imported from the maxwell FDTD CUDA build.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import shutil
13
+ import subprocess
14
+ import sys
15
+ import tempfile
16
+ from pathlib import Path
17
+
18
+ import torch
19
+ from torch.utils.cpp_extension import load
20
+
21
+
22
+ EXTENSION_NAME = "witwin_core_mesh_sdf_cuda"
23
+
24
+
25
+ def _candidate_vcvars64_paths() -> list[Path]:
26
+ paths: list[Path] = []
27
+ program_files_x86 = os.environ.get("ProgramFiles(x86)")
28
+ if program_files_x86:
29
+ vswhere = Path(program_files_x86) / "Microsoft Visual Studio" / "Installer" / "vswhere.exe"
30
+ if vswhere.exists():
31
+ try:
32
+ install_root = subprocess.check_output(
33
+ [
34
+ str(vswhere),
35
+ "-latest",
36
+ "-products",
37
+ "*",
38
+ "-requires",
39
+ "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
40
+ "-property",
41
+ "installationPath",
42
+ ],
43
+ text=True,
44
+ encoding="mbcs",
45
+ errors="replace",
46
+ ).strip()
47
+ except (OSError, subprocess.CalledProcessError):
48
+ install_root = ""
49
+ if install_root:
50
+ paths.append(Path(install_root) / "VC" / "Auxiliary" / "Build" / "vcvars64.bat")
51
+
52
+ root = os.environ.get("VSINSTALLDIR")
53
+ if root:
54
+ paths.append(Path(root) / "VC" / "Auxiliary" / "Build" / "vcvars64.bat")
55
+
56
+ program_files = os.environ.get("ProgramFiles", r"C:\Program Files")
57
+ for edition in ("Community", "Professional", "Enterprise", "BuildTools"):
58
+ paths.append(
59
+ Path(program_files)
60
+ / "Microsoft Visual Studio"
61
+ / "2022"
62
+ / edition
63
+ / "VC"
64
+ / "Auxiliary"
65
+ / "Build"
66
+ / "vcvars64.bat"
67
+ )
68
+
69
+ seen: set[Path] = set()
70
+ unique_paths: list[Path] = []
71
+ for path in paths:
72
+ resolved = path.resolve() if path.exists() else path
73
+ if resolved in seen:
74
+ continue
75
+ seen.add(resolved)
76
+ unique_paths.append(path)
77
+ return unique_paths
78
+
79
+
80
+ def _load_vcvars64_environment() -> bool:
81
+ for vcvars in _candidate_vcvars64_paths():
82
+ if not vcvars.exists():
83
+ continue
84
+ fd, probe_name = tempfile.mkstemp(prefix="witwin_core_mesh_sdf_vcvars_probe_", suffix=".cmd")
85
+ os.close(fd)
86
+ probe = Path(probe_name)
87
+ probe.write_text(f'@echo off\ncall "{vcvars}" >nul\nset\n', encoding="utf-8")
88
+ try:
89
+ output = subprocess.check_output(
90
+ ["cmd.exe", "/d", "/c", str(probe)],
91
+ text=True,
92
+ encoding="mbcs",
93
+ errors="replace",
94
+ )
95
+ except (OSError, subprocess.CalledProcessError):
96
+ continue
97
+ finally:
98
+ try:
99
+ probe.unlink()
100
+ except OSError:
101
+ pass
102
+ updates: dict[str, str] = {}
103
+ for line in output.splitlines():
104
+ if "=" not in line:
105
+ continue
106
+ key, value = line.split("=", 1)
107
+ updates[key] = value
108
+ for key, value in updates.items():
109
+ os.environ[key] = value
110
+ path_value = updates.get("PATH", updates.get("Path"))
111
+ if path_value is not None:
112
+ os.environ["PATH"] = path_value
113
+ os.environ["Path"] = path_value
114
+ return True
115
+ return False
116
+
117
+
118
+ def _ensure_windows_build_tools_on_path() -> None:
119
+ if os.name != "nt":
120
+ return
121
+ if shutil.which("cl") is None:
122
+ _load_vcvars64_environment()
123
+ if shutil.which("cl") is None:
124
+ return
125
+
126
+ current_path = os.environ.get("PATH") or os.environ.get("Path") or ""
127
+ prefixes: list[str] = []
128
+ vc_tools = os.environ.get("VCToolsInstallDir")
129
+ if vc_tools:
130
+ prefixes.append(str(Path(vc_tools) / "bin" / "Hostx64" / "x64"))
131
+ vs_install = os.environ.get("VSINSTALLDIR")
132
+ if vs_install:
133
+ prefixes.append(
134
+ str(Path(vs_install) / "Common7" / "IDE" / "CommonExtensions" / "Microsoft" / "CMake" / "Ninja")
135
+ )
136
+ if not prefixes:
137
+ return
138
+ merged_path = os.pathsep.join([*prefixes, current_path])
139
+ os.environ["PATH"] = merged_path
140
+ os.environ["Path"] = merged_path
141
+
142
+
143
+ def _ensure_cuda_home_from_nvcc() -> None:
144
+ if os.environ.get("CUDA_HOME") or os.environ.get("CUDA_PATH"):
145
+ return
146
+ nvcc = shutil.which("nvcc")
147
+ if nvcc is None:
148
+ return
149
+ cuda_home = Path(nvcc).resolve().parents[1]
150
+ os.environ["CUDA_HOME"] = str(cuda_home)
151
+ if os.name == "nt":
152
+ os.environ["CUDA_PATH"] = str(cuda_home)
153
+
154
+
155
+ def _conda_torch_ldflags() -> list[str]:
156
+ # Conda-distributed torch keeps c10.lib/torch_cuda.lib in <env>\Library\lib
157
+ # instead of site-packages\torch\lib, which cpp_extension does not search.
158
+ if os.name != "nt":
159
+ return []
160
+ library_lib = Path(sys.prefix) / "Library" / "lib"
161
+ if (library_lib / "c10.lib").exists():
162
+ return [f"/LIBPATH:{library_lib}"]
163
+ return []
164
+
165
+
166
+ def source_root() -> Path:
167
+ return Path(__file__).resolve().parent
168
+
169
+
170
+ def prebuilt_root() -> Path:
171
+ return source_root() / "prebuilt"
172
+
173
+
174
+ def extension_suffix() -> str:
175
+ return ".pyd" if os.name == "nt" else ".so"
176
+
177
+
178
+ def prebuilt_extension_path() -> Path:
179
+ return prebuilt_root() / f"{EXTENSION_NAME}{extension_suffix()}"
180
+
181
+
182
+ def extension_sources() -> list[Path]:
183
+ root = source_root()
184
+ return [root / "extension.cpp", root / "mesh_sdf_kernels.cu"]
185
+
186
+
187
+ def _cuda_gencode_flags() -> list[str]:
188
+ """Translate the release architecture list directly into nvcc flags."""
189
+ value = os.environ.get("WITWIN_CUDA_GENCODE_ARCHES")
190
+ if not value:
191
+ return []
192
+ flags: list[str] = []
193
+ for entry in value.split(";"):
194
+ entry = entry.strip()
195
+ if not entry:
196
+ continue
197
+ include_ptx = entry.endswith("+PTX")
198
+ number = entry.removesuffix("+PTX").replace(".", "")
199
+ if not number.isdigit():
200
+ raise ValueError(f"Invalid CUDA architecture {entry!r} in WITWIN_CUDA_GENCODE_ARCHES.")
201
+ flags.append(f"-gencode=arch=compute_{number},code=sm_{number}")
202
+ if include_ptx:
203
+ flags.append(f"-gencode=arch=compute_{number},code=compute_{number}")
204
+ return flags
205
+
206
+
207
+ class _StableOpsModule:
208
+ """Attribute-compatible view of the Stable ABI dispatcher operators."""
209
+
210
+ def __init__(self, library_path: Path) -> None:
211
+ self.__file__ = str(library_path)
212
+
213
+ def is_available(self) -> bool:
214
+ return bool(torch.cuda.is_available())
215
+
216
+ def __getattr__(self, name: str):
217
+ return getattr(torch.ops.witwin_core_mesh_sdf_cuda, name)
218
+
219
+
220
+ def _load_extension_file(library_path: Path) -> _StableOpsModule:
221
+ torch.ops.load_library(str(library_path))
222
+ if not hasattr(torch.ops.witwin_core_mesh_sdf_cuda, "query_mesh_unsigned_distance"):
223
+ raise ImportError(f"{library_path} does not register the Stable ABI mesh SDF operators.")
224
+ return _StableOpsModule(library_path)
225
+
226
+
227
+ def _load_packaged_prebuilt_extension():
228
+ # Wheels ship a Stable ABI library in ``prebuilt/`` so installs never invoke
229
+ # nvcc/MSVC and the same binary loads across supported Python/Torch versions.
230
+ module_path = prebuilt_extension_path()
231
+ if not module_path.exists():
232
+ return None
233
+ return _load_extension_file(module_path)
234
+
235
+
236
+ def _load_prebuilt_extension(build_directory: Path):
237
+ # Load the already-built module directly, bypassing the build toolchain.
238
+ module_path = build_directory / f"{EXTENSION_NAME}{extension_suffix()}"
239
+ if not module_path.exists():
240
+ raise FileNotFoundError(
241
+ f"WITWIN_CORE_MESH_SDF_CUDA_PREBUILT=1 but {module_path} does not exist; "
242
+ "run a normal build first."
243
+ )
244
+ return _load_extension_file(module_path)
245
+
246
+
247
+ def _jit_build(build_directory: Path, *, verbose: bool):
248
+ _ensure_windows_build_tools_on_path()
249
+ _ensure_cuda_home_from_nvcc()
250
+ build_directory.mkdir(parents=True, exist_ok=True)
251
+ library_path = load(
252
+ name=EXTENSION_NAME,
253
+ sources=[str(path) for path in extension_sources()],
254
+ build_directory=str(build_directory),
255
+ extra_cflags=(
256
+ ["/O2", "/DTORCH_TARGET_VERSION=0x020a000000000000"]
257
+ if os.name == "nt"
258
+ else ["-O3", "-DTORCH_TARGET_VERSION=0x020a000000000000"]
259
+ ),
260
+ extra_cuda_cflags=[
261
+ "-O3",
262
+ "-DTORCH_TARGET_VERSION=0x020a000000000000",
263
+ "-DUSE_CUDA",
264
+ *_cuda_gencode_flags(),
265
+ ],
266
+ extra_ldflags=_conda_torch_ldflags(),
267
+ is_python_module=False,
268
+ verbose=verbose,
269
+ )
270
+ return _StableOpsModule(Path(library_path))
271
+
272
+
273
+ def build_extension(*, verbose: bool = False):
274
+ default_build_directory = Path(tempfile.gettempdir()) / EXTENSION_NAME / "stable_abi_v1"
275
+ build_directory = Path(os.environ.get("WITWIN_CORE_MESH_SDF_CUDA_BUILD_DIR", default_build_directory))
276
+
277
+ if os.environ.get("WITWIN_CORE_MESH_SDF_CUDA_SKIP_PREBUILT") != "1":
278
+ try:
279
+ module = _load_packaged_prebuilt_extension()
280
+ except Exception: # noqa: BLE001 - stale/ABI-mismatched prebuilt, rebuild instead
281
+ module = None
282
+ if module is not None:
283
+ return module
284
+
285
+ if os.environ.get("WITWIN_CORE_MESH_SDF_CUDA_PREBUILT") == "1":
286
+ return _load_prebuilt_extension(build_directory)
287
+
288
+ return _jit_build(build_directory, verbose=verbose)
@@ -0,0 +1,29 @@
1
+ #include <torch/csrc/stable/library.h>
2
+
3
+ STABLE_TORCH_LIBRARY(witwin_core_mesh_sdf_cuda, m) {
4
+ m.def(
5
+ "query_mesh_unsigned_distance(Tensor triangles, Tensor points, "
6
+ "Tensor(a!) unsigned_distance, Tensor(b!) closest_triangle_index) -> ()");
7
+ m.def(
8
+ "query_mesh_distance_and_winding(Tensor triangles, Tensor points, "
9
+ "Tensor(a!) unsigned_distance, Tensor(b!) winding_angle, "
10
+ "Tensor(c!) closest_triangle_index) -> ()");
11
+ m.def(
12
+ "query_mesh_unsigned_distance_bvh(Tensor triangles, Tensor points, "
13
+ "Tensor node_bbox_min, Tensor node_bbox_max, Tensor node_left, Tensor node_right, "
14
+ "Tensor node_start, Tensor node_count, Tensor triangle_indices, "
15
+ "Tensor(a!) unsigned_distance, Tensor(b!) closest_triangle_index) -> ()");
16
+ m.def(
17
+ "query_mesh_parity_sign_bvh(Tensor triangles, Tensor points, "
18
+ "Tensor node_bbox_min, Tensor node_bbox_max, Tensor node_left, Tensor node_right, "
19
+ "Tensor node_start, Tensor node_count, Tensor triangle_indices, float jitter_scale, "
20
+ "Tensor(a!) inside) -> ()");
21
+ m.def(
22
+ "backward_mesh_unsigned_distance(Tensor triangles, Tensor points, "
23
+ "Tensor closest_triangle_index, Tensor grad_unsigned_distance, "
24
+ "Tensor(a!) grad_triangles, Tensor(b!) grad_points) -> ()");
25
+ m.def(
26
+ "backward_mesh_distance_and_winding(Tensor triangles, Tensor points, "
27
+ "Tensor closest_triangle_index, Tensor grad_unsigned_distance, Tensor grad_winding_angle, "
28
+ "Tensor(a!) grad_triangles, Tensor(b!) grad_points) -> ()");
29
+ }