Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- app.py +77 -0
- hobbylm/model.py +6 -2
- hobbylm/moe.py +3 -0
- requirements.txt +1 -0
app.py
CHANGED
|
@@ -318,6 +318,68 @@ def generate_image_fn(prompt, negative, steps, guidance, seed, progress=gr.Progr
|
|
| 318 |
threading.Thread(target=_warmup, daemon=True).start()
|
| 319 |
|
| 320 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
# --------------------------------------------------------------------------- UI
|
| 322 |
|
| 323 |
INTRO = """# 🪶 HobbyLM Playground
|
|
@@ -379,5 +441,20 @@ with gr.Blocks(title="HobbyLM Playground", theme=gr.themes.Soft()) as demo:
|
|
| 379 |
["a bowl of fresh strawberries, studio food photography", NEG_DEFAULT, 60, 5.0, 42]],
|
| 380 |
[g_prompt, g_neg, g_steps, g_cfg, g_seed], cache_examples=False)
|
| 381 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
if __name__ == "__main__":
|
| 383 |
demo.queue(max_size=20).launch()
|
|
|
|
| 318 |
threading.Thread(target=_warmup, daemon=True).start()
|
| 319 |
|
| 320 |
|
| 321 |
+
# --------------------------------------------------------------------------- how it works (MoE routing)
|
| 322 |
+
|
| 323 |
+
@spaces.GPU(duration=90)
|
| 324 |
+
def how_it_works(prompt, layer):
|
| 325 |
+
import matplotlib
|
| 326 |
+
matplotlib.use("Agg")
|
| 327 |
+
import matplotlib.pyplot as plt
|
| 328 |
+
import numpy as np
|
| 329 |
+
dev = _device()
|
| 330 |
+
enc = _enc()
|
| 331 |
+
model, cfg = _load_llm("HobbyLM-Base")
|
| 332 |
+
model.to(dev)
|
| 333 |
+
try:
|
| 334 |
+
ids = enc.encode_ordinary(prompt or "The quick brown fox jumps over the lazy dog.")[:40]
|
| 335 |
+
if not ids:
|
| 336 |
+
ids = enc.encode_ordinary("Hello world")
|
| 337 |
+
toks = torch.tensor([ids], device=dev)
|
| 338 |
+
with torch.no_grad():
|
| 339 |
+
model(toks) # populates last_topi on each MoE block
|
| 340 |
+
ne, S = cfg.n_experts, len(ids)
|
| 341 |
+
moe_layers = [i for i, b in enumerate(model.blocks) if getattr(b, "is_moe", False)]
|
| 342 |
+
layer = min(max(int(layer), moe_layers[0]), moe_layers[-1])
|
| 343 |
+
blk = model.blocks[layer]
|
| 344 |
+
topi = blk.ffn.last_topi.reshape(S, -1).cpu().numpy()
|
| 345 |
+
topv = blk.ffn.last_topv.reshape(S, -1).cpu().float().numpy()
|
| 346 |
+
labels = [repr(enc.decode([i]))[1:-1][:12] for i in ids]
|
| 347 |
+
|
| 348 |
+
# (1) per-token routing heatmap at the chosen layer
|
| 349 |
+
M = np.zeros((S, ne))
|
| 350 |
+
for s in range(S):
|
| 351 |
+
for j in range(topi.shape[1]):
|
| 352 |
+
M[s, int(topi[s, j])] = topv[s, j]
|
| 353 |
+
fig1, ax = plt.subplots(figsize=(11, max(2.5, S * 0.32)))
|
| 354 |
+
im = ax.imshow(M, aspect="auto", cmap="magma")
|
| 355 |
+
ax.set_yticks(range(S)); ax.set_yticklabels(labels, fontsize=8)
|
| 356 |
+
ax.set_xlabel(f"expert (0–{ne - 1})"); ax.set_ylabel("token")
|
| 357 |
+
ax.set_title(f"Layer {layer}: each token routes to its top-{cfg.top_k} of {ne} experts (+1 shared, always on)")
|
| 358 |
+
fig1.colorbar(im, ax=ax, label="gate weight", fraction=0.025)
|
| 359 |
+
fig1.tight_layout()
|
| 360 |
+
|
| 361 |
+
# (2) expert load across ALL MoE layers (the load-balancing story)
|
| 362 |
+
load = np.zeros(ne)
|
| 363 |
+
for i in moe_layers:
|
| 364 |
+
for e in model.blocks[i].ffn.last_topi.reshape(-1).cpu().numpy():
|
| 365 |
+
load[int(e)] += 1
|
| 366 |
+
fig2, ax2 = plt.subplots(figsize=(11, 2.6))
|
| 367 |
+
ax2.bar(range(ne), load, color="#7c3aed")
|
| 368 |
+
ax2.set_xlabel("expert"); ax2.set_ylabel("tokens routed")
|
| 369 |
+
ax2.set_title(f"Expert load over all {len(moe_layers)} MoE layers — fairly even = aux-loss-free balancing working")
|
| 370 |
+
fig2.tight_layout()
|
| 371 |
+
|
| 372 |
+
active = cfg.top_k + cfg.n_shared
|
| 373 |
+
summary = (f"**{S} tokens** · **{ne} experts/layer**, top-{cfg.top_k} routed + {cfg.n_shared} shared. "
|
| 374 |
+
f"At each of the {len(moe_layers)} MoE layers every token uses only **{active}/{ne + cfg.n_shared} "
|
| 375 |
+
f"experts** → that's the *sparse* in sparse-MoE: a 500M model that computes like a far smaller one "
|
| 376 |
+
f"per token. Different tokens pick different experts (the heatmap); across the whole prompt the load "
|
| 377 |
+
f"spreads fairly evenly (the bar chart).")
|
| 378 |
+
return fig1, fig2, summary
|
| 379 |
+
finally:
|
| 380 |
+
model.to("cpu")
|
| 381 |
+
|
| 382 |
+
|
| 383 |
# --------------------------------------------------------------------------- UI
|
| 384 |
|
| 385 |
INTRO = """# 🪶 HobbyLM Playground
|
|
|
|
| 441 |
["a bowl of fresh strawberries, studio food photography", NEG_DEFAULT, 60, 5.0, 42]],
|
| 442 |
[g_prompt, g_neg, g_steps, g_cfg, g_seed], cache_examples=False)
|
| 443 |
|
| 444 |
+
with gr.Tab("🔬 How it works"):
|
| 445 |
+
gr.Markdown(
|
| 446 |
+
"HobbyLM is a **sparse Mixture-of-Experts**: each MoE layer holds **36 little expert networks**, "
|
| 447 |
+
"but a router sends every token through only its **top-6** (plus 1 always-on shared expert). "
|
| 448 |
+
"So a 500M model does the *compute* of a much smaller one per token. Type some text and watch the "
|
| 449 |
+
"router decide — which experts each token uses, and how the load spreads across all 36.")
|
| 450 |
+
with gr.Row():
|
| 451 |
+
hiw_prompt = gr.Textbox(label="Text", value="The capital of France is Paris, a beautiful city.", scale=4)
|
| 452 |
+
hiw_layer = gr.Slider(1, 15, value=8, step=1, label="MoE layer", scale=1)
|
| 453 |
+
hiw_btn = gr.Button("Visualize routing", variant="primary")
|
| 454 |
+
hiw_summary = gr.Markdown()
|
| 455 |
+
hiw_heat = gr.Plot(label="Per-token expert routing")
|
| 456 |
+
hiw_load = gr.Plot(label="Expert load (balancing)")
|
| 457 |
+
hiw_btn.click(how_it_works, [hiw_prompt, hiw_layer], [hiw_heat, hiw_load, hiw_summary])
|
| 458 |
+
|
| 459 |
if __name__ == "__main__":
|
| 460 |
demo.queue(max_size=20).launch()
|
hobbylm/model.py
CHANGED
|
@@ -320,7 +320,8 @@ class MoETransformer(nn.Module):
|
|
| 320 |
return self._rope_cache[key]
|
| 321 |
|
| 322 |
def forward(self, idx: Tensor | None = None, targets: Tensor | None = None,
|
| 323 |
-
inputs_embeds: Tensor | None = None, p_mask: Tensor | None = None
|
|
|
|
| 324 |
# accept either token ids OR precomputed embeddings (inputs_embeds), for multimodal splicing.
|
| 325 |
if inputs_embeds is None:
|
| 326 |
x = self.embed(idx)
|
|
@@ -333,9 +334,12 @@ class MoETransformer(nn.Module):
|
|
| 333 |
B, S = x.shape[0], x.shape[1]
|
| 334 |
cos, sin = self.rope(S, device, x.dtype)
|
| 335 |
aux_sum = x.new_zeros(())
|
| 336 |
-
for blk in self.blocks:
|
| 337 |
x, aux = blk(x, cos, sin)
|
| 338 |
aux_sum = aux_sum + aux
|
|
|
|
|
|
|
|
|
|
| 339 |
x = self.final_norm(x)
|
| 340 |
cfg = self.cfg
|
| 341 |
sc = cfg.logit_softcap
|
|
|
|
| 320 |
return self._rope_cache[key]
|
| 321 |
|
| 322 |
def forward(self, idx: Tensor | None = None, targets: Tensor | None = None,
|
| 323 |
+
inputs_embeds: Tensor | None = None, p_mask: Tensor | None = None,
|
| 324 |
+
capture_layer: int | None = None):
|
| 325 |
# accept either token ids OR precomputed embeddings (inputs_embeds), for multimodal splicing.
|
| 326 |
if inputs_embeds is None:
|
| 327 |
x = self.embed(idx)
|
|
|
|
| 334 |
B, S = x.shape[0], x.shape[1]
|
| 335 |
cos, sin = self.rope(S, device, x.dtype)
|
| 336 |
aux_sum = x.new_zeros(())
|
| 337 |
+
for i, blk in enumerate(self.blocks):
|
| 338 |
x, aux = blk(x, cos, sin)
|
| 339 |
aux_sum = aux_sum + aux
|
| 340 |
+
# interp hook: return the residual stream after block `capture_layer` (and skip the rest)
|
| 341 |
+
if capture_layer is not None and i == capture_layer:
|
| 342 |
+
return x
|
| 343 |
x = self.final_norm(x)
|
| 344 |
cfg = self.cfg
|
| 345 |
sc = cfg.logit_softcap
|
hobbylm/moe.py
CHANGED
|
@@ -145,6 +145,9 @@ class MoE(nn.Module):
|
|
| 145 |
B, S, d = x.shape
|
| 146 |
xf = x.reshape(-1, d)
|
| 147 |
topi, topv, aux = self._route(xf)
|
|
|
|
|
|
|
|
|
|
| 148 |
if self.backend == "grouped":
|
| 149 |
out = self._experts_grouped(xf, topi, topv)
|
| 150 |
else:
|
|
|
|
| 145 |
B, S, d = x.shape
|
| 146 |
xf = x.reshape(-1, d)
|
| 147 |
topi, topv, aux = self._route(xf)
|
| 148 |
+
# interp capture: remember this layer's per-token expert selection (top-k indices + gate weights)
|
| 149 |
+
self.last_topi = topi.detach() # (T=B*S, k)
|
| 150 |
+
self.last_topv = topv.detach() # (T=B*S, k)
|
| 151 |
if self.backend == "grouped":
|
| 152 |
out = self._experts_grouped(xf, topi, topv)
|
| 153 |
else:
|
requirements.txt
CHANGED
|
@@ -8,3 +8,4 @@ tiktoken
|
|
| 8 |
pillow
|
| 9 |
numpy
|
| 10 |
sentencepiece
|
|
|
|
|
|
| 8 |
pillow
|
| 9 |
numpy
|
| 10 |
sentencepiece
|
| 11 |
+
matplotlib
|