Ephraimmm commited on
Commit
3762381
·
verified ·
1 Parent(s): 40202db

Update handler.py

Browse files
Files changed (1) hide show
  1. handler.py +271 -107
handler.py CHANGED
@@ -1,28 +1,28 @@
 
1
  # handler.py
2
-
3
- from typing import Any, Dict, List, Union
4
-
5
  import torch
6
  from huggingface_inference_toolkit.logging import logger
7
  from unsloth import FastLanguageModel
8
  from peft import PeftModel
9
-
10
  Prompt = Union[str, List[Dict[str, str]]]
11
-
12
-
13
  class EndpointHandler:
14
  """
15
- Custom handler for Hugging Face Inference Endpoints.
16
- Loads an Unsloth 4-bit base and attaches a LoRA adapter.
 
 
 
17
  """
18
-
19
  def __init__(self, model_dir: str, **_: Any):
20
- """
21
- Called once at startup. `model_dir` is the downloaded repo path.
22
- """
23
- logger.info(f"Initializing EndpointHandler with model_dir = {model_dir}")
24
-
25
- # 1) Load Unsloth 4-bit base
26
  base_model_name = "unsloth/gemma-3-4b-it-unsloth-bnb-4bit"
27
  model, tokenizer = FastLanguageModel.from_pretrained(
28
  model_name=base_model_name,
@@ -30,125 +30,289 @@ class EndpointHandler:
30
  dtype=torch.float16,
31
  load_in_4bit=True,
32
  )
33
-
34
- # 2) Attach LoRA adapter
35
  lora_repo = "Ephraimmm/PIDGIN_gemma-3"
36
  model = PeftModel.from_pretrained(model, lora_repo) # default adapter
37
- FastLanguageModel.for_inference(model) # optimize for generation
38
-
39
- # 3) Finalize
40
  self.model = model.eval()
41
  self.tokenizer = tokenizer
42
- self.device = getattr(
43
- self.model,
44
- "device",
45
- torch.device("cuda" if torch.cuda.is_available() else "cpu"),
 
 
 
 
46
  )
47
- logger.info(f"Model ready. Device: {self.device}, EOS: {self.tokenizer.eos_token_id}")
48
-
49
- def _build_input_ids(self, prompt: Prompt):
 
 
 
 
 
 
 
 
 
 
 
 
50
  """
51
- Accepts either a single string, or OpenAI-style chat messages:
52
- [{"role":"system|user|assistant","content":"..."}]
53
- Always injects system prompt for Pidgin English.
54
  """
55
- system_instruction = {
56
- "role": "system",
57
- "content": "Always reply in Pidgin English, answer in context of question."
58
- }
59
-
60
- if isinstance(prompt, list):
61
- # Ensure system prompt is first
62
- if not any(m.get("role") == "system" for m in prompt):
63
- prompt = [system_instruction] + prompt
64
-
65
- if hasattr(self.tokenizer, "apply_chat_template"):
66
- text = self.tokenizer.apply_chat_template(
67
- prompt, tokenize=False, add_generation_prompt=True
68
- )
69
- else:
70
- # Fallback concat
71
- text = "\n".join(
72
- f"{m.get('role','user')}: {m.get('content','')}"
73
- for m in prompt
74
- ) + "\nassistant:"
75
- else:
76
- # If it's just a raw string, prepend system message
77
- text = f"System: {system_instruction['content']}\nUser: {prompt}\nAssistant:"
78
-
79
- return self.tokenizer(text, return_tensors="pt").to(self.device), text
80
-
81
  def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
82
  """
83
- Called per request. Expects {"inputs": <str|messages>, "parameters": {...}}.
 
84
  """
85
- logger.info(f"EndpointHandler received data keys: {list(data.keys())}")
86
-
87
  if "inputs" not in data:
88
- raise ValueError("Request JSON must contain an `inputs` field (str or chat messages).")
89
-
90
  prompt: Prompt = data["inputs"]
91
- params = data.get("parameters", {}) or {}
92
-
93
- max_new_tokens = int(params.get("max_new_tokens", 64))
94
- temperature = float(params.get("temperature", 1))
95
- top_k = int(params.get("top_k", 64))
96
- top_p = float(params.get("top_p", 0.95))
97
- do_sample = bool(params.get("do_sample", True))
98
- repetition_penalty = float(params.get("repetition_penalty", 1.1))
 
99
  eos_token_id = params.get("eos_token_id", self.tokenizer.eos_token_id)
100
-
101
- inputs, rendered_prompt = self._build_input_ids(prompt)
102
-
103
- with torch.no_grad():
104
- output_ids = self.model.generate(
105
- **inputs,
106
- max_new_tokens=max_new_tokens,
107
- temperature=temperature,
108
- top_k=top_k,
109
- top_p=top_p,
110
- do_sample=do_sample,
111
- repetition_penalty=repetition_penalty,
112
- eos_token_id=eos_token_id,
113
- )
114
-
115
- full_text = self.tokenizer.decode(output_ids[0], skip_special_tokens=True)
116
-
117
- # Best-effort continuation extraction
118
- continuation = (
119
- full_text[len(rendered_prompt):]
120
- if isinstance(prompt, str) and full_text.startswith(rendered_prompt)
121
- else full_text
122
  )
123
-
 
 
 
 
 
 
 
 
 
 
 
 
124
  return {
125
- "reply": [full_text],
126
  "usage": {
127
- "prompt_tokens": int(inputs["input_ids"].shape[-1]),
128
- "generated_tokens": int((output_ids.shape[-1] - inputs["input_ids"].shape[-1])),
129
  },
130
  }
131
-
132
-
133
  # Local quick test
134
  if __name__ == "__main__":
135
  h = EndpointHandler(model_dir=".")
136
-
137
- # Test with plain string
138
- resp1 = h({
139
- "inputs": "Explain why sky dey blue.",
140
  "parameters": {"max_new_tokens": 64}
141
  })
142
- print("Response (string):", resp1)
143
-
144
- # Test with chat-style messages
145
- resp2 = h({
146
  "inputs": [
147
- {"role": "user", "content": "How I go cook jollof rice?"}
148
  ],
149
  "parameters": {"max_new_tokens": 64}
150
  })
151
- print("Response (chat):", resp2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
 
154
 
 
1
+ #tope_version
2
  # handler.py
3
+ from typing import Any, Dict, List, Union, Optional
4
+
 
5
  import torch
6
  from huggingface_inference_toolkit.logging import logger
7
  from unsloth import FastLanguageModel
8
  from peft import PeftModel
9
+
10
  Prompt = Union[str, List[Dict[str, str]]]
11
+
12
+
13
  class EndpointHandler:
14
  """
15
+ Hugging Face Inference Endpoint handler.
16
+ - Loads Unsloth 4-bit base
17
+ - Attaches LoRA adapter
18
+ - Uses tokenizer.apply_chat_template like your Gradio app
19
+ - Always injects the Pidgin-English system instruction
20
  """
21
+
22
  def __init__(self, model_dir: str, **_: Any):
23
+ logger.info(f"Initializing with model_dir={model_dir}")
24
+
25
+ # --- Base model (4-bit) ---
 
 
 
26
  base_model_name = "unsloth/gemma-3-4b-it-unsloth-bnb-4bit"
27
  model, tokenizer = FastLanguageModel.from_pretrained(
28
  model_name=base_model_name,
 
30
  dtype=torch.float16,
31
  load_in_4bit=True,
32
  )
33
+
34
+ # --- LoRA adapter ---
35
  lora_repo = "Ephraimmm/PIDGIN_gemma-3"
36
  model = PeftModel.from_pretrained(model, lora_repo) # default adapter
37
+ FastLanguageModel.for_inference(model)
38
+
 
39
  self.model = model.eval()
40
  self.tokenizer = tokenizer
41
+ self.device = getattr(self.model, "device",
42
+ torch.device("cuda" if torch.cuda.is_available() else "cpu"))
43
+ logger.info(f"Device: {self.device} | eos_id: {self.tokenizer.eos_token_id}")
44
+
45
+ # System prompt copied from your Gradio app
46
+ self._system_text = (
47
+ "You are a Nigerian assistant that speaks PIDGIN ENGLISH."
48
+ " when asked how far reply I de o, how you de"
49
  )
50
+
51
+ # -----------------------------
52
+ # Input preparation
53
+ # -----------------------------
54
+ def _ensure_messages(self, prompt: Prompt) -> List[Dict[str, str]]:
55
+ """Normalize to a list of messages and inject the system instruction."""
56
+ sys_msg = {"role": "system", "content": self._system_text}
57
+ if isinstance(prompt, str):
58
+ return [sys_msg, {"role": "user", "content": prompt}]
59
+ if isinstance(prompt, list):
60
+ has_system = any(m.get("role") == "system" for m in prompt)
61
+ return ([sys_msg] + prompt) if not has_system else prompt
62
+ raise ValueError("`inputs` must be a string or a list of {role, content} messages.")
63
+
64
+ def _build_model_inputs(self, prompt: Prompt):
65
  """
66
+ Use tokenizer.apply_chat_template exactly like your Gradio code:
67
+ add_generation_prompt=True, return_tensors='pt', tokenize=True.
 
68
  """
69
+ messages = self._ensure_messages(prompt)
70
+
71
+ # Some tokenizers support content as either str or structured content.
72
+ # We pass OpenAI-style messages; Unsloth/Gemma-3 chat templates handle it.
73
+ model_inputs = self.tokenizer.apply_chat_template(
74
+ messages,
75
+ add_generation_prompt=True,
76
+ return_tensors="pt",
77
+ tokenize=True,
78
+ return_dict=True,
79
+ ).to(self.device)
80
+
81
+ return model_inputs
82
+
83
+ # -----------------------------
84
+ # Generation
85
+ # -----------------------------
 
 
 
 
 
 
 
 
 
86
  def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
87
  """
88
+ Expects: {"inputs": <str|messages>, "parameters": {...}}
89
+ Mirrors your Gradio defaults (temperature=0.1, top_p=1.0, top_k=None, use_cache=False).
90
  """
91
+ logger.info(f"Incoming keys: {list(data.keys())}")
 
92
  if "inputs" not in data:
93
+ raise ValueError("Missing `inputs` in request body.")
94
+
95
  prompt: Prompt = data["inputs"]
96
+ params: Dict[str, Any] = (data.get("parameters") or {})
97
+
98
+ # Map your defaults
99
+ max_new_tokens = int(params.get("max_new_tokens", 256)) # safer default than 9000
100
+ temperature = float(params.get("temperature", 0.1))
101
+ top_p = float(params.get("top_p", 1.0))
102
+ top_k: Optional[int] = params.get("top_k", None)
103
+ use_cache = bool(params.get("use_cache", False))
104
+ repetition_penalty = float(params.get("repetition_penalty", 1.0))
105
  eos_token_id = params.get("eos_token_id", self.tokenizer.eos_token_id)
106
+ pad_token_id = params.get("pad_token_id", getattr(self.tokenizer, "pad_token_id", None))
107
+
108
+ # Prepare inputs via chat template (like your Gradio code)
109
+ inputs = self._build_model_inputs(prompt)
110
+
111
+ gen_kwargs = dict(
112
+ **inputs,
113
+ max_new_tokens=max_new_tokens,
114
+ temperature=temperature,
115
+ top_p=top_p,
116
+ do_sample=True, # sampling on (like your streamer setup)
117
+ use_cache=use_cache,
118
+ repetition_penalty=repetition_penalty,
119
+ eos_token_id=eos_token_id,
 
 
 
 
 
 
 
 
120
  )
121
+ if pad_token_id is not None:
122
+ gen_kwargs["pad_token_id"] = pad_token_id
123
+ if top_k is not None:
124
+ gen_kwargs["top_k"] = int(top_k)
125
+
126
+ with torch.no_grad():
127
+ output_ids = self.model.generate(**gen_kwargs)
128
+
129
+ # Return only the continuation (like what streaming would show)
130
+ input_len = inputs["input_ids"].shape[-1]
131
+ generated_ids = output_ids[0, input_len:]
132
+ reply_text = self.tokenizer.decode(generated_ids, skip_special_tokens=True)
133
+
134
  return {
135
+ "reply": [reply_text],
136
  "usage": {
137
+ "prompt_tokens": int(input_len),
138
+ "generated_tokens": int(generated_ids.shape[-1]),
139
  },
140
  }
141
+
142
+
143
  # Local quick test
144
  if __name__ == "__main__":
145
  h = EndpointHandler(model_dir=".")
146
+
147
+ # Raw string
148
+ out1 = h({
149
+ "inputs": "How far?",
150
  "parameters": {"max_new_tokens": 64}
151
  })
152
+ print(">> STRING:", out1["reply"][0])
153
+
154
+ # Chat-style messages
155
+ out2 = h({
156
  "inputs": [
157
+ {"role": "user", "content": "Explain why sky dey blue."}
158
  ],
159
  "parameters": {"max_new_tokens": 64}
160
  })
161
+ print(">> CHAT:", out2["reply"][0])
162
+
163
+
164
+
165
+ # # handler.py
166
+
167
+ # from typing import Any, Dict, List, Union
168
+
169
+ # import torch
170
+ # from huggingface_inference_toolkit.logging import logger
171
+ # from unsloth import FastLanguageModel
172
+ # from peft import PeftModel
173
+
174
+ # Prompt = Union[str, List[Dict[str, str]]]
175
+
176
+
177
+ # class EndpointHandler:
178
+ # """
179
+ # Custom handler for Hugging Face Inference Endpoints.
180
+ # Loads an Unsloth 4-bit base and attaches a LoRA adapter.
181
+ # """
182
+
183
+ # def __init__(self, model_dir: str, **_: Any):
184
+ # """
185
+ # Called once at startup. `model_dir` is the downloaded repo path.
186
+ # """
187
+ # logger.info(f"Initializing EndpointHandler with model_dir = {model_dir}")
188
+
189
+ # # 1) Load Unsloth 4-bit base
190
+ # base_model_name = "unsloth/gemma-3-4b-it-unsloth-bnb-4bit"
191
+ # model, tokenizer = FastLanguageModel.from_pretrained(
192
+ # model_name=base_model_name,
193
+ # max_seq_length=2048,
194
+ # dtype=torch.float16,
195
+ # load_in_4bit=True,
196
+ # )
197
+
198
+ # # 2) Attach LoRA adapter
199
+ # lora_repo = "Ephraimmm/PIDGIN_gemma-3"
200
+ # model = PeftModel.from_pretrained(model, lora_repo) # default adapter
201
+ # FastLanguageModel.for_inference(model) # optimize for generation
202
+
203
+ # # 3) Finalize
204
+ # self.model = model.eval()
205
+ # self.tokenizer = tokenizer
206
+ # self.device = getattr(
207
+ # self.model,
208
+ # "device",
209
+ # torch.device("cuda" if torch.cuda.is_available() else "cpu"),
210
+ # )
211
+ # logger.info(f"Model ready. Device: {self.device}, EOS: {self.tokenizer.eos_token_id}")
212
+
213
+ # def _build_input_ids(self, prompt: Prompt):
214
+ # """
215
+ # Accepts either a single string, or OpenAI-style chat messages:
216
+ # [{"role":"system|user|assistant","content":"..."}]
217
+ # Always injects system prompt for Pidgin English.
218
+ # """
219
+ # system_instruction = {
220
+ # "role": "system",
221
+ # "content": "Always reply in Pidgin English, answer in context of question."
222
+ # }
223
+
224
+ # if isinstance(prompt, list):
225
+ # # Ensure system prompt is first
226
+ # if not any(m.get("role") == "system" for m in prompt):
227
+ # prompt = [system_instruction] + prompt
228
+
229
+ # if hasattr(self.tokenizer, "apply_chat_template"):
230
+ # text = self.tokenizer.apply_chat_template(
231
+ # prompt, tokenize=False, add_generation_prompt=True
232
+ # )
233
+ # else:
234
+ # # Fallback concat
235
+ # text = "\n".join(
236
+ # f"{m.get('role','user')}: {m.get('content','')}"
237
+ # for m in prompt
238
+ # ) + "\nassistant:"
239
+ # else:
240
+ # # If it's just a raw string, prepend system message
241
+ # text = f"System: {system_instruction['content']}\nUser: {prompt}\nAssistant:"
242
+
243
+ # return self.tokenizer(text, return_tensors="pt").to(self.device), text
244
+
245
+ # def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
246
+ # """
247
+ # Called per request. Expects {"inputs": <str|messages>, "parameters": {...}}.
248
+ # """
249
+ # logger.info(f"EndpointHandler received data keys: {list(data.keys())}")
250
+
251
+ # if "inputs" not in data:
252
+ # raise ValueError("Request JSON must contain an `inputs` field (str or chat messages).")
253
+
254
+ # prompt: Prompt = data["inputs"]
255
+ # params = data.get("parameters", {}) or {}
256
+
257
+ # max_new_tokens = int(params.get("max_new_tokens", 64))
258
+ # temperature = float(params.get("temperature", 1))
259
+ # top_k = int(params.get("top_k", 64))
260
+ # top_p = float(params.get("top_p", 0.95))
261
+ # do_sample = bool(params.get("do_sample", True))
262
+ # repetition_penalty = float(params.get("repetition_penalty", 1.1))
263
+ # eos_token_id = params.get("eos_token_id", self.tokenizer.eos_token_id)
264
+
265
+ # inputs, rendered_prompt = self._build_input_ids(prompt)
266
+
267
+ # with torch.no_grad():
268
+ # output_ids = self.model.generate(
269
+ # **inputs,
270
+ # max_new_tokens=max_new_tokens,
271
+ # temperature=temperature,
272
+ # top_k=top_k,
273
+ # top_p=top_p,
274
+ # do_sample=do_sample,
275
+ # repetition_penalty=repetition_penalty,
276
+ # eos_token_id=eos_token_id,
277
+ # )
278
+
279
+ # full_text = self.tokenizer.decode(output_ids[0], skip_special_tokens=True)
280
+
281
+ # # Best-effort continuation extraction
282
+ # continuation = (
283
+ # full_text[len(rendered_prompt):]
284
+ # if isinstance(prompt, str) and full_text.startswith(rendered_prompt)
285
+ # else full_text
286
+ # )
287
+
288
+ # return {
289
+ # "reply": [full_text],
290
+ # "usage": {
291
+ # "prompt_tokens": int(inputs["input_ids"].shape[-1]),
292
+ # "generated_tokens": int((output_ids.shape[-1] - inputs["input_ids"].shape[-1])),
293
+ # },
294
+ # }
295
+
296
+
297
+ # # Local quick test
298
+ # if __name__ == "__main__":
299
+ # h = EndpointHandler(model_dir=".")
300
+
301
+ # # Test with plain string
302
+ # resp1 = h({
303
+ # "inputs": "Explain why sky dey blue.",
304
+ # "parameters": {"max_new_tokens": 64}
305
+ # })
306
+ # print("Response (string):", resp1)
307
+
308
+ # # Test with chat-style messages
309
+ # resp2 = h({
310
+ # "inputs": [
311
+ # {"role": "user", "content": "How I go cook jollof rice?"}
312
+ # ],
313
+ # "parameters": {"max_new_tokens": 64}
314
+ # })
315
+ # print("Response (chat):", resp2)
316
 
317
 
318