openvm_cuda_builder/
lib.rs1use std::{env, path::Path, process::Command};
2
3#[derive(Debug, Clone)]
5pub struct CudaBuilder {
6 include_paths: Vec<String>,
7 source_files: Vec<String>,
8 watch_paths: Vec<String>,
9 watch_globs: Vec<String>,
10 library_name: String,
11 cuda_arch: Vec<String>,
12 cuda_opt_level: Option<String>,
13 lineinfo: bool,
14 custom_flags: Vec<String>,
15 link_libraries: Vec<String>,
16 link_search_paths: Vec<String>,
17}
18
19impl Default for CudaBuilder {
20 fn default() -> Self {
21 let mut link_search_paths = Vec::new();
22 if let Ok(cuda_lib_dir) = env::var("CUDA_LIB_DIR") {
23 link_search_paths.push(cuda_lib_dir);
24 } else {
25 link_search_paths.push("/usr/local/cuda/lib64".to_string());
26 }
27
28 Self {
29 include_paths: Vec::new(),
30 source_files: Vec::new(),
31 watch_paths: vec!["build.rs".to_string()],
32 watch_globs: Vec::new(),
33 library_name: String::new(),
34 cuda_arch: Vec::new(),
35 cuda_opt_level: None,
36 lineinfo: false,
37 custom_flags: vec![
38 "--std=c++17".to_string(),
39 "--expt-relaxed-constexpr".to_string(),
40 "-Xfatbin=-compress-all".to_string(),
41 ],
42 link_libraries: vec!["cudart".to_string(), "cuda".to_string()],
43 link_search_paths,
44 }
45 }
46}
47
48impl CudaBuilder {
49 pub fn new() -> Self {
51 Self::default()
52 }
53
54 pub fn library_name(mut self, name: &str) -> Self {
56 self.library_name = name.to_string();
57 self
58 }
59
60 pub fn include<P: AsRef<Path>>(mut self, path: P) -> Self {
62 let path_str = path.as_ref().to_string_lossy().to_string();
63 self.include_paths.push(path_str.clone());
64 self.watch_paths.push(path_str);
65 self
66 }
67
68 pub fn include_from_dep(mut self, dep_env_var: &str) -> Self {
70 if let Ok(path) = env::var(dep_env_var) {
71 self.include_paths.push(path);
72 }
73 self
74 }
75
76 pub fn file<P: AsRef<Path>>(mut self, path: P) -> Self {
78 let path_str = path.as_ref().to_string_lossy().to_string();
79 self.source_files.push(path_str.clone());
80 self.watch_paths.push(path_str);
81 self
82 }
83
84 pub fn files<P: AsRef<Path>, I: IntoIterator<Item = P>>(mut self, paths: I) -> Self {
86 for path in paths {
87 let path_str = path.as_ref().to_string_lossy().to_string();
88 self.source_files.push(path_str.clone());
89 self.watch_paths.push(path_str);
90 }
91 self
92 }
93
94 pub fn files_from_glob(mut self, pattern: &str) -> Self {
96 self.watch_globs.push(pattern.to_string());
97 for path in glob::glob(pattern).expect("Invalid glob pattern").flatten() {
98 if path.is_file() && path.extension().is_some_and(|ext| ext == "cu") {
99 self.source_files.push(path.to_string_lossy().to_string());
100 }
101 }
102 self
103 }
104
105 pub fn watch<P: AsRef<Path>>(mut self, path: P) -> Self {
107 self.watch_paths
108 .push(path.as_ref().to_string_lossy().to_string());
109 self
110 }
111
112 pub fn watch_glob(mut self, pattern: &str) -> Self {
114 self.watch_globs.push(pattern.to_string());
115 self
116 }
117
118 pub fn cuda_arch(mut self, arch: &str) -> Self {
120 self.cuda_arch = vec![arch.to_string()];
121 self
122 }
123
124 pub fn cuda_archs(mut self, archs: Vec<&str>) -> Self {
126 self.cuda_arch = archs.iter().map(|s| s.to_string()).collect();
127 self
128 }
129
130 pub fn cuda_opt_level(mut self, level: u8) -> Self {
132 self.cuda_opt_level = Some(level.to_string());
133 self
134 }
135
136 pub fn lineinfo(mut self, enabled: bool) -> Self {
138 self.lineinfo = enabled;
139 self
140 }
141
142 pub fn flag(mut self, flag: &str) -> Self {
144 self.custom_flags.push(flag.to_string());
145 self
146 }
147
148 pub fn link_lib(mut self, lib: &str) -> Self {
150 self.link_libraries.push(lib.to_string());
151 self
152 }
153
154 pub fn link_search<P: AsRef<Path>>(mut self, path: P) -> Self {
156 self.link_search_paths
157 .push(path.as_ref().to_string_lossy().to_string());
158 self
159 }
160
161 pub fn build(self) {
163 self.validate();
165
166 self.setup_rerun_conditions();
168
169 let cuda_archs = self.get_cuda_arch();
171
172 let mut builder = cc::Build::new();
174 builder.cuda(true);
175
176 self.handle_debug_shortcuts(&mut builder);
178
179 let cuda_opt_level = self.get_cuda_opt_level();
181
182 for include in &self.include_paths {
184 builder.include(include);
185 }
186
187 if let Ok(cuda_path) = env::var("CUDA_PATH") {
189 builder.include(format!("{}/include", cuda_path));
190 }
191
192 for flag in &self.custom_flags {
194 builder.flag(flag);
195 }
196
197 for arch in &cuda_archs {
199 builder
200 .flag("-gencode")
201 .flag(format!("arch=compute_{},code=sm_{}", arch, arch));
202 }
203
204 if let Some(max_arch) = cuda_archs.iter().max() {
207 builder.flag("-gencode").flag(format!(
208 "arch=compute_{},code=compute_{}",
209 max_arch, max_arch
210 ));
211 }
212
213 builder.flag(nvcc_parallel_jobs());
215
216 if cuda_opt_level == "0" {
218 builder.debug(true).flag("-O0");
219 } else {
220 builder
221 .debug(false)
222 .flag(format!("--ptxas-options=-O{}", cuda_opt_level));
223 }
224
225 if self.get_lineinfo() {
227 builder.flag("-lineinfo");
228 }
229
230 for file in &self.source_files {
232 builder.file(file);
233 }
234
235 builder.compile(&self.library_name);
237 }
238
239 fn validate(&self) {
241 if self.library_name.is_empty() {
242 panic!(
243 "Library name must be set using .library_name(\"name\") before calling .build()"
244 );
245 }
246
247 if self.source_files.is_empty() {
248 panic!("At least one source file must be added using .file() or .files() before calling .build()");
249 }
250
251 for file in &self.source_files {
253 if !Path::new(file).exists() {
254 eprintln!("cargo:warning=CUDA source file does not exist: {}", file);
255 }
256 }
257
258 for include in &self.include_paths {
260 if !Path::new(include).exists() {
261 eprintln!("cargo:warning=Include path does not exist: {}", include);
262 }
263 }
264 }
265
266 pub fn emit_link_directives(&self) {
267 for path in &self.link_search_paths {
268 println!("cargo:rustc-link-search=native={}", path);
269 }
270 for lib in &self.link_libraries {
271 println!("cargo:rustc-link-lib={}", lib);
272 }
273 }
274
275 fn setup_rerun_conditions(&self) {
276 println!("cargo:rerun-if-env-changed=CUDA_ARCH");
278 println!("cargo:rerun-if-env-changed=CUDA_OPT_LEVEL");
279 println!("cargo:rerun-if-env-changed=CUDA_DEBUG");
280 println!("cargo:rerun-if-env-changed=CUDA_LINEINFO");
281 println!("cargo:rerun-if-env-changed=NVCC_THREADS");
282
283 for path in &self.watch_paths {
285 println!("cargo:rerun-if-changed={}", path);
286 }
287
288 for pattern in &self.watch_globs {
290 watch_glob(pattern);
291 }
292 }
293
294 fn get_cuda_arch(&self) -> Vec<String> {
295 if !self.cuda_arch.is_empty() {
296 return self.cuda_arch.clone();
297 }
298
299 if let Ok(env_archs) = env::var("CUDA_ARCH") {
301 return env_archs
302 .split(',')
303 .map(|s| s.trim().to_string())
304 .filter(|s| !s.is_empty())
305 .collect();
306 }
307
308 vec![detect_cuda_arch()]
310 }
311
312 fn get_cuda_opt_level(&self) -> String {
313 if let Some(level) = &self.cuda_opt_level {
314 return level.clone();
315 }
316
317 env::var("CUDA_OPT_LEVEL").unwrap_or_else(|_| "3".to_string())
318 }
319
320 fn get_lineinfo(&self) -> bool {
321 self.lineinfo || env::var("CUDA_LINEINFO").map(|v| v == "1").unwrap_or(false)
322 }
323
324 fn handle_debug_shortcuts(&self, builder: &mut cc::Build) {
325 if env::var("CUDA_DEBUG").map(|v| v == "1").unwrap_or(false) {
326 env::set_var("CUDA_OPT_LEVEL", "0");
327 env::set_var("CUDA_LAUNCH_BLOCKING", "1");
328 env::set_var("RUST_BACKTRACE", "full");
329 env::set_var("CUDA_ENABLE_COREDUMP_ON_EXCEPTION", "1");
330 env::set_var("CUDA_DEVICE_WAITS_ON_EXCEPTION", "1");
331
332 println!("cargo:warning=CUDA_DEBUG=1 → Enabling comprehensive debugging:");
333 println!("cargo:warning= → CUDA_OPT_LEVEL=0 (no optimization)");
334 println!("cargo:warning= → CUDA_LAUNCH_BLOCKING=1 (synchronous kernels)");
335 println!("cargo:warning= → Line info and device debug symbols enabled");
336 println!("cargo:warning= → CUDA_DEBUG macro defined for preprocessor");
337
338 builder.flag("-G"); builder.flag("-Xcompiler=-fno-omit-frame-pointer"); builder.flag("-Xptxas=-v"); builder.define("CUDA_DEBUG", "1"); }
343 }
344}
345
346pub fn cuda_available() -> bool {
348 Command::new("nvcc").arg("--version").output().is_ok()
349}
350
351pub fn detect_cuda_arch() -> String {
353 let output = Command::new("nvidia-smi")
354 .args(["--query-gpu=compute_cap", "--format=csv,noheader"])
355 .output()
356 .expect("Failed to run nvidia-smi - make sure NVIDIA drivers are installed");
357
358 let full_output =
359 String::from_utf8(output.stdout).expect("nvidia-smi output is not valid UTF-8");
360
361 let arch = full_output
362 .lines()
363 .next()
364 .expect("nvidia-smi failed to return compute capability")
365 .trim()
366 .replace('.', ""); println!("cargo:rustc-env=CUDA_ARCH={}", arch);
370 env::set_var("CUDA_ARCH", &arch);
371 arch
372}
373
374pub fn nvcc_parallel_jobs() -> String {
376 let threads = std::thread::available_parallelism()
377 .map(|n| n.get())
378 .unwrap_or(1);
379
380 let threads = env::var("NVCC_THREADS")
381 .ok()
382 .and_then(|v| v.parse::<usize>().ok())
383 .unwrap_or(threads);
384
385 format!("-t{}", threads)
386}
387
388fn watch_glob(pattern: &str) {
390 for path in glob::glob(pattern).expect("Invalid glob pattern").flatten() {
391 if path.is_file() {
392 println!("cargo:rerun-if-changed={}", path.display());
393 }
394 }
395}