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