File size: 12,479 Bytes
f0e6681
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
"""
RoFormer Model Architecture.

This module defines a custom Transformer model based on the non-autoregressive
architecture, adapted for ancient Slavic texts.
It overrides the standard HuggingFace BERT encoder to introduce the following innovations:

1. Pure Character-Level Representation: Better handle highly inflected languages
and physically fragmented text (e.g., birch bark).
2. Rotary Position Embeddings (RoPE): Replaces absolute positional embeddings.
   RoPE rotates query and key vectors in the self-attention mechanism, allowing the model
   to natively understand relative distances between characters across variable-length gaps.
3. Multi-Task Learning (4 Heads):
   - Restore Head (Local): Standard Masked Language Modeling (MLM) to predict missing characters `[-]`.
   - Unk Head (Local): A binary classifier predicting whether a gap token `[#]` should be expanded.
   - Date Head (Global): Predicts the document's chronological distribution (reads from `[SOS]`).
   - Region Head (Global): Classifies the dialect/region of the document (reads from `[SOS]`).
"""

import math
from dataclasses import dataclass
from typing import Optional, Tuple

import torch
import torch.nn as nn

try:
    from .config import RoFormerConfig  # package-style import (e.g. inference.py)
except ImportError:
    from config import RoFormerConfig  # script-mode import (e.g. train.py)
from transformers import BertPreTrainedModel
from transformers.models.bert.modeling_bert import BertEncoder, BertSelfAttention
from transformers.utils.generic import ModelOutput


from transformers.models.llama.modeling_llama import LlamaRotaryEmbedding, apply_rotary_pos_emb
from transformers.models.llama.configuration_llama import LlamaConfig


# CUSTOM ATTENTION WITH RoPE
class BertSelfAttentionWithRoPE(BertSelfAttention):
    def __init__(self, config: RoFormerConfig):
        super().__init__(config)
        # Some transformers versions' BertSelfAttention.__init__ doesn't keep
        # a `self.config` reference -- set it explicitly so forward() below
        # doesn't depend on that varying by version.
        self.config = config
        llama_config = LlamaConfig(
            hidden_size=self.attention_head_size * self.num_attention_heads,
            num_attention_heads=self.num_attention_heads,
            max_position_embeddings=config.max_position_embeddings,
        )
        self.rotary_emb = LlamaRotaryEmbedding(
            config=llama_config,
        )

    def transpose_for_scores(self, x: torch.Tensor) -> torch.Tensor:
        new_x_shape = x.size()[:-1] + (
            self.num_attention_heads,
            self.attention_head_size,
        )
        x = x.view(new_x_shape)
        return x.permute(0, 2, 1, 3)

    def forward(
        self,
        hidden_states: torch.Tensor,
        attention_mask: Optional[torch.Tensor] = None,
        head_mask: Optional[torch.Tensor] = None,
        encoder_hidden_states: Optional[torch.Tensor] = None,
        encoder_attention_mask: Optional[torch.Tensor] = None,
        past_key_value: Optional[Tuple[torch.Tensor]] = None,
        output_attentions: bool = False,
        **kwargs,
    ) -> Tuple[torch.Tensor, ...]:
        """
        Forward pass for the custom Self-Attention layer injecting RoPE.

        Args:
            hidden_states (torch.Tensor): Input hidden states.
            attention_mask (Optional[torch.Tensor]): Mask to avoid performing attention on padding token indices.
            head_mask (Optional[torch.Tensor]): Mask to nullify selected heads of the self-attention modules.
            encoder_hidden_states (Optional[torch.Tensor]): Used in cross-attention (not applicable here).
            encoder_attention_mask (Optional[torch.Tensor]): Used in cross-attention (not applicable here).
            past_key_value (Optional[Tuple[torch.Tensor]]): Cached key and value states.
            output_attentions (bool): Whether or not to return the attentions tensors.
        """
        output_attentions = getattr(
            self.config, "output_attentions", output_attentions
        ) or kwargs.get("output_attentions", output_attentions)
        mixed_query_layer = self.query(hidden_states)
        key_layer = self.transpose_for_scores(self.key(hidden_states))
        value_layer = self.transpose_for_scores(self.value(hidden_states))
        query_layer = self.transpose_for_scores(mixed_query_layer)

        # Apply RoPE (Rotary Position Embeddings) to Query and Key matrices
        seq_length = hidden_states.shape[1]
        position_ids = torch.arange(
            seq_length, dtype=torch.long, device=hidden_states.device
        )
        position_ids = position_ids.unsqueeze(0).expand(hidden_states.shape[0], -1)

        cos, sin = self.rotary_emb(value_layer, position_ids)
        query_layer, key_layer = apply_rotary_pos_emb(
            query_layer, key_layer, cos, sin
        )

        # Manual attention instead of SDPA: ROCm's SDPA backward pass produces
        # nan/inf gradients in bfloat16 once LR ramps up past warmup (~600
        # steps) -- confirmed twice on MI300X (see commits a283f5f/9e8a617).
        # Do not switch this back to SDPA even for the non-output_attentions
        # path; the regression that reintroduced it (aa11bfa) was a mistake.
        attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2))
        attention_scores = attention_scores / math.sqrt(self.attention_head_size)

        if attention_mask is not None:
            attention_scores = attention_scores + attention_mask

        attention_probs = nn.functional.softmax(attention_scores, dim=-1)
        attention_probs = self.dropout(attention_probs)

        if output_attentions:
            self._last_attention = attention_probs
        else:
            self._last_attention = None

        context_layer = torch.matmul(attention_probs, value_layer)

        context_layer = context_layer.permute(0, 2, 1, 3).contiguous()
        new_context_layer_shape = context_layer.size()[:-2] + (self.all_head_size,)
        context_layer = context_layer.view(*new_context_layer_shape)

        return context_layer, (attention_probs if output_attentions else None)


@dataclass
class RoFormerOutput(ModelOutput):
    logits_restore: torch.FloatTensor = None
    logits_unk: torch.FloatTensor = None
    logits_date: Optional[torch.FloatTensor] = None
    logits_region: Optional[torch.FloatTensor] = None
    attentions: Optional[Tuple[torch.FloatTensor]] = None


# MAIN MODEL ARCHITECTURE
class RoFormer(BertPreTrainedModel):
    config_class = RoFormerConfig

    def __init__(
        self, config: RoFormerConfig, num_date_bins: int = 20, num_regions: int = 4
    ):
        super().__init__(config)
        # Disable absolute positional embeddings inherited from BERT
        config.position_embedding_type = "none"

        # 1. Pure Character Embeddings Layer (No Word IDs)
        self.char_embeddings = nn.Embedding(
            config.vocab_char_size, config.hidden_size, padding_idx=config.pad_token_id
        )
        self.emb_norm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
        self.emb_dropout = nn.Dropout(config.hidden_dropout_prob)

        # 2. Transformer Encoder with RoPE Injection
        self.encoder = BertEncoder(config)
        for layer in self.encoder.layer:
            # Overwrite the standard self-attention module with our custom RoPE implementation
            layer.attention.self = BertSelfAttentionWithRoPE(config)

        # 3. Multi-Task Output Heads
        # Local Head A: Restores missing characters (tied to char_embeddings weights)
        self.restore_dense = nn.Linear(config.hidden_size, config.hidden_size)
        self.restore_act = nn.GELU()
        self.restore_norm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
        self.restore_bias = nn.Parameter(torch.zeros(config.vocab_char_size))

        # Local Head B: Predicts gap extension for the `[#]` token
        self.unk_head = nn.Sequential(
            nn.Linear(config.hidden_size, config.hidden_size),
            nn.GELU(),
            nn.Linear(config.hidden_size, 2),
        )

        # Global Head A: Predicts historical date distribution (Reads from [SOS])
        self.date_head = nn.Sequential(
            nn.Linear(config.hidden_size, config.hidden_size),
            nn.GELU(),
            nn.Linear(config.hidden_size, num_date_bins),
        )

        # Global Head B: Classifies dialect/region (Reads from [SOS])
        self.region_head = nn.Sequential(
            nn.Linear(config.hidden_size, config.hidden_size),
            nn.GELU(),
            nn.Linear(config.hidden_size, num_regions),
        )
        self.post_init()

    def get_input_embeddings(self) -> nn.Embedding:
        """
        Required by HuggingFace to support `resize_token_embeddings`.

        Returns:
            nn.Embedding: The embedding layer.
        """
        return self.char_embeddings

    def set_input_embeddings(self, value: nn.Embedding) -> None:
        """
        Required by HuggingFace to support `resize_token_embeddings`.

        Args:
            value (nn.Embedding): The new embedding layer.
        """
        self.char_embeddings = value

    def forward(
        self,
        input_ids: Optional[torch.Tensor] = None,
        attention_mask: Optional[torch.Tensor] = None,
        output_attentions: Optional[bool] = None,
        **kwargs,
    ) -> RoFormerOutput:
        """
        Forward pass for the multi-task Transformer.

        Args:
            input_ids (Optional[torch.Tensor]): Indices of input sequence tokens in the vocabulary.
            attention_mask (Optional[torch.Tensor]): Mask to avoid performing attention on padding token indices.
            output_attentions (Optional[bool]): Whether or not to return the attentions tensors.
            **kwargs: Additional keyword arguments.

        Returns:
            RoFormerOutput: A custom dataclass containing 4 sets of logits and optional attention weights.
        """
        output_attentions = (
            output_attentions
            if output_attentions is not None
            else self.config.output_attentions
        )

        # Fast character embedding generation
        x = self.char_embeddings(input_ids)
        x = self.emb_norm(x)
        x = self.emb_dropout(x)

        ext_mask = self.get_extended_attention_mask(attention_mask, input_ids.shape)

        # Pass through the RoPE-enabled encoder torso
        enc_out = self.encoder(
            x,
            attention_mask=ext_mask,
            output_attentions=output_attentions,
            return_dict=True,
        )
        seq = enc_out.last_hidden_state  # Shape: [Batch, Seq_Len, Hidden_Dim]

        # Process local sequence heads (applied to all tokens)
        x_res = self.restore_norm(self.restore_act(self.restore_dense(seq)))
        # Tied-embedding projection. Aeneas pairs sqrt(hidden_size) normalization +
        # dropout-on-the-weight-matrix with weight_decay=0.0 and LAMB (see config_greek.py);
        # under plain AdamW + weight_decay>0 on a short from-scratch budget, either half of
        # that combination may itself cap restore-head confidence (ablated independently of
        # weight_decay -- see RoFormerConfig.tied_proj_scale/tied_proj_dropout).
        emb_weights = self.char_embeddings.weight
        if self.config.tied_proj_dropout:
            emb_weights = self.emb_dropout(emb_weights)
        logits_restore = x_res @ emb_weights.T
        if self.config.tied_proj_scale:
            logits_restore = logits_restore / math.sqrt(emb_weights.shape[-1])
        logits_restore = logits_restore + self.restore_bias
        logits_unk = self.unk_head(seq)

        # Process global heads (applied exclusively to the [SOS] token at index 0)
        sos_vectors = seq[:, 0, :]
        logits_date = self.date_head(sos_vectors)
        logits_region = self.region_head(sos_vectors)

        # Manually extract the attention from the last layer to bypass HF tuple dropping
        last_attn = self.encoder.layer[-1].attention.self._last_attention
        extracted_attentions = (last_attn,) if last_attn is not None else None

        return RoFormerOutput(
            logits_restore=logits_restore,
            logits_unk=logits_unk,
            logits_date=logits_date,
            logits_region=logits_region,
            attentions=extracted_attentions,
        )