Spaces:
Running on Zero
Running on Zero
| """HobbyLM Playground — a Gradio Space to chat with the HobbyLM models, ask questions about an | |
| image (the multimodal Omni model), and generate images (the 1024px DiT + DC-AE pipeline). | |
| All models are the from-scratch 500M sparse-MoE family (+ a 333M image DiT) published at | |
| https://huggingface.co/rootxhacker . They use a custom architecture, so the Space vendors the | |
| reference implementation (`hobbylm/`, `hobby_image/`) instead of going through transformers' AutoModel. | |
| Runs on ZeroGPU (the heavy functions are @spaces.GPU); falls back to CPU when run locally. | |
| """ | |
| import json | |
| import threading | |
| import gradio as gr | |
| import torch | |
| from huggingface_hub import hf_hub_download | |
| from safetensors.torch import load_file | |
| # --- Work around a long-standing gradio_client bug ("argument of type 'bool' is not iterable" in | |
| # get_type / json_schema_to_python_type when a component schema has a boolean `additionalProperties`). | |
| # It crashes the /info endpoint, so the Gradio frontend shows "No API found" and can't call functions. | |
| # Treat boolean schemas as `Any`. (Present in both gradio 4.44 and 5.9's bundled gradio_client.) | |
| import gradio_client.utils as _gcu # noqa: E402 | |
| _orig_get_type = _gcu.get_type | |
| def _safe_get_type(schema): | |
| if not isinstance(schema, dict): | |
| return "Any" | |
| return _orig_get_type(schema) | |
| _gcu.get_type = _safe_get_type | |
| _orig_jstpt = _gcu._json_schema_to_python_type | |
| def _safe_jstpt(schema, defs=None): | |
| if isinstance(schema, bool): | |
| return "Any" | |
| return _orig_jstpt(schema, defs) | |
| _gcu._json_schema_to_python_type = _safe_jstpt | |
| # ZeroGPU decorator — with a no-op fallback so the app also runs on plain CPU / locally. | |
| try: | |
| import spaces | |
| except Exception: # not on a ZeroGPU Space | |
| class _Spaces: | |
| def GPU(*a, **k): | |
| if a and callable(a[0]): | |
| return a[0] | |
| def deco(f): | |
| return f | |
| return deco | |
| spaces = _Spaces() | |
| HF_USER = "rootxhacker" | |
| VISION_ID = "google/siglip2-so400m-patch16-512" # the encoder HobbyLM-Omni was trained with | |
| DCAE_ID = "mit-han-lab/dc-ae-f32c32-sana-1.1-diffusers" | |
| CLIP_ID = "openai/clip-vit-large-patch14" | |
| NEG_DEFAULT = "blurry, low quality, watermark, signature, text, jpeg artifacts, deformed, distorted" | |
| # chat dropdown -> (repo suffix, decoding kind) | |
| CHAT_MODELS = { | |
| "HobbyLM-Chat — instruction / conversation": ("HobbyLM-Chat", "chat"), | |
| "HobbyLM-Base — raw text completion": ("HobbyLM-Base", "base"), | |
| "HobbyLM-Computer-Use — tools / GUI agent": ("HobbyLM-Computer-Use", "chat"), | |
| "HobbyLM-Omni — multimodal core (text)": ("HobbyLM-Omni", "chat"), | |
| "HobbyLM-Diffusion — masked-diffusion LM": ("HobbyLM-Diffusion", "diffusion"), | |
| } | |
| DEFAULT_CHAT = list(CHAT_MODELS)[0] | |
| _cache = {} | |
| _lock = threading.Lock() | |
| def _warmup(): | |
| """Build the heavy models in the MAIN process at startup. ZeroGPU runs each @spaces.GPU call in a | |
| forked worker that inherits the main process's memory, so models built here are reused across calls | |
| (no per-call rebuild) — which is what was blowing the Omni GPU-time budget. Chat LLMs stay lazy | |
| (they're light enough to rebuild per call). Runs in a daemon thread so the app binds the port now.""" | |
| try: | |
| from huggingface_hub import snapshot_download | |
| for mid in [VISION_ID, DCAE_ID, CLIP_ID]: | |
| snapshot_download(mid, allow_patterns=["*.json", "*.safetensors", "*.txt", "*.model"]) | |
| _load_vlm() # Omni LLM + SigLIP2 + projector (the expensive one for the image tab) | |
| _load_image_models() # DiT + DC-AE + CLIP | |
| print("[warmup] VLM + image models built in main process", flush=True) | |
| except Exception as e: | |
| print(f"[warmup] warning: {e}", flush=True) | |
| def _device(): | |
| return "cuda" if torch.cuda.is_available() else "cpu" | |
| def _enc(): | |
| import tiktoken | |
| return tiktoken.get_encoding("gpt2") | |
| # --------------------------------------------------------------------------- loaders (cached) | |
| # NOTE: ZeroGPU releases/re-attaches the GPU between calls, so models are cached on **CPU** and moved | |
| # to CUDA *inside* each @spaces.GPU call (then back to CPU) — caching a model on CUDA and reusing it | |
| # across calls crashes the ZeroGPU worker. | |
| # Loaders are LOCK-FREE: Gradio serializes requests (concurrency 1), and a lock held during a slow | |
| # build would deadlock a ZeroGPU fork. Dict get/set is atomic under the GIL. | |
| def _load_llm(repo): | |
| key = ("llm", repo) | |
| if key in _cache: | |
| return _cache[key] | |
| from hobbylm.config import ModelConfig | |
| from hobbylm.model import MoETransformer | |
| cfg_d = {k: v for k, v in json.load( | |
| open(hf_hub_download(f"{HF_USER}/{repo}", "config.json"))).items() if k != "preset"} | |
| cfg = ModelConfig(**cfg_d) | |
| cfg.expert_backend = "bmm" # universal MoE backend (CPU + GPU) | |
| model = MoETransformer(cfg).eval() | |
| model.load_state_dict(load_file(hf_hub_download(f"{HF_USER}/{repo}", "model.safetensors"))) | |
| _cache[key] = (model, cfg) | |
| return _cache[key] | |
| def _load_vlm(): | |
| key = ("vlm",) | |
| if key in _cache: | |
| return _cache[key] | |
| from hobbylm.vision import SiglipVision | |
| from hobbylm.multimodal import MoEVLM | |
| llm, _ = _load_llm("HobbyLM-Omni") | |
| enc = SiglipVision(model_id=VISION_ID, device="cpu", dtype=torch.float32) | |
| vlm = MoEVLM(llm, vision_dim=enc.hidden) | |
| vlm.mm_projector.load_state_dict( | |
| load_file(hf_hub_download(f"{HF_USER}/HobbyLM-Omni", "vision_projector.safetensors"))) | |
| vlm.eval() | |
| _cache[key] = (vlm, enc) | |
| return _cache[key] | |
| def _load_image_models(): | |
| if ("dit",) not in _cache: | |
| from hobby_image.dit import HobbyImageDiT, DiTConfig | |
| cfg = json.load(open(hf_hub_download(f"{HF_USER}/HobbyLM-Image", "config.json"))) | |
| dit = HobbyImageDiT(DiTConfig(**cfg["dit_config"])).eval() | |
| dit.load_state_dict(load_file(hf_hub_download(f"{HF_USER}/HobbyLM-Image", "model.safetensors"))) | |
| _cache[("dit",)] = (dit, cfg["dit_config"]["latent_h"], float(cfg["lat_std"]), float(cfg["scaling_factor"])) | |
| if ("dcae",) not in _cache: | |
| from diffusers import AutoencoderDC | |
| # bf16 (NOT fp16): the DiT/DC-AE overflow in fp16 -> NaN -> black images. | |
| _cache[("dcae",)] = AutoencoderDC.from_pretrained(DCAE_ID, torch_dtype=torch.bfloat16).eval() | |
| if ("clip",) not in _cache: | |
| from transformers import CLIPTextModel, CLIPTokenizer | |
| _cache[("clip",)] = (CLIPTokenizer.from_pretrained(CLIP_ID), | |
| CLIPTextModel.from_pretrained(CLIP_ID, torch_dtype=torch.bfloat16).eval()) | |
| dit, lat, lat_std, sf = _cache[("dit",)] | |
| ae = _cache[("dcae",)] | |
| tok, clip = _cache[("clip",)] | |
| return dit, lat, lat_std, sf, ae, tok, clip | |
| SAE_REPO = "rootxhacker/HobbyLM-SAE" | |
| def _load_sae(): | |
| key = ("sae",) | |
| if key in _cache: | |
| return _cache[key] | |
| import json | |
| from hobbylm.sae import TopKSAE, SAEConfig | |
| meta = json.load(open(hf_hub_download(SAE_REPO, "meta.json"))) | |
| labels = json.load(open(hf_hub_download(SAE_REPO, "labels.json"))) | |
| sae = TopKSAE(SAEConfig(**meta["cfg"])).eval() | |
| sae.load_state_dict(load_file(hf_hub_download(SAE_REPO, "sae.safetensors"))) | |
| _cache[key] = (sae, meta, labels) | |
| return _cache[key] | |
| # --------------------------------------------------------------------------- chat | |
| def _build_prompt(repo, message, history): | |
| if repo == "HobbyLM-Base": | |
| return message # base = pure completion | |
| s = "" | |
| for turn in history or []: | |
| if isinstance(turn, dict): # gradio 5 "messages" format | |
| role, content = turn.get("role"), turn.get("content", "") | |
| if not isinstance(content, str): | |
| content = str(content) | |
| if role == "user": | |
| s += f"USER: {content}\n" | |
| elif role == "assistant" and content: | |
| s += f"ASSISTANT: {content}\n" | |
| elif isinstance(turn, (list, tuple)) and len(turn) == 2: # legacy "tuples" format | |
| u, a = turn | |
| if u: | |
| s += f"USER: {u}\n" | |
| if a: | |
| s += f"ASSISTANT: {a}\n" | |
| return s + f"USER: {message}\nASSISTANT:" | |
| def chat_fn(message, history, model_name, max_new, temperature): | |
| from hobbylm.generate import generate as ar_generate | |
| repo, kind = CHAT_MODELS[model_name] | |
| dev = _device() | |
| enc = _enc() | |
| prompt = _build_prompt(repo, message, history) | |
| model, cfg = _load_llm(repo) | |
| model.to(dev) | |
| try: | |
| ids = torch.tensor([enc.encode_ordinary(prompt)], device=dev) | |
| if kind == "diffusion": | |
| from hobbylm.diffusion import generate as dgen | |
| gen_len = int(max_new) | |
| out = dgen(model, ids, gen_len=gen_len, steps=max(32, 2 * gen_len), | |
| temperature=max(0.0, float(temperature) - 0.4), rep_penalty=1.5, remask_steps=2) | |
| return enc.decode(out[0].tolist()).strip() | |
| ctx_len = min(getattr(cfg, "context_length", 1024), 2048) | |
| out = ar_generate(model, ids, int(max_new), float(temperature), 0, torch.device(dev), | |
| top_p=0.95, repetition_penalty=1.3, no_repeat_ngram_size=3, ctx_len=ctx_len) | |
| return enc.decode(out[0, ids.shape[1]:].tolist()).strip() | |
| except Exception as e: | |
| import traceback | |
| traceback.print_exc() | |
| return f"⚠️ error: {e}" | |
| finally: | |
| model.to("cpu") | |
| # --------------------------------------------------------------------------- image understanding (Omni) | |
| def understand_fn(image, question, max_new): | |
| if image is None: | |
| return "Please upload an image first." | |
| from hobbylm.multimodal import IMAGE_TOKEN | |
| from hobbylm.generate import GPT2_VALID, EOT | |
| dev = _device() | |
| enc = _enc() | |
| vlm, venc = _load_vlm() | |
| vlm.to(dev) | |
| venc.vision.to(dev) | |
| venc.device = dev | |
| try: | |
| from contextlib import nullcontext | |
| amp = torch.autocast("cuda", dtype=torch.bfloat16) if dev == "cuda" else nullcontext() | |
| with torch.no_grad(), amp: | |
| feats = venc.encode([image.convert("RGB")]).float() | |
| q = (question or "Describe this image in detail.").strip() | |
| pre = enc.encode_ordinary(f"USER: {q}\nASSISTANT:") | |
| ids = torch.tensor([[IMAGE_TOKEN] + pre], device=dev) | |
| cur, _ = vlm.build_inputs_embeds(ids, image_features=feats) | |
| outs = [] | |
| for _ in range(int(max_new)): | |
| logits, _ = vlm.llm(inputs_embeds=cur) | |
| lg = logits[:, -1, :].float() | |
| lg[:, GPT2_VALID:] = -float("inf") | |
| if outs: # repetition penalty | |
| u = torch.tensor(sorted(set(outs)), device=dev) | |
| v = lg[0, u] | |
| lg[0, u] = torch.where(v > 0, v / 1.3, v * 1.3) | |
| t = int(lg.argmax(-1).item()) | |
| if t == EOT: | |
| break | |
| outs.append(t) | |
| e = vlm.llm.embed(torch.tensor([[t]], device=dev)).to(cur.dtype) | |
| cur = torch.cat([cur, e], dim=1) | |
| return enc.decode(outs).strip() or "(no answer)" | |
| except Exception as e: | |
| import traceback | |
| traceback.print_exc() | |
| return f"⚠️ error: {e}" | |
| finally: | |
| vlm.to("cpu") | |
| venc.vision.to("cpu") | |
| venc.device = "cpu" | |
| # --------------------------------------------------------------------------- image generation | |
| def generate_image_fn(prompt, negative, steps, guidance, seed, progress=gr.Progress()): | |
| if not prompt or not prompt.strip(): | |
| raise gr.Error("Enter a prompt.") | |
| from PIL import Image | |
| import numpy as np | |
| from contextlib import nullcontext | |
| dev = _device() | |
| dit, lat, lat_std, sf, ae, tok, clip = _load_image_models() | |
| dit.to(dev) | |
| ae.to(dev) | |
| clip.to(dev) | |
| steps = int(steps) | |
| neg = (negative or "").strip() | |
| def clip_encode(texts): | |
| ids = tok(texts, padding="max_length", max_length=64, truncation=True, | |
| return_tensors="pt").input_ids.to(dev) | |
| with torch.no_grad(): | |
| return clip(ids).last_hidden_state.float() | |
| try: | |
| g = torch.Generator(device=dev).manual_seed(int(seed)) | |
| ctx = clip_encode([prompt]) | |
| uncond = clip_encode([neg]) if neg else torch.zeros_like(ctx) | |
| task = torch.zeros(1, dtype=torch.long, device=dev) | |
| z = torch.randn(1, 32, lat, lat, generator=g, device=dev) | |
| zs = torch.zeros(1, 32, lat, lat, device=dev) | |
| em = torch.zeros(1, 1, lat, 2 * lat, device=dev) | |
| amp = torch.autocast("cuda", dtype=torch.bfloat16) if dev == "cuda" else nullcontext() | |
| ae_dtype = next(ae.parameters()).dtype | |
| with torch.no_grad(): | |
| for i in progress.tqdm(range(steps), desc="denoising"): | |
| tt = torch.full((1,), i / steps, device=dev) | |
| inp = torch.cat([torch.cat([z, zs], dim=-1), em, em], dim=1) | |
| with amp: | |
| vc = dit(inp, tt, ctx, task)[..., :lat].float() | |
| vu = dit(inp, tt, uncond, task)[..., :lat].float() | |
| z = z + (vu + float(guidance) * (vc - vu)) / steps | |
| with amp: | |
| img = ae.decode((z * lat_std / sf).to(ae_dtype)).sample | |
| img = img.float().clamp(-1, 1)[0] | |
| arr = ((img.permute(1, 2, 0).cpu().numpy() + 1) * 127.5).clip(0, 255).astype(np.uint8) | |
| return Image.fromarray(arr) | |
| finally: | |
| dit.to("cpu") | |
| ae.to("cpu") | |
| clip.to("cpu") | |
| # Pre-build the heavy models in the MAIN process, in a background thread (non-blocking startup). The | |
| # Omni VLM was crashing because building it *inside* the GPU window blew the time limit and the worker | |
| # was killed before the result could cache — so it rebuilt and died every call. Building here once means | |
| # ZeroGPU workers inherit it and only do (fast) inference. Lock-free loaders => no fork-while-locked hang. | |
| threading.Thread(target=_warmup, daemon=True).start() | |
| # --------------------------------------------------------------------------- how it works (MoE routing) | |
| def how_it_works(prompt, layer): | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| dev = _device() | |
| enc = _enc() | |
| model, cfg = _load_llm("HobbyLM-Base") | |
| model.to(dev) | |
| try: | |
| ids = enc.encode_ordinary(prompt or "The quick brown fox jumps over the lazy dog.")[:40] | |
| if not ids: | |
| ids = enc.encode_ordinary("Hello world") | |
| toks = torch.tensor([ids], device=dev) | |
| with torch.no_grad(): | |
| model(toks) # populates last_topi on each MoE block | |
| ne, S = cfg.n_experts, len(ids) | |
| moe_layers = [i for i, b in enumerate(model.blocks) if getattr(b, "is_moe", False)] | |
| layer = min(max(int(layer), moe_layers[0]), moe_layers[-1]) | |
| blk = model.blocks[layer] | |
| topi = blk.ffn.last_topi.reshape(S, -1).cpu().numpy() | |
| topv = blk.ffn.last_topv.reshape(S, -1).cpu().float().numpy() | |
| labels = [repr(enc.decode([i]))[1:-1][:12] for i in ids] | |
| # (1) per-token routing heatmap at the chosen layer | |
| M = np.zeros((S, ne)) | |
| for s in range(S): | |
| for j in range(topi.shape[1]): | |
| M[s, int(topi[s, j])] = topv[s, j] | |
| fig1, ax = plt.subplots(figsize=(11, max(2.5, S * 0.32))) | |
| im = ax.imshow(M, aspect="auto", cmap="magma") | |
| ax.set_yticks(range(S)); ax.set_yticklabels(labels, fontsize=8) | |
| ax.set_xlabel(f"expert (0–{ne - 1})"); ax.set_ylabel("token") | |
| ax.set_title(f"Layer {layer}: each token routes to its top-{cfg.top_k} of {ne} experts (+1 shared, always on)") | |
| fig1.colorbar(im, ax=ax, label="gate weight", fraction=0.025) | |
| fig1.tight_layout() | |
| # (2) expert load across ALL MoE layers (the load-balancing story) | |
| load = np.zeros(ne) | |
| for i in moe_layers: | |
| for e in model.blocks[i].ffn.last_topi.reshape(-1).cpu().numpy(): | |
| load[int(e)] += 1 | |
| fig2, ax2 = plt.subplots(figsize=(11, 2.6)) | |
| ax2.bar(range(ne), load, color="#7c3aed") | |
| ax2.set_xlabel("expert"); ax2.set_ylabel("tokens routed") | |
| ax2.set_title(f"Expert load over all {len(moe_layers)} MoE layers — fairly even = aux-loss-free balancing working") | |
| fig2.tight_layout() | |
| active = cfg.top_k + cfg.n_shared | |
| summary = (f"**{S} tokens** · **{ne} experts/layer**, top-{cfg.top_k} routed + {cfg.n_shared} shared. " | |
| f"At each of the {len(moe_layers)} MoE layers every token uses only **{active}/{ne + cfg.n_shared} " | |
| f"experts** → that's the *sparse* in sparse-MoE: a 500M model that computes like a far smaller one " | |
| f"per token. Different tokens pick different experts (the heatmap); across the whole prompt the load " | |
| f"spreads fairly evenly (the bar chart).") | |
| return fig1, fig2, summary | |
| finally: | |
| model.to("cpu") | |
| # --------------------------------------------------------------------------- how it works (SAE features) | |
| def sae_features(prompt, topn): | |
| dev = _device() | |
| enc = _enc() | |
| try: | |
| sae, meta, labels = _load_sae() | |
| except Exception as e: | |
| return f"⚠️ SAE not available yet: {e}" | |
| model, _ = _load_llm("HobbyLM-Base") | |
| model.to(dev); sae.to(dev) | |
| layer, scale = meta["layer"], float(meta["scale"]) | |
| topn = int(topn) | |
| try: | |
| ids = enc.encode_ordinary(prompt or "I love listening to music while coding software.")[:48] | |
| if not ids: | |
| ids = enc.encode_ordinary("Hello world") | |
| toks = torch.tensor([ids], device=dev) | |
| with torch.no_grad(): | |
| h = model(toks, capture_layer=layer).float() * scale | |
| z = sae.encode(h.reshape(-1, sae.cfg.d_in)) # (S, m) | |
| md = ("Each token's residual is decomposed into a few **interpretable features** from the SAE " | |
| "dictionary. Below: per token, the strongest features (auto-labelled by the tokens they " | |
| "fire on most).\n\n| token | top active features · *(label · strength)* |\n|---|---|\n") | |
| for s, tid in enumerate(ids): | |
| v, f = z[s].topk(min(topn, z.shape[1])) | |
| tok_str = enc.decode([tid]).replace("|", "¦").replace("\n", "⏎").strip() or "·" | |
| parts = [] | |
| for val, fi in zip(v.tolist(), f.tolist()): | |
| if val <= 1e-4: | |
| continue | |
| lab = labels.get(str(int(fi)), {}).get("label") or f"feat#{int(fi)}" | |
| parts.append(f"**{lab}** ({val:.1f})") | |
| md += f"| `{tok_str}` | {' · '.join(parts) or '—'} |\n" | |
| return md | |
| finally: | |
| model.to("cpu"); sae.to("cpu") | |
| # --------------------------------------------------------------------------- UI | |
| INTRO = """# 🪶 HobbyLM Playground | |
| A from-scratch **500M sparse Mixture-of-Experts** model family (+ a 333M image DiT), trained on a hobby | |
| budget. Chat with any variant, ask questions about an image with the multimodal **Omni** model, or | |
| generate a 1024px image. Models: [rootxhacker on Hugging Face](https://huggingface.co/rootxhacker) · | |
| code: [GitHub](https://github.com/harishsg993010/HobbyLM). | |
| *These are tiny research models — fluent and fun, with the capability ceiling of a 500M model.* | |
| """ | |
| with gr.Blocks(title="HobbyLM Playground", theme=gr.themes.Soft()) as demo: | |
| gr.Markdown(INTRO) | |
| with gr.Tab("💬 Chat"): | |
| model_dd = gr.Dropdown(list(CHAT_MODELS), value=DEFAULT_CHAT, label="Model") | |
| with gr.Row(): | |
| max_new = gr.Slider(16, 512, value=200, step=8, label="Max new tokens") | |
| temp = gr.Slider(0.0, 1.5, value=0.7, step=0.05, label="Temperature (0 = greedy)") | |
| gr.ChatInterface( | |
| fn=chat_fn, | |
| type="messages", | |
| additional_inputs=[model_dd, max_new, temp], | |
| # with additional_inputs, each example row is [message, model, max_new, temp] | |
| examples=[["Give me three tips for better sleep.", DEFAULT_CHAT, 200, 0.7], | |
| ["Explain a mixture-of-experts model in one sentence.", DEFAULT_CHAT, 200, 0.7], | |
| ["Write a short poem about the ocean.", DEFAULT_CHAT, 200, 0.7]], | |
| cache_examples=False, | |
| ) | |
| with gr.Tab("🖼️ Ask about an image"): | |
| gr.Markdown("Upload an image and ask the **HobbyLM-Omni** multimodal model about it.") | |
| with gr.Row(): | |
| with gr.Column(): | |
| u_img = gr.Image(type="pil", label="Image") | |
| u_q = gr.Textbox(label="Question", value="Describe this image in detail.") | |
| u_max = gr.Slider(16, 128, value=48, step=8, label="Max new tokens") | |
| u_btn = gr.Button("Ask", variant="primary") | |
| u_out = gr.Textbox(label="Answer", lines=6) | |
| u_btn.click(understand_fn, [u_img, u_q, u_max], u_out) | |
| with gr.Tab("🎨 Generate an image"): | |
| gr.Markdown("Text-to-image with **HobbyLM-Image** (1024px DiT in DC-AE latent space). " | |
| "Strongest on single objects and cinematic scenes.") | |
| with gr.Row(): | |
| with gr.Column(): | |
| g_prompt = gr.Textbox(label="Prompt", value="a red convertible car on a coastal road, golden hour") | |
| g_neg = gr.Textbox(label="Negative prompt", value=NEG_DEFAULT) | |
| with gr.Row(): | |
| g_steps = gr.Slider(20, 120, value=60, step=5, label="Steps") | |
| g_cfg = gr.Slider(1.0, 10.0, value=5.0, step=0.5, label="Guidance (CFG)") | |
| g_seed = gr.Number(value=1234, label="Seed", precision=0) | |
| g_btn = gr.Button("Generate", variant="primary") | |
| g_out = gr.Image(label="Result", height=512) | |
| g_btn.click(generate_image_fn, [g_prompt, g_neg, g_steps, g_cfg, g_seed], g_out) | |
| gr.Examples([["a photograph of a single red apple on a plain white background", NEG_DEFAULT, 60, 5.0, 1234], | |
| ["a cozy library with tall wooden bookshelves, warm light", NEG_DEFAULT, 80, 5.0, 7], | |
| ["a bowl of fresh strawberries, studio food photography", NEG_DEFAULT, 60, 5.0, 42]], | |
| [g_prompt, g_neg, g_steps, g_cfg, g_seed], cache_examples=False) | |
| with gr.Tab("🔬 How it works"): | |
| gr.Markdown( | |
| "HobbyLM is a **sparse Mixture-of-Experts**: each MoE layer holds **36 little expert networks**, " | |
| "but a router sends every token through only its **top-6** (plus 1 always-on shared expert). " | |
| "So a 500M model does the *compute* of a much smaller one per token. Type some text and watch the " | |
| "router decide — which experts each token uses, and how the load spreads across all 36.") | |
| with gr.Row(): | |
| hiw_prompt = gr.Textbox(label="Text", value="The capital of France is Paris, a beautiful city.", scale=4) | |
| hiw_layer = gr.Slider(1, 15, value=8, step=1, label="MoE layer", scale=1) | |
| hiw_btn = gr.Button("Visualize routing", variant="primary") | |
| hiw_summary = gr.Markdown() | |
| hiw_heat = gr.Plot(label="Per-token expert routing") | |
| hiw_load = gr.Plot(label="Expert load (balancing)") | |
| hiw_btn.click(how_it_works, [hiw_prompt, hiw_layer], [hiw_heat, hiw_load, hiw_summary]) | |
| with gr.Tab("🧠 What it represents"): | |
| gr.Markdown( | |
| "A **sparse autoencoder** (SAE) trained on HobbyLM-Base's layer-8 residual stream pulls apart each " | |
| "activation into a handful of **interpretable features** from a 12,288-entry dictionary. Type text and " | |
| "see which concepts light up on each token — words, synonym clusters, syntax, formatting. This is " | |
| "*mechanistic interpretability*: looking at what the model actually represents inside.") | |
| sae_prompt = gr.Textbox(label="Text", value="I love listening to music while coding software.") | |
| sae_top = gr.Slider(2, 8, value=4, step=1, label="Features shown per token") | |
| sae_btn = gr.Button("Show features", variant="primary") | |
| sae_out = gr.Markdown() | |
| sae_btn.click(sae_features, [sae_prompt, sae_top], sae_out) | |
| if __name__ == "__main__": | |
| demo.queue(max_size=20).launch() | |