GLM prefill now attends only to the keys its indexer picks

2 days ago

This pull request makes long prompts much faster to prefill on GLM models with DeepSeek sparse attention. The indexer already picks the top-k keys for each query. Now prefill attention reads only those keys, gathered from the cache, instead of masking all the others. It's on by default once the cache holds about 16,000 tokens.

Ask about this presentation

Chapters

  1. 0:00Sparse prefill for GLM
  2. 0:22The dense mask
  3. 0:441 · How it works
  4. 0:46When it runs
  5. 1:09Sparse attention
  6. 1:38Blocked top-k
  7. 2:04What it touches
  8. 2:252 · Evidence
  9. 2:26Prefill speed
  10. 2:57Peak memory
  11. 3:28Accuracy and tests
  12. 3:57Limits and what to do
Show transcript
  1. This pull request makes long prompts much faster to prefill on GLM models with DeepSeek sparse attention. The indexer already picks the top-k keys for each query. Now prefill attention reads only those keys, gathered from the cache, instead of masking all the others. It's on by default once the cache holds about 16,000 tokens.

    Slide text
    The change How it works Evidence

    GLM prefill now attends only to the keys its indexer picks

    sparse_prefill · on by default from 16,384 cached tokens
    MLX
  2. Before this change, prefill turned the indexer's selection into a dense boolean mask, one row per query and one column per key, and then ran masked attention over every key. In the author's words, the selection is computed, then the work is spent on all N keys anyway. The mask and the expanded keys and values also grow linearly with context.

    Slide text
    The change How it works Evidence

    Prefill picked the top-k keys, then attended to every key anyway

    N keys in the cache
    dense [L, N] boolean mask · black = picked, gray = attended anyway
    Grows with context: the mask and the expanded K/V scale with N.
  3. Here's how the sparse path works.

    Slide text
    1

    How the sparse path works

  4. Two new fields on ModelArgs control it. Sparse prefill is on by default, with a minimum context of 16,384 tokens. It only runs for a single sequence, so batches keep the masked dense path. It only runs in prefill, so decode is untouched. And below the minimum context the dense path is faster, so the code keeps it there.

    Slide text
    The change How it works Evidence

    Two config fields decide when the sparse path runs

    class ModelArgs(BaseModelArgs):
        sparse_prefill: bool = True
        sparse_prefill_min_context: int = 16384
    
    # GlmMoeDsaAttention.__call__
    sparse = (
        self.sparse_prefill
        and B == 1
        and L > 1
        and kv_latent.shape[2] >= self.sparse_prefill_min_context
    )
    One sequence Batches keep the masked dense path.
    Prefill only Decode is untouched.
    16,384 cached tokens Below that, dense is faster.
  5. The attention half is a new function, sparse prefill. It joins the latent and rope caches into one 576-wide key per position, then gathers just the top-k rows each query picked. All heads share that gathered block. Attention runs in the absorbed latent space, the same form decode already uses, so each chunk costs L times top-k instead of L times N. Chunks longer than 1,024 queries split into query blocks, which keeps the gathered keys small.

    Slide text
    The change How it works Evidence

    Attention now costs L × topk per chunk, down from L × N

    Latent + rope cache N rows, 576 wide = 512 latent + 64 rope
    Gathered [L, K, 576], shared by all heads
    Attention absorbed latent space, as in decode
    O(L · topk)
    O(L · N)
    Chunks longer than 1,024 queries run in query blocks of _SPARSE_QUERY_BLOCK = 1024.
  6. The indexer gets the same treatment, in a function called blocked top-k. It scores the keys in blocks of 4,096 and folds each block into a running top-k list, so each argpartition sorts rows of top-k plus 4,096 instead of N. The full score tensor, batch by 32 heads by L by N, is no longer built. It selects the same keys as the stock indexer, up to the order of equal scores.

    Slide text
    The change How it works Evidence

    The indexer scores 4,096 keys at a time and keeps a running top-k

    keys 0–4,095
    4,096–8,191
    8,192–12,287
    12,288–16,383
    …
    running top-k argpartition over topk + 4,096, not N
    The full score tensor [B, 32, L, N] is no longer built.
  7. Almost all of it lands in two files: the model file, with about 100 added lines, and a new test file of 175 lines. The rest rides along. Pillow joins the package dependencies, the black pre-commit hook gets a version bump, and two init files turn the test folders into packages.

    Slide text
    The change How it works Evidence

    Two files carry the change

    mlx_lm/models/glm_moe_dsa.py +100 −1
    tests/models/test_glm_sparse_prefill.py +175
    pyproject.toml · adds pillow +1
    .pre-commit-config.yaml · black 26.10.1 +1 −1
    tests/__init__.py, tests/models/__init__.py +1 each
    4 more files under tests/models/ +0 −0
  8. Now the evidence.

    Slide text
    2

    The evidence

  9. This is an M3 Ultra running GLM 5.3 at 4-bit, with 512-token prefill chunks. Black is the patch, and the dashed gray line is the baseline. Up to 4,000 tokens of context the two match, because the gate keeps the dense path. From 16,000 on, the baseline falls off, and the patch slows far more gently. At 128,000 tokens, prefill goes from about 12 to 96 tokens per second, roughly eight times faster. Decode speed is unchanged.

    Slide text
    The change How it works Evidence

    At 128K context, prefill runs about 8× faster

    8.33×

    Prefill throughput at 128K context, 512-token chunks: 11.5 tok/s baseline, 96.0 tok/s patch.

    With mlx_lm.benchmark: 32K prompt 87.3 to 159.3 tok/s (1.82×); 64K prompt 51.6 to 142.2 tok/s (2.76×).

    11.5 to 96.0 tok/s at 128K.
    Decode: 1.00× at every depth.

    M3 Ultra, 512 GB · MLX 0.32.3
    mlx-community/GLM-5.3-4bit
    mean of 3 runs after 1 warmup

  10. Memory follows the same curve. With 1,024-token chunks, the baseline's peak climbs fast with context, and the patch's climbs slowly. At 128,000 tokens it drops from about 480 to about 440 gigabytes. With 2,048-token chunks at 128,000 tokens, the baseline was skipped because its predicted peak was above the GPU working set. The patch ran it, peaking at about 440 gigabytes.

    Slide text
    The change How it works Evidence

    Peak memory at 128K falls from 482.5 GB to 437.5 GB

    contextbaselinepatch
    0420.0 GB420.0 GB
    16K428.3 GB426.3 GB
    32K437.3 GB427.9 GB
    64K450.8 GB431.1 GB
    128K482.5 GB437.5 GB
    peak during the prefill chunk, 1,024-token chunks
    2,048-token chunks at 128K
    Baseline: skipped, predicted above the GPU working set
    Patch: 440.5 GB
  11. Output stays close to stock. On a prompt of about 12,000 tokens, mean KL divergence is 0.0046, and the top token matches stock at almost 99 of every 100 positions. Eight new tests cover the path. Sparse output matches dense within a ten-thousandth, batches and short or gated contexts take the dense path unchanged, query blocks match, and decode works after a sparse prefill. Three of them fail without this change.

    Slide text
    The change How it works Evidence

    Sparse prefill keeps the stock top token at almost every position

    98.68%

    top-1 agreement with stock
    mean KL divergence 0.0046

    12,288-token prompt, on M3

    8 new tests, 3 fail without the change
    test_default_is_on
    test_sparse_prefill_matches_dense
    test_blocked_topk_matches_indexer
    test_short_context_is_unchanged
    test_batch_uses_dense_path
    test_min_context_uses_dense_path
    test_query_blocks_match
    test_decode_after_sparse_prefill
  12. Two limits. Batches still take the dense path, and DeepSeek models with sparse attention aren't covered yet. They're planned for a follow-up, with a smaller expected gain, since their indexer runs on every layer. It's on by default, and setting sparse prefill to false in the model config turns it off. GLM prefill now attends only to the keys its indexer picks.

    Slide text

    Long GLM prompts prefill faster, one sequence at a time

    Batches still take the masked dense path.
    DeepSeek models with DSA are a planned follow-up, with a smaller expected gain.

    On by default. To turn it off, set "sparse_prefill": false in the model config.

    github.com/ml-explore/mlx-lm/pull/1997

What happened in AI today, in your inbox

One short email each evening. Read it and you’re caught up.

Built in New York with ❤️