Glazkov commited on
Commit
0b28d57
·
verified ·
1 Parent(s): 57b70a4

Add modeling_mars.py

Browse files
Files changed (1) hide show
  1. modeling_mars.py +171 -0
modeling_mars.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Self-contained model definition for the MARS shared-cross-attention checkpoint.
2
+
3
+ This is a faithful reproduction of `SharedCrossAttentionModel` from the
4
+ `summ-mask-benchmark` research repo, stripped of optional research flags
5
+ (copy mechanism, type bias) that are NOT used by this checkpoint.
6
+
7
+ Architecture: a single ModernBERT-base encoder shared between the summary
8
+ and the masked text, followed by 2 cross-attention layers where the masked
9
+ text attends to the summary, then a vocab projection.
10
+ """
11
+
12
+ import math
13
+ from typing import Optional
14
+
15
+ import torch
16
+ import torch.nn as nn
17
+ import torch.nn.functional as F
18
+ from transformers import AutoModel
19
+ from transformers.modeling_outputs import MaskedLMOutput
20
+
21
+
22
+ ENTMASK = "[ENTMASK]"
23
+ ENTSTART = "[ENTSTART]"
24
+ ENTEND = "[ENTEND]"
25
+
26
+ ENTITY_TYPES = [
27
+ "PERSON", "ORG", "GPE", "LOC", "DATE", "TIME", "MONEY", "QUANTITY",
28
+ "PERCENT", "CARDINAL", "ORDINAL", "EVENT", "WORK_OF_ART", "LAW",
29
+ "LANGUAGE", "FAC", "PRODUCT", "NORP",
30
+ ]
31
+ TYPED_ENTMASK_TOKENS = {t: f"[ENTMASK_{t}]" for t in ENTITY_TYPES}
32
+
33
+
34
+ class CrossAttentionLayer(nn.Module):
35
+ def __init__(self, hidden_size: int, num_attention_heads: int = 12, attention_dropout: float = 0.1):
36
+ super().__init__()
37
+ self.num_attention_heads = num_attention_heads
38
+ self.attention_head_size = hidden_size // num_attention_heads
39
+ self.all_head_size = self.num_attention_heads * self.attention_head_size
40
+ self.query = nn.Linear(hidden_size, self.all_head_size)
41
+ self.key = nn.Linear(hidden_size, self.all_head_size)
42
+ self.value = nn.Linear(hidden_size, self.all_head_size)
43
+ self.dropout = nn.Dropout(attention_dropout)
44
+ self.dense = nn.Linear(hidden_size, hidden_size)
45
+ self.layer_norm = nn.LayerNorm(hidden_size)
46
+
47
+ def _shape(self, x: torch.Tensor) -> torch.Tensor:
48
+ new_shape = x.size()[:-1] + (self.num_attention_heads, self.attention_head_size)
49
+ return x.view(*new_shape).permute(0, 2, 1, 3)
50
+
51
+ def forward(
52
+ self,
53
+ hidden_states: torch.Tensor,
54
+ encoder_hidden_states: torch.Tensor,
55
+ encoder_attention_mask: Optional[torch.Tensor] = None,
56
+ ) -> torch.Tensor:
57
+ q = self._shape(self.query(hidden_states))
58
+ k = self._shape(self.key(encoder_hidden_states))
59
+ v = self._shape(self.value(encoder_hidden_states))
60
+
61
+ scores = torch.matmul(q, k.transpose(-1, -2)) / math.sqrt(self.attention_head_size)
62
+ if encoder_attention_mask is not None:
63
+ ext = encoder_attention_mask[:, None, None, :]
64
+ ext = (1.0 - ext) * torch.finfo(scores.dtype).min
65
+ scores = scores + ext
66
+ probs = self.dropout(F.softmax(scores, dim=-1))
67
+
68
+ ctx = torch.matmul(probs, v).permute(0, 2, 1, 3).contiguous()
69
+ ctx = ctx.view(*ctx.size()[:-2], self.all_head_size)
70
+
71
+ out = self.dropout(self.dense(ctx))
72
+ return self.layer_norm(hidden_states + out)
73
+
74
+
75
+ class SharedCrossAttentionModel(nn.Module):
76
+ """Shared-encoder cross-attention model for masked entity infilling.
77
+
78
+ Use `summary_input_ids` / `masked_input_ids` (both encoded with the
79
+ same tokenizer that includes [ENTMASK], [ENTSTART], [ENTEND] and the
80
+ typed [ENTMASK_<TYPE>] specials).
81
+ """
82
+
83
+ def __init__(
84
+ self,
85
+ model_name: str = "answerdotai/ModernBERT-base",
86
+ num_cross_attention_layers: int = 2,
87
+ num_attention_heads: int = 12,
88
+ dropout: float = 0.1,
89
+ ):
90
+ super().__init__()
91
+ self.encoder = AutoModel.from_pretrained(model_name)
92
+ hidden_size = self.encoder.config.hidden_size
93
+ vocab_size = self.encoder.config.vocab_size
94
+
95
+ encoder_heads = getattr(self.encoder.config, "num_attention_heads", num_attention_heads)
96
+ if hidden_size % num_attention_heads != 0:
97
+ num_attention_heads = encoder_heads
98
+
99
+ self.cross_attention_layers = nn.ModuleList(
100
+ [CrossAttentionLayer(hidden_size, num_attention_heads, dropout)
101
+ for _ in range(num_cross_attention_layers)]
102
+ )
103
+ self.predictions = nn.Linear(hidden_size, vocab_size)
104
+ self.hidden_size = hidden_size
105
+ self.vocab_size = vocab_size
106
+
107
+ def resize_token_embeddings(self, new_num_tokens: int):
108
+ self.encoder.resize_token_embeddings(new_num_tokens)
109
+ old = self.predictions
110
+ self.predictions = nn.Linear(self.hidden_size, new_num_tokens)
111
+ n = min(old.out_features, new_num_tokens)
112
+ self.predictions.weight.data[:n] = old.weight.data[:n]
113
+ self.predictions.bias.data[:n] = old.bias.data[:n]
114
+ self.vocab_size = new_num_tokens
115
+
116
+ def forward(
117
+ self,
118
+ summary_input_ids: torch.Tensor,
119
+ summary_attention_mask: torch.Tensor,
120
+ masked_input_ids: torch.Tensor,
121
+ masked_attention_mask: torch.Tensor,
122
+ labels: Optional[torch.Tensor] = None,
123
+ ) -> MaskedLMOutput:
124
+ s_hid = self.encoder(input_ids=summary_input_ids,
125
+ attention_mask=summary_attention_mask).last_hidden_state
126
+ m_hid = self.encoder(input_ids=masked_input_ids,
127
+ attention_mask=masked_attention_mask).last_hidden_state
128
+
129
+ for cross in self.cross_attention_layers:
130
+ m_hid = cross(m_hid, s_hid, summary_attention_mask)
131
+
132
+ logits = self.predictions(m_hid)
133
+ loss = None
134
+ if labels is not None:
135
+ loss = nn.CrossEntropyLoss(ignore_index=-100)(
136
+ logits.view(-1, self.vocab_size), labels.view(-1)
137
+ )
138
+ return MaskedLMOutput(loss=loss, logits=logits, hidden_states=m_hid)
139
+
140
+
141
+ def load_model_from_checkpoint(
142
+ repo_or_path: str,
143
+ base_model: str = "answerdotai/ModernBERT-base",
144
+ device: Optional[str] = None,
145
+ ):
146
+ """Load the MARS checkpoint and matching tokenizer from a local dir or
147
+ a Hugging Face repo id.
148
+
149
+ Returns: (model, tokenizer, device_str)
150
+ """
151
+ from pathlib import Path
152
+ from transformers import AutoTokenizer
153
+ from huggingface_hub import snapshot_download
154
+
155
+ if device is None:
156
+ device = "cuda" if torch.cuda.is_available() else (
157
+ "mps" if torch.backends.mps.is_available() else "cpu"
158
+ )
159
+
160
+ path = Path(repo_or_path)
161
+ if not path.exists():
162
+ path = Path(snapshot_download(repo_id=repo_or_path))
163
+
164
+ tokenizer = AutoTokenizer.from_pretrained(path, use_fast=True)
165
+ model = SharedCrossAttentionModel(model_name=base_model)
166
+ model.resize_token_embeddings(len(tokenizer))
167
+
168
+ state = torch.load(path / "model.pt", map_location=device, weights_only=True)
169
+ model.load_state_dict(state)
170
+ model.to(device).eval()
171
+ return model, tokenizer, device