CompressedGemma commited on
Commit
4840bdd
·
verified ·
1 Parent(s): 2a0e152

Upload gui.py

Browse files
Files changed (1) hide show
  1. gui.py +740 -0
gui.py ADDED
@@ -0,0 +1,740 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Graphical User Interface for HPC-Quantize V2 (Factored Q2_K Engine).
3
+
4
+ Launch::
5
+
6
+ python3 gui.py
7
+
8
+ Provides:
9
+ * Source GGUF, iMatrix (.gguf / .dat), and Output (.gguf / .npz) file pickers
10
+ * Interactive GGUF tensor table with Q2_K eligibility & iMatrix match status
11
+ * Full HPCConfig controls (Geometry-Preserving Analogs, Coupled Bilinear Q2_K,
12
+ Sieve + 36-state Viterbi, Spectral/Vesica/DC shaping, Candidate lattice size)
13
+ * Live CLI command preview with one-click copy
14
+ * Threaded execution with per-tensor progress bar, live log, and cancellation
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import queue
21
+ import shlex
22
+ import threading
23
+ import time
24
+ import tkinter as tk
25
+ from pathlib import Path
26
+ from tkinter import filedialog, messagebox, ttk
27
+
28
+ import numpy as np
29
+
30
+ from hpc_quantize_v2 import (
31
+ GGUFReader,
32
+ HPCConfig,
33
+ Q2KQuantizer,
34
+ expand_importance,
35
+ factor_only_solve,
36
+ lookup_imatrix,
37
+ read_imatrix,
38
+ )
39
+ from hpc_quantize_v2.gguf import is_q2k_eligible, write_quantized_gguf
40
+ from hpc_quantize_v2.reference import reference_solve, synthetic_cases
41
+
42
+
43
+ class HPCQuantizeGUI:
44
+ def __init__(self, root: tk.Tk) -> None:
45
+ self.root = root
46
+ self.root.title("HPC-Quantize V2 — Factored Q2_K & Geometry-Preserving Analog Engine")
47
+ self.root.geometry("1180x820")
48
+ self.root.minsize(980, 680)
49
+
50
+ self._log_queue: queue.Queue[tuple[str, object]] = queue.Queue()
51
+ self._stop_event = threading.Event()
52
+ self._worker: threading.Thread | None = None
53
+ self._reader: GGUFReader | None = None
54
+ self._imatrix: dict[str, np.ndarray] | None = None
55
+
56
+ self._init_vars()
57
+ self._apply_theme()
58
+ self._build_layout()
59
+ self._auto_populate_defaults()
60
+ self._update_cli_preview()
61
+ self.root.after(100, self._poll_queue)
62
+
63
+ def _init_vars(self) -> None:
64
+ self.gguf_var = tk.StringVar()
65
+ self.imatrix_var = tk.StringVar()
66
+ self.output_var = tk.StringVar()
67
+ self.mode_var = tk.StringVar(value="all")
68
+ self.filter_var = tk.StringVar()
69
+
70
+ self.optimizer_var = tk.StringVar(value="hpc")
71
+ self.cand_d_var = tk.IntVar(value=6)
72
+ self.cand_m_var = tk.IntVar(value=6)
73
+ self.starts_var = tk.IntVar(value=2)
74
+ self.iters_var = tk.IntVar(value=8)
75
+ self.ulp_var = tk.IntVar(value=4)
76
+ self.limit_var = tk.IntVar(value=0)
77
+
78
+ self.use_analogs_var = tk.BooleanVar(value=True)
79
+ self.coupled_q2k_var = tk.BooleanVar(value=True)
80
+ self.joint_subscales_var = tk.BooleanVar(value=True)
81
+ self.use_spectral_var = tk.BooleanVar(value=True)
82
+ self.subblock_sieve_var = tk.BooleanVar(value=True)
83
+ self.factor_floor_var = tk.BooleanVar(value=True)
84
+ self.cand_floor_var = tk.BooleanVar(value=True)
85
+ self.dc_carry_var = tk.BooleanVar(value=True)
86
+
87
+ self.dc_lambda_var = tk.DoubleVar(value=1.0)
88
+ self.vw_lambda_var = tk.DoubleVar(value=1.0)
89
+ self.dc_decay_var = tk.DoubleVar(value=0.85)
90
+ self.sse_budget_var = tk.DoubleVar(value=0.0005)
91
+
92
+ self.cli_preview_var = tk.StringVar()
93
+ self.status_var = tk.StringVar(value="Ready.")
94
+ self.progress_var = tk.DoubleVar(value=0.0)
95
+
96
+ for v in (
97
+ self.gguf_var,
98
+ self.imatrix_var,
99
+ self.output_var,
100
+ self.mode_var,
101
+ self.optimizer_var,
102
+ self.cand_d_var,
103
+ self.cand_m_var,
104
+ self.limit_var,
105
+ self.use_spectral_var,
106
+ ):
107
+ v.trace_add("write", lambda *_: self._update_cli_preview())
108
+
109
+ def _apply_theme(self) -> None:
110
+ style = ttk.Style(self.root)
111
+ if "clam" in style.theme_names():
112
+ style.theme_use("clam")
113
+ bg = "#141820"
114
+ panel = "#1c222d"
115
+ fg = "#e6edf3"
116
+ accent = "#3b82f6"
117
+ self.root.configure(bg=bg)
118
+ style.configure(".", background=bg, foreground=fg, fieldbackground=panel)
119
+ style.configure("TFrame", background=bg)
120
+ style.configure("TLabelframe", background=bg, foreground=fg)
121
+ style.configure("TLabelframe.Label", background=bg, foreground="#93c5fd", font=("Sans", 10, "bold"))
122
+ style.configure("TLabel", background=bg, foreground=fg)
123
+ style.configure("TCheckbutton", background=bg, foreground=fg)
124
+ style.configure("TRadiobutton", background=bg, foreground=fg)
125
+ style.configure("TButton", background=panel, foreground=fg, padding=(10, 5))
126
+ style.map("TButton", background=[("active", accent)])
127
+ style.configure("Accent.TButton", background=accent, foreground="#ffffff", font=("Sans", 10, "bold"))
128
+ style.configure(
129
+ "Treeview",
130
+ background=panel,
131
+ fieldbackground=panel,
132
+ foreground=fg,
133
+ rowheight=24,
134
+ )
135
+ style.configure("Treeview.Heading", background="#262f3f", foreground=fg, font=("Sans", 9, "bold"))
136
+ style.map("Treeview", background=[("selected", accent)])
137
+
138
+ def _build_layout(self) -> None:
139
+ main = ttk.Frame(self.root, padding=10)
140
+ main.pack(fill=tk.BOTH, expand=True)
141
+
142
+ # 1. File Selection Frame
143
+ files_frame = ttk.LabelFrame(main, text="1. GGUF & iMatrix Paths", padding=10)
144
+ files_frame.pack(fill=tk.X, pady=(0, 8))
145
+ files_frame.columnconfigure(1, weight=1)
146
+
147
+ ttk.Label(files_frame, text="Source GGUF:").grid(row=0, column=0, sticky=tk.W, padx=(0, 8), pady=3)
148
+ ttk.Entry(files_frame, textvariable=self.gguf_var).grid(row=0, column=1, sticky=tk.EW, pady=3)
149
+ ttk.Button(files_frame, text="Browse...", command=self._browse_gguf).grid(row=0, column=2, padx=4, pady=3)
150
+ ttk.Button(files_frame, text="Inspect GGUF", command=self.inspect_files).grid(row=0, column=3, pady=3)
151
+
152
+ ttk.Label(files_frame, text="iMatrix (.gguf/.dat):").grid(row=1, column=0, sticky=tk.W, padx=(0, 8), pady=3)
153
+ ttk.Entry(files_frame, textvariable=self.imatrix_var).grid(row=1, column=1, sticky=tk.EW, pady=3)
154
+ ttk.Button(files_frame, text="Browse...", command=self._browse_imatrix).grid(row=1, column=2, padx=4, pady=3)
155
+ ttk.Button(files_frame, text="Clear", command=lambda: self.imatrix_var.set("")).grid(row=1, column=3, pady=3)
156
+
157
+ ttk.Label(files_frame, text="Output (.gguf/.npz):").grid(row=2, column=0, sticky=tk.W, padx=(0, 8), pady=3)
158
+ ttk.Entry(files_frame, textvariable=self.output_var).grid(row=2, column=1, sticky=tk.EW, pady=3)
159
+ ttk.Button(files_frame, text="Save As...", command=self._browse_output).grid(row=2, column=2, padx=4, pady=3)
160
+
161
+ # 2. Middle Split: Left = Settings, Right = Tensor Browser
162
+ mid = ttk.PanedWindow(main, orient=tk.HORIZONTAL)
163
+ mid.pack(fill=tk.BOTH, expand=True, pady=(0, 8))
164
+
165
+ left_pane = ttk.Frame(mid)
166
+ right_pane = ttk.Frame(mid)
167
+ mid.add(left_pane, weight=2)
168
+ mid.add(right_pane, weight=3)
169
+
170
+ # Left: Engine & Factorization Options
171
+ opt_frame = ttk.LabelFrame(left_pane, text="2. Factored Q2_K & Analog Controls", padding=10)
172
+ opt_frame.pack(fill=tk.BOTH, expand=True, padx=(0, 6))
173
+
174
+ mode_row = ttk.Frame(opt_frame)
175
+ mode_row.pack(fill=tk.X, pady=(0, 6))
176
+ ttk.Label(mode_row, text="Target Scope:").pack(side=tk.LEFT, padx=(0, 8))
177
+ ttk.Radiobutton(
178
+ mode_row, text="All Q2_K-Eligible Tensors", variable=self.mode_var, value="all"
179
+ ).pack(side=tk.LEFT, padx=4)
180
+ ttk.Radiobutton(
181
+ mode_row, text="Selected Tensor(s)", variable=self.mode_var, value="selected"
182
+ ).pack(side=tk.LEFT, padx=4)
183
+
184
+ opt_row = ttk.Frame(opt_frame)
185
+ opt_row.pack(fill=tk.X, pady=(0, 6))
186
+ ttk.Label(opt_row, text="Global Optimizer:").pack(side=tk.LEFT, padx=(0, 8))
187
+ ttk.Radiobutton(
188
+ opt_row, text="HPC (Sieve + 36-State Viterbi)", variable=self.optimizer_var, value="hpc"
189
+ ).pack(side=tk.LEFT, padx=4)
190
+ ttk.Radiobutton(
191
+ opt_row, text="MSE (Local Greedy)", variable=self.optimizer_var, value="mse"
192
+ ).pack(side=tk.LEFT, padx=4)
193
+
194
+ checks = ttk.Frame(opt_frame)
195
+ checks.pack(fill=tk.X, pady=4)
196
+ ttk.Checkbutton(
197
+ checks,
198
+ text="Geometry-Preserving Weight Analogs (Iso-Moment + Vesica Gram + Subscale Lattice)",
199
+ variable=self.use_analogs_var,
200
+ ).pack(anchor=tk.W, pady=2)
201
+ ttk.Checkbutton(
202
+ checks,
203
+ text="Coupled Orthogonal-Basis Bilinear Q2_K Factorization",
204
+ variable=self.coupled_q2k_var,
205
+ ).pack(anchor=tk.W, pady=2)
206
+ ttk.Checkbutton(
207
+ checks,
208
+ text="Exact Joint (Ls, Lm) 16x16 Sub-block Scan",
209
+ variable=self.joint_subscales_var,
210
+ ).pack(anchor=tk.W, pady=2)
211
+ ttk.Checkbutton(
212
+ checks,
213
+ text="Spectral Fold / Vesica / DC Objective Penalty",
214
+ variable=self.use_spectral_var,
215
+ ).pack(anchor=tk.W, pady=2)
216
+ ttk.Checkbutton(
217
+ checks,
218
+ text="Sub-block 6-State Sieve & Rolling DC Residual Carry",
219
+ variable=self.subblock_sieve_var,
220
+ ).pack(anchor=tk.W, pady=2)
221
+ ttk.Checkbutton(
222
+ checks,
223
+ text="Local-Safety Candidate & Factored Floors (Monotone Bound)",
224
+ variable=self.factor_floor_var,
225
+ ).pack(anchor=tk.W, pady=2)
226
+
227
+ grid_f = ttk.Frame(opt_frame)
228
+ grid_f.pack(fill=tk.X, pady=6)
229
+ for col in range(4):
230
+ grid_f.columnconfigure(col, weight=1)
231
+
232
+ self._spin_pair(grid_f, 0, 0, "Cand d (1-24):", self.cand_d_var, 1, 24)
233
+ self._spin_pair(grid_f, 0, 2, "Cand dmin (1-24):", self.cand_m_var, 1, 24)
234
+ self._spin_pair(grid_f, 1, 0, "Solver Starts:", self.starts_var, 1, 8)
235
+ self._spin_pair(grid_f, 1, 2, "Solver Iters:", self.iters_var, 1, 32)
236
+ self._spin_pair(grid_f, 2, 0, "FP16 ULP Radius:", self.ulp_var, 0, 16)
237
+ self._spin_pair(grid_f, 2, 2, "Tensor Limit (0=all):", self.limit_var, 0, 9999)
238
+
239
+ spec_f = ttk.LabelFrame(opt_frame, text="Spectral & DC Parameters", padding=6)
240
+ spec_f.pack(fill=tk.X, pady=4)
241
+ for col in range(4):
242
+ spec_f.columnconfigure(col, weight=1)
243
+ ttk.Label(spec_f, text="DC Lambda:").grid(row=0, column=0, sticky=tk.W, padx=4, pady=2)
244
+ ttk.Entry(spec_f, textvariable=self.dc_lambda_var, width=8).grid(row=0, column=1, sticky=tk.EW, padx=4, pady=2)
245
+ ttk.Label(spec_f, text="Vesica Lambda:").grid(row=0, column=2, sticky=tk.W, padx=4, pady=2)
246
+ ttk.Entry(spec_f, textvariable=self.vw_lambda_var, width=8).grid(row=0, column=3, sticky=tk.EW, padx=4, pady=2)
247
+ ttk.Label(spec_f, text="DC Decay:").grid(row=1, column=0, sticky=tk.W, padx=4, pady=2)
248
+ ttk.Entry(spec_f, textvariable=self.dc_decay_var, width=8).grid(row=1, column=1, sticky=tk.EW, padx=4, pady=2)
249
+ ttk.Label(spec_f, text="SSE Budget:").grid(row=1, column=2, sticky=tk.W, padx=4, pady=2)
250
+ ttk.Entry(spec_f, textvariable=self.sse_budget_var, width=8).grid(row=1, column=3, sticky=tk.EW, padx=4, pady=2)
251
+
252
+ # Right: GGUF Tensor Browser
253
+ browser_frame = ttk.LabelFrame(right_pane, text="3. GGUF Tensor Inspector", padding=10)
254
+ browser_frame.pack(fill=tk.BOTH, expand=True)
255
+
256
+ search_row = ttk.Frame(browser_frame)
257
+ search_row.pack(fill=tk.X, pady=(0, 6))
258
+ ttk.Label(search_row, text="Filter:").pack(side=tk.LEFT, padx=(0, 6))
259
+ filter_entry = ttk.Entry(search_row, textvariable=self.filter_var)
260
+ filter_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
261
+ filter_entry.bind("<KeyRelease>", lambda _e: self._populate_tensor_tree())
262
+ ttk.Button(
263
+ search_row, text="Select Eligible", command=self._select_all_eligible
264
+ ).pack(side=tk.LEFT, padx=4)
265
+
266
+ cols = ("name", "type", "dims", "row", "q2k", "imat")
267
+ self.tree = ttk.Treeview(browser_frame, columns=cols, show="headings", selectmode="extended")
268
+ self.tree.heading("name", text="Tensor Name")
269
+ self.tree.heading("type", text="Type")
270
+ self.tree.heading("dims", text="Dims")
271
+ self.tree.heading("row", text="Row Width")
272
+ self.tree.heading("q2k", text="Q2_K")
273
+ self.tree.heading("imat", text="iMatrix")
274
+
275
+ self.tree.column("name", width=250, anchor=tk.W)
276
+ self.tree.column("type", width=60, anchor=tk.CENTER)
277
+ self.tree.column("dims", width=110, anchor=tk.CENTER)
278
+ self.tree.column("row", width=75, anchor=tk.E)
279
+ self.tree.column("q2k", width=55, anchor=tk.CENTER)
280
+ self.tree.column("imat", width=65, anchor=tk.CENTER)
281
+ self.tree.bind("<<TreeviewSelect>>", lambda _e: self._update_cli_preview())
282
+
283
+ tree_scroll = ttk.Scrollbar(browser_frame, orient=tk.VERTICAL, command=self.tree.yview)
284
+ self.tree.configure(yscrollcommand=tree_scroll.set)
285
+ self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
286
+ tree_scroll.pack(side=tk.RIGHT, fill=tk.Y)
287
+
288
+ # 4. Equivalent CLI Command Preview
289
+ cli_frame = ttk.LabelFrame(main, text="4. Equivalent Terminal Command", padding=8)
290
+ cli_frame.pack(fill=tk.X, pady=(0, 8))
291
+ ttk.Entry(cli_frame, textvariable=self.cli_preview_var, state="readonly").pack(
292
+ side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 6)
293
+ )
294
+ ttk.Button(cli_frame, text="Copy Command", command=self._copy_cli).pack(side=tk.RIGHT)
295
+
296
+ # 5. Action Buttons + Progress + Log Console
297
+ bottom = ttk.LabelFrame(main, text="5. Execution & Live Output", padding=8)
298
+ bottom.pack(fill=tk.BOTH, expand=False)
299
+
300
+ btn_row = ttk.Frame(bottom)
301
+ btn_row.pack(fill=tk.X, pady=(0, 6))
302
+ self.run_btn = ttk.Button(
303
+ btn_row, text="Run Quantization", style="Accent.TButton", command=self.start_quantization
304
+ )
305
+ self.run_btn.pack(side=tk.LEFT, padx=(0, 6))
306
+ self.stop_btn = ttk.Button(
307
+ btn_row, text="Cancel", state=tk.DISABLED, command=self.stop_worker
308
+ )
309
+ self.stop_btn.pack(side=tk.LEFT, padx=(0, 6))
310
+ self.bench_btn = ttk.Button(
311
+ btn_row, text="Run Solver Benchmark", command=self.start_benchmark
312
+ )
313
+ self.bench_btn.pack(side=tk.LEFT, padx=(0, 12))
314
+ ttk.Label(btn_row, textvariable=self.status_var).pack(side=tk.LEFT, fill=tk.X, expand=True)
315
+
316
+ self.progress = ttk.Progressbar(bottom, variable=self.progress_var, maximum=100.0)
317
+ self.progress.pack(fill=tk.X, pady=(0, 6))
318
+
319
+ log_frame = ttk.Frame(bottom)
320
+ log_frame.pack(fill=tk.BOTH, expand=True)
321
+ self.log_text = tk.Text(
322
+ log_frame,
323
+ height=9,
324
+ bg="#0f131a",
325
+ fg="#d1d5db",
326
+ insertbackground="#ffffff",
327
+ font=("Monospace", 9),
328
+ wrap=tk.WORD,
329
+ )
330
+ log_scroll = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self.log_text.yview)
331
+ self.log_text.configure(yscrollcommand=log_scroll.set)
332
+ self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
333
+ log_scroll.pack(side=tk.RIGHT, fill=tk.Y)
334
+
335
+ def _spin_pair(self, parent, row, col, label, var, low, high) -> None:
336
+ ttk.Label(parent, text=label).grid(row=row, column=col, sticky=tk.W, padx=4, pady=2)
337
+ sp = ttk.Spinbox(parent, from_=low, to=high, textvariable=var, width=6)
338
+ sp.grid(row=row, column=col + 1, sticky=tk.EW, padx=4, pady=2)
339
+
340
+ def _auto_populate_defaults(self) -> None:
341
+ default_gguf = Path("Raon.gguf")
342
+ default_imat = Path("imatrix.gguf")
343
+ if default_imat.exists():
344
+ self.imatrix_var.set(str(default_imat.resolve()))
345
+ if default_gguf.exists():
346
+ self.gguf_var.set(str(default_gguf.resolve()))
347
+ self.output_var.set(str(default_gguf.with_name("Raon-Q2K.gguf").resolve()))
348
+ self.inspect_files()
349
+
350
+ def _browse_gguf(self) -> None:
351
+ path = filedialog.askopenfilename(
352
+ title="Select Source GGUF",
353
+ filetypes=[("GGUF Model", "*.gguf"), ("All Files", "*.*")],
354
+ )
355
+ if path:
356
+ self.gguf_var.set(path)
357
+ p = Path(path)
358
+ if not self.output_var.get():
359
+ self.output_var.set(str(p.with_name(f"{p.stem}-Q2K.gguf")))
360
+ self.inspect_files()
361
+
362
+ def _browse_imatrix(self) -> None:
363
+ path = filedialog.askopenfilename(
364
+ title="Select iMatrix File",
365
+ filetypes=[("iMatrix Files", "*.gguf *.dat *.imatrix"), ("All Files", "*.*")],
366
+ )
367
+ if path:
368
+ self.imatrix_var.set(path)
369
+ self.inspect_files()
370
+
371
+ def _browse_output(self) -> None:
372
+ path = filedialog.asksaveasfilename(
373
+ title="Select Output File",
374
+ defaultextension=".gguf",
375
+ filetypes=[("GGUF Model", "*.gguf"), ("NumPy Archive", "*.npz"), ("All Files", "*.*")],
376
+ )
377
+ if path:
378
+ self.output_var.set(path)
379
+
380
+ def _log(self, msg: str) -> None:
381
+ self.log_text.insert(tk.END, msg + "\n")
382
+ self.log_text.see(tk.END)
383
+
384
+ def build_config(self) -> HPCConfig:
385
+ cfg = HPCConfig(
386
+ n_cand_d=int(self.cand_d_var.get()),
387
+ n_cand_m=int(self.cand_m_var.get()),
388
+ factor_starts=int(self.starts_var.get()),
389
+ factor_standalone_iters=int(self.iters_var.get()),
390
+ ulp_radius=int(self.ulp_var.get()),
391
+ optimizer=self.optimizer_var.get(),
392
+ use_weight_analogs=bool(self.use_analogs_var.get()),
393
+ factor_coupled_q2k=bool(self.coupled_q2k_var.get()),
394
+ factor_joint_subscales=bool(self.joint_subscales_var.get()),
395
+ use_spectral=bool(self.use_spectral_var.get()),
396
+ subblock_sieve=bool(self.subblock_sieve_var.get()),
397
+ use_factor_floor=bool(self.factor_floor_var.get()),
398
+ use_candidate_floor=bool(self.cand_floor_var.get()),
399
+ apply_dc_carry=bool(self.dc_carry_var.get()),
400
+ dc_lambda=float(self.dc_lambda_var.get()),
401
+ vw_lambda=float(self.vw_lambda_var.get()),
402
+ dc_decay=float(self.dc_decay_var.get()),
403
+ sse_budget=float(self.sse_budget_var.get()),
404
+ verbose=False,
405
+ )
406
+ return cfg.validate()
407
+
408
+ def build_cli_command(self) -> str:
409
+ cmd = ["python3", "-m", "hpc_quantize_v2"]
410
+ gguf = self.gguf_var.get().strip()
411
+ if gguf:
412
+ cmd += ["--gguf", gguf]
413
+ selected = self._selected_tensor_names()
414
+ if self.mode_var.get() == "selected" and len(selected) == 1:
415
+ cmd += ["--tensor", next(iter(selected))]
416
+ else:
417
+ cmd.append("--all-q2k")
418
+ imat = self.imatrix_var.get().strip()
419
+ if imat:
420
+ cmd += ["--imatrix-gguf", imat]
421
+ out = self.output_var.get().strip()
422
+ if out:
423
+ cmd += ["--output", out]
424
+ if self.limit_var.get() > 0:
425
+ cmd += ["--limit", str(self.limit_var.get())]
426
+ cmd += ["--cand-d", str(self.cand_d_var.get()), "--cand-m", str(self.cand_m_var.get())]
427
+ if self.optimizer_var.get() == "mse":
428
+ cmd.append("--mse")
429
+ if not self.use_spectral_var.get():
430
+ cmd.append("--no-spectral")
431
+ cmd.append("--verbose")
432
+ return " ".join(shlex.quote(part) for part in cmd)
433
+
434
+ def _update_cli_preview(self) -> None:
435
+ try:
436
+ self.cli_preview_var.set(self.build_cli_command())
437
+ except Exception:
438
+ pass
439
+
440
+ def _copy_cli(self) -> None:
441
+ cmd = self.cli_preview_var.get()
442
+ self.root.clipboard_clear()
443
+ self.root.clipboard_append(cmd)
444
+ self.status_var.set("Copied CLI command to clipboard.")
445
+
446
+ def inspect_files(self) -> None:
447
+ imat_path = self.imatrix_var.get().strip()
448
+ if imat_path and Path(imat_path).exists():
449
+ try:
450
+ self._imatrix = read_imatrix(imat_path)
451
+ self._log(f"Loaded iMatrix: {len(self._imatrix)} entries from {imat_path}")
452
+ except Exception as exc:
453
+ self._imatrix = None
454
+ self._log(f"iMatrix load error: {exc}")
455
+ else:
456
+ self._imatrix = None
457
+
458
+ gguf_path = self.gguf_var.get().strip()
459
+ if not gguf_path or not Path(gguf_path).exists():
460
+ return
461
+ try:
462
+ self._reader = GGUFReader(gguf_path)
463
+ eligible = sum(1 for t in self._reader.tensors if is_q2k_eligible(t.name, t.dims))
464
+ self._log(
465
+ f"Inspected {gguf_path}: {len(self._reader.tensors)} tensors "
466
+ f"({eligible} Q2_K-eligible), {len(self._reader.metadata)} metadata keys."
467
+ )
468
+ self.status_var.set(f"Loaded {len(self._reader.tensors)} tensors ({eligible} Q2_K-eligible).")
469
+ self._populate_tensor_tree()
470
+ except Exception as exc:
471
+ self._reader = None
472
+ self._log(f"GGUF inspect error: {exc}")
473
+
474
+ def _populate_tensor_tree(self) -> None:
475
+ self.tree.delete(*self.tree.get_children())
476
+ if self._reader is None:
477
+ return
478
+ filt = self.filter_var.get().strip().lower()
479
+ for ti in self._reader.tensors:
480
+ if filt and filt not in ti.name.lower() and filt not in ti.type_name.lower():
481
+ continue
482
+ elig = is_q2k_eligible(ti.name, ti.dims)
483
+ has_imat = (
484
+ self._imatrix is not None
485
+ and lookup_imatrix(self._imatrix, ti.name) is not None
486
+ )
487
+ self.tree.insert(
488
+ "",
489
+ tk.END,
490
+ iid=ti.name,
491
+ values=(
492
+ ti.name,
493
+ ti.type_name,
494
+ str(ti.dims),
495
+ ti.row_width,
496
+ "Yes" if elig else "No",
497
+ "Yes" if has_imat else "-",
498
+ ),
499
+ )
500
+
501
+ def _select_all_eligible(self) -> None:
502
+ if self._reader is None:
503
+ return
504
+ eligible_ids = [
505
+ ti.name
506
+ for ti in self._reader.tensors
507
+ if is_q2k_eligible(ti.name, ti.dims) and self.tree.exists(ti.name)
508
+ ]
509
+ self.tree.selection_set(eligible_ids)
510
+
511
+ def _selected_tensor_names(self) -> set[str]:
512
+ return set(self.tree.selection())
513
+
514
+ def stop_worker(self) -> None:
515
+ if self._worker and self._worker.is_alive():
516
+ self._stop_event.set()
517
+ self.status_var.set("Cancelling after current tensor...")
518
+
519
+ def start_quantization(self) -> None:
520
+ if self._worker and self._worker.is_alive():
521
+ return
522
+ gguf_path = self.gguf_var.get().strip()
523
+ if not gguf_path or not Path(gguf_path).exists():
524
+ messagebox.showerror("Missing Source GGUF", "Please select a valid source GGUF file.")
525
+ return
526
+ out_path = self.output_var.get().strip()
527
+ if not out_path:
528
+ p = Path(gguf_path)
529
+ out_path = str(p.with_name(f"{p.stem}-Q2K.gguf"))
530
+ self.output_var.set(out_path)
531
+ try:
532
+ cfg = self.build_config()
533
+ except Exception as exc:
534
+ messagebox.showerror("Invalid Configuration", str(exc))
535
+ return
536
+
537
+ selected = self._selected_tensor_names() if self.mode_var.get() == "selected" else None
538
+ if self.mode_var.get() == "selected" and not selected:
539
+ messagebox.showwarning("No Tensor Selected", "Select one or more tensors in the table first.")
540
+ return
541
+
542
+ imat_path = self.imatrix_var.get().strip()
543
+ limit = int(self.limit_var.get())
544
+
545
+ self._stop_event.clear()
546
+ self.progress_var.set(0.0)
547
+ self.run_btn.configure(state=tk.DISABLED)
548
+ self.bench_btn.configure(state=tk.DISABLED)
549
+ self.stop_btn.configure(state=tk.NORMAL)
550
+ self.status_var.set("Running quantization...")
551
+
552
+ self._worker = threading.Thread(
553
+ target=self._quantize_worker,
554
+ args=(gguf_path, imat_path, out_path, cfg, selected, limit),
555
+ daemon=True,
556
+ )
557
+ self._worker.start()
558
+
559
+ def _quantize_worker(
560
+ self,
561
+ gguf_path: str,
562
+ imat_path: str,
563
+ out_path: str,
564
+ cfg: HPCConfig,
565
+ selected: set[str] | None,
566
+ limit: int,
567
+ ) -> None:
568
+ t0 = time.time()
569
+ try:
570
+ imat = None
571
+ if imat_path and Path(imat_path).exists():
572
+ imat = read_imatrix(imat_path)
573
+ self._log_queue.put(("log", f"Loaded iMatrix ({len(imat)} tensors) from {imat_path}"))
574
+
575
+ quantizer = Q2KQuantizer(cfg)
576
+ if out_path.lower().endswith(".npz") and selected and len(selected) == 1:
577
+ reader = GGUFReader(gguf_path)
578
+ tname = next(iter(selected))
579
+ x, row = reader.read_tensor(tname)
580
+ imp = None
581
+ if imat is not None:
582
+ raw = lookup_imatrix(imat, tname)
583
+ if raw is not None:
584
+ imp = expand_importance(raw, x.size, row)
585
+ res = quantizer.quantize(x, importance=imp, row_width=row)
586
+ np.savez_compressed(
587
+ out_path,
588
+ scales=res.scales,
589
+ qs=res.qs,
590
+ dm=res.dm,
591
+ dequantized=res.dequantize(),
592
+ rmse=np.float32(res.stats.rmse),
593
+ )
594
+ self._log_queue.put(
595
+ ("done", f"Saved {out_path} (RMSE={res.stats.rmse:.6e}, {time.time() - t0:.1f}s)")
596
+ )
597
+ return
598
+
599
+ def on_progress(done: int, total: int, name: str, action: str, rmse: float | None, has_imat: bool):
600
+ pct = 100.0 * done / max(total, 1)
601
+ self._log_queue.put(("progress", (pct, f"[{done}/{total}] {action} {name}")))
602
+ if action == "Q2_K" and rmse is not None:
603
+ self._log_queue.put(
604
+ (
605
+ "log",
606
+ f"[{done}/{total}] Q2_K {name:44s} RMSE={rmse:.6e} iMatrix={'yes' if has_imat else 'no'}",
607
+ )
608
+ )
609
+
610
+ stats = write_quantized_gguf(
611
+ gguf_path,
612
+ out_path,
613
+ quantizer,
614
+ imat=imat,
615
+ quantize_all=(selected is None),
616
+ tensor_names=selected,
617
+ limit=limit,
618
+ verbose=False,
619
+ progress_callback=on_progress,
620
+ stop_event=self._stop_event,
621
+ )
622
+ dt = time.time() - t0
623
+ self._log_queue.put(
624
+ (
625
+ "done",
626
+ f"Wrote {out_path}: {int(stats['quantized'])}/{int(stats['tensors'])} tensors Q2_K, "
627
+ f"overall RMSE={stats['rmse']:.6e} ({dt:.1f}s)",
628
+ )
629
+ )
630
+ except InterruptedError as exc:
631
+ self._log_queue.put(("cancelled", str(exc)))
632
+ except Exception as exc:
633
+ self._log_queue.put(("error", str(exc)))
634
+
635
+ def start_benchmark(self) -> None:
636
+ if self._worker and self._worker.is_alive():
637
+ return
638
+ try:
639
+ cfg = self.build_config()
640
+ except Exception as exc:
641
+ messagebox.showerror("Invalid Configuration", str(exc))
642
+ return
643
+ self._stop_event.clear()
644
+ self.progress_var.set(0.0)
645
+ self.run_btn.configure(state=tk.DISABLED)
646
+ self.bench_btn.configure(state=tk.DISABLED)
647
+ self.stop_btn.configure(state=tk.NORMAL)
648
+ self.status_var.set("Running factored solver benchmark...")
649
+ self._worker = threading.Thread(target=self._bench_worker, args=(cfg,), daemon=True)
650
+ self._worker.start()
651
+
652
+ def _bench_worker(self, cfg: HPCConfig) -> None:
653
+ from collections import defaultdict
654
+
655
+ try:
656
+ cases = synthetic_cases(123, 8)
657
+ gains = defaultdict(list)
658
+ total = len(cases)
659
+ for idx, (kind, x) in enumerate(cases, 1):
660
+ if self._stop_event.is_set():
661
+ raise InterruptedError("Benchmark cancelled.")
662
+ w = np.ones(x.size, dtype=np.float32)
663
+ v2 = factor_only_solve(x, w, cfg)
664
+ ref_obj, _ = reference_solve(x, w, cfg, cfg.use_spectral)
665
+ gains[kind].append((ref_obj - v2.objective) / max(abs(ref_obj), 1.0e-30))
666
+ self._log_queue.put(("progress", (100.0 * idx / total, f"Bench {idx}/{total} ({kind})")))
667
+
668
+ self._log_queue.put(("log", f"--- Benchmark (spectral={cfg.use_spectral}, analogs={cfg.use_weight_analogs}) ---"))
669
+ for kind, vals in gains.items():
670
+ arr = np.asarray(vals)
671
+ self._log_queue.put(
672
+ (
673
+ "log",
674
+ f" {kind:14s} mean={arr.mean()*100:6.2f}% median={np.median(arr)*100:6.2f}% "
675
+ f"min={arr.min()*100:6.2f}% worse={int(np.sum(arr < -1e-9))}/{arr.size}",
676
+ )
677
+ )
678
+ self._log_queue.put(("done", "Benchmark complete."))
679
+ except InterruptedError as exc:
680
+ self._log_queue.put(("cancelled", str(exc)))
681
+ except Exception as exc:
682
+ self._log_queue.put(("error", str(exc)))
683
+
684
+ def _poll_queue(self) -> None:
685
+ while True:
686
+ try:
687
+ kind, payload = self._log_queue.get_nowait()
688
+ except queue.Empty:
689
+ break
690
+ if kind == "log":
691
+ self._log(str(payload))
692
+ elif kind == "progress":
693
+ pct, msg = payload # type: ignore[misc]
694
+ self.progress_var.set(float(pct))
695
+ self.status_var.set(str(msg))
696
+ elif kind == "done":
697
+ self.progress_var.set(100.0)
698
+ self.status_var.set(str(payload))
699
+ self._log(str(payload))
700
+ self._reset_buttons()
701
+ elif kind in ("cancelled", "error"):
702
+ self.status_var.set(f"{kind.upper()}: {payload}")
703
+ self._log(f"{kind.upper()}: {payload}")
704
+ self._reset_buttons()
705
+ self.root.after(100, self._poll_queue)
706
+
707
+ def _reset_buttons(self) -> None:
708
+ self.run_btn.configure(state=tk.NORMAL)
709
+ self.bench_btn.configure(state=tk.NORMAL)
710
+ self.stop_btn.configure(state=tk.DISABLED)
711
+
712
+
713
+ def main() -> None:
714
+ parser = argparse.ArgumentParser(description="HPC-Quantize V2 GUI")
715
+ parser.add_argument("--gguf", help="pre-populate source GGUF path")
716
+ parser.add_argument("--imatrix", help="pre-populate iMatrix path")
717
+ parser.add_argument("--output", help="pre-populate output GGUF path")
718
+ parser.add_argument("--check", action="store_true", help="validate GUI bindings without opening a window")
719
+ args = parser.parse_args()
720
+
721
+ if args.check:
722
+ cfg = HPCConfig().validate()
723
+ print("GUI check OK:", cfg)
724
+ return
725
+
726
+ root = tk.Tk()
727
+ app = HPCQuantizeGUI(root)
728
+ if args.gguf:
729
+ app.gguf_var.set(args.gguf)
730
+ if args.imatrix:
731
+ app.imatrix_var.set(args.imatrix)
732
+ if args.output:
733
+ app.output_var.set(args.output)
734
+ if args.gguf or args.imatrix:
735
+ app.inspect_files()
736
+ root.mainloop()
737
+
738
+
739
+ if __name__ == "__main__":
740
+ main()