Fix HF v5 inference

#1
by Qubitium - opened
Files changed (1) hide show
  1. modeling_ricursive.py +164 -37
modeling_ricursive.py CHANGED
@@ -6,6 +6,25 @@ standard Llama/Mistral:
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
 
11
  from typing import Optional, Tuple, Union
@@ -20,9 +39,20 @@ from transformers.modeling_outputs import (
20
  CausalLMOutputWithPast,
21
  )
22
 
 
 
 
 
 
23
  from .configuration_ricursive import RicursiveConfig
24
 
25
 
 
 
 
 
 
 
26
  class RicursiveRMSNorm(nn.Module):
27
  def __init__(self, hidden_size: int, eps: float = 1e-6):
28
  super().__init__()
@@ -140,13 +170,37 @@ class RicursiveAttention(nn.Module):
140
  if self.layer_type != "sliding_attention":
141
  return attention_mask
142
 
143
- key_positions = torch.arange(kv_len, device=device)
 
 
 
 
 
 
144
  if cache_position is not None:
145
- query_positions = cache_position.to(device=device)
146
  else:
147
  query_positions = torch.arange(kv_len - q_len, kv_len, device=device)
148
 
149
  window = self.config.sliding_window
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  allowed = (
151
  (key_positions[None, :] <= query_positions[:, None])
152
  & (key_positions[None, :] >= query_positions[:, None] - (window - 1))
@@ -466,43 +520,66 @@ class RicursiveModel(RicursivePreTrainedModel):
466
  dtype, device = input_tensor.dtype, input_tensor.device
467
  sequence_length = input_tensor.shape[1]
468
 
469
- if past_key_values is not None:
470
- target_length = past_key_values.get_max_cache_shape()
471
- if target_length is None or target_length < 0:
472
- target_length = (
473
- cache_position[-1].item() + 1 if cache_position is not None else sequence_length
474
- )
475
  else:
476
- target_length = sequence_length
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
 
478
- causal_mask = torch.full(
479
- (sequence_length, target_length),
480
- fill_value=torch.finfo(dtype).min,
481
  dtype=dtype,
482
  device=device,
483
  )
484
- if sequence_length != 1:
485
- causal_mask = torch.triu(causal_mask, diagonal=target_length - sequence_length + 1)
486
-
487
- causal_mask = causal_mask[None, None, :, :].expand(input_tensor.shape[0], 1, -1, -1)
488
-
489
- if attention_mask.dim() == 2:
490
- mask_length = attention_mask.shape[-1]
491
- padding_mask = causal_mask[:, :, :, :mask_length] + attention_mask[:, None, None, :].to(
492
- causal_mask.dtype
493
- ).masked_fill(attention_mask[:, None, None, :] == 0, torch.finfo(dtype).min)
494
- causal_mask = (
495
- torch.cat([padding_mask, causal_mask[:, :, :, mask_length:]], dim=-1)
496
- if mask_length < target_length
497
- else padding_mask
498
- )
499
-
500
- return causal_mask
501
-
502
-
503
- class RicursiveForCausalLM(RicursivePreTrainedModel):
504
  _tied_weights_keys = []
505
 
 
 
 
 
 
 
 
 
506
  def __init__(self, config: RicursiveConfig):
507
  super().__init__(config)
508
  self.model = RicursiveModel(config)
@@ -583,20 +660,70 @@ class RicursiveForCausalLM(RicursivePreTrainedModel):
583
  inputs_embeds: Optional[torch.FloatTensor] = None,
584
  cache_position: Optional[torch.LongTensor] = None,
585
  use_cache: Optional[bool] = None,
 
586
  **kwargs,
587
  ):
588
- if past_key_values is not None:
589
- if input_ids.shape[1] != 1:
590
- input_ids = input_ids[:, cache_position]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
591
 
592
- if inputs_embeds is not None and cache_position[0] == 0:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
  model_inputs = {"inputs_embeds": inputs_embeds}
594
  else:
595
  model_inputs = {"input_ids": input_ids}
596
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597
  model_inputs.update(
598
  {
599
- "position_ids": cache_position.unsqueeze(0) if cache_position is not None else None,
600
  "past_key_values": past_key_values,
601
  "use_cache": use_cache,
602
  "attention_mask": attention_mask,
 
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
 
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__()
 
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))
 
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)
 
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,