Skip to main content

teeny_cuda/compiler/
options.rs

1/*
2 * Copyright (c) 2026 Teenygrad.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *   http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use derive_builder::Builder;
18use derive_more::Display;
19use teeny_compiler::compiler::backend::llvm::compiler::LogLevel;
20
21use crate::compiler::target::Capability;
22use crate::errors::{Error, Result};
23
24/// `nvptxcompiler` `--sanitize` value.
25#[derive(Debug, Clone, Copy, Display)]
26pub enum Sanitizer {
27    /// Enable `memcheck`-style memory error detection.
28    #[display("memcheck")]
29    MemCheck,
30}
31
32/// `nvptxcompiler` `--opt-level` value.
33#[derive(Debug, Clone, Copy, Display)]
34pub enum OptLevel {
35    /// No optimization.
36    #[display("0")]
37    O0,
38    /// Light optimization.
39    #[display("1")]
40    O1,
41    /// Default optimization.
42    #[display("2")]
43    O2,
44    /// Aggressive optimization.
45    #[display("3")]
46    O3,
47}
48
49/// `nvptxcompiler` compile options, translated to CLI flags by [`Options::to_compile_options`].
50/// Each field corresponds 1:1 to an `nvptxcompiler`/`teenyc` flag of the same name (with `_`
51/// replaced by `-`) — see [`Options::parse`] for the string-based `--options` CLI encoding.
52#[derive(Builder)]
53pub struct Options {
54    /// `--allow-expensive-optimizations`.
55    #[builder(default = "false")]
56    pub allow_expensive_optimizations: bool,
57
58    /// `--compile-as-tools-patch`.
59    #[builder(default = "false")]
60    pub compile_as_tools_patch: bool,
61
62    /// `--compile-only`.
63    #[builder(default = "false")]
64    pub compile_only: bool,
65
66    /// `--def-load-cache`.
67    #[builder(default = "false")]
68    pub def_load_cache: bool,
69
70    /// `--def-store-cache`.
71    #[builder(default = "false")]
72    pub def_store_cache: bool,
73
74    /// `--device-debug`.
75    #[builder(default = "false")]
76    pub device_debug: bool,
77
78    /// `--device-function-maxrregcount`.
79    #[builder(default = "None")]
80    pub device_function_maxrregcount: Option<u32>,
81
82    /// `--disable-optimizer-constants`.
83    #[builder(default = "false")]
84    pub disable_optimizer_constants: bool,
85
86    /// `--disable-warnings`.
87    #[builder(default = "false")]
88    pub disable_warnings: bool,
89
90    /// `--dont-merge-basicblocks`.
91    #[builder(default = "false")]
92    pub dont_merge_basicblocks: bool,
93
94    /// `--entry`: the kernel entry point name.
95    #[builder(default = "String::from(\"entry_point\")")]
96    pub entry: String,
97
98    /// `--extensible-whole-program`.
99    #[builder(default = "false")]
100    pub extensible_whole_program: bool,
101
102    /// `--fmad`: enable fused multiply-add contraction.
103    #[builder(default = "false")]
104    pub fmad: bool,
105
106    /// `--force-load-cache`.
107    #[builder(default = "false")]
108    pub force_load_cache: bool,
109
110    /// `--force-store-cache`.
111    #[builder(default = "false")]
112    pub force_store_cache: bool,
113
114    /// `--generate-line-info`.
115    #[builder(default = "false")]
116    pub generate_line_info: bool,
117
118    /// `--gpu-name`: the target GPU's compute capability.
119    #[builder]
120    pub gpu_name: Capability,
121
122    /// Explicit PTX ISA version to request from `teenyc` (e.g. `82` for
123    /// `8.2`), encoded as `major*10 + minor`. Not an `nvptxcompiler` flag —
124    /// excluded from [`Options::to_compile_options`]; consumed separately by
125    /// [`crate::compiler::aot::compile_graph`] to override `teenyc`'s
126    /// capability-based default when the deployment target's exact CUDA
127    /// version is known (e.g. `ptx-version=82` for a Jetson Orin Nano on
128    /// CUDA 12.2, since sm_87's own default floor is conservative).
129    #[builder(default = "None")]
130    pub ptx_version: Option<u32>,
131
132    /// `teenyc`'s diagnostic verbosity (see [`LogLevel`]). Not an `nvptxcompiler` flag —
133    /// excluded from [`Options::to_compile_options`]; consumed by
134    /// [`crate::compiler::aot::compile_graph`] to set `LlvmCompiler::with_log_level`, which in
135    /// turn sets `RUSTC_LOG` on the `teenyc` subprocess. Leaving this unset (the default)
136    /// preserves `teenyc`'s own default verbosity and skips capturing any pipeline-stage IR.
137    #[builder(default = "None")]
138    pub log_level: Option<LogLevel>,
139
140    /// Target device's SM (streaming multiprocessor) count, for shape-adaptive kernel
141    /// tile-size selection. Not an `nvptxcompiler` flag — excluded from
142    /// [`Options::to_compile_options`]; consumed by `teeny-kernels`' `TritonLowering` to
143    /// pick smaller tile sizes (larger grids) for conv layers whose default tile size
144    /// would otherwise launch too few thread blocks to occupy the target device (e.g.
145    /// deep layers with small spatial dims after repeated downsampling). Not auto-queried
146    /// from a live device: AOT compilation may target a device that isn't the one doing
147    /// the compiling (e.g. cross-compiling for a Jetson from an x86 host), so this must be
148    /// supplied explicitly when known (e.g. `sm-count=20` for a Jetson Orin Nano). Leaving
149    /// this unset (the default) preserves today's fixed tile-size behavior unchanged.
150    #[builder(default = "None")]
151    pub sm_count: Option<u32>,
152
153    /// `--maxrregcount` (alias `maxnreg` in [`Options::parse`]'s string encoding).
154    #[builder(default = "None")]
155    pub maxrregcount: Option<u32>,
156
157    /// `--opt-level`.
158    #[builder(default = "None")]
159    pub opt_level: Option<OptLevel>,
160
161    /// `--position-independent-code`.
162    #[builder(default = "false")]
163    pub position_independent_code: bool,
164
165    /// `--preserve-relocs`.
166    #[builder(default = "false")]
167    pub preserve_relocs: bool,
168
169    /// `--return-at-end`.
170    #[builder(default = "false")]
171    pub return_at_end: bool,
172
173    /// `--sanitize`.
174    #[builder(default = "None")]
175    pub sanitize: Option<Sanitizer>,
176
177    /// `--suppress-async-bulk-multicast-advisory-warning`.
178    #[builder(default = "false")]
179    pub suppress_async_bulk_multicast_advisory_warning: bool,
180
181    /// `--suppress-stack-size-warning`.
182    #[builder(default = "false")]
183    pub suppress_stack_size_warning: bool,
184
185    /// `--verbose`.
186    #[builder(default = "false")]
187    pub verbose: bool,
188
189    /// `--warn-on-double-precision-use`.
190    #[builder(default = "false")]
191    pub warn_on_double_precision_use: bool,
192
193    /// `--warn-on-local-memory-usage`.
194    #[builder(default = "false")]
195    pub warn_on_local_memory_usage: bool,
196
197    /// `--warn-on-spills`.
198    #[builder(default = "false")]
199    pub warn_on_spills: bool,
200
201    /// `--warning-as-error`.
202    #[builder(default = "false")]
203    pub warning_as_error: bool,
204
205    /// `--maxntid`.
206    #[builder(default = "None")]
207    pub maxntid: Option<u32>,
208
209    /// `--minnctapersm`.
210    #[builder(default = "None")]
211    pub minnctapersm: Option<u32>,
212
213    /// `--override-directive-values`.
214    #[builder(default = "false")]
215    pub override_directive_values: bool,
216
217    /// `--make-errors-visible-at-exit`.
218    #[builder(default = "false")]
219    pub make_errors_visible_at_exit: bool,
220
221    /// `--oFast-compile`.
222    #[builder(default = "None")]
223    pub ofast_compile: Option<u32>,
224
225    /// `--device-stack-protector`.
226    #[builder(default = "false")]
227    pub device_stack_protector: bool,
228
229    /// `--g-tensor-memory-access-check`.
230    #[builder(default = "false")]
231    pub g_tensor_memory_access_check: bool,
232
233    /// `--gno-tensor-memory-access-check`.
234    #[builder(default = "false")]
235    pub gno_tensor_memory_access_check: bool,
236
237    /// `--split-compile`.
238    #[builder(default = "None")]
239    pub split_compile: Option<u32>,
240}
241
242impl Options {
243    /// Renders these options as `nvptxcompiler`/`teenyc` CLI flags.
244    pub fn to_compile_options(&self) -> Vec<String> {
245        let mut args: Vec<String> = Vec::new();
246
247        if self.allow_expensive_optimizations {
248            args.push(String::from("--allow-expensive-optimizations"));
249        }
250
251        if self.compile_as_tools_patch {
252            args.push(String::from("--compile-as-tools-patch"));
253        }
254
255        if self.compile_only {
256            args.push(String::from("--compile-only"));
257        }
258
259        if self.def_load_cache {
260            args.push(String::from("--def-load-cache"));
261        }
262
263        if self.def_store_cache {
264            args.push(String::from("--def-store-cache"));
265        }
266
267        if self.device_debug {
268            args.push(String::from("--device-debug"));
269        }
270
271        if let Some(device_function_maxrregcount) = self.device_function_maxrregcount {
272            args.push(format!(
273                "--device-function-maxrregcount={}",
274                device_function_maxrregcount
275            ));
276        }
277
278        if self.disable_optimizer_constants {
279            args.push(String::from("--disable-optimizer-constants"));
280        }
281
282        if self.disable_warnings {
283            args.push(String::from("--disable-warnings"));
284        }
285
286        if self.dont_merge_basicblocks {
287            args.push(String::from("--dont-merge-basicblocks"));
288        }
289
290        args.push(format!("--entry={}", self.entry));
291
292        if self.extensible_whole_program {
293            args.push(String::from("--extensible-whole-program"));
294        }
295
296        if self.fmad {
297            args.push(String::from("--fmad"));
298        }
299
300        if self.force_load_cache {
301            args.push(String::from("--force-load-cache"));
302        }
303
304        if self.force_store_cache {
305            args.push(String::from("--force-store-cache"));
306        }
307
308        if self.generate_line_info {
309            args.push(String::from("--generate-line-info"));
310        }
311
312        args.push(format!("--gpu-name={}", self.gpu_name));
313
314        if let Some(maxrregcount) = self.maxrregcount {
315            args.push(format!("--maxrregcount={}", maxrregcount));
316        }
317
318        if let Some(opt_level) = self.opt_level {
319            args.push(format!("--opt-level={}", opt_level));
320        }
321
322        if self.position_independent_code {
323            args.push(String::from("--position-independent-code"));
324        }
325
326        if self.preserve_relocs {
327            args.push(String::from("--preserve-relocs"));
328        }
329
330        if self.return_at_end {
331            args.push(String::from("--return-at-end"));
332        }
333
334        if let Some(sanitize) = self.sanitize {
335            args.push(format!("--sanitize={}", sanitize));
336        }
337
338        if self.suppress_async_bulk_multicast_advisory_warning {
339            args.push(String::from(
340                "--suppress-async-bulk-multicast-advisory-warning",
341            ));
342        }
343
344        if self.suppress_stack_size_warning {
345            args.push(String::from("--suppress-stack-size-warning"));
346        }
347
348        if self.verbose {
349            args.push(String::from("--verbose"));
350        }
351
352        if self.warn_on_double_precision_use {
353            args.push(String::from("--warn-on-double-precision-use"));
354        }
355
356        if self.warn_on_local_memory_usage {
357            args.push(String::from("--warn-on-local-memory-usage"));
358        }
359
360        if self.warn_on_spills {
361            args.push(String::from("--warn-on-spills"));
362        }
363
364        if self.warning_as_error {
365            args.push(String::from("--warning-as-error"));
366        }
367
368        if let Some(maxntid) = self.maxntid {
369            args.push(format!("--maxntid={}", maxntid));
370        }
371
372        if let Some(minnctapersm) = self.minnctapersm {
373            args.push(format!("--minnctapersm={}", minnctapersm));
374        }
375
376        if self.override_directive_values {
377            args.push(String::from("--override-directive-values"));
378        }
379
380        if self.make_errors_visible_at_exit {
381            args.push(String::from("--make-errors-visible-at-exit"));
382        }
383
384        if let Some(ofast_compile) = self.ofast_compile {
385            args.push(format!("--oFast-compile={}", ofast_compile));
386        }
387
388        if self.device_stack_protector {
389            args.push(String::from("--device-stack-protector"));
390        }
391
392        if self.g_tensor_memory_access_check {
393            args.push(String::from("--g-tensor-memory-access-check"));
394        }
395
396        if self.gno_tensor_memory_access_check {
397            args.push(String::from("--gno-tensor-memory-access-check"));
398        }
399
400        if let Some(split_compile) = self.split_compile {
401            args.push(format!("--split-compile={}", split_compile));
402        }
403
404        args
405    }
406}
407
408impl Options {
409    /// Parse a comma-separated `key=value` string (as passed via `--options` on
410    /// the AOT compile CLI, e.g. `"capability=sm_90,maxnreg=16"`) into `Options`.
411    ///
412    /// `capability` (alias `gpu-name`) is required. Boolean flags may be given
413    /// bare (`key`, meaning `true`) or as `key=true`/`key=false`. Unknown keys
414    /// are rejected outright rather than silently ignored, so typos and
415    /// not-yet-supported knobs (e.g. a shared-memory limit) surface immediately.
416    pub fn parse(input: &str) -> Result<Options> {
417        let mut builder = OptionsBuilder::default();
418        let mut capability: Option<Capability> = None;
419
420        for pair in input.split(',').map(str::trim).filter(|s| !s.is_empty()) {
421            let (raw_key, value) = match pair.split_once('=') {
422                Some((k, v)) => (k.trim(), Some(v.trim())),
423                None => (pair, None),
424            };
425            let key = raw_key.to_ascii_lowercase().replace('_', "-");
426
427            match key.as_str() {
428                "capability" | "gpu-name" => {
429                    let v = require_value(input, &key, value)?;
430                    capability =
431                        Some(
432                            v.parse::<Capability>()
433                                .map_err(|reason| Error::InvalidOptions {
434                                    input: input.to_string(),
435                                    reason,
436                                })?,
437                        );
438                }
439                "ptx-version" => {
440                    builder.ptx_version(Some(parse_u32(input, &key, value)?));
441                }
442                "sm-count" => {
443                    builder.sm_count(Some(parse_u32(input, &key, value)?));
444                }
445                "log-level" => {
446                    let v = require_value(input, &key, value)?;
447                    builder.log_level(Some(v.parse::<LogLevel>().map_err(|reason| {
448                        Error::InvalidOptions {
449                            input: input.to_string(),
450                            reason,
451                        }
452                    })?));
453                }
454                "allow-expensive-optimizations" => {
455                    builder.allow_expensive_optimizations(parse_bool(input, &key, value)?);
456                }
457                "compile-as-tools-patch" => {
458                    builder.compile_as_tools_patch(parse_bool(input, &key, value)?);
459                }
460                "compile-only" => {
461                    builder.compile_only(parse_bool(input, &key, value)?);
462                }
463                "def-load-cache" => {
464                    builder.def_load_cache(parse_bool(input, &key, value)?);
465                }
466                "def-store-cache" => {
467                    builder.def_store_cache(parse_bool(input, &key, value)?);
468                }
469                "device-debug" => {
470                    builder.device_debug(parse_bool(input, &key, value)?);
471                }
472                "device-function-maxrregcount" => {
473                    builder.device_function_maxrregcount(Some(parse_u32(input, &key, value)?));
474                }
475                "disable-optimizer-constants" => {
476                    builder.disable_optimizer_constants(parse_bool(input, &key, value)?);
477                }
478                "disable-warnings" => {
479                    builder.disable_warnings(parse_bool(input, &key, value)?);
480                }
481                "dont-merge-basicblocks" => {
482                    builder.dont_merge_basicblocks(parse_bool(input, &key, value)?);
483                }
484                "entry" => {
485                    builder.entry(require_value(input, &key, value)?.to_string());
486                }
487                "extensible-whole-program" => {
488                    builder.extensible_whole_program(parse_bool(input, &key, value)?);
489                }
490                "fmad" => {
491                    builder.fmad(parse_bool(input, &key, value)?);
492                }
493                "force-load-cache" => {
494                    builder.force_load_cache(parse_bool(input, &key, value)?);
495                }
496                "force-store-cache" => {
497                    builder.force_store_cache(parse_bool(input, &key, value)?);
498                }
499                "generate-line-info" => {
500                    builder.generate_line_info(parse_bool(input, &key, value)?);
501                }
502                "maxnreg" | "maxrregcount" => {
503                    builder.maxrregcount(Some(parse_u32(input, &key, value)?));
504                }
505                "opt-level" => {
506                    builder.opt_level(Some(parse_opt_level(input, &key, value)?));
507                }
508                "position-independent-code" => {
509                    builder.position_independent_code(parse_bool(input, &key, value)?);
510                }
511                "preserve-relocs" => {
512                    builder.preserve_relocs(parse_bool(input, &key, value)?);
513                }
514                "return-at-end" => {
515                    builder.return_at_end(parse_bool(input, &key, value)?);
516                }
517                "sanitize" => {
518                    builder.sanitize(Some(parse_sanitizer(input, &key, value)?));
519                }
520                "suppress-async-bulk-multicast-advisory-warning" => {
521                    builder.suppress_async_bulk_multicast_advisory_warning(parse_bool(
522                        input, &key, value,
523                    )?);
524                }
525                "suppress-stack-size-warning" => {
526                    builder.suppress_stack_size_warning(parse_bool(input, &key, value)?);
527                }
528                "verbose" => {
529                    builder.verbose(parse_bool(input, &key, value)?);
530                }
531                "warn-on-double-precision-use" => {
532                    builder.warn_on_double_precision_use(parse_bool(input, &key, value)?);
533                }
534                "warn-on-local-memory-usage" => {
535                    builder.warn_on_local_memory_usage(parse_bool(input, &key, value)?);
536                }
537                "warn-on-spills" => {
538                    builder.warn_on_spills(parse_bool(input, &key, value)?);
539                }
540                "warning-as-error" => {
541                    builder.warning_as_error(parse_bool(input, &key, value)?);
542                }
543                "maxntid" => {
544                    builder.maxntid(Some(parse_u32(input, &key, value)?));
545                }
546                "minnctapersm" => {
547                    builder.minnctapersm(Some(parse_u32(input, &key, value)?));
548                }
549                "override-directive-values" => {
550                    builder.override_directive_values(parse_bool(input, &key, value)?);
551                }
552                "make-errors-visible-at-exit" => {
553                    builder.make_errors_visible_at_exit(parse_bool(input, &key, value)?);
554                }
555                "ofast-compile" => {
556                    builder.ofast_compile(Some(parse_u32(input, &key, value)?));
557                }
558                "device-stack-protector" => {
559                    builder.device_stack_protector(parse_bool(input, &key, value)?);
560                }
561                "g-tensor-memory-access-check" => {
562                    builder.g_tensor_memory_access_check(parse_bool(input, &key, value)?);
563                }
564                "gno-tensor-memory-access-check" => {
565                    builder.gno_tensor_memory_access_check(parse_bool(input, &key, value)?);
566                }
567                "split-compile" => {
568                    builder.split_compile(Some(parse_u32(input, &key, value)?));
569                }
570                other => {
571                    return Err(Error::InvalidOptions {
572                        input: input.to_string(),
573                        reason: format!("unknown option key '{other}'"),
574                    }
575                    .into());
576                }
577            }
578        }
579
580        let capability = capability.ok_or_else(|| Error::InvalidOptions {
581            input: input.to_string(),
582            reason: "missing required 'capability' key, e.g. capability=sm_90".to_string(),
583        })?;
584        builder.gpu_name(capability);
585
586        builder.build().map_err(|e| {
587            Error::InvalidOptions {
588                input: input.to_string(),
589                reason: e.to_string(),
590            }
591            .into()
592        })
593    }
594}
595
596fn require_value<'a>(input: &str, key: &str, value: Option<&'a str>) -> Result<&'a str> {
597    value.ok_or_else(|| {
598        Error::InvalidOptions {
599            input: input.to_string(),
600            reason: format!("'{key}' requires a value, e.g. {key}=<value>"),
601        }
602        .into()
603    })
604}
605
606fn parse_bool(input: &str, key: &str, value: Option<&str>) -> Result<bool> {
607    match value {
608        None => Ok(true),
609        Some(v) => match v.to_ascii_lowercase().as_str() {
610            "true" | "1" | "yes" => Ok(true),
611            "false" | "0" | "no" => Ok(false),
612            other => Err(Error::InvalidOptions {
613                input: input.to_string(),
614                reason: format!("invalid boolean value '{other}' for '{key}'"),
615            }
616            .into()),
617        },
618    }
619}
620
621fn parse_u32(input: &str, key: &str, value: Option<&str>) -> Result<u32> {
622    let v = require_value(input, key, value)?;
623    v.parse::<u32>().map_err(|_| {
624        Error::InvalidOptions {
625            input: input.to_string(),
626            reason: format!("invalid integer value '{v}' for '{key}'"),
627        }
628        .into()
629    })
630}
631
632fn parse_opt_level(input: &str, key: &str, value: Option<&str>) -> Result<OptLevel> {
633    let v = require_value(input, key, value)?;
634    match v.to_ascii_lowercase().as_str() {
635        "0" | "o0" => Ok(OptLevel::O0),
636        "1" | "o1" => Ok(OptLevel::O1),
637        "2" | "o2" => Ok(OptLevel::O2),
638        "3" | "o3" => Ok(OptLevel::O3),
639        other => Err(Error::InvalidOptions {
640            input: input.to_string(),
641            reason: format!("invalid opt-level '{other}'; expected one of 0, 1, 2, 3"),
642        }
643        .into()),
644    }
645}
646
647fn parse_sanitizer(input: &str, key: &str, value: Option<&str>) -> Result<Sanitizer> {
648    let v = require_value(input, key, value)?;
649    match v.to_ascii_lowercase().as_str() {
650        "memcheck" => Ok(Sanitizer::MemCheck),
651        other => Err(Error::InvalidOptions {
652            input: input.to_string(),
653            reason: format!("invalid sanitize value '{other}'; expected 'memcheck'"),
654        }
655        .into()),
656    }
657}
658
659#[cfg(test)]
660mod parse_tests {
661    use super::*;
662
663    #[test]
664    fn parses_capability_and_maxnreg_alias() {
665        let opts = Options::parse("capability=sm_90,maxnreg=16").unwrap();
666        assert_eq!(opts.gpu_name, Capability::Sm90);
667        assert_eq!(opts.maxrregcount, Some(16));
668    }
669
670    #[test]
671    fn bare_bool_flag_means_true() {
672        let opts = Options::parse("capability=sm_90,verbose").unwrap();
673        assert!(opts.verbose);
674    }
675
676    #[test]
677    fn missing_capability_errors() {
678        assert!(Options::parse("maxnreg=16").is_err());
679    }
680
681    #[test]
682    fn unknown_key_errors() {
683        assert!(Options::parse("capability=sm_90,shared-memory=25k").is_err());
684    }
685
686    #[test]
687    fn parses_ptx_version_override() {
688        let opts = Options::parse("capability=sm_87,ptx-version=82").unwrap();
689        assert_eq!(opts.gpu_name, Capability::Sm87);
690        assert_eq!(opts.ptx_version, Some(82));
691    }
692
693    #[test]
694    fn ptx_version_defaults_to_none() {
695        let opts = Options::parse("capability=sm_87").unwrap();
696        assert_eq!(opts.ptx_version, None);
697    }
698
699    #[test]
700    fn parses_sm_count_override() {
701        let opts = Options::parse("capability=sm_87,sm-count=20").unwrap();
702        assert_eq!(opts.gpu_name, Capability::Sm87);
703        assert_eq!(opts.sm_count, Some(20));
704    }
705
706    #[test]
707    fn sm_count_defaults_to_none() {
708        let opts = Options::parse("capability=sm_87").unwrap();
709        assert_eq!(opts.sm_count, None);
710    }
711
712    #[test]
713    fn parses_log_level() {
714        let opts = Options::parse("capability=sm_87,log-level=debug").unwrap();
715        assert_eq!(opts.log_level, Some(LogLevel::Debug));
716    }
717
718    #[test]
719    fn log_level_defaults_to_none() {
720        let opts = Options::parse("capability=sm_87").unwrap();
721        assert_eq!(opts.log_level, None);
722    }
723
724    #[test]
725    fn invalid_log_level_errors() {
726        assert!(Options::parse("capability=sm_87,log-level=verbose").is_err());
727    }
728}