TiGa-RCE commited on
Commit
1a6347c
·
verified ·
1 Parent(s): bc3f111

Add bounded bitsandbytes INT8 and NF4 controls

Browse files
Files changed (4) hide show
  1. README.md +4 -0
  2. __pycache__/app.cpython-313.pyc +0 -0
  3. app.py +136 -3
  4. requirements.txt +2 -0
README.md CHANGED
@@ -16,3 +16,7 @@ A bounded ZeroGPU control lane for the matched local MLX embedding
16
  quantization experiment. The first milestone reproduces the frozen
17
  Qwen3-Embedding-0.6B BF16 vectors on CUDA and compares them with the saved MLX
18
  BF16 vectors. CUDA results do not reproduce MLX/Metal performance.
 
 
 
 
 
16
  quantization experiment. The first milestone reproduces the frozen
17
  Qwen3-Embedding-0.6B BF16 vectors on CUDA and compares them with the saved MLX
18
  BF16 vectors. CUDA results do not reproduce MLX/Metal performance.
19
+
20
+ The second milestone adds CUDA-native bitsandbytes INT8 and NF4 controls.
21
+ They are deliberately reported as separate quantizers and are not treated as
22
+ equivalents of MLX Q, oQ, or oQe formats.
__pycache__/app.cpython-313.pyc CHANGED
Binary files a/__pycache__/app.cpython-313.pyc and b/__pycache__/app.cpython-313.pyc differ
 
app.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
  import json
4
  import os
5
  import time
 
6
  from pathlib import Path
7
 
8
  import gradio as gr
@@ -10,7 +11,7 @@ import numpy as np
10
  import spaces
11
  import torch
12
  import torch.nn.functional as F
13
- from transformers import AutoModel, AutoTokenizer
14
 
15
 
16
  MODEL_PATH = Path("/models/qwen3-embedding-0.6b")
@@ -18,6 +19,7 @@ DATA_ROOT = Path(os.environ.get("REPRO_DATA_ROOT", "/data"))
18
  INPUT_PATH = DATA_ROOT / "inputs/retrieval_pairs.json"
19
  MLX_REFERENCE = DATA_ROOT / "local-reference/qwen3-embedding-0.6b/bf16.npz"
20
  OUTPUT_DIR = DATA_ROOT / "cloud-results/qwen3-embedding-0.6b"
 
21
  TASK = (
22
  "Given a natural-language search query, retrieve the single passage "
23
  "that best answers it"
@@ -36,15 +38,19 @@ def detailed_instruction(query: str) -> str:
36
  return f"Instruct: {TASK}\nQuery:{query}"
37
 
38
 
39
- def encode(text: str) -> np.ndarray:
40
  batch = tokenizer(text, return_tensors="pt", truncation=True, max_length=32768)
41
  batch = {key: value.to("cuda") for key, value in batch.items()}
42
  with torch.inference_mode():
43
- output = model(**batch)
44
  vector = F.normalize(output.last_hidden_state[:, -1, :].float(), p=2, dim=-1)
45
  return vector[0].cpu().numpy()
46
 
47
 
 
 
 
 
48
  def retrieval_metrics(queries: np.ndarray, documents: np.ndarray) -> tuple[dict, np.ndarray, np.ndarray]:
49
  scores = queries @ documents.T
50
  order = np.argsort(-scores, axis=1)
@@ -123,6 +129,117 @@ def run_bf16_control() -> dict:
123
  return result
124
 
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  with gr.Blocks() as demo:
127
  gr.Markdown(
128
  "# Embedding Quantization CUDA Control\n"
@@ -137,6 +254,22 @@ with gr.Blocks() as demo:
137
  concurrency_limit=1,
138
  api_name="run_bf16_control",
139
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
 
142
  if __name__ == "__main__":
 
3
  import json
4
  import os
5
  import time
6
+ import gc
7
  from pathlib import Path
8
 
9
  import gradio as gr
 
11
  import spaces
12
  import torch
13
  import torch.nn.functional as F
14
+ from transformers import AutoModel, AutoTokenizer, BitsAndBytesConfig
15
 
16
 
17
  MODEL_PATH = Path("/models/qwen3-embedding-0.6b")
 
19
  INPUT_PATH = DATA_ROOT / "inputs/retrieval_pairs.json"
20
  MLX_REFERENCE = DATA_ROOT / "local-reference/qwen3-embedding-0.6b/bf16.npz"
21
  OUTPUT_DIR = DATA_ROOT / "cloud-results/qwen3-embedding-0.6b"
22
+ CUDA_BF16_REFERENCE = OUTPUT_DIR / "cuda-bf16.npz"
23
  TASK = (
24
  "Given a natural-language search query, retrieve the single passage "
25
  "that best answers it"
 
38
  return f"Instruct: {TASK}\nQuery:{query}"
39
 
40
 
41
+ def encode_with(active_model, text: str) -> np.ndarray:
42
  batch = tokenizer(text, return_tensors="pt", truncation=True, max_length=32768)
43
  batch = {key: value.to("cuda") for key, value in batch.items()}
44
  with torch.inference_mode():
45
+ output = active_model(**batch)
46
  vector = F.normalize(output.last_hidden_state[:, -1, :].float(), p=2, dim=-1)
47
  return vector[0].cpu().numpy()
48
 
49
 
50
+ def encode(text: str) -> np.ndarray:
51
+ return encode_with(model, text)
52
+
53
+
54
  def retrieval_metrics(queries: np.ndarray, documents: np.ndarray) -> tuple[dict, np.ndarray, np.ndarray]:
55
  scores = queries @ documents.T
56
  order = np.argsort(-scores, axis=1)
 
129
  return result
130
 
131
 
132
+ def compare_vectors(
133
+ queries: np.ndarray,
134
+ documents: np.ndarray,
135
+ scores: np.ndarray,
136
+ ranks: np.ndarray,
137
+ reference_path: Path,
138
+ prefix: str,
139
+ ) -> dict | None:
140
+ if not reference_path.exists():
141
+ return None
142
+ reference = np.load(reference_path)
143
+ ref_all = np.concatenate([reference["queries"], reference["documents"]])
144
+ candidate_all = np.concatenate([queries, documents])
145
+ aligned = np.sum(ref_all * candidate_all, axis=1)
146
+ score_delta = scores - reference["scores"]
147
+ return {
148
+ f"mean_aligned_cosine_{prefix}": float(aligned.mean()),
149
+ f"minimum_aligned_cosine_{prefix}": float(aligned.min()),
150
+ f"score_rmse_{prefix}": float(np.sqrt(np.mean(score_delta ** 2))),
151
+ f"queries_with_rank_change_{prefix}": int(
152
+ np.count_nonzero(ranks - reference["ranks"])
153
+ ),
154
+ }
155
+
156
+
157
+ QUANTIZERS = {
158
+ "bnb-int8": BitsAndBytesConfig(load_in_8bit=True),
159
+ "bnb-nf4": BitsAndBytesConfig(
160
+ load_in_4bit=True,
161
+ bnb_4bit_quant_type="nf4",
162
+ bnb_4bit_compute_dtype=torch.bfloat16,
163
+ bnb_4bit_use_double_quant=False,
164
+ ),
165
+ }
166
+
167
+
168
+ @spaces.GPU(duration=600)
169
+ def run_quantized_control(variant: str) -> dict:
170
+ if variant not in QUANTIZERS:
171
+ raise ValueError(f"unsupported quantizer: {variant}")
172
+ pairs = json.loads(INPUT_PATH.read_text())
173
+ query_texts = [detailed_instruction(item["query"]) for item in pairs]
174
+ document_texts = [item["document"] for item in pairs]
175
+
176
+ gc.collect()
177
+ torch.cuda.empty_cache()
178
+ torch.cuda.reset_peak_memory_stats()
179
+ torch.cuda.synchronize()
180
+ allocation_before = int(torch.cuda.memory_allocated())
181
+ load_started = time.perf_counter()
182
+ quantized_model = AutoModel.from_pretrained(
183
+ MODEL_PATH,
184
+ quantization_config=QUANTIZERS[variant],
185
+ device_map={"": 0},
186
+ trust_remote_code=True,
187
+ ).eval()
188
+ torch.cuda.synchronize()
189
+ load_seconds = time.perf_counter() - load_started
190
+ allocation_after_load = int(torch.cuda.memory_allocated())
191
+
192
+ encode_started = time.perf_counter()
193
+ queries = np.stack([encode_with(quantized_model, text) for text in query_texts])
194
+ documents = np.stack([
195
+ encode_with(quantized_model, text) for text in document_texts
196
+ ])
197
+ torch.cuda.synchronize()
198
+ encode_seconds = time.perf_counter() - encode_started
199
+ metrics, scores, ranks = retrieval_metrics(queries, documents)
200
+ metrics.update({
201
+ "lane": f"cuda-zerogpu-{variant}",
202
+ "model": "Qwen/Qwen3-Embedding-0.6B",
203
+ "source_revision": "97b0c614be4d77ee51c0cef4e5f07c00f9eb65b3",
204
+ "quantizer": variant,
205
+ "quantization_config": QUANTIZERS[variant].to_dict(),
206
+ "load_seconds": load_seconds,
207
+ "encode_seconds": encode_seconds,
208
+ "texts_per_second": len(query_texts + document_texts) / encode_seconds,
209
+ "torch_version": torch.__version__,
210
+ "cuda_device": torch.cuda.get_device_name(0),
211
+ "cuda_allocation_before_load": allocation_before,
212
+ "cuda_allocation_after_load": allocation_after_load,
213
+ "cuda_incremental_model_allocation": allocation_after_load - allocation_before,
214
+ "cuda_peak_bytes": int(torch.cuda.max_memory_allocated()),
215
+ })
216
+ result = {
217
+ "metrics": metrics,
218
+ "cuda_bf16_comparison": compare_vectors(
219
+ queries, documents, scores, ranks, CUDA_BF16_REFERENCE, "vs_cuda_bf16"
220
+ ),
221
+ "mlx_bf16_comparison": compare_vectors(
222
+ queries, documents, scores, ranks, MLX_REFERENCE, "vs_mlx_bf16"
223
+ ),
224
+ }
225
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
226
+ stem = f"cuda-{variant}"
227
+ np.savez_compressed(
228
+ OUTPUT_DIR / f"{stem}.npz",
229
+ queries=queries,
230
+ documents=documents,
231
+ scores=scores,
232
+ ranks=ranks,
233
+ metrics=np.array(json.dumps(metrics)),
234
+ )
235
+ (OUTPUT_DIR / f"{stem}.json").write_text(json.dumps(result, indent=2) + "\n")
236
+
237
+ del quantized_model
238
+ gc.collect()
239
+ torch.cuda.empty_cache()
240
+ return result
241
+
242
+
243
  with gr.Blocks() as demo:
244
  gr.Markdown(
245
  "# Embedding Quantization CUDA Control\n"
 
254
  concurrency_limit=1,
255
  api_name="run_bf16_control",
256
  )
257
+ gr.Markdown(
258
+ "## CUDA-native quantizer controls\n"
259
+ "These are bitsandbytes INT8/NF4 controls, not MLX Q/oQ/oQe replicas."
260
+ )
261
+ quantizer = gr.Dropdown(
262
+ choices=list(QUANTIZERS), value="bnb-int8", label="Quantizer"
263
+ )
264
+ quant_button = gr.Button("Run bounded CUDA quantizer control")
265
+ quant_output = gr.JSON(label="Quantized result")
266
+ quant_button.click(
267
+ fn=run_quantized_control,
268
+ inputs=quantizer,
269
+ outputs=quant_output,
270
+ concurrency_limit=1,
271
+ api_name="run_quantized_control",
272
+ )
273
 
274
 
275
  if __name__ == "__main__":
requirements.txt CHANGED
@@ -2,3 +2,5 @@ transformers>=4.51,<5
2
  numpy>=2,<3
3
  spaces>=0.45
4
  torch>=2.8
 
 
 
2
  numpy>=2,<3
3
  spaces>=0.45
4
  torch>=2.8
5
+ accelerate>=1.2,<2
6
+ bitsandbytes>=0.48,<1