Skip to content

Binary cache sql backend - #5232

Draft
pnikolic-amd wants to merge 4 commits into
binary-cachefrom
binary-cache-sql-backend
Draft

Binary cache sql backend#5232
pnikolic-amd wants to merge 4 commits into
binary-cachefrom
binary-cache-sql-backend

Conversation

@pnikolic-amd

@pnikolic-amd pnikolic-amd commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Create alternative to file based binary cache. Possible benefits of sql cache are: writing/reding from one file, faster write/read, saving additional data that relates to kernel usage.

Technical Details

  • Extended migraphx::sqlite with prepared statements. The existing wrapper was sqlite3_exec-based, text-only, and had
    no parameter binding, so it could not carry BLOBs. Added sqlite_stmt (bind/step/reset/column), an RAII
    sqlite_stmt_reset guard, plus prepare, set_busy_timeout, and a non-throwing try_write. Strictly additive — none of
    the three existing consumers changed, and sqlite3.h stayed out of the GPU target.
  • Extracted a storage backend abstraction, with no behavior change. Generated a type-erased binary_cache_backend
    concept from a te.py DSL input, copying the problem_cache_backend precedent. Hoisted binary_cache::entry into its
    own header so the interface could name it, moved the existing filesystem code into file_binary_cache unchanged, and
    left the shared msgpack decode and key-collision check above the storage layer so both backends inherit them.
    binary_cache now holds an optional<binary_cache_backend>. The only visible change was two warning messages naming
    the key hash instead of a file path.
  • Added sqlite_binary_cache. One cache_v1 table keyed (version, device, key_hash), holding the msgpack entry as a BLOB
    byte-identical to a .mxr body, with op_name/problem/solution denormalized into columns for SQL inspection, plus a
    cache_info_v1 provenance table. Statements are prepared once and reused, INSERT OR REPLACE stands in for
    publish-by-rename, and busy_timeout(5000) is the whole cross-process strategy. Every failure degrades to a recompile
    — an unopenable database disables persistence, a failed store statement leaves the cache read-only.
  • Selected the backend by file extension. Added one file-static factory mirroring make_problem_cache_backend, with
    zero new configuration surface. Kept the version_dir()-empty guard in the binary_cache constructor rather than
    folding it into the factory, and made sqlite_binary_cache::open create the parent directory so a .db path behaves
    like a directory path on a fresh machine. target.cpp and binary_cache_settings were left untouched.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The cache initialization and SQLite failure-handling issues must be resolved before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a SQLite-backed GPU binary cache while retaining filesystem storage.

Changes:

  • Extended SQLite support with prepared statements and BLOB handling.
  • Added type-erased file and SQLite cache backends selected by extension.
  • Added backend and SQLite tests.
File summaries
File Description Review findings
tools/include/gpu/binary_cache_backend.hpp Defines the backend type-erasure template. None.
test/sqlite.cpp Tests prepared statements and BLOB handling. None.
test/gpu/binary_cache.cpp Tests cache backends and selection. Nit: Preserve integration coverage for directory-backed caching.
src/targets/gpu/sqlite_binary_cache.cpp Implements SQLite persistence. Moderate: Add a read-only fallback and disable writes after the first failure. Nit: Verify denormalized column values.
src/targets/gpu/include/migraphx/gpu/sqlite_binary_cache.hpp Declares SQLite storage. Nit: Clarify partial-backend behavior after statement-preparation failures.
src/targets/gpu/include/migraphx/gpu/file_binary_cache.hpp Declares filesystem storage. None.
src/targets/gpu/include/migraphx/gpu/binary_cache.hpp Integrates optional storage backends. None.
src/targets/gpu/include/migraphx/gpu/binary_cache_entry.hpp Extracts the serialized cache entry. None.
src/targets/gpu/include/migraphx/gpu/binary_cache_backend.hpp Adds generated backend dispatch. None.
src/targets/gpu/file_binary_cache.cpp Implements filesystem persistence. None.
src/targets/gpu/CMakeLists.txt Builds the new backend sources. None.
src/targets/gpu/binary_cache.cpp Selects and operates the storage backend. Moderate: Avoid computing version_dir() when persistence is disabled. Nit: Document extension-based selection and database maintenance.
src/sqlite.cpp Implements statement binding, stepping, and BLOB access. None.
src/include/migraphx/sqlite.hpp Exposes prepared-statement APIs. None.
Review details

Suppressed comments (2)

src/targets/gpu/include/migraphx/gpu/sqlite_binary_cache.hpp:52

  • The documented contract says any statement-preparation failure returns nullopt, but open() catches store/info preparation failures and still returns a partially usable backend. Describe which failures disable the backend and which leave available operations usable.
    /// Open the database, create the schema and prepare the statements. `stamp` is the text
    /// recorded in cache_info_v1 to describe the build. Returns nullopt when any of that fails,
    /// so an unusable database leaves the cache memory-only rather than raising an error.
    /// Returns the wrapper so the caller can hand the result straight back.

src/targets/gpu/sqlite_binary_cache.cpp:199

  • No test verifies the new denormalized op_name, problem, and solution column values; current database tests only count rows or reload the authoritative blob. A swapped binding or incorrect JSON representation would therefore pass. Add a query that asserts all three stored values against the entry.
            .bind(4, e.op_name)
            .bind(5, to_json_string(e.problem))
            .bind(6, to_json_string(e.solution))
            .bind(7, blob);
  • Files reviewed: 14/14 changed files
  • Comments generated: 5
  • Review effort level: Balanced

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

// of the storage medium, so it is checked here instead of in each backend.
binary_cache::binary_cache(binary_cache_settings s) : settings(std::move(s))
{
if(not version_dir().empty())
auto parent = fs::path{path}.parent_path();
if(not parent.empty())
fs::create_directories(parent);
auto db = sqlite::try_write(path);
Comment on lines +202 to +205
catch(const std::exception& ex)
{
log::warn() << "Failed to write binary cache entry " << key_hash << ": " << ex.what();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about this though.

Comment on lines +145 to +146
if(ends_with(path, ".db") or ends_with(path, ".sqlite"))
return sqlite_binary_cache::open(path, stamp); // nullopt when the database is unusable
Comment thread test/gpu/binary_cache.cpp
Comment on lines +168 to +172
// The cases below are registered only against a database path, not a directory. Driving the file
// backend through binary_cache puts entries under version_dir()/device_dir(), and write_atomically
// then creates a temp directory inside that, which pushes the file past Windows' MAX_PATH:
// fs::create_directories succeeds because std::filesystem uses the \\?\ prefix, but the
// std::ofstream in write_buffer does not, so every store fails and nothing is persisted. The

/// Read a column of the current row. Indices here are 0-based, again matching sqlite.
std::string column_text(int i) const;
std::vector<char> column_blob(int i) const;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these methods should be private. A call operator should be execute the statement and return a range over the rows. Each of the columns should be converted to the value class. Something like this:

    struct rows
    {
        rows(sqlite_stmt s) : stmt(s)
        {}

        struct iterator : iterator_operators<iterator>
        {
            using value_type        = value;
            using reference         = value_type;
            using difference_type   = std::ptrdiff_t;
            using iterator_category = std::input_iterator_tag;
            using pointer           = std::add_pointer_t<std::remove_reference_t<reference>>;
            
            iterator() = default;

            iterator(const rows* pparent)
                : parent(pparent), available(pparent->stmt.step())
            {
            }

            iterator(const rows* pparent, bool a)
                : parent(pparent), available(a)
            {
            }

            reference operator*() const
            { 
                // Add a method to conver the columns to a `value` type
                return parent->stmt.to_value();
            }

            template <class U>
            static void increment(U& x)
            {
                x.available = x.parent->stmt.step();
            }

            template <class U, class V>
            static auto equal(const U& x, const V& y)
            {
                return x.parent == y.parent and x.available == y.available;
            }

        private:
            rows* parent = nullptr;
            bool available = false;
        };

        iterator begin() { return iterator{this}; }
        iterator end() { return iterator{this, false}; }

    private:
        sqlite_stmt smt;
    };
    
    template<class... Ts>
    auto operator()(const Ts&... xs) const
    {
        // Clear any bindings first
        reset();
        // Call bind on each argument passed in
        sequence_c<sizeof...(Ts)>([&](auto... is) {
            swallow{(bind(is+1, xs), 0)...};
        });

        return rows(*this);
    }

So then we can read the rows using:

for(auto row:get_stmt(version, device, key_hash))
    return row["kernel"].get_binary(); // or whatever the column name is for where the kernel is stored
return nullopt;


/// Resets a statement on scope exit, so an early return or a thrown exception cannot leave
/// bindings or a half-consumed result set behind for whoever uses the statement next.
struct sqlite_stmt_reset

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think we need this. We should always call reset before binding and executing.


/// A human-readable description of what version_dir() encodes. Handed to a backend when one
/// is constructed, so it can store a self-describing marker alongside its entries.
static const std::string& version_stamp();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am removing the stamp as its clunky to past. Instead there will be a version_id function.

Comment thread test/gpu/binary_cache.cpp
// bodies are still parameterized by path, so a directory case is one TEST_CASE to restore once
// write_atomically writes its temporary as a sibling instead of nesting a directory.
// backends_round_trip_through_the_wrapper still covers the file backend, where it is driven
// directly and the version and device strings are short.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this still test the directory-based version on linux?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants