cronos3k commited on
Commit
45d8b0a
·
verified ·
1 Parent(s): 3d5c035

Add LARQL Explorer: Gradio UI + Docker build

Browse files
Files changed (5) hide show
  1. Dockerfile +35 -0
  2. README.md +41 -3
  3. app.py +562 -0
  4. requirements.txt +2 -0
  5. utils.py +178 -0
Dockerfile ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 1: Build the larql binary from source
2
+ FROM rust:1.82-slim-bookworm AS builder
3
+
4
+ RUN apt-get update && apt-get install -y \
5
+ git pkg-config libssl-dev ca-certificates \
6
+ && rm -rf /var/lib/apt/lists/*
7
+
8
+ # Clone the fork (CPU-only build; no CUDA toolkit needed in the Space)
9
+ RUN git clone --depth 1 https://github.com/cronos3k/larql /build
10
+ WORKDIR /build
11
+
12
+ # Build only the CLI crate (avoids protobuf issues in larql-server)
13
+ RUN cargo build --release -p larql-cli
14
+
15
+ # Stage 2: Runtime image
16
+ FROM python:3.11-slim-bookworm
17
+
18
+ # Copy the compiled binary
19
+ COPY --from=builder /build/target/release/larql /usr/local/bin/larql
20
+
21
+ # Copy the Gradio demo
22
+ COPY app.py utils.py requirements.txt /app/
23
+
24
+ WORKDIR /app
25
+ RUN pip install --no-cache-dir -r requirements.txt
26
+
27
+ # HuggingFace Spaces expects a non-root user with UID 1000
28
+ RUN useradd -m -u 1000 hfuser
29
+ USER hfuser
30
+
31
+ EXPOSE 7860
32
+ ENV GRADIO_SERVER_NAME=0.0.0.0
33
+ ENV GRADIO_SERVER_PORT=7860
34
+
35
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,10 +1,48 @@
1
  ---
2
  title: LARQL Explorer
3
- emoji: 💻
4
- colorFrom: blue
5
  colorTo: blue
6
  sdk: docker
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: LARQL Explorer
3
+ emoji: 🧠
4
+ colorFrom: purple
5
  colorTo: blue
6
  sdk: docker
7
  pinned: false
8
+ license: apache-2.0
9
+ short_description: Query neural network weights like a knowledge graph
10
  ---
11
 
12
+ # LARQL Explorer
13
+
14
+ **The model IS the database.** Browse transformer weight space as a knowledge graph —
15
+ no terminal, no LQL syntax needed.
16
+
17
+ Original LARQL system by **Chris Hayuk** — [chrishayuk/larql](https://github.com/chrishayuk/larql)
18
+ This UI + Windows/Linux/CUDA port by **Gregor Koch** — [cronos3k/larql](https://github.com/cronos3k/larql)
19
+
20
+ > The original was a command-line tool for macOS only.
21
+ > This fork opens it up: any platform, any hardware, any browser.
22
+
23
+ ## Features
24
+
25
+ | Tab | What it does |
26
+ |---|---|
27
+ | 🔍 Walk Explorer | See which FFN features fire for any prompt, layer by layer |
28
+ | 🧪 Knowledge Probe | Compare three prompts side-by-side at the same layer |
29
+ | 💻 LQL Console | Full LQL query interface with example buttons |
30
+ | 📊 Vindex Info | Model metadata + SHA256 checksum verification |
31
+ | ⬇️ Extract | Download + extract a model from HuggingFace Hub |
32
+ | ℹ️ Setup | Build instructions and environment info |
33
+
34
+ ## Running locally
35
+
36
+ ```bash
37
+ git clone https://github.com/cronos3k/larql
38
+ cd larql
39
+ cargo build --release # or --features cuda for NVIDIA GPU
40
+ pip install -r demo/requirements.txt
41
+ python demo/app.py
42
+ ```
43
+
44
+ ## Credits
45
+
46
+ - **LARQL / LQL / vindex format:** Chris Hayuk ([@chrishayuk](https://github.com/chrishayuk))
47
+ - **Gradio UI + cross-platform port:** Gregor Koch ([@cronos3k](https://github.com/cronos3k))
48
+ - License: Apache-2.0
app.py ADDED
@@ -0,0 +1,562 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LARQL Explorer — Gradio 6 demo
3
+ Browse neural network weights as a knowledge graph using LQL (Lazarus Query Language).
4
+
5
+ Built on top of: https://github.com/chrishayuk/larql (Chris Hayuk)
6
+ Fork / Windows + CUDA port: https://github.com/cronos3k/larql
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import json
12
+ import subprocess
13
+ from pathlib import Path
14
+
15
+ import gradio as gr
16
+ import pandas as pd
17
+
18
+ # Add demo dir to path so utils is importable both locally and on HF Spaces
19
+ sys.path.insert(0, str(Path(__file__).parent))
20
+ from utils import (
21
+ LARQL, larql_available, run_larql,
22
+ parse_walk_output, load_vindex_info, format_vindex_summary,
23
+ list_local_vindexes,
24
+ )
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Paths & defaults
28
+ # ---------------------------------------------------------------------------
29
+ REPO_ROOT = Path(__file__).parent.parent
30
+ MODELS_DIR = REPO_ROOT / "models"
31
+
32
+ def get_vindex_choices():
33
+ paths = list_local_vindexes(str(MODELS_DIR)) if MODELS_DIR.exists() else []
34
+ paths += list_local_vindexes(".")
35
+ # deduplicate
36
+ seen = set()
37
+ unique = []
38
+ for p in paths:
39
+ key = str(Path(p).resolve())
40
+ if key not in seen:
41
+ seen.add(key)
42
+ unique.append(p)
43
+ return unique if unique else ["(no vindexes found — enter path manually)"]
44
+
45
+ DEFAULT_VINDEX = get_vindex_choices()[0]
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Backend check banner
49
+ # ---------------------------------------------------------------------------
50
+ def binary_status_md() -> str:
51
+ if larql_available():
52
+ rc, out, err = run_larql("--version")
53
+ ver = (out or err).strip().split("\n")[0]
54
+ return f"✅ **larql binary found:** `{LARQL}` \n_Version: {ver}_"
55
+ return (
56
+ "⚠️ **larql binary not found.** \n"
57
+ "Build it with `cargo build --release` from the repo root, "
58
+ "or see the **Setup** tab for instructions."
59
+ )
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # Tab 1 — Walk Explorer
64
+ # ---------------------------------------------------------------------------
65
+ def do_walk(vindex_path, prompt, layer_from, layer_to, top_k):
66
+ if not prompt.strip():
67
+ return pd.DataFrame(), "Enter a prompt above."
68
+ if not vindex_path.strip():
69
+ return pd.DataFrame(), "Enter a vindex path."
70
+
71
+ layers_arg = f"{int(layer_from)}-{int(layer_to)}"
72
+ rc, out, err = run_larql(
73
+ "walk",
74
+ "--prompt", prompt,
75
+ "--index", vindex_path.strip(),
76
+ "--layers", layers_arg,
77
+ "--top-k", str(int(top_k)),
78
+ timeout=60,
79
+ )
80
+ combined = (out + "\n" + err).strip()
81
+ if rc != 0:
82
+ return pd.DataFrame(), f"**Error:**\n```\n{combined}\n```"
83
+
84
+ rows = parse_walk_output(combined)
85
+ if not rows:
86
+ return pd.DataFrame(), f"No features returned.\n\nRaw output:\n```\n{combined}\n```"
87
+
88
+ df = pd.DataFrame(rows)
89
+ # Summary footer from last line of output
90
+ summary = [l for l in combined.splitlines() if l.startswith("Walk:")]
91
+ status = summary[-1] if summary else ""
92
+ return df, f"✓ {status}"
93
+
94
+
95
+ def update_layer_max(vindex_path):
96
+ """Read num_layers from index.json to set sensible layer slider bounds."""
97
+ try:
98
+ info = load_vindex_info(vindex_path.strip())
99
+ n = info.get("num_layers", 24)
100
+ return gr.Slider(maximum=n - 1, value=n - 1), gr.Slider(maximum=n - 1, value=max(0, n - 4))
101
+ except Exception:
102
+ return gr.Slider(maximum=47), gr.Slider(maximum=47)
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Tab 2 — Knowledge Probe (side-by-side comparison)
107
+ # ---------------------------------------------------------------------------
108
+ def do_probe(vindex_path, prompt1, prompt2, prompt3, layer, top_k):
109
+ results = []
110
+ for prompt in [prompt1, prompt2, prompt3]:
111
+ if not prompt.strip():
112
+ results.append("_(empty)_")
113
+ continue
114
+ rc, out, err = run_larql(
115
+ "walk",
116
+ "--prompt", prompt,
117
+ "--index", vindex_path.strip(),
118
+ "--layers", str(int(layer)),
119
+ "--top-k", str(int(top_k)),
120
+ timeout=60,
121
+ )
122
+ combined = (out + "\n" + err).strip()
123
+ rows = parse_walk_output(combined)
124
+ if not rows:
125
+ results.append(f"```\n{combined[:400]}\n```")
126
+ continue
127
+ lines = [f"**Prompt:** _{prompt}_\n"]
128
+ for r in rows:
129
+ bar = "█" * int(abs(r["Gate"]) * 100) or "·"
130
+ arrow = "▲" if r["Gate"] > 0 else "▼"
131
+ lines.append(
132
+ f"`{r['Feature']}` {arrow} gate={r['Gate']:+.3f} "
133
+ f"hears=**\"{r['Hears']}\"** → {r['Top tokens (down)']}"
134
+ )
135
+ results.append("\n".join(lines))
136
+
137
+ return results[0], results[1], results[2]
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # Tab 3 — LQL Console
142
+ # ---------------------------------------------------------------------------
143
+ LQL_EXAMPLES = [
144
+ 'USE "{vindex}"; WALK "The capital of France is" TOP 10;',
145
+ 'USE "{vindex}"; WALK "Python is a programming" TOP 5;',
146
+ 'USE "{vindex}"; WALK "Shakespeare wrote" TOP 8;',
147
+ 'USE "{vindex}"; WALK "Water boils at 100 degrees" TOP 5;',
148
+ ]
149
+
150
+ def do_lql(vindex_path, statement):
151
+ if not statement.strip():
152
+ return "Enter an LQL statement."
153
+ # Auto-inject USE if the user forgot it
154
+ stmt = statement.strip()
155
+ if not stmt.upper().startswith("USE") and vindex_path.strip():
156
+ stmt = f'USE "{vindex_path.strip()}"; {stmt}'
157
+ rc, out, err = run_larql("lql", stmt, timeout=90)
158
+ combined = (out + "\n" + err).strip()
159
+ return combined if combined else "(no output)"
160
+
161
+
162
+ def fill_lql_example(vindex_path, example_template):
163
+ return example_template.replace("{vindex}", vindex_path.strip() or "path/to/your.vindex")
164
+
165
+
166
+ # ---------------------------------------------------------------------------
167
+ # Tab 4 — Vindex Info & Verify
168
+ # ---------------------------------------------------------------------------
169
+ def do_vindex_info(vindex_path):
170
+ path = vindex_path.strip()
171
+ if not path:
172
+ return "_Enter a vindex path._", "_—_"
173
+ info = load_vindex_info(path)
174
+ summary = format_vindex_summary(info, path)
175
+
176
+ # Run verify
177
+ rc, out, err = run_larql("verify", path, timeout=120)
178
+ verify_out = (out + "\n" + err).strip()
179
+ verify_md = f"```\n{verify_out}\n```"
180
+ return summary, verify_md
181
+
182
+
183
+ # ---------------------------------------------------------------------------
184
+ # Tab 5 — Extract / Download
185
+ # ---------------------------------------------------------------------------
186
+ def do_extract(model_id, output_name, level, hf_token):
187
+ if not model_id.strip():
188
+ return "Enter a HuggingFace model ID."
189
+ out_dir = str(MODELS_DIR / (output_name.strip() or model_id.split("/")[-1] + ".vindex"))
190
+ level_flag = {"Browse (smallest, ~0.5 GB)": "browse",
191
+ "Inference (~1 GB)": "inference",
192
+ "All (~2 GB)": "all"}[level]
193
+ env_extra = {}
194
+ if hf_token.strip():
195
+ env_extra["HF_TOKEN"] = hf_token.strip()
196
+ yield f"⏳ Extracting `{model_id}` → `{out_dir}` (level={level_flag})…\n\nThis can take 5–20 minutes."
197
+ rc, out, err = run_larql(
198
+ "extract-index", model_id.strip(),
199
+ "-o", out_dir,
200
+ "--level", level_flag,
201
+ timeout=1800,
202
+ env_extra=env_extra,
203
+ )
204
+ combined = (out + "\n" + err).strip()
205
+ if rc == 0:
206
+ yield f"✅ Done!\n\nVindex saved to: `{out_dir}`\n\n```\n{combined[-1000:]}\n```"
207
+ else:
208
+ yield f"❌ Failed (exit {rc})\n\n```\n{combined[-2000:]}\n```"
209
+
210
+
211
+ # ---------------------------------------------------------------------------
212
+ # Tab 6 — Setup / About
213
+ # ---------------------------------------------------------------------------
214
+ SETUP_MD = """
215
+ ## About LARQL
216
+
217
+ **LARQL** decompiles transformer models into a queryable format called a **vindex**,
218
+ then provides **LQL** (Lazarus Query Language) to browse and edit the model's knowledge —
219
+ without running a forward pass.
220
+
221
+ > _"The model IS the database."_
222
+
223
+ | Original work | [chrishayuk/larql](https://github.com/chrishayuk/larql) — **Chris Hayuk** |
224
+ |---|---|
225
+ | This fork | [cronos3k/larql](https://github.com/cronos3k/larql) — Windows/Linux + CUDA port |
226
+
227
+ ---
228
+
229
+ ## Build the binary (first time)
230
+
231
+ ```bash
232
+ # CPU only (works everywhere)
233
+ cargo build --release
234
+
235
+ # With NVIDIA CUDA GPU acceleration
236
+ cargo build --release --features cuda
237
+
238
+ # The binary ends up at:
239
+ # target/release/larql (Linux/macOS)
240
+ # target/release/larql.exe (Windows)
241
+ ```
242
+
243
+ **Requirements:** Rust stable (`rustup`), a C compiler (gcc/clang/MSVC).
244
+ For CUDA: CUDA 12.x toolkit installed and `nvcc` in PATH.
245
+
246
+ ---
247
+
248
+ ## Quick LQL reference
249
+
250
+ ```sql
251
+ -- Load a vindex
252
+ USE "path/to/model.vindex";
253
+
254
+ -- Walk: what features fire for this prompt?
255
+ WALK "The capital of France is" TOP 10;
256
+
257
+ -- Predict next token (needs --level inference or higher)
258
+ INFER "The capital of France is" TOP 5;
259
+
260
+ -- Edit knowledge
261
+ INSERT INTO EDGES (entity, relation, target)
262
+ VALUES ("Atlantis", "capital-of", "Poseidon");
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Running on HuggingFace Spaces
268
+
269
+ 1. Fork [cronos3k/larql](https://github.com/cronos3k/larql)
270
+ 2. Create a new Space (Gradio SDK)
271
+ 3. Add this `demo/` folder as your Space root
272
+ 4. Add a `setup.sh` that builds the binary (see the repo for the template)
273
+ 5. Upload a pre-extracted vindex as a Space dataset
274
+ """
275
+
276
+
277
+ # ---------------------------------------------------------------------------
278
+ # Build the Gradio app
279
+ # ---------------------------------------------------------------------------
280
+ _THEME = gr.themes.Soft(
281
+ primary_hue="violet",
282
+ secondary_hue="blue",
283
+ neutral_hue="slate",
284
+ )
285
+ _CSS = """
286
+ .feature-row-up { background: #f0fff4 !important; }
287
+ .feature-row-down { background: #fff5f5 !important; }
288
+ .larql-header { font-size: 1.6em; font-weight: bold; margin-bottom: 0.2em; }
289
+ footer { display: none !important; }
290
+ """
291
+
292
+ with gr.Blocks(title="LARQL Explorer") as demo:
293
+
294
+ # ── Header ──────────────────────────────────────────────────────────────
295
+ gr.HTML("""
296
+ <div style="text-align:center; padding: 1.2em 0 0.6em 0;">
297
+ <div style="font-size:2.2em; font-weight:800; letter-spacing:-1px;">
298
+ 🧠 LARQL Explorer
299
+ </div>
300
+ <div style="color:#666; font-size:1.05em; margin-top:0.3em;">
301
+ Query neural network weights like a graph database &mdash; no SQL needed
302
+ </div>
303
+ <div style="font-size:0.85em; margin-top:0.5em; color:#888;">
304
+ Based on <a href="https://github.com/chrishayuk/larql" target="_blank">chrishayuk/larql</a>
305
+ by Chris Hayuk &nbsp;·&nbsp;
306
+ Windows/CUDA fork: <a href="https://github.com/cronos3k/larql" target="_blank">cronos3k/larql</a>
307
+ </div>
308
+ </div>
309
+ """)
310
+
311
+ binary_status = gr.Markdown(binary_status_md())
312
+
313
+ # ── Shared vindex selector (visible at top) ─────────────────────────────
314
+ with gr.Row():
315
+ vindex_choices = get_vindex_choices()
316
+ vindex_dd = gr.Dropdown(
317
+ choices=vindex_choices,
318
+ value=vindex_choices[0],
319
+ label="Active vindex",
320
+ allow_custom_value=True,
321
+ scale=4,
322
+ info="Select a pre-extracted vindex or type a custom path",
323
+ )
324
+ refresh_btn = gr.Button("🔄 Refresh list", scale=1, variant="secondary")
325
+
326
+ def refresh_vindex_list():
327
+ choices = get_vindex_choices()
328
+ return gr.Dropdown(choices=choices, value=choices[0])
329
+
330
+ refresh_btn.click(refresh_vindex_list, outputs=vindex_dd)
331
+
332
+ # ── Tabs ─────────────────────────────────────────────────────────────────
333
+ with gr.Tabs():
334
+
335
+ # ── Tab 1: Walk Explorer ─────────────────────────────────────────────
336
+ with gr.Tab("🔍 Walk Explorer"):
337
+ gr.Markdown("""
338
+ **Walk the model:** for each layer in the range, find the FFN features
339
+ that fire most strongly for your prompt. Positive gate = the feature
340
+ *pushes* the residual stream toward its output tokens. Negative = it
341
+ *pulls* away.
342
+ """)
343
+ with gr.Row():
344
+ walk_prompt = gr.Textbox(
345
+ label="Prompt",
346
+ placeholder="The capital of France is",
347
+ scale=4,
348
+ )
349
+ walk_btn = gr.Button("Walk →", variant="primary", scale=1)
350
+
351
+ with gr.Row():
352
+ layer_from = gr.Slider(
353
+ minimum=0, maximum=23, value=20, step=1,
354
+ label="Layer from", scale=2,
355
+ )
356
+ layer_to = gr.Slider(
357
+ minimum=0, maximum=23, value=23, step=1,
358
+ label="Layer to", scale=2,
359
+ )
360
+ walk_topk = gr.Slider(
361
+ minimum=1, maximum=50, value=10, step=1,
362
+ label="Top-K features per layer", scale=2,
363
+ )
364
+
365
+ walk_status = gr.Markdown("")
366
+ walk_table = gr.DataFrame(
367
+ label="Active features",
368
+ wrap=True,
369
+ column_widths=["80px", "90px", "80px", "110px", "140px", "80px", "auto"],
370
+ )
371
+
372
+ walk_btn.click(
373
+ do_walk,
374
+ inputs=[vindex_dd, walk_prompt, layer_from, layer_to, walk_topk],
375
+ outputs=[walk_table, walk_status],
376
+ )
377
+ walk_prompt.submit(
378
+ do_walk,
379
+ inputs=[vindex_dd, walk_prompt, layer_from, layer_to, walk_topk],
380
+ outputs=[walk_table, walk_status],
381
+ )
382
+ vindex_dd.change(
383
+ update_layer_max,
384
+ inputs=[vindex_dd],
385
+ outputs=[layer_to, layer_from],
386
+ )
387
+
388
+ gr.Examples(
389
+ examples=[
390
+ ["The capital of France is"],
391
+ ["Python is a programming"],
392
+ ["Shakespeare wrote"],
393
+ ["Water boils at 100 degrees"],
394
+ ["The speed of light is"],
395
+ ["Einstein discovered"],
396
+ ["The largest planet in the solar system is"],
397
+ ],
398
+ inputs=walk_prompt,
399
+ label="Try these prompts",
400
+ )
401
+
402
+ # ── Tab 2: Knowledge Probe ───────────────────────────────────────────
403
+ with gr.Tab("🧪 Knowledge Probe"):
404
+ gr.Markdown("""
405
+ Compare how the model encodes **three different prompts** at the same layer.
406
+ Use this to see which features are concept-specific vs. shared.
407
+ """)
408
+ with gr.Row():
409
+ probe_layer = gr.Slider(
410
+ minimum=0, maximum=23, value=23, step=1,
411
+ label="Layer to inspect", scale=3,
412
+ )
413
+ probe_topk = gr.Slider(
414
+ minimum=1, maximum=20, value=5, step=1,
415
+ label="Top-K features", scale=2,
416
+ )
417
+ probe_btn = gr.Button("Compare →", variant="primary", scale=1)
418
+
419
+ with gr.Row():
420
+ probe_p1 = gr.Textbox(label="Prompt A", value="The capital of France is", scale=1)
421
+ probe_p2 = gr.Textbox(label="Prompt B", value="Python is a programming", scale=1)
422
+ probe_p3 = gr.Textbox(label="Prompt C", value="Shakespeare wrote", scale=1)
423
+
424
+ with gr.Row():
425
+ probe_out1 = gr.Markdown(label="Result A")
426
+ probe_out2 = gr.Markdown(label="Result B")
427
+ probe_out3 = gr.Markdown(label="Result C")
428
+
429
+ probe_btn.click(
430
+ do_probe,
431
+ inputs=[vindex_dd, probe_p1, probe_p2, probe_p3, probe_layer, probe_topk],
432
+ outputs=[probe_out1, probe_out2, probe_out3],
433
+ )
434
+
435
+ # ── Tab 3: LQL Console ───────────────────────────────────────────────
436
+ with gr.Tab("💻 LQL Console"):
437
+ gr.Markdown("""
438
+ **LQL** (Lazarus Query Language) — the full query interface.
439
+ The active vindex above is injected automatically as `USE "…";` if not already present.
440
+ """)
441
+ with gr.Row():
442
+ lql_input = gr.Textbox(
443
+ label="LQL statement",
444
+ placeholder='WALK "The capital of France is" TOP 10;',
445
+ lines=4,
446
+ scale=5,
447
+ )
448
+ lql_btn = gr.Button("Run ▶", variant="primary", scale=1)
449
+
450
+ lql_output = gr.Code(label="Output", language=None, lines=20)
451
+
452
+ lql_btn.click(do_lql, inputs=[vindex_dd, lql_input], outputs=lql_output)
453
+ lql_input.submit(do_lql, inputs=[vindex_dd, lql_input], outputs=lql_output)
454
+
455
+ gr.Markdown("**Quick examples** (click to load):")
456
+ with gr.Row():
457
+ for tpl in LQL_EXAMPLES:
458
+ short = tpl.split(";")[1].strip()[:50] if ";" in tpl else tpl[:50]
459
+ btn = gr.Button(short, size="sm", variant="secondary")
460
+ btn.click(
461
+ lambda vp, t=tpl: fill_lql_example(vp, t),
462
+ inputs=[vindex_dd],
463
+ outputs=lql_input,
464
+ )
465
+
466
+ # ── Tab 4: Vindex Info ───────────────────────────────────────────────
467
+ with gr.Tab("📊 Vindex Info"):
468
+ gr.Markdown("Inspect the active vindex's metadata and verify file integrity.")
469
+ info_btn = gr.Button("Load info + verify checksums", variant="primary")
470
+ with gr.Row():
471
+ info_summary = gr.Markdown("_Click the button above._")
472
+ verify_out = gr.Markdown("_—_")
473
+
474
+ info_btn.click(
475
+ do_vindex_info,
476
+ inputs=[vindex_dd],
477
+ outputs=[info_summary, verify_out],
478
+ )
479
+
480
+ # ── Tab 5: Extract New Vindex ────────────────────────────────────────
481
+ with gr.Tab("⬇️ Extract"):
482
+ gr.Markdown("""
483
+ Download a model from HuggingFace and extract it into a vindex.
484
+ **Browse** level is enough for `WALK` and `DESCRIBE` queries.
485
+ You need **Inference** level for `INFER` (next-token prediction).
486
+ """)
487
+ with gr.Row():
488
+ extract_model = gr.Textbox(
489
+ label="HuggingFace model ID",
490
+ placeholder="Qwen/Qwen2.5-0.5B-Instruct",
491
+ scale=3,
492
+ )
493
+ extract_name = gr.Textbox(
494
+ label="Output vindex name",
495
+ placeholder="qwen2.5-0.5b.vindex (auto if empty)",
496
+ scale=2,
497
+ )
498
+ with gr.Row():
499
+ extract_level = gr.Radio(
500
+ choices=["Browse (smallest, ~0.5 GB)", "Inference (~1 GB)", "All (~2 GB)"],
501
+ value="Browse (smallest, ~0.5 GB)",
502
+ label="Extraction level",
503
+ )
504
+ with gr.Row():
505
+ hf_token = gr.Textbox(
506
+ label="HuggingFace token (required for gated models)",
507
+ placeholder="hf_…",
508
+ type="password",
509
+ scale=2,
510
+ )
511
+ extract_btn = gr.Button("Extract →", variant="primary", scale=1)
512
+
513
+ extract_out = gr.Markdown("_Enter a model ID and click Extract._")
514
+
515
+ extract_btn.click(
516
+ do_extract,
517
+ inputs=[extract_model, extract_name, extract_level, hf_token],
518
+ outputs=extract_out,
519
+ )
520
+
521
+ # ── Tab 6: Setup / About ─────────────────────────────────────────────
522
+ with gr.Tab("ℹ️ Setup & About"):
523
+ gr.Markdown(SETUP_MD)
524
+ gr.Markdown("### Current environment")
525
+ gr.Markdown(binary_status_md())
526
+ gr.Markdown(
527
+ f"- Python: `{sys.version.split()[0]}`\n"
528
+ f"- Gradio: `{gr.__version__}`\n"
529
+ f"- Repo root: `{REPO_ROOT}`\n"
530
+ f"- Models dir: `{MODELS_DIR}`\n"
531
+ )
532
+
533
+
534
+ # ---------------------------------------------------------------------------
535
+ # Entry point
536
+ # ---------------------------------------------------------------------------
537
+ if __name__ == "__main__":
538
+ # Check for an available port
539
+ import socket
540
+
541
+ def is_port_free(port: int) -> bool:
542
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
543
+ try:
544
+ s.bind(("0.0.0.0", port))
545
+ return True
546
+ except OSError:
547
+ return False
548
+
549
+ port = 7860
550
+ for candidate in range(7860, 7880):
551
+ if is_port_free(candidate):
552
+ port = candidate
553
+ break
554
+
555
+ demo.launch(
556
+ server_name="0.0.0.0",
557
+ server_port=port,
558
+ share=False, # set True to get a public Gradio link
559
+ show_error=True,
560
+ theme=_THEME,
561
+ css=_CSS,
562
+ )
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio>=6.0.0
2
+ pandas>=2.0.0
utils.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utility helpers for the LARQL Gradio demo.
3
+ Handles binary discovery, subprocess calls, and output parsing.
4
+ """
5
+ import os
6
+ import re
7
+ import sys
8
+ import json
9
+ import shutil
10
+ import subprocess
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Binary discovery
17
+ # ---------------------------------------------------------------------------
18
+
19
+ def find_larql_binary() -> Optional[str]:
20
+ """Locate the larql executable. Returns the path or None."""
21
+ candidates = [
22
+ # Sibling target directory (running from demo/)
23
+ Path(__file__).parent.parent / "target" / "release" / "larql",
24
+ Path(__file__).parent.parent / "target" / "release" / "larql.exe",
25
+ # Current working directory
26
+ Path("larql"),
27
+ Path("larql.exe"),
28
+ # HuggingFace Spaces: binary shipped alongside app.py
29
+ Path(__file__).parent / "larql",
30
+ Path(__file__).parent / "larql.exe",
31
+ ]
32
+ for c in candidates:
33
+ if c.exists():
34
+ return str(c)
35
+ # Fall back to PATH
36
+ found = shutil.which("larql")
37
+ return found
38
+
39
+
40
+ LARQL = find_larql_binary()
41
+
42
+
43
+ def larql_available() -> bool:
44
+ return LARQL is not None
45
+
46
+
47
+ def run_larql(*args, timeout: int = 120, env_extra: Optional[dict] = None) -> tuple[int, str, str]:
48
+ """
49
+ Run larql with the given arguments.
50
+ Returns (returncode, stdout, stderr).
51
+ """
52
+ if LARQL is None:
53
+ return 1, "", "larql binary not found. See the Setup tab."
54
+ env = os.environ.copy()
55
+ if env_extra:
56
+ env.update(env_extra)
57
+ try:
58
+ result = subprocess.run(
59
+ [LARQL] + list(args),
60
+ capture_output=True,
61
+ text=True,
62
+ encoding="utf-8",
63
+ errors="replace",
64
+ timeout=timeout,
65
+ env=env,
66
+ )
67
+ return result.returncode, result.stdout or "", result.stderr or ""
68
+ except subprocess.TimeoutExpired:
69
+ return 1, "", f"Timed out after {timeout}s"
70
+ except Exception as e:
71
+ return 1, "", str(e)
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Output parsers
76
+ # ---------------------------------------------------------------------------
77
+
78
+ _WALK_FEATURE_RE = re.compile(
79
+ r"^\s+\d+\.\s+(F\d+)\s+gate=([+-]?\d+\.\d+)\s+hears=\"([^\"]*)\"\s+c=(\d+\.\d+)\s+down=\[(.+)\]\s*$"
80
+ )
81
+ _WALK_LAYER_RE = re.compile(r"^Layer (\d+):")
82
+
83
+
84
+ def parse_walk_output(text: str) -> list[dict]:
85
+ """
86
+ Parse the text output of `larql walk` into a list of row dicts.
87
+ Each row: {layer, rank, feature_id, gate, hears, cosine, down_tokens}
88
+ """
89
+ rows = []
90
+ current_layer = None
91
+ for line in text.splitlines():
92
+ m = _WALK_LAYER_RE.match(line)
93
+ if m:
94
+ current_layer = int(m.group(1))
95
+ continue
96
+ m = _WALK_FEATURE_RE.match(line)
97
+ if m and current_layer is not None:
98
+ feature_id, gate_raw, hears, cosine_raw, down_raw = m.groups()
99
+ gate = float(gate_raw)
100
+ cosine = float(cosine_raw)
101
+ # Parse down tokens: "token (score), token2 (score2), ..."
102
+ down_tokens = re.findall(r"(.+?)\s+\([\d.]+\)", down_raw)
103
+ down_str = " · ".join(down_tokens[:5])
104
+ rows.append({
105
+ "Layer": current_layer,
106
+ "Feature": feature_id,
107
+ "Gate": round(gate, 4),
108
+ "Direction": "▲ excites" if gate > 0 else "▼ inhibits",
109
+ "Hears": hears,
110
+ "Cosine": round(cosine, 3),
111
+ "Top tokens (down)": down_str,
112
+ })
113
+ return rows
114
+
115
+
116
+ def load_vindex_info(vindex_path: str) -> dict:
117
+ """Load index.json from a vindex directory."""
118
+ idx = Path(vindex_path) / "index.json"
119
+ if not idx.exists():
120
+ return {}
121
+ try:
122
+ with open(idx) as f:
123
+ return json.load(f)
124
+ except Exception:
125
+ return {}
126
+
127
+
128
+ def format_vindex_summary(info: dict, vindex_path: str) -> str:
129
+ """Render a Markdown summary of a vindex."""
130
+ if not info:
131
+ return "_No index.json found._"
132
+ model = info.get("model", "unknown")
133
+ family = info.get("family", "?")
134
+ layers = info.get("num_layers", "?")
135
+ hidden = info.get("hidden_size", "?")
136
+ intermediate = info.get("intermediate_size", "?")
137
+ vocab = info.get("vocab_size", "?")
138
+ level = info.get("extract_level", "?")
139
+ dtype = info.get("dtype", "?")
140
+ extracted_at = info.get("source", {}).get("extracted_at", "?")
141
+ bands = info.get("layer_bands", {})
142
+ band_str = " ".join(f"**{k}**: L{v[0]}–L{v[1]}" for k, v in bands.items())
143
+
144
+ # File sizes
145
+ size_str = ""
146
+ for fname in ["gate_vectors.bin", "embeddings.bin", "down_meta.bin"]:
147
+ fp = Path(vindex_path) / fname
148
+ if fp.exists():
149
+ mb = fp.stat().st_size / 1e6
150
+ size_str += f" - `{fname}`: {mb:.1f} MB\n"
151
+
152
+ return f"""### {model}
153
+
154
+ | Property | Value |
155
+ |---|---|
156
+ | Family | `{family}` |
157
+ | Layers | {layers} |
158
+ | Hidden size | {hidden} |
159
+ | Intermediate size | {intermediate} |
160
+ | Vocab size | {vocab:,} |
161
+ | Extract level | `{level}` |
162
+ | Storage dtype | `{dtype}` |
163
+ | Extracted | {extracted_at} |
164
+
165
+ **Layer bands:** {band_str}
166
+
167
+ **Files:**
168
+ {size_str}"""
169
+
170
+
171
+ def list_local_vindexes(root: str = ".") -> list[str]:
172
+ """Find all .vindex directories in the given root."""
173
+ results = []
174
+ for entry in Path(root).rglob("index.json"):
175
+ vindex_dir = str(entry.parent)
176
+ if not any(part.startswith(".") for part in entry.parts):
177
+ results.append(vindex_dir)
178
+ return sorted(results)