Skip to content

feat: add Gemma2 sliding-window (local/global) attention#555

Merged
inureyes merged 2 commits into
mainfrom
feature/issue-495-gemma2-sliding-window
Jul 1, 2026
Merged

feat: add Gemma2 sliding-window (local/global) attention#555
inureyes merged 2 commits into
mainfrom
feature/issue-495-gemma2-sliding-window

Conversation

@inureyes

@inureyes inureyes commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Implements Gemma2's alternating sliding-window (local) / full (global) attention in the OpenXLA emitter (issue #495). The emitter previously emitted global attention on every layer, which is correct only while the context stays under the window; the local (even) layers must stop attending to keys older than the window. The mask is wired from Config::from_json, so a Gemma2 checkpoint runs with the correct local/global alternation on both the single-sequence CLI (MLXCEL_BACKEND=xla) and the mlxcel-server serve path, since both compile the same emitter graphs at load.

What changed

  • emitter/config.rs: new Option<usize> sliding_window field, read from a gemma2 config.json (HF Gemma2 default 4096), left None for Llama/Qwen2 (Qwen2's own sliding_window is ignored, matching use_sliding_window = false). New is_sliding_layer(li) schedule: even layers local, odd global (matches HF Gemma2DecoderLayer).
  • emitter/model.rs: in each of the three graph kinds (single decode, prefill, ragged decode), build a local mask once by anding the window into the existing causal mask with one extra select (within-window keeps the causal value, else -1e30); each local layer broadcasts that mask, each global layer the unchanged causal mask. Emitted only for a windowed config, so Llama/Qwen2 graphs stay byte-identical; a value no-op when the window >= MAX_SEQ, so short-context Gemma2 output is unchanged.
  • emitter/mod.rs: structural tests plus an opt-in (#[ignore]) graph-dump test used by the execution check.
  • spike/openxla/gemma2_sliding_window_check.py: independent HF-oracle execution check (emit -> IREE llvm-cpu -> compare to HF eager fp32).

Validation

Numbers are from the local run; the full >4096-token GPU sweep is bounded by MAX_SEQ = 256 (a separate follow-up) and is left for the post-merge gate.

  • cargo test -p mlxcel-xla --lib emitter::tests: 16 passed (1 ignored). Includes the Llama-3.2-1B byte-for-byte asset test (non-Gemma2 emission unchanged), the even-local schedule, the one-time local-mask block per graph, the emitted window constant, and the per-layer alternation (even layers consume the local mask, odd the global, and they differ).
  • spike/openxla/gemma2_sliding_window_check.py: a matched synthetic Gemma2 is token-exact and bit-exact vs HF eager fp32 at an inert window (max |logit diff| 4.5e-8) and at a window small enough to bite inside the 256-slot cache (5.9e-8); HF's own output changes by 4.7e-2 between the two, so the biting case is not vacuous.

Test plan

  • cargo check -p mlxcel-xla --lib --tests
  • cargo clippy -p mlxcel-xla --lib --tests -- -D warnings
  • cargo test -p mlxcel-xla --lib emitter::tests
  • spike/openxla/gemma2_sliding_window_check.py (HF fp32 oracle, inert + biting window)

Closes #495

Gemma2 alternates sliding-window (local) and full (global) attention across layers, but the OpenXLA emitter previously emitted global attention on every layer. That is correct only while the context stays under the window; once it exceeds the window the local layers must stop attending to older keys, so long-context Gemma2 output was wrong. This adds the per-layer sliding-window mask so the local/global alternation matches Gemma2 (issue #495).

Config gains an Option<usize> sliding_window, read from a gemma2 checkpoint's config.json (HF default 4096) and left None for Llama/Qwen2 (whose own sliding_window field is ignored, matching use_sliding_window = false). is_sliding_layer encodes the Gemma2 schedule: even layers local, odd layers global. The three graph kinds the CLI and server compile at load (single decode, prefill, ragged decode) build a local mask once by anding the window into the existing causal mask with one extra select (within-window keeps the causal value, else -1e30), and each local layer broadcasts that mask instead of the global one. The local mask is emitted only for a windowed config, so Llama/Qwen2 graphs are byte-identical (the bundled Llama-3.2-1B asset test still passes), and for a window >= MAX_SEQ it is a value no-op, so short-context Gemma2 output is unchanged.

Validated by pure-Rust structural tests (window parse and default, the even-local schedule, one local-mask block per graph, the window constant emitted, and the per-layer alternation) plus an independent HF-oracle execution check (spike/openxla/gemma2_sliding_window_check.py): a matched synthetic Gemma2 is token-exact and bit-exact (max abs logit diff ~6e-8) against HF eager fp32 at both an inert window and a window small enough to bite inside the 256-slot cache, with HF's own output changing between the two.
@inureyes inureyes added type:enhancement New features, capabilities, or significant additions priority:medium Medium priority area:architecture Architecture and code structure changes status:done Completed labels Jul 1, 2026
origin/main landed #554 (issue #494), which unified the three attention emit paths (single decode, ragged decode, prefill) into one shared `emit_attention` driver parameterized by an `AttnLayout` enum, applying the additive key mask in a single shared step. This branch (issue #495) had added Gemma2 sliding-window (local/global alternation) to the pre-#494 three separate emit paths, so the two changes collided in `emitter/model.rs`.

Resolved by fitting #495's per-layer mask choice into #494's shared core: each `AttnLayout` variant now carries an optional `mask_local` (the sliding-window mask, built once per graph head only when a window is configured), and the shared `add_mask` step selects local vs global per layer through a single `layer_mask`/`is_sliding_layer` check. All three layouts (Single, Ragged, Prefill) get the alternation uniformly from one authoring site.

Non-windowed architectures (Llama, Qwen2) parse `sliding_window = None`, so no window ops are emitted and `layer_mask` returns the global mask handle unchanged: the emitted graphs stay byte-for-byte identical to origin/main, confirmed by the Llama byte-exact asset guard across all five graph kinds. Also unified the two `gemma2_like` test helpers (one added by each side) into a single windowed helper that keeps #494's non-square o_proj coverage and #495's four-layer alternation.

emitter::tests: 17 passed, 1 ignored; clippy and fmt clean.
@inureyes
inureyes merged commit d29a2e8 into main Jul 1, 2026
5 checks passed
@inureyes
inureyes deleted the feature/issue-495-gemma2-sliding-window branch July 2, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:architecture Architecture and code structure changes priority:medium Medium priority status:done Completed type:enhancement New features, capabilities, or significant additions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: implement Gemma2 sliding-window attention (local/global alternation)

1 participant