Zemulax commited on
Commit
ea2eee0
Β·
verified Β·
1 Parent(s): 2b32287

files for inference

Browse files

Architecture, Huggingface generator and interaction code

Files changed (3) hide show
  1. Architecture.py +146 -0
  2. GeneratorHF.py +199 -0
  3. chat.py +131 -0
Architecture.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import math
4
+
5
+
6
+ class MaskedMultiHeadedSelfAttention(nn.Module):
7
+ def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):
8
+ super().__init__()
9
+ assert (d_out % num_heads == 0), "d_out must be divisible by num_heads"
10
+
11
+ self.d_out = d_out
12
+ self.num_heads = num_heads
13
+ self.head_dim = d_out // num_heads
14
+
15
+ # ── Fused QKV projection (one matmul instead of three) ──────────────
16
+ self.W_qkv = nn.Linear(d_in, 3 * d_out, bias=qkv_bias)
17
+ self.out_proj = nn.Linear(d_out, d_out, bias=False)
18
+ self.dropout = nn.Dropout(dropout)
19
+
20
+ def forward(self, x):
21
+ batch_size, num_tokens, d_in = x.shape
22
+
23
+ # ── Single fused projection then split ──────────────────────────────
24
+ qkv = self.W_qkv(x) # (B, T, 3 * d_out)
25
+ q, k, v = qkv.split(self.d_out, dim=-1) # each: (B, T, d_out)
26
+
27
+ # Reshape to (B, num_heads, T, head_dim)
28
+ q = q.view(batch_size, num_tokens, self.num_heads, self.head_dim).transpose(1, 2)
29
+ k = k.view(batch_size, num_tokens, self.num_heads, self.head_dim).transpose(1, 2)
30
+ v = v.view(batch_size, num_tokens, self.num_heads, self.head_dim).transpose(1, 2)
31
+
32
+ # ── Flash Attention β€” no manual mask, is_causal handles it ──────────
33
+ context_vec = torch.nn.functional.scaled_dot_product_attention(
34
+ q, k, v,
35
+ attn_mask=None,
36
+ dropout_p=self.dropout.p if self.training else 0.0,
37
+ is_causal=True
38
+ )
39
+
40
+ # Merge heads and project
41
+ context_vec = context_vec.transpose(1, 2).reshape(batch_size, num_tokens, self.d_out)
42
+ context_vec = self.out_proj(context_vec)
43
+ return context_vec
44
+
45
+
46
+ class FeedForward(nn.Module):
47
+ def __init__(self, configuration):
48
+ super().__init__()
49
+ dim = configuration["embedding_dim"]
50
+ self.layers = nn.Sequential(
51
+ nn.Linear(dim, 4 * dim),
52
+ nn.GELU(approximate='tanh'), # fused kernel, faster than manual tanh approx
53
+ nn.Linear(4 * dim, dim),
54
+ )
55
+
56
+ def forward(self, x):
57
+ return self.layers(x)
58
+
59
+
60
+ class TransformerBlock(nn.Module):
61
+ def __init__(self, configuration):
62
+ super().__init__()
63
+ self.attention = MaskedMultiHeadedSelfAttention(
64
+ d_in=configuration["embedding_dim"],
65
+ d_out=configuration["embedding_dim"],
66
+ context_length=configuration["context_length"],
67
+ num_heads=configuration["n_heads"],
68
+ dropout=configuration["dropout_rate"],
69
+ qkv_bias=configuration["qkv_bias"]
70
+ )
71
+ self.feed_forward = FeedForward(configuration)
72
+
73
+ # ── nn.LayerNorm uses a fused CUDA kernel β€” faster than custom impl ──
74
+ self.norm1 = nn.LayerNorm(configuration["embedding_dim"])
75
+ self.norm2 = nn.LayerNorm(configuration["embedding_dim"])
76
+ self.drop_shortcut = nn.Dropout(configuration["dropout_rate"])
77
+
78
+ def forward(self, x):
79
+ # ── Attention block with residual ────────────────────────────────────
80
+ shortcut = x
81
+ x = self.norm1(x)
82
+ x = self.attention(x)
83
+ x = self.drop_shortcut(x)
84
+ x = x + shortcut
85
+
86
+ # ── Feed-forward block with residual ─────────────────────────────────
87
+ shortcut = x
88
+ x = self.norm2(x)
89
+ x = self.feed_forward(x)
90
+ x = self.drop_shortcut(x)
91
+ x = x + shortcut
92
+
93
+ return x
94
+
95
+
96
+ class LanguageModel(nn.Module):
97
+ def __init__(self, configuration):
98
+ super().__init__()
99
+ self.config = configuration
100
+
101
+ self.token_embedding = nn.Embedding(configuration["vocab_size"], configuration["embedding_dim"])
102
+ self.pos_embedding = nn.Embedding(configuration["context_length"], configuration["embedding_dim"])
103
+ self.drop_embedding = nn.Dropout(configuration["dropout_rate"])
104
+
105
+ self.transformer_blocks = nn.Sequential(
106
+ *[TransformerBlock(configuration) for _ in range(configuration["n_layers"])]
107
+ )
108
+
109
+ # ── Final norm also switched to nn.LayerNorm ─────────────────────────
110
+ self.final_norm = nn.LayerNorm(configuration["embedding_dim"])
111
+ self.out_head = nn.Linear(configuration["embedding_dim"], configuration["vocab_size"], bias=False)
112
+
113
+ # Weight tying β€” output projection shares weights with token embedding
114
+ self.out_head.weight = self.token_embedding.weight
115
+
116
+ # ── GPT-2 style weight initialization ────────────────────────────────
117
+ self.apply(self._init_weights)
118
+
119
+ # Scale residual-path projections by 1/√(2*n_layers) to prevent the
120
+ # residual stream from blowing up in deep networks. Targets:
121
+ # - attention out_proj (ends with 'out_proj.weight')
122
+ # - FFN second linear (ends with 'layers.2.weight')
123
+ residual_std = 0.02 / math.sqrt(2 * configuration["n_layers"])
124
+ for pn, p in self.named_parameters():
125
+ if pn.endswith('out_proj.weight') or pn.endswith('layers.2.weight'):
126
+ torch.nn.init.normal_(p, mean=0.0, std=residual_std)
127
+
128
+ def _init_weights(self, module):
129
+ """Initialize weights following GPT-2: N(0, 0.02) for all projections."""
130
+ if isinstance(module, nn.Linear):
131
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
132
+ if module.bias is not None:
133
+ torch.nn.init.zeros_(module.bias)
134
+ elif isinstance(module, nn.Embedding):
135
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
136
+
137
+ def forward(self, in_idx):
138
+ batch_size, seq_len = in_idx.shape
139
+ tok_embeds = self.token_embedding(in_idx)
140
+ pos_embeds = self.pos_embedding(torch.arange(seq_len, device=in_idx.device))
141
+ x = tok_embeds + pos_embeds
142
+ x = self.drop_embedding(x)
143
+ x = self.transformer_blocks(x)
144
+ x = self.final_norm(x)
145
+ logits = self.out_head(x)
146
+ return logits
GeneratorHF.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HuggingFace-compatible generator for custom LanguageModel.
3
+
4
+ Wraps the custom model in a minimal HF-compatible interface so we can use
5
+ transformers.generate() with all its bells and whistles (beam search,
6
+ contrastive search, repetition penalty, etc.) while keeping our own weights.
7
+
8
+ Usage:
9
+ from GeneratorHF import HFTextGenerator
10
+
11
+ gen = HFTextGenerator(model, tokenizer, device, context_size=1024)
12
+ text = gen.generate("Once upon a time", max_new_tokens=200)
13
+ """
14
+
15
+ import torch
16
+ import torch.nn as nn
17
+ from transformers import GenerationConfig, GenerationMixin, PretrainedConfig
18
+
19
+
20
+ class LanguageModelHF(nn.Module, GenerationMixin):
21
+ """Thin wrapper that makes our custom LanguageModel compatible with
22
+ HuggingFace's GenerationMixin (generate, beam_search, sample, etc.)."""
23
+
24
+ _is_stateful = False
25
+ _supports_cache_class = False
26
+
27
+ def __init__(self, model, context_size, device):
28
+ super().__init__()
29
+ self.model = model
30
+ self.config = _make_hf_config(model, context_size)
31
+ self.generation_config = GenerationConfig(
32
+ max_new_tokens=200,
33
+ do_sample=True,
34
+ temperature=0.8,
35
+ top_k=40,
36
+ top_p=0.95,
37
+ repetition_penalty=1.1,
38
+ eos_token_id=50256,
39
+ pad_token_id=50256,
40
+ )
41
+ self.main_input_name = "input_ids"
42
+
43
+ @property
44
+ def device(self):
45
+ return next(self.parameters()).device
46
+
47
+ def forward(self, input_ids, attention_mask=None, **kwargs):
48
+ # Trim to context window (HF might feed longer sequences)
49
+ input_ids = input_ids[:, -self.config.n_positions:]
50
+ logits = self.model(input_ids)
51
+ return _CausalLMOutput(logits=logits)
52
+
53
+ def prepare_inputs_for_generation(self, input_ids, attention_mask=None, **kwargs):
54
+ return {"input_ids": input_ids}
55
+
56
+ def can_generate(self):
57
+ return True
58
+
59
+ def _reorder_cache(self, past, beam_idx):
60
+ return past
61
+
62
+
63
+ class _CausalLMOutput:
64
+ """Minimal output container matching HF's CausalLMOutput interface."""
65
+ def __init__(self, logits):
66
+ self.logits = logits
67
+ self.past_key_values = None
68
+ self.hidden_states = None
69
+ self.attentions = None
70
+
71
+ def __getitem__(self, key):
72
+ return getattr(self, key)
73
+
74
+ def __contains__(self, key):
75
+ return hasattr(self, key) and getattr(self, key) is not None
76
+
77
+ def keys(self):
78
+ return [k for k in ["logits", "past_key_values", "hidden_states", "attentions"]
79
+ if getattr(self, k, None) is not None]
80
+
81
+
82
+ class _MinimalConfig(PretrainedConfig):
83
+ """Config that satisfies HF GenerationMixin by inheriting PretrainedConfig."""
84
+ model_type = "custom_gpt2"
85
+
86
+ def __init__(self, **kwargs):
87
+ super().__init__(**kwargs)
88
+ self.is_encoder_decoder = False
89
+ self.n_positions = kwargs.get("n_positions", 1024)
90
+ self.vocab_size = kwargs.get("vocab_size", 50257)
91
+ self.eos_token_id = 50256
92
+ self.pad_token_id = 50256
93
+ self.bos_token_id = 50256
94
+ self.use_cache = False
95
+ self.num_hidden_layers = kwargs.get("num_hidden_layers", 12)
96
+
97
+
98
+ def _make_hf_config(model, context_size):
99
+ vocab_size = 50257
100
+ n_positions = context_size
101
+ num_hidden_layers = 12
102
+ if hasattr(model, 'config') and isinstance(model.config, dict):
103
+ vocab_size = model.config.get("vocab_size", 50257)
104
+ n_positions = model.config.get("context_length", context_size)
105
+ num_hidden_layers = model.config.get("n_layers", 12)
106
+
107
+ cfg = _MinimalConfig(vocab_size=vocab_size, n_positions=n_positions, num_hidden_layers=num_hidden_layers)
108
+ return cfg
109
+
110
+
111
+ class HFTextGenerator:
112
+ """Drop-in replacement for TextGenerator using HF generate()."""
113
+
114
+ def __init__(self, model, tokenizer, device, context_size=1024):
115
+ self.tokenizer = tokenizer
116
+ self.device = device
117
+ self.context_size = context_size
118
+ self.hf_model = LanguageModelHF(model, context_size, device)
119
+ self.hf_model.to(device)
120
+ self.hf_model.eval()
121
+
122
+ def generate(self, prompt, max_new_tokens=200,
123
+ temperature=0.8, top_k=40, top_p=0.95,
124
+ repetition_penalty=1.1, num_beams=1,
125
+ do_sample=True, eos_id=50256):
126
+ """Generate text from a string prompt. Returns the generated string."""
127
+ input_ids = torch.tensor(
128
+ [self.tokenizer.encode(prompt, allowed_special={"<|endoftext|>"})]
129
+ ).to(self.device)
130
+
131
+ gen_config = GenerationConfig(
132
+ max_new_tokens=max_new_tokens,
133
+ do_sample=do_sample,
134
+ temperature=temperature,
135
+ top_k=top_k,
136
+ top_p=top_p,
137
+ repetition_penalty=repetition_penalty,
138
+ num_beams=num_beams,
139
+ eos_token_id=eos_id,
140
+ pad_token_id=eos_id,
141
+ )
142
+
143
+ attention_mask = torch.ones_like(input_ids)
144
+
145
+ with torch.no_grad():
146
+ output_ids = self.hf_model.generate(
147
+ input_ids,
148
+ attention_mask=attention_mask,
149
+ generation_config=gen_config,
150
+ )
151
+
152
+ # Decode only the newly generated tokens
153
+ new_tokens = output_ids[0, input_ids.shape[1]:]
154
+
155
+ # Filter out the EOS token if it was generated
156
+ if len(new_tokens) > 0 and new_tokens[-1].item() == eos_id:
157
+ new_tokens = new_tokens[:-1]
158
+
159
+ decoded_text = self.tokenizer.decode(new_tokens.tolist())
160
+ return decoded_text.replace("<|endoftext|>", "").strip()
161
+
162
+ def generate_ids(self, idx, max_new_tokens=200,
163
+ temperature=0.8, top_k=40, top_p=0.95,
164
+ repetition_penalty=1.1, num_beams=1,
165
+ do_sample=True, eos_id=50256):
166
+ """Generate from token IDs tensor (same interface as original).
167
+ Returns full sequence including prompt."""
168
+ gen_config = GenerationConfig(
169
+ max_new_tokens=max_new_tokens,
170
+ do_sample=do_sample,
171
+ temperature=temperature,
172
+ top_k=top_k,
173
+ top_p=top_p,
174
+ repetition_penalty=repetition_penalty,
175
+ num_beams=num_beams,
176
+ eos_token_id=eos_id,
177
+ pad_token_id=eos_id,
178
+ )
179
+
180
+ idx = idx.to(self.device)
181
+ attention_mask = torch.ones_like(idx)
182
+
183
+ with torch.no_grad():
184
+ output_ids = self.hf_model.generate(
185
+ idx,
186
+ attention_mask=attention_mask,
187
+ generation_config=gen_config,
188
+ )
189
+
190
+ return output_ids
191
+
192
+ # ── Backward-compatible helpers (match TextGenerator interface) ───────
193
+ def token_ids_to_text(self, token_ids):
194
+ flat = token_ids.squeeze(0)
195
+ return self.tokenizer.decode(flat.tolist())
196
+
197
+ def text_to_token_ids(self, text):
198
+ encoded = self.tokenizer.encode(text, allowed_special={"<|endoftext|>"})
199
+ return torch.tensor(encoded).unsqueeze(0).to(self.device)
chat.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Interactive chat with your GPT-2 model β€” Notebook version
3
+ Run Cell 1 and Cell 2 in order, then use the chat widget.
4
+ You can copy the code into colab cells. ensure you rename to your model path
5
+ """
6
+
7
+ # ── Cell 1: Setup & load model ────────────────────────────────────────────────
8
+ import torch
9
+ import tiktoken
10
+ import ipywidgets as widgets
11
+ from IPython.display import display, HTML
12
+ from Architecture import LanguageModel
13
+ from GeneratorHF import HFTextGenerator
14
+
15
+ CHECKPOINT_PATH = "/content/drive/MyDrive/GPT2-small/refinedGPT2small.pth" #on google colab
16
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
17
+
18
+ def load_model(checkpoint_path, device):
19
+ print(f"Loading model from {checkpoint_path}...")
20
+ ckpt = torch.load(checkpoint_path, map_location=device, weights_only=False)
21
+ config = ckpt["config"]
22
+ model = LanguageModel(config)
23
+ state_dict = ckpt["model_state_dict"]
24
+
25
+ cleaned = {}
26
+ for k, v in state_dict.items():
27
+ for prefix in ["module.", "_orig_mod."]:
28
+ while k.startswith(prefix):
29
+ k = k[len(prefix):]
30
+ cleaned[k] = v
31
+
32
+ model.load_state_dict(cleaned)
33
+ model.to(device)
34
+ model.eval()
35
+
36
+ params = sum(p.numel() for p in model.parameters()) / 1e6
37
+ val_loss = ckpt.get("loss", "?")
38
+ print(f" {params:.1f}M params | val_loss {val_loss} | device {device}")
39
+ return model, config
40
+
41
+ model, config = load_model(CHECKPOINT_PATH, DEVICE)
42
+ enc = tiktoken.get_encoding("gpt2")
43
+ gen = HFTextGenerator(model, enc, DEVICE, context_size=config["context_length"])
44
+ print("Ready.")
45
+
46
+
47
+ # ── Cell 2: Chat widget ───────────────────────────────────────────────────────
48
+
49
+ # ── Sampling controls ─────────────────────────────────────────────────────────
50
+ temp_slider = widgets.FloatSlider(
51
+ value=0.8, min=0.1, max=2.0, step=0.05,
52
+ description="Temperature",
53
+ style={"description_width": "90px"},
54
+ layout=widgets.Layout(width="400px")
55
+ )
56
+ topk_slider = widgets.IntSlider(
57
+ value=40, min=1, max=200, step=1,
58
+ description="Top-k",
59
+ style={"description_width": "90px"},
60
+ layout=widgets.Layout(width="400px")
61
+ )
62
+ tokens_slider = widgets.IntSlider(
63
+ value=150, min=10, max=500, step=10,
64
+ description="Max tokens",
65
+ style={"description_width": "90px"},
66
+ layout=widgets.Layout(width="400px")
67
+ )
68
+
69
+ # ── Input & buttons ───────────────────────────────────────────────────────────
70
+ prompt_box = widgets.Textarea(
71
+ placeholder="Type your prompt here...",
72
+ layout=widgets.Layout(width="700px", height="70px")
73
+ )
74
+ generate_btn = widgets.Button(
75
+ description="Generate",
76
+ button_style="primary",
77
+ layout=widgets.Layout(width="120px")
78
+ )
79
+ clear_btn = widgets.Button(
80
+ description="Clear",
81
+ button_style="warning",
82
+ layout=widgets.Layout(width="80px")
83
+ )
84
+
85
+ # ── Output area ───────────────────────────────────────────────────────────────
86
+ output_area = widgets.Output()
87
+
88
+ def on_generate(b):
89
+ prompt = prompt_box.value.strip()
90
+ if not prompt:
91
+ return
92
+
93
+ with output_area:
94
+ print(f"\nYOU > {prompt}")
95
+ print("MODEL > ", end="", flush=True)
96
+
97
+ continuation = gen.generate(
98
+ prompt,
99
+ max_new_tokens=tokens_slider.value,
100
+ temperature=temp_slider.value,
101
+ top_k=topk_slider.value,
102
+ )
103
+ print(continuation)
104
+ print("-" * 70)
105
+
106
+ def on_clear(b):
107
+ output_area.clear_output()
108
+ prompt_box.value = ""
109
+
110
+ generate_btn.on_click(on_generate)
111
+ clear_btn.on_click(on_clear)
112
+
113
+ # ── Layout ────────────────────────────────────────────────────────────────────
114
+ controls = widgets.VBox([
115
+ widgets.HTML("<b>Sampling settings</b>"),
116
+ temp_slider,
117
+ topk_slider,
118
+ tokens_slider,
119
+ ])
120
+ buttons = widgets.HBox([generate_btn, clear_btn])
121
+ ui = widgets.VBox([
122
+ widgets.HTML("<h3>GPT-2 Small β€” Chat</h3>"),
123
+ controls,
124
+ widgets.HTML("<br><b>Prompt</b>"),
125
+ prompt_box,
126
+ buttons,
127
+ widgets.HTML("<br><b>Output</b>"),
128
+ output_area,
129
+ ])
130
+
131
+ display(ui)