1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
use std::env;
use std::path::{Path, PathBuf};
/// Whether the C++ compiler resolves `#include <header>`, given the
/// same include directory the real compile will get.
///
/// Kept in sync by hand with flodl-cli's `util/requirements.rs`
/// (`header_reachable`); build scripts cannot depend on that crate. A
/// missing or unusable compiler answers `false`, which leaves the header
/// reported as missing — the honest fallback, since a box with no C++
/// compiler cannot build this crate anyway and the message names that.
fn header_reachable(header: &str, root_include: &Path) -> bool {
use std::io::Write;
use std::process::{Command, Stdio};
let cxx = std::env::var("CXX").unwrap_or_else(|_| "c++".to_string());
let Ok(mut child) = Command::new(&cxx)
.arg("-I")
.arg(root_include)
// Discarded through `Stdio::null()`, not `-o /dev/null`: that is
// not a path everywhere, and where it is not, every header comes
// back missing.
.args(["-E", "-x", "c++", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
else {
return false;
};
if let Some(stdin) = child.stdin.as_mut()
&& stdin
.write_all(format!("#include <{header}>\n").as_bytes())
.is_err()
{
return false;
}
child.wait().map(|s| s.success()).unwrap_or(false)
}
fn main() {
// docs.rs builds without libtorch — skip C++ compilation entirely.
// cargo doc does not link, so unresolved extern symbols are fine.
if env::var("DOCS_RS").is_ok() {
return;
}
// --- vendor selection -------------------------------------------
//
// `gpu` is the vendor-neutral gate every GPU code path uses; `cuda`
// and `rocm` are the selectors that decide what gets linked. Cargo
// features are additive and cannot be made exclusive, so the
// combinations that make no sense are rejected here.
//
// Both checks sit AFTER the DOCS_RS early return above, so an
// `--all-features` documentation build (which necessarily enables
// both vendors) is unaffected.
let want_cuda = cfg!(feature = "cuda");
let want_rocm = cfg!(feature = "rocm");
if want_cuda && want_rocm {
eprintln!(
"\nflodl-sys: features `cuda` and `rocm` are mutually exclusive.\n\
They select which libtorch backend to link against, and a build\n\
can only link one. Enable exactly one.\n"
);
std::process::exit(1);
}
if cfg!(feature = "gpu") && !want_cuda && !want_rocm {
eprintln!(
"\nflodl-sys: feature `gpu` is enabled with no vendor selected.\n\
`gpu` marks the vendor-neutral GPU code paths; it does not say\n\
what to link. Enable `cuda` or `rocm` instead -- both imply it.\n"
);
std::process::exit(1);
}
let libtorch = env::var("LIBTORCH_PATH").unwrap_or_else(|_| "/usr/local/libtorch".to_string());
let libtorch = PathBuf::from(&libtorch);
// Preflight: confirm libtorch is actually present before cc::Build
// launches a multi-minute compile that would otherwise fail with a
// cryptic `fatal error: torch/torch.h: No such file or directory`
// deep in the C++ output. Pointing users at `fdl setup` is the
// canonical fix; the manual override is documented for users who
// are bypassing fdl on purpose.
// Match the same header file cc::Build's include path resolves
// (`include/torch/csrc/api/include`); presence here is the
// canonical "libtorch is installed" sentinel for both the
// pre-built and source-built variants.
let torch_header = libtorch.join("include/torch/csrc/api/include/torch/torch.h");
if !torch_header.exists() {
eprintln!(
"\nflodl-sys: libtorch not found at `{}`\n\
(expected `{}` to exist).\n\n\
Recommended fix: install `flodl-cli` and run `fdl setup` from\n\
your project root. It auto-detects your hardware, downloads or\n\
builds the matching libtorch variant, and points LIBTORCH_PATH\n\
at it for you.\n\n\
Manual override: set LIBTORCH_PATH=/path/to/libtorch where the\n\
directory contains both `include/torch/csrc/api/include/torch/torch.h`\n\
and `lib/libtorch.so` (or the platform equivalent).\n",
libtorch.display(),
torch_header.display(),
);
std::process::exit(1);
}
// Unity build: shim.cpp #includes the topic-focused ops_*.cpp files so the
// C++ compiler parses torch/torch.h exactly once. Splitting into separate
// TUs would multiply torch.h parse cost (~17s/TU) since cc::Build rebuilds
// every TU on any change.
//
// Files listed for cargo:rerun-if-changed below; only shim.cpp is compiled.
let shim_includes = [
"shim.h",
"helpers.h",
"ops_tensor.cpp",
"ops_nn.cpp",
"ops_math_ext.cpp",
"ops_training.cpp",
"ops_cuda.cpp",
];
// --- ROCm: proceed only against a libtorch that actually is one ---
//
// The refusal is conditional rather than absolute. Compiling and
// linking the shim needs headers and .so files, NOT a GPU -- only
// *running* needs silicon -- so a ROCm container with a ROCm
// libtorch mounted can legitimately build this, and that is how the
// remaining unknown gets settled. What cannot work is `--features
// rocm` against a CUDA (or absent) libtorch, and that is the case
// worth catching early with a message instead of a missing-header
// error deep in the C++ compile.
if want_rocm && !libtorch.join("lib/libtorch_hip.so").exists() {
eprintln!(
"\nflodl-sys: `--features rocm` needs a ROCm libtorch, but `{}`\n\
has no `lib/libtorch_hip.so` (so it is a CUDA or CPU build).\n\n\
Point LIBTORCH_PATH at a ROCm variant -- `fdl libtorch download\n\
--rocm 7.0` fetches one -- or build with `--features cuda`.\n",
libtorch.display(),
);
std::process::exit(1);
}
// Where each vendor's toolkit lives. Read once: both the guard below
// and the include setup further down need them. ROCm resolution
// mirrors flodl-hw's (`$ROCM_PATH` / `$HIP_PATH` / `$HSA_PATH`, then
// the convention) — build.rs cannot depend on that crate, so the
// order is kept in sync by hand.
let rocm_path = ["ROCM_PATH", "HIP_PATH", "HSA_PATH"]
.iter()
.filter_map(|k| env::var(k).ok())
.find(|v| !v.trim().is_empty())
.unwrap_or_else(|| "/opt/rocm".to_string());
let cuda_home = env::var("CUDA_HOME").unwrap_or_else(|_| "/usr/local/cuda".to_string());
// Vendor toolkit headers, as (header relative to an include dir,
// package that owns it). Covers the whole include chain rather than
// the headers the shim names directly: torch's vendor trees pull in
// more, so checking only the direct includes passes while the
// compile still fails.
//
// Regenerate after a libtorch bump with `c++ -M` over shim.cpp using
// the same -I/-D flags set below. Use -M and not -MM: -MM omits
// system headers, which drops nccl.h (it lives in /usr/include, not
// under $CUDA_HOME).
//
// Kept in sync by hand with flodl-cli's util/requirements.rs;
// build.rs cannot depend on that crate.
const ROCM_HEADERS: &[(&str, &str)] = &[
("hip/hip_runtime.h", "hip-dev"),
("rccl/rccl.h", "rccl-dev"),
("hipblas/hipblas.h", "hipblas-dev"),
("hipblas-common/hipblas-common.h", "hipblas-common-dev"),
("hipblaslt/hipblaslt.h", "hipblaslt-dev"),
("hipsolver/hipsolver.h", "hipsolver-dev"),
("hipsparse/hipsparse.h", "hipsparse-dev"),
];
const CUDA_HEADERS: &[(&str, &str)] = &[
("cuda_runtime.h", "cuda-cudart-dev-<M>-<m>"),
("crt/host_config.h", "cuda-crt-<M>-<m>"),
("cublas_v2.h", "libcublas-dev-<M>-<m>"),
("cusolverDn.h", "libcusolver-dev-<M>-<m>"),
("cusparse.h", "libcusparse-dev-<M>-<m>"),
("nccl.h", "libnccl-dev"),
];
let toolkit = if want_rocm {
Some(("rocm", &rocm_path, "ROCM_PATH", "/opt/rocm", ROCM_HEADERS))
} else if want_cuda {
Some((
"cuda",
&cuda_home,
"CUDA_HOME",
"/usr/local/cuda",
CUDA_HEADERS,
))
} else {
None
};
if let Some((feature, root, root_env, root_default, headers)) = toolkit {
// Present under the toolkit root OR a default system include
// dir -- `nccl.h` ships in libnccl-dev at /usr/include/nccl.h,
// not under $CUDA_HOME, and checking only the toolkit root
// reported it missing on an image that builds fine.
// (Kept in sync by hand with flodl-cli's util/requirements.rs;
// build.rs cannot depend on that crate.)
let sys_dirs = ["/usr/include", "/usr/local/include"];
let root_include = Path::new(root).join("include");
let missing: Vec<&(&str, &str)> = headers
.iter()
.filter(|(h, _)| {
if root_include.join(h).exists()
|| sys_dirs.iter().any(|d| Path::new(d).join(h).exists())
{
return false;
}
// The three directories above are a guess at where a
// distro put things, and distros disagree: a box can
// carry every CUDA header in /usr/include with no
// /usr/local/cuda at all and compile perfectly. Before
// refusing to build, ask the compiler that would do the
// building, with the same -I it will get. Only reached
// for a header already believed missing, so the ordinary
// build spawns nothing.
!header_reachable(h, &root_include)
})
.collect();
if !missing.is_empty() {
let mut pkgs: Vec<&str> = missing.iter().map(|(_, p)| *p).collect();
pkgs.dedup();
// Both vendors ship the same packages to Debian and
// RHEL-family repos with identical stems and two dev-suffix
// conventions (verified by repoquery against cuda-rhel9 and
// rocm/rhel9), so the rpm spelling is a transform, not a
// second table. Kept in sync by hand with flodl-cli's
// util/requirements.rs `rpm_name`.
let rpm = |deb: &str| -> String {
match deb.strip_suffix("-dev") {
Some(stem) => format!("{stem}-devel"),
None => deb.replace("-dev-", "-devel-"),
}
};
let rpm_pkgs: Vec<String> = pkgs.iter().map(|p| rpm(p)).collect();
eprintln!(
"\nflodl-sys: `--features {feature}` needs vendor toolkit headers\n\
that are missing under `{root}`:\n\n{}\n\n\
libtorch bundles the runtime libraries but not these headers.\n\n\
\x20 Ubuntu/Debian: sudo apt install {}\n\
\x20 RHEL/Fedora: sudo dnf install {}\n\
\x20 Other Linux: install the vendor SDK\n\
\x20 macOS/Windows: no GPU libtorch exists; on Windows use WSL2\n\n\
Set {root_env} if your install is not at {root_default}.\n",
missing
.iter()
.map(|(h, p)| format!(" {h} ({p})"))
.collect::<Vec<_>>()
.join("\n"),
pkgs.join(" "),
rpm_pkgs.join(" "),
);
std::process::exit(1);
}
}
let mut build = cc::Build::new();
build
.cpp(true)
.std("c++17")
.file("shim.cpp")
.include(".")
.include(libtorch.join("include"))
.include(libtorch.join("include/torch/csrc/api/include"))
.warnings(false);
// The define gates the shim's GPU code, which is vendor-neutral:
// ROCm libtorch keeps `kCUDA` and the `c10::cuda` namespaces, so the
// same blocks serve both backends. Named for what it means.
if cfg!(feature = "gpu") {
build.define("FLODL_BUILD_GPU", "1");
}
if want_cuda {
// CUDA toolkit headers (the one genuinely vendor-specific part
// of the compile step).
build.include(format!("{}/include", cuda_home));
}
if want_rocm {
// ROCm supplies what libtorch-rocm does NOT bundle: the HIP
// runtime headers (`hip/hip_runtime.h`) and RCCL's `rccl/rccl.h`.
//
// libtorch-rocm DOES ship the `c10/cuda/*` and `ATen/cuda/*`
// header trees, but they are dead weight -- the unbuilt CUDA
// headers, missing their generated `cuda_cmake_macros.h`, with no
// `libc10_cuda.so` to link against. `gpu_compat.h` maps onto the
// `c10/hip/*` + `ATen/hip/*` trees instead. There is likewise no
// `nccl.h` anywhere in ROCm: RCCL exports the nccl symbol names
// but ships them as `rccl/rccl.h` only.
build.include(format!("{rocm_path}/include"));
// `__HIP_PLATFORM_AMD__` is HIP's own "compiling for AMD" macro,
// which `gpu_compat.h` keys the whole vendor mapping on.
build.define("__HIP_PLATFORM_AMD__", "1");
// `USE_ROCM` is torch's own switch inside the hipified headers.
// Without it they take their `#else` branch and reach for CUDA
// headers that a ROCm install does not have -- e.g.
// `ATen/hip/Exceptions.h` includes `<cusolver_common.h>` unless
// USE_ROCM is set, and it is reached from `ATen/hip/HIPEvent.h`.
build.define("USE_ROCM", "1");
}
build.compile("flodl_shim");
// Link libtorch shared libraries
println!(
"cargo:rustc-link-search=native={}",
libtorch.join("lib").display()
);
println!("cargo:rustc-link-lib=dylib=torch");
println!("cargo:rustc-link-lib=dylib=torch_cpu");
println!("cargo:rustc-link-lib=dylib=c10");
if want_rocm {
// Link set read off libtorch 2.7.0+rocm6.3's own file list --
// all four ship inside libtorch/lib, RCCL included.
println!("cargo:rustc-link-lib=dylib=torch_hip");
println!("cargo:rustc-link-lib=dylib=c10_hip");
println!("cargo:rustc-link-lib=dylib=amdhip64");
let rocm_path = ["ROCM_PATH", "HIP_PATH", "HSA_PATH"]
.iter()
.filter_map(|k| env::var(k).ok())
.find(|v| !v.trim().is_empty())
.unwrap_or_else(|| "/opt/rocm".to_string());
// Both layouts: `lib64` on RHEL/SUSE. A search path that does
// not exist is ignored, so emitting both costs nothing.
println!("cargo:rustc-link-search=native={rocm_path}/lib");
println!("cargo:rustc-link-search=native={rocm_path}/lib64");
// dlopen, for the force-load and the allocator probes.
println!("cargo:rustc-link-lib=dylib=dl");
// RCCL is NCCL's API-compatible counterpart and exports the same
// symbol names, so the shim's collective code is unchanged.
println!("cargo:rustc-link-lib=dylib=rccl");
}
if want_cuda {
println!("cargo:rustc-link-lib=dylib=torch_cuda");
println!("cargo:rustc-link-lib=dylib=c10_cuda");
let cuda_home = env::var("CUDA_HOME").unwrap_or_else(|_| "/usr/local/cuda".to_string());
println!("cargo:rustc-link-search=native={}/lib64", cuda_home);
println!("cargo:rustc-link-lib=dylib=cudart");
// dlopen for NVML GPU utilization queries
println!("cargo:rustc-link-lib=dylib=dl");
// NCCL for multi-GPU collective operations
println!("cargo:rustc-link-lib=dylib=nccl");
}
// Rerun if sources change (shim.cpp + every #included unit + headers).
println!("cargo:rerun-if-changed=shim.cpp");
for src in &shim_includes {
println!("cargo:rerun-if-changed={}", src);
}
println!("cargo:rerun-if-env-changed=LIBTORCH_PATH");
println!("cargo:rerun-if-env-changed=CUDA_HOME");
println!("cargo:rerun-if-env-changed=ROCM_PATH");
println!("cargo:rerun-if-env-changed=HIP_PATH");
println!("cargo:rerun-if-env-changed=HSA_PATH");
}