barakplasma commited on
Commit
4f5d51c
·
unverified ·
1 Parent(s): ae81e7b

Upload scripts/multi_quant_build_upload.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. scripts/multi_quant_build_upload.py +480 -0
scripts/multi_quant_build_upload.py ADDED
@@ -0,0 +1,480 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import importlib
4
+ import json
5
+ import os
6
+ import shlex
7
+ import signal
8
+ import subprocess
9
+ import sys
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ # Must match your converter's accepted quant modes
15
+ SUPPORTED_BY_CONVERTER = {"none", "dynamic_int8", "float16", "int8", "int4"}
16
+
17
+
18
+ def run(
19
+ cmd,
20
+ cwd=None,
21
+ check=True,
22
+ env=None,
23
+ log_file: Optional[Path] = None,
24
+ timeout_sec: int = 0,
25
+ ):
26
+ print(f"[CMD] {' '.join(shlex.quote(c) for c in cmd)}", flush=True)
27
+
28
+ p = subprocess.Popen(
29
+ cmd,
30
+ cwd=cwd,
31
+ env=env,
32
+ stdout=subprocess.PIPE,
33
+ stderr=subprocess.STDOUT,
34
+ text=True,
35
+ bufsize=1,
36
+ preexec_fn=os.setsid, # new process group so we can kill children too
37
+ )
38
+
39
+ lines = []
40
+ timed_out = False
41
+
42
+ try:
43
+ assert p.stdout is not None
44
+ for line in p.stdout:
45
+ print(line, end="", flush=True)
46
+ lines.append(line)
47
+
48
+ if timeout_sec > 0:
49
+ rc = p.wait(timeout=timeout_sec)
50
+ else:
51
+ rc = p.wait()
52
+
53
+ except subprocess.TimeoutExpired:
54
+ timed_out = True
55
+ print(f"\n[!] Timeout after {timeout_sec}s. Terminating process group...", flush=True)
56
+ try:
57
+ os.killpg(os.getpgid(p.pid), signal.SIGTERM)
58
+ except Exception:
59
+ pass
60
+ try:
61
+ p.wait(timeout=8)
62
+ except Exception:
63
+ try:
64
+ os.killpg(os.getpgid(p.pid), signal.SIGKILL)
65
+ except Exception:
66
+ pass
67
+ rc = 124
68
+
69
+ except KeyboardInterrupt:
70
+ print("\n[!] KeyboardInterrupt received. Terminating process group...", flush=True)
71
+ try:
72
+ os.killpg(os.getpgid(p.pid), signal.SIGTERM)
73
+ except Exception:
74
+ pass
75
+ try:
76
+ p.wait(timeout=8)
77
+ except Exception:
78
+ try:
79
+ os.killpg(os.getpgid(p.pid), signal.SIGKILL)
80
+ except Exception:
81
+ pass
82
+ raise
83
+
84
+ out = "".join(lines)
85
+ if log_file:
86
+ log_file.parent.mkdir(parents=True, exist_ok=True)
87
+ log_file.write_text(out, encoding="utf-8")
88
+
89
+ if check and rc != 0:
90
+ msg = f"Command failed ({rc})"
91
+ if timed_out:
92
+ msg += " [timeout]"
93
+ msg += f": {' '.join(cmd)}"
94
+ raise RuntimeError(msg)
95
+
96
+ return rc, out, timed_out
97
+
98
+
99
+ def ensure_hf_repo(repo_id: str, private: bool):
100
+ cmd = ["hf", "repo", "create", repo_id, "--type", "model"]
101
+ if private:
102
+ cmd.append("--private")
103
+ else:
104
+ cmd.append("--public")
105
+
106
+ rc, out, _ = run(cmd, check=False)
107
+ low = out.lower()
108
+ if rc != 0 and "already exists" not in low:
109
+ raise RuntimeError(f"Failed creating repo {repo_id}")
110
+
111
+
112
+ def hf_upload(repo_id: str, local_path: Path, path_in_repo: str):
113
+ cmd = [
114
+ "hf",
115
+ "upload",
116
+ repo_id,
117
+ str(local_path),
118
+ path_in_repo,
119
+ "--repo-type",
120
+ "model",
121
+ ]
122
+ run(cmd, check=True)
123
+
124
+
125
+ def normalize_quant(q: str) -> str:
126
+ q = q.strip().lower()
127
+ aliases = {
128
+ "fp16": "float16",
129
+ "f16": "float16",
130
+ "i8": "int8",
131
+ "i4": "int4",
132
+ "q8": "int8",
133
+ "q4": "int4",
134
+ "fp32": "none",
135
+ "off": "none",
136
+ "no": "none",
137
+ }
138
+ return aliases.get(q, q)
139
+
140
+
141
+ def detect_native_quant_possible(model_dir: Path):
142
+ """
143
+ Heuristic for your environment:
144
+ - For gemma3, native quant is likely usable only if build_model_4b exists.
145
+ """
146
+ cfg = model_dir / "config.json"
147
+ if not cfg.exists():
148
+ return False, "config.json missing"
149
+
150
+ try:
151
+ j = json.loads(cfg.read_text())
152
+ model_type = (j.get("model_type") or "").lower()
153
+ except Exception as e:
154
+ return False, f"config parse failed: {e}"
155
+
156
+ if model_type == "gemma3":
157
+ try:
158
+ mod = importlib.import_module("litert_torch.generative.examples.gemma3.gemma3")
159
+ available = {n for n in dir(mod) if n.startswith("build_model")}
160
+ if "build_model_4b" in available:
161
+ return True, "gemma3 build_model_4b present"
162
+ return False, f"gemma3 4b builder missing; available={sorted(available)}"
163
+ except Exception as e:
164
+ return False, f"cannot import gemma3 converter module: {e}"
165
+
166
+ return True, f"model_type={model_type or 'unknown'}"
167
+
168
+
169
+ def plan_quants(requested_quants, native_ok: bool):
170
+ """
171
+ Build plan for requested quantization modes.
172
+
173
+ Strategy 3 (post-TFLite quantization) gives int4/int8/dynamic_int8 genuinely
174
+ different output sizes, so they are each built independently.
175
+
176
+ The only deduplication is: if 'none' is requested alongside int4/int8/dynamic_int8,
177
+ the float32 TFLite produced by those builds also satisfies 'none', so we skip a
178
+ redundant bare 'none' build.
179
+ """
180
+ requested = []
181
+ unsupported = []
182
+
183
+ for q in requested_quants:
184
+ if q not in SUPPORTED_BY_CONVERTER:
185
+ unsupported.append(q)
186
+ else:
187
+ requested.append(q)
188
+
189
+ # All supported modes each get their own build — Strategy 3 makes them genuinely distinct
190
+ build_plan = requested[:]
191
+ alias_map = {q: q for q in requested}
192
+ mode = "native_quant_available" if native_ok else "strategy3_post_tflite"
193
+
194
+ return build_plan, alias_map, unsupported, mode
195
+
196
+
197
+ def main():
198
+ ap = argparse.ArgumentParser(
199
+ description="Run multi-quant conversion+bundle and upload successful artifacts to HF."
200
+ )
201
+ ap.add_argument("--converter-script", default="/home/ubuntu/convert_translategemma_android.py")
202
+ ap.add_argument("--model-id", default="google/translategemma-4b-it")
203
+ ap.add_argument("--model-dir", default="/home/ubuntu/translategemma-4b-it")
204
+ ap.add_argument("--tflite-root", default="/home/ubuntu/tflite_output")
205
+ ap.add_argument("--output-dir", default="/home/ubuntu/output")
206
+ ap.add_argument("--log-dir", default="/home/ubuntu/logs")
207
+ ap.add_argument("--prefill", type=int, default=1024)
208
+ ap.add_argument("--kvcache", type=int, default=1024)
209
+ ap.add_argument("--timeout-sec", type=int, default=0, help="Per-quant timeout. 0 = no timeout.")
210
+
211
+ ap.add_argument(
212
+ "--quants",
213
+ default="int4,int8,fp8,fp16,dynamic_int8",
214
+ help="Comma-separated requested modes",
215
+ )
216
+ ap.add_argument(
217
+ "--repo-id",
218
+ default="barakplasma/translategemma-4b-it-android-task-quantized",
219
+ )
220
+
221
+ vis = ap.add_mutually_exclusive_group()
222
+ vis.add_argument("--private", action="store_true", default=True)
223
+ vis.add_argument("--public", action="store_true")
224
+
225
+ ap.add_argument("--no-upload", action="store_true")
226
+ args = ap.parse_args()
227
+
228
+ converter_script = Path(args.converter_script)
229
+ if not converter_script.exists():
230
+ print(f"[x] converter script not found: {converter_script}", file=sys.stderr)
231
+ sys.exit(1)
232
+
233
+ model_dir = Path(args.model_dir)
234
+ tflite_root = Path(args.tflite_root)
235
+ output_dir = Path(args.output_dir)
236
+ log_dir = Path(args.log_dir)
237
+
238
+ tflite_root.mkdir(parents=True, exist_ok=True)
239
+ output_dir.mkdir(parents=True, exist_ok=True)
240
+ log_dir.mkdir(parents=True, exist_ok=True)
241
+
242
+ # Parse and dedup quants
243
+ quant_list = [normalize_quant(x) for x in args.quants.split(",") if x.strip()]
244
+ seen = set()
245
+ quant_list = [q for q in quant_list if not (q in seen or seen.add(q))]
246
+
247
+ native_ok, native_reason = detect_native_quant_possible(model_dir)
248
+ print(f"[+] native quant capability: {native_ok} ({native_reason})")
249
+
250
+ build_plan, alias_map, unsupported, plan_mode = plan_quants(quant_list, native_ok=native_ok)
251
+ print(f"[+] plan mode: {plan_mode}")
252
+ print(f"[+] requested quants: {quant_list}")
253
+ print(f"[+] build plan: {build_plan}")
254
+ if unsupported:
255
+ print(f"[!] unsupported (skipped): {unsupported}")
256
+
257
+ built = {}
258
+ results = []
259
+
260
+ try:
261
+ for q in build_plan:
262
+ print(f"\n=== BUILD QUANT: {q} ===", flush=True)
263
+ q_tflite_dir = tflite_root / q
264
+ q_tflite_dir.mkdir(parents=True, exist_ok=True)
265
+
266
+ task_file = output_dir / f"translategemma-4b-it-{q}.task"
267
+ log_file = log_dir / f"convert_{q}.log"
268
+
269
+ # Reuse existing float32 TFLite if available — Strategy 3 will quantize it
270
+ base_tflite = tflite_root / "none" / "translategemma-4b-it-generic-none.tflite"
271
+ if q not in ("none", "float16") and base_tflite.exists():
272
+ print(f"[+] Reusing existing float32 TFLite for {q} (Strategy 3 will quantize)", flush=True)
273
+ cmd = [
274
+ sys.executable,
275
+ str(converter_script),
276
+ "--bundle-only",
277
+ "--existing-tflite",
278
+ str(base_tflite),
279
+ "--tflite-dir",
280
+ str(q_tflite_dir),
281
+ "--output-dir",
282
+ str(output_dir),
283
+ "--task-file",
284
+ str(task_file),
285
+ "--quantize",
286
+ q,
287
+ "--model-dir",
288
+ str(model_dir),
289
+ ]
290
+ else:
291
+ cmd = [
292
+ sys.executable,
293
+ str(converter_script),
294
+ "--model-id",
295
+ args.model_id,
296
+ "--model-dir",
297
+ str(model_dir),
298
+ "--tflite-dir",
299
+ str(q_tflite_dir),
300
+ "--output-dir",
301
+ str(output_dir),
302
+ "--task-file",
303
+ str(task_file),
304
+ "--quantize",
305
+ q,
306
+ "--prefill",
307
+ str(args.prefill),
308
+ "--kvcache",
309
+ str(args.kvcache),
310
+ "--allow-no-token",
311
+ ]
312
+
313
+ rc, _, timed_out = run(
314
+ cmd,
315
+ check=False,
316
+ log_file=log_file,
317
+ timeout_sec=args.timeout_sec,
318
+ )
319
+
320
+ tflites = sorted(q_tflite_dir.glob("*.tflite"))
321
+ tflite_file = tflites[-1] if tflites else None
322
+ ok = (rc == 0) and task_file.exists()
323
+
324
+ built[q] = {
325
+ "quant": q,
326
+ "ok": ok,
327
+ "rc": rc,
328
+ "timed_out": timed_out,
329
+ "task": str(task_file) if task_file.exists() else "",
330
+ "task_size": task_file.stat().st_size if task_file.exists() else 0,
331
+ "tflite": str(tflite_file) if tflite_file and tflite_file.exists() else "",
332
+ "log": str(log_file),
333
+ }
334
+
335
+ except KeyboardInterrupt:
336
+ print("\n[!] Stopped by user (Ctrl+C). Partial results will be saved.", flush=True)
337
+
338
+ # Expand to requested list
339
+ for q in quant_list:
340
+ if q in unsupported:
341
+ results.append(
342
+ {
343
+ "quant": q,
344
+ "ok": False,
345
+ "rc": 2,
346
+ "timed_out": False,
347
+ "task": "",
348
+ "task_size": 0,
349
+ "tflite": "",
350
+ "log": "",
351
+ "status": "unsupported",
352
+ "alias_of": "",
353
+ }
354
+ )
355
+ continue
356
+
357
+ bq = alias_map.get(q, q)
358
+ b = built.get(bq)
359
+ if not b:
360
+ results.append(
361
+ {
362
+ "quant": q,
363
+ "ok": False,
364
+ "rc": 130,
365
+ "timed_out": False,
366
+ "task": "",
367
+ "task_size": 0,
368
+ "tflite": "",
369
+ "log": "",
370
+ "status": "not_built",
371
+ "alias_of": bq,
372
+ }
373
+ )
374
+ continue
375
+
376
+ status = "built" if q == bq else f"aliased_to_{bq}"
377
+ results.append(
378
+ {
379
+ **b,
380
+ "quant": q,
381
+ "status": status,
382
+ "alias_of": bq if q != bq else "",
383
+ }
384
+ )
385
+
386
+ # Write summary JSON
387
+ summary = {
388
+ "timestamp_utc": datetime.now(timezone.utc).isoformat(),
389
+ "native_quant_capability": native_ok,
390
+ "native_quant_reason": native_reason,
391
+ "plan_mode": plan_mode,
392
+ "requested_quants": quant_list,
393
+ "build_plan": build_plan,
394
+ "unsupported_quants": unsupported,
395
+ "results": results,
396
+ }
397
+
398
+ summary_json = output_dir / "quantization_summary.json"
399
+ summary_json.write_text(json.dumps(summary, indent=2), encoding="utf-8")
400
+ print(f"\n[+] Wrote summary: {summary_json}")
401
+
402
+ # Write README
403
+ readme = output_dir / "README.md"
404
+ lines = []
405
+ lines.append("---")
406
+ lines.append("license: other")
407
+ lines.append("library_name: mediapipe")
408
+ lines.append("pipeline_tag: text-generation")
409
+ lines.append("---\n")
410
+ lines.append("# TranslateGemma 4B IT - Quantized Android Task Bundles\n")
411
+ lines.append(f"Generated: `{datetime.now(timezone.utc).isoformat()}`\n")
412
+ lines.append(f"- Native quant capability: `{native_ok}`")
413
+ lines.append(f"- Reason: `{native_reason}`")
414
+ lines.append(f"- Plan mode: `{plan_mode}`\n")
415
+ lines.append("| Requested quant | Status | Built from | Task file | Size (bytes) |")
416
+ lines.append("|---|---|---|---|---|")
417
+ for r in results:
418
+ if r.get("status") == "unsupported":
419
+ status = "⏭️ unsupported by converter"
420
+ built_from = "-"
421
+ elif str(r.get("status", "")).startswith("aliased_to_"):
422
+ status = "↪️ aliased"
423
+ built_from = f"`{r.get('alias_of','-')}`"
424
+ else:
425
+ if r.get("timed_out"):
426
+ status = "⏱️ timeout"
427
+ else:
428
+ status = "✅ success" if r.get("ok") else f"❌ failed (rc={r.get('rc')})"
429
+ built_from = "`self`"
430
+
431
+ task_name = Path(r["task"]).name if r.get("task") else "-"
432
+ lines.append(
433
+ f"| `{r['quant']}` | {status} | {built_from} | `{task_name}` | `{r.get('task_size',0)}` |"
434
+ )
435
+
436
+ lines.append("\n## Notes")
437
+ lines.append("- Aliased entries are not rebuilt; they point to an equivalent built variant.")
438
+ lines.append("- `fp8` is often unsupported in current converter/runtime stacks.")
439
+ lines.append("- Verify on-device compatibility before public release.")
440
+
441
+ readme.write_text("\n".join(lines), encoding="utf-8")
442
+ print(f"[+] Wrote README: {readme}")
443
+
444
+ if args.no_upload:
445
+ print("[!] --no-upload set. Done.")
446
+ return
447
+
448
+ private = False if args.public else True
449
+ ensure_hf_repo(args.repo_id, private=private)
450
+
451
+ hf_upload(args.repo_id, readme, "README.md")
452
+ hf_upload(args.repo_id, summary_json, "quantization_summary.json")
453
+
454
+ uploaded = set()
455
+
456
+ # Upload only unique built outputs
457
+ for q, b in built.items():
458
+ if b.get("log"):
459
+ lp = Path(b["log"])
460
+ if lp.exists() and str(lp) not in uploaded:
461
+ hf_upload(args.repo_id, lp, f"logs/{lp.name}")
462
+ uploaded.add(str(lp))
463
+
464
+ if b.get("tflite"):
465
+ tp = Path(b["tflite"])
466
+ if tp.exists() and str(tp) not in uploaded:
467
+ hf_upload(args.repo_id, tp, f"artifacts/{q}/{tp.name}")
468
+ uploaded.add(str(tp))
469
+
470
+ if b.get("task"):
471
+ tk = Path(b["task"])
472
+ if tk.exists() and str(tk) not in uploaded:
473
+ hf_upload(args.repo_id, tk, f"artifacts/{q}/{tk.name}")
474
+ uploaded.add(str(tk))
475
+
476
+ print(f"\n[+] Upload complete: https://huggingface.co/{args.repo_id}")
477
+
478
+
479
+ if __name__ == "__main__":
480
+ main()