oddadmix commited on
Commit
73caa3e
·
verified ·
1 Parent(s): e982c21

Add the 6M BERT encoder as a switchable backbone alongside the 52M decoder

Browse files
Files changed (3) hide show
  1. app.py +31 -10
  2. static/app.js +26 -4
  3. 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
- print(f"[*] loading {MODEL_ID}", flush=True)
23
- TOK = AutoTokenizer.from_pretrained(MODEL_ID, token=HF_TOKEN)
24
- MODEL = RouterModel.from_pretrained(MODEL_ID, token=HF_TOKEN)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  torch.set_num_threads(int(os.environ.get("OMP_NUM_THREADS", 4)))
26
- print(f"[+] ready — {sum(p.numel() for p in MODEL.parameters())/1e6:.2f}M params", flush=True)
 
 
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": sum(p.numel() for p in MODEL.parameters()), "max_routes": MAX_ROUTES}
 
 
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(MODEL, TOK, text, cats)
55
  full, _ = build_text(text, cats)
56
- ntok = len(TOK.encode(full, add_special_tokens=False))
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
- }).catch(() => { $("modelname").textContent = "غير متصل"; });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <span id="modelname">…</span>
 
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>