Spaces:
Running
Running
Add LARQL Explorer: Gradio UI + Docker build
Browse files- Dockerfile +35 -0
- README.md +41 -3
- app.py +562 -0
- requirements.txt +2 -0
- 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:
|
| 5 |
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 — 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 ·
|
| 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)
|