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

Update handler.py

Browse files
Files changed (1) hide show
  1. handler.py +245 -93
handler.py CHANGED
@@ -3,195 +3,347 @@
3
  from typing import Any, Dict, List, Union
4
 
5
  import torch
6
-
7
  from huggingface_inference_toolkit.logging import logger
8
-
9
  from unsloth import FastLanguageModel
10
-
11
  from peft import PeftModel
12
-
13
  Prompt = Union[str, List[Dict[str, str]]]
14
-
15
-
16
- class EndpointHandler:
17
 
18
- """
19
 
 
 
20
  Custom handler for Hugging Face Inference Endpoints.
21
-
22
  Loads an Unsloth 4-bit base and attaches a LoRA adapter.
23
-
24
  """
25
-
26
  def __init__(self, model_dir: str, **_: Any):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
 
 
 
 
 
28
  """
 
 
 
 
29
 
30
- Called once at startup. `model_dir` is the downloaded repo path.
 
 
 
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  """
 
 
 
33
 
34
- logger.info(f"Initializing EndpointHandler with model_dir = {model_dir}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
- # 1) Load Unsloth 4-bit base
 
 
 
37
 
38
- base_model_name = "unsloth/gemma-3-4b-it-unsloth-bnb-4bit"
39
 
40
- model, tokenizer = FastLanguageModel.from_pretrained(
41
 
42
- model_name = base_model_name,
43
 
44
- max_seq_length = 2048,
 
 
45
 
46
- dtype = torch.float16,
47
 
48
- load_in_4bit = True,
49
 
50
- )
 
 
51
 
52
- # 2) Attach LoRA adapter
53
 
54
- lora_repo = "Ephraimmm/PIDGIN_gemma-3"
 
 
55
 
56
- model = PeftModel.from_pretrained(model, lora_repo) # use default adapter name
57
 
58
- FastLanguageModel.for_inference(model) # optimize for generation
 
 
 
 
 
 
59
 
60
- # 3) Finalize
61
 
62
- self.model = model.eval()
63
 
64
- self.tokenizer = tokenizer
65
 
66
- self.device = getattr(self.model, "device", torch.device("cuda" if torch.cuda.is_available() else "cpu"))
 
 
67
 
68
- logger.info(f"Model ready. Device: {self.device}, EOS: {self.tokenizer.eos_token_id}")
 
 
 
 
 
 
69
 
70
- def _build_input_ids(self, prompt: Prompt):
71
 
72
- """
73
 
74
- Accepts either a single string, or OpenAI-style chat messages:
75
 
76
- [{"role":"system|user|assistant","content":"..."}]
77
 
78
- """
79
 
80
- if isinstance(prompt, list):
81
 
82
- # If tokenizer has a chat template, use it.
83
 
84
- if hasattr(self.tokenizer, "apply_chat_template"):
85
 
86
- text = self.tokenizer.apply_chat_template(
87
 
88
- prompt, tokenize=False, add_generation_prompt=True
89
 
90
- )
91
 
92
- else:
93
 
94
- # Fallback: naive concat
95
 
96
- text = "\n".join(f"{m.get('role','user')}: {m.get('content','')}" for m in prompt) + "\nassistant:"
97
 
98
- else:
99
 
100
- text = prompt
101
 
102
- return self.tokenizer(text, return_tensors="pt").to(self.device), text
103
 
104
- def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
105
 
106
- """
107
 
108
- Called per request. Expects {"inputs": <str|messages>, "parameters": {...}}.
109
 
110
- """
111
 
112
- logger.info(f"EndpointHandler received data keys: {list(data.keys())}")
113
 
114
- if "inputs" not in data:
115
 
116
- raise ValueError("Request JSON must contain an `inputs` field (str or chat messages).")
117
 
118
- prompt: Prompt = data["inputs"]
119
 
120
- params = data.get("parameters", {}) or {}
121
 
122
- max_new_tokens = int(params.get("max_new_tokens", 64))
123
 
124
- temperature = float(params.get("temperature", 1))
125
 
126
- top_k = int(params.get("top_k", 64))
127
 
128
- top_p = float(params.get("top_p", 0.95))
129
 
130
- do_sample = bool(params.get("do_sample", True))
131
 
132
- repetition_penalty = float(params.get("repetition_penalty", 1.1))
133
 
134
- eos_token_id = params.get("eos_token_id", self.tokenizer.eos_token_id)
135
 
136
- inputs, rendered_prompt = self._build_input_ids(prompt)
137
 
138
- with torch.no_grad():
139
 
140
- output_ids = self.model.generate(
141
 
142
- **inputs,
143
 
144
- max_new_tokens=max_new_tokens,
145
 
146
- temperature=temperature,
147
 
148
- top_k=top_k,
149
 
150
- top_p=top_p,
151
 
152
- do_sample=do_sample,
153
 
154
- repetition_penalty=repetition_penalty,
155
 
156
- eos_token_id=eos_token_id,
157
 
158
- )
159
 
160
- full_text = self.tokenizer.decode(output_ids[0], skip_special_tokens=True)
161
 
162
- # Best-effort continuation extraction
163
 
164
- continuation = full_text[len(rendered_prompt):] if isinstance(prompt, str) and full_text.startswith(rendered_prompt) else full_text
165
 
166
- return {
167
 
168
- "reply": [full_text],
169
 
170
- # "continuations": [continuation],
171
 
172
- "usage": {
173
 
174
- "prompt_tokens": int(inputs["input_ids"].shape[-1]),
175
 
176
- "generated_tokens": int((output_ids.shape[-1] - inputs["input_ids"].shape[-1])),
177
 
178
- },}
179
 
180
 
181
- # Local quick test
182
 
183
- if __name__ == "__main__":
184
 
185
- h = EndpointHandler(model_dir=".")
186
 
187
- resp = h({
188
 
189
- "inputs": "Write in Nigerian Pidgin: How you dey?",
190
 
191
- "parameters": {"max_new_tokens": 64}
192
 
193
- })
194
 
195
- print("Response:", resp)
196
 
197
 
 
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,
29
+ max_seq_length=2048,
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
+
155
+ # from typing import Any, Dict, List, Union
156
+
157
+ # import torch
158
+
159
+ # from huggingface_inference_toolkit.logging import logger
160
+
161
+ # from unsloth import FastLanguageModel
162
+
163
+ # from peft import PeftModel
164
 
165
+ # Prompt = Union[str, List[Dict[str, str]]]
166
+
167
+
168
+ # class EndpointHandler:
169
 
170
+ # """
171
 
172
+ # Custom handler for Hugging Face Inference Endpoints.
173
 
174
+ # Loads an Unsloth 4-bit base and attaches a LoRA adapter.
175
 
176
+ # """
177
+
178
+ # def __init__(self, model_dir: str, **_: Any):
179
 
180
+ # """
181
 
182
+ # Called once at startup. `model_dir` is the downloaded repo path.
183
 
184
+ # """
185
+
186
+ # logger.info(f"Initializing EndpointHandler with model_dir = {model_dir}")
187
 
188
+ # # 1) Load Unsloth 4-bit base
189
 
190
+ # base_model_name = "unsloth/gemma-3-4b-it-unsloth-bnb-4bit"
191
+
192
+ # model, tokenizer = FastLanguageModel.from_pretrained(
193
 
194
+ # model_name = base_model_name,
195
 
196
+ # max_seq_length = 2048,
197
+
198
+ # dtype = torch.float16,
199
+
200
+ # load_in_4bit = True,
201
+
202
+ # )
203
 
204
+ # # 2) Attach LoRA adapter
205
 
206
+ # lora_repo = "Ephraimmm/PIDGIN_gemma-3"
207
 
208
+ # model = PeftModel.from_pretrained(model, lora_repo) # use default adapter name
209
 
210
+ # FastLanguageModel.for_inference(model) # optimize for generation
211
+
212
+ # # 3) Finalize
213
 
214
+ # self.model = model.eval()
215
+
216
+ # self.tokenizer = tokenizer
217
+
218
+ # self.device = getattr(self.model, "device", torch.device("cuda" if torch.cuda.is_available() else "cpu"))
219
+
220
+ # logger.info(f"Model ready. Device: {self.device}, EOS: {self.tokenizer.eos_token_id}")
221
 
222
+ # def _build_input_ids(self, prompt: Prompt):
223
 
224
+ # """
225
 
226
+ # Accepts either a single string, or OpenAI-style chat messages:
227
 
228
+ # [{"role":"system|user|assistant","content":"..."}]
229
 
230
+ # """
231
 
232
+ # if isinstance(prompt, list):
233
 
234
+ # # If tokenizer has a chat template, use it.
235
 
236
+ # if hasattr(self.tokenizer, "apply_chat_template"):
237
 
238
+ # text = self.tokenizer.apply_chat_template(
239
 
240
+ # prompt, tokenize=False, add_generation_prompt=True
241
 
242
+ # )
243
 
244
+ # else:
245
 
246
+ # # Fallback: naive concat
247
 
248
+ # text = "\n".join(f"{m.get('role','user')}: {m.get('content','')}" for m in prompt) + "\nassistant:"
249
 
250
+ # else:
251
 
252
+ # text = prompt
253
 
254
+ # return self.tokenizer(text, return_tensors="pt").to(self.device), text
255
 
256
+ # def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
257
 
258
+ # """
259
 
260
+ # Called per request. Expects {"inputs": <str|messages>, "parameters": {...}}.
261
 
262
+ # """
263
 
264
+ # logger.info(f"EndpointHandler received data keys: {list(data.keys())}")
265
 
266
+ # if "inputs" not in data:
267
 
268
+ # raise ValueError("Request JSON must contain an `inputs` field (str or chat messages).")
269
 
270
+ # prompt: Prompt = data["inputs"]
271
 
272
+ # params = data.get("parameters", {}) or {}
273
 
274
+ # max_new_tokens = int(params.get("max_new_tokens", 64))
275
 
276
+ # temperature = float(params.get("temperature", 1))
277
 
278
+ # top_k = int(params.get("top_k", 64))
279
 
280
+ # top_p = float(params.get("top_p", 0.95))
281
 
282
+ # do_sample = bool(params.get("do_sample", True))
283
 
284
+ # repetition_penalty = float(params.get("repetition_penalty", 1.1))
285
 
286
+ # eos_token_id = params.get("eos_token_id", self.tokenizer.eos_token_id)
287
 
288
+ # inputs, rendered_prompt = self._build_input_ids(prompt)
289
 
290
+ # with torch.no_grad():
291
 
292
+ # output_ids = self.model.generate(
293
 
294
+ # **inputs,
295
 
296
+ # max_new_tokens=max_new_tokens,
297
 
298
+ # temperature=temperature,
299
 
300
+ # top_k=top_k,
301
 
302
+ # top_p=top_p,
303
 
304
+ # do_sample=do_sample,
305
 
306
+ # repetition_penalty=repetition_penalty,
307
 
308
+ # eos_token_id=eos_token_id,
309
 
310
+ # )
311
 
312
+ # full_text = self.tokenizer.decode(output_ids[0], skip_special_tokens=True)
313
 
314
+ # # Best-effort continuation extraction
315
 
316
+ # continuation = full_text[len(rendered_prompt):] if isinstance(prompt, str) and full_text.startswith(rendered_prompt) else full_text
317
 
318
+ # return {
319
 
320
+ # "reply": [full_text],
321
 
322
+ # # "continuations": [continuation],
323
 
324
+ # "usage": {
325
 
326
+ # "prompt_tokens": int(inputs["input_ids"].shape[-1]),
327
 
328
+ # "generated_tokens": int((output_ids.shape[-1] - inputs["input_ids"].shape[-1])),
329
 
330
+ # },}
331
 
332
 
333
+ # # Local quick test
334
 
335
+ # if __name__ == "__main__":
336
 
337
+ # h = EndpointHandler(model_dir=".")
338
 
339
+ # resp = h({
340
 
341
+ # "inputs": "Write in Nigerian Pidgin: How you dey?",
342
 
343
+ # "parameters": {"max_new_tokens": 64}
344
 
345
+ # })
346
 
347
+ # print("Response:", resp)
348
 
349