Kasualdad commited on
Commit
de794a7
·
1 Parent(s): 53a83b7

Switch inference to transformers + bnb-4bit + LoRA for ZeroGPU

Browse files

llama.cpp cannot use ZeroGPU's PyTorch-only CUDA emulation. Replace it
with the QLoRA training-time config: pre-quantized 4-bit base
(unsloth/qwen2.5-coder-14b-instruct-bnb-4bit) + LoRA adapter
(build-small-hackathon/lfed-qwen2.5-coder-14b-sql-lora).

- model_inference.py: TransformersLLM wrapper, llama.cpp-compatible
call/response schema, TextIteratorStreamer streaming
- app.py: spaces.GPU(duration=120)
- requirements.txt: drop llama-cpp-python; add torch/transformers/
peft/bitsandbytes/accelerate/hf_transfer

Files changed (4) hide show
  1. app.py +6 -3
  2. model_inference.py +147 -178
  3. requirements.txt +6 -1
  4. tests/test_model_inference.py +4 -6
app.py CHANGED
@@ -3,7 +3,7 @@ app.py — Kasualdad LFED: Local-First Education Data Analytics.
3
 
4
  Thin Gradio controller. All logic lives in:
5
  - prompts.py (system prompt, schema docs, few-shot examples)
6
- - model_inference.py (llama.cpp wrapper, SQL generation + streaming)
7
  - data_engine.py (DuckDB lifecycle, schema seeding, execution guard)
8
  """
9
 
@@ -12,7 +12,7 @@ import gradio as gr
12
  # spaces.GPU is only available on HF Spaces — use a no-op locally
13
  try:
14
  import spaces
15
- _gpu_decorator = spaces.GPU
16
  except ImportError:
17
  _gpu_decorator = lambda fn: fn # no-op for local dev
18
 
@@ -37,7 +37,10 @@ if not _pq_found:
37
  _pq_out = _parquet_dirs[0] if _parquet_dirs[0].exists() else _parquet_dirs[1]
38
  export_parquet(_pq_out)
39
 
40
- print("🦙 Loading model...")
 
 
 
41
  llm = load_model()
42
  print("✅ Ready.")
43
 
 
3
 
4
  Thin Gradio controller. All logic lives in:
5
  - prompts.py (system prompt, schema docs, few-shot examples)
6
+ - model_inference.py (transformers + LoRA wrapper, SQL generation + streaming)
7
  - data_engine.py (DuckDB lifecycle, schema seeding, execution guard)
8
  """
9
 
 
12
  # spaces.GPU is only available on HF Spaces — use a no-op locally
13
  try:
14
  import spaces
15
+ _gpu_decorator = spaces.GPU(duration=120)
16
  except ImportError:
17
  _gpu_decorator = lambda fn: fn # no-op for local dev
18
 
 
37
  _pq_out = _parquet_dirs[0] if _parquet_dirs[0].exists() else _parquet_dirs[1]
38
  export_parquet(_pq_out)
39
 
40
+ # Load the model at startup. On ZeroGPU, bitsandbytes/transformers go through
41
+ # PyTorch, so the model loads onto the emulated CUDA device here and runs on
42
+ # the real GPU inside @spaces.GPU. (llama.cpp could not do this.)
43
+ print("🤖 Loading model (Qwen2.5-Coder-14B bnb-4bit + LoRA)...")
44
  llm = load_model()
45
  print("✅ Ready.")
46
 
model_inference.py CHANGED
@@ -1,11 +1,25 @@
1
  """
2
- model_inference.py — llama.cpp wrapper for local SQL generation.
 
 
 
 
 
 
 
 
 
3
 
4
  Handles:
5
- - Model loading (lazy, cached) from local GGUF
6
  - generate_sql(): prompt → raw text → cleaned SQL
7
- - generate_sql_streaming(): yields tokens for Gradio stream=True
8
- - Model download from Hugging Face Hub (fallback if not cached locally)
 
 
 
 
 
9
  """
10
 
11
  from __future__ import annotations
@@ -13,156 +27,146 @@ from __future__ import annotations
13
  import os
14
  import time
15
  import threading
16
- from pathlib import Path
17
  from typing import Generator, Optional
18
 
19
- os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1"
20
-
21
- # ── CUDA preload ────────────────────────────────────────────────────────
22
- # llama-cpp-python's libllama.so (CUDA build) needs libcudart, libcublas,
23
- # libcublasLt, and possibly others at dlopen time. Setting LD_LIBRARY_PATH
24
- # via os.environ does NOT help — the dynamic linker reads it once at process
25
- # start. Instead we preload every lib*.so* we can find (system CUDA paths +
26
- # pip-installed nvidia-*-cu12 packages) with RTLD_GLOBAL so they're already
27
- # resident when libllama.so loads.
28
-
29
- import ctypes
30
- import sys
31
-
32
- _CUDA_PRELOADED = 0
33
-
34
-
35
- def _preload_dir(lib_dir: str) -> int:
36
- """Preload every lib*.so* in *lib_dir* with RTLD_GLOBAL. Returns count."""
37
- n = 0
38
- if not os.path.isdir(lib_dir):
39
- return n
40
- for _f in sorted(os.listdir(lib_dir)):
41
- if not (_f.startswith("lib") and ".so" in _f):
42
- continue
43
- _full = os.path.join(lib_dir, _f)
44
- if not os.path.isfile(_full):
45
- continue
46
- try:
47
- ctypes.CDLL(_full, mode=ctypes.RTLD_GLOBAL)
48
- n += 1
49
- except OSError:
50
- pass
51
- return n
52
-
53
-
54
- # 1. System CUDA installs
55
- for _p in [
56
- "/usr/local/cuda/lib64",
57
- "/usr/local/cuda-12.1/lib64",
58
- "/usr/local/cuda-12.4/lib64",
59
- ]:
60
- _CUDA_PRELOADED += _preload_dir(_p)
61
-
62
- # 2. Pip-installed nvidia-*-cu12 packages (common on HF Spaces)
63
- for _sp in sys.path:
64
- _nvidia_root = os.path.join(_sp, "nvidia")
65
- if not os.path.isdir(_nvidia_root):
66
- continue
67
- for _pkg in os.listdir(_nvidia_root):
68
- _CUDA_PRELOADED += _preload_dir(os.path.join(_nvidia_root, _pkg, "lib"))
69
-
70
- if _CUDA_PRELOADED:
71
- print(f"🔧 Preloaded {_CUDA_PRELOADED} CUDA shared librar{'y' if _CUDA_PRELOADED == 1 else 'ies'}")
72
- else:
73
- print("ℹ️ No CUDA libraries found — if GPU is available install CUDA 12 or nvidia-*-cu12 pip packages.")
74
-
75
- # ── Imports ────────────────────────────────────────────────────────────
76
-
77
- from huggingface_hub import hf_hub_download
78
-
79
- try:
80
- from llama_cpp import Llama
81
- except (OSError, RuntimeError) as e:
82
- _msg = str(e)
83
- if any(_kw in _msg.lower() for _kw in ("libcuda", "libcudart", "libcublas", "libcusparse", "libnv", "cudart", "cublas")):
84
- raise RuntimeError(
85
- "Failed to load llama-cpp-python — CUDA runtime not found.\n"
86
- "If this is a CPU-only machine, install the CPU wheel:\n"
87
- " pip uninstall llama-cpp-python -y && pip install llama-cpp-python\n"
88
- f"Original error: {e}"
89
- ) from e
90
- raise
91
 
92
  from prompts import build_prompt
93
 
94
 
95
  # ── Model configuration ────────────────────────────────────────────────
96
 
97
- # Cache: try 14B (production), then 7B (dev), then download fallback
98
- _LOCAL_14B_PATH = os.path.expanduser(
99
- "~/.cache/huggingface/hub/models--build-small-hackathon--lfed-qwen2.5-coder-14b-sql-gguf"
100
- "/snapshots/a46c7209055a9447b8e3a85a714d3c662d8c6149/lfed-qwen2.5-coder-14b-sql-Q4_K_M.gguf"
101
- )
102
- _LOCAL_7B_PATH = os.path.expanduser(
103
- "~/.cache/huggingface/hub/models--build-small-hackathon--lfed-qwen2.5-coder-7b-sql-gguf"
104
- "/snapshots/2a171f8745823d1b8c03e7467d046bcfedea7fa0/lfed-qwen2.5-coder-7b-sql-Q4_K_M.gguf"
105
- )
106
-
107
- # Cascade: 14B → 7B → old explicit path
108
- LOCAL_MODEL_PATH = (
109
- _LOCAL_14B_PATH if os.path.exists(_LOCAL_14B_PATH)
110
- else _LOCAL_7B_PATH if os.path.exists(_LOCAL_7B_PATH)
111
- else "/tmp/lfed-models/qwen/Qwen2.5-Coder-7B-Instruct.Q4_K_M.gguf"
112
- )
113
-
114
- # HF Hub backup (download if nothing cached locally)
115
- HF_REPO_ID = "build-small-hackathon/lfed-qwen2.5-coder-14b-sql-gguf"
116
- HF_MODEL_FILE = "lfed-qwen2.5-coder-14b-sql-Q4_K_M.gguf"
117
-
118
- # Inference defaults
119
- _ZERO_GPU = os.environ.get("SPACES_ZERO_GPU", "").lower() == "true"
120
-
121
- DEFAULT_N_CTX = 4096
122
- DEFAULT_N_THREADS = 4 if _ZERO_GPU else 2 # Zero GPU: fewer threads needed with GPU offload
123
- DEFAULT_N_GPU_LAYERS = -1 # Always GPU offload; Zero GPU emulation handles module-level CUDA
124
  DEFAULT_MAX_TOKENS = 256
125
  DEFAULT_TEMPERATURE = 0.0
126
  STOP_SEQUENCES = ["\n\n", "Question:", "User:", "<|im_end|>", "<|im_start|>"]
127
 
128
  # Thread-safe model cache
129
  _lock = threading.Lock()
130
- _llm: Optional[Llama] = None
131
 
132
 
133
- # ── Model loading ──────────────────────────────────────────────────────
134
 
135
- def _resolve_model_path() -> str:
136
- """Find the GGUF model local cache first, then download from Hub."""
137
- local = Path(LOCAL_MODEL_PATH)
138
- if local.exists():
139
- print(f"📦 Using cached model: {local}")
140
- return str(local)
141
-
142
- print(f"⬇️ Local model not found, downloading from HF Hub: {HF_REPO_ID}/{HF_MODEL_FILE}")
143
- path = hf_hub_download(repo_id=HF_REPO_ID, filename=HF_MODEL_FILE)
144
- print(f"✅ Downloaded to: {path}")
145
- return path
146
-
147
-
148
- def load_model(
149
- model_path: str | None = None,
150
- n_ctx: int = DEFAULT_N_CTX,
151
- n_threads: int = DEFAULT_N_THREADS,
152
- verbose: bool = False,
153
- ) -> Llama:
154
- """
155
- Load the llama.cpp model. Thread-safe; caches the model globally.
156
 
157
- Args:
158
- model_path: Path to GGUF file. Auto-resolves if None.
159
- n_ctx: Context window size.
160
- n_threads: CPU threads for inference.
161
- verbose: Print llama.cpp diagnostics.
162
 
163
- Returns:
164
- Loaded Llama instance.
165
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  global _llm
167
 
168
  if _llm is not None:
@@ -172,29 +176,13 @@ def load_model(
172
  if _llm is not None: # Double-check after acquiring lock
173
  return _llm
174
 
175
- if model_path is None:
176
- model_path = _resolve_model_path()
177
-
178
  t0 = time.time()
179
- print(f"🦙 Loading model: {model_path}")
180
-
181
- # Zero GPU uses CUDA emulation at module level (startup) and real GPU
182
- # inside @spaces.GPU. Loading with GPU layers at startup is the
183
- # recommended pattern — Zero GPU handles the context switch.
184
- n_gpu = DEFAULT_N_GPU_LAYERS
185
- _llm = Llama(
186
- model_path=model_path,
187
- n_ctx=n_ctx,
188
- n_threads=n_threads,
189
- n_gpu_layers=n_gpu,
190
- verbose=verbose,
191
- )
192
-
193
- print(f"✅ Model loaded in {time.time() - t0:.1f}s (n_gpu_layers={n_gpu}, threads={n_threads})")
194
  return _llm
195
 
196
 
197
- def get_model() -> Llama | None:
198
  """Return the cached model, or None if not loaded."""
199
  return _llm
200
 
@@ -203,7 +191,7 @@ def get_model() -> Llama | None:
203
 
204
  def generate_sql(
205
  user_question: str,
206
- llm: Llama | None = None,
207
  max_tokens: int = DEFAULT_MAX_TOKENS,
208
  temperature: float = DEFAULT_TEMPERATURE,
209
  schema: dict | None = None,
@@ -211,15 +199,8 @@ def generate_sql(
211
  """
212
  Generate SQL from a natural-language question.
213
 
214
- Args:
215
- user_question: The admin's question in plain English.
216
- llm: Llama instance (uses cached global if None).
217
- max_tokens: Max tokens to generate.
218
- temperature: Sampling temperature (0 = deterministic).
219
- schema: Table schema dict for prompt context.
220
-
221
  Returns:
222
- (raw_output, prompt) tuple — raw_output includes ```sql``` wrapping.
223
  """
224
  if llm is None:
225
  llm = get_model()
@@ -245,26 +226,14 @@ def generate_sql(
245
 
246
  def generate_sql_streaming(
247
  user_question: str,
248
- llm: Llama | None = None,
249
  max_tokens: int = DEFAULT_MAX_TOKENS,
250
  temperature: float = DEFAULT_TEMPERATURE,
251
  schema: dict | None = None,
252
  ) -> Generator[str, None, None]:
253
  """
254
- Stream SQL tokens for real-time Gradio display.
255
-
256
- Accumulates tokens and yields the full text so far on each chunk.
257
- Stops when a stop sequence appears in the accumulated text.
258
-
259
- Args:
260
- user_question: The admin's question in plain English.
261
- llm: Llama instance (uses cached global if None).
262
- max_tokens: Max tokens to generate.
263
- temperature: Sampling temperature.
264
- schema: Table schema dict for prompt context.
265
-
266
- Yields:
267
- The full accumulated SQL text so far (for display replacement).
268
  """
269
  if llm is None:
270
  llm = get_model()
 
1
  """
2
+ model_inference.py — transformers + PEFT wrapper for local SQL generation.
3
+
4
+ ZeroGPU-compatible: uses PyTorch (transformers + bitsandbytes 4-bit), which is
5
+ the only CUDA path supported by HF Spaces ZeroGPU. The previous llama.cpp
6
+ backend could not access ZeroGPU's PyTorch-only CUDA emulation.
7
+
8
+ Model = pre-quantized 4-bit base (unsloth/qwen2.5-coder-14b-instruct-bnb-4bit)
9
+ + LoRA adapter (build-small-hackathon/lfed-qwen2.5-coder-14b-sql-lora)
10
+
11
+ This is exactly the configuration the model was QLoRA fine-tuned in.
12
 
13
  Handles:
14
+ - Model loading (lazy, cached, thread-safe)
15
  - generate_sql(): prompt → raw text → cleaned SQL
16
+ - generate_sql_streaming(): yields accumulated text for Gradio stream=True
17
+
18
+ The loaded object (`TransformersLLM`) is callable with the same signature and
19
+ response schema as llama_cpp.Llama, so downstream code is backend-agnostic:
20
+
21
+ out = llm(prompt, max_tokens=256, stop=[...], temperature=0.0)
22
+ text = out["choices"][0]["text"]
23
  """
24
 
25
  from __future__ import annotations
 
27
  import os
28
  import time
29
  import threading
 
30
  from typing import Generator, Optional
31
 
32
+ os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  from prompts import build_prompt
35
 
36
 
37
  # ── Model configuration ────────────────────────────────────────────────
38
 
39
+ BASE_MODEL_4BIT = "unsloth/qwen2.5-coder-14b-instruct-bnb-4bit"
40
+ ADAPTER_REPO = "build-small-hackathon/lfed-qwen2.5-coder-14b-sql-lora"
41
+
42
+ # Override via env for local dev (e.g. a smaller model on a Mac)
43
+ BASE_MODEL_4BIT = os.environ.get("LFED_BASE_MODEL", BASE_MODEL_4BIT)
44
+ ADAPTER_REPO = os.environ.get("LFED_ADAPTER_REPO", ADAPTER_REPO)
45
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  DEFAULT_MAX_TOKENS = 256
47
  DEFAULT_TEMPERATURE = 0.0
48
  STOP_SEQUENCES = ["\n\n", "Question:", "User:", "<|im_end|>", "<|im_start|>"]
49
 
50
  # Thread-safe model cache
51
  _lock = threading.Lock()
52
+ _llm: Optional["TransformersLLM"] = None
53
 
54
 
55
+ # ── llama.cpp-compatible wrapper ───────────────────────────────────────
56
 
57
+ class TransformersLLM:
58
+ """Callable wrapper around transformers generate() that mimics the
59
+ llama_cpp.Llama response schema used by the rest of the app."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
+ def __init__(self, base_model: str = BASE_MODEL_4BIT, adapter: str = ADAPTER_REPO):
62
+ import torch
63
+ from transformers import AutoModelForCausalLM, AutoTokenizer
64
+ from peft import PeftModel
 
65
 
66
+ self.torch = torch
67
+ use_cuda = torch.cuda.is_available()
68
+ print(f"🤖 Loading base model: {base_model} (cuda={use_cuda})")
69
+
70
+ # Tokenizer comes from the adapter repo (carries the fine-tune's
71
+ # chat template); falls back to the base model.
72
+ try:
73
+ self.tokenizer = AutoTokenizer.from_pretrained(adapter)
74
+ except Exception:
75
+ self.tokenizer = AutoTokenizer.from_pretrained(base_model)
76
+
77
+ load_kwargs = {"low_cpu_mem_usage": True}
78
+ if use_cuda:
79
+ # Pre-quantized bnb-4bit checkpoint: no BitsAndBytesConfig needed.
80
+ load_kwargs["device_map"] = "auto"
81
+ load_kwargs["torch_dtype"] = torch.bfloat16
82
+ else:
83
+ # CPU/MPS dev fallback — bitsandbytes requires CUDA. Expect this
84
+ # only with LFED_BASE_MODEL pointing at a small fp16 model.
85
+ load_kwargs["torch_dtype"] = torch.float32
86
+
87
+ model = AutoModelForCausalLM.from_pretrained(base_model, **load_kwargs)
88
+
89
+ if adapter:
90
+ print(f"🔗 Applying LoRA adapter: {adapter}")
91
+ model = PeftModel.from_pretrained(model, adapter)
92
+
93
+ model.eval()
94
+ self.model = model
95
+
96
+ # -- helpers --------------------------------------------------------
97
+
98
+ def _truncate_on_stop(self, text: str, stop: list[str] | None) -> tuple[str, bool]:
99
+ if not stop:
100
+ return text, False
101
+ cut = len(text)
102
+ hit = False
103
+ for s in stop:
104
+ idx = text.find(s)
105
+ if idx != -1 and idx < cut:
106
+ cut = idx
107
+ hit = True
108
+ return text[:cut], hit
109
+
110
+ def _gen_kwargs(self, max_tokens: int, temperature: float) -> dict:
111
+ kwargs = {
112
+ "max_new_tokens": max_tokens,
113
+ "pad_token_id": self.tokenizer.pad_token_id or self.tokenizer.eos_token_id,
114
+ }
115
+ if temperature and temperature > 0:
116
+ kwargs.update(do_sample=True, temperature=temperature)
117
+ else:
118
+ kwargs.update(do_sample=False)
119
+ return kwargs
120
+
121
+ # -- llama.cpp-style call -------------------------------------------
122
+
123
+ def __call__(
124
+ self,
125
+ prompt: str,
126
+ max_tokens: int = DEFAULT_MAX_TOKENS,
127
+ stop: list[str] | None = None,
128
+ temperature: float = DEFAULT_TEMPERATURE,
129
+ echo: bool = False,
130
+ stream: bool = False,
131
+ ):
132
+ if stream:
133
+ return self._stream(prompt, max_tokens, stop, temperature)
134
+
135
+ inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)
136
+ with self.torch.inference_mode():
137
+ output_ids = self.model.generate(
138
+ **inputs, **self._gen_kwargs(max_tokens, temperature)
139
+ )
140
+ new_ids = output_ids[0][inputs["input_ids"].shape[1]:]
141
+ text = self.tokenizer.decode(new_ids, skip_special_tokens=True)
142
+ text, _ = self._truncate_on_stop(text, stop)
143
+ return {"choices": [{"text": text}]}
144
+
145
+ def _stream(
146
+ self,
147
+ prompt: str,
148
+ max_tokens: int,
149
+ stop: list[str] | None,
150
+ temperature: float,
151
+ ) -> Generator[dict, None, None]:
152
+ from transformers import TextIteratorStreamer
153
+
154
+ inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)
155
+ streamer = TextIteratorStreamer(
156
+ self.tokenizer, skip_prompt=True, skip_special_tokens=True
157
+ )
158
+ kwargs = dict(inputs, streamer=streamer, **self._gen_kwargs(max_tokens, temperature))
159
+ thread = threading.Thread(target=self.model.generate, kwargs=kwargs)
160
+ thread.start()
161
+ for piece in streamer:
162
+ yield {"choices": [{"text": piece}]}
163
+ thread.join()
164
+
165
+
166
+ # ── Model loading ──────────────────────────────────────────────────────
167
+
168
+ def load_model(verbose: bool = False) -> TransformersLLM:
169
+ """Load the model (base 4-bit + LoRA). Thread-safe global singleton."""
170
  global _llm
171
 
172
  if _llm is not None:
 
176
  if _llm is not None: # Double-check after acquiring lock
177
  return _llm
178
 
 
 
 
179
  t0 = time.time()
180
+ _llm = TransformersLLM()
181
+ print(f"✅ Model loaded in {time.time() - t0:.1f}s")
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  return _llm
183
 
184
 
185
+ def get_model() -> TransformersLLM | None:
186
  """Return the cached model, or None if not loaded."""
187
  return _llm
188
 
 
191
 
192
  def generate_sql(
193
  user_question: str,
194
+ llm=None,
195
  max_tokens: int = DEFAULT_MAX_TOKENS,
196
  temperature: float = DEFAULT_TEMPERATURE,
197
  schema: dict | None = None,
 
199
  """
200
  Generate SQL from a natural-language question.
201
 
 
 
 
 
 
 
 
202
  Returns:
203
+ (raw_output, prompt) tuple — raw_output may include ```sql``` wrapping.
204
  """
205
  if llm is None:
206
  llm = get_model()
 
226
 
227
  def generate_sql_streaming(
228
  user_question: str,
229
+ llm=None,
230
  max_tokens: int = DEFAULT_MAX_TOKENS,
231
  temperature: float = DEFAULT_TEMPERATURE,
232
  schema: dict | None = None,
233
  ) -> Generator[str, None, None]:
234
  """
235
+ Stream SQL for real-time Gradio display. Yields the full accumulated
236
+ text so far on each chunk; stops when a stop sequence appears.
 
 
 
 
 
 
 
 
 
 
 
 
237
  """
238
  if llm is None:
239
  llm = get_model()
requirements.txt CHANGED
@@ -2,4 +2,9 @@ spaces
2
  gradio>=6.15.0
3
  duckdb==1.5.3
4
  huggingface_hub>=0.26.0
5
- llama-cpp-python
 
 
 
 
 
 
2
  gradio>=6.15.0
3
  duckdb==1.5.3
4
  huggingface_hub>=0.26.0
5
+ hf_transfer
6
+ torch
7
+ transformers>=4.46.0
8
+ peft>=0.13.0
9
+ bitsandbytes>=0.44.0
10
+ accelerate>=1.0.0
tests/test_model_inference.py CHANGED
@@ -147,22 +147,20 @@ class TestModelSingleton:
147
  assert model_inference.get_model() is None
148
 
149
  def test_load_model_caches_instance(self):
150
- """Mock Llama to avoid loading a real 4GB model."""
151
  import model_inference
152
 
153
  # Save original
154
  original = model_inference._llm
155
 
156
- with patch("model_inference.Llama") as mock_llama_class:
157
  mock_llama = MagicMock()
158
  mock_llama_class.return_value = mock_llama
159
 
160
  # Reset cache
161
  model_inference._llm = None
162
 
163
- # Also mock _resolve_model_path to avoid file checks
164
- with patch("model_inference._resolve_model_path", return_value="/fake/path.gguf"):
165
- result = model_inference.load_model()
166
 
167
  assert result is mock_llama
168
  assert model_inference.get_model() is mock_llama
@@ -170,7 +168,7 @@ class TestModelSingleton:
170
  # Second call returns cached
171
  result2 = model_inference.load_model()
172
  assert result2 is mock_llama
173
- # Llama constructor should only be called once
174
  mock_llama_class.assert_called_once()
175
 
176
  # Restore original
 
147
  assert model_inference.get_model() is None
148
 
149
  def test_load_model_caches_instance(self):
150
+ """Mock TransformersLLM to avoid loading a real 14B model."""
151
  import model_inference
152
 
153
  # Save original
154
  original = model_inference._llm
155
 
156
+ with patch("model_inference.TransformersLLM") as mock_llama_class:
157
  mock_llama = MagicMock()
158
  mock_llama_class.return_value = mock_llama
159
 
160
  # Reset cache
161
  model_inference._llm = None
162
 
163
+ result = model_inference.load_model()
 
 
164
 
165
  assert result is mock_llama
166
  assert model_inference.get_model() is mock_llama
 
168
  # Second call returns cached
169
  result2 = model_inference.load_model()
170
  assert result2 is mock_llama
171
+ # Constructor should only be called once
172
  mock_llama_class.assert_called_once()
173
 
174
  # Restore original