Skip to content

feat(paged): port PagedKVCache + BlockAllocator foundation - #36

Closed
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1781394275-paged-kv-cache
Closed

feat(paged): port PagedKVCache + BlockAllocator foundation#36
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1781394275-paged-kv-cache

Conversation

@devin-ai-integration

Copy link
Copy Markdown

Proposed changes

Ports the PagedAttention foundation layer from ekryski/mlx-swift-lm#97 into MLXLMCommon, adapted to this repo's KVCache surface. This is the data-structure layer only — there is no Metal paged-attention kernel (still unsolved in Swift; tracked as ekryski issue ml-explore#127) and no model is wired to this cache.

What's added

  • Libraries/MLXLMCommon/BlockAllocator.swift — refcounted, lock-guarded physical block pool (LIFO free list). retain() bumps refcounts so multiple sequences can share blocks (prefix sharing / copy-on-write hook). Throws AllocatorError.exhausted on pool exhaustion.
  • Libraries/MLXLMCommon/PagedKVCache.swift — per-layer block KV storage subclassing BaseKVCache. Block layout matches vLLM's MetalPagedKVCache exactly so a future Metal kernel can read it directly:
    keyBlocks / valueBlocks: [numBlocks, blockSize, numKVHeads, headDim]
    
    Forward path is scatter() (write new tokens into blocks at offset) → gather() (reconstruct contiguous [1, kvHeads, T, headDim], trimming the partial last block) → SDPA. update() composes the two and is element-wise equivalent to KVCacheSimple.update().
  • Tests/MLXLMTests/PagedKVCacheTests.swift — allocator semantics (allocate/free/retain/exhaustion) + round-trip identity + forward-equivalence vs KVCacheSimple over a varying-length chunk sequence (7,1,1,4,1,1) that crosses block boundaries.

Adaptation notes (vs ekryski source)

  • Reference cache for the equivalence test is KVCacheSimple (this repo) rather than StandardKVCache.
  • Dropped peek() / memoryBytes / storageKind overrides — those members don't exist on this repo's KVCache/BaseKVCache.
  • Added a copy() implementation (required by this repo's KVCache protocol; BaseKVCache's default fatalErrors).

Scope / non-goals

  • No fused Metal paged-attention kernel (gather()+SDPA is a foundation path, ~4.7× slower than a true paged kernel at B=32/kv=8K per upstream's microbench — not a production decode path).
  • No attention-layer rewiring; no continuous-batching migration; no full prefix caching (only the retain() hook).

Validation

Could not build/test in this environment (Linux, no Swift toolchain; MLX needs Metal/Apple Silicon). Needs swift test --filter PagedKVCache on a Mac. Note: the Build and Test workflow jobs are gated if: github.repository == 'ml-explore/mlx-swift-lm', so CI does not run on this fork.

Checklist

  • I have read the CONTRIBUTING document
  • I have run pre-commit run --all-files to format my code / installed pre-commit prior to committing changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the necessary documentation (if needed)

Link to Devin session: https://app.devin.ai/sessions/95935953320b49d1a083d0385740ae2c

Port the paged-attention data-structure layer from ekryski/mlx-swift-lm PR #97, adapted to this repo's KVCache surface (KVCacheSimple reference, no peek/memoryBytes/storageKind):

- BlockAllocator: refcounted free-list block pool with retain() for prefix sharing.
- PagedKVCache: per-layer block-based KV storage (block layout matches vLLM's MetalPagedKVCache) with a scatter -> gather -> SDPA forward path.
- Tests: allocator semantics + forward-equivalence vs KVCacheSimple.

No model is wired to this cache and there is no Metal paged kernel yet; gather()+SDPA is a foundation path, not a production decode path.
@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

Comment on lines +17 to +45
name: PagedKVCache Swift tests
runs-on: blacksmith-12vcpu-macos-26
steps:
- uses: actions/checkout@v4

- name: Build tests
run: swift build --build-tests

- name: Extract and colocate mlx.metallib
run: |
python3 -m venv /tmp/mlxvenv
/tmp/mlxvenv/bin/pip install 'mlx==0.31.1'
pyver="$(/tmp/mlxvenv/bin/python -c 'import sys;print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
metallib="/tmp/mlxvenv/lib/python${pyver}/site-packages/mlx/lib/mlx.metallib"
test -f "$metallib" || { echo "::error::mlx.metallib not found at $metallib"; exit 1; }
# MLX's C++ loader looks for mlx.metallib next to the running binary.
# Under `swift test` that is the xctest runner inside the bundle, so
# place a copy there as well as in .build/debug.
cp "$metallib" .build/debug/mlx.metallib
for bundle in .build/debug/*PackageTests.xctest; do
macos="$bundle/Contents/MacOS"
if [ -d "$macos" ]; then
cp "$metallib" "$macos/mlx.metallib"
echo "placed metallib in $macos"
fi
done

- name: Run PagedKVCache tests
run: swift test --filter PagedKVCache
@Gajesh2007 Gajesh2007 closed this Jun 26, 2026
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.

2 participants