Spaces:
Running
Running
File size: 17,131 Bytes
a2124e2 adcbc7e a2124e2 4aa8326 a2124e2 ec34b87 a2124e2 ec34b87 cdad13a 933ff98 a2124e2 cdad13a a2124e2 4aa8326 a2124e2 4aa8326 0ace216 4aa8326 0ace216 4aa8326 a2124e2 4aa8326 a2124e2 4aa8326 a2124e2 adcbc7e 3aec1bb ec34b87 3aec1bb ec34b87 3aec1bb d0f24f6 a2124e2 488f97e b356ed7 488f97e a2124e2 4aa8326 a2124e2 4aa8326 a2124e2 4eb0c7c a2124e2 4eb0c7c a2124e2 cdad13a a570df2 cdad13a a2124e2 a53b1da a2124e2 a53b1da a2124e2 3e5b14e a2124e2 3e5b14e a2124e2 b356ed7 a2124e2 8f7952f a2124e2 4aa8326 a2124e2 4aa8326 a2124e2 b356ed7 a2124e2 b356ed7 c1298e1 a2124e2 26b395e a2124e2 cdad13a a2124e2 cdad13a a2124e2 26b395e a2124e2 cdad13a a2124e2 cdad13a a2124e2 4aa8326 a2124e2 488f97e b356ed7 488f97e b356ed7 488f97e 4aa8326 a2124e2 4eb0c7c a2124e2 4eb0c7c 92af58e 26b395e a2124e2 92af58e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 | #!/usr/bin/env python3
"""
Phox on Hugging Face Spaces — BQSM Wave-Rider Inference Engine.
This app wraps the Phoenix Brain C inference binary in a Gradio interface.
At Space startup it:
1. Downloads the tokenizer from HF Hub (tokenizers lib, no PyTorch)
2. On button click: downloads pre-built phoenix binary + model from HF Hub
3. Starts phoenix in --chat mode with shared-memory ring buffers
4. Provides a chat UI via Gradio
The phoenix binary runs on CPU (AVX2) — the wave-rider physics engine
doesn't use PyTorch, so ZeroGPU doesn't accelerate it directly.
But the Space provides a free public endpoint to chat with Phox.
"""
import os
import sys
import time
import json
import mmap
import struct
import subprocess
import threading
import gradio as gr
from huggingface_hub import hf_hub_download
# ── Config ──
SPACE_DIR = os.path.dirname(os.path.abspath(__file__))
MODEL_REPO = "compunerd/emerging-systems-models"
MODEL_FILENAME = "gemma4-12b-ternary-normed.bqsm"
MODEL_SUBDIR = "/tmp/phoenix_models"
MODEL_PATH = os.path.join(MODEL_SUBDIR, MODEL_FILENAME)
STATE_FILE = "/tmp/phoenix_state.jsonl"
STATE_LOG = "/tmp/phoenix_daemon.log"
RING_IN_PATH = "/tmp/phoenix_ring_in"
RING_OUT_PATH = "/tmp/phoenix_ring_out"
RING_CAPACITY = 4096
RING_BUF_SIZE = RING_CAPACITY * 4 + 16
PHOENIX_BIN = "/tmp/phoenix"
MIXER_BIN = "/tmp/mixer"
CHAT_TIMEOUT_S = 120
SENTINEL_QUIT = 0xFFFFFFFE
# ── Global state ──
_phoenix_proc = None
_mixer_proc = None
_ring_in_mm = None
_ring_out_mm = None
_ring_in_fd = None
_ring_out_fd = None
_state_lock = threading.Lock()
_chat_messages = []
_boot_status = {"ready": False, "phase": "idle", "message": "Not started"}
_boot_thread = None
_tokenizer = None
def _update_boot(phase, message, ready=False):
"""Update boot status (thread-safe)."""
with _state_lock:
_boot_status["phase"] = phase
_boot_status["message"] = message
_boot_status["ready"] = ready
def load_tokenizer():
"""Download and load the Gemma 4 tokenizer from HF Hub."""
global _tokenizer
print("[Phox Space] Loading Gemma 4 tokenizer from Hub...")
try:
tok_path = hf_hub_download(
repo_id=MODEL_REPO,
filename="tokenizer/tokenizer.json")
from tokenizers import Tokenizer
_tokenizer = Tokenizer.from_file(tok_path)
# Build a simple vocab-size attribute for compatibility
_tokenizer.vocab_size = len(_tokenizer.get_vocab())
print(f"[Phox Space] Tokenizer loaded (vocab={_tokenizer.vocab_size})")
except Exception as e:
print(f"[Phox Space] Tokenizer download failed: {e}")
# Fallback: try local paths
for tok_path in [
os.path.join(MODEL_SUBDIR, "tokenizer", "tokenizer.json"),
"/home/compunerd/models/gemma4-tokenizer/tokenizer.json",
]:
try:
from tokenizers import Tokenizer
_tokenizer = Tokenizer.from_file(tok_path)
_tokenizer.vocab_size = len(_tokenizer.get_vocab())
print(f"[Phox Space] Tokenizer loaded from {tok_path}")
break
except Exception:
pass
if _tokenizer is None:
print("[Phox Space] WARNING: No tokenizer available — chat will not work")
def boot_phoenix():
"""Full startup sequence: download model, build binary, start phoenix."""
# Phase 0: Tokenizer
_update_boot("tokenizer", "Loading tokenizer from Hub...")
load_tokenizer()
# Phase 1: Download model
_update_boot("download", "Downloading BQSM model from HF Hub...")
try:
os.makedirs(MODEL_SUBDIR, exist_ok=True)
if not os.path.exists(MODEL_PATH):
hf_hub_download(
repo_id=MODEL_REPO,
filename=MODEL_FILENAME,
local_dir=MODEL_SUBDIR,
local_dir_use_symlinks=False,
)
_update_boot("download", f"Model ready: {os.path.getsize(MODEL_PATH)} bytes")
except Exception as e:
_update_boot("download", f"Model download FAILED: {e}")
return
# Phase 2: Download pre-built binary from HF Hub (avoids compilation on Space)
_update_boot("build", "Checking for phoenix binary...")
# Only download if binary doesn't exist (cached after first download)
if not os.path.exists(PHOENIX_BIN):
try:
_update_boot("build", "Downloading phoenix binary from Hub...")
hf_hub_download(
repo_id=MODEL_REPO,
filename="phoenix",
local_dir="/tmp",
local_dir_use_symlinks=False,
force_filename="phoenix",
)
os.chmod(PHOENIX_BIN, 0o755)
except Exception as e:
# Fallback: compile from source
_update_boot("build", f"Binary download failed, compiling... ({e})")
src_path = os.path.join(SPACE_DIR, "phoenix_brain.c")
if os.path.exists(src_path):
cc = os.environ.get("CC", "cc")
cmd = [cc, "-O3", "-std=c11", "-march=native", "-fopenmp",
src_path, "-o", PHOENIX_BIN, "-lm"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
if result.returncode != 0:
_update_boot("build", f"Build FAILED: {result.stderr[:500]}")
return
else:
_update_boot("build", "No binary or source found!")
return
_update_boot("build", "Binary ready")
# Phase 3: Start phoenix
_update_boot("start", "Starting phoenix in chat mode...")
global _phoenix_proc
with _state_lock:
if _phoenix_proc and _phoenix_proc.poll() is None:
_update_boot("start", "Already running", ready=True)
return
init_ring_buffers()
_phoenix_proc = subprocess.Popen(
[PHOENIX_BIN, MODEL_PATH, "--chat"],
stdout=open(STATE_LOG, 'a'),
stderr=subprocess.STDOUT,
)
_update_boot("start", f"Phoenix PID {_phoenix_proc.pid} — loading model (~40s)...")
# Start a monitor thread to check when phoenix enters chat mode
threading.Thread(target=_monitor_phoenix, daemon=True).start()
def _monitor_phoenix():
"""Background thread: watches daemon log for 'Chat Mode', then sets ready."""
import time as _time
for _ in range(120): # Check for up to 2 minutes
_time.sleep(0.5)
try:
size = os.path.getsize(STATE_LOG)
with open(STATE_LOG, 'rb') as f:
f.seek(max(0, size - 5000))
log = f.read().decode('utf-8', errors='replace')
if "Chat Mode" in log or "ring buffer" in log.lower() or "ready" in log.lower():
_update_boot("ready", "Phoenix is ready to chat!")
return
# Check if process died
with _state_lock:
if _phoenix_proc and _phoenix_proc.poll() is not None:
_update_boot("error", f"Phoenix exited (code {_phoenix_proc.returncode})")
return
except (FileNotFoundError, PermissionError):
pass
_update_boot("ready", "Phoenix boot timed out — may still be initializing.")
def init_ring_buffers():
"""Create ring buffer files with initialized headers."""
for path in [RING_IN_PATH, RING_OUT_PATH]:
try:
os.unlink(path)
except FileNotFoundError:
pass
fd = os.open(path, os.O_CREAT | os.O_RDWR, 0o666)
os.write(fd, b'\x00' * RING_BUF_SIZE)
os.lseek(fd, 0, 0)
os.write(fd, struct.pack('<IIII', 0, 0, 0, RING_CAPACITY))
os.close(fd)
global _ring_in_fd, _ring_out_fd, _ring_in_mm, _ring_out_mm
_ring_in_fd = os.open(RING_IN_PATH, os.O_RDWR)
_ring_in_mm = mmap.mmap(_ring_in_fd, RING_BUF_SIZE,
mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE)
_ring_out_fd = os.open(RING_OUT_PATH, os.O_RDWR)
_ring_out_mm = mmap.mmap(_ring_out_fd, RING_BUF_SIZE,
mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE)
def ring_push(mm, val):
head = struct.unpack_from('<I', mm, 0)[0]
tail = struct.unpack_from('<I', mm, 4)[0]
sz = struct.unpack_from('<I', mm, 8)[0]
next_tail = (tail + 1) % RING_CAPACITY
if next_tail == head:
return False
struct.pack_into('<I', mm, 16 + tail * 4, val)
struct.pack_into('<I', mm, 4, next_tail)
struct.pack_into('<I', mm, 8, sz + 1)
return True
def ring_pop(mm):
head = struct.unpack_from('<I', mm, 0)[0]
tail = struct.unpack_from('<I', mm, 4)[0]
sz = struct.unpack_from('<I', mm, 8)[0]
if sz == 0:
return None
val = struct.unpack_from('<I', mm, 16 + head * 4)[0]
next_head = (head + 1) % RING_CAPACITY
struct.pack_into('<I', mm, 0, next_head)
struct.pack_into('<I', mm, 8, sz - 1)
return val
def send_tokens_to_phoenix(token_ids):
if not _ring_in_mm:
return 0
pushed = 0
for tid in token_ids:
if ring_push(_ring_in_mm, tid):
pushed += 1
return pushed
def poll_predictions(timeout_s=CHAT_TIMEOUT_S):
results = []
start = time.time()
while time.time() - start < timeout_s:
tok = ring_pop(_ring_out_mm)
if tok is not None:
if tok == SENTINEL_QUIT:
break
results.append(tok)
else:
time.sleep(0.05)
return results
def start_phoenix_manual():
"""Start phoenix: download model, build binary, launch in chat mode."""
global _phoenix_proc
with _state_lock:
if _phoenix_proc and _phoenix_proc.poll() is None:
return "Already running"
# Run boot in a thread so the Gradio event loop isn't blocked
threading.Thread(target=boot_phoenix, daemon=True).start()
return " Booting... check Engine Status for progress"
def start_mixer():
"""Start the mixing ring engine (lighter-weight alternative to phoenix)."""
global _mixer_proc
# Check if already running
if _mixer_proc and _mixer_proc.poll() is None:
return "Mixer already running"
# Download binary to /tmp
if not os.path.exists(MIXER_BIN):
try:
hf_hub_download(
repo_id=MODEL_REPO,
filename="mixer",
local_dir="/tmp",
local_dir_use_symlinks=False,
)
os.chmod(MIXER_BIN, 0o755)
except Exception as e:
return f"Failed to download mixer binary: {e}"
# Ensure model exists
if not os.path.exists(MODEL_PATH):
os.makedirs(MODEL_SUBDIR, exist_ok=True)
try:
hf_hub_download(
repo_id=MODEL_REPO,
filename=MODEL_FILENAME,
local_dir=MODEL_SUBDIR,
local_dir_use_symlinks=False,
)
except Exception as e:
return f"Failed to download model: {e}"
# Launch mixer binary — it runs inference and outputs to stdout
init_ring_buffers()
_mixer_proc = subprocess.Popen(
[MIXER_BIN, MODEL_PATH, "-s", "2"],
stdout=open(STATE_LOG, 'a'),
stderr=subprocess.STDOUT,
)
return f"Mixer started (PID {_mixer_proc.pid}) — check daemon log"
def stop_phoenix():
global _phoenix_proc, _ring_in_mm, _ring_out_mm, _ring_in_fd, _ring_out_fd
# Send quit signal via ring buffer (no lock needed for ring_push)
if _ring_in_mm:
try:
ring_push(_ring_in_mm, SENTINEL_QUIT)
except Exception:
pass
with _state_lock:
if _phoenix_proc and _phoenix_proc.poll() is None:
_phoenix_proc.terminate()
# Close ring buffers outside the lock to avoid blocking
if _ring_in_mm:
try:
_ring_in_mm.close()
except Exception:
pass
_ring_in_mm = None
if _ring_out_mm:
try:
_ring_out_mm.close()
except Exception:
pass
_ring_out_mm = None
if _ring_in_fd is not None:
try:
os.close(_ring_in_fd)
except Exception:
pass
_ring_in_fd = None
if _ring_out_fd is not None:
try:
os.close(_ring_out_fd)
except Exception:
pass
_ring_out_fd = None
return "Stopped"
def get_boot_status():
"""Return current boot status for the UI."""
# Read without lock to avoid contention with boot_phoenix thread
try:
return dict(_boot_status)
except Exception:
return {"ready": False, "phase": "error", "message": "Status unavailable"}
def phox_chat(message, history):
"""Main chat function — writes tokens to ring buffer, reads predictions."""
if not _ring_in_mm or not _ring_out_mm:
return "Engine not initialized. Please wait for startup."
with _state_lock:
engine_running = (_phoenix_proc is not None and _phoenix_proc.poll() is None) or \
(_mixer_proc is not None and _mixer_proc.poll() is None)
if not engine_running:
return "Engine not running. Please click 'Start Engine' or 'Start Mixer' first."
if _tokenizer is None:
return "Tokenizer not loaded."
try:
token_ids = _tokenizer.encode(message).ids
except Exception as e:
return f"Tokenization error: {e}"
if not token_ids:
return "Empty input."
pushed = send_tokens_to_phoenix(token_ids)
if pushed == 0:
return "Ring buffer full. Try again."
pred_ids = poll_predictions()
# Debug: log what we got
import os
with open("/tmp/chat_debug.log", "a") as f:
f.write(f"Input tokens: {token_ids}, Output tokens: {pred_ids}\n")
if not pred_ids:
return "No prediction received (timeout). Try again."
response = _tokenizer.decode(pred_ids, skip_special_tokens=True)
return response.strip() if response else "[no output]"
# ── Gradio interface ──
with gr.Blocks(title="Phox — Wave-Rider Brain") as demo:
gr.Markdown("""
# 🌀 Phox — Wave-Rider Inference Engine
**Two engines available:**
- **Phox Brain** (heavy): 2.98GB model, ~1.1 tok/s, full wave-rider physics
- **Mixing Ring** (fast): 100-ring topology, ~8000+ tok/s, parallel settle
Both use the same Gemma-4-12B ternary weights via mmap. Click the appropriate
button to start, then chat below.
""")
with gr.Row():
with gr.Column(scale=3):
chatbot = gr.ChatInterface(
fn=phox_chat,
title="Talk to Phox",
description="Type a message and watch the oscillators work.",
)
with gr.Column(scale=1):
gr.Markdown("### Engine Status")
status_json = gr.JSON(label="Boot Status")
gr.Markdown("### Controls")
with gr.Row():
start_btn = gr.Button("Start Engine", variant="primary")
mixer_btn = gr.Button("Start Mixer (fast)", variant="secondary")
stop_btn = gr.Button("Stop Engine", variant="secondary")
status_text = gr.Textbox(label="Engine Output", interactive=False)
start_btn.click(fn=start_phoenix_manual, outputs=status_text)
mixer_btn.click(fn=start_mixer, outputs=status_text)
stop_btn.click(fn=stop_phoenix, outputs=status_text)
# Periodic status refresh using gr.Timer (Gradio 6.x API)
def tick():
base = f"Phase: {_boot_status['phase']} | {_boot_status['message']}"
# Show last line of daemon log if available (read tail only)
try:
import os as _os
if _os.path.exists(STATE_LOG):
size = _os.path.getsize(STATE_LOG)
with open(STATE_LOG, 'rb') as f:
f.seek(max(0, size - 500))
tail = f.read().decode('utf-8', errors='replace')
lines = [l.strip() for l in tail.split('\n') if l.strip()]
if lines:
last = lines[-1]
if last:
base += f"\n[daemon] {last[:200]}"
except (FileNotFoundError, PermissionError):
pass
return base
timer1 = gr.Timer(3, active=True)
timer1.tick(fn=get_boot_status, outputs=status_json)
timer2 = gr.Timer(2, active=True)
timer2.tick(fn=tick, outputs=status_text)
if __name__ == "__main__":
# Load tokenizer at startup only (fast, ~32MB)
# Model download + binary build happen on button click
_boot_thread = threading.Thread(target=load_tokenizer, daemon=True)
_boot_thread.start()
# Don't auto-boot phoenix — user clicks "Start Engine"
_update_boot("idle", "Click 'Start Engine' to boot the BQSM wave-rider (mixer auto-downloads)")
demo.queue().launch(
server_name="0.0.0.0",
server_port=int(os.environ.get("PORT", 7860)),
share=False,
)
# Force rebuild Sat Aug 8 02:08:01 PM EDT 2026
|