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

Update handler.py

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