Repository navigation
perf(cache): write the decode KV row in place instead of copying the cache - #1961
Merged
Merged
Conversation
…cache A single-token FP16 decode step wrote its K/V row with slice_update, which copies the whole buffer unless it is donatable. During pipelined decode it never is: MLX's Metal backend keeps each kernel's input buffers alive until the command buffer completes, and the next step is encoded while the previous one still runs, so every layer's K and V cache was copied every token. A new primitive, inplace_slice_write (turbo/kv_inplace_write), adopts the destination buffer and writes only the new rows. KVCache takes it only when its own last write produced the current buffers and the row lands exactly at that write's length, so it never rewrites a row another holder (prompt-cache snapshot, restored or detached cache) could consider valid; anything else takes one copying write and returns to in-place. MLXCEL_KV_INPLACE_WRITE=0 restores the copying path. M1 Ultra, bench_decode.sh tg128: command-r7b 4-bit +2.5% at context 16, +4.1% at 512 (115.5 to 120.2), +8.2% at 2048; server c=1 decode 115-117 to 120-121; Llama 3.1 8B +4.6%, Qwen2.5 7B +2.7%. Peak memory footprint about 0.6 GB lower at 2048+512. Greedy output identical across those models and on server requests that restore prompt-cache snapshots. New tests cover the ownership rule and a shared snapshot surviving continued in-place writes; dropping the rule fails the latter. Closes #1959
This was referenced Sep 24, 2026
This was referenced Oct 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A single-token FP16 decode step now writes its K/V row into the dense cache buffer in place instead of through
slice_update. During pipelined decodeslice_updatecould never donate the buffer: MLX's Metal backend keeps each kernel's input buffers alive until the command buffer completes, and the next step is encoded while the previous one still runs, so every layer's K and V cache was copied every token. The copy grows with context.inplace_slice_write(src/lib/mlx-cpp/turbo/kv_inplace_write): a primitive whose output adopts the destination buffer (copy_shared_buffer) and writes only the new rows (copy_gpu_inplace), i.e.SliceUpdatewithout the leading full copy.KVCachetakes the in-place write only when the currentkeys/valuesare the arrays its own last write produced (compared by MLX array id, with the handles held so ids cannot be reused) and the row lands exactly at that write's length. It therefore never rewrites a row another holder (prompt-cache snapshot, restored or detached cache) could consider valid. Anything else takes one copying write and returns to in-place.MLXCEL_KV_INPLACE_WRITE=0restores the copying path. Documented indocs/environment-variables.md.Measurement (M1 Ultra)
bench_decode.sh, median of 6)Peak memory footprint at 2048 + 512 drops from 7.41/7.53 GB to 6.80/6.82 GB. Gemma 3 moves 0.6%, as its sliding layers use the rotating cache. Record:
docs/benchmark_results/kv-inplace-write-m1ultra-2026-09-23.md.Validation
make verify-test: 11430 passed, 0 failed. Workspace clippy and fmt pass.Closes #1959