Skip to content

[MLX] Add off-graph KV-cache flat runtime - #21501

Open
kiymetakdemir wants to merge 1 commit into
pytorch:mainfrom
kiymetakdemir:kv-cache-mlx-flat
Open

[MLX] Add off-graph KV-cache flat runtime#21501
kiymetakdemir wants to merge 1 commit into
pytorch:mainfrom
kiymetakdemir:kv-cache-mlx-flat

Conversation

@kiymetakdemir

Copy link
Copy Markdown
Contributor

Summary

Adds the MLX runtime for the off-graph KV cache: a flat (full-history) cache plus the update_and_attend op handler, wiring the neutral SequenceCache/FlatPolicy bookkeeping to real MLX tensors. The graph carries a cache-agnostic UpdateAndAttendNode — no cache operand, since the cache is off-graph runtime state keyed by layer_id. At runtime the handler asks the cache for the K/V window and mask kind (AttendSpec) and calls SDPA: the cache owns KV byte movement + attention semantics, the handler owns q/scale + the SDPA call. Single-sequence, fp16, full-history; the per-query mask stays in the backend as MLX's fused "causal"/none (no materialized tensor).

Files

  • backends/mlx/serialization/schema.fbs — UpdateAndAttendNode (q,k,v,position,out; layer_id, scale, causal) appended to the OpNode union.
  • backends/mlx/runtime/MLXCacheImpl.h — op-facing MLXCacheImpl interface + AttendSpec (the K/V window + mask kind None/Causal/Explicit; window (SWA) and score/softcap are deferred.
  • backends/mlx/runtime/MLXFlatCache.h — FlatPool + MLXFlatCache.
  • backends/mlx/runtime/MLXExecutor.h — ExecutionState gains a per-session MLXCacheImpl* cache (survives reset(), mirrors mutable_buffers).
  • backends/mlx/runtime/MLXInterpreter.h — exec_update_and_attend handler (fetch AttendSpec → SDPA) + the OpCode::UPDATE_AND_ATTEND dispatch case.
  • backends/mlx/test/mlx_flat_cache_test.cpp, backends/mlx/test/CMakeLists.txt — op-level parity test.

Testing

C++ op-level test: drives MLXFlatCache::update_and_fetch directly and attends the returned AttendSpec exactly as the handler does, comparing to MLX SDPA over the full K/V history the cache should have assembled, prefill (Causal), decode (None), and the capacity-reject path. Runs on Apple Silicon (MLX SDPA needs the Metal backend). Built and ran via the standard flow, passes:

cmake --preset mlx-release -DEXECUTORCH_BUILD_TESTS=ON
cmake --build cmake-out --target mlx_flat_cache_test
ctest --test-dir cmake-out -R mlx_flat_cache --output-on-failure

@pytorch-bot

pytorch-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21501

Note: Links to docs will display an error until the docs builds have been completed.

⏳ No Failures, 17 Pending

As of commit 011bd32 with merge base 43cb2b2 (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jul 30, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

causal: bool = false;
}

table UpdateAndAttendNode {

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.

Didn't the op have an output dtype?

Comment thread backends/mlx/runtime/MLXFlatCache.h Outdated
// write is a slice_update run, a read a [0, len) slice. H/D inferred on write.
class FlatPool {
public:
void write(int start, const Tensor& kv, int capacity, StreamOrDevice s) {

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.

kv here is just update?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes it is.

Comment thread backends/mlx/runtime/MLXFlatCache.h Outdated
// write is a slice_update run, a read a [0, len) slice. H/D inferred on write.
class FlatPool {
public:
void write(int start, const Tensor& kv, int capacity, StreamOrDevice s) {

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.

Why is capacity a property of write?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixing this.

buf_ = ::mlx::core::slice_update(*buf_, kv, to_shape(lo), to_shape(hi), s);
}

Tensor read(int len, StreamOrDevice s) const {

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.

Is return type Tensor or ::mlx::array?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Tensor is the runtime's existing alias for ::mlx::core::array (in MLXExecutor.h), so I used it as in there.

@kiymetakdemir
kiymetakdemir force-pushed the kv-cache-mlx-flat branch 2 times, most recently from 88761ae to 9321153 Compare July 30, 2026 23:23
@kiymetakdemir

kiymetakdemir commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Recent changes:

  1. Op contract; dropped causal, added out_dtype
  2. Allocate at construction
  3. Bounds assertion on the write position
  4. Storage dtype from config + cast-if-differ
    Note: I kept capacity, it is the max cap, behaves like static now, later will allocate initial capacity and have dynamic grow.

Comment thread backends/mlx/runtime/MLXFlatCache.h Outdated
: cache::SequenceCache(cfg) {
const ::mlx::core::Dtype dt = cfg.kv_dtype
? resolve_dtype(static_cast<int8_t>(*cfg.kv_dtype))
: ::mlx::core::float16;

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.

Raise if dtype not specified.

Comment thread backends/mlx/runtime/MLXFlatCache.h Outdated
// position vector is tiny). Cast to int32 so an int64 position also works.
static int read_start(const Tensor& position, StreamOrDevice s) {
Tensor p = ::mlx::core::astype(position, ::mlx::core::int32, s);
::mlx::core::eval(p);

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.

I'm quite worried about this. It breaks async evalution.

Can we lift this logic to exec_update_and_attend at least and pass an int down. I think it'll be easier to correct at that level if needed.

#include "MLXFlatCache.h"

#include <mlx/mlx.h>

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.

Use GTEST

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.

We also need something to run the c++ test in CI. You might need to modify mlx.yml

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Okay, added!

@kiymetakdemir
kiymetakdemir force-pushed the kv-cache-mlx-flat branch 2 times, most recently from ad06aae to b27b89c Compare July 31, 2026 00:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants