ming commited on
Commit
f293bb9
·
1 Parent(s): 3570bfd

Harden HF V2 streaming + full traceback logging

Browse files

- Normalize tokenizer output and force batch size = 1 for streamer
- Ensure attention_mask exists for all tokenizer paths
- Validate pad_token_id / eos_token_id with proper fallbacks
- Don't move inputs to single device when using device_map='auto'
- Log full exception trace (not just str(e)) for better debugging
- Add daemon=True to generation thread for proper cleanup

Fixes instant failure (~20ms) and empty error string issues during /api/v2/summarize/stream

app/services/hf_streaming_summarizer.py CHANGED
@@ -172,7 +172,7 @@ class HFStreamingSummarizer:
172
  temperature = temperature or settings.hf_temperature
173
  top_p = top_p or settings.hf_top_p
174
 
175
- # --- Build tokenized inputs robustly ---
176
  if "t5" in settings.hf_model_id.lower():
177
  full_prompt = f"summarize: {text}"
178
  inputs_raw = self.tokenizer(full_prompt, return_tensors="pt", max_length=512, truncation=True)
@@ -181,31 +181,58 @@ class HFStreamingSummarizer:
181
  else:
182
  messages = [
183
  {"role": "system", "content": prompt},
184
- {"role": "user", "content": text},
185
  ]
 
186
  if hasattr(self.tokenizer, "apply_chat_template") and self.tokenizer.chat_template:
187
  inputs_raw = self.tokenizer.apply_chat_template(
188
- messages, tokenize=True, add_generation_prompt=True, return_tensors="pt"
 
 
 
189
  )
190
  else:
191
  full_prompt = f"{prompt}\n\n{text}"
192
  inputs_raw = self.tokenizer(full_prompt, return_tensors="pt")
193
 
194
- # Normalize to dict regardless of tokenizer return type
195
  if isinstance(inputs_raw, dict):
196
  inputs = inputs_raw
197
  else:
198
  inputs = {"input_ids": inputs_raw}
199
 
200
- # Move to model device
201
- inputs = {k: v.to(self.model.device) for k, v in inputs.items()}
 
 
202
 
203
- # Enforce batch size == 1 for streamer safety
204
  for k, v in list(inputs.items()):
205
- if v.dim() == 1:
206
- inputs[k] = v.unsqueeze(0) # [seq] -> [1, seq]
207
- elif v.dim() >= 2 and v.size(0) > 1:
208
- inputs[k] = v[:1] # [B, ...] -> [1, ...]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
 
210
  # Create streamer for token-by-token output
211
  streamer = TextIteratorStreamer(
@@ -214,7 +241,6 @@ class HFStreamingSummarizer:
214
  skip_special_tokens=True
215
  )
216
 
217
- # Generation parameters - T5 models use different parameters
218
  gen_kwargs = {
219
  **inputs,
220
  "streamer": streamer,
@@ -222,16 +248,13 @@ class HFStreamingSummarizer:
222
  "do_sample": True,
223
  "temperature": temperature,
224
  "top_p": top_p,
225
- "pad_token_id": self.tokenizer.pad_token_id or self.tokenizer.eos_token_id,
 
226
  }
227
- # Streamer only supports a single sequence
228
  gen_kwargs["num_return_sequences"] = 1
229
 
230
- # Run generation in background thread
231
- generation_thread = threading.Thread(
232
- target=self.model.generate,
233
- kwargs=gen_kwargs
234
- )
235
  generation_thread.start()
236
 
237
  # Stream tokens as they arrive
@@ -262,13 +285,14 @@ class HFStreamingSummarizer:
262
 
263
  logger.info(f"✅ HuggingFace summarization completed in {latency_ms:.2f}ms")
264
 
265
- except Exception as e:
266
- logger.error(f"❌ HuggingFace summarization failed: {e}")
 
267
  # Yield error chunk
268
  yield {
269
  "content": "",
270
  "done": True,
271
- "error": str(e),
272
  }
273
 
274
  async def check_health(self) -> bool:
 
172
  temperature = temperature or settings.hf_temperature
173
  top_p = top_p or settings.hf_top_p
174
 
175
+ # Build tokenized inputs (normalize return types across tokenizers)
176
  if "t5" in settings.hf_model_id.lower():
177
  full_prompt = f"summarize: {text}"
178
  inputs_raw = self.tokenizer(full_prompt, return_tensors="pt", max_length=512, truncation=True)
 
181
  else:
182
  messages = [
183
  {"role": "system", "content": prompt},
184
+ {"role": "user", "content": text}
185
  ]
186
+
187
  if hasattr(self.tokenizer, "apply_chat_template") and self.tokenizer.chat_template:
188
  inputs_raw = self.tokenizer.apply_chat_template(
189
+ messages,
190
+ tokenize=True,
191
+ add_generation_prompt=True,
192
+ return_tensors="pt"
193
  )
194
  else:
195
  full_prompt = f"{prompt}\n\n{text}"
196
  inputs_raw = self.tokenizer(full_prompt, return_tensors="pt")
197
 
198
+ # Normalize to dict regardless of return type
199
  if isinstance(inputs_raw, dict):
200
  inputs = inputs_raw
201
  else:
202
  inputs = {"input_ids": inputs_raw}
203
 
204
+ # Ensure attention_mask (some tokenizers/models don't return it by default)
205
+ if "attention_mask" not in inputs and "input_ids" in inputs:
206
+ import torch
207
+ inputs["attention_mask"] = torch.ones_like(inputs["input_ids"])
208
 
209
+ # Enforce batch size == 1 for streamer
210
  for k, v in list(inputs.items()):
211
+ if hasattr(v, "dim"):
212
+ if v.dim() == 1:
213
+ inputs[k] = v.unsqueeze(0) # [seq] -> [1, seq]
214
+ elif v.dim() >= 2 and v.size(0) > 1:
215
+ inputs[k] = v[:1] # [B, ...] -> [1, ...]
216
+
217
+ # IMPORTANT: with device_map="auto", let HF move tensors as needed.
218
+ # If you are *not* using device_map="auto", uncomment the line below:
219
+ # inputs = {k: v.to(self.model.device) for k, v in inputs.items()}
220
+
221
+ # Validate pad/eos ids
222
+ pad_id = self.tokenizer.pad_token_id
223
+ eos_id = self.tokenizer.eos_token_id
224
+ if pad_id is None and eos_id is not None:
225
+ pad_id = eos_id
226
+ elif pad_id is None and eos_id is None:
227
+ # Last resort: set pad to 0 to avoid None in generate()
228
+ pad_id = 0
229
+
230
+ # Helpful debug: log shapes once
231
+ try:
232
+ _shapes = {k: tuple(v.shape) for k, v in inputs.items() if hasattr(v, "shape")}
233
+ logger.debug(f"HF V2 inputs shapes: {_shapes}, pad_id={pad_id}, eos_id={eos_id}")
234
+ except Exception:
235
+ pass
236
 
237
  # Create streamer for token-by-token output
238
  streamer = TextIteratorStreamer(
 
241
  skip_special_tokens=True
242
  )
243
 
 
244
  gen_kwargs = {
245
  **inputs,
246
  "streamer": streamer,
 
248
  "do_sample": True,
249
  "temperature": temperature,
250
  "top_p": top_p,
251
+ "pad_token_id": pad_id,
252
+ "eos_token_id": eos_id,
253
  }
254
+ # Streamer requires single sequence
255
  gen_kwargs["num_return_sequences"] = 1
256
 
257
+ generation_thread = threading.Thread(target=self.model.generate, kwargs=gen_kwargs, daemon=True)
 
 
 
 
258
  generation_thread.start()
259
 
260
  # Stream tokens as they arrive
 
285
 
286
  logger.info(f"✅ HuggingFace summarization completed in {latency_ms:.2f}ms")
287
 
288
+ except Exception:
289
+ # Capture full traceback to aid debugging (the message may be empty otherwise)
290
+ logger.exception("❌ HuggingFace summarization failed with an exception")
291
  # Yield error chunk
292
  yield {
293
  "content": "",
294
  "done": True,
295
+ "error": "HF summarization failed. See server logs for traceback.",
296
  }
297
 
298
  async def check_health(self) -> bool: