fix: per-layer KV cache buffers — full-tensor copy per update made Fish S2 Pro decode unusably slow#17
Open
ComicBit wants to merge 1 commit into
Open
Conversation
KVCache stored all layers' keys/values in a single 5-D tensor (num_layers, batch, heads, max_length, head_dim). MLX __setitem__ is a functional scatter, so every per-layer slice write copied the entire tensor. At 36 layers x (K+V) that is 72 full-tensor copies per generated token — multi-GB of memcopy per token, with transient allocations pushing past physical RAM into swap on 16 GB machines. Per-token decode time grew monotonically (measured 38s -> 69s -> 87s+ per token on M2 Pro 16GB) and generation of a single sentence never completed. Replace with per-layer buffers grown in fixed chunks, so a slice update touches only that layer's storage and MLX can donate the buffer in place. Public API unchanged: update/get/offset/_offsets/reset/trim_to all behave identically; get(None) stacks per-layer slices to preserve the previous 5-D return shape. After the fix decode cost is flat per token (2-3s/step under heavy system load, expected ~50ms idle) and MLX active memory stays constant at 6.7GB for the int8 model. All 48 fish_s2 unit tests pass.
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.
Problem
KVCache(used by Fish S2 Pro decode) stores all layers' K/V in a single 5-D tensor(num_layers, batch, heads, max_length, head_dim). MLX's__setitem__is a functional scatter, so every per-layer slice write copies the entire tensor. With 36 slow-AR layers × (K+V), that's 72 full-tensor copies per generated token — multi-GB of memcopy per token, with transient allocations pushing past physical RAM into swap on 16 GB machines.Observed on an M2 Pro 16 GB with
appautomaton/fishaudio-s2-pro-8bit-mlx: per-token decode time grew monotonically (38s → 69s → 87s+), and a single short sentence never finished generating (killed after 50+ min).Fix
Per-layer buffers grown in fixed chunks (
_ALLOC_STEP = 256) — the same pattern mlx-lm uses. A slice update now touches only that layer's ~2 MB buffer and MLX can donate it in place.Public API unchanged:
update,get(layer_idx),offset,_offsets(read directly bylayers.pyfor the RoPE offset),reset,trim_toall behave identically.get(None)stacks per-layer slices to preserve the previous 5-D return shape.Verification
fish_s2unit tests pass (test_fish_s2_model,test_fish_s2_generation,test_fish_s2_config,test_fish_s2_checkpoint_remap).