From b4acc68976932e09767b837e70aef571da721e30 Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:52:33 -0500 Subject: [PATCH 01/19] Introduce --batch-target-time CLI option The option specifies target accumulated GPU time for batched measurements. This option is used to determine batch size and to decide when batched measurements loop terminates. (total accummulated time must exceed the target time and the number of launches executed should >= min_samples). --- docs/cli_help.md | 10 +++++++-- nvbench/benchmark_base.cuh | 14 +++++++++++-- nvbench/benchmark_base.cxx | 5 +++-- nvbench/detail/measure_hot.cu | 38 +++++++++++++++++++++++----------- nvbench/detail/measure_hot.cuh | 10 ++++----- nvbench/json_printer.cu | 2 ++ nvbench/option_parser.cu | 14 ++++++++++++- nvbench/state.cuh | 13 ++++++++++-- nvbench/state.cxx | 2 ++ testing/option_parser.cu | 38 ++++++++++++++++++++++++++++++++++ 10 files changed, 120 insertions(+), 26 deletions(-) diff --git a/docs/cli_help.md b/docs/cli_help.md index 94eb7cb4..8b0d759a 100644 --- a/docs/cli_help.md +++ b/docs/cli_help.md @@ -136,6 +136,12 @@ * Applied to the most recent `--benchmark`, or all benchmarks if specified before any `--benchmark` arguments. +* `--batch-target-time ` + * Target accumulated GPU time for batched measurements. + * Default is 0.5 seconds. + * Applies to the most recent `--benchmark`, or all benchmarks if specified + before any `--benchmark` arguments. + ## Measurement Collection * `--timeout ` @@ -143,8 +149,8 @@ * Default is 15 seconds. * `` is walltime, not accumulated sample time. * If a measurement times out, the default markdown log will print a warning to - report any outstanding termination criteria (min samples, min time, max - noise). + report any outstanding termination criteria (min samples, batch target time, + max noise). * Applies to the most recent `--benchmark`, or all benchmarks if specified before any `--benchmark` arguments. diff --git a/nvbench/benchmark_base.cuh b/nvbench/benchmark_base.cuh index 9ceea815..91cb0d5d 100644 --- a/nvbench/benchmark_base.cuh +++ b/nvbench/benchmark_base.cuh @@ -224,6 +224,15 @@ struct benchmark_base } /// @} + /// Target accumulated GPU time for batched measurements. @{ + [[nodiscard]] nvbench::float64_t get_batch_target_time() const { return m_batch_target_time; } + benchmark_base &set_batch_target_time(nvbench::float64_t batch_target_time) + { + m_batch_target_time = batch_target_time; + return *this; + } + /// @} + /// If true, the benchmark does not use the blocking_kernel. This is intended /// for use with external profiling tools. @{ [[nodiscard]] bool get_disable_blocking_kernel() const { return m_disable_blocking_kernel; } @@ -237,8 +246,8 @@ struct benchmark_base /// If a warmup run finishes in less than `skip_time`, the measurement will /// be skipped. /// Extremely fast kernels (< 5000 ns) often timeout before they can - /// accumulate `min_time` measurements, and are often uninteresting. Setting - /// this value can help improve performance by skipping time consuming + /// accumulate enough measurement time, and are often uninteresting. Setting + /// this value can help improve performance by skipping time-consuming /// measurement that don't provide much information. /// Default value is -1., which disables the feature. /// @{ @@ -349,6 +358,7 @@ protected: nvbench::int64_t m_cold_warmup_runs{1}; nvbench::float64_t m_cold_max_warmup_walltime{-1.}; + nvbench::float64_t m_batch_target_time{0.5}; nvbench::float64_t m_skip_time{-1.}; nvbench::float64_t m_timeout{15.}; diff --git a/nvbench/benchmark_base.cxx b/nvbench/benchmark_base.cxx index 06db10bb..eb80b684 100644 --- a/nvbench/benchmark_base.cxx +++ b/nvbench/benchmark_base.cxx @@ -52,8 +52,9 @@ std::unique_ptr benchmark_base::clone() const result->m_cold_warmup_runs = m_cold_warmup_runs; result->m_cold_max_warmup_walltime = m_cold_max_warmup_walltime; - result->m_skip_time = m_skip_time; - result->m_timeout = m_timeout; + result->m_batch_target_time = m_batch_target_time; + result->m_skip_time = m_skip_time; + result->m_timeout = m_timeout; result->m_criterion_params = m_criterion_params; result->m_throttle_threshold = m_throttle_threshold; diff --git a/nvbench/detail/measure_hot.cu b/nvbench/detail/measure_hot.cu index fe36a57e..4a3e9262 100644 --- a/nvbench/detail/measure_hot.cu +++ b/nvbench/detail/measure_hot.cu @@ -27,6 +27,7 @@ #include #include +#include #include #include #include @@ -34,14 +35,28 @@ namespace nvbench::detail { +namespace +{ + +// Keep hot batches long enough to amortize launch and timing overhead. +constexpr nvbench::float64_t smallest_hot_batch_target_time = 100e-6; + +nvbench::float64_t normalize_batch_target_time(nvbench::float64_t target_time) +{ + if (!std::isfinite(target_time) || target_time <= nvbench::float64_t{0}) + { + return smallest_hot_batch_target_time; + } + return std::max(target_time, smallest_hot_batch_target_time); +} + +} // namespace measure_hot_base::measure_hot_base(state &exec_state) : m_state{exec_state} , m_launch{exec_state.get_cuda_stream()} , m_min_samples{exec_state.get_min_samples()} - , m_min_time{exec_state.get_criterion_params().has_value("min-time") - ? exec_state.get_criterion_params().get_float64("min-time") - : 0.5} + , m_batch_target_time{normalize_batch_target_time(exec_state.get_batch_target_time())} , m_skip_time{exec_state.get_skip_time()} , m_timeout{exec_state.get_timeout()} { @@ -59,12 +74,11 @@ measure_hot_base::measure_hot_base(state &exec_state) catch (...) { // If the above threw an exception, we don't have a cold measurement to use. - // Estimate a target_time between m_min_time and m_timeout. - // Use the average of the min_time and timeout, but don't go over 5x - // min_time in case timeout is huge. - // We could expose a `target_time` property on benchmark_base/state if - // needed. - m_min_time = std::min((m_min_time + m_timeout) / 2., m_min_time * 5); + // Estimate a target time between the configured batch target and timeout. + // Use their average, but don't go over 5x the configured target in case + // timeout is huge. + m_batch_target_time = normalize_batch_target_time( + std::min((m_batch_target_time + m_timeout) / 2., m_batch_target_time * 5)); } } @@ -131,15 +145,15 @@ void measure_hot_base::generate_summaries() m_total_samples, m_min_samples)); } - if (m_total_cuda_time < m_min_time) + if (m_total_cuda_time < m_batch_target_time) { printer.log(nvbench::log_level::warn, fmt::format("Current measurement timed out ({:0.2f}s) " - "before accumulating min_time ({:0.2f}s < " + "before accumulating batch target time ({:0.2f}s < " "{:0.2f}s)", timeout, m_total_cuda_time, - m_min_time)); + m_batch_target_time)); } } diff --git a/nvbench/detail/measure_hot.cuh b/nvbench/detail/measure_hot.cuh index c8fb808a..0d3cc9f7 100644 --- a/nvbench/detail/measure_hot.cuh +++ b/nvbench/detail/measure_hot.cuh @@ -93,7 +93,7 @@ protected: nvbench::blocking_kernel m_blocker; nvbench::int64_t m_min_samples{}; - nvbench::float64_t m_min_time{}; + nvbench::float64_t m_batch_target_time{}; nvbench::float64_t m_skip_time{}; nvbench::float64_t m_timeout{}; @@ -147,7 +147,7 @@ private: // The .95 factor here pads the batch_size a bit to avoid needing a second // batch due to noise. const auto time_estimate = m_cuda_timer.get_duration() * 0.95; - auto batch_size = static_cast(m_min_time / time_estimate); + auto batch_size = static_cast(m_batch_target_time / time_estimate); do { @@ -199,11 +199,11 @@ private: // Predict number of remaining iterations: batch_size = static_cast( - (m_min_time - m_total_cuda_time) / + (m_batch_target_time - m_total_cuda_time) / (m_total_cuda_time / static_cast(m_total_samples))); - if (m_total_cuda_time > m_min_time && // min time okay - m_total_samples >= m_min_samples) // min samples okay + if (m_total_cuda_time > m_batch_target_time && // batch target time okay + m_total_samples >= m_min_samples) // min samples okay { break; // Stop iterating } diff --git a/nvbench/json_printer.cu b/nvbench/json_printer.cu index 9871eaed..e06f25c9 100644 --- a/nvbench/json_printer.cu +++ b/nvbench/json_printer.cu @@ -476,6 +476,7 @@ void json_printer::do_print_benchmark_results(const benchmark_vector &benches) bench["min_samples"] = bench_ptr->get_min_samples(); bench["cold_warmup_runs"] = bench_ptr->get_cold_warmup_runs(); bench["cold_max_warmup_walltime"] = bench_ptr->get_cold_max_warmup_walltime(); + bench["batch_target_time"] = bench_ptr->get_batch_target_time(); bench["skip_time"] = bench_ptr->get_skip_time(); bench["timeout"] = bench_ptr->get_timeout(); @@ -535,6 +536,7 @@ void json_printer::do_print_benchmark_results(const benchmark_vector &benches) st["min_samples"] = exec_state.get_min_samples(); st["cold_warmup_runs"] = exec_state.get_cold_warmup_runs(); st["cold_max_warmup_walltime"] = exec_state.get_cold_max_warmup_walltime(); + st["batch_target_time"] = exec_state.get_batch_target_time(); st["skip_time"] = exec_state.get_skip_time(); st["timeout"] = exec_state.get_timeout(); diff --git a/nvbench/option_parser.cu b/nvbench/option_parser.cu index 00580838..0d3e7c74 100644 --- a/nvbench/option_parser.cu +++ b/nvbench/option_parser.cu @@ -38,6 +38,7 @@ #include #include +#include #include #include #include @@ -578,7 +579,8 @@ void option_parser::parse_range(option_parser::arg_iterator_t first, first += 2; } else if (arg == "--skip-time" || arg == "--timeout" || arg == "--cold-max-warmup-walltime" || - arg == "--throttle-threshold" || arg == "--throttle-recovery-delay") + arg == "--batch-target-time" || arg == "--throttle-threshold" || + arg == "--throttle-recovery-delay") { check_params(1); this->update_float64_prop(first[0], first[1]); @@ -1165,6 +1167,16 @@ try { bench.set_cold_max_warmup_walltime(value); } + else if (prop_arg == "--batch-target-time") + { + if (!std::isfinite(value) || value <= nvbench::float64_t{0}) + { + NVBENCH_THROW(std::runtime_error, + "{}", + "--batch-target-time must be a finite positive duration."); + } + bench.set_batch_target_time(value); + } else if (prop_arg == "--throttle-threshold") { bench.set_throttle_threshold(static_cast(value) / 100.0f); diff --git a/nvbench/state.cuh b/nvbench/state.cuh index fb506e8d..79812b1d 100644 --- a/nvbench/state.cuh +++ b/nvbench/state.cuh @@ -201,6 +201,14 @@ struct state void set_skip_batched(bool v) { m_skip_batched = v; } /// @} + /// Target accumulated GPU time for batched measurements. @{ + [[nodiscard]] nvbench::float64_t get_batch_target_time() const { return m_batch_target_time; } + void set_batch_target_time(nvbench::float64_t batch_target_time) + { + m_batch_target_time = batch_target_time; + } + /// @} + /// If true, the benchmark does not use the blocking_kernel. This is intended /// for use with external profiling tools. @{ [[nodiscard]] bool get_disable_blocking_kernel() const { return m_disable_blocking_kernel; } @@ -210,8 +218,8 @@ struct state /// If a warmup run finishes in less than `skip_time`, the measurement will /// be skipped. /// Extremely fast kernels (< 5000 ns) often timeout before they can - /// accumulate `min_time` measurements, and are often uninteresting. Setting - /// this value can help improve performance by skipping time consuming + /// accumulate enough measurement time, and are often uninteresting. Setting + /// this value can help improve performance by skipping time-consuming /// measurement that don't provide much information. /// Default value is -1., which disables the feature. /// @{ @@ -358,6 +366,7 @@ private: nvbench::int64_t m_cold_warmup_runs; nvbench::float64_t m_cold_max_warmup_walltime; + nvbench::float64_t m_batch_target_time; nvbench::float64_t m_skip_time; nvbench::float64_t m_timeout; diff --git a/nvbench/state.cxx b/nvbench/state.cxx index a81fe693..0de092cc 100644 --- a/nvbench/state.cxx +++ b/nvbench/state.cxx @@ -48,6 +48,7 @@ state::state(const benchmark_base &bench) , m_min_samples{bench.get_min_samples()} , m_cold_warmup_runs{bench.get_cold_warmup_runs()} , m_cold_max_warmup_walltime{bench.get_cold_max_warmup_walltime()} + , m_batch_target_time{bench.get_batch_target_time()} , m_skip_time{bench.get_skip_time()} , m_timeout{bench.get_timeout()} , m_throttle_threshold{bench.get_throttle_threshold()} @@ -72,6 +73,7 @@ state::state(const benchmark_base &bench, , m_min_samples{bench.get_min_samples()} , m_cold_warmup_runs{bench.get_cold_warmup_runs()} , m_cold_max_warmup_walltime{bench.get_cold_max_warmup_walltime()} + , m_batch_target_time{bench.get_batch_target_time()} , m_skip_time{bench.get_skip_time()} , m_timeout{bench.get_timeout()} , m_throttle_threshold{bench.get_throttle_threshold()} diff --git a/testing/option_parser.cu b/testing/option_parser.cu index 9634e90c..9dd00ea2 100644 --- a/testing/option_parser.cu +++ b/testing/option_parser.cu @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -1265,6 +1266,42 @@ void test_timeout() ASSERT(std::abs(states[0].get_timeout() - 12345e2) < 1.); } +void test_batch_target_time() +{ + { + nvbench::option_parser parser; + parser.parse({"--benchmark", "DummyBench", "--batch-target-time", "1.25"}); + const auto &states = parser_to_states(parser); + + ASSERT(states.size() == 1); + ASSERT(std::abs(states[0].get_batch_target_time() - 1.25) < 1e-6); + } + + { + nvbench::option_parser parser; + parser.parse({"--batch-target-time", "2.5", "--benchmark", "DummyBench"}); + const auto &states = parser_to_states(parser); + + ASSERT(states.size() == 1); + ASSERT(std::abs(states[0].get_batch_target_time() - 2.5) < 1e-6); + } + + { + nvbench::option_parser parser; + ASSERT_THROWS_ANY(parser.parse({"--benchmark", "DummyBench", "--batch-target-time", "0"})); + } + + { + nvbench::option_parser parser; + ASSERT_THROWS_ANY(parser.parse({"--benchmark", "DummyBench", "--batch-target-time", "-1"})); + } + + { + nvbench::option_parser parser; + ASSERT_THROWS_ANY(parser.parse({"--benchmark", "DummyBench", "--batch-target-time", "inf"})); + } +} + void test_json_stream_destinations() { { @@ -1677,6 +1714,7 @@ try test_skip_time(); test_cold_max_warmup_walltime(); test_timeout(); + test_batch_target_time(); test_json_stream_destinations(); test_output_parent_directories_created(); From a17657b2ebf0cbdca97a668b091c7dfc78438b1c Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:56:07 -0500 Subject: [PATCH 02/19] Add Python API for batch target time method --- python/cuda/bench/_decorators.py | 10 ++++++++++ python/src/py_nvbench.cpp | 32 ++++++++++++++++++++++++++++++++ python/test/test_cuda_bench.py | 11 +++++++++++ 3 files changed, 53 insertions(+) diff --git a/python/cuda/bench/_decorators.py b/python/cuda/bench/_decorators.py index a45f0e92..bee2fbc1 100644 --- a/python/cuda/bench/_decorators.py +++ b/python/cuda/bench/_decorators.py @@ -240,6 +240,16 @@ def set_timeout(self, duration_seconds: float) -> Callable[[_F], _F]: lambda benchmark: benchmark.set_timeout(duration_seconds) ) + def batch_target_time(self, duration_seconds: float) -> Callable[[_F], _F]: + """Set the target accumulated GPU time for batched measurements.""" + return self.set_batch_target_time(duration_seconds) + + def set_batch_target_time(self, duration_seconds: float) -> Callable[[_F], _F]: + """Set the target accumulated GPU time for batched measurements.""" + return _append_benchmark_action( + lambda benchmark: benchmark.set_batch_target_time(duration_seconds) + ) + def stopping_criterion(self, criterion: str) -> Callable[[_F], _F]: """Set the benchmark stopping criterion.""" return self.set_stopping_criterion(criterion) diff --git a/python/src/py_nvbench.cpp b/python/src/py_nvbench.cpp index 82894e1e..d37c3763 100644 --- a/python/src/py_nvbench.cpp +++ b/python/src/py_nvbench.cpp @@ -627,6 +627,21 @@ Set benchmark run duration timeout value, in seconds py::return_value_policy::reference, py::arg("duration_seconds")); + // method Benchmark.set_batch_target_time + auto method_set_batch_target_time_impl = [](nvbench::benchmark_base &self, + nvbench::float64_t duration_seconds) { + self.set_batch_target_time(duration_seconds); + return std::ref(self); + }; + static constexpr const char *method_set_batch_target_time_doc = R"XXXX( +Set target accumulated GPU time for batched measurements, in seconds +)XXXX"; + py_benchmark_cls.def("set_batch_target_time", + method_set_batch_target_time_impl, + method_set_batch_target_time_doc, + py::return_value_policy::reference, + py::arg("duration_seconds")); + // method Benchmark.set_throttle_threshold auto method_set_throttle_threshold_impl = [](nvbench::benchmark_base &self, nvbench::float32_t threshold) { @@ -809,6 +824,8 @@ void def_class_State(py::module_ m) // nvbench::state::get_skip_time // nvbench::state::set_timeout // nvbench::state::get_timeout + // nvbench::state::set_batch_target_time + // nvbench::state::get_batch_target_time // nvbench::state::set_throttle_threshold // nvbench::state::get_throttle_threshold // nvbench::state::set_throttle_recovery_delay @@ -1136,6 +1153,21 @@ Use argument True to disable use of blocking kernel by NVBench" method_set_timeout_doc, py::arg("duration_seconds")); + // method State.get_batch_target_time + static constexpr const char *method_get_batch_target_time_doc = + R"XXXX(Get target accumulated GPU time for batched measurements, in seconds)XXXX"; + pystate_cls.def("get_batch_target_time", + &nvbench::state::get_batch_target_time, + method_get_batch_target_time_doc); + + // method State.set_batch_target_time + static constexpr const char *method_set_batch_target_time_doc = + R"XXXX(Set target accumulated GPU time for batched measurements, in seconds)XXXX"; + pystate_cls.def("set_batch_target_time", + &nvbench::state::set_batch_target_time, + method_set_batch_target_time_doc, + py::arg("duration_seconds")); + // method State.get_blocking_kernel_timeout static constexpr const char *method_get_blocking_kernel_timeout_doc = R"XXXX(Get time-out value for execution of blocking kernel, in seconds)XXXX"; diff --git a/python/test/test_cuda_bench.py b/python/test/test_cuda_bench.py index de8a1dfe..67adc409 100644 --- a/python/test/test_cuda_bench.py +++ b/python/test/test_cuda_bench.py @@ -142,6 +142,8 @@ def test_decorator_docstrings(): obj_has_docstring_check(bench.option.set_throttle_threshold) obj_has_docstring_check(bench.option.timeout) obj_has_docstring_check(bench.option.set_timeout) + obj_has_docstring_check(bench.option.batch_target_time) + obj_has_docstring_check(bench.option.set_batch_target_time) obj_has_docstring_check(bench.option.stopping_criterion) obj_has_docstring_check(bench.option.set_stopping_criterion) obj_has_docstring_check(bench.option.criterion_param_float64) @@ -181,6 +183,10 @@ def set_cold_max_warmup_walltime(self, duration_seconds): self.calls.append(("cold_max_warmup_walltime", duration_seconds)) return self + def set_batch_target_time(self, duration_seconds): + self.calls.append(("batch_target_time", duration_seconds)) + return self + fake_benchmark = FakeBenchmark() registered_functions = [] @@ -195,6 +201,7 @@ def fake_register(fn): @bench.option.min_samples(11) @bench.option.cold_warmup_runs(7) @bench.option.cold_max_warmup_walltime(0.25) + @bench.option.batch_target_time(0.75) def decorated(state: bench.State): pass @@ -204,6 +211,7 @@ def decorated(state: bench.State): ("min_samples", 11), ("cold_warmup_runs", 7), ("cold_max_warmup_walltime", 0.25), + ("batch_target_time", 0.75), ] assert callable(decorated) @@ -314,6 +322,8 @@ def test_State_doc(): obj_has_docstring_check(cl.set_cold_warmup_runs) obj_has_docstring_check(cl.get_cold_max_warmup_walltime) obj_has_docstring_check(cl.set_cold_max_warmup_walltime) + obj_has_docstring_check(cl.get_batch_target_time) + obj_has_docstring_check(cl.set_batch_target_time) obj_has_docstring_check(cl.skip) @@ -344,3 +354,4 @@ def test_Benchmark_doc(): obj_has_docstring_check(cl.add_string_axis) obj_has_docstring_check(cl.set_cold_warmup_runs) obj_has_docstring_check(cl.set_cold_max_warmup_walltime) + obj_has_docstring_check(cl.set_batch_target_time) From 739474ed921d0175fd68df583e8bdf27861c7781 Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:07:37 -0500 Subject: [PATCH 03/19] Change to measure_hot_base::run_trials If time estimate results in batch size smaller than m_min_samples, use that instead so that batch does not degenerated into single launch and one does not observe the benefit of using warm L2-cache. Setting it this way permits batched measurement to complete in a single iteration of out loop. Also, update batch_size only after exit condition has been checked, and we know that m_batch_target_time is greater than m_total_cuda_time. Handle the possibility of `m_total_cuda_time` being zero. --- nvbench/detail/measure_hot.cuh | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/nvbench/detail/measure_hot.cuh b/nvbench/detail/measure_hot.cuh index 0d3cc9f7..1bd4b821 100644 --- a/nvbench/detail/measure_hot.cuh +++ b/nvbench/detail/measure_hot.cuh @@ -149,9 +149,14 @@ private: const auto time_estimate = m_cuda_timer.get_duration() * 0.95; auto batch_size = static_cast(m_batch_target_time / time_estimate); + const auto min_batch_size = std::max(m_min_samples, nvbench::int64_t{1}); + do { - batch_size = std::max(batch_size, nvbench::int64_t{1}); + // in m_batch_target_time is too small set the batch size to + // min_batch_size so that batch measurement + // completes in single iteration of this do/while loop + batch_size = std::max(batch_size, min_batch_size); nvbench::detail::stream_cleanup_guard cleanup{*this}; @@ -197,17 +202,25 @@ private: m_total_cuda_time += m_cuda_timer.get_duration(); m_total_samples += batch_size; - // Predict number of remaining iterations: - batch_size = static_cast( - (m_batch_target_time - m_total_cuda_time) / - (m_total_cuda_time / static_cast(m_total_samples))); - if (m_total_cuda_time > m_batch_target_time && // batch target time okay m_total_samples >= m_min_samples) // min samples okay { break; // Stop iterating } + // Predict number of remaining iterations: + if (m_total_cuda_time > 0.) + { + const auto remaining_time = m_batch_target_time - m_total_cuda_time; + const auto time_per_sample = m_total_cuda_time / + static_cast(m_total_samples); + batch_size = static_cast(remaining_time / time_per_sample); + } + else + { + batch_size *= 2; // Double the batch size if no time has elapsed. + } + m_walltime_timer.stop(); if (m_walltime_timer.get_duration() > m_timeout) { From 9fc4eec7415dd3f0b73e16fd0a5c5ebd099beae0 Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:25:14 -0500 Subject: [PATCH 04/19] Handle possible fp-exceptions during estimation of batch_size --- nvbench/detail/measure_hot.cu | 41 ++++++++++++++++++++++++++++++++++ nvbench/detail/measure_hot.cuh | 37 +++++++++++++++--------------- 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/nvbench/detail/measure_hot.cu b/nvbench/detail/measure_hot.cu index 4a3e9262..d1afd568 100644 --- a/nvbench/detail/measure_hot.cu +++ b/nvbench/detail/measure_hot.cu @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -82,6 +83,46 @@ measure_hot_base::measure_hot_base(state &exec_state) } } +nvbench::int64_t measure_hot_base::predict_batch_size(nvbench::float64_t target_time, + nvbench::float64_t time_estimate, + nvbench::int64_t fallback_batch_size) +{ + const auto fallback = std::max(fallback_batch_size, nvbench::int64_t{1}); + if (!std::isfinite(target_time) || target_time <= nvbench::float64_t{0} || + !std::isfinite(time_estimate) || time_estimate <= nvbench::float64_t{0}) + { + return fallback; + } + + const auto predicted_size = target_time / time_estimate; + if (!std::isfinite(predicted_size) || predicted_size <= static_cast(fallback)) + { + return fallback; + } + + if (predicted_size >= + static_cast(std::numeric_limits::max())) + { + return fallback; + } + + return static_cast(predicted_size); +} + +nvbench::int64_t measure_hot_base::grow_batch_size(nvbench::int64_t batch_size, + nvbench::int64_t minimum_batch_size) +{ + const auto fallback = std::max(minimum_batch_size, nvbench::int64_t{1}); + const auto batch = std::max(batch_size, fallback); + constexpr auto max_batch_size = std::numeric_limits::max(); + if (batch > max_batch_size / nvbench::int64_t{2}) + { + return max_batch_size; + } + + return std::max(batch * nvbench::int64_t{2}, fallback); +} + void measure_hot_base::check() { const auto device = m_state.get_device(); diff --git a/nvbench/detail/measure_hot.cuh b/nvbench/detail/measure_hot.cuh index 1bd4b821..004842c6 100644 --- a/nvbench/detail/measure_hot.cuh +++ b/nvbench/detail/measure_hot.cuh @@ -75,6 +75,14 @@ protected: void block_stream(); + static nvbench::int64_t predict_batch_size(nvbench::float64_t target_time, + nvbench::float64_t time_estimate, + nvbench::int64_t fallback_batch_size); + static nvbench::int64_t grow_batch_size(nvbench::int64_t batch_size, + nvbench::int64_t minimum_batch_size); + + static constexpr nvbench::int64_t minimum_hot_batch_size = 4; + __forceinline__ void unblock_stream() { m_blocker.unblock(); } __forceinline__ void unblock_stream_noexcept() noexcept { m_blocker.unblock_noexcept(); } @@ -146,16 +154,14 @@ private: // Use warmup results to estimate the number of iterations to run. // The .95 factor here pads the batch_size a bit to avoid needing a second // batch due to noise. - const auto time_estimate = m_cuda_timer.get_duration() * 0.95; - auto batch_size = static_cast(m_batch_target_time / time_estimate); - - const auto min_batch_size = std::max(m_min_samples, nvbench::int64_t{1}); + const auto min_batch_size = minimum_hot_batch_size; + const auto time_estimate = m_cuda_timer.get_duration() * 0.95; + auto batch_size = this->predict_batch_size(m_batch_target_time, time_estimate, min_batch_size); do { - // in m_batch_target_time is too small set the batch size to - // min_batch_size so that batch measurement - // completes in single iteration of this do/while loop + // If m_batch_target_time is too small, use min_batch_size so the + // batch measurement can complete in a single loop iteration. batch_size = std::max(batch_size, min_batch_size); nvbench::detail::stream_cleanup_guard cleanup{*this}; @@ -209,17 +215,12 @@ private: } // Predict number of remaining iterations: - if (m_total_cuda_time > 0.) - { - const auto remaining_time = m_batch_target_time - m_total_cuda_time; - const auto time_per_sample = m_total_cuda_time / - static_cast(m_total_samples); - batch_size = static_cast(remaining_time / time_per_sample); - } - else - { - batch_size *= 2; // Double the batch size if no time has elapsed. - } + const auto remaining_time = m_batch_target_time - m_total_cuda_time; + const auto time_per_sample = m_total_cuda_time / + static_cast(m_total_samples); + batch_size = this->predict_batch_size(remaining_time, + time_per_sample, + this->grow_batch_size(batch_size, min_batch_size)); m_walltime_timer.stop(); if (m_walltime_timer.get_duration() > m_timeout) From 689d766d478355fcc7ffac4f912bfe0983e55ddc Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Tue, 28 Jul 2026 07:52:17 -0500 Subject: [PATCH 05/19] Expand notes for batch-target-time --- docs/cli_help.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/cli_help.md b/docs/cli_help.md index 8b0d759a..d146ccf4 100644 --- a/docs/cli_help.md +++ b/docs/cli_help.md @@ -139,6 +139,10 @@ * `--batch-target-time ` * Target accumulated GPU time for batched measurements. * Default is 0.5 seconds. + * `` must be finite and positive. + * Batched measurements continue until both `--min-samples` and the + accumulated GPU-time target are satisfied, unless `--timeout` is reached + first. * Applies to the most recent `--benchmark`, or all benchmarks if specified before any `--benchmark` arguments. From 04a40bf3da72fbc547d227602e31a7ce7fb78920 Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Tue, 28 Jul 2026 07:53:03 -0500 Subject: [PATCH 06/19] Better error message on unrecognized stopping criterion parameter When an unknown CLI option is encountered, we try it as a stopping criterion parameter. The error message now states the option that tripped the error, active stopping criterion and its parameters --- nvbench/option_parser.cu | 78 +++++++++++++++++++++++++------- testing/option_parser.cu | 96 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 17 deletions(-) diff --git a/nvbench/option_parser.cu b/nvbench/option_parser.cu index 0d3e7c74..49284eeb 100644 --- a/nvbench/option_parser.cu +++ b/nvbench/option_parser.cu @@ -111,6 +111,60 @@ std::string_view submatch_to_sv(const sv_submatch &in) } //============================================================================== +std::string format_criterion_param_options(const nvbench::criterion_params ¶ms) +{ + auto names = params.get_names(); + if (names.empty()) + { + return ""; + } + + fmt::memory_buffer buffer; + bool first = true; + for (const auto &name : names) + { + if (!first) + { + fmt::format_to(fmt::appender(buffer), ", "); + } + fmt::format_to(fmt::appender(buffer), "--{}", name); + first = false; + } + return fmt::to_string(buffer); +} + +[[noreturn]] void +throw_unrecognized_criterion_param(const std::string &prop_arg, + const std::string &criterion_name, + const nvbench::criterion_params &criterion_params) +{ + NVBENCH_THROW(std::runtime_error, + "{} is not valid for the active stopping criterion '{}'.\n" + " Current criterion '{}' accepts: {}.", + prop_arg, + criterion_name, + criterion_name, + format_criterion_param_options(criterion_params)); +} + +std::string current_global_stopping_criterion(const std::vector &global_args) +{ + std::string criterion_name = nvbench::detail::default_stopping_criterion(); + for (auto arg_it = global_args.cbegin(); arg_it != global_args.cend(); ++arg_it) + { + if (*arg_it == "--stopping-criterion") + { + const auto value_it = std::next(arg_it); + if (value_it != global_args.cend()) + { + criterion_name = *value_it; + arg_it = value_it; + } + } + } + return criterion_name; +} + // These numeric overloads /could/ be written in a single function using // std::from_chars, but charconv is a mess on GCC. Even GCC 10 only partially // implements it (missing support for floats). @@ -1074,20 +1128,11 @@ try // If no active benchmark, save args as global. if (m_benchmarks.empty()) { - // Any global params must either belong to the default criterion or follow a - // `--stopping-criterion` arg: - nvbench::criterion_params params = - criterion_manager::get() - .get_criterion(nvbench::detail::default_stopping_criterion()) - .get_params(); - if (!params.has_value(name) && - std::find(m_global_benchmark_args.cbegin(), - m_global_benchmark_args.cend(), - "--stopping-criterion") == m_global_benchmark_args.cend()) + const auto criterion_name = current_global_stopping_criterion(m_global_benchmark_args); + const auto params = criterion_manager::get().get_criterion(criterion_name).get_params(); + if (!params.has_value(name)) { - NVBENCH_THROW(std::runtime_error, - "Unrecognized stopping criterion parameter: `{}` for default criterion.", - name); + throw_unrecognized_criterion_param(prop_arg, criterion_name, params); } m_global_benchmark_args.push_back(prop_arg); @@ -1099,10 +1144,9 @@ try if (!bench.has_criterion_param(name)) { - NVBENCH_THROW(std::runtime_error, - "Unrecognized stopping criterion parameter: `{}` for `{}`.", - name, - bench.get_stopping_criterion()); + throw_unrecognized_criterion_param(prop_arg, + bench.get_stopping_criterion(), + bench.get_criterion_params()); } if (type == nvbench::named_values::type::float64) diff --git a/testing/option_parser.cu b/testing/option_parser.cu index 9dd00ea2..f85bee1e 100644 --- a/testing/option_parser.cu +++ b/testing/option_parser.cu @@ -25,8 +25,11 @@ #include #include +#include #include #include +#include +#include #include #if __has_include() @@ -157,6 +160,30 @@ struct temp_tree return states_to_string(parser_to_states(parser)); } +void assert_parse_error_contains(std::vector args, + std::initializer_list snippets) +{ + try + { + nvbench::option_parser parser; + parser.parse(std::move(args)); + } + catch (const std::runtime_error &ex) + { + const std::string message = ex.what(); + for (const auto snippet : snippets) + { + ASSERT_MSG(message.find(std::string{snippet}) != std::string::npos, + "Expected error message to contain `{}`. Message:\n{}", + snippet, + message); + } + return; + } + + ASSERT_MSG(false, "Expected parser error.", ""); +} + } // namespace void test_empty() @@ -1268,6 +1295,15 @@ void test_timeout() void test_batch_target_time() { + { + nvbench::option_parser parser; + parser.parse({"--benchmark", "DummyBench"}); + const auto &states = parser_to_states(parser); + + ASSERT(states.size() == 1); + ASSERT(std::abs(states[0].get_batch_target_time() - 0.5) < 1e-6); + } + { nvbench::option_parser parser; parser.parse({"--benchmark", "DummyBench", "--batch-target-time", "1.25"}); @@ -1286,6 +1322,36 @@ void test_batch_target_time() ASSERT(std::abs(states[0].get_batch_target_time() - 2.5) < 1e-6); } + { + nvbench::option_parser parser; + parser.parse({ + "--batch-target-time", + "2.5", + "--benchmark", + "DummyBench", + "--batch-target-time", + "1.0", + "--benchmark", + "TestBench", + }); + + const auto &benches = parser.get_benchmarks(); + ASSERT(benches.size() == 2); + ASSERT(benches[0] != nullptr); + ASSERT(benches[1] != nullptr); + + const auto dummy_states = nvbench::detail::state_generator::create(*benches[0]); + ASSERT(dummy_states.size() == 1); + ASSERT(std::abs(dummy_states[0].get_batch_target_time() - 1.0) < 1e-6); + + const auto test_states = nvbench::detail::state_generator::create(*benches[1]); + ASSERT(!test_states.empty()); + for (const auto &state : test_states) + { + ASSERT(std::abs(state.get_batch_target_time() - 2.5) < 1e-6); + } + } + { nvbench::option_parser parser; ASSERT_THROWS_ANY(parser.parse({"--benchmark", "DummyBench", "--batch-target-time", "0"})); @@ -1421,6 +1487,36 @@ void test_stopping_criterion() ASSERT(criterion_params.get_float64("max-angle") == 0.42); ASSERT(criterion_params.get_float64("min-r2") == 0.6); } + { // Global criterion rejects params it does not accept: + assert_parse_error_contains( + { + "--stopping-criterion", + "entropy", + "--min-time", + "0.1", + "--benchmark", + "DummyBench", + }, + { + "--min-time is not valid for the active stopping criterion 'entropy'.", + "Current criterion 'entropy' accepts: --max-angle, --min-r2.", + }); + } + { // Per-benchmark criterion rejects params it does not accept: + assert_parse_error_contains( + { + "--benchmark", + "DummyBench", + "--stopping-criterion", + "entropy", + "--min-time", + "0.1", + }, + { + "--min-time is not valid for the active stopping criterion 'entropy'.", + "Current criterion 'entropy' accepts: --max-angle, --min-r2.", + }); + } { // Global params to default criterion should work: nvbench::option_parser parser; parser.parse({ From 82b06fe5453f27761064b2bf41c14f50fcdcadcc Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Tue, 28 Jul 2026 08:24:19 -0500 Subject: [PATCH 07/19] Corrected termination check actual_time > target_time to actual_time >= target_time --- nvbench/detail/measure_hot.cuh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nvbench/detail/measure_hot.cuh b/nvbench/detail/measure_hot.cuh index 004842c6..b9c4b4ed 100644 --- a/nvbench/detail/measure_hot.cuh +++ b/nvbench/detail/measure_hot.cuh @@ -208,8 +208,8 @@ private: m_total_cuda_time += m_cuda_timer.get_duration(); m_total_samples += batch_size; - if (m_total_cuda_time > m_batch_target_time && // batch target time okay - m_total_samples >= m_min_samples) // min samples okay + if (m_total_cuda_time >= m_batch_target_time && // batch target time okay + m_total_samples >= m_min_samples) // min samples okay { break; // Stop iterating } From 7d8eb11162cc42c391a518b87ecb9480a05dac4b Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:06:18 -0500 Subject: [PATCH 08/19] Validate batch-target-time in setter methods using common helper Remove dedicated validation in option parser, a rely on validation in benchmark setter method instead. --- nvbench/benchmark_base.cuh | 2 + nvbench/detail/validate_batch_target_time.cuh | 47 +++++++++++++++++++ nvbench/option_parser.cu | 6 --- nvbench/state.cuh | 2 + 4 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 nvbench/detail/validate_batch_target_time.cuh diff --git a/nvbench/benchmark_base.cuh b/nvbench/benchmark_base.cuh index 91cb0d5d..6d297f87 100644 --- a/nvbench/benchmark_base.cuh +++ b/nvbench/benchmark_base.cuh @@ -29,6 +29,7 @@ #endif #include +#include #include #include #include @@ -228,6 +229,7 @@ struct benchmark_base [[nodiscard]] nvbench::float64_t get_batch_target_time() const { return m_batch_target_time; } benchmark_base &set_batch_target_time(nvbench::float64_t batch_target_time) { + nvbench::detail::validate_batch_target_time(batch_target_time); m_batch_target_time = batch_target_time; return *this; } diff --git a/nvbench/detail/validate_batch_target_time.cuh b/nvbench/detail/validate_batch_target_time.cuh new file mode 100644 index 00000000..1f92f19e --- /dev/null +++ b/nvbench/detail/validate_batch_target_time.cuh @@ -0,0 +1,47 @@ +/* + * Copyright 2026 NVIDIA Corporation + * + * Licensed under the Apache License, Version 2.0 with the LLVM exception + * (the "License"); you may not use this file except in compliance with + * the License. + * + * You may obtain a copy of the License at + * + * http://llvm.org/foundation/relicensing/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#if defined(NVBENCH_IMPLICIT_SYSTEM_HEADER_GCC) +#pragma GCC system_header +#elif defined(NVBENCH_IMPLICIT_SYSTEM_HEADER_CLANG) +#pragma clang system_header +#elif defined(NVBENCH_IMPLICIT_SYSTEM_HEADER_MSVC) +#pragma system_header +#endif + +#include + +#include +#include + +namespace nvbench::detail +{ + +inline void validate_batch_target_time(nvbench::float64_t batch_target_time) +{ + if (!std::isfinite(batch_target_time) || batch_target_time <= nvbench::float64_t{0}) + { + throw std::invalid_argument{"batch_target_time must be finite and positive."}; + } +} + +} // namespace nvbench::detail diff --git a/nvbench/option_parser.cu b/nvbench/option_parser.cu index 49284eeb..15164f68 100644 --- a/nvbench/option_parser.cu +++ b/nvbench/option_parser.cu @@ -1213,12 +1213,6 @@ try } else if (prop_arg == "--batch-target-time") { - if (!std::isfinite(value) || value <= nvbench::float64_t{0}) - { - NVBENCH_THROW(std::runtime_error, - "{}", - "--batch-target-time must be a finite positive duration."); - } bench.set_batch_target_time(value); } else if (prop_arg == "--throttle-threshold") diff --git a/nvbench/state.cuh b/nvbench/state.cuh index 79812b1d..7a03d678 100644 --- a/nvbench/state.cuh +++ b/nvbench/state.cuh @@ -29,6 +29,7 @@ #endif #include +#include #include #include #include @@ -205,6 +206,7 @@ struct state [[nodiscard]] nvbench::float64_t get_batch_target_time() const { return m_batch_target_time; } void set_batch_target_time(nvbench::float64_t batch_target_time) { + nvbench::detail::validate_batch_target_time(batch_target_time); m_batch_target_time = batch_target_time; } /// @} From 40d40fd36396fd697d197de5d00ceb1025cb8551 Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:07:42 -0500 Subject: [PATCH 09/19] Add validation for setting batch-target-time in Python --- python/test/test_cuda_bench.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/python/test/test_cuda_bench.py b/python/test/test_cuda_bench.py index 67adc409..e5379d94 100644 --- a/python/test/test_cuda_bench.py +++ b/python/test/test_cuda_bench.py @@ -86,6 +86,26 @@ def cold_warmup_state_probe(state: bench.State): state.exec(lambda launch: None) + def batch_target_state_probe(state: bench.State): + observed["benchmark_batch_target_time"] = state.get_batch_target_time() + + state.set_batch_target_time(0.125) + observed["state_batch_target_time"] = state.get_batch_target_time() + + for duration_seconds in [0.0, -1.0, float("inf"), float("nan")]: + with pytest.raises(ValueError, match="finite and positive"): + state.set_batch_target_time(duration_seconds) + + state.exec(lambda launch: None) + + batch_target_benchmark = bench.register(batch_target_state_probe) + batch_target_benchmark.set_is_cpu_only(True) + batch_target_benchmark.set_batch_target_time(0.75) + + for duration_seconds in [0.0, -1.0, float("inf"), float("nan")]: + with pytest.raises(ValueError, match="finite and positive"): + batch_target_benchmark.set_batch_target_time(duration_seconds) + bench.run_all_benchmarks(["-q", "--profile"]) assert saved_timers @@ -97,6 +117,8 @@ def cold_warmup_state_probe(state: bench.State): "benchmark_walltime": 0.5, "state_runs": 3, "state_walltime": 0.125, + "benchmark_batch_target_time": 0.75, + "state_batch_target_time": 0.125, } From 8ff12a96b735c91230614ff9830d63bb220c706b Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:05:08 -0500 Subject: [PATCH 10/19] Remove no-cold overwrite of m_batch_target_time, and use of floor The floor existed to prevent batch-size from degenerating to 1, which we now cap at 4 launches, so use of time floor is unnecessary. Removed no-cold measurements overwrite of batch target time, as it leads to non-transparent to the user significant increase in the target time used (say target time is default at 0.5 seconds, and timeout is default at 15 seconds, the overwrite would bump target time to 2.5 seconds). Present day NVBench only starts measure_hot after measure_cold has completed by default. If test is skipped/encountered errors during cold measurement loop, the measure_hot is not reached. The only way for a user to do batched measurements with cold measurments unavailable would be to use state::exec(nvbench::exec_tag::hot, launchable); But even in this case we want to honor requested `--batch-target-time` value. --- nvbench/detail/measure_hot.cu | 29 ++++------------------------- nvbench/detail/measure_hot.cuh | 4 ++-- 2 files changed, 6 insertions(+), 27 deletions(-) diff --git a/nvbench/detail/measure_hot.cu b/nvbench/detail/measure_hot.cu index d1afd568..8effce9d 100644 --- a/nvbench/detail/measure_hot.cu +++ b/nvbench/detail/measure_hot.cu @@ -36,37 +36,21 @@ namespace nvbench::detail { -namespace -{ - -// Keep hot batches long enough to amortize launch and timing overhead. -constexpr nvbench::float64_t smallest_hot_batch_target_time = 100e-6; - -nvbench::float64_t normalize_batch_target_time(nvbench::float64_t target_time) -{ - if (!std::isfinite(target_time) || target_time <= nvbench::float64_t{0}) - { - return smallest_hot_batch_target_time; - } - return std::max(target_time, smallest_hot_batch_target_time); -} - -} // namespace measure_hot_base::measure_hot_base(state &exec_state) : m_state{exec_state} , m_launch{exec_state.get_cuda_stream()} , m_min_samples{exec_state.get_min_samples()} - , m_batch_target_time{normalize_batch_target_time(exec_state.get_batch_target_time())} + , m_batch_target_time{exec_state.get_batch_target_time()} , m_skip_time{exec_state.get_skip_time()} , m_timeout{exec_state.get_timeout()} { - // Since cold measures converge to a stable result, increase the min_samples - // to match the cold result if available. try { nvbench::int64_t cold_samples = m_state.get_summary("nv/cold/sample_size").get_int64("value"); - m_min_samples = std::max(m_min_samples, cold_samples); + // Since cold measures converge to a stable result, increase the min_samples + // to match the cold result if available. + m_min_samples = std::max(m_min_samples, cold_samples); // If the cold measurement ran successfully, disable skip_time. It'd just // be annoying to skip now. @@ -75,11 +59,6 @@ measure_hot_base::measure_hot_base(state &exec_state) catch (...) { // If the above threw an exception, we don't have a cold measurement to use. - // Estimate a target time between the configured batch target and timeout. - // Use their average, but don't go over 5x the configured target in case - // timeout is huge. - m_batch_target_time = normalize_batch_target_time( - std::min((m_batch_target_time + m_timeout) / 2., m_batch_target_time * 5)); } } diff --git a/nvbench/detail/measure_hot.cuh b/nvbench/detail/measure_hot.cuh index b9c4b4ed..4ec17145 100644 --- a/nvbench/detail/measure_hot.cuh +++ b/nvbench/detail/measure_hot.cuh @@ -160,8 +160,8 @@ private: do { - // If m_batch_target_time is too small, use min_batch_size so the - // batch measurement can complete in a single loop iteration. + // Keep hot measurements batched even when the configured target time is + // smaller than a few launch durations. batch_size = std::max(batch_size, min_batch_size); nvbench::detail::stream_cleanup_guard cleanup{*this}; From c121a91bbdd6e7f06d914bb162e33e68842b7b41 Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:28:31 -0500 Subject: [PATCH 11/19] Do not use compact namespace per coding guidelines --- nvbench/detail/validate_batch_target_time.cuh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nvbench/detail/validate_batch_target_time.cuh b/nvbench/detail/validate_batch_target_time.cuh index 1f92f19e..e255eb62 100644 --- a/nvbench/detail/validate_batch_target_time.cuh +++ b/nvbench/detail/validate_batch_target_time.cuh @@ -33,7 +33,9 @@ #include #include -namespace nvbench::detail +namespace nvbench +{ +namespace detail { inline void validate_batch_target_time(nvbench::float64_t batch_target_time) @@ -44,4 +46,5 @@ inline void validate_batch_target_time(nvbench::float64_t batch_target_time) } } -} // namespace nvbench::detail +} // namespace detail +} // namespace nvbench From ece54e436e88a9afb2cbd98b5f0aab6198e7f3e4 Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:46:14 -0500 Subject: [PATCH 12/19] Update testing/option_parser.cu Also test --batch-target-time nan --- testing/option_parser.cu | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/testing/option_parser.cu b/testing/option_parser.cu index f85bee1e..aa386e65 100644 --- a/testing/option_parser.cu +++ b/testing/option_parser.cu @@ -1366,6 +1366,10 @@ void test_batch_target_time() nvbench::option_parser parser; ASSERT_THROWS_ANY(parser.parse({"--benchmark", "DummyBench", "--batch-target-time", "inf"})); } + { + nvbench::option_parser parser; + ASSERT_THROWS_ANY(parser.parse({"--benchmark", "DummyBench", "--batch-target-time", "nan"})); + } } void test_json_stream_destinations() From 236cf11d2b13d76bdfa7edcaf25949309925e043 Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:50:07 -0500 Subject: [PATCH 13/19] measure_hot now estimates batch_size two ways and takes smallest estimate 1. run_warmup is modified to measure launch + sync duration using wall_timer to get initial estimate of wallclock duration 2. run_trials computes two estimates for batch size. - one of remaining cuda-timer budget (clipped >= 4) - one of ramaining walltime timeout budget (clipped >= 1) the smallest of these is taken. This way, if timeout of generous, and --batch-target-time is small, we still batch at least 4 launches to ensure that measurement loop probes benefits of warm L2-cache, or persistent data. When time-out budget is tight, we do not use 4 launches anymore, making measure loop less likely to overshoot timeout budget by more than wall-clock duration of a single submission. --- nvbench/detail/measure_hot.cuh | 43 ++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/nvbench/detail/measure_hot.cuh b/nvbench/detail/measure_hot.cuh index 4ec17145..a39aeb48 100644 --- a/nvbench/detail/measure_hot.cuh +++ b/nvbench/detail/measure_hot.cuh @@ -137,11 +137,16 @@ private: { nvbench::detail::stream_cleanup_guard cleanup{*this}; - m_cuda_timer.start(m_launch.get_stream()); - this->launch_kernel(); - m_cuda_timer.stop(m_launch.get_stream()); + m_walltime_timer.start(); + { + m_cuda_timer.start(m_launch.get_stream()); + this->launch_kernel(); + m_cuda_timer.stop(m_launch.get_stream()); - this->sync_stream(); + this->sync_stream(); + } + // get wall-clock estimate of launch execution + m_walltime_timer.stop(); cleanup.release(); this->check_skip_time(m_cuda_timer.get_duration()); @@ -149,20 +154,25 @@ private: void run_trials() { + const auto wallclock_time_initial_estimate = m_walltime_timer.get_duration(); + const auto cuda_time_initial_estimate = m_cuda_timer.get_duration(); + m_walltime_timer.start(); // Use warmup results to estimate the number of iterations to run. // The .95 factor here pads the batch_size a bit to avoid needing a second // batch due to noise. + const auto time_estimate = cuda_time_initial_estimate * 0.95; const auto min_batch_size = minimum_hot_batch_size; - const auto time_estimate = m_cuda_timer.get_duration() * 0.95; auto batch_size = this->predict_batch_size(m_batch_target_time, time_estimate, min_batch_size); + const nvbench::int64_t timeout_min_batch_size = 1; + auto timeout_batch_size = + this->predict_batch_size(m_timeout, wallclock_time_initial_estimate, timeout_min_batch_size); + do { - // Keep hot measurements batched even when the configured target time is - // smaller than a few launch durations. - batch_size = std::max(batch_size, min_batch_size); + batch_size = std::min(batch_size, timeout_batch_size); nvbench::detail::stream_cleanup_guard cleanup{*this}; @@ -214,20 +224,29 @@ private: break; // Stop iterating } - // Predict number of remaining iterations: + const auto sample_count = static_cast(m_total_samples); + + // Predict number of remaining iterations based on cuda-time budget const auto remaining_time = m_batch_target_time - m_total_cuda_time; - const auto time_per_sample = m_total_cuda_time / - static_cast(m_total_samples); + const auto time_per_sample = m_total_cuda_time / sample_count; batch_size = this->predict_batch_size(remaining_time, time_per_sample, this->grow_batch_size(batch_size, min_batch_size)); m_walltime_timer.stop(); - if (m_walltime_timer.get_duration() > m_timeout) + const auto total_walltime = m_walltime_timer.get_duration(); + if (total_walltime > m_timeout) { m_max_time_exceeded = true; break; } + + // predict number of ramaining iterations based on timeout budget + const auto remaining_walltime = m_timeout - total_walltime; + const auto walltime_per_sample = total_walltime / sample_count; + timeout_batch_size = + this->predict_batch_size(remaining_walltime, walltime_per_sample, timeout_min_batch_size); + } while (true); m_walltime_timer.stop(); From 613a02fd1beb0564077fbbaa5be006035e7fa6e6 Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:18:53 -0500 Subject: [PATCH 14/19] Add missing Python stubs for batch-target-time setters/getters --- python/cuda/bench/__init__.pyi | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/python/cuda/bench/__init__.pyi b/python/cuda/bench/__init__.pyi index 5be55327..e3b64531 100644 --- a/python/cuda/bench/__init__.pyi +++ b/python/cuda/bench/__init__.pyi @@ -59,6 +59,7 @@ class Benchmark: def set_throttle_recovery_delay(self, delay_seconds: SupportsFloat) -> Self: ... def set_throttle_threshold(self, threshold: SupportsFloat) -> Self: ... def set_timeout(self, duration_seconds: SupportsFloat) -> Self: ... + def set_batch_target_time(self, duration_seconds: SupportsFloat) -> Self: ... def set_stopping_criterion(self, criterion: str) -> Self: ... def set_criterion_param_float64(self, name: str, value: SupportsFloat) -> Self: ... def set_criterion_param_int64(self, name: str, value: SupportsInt) -> Self: ... @@ -117,6 +118,8 @@ class State: def set_run_once(self, run_once_flag: bool) -> None: ... def get_timeout(self) -> float: ... def set_timeout(self, duration: SupportsFloat) -> None: ... + def get_batch_target_time(self) -> float: ... + def set_batch_target_time(self, duration_seconds: SupportsFloat) -> None: ... def get_blocking_kernel_timeout(self) -> float: ... def set_blocking_kernel_timeout(self, duration: SupportsFloat) -> None: ... @overload @@ -193,6 +196,12 @@ class _OptionDecorators: ) -> Callable[[_F], _F]: ... def timeout(self, duration_seconds: SupportsFloat) -> Callable[[_F], _F]: ... def set_timeout(self, duration_seconds: SupportsFloat) -> Callable[[_F], _F]: ... + def batch_target_time( + self, duration_seconds: SupportsFloat + ) -> Callable[[_F], _F]: ... + def set_batch_target_time( + self, duration_seconds: SupportsFloat + ) -> Callable[[_F], _F]: ... def stopping_criterion(self, criterion: str) -> Callable[[_F], _F]: ... def set_stopping_criterion(self, criterion: str) -> Callable[[_F], _F]: ... def criterion_param_float64( From bc114ce722081d5677f7f79d2914cca3be1aa0b7 Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:48:33 -0500 Subject: [PATCH 15/19] Support --timeout inf in measure_hot_base::run_trials() logic --- nvbench/detail/measure_hot.cu | 2 ++ nvbench/detail/measure_hot.cuh | 31 ++++++++++++++++++++++--------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/nvbench/detail/measure_hot.cu b/nvbench/detail/measure_hot.cu index 8effce9d..9a987960 100644 --- a/nvbench/detail/measure_hot.cu +++ b/nvbench/detail/measure_hot.cu @@ -62,6 +62,8 @@ measure_hot_base::measure_hot_base(state &exec_state) } } +bool measure_hot_base::use_timeout_batch_cap() const { return std::isfinite(m_timeout); } + nvbench::int64_t measure_hot_base::predict_batch_size(nvbench::float64_t target_time, nvbench::float64_t time_estimate, nvbench::int64_t fallback_batch_size) diff --git a/nvbench/detail/measure_hot.cuh b/nvbench/detail/measure_hot.cuh index a39aeb48..6ba39aaa 100644 --- a/nvbench/detail/measure_hot.cuh +++ b/nvbench/detail/measure_hot.cuh @@ -80,6 +80,7 @@ protected: nvbench::int64_t fallback_batch_size); static nvbench::int64_t grow_batch_size(nvbench::int64_t batch_size, nvbench::int64_t minimum_batch_size); + [[nodiscard]] bool use_timeout_batch_cap() const; static constexpr nvbench::int64_t minimum_hot_batch_size = 4; @@ -166,13 +167,22 @@ private: const auto min_batch_size = minimum_hot_batch_size; auto batch_size = this->predict_batch_size(m_batch_target_time, time_estimate, min_batch_size); + const bool use_timeout_batch_cap = this->use_timeout_batch_cap(); const nvbench::int64_t timeout_min_batch_size = 1; - auto timeout_batch_size = - this->predict_batch_size(m_timeout, wallclock_time_initial_estimate, timeout_min_batch_size); + nvbench::int64_t timeout_batch_size = timeout_min_batch_size; + if (use_timeout_batch_cap) + { + timeout_batch_size = this->predict_batch_size(m_timeout, + wallclock_time_initial_estimate, + timeout_min_batch_size); + } do { - batch_size = std::min(batch_size, timeout_batch_size); + if (use_timeout_batch_cap) + { + batch_size = std::min(batch_size, timeout_batch_size); + } nvbench::detail::stream_cleanup_guard cleanup{*this}; @@ -235,17 +245,20 @@ private: m_walltime_timer.stop(); const auto total_walltime = m_walltime_timer.get_duration(); - if (total_walltime > m_timeout) + if (use_timeout_batch_cap && total_walltime > m_timeout) { m_max_time_exceeded = true; break; } - // predict number of ramaining iterations based on timeout budget - const auto remaining_walltime = m_timeout - total_walltime; - const auto walltime_per_sample = total_walltime / sample_count; - timeout_batch_size = - this->predict_batch_size(remaining_walltime, walltime_per_sample, timeout_min_batch_size); + if (use_timeout_batch_cap) + { + // Predict number of remaining iterations based on timeout budget. + const auto remaining_walltime = m_timeout - total_walltime; + const auto walltime_per_sample = total_walltime / sample_count; + timeout_batch_size = + this->predict_batch_size(remaining_walltime, walltime_per_sample, timeout_min_batch_size); + } } while (true); From 5053cd3ec88031e6307da593ea758c9d09ffad57 Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Thu, 30 Jul 2026 22:32:40 -0500 Subject: [PATCH 16/19] Move helper predicates to .cu file to avoid pull include cmake into header Strengthen computation of batch size to fallback on invalid estimate from quotient of durations. This keep very large time-out values for which quotient value exceed int64 max value to be effectively equivalent to inf. --- nvbench/benchmark_base.cuh | 8 +--- nvbench/benchmark_base.cxx | 8 ++++ nvbench/detail/measure_hot.cu | 80 +++++++++++++++++++++++++--------- nvbench/detail/measure_hot.cuh | 59 +++++++++++-------------- nvbench/state.cuh | 7 +-- nvbench/state.cxx | 7 +++ 6 files changed, 102 insertions(+), 67 deletions(-) diff --git a/nvbench/benchmark_base.cuh b/nvbench/benchmark_base.cuh index 6d297f87..d3900f48 100644 --- a/nvbench/benchmark_base.cuh +++ b/nvbench/benchmark_base.cuh @@ -29,7 +29,6 @@ #endif #include -#include #include #include #include @@ -227,12 +226,7 @@ struct benchmark_base /// Target accumulated GPU time for batched measurements. @{ [[nodiscard]] nvbench::float64_t get_batch_target_time() const { return m_batch_target_time; } - benchmark_base &set_batch_target_time(nvbench::float64_t batch_target_time) - { - nvbench::detail::validate_batch_target_time(batch_target_time); - m_batch_target_time = batch_target_time; - return *this; - } + benchmark_base &set_batch_target_time(nvbench::float64_t batch_target_time); /// @} /// If true, the benchmark does not use the blocking_kernel. This is intended diff --git a/nvbench/benchmark_base.cxx b/nvbench/benchmark_base.cxx index eb80b684..d9f25697 100644 --- a/nvbench/benchmark_base.cxx +++ b/nvbench/benchmark_base.cxx @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -65,6 +66,13 @@ std::unique_ptr benchmark_base::clone() const return result; } +benchmark_base &benchmark_base::set_batch_target_time(nvbench::float64_t batch_target_time) +{ + nvbench::detail::validate_batch_target_time(batch_target_time); + m_batch_target_time = batch_target_time; + return *this; +} + benchmark_base &benchmark_base::set_devices(std::vector device_ids) { std::vector devices; diff --git a/nvbench/detail/measure_hot.cu b/nvbench/detail/measure_hot.cu index 9a987960..e5199c25 100644 --- a/nvbench/detail/measure_hot.cu +++ b/nvbench/detail/measure_hot.cu @@ -37,6 +37,40 @@ namespace nvbench::detail { +namespace +{ + +nvbench::int64_t predict_batch_size(nvbench::float64_t target_duration, + nvbench::float64_t duration_per_launch, + nvbench::int64_t minimum_size, + nvbench::int64_t fallback_size_on_invalid_prediction) +{ + const auto clamped_min_size = std::max(minimum_size, nvbench::int64_t{1}); + const auto clamped_fallback = std::max(fallback_size_on_invalid_prediction, nvbench::int64_t{1}); + if (!std::isfinite(target_duration) || target_duration <= nvbench::float64_t{0} || + !std::isfinite(duration_per_launch) || duration_per_launch <= nvbench::float64_t{0}) + { + return clamped_fallback; + } + + const auto predicted_launches = target_duration / duration_per_launch; + if (!std::isfinite(predicted_launches) || + predicted_launches >= + static_cast(std::numeric_limits::max())) + { + return clamped_fallback; + } + + if (predicted_launches <= static_cast(clamped_min_size)) + { + return clamped_min_size; + } + + return static_cast(predicted_launches); +} + +} // namespace + measure_hot_base::measure_hot_base(state &exec_state) : m_state{exec_state} , m_launch{exec_state.get_cuda_stream()} @@ -62,32 +96,36 @@ measure_hot_base::measure_hot_base(state &exec_state) } } -bool measure_hot_base::use_timeout_batch_cap() const { return std::isfinite(m_timeout); } - -nvbench::int64_t measure_hot_base::predict_batch_size(nvbench::float64_t target_time, - nvbench::float64_t time_estimate, - nvbench::int64_t fallback_batch_size) +// CUDA-time predictions choose how many launches are needed to reach the +// accumulated GPU-time target. Valid small predictions are raised to the +// supplied minimum; invalid or overflowing predictions fall back to the +// caller-provided conservative batch size, usually m_min_samples. +nvbench::int64_t +measure_hot_base::predict_cuda_batch_size(nvbench::float64_t target_time, + nvbench::float64_t time_estimate, + nvbench::int64_t minimum_batch_size, + nvbench::int64_t fallback_on_invalid_prediction) { - const auto fallback = std::max(fallback_batch_size, nvbench::int64_t{1}); - if (!std::isfinite(target_time) || target_time <= nvbench::float64_t{0} || - !std::isfinite(time_estimate) || time_estimate <= nvbench::float64_t{0}) - { - return fallback; - } - - const auto predicted_size = target_time / time_estimate; - if (!std::isfinite(predicted_size) || predicted_size <= static_cast(fallback)) - { - return fallback; - } + return predict_batch_size(target_time, + time_estimate, + minimum_batch_size, + fallback_on_invalid_prediction); +} - if (predicted_size >= - static_cast(std::numeric_limits::max())) +// Timeout predictions are caps on the CUDA-time batch estimate. They only +// shrink the CUDA estimate when the wall-time model produces a meaningful +// finite cap; exhausted budgets return one launch, while non-finite or +// overflowing predictions return the CUDA estimate. +nvbench::int64_t measure_hot_base::predict_timeout_batch_cap(nvbench::float64_t target_time, + nvbench::float64_t time_estimate, + nvbench::int64_t cuda_batch_size) +{ + if (target_time <= nvbench::float64_t{0}) { - return fallback; + return nvbench::int64_t{1}; } - return static_cast(predicted_size); + return predict_batch_size(target_time, time_estimate, nvbench::int64_t{1}, cuda_batch_size); } nvbench::int64_t measure_hot_base::grow_batch_size(nvbench::int64_t batch_size, diff --git a/nvbench/detail/measure_hot.cuh b/nvbench/detail/measure_hot.cuh index 6ba39aaa..f04968c7 100644 --- a/nvbench/detail/measure_hot.cuh +++ b/nvbench/detail/measure_hot.cuh @@ -75,12 +75,15 @@ protected: void block_stream(); - static nvbench::int64_t predict_batch_size(nvbench::float64_t target_time, - nvbench::float64_t time_estimate, - nvbench::int64_t fallback_batch_size); + static nvbench::int64_t predict_cuda_batch_size(nvbench::float64_t target_time, + nvbench::float64_t time_estimate, + nvbench::int64_t minimum_batch_size, + nvbench::int64_t fallback_on_invalid_prediction); + static nvbench::int64_t predict_timeout_batch_cap(nvbench::float64_t target_time, + nvbench::float64_t time_estimate, + nvbench::int64_t cuda_batch_size); static nvbench::int64_t grow_batch_size(nvbench::int64_t batch_size, nvbench::int64_t minimum_batch_size); - [[nodiscard]] bool use_timeout_batch_cap() const; static constexpr nvbench::int64_t minimum_hot_batch_size = 4; @@ -163,26 +166,17 @@ private: // Use warmup results to estimate the number of iterations to run. // The .95 factor here pads the batch_size a bit to avoid needing a second // batch due to noise. - const auto time_estimate = cuda_time_initial_estimate * 0.95; - const auto min_batch_size = minimum_hot_batch_size; - auto batch_size = this->predict_batch_size(m_batch_target_time, time_estimate, min_batch_size); - - const bool use_timeout_batch_cap = this->use_timeout_batch_cap(); - const nvbench::int64_t timeout_min_batch_size = 1; - nvbench::int64_t timeout_batch_size = timeout_min_batch_size; - if (use_timeout_batch_cap) - { - timeout_batch_size = this->predict_batch_size(m_timeout, - wallclock_time_initial_estimate, - timeout_min_batch_size); - } + const auto time_estimate = cuda_time_initial_estimate * 0.95; + auto batch_size = this->predict_cuda_batch_size(m_batch_target_time, + time_estimate, + minimum_hot_batch_size, + m_min_samples); + auto timeout_batch_size = + this->predict_timeout_batch_cap(m_timeout, wallclock_time_initial_estimate, batch_size); do { - if (use_timeout_batch_cap) - { - batch_size = std::min(batch_size, timeout_batch_size); - } + batch_size = std::min(batch_size, timeout_batch_size); nvbench::detail::stream_cleanup_guard cleanup{*this}; @@ -239,26 +233,25 @@ private: // Predict number of remaining iterations based on cuda-time budget const auto remaining_time = m_batch_target_time - m_total_cuda_time; const auto time_per_sample = m_total_cuda_time / sample_count; - batch_size = this->predict_batch_size(remaining_time, - time_per_sample, - this->grow_batch_size(batch_size, min_batch_size)); + batch_size = + this->predict_cuda_batch_size(remaining_time, + time_per_sample, + this->grow_batch_size(batch_size, minimum_hot_batch_size), + m_min_samples); m_walltime_timer.stop(); const auto total_walltime = m_walltime_timer.get_duration(); - if (use_timeout_batch_cap && total_walltime > m_timeout) + if (total_walltime > m_timeout) { m_max_time_exceeded = true; break; } - if (use_timeout_batch_cap) - { - // Predict number of remaining iterations based on timeout budget. - const auto remaining_walltime = m_timeout - total_walltime; - const auto walltime_per_sample = total_walltime / sample_count; - timeout_batch_size = - this->predict_batch_size(remaining_walltime, walltime_per_sample, timeout_min_batch_size); - } + // Predict number of remaining iterations based on timeout budget. + const auto remaining_walltime = m_timeout - total_walltime; + const auto walltime_per_sample = total_walltime / sample_count; + timeout_batch_size = + this->predict_timeout_batch_cap(remaining_walltime, walltime_per_sample, batch_size); } while (true); diff --git a/nvbench/state.cuh b/nvbench/state.cuh index 7a03d678..ba0721bf 100644 --- a/nvbench/state.cuh +++ b/nvbench/state.cuh @@ -29,7 +29,6 @@ #endif #include -#include #include #include #include @@ -204,11 +203,7 @@ struct state /// Target accumulated GPU time for batched measurements. @{ [[nodiscard]] nvbench::float64_t get_batch_target_time() const { return m_batch_target_time; } - void set_batch_target_time(nvbench::float64_t batch_target_time) - { - nvbench::detail::validate_batch_target_time(batch_target_time); - m_batch_target_time = batch_target_time; - } + void set_batch_target_time(nvbench::float64_t batch_target_time); /// @} /// If true, the benchmark does not use the blocking_kernel. This is intended diff --git a/nvbench/state.cxx b/nvbench/state.cxx index 0de092cc..23166e7f 100644 --- a/nvbench/state.cxx +++ b/nvbench/state.cxx @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -81,6 +82,12 @@ state::state(const benchmark_base &bench, , m_cuda_stream{std::nullopt} {} +void state::set_batch_target_time(nvbench::float64_t batch_target_time) +{ + nvbench::detail::validate_batch_target_time(batch_target_time); + m_batch_target_time = batch_target_time; +} + nvbench::int64_t state::get_int64(const std::string &axis_name) const { return m_axis_values.get_int64(axis_name); From abdbac9c52512de7a5b8c378629fe579a13aa1a6 Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Fri, 31 Jul 2026 07:58:22 -0500 Subject: [PATCH 17/19] Correct batch-size re-estimate when target-batch-time is reached When total CUDA time is greater than target batch size, estimation function will return fallback-on-invalid-estimate. This fallback used to be m_min_samples, but it should be remaining_sample to reach that exit target. --- nvbench/detail/measure_hot.cuh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/nvbench/detail/measure_hot.cuh b/nvbench/detail/measure_hot.cuh index f04968c7..c5ae8c0b 100644 --- a/nvbench/detail/measure_hot.cuh +++ b/nvbench/detail/measure_hot.cuh @@ -231,13 +231,19 @@ private: const auto sample_count = static_cast(m_total_samples); // Predict number of remaining iterations based on cuda-time budget - const auto remaining_time = m_batch_target_time - m_total_cuda_time; - const auto time_per_sample = m_total_cuda_time / sample_count; + const auto remaining_time = m_batch_target_time - m_total_cuda_time; + const auto time_per_sample = m_total_cuda_time / sample_count; + const auto remaining_samples_to_minimum = std::max(m_min_samples - m_total_samples, + nvbench::int64_t{1}); + const auto batch_target_time_satisfied = remaining_time <= nvbench::float64_t{0}; + const auto fallback_size_on_invalid_cuda_prediction = batch_target_time_satisfied + ? remaining_samples_to_minimum + : m_min_samples; batch_size = this->predict_cuda_batch_size(remaining_time, time_per_sample, this->grow_batch_size(batch_size, minimum_hot_batch_size), - m_min_samples); + fallback_size_on_invalid_cuda_prediction); m_walltime_timer.stop(); const auto total_walltime = m_walltime_timer.get_duration(); From 28e8dd967b5e94a0d2e62f2b74b4bf379db4d2f2 Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Fri, 31 Jul 2026 08:01:02 -0500 Subject: [PATCH 18/19] Make description of --timeout option more generic re was is logged The precise logging depends on measure-loop used and stopping criterion --- docs/cli_help.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/cli_help.md b/docs/cli_help.md index d146ccf4..59de7b47 100644 --- a/docs/cli_help.md +++ b/docs/cli_help.md @@ -152,9 +152,9 @@ * Measurements will timeout after `` have elapsed. * Default is 15 seconds. * `` is walltime, not accumulated sample time. - * If a measurement times out, the default markdown log will print a warning to - report any outstanding termination criteria (min samples, batch target time, - max noise). + * If a measurement times out, the default markdown log will report which + termination conditions were still unmet. The exact warnings depend on the + measurement type and active stopping criterion. * Applies to the most recent `--benchmark`, or all benchmarks if specified before any `--benchmark` arguments. From 64d88684c0378b24fdfbdc2d19b8bc76d5053b58 Mon Sep 17 00:00:00 2001 From: Oleksandr Pavlyk <21087696+oleksandr-pavlyk@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:25:50 -0500 Subject: [PATCH 19/19] Initial batch-size estimate to use min(m_min_samples, 4) This way very large --timeout values would not use more than --min-samples if set very low. --- nvbench/detail/measure_hot.cuh | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/nvbench/detail/measure_hot.cuh b/nvbench/detail/measure_hot.cuh index c5ae8c0b..e53088b2 100644 --- a/nvbench/detail/measure_hot.cuh +++ b/nvbench/detail/measure_hot.cuh @@ -166,11 +166,13 @@ private: // Use warmup results to estimate the number of iterations to run. // The .95 factor here pads the batch_size a bit to avoid needing a second // batch due to noise. - const auto time_estimate = cuda_time_initial_estimate * 0.95; - auto batch_size = this->predict_cuda_batch_size(m_batch_target_time, - time_estimate, - minimum_hot_batch_size, - m_min_samples); + const auto hot_batch_size_floor = std::min(std::max(m_min_samples, nvbench::int64_t{1}), + minimum_hot_batch_size); + const auto time_estimate = cuda_time_initial_estimate * 0.95; + auto batch_size = this->predict_cuda_batch_size(m_batch_target_time, + time_estimate, + hot_batch_size_floor, + m_min_samples); auto timeout_batch_size = this->predict_timeout_batch_cap(m_timeout, wallclock_time_initial_estimate, batch_size); @@ -242,7 +244,7 @@ private: batch_size = this->predict_cuda_batch_size(remaining_time, time_per_sample, - this->grow_batch_size(batch_size, minimum_hot_batch_size), + this->grow_batch_size(batch_size, hot_batch_size_floor), fallback_size_on_invalid_cuda_prediction); m_walltime_timer.stop();