Skip to content

Add TurboQuant serial Metal decode fast path#91

Open
scouzi1966 wants to merge 5 commits intofeature/codex-turboquant-corefrom
feature/codex-turboquant-fastpath
Open

Add TurboQuant serial Metal decode fast path#91
scouzi1966 wants to merge 5 commits intofeature/codex-turboquant-corefrom
feature/codex-turboquant-fastpath

Conversation

@scouzi1966
Copy link
Copy Markdown
Owner

@scouzi1966 scouzi1966 commented Apr 5, 2026

Summary

Add the first inline Metal-backed TurboQuant execution slice on top of the core serial implementation.

What this PR includes

  • inline Metal kernel-backed TurboQuant decode scoring
  • serial decode fast-path scaffolding in TurboQuantKVCache
  • packed-cache restore fix so decode preserves prior history
  • design note for the Metal slice

What this PR does not include

  • fully packed value-side Metal weighted-sum execution
  • fully fused end-to-end TurboQuant SDPA
  • batch/concurrent TurboQuant integration

Validation

  • MACAFM_MLX_METALLIB="$PWD/default.metallib" swift test --filter TurboQuantCacheTests --parallel --num-workers 1
  • MACAFM_MLX_METALLIB="$PWD/default.metallib" swift test --filter 'KVCacheTruncateTests|BatchedPrefillTests' --parallel --num-workers 1

Stack

This is PR 3 of a stacked TurboQuant series and targets PR 2.

Summary by Sourcery

Introduce a Metal-backed TurboQuant serial decode path and improve TurboQuant cache correctness around packed-state restores.

New Features:

  • Add inline Metal kernels and kernel manager for TurboQuant MSE decode scoring and weighted-sum operations.
  • Add a TurboQuantKVCache fast-path for single-token decode that uses the packed TurboQuant representation when layout and mask constraints are met.

Bug Fixes:

  • Ensure TurboQuant caches restored from packed prompt-cache state correctly rebuild shadow history before appending new tokens, preserving decode context.

Enhancements:

  • Refactor TurboQuantKVCache to separate ingestion of new keys/values from dense-state materialization, enabling reuse by the decode fast path.

Documentation:

  • Add a design note documenting the TurboQuant Metal decode path scope, behavior, and limitations.

Tests:

  • Add a regression test verifying TurboQuant decode correctness after loading from a packed cache file.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 5, 2026

Reviewer's Guide

Implements a narrow serial TurboQuant Metal-backed decode path by adding inline Metal kernels for MSE scoring/weighted sum, wiring a decode-only fast path through TurboQuantKVCache that ingests into the quantized cache and falls back to dense SDPA when unsupported, fixes shadow cache rehydration after packed cache restore, and adds a regression test plus a design note documenting the new Metal slice.

Class diagram for TurboQuant Metal-backed decode path

classDiagram
    class TurboQuantMSECodec {
        +prepareQueries(queries: MLXArray) MLXArray
        +finalizeWeightedSum(output: MLXArray) MLXArray
        +quantize(vectors: MLXArray) TurboQuantMSEState
        +dequantize(state: TurboQuantMSEState) MLXArray
        +bits: Int
        +codebook: MLXArray
    }

    class TurboQuantMSEState {
        +norms: MLXArray
        +indices: MLXArray
    }

    class TurboQuantMSEKernelManager {
        <<singleton>>
        +shared: TurboQuantMSEKernelManager
        +scoreKernel: MLXFast.MLXFastKernel?
        +weightedSumKernel: MLXFast.MLXFastKernel?
        -TurboQuantMSEKernelManager()
    }

    class TurboQuantKVCache {
        +offset: Int
        +keyState: TurboQuantMSEState?
        +valueState: TurboQuantMSEState?
        +keyCodec: TurboQuantMSECodec?
        +valueCodec: TurboQuantMSECodec?
        +update(keys: MLXArray, values: MLXArray) (MLXArray, MLXArray)
        -ingest(keys: MLXArray, values: MLXArray) (TurboQuantMSEState, TurboQuantMSEState)
        -appendShadow(keys: MLXArray, values: MLXArray, previous: Int) void
        -rehydrateShadowFromPackedState() void
        -groupedDecodeQueries(queries: MLXArray) MLXArray?
        -fastDecodeAttention(queries: MLXArray, scale: Float, mask: MLXFast.ScaledDotProductAttentionMaskMode) MLXArray?
        +decodeAttention(queries: MLXArray, keys: MLXArray, values: MLXArray, scale: Float, mask: MLXFast.ScaledDotProductAttentionMaskMode) MLXArray
        -denseState() (MLXArray, MLXArray)
    }

    class MLXFastKernel {
        +call(inputs: [MLXArray], template: [(String, Int)], grid: (Int, Int, Int), threadGroup: (Int, Int, Int), outputShapes: [[Int]], outputDTypes: [MLXArray.DType]) [MLXArray]
    }

    class MLXFast {
        +metalKernel(name: String, inputNames: [String], outputNames: [String], source: String) MLXFastKernel?
        +scaledDotProductAttention(queries: MLXArray, keys: MLXArray, values: MLXArray, scale: Float, mask: MLXFast.ScaledDotProductAttentionMaskMode) MLXArray
        <<enum>> ScaledDotProductAttentionMaskMode
    }

    TurboQuantMSECodec --> TurboQuantMSEState : produces
    TurboQuantMSEKernelManager --> MLXFastKernel : owns
    TurboQuantMSEKernelManager ..> MLXFast : creates kernels via metalKernel
    TurboQuantKVCache --> TurboQuantMSECodec : uses keyCodec
    TurboQuantKVCache --> TurboQuantMSECodec : uses valueCodec
    TurboQuantKVCache --> TurboQuantMSEState : maintains keyState
    TurboQuantKVCache --> TurboQuantMSEState : maintains valueState
    TurboQuantKVCache ..> TurboQuantMSEKernelManager : uses shared kernels
    TurboQuantKVCache ..> MLXFast : falls back to scaledDotProductAttention
    MLXFastKernel <|-- MLXFast.MLXFastKernel
Loading

File-Level Changes

Change Details Files
Add inline Metal kernels and Swift plumbing for TurboQuant MSE decode scoring and (non-yet-used) weighted-sum.
  • Introduce a Metal score kernel that computes TurboQuant MSE dot products over packed indices using per-head norms and a codebook.
  • Introduce a Metal weighted-sum kernel that reconstructs values from packed indices, norms, weights, and a codebook, parameterized by template constants for bits, dimension, repeat count, and packed width.
  • Add a kernel manager singleton to compile/cache the score and weighted-sum kernels via MLXFast and expose them through Swift helpers.
Scripts/patches/KVCache.swift
Add a serial TurboQuant decode fast path in TurboQuantKVCache that uses the Metal score kernel for single-token grouped-query decode while safely falling back to dense attention for unsupported cases.
  • Extend TurboQuantMSECodec with query preparation and result finalization hooks for the decode path.
  • Refactor cache update logic into an ingest method that returns quantized key/value states and is reused by both update and decode paths.
  • Add groupedDecodeQueries to reshape single-token queries into a grouped layout compatible with packed KV heads.
  • Implement fastDecodeAttention to route eligible decode calls through turboQuantMSEDecodeScores, softmax the scores, and perform a matmul with dequantized values, returning results shaped like standard attention output.
  • Change decodeAttention to ingest new keys/values into the quantized cache, attempt the TurboQuant fast path first, and otherwise call MLXFast.scaledDotProductAttention on the dense cached state.
Scripts/patches/KVCache.swift
Fix TurboQuant shadow cache rehydration and history preservation after packed prompt-cache restore.
  • Ensure appendShadow rehydrates shadow keys/values from packed state when appending new tokens to a partially restored cache.
  • Adjust the packed-cache restoration/decode flow so decodeAttention operates on a correctly reconstructed history slice up to the current offset.
  • Add a regression test that saves a TurboQuant cache to a packed prompt-cache file, reloads it, decodes with new tokens, and compares the result against dense scaled-dot-product attention while checking offset and shape.
  • Validate numerics by bounding the max difference between TurboQuant and dense attention outputs.
Scripts/patches/KVCache.swift
Tests/MacLocalAPITests/TurboQuantCacheTests.swift
Document the scope and behavior of the TurboQuant Metal decode path.
  • Add a design note describing the new TurboQuant Metal-backed decode slice, supported shapes, fallback behavior, and non-goals for this branch.
  • Clarify that only score-side decode is currently Metal-backed while value accumulation remains on the conservative dense path.
docs/feature-codex-turboquant-metal.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@scouzi1966 scouzi1966 marked this pull request as ready for review April 5, 2026 00:49
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • The Metal-backed weighted-sum path (makeTurboQuantMSEWeightedSumKernel, turboQuantMSEDecodeWeightedSum, and TurboQuantMSECodec.finalizeWeightedSum) is currently unused; consider either wiring it into fastDecodeAttention or dropping it for now to keep the slice minimal and avoid dead code that may drift from the score path.
  • fastDecodeAttention assumes a single-token, grouped-query layout via groupedDecodeQueries but this is only enforced implicitly by dim checks; it may be worth adding lightweight assertions or clearer early-returns (e.g., when queries.dim(2) != 1 or queryHeads % kvHeads != 0) to make unsupported shapes fail in a more obvious and maintainable way.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The Metal-backed weighted-sum path (makeTurboQuantMSEWeightedSumKernel, turboQuantMSEDecodeWeightedSum, and TurboQuantMSECodec.finalizeWeightedSum) is currently unused; consider either wiring it into fastDecodeAttention or dropping it for now to keep the slice minimal and avoid dead code that may drift from the score path.
- fastDecodeAttention assumes a single-token, grouped-query layout via groupedDecodeQueries but this is only enforced implicitly by dim checks; it may be worth adding lightweight assertions or clearer early-returns (e.g., when queries.dim(2) != 1 or queryHeads % kvHeads != 0) to make unsupported shapes fail in a more obvious and maintainable way.

## Individual Comments

### Comment 1
<location path="Scripts/patches/KVCache.swift" line_range="933-941" />
<code_context>
+    let D = queries.dim(3)
+    let T = state.norms.dim(2)
+
+    return kernel(
+        [queries, state.norms, state.indices, codebook],
+        template: [
+            ("Dim", D),
+            ("RepeatCount", R),
+            ("Bits", bits),
+            ("PackedWidth", state.indices.dim(3)),
+        ],
+        grid: (32, R, B * H * T),
+        threadGroup: (32, 1, 1),
+        outputShapes: [[B, H, R, T]],
</code_context>
<issue_to_address>
**issue (bug_risk):** The score kernel does not validate shape consistency between `queries` and `state`, which can lead to subtle misindexing.

`turboQuantMSEDecodeScores` only checks `queries.ndim == 4` and `state.norms.dim(2) > 0`, but not that `B`, `H`, `R`, `T` from `queries` match `state.norms`/`state.indices`. With mismatched shapes (e.g., different `B`/`H`), the kernel will still launch and misinterpret memory. Please add cheap shape checks (e.g., `state.norms.dim(0) == B`, `state.norms.dim(1) == H`, `state.indices.dim(2) == T`) to enforce consistency before invoking the Metal kernel.
</issue_to_address>

### Comment 2
<location path="Scripts/patches/KVCache.swift" line_range="1896-1897" />
<code_context>
-    public override func update(keys: MLXArray, values: MLXArray) -> (MLXArray, MLXArray) {
+    @discardableResult
+    private func ingest(keys: MLXArray, values: MLXArray) -> (TurboQuantMSEState, TurboQuantMSEState) {
         let previous = offset
         ensureCodecs(keyDim: keys.dim(3), valueDim: values.dim(3))

</code_context>
<issue_to_address>
**nitpick (bug_risk):** `previous` is computed but not used in `ingest`, which suggests either dead code or a missing use.

Since `previous` was previously passed to `appendShadow`, its presence here without any usage suggests ingestion/shadow logic may not have been fully migrated. If shadow handling is now separate, remove `previous`; otherwise, reintroduce the relevant call so state and shadow remain consistent.
</issue_to_address>

### Comment 3
<location path="docs/feature-codex-turboquant-metal.md" line_range="24" />
<code_context>
+- [KVCache.swift](/Volumes/edata/codex/dev/git/apr3/maclocal-api/Scripts/patches/KVCache.swift)
</code_context>
<issue_to_address>
**suggestion:** Consider using repository-relative links instead of absolute filesystem paths in markdown links.

Linking to `/Volumes/edata/...` will only work on your local machine. Use a repo-relative path like `/Scripts/patches/KVCache.swift` so the link works for other users and in documentation viewers.

```suggestion
- [KVCache.swift](/Scripts/patches/KVCache.swift)
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread Scripts/patches/KVCache.swift Outdated
Comment thread Scripts/patches/KVCache.swift Outdated
Comment thread docs/feature-codex-turboquant-metal.md Outdated
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.

1 participant