Taykhoom commited on
Commit
560f4e9
·
verified ·
1 Parent(s): 08a995e

Enable upstream-compatible recurrent caching with dynamic allocation

Browse files
Files changed (3) hide show
  1. README.md +2 -2
  2. cache.py +3 -0
  3. modeling_evo2.py +34 -8
README.md CHANGED
@@ -175,7 +175,7 @@ print(tokenizer.decode(out[0]))
175
 
176
  `generation_config.json` ships with `eos_token_id = 0` (the EOD byte) and `pad_token_id = 1` so `model.generate()` stops naturally at the trained end-of-document token.
177
 
178
- Generation currently uses full recomputation rather than the experimental recurrent cache. `use_cache=True` requests safely fall back to the same numerically verified path, avoiding variant-dependent cache divergence and context-sized preallocation.
179
 
180
  ### Attention weights
181
 
@@ -230,7 +230,7 @@ This HuggingFace port has **not** been tested for fine-tuning - it's verified on
230
  - `"eager"`: textbook einsum + softmax(QK^T) + einsum. Slowest, used internally when `output_attentions=True` so the attention matrix is materialized.
231
  - **Block dispatch (`hyena.py`).** StripedHyena 2 has 4 block types, dispatched by `layer_idx` membership in four config lists: `attn_layer_idxs` (MHA + RoPE), `hcl_layer_idxs` (modal-form IIR via FFT), `hcm_layer_idxs` (medium FIR cascade, inner length 128), `hcs_layer_idxs` (short FIR cascade, inner length 7). The disjoint union must equal `range(num_layers)`.
232
  - **`TELinear` with pure-PyTorch fallback (`layers.py`).** Hyena cascade blocks use a TransformerEngine-backed input projection (3x hidden_size output) that supports FP8 quantisation. When TE is not installed, a `TELinear` fallback class with the same state_dict layout (`weight`, `bias`) is used - checkpoints are cross-loadable.
233
- - **Generation.** The experimental recurrent cache classes remain available for development, but public generation uses full recomputation because it is numerically consistent across every released variant and avoids context-sized cache allocation.
234
  - **Tokenizer (`tokenization_evo2.py`).** Byte-level UTF-8, vocab_size = 512. Pad token = byte `\x01`. EOS = byte `\x00` (set as `eos_token_id` in `generation_config.json`). Tokenizer does not add EOS at encoding time - matches the original Evo 2 inference pipeline.
235
  - **Dependencies.** `torch`, `transformers`, `numpy`, `safetensors`, `huggingface_hub`. `transformer-engine[pytorch]` is required for this variant's FP8 input projections. `accelerate` is optional but recommended if you want to load with `device_map="auto"` for multi-GPU sharding. `flash_attn` is optional (only needed if you pass `attn_implementation="flash_attention_2"`).
236
 
 
175
 
176
  `generation_config.json` ships with `eos_token_id = 0` (the EOD byte) and `pad_token_id = 1` so `model.generate()` stops naturally at the trained end-of-document token.
177
 
178
+ Generation uses recurrent MHA/Hyena state for unpadded prompts. Padded batches automatically use the exact full-recomputation path. Attention KV storage grows with the decoded sequence instead of preallocating the model's full configured context.
179
 
180
  ### Attention weights
181
 
 
230
  - `"eager"`: textbook einsum + softmax(QK^T) + einsum. Slowest, used internally when `output_attentions=True` so the attention matrix is materialized.
231
  - **Block dispatch (`hyena.py`).** StripedHyena 2 has 4 block types, dispatched by `layer_idx` membership in four config lists: `attn_layer_idxs` (MHA + RoPE), `hcl_layer_idxs` (modal-form IIR via FFT), `hcm_layer_idxs` (medium FIR cascade, inner length 128), `hcs_layer_idxs` (short FIR cascade, inner length 7). The disjoint union must equal `range(num_layers)`.
232
  - **`TELinear` with pure-PyTorch fallback (`layers.py`).** Hyena cascade blocks use a TransformerEngine-backed input projection (3x hidden_size output) that supports FP8 quantisation. When TE is not installed, a `TELinear` fallback class with the same state_dict layout (`weight`, `bias`) is used - checkpoints are cross-loadable.
233
+ - **Generation cache.** Unpadded prompts use recurrent MHA KV plus HCL/HCM/HCS FIR/IIR state matching the upstream Vortex cache path. Padded batches use full recomputation. KV tensors grow geometrically with the decoded length instead of allocating `max_seqlen` up front.
234
  - **Tokenizer (`tokenization_evo2.py`).** Byte-level UTF-8, vocab_size = 512. Pad token = byte `\x01`. EOS = byte `\x00` (set as `eos_token_id` in `generation_config.json`). Tokenizer does not add EOS at encoding time - matches the original Evo 2 inference pipeline.
235
  - **Dependencies.** `torch`, `transformers`, `numpy`, `safetensors`, `huggingface_hub`. `transformer-engine[pytorch]` is required for this variant's FP8 input projections. `accelerate` is optional but recommended if you want to load with `device_map="auto"` for multi-GPU sharding. `flash_attn` is optional (only needed if you pass `attn_implementation="flash_attention_2"`).
236
 
cache.py CHANGED
@@ -114,6 +114,7 @@ class Evo2Cache:
114
  )
115
  self.input_ids: Optional[Tensor] = None
116
  self.attention_mask: Optional[Tensor] = None
 
117
 
118
  @property
119
  def seqlen_offset(self) -> int:
@@ -147,6 +148,7 @@ class Evo2Cache:
147
  self.hcs.reset()
148
  self.input_ids = None
149
  self.attention_mask = None
 
150
 
151
  def prepare_inputs(
152
  self,
@@ -154,6 +156,7 @@ class Evo2Cache:
154
  attention_mask: Optional[Tensor],
155
  ) -> tuple[Tensor, Tensor, bool]:
156
  """Return full history and whether the caller supplied only a suffix."""
 
157
  requested_mask = (
158
  attention_mask.ne(0)
159
  if attention_mask is not None
 
114
  )
115
  self.input_ids: Optional[Tensor] = None
116
  self.attention_mask: Optional[Tensor] = None
117
+ self.mode: Optional[str] = None
118
 
119
  @property
120
  def seqlen_offset(self) -> int:
 
148
  self.hcs.reset()
149
  self.input_ids = None
150
  self.attention_mask = None
151
+ self.mode = None
152
 
153
  def prepare_inputs(
154
  self,
 
156
  attention_mask: Optional[Tensor],
157
  ) -> tuple[Tensor, Tensor, bool]:
158
  """Return full history and whether the caller supplied only a suffix."""
159
+ self.mode = "recompute"
160
  requested_mask = (
161
  attention_mask.ne(0)
162
  if attention_mask is not None
modeling_evo2.py CHANGED
@@ -8,9 +8,8 @@ Two top-level model classes:
8
  * Evo2ForCausalLM -- with the tied LM head + GenerationMixin so
9
  model.generate() works out of the box.
10
 
11
- The custom Evo2Cache implementation is retained for future development, but
12
- generation currently uses full recomputation because cached recurrence is not
13
- numerically equivalent for every released variant.
14
 
15
  Per-block dtype: bfloat16 for everything except the modal-form filter
16
  parameters (`log_poles`, `residues`) which must stay fp32 for stability.
@@ -136,21 +135,48 @@ class StripedHyena2(nn.Module):
136
  past_key_values = self.initialize_inference_params(
137
  max_batch_size=input_ids.shape[0],
138
  )
139
- input_ids, attention_mask, suffix_only = past_key_values.prepare_inputs(
140
- input_ids,
141
- attention_mask,
142
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
  last_hidden, past_kv, hidden_states, attentions = self._forward_features(
145
  input_ids,
146
- inference_params_dict=None,
 
 
 
 
147
  attention_mask=attention_mask,
148
  output_hidden_states=output_hidden_states,
149
  output_attentions=output_attentions,
150
  )
151
  if use_cache:
152
- past_key_values.update_history(input_ids, attention_mask)
153
  past_kv = past_key_values
 
 
 
 
154
  if suffix_only:
155
  last_hidden = last_hidden[:, -requested_length:]
156
  if output_hidden_states:
 
8
  * Evo2ForCausalLM -- with the tied LM head + GenerationMixin so
9
  model.generate() works out of the box.
10
 
11
+ Evo2Cache uses optimized recurrent MHA/Hyena state for unpadded prompts and
12
+ automatically uses exact full recomputation for padded batches.
 
13
 
14
  Per-block dtype: bfloat16 for everything except the modal-form filter
15
  parameters (`log_poles`, `residues`) which must stay fp32 for stability.
 
135
  past_key_values = self.initialize_inference_params(
136
  max_batch_size=input_ids.shape[0],
137
  )
138
+ has_padding = (
139
+ attention_mask is not None
140
+ and not attention_mask.bool().all()
141
  )
142
+ if past_key_values.mode is None:
143
+ past_key_values.mode = (
144
+ "recompute" if has_padding else "recurrent"
145
+ )
146
+ if past_key_values.mode == "recurrent" and has_padding:
147
+ raise ValueError(
148
+ "A recurrent Evo2 cache cannot switch to padded inputs. "
149
+ "Reset the cache and prefill the padded batch again."
150
+ )
151
+ if past_key_values.mode == "recurrent":
152
+ cached_length = past_key_values.get_seq_length()
153
+ if cached_length > 0 and input_ids.shape[1] > cached_length:
154
+ input_ids = input_ids[:, cached_length:]
155
+ if attention_mask is not None:
156
+ attention_mask = attention_mask[:, cached_length:]
157
+ else:
158
+ input_ids, attention_mask, suffix_only = past_key_values.prepare_inputs(
159
+ input_ids,
160
+ attention_mask,
161
+ )
162
 
163
  last_hidden, past_kv, hidden_states, attentions = self._forward_features(
164
  input_ids,
165
+ inference_params_dict=(
166
+ past_key_values
167
+ if use_cache and past_key_values.mode == "recurrent"
168
+ else None
169
+ ),
170
  attention_mask=attention_mask,
171
  output_hidden_states=output_hidden_states,
172
  output_attentions=output_attentions,
173
  )
174
  if use_cache:
 
175
  past_kv = past_key_values
176
+ if past_key_values.mode == "recurrent":
177
+ past_key_values.advance(input_ids.shape[1])
178
+ else:
179
+ past_key_values.update_history(input_ids, attention_mask)
180
  if suffix_only:
181
  last_hidden = last_hidden[:, -requested_length:]
182
  if output_hidden_states: