Skip to content

Binary cache sql backend - #5232

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

pnikolic-amd wants to merge 16 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.

Comment thread src/targets/gpu/binary_cache.cpp Outdated
Comment thread src/targets/gpu/sqlite_binary_cache.cpp
Comment thread src/targets/gpu/sqlite_binary_cache.cpp Outdated
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 thread src/targets/gpu/binary_cache.cpp Outdated
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 Outdated
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
Comment thread src/include/migraphx/sqlite.hpp Outdated

/// 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;

Comment thread src/include/migraphx/sqlite.hpp Outdated

/// 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 Outdated
// 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?

pnikolic-amd and others added 8 commits September 25, 2026 14:41
- sqlite_stmt: make bind/step/reset private; calling it binds its args
  and returns a range of rows as values; drop sqlite_stmt_reset
- Reset the statement when its rows go away so reads don't keep a lock
- Fall back to read-only use of a database that can't be written
- Batch stores in one transaction (optional begin/end_batch on backends)
- Open cache storage on first use instead of at construction
- Restore directory-backend tests on non-Windows; add SQLite tests
- Document .db/.sqlite selection and database maintenance

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…sql-backend

Conflicts resolved:
- CHANGELOG.md: take the restructured changelog from binary-cache, keep the
  SQLite wording on the binary cache entry, and drop the duplicate
  problem-cache entry (now listed under 2.18 as #5117).
- src/targets/gpu/compile_ops.cpp: keep store_results() with batched stores
  and call it at the end of the new compile_cells(); compile() now calls
  add_cells() and then compile_cells().

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
/// error, it is a cache miss, and the caller recompiles.
///
/// Must not throw.
optional<std::vector<char>>

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.

This should return a binary_cache_entry.

const std::string& device,
const std::string& key_hash,
const binary_cache_entry& e,
const std::vector<char>& blob);

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.

The blob parameter should be removed. It just duplicates the binary_cache_entry.

I think the backends should decide how they want to serialize the entry. sqlite could make each field a seperate column whereas the file-based would just serialize it to msgpack.

///
/// Must not throw. A backend that cannot start a batch stores entries one
/// at a time instead.
void begin_batch();

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.

Remove this. Batches should be written by passing a vector into store. There is also no reason to add another overload for this either.

void store(const std::string& version,
const std::string& device,
const std::string& key_hash,
const binary_cache_entry& e,

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.

This should take a vector of entries.

Comment thread src/include/migraphx/sqlite.hpp Outdated
/// an insert, has already run by the time the call returns, whether or not the range is
/// iterated. The statement is reset when the range is destroyed: an unfinished select holds a
/// read lock on the database until then, which would stall writers in other processes.
struct sqlite_stmt::rows

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.

This should be defined inline in the sqlite_stmt otherwise this wont compile due to incomplete types.

/// in memory.
void store(const std::string& version,
const std::string& device,
const std::string& key_hash,

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.

What is this parameter? It looks like it duplicates the key in binary_cache_entry. If so, it should be removed.

///
/// Must not throw.
optional<std::vector<char>>
load(const std::string& version, const std::string& device, const std::string& key_hash);

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.

This should take the key and the backends can use a hash if necessary.

Comment thread src/sqlite.cpp Outdated
const auto* data = static_cast<const char*>(sqlite3_column_blob(stmt, i));
auto bytes = sqlite3_column_bytes(stmt, i);
assert(bytes >= 0);
auto size = data == nullptr ? 0 : static_cast<std::size_t>(bytes);

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.

Declare size as std::size_t and drop the static_cast:

Suggested change
auto size = data == nullptr ? 0 : static_cast<std::size_t>(bytes);
std::size_t size = data == nullptr ? 0 : bytes;

Comment thread src/sqlite.cpp Outdated
assert(bytes >= 0);
auto size = data == nullptr ? 0 : static_cast<std::size_t>(bytes);
if(type == SQLITE_TEXT)
// NOLINTNEXTLINE(modernize-return-braced-init-list)

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.

These modernize-return-braced-init-list tidy warning are little annoying here. Another option instead of suppressing them is to add a pair function to the value class:

struct MIGRAPHX_EXPORT value
{
    ...
    template<class... Ts>
    static pair(const std::string& pkey, Ts&&... xs)
    {
        return value(pkey, static_cast<Ts&&>(xs...));
    }
    ...
};

Then you can add whatever suppressions that are needed in the pair function, and then just use value::pair instead of value and there shouldn't be tidy warnings for everytime.

Comment thread src/sqlite.cpp Outdated
{
auto* stmt = impl->get();
// Built as keyed values rather than a map, so a blob is moved into place instead of copied.
std::vector<value> columns;

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.

Why use the intermediate vector? Just insert directly into the returned value:

Suggested change
std::vector<value> columns;
value columns = value::object{};

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.

Comment thread src/include/migraphx/sqlite.hpp Outdated

private:
friend struct sqlite_stmt;
explicit rows(sqlite_stmt s) : stmt(std::move(s)), first(stmt.step()) {}
Comment thread src/sqlite.cpp Outdated
Comment on lines +155 to +156
// sqlite succeeds without a statement for text that holds none, such as only a comment.
assert(stmt_tmp != nullptr);
Comment on lines +58 to +59
constexpr const char* schema_sql = R"__migraphx__(
CREATE TABLE IF NOT EXISTS cache_v1 (

This branch has not been deployed

No deployments
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