Mehdi commited on
Commit
12c4c0f
·
1 Parent(s): 12edc45

fix: infinite spinner on question gen, 4-bit quant for low VRAM

Browse files

- new_question returns error text in question_box (was empty string)
so JS polling can detect completion even on failure
- JS polling exits spinner on error instead of waiting forever
- @spaces.GPU timeout raised 60s -> 180s
- LLM loads in 4-bit (bitsandbytes) when VRAM < 17GB, bfloat16 otherwise
- Pre-load libnvJitLink.so.13 so bitsandbytes works locally with cu130
- Add bitsandbytes to requirements.txt

Files changed (3) hide show
  1. app.py +17 -4
  2. model/llm.py +36 -2
  3. requirements.txt +1 -0
app.py CHANGED
@@ -267,17 +267,18 @@ def load_pdf(pdf_file, language):
267
  return chunks, s["loaded"](len(chunks)), 0, 0
268
 
269
 
270
- @spaces.GPU(duration=60)
271
  def new_question(chunks, language):
272
  s = UI[language]
273
  if not chunks:
274
- return "", "", s["no_pdf"]
275
  try:
276
  chunk = random.choice(chunks)
277
  question = generate_question(chunk, language=language)
278
  return question, chunk, ""
279
  except Exception as e:
280
- return "", "", f"Erreur : {e}"
 
281
 
282
 
283
  @spaces.GPU(duration=120)
@@ -526,7 +527,19 @@ BRIDGE_JS = """() => {
526
 
527
  if (S.waitingQ) {
528
  const qEl = document.querySelector('#hidden-question-output textarea');
529
- if (qEl && qEl.value && qEl.value !== S.prevQuestion) showQuestion(qEl.value);
 
 
 
 
 
 
 
 
 
 
 
 
530
  }
531
 
532
  if (S.waitingFB) {
 
267
  return chunks, s["loaded"](len(chunks)), 0, 0
268
 
269
 
270
+ @spaces.GPU(duration=180)
271
  def new_question(chunks, language):
272
  s = UI[language]
273
  if not chunks:
274
+ return s["no_pdf"], "", s["no_pdf"]
275
  try:
276
  chunk = random.choice(chunks)
277
  question = generate_question(chunk, language=language)
278
  return question, chunk, ""
279
  except Exception as e:
280
+ err = f"Erreur : {e}"
281
+ return err, "", err
282
 
283
 
284
  @spaces.GPU(duration=120)
 
527
 
528
  if (S.waitingQ) {
529
  const qEl = document.querySelector('#hidden-question-output textarea');
530
+ if (qEl && qEl.value && qEl.value !== S.prevQuestion) {
531
+ const val = qEl.value;
532
+ const isErr = val.startsWith('Erreur') || val.startsWith('⚠️');
533
+ if (isErr) {
534
+ S.waitingQ = false;
535
+ const ql=$('q-loading'), nq=$('new-q-btn');
536
+ if(ql) ql.classList.add('hidden');
537
+ if(nq) nq.disabled=false;
538
+ applyStatus(val);
539
+ } else {
540
+ showQuestion(val);
541
+ }
542
+ }
543
  }
544
 
545
  if (S.waitingFB) {
model/llm.py CHANGED
@@ -22,13 +22,39 @@ Public API:
22
  """
23
 
24
  import os
 
25
  import torch
26
  from functools import lru_cache
27
- from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
28
 
29
  DEFAULT_MODEL_ID = "openbmb/MiniCPM4-8B"
30
  DEFAULT_MAX_NEW_TOKENS = 512
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  class LLM:
34
  """Thin wrapper around a HuggingFace text-generation pipeline."""
@@ -36,9 +62,17 @@ class LLM:
36
  def __init__(self, model_id: str, device: str):
37
  self.model_id = model_id
38
  self._tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
 
 
 
 
 
 
 
39
  model = AutoModelForCausalLM.from_pretrained(
40
  model_id,
41
- dtype=torch.bfloat16,
 
42
  device_map=device,
43
  trust_remote_code=True,
44
  )
 
22
  """
23
 
24
  import os
25
+ import ctypes
26
  import torch
27
  from functools import lru_cache
28
+ from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline, BitsAndBytesConfig
29
 
30
  DEFAULT_MODEL_ID = "openbmb/MiniCPM4-8B"
31
  DEFAULT_MAX_NEW_TOKENS = 512
32
 
33
+ # Pre-load libnvJitLink.so.13 bundled with the nvidia-cu13 wheel so that
34
+ # bitsandbytes can find it when it calls dlopen internally.
35
+ def _preload_nvjitlink() -> None:
36
+ try:
37
+ import site
38
+ for sp in site.getsitepackages():
39
+ candidate = os.path.join(sp, "nvidia", "cu13", "lib", "libnvJitLink.so.13")
40
+ if os.path.exists(candidate):
41
+ ctypes.CDLL(candidate)
42
+ return
43
+ except Exception:
44
+ pass
45
+
46
+ _preload_nvjitlink()
47
+
48
+
49
+ def _build_quantization_config(vram_gb: float):
50
+ if vram_gb < 17:
51
+ try:
52
+ import bitsandbytes # noqa: F401
53
+ return BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16)
54
+ except Exception:
55
+ pass
56
+ return None
57
+
58
 
59
  class LLM:
60
  """Thin wrapper around a HuggingFace text-generation pipeline."""
 
62
  def __init__(self, model_id: str, device: str):
63
  self.model_id = model_id
64
  self._tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
65
+
66
+ vram_gb = 0.0
67
+ if torch.cuda.is_available():
68
+ vram_gb = torch.cuda.get_device_properties(0).total_memory / 1024**3
69
+ quant_cfg = _build_quantization_config(vram_gb)
70
+ print(f"[LLM] VRAM={vram_gb:.1f}GB — {'4-bit quant' if quant_cfg else 'bfloat16'}")
71
+
72
  model = AutoModelForCausalLM.from_pretrained(
73
  model_id,
74
+ quantization_config=quant_cfg,
75
+ torch_dtype=torch.bfloat16 if quant_cfg is None else None,
76
  device_map=device,
77
  trust_remote_code=True,
78
  )
requirements.txt CHANGED
@@ -16,3 +16,4 @@ transformers==4.57.1
16
  accelerate>=0.31.0
17
  sentencepiece>=0.2.0
18
  protobuf>=4.25.0
 
 
16
  accelerate>=0.31.0
17
  sentencepiece>=0.2.0
18
  protobuf>=4.25.0
19
+ bitsandbytes>=0.43.0