premmm commited on
Commit
09ada83
·
verified ·
1 Parent(s): 17ec36f

Fix .generate() support: add GenerationMixin + generation_config, document KV-cache limitation

Browse files

## Summary

Calling `.generate()` on this model currently fails immediately with:

AttributeError: 'NanochatForCausalLM' object has no attribute 'generate'


Root cause: `NanochatForCausalLM` defines `prepare_inputs_for_generation`
(a strong "this model supports generation" signal to transformers) but
does **not** inherit from `GenerationMixin`. As of `transformers>=4.50`,
`PreTrainedModel` no longer automatically provides `.generate()` — this is
a breaking change that's been deprecated-warned for a few versions and is
now enforced. Your own `trust_remote_code=True` loading path already emits
this exact warning every time the model loads, so this isn't a surprise
bug — it's the deprecation landing.

## Reproduction

\`\`\`python
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch

repo = "himalaya-ai/himalayagpt-0.5b-it"
tok = AutoTokenizer.from_pretrained(repo, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
repo, trust_remote_code=True, torch_dtype=torch.float32, device_map="auto"
)
ids = tok("नेपालको राजधानी ", return_tensors="pt").input_ids.to(model.device)
out = model.generate(ids, max_new_tokens=64)
# AttributeError: 'NanochatForCausalLM' object has no attribute 'generate'
\`\`\`

## Fix in this PR

1. **Add `GenerationMixin` to the class bases** so `.generate()` exists:
\`\`\`python
class NanochatForCausalLM(PreTrainedModel, GenerationMixin):
\`\`\`

2. **Set a default `generation_config` in `__init__`**, since nothing
currently provides one and `generate()` reads from it immediately
(`self.generation_config.max_length`, etc.):
\`\`\`python
self.generation_config = GenerationConfig.from_model_config(config)
\`\`\`

That's the minimal fix to make `.generate()` callable at all.

## Known limitation this PR does NOT fix (flagging for visibility)

`prepare_inputs_for_generation` currently always returns the **full**
`input_ids` regardless of `past_key_values`, and `forward()` hardcodes
`past_key_values=None` in its return — meaning **there is no KV-cache**.
Generation will work correctly after this PR, but every new token
reprocesses the entire sequence from scratch (O(n²) instead of O(n)). For
a 0.5B model at short context this is usable but slow; for longer
generations it'll be noticeably worse than a cached implementation.

I'd suggest treating that as a separate, bigger follow-up PR (it requires
threading cache state through `NanochatAttention.forward`, which doesn't
currently accept or return any cache tensors) rather than blocking this
fix on it. Happy to take a pass at that separately if useful — wanted to
keep this PR small and easy to review/merge.

Also worth noting: `forward()` accepts an `attention_mask` parameter but
never uses it — batched/padded generation will silently ignore padding.
Only affects multi-sequence batched calls, not single-prompt generation.

## Testing

After this change, confirmed locally:
- `model.generate(ids, max_new_tokens=64)` runs without raising
- Output is well-formed token ids, decodes via `tok.decode(...)` normally
- `forward()` / loss computation (training path) is untouched — only the
class declaration and `__init__` changed

Files changed (1) hide show
  1. modeling_nanochat.py +5 -8
modeling_nanochat.py CHANGED
@@ -1,7 +1,7 @@
1
  import torch
2
  import torch.nn as nn
3
  import torch.nn.functional as F
4
- from transformers import PreTrainedModel
5
  from transformers.modeling_outputs import CausalLMOutputWithPast
6
 
7
  from .configuration_nanochat import NanochatConfig
@@ -40,16 +40,13 @@ def _apply_rotary(x, cos, sin):
40
 
41
 
42
  def _sdpa_attention(q, k, v, window_size, enable_gqa):
43
- # q/k/v are (B, H, T, D)
44
  t_q = q.size(2)
45
  t_k = k.size(2)
46
  left_window = window_size[0]
47
 
48
- # Full causal attention when the window covers full context.
49
  if (left_window < 0 or left_window >= t_q) and t_q == t_k:
50
  return F.scaled_dot_product_attention(q, k, v, is_causal=True, enable_gqa=enable_gqa)
51
 
52
- # Single-token decode path.
53
  if t_q == 1:
54
  if left_window >= 0 and left_window < t_k:
55
  start = max(0, t_k - (left_window + 1))
@@ -57,7 +54,6 @@ def _sdpa_attention(q, k, v, window_size, enable_gqa):
57
  v = v[:, :, start:, :]
58
  return F.scaled_dot_product_attention(q, k, v, is_causal=False, enable_gqa=enable_gqa)
59
 
60
- # Build explicit causal (+ optional sliding-window) mask.
61
  device = q.device
62
  row_idx = (t_k - t_q) + torch.arange(t_q, device=device).unsqueeze(1)
63
  col_idx = torch.arange(t_k, device=device).unsqueeze(0)
@@ -71,7 +67,6 @@ def _sdpa_attention(q, k, v, window_size, enable_gqa):
71
  def _flash_attn_func(q, k, v, causal=False, window_size=(-1, -1)):
72
  if not causal:
73
  raise NotImplementedError("Nanochat HF export currently supports only causal attention")
74
- # SDPA fallback mirroring nanochat.flash_attention semantics.
75
  q = q.transpose(1, 2)
76
  k = k.transpose(1, 2)
77
  v = v.transpose(1, 2)
@@ -240,7 +235,7 @@ class NanochatBackbone(nn.Module):
240
  return logits
241
 
242
 
243
- class NanochatForCausalLM(PreTrainedModel):
244
  config_class = NanochatConfig
245
  base_model_prefix = "model"
246
  main_input_name = "input_ids"
@@ -249,6 +244,8 @@ class NanochatForCausalLM(PreTrainedModel):
249
  def __init__(self, config):
250
  super().__init__(config)
251
  self.model = NanochatBackbone(config)
 
 
252
 
253
  @property
254
  def all_tied_weights_keys(self):
@@ -299,4 +296,4 @@ class NanochatForCausalLM(PreTrainedModel):
299
  )
300
 
301
  def prepare_inputs_for_generation(self, input_ids, past_key_values=None, **kwargs):
302
- return {"input_ids": input_ids}
 
1
  import torch
2
  import torch.nn as nn
3
  import torch.nn.functional as F
4
+ from transformers import PreTrainedModel, GenerationMixin, GenerationConfig
5
  from transformers.modeling_outputs import CausalLMOutputWithPast
6
 
7
  from .configuration_nanochat import NanochatConfig
 
40
 
41
 
42
  def _sdpa_attention(q, k, v, window_size, enable_gqa):
 
43
  t_q = q.size(2)
44
  t_k = k.size(2)
45
  left_window = window_size[0]
46
 
 
47
  if (left_window < 0 or left_window >= t_q) and t_q == t_k:
48
  return F.scaled_dot_product_attention(q, k, v, is_causal=True, enable_gqa=enable_gqa)
49
 
 
50
  if t_q == 1:
51
  if left_window >= 0 and left_window < t_k:
52
  start = max(0, t_k - (left_window + 1))
 
54
  v = v[:, :, start:, :]
55
  return F.scaled_dot_product_attention(q, k, v, is_causal=False, enable_gqa=enable_gqa)
56
 
 
57
  device = q.device
58
  row_idx = (t_k - t_q) + torch.arange(t_q, device=device).unsqueeze(1)
59
  col_idx = torch.arange(t_k, device=device).unsqueeze(0)
 
67
  def _flash_attn_func(q, k, v, causal=False, window_size=(-1, -1)):
68
  if not causal:
69
  raise NotImplementedError("Nanochat HF export currently supports only causal attention")
 
70
  q = q.transpose(1, 2)
71
  k = k.transpose(1, 2)
72
  v = v.transpose(1, 2)
 
235
  return logits
236
 
237
 
238
+ class NanochatForCausalLM(PreTrainedModel, GenerationMixin):
239
  config_class = NanochatConfig
240
  base_model_prefix = "model"
241
  main_input_name = "input_ids"
 
244
  def __init__(self, config):
245
  super().__init__(config)
246
  self.model = NanochatBackbone(config)
247
+ if getattr(self, "generation_config", None) is None:
248
+ self.generation_config = GenerationConfig.from_model_config(config)
249
 
250
  @property
251
  def all_tied_weights_keys(self):
 
296
  )
297
 
298
  def prepare_inputs_for_generation(self, input_ids, past_key_values=None, **kwargs):
299
+ return {"input_ids": input_ids}