// Copyright (C) 2024-2025 Advanced Micro Devices, Inc. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #ifndef BWR_DB_HPP #define BWR_DB_HPP #include #include "bwr_utils.hpp" #include /*! \brief Database that can be used to store function call information between runs. * This allows us to test whether the input has changed between runs. */ class BitwiseReproDB { public: /*! \brief Enum class used to control the mode the database operates in. * There are two reasons that a row might not be found in the database: * 1. There is a run-to-run reproducibility error. * 2. The database entries for this architecture/rocm version/rocThrust version * haven't been generated yet. * * This enum class allows us to distinguish between these two cases. * * * In test mode, if an entry is not found, it is not inserted. This allows * errors to be detected (case 1 above). * * In generate_mode, if an entry is not found, it is inserted (case 2 above). * No run-to-run errors will be detected while in generate mode (the match functions * will always report that a match was found). */ enum class Mode { test_mode, generate_mode }; /*! \brief Database constructor. Will create the SQLite database file if it doesn't already exist (and db_path is not * null). \param db_path Path to the database file (eg. "./repro.db"). If null, database won't be created. \param mode * The database mode (generate or test). See the enum class above for details. */ BitwiseReproDB(const char* db_path, const BitwiseReproDB::Mode mode) : m_db_conn(nullptr) , m_insert_stmt(nullptr) , m_match_stmt(nullptr) , m_mode(mode) { if (!db_path) { throw std::runtime_error("No database path given (ROCTHRUST_REPRO_DB_PATH environment variable is not set)."); } int ret = sqlite3_open(db_path, &m_db_conn); if (ret != SQLITE_OK) { throw std::runtime_error("Cannot open run-to-run bitwise reproducibility database: " + std::string(db_path)); } // Access to a database file may occur in parallel. // Increase default sqlite timeout, so diferent process // can wait for one another. sqlite3_busy_timeout(m_db_conn, 30000); // Set sqlite3 engine to WAL mode to avoid potential deadlocks with multiple // concurrent processes (if a deadlock occurs, the busy timeout is not honored). ret = sqlite3_exec(m_db_conn, "PRAGMA journal_mode = WAL", nullptr, nullptr, nullptr); if (ret != SQLITE_OK) { throw std::runtime_error("Error setting WAL mode: " + std::string(sqlite3_errmsg(m_db_conn))); } // Create the rocthrust_test_run table if it doesn't already exist. ret = sqlite3_exec(m_db_conn, BitwiseReproDB::get_create_table_sql().c_str(), nullptr, nullptr, nullptr); if (ret != SQLITE_OK) { throw std::runtime_error("Error creating table: " + std::string(sqlite3_errmsg(m_db_conn))); } // Initialize prepared statements. prepare_match_stmt(); prepare_insert_stmt(); } /*! \brief Destructor - cleans up and closes the database connection. */ ~BitwiseReproDB() { sqlite3_finalize(m_insert_stmt); sqlite3_finalize(m_match_stmt); sqlite3_close(m_db_conn); } /*! \brief Given a pair of input and output "tokens" (which uniquely identify a function call), * looks for a match in the database. If the DB is in generate mode and a match is not found, * the (input, output) token pair will be inserted into the database. In test mode, no insertion * is performed. * * \param input_token String that uniquely identifies a function call's inputs. See bwr_utils.hpp for details. * \param output_token String that uniquely identifies a function call's outputs. See bwr_utils.hpp for details. * \param match_found [out] In test mode, set to true if the (input, output) token pair is already in the database. * In generate mode, rows are inserted if they don't already exist, and this is always set to true. * \param inserted [out] Set to true if a row was inserted. */ void match(const std::string& input_token, const std::string& output_token, bool& match_found, bool& inserted) { match_found = false; inserted = false; // A test_run is a convenience struct that encapsulates all the information in a single row of the database table. // Create one using the given input/output pair. const rocthrust_test_run test_run(input_token, output_token); // Do a select to check for a matching existing row. const int match_count = select(test_run); // Note: Because of our database constraints (the unique index), // we know that match_count will either be 0 or 1 here. match_found = (match_count == 1); // Only insert if we are in generate mode and an entry does // not already exist. if (m_mode == Mode::generate_mode && !match_found) { try { inserted = insert(test_run); // If the insertion was successful, set match to true, since a matching row now exists. match_found = inserted; } catch (const std::exception& e) { std::cerr << e.what() << '\n'; std::cerr << "input_token: " << input_token << std::endl; std::cerr << "output_token: " << output_token << std::endl; std::cerr << "match_found: " << int(match_found) << std::endl; std::cerr << "inserted: " << int(inserted) << std::endl; } } } /*! \brief See above. This overload exists for convenience - you can call it when you don't need to check if anything * was inserted. \return A bool indicating whether or not a match was found. */ bool match(const std::string& input_token, const std::string& output_token) { bool match_found; bool inserted; match(input_token, output_token, match_found, inserted); return match_found; } private: struct rocthrust_test_run { rocthrust_test_run(const std::string& input_token, const std::string& output_token, const std::string& rocm_version, const std::string& rocthrust_version, const std::string& gpu_arch) : input_token(input_token) , output_token(output_token) , rocm_version(rocm_version) , rocthrust_version(rocthrust_version) , gpu_arch(gpu_arch) {} rocthrust_test_run(const std::string& input_token, const std::string& output_token) : input_token(input_token) , output_token(output_token) , rocm_version(bwr_utils::get_rocm_version()) , rocthrust_version(bwr_utils::get_rocthrust_version()) , gpu_arch(bwr_utils::get_gpu_arch()) {} std::string input_token; std::string output_token; std::string rocm_version; std::string rocthrust_version; std::string gpu_arch; }; static const std::string get_create_table_sql() { return "CREATE TABLE IF NOT EXISTS rocthrust_test_run(" "input_token TEXT NOT NULL, " "output_token TEXT NOT NULL, " "rocm_version TEXT NOT NULL, " "rocthrust_version TEXT NOT NULL, " "gpu_arch TEXT NOT NULL);" "CREATE UNIQUE INDEX IF NOT EXISTS id_index_unique_run ON rocthrust_test_run(" "input_token, rocm_version, rocthrust_version, gpu_arch);"; } static const std::string get_insert_sql() { return "INSERT INTO rocthrust_test_run(" "input_token, output_token, rocm_version, rocthrust_version, gpu_arch) " "VALUES (?, ?, ?, ?, ?);"; } static const std::string get_match_sql() { return "SELECT COUNT(*) FROM rocthrust_test_run WHERE " "input_token = ? AND output_token = ? AND rocm_version = ? AND rocthrust_version = ? AND gpu_arch = ?;"; } void prepare_match_stmt() { static const std::string match_sql = get_match_sql(); const int ret = sqlite3_prepare_v2(m_db_conn, match_sql.c_str(), -1, &m_match_stmt, nullptr); if (ret != SQLITE_OK) { throw std::runtime_error("Cannot prepare match statement: " + std::string(sqlite3_errmsg(m_db_conn))); } } void prepare_insert_stmt() { static const std::string insert_sql = get_insert_sql(); const int ret = sqlite3_prepare_v2(m_db_conn, insert_sql.c_str(), -1, &m_insert_stmt, nullptr); if (ret != SQLITE_OK) { throw std::runtime_error("Cannot prepare insert statement: " + std::string(sqlite3_errmsg(m_db_conn))); } } int select(const rocthrust_test_run& test_run) { int count = 0; bind_match_stmt(test_run); const int ret = sqlite3_step(m_match_stmt); if (ret != SQLITE_ROW) { throw std::runtime_error( std::string("Error executing select statement: ") + std::string(sqlite3_errmsg(m_db_conn))); } // Note: select indices start at 0 count = sqlite3_column_int(m_match_stmt, 0); return count; } bool insert(const rocthrust_test_run& test_run) { bind_insert_stmt(test_run); const int ret = sqlite3_step(m_insert_stmt); const bool inserted = (ret == SQLITE_DONE); if (!inserted) { throw std::runtime_error( std::string("Error executing insert statement: ") + std::string(sqlite3_errmsg(m_db_conn))); } return inserted; } void bind_insert_stmt(const rocthrust_test_run& test_run) { // Note: bind indices start at 1 sqlite3_reset(m_insert_stmt); bind_text(m_insert_stmt, test_run.input_token, 1); bind_text(m_insert_stmt, test_run.output_token, 2); bind_text(m_insert_stmt, test_run.rocm_version, 3); bind_text(m_insert_stmt, test_run.rocthrust_version, 4); bind_text(m_insert_stmt, test_run.gpu_arch, 5); } void bind_match_stmt(const rocthrust_test_run& test_run) { // Note: bind indices start at 1 sqlite3_reset(m_match_stmt); bind_text(m_match_stmt, test_run.input_token, 1); bind_text(m_match_stmt, test_run.output_token, 2); bind_text(m_match_stmt, test_run.rocm_version, 3); bind_text(m_match_stmt, test_run.rocthrust_version, 4); bind_text(m_match_stmt, test_run.gpu_arch, 5); } void bind_text(sqlite3_stmt* stmt, const std::string& text, const int index) { const int ret = sqlite3_bind_text(stmt, index, text.c_str(), -1, SQLITE_TRANSIENT); if (ret != SQLITE_OK) { throw std::runtime_error(std::string( "Error binding text field in insert statement:\n" "index: " + std::to_string(index) + "\n" "value: " + text)); } } sqlite3* m_db_conn; sqlite3_stmt* m_insert_stmt; sqlite3_stmt* m_match_stmt; Mode m_mode; }; #endif // BWR_DB_HPP