1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//! Prefill attention-mask policy — the per-query KEY SPAN a decoder column may read.
//!
//! The engine's attention kernels ([`crate::forward`]'s `ATTN_K` family) never materialize a 2-D
//! mask. Each query column carries its own contiguous key span and the kernel walks it, which is
//! why a mask policy here is a SPAN RULE and not a new kernel family: see `ATTN_K`'s
//! `t = cmeta[..] + 1` (span end) and `s0 = t - window` (span start).
//!
//! Today every decoder column is causal — span end is the column's own position. That is correct
//! for text and for Qwen3-VL, and WRONG for Gemma 3, whose decoder attends bidirectionally within
//! each contiguous run of image tokens. See `bench/gemma3_mask_fixture/` for the HF-derived
//! reference this module is gated against, and `docs/LITERT_LM_PARITY_PLAN.md` item V1.
/// The inclusive key span `[lo, hi]` a query column attends.
///
/// Both ends are absolute positions in the sequence, so `hi` is the LAST key read (not a
/// one-past-the-end bound) — `ATTN_K` consumes it as `t = hi + 1`.
/// Which mask a prefill column uses.
/// Per-layer key-range floor: `None` for a global (full-attention) layer, `Some(window)` for a
/// sliding-window layer, where the layer may read keys `k > q - window`.
///
/// This is the global-vs-local split: the same policy produces different spans per layer type,
/// and a model can be causal-global / bidirectional-local.
pub type Window = ;
/// The span start for query `q` under `window` — identical to `ATTN_K`'s `s0`.
///
/// The floor is a function of the QUERY position, never of the span end: HF composes the window
/// as `AND(k > q - window, ...)`, so widening the top of a span must not drag its floor up with
/// it. Getting this wrong is invisible until an image run ends beyond `window` tokens in.
/// The key spans for a prefill chunk of `n` columns, one per query column.
///
/// `is_image[q]` marks a column belonging to a bidirectionally-attended block. It is deliberately
/// NOT a token-id test (no hard-coded linguistic data) and NOT "the embedding came from the vision
/// arena" — Gemma 3 is exactly the case that separates the two, attending bidirectionally over
/// image runs while having no M-RoPE and therefore no arena. See `BatchCol::is_image`.
/// One column of a batch step, as far as the mask is concerned.
/// Per-column span END (`hi`) for a whole batch step — the value the attention meta carries.
///
/// A batch step interleaves columns from DIFFERENT sequences (continuous batching) in arbitrary
/// order, so a run is a maximal set of columns that share a `btrow`, are all image, and occupy
/// CONSECUTIVE positions. Two image columns that merely sit next to each other in the batch are
/// not a run — that would let one sequence's image widen into another's, which is why this takes
/// `MaskCol` rather than the flat `is_image` slice [`spans`] uses.
///
/// A run's ceiling is the largest position of that run PRESENT IN THIS STEP. Keys beyond the step
/// are not in the KV cache yet, so they cannot be read regardless of policy — see
/// [`chunk_splits_image_run`], which is how a caller detects that it has cut a run short.
/// Does this chunk split an image run at its END boundary?
///
/// A column may only read keys already written to the KV cache. Under
/// [`MaskPolicy::VisionBidirectional`] an image column reads FORWARD, so if a prefill chunk ends
/// mid-image-run the keys past the boundary do not exist yet and the run's spans would be
/// silently truncated — the failure mode V1 exists to prevent, one step removed. Callers that
/// chunk prefill must extend the chunk to the run's end (or refuse) when this returns true.