ihtesham0345 commited on
Commit
efb4fe4
Β·
1 Parent(s): 6e467d1

feat: Add 3-tier quantization with auto hardware detection

Browse files

- Tier 1: Qwen2.5-7B with 4-bit NF4 quantization (needs GPU 5.5GB+)
- Tier 2: Qwen2.5-1.5B with 8-bit quantization (GPU or CPU)
- Tier 3: Qwen2.5-0.5B bfloat16 fallback (CPU, default)
- Auto-detect GPU/CPU and select optimal quantization
- Auto-fallback: 7B→1.5B if GPU VRAM insufficient
- Configurable via .env: QUANTIZATION=auto/4bit/8bit/none
- Graceful fallback if bitsandbytes not installed
- Update requirements.txt with bitsandbytes>=0.43.0
- Update Dockerfile with GPU base image option

Files changed (4) hide show
  1. .env.example +10 -0
  2. Dockerfile +5 -0
  3. requirements.txt +1 -0
  4. services/model_loader.py +125 -10
.env.example CHANGED
@@ -4,3 +4,13 @@
4
  # Qwen/Qwen2.5-1.5B-Instruct (Goldilocks - ~3GB, recommended for GPU)
5
  # Qwen/Qwen2.5-7B-Instruct (Smartest - needs 4-bit quantization + GPU)
6
  MODEL_ID=Qwen/Qwen2.5-0.5B-Instruct
 
 
 
 
 
 
 
 
 
 
 
4
  # Qwen/Qwen2.5-1.5B-Instruct (Goldilocks - ~3GB, recommended for GPU)
5
  # Qwen/Qwen2.5-7B-Instruct (Smartest - needs 4-bit quantization + GPU)
6
  MODEL_ID=Qwen/Qwen2.5-0.5B-Instruct
7
+
8
+ # Quantization mode: auto, 4bit, 8bit, none
9
+ # auto = 4-bit on GPU for 7B, 8-bit on GPU for 1.5B, bf16 on CPU
10
+ # 4bit = force 4-bit quantization (requires GPU + bitsandbytes)
11
+ # 8bit = force 8-bit quantization
12
+ # none = disable quantization, uses bfloat16
13
+ QUANTIZATION=auto
14
+
15
+ # Double quantization for 4-bit (saves ~10% more memory)
16
+ USE_DOUBLE_QUANT=true
Dockerfile CHANGED
@@ -1,5 +1,10 @@
 
1
  FROM python:3.9
2
 
 
 
 
 
3
  RUN useradd -m -u 1000 user
4
  USER user
5
  ENV PATH="/home/user/.local/bin:$PATH"
 
1
+ # CPU-optimized default image (works on HuggingFace free tier)
2
  FROM python:3.9
3
 
4
+ # For GPU support (uncomment to use):
5
+ # FROM pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime
6
+ # RUN pip install bitsandbytes>=0.43.0
7
+
8
  RUN useradd -m -u 1000 user
9
  USER user
10
  ENV PATH="/home/user/.local/bin:$PATH"
requirements.txt CHANGED
@@ -7,3 +7,4 @@ torch
7
  transformers
8
  accelerate
9
  sentencepiece
 
 
7
  transformers
8
  accelerate
9
  sentencepiece
10
+ bitsandbytes>=0.43.0
services/model_loader.py CHANGED
@@ -1,6 +1,6 @@
1
  import os
2
  import torch
3
- from transformers import pipeline
4
  from dotenv import load_dotenv
5
  from pathlib import Path
6
 
@@ -8,25 +8,140 @@ env_path = Path(__file__).resolve().parent.parent / ".env"
8
  load_dotenv(dotenv_path=env_path)
9
 
10
  MODEL_ID = os.getenv("MODEL_ID", "Qwen/Qwen2.5-0.5B-Instruct")
 
 
11
 
12
  _pipe = None
 
13
 
14
- def get_pipe():
15
- global _pipe
16
- if _pipe is None:
17
- print(f"⏳ Loading Local Model {MODEL_ID}...")
 
 
 
 
 
 
 
 
 
18
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  _pipe = pipeline(
20
  "text-generation",
21
- model=MODEL_ID,
22
  torch_dtype=torch.bfloat16,
23
  device_map="auto",
24
- trust_remote_code=True
25
  )
26
- print("βœ… Model Loaded Successfully!")
27
- except Exception as e:
28
- print(f"❌ Model Load Failed: {e}")
 
29
  _pipe = None
 
 
 
 
30
  return _pipe
31
 
32
  def generate_text(messages, temperature=0.3, max_new_tokens=2000):
 
1
  import os
2
  import torch
3
+ from transformers import pipeline, BitsAndBytesConfig
4
  from dotenv import load_dotenv
5
  from pathlib import Path
6
 
 
8
  load_dotenv(dotenv_path=env_path)
9
 
10
  MODEL_ID = os.getenv("MODEL_ID", "Qwen/Qwen2.5-0.5B-Instruct")
11
+ QUANTIZATION = os.getenv("QUANTIZATION", "auto")
12
+ USE_DOUBLE_QUANT = os.getenv("USE_DOUBLE_QUANT", "true").lower() == "true"
13
 
14
  _pipe = None
15
+ _current_model = None
16
 
17
+ def _log(msg: str):
18
+ print(f"[ModelLoader] {msg}")
19
+
20
+ def _has_gpu() -> bool:
21
+ return torch.cuda.is_available()
22
+
23
+ def _gpu_name() -> str:
24
+ if _has_gpu():
25
+ return torch.cuda.get_device_name(0)
26
+ return "None"
27
+
28
+ def _gpu_memory_gb() -> float:
29
+ if _has_gpu():
30
  try:
31
+ return torch.cuda.get_device_properties(0).total_mem / 1e9
32
+ except:
33
+ return 0
34
+ return 0
35
+
36
+ def _select_quantization() -> str:
37
+ """Auto-select quantization tier based on MODEL_ID and hardware."""
38
+ user_mode = QUANTIZATION.lower()
39
+
40
+ if user_mode == "none":
41
+ return "none"
42
+
43
+ if user_mode != "auto":
44
+ return user_mode
45
+
46
+ # Auto-detect: GPU with enough VRAM for requested model
47
+ if "7B" in MODEL_ID:
48
+ if _has_gpu() and _gpu_memory_gb() >= 5.5:
49
+ _log(f"7B model detected, GPU {_gpu_name()} ({_gpu_memory_gb():.1f}GB) β€” using 4-bit")
50
+ return "4bit"
51
+ _log("7B model requested but no GPU with 5.5GB+ VRAM β€” falling back to 1.5B 8-bit")
52
+ return "cpu_fallback_8bit"
53
+
54
+ if "1.5B" in MODEL_ID:
55
+ if _has_gpu():
56
+ _log(f"1.5B model detected, GPU available β€” using 8-bit")
57
+ return "8bit"
58
+ _log("1.5B model detected, CPU only β€” using bfloat16")
59
+ return "none"
60
+
61
+ return "none"
62
+
63
+ def _build_model_kwargs(quant_mode: str) -> dict:
64
+ """Build pipeline kwargs based on quantization mode."""
65
+ kwargs = {
66
+ "trust_remote_code": True,
67
+ }
68
+
69
+ if quant_mode == "4bit":
70
+ kwargs["device_map"] = "auto"
71
+ kwargs["quantization_config"] = BitsAndBytesConfig(
72
+ load_in_4bit=True,
73
+ bnb_4bit_compute_dtype=torch.bfloat16,
74
+ bnb_4bit_use_double_quant=USE_DOUBLE_QUANT,
75
+ bnb_4bit_quant_type="nf4",
76
+ )
77
+ _log("⚑ 4-bit quantization enabled (NF4, double quant)")
78
+
79
+ elif quant_mode == "8bit":
80
+ kwargs["device_map"] = "auto"
81
+ kwargs["quantization_config"] = BitsAndBytesConfig(
82
+ load_in_8bit=True,
83
+ )
84
+ _log("⚑ 8-bit quantization enabled")
85
+
86
+ elif quant_mode == "cpu_fallback_8bit":
87
+ kwargs["device_map"] = "auto"
88
+ kwargs["quantization_config"] = BitsAndBytesConfig(
89
+ load_in_8bit=True,
90
+ )
91
+ _log("⚑ CPU fallback 8-bit for 1.5B model")
92
+
93
+ else:
94
+ kwargs["torch_dtype"] = torch.bfloat16
95
+ kwargs["device_map"] = "auto"
96
+ _log(f"πŸ“¦ Loading {MODEL_ID} in bfloat16 (CPU-friendly)")
97
+
98
+ return kwargs
99
+
100
+ def get_pipe():
101
+ global _pipe, _current_model
102
+
103
+ if _pipe is not None:
104
+ return _pipe
105
+
106
+ actual_model_id = MODEL_ID
107
+ quant_mode = _select_quantization()
108
+
109
+ # Handle CPU fallback for 7B β†’ 1.5B
110
+ if quant_mode == "cpu_fallback_8bit":
111
+ actual_model_id = "Qwen/Qwen2.5-1.5B-Instruct"
112
+ _log(f"πŸ”„ Fallback: loading {actual_model_id} instead of {MODEL_ID}")
113
+
114
+ _log(f"πŸš€ Loading {actual_model_id} (quantization: {quant_mode})")
115
+ _log(f" Hardware: GPU={_gpu_name()}, VRAM={_gpu_memory_gb():.1f}GB, CUDA={_has_gpu()}")
116
+
117
+ try:
118
+ kwargs = _build_model_kwargs(quant_mode)
119
+ _pipe = pipeline(
120
+ "text-generation",
121
+ model=actual_model_id,
122
+ **kwargs
123
+ )
124
+ _current_model = actual_model_id
125
+ _log("βœ… Model loaded successfully!")
126
+ except ImportError as e:
127
+ if "bitsandbytes" in str(e):
128
+ _log("❌ bitsandbytes not installed. Falling back to CPU bfloat16.")
129
  _pipe = pipeline(
130
  "text-generation",
131
+ model=actual_model_id,
132
  torch_dtype=torch.bfloat16,
133
  device_map="auto",
134
+ trust_remote_code=True,
135
  )
136
+ _current_model = actual_model_id
137
+ _log("βœ… Model loaded with CPU fallback")
138
+ else:
139
+ _log(f"❌ Model load failed: {e}")
140
  _pipe = None
141
+ except Exception as e:
142
+ _log(f"❌ Model load failed: {e}")
143
+ _pipe = None
144
+
145
  return _pipe
146
 
147
  def generate_text(messages, temperature=0.3, max_new_tokens=2000):