Qubitium commited on
Commit
ede5883
·
verified ·
1 Parent(s): f131c53

Transformers 5.x generate() and attention-mask fixes for Ripple

Browse files
Files changed (3) hide show
  1. PATCH.diff +260 -0
  2. README.md +122 -0
  3. modeling_ricursive.py +733 -0
PATCH.diff ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --- a/modeling_ricursive.py
2
+ +++ b/modeling_ricursive.py
3
+ @@ -6,6 +6,25 @@
4
+ - Gated attention: elementwise sigmoid gate on attention output
5
+ - Mixed local/global layers: local = sliding window + RoPE, global = full attention + no RoPE
6
+ - Per-head Q/K RMSNorm
7
+ +
8
+ +Transformers 5.x compatibility (2026-11-05)
9
+ +------------------------------------------
10
+ +`generate()` used to break on the very first prefill because
11
+ +`prepare_inputs_for_generation` sliced `input_ids` with a `None`
12
+ +`cache_position`, and the causal/sliding-window mask builders assumed the
13
+ +transformers 4.x cache API. The fixes are applied inline below:
14
+ +
15
+ +* `prepare_inputs_for_generation` rebuilds a missing `cache_position`, slices
16
+ + only when the cache is warm, derives per-row `position_ids` from the padding
17
+ + mask, and moves inputs to the model device.
18
+ +* `_update_causal_mask` sizes the key axis from `cache_position` instead of the
19
+ + removed `get_max_cache_shape()`, emits `0.0` for visible keys, and never
20
+ + returns an all-masked single-token decode row.
21
+ +* `_apply_sliding_window_mask` anchors key positions at the first key the layer
22
+ + actually holds (the cache evicts from the left) and is skipped when the
23
+ + window cannot bind.
24
+ +* `RicursiveForCausalLM` inherits `GenerationMixin` explicitly; transformers 5
25
+ + no longer injects it into remote-code classes.
26
+ """
27
+
28
+ from typing import Optional, Tuple, Union
29
+ @@ -20,9 +39,20 @@
30
+ CausalLMOutputWithPast,
31
+ )
32
+
33
+ +try: # transformers >= 4.26, re-exported from here in 5.x
34
+ + from transformers.generation.utils import GenerationMixin
35
+ +except ImportError: # pragma: no cover - very old transformers
36
+ + from transformers.generation import GenerationMixin
37
+ +
38
+ from .configuration_ricursive import RicursiveConfig
39
+
40
+
41
+ +# Set when the transformers-5.x `generate()` fixes above are present natively.
42
+ +# Downstream loaders that monkeypatch older checkpoints check this marker and
43
+ +# stand down instead of re-installing the equivalent runtime patch.
44
+ +RICURSIVE_TF5_COMPAT = "2026-11-05"
45
+ +
46
+ +
47
+ class RicursiveRMSNorm(nn.Module):
48
+ def __init__(self, hidden_size: int, eps: float = 1e-6):
49
+ super().__init__()
50
+ @@ -140,13 +170,37 @@
51
+ if self.layer_type != "sliding_attention":
52
+ return attention_mask
53
+
54
+ - key_positions = torch.arange(kv_len, device=device)
55
+ + # transformers >= 5 builds a hybrid cache: a sliding layer holds at most
56
+ + # `sliding_window` keys while a full-attention layer keeps growing, so the
57
+ + # shared mask produced by `_update_causal_mask` can be longer than this
58
+ + # layer's key tensor. Keep only the keys this layer actually holds.
59
+ + if attention_mask is not None and attention_mask.shape[-1] > kv_len:
60
+ + attention_mask = attention_mask[..., -kv_len:]
61
+ +
62
+ if cache_position is not None:
63
+ - query_positions = cache_position.to(device=device)
64
+ + query_positions = cache_position.to(device=device).reshape(-1)
65
+ else:
66
+ query_positions = torch.arange(kv_len - q_len, kv_len, device=device)
67
+
68
+ window = self.config.sliding_window
69
+ + if window is None:
70
+ + return attention_mask
71
+ + if kv_len <= window:
72
+ + # The window cannot exclude any key, so the result is either the
73
+ + # unchanged (already causal) mask or, without one, plain causal
74
+ + # attention -- which SDPA applies through `is_causal`.
75
+ + if attention_mask is None:
76
+ + return None
77
+ + if q_len == 1:
78
+ + return attention_mask
79
+ +
80
+ + # Keys are contiguous and end at the newest query position. After the cache
81
+ + # evicts from the left they no longer start at position 0, so anchor the
82
+ + # range at the first key the layer holds instead of at zero.
83
+ + key_positions = torch.arange(kv_len, device=device) + (
84
+ + query_positions[-1] - kv_len + 1
85
+ + )
86
+ +
87
+ allowed = (
88
+ (key_positions[None, :] <= query_positions[:, None])
89
+ & (key_positions[None, :] >= query_positions[:, None] - (window - 1))
90
+ @@ -466,43 +520,66 @@
91
+ dtype, device = input_tensor.dtype, input_tensor.device
92
+ sequence_length = input_tensor.shape[1]
93
+
94
+ - if past_key_values is not None:
95
+ - target_length = past_key_values.get_max_cache_shape()
96
+ - if target_length is None or target_length < 0:
97
+ - target_length = (
98
+ - cache_position[-1].item() + 1 if cache_position is not None else sequence_length
99
+ - )
100
+ + if cache_position is None:
101
+ + query_positions = torch.arange(sequence_length, device=device)
102
+ else:
103
+ - target_length = sequence_length
104
+ + query_positions = cache_position.to(device=device).reshape(-1)
105
+
106
+ - causal_mask = torch.full(
107
+ - (sequence_length, target_length),
108
+ - fill_value=torch.finfo(dtype).min,
109
+ + # Size the key axis from `cache_position`, which is exactly what the
110
+ + # attention layer uses to size its key tensor.
111
+ + # `past_key_values.get_max_cache_shape()` was removed in transformers 5,
112
+ + # and its replacement reports the *capacity* (e.g. `sliding_window`),
113
+ + # which disagrees with the live key count whenever the cache is not full.
114
+ + target_length = int(query_positions[-1].item()) + 1
115
+ + target_length = max(target_length, attention_mask.shape[-1])
116
+ +
117
+ + # A shorter padding mask leaves the trailing keys unspecified; treat them as
118
+ + # visible -- the causal term below still applies.
119
+ + if attention_mask.shape[-1] < target_length:
120
+ + pad = attention_mask.new_ones(
121
+ + attention_mask.shape[0], target_length - attention_mask.shape[-1]
122
+ + )
123
+ + attention_mask = torch.cat([pad, attention_mask], dim=-1)
124
+ +
125
+ + key_positions = torch.arange(target_length, device=device)
126
+ + allowed = key_positions[None, :] <= query_positions[:, None]
127
+ + allowed = allowed[None, None, :, :] & attention_mask[:, None, None, :].to(torch.bool)
128
+ +
129
+ + causal_mask = torch.zeros(
130
+ + (attention_mask.shape[0], 1, sequence_length, target_length),
131
+ dtype=dtype,
132
+ device=device,
133
+ )
134
+ - if sequence_length != 1:
135
+ - causal_mask = torch.triu(causal_mask, diagonal=target_length - sequence_length + 1)
136
+ -
137
+ - causal_mask = causal_mask[None, None, :, :].expand(input_tensor.shape[0], 1, -1, -1)
138
+ + # `0.0` for visible keys (not `1.0`, which would bias every score) and the
139
+ + # dtype minimum elsewhere. Building the mask from `allowed` also keeps a
140
+ + # single-token decode step from coming out fully masked.
141
+ + return causal_mask.masked_fill(~allowed, torch.finfo(dtype).min)
142
+ +
143
+ +
144
+ +def _cache_seq_length(past_key_values: Optional[Cache]) -> int:
145
+ + """Number of cached keys, across transformers 4.x and 5.x cache objects."""
146
+ + if past_key_values is None:
147
+ + return 0
148
+ + getter = getattr(past_key_values, "get_seq_length", None)
149
+ + if callable(getter):
150
+ + return int(getter())
151
+ + try:
152
+ + return int(past_key_values[0][0].shape[-2])
153
+ + except Exception: # pragma: no cover - unknown cache implementations
154
+ + return 0
155
+
156
+ - if attention_mask.dim() == 2:
157
+ - mask_length = attention_mask.shape[-1]
158
+ - padding_mask = causal_mask[:, :, :, :mask_length] + attention_mask[:, None, None, :].to(
159
+ - causal_mask.dtype
160
+ - ).masked_fill(attention_mask[:, None, None, :] == 0, torch.finfo(dtype).min)
161
+ - causal_mask = (
162
+ - torch.cat([padding_mask, causal_mask[:, :, :, mask_length:]], dim=-1)
163
+ - if mask_length < target_length
164
+ - else padding_mask
165
+ - )
166
+ -
167
+ - return causal_mask
168
+
169
+ -
170
+ -class RicursiveForCausalLM(RicursivePreTrainedModel):
171
+ +class RicursiveForCausalLM(RicursivePreTrainedModel, GenerationMixin):
172
+ _tied_weights_keys = []
173
+
174
+ + # transformers 5 no longer injects `GenerationMixin` into remote-code classes,
175
+ + # so inherit it explicitly above. Without it `generate` is missing entirely and
176
+ + # `PreTrainedModel.__init__` never builds `generation_config`.
177
+ + #
178
+ + # This attribute marks the native transformers-5 compatibility fixes so that
179
+ + # downstream loaders can skip their runtime monkeypatch.
180
+ + _ricursive_tf5_native_fix = RICURSIVE_TF5_COMPAT
181
+ +
182
+ def __init__(self, config: RicursiveConfig):
183
+ super().__init__(config)
184
+ self.model = RicursiveModel(config)
185
+ @@ -583,20 +660,70 @@
186
+ inputs_embeds: Optional[torch.FloatTensor] = None,
187
+ cache_position: Optional[torch.LongTensor] = None,
188
+ use_cache: Optional[bool] = None,
189
+ + position_ids: Optional[torch.LongTensor] = None,
190
+ **kwargs,
191
+ ):
192
+ - if past_key_values is not None:
193
+ - if input_ids.shape[1] != 1:
194
+ - input_ids = input_ids[:, cache_position]
195
+ + # transformers 5.x does not move remote-code inputs onto the model device for
196
+ + # this model, so a plain CPU `input_ids` reaches the embedding lookup and
197
+ + # raises. Its own generation helper performs this move, so mirror it here.
198
+ + device = getattr(self, "device", None)
199
+ + if device is not None and getattr(device, "type", None) != "meta":
200
+ + if input_ids is not None and input_ids.device != device:
201
+ + input_ids = input_ids.to(device)
202
+ + if attention_mask is not None and attention_mask.device != device:
203
+ + attention_mask = attention_mask.to(device)
204
+ + if inputs_embeds is not None and inputs_embeds.device != device:
205
+ + inputs_embeds = inputs_embeds.to(device)
206
+ + # transformers derives `position_ids` from the *caller's* tensors before
207
+ + # this hook runs, so a CPU prompt yields CPU positions even after the
208
+ + # move above. RoPE then multiplies them against CUDA frequencies and fails.
209
+ + if position_ids is not None and position_ids.device != device:
210
+ + position_ids = position_ids.to(device)
211
+ +
212
+ + past_length = _cache_seq_length(past_key_values)
213
+ +
214
+ + if cache_position is None:
215
+ + # With a non-empty cache and the full sequence supplied, only the trailing
216
+ + # (uncached) tokens are new. Otherwise every token in `input_ids` is new.
217
+ + if past_length > 0 and input_ids.shape[1] > 1:
218
+ + new_len = input_ids.shape[1] - past_length
219
+ + cache_position = torch.arange(
220
+ + past_length, past_length + new_len, device=input_ids.device
221
+ + )
222
+ + else:
223
+ + cache_position = torch.arange(
224
+ + past_length, past_length + input_ids.shape[1], device=input_ids.device
225
+ + )
226
+ +
227
+ + # Only slice when the cache is warm and `input_ids` still carries cached
228
+ + # tokens. Slicing unconditionally turned a `None` cache position into
229
+ + # `input_ids[:, None]` and injected a bogus axis.
230
+ + sliced = past_length > 0 and input_ids.shape[1] > cache_position.numel()
231
+ + if sliced:
232
+ + input_ids = input_ids[:, cache_position]
233
+
234
+ - if inputs_embeds is not None and cache_position[0] == 0:
235
+ + if inputs_embeds is not None and cache_position.numel() and int(cache_position[0]) == 0:
236
+ model_inputs = {"inputs_embeds": inputs_embeds}
237
+ else:
238
+ model_inputs = {"input_ids": input_ids}
239
+
240
+ + # Derive per-row positions from the padding mask. Using `cache_position` for
241
+ + # every row is only correct when nothing is padded: with left padding the real
242
+ + # tokens of a short row sit at higher indices, so a shared position vector
243
+ + # shifts its RoPE phases and the row decodes to garbage.
244
+ + if position_ids is not None:
245
+ + if sliced:
246
+ + position_ids = position_ids[:, cache_position]
247
+ + elif attention_mask is not None and attention_mask.dim() == 2:
248
+ + position_ids = attention_mask.long().cumsum(-1) - 1
249
+ + position_ids = position_ids.masked_fill(attention_mask == 0, 0)
250
+ + position_ids = position_ids[:, -input_ids.shape[1]:]
251
+ + else:
252
+ + position_ids = cache_position.unsqueeze(0)
253
+ +
254
+ model_inputs.update(
255
+ {
256
+ - "position_ids": cache_position.unsqueeze(0) if cache_position is not None else None,
257
+ + "position_ids": position_ids,
258
+ "past_key_values": past_key_values,
259
+ "use_cache": use_cache,
260
+ "attention_mask": attention_mask,
README.md ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ library_name: transformers
4
+ tags:
5
+ - ricursive
6
+ - ripple
7
+ - transformers
8
+ - patch
9
+ ---
10
+
11
+ # Ripple-65B-Preview — Transformers 5.x compatibility patch
12
+
13
+ Drop-in replacement for `modeling_ricursive.py` from
14
+ [`ricursiveAI/Ripple-65B-Preview`](https://huggingface.co/ricursiveAI/Ripple-65B-Preview).
15
+
16
+ The upstream custom code was written against the Transformers 4.x cache API.
17
+ Under Transformers 5.x, `generate()` fails on the very first prefill, and even
18
+ after that single line is fixed, padded batches decode incorrectly and
19
+ generations longer than `sliding_window` crash.
20
+
21
+ This repository distributes the fix only. It contains **no weights**; use it
22
+ with the original checkpoint.
23
+
24
+ ## Apply
25
+
26
+ Copy the patched file over the upstream one:
27
+
28
+ ```bash
29
+ cp modeling_ricursive.py /path/to/Ripple-65B-Preview/modeling_ricursive.py
30
+ ```
31
+
32
+ Or apply the unified diff from the root of a checkout of the model repository:
33
+
34
+ ```bash
35
+ git apply PATCH.diff
36
+ ```
37
+
38
+ Then load as usual:
39
+
40
+ ```python
41
+ from transformers import AutoModelForCausalLM
42
+
43
+ model = AutoModelForCausalLM.from_pretrained(
44
+ "ricursiveAI/Ripple-65B-Preview",
45
+ trust_remote_code=True,
46
+ )
47
+ ```
48
+
49
+ ## What is broken upstream
50
+
51
+ 1. **Spurious axis on prefill.** `prepare_inputs_for_generation` does
52
+ `input_ids[:, cache_position]` whenever `input_ids.shape[1] != 1`.
53
+ Transformers 5 pre-creates an empty `DynamicCache` and passes
54
+ `cache_position=None` for the prefill call, so this becomes
55
+ `input_ids[:, None]`: `(batch, seq)` turns into `(batch, 1, seq)`. The
56
+ embedding lookup returns 4-D hidden states and the first decoder layer dies
57
+ on `bsz, q_len, _ = hidden_states.shape`.
58
+
59
+ 2. **Every decode step fully masked.** `_update_causal_mask` seeds the mask with
60
+ `finfo.min` and only writes the visible diagonal via `triu` when
61
+ `sequence_length != 1`. At decode `sequence_length == 1`, so the whole row
62
+ stays masked and the model emits a constant token. The function also writes
63
+ `1.0` (not `0.0`) for visible keys, which biases every score, and calls
64
+ `past_key_values.get_max_cache_shape()`, which Transformers 5 removed.
65
+
66
+ 3. **`position_ids` ignore padding.** They are overwritten with
67
+ `cache_position` for every row, which is only correct when nothing is padded.
68
+ With left padding, a short row's real tokens sit at higher indices, so a
69
+ shared position vector shifts its RoPE phases and the row decodes to garbage.
70
+
71
+ 4. **Sliding window uses the wrong absolute positions.**
72
+ `_apply_sliding_window_mask` builds `arange(kv_len)` and assumes the keys sit
73
+ at positions `0..kv_len-1`. Once the hybrid cache evicts from the left they
74
+ sit at `[cp-kv_len+1 .. cp]`, so the mask excludes progressively more valid
75
+ keys (e.g. window 16 at position 32 leaves 0 of 16 keys visible).
76
+
77
+ 5. **Mask shape crash past the window.** Transformers 5 uses a hybrid cache:
78
+ sliding layers hold at most `sliding_window` keys while full-attention layers
79
+ keep growing. The model builds one mask per forward, so the shape no longer
80
+ matches the sliding layer's key tensor
81
+ (`RuntimeError: expanded size (1024) must match existing size (1025)`).
82
+
83
+ 6. **`position_ids` left on CPU.** Transformers derives them from the caller's
84
+ tensors before the generation hook runs, so a CPU prompt yields CPU positions
85
+ even after the inputs are moved, and RoPE fails with `mat2 is on cpu`.
86
+
87
+ 7. **Missing `GenerationMixin`.** Transformers 5 no longer injects it into
88
+ remote-code classes, so `generate` can be missing entirely and
89
+ `generation_config` is never built.
90
+
91
+ ## What the patch changes
92
+
93
+ * `prepare_inputs_for_generation` rebuilds a missing `cache_position`, slices
94
+ only when the cache is warm, derives per-row `position_ids` from the padding
95
+ mask, and moves `input_ids` / `attention_mask` / `inputs_embeds` /
96
+ `position_ids` onto the model device.
97
+ * `_update_causal_mask` sizes the key axis from `cache_position`, emits `0.0`
98
+ for visible keys and `finfo.min` elsewhere, and never returns an all-masked
99
+ single-token decode row.
100
+ * `_apply_sliding_window_mask` trims the shared mask to the keys the layer
101
+ holds, anchors key positions at the first key the cache still holds, and is
102
+ skipped when the window cannot bind.
103
+ * `RicursiveForCausalLM` inherits `GenerationMixin` explicitly.
104
+
105
+ The patch is backward compatible with Transformers 4.x.
106
+
107
+ ## Validation
108
+
109
+ * `generate()` works under Transformers 5.22.1.
110
+ * Cached and uncached generation are bit-identical, including past
111
+ `sliding_window` (the eviction path).
112
+ * Left-padded batches decode bit-identically to the same rows generated
113
+ unpadded.
114
+ * The patched implementation is bit-identical to an independently written
115
+ reference patch across prefill logits, decode, padded and uncached paths.
116
+
117
+ ## Files
118
+
119
+ ```
120
+ modeling_ricursive.py patched drop-in replacement
121
+ PATCH.diff unified diff against the upstream file
122
+ ```
modeling_ricursive.py ADDED
@@ -0,0 +1,733 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HuggingFace modeling implementation for the Ricursive transformer architecture.
2
+
3
+ Designed for inference with vLLM (via --trust-remote-code). Key differences from
4
+ standard Llama/Mistral:
5
+ - PeriNorm blocks: 4 layer norms per block (pre/post attention + pre/post FFN)
6
+ - Gated attention: elementwise sigmoid gate on attention output
7
+ - Mixed local/global layers: local = sliding window + RoPE, global = full attention + no RoPE
8
+ - Per-head Q/K RMSNorm
9
+
10
+ Transformers 5.x compatibility (2026-11-05)
11
+ ------------------------------------------
12
+ `generate()` used to break on the very first prefill because
13
+ `prepare_inputs_for_generation` sliced `input_ids` with a `None`
14
+ `cache_position`, and the causal/sliding-window mask builders assumed the
15
+ transformers 4.x cache API. The fixes are applied inline below:
16
+
17
+ * `prepare_inputs_for_generation` rebuilds a missing `cache_position`, slices
18
+ only when the cache is warm, derives per-row `position_ids` from the padding
19
+ mask, and moves inputs to the model device.
20
+ * `_update_causal_mask` sizes the key axis from `cache_position` instead of the
21
+ removed `get_max_cache_shape()`, emits `0.0` for visible keys, and never
22
+ returns an all-masked single-token decode row.
23
+ * `_apply_sliding_window_mask` anchors key positions at the first key the layer
24
+ actually holds (the cache evicts from the left) and is skipped when the
25
+ window cannot bind.
26
+ * `RicursiveForCausalLM` inherits `GenerationMixin` explicitly; transformers 5
27
+ no longer injects it into remote-code classes.
28
+ """
29
+
30
+ from typing import Optional, Tuple, Union
31
+
32
+ import torch
33
+ import torch.nn as nn
34
+ import torch.nn.functional as F
35
+ from transformers import PreTrainedModel
36
+ from transformers.cache_utils import Cache, DynamicCache
37
+ from transformers.modeling_outputs import (
38
+ BaseModelOutputWithPast,
39
+ CausalLMOutputWithPast,
40
+ )
41
+
42
+ try: # transformers >= 4.26, re-exported from here in 5.x
43
+ from transformers.generation.utils import GenerationMixin
44
+ except ImportError: # pragma: no cover - very old transformers
45
+ from transformers.generation import GenerationMixin
46
+
47
+ from .configuration_ricursive import RicursiveConfig
48
+
49
+
50
+ # Set when the transformers-5.x `generate()` fixes above are present natively.
51
+ # Downstream loaders that monkeypatch older checkpoints check this marker and
52
+ # stand down instead of re-installing the equivalent runtime patch.
53
+ RICURSIVE_TF5_COMPAT = "2026-11-05"
54
+
55
+
56
+ class RicursiveRMSNorm(nn.Module):
57
+ def __init__(self, hidden_size: int, eps: float = 1e-6):
58
+ super().__init__()
59
+ self.weight = nn.Parameter(torch.ones(hidden_size))
60
+ self.eps = eps
61
+
62
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
63
+ input_dtype = hidden_states.dtype
64
+ hidden_states = hidden_states.float()
65
+ variance = hidden_states.pow(2).mean(-1, keepdim=True)
66
+ hidden_states = hidden_states * torch.rsqrt(variance + self.eps)
67
+ return self.weight * hidden_states.to(input_dtype)
68
+
69
+
70
+ class RicursiveRotaryEmbedding(nn.Module):
71
+ def __init__(self, dim: int, theta: float = 10000.0):
72
+ super().__init__()
73
+ self.dim = dim
74
+ self.theta = theta
75
+ # NOTE: inv_freq is computed on-the-fly in forward() rather than stored
76
+ # as a buffer, because non-persistent buffers get corrupted during
77
+ # from_pretrained() loading.
78
+
79
+ @torch.no_grad()
80
+ def forward(self, x: torch.Tensor, position_ids: torch.Tensor):
81
+ inv_freq = 1.0 / (
82
+ self.theta ** (torch.arange(0, self.dim, 2, device=x.device).float() / self.dim)
83
+ )
84
+ inv_freq_expanded = inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1)
85
+ position_ids_expanded = position_ids[:, None, :].float()
86
+
87
+ device_type = x.device.type
88
+ with torch.autocast(device_type=device_type, enabled=False):
89
+ freqs = (inv_freq_expanded @ position_ids_expanded).transpose(1, 2)
90
+ emb = torch.cat((freqs, freqs), dim=-1)
91
+ cos = emb.cos()
92
+ sin = emb.sin()
93
+ return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
94
+
95
+
96
+ def rotate_half(x: torch.Tensor) -> torch.Tensor:
97
+ x1 = x[..., : x.shape[-1] // 2]
98
+ x2 = x[..., x.shape[-1] // 2 :]
99
+ return torch.cat((-x2, x1), dim=-1)
100
+
101
+
102
+ def apply_rotary_pos_emb(
103
+ q: torch.Tensor,
104
+ k: torch.Tensor,
105
+ cos: torch.Tensor,
106
+ sin: torch.Tensor,
107
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
108
+ cos = cos.unsqueeze(1) # (B, 1, T, dim)
109
+ sin = sin.unsqueeze(1) # (B, 1, T, dim)
110
+ # OLMo-core applies RoPE in full precision (float32) for numerical stability
111
+ orig_dtype = q.dtype
112
+ q_float, k_float = q.float(), k.float()
113
+ cos_float, sin_float = cos.float(), sin.float()
114
+ q_embed = (q_float * cos_float) + (rotate_half(q_float) * sin_float)
115
+ k_embed = (k_float * cos_float) + (rotate_half(k_float) * sin_float)
116
+ return q_embed.to(orig_dtype), k_embed.to(orig_dtype)
117
+
118
+
119
+ def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor:
120
+ if n_rep == 1:
121
+ return hidden_states
122
+ batch, num_kv_heads, slen, head_dim = hidden_states.shape
123
+ hidden_states = hidden_states[:, :, None, :, :].expand(
124
+ batch, num_kv_heads, n_rep, slen, head_dim
125
+ )
126
+ return hidden_states.reshape(batch, num_kv_heads * n_rep, slen, head_dim)
127
+
128
+
129
+ class RicursiveAttention(nn.Module):
130
+ def __init__(self, config: RicursiveConfig, layer_idx: int):
131
+ super().__init__()
132
+ self.config = config
133
+ self.layer_idx = layer_idx
134
+ self.layer_type = config.layer_types[layer_idx]
135
+
136
+ self.hidden_size = config.hidden_size
137
+ self.num_heads = config.num_attention_heads
138
+ self.num_kv_heads = config.num_key_value_heads
139
+ self.head_dim = config.head_dim
140
+ self.num_kv_groups = self.num_heads // self.num_kv_heads
141
+
142
+ self.q_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=False)
143
+ self.k_proj = nn.Linear(self.hidden_size, self.num_kv_heads * self.head_dim, bias=False)
144
+ self.v_proj = nn.Linear(self.hidden_size, self.num_kv_heads * self.head_dim, bias=False)
145
+ self.o_proj = nn.Linear(self.num_heads * self.head_dim, self.hidden_size, bias=False)
146
+ if config.use_valres:
147
+ self.value_res_lambda = nn.Parameter(torch.zeros(1))
148
+
149
+ # Per-head Q/K norms (RMSNorm on head_dim)
150
+ self.q_norm = RicursiveRMSNorm(self.head_dim, eps=config.rms_norm_eps)
151
+ self.k_norm = RicursiveRMSNorm(self.head_dim, eps=config.rms_norm_eps)
152
+
153
+ # Gated attention
154
+ if config.use_gated_attention:
155
+ self.g_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=False)
156
+
157
+ # RoPE only for local (sliding_attention) layers
158
+ if self.layer_type == "sliding_attention":
159
+ self.rotary_emb = RicursiveRotaryEmbedding(self.head_dim, theta=config.rope_theta)
160
+
161
+ def _apply_sliding_window_mask(
162
+ self,
163
+ attention_mask: Optional[torch.Tensor],
164
+ q_len: int,
165
+ kv_len: int,
166
+ dtype: torch.dtype,
167
+ device: torch.device,
168
+ cache_position: Optional[torch.LongTensor],
169
+ ) -> Optional[torch.Tensor]:
170
+ if self.layer_type != "sliding_attention":
171
+ return attention_mask
172
+
173
+ # transformers >= 5 builds a hybrid cache: a sliding layer holds at most
174
+ # `sliding_window` keys while a full-attention layer keeps growing, so the
175
+ # shared mask produced by `_update_causal_mask` can be longer than this
176
+ # layer's key tensor. Keep only the keys this layer actually holds.
177
+ if attention_mask is not None and attention_mask.shape[-1] > kv_len:
178
+ attention_mask = attention_mask[..., -kv_len:]
179
+
180
+ if cache_position is not None:
181
+ query_positions = cache_position.to(device=device).reshape(-1)
182
+ else:
183
+ query_positions = torch.arange(kv_len - q_len, kv_len, device=device)
184
+
185
+ window = self.config.sliding_window
186
+ if window is None:
187
+ return attention_mask
188
+ if kv_len <= window:
189
+ # The window cannot exclude any key, so the result is either the
190
+ # unchanged (already causal) mask or, without one, plain causal
191
+ # attention -- which SDPA applies through `is_causal`.
192
+ if attention_mask is None:
193
+ return None
194
+ if q_len == 1:
195
+ return attention_mask
196
+
197
+ # Keys are contiguous and end at the newest query position. After the cache
198
+ # evicts from the left they no longer start at position 0, so anchor the
199
+ # range at the first key the layer holds instead of at zero.
200
+ key_positions = torch.arange(kv_len, device=device) + (
201
+ query_positions[-1] - kv_len + 1
202
+ )
203
+
204
+ allowed = (
205
+ (key_positions[None, :] <= query_positions[:, None])
206
+ & (key_positions[None, :] >= query_positions[:, None] - (window - 1))
207
+ )[None, None, :, :]
208
+
209
+ if attention_mask is None:
210
+ return allowed
211
+ if attention_mask.dtype == torch.bool:
212
+ return attention_mask & allowed
213
+ return attention_mask.masked_fill(~allowed, torch.finfo(dtype).min)
214
+
215
+ def forward(
216
+ self,
217
+ hidden_states: torch.Tensor,
218
+ attention_mask: Optional[torch.Tensor] = None,
219
+ position_ids: Optional[torch.Tensor] = None,
220
+ past_key_value: Optional[Cache] = None,
221
+ output_attentions: bool = False,
222
+ use_cache: bool = False,
223
+ cache_position: Optional[torch.LongTensor] = None,
224
+ value_residual: Optional[torch.Tensor] = None,
225
+ **kwargs,
226
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]:
227
+ bsz, q_len, _ = hidden_states.shape
228
+
229
+ # Project Q, K, V
230
+ q = self.q_proj(hidden_states)
231
+ k = self.k_proj(hidden_states)
232
+ value_states = hidden_states
233
+ if value_residual is not None:
234
+ if not self.config.use_valres:
235
+ raise RuntimeError("value_residual was supplied but config.use_valres is false")
236
+ blend = torch.sigmoid(self.value_res_lambda[0]).to(dtype=hidden_states.dtype)
237
+ value_states = torch.lerp(
238
+ value_residual.to(dtype=hidden_states.dtype), hidden_states, blend
239
+ )
240
+ v = self.v_proj(value_states)
241
+
242
+ # Reshape to (B, T, n_heads, head_dim) then transpose to (B, n_heads, T, head_dim)
243
+ q = q.view(bsz, q_len, self.num_heads, self.head_dim)
244
+ k = k.view(bsz, q_len, self.num_kv_heads, self.head_dim)
245
+ v = v.view(bsz, q_len, self.num_kv_heads, self.head_dim)
246
+
247
+ # Per-head Q/K norm (applied before transpose, on head_dim)
248
+ q = self.q_norm(q)
249
+ k = self.k_norm(k)
250
+
251
+ # Transpose to (B, n_heads, T, head_dim) for attention
252
+ q = q.transpose(1, 2)
253
+ k = k.transpose(1, 2)
254
+ v = v.transpose(1, 2)
255
+
256
+ # RoPE only for local layers
257
+ if self.layer_type == "sliding_attention":
258
+ cos, sin = self.rotary_emb(v, position_ids)
259
+ q, k = apply_rotary_pos_emb(q, k, cos, sin)
260
+
261
+ # KV cache
262
+ if past_key_value is not None:
263
+ k, v = past_key_value.update(k, v, self.layer_idx)
264
+
265
+ # GQA: expand KV heads
266
+ k = repeat_kv(k, self.num_kv_groups)
267
+ v = repeat_kv(v, self.num_kv_groups)
268
+
269
+ attention_mask = self._apply_sliding_window_mask(
270
+ attention_mask=attention_mask,
271
+ q_len=q.shape[2],
272
+ kv_len=k.shape[2],
273
+ dtype=q.dtype,
274
+ device=q.device,
275
+ cache_position=cache_position,
276
+ )
277
+
278
+ # Attention (use is_causal when no explicit mask for correct causal behavior)
279
+ # Cast to plain bool to avoid SymBool issues with torch.compile
280
+ is_causal = bool(attention_mask is None and q.shape[2] > 1)
281
+ attn_output = F.scaled_dot_product_attention(
282
+ q, k, v, attn_mask=attention_mask, dropout_p=0.0, is_causal=is_causal
283
+ )
284
+
285
+ # (B, n_heads, T, head_dim) -> (B, T, n_heads * head_dim)
286
+ attn_output = attn_output.transpose(1, 2).contiguous()
287
+
288
+ # Gated attention: output * sigmoid(g_proj(input))
289
+ if self.config.use_gated_attention:
290
+ gate = self.g_proj(hidden_states)
291
+ if self.config.gate_full_precision:
292
+ gate = gate.float()
293
+ gate = torch.sigmoid(gate).to(attn_output.dtype)
294
+ attn_output = attn_output.view(bsz, q_len, -1) * gate
295
+ else:
296
+ attn_output = attn_output.view(bsz, q_len, -1)
297
+
298
+ # Output projection
299
+ attn_output = self.o_proj(attn_output)
300
+
301
+ return attn_output, None, past_key_value
302
+
303
+
304
+ class RicursiveMLP(nn.Module):
305
+ def __init__(self, config: RicursiveConfig):
306
+ super().__init__()
307
+ self.clamp_threshold = config.ffn_clamp_threshold
308
+ self.gate_proj = nn.Linear(config.hidden_size, config.intermediate_size, bias=False)
309
+ self.up_proj = nn.Linear(config.hidden_size, config.intermediate_size, bias=False)
310
+ self.down_proj = nn.Linear(config.intermediate_size, config.hidden_size, bias=False)
311
+
312
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
313
+ gate = self.gate_proj(x)
314
+ up = self.up_proj(x)
315
+ if self.clamp_threshold:
316
+ gate = gate.clamp(min=None, max=self.clamp_threshold)
317
+ up = up.clamp(min=-self.clamp_threshold, max=self.clamp_threshold)
318
+ return self.down_proj(F.silu(gate) * up)
319
+
320
+
321
+ class RicursiveDecoderLayer(nn.Module):
322
+ """PeriNorm decoder layer with 4 layer norms."""
323
+
324
+ def __init__(self, config: RicursiveConfig, layer_idx: int):
325
+ super().__init__()
326
+ self.self_attn = RicursiveAttention(config, layer_idx)
327
+ self.mlp = RicursiveMLP(config)
328
+
329
+ # PeriNorm: 4 norms
330
+ self.input_layernorm = RicursiveRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
331
+ self.post_attention_layernorm = RicursiveRMSNorm(
332
+ config.hidden_size, eps=config.rms_norm_eps
333
+ )
334
+ self.pre_feedforward_layernorm = RicursiveRMSNorm(
335
+ config.hidden_size, eps=config.rms_norm_eps
336
+ )
337
+ self.post_feedforward_layernorm = RicursiveRMSNorm(
338
+ config.hidden_size, eps=config.rms_norm_eps
339
+ )
340
+
341
+ def forward(
342
+ self,
343
+ hidden_states: torch.Tensor,
344
+ attention_mask: Optional[torch.Tensor] = None,
345
+ position_ids: Optional[torch.Tensor] = None,
346
+ past_key_value: Optional[Cache] = None,
347
+ output_attentions: bool = False,
348
+ use_cache: bool = False,
349
+ cache_position: Optional[torch.LongTensor] = None,
350
+ value_residual: Optional[torch.Tensor] = None,
351
+ **kwargs,
352
+ ) -> Tuple[torch.Tensor, ...]:
353
+ # Attention sub-block (PeriNorm: pre-norm -> attn -> post-norm -> residual)
354
+ normed = self.input_layernorm(hidden_states)
355
+ attn_output, attn_weights, present_key_value = self.self_attn(
356
+ normed,
357
+ attention_mask=attention_mask,
358
+ position_ids=position_ids,
359
+ past_key_value=past_key_value,
360
+ output_attentions=output_attentions,
361
+ use_cache=use_cache,
362
+ cache_position=cache_position,
363
+ value_residual=value_residual,
364
+ **kwargs,
365
+ )
366
+ hidden_states = hidden_states + self.post_attention_layernorm(attn_output)
367
+
368
+ # FFN sub-block (PeriNorm: pre-norm -> ffn -> post-norm -> residual)
369
+ normed = self.pre_feedforward_layernorm(hidden_states)
370
+ ff_output = self.mlp(normed)
371
+ hidden_states = hidden_states + self.post_feedforward_layernorm(ff_output)
372
+
373
+ outputs = (hidden_states,)
374
+ if output_attentions:
375
+ outputs += (attn_weights,)
376
+ if use_cache:
377
+ outputs += (present_key_value,)
378
+ return outputs
379
+
380
+
381
+ class RicursivePreTrainedModel(PreTrainedModel):
382
+ config_class = RicursiveConfig
383
+ base_model_prefix = "model"
384
+ supports_gradient_checkpointing = True
385
+ _no_split_modules = ["RicursiveDecoderLayer"]
386
+ _supports_sdpa = True
387
+ _supports_cache_class = True
388
+
389
+
390
+ class RicursiveModel(RicursivePreTrainedModel):
391
+ def __init__(self, config: RicursiveConfig):
392
+ super().__init__(config)
393
+ self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
394
+ self.layers = nn.ModuleList(
395
+ [RicursiveDecoderLayer(config, i) for i in range(config.num_hidden_layers)]
396
+ )
397
+ self.norm = RicursiveRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
398
+ self.post_init()
399
+
400
+ def forward(
401
+ self,
402
+ input_ids: Optional[torch.LongTensor] = None,
403
+ attention_mask: Optional[torch.Tensor] = None,
404
+ position_ids: Optional[torch.LongTensor] = None,
405
+ past_key_values: Optional[Cache] = None,
406
+ inputs_embeds: Optional[torch.FloatTensor] = None,
407
+ use_cache: Optional[bool] = None,
408
+ output_attentions: Optional[bool] = None,
409
+ output_hidden_states: Optional[bool] = None,
410
+ return_dict: Optional[bool] = None,
411
+ cache_position: Optional[torch.LongTensor] = None,
412
+ **kwargs,
413
+ ) -> Union[Tuple, BaseModelOutputWithPast]:
414
+ output_attentions = (
415
+ output_attentions if output_attentions is not None else self.config.output_attentions
416
+ )
417
+ output_hidden_states = (
418
+ output_hidden_states
419
+ if output_hidden_states is not None
420
+ else self.config.output_hidden_states
421
+ )
422
+ use_cache = use_cache if use_cache is not None else self.config.use_cache
423
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
424
+
425
+ if inputs_embeds is None:
426
+ inputs_embeds = self.embed_tokens(input_ids)
427
+
428
+ # embed_scale: non-folded checkpoints carry the scale (e.g. sqrt(hidden_size)) in the
429
+ # config and apply it here at runtime; folded checkpoints set embed_scale=None and bake
430
+ # the scale into embed_tokens.weight, so this is a no-op. This mirrors the SGLang native
431
+ # port (sglang_ricursive) so served and trained forwards agree. It must run BEFORE the
432
+ # value-residual seed below, whose source is the (scaled) layer-0 embeddings.
433
+ embed_scale = getattr(self.config, "embed_scale", None)
434
+ if embed_scale is not None:
435
+ inputs_embeds = inputs_embeds * embed_scale
436
+
437
+ if use_cache and past_key_values is None:
438
+ past_key_values = DynamicCache()
439
+
440
+ if cache_position is None:
441
+ past_seen_tokens = (
442
+ past_key_values.get_seq_length() if past_key_values is not None else 0
443
+ )
444
+ cache_position = torch.arange(
445
+ past_seen_tokens,
446
+ past_seen_tokens + inputs_embeds.shape[1],
447
+ device=inputs_embeds.device,
448
+ )
449
+
450
+ if position_ids is None:
451
+ position_ids = cache_position.unsqueeze(0)
452
+
453
+ # Build causal mask
454
+ causal_mask = self._update_causal_mask(
455
+ attention_mask, inputs_embeds, cache_position, past_key_values
456
+ )
457
+
458
+ hidden_states = inputs_embeds
459
+ value_residual = hidden_states if self.config.use_valres else None
460
+ all_hidden_states = () if output_hidden_states else None
461
+ all_self_attns = () if output_attentions else None
462
+
463
+ for layer in self.layers:
464
+ if output_hidden_states:
465
+ all_hidden_states += (hidden_states,)
466
+
467
+ layer_outputs = layer(
468
+ hidden_states,
469
+ attention_mask=causal_mask,
470
+ position_ids=position_ids,
471
+ past_key_value=past_key_values,
472
+ output_attentions=output_attentions,
473
+ use_cache=use_cache,
474
+ cache_position=cache_position,
475
+ value_residual=value_residual,
476
+ **kwargs,
477
+ )
478
+ hidden_states = layer_outputs[0]
479
+
480
+ if output_attentions:
481
+ all_self_attns += (layer_outputs[1],)
482
+
483
+ hidden_states = self.norm(hidden_states)
484
+
485
+ if output_hidden_states:
486
+ all_hidden_states += (hidden_states,)
487
+
488
+ next_cache = past_key_values if use_cache else None
489
+
490
+ if not return_dict:
491
+ return tuple(
492
+ v
493
+ for v in [hidden_states, next_cache, all_hidden_states, all_self_attns]
494
+ if v is not None
495
+ )
496
+ return BaseModelOutputWithPast(
497
+ last_hidden_state=hidden_states,
498
+ past_key_values=next_cache,
499
+ hidden_states=all_hidden_states,
500
+ attentions=all_self_attns,
501
+ )
502
+
503
+ def _update_causal_mask(
504
+ self,
505
+ attention_mask: Optional[torch.Tensor],
506
+ input_tensor: torch.Tensor,
507
+ cache_position: torch.Tensor,
508
+ past_key_values: Optional[Cache],
509
+ ) -> Optional[torch.Tensor]:
510
+ # When there's no explicit attention_mask (no padding) and we're doing
511
+ # standard prefill or cached decoding, return None and let SDPA use
512
+ # its efficient is_causal path. This also avoids numerical issues
513
+ # from float min mask values with large activations.
514
+ if attention_mask is None:
515
+ return None
516
+ if torch.all(attention_mask == 1):
517
+ return None
518
+
519
+ # Only build an explicit mask when we have a padding mask
520
+ dtype, device = input_tensor.dtype, input_tensor.device
521
+ sequence_length = input_tensor.shape[1]
522
+
523
+ if cache_position is None:
524
+ query_positions = torch.arange(sequence_length, device=device)
525
+ else:
526
+ query_positions = cache_position.to(device=device).reshape(-1)
527
+
528
+ # Size the key axis from `cache_position`, which is exactly what the
529
+ # attention layer uses to size its key tensor.
530
+ # `past_key_values.get_max_cache_shape()` was removed in transformers 5,
531
+ # and its replacement reports the *capacity* (e.g. `sliding_window`),
532
+ # which disagrees with the live key count whenever the cache is not full.
533
+ target_length = int(query_positions[-1].item()) + 1
534
+ target_length = max(target_length, attention_mask.shape[-1])
535
+
536
+ # A shorter padding mask leaves the trailing keys unspecified; treat them as
537
+ # visible -- the causal term below still applies.
538
+ if attention_mask.shape[-1] < target_length:
539
+ pad = attention_mask.new_ones(
540
+ attention_mask.shape[0], target_length - attention_mask.shape[-1]
541
+ )
542
+ attention_mask = torch.cat([pad, attention_mask], dim=-1)
543
+
544
+ key_positions = torch.arange(target_length, device=device)
545
+ allowed = key_positions[None, :] <= query_positions[:, None]
546
+ allowed = allowed[None, None, :, :] & attention_mask[:, None, None, :].to(torch.bool)
547
+
548
+ causal_mask = torch.zeros(
549
+ (attention_mask.shape[0], 1, sequence_length, target_length),
550
+ dtype=dtype,
551
+ device=device,
552
+ )
553
+ # `0.0` for visible keys (not `1.0`, which would bias every score) and the
554
+ # dtype minimum elsewhere. Building the mask from `allowed` also keeps a
555
+ # single-token decode step from coming out fully masked.
556
+ return causal_mask.masked_fill(~allowed, torch.finfo(dtype).min)
557
+
558
+
559
+ def _cache_seq_length(past_key_values: Optional[Cache]) -> int:
560
+ """Number of cached keys, across transformers 4.x and 5.x cache objects."""
561
+ if past_key_values is None:
562
+ return 0
563
+ getter = getattr(past_key_values, "get_seq_length", None)
564
+ if callable(getter):
565
+ return int(getter())
566
+ try:
567
+ return int(past_key_values[0][0].shape[-2])
568
+ except Exception: # pragma: no cover - unknown cache implementations
569
+ return 0
570
+
571
+
572
+ class RicursiveForCausalLM(RicursivePreTrainedModel, GenerationMixin):
573
+ _tied_weights_keys = []
574
+
575
+ # transformers 5 no longer injects `GenerationMixin` into remote-code classes,
576
+ # so inherit it explicitly above. Without it `generate` is missing entirely and
577
+ # `PreTrainedModel.__init__` never builds `generation_config`.
578
+ #
579
+ # This attribute marks the native transformers-5 compatibility fixes so that
580
+ # downstream loaders can skip their runtime monkeypatch.
581
+ _ricursive_tf5_native_fix = RICURSIVE_TF5_COMPAT
582
+
583
+ def __init__(self, config: RicursiveConfig):
584
+ super().__init__(config)
585
+ self.model = RicursiveModel(config)
586
+ self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
587
+ self.post_init()
588
+
589
+ def get_input_embeddings(self):
590
+ return self.model.embed_tokens
591
+
592
+ def set_input_embeddings(self, value):
593
+ self.model.embed_tokens = value
594
+
595
+ def get_output_embeddings(self):
596
+ return self.lm_head
597
+
598
+ def set_output_embeddings(self, new_embeddings):
599
+ self.lm_head = new_embeddings
600
+
601
+ def forward(
602
+ self,
603
+ input_ids: Optional[torch.LongTensor] = None,
604
+ attention_mask: Optional[torch.Tensor] = None,
605
+ position_ids: Optional[torch.LongTensor] = None,
606
+ past_key_values: Optional[Cache] = None,
607
+ inputs_embeds: Optional[torch.FloatTensor] = None,
608
+ labels: Optional[torch.LongTensor] = None,
609
+ use_cache: Optional[bool] = None,
610
+ output_attentions: Optional[bool] = None,
611
+ output_hidden_states: Optional[bool] = None,
612
+ return_dict: Optional[bool] = None,
613
+ cache_position: Optional[torch.LongTensor] = None,
614
+ **kwargs,
615
+ ) -> Union[Tuple, CausalLMOutputWithPast]:
616
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
617
+
618
+ outputs = self.model(
619
+ input_ids=input_ids,
620
+ attention_mask=attention_mask,
621
+ position_ids=position_ids,
622
+ past_key_values=past_key_values,
623
+ inputs_embeds=inputs_embeds,
624
+ use_cache=use_cache,
625
+ output_attentions=output_attentions,
626
+ output_hidden_states=output_hidden_states,
627
+ return_dict=return_dict,
628
+ cache_position=cache_position,
629
+ **kwargs,
630
+ )
631
+ hidden_states = outputs[0]
632
+ logits = self.lm_head(hidden_states)
633
+
634
+ loss = None
635
+ if labels is not None:
636
+ shift_logits = logits[..., :-1, :].contiguous()
637
+ shift_labels = labels[..., 1:].contiguous()
638
+ loss = F.cross_entropy(
639
+ shift_logits.view(-1, shift_logits.size(-1)),
640
+ shift_labels.view(-1),
641
+ )
642
+
643
+ if not return_dict:
644
+ output = (logits,) + outputs[1:]
645
+ return (loss,) + output if loss is not None else output
646
+
647
+ return CausalLMOutputWithPast(
648
+ loss=loss,
649
+ logits=logits,
650
+ past_key_values=outputs.past_key_values,
651
+ hidden_states=outputs.hidden_states,
652
+ attentions=outputs.attentions,
653
+ )
654
+
655
+ def prepare_inputs_for_generation(
656
+ self,
657
+ input_ids: torch.LongTensor,
658
+ past_key_values: Optional[Cache] = None,
659
+ attention_mask: Optional[torch.Tensor] = None,
660
+ inputs_embeds: Optional[torch.FloatTensor] = None,
661
+ cache_position: Optional[torch.LongTensor] = None,
662
+ use_cache: Optional[bool] = None,
663
+ position_ids: Optional[torch.LongTensor] = None,
664
+ **kwargs,
665
+ ):
666
+ # transformers 5.x does not move remote-code inputs onto the model device for
667
+ # this model, so a plain CPU `input_ids` reaches the embedding lookup and
668
+ # raises. Its own generation helper performs this move, so mirror it here.
669
+ device = getattr(self, "device", None)
670
+ if device is not None and getattr(device, "type", None) != "meta":
671
+ if input_ids is not None and input_ids.device != device:
672
+ input_ids = input_ids.to(device)
673
+ if attention_mask is not None and attention_mask.device != device:
674
+ attention_mask = attention_mask.to(device)
675
+ if inputs_embeds is not None and inputs_embeds.device != device:
676
+ inputs_embeds = inputs_embeds.to(device)
677
+ # transformers derives `position_ids` from the *caller's* tensors before
678
+ # this hook runs, so a CPU prompt yields CPU positions even after the
679
+ # move above. RoPE then multiplies them against CUDA frequencies and fails.
680
+ if position_ids is not None and position_ids.device != device:
681
+ position_ids = position_ids.to(device)
682
+
683
+ past_length = _cache_seq_length(past_key_values)
684
+
685
+ if cache_position is None:
686
+ # With a non-empty cache and the full sequence supplied, only the trailing
687
+ # (uncached) tokens are new. Otherwise every token in `input_ids` is new.
688
+ if past_length > 0 and input_ids.shape[1] > 1:
689
+ new_len = input_ids.shape[1] - past_length
690
+ cache_position = torch.arange(
691
+ past_length, past_length + new_len, device=input_ids.device
692
+ )
693
+ else:
694
+ cache_position = torch.arange(
695
+ past_length, past_length + input_ids.shape[1], device=input_ids.device
696
+ )
697
+
698
+ # Only slice when the cache is warm and `input_ids` still carries cached
699
+ # tokens. Slicing unconditionally turned a `None` cache position into
700
+ # `input_ids[:, None]` and injected a bogus axis.
701
+ sliced = past_length > 0 and input_ids.shape[1] > cache_position.numel()
702
+ if sliced:
703
+ input_ids = input_ids[:, cache_position]
704
+
705
+ if inputs_embeds is not None and cache_position.numel() and int(cache_position[0]) == 0:
706
+ model_inputs = {"inputs_embeds": inputs_embeds}
707
+ else:
708
+ model_inputs = {"input_ids": input_ids}
709
+
710
+ # Derive per-row positions from the padding mask. Using `cache_position` for
711
+ # every row is only correct when nothing is padded: with left padding the real
712
+ # tokens of a short row sit at higher indices, so a shared position vector
713
+ # shifts its RoPE phases and the row decodes to garbage.
714
+ if position_ids is not None:
715
+ if sliced:
716
+ position_ids = position_ids[:, cache_position]
717
+ elif attention_mask is not None and attention_mask.dim() == 2:
718
+ position_ids = attention_mask.long().cumsum(-1) - 1
719
+ position_ids = position_ids.masked_fill(attention_mask == 0, 0)
720
+ position_ids = position_ids[:, -input_ids.shape[1]:]
721
+ else:
722
+ position_ids = cache_position.unsqueeze(0)
723
+
724
+ model_inputs.update(
725
+ {
726
+ "position_ids": position_ids,
727
+ "past_key_values": past_key_values,
728
+ "use_cache": use_cache,
729
+ "attention_mask": attention_mask,
730
+ "cache_position": cache_position,
731
+ }
732
+ )
733
+ return model_inputs