Spaces:
Running
Running
Add the 6M BERT encoder as a switchable backbone alongside the 52M decoder
Browse files- app.py +31 -10
- static/app.js +26 -4
- static/index.html +2 -1
app.py
CHANGED
|
@@ -16,14 +16,30 @@ from transformers import AutoTokenizer
|
|
| 16 |
|
| 17 |
from routing_model import MAX_ROUTES, RouterModel, build_text, route
|
| 18 |
|
| 19 |
-
MODEL_ID = os.environ.get("MODEL_ID", "oddadmix/Nawah-Router-v3")
|
| 20 |
HF_TOKEN = os.environ.get("MODEL_HF_TOKEN") or os.environ.get("HF_TOKEN")
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
torch.set_num_threads(int(os.environ.get("OMP_NUM_THREADS", 4)))
|
| 26 |
-
|
|
|
|
|
|
|
| 27 |
|
| 28 |
app = FastAPI()
|
| 29 |
|
|
@@ -31,6 +47,7 @@ app = FastAPI()
|
|
| 31 |
class RouteReq(BaseModel):
|
| 32 |
text: str = ""
|
| 33 |
cats: list[str] = []
|
|
|
|
| 34 |
|
| 35 |
|
| 36 |
@app.get("/")
|
|
@@ -40,8 +57,10 @@ def index():
|
|
| 40 |
|
| 41 |
@app.get("/api/ready")
|
| 42 |
def ready():
|
| 43 |
-
return {"ready": True, "model": MODEL_ID,
|
| 44 |
-
"params":
|
|
|
|
|
|
|
| 45 |
|
| 46 |
|
| 47 |
@app.post("/api/route")
|
|
@@ -50,16 +69,18 @@ def api_route(req: RouteReq):
|
|
| 50 |
text = (req.text or "").strip()
|
| 51 |
if not text or not cats:
|
| 52 |
return JSONResponse({"results": [], "ms": 0, "tokens": 0})
|
|
|
|
| 53 |
t0 = time.perf_counter()
|
| 54 |
-
res = route(
|
| 55 |
full, _ = build_text(text, cats)
|
| 56 |
-
ntok = len(
|
| 57 |
order = {r["route"]: r["score"] for r in res}
|
| 58 |
return JSONResponse({
|
| 59 |
# returned in the caller's order so the UI does not reshuffle rows under the cursor
|
| 60 |
"results": [{"route": c, "score": order.get(c, 0.0)} for c in cats],
|
| 61 |
"top": max(range(len(cats)), key=lambda i: order.get(cats[i], 0.0)),
|
| 62 |
-
"ms": round((time.perf_counter() - t0) * 1000), "tokens": ntok
|
|
|
|
| 63 |
|
| 64 |
|
| 65 |
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
|
|
| 16 |
|
| 17 |
from routing_model import MAX_ROUTES, RouterModel, build_text, route
|
| 18 |
|
|
|
|
| 19 |
HF_TOKEN = os.environ.get("MODEL_HF_TOKEN") or os.environ.get("HF_TOKEN")
|
| 20 |
|
| 21 |
+
# Two backbones on the same head and the same task. The 6M is a bidirectional
|
| 22 |
+
# BERT encoder; the 52M is a Llama decoder. Measured on the same eval sets in one
|
| 23 |
+
# session, the encoder wins three of four - see the model cards.
|
| 24 |
+
REGISTRY = {
|
| 25 |
+
"52M": {"repo": "oddadmix/Nawah-Router-v3", "label": "Nawah-Router-v3 (Llama decoder)"},
|
| 26 |
+
"6M-BERT": {"repo": "oddadmix/Nawah-Router-BERT-6M-v2", "label": "Nawah-Router-BERT-6M-v2 (BERT encoder)"},
|
| 27 |
+
}
|
| 28 |
+
DEFAULT = os.environ.get("MODEL_KEY", "6M-BERT")
|
| 29 |
+
|
| 30 |
+
LOADED = {}
|
| 31 |
+
for key, spec in REGISTRY.items():
|
| 32 |
+
print(f"[*] loading {spec['repo']}", flush=True)
|
| 33 |
+
tok = AutoTokenizer.from_pretrained(spec["repo"], token=HF_TOKEN)
|
| 34 |
+
mdl = RouterModel.from_pretrained(spec["repo"], token=HF_TOKEN)
|
| 35 |
+
LOADED[key] = {"tok": tok, "model": mdl, "label": spec["label"],
|
| 36 |
+
"repo": spec["repo"],
|
| 37 |
+
"params": sum(p.numel() for p in mdl.parameters())}
|
| 38 |
+
print(f"[+] {key}: {LOADED[key]['params']/1e6:.2f}M params", flush=True)
|
| 39 |
torch.set_num_threads(int(os.environ.get("OMP_NUM_THREADS", 4)))
|
| 40 |
+
|
| 41 |
+
MODEL_ID = REGISTRY[DEFAULT]["repo"]
|
| 42 |
+
TOK, MODEL = LOADED[DEFAULT]["tok"], LOADED[DEFAULT]["model"]
|
| 43 |
|
| 44 |
app = FastAPI()
|
| 45 |
|
|
|
|
| 47 |
class RouteReq(BaseModel):
|
| 48 |
text: str = ""
|
| 49 |
cats: list[str] = []
|
| 50 |
+
model: str = DEFAULT
|
| 51 |
|
| 52 |
|
| 53 |
@app.get("/")
|
|
|
|
| 57 |
|
| 58 |
@app.get("/api/ready")
|
| 59 |
def ready():
|
| 60 |
+
return {"ready": True, "model": MODEL_ID, "default": DEFAULT,
|
| 61 |
+
"params": LOADED[DEFAULT]["params"], "max_routes": MAX_ROUTES,
|
| 62 |
+
"models": [{"key": k, "label": v["label"], "repo": v["repo"],
|
| 63 |
+
"params": v["params"]} for k, v in LOADED.items()]}
|
| 64 |
|
| 65 |
|
| 66 |
@app.post("/api/route")
|
|
|
|
| 69 |
text = (req.text or "").strip()
|
| 70 |
if not text or not cats:
|
| 71 |
return JSONResponse({"results": [], "ms": 0, "tokens": 0})
|
| 72 |
+
sel = LOADED.get(req.model) or LOADED[DEFAULT]
|
| 73 |
t0 = time.perf_counter()
|
| 74 |
+
res = route(sel["model"], sel["tok"], text, cats)
|
| 75 |
full, _ = build_text(text, cats)
|
| 76 |
+
ntok = len(sel["tok"].encode(full, add_special_tokens=False))
|
| 77 |
order = {r["route"]: r["score"] for r in res}
|
| 78 |
return JSONResponse({
|
| 79 |
# returned in the caller's order so the UI does not reshuffle rows under the cursor
|
| 80 |
"results": [{"route": c, "score": order.get(c, 0.0)} for c in cats],
|
| 81 |
"top": max(range(len(cats)), key=lambda i: order.get(cats[i], 0.0)),
|
| 82 |
+
"ms": round((time.perf_counter() - t0) * 1000), "tokens": ntok,
|
| 83 |
+
"model": sel["label"], "params": sel["params"]})
|
| 84 |
|
| 85 |
|
| 86 |
app.mount("/static", StaticFiles(directory="static"), name="static")
|
static/app.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
| 1 |
/* Nawah-Router demo — editable categories, live routing, connectors weighted by probability. */
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
const $ = id => document.getElementById(id);
|
| 3 |
|
| 4 |
const PRESETS = [
|
|
@@ -132,7 +136,7 @@ async function run() {
|
|
| 132 |
try {
|
| 133 |
const res = await fetch("/api/route", {
|
| 134 |
method: "POST", headers: { "Content-Type": "application/json" },
|
| 135 |
-
body: JSON.stringify({ text, cats: cats() })
|
| 136 |
});
|
| 137 |
const data = await res.json();
|
| 138 |
if (my !== seq) return; // a newer keystroke already superseded this
|
|
@@ -169,7 +173,25 @@ PRESETS.forEach((p, i) => {
|
|
| 169 |
|
| 170 |
fetch("/api/ready").then(r => r.json()).then(d => {
|
| 171 |
$("dot").classList.add("on");
|
| 172 |
-
$("modelname").textContent = d.model.split("/").pop();
|
| 173 |
-
$("modelsize").textContent = (d.params / 1e6).toFixed(1) + "M";
|
| 174 |
$("nodemeta").textContent = `CPU · مسار واحد · حتى ${d.max_routes} فئات`;
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
/* Nawah-Router demo — editable categories, live routing, connectors weighted by probability. */
|
| 2 |
+
/* Two backbones on the same head: a 6M bidirectional BERT encoder and the 52M
|
| 3 |
+
Llama decoder. Switching re-routes immediately so the comparison lands on the
|
| 4 |
+
same text and the same categories. Declared here because run() references it. */
|
| 5 |
+
var currentModel = "6M-BERT";
|
| 6 |
const $ = id => document.getElementById(id);
|
| 7 |
|
| 8 |
const PRESETS = [
|
|
|
|
| 136 |
try {
|
| 137 |
const res = await fetch("/api/route", {
|
| 138 |
method: "POST", headers: { "Content-Type": "application/json" },
|
| 139 |
+
body: JSON.stringify({ text, cats: cats(), model: currentModel })
|
| 140 |
});
|
| 141 |
const data = await res.json();
|
| 142 |
if (my !== seq) return; // a newer keystroke already superseded this
|
|
|
|
| 173 |
|
| 174 |
fetch("/api/ready").then(r => r.json()).then(d => {
|
| 175 |
$("dot").classList.add("on");
|
|
|
|
|
|
|
| 176 |
$("nodemeta").textContent = `CPU · مسار واحد · حتى ${d.max_routes} فئات`;
|
| 177 |
+
currentModel = d.default || currentModel;
|
| 178 |
+
const pick = $("modelpick");
|
| 179 |
+
(d.models || []).forEach(m => {
|
| 180 |
+
const o = document.createElement("option");
|
| 181 |
+
o.value = m.key;
|
| 182 |
+
o.textContent = m.key === "6M-BERT" ? "🔵 6M encoder" : "🧱 52M decoder";
|
| 183 |
+
o.title = m.label;
|
| 184 |
+
if (m.key === currentModel) o.selected = true;
|
| 185 |
+
pick.appendChild(o);
|
| 186 |
+
});
|
| 187 |
+
const sizeOf = k => {
|
| 188 |
+
const m = (d.models || []).find(x => x.key === k);
|
| 189 |
+
return m ? (m.params / 1e6).toFixed(1) + "M" : "";
|
| 190 |
+
};
|
| 191 |
+
$("modelsize").textContent = sizeOf(currentModel);
|
| 192 |
+
pick.addEventListener("change", () => {
|
| 193 |
+
currentModel = pick.value;
|
| 194 |
+
$("modelsize").textContent = sizeOf(currentModel);
|
| 195 |
+
run(); // same text, same categories, other backbone
|
| 196 |
+
});
|
| 197 |
+
}).catch(() => { $("modelsize").textContent = "غير متصل"; });
|
static/index.html
CHANGED
|
@@ -22,7 +22,8 @@
|
|
| 22 |
</div>
|
| 23 |
<div class="modelchip" id="modelchip">
|
| 24 |
<span class="dot" id="dot"></span>
|
| 25 |
-
<
|
|
|
|
| 26 |
<span class="mono" id="modelsize"></span>
|
| 27 |
</div>
|
| 28 |
</header>
|
|
|
|
| 22 |
</div>
|
| 23 |
<div class="modelchip" id="modelchip">
|
| 24 |
<span class="dot" id="dot"></span>
|
| 25 |
+
<select id="modelpick" style="background:transparent;border:0;color:inherit;
|
| 26 |
+
font:inherit;cursor:pointer;outline:none"></select>
|
| 27 |
<span class="mono" id="modelsize"></span>
|
| 28 |
</div>
|
| 29 |
</header>
|