Problem / Background
PR #412 (#408) hardened sliding-window >window single-pass prefill across models. For dense-KVCache sliding-window models (cohere2, gemma3n, olmo3) it routes mask construction through the shared create_sliding_window_prefill_mask(size, sliding_offset, window) helper and slices K/V to the mask's key axis.
That helper only builds the uncapped full windowed mask when size > window && sliding_offset == 0 (a FRESH prefill). For sliding_offset > 0 && seq_len > window (a rolled-over / continued prefill, e.g. a multi-turn continuation chunk longer than the window with prior cache, or a chunked-prefill chunk larger than the window) it falls through to the clamped [seq_len, window] mask.
This was explicitly left out of #412's scope. It is pre-existing (NOT introduced by #412): the behavior is byte-identical to the pre-#412 code path, so this is a latent degeneration, not a regression.
The bug
A dense KVCache retains ALL keys (unlike a RotatingKVCache, which trims to window). With the clamped [seq_len, window] mask, whose tril diagonal window - seq_len < 0, and the model slicing K/V to the trailing window keys, the earliest query rows (logical position < seq_len - window) are stranded with an all--inf row. That produces NaN, which decodes to <pad>.
So a rolled-over over-window prefill on cohere2 / gemma3n / olmo3 degenerates to <pad> / NaN output for the early positions.
Root cause (the asymmetry)
The shared helper's gate size > window && sliding_offset == 0 is NARROWER than the two other windowed-mask gates in the tree:
Models that reach causal_attention's windowed path (e.g. baichuan, gemma4) therefore handle the offset>0 over-window case correctly. The dense-cache models that build the helper mask directly do not, because of the extra sliding_offset == 0 condition.
Proposed Solution
Extend create_sliding_window_prefill_mask (or the dense-cache call sites) to build the correct full windowed mask for the sliding_offset > 0 && seq_len > window case as well: the full [seq_len, seq_len + sliding_offset] windowed-causal mask over all retained keys, mirroring create_causal_mask_with_window_full with a non-zero offset. Keep the dense-cache mask-key-axis slicing consistent (a full mask keeps every key).
Coordinate with #410 (the sliding_prefill_mask consolidation), since both touch this helper and its gate. Verify that gemma3 / gemma4 single-process and the RotatingKVCache models are not affected by the helper change (RotatingKVCache trims to window, so its offset>0 path differs and must not be regressed).
Acceptance Criteria
Technical Considerations
Problem / Background
PR #412 (#408) hardened sliding-window
>windowsingle-pass prefill across models. For dense-KVCachesliding-window models (cohere2, gemma3n, olmo3) it routes mask construction through the sharedcreate_sliding_window_prefill_mask(size, sliding_offset, window)helper and slices K/V to the mask's key axis.That helper only builds the uncapped full windowed mask when
size > window && sliding_offset == 0(a FRESH prefill). Forsliding_offset > 0 && seq_len > window(a rolled-over / continued prefill, e.g. a multi-turn continuation chunk longer than the window with prior cache, or a chunked-prefill chunk larger than the window) it falls through to the clamped[seq_len, window]mask.This was explicitly left out of #412's scope. It is pre-existing (NOT introduced by #412): the behavior is byte-identical to the pre-#412 code path, so this is a latent degeneration, not a regression.
The bug
A dense
KVCacheretains ALL keys (unlike aRotatingKVCache, which trims towindow). With the clamped[seq_len, window]mask, whosetrildiagonalwindow - seq_len < 0, and the model slicing K/V to the trailingwindowkeys, the earliest query rows (logical position< seq_len - window) are stranded with an all--infrow. That produces NaN, which decodes to<pad>.So a rolled-over over-window prefill on cohere2 / gemma3n / olmo3 degenerates to
<pad>/ NaN output for the early positions.Root cause (the asymmetry)
The shared helper's gate
size > window && sliding_offset == 0is NARROWER than the two other windowed-mask gates in the tree:causal_attention's windowed path gates onq_len > 1 && k_len > window(no offset gate; the offset case was fixed in fix: harden sliding-window >window prefill across non-Gemma models; guard causal_attention windowed K/V slice #408).sliding_prefill_maskgates onl > window(no offset gate; protected bytrim_mask_to_keys).Models that reach
causal_attention's windowed path (e.g. baichuan, gemma4) therefore handle theoffset>0over-window case correctly. The dense-cache models that build the helper mask directly do not, because of the extrasliding_offset == 0condition.Proposed Solution
Extend
create_sliding_window_prefill_mask(or the dense-cache call sites) to build the correct full windowed mask for thesliding_offset > 0 && seq_len > windowcase as well: the full[seq_len, seq_len + sliding_offset]windowed-causal mask over all retained keys, mirroringcreate_causal_mask_with_window_fullwith a non-zero offset. Keep the dense-cache mask-key-axis slicing consistent (a full mask keeps every key).Coordinate with #410 (the
sliding_prefill_maskconsolidation), since both touch this helper and its gate. Verify that gemma3 / gemma4 single-process and theRotatingKVCachemodels are not affected by the helper change (RotatingKVCachetrims towindow, so itsoffset>0path differs and must not be regressed).Acceptance Criteria
<pad>/ NaN.RotatingKVCachemodels and gemma3 / gemma4 are not regressed.offset>0over-window case.Technical Considerations
KVCachevsRotatingKVCachedivergence is the crux: the helper change must keep every retained key for dense caches while leaving the trimming-cache path alone.