Binary cache sql backend - #5232
Conversation
…to support mutiple backend in binary cache; Added functionalities to sql wrapper to support blobs
There was a problem hiding this comment.
🟡 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, butopen()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, andsolutioncolumn 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); |
| catch(const std::exception& ex) | ||
| { | ||
| log::warn() << "Failed to write binary cache entry " << key_hash << ": " << ex.what(); | ||
| } |
There was a problem hiding this comment.
I am not sure about this though.
| if(ends_with(path, ".db") or ends_with(path, ".sqlite")) | ||
| return sqlite_binary_cache::open(path, stamp); // nullopt when the database is unusable |
| // 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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
I am removing the stamp as its clunky to past. Instead there will be a version_id function.
| // 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. |
There was a problem hiding this comment.
Does this still test the directory-based version on linux?
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
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.
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.
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.
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.