diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index c6e227558e..5212732e9a 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -44,7 +44,9 @@ function(kvikio_add_benchmark) CUDA_STANDARD_REQUIRED ON ) - target_link_libraries(${_KVIKIO_NAME} PUBLIC benchmark::benchmark kvikio::kvikio) + target_link_libraries( + ${_KVIKIO_NAME} PUBLIC benchmark::benchmark kvikio::kvikio CUDA::cudart_static + ) if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") set(KVIKIO_CXX_FLAGS "-Wall;-Werror;-Wno-unknown-pragmas") @@ -60,3 +62,7 @@ function(kvikio_add_benchmark) endfunction() kvikio_add_benchmark(NAME THREADPOOL_BENCHMARK SOURCES "threadpool/threadpool_benchmark.cpp") + +kvikio_add_benchmark( + NAME SEQUENTIAL_BENCHMARK SOURCES "io/sequential_benchmark.cpp" "io/common.cpp" +) diff --git a/cpp/benchmarks/io/common.cpp b/cpp/benchmarks/io/common.cpp new file mode 100644 index 0000000000..de25dae56b --- /dev/null +++ b/cpp/benchmarks/io/common.cpp @@ -0,0 +1,302 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "common.hpp" + +#include + +#include +#include +#include + +#include + +namespace kvikio::benchmark { + +Backend parse_backend(std::string const& str) +{ + if (str.empty()) { throw std::invalid_argument("Empty size string"); } + + // Normalize to lowercase for case-insensitive comparison + auto backend{str}; + std::transform(backend.begin(), backend.end(), backend.begin(), [](unsigned char c) { + return std::tolower(c); + }); + if (backend == "filehandle") { + return Backend::FILEHANDLE; + } else if (backend == "cufile") { + return Backend::CUFILE; + } else { + throw std::invalid_argument("Invalid flag: '" + str + "' (use \"FileHandle\", \"cuFile\") "); + } +} + +std::size_t parse_size(std::string const& str) +{ + if (str.empty()) { throw std::invalid_argument("Empty size string"); } + + // Parse the numeric part + std::size_t pos{}; + double value{}; + try { + value = std::stod(str, &pos); + } catch (std::exception const& e) { + throw std::invalid_argument("Invalid size format: " + str); + } + + if (value < 0) { throw std::invalid_argument("Size cannot be negative"); } + + // Extract suffix (everything after the number) + auto suffix = str.substr(pos); + + // No suffix means raw bytes + if (suffix.empty()) { return static_cast(value); } + + // Normalize to lowercase for case-insensitive comparison + std::transform( + suffix.begin(), suffix.end(), suffix.begin(), [](unsigned char c) { return std::tolower(c); }); + + // All multipliers use 1024 (binary), not 1000 + std::size_t multiplier{1}; + + // Support both K/Ki, M/Mi, etc. as synonyms (all 1024-based) + std::size_t constexpr one_Ki{1024ULL}; + std::size_t constexpr one_Mi{1024ULL * one_Ki}; + std::size_t constexpr one_Gi{1024ULL * one_Mi}; + std::size_t constexpr one_Ti{1024ULL * one_Gi}; + std::size_t constexpr one_Pi{1024ULL * one_Ti}; + if (suffix == "k" || suffix == "ki" || suffix == "kb" || suffix == "kib") { + multiplier = one_Ki; + } else if (suffix == "m" || suffix == "mi" || suffix == "mb" || suffix == "mib") { + multiplier = one_Mi; + } else if (suffix == "g" || suffix == "gi" || suffix == "gb" || suffix == "gib") { + multiplier = one_Gi; + } else if (suffix == "t" || suffix == "ti" || suffix == "tb" || suffix == "tib") { + multiplier = one_Ti; + } else if (suffix == "p" || suffix == "pi" || suffix == "pb" || suffix == "pib") { + multiplier = one_Pi; + } else { + throw std::invalid_argument( + "Invalid size suffix: '" + suffix + + "' (use K/Ki/KB/KiB, M/Mi/MB/MiB, G/Gi/GB/GiB, T/Ti/TB/TiB, or P/Pi/PB/PiB)"); + } + + return static_cast(value * multiplier); +} + +bool parse_flag(std::string const& str) +{ + if (str.empty()) { throw std::invalid_argument("Empty flag"); } + + // Normalize to lowercase for case-insensitive comparison + auto result{str}; + std::transform( + result.begin(), result.end(), result.begin(), [](unsigned char c) { return std::tolower(c); }); + + if (result == "true" || result == "on" || result == "yes" || result == "1") { + return true; + } else if (result == "false" || result == "off" || result == "no" || result == "0") { + return false; + } else { + throw std::invalid_argument("Invalid flag: '" + str + + "' (use true/false, on/off, yes/no, or 1/0)"); + } +} + +Backend parse_backend(int argc, char** argv) +{ + constexpr int BACKEND = 1000; + Backend result{FILEHANDLE}; + static option long_options[] = { + {"backend", required_argument, nullptr, BACKEND}, {0, 0, 0, 0} + // Sentinel to mark the end of the array. Needed by getopt_long() + }; + + int opt{0}; + int option_index{-1}; + while ((opt = getopt_long(argc, argv, "-:", long_options, &option_index)) != -1) { + switch (opt) { + case BACKEND: { + result = parse_backend(optarg); + break; + } + case ':': { + // The parsed option has missing argument + std::stringstream ss; + ss << "Missing argument for option " << argv[optind - 1] << " (-" + << static_cast(optopt) << ")"; + throw std::runtime_error(ss.str()); + break; + } + default: { + // Unknown option is deferred to subsequent parsing, if any + break; + } + } + } + + // Reset getopt state for second pass in the future + optind = 0; + + return result; +} + +void Config::parse_args(int argc, char** argv) +{ + enum LongOnlyOpts { + O_DIRECT = 1000, + ALIGN_BUFFER, + DROP_CACHE, + OPEN_ONCE, + }; + + static option long_options[] = { + {"file", required_argument, nullptr, 'f'}, + {"size", required_argument, nullptr, 's'}, + {"threads", required_argument, nullptr, 't'}, + {"use-gpu-buffer", required_argument, nullptr, 'g'}, + {"gpu-index", required_argument, nullptr, 'd'}, + {"repetitions", required_argument, nullptr, 'r'}, + {"o-direct", required_argument, nullptr, LongOnlyOpts::O_DIRECT}, + {"align-buffer", required_argument, nullptr, LongOnlyOpts::ALIGN_BUFFER}, + {"drop-cache", required_argument, nullptr, LongOnlyOpts::DROP_CACHE}, + {"overwrite", required_argument, nullptr, 'w'}, + {"open-once", required_argument, nullptr, LongOnlyOpts::OPEN_ONCE}, + {"help", no_argument, nullptr, 'h'}, + {0, 0, 0, 0} // Sentinel to mark the end of the array. Needed by getopt_long() + }; + + int opt{0}; + int option_index{-1}; + + // - By default getopt_long() returns '?' to indicate errors if an option has missing argument or + // if an unknown option is encountered. The starting ':' in the optstring modifies this behavior. + // Missing argument error now causes the return value to be ':'. Unknow option still leads to '?' + // and its processing is deferred. + // - "f:" means option "-f" takes an argument + // - "c" means option "-c" does not take an argument + while ((opt = getopt_long(argc, argv, "-:f:s:t:g:d:r:w:h", long_options, &option_index)) != -1) { + switch (opt) { + case 'f': { + filepaths.push_back(optarg); + break; + } + case 's': { + num_bytes = parse_size(optarg); // Helper to parse "1G", "500M", etc. + break; + } + case 't': { + num_threads = std::stoul(optarg); + break; + } + case 'g': { + use_gpu_buffer = parse_flag(optarg); + break; + } + case 'd': { + gpu_index = std::stoi(optarg); + break; + } + case 'r': { + repetition = std::stoi(optarg); + break; + } + case 'w': { + overwrite_file = parse_flag(optarg); + break; + } + case LongOnlyOpts::O_DIRECT: { + o_direct = parse_flag(optarg); + break; + } + case LongOnlyOpts::ALIGN_BUFFER: { + align_buffer = parse_flag(optarg); + break; + } + case LongOnlyOpts::DROP_CACHE: { + drop_file_cache = parse_flag(optarg); + break; + } + case LongOnlyOpts::OPEN_ONCE: { + open_file_once = parse_flag(optarg); + break; + } + case 'h': { + print_usage(argv[0]); + std::exit(0); + break; + } + case ':': { + // The parsed option has missing argument + std::stringstream ss; + ss << "Missing argument for option " << argv[optind - 1] << " (-" + << static_cast(optopt) << ")"; + throw std::runtime_error(ss.str()); + break; + } + default: { + // Unknown option is deferred to subsequent parsing, if any + break; + } + } + } + + // Validation + if (filepaths.empty()) { throw std::invalid_argument("--file is required"); } + + // Reset getopt state for second pass in the future + optind = 0; +} + +void Config::print_usage(std::string const& program_name) +{ + std::cout + << "Usage: " << program_name << " [OPTIONS]\n\n" + << "Options:\n" + << " -f, --file PATH File path to benchmark (required, repeatable)\n" + << " -s, --size SIZE Number of bytes to read (default: 4G)\n" + << " Supports suffixes: K, M, G, T, P\n" + << " -t, --threads NUM Number of threads (default: 1)\n" + << " -r, --repetitions NUM Number of repetitions (default: 5)\n" + << " -g, --use-gpu-buffer BOOL Use GPU device memory (default: false)\n" + << " -d, --gpu-index INDEX GPU device index (default: 0)\n" + << " -w, --overwrite BOOL Overwrite existing file (default: false)\n" + << " --o-direct BOOL Use O_DIRECT (default: true)\n" + << " --align-buffer BOOL Use aligned buffer (default: true)\n" + << " --drop-cache BOOL Drop page cache before each run (default: false)\n" + << " --open-once BOOL Open file once, not per repetition (default: false)\n" + << " -h, --help Show this help message\n"; +} + +void* CudaPageAlignedDeviceAllocator::allocate(std::size_t size) +{ + void* buffer{}; + auto const page_size = get_page_size(); + auto const up_size = size + page_size; + KVIKIO_CHECK_CUDA(cudaMalloc(&buffer, up_size)); + auto* aligned_buffer = detail::align_up(buffer, page_size); + return aligned_buffer; +} + +void CudaPageAlignedDeviceAllocator::deallocate(void* buffer, std::size_t /*size*/) {} + +CuFileHandle::CuFileHandle(std::string const& file_path, + std::string const& flags, + bool o_direct, + mode_t mode) + : _file_wrapper(file_path, flags, o_direct, mode) +{ + _cufile_handle_wrapper.register_handle(_file_wrapper.fd()); +} + +void CuFileHandle::close() +{ + _cufile_handle_wrapper.unregister_handle(); + _file_wrapper.close(); +} + +CUfileHandle_t CuFileHandle::handle() const noexcept { return _cufile_handle_wrapper.handle(); } + +} // namespace kvikio::benchmark diff --git a/cpp/benchmarks/io/common.hpp b/cpp/benchmarks/io/common.hpp new file mode 100644 index 0000000000..c150c3f2cf --- /dev/null +++ b/cpp/benchmarks/io/common.hpp @@ -0,0 +1,231 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#define KVIKIO_CHECK_CUDA(err_code) kvikio::benchmark::check_cuda(err_code, __FILE__, __LINE__) + +namespace kvikio::benchmark { +inline void check_cuda(cudaError_t err_code, const char* file, int line) +{ + if (err_code == cudaError_t::cudaSuccess) { return; } + std::stringstream ss; + int current_device{}; + cudaGetDevice(¤t_device); + ss << "CUDA runtime error on device " << current_device << ": " << cudaGetErrorName(err_code) + << " (" << err_code << "): " << cudaGetErrorString(err_code) << " at " << file << ":" << line + << "\n"; + throw std::runtime_error(ss.str()); +} + +enum Backend { + FILEHANDLE, + CUFILE, +}; + +Backend parse_backend(std::string const& str); +Backend parse_backend(int argc, char** argv); + +// Helper to parse size strings like "1GiB", "1Gi", "1G". +std::size_t parse_size(std::string const& str); + +bool parse_flag(std::string const& str); + +struct Config { + std::vector filepaths; + std::size_t num_bytes{4ull * 1024ull * 1024ull * 1024ull}; + unsigned int num_threads{1}; + bool use_gpu_buffer{false}; + int gpu_index{0}; + int repetition{5}; + bool overwrite_file{false}; + bool o_direct{true}; + bool align_buffer{true}; + bool drop_file_cache{false}; + bool open_file_once{false}; + + virtual void parse_args(int argc, char** argv); + virtual void print_usage(std::string const& program_name); +}; + +template +class Benchmark { + protected: + ConfigType const& _config; + + void initialize() { static_cast(this)->initialize_impl(); } + void cleanup() { static_cast(this)->cleanup_impl(); } + void run_target() { static_cast(this)->run_target_impl(); } + std::size_t nbytes() { return static_cast(this)->nbytes_impl(); } + + public: + Benchmark(ConfigType const& config) : _config(config) + { + defaults::set_thread_pool_nthreads(_config.num_threads); + } + + void run() + { + if (_config.open_file_once) { initialize(); } + + decltype(_config.repetition) count{0}; + double time_elapsed_total_us{0.0}; + for (decltype(_config.repetition) idx = 0; idx < _config.repetition; ++idx) { + if (_config.drop_file_cache) { kvikio::clear_page_cache(); } + + if (!_config.open_file_once) { initialize(); } + + auto start = std::chrono::steady_clock::now(); + run_target(); + auto end = std::chrono::steady_clock::now(); + + std::chrono::duration time_elapsed = end - start; + double time_elapsed_us = time_elapsed.count(); + if (idx > 0) { + ++count; + time_elapsed_total_us += time_elapsed_us; + } + double bandwidth = nbytes() / time_elapsed_us * 1e6 / 1024.0 / 1024.0; + std::cout << std::string(4, ' ') << std::left << std::setw(4) << idx << std::setw(10) + << bandwidth << " [MiB/s]" << std::endl; + + if (!_config.open_file_once) { cleanup(); } + } + double average_bandwidth = nbytes() * count / time_elapsed_total_us * 1e6 / 1024.0 / 1024.0; + std::cout << std::string(4, ' ') << "Average bandwidth: " << std::setw(10) << average_bandwidth + << " [MiB/s]" << std::endl; + + if (_config.open_file_once) { cleanup(); } + } +}; + +class CudaPageAlignedDeviceAllocator { + public: + void* allocate(std::size_t size); + + void deallocate(void* buffer, std::size_t size); +}; + +template +class Buffer { + public: + Buffer(ConfigType config) : _config(config) { allocate(); } + ~Buffer() { deallocate(); } + + Buffer(Buffer const&) = delete; + Buffer& operator=(Buffer const&) = delete; + + Buffer(Buffer&& o) noexcept + : _config(std::exchange(o._config, {})), + _data(std::exchange(o._data, {})), + _original_data(std::exchange(o._original_data, {})) + { + } + + Buffer& operator=(Buffer&& o) noexcept + { + if (this == &o) { return *this; } + deallocate(); + _config = std::exchange(o._config, {}); + _data = std::exchange(o._data, {}); + _original_data = std::exchange(o._original_data, {}); + } + + void* data() const { return _data; } + void* size() const { return _size; } + + private: + void allocate() + { + if (_config.use_gpu_buffer) { + KVIKIO_CHECK_CUDA(cudaSetDevice(_config.gpu_index)); + if (_config.align_buffer) { + CudaPageAlignedDeviceAllocator alloc; + _original_data = alloc.allocate(_config.num_bytes); + } else { + KVIKIO_CHECK_CUDA(cudaMalloc(&_original_data, _config.num_bytes)); + } + } else { + if (_config.align_buffer) { + PageAlignedAllocator alloc; + _original_data = alloc.allocate(_config.num_bytes); + } else { + _original_data = std::malloc(_config.num_bytes); + } + } + + _data = _original_data; + } + + void deallocate() + { + if (_config.use_gpu_buffer) { + if (_config.align_buffer) { + } else { + KVIKIO_CHECK_CUDA(cudaFree(_original_data)); + } + } else { + std::free(_original_data); + } + + _data = nullptr; + _original_data = nullptr; + _size = 0; + } + + ConfigType _config; + void* _data{}; + void* _original_data{}; + std::size_t _size{}; +}; + +template +void create_file(ConfigType const& config, + std::string const& filepath, + Buffer const& buf) +{ + // Create the file if the overwrite flag is on, or if the file does not exist, or if the file size + // is wrong. + if (config.overwrite_file || access(filepath.c_str(), F_OK) != 0 || + get_file_size(filepath) != config.num_bytes) { + FileHandle file_handle(filepath, "w", FileHandle::m644); + auto fut = file_handle.pwrite(buf.data(), config.num_bytes); + fut.get(); + } +} + +class CuFileHandle { + public: + CuFileHandle(std::string const& file_path, std::string const& flags, bool o_direct, mode_t mode); + ~CuFileHandle() = default; + + CuFileHandle(CuFileHandle const&) = delete; + CuFileHandle& operator=(CuFileHandle const&) = delete; + + CuFileHandle(CuFileHandle&&) noexcept = default; + CuFileHandle& operator=(CuFileHandle&&) = default; + + void close(); + + CUfileHandle_t handle() const noexcept; + + private: + FileWrapper _file_wrapper; + CUFileHandleWrapper _cufile_handle_wrapper; +}; +} // namespace kvikio::benchmark diff --git a/cpp/benchmarks/io/sequential_benchmark.cpp b/cpp/benchmarks/io/sequential_benchmark.cpp new file mode 100644 index 0000000000..82f12ca4dc --- /dev/null +++ b/cpp/benchmarks/io/sequential_benchmark.cpp @@ -0,0 +1,221 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "sequential_benchmark.hpp" +#include "common.hpp" +#include "kvikio/shim/cufile.hpp" + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace kvikio::benchmark { + +void KvikIOSequentialConfig::parse_args(int argc, char** argv) +{ + Config::parse_args(argc, argv); + static option long_options[] = { + {0, 0, 0, 0} // Sentinel to mark the end of the array. Needed by getopt_long() + + }; + + int opt{0}; + int option_index{-1}; + + while ((opt = getopt_long(argc, argv, "-:", long_options, &option_index)) != -1) { + switch (opt) { + case ':': { + // The parsed option has missing argument + std::stringstream ss; + ss << "Missing argument for option " << argv[optind - 1] << " (-" + << static_cast(optopt) << ")"; + throw std::runtime_error(ss.str()); + break; + } + default: { + // Unknown option is deferred to subsequent parsing, if any + break; + } + } + } + + // Reset getopt state for second pass in the future + optind = 0; +} + +void KvikIOSequentialConfig::print_usage(std::string const& program_name) +{ + Config::print_usage(program_name); +} + +KvikIOSequentialBenchmark::KvikIOSequentialBenchmark(KvikIOSequentialConfig const& config) + : Benchmark(config) +{ + for (auto const& filepath : _config.filepaths) { + _bufs.emplace_back(std::make_unique>(_config)); + create_file(_config, filepath, *_bufs.back()); + } +} + +KvikIOSequentialBenchmark::~KvikIOSequentialBenchmark() {} + +void KvikIOSequentialBenchmark::initialize_impl() +{ + _file_handles.clear(); + + for (auto const& filepath : _config.filepaths) { + auto p = std::make_unique(filepath, "r"); + + if (_config.o_direct) { + auto file_status_flags = fcntl(p->fd(), F_GETFL); + SYSCALL_CHECK(file_status_flags); + SYSCALL_CHECK(fcntl(p->fd(), F_SETFL, file_status_flags | O_DIRECT)); + } + + _file_handles.push_back(std::move(p)); + } +} + +void KvikIOSequentialBenchmark::cleanup_impl() +{ + for (auto&& file_handle : _file_handles) { + file_handle->close(); + } +} + +void KvikIOSequentialBenchmark::run_target_impl() +{ + std::vector> futs; + + for (std::size_t i = 0; i < _file_handles.size(); ++i) { + auto fut = _file_handles[i]->pread(_bufs[i]->data(), _config.num_bytes); + futs.push_back(std::move(fut)); + } + + for (auto&& fut : futs) { + fut.get(); + } +} + +std::size_t KvikIOSequentialBenchmark::nbytes_impl() +{ + return _config.num_bytes * _config.filepaths.size(); +} + +void CuFileSequentialConfig::parse_args(int argc, char** argv) +{ + Config::parse_args(argc, argv); + static option long_options[] = { + {0, 0, 0, 0} // Sentinel to mark the end of the array. Needed by getopt_long() + + }; + + int opt{0}; + int option_index{-1}; + + while ((opt = getopt_long(argc, argv, "-:", long_options, &option_index)) != -1) { + switch (opt) { + case ':': { + // The parsed option has missing argument + std::stringstream ss; + ss << "Missing argument for option " << argv[optind - 1] << " (-" + << static_cast(optopt) << ")"; + throw std::runtime_error(ss.str()); + break; + } + default: { + // Unknown option is deferred to subsequent parsing, if any + break; + } + } + } + + // Reset getopt state for second pass in the future + optind = 0; +} + +void CuFileSequentialConfig::print_usage(std::string const& program_name) +{ + Config::print_usage(program_name); +} + +CuFileSequentialBenchmark::CuFileSequentialBenchmark(CuFileSequentialConfig const& config) + : Benchmark(config) +{ + for (auto const& filepath : _config.filepaths) { + _bufs.emplace_back(std::make_unique>(_config)); + create_file(_config, filepath, *_bufs.back()); + } +} + +CuFileSequentialBenchmark::~CuFileSequentialBenchmark() {} + +void CuFileSequentialBenchmark::initialize_impl() +{ + _file_handles.clear(); + + for (auto const& filepath : _config.filepaths) { + auto o_direct = _config.o_direct; + auto p = std::make_unique(filepath, "r", o_direct, FileHandle::m644); + _file_handles.push_back(std::move(p)); + } +} + +void CuFileSequentialBenchmark::cleanup_impl() +{ + for (auto&& file_handle : _file_handles) { + file_handle->close(); + } +} + +void CuFileSequentialBenchmark::run_target_impl() +{ + for (std::size_t i = 0; i < _file_handles.size(); ++i) { + off_t file_offset{0}; + off_t dev_ptr_offset{0}; + cuFileAPI::instance().Read( + _file_handles[i]->handle(), _bufs[i]->data(), _config.num_bytes, file_offset, dev_ptr_offset); + } +} + +std::size_t CuFileSequentialBenchmark::nbytes_impl() +{ + return _config.num_bytes * _config.filepaths.size(); +} +} // namespace kvikio::benchmark + +int main(int argc, char* argv[]) +{ + try { + auto backend = kvikio::benchmark::parse_backend(argc, argv); + + if (backend == kvikio::benchmark::Backend::FILEHANDLE) { + kvikio::benchmark::KvikIOSequentialConfig config; + config.parse_args(argc, argv); + kvikio::benchmark::KvikIOSequentialBenchmark bench(config); + bench.run(); + } else { + kvikio::benchmark::CuFileSequentialConfig config; + config.parse_args(argc, argv); + kvikio::benchmark::CuFileSequentialBenchmark bench(config); + bench.run(); + } + } catch (std::exception const& e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + return 0; +} diff --git a/cpp/benchmarks/io/sequential_benchmark.hpp b/cpp/benchmarks/io/sequential_benchmark.hpp new file mode 100644 index 0000000000..40bb46a8ba --- /dev/null +++ b/cpp/benchmarks/io/sequential_benchmark.hpp @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2021-2025, NVIDIA CORPORATION. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "common.hpp" + +#include +#include +#include +#include + +#include +#include + +namespace kvikio::benchmark { + +struct KvikIOSequentialConfig : Config { + virtual void parse_args(int argc, char** argv) override; + virtual void print_usage(std::string const& program_name) override; +}; + +class KvikIOSequentialBenchmark + : public Benchmark { + friend class Benchmark; + + protected: + std::vector> _file_handles; + std::vector>> _bufs; + + void initialize_impl(); + void cleanup_impl(); + void run_target_impl(); + std::size_t nbytes_impl(); + + public: + KvikIOSequentialBenchmark(KvikIOSequentialConfig const& config); + ~KvikIOSequentialBenchmark(); +}; + +struct CuFileSequentialConfig : Config { + virtual void parse_args(int argc, char** argv) override; + virtual void print_usage(std::string const& program_name) override; +}; + +class CuFileSequentialBenchmark + : public Benchmark { + friend class Benchmark; + + protected: + std::vector> _file_handles; + std::vector>> _bufs; + + void initialize_impl(); + void cleanup_impl(); + void run_target_impl(); + std::size_t nbytes_impl(); + + public: + CuFileSequentialBenchmark(CuFileSequentialConfig const& config); + ~CuFileSequentialBenchmark(); +}; + +} // namespace kvikio::benchmark