nisarg6502 commited on
Commit
ade0829
·
verified ·
1 Parent(s): 5f5492a

Add KV caching to inference, ~5.9x average speedup on CPU

Browse files
Files changed (1) hide show
  1. app.py +276 -232
app.py CHANGED
@@ -1,232 +1,276 @@
1
- import gradio as gr
2
- import torch
3
- import torch.nn as nn
4
- import torch.nn.functional as F
5
- import math
6
- import tiktoken
7
- from dataclasses import dataclass
8
- from huggingface_hub import hf_hub_download
9
- from safetensors.torch import load_file
10
-
11
- # ==========================================
12
- # 1. ARCHITECTURE (Required to load pii_model.pt)
13
- # ==========================================
14
- @dataclass
15
- class TokenizerConfig:
16
- name: str = "gpt2"
17
- vocab_size: int = 50257
18
-
19
- class SimpleTokenizer:
20
- def __init__(self, config=None):
21
- self.config = config or TokenizerConfig()
22
- self.enc = tiktoken.get_encoding(self.config.name)
23
- self.eos_token = "<|endoftext|>"
24
- self.eos_token_id = self.enc.encode(self.eos_token, allowed_special={self.eos_token})[0]
25
-
26
- def encode(self, text):
27
- return self.enc.encode(text, allowed_special={self.eos_token})
28
-
29
- def decode(self, ids):
30
- return self.enc.decode(ids)
31
-
32
- class RotaryPositionalEmbedding(nn.Module):
33
- def __init__(self, d_model, max_seq_len=2048, theta=10000.0):
34
- super().__init__()
35
- assert d_model % 2 == 0
36
- dim_indices = torch.arange(0, d_model, 2).float()
37
- inv_freq = 1.0 / (theta ** (dim_indices / d_model))
38
- positions = torch.arange(max_seq_len).float()
39
- freqs = torch.outer(positions, inv_freq)
40
- emb = freqs.repeat_interleave(2, dim=-1)
41
- self.register_buffer("cos_cached", emb.cos())
42
- self.register_buffer("sin_cached", emb.sin())
43
-
44
- @staticmethod
45
- def rotate_half(x):
46
- x1 = x[..., : x.shape[-1] // 2]
47
- x2 = x[..., x.shape[-1] // 2 :]
48
- return torch.cat([-x2, x1], dim=-1)
49
-
50
- def forward(self, x, seq_len):
51
- cos = self.cos_cached[:seq_len].unsqueeze(0).unsqueeze(0)
52
- sin = self.sin_cached[:seq_len].unsqueeze(0).unsqueeze(0)
53
- return (x * cos) + (self.rotate_half(x) * sin)
54
-
55
- def create_causal_mask(seq_len, device):
56
- return torch.tril(torch.ones(seq_len, seq_len, device=device)).view(1, 1, seq_len, seq_len)
57
-
58
- class MultiHeadAttention(nn.Module):
59
- def __init__(self, d_model, num_heads, dropout=0.1):
60
- super().__init__()
61
- self.d_model = d_model
62
- self.num_heads = num_heads
63
- self.head_dim = d_model // num_heads
64
- self.qkv_proj = nn.Linear(d_model, 3 * d_model, bias=False)
65
- self.out_proj = nn.Linear(d_model, d_model, bias=False)
66
- self.rotary = RotaryPositionalEmbedding(self.head_dim)
67
- self.attn_dropout = nn.Dropout(dropout)
68
- self.resid_dropout = nn.Dropout(dropout)
69
-
70
- def forward(self, x, mask=None):
71
- batch_size, seq_len, _ = x.shape
72
- qkv = self.qkv_proj(x).reshape(batch_size, seq_len, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
73
- q, k, v = qkv[0], qkv[1], qkv[2]
74
- q, k = self.rotary(q, seq_len), self.rotary(k, seq_len)
75
- attn_scores = (q @ k.transpose(-2, -1)) / (self.head_dim ** 0.5)
76
- if mask is not None:
77
- attn_scores = attn_scores.masked_fill(mask == 0, float('-inf'))
78
- attn_weights = self.attn_dropout(F.softmax(attn_scores, dim=-1))
79
- attn_output = (attn_weights @ v).transpose(1, 2).contiguous().reshape(batch_size, seq_len, self.d_model)
80
- return self.resid_dropout(self.out_proj(attn_output))
81
-
82
- class RMSNorm(nn.Module):
83
- def __init__(self, d_model, eps=1e-6):
84
- super().__init__()
85
- self.weight = nn.Parameter(torch.ones(d_model))
86
- self.eps = eps
87
-
88
- def forward(self, x):
89
- return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) * self.weight
90
-
91
- class SwiGLU(nn.Module):
92
- def __init__(self, d_model, expansion_factor=4):
93
- super().__init__()
94
- hidden_dim = expansion_factor * d_model
95
- self.w1 = nn.Linear(d_model, hidden_dim, bias=False)
96
- self.w2 = nn.Linear(d_model, hidden_dim, bias=False)
97
- self.w3 = nn.Linear(hidden_dim, d_model, bias=False)
98
-
99
- def forward(self, x):
100
- return self.w3(F.silu(self.w1(x)) * self.w2(x))
101
-
102
- class TransformerBlock(nn.Module):
103
- def __init__(self, d_model, num_heads, dropout=0.1):
104
- super().__init__()
105
- self.norm1 = RMSNorm(d_model)
106
- self.attention = MultiHeadAttention(d_model, num_heads, dropout)
107
- self.norm2 = RMSNorm(d_model)
108
- self.ffn = SwiGLU(d_model)
109
-
110
- def forward(self, x, mask=None):
111
- x = x + self.attention(self.norm1(x), mask)
112
- x = x + self.ffn(self.norm2(x))
113
- return x
114
-
115
- @dataclass
116
- class GPTConfig:
117
- vocab_size: int = 50257
118
- d_model: int = 768
119
- num_heads: int = 12
120
- num_layers: int = 12
121
- max_seq_len: int = 512
122
- dropout: float = 0.1
123
- embd_dropout: float = 0.1
124
-
125
- class GPT(nn.Module):
126
- def __init__(self, config):
127
- super().__init__()
128
- self.config = config
129
- self.token_embedding = nn.Embedding(config.vocab_size, config.d_model)
130
- self.embd_dropout = nn.Dropout(config.embd_dropout)
131
- self.layers = nn.ModuleList([TransformerBlock(config.d_model, config.num_heads, config.dropout) for _ in range(config.num_layers)])
132
- self.final_norm = RMSNorm(config.d_model)
133
- self.lm_head = nn.Linear(config.d_model, config.vocab_size, bias=False)
134
- self.token_embedding.weight = self.lm_head.weight
135
-
136
- def forward(self, input_ids):
137
- batch_size, seq_len = input_ids.shape
138
- x = self.embd_dropout(self.token_embedding(input_ids))
139
- mask = create_causal_mask(seq_len, input_ids.device)
140
- for layer in self.layers: x = layer(x, mask)
141
- return self.lm_head(self.final_norm(x))
142
-
143
- @torch.no_grad()
144
- def generate(self, input_ids, max_new_tokens, temperature=0.2, stop_token_id=None):
145
- self.eval()
146
- for _ in range(max_new_tokens):
147
- if input_ids.shape[1] > self.config.max_seq_len:
148
- input_ids = input_ids[:, -self.config.max_seq_len:]
149
-
150
- logits = self.forward(input_ids)
151
- logits = logits[:, -1, :] / temperature
152
- probs = F.softmax(logits, dim=-1)
153
- next_token = torch.multinomial(probs, num_samples=1)
154
-
155
- input_ids = torch.cat([input_ids, next_token], dim=1)
156
-
157
- if stop_token_id is not None and next_token.item() == stop_token_id:
158
- break
159
-
160
- return input_ids
161
-
162
- # ==========================================
163
- # 2. GRADIO INTERFACE SETUP (The Pro Way)
164
- # ==========================================
165
- print("Starting up PII Firewall...")
166
- device = torch.device("cpu")
167
- tokenizer = SimpleTokenizer()
168
-
169
- try:
170
- print("Downloading weights from Hugging Face Hub...")
171
- model_path = hf_hub_download(
172
- repo_id="nisarg6502/Llama3-150M-PII-Redactor",
173
- filename="pii_model_epoch_3.safetensors"
174
- )
175
-
176
- # Instantiate the architecture
177
- config = GPTConfig()
178
- model = GPT(config)
179
-
180
- print("Loading safetensors into memory...")
181
- state_dict = load_file(model_path, device=str(device))
182
- model.load_state_dict(state_dict)
183
- model.to(device)
184
- model.eval()
185
-
186
- model_loaded = True
187
- print("Model loaded successfully!")
188
- except Exception as e:
189
- model_loaded = False
190
- print(f"Failed to load model: {str(e)}")
191
-
192
- # ... (The rest of your scrub_text function and Gradio UI code stays exactly the same!) ...
193
-
194
- def scrub_text(user_input):
195
- if not model_loaded:
196
- return f"Error: Could not load pii_model.pt."
197
-
198
- if not user_input.strip():
199
- return "Please enter text to redact."
200
-
201
- # INVISIBLE FORMATTING: The user just types normal text, but we wrap it in the triggers!
202
- prompt = f"[RAW] {user_input} [REDACTED] "
203
- input_ids = torch.tensor([tokenizer.encode(prompt)], dtype=torch.long, device=device)
204
-
205
- # Generate text with low temperature for strict factual output
206
- output_ids = model.generate(input_ids, max_new_tokens=100, temperature=0.2, stop_token_id=tokenizer.eos_token_id)
207
- full_output = tokenizer.decode(output_ids[0].tolist())
208
-
209
- # Extract only the redacted portion to show the user
210
- if "[REDACTED]" in full_output:
211
- final_result = full_output.split("[REDACTED]")[-1].replace("<|endoftext|>", "").strip()
212
- else:
213
- final_result = full_output
214
-
215
- return final_result
216
-
217
- # Build the Web UI
218
- with gr.Blocks() as demo:
219
- gr.Markdown("# 🛡️ Local Privacy Firewall (150M Parameters)")
220
- gr.Markdown("This model was fine-tuned from scratch to detect and redact Personally Identifiable Information (PII) before it ever leaves the local network.")
221
-
222
- with gr.Row():
223
- with gr.Column():
224
- prompt_input = gr.Textbox(lines=4, label="Raw Text (Contains PII)", placeholder="Please send the receipt to michael.scott@dundermifflin.com...")
225
- submit_btn = gr.Button("Scrub Data", variant="primary")
226
-
227
- with gr.Column():
228
- output_text = gr.Textbox(lines=4, label="Safe Text (Redacted)")
229
-
230
- submit_btn.click(fn=scrub_text, inputs=[prompt_input], outputs=output_text)
231
-
232
- demo.launch(share=True, theme=gr.themes.Monochrome())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.nn.functional as F
5
+ import math
6
+ import tiktoken
7
+ from dataclasses import dataclass
8
+ from huggingface_hub import hf_hub_download
9
+ from safetensors.torch import load_file
10
+
11
+ # ==========================================
12
+ # 1. ARCHITECTURE (Required to load pii_model.pt)
13
+ # ==========================================
14
+ @dataclass
15
+ class TokenizerConfig:
16
+ name: str = "gpt2"
17
+ vocab_size: int = 50257
18
+
19
+ class SimpleTokenizer:
20
+ def __init__(self, config=None):
21
+ self.config = config or TokenizerConfig()
22
+ self.enc = tiktoken.get_encoding(self.config.name)
23
+ self.eos_token = "<|endoftext|>"
24
+ self.eos_token_id = self.enc.encode(self.eos_token, allowed_special={self.eos_token})[0]
25
+
26
+ def encode(self, text):
27
+ return self.enc.encode(text, allowed_special={self.eos_token})
28
+
29
+ def decode(self, ids):
30
+ return self.enc.decode(ids)
31
+
32
+ class RotaryPositionalEmbedding(nn.Module):
33
+ def __init__(self, d_model, max_seq_len=2048, theta=10000.0):
34
+ super().__init__()
35
+ assert d_model % 2 == 0
36
+ dim_indices = torch.arange(0, d_model, 2).float()
37
+ inv_freq = 1.0 / (theta ** (dim_indices / d_model))
38
+ positions = torch.arange(max_seq_len).float()
39
+ freqs = torch.outer(positions, inv_freq)
40
+ emb = freqs.repeat_interleave(2, dim=-1)
41
+ self.register_buffer("cos_cached", emb.cos())
42
+ self.register_buffer("sin_cached", emb.sin())
43
+
44
+ @staticmethod
45
+ def rotate_half(x):
46
+ x1 = x[..., : x.shape[-1] // 2]
47
+ x2 = x[..., x.shape[-1] // 2 :]
48
+ return torch.cat([-x2, x1], dim=-1)
49
+
50
+ def forward(self, x, offset=0):
51
+ # offset = absolute position of x[..., 0, :] in the full sequence.
52
+ # Needed for KV caching: a newly generated token at position `offset`
53
+ # must be rotated with that position's angle, not position 0.
54
+ seq_len = x.shape[-2]
55
+ cos = self.cos_cached[offset:offset + seq_len].unsqueeze(0).unsqueeze(0)
56
+ sin = self.sin_cached[offset:offset + seq_len].unsqueeze(0).unsqueeze(0)
57
+ return (x * cos) + (self.rotate_half(x) * sin)
58
+
59
+ def create_causal_mask(seq_len, device):
60
+ return torch.tril(torch.ones(seq_len, seq_len, device=device)).view(1, 1, seq_len, seq_len)
61
+
62
+ class MultiHeadAttention(nn.Module):
63
+ def __init__(self, d_model, num_heads, dropout=0.1):
64
+ super().__init__()
65
+ self.d_model = d_model
66
+ self.num_heads = num_heads
67
+ self.head_dim = d_model // num_heads
68
+ self.qkv_proj = nn.Linear(d_model, 3 * d_model, bias=False)
69
+ self.out_proj = nn.Linear(d_model, d_model, bias=False)
70
+ self.rotary = RotaryPositionalEmbedding(self.head_dim)
71
+ self.attn_dropout = nn.Dropout(dropout)
72
+ self.resid_dropout = nn.Dropout(dropout)
73
+
74
+ def forward(self, x, mask=None, past_kv=None, use_cache=False):
75
+ batch_size, seq_len, _ = x.shape
76
+ qkv = self.qkv_proj(x).reshape(batch_size, seq_len, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
77
+ q, k, v = qkv[0], qkv[1], qkv[2]
78
+
79
+ offset = past_kv[0].shape[-2] if past_kv is not None else 0
80
+ q = self.rotary(q, offset=offset)
81
+ k = self.rotary(k, offset=offset)
82
+
83
+ if past_kv is not None:
84
+ past_k, past_v = past_kv
85
+ k = torch.cat([past_k, k], dim=-2)
86
+ v = torch.cat([past_v, v], dim=-2)
87
+ new_kv = (k, v) if use_cache else None
88
+
89
+ attn_scores = (q @ k.transpose(-2, -1)) / (self.head_dim ** 0.5)
90
+ if mask is not None:
91
+ attn_scores = attn_scores.masked_fill(mask == 0, float('-inf'))
92
+ attn_weights = self.attn_dropout(F.softmax(attn_scores, dim=-1))
93
+ attn_output = (attn_weights @ v).transpose(1, 2).contiguous().reshape(batch_size, seq_len, self.d_model)
94
+ return self.resid_dropout(self.out_proj(attn_output)), new_kv
95
+
96
+ class RMSNorm(nn.Module):
97
+ def __init__(self, d_model, eps=1e-6):
98
+ super().__init__()
99
+ self.weight = nn.Parameter(torch.ones(d_model))
100
+ self.eps = eps
101
+
102
+ def forward(self, x):
103
+ return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) * self.weight
104
+
105
+ class SwiGLU(nn.Module):
106
+ def __init__(self, d_model, expansion_factor=4):
107
+ super().__init__()
108
+ hidden_dim = expansion_factor * d_model
109
+ self.w1 = nn.Linear(d_model, hidden_dim, bias=False)
110
+ self.w2 = nn.Linear(d_model, hidden_dim, bias=False)
111
+ self.w3 = nn.Linear(hidden_dim, d_model, bias=False)
112
+
113
+ def forward(self, x):
114
+ return self.w3(F.silu(self.w1(x)) * self.w2(x))
115
+
116
+ class TransformerBlock(nn.Module):
117
+ def __init__(self, d_model, num_heads, dropout=0.1):
118
+ super().__init__()
119
+ self.norm1 = RMSNorm(d_model)
120
+ self.attention = MultiHeadAttention(d_model, num_heads, dropout)
121
+ self.norm2 = RMSNorm(d_model)
122
+ self.ffn = SwiGLU(d_model)
123
+
124
+ def forward(self, x, mask=None, past_kv=None, use_cache=False):
125
+ attn_out, new_kv = self.attention(self.norm1(x), mask, past_kv, use_cache)
126
+ x = x + attn_out
127
+ x = x + self.ffn(self.norm2(x))
128
+ return x, new_kv
129
+
130
+ @dataclass
131
+ class GPTConfig:
132
+ vocab_size: int = 50257
133
+ d_model: int = 768
134
+ num_heads: int = 12
135
+ num_layers: int = 12
136
+ max_seq_len: int = 512
137
+ dropout: float = 0.1
138
+ embd_dropout: float = 0.1
139
+
140
+ class GPT(nn.Module):
141
+ def __init__(self, config):
142
+ super().__init__()
143
+ self.config = config
144
+ self.token_embedding = nn.Embedding(config.vocab_size, config.d_model)
145
+ self.embd_dropout = nn.Dropout(config.embd_dropout)
146
+ self.layers = nn.ModuleList([TransformerBlock(config.d_model, config.num_heads, config.dropout) for _ in range(config.num_layers)])
147
+ self.final_norm = RMSNorm(config.d_model)
148
+ self.lm_head = nn.Linear(config.d_model, config.vocab_size, bias=False)
149
+ self.token_embedding.weight = self.lm_head.weight
150
+
151
+ def forward(self, input_ids, past_kv_list=None, use_cache=False):
152
+ batch_size, seq_len = input_ids.shape
153
+ x = self.embd_dropout(self.token_embedding(input_ids))
154
+
155
+ if past_kv_list is None:
156
+ mask = create_causal_mask(seq_len, input_ids.device)
157
+ past_kv_list = [None] * len(self.layers)
158
+ else:
159
+ # decode step: single new token attending to cache + itself,
160
+ # every cached position is a valid attend target -> no mask needed
161
+ mask = None
162
+
163
+ new_past_kv_list = []
164
+ for layer, past_kv in zip(self.layers, past_kv_list):
165
+ x, new_kv = layer(x, mask, past_kv, use_cache)
166
+ new_past_kv_list.append(new_kv)
167
+
168
+ logits = self.lm_head(self.final_norm(x))
169
+ if use_cache:
170
+ return logits, new_past_kv_list
171
+ return logits
172
+
173
+ @torch.no_grad()
174
+ def generate(self, input_ids, max_new_tokens, temperature=0.2, stop_token_id=None):
175
+ # KV-cached generation: the prompt is processed once (prefill), then
176
+ # each new token only attends against its own Q against the cached
177
+ # K/V instead of recomputing attention over the whole sequence.
178
+ self.eval()
179
+ if input_ids.shape[1] > self.config.max_seq_len:
180
+ input_ids = input_ids[:, -self.config.max_seq_len:]
181
+
182
+ logits, past_kv = self.forward(input_ids, use_cache=True)
183
+ logits = logits[:, -1, :] / temperature
184
+ probs = F.softmax(logits, dim=-1)
185
+ next_token = torch.multinomial(probs, num_samples=1)
186
+ all_ids = torch.cat([input_ids, next_token], dim=1)
187
+
188
+ cur_len = input_ids.shape[1]
189
+ for _ in range(max_new_tokens - 1):
190
+ if cur_len >= self.config.max_seq_len:
191
+ break # no cache-eviction / sliding window implemented; stop cleanly
192
+
193
+ logits, past_kv = self.forward(next_token, past_kv_list=past_kv, use_cache=True)
194
+ logits = logits[:, -1, :] / temperature
195
+ probs = F.softmax(logits, dim=-1)
196
+ next_token = torch.multinomial(probs, num_samples=1)
197
+
198
+ all_ids = torch.cat([all_ids, next_token], dim=1)
199
+ cur_len += 1
200
+
201
+ if stop_token_id is not None and next_token.item() == stop_token_id:
202
+ break
203
+
204
+ return all_ids
205
+
206
+ # ==========================================
207
+ # 2. GRADIO INTERFACE SETUP (The Pro Way)
208
+ # ==========================================
209
+ print("Starting up PII Firewall...")
210
+ device = torch.device("cpu")
211
+ tokenizer = SimpleTokenizer()
212
+
213
+ try:
214
+ print("Downloading weights from Hugging Face Hub...")
215
+ model_path = hf_hub_download(
216
+ repo_id="nisarg6502/Llama3-150M-PII-Redactor",
217
+ filename="pii_model_epoch_3.safetensors"
218
+ )
219
+
220
+ # Instantiate the architecture
221
+ config = GPTConfig()
222
+ model = GPT(config)
223
+
224
+ print("Loading safetensors into memory...")
225
+ state_dict = load_file(model_path, device=str(device))
226
+ model.load_state_dict(state_dict)
227
+ model.to(device)
228
+ model.eval()
229
+
230
+ model_loaded = True
231
+ print("Model loaded successfully!")
232
+ except Exception as e:
233
+ model_loaded = False
234
+ print(f"Failed to load model: {str(e)}")
235
+
236
+ # ... (The rest of your scrub_text function and Gradio UI code stays exactly the same!) ...
237
+
238
+ def scrub_text(user_input):
239
+ if not model_loaded:
240
+ return f"Error: Could not load pii_model.pt."
241
+
242
+ if not user_input.strip():
243
+ return "Please enter text to redact."
244
+
245
+ # INVISIBLE FORMATTING: The user just types normal text, but we wrap it in the triggers!
246
+ prompt = f"[RAW] {user_input} [REDACTED] "
247
+ input_ids = torch.tensor([tokenizer.encode(prompt)], dtype=torch.long, device=device)
248
+
249
+ # Generate text with low temperature for strict factual output
250
+ output_ids = model.generate(input_ids, max_new_tokens=100, temperature=0.2, stop_token_id=tokenizer.eos_token_id)
251
+ full_output = tokenizer.decode(output_ids[0].tolist())
252
+
253
+ # Extract only the redacted portion to show the user
254
+ if "[REDACTED]" in full_output:
255
+ final_result = full_output.split("[REDACTED]")[-1].replace("<|endoftext|>", "").strip()
256
+ else:
257
+ final_result = full_output
258
+
259
+ return final_result
260
+
261
+ # Build the Web UI
262
+ with gr.Blocks() as demo:
263
+ gr.Markdown("# 🛡️ Local Privacy Firewall (150M Parameters)")
264
+ gr.Markdown("This model was fine-tuned from scratch to detect and redact Personally Identifiable Information (PII) before it ever leaves the local network.")
265
+
266
+ with gr.Row():
267
+ with gr.Column():
268
+ prompt_input = gr.Textbox(lines=4, label="Raw Text (Contains PII)", placeholder="Please send the receipt to michael.scott@dundermifflin.com...")
269
+ submit_btn = gr.Button("Scrub Data", variant="primary")
270
+
271
+ with gr.Column():
272
+ output_text = gr.Textbox(lines=4, label="Safe Text (Redacted)")
273
+
274
+ submit_btn.click(fn=scrub_text, inputs=[prompt_input], outputs=output_text)
275
+
276
+ demo.launch(share=True, theme=gr.themes.Monochrome())