mirror of
https://github.com/Helldez/BigMoeOnEdge.git
synced 2026-10-21 13:51:31 +00:00
Three pieces that sit on one another, for every target the engine builds on. The parameter table (bmoe/params.h, core/src/params.cpp). Every RunConfig tunable is one row: key, type, group, bounds written with the constants validate() checks, help, reader and writer. bmoe-cli parses its engine flags and prints their usage from it; --describe-params prints it as JSON for a front-end to render, --show-config the resolved configuration. The hardware planner (--auto, --plan, --probe; core/src/plan). The engine measures the machine - memory it can keep and what a reclaim costs, core classes and a thread sweep, the bandwidth of the CPU and of every device on the model's own quantized matmul, what a host/device crossing costs, the storage rate by request size and lane count - reads the model from its gguf metadata, runs llama.cpp's capacity fitter, and resolves the streaming knobs with the fact behind each one. The rules are pure functions over two profiles and name no platform, vendor, device or architecture; the *_probe adapters are the only files that may. A knob set by hand is never touched, a missing fact declines instead of guessing, and nothing lossy arms itself. One memory ledger with two ceilings. Everything a run holds - context and compute buffers, a device armed for prefill, the dense set, the expert cache - is a row charged against what the process may hold and, separately, against what it may hold reclaim-exempt, net of what is already locked. The dense set is pinned whole or not at all, and the cache is sized to what can be locked where it can be locked at all. --cache-pin-fit steps the cache budget down to what the platform grants when a lock is refused part way, instead of ending pinning; off by default, set by the planner when it sized the cache to the lock. macOS. Metal in every build, the planner, --release-mmap and the memory telemetry on Darwin, --devices auto (a device with nothing placed on it stays out of the graph), the expert cache pinned in RAM, an evicted expert's pages really released, and --dense-weights ahwb as wired memory. Also: --release-mmap (+46% decode on Windows, where a live mapping serialises the streamer's unbuffered reads), a portable build that collects every binary and backend library in build/bin, and the planner in the layer maps of docs/architecture.md and AGENTS.md. Measured on a 16 GB Apple-silicon laptop with --auto alone: DeepSeek V4 Flash UD-IQ2_M 2.46 to 2.48 tok/s (0.27 before the ledger, when the plan asked for more locked memory than the machine grants), Qwen3.6-35B-A3B Q4_K_M 23.1 to 23.5 tok/s. ci: the workflows, the Android staging script and two bench scripts look for the binaries in build/bin, where the build now collects them. docs: docs/backends.md lists the eighteen compute backends in the pinned llama.cpp and which three have a measurement behind them here.
171 lines
7.5 KiB
Python
171 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""tinyMMLU under a lossy routing setting, from the desktop.
|
|
|
|
tinyMMLU (tinyBenchmarks, arXiv:2402.14992) is 100 MMLU questions chosen so that accuracy on them
|
|
estimates accuracy on the full 14k. Each question is one pass of `--ppl --ppl-step --ppl-choices`:
|
|
the question and its four options are decoded token by token, in the decode regime a cache-aware
|
|
policy actually acts in, and the answer is the letter with the highest log-probability at the end.
|
|
Zero-shot, plain prompt, no chat template, no thinking. One model load per cell.
|
|
|
|
The absolute score is below the model's published one (quantized, zero-shot, no reasoning); what
|
|
this measures is the DIFFERENCE between cells, on the same 100 questions, deterministically.
|
|
|
|
Usage:
|
|
python scripts/tinymmlu-bench.py --parquet test.parquet --cli build/bin/Release/bmoe-cli.exe \
|
|
--model M.gguf --out results/ --lambda 0 --lambda 0.15 [--limit N]
|
|
|
|
The parquet is data/test-00000-of-00001.parquet from huggingface.co/datasets/tinyBenchmarks/tinyMMLU.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
LETTERS = ["A", "B", "C", "D"]
|
|
|
|
|
|
def build_prompts(parquet, out_dir, limit):
|
|
import pandas as pd
|
|
|
|
df = pd.read_parquet(parquet)
|
|
if limit:
|
|
df = df.iloc[:limit]
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
keys = []
|
|
for i, row in df.iterrows():
|
|
subject = str(row["subject"]).replace("_", " ")
|
|
lines = [f"The following is a multiple choice question about {subject}.", "", str(row["question"]).strip()]
|
|
for letter, choice in zip(LETTERS, list(row["choices"])):
|
|
lines.append(f"{letter}. {str(choice).strip()}")
|
|
lines.append("Answer:")
|
|
path = os.path.join(out_dir, f"q{i:03d}.txt")
|
|
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
|
f.write("\n".join(lines))
|
|
keys.append({"path": path, "answer": int(row["answer"]), "subject": str(row["subject"])})
|
|
with open(os.path.join(out_dir, "list.txt"), "w", encoding="utf-8", newline="\n") as f:
|
|
f.write("\n".join(k["path"] for k in keys) + "\n")
|
|
with open(os.path.join(out_dir, "key.json"), "w", encoding="utf-8") as f:
|
|
json.dump(keys, f, indent=1)
|
|
return keys
|
|
|
|
|
|
def done_paths(log_path):
|
|
"""Questions a previous, interrupted run of this cell already scored."""
|
|
done = set()
|
|
if not os.path.exists(log_path):
|
|
return done
|
|
current = None
|
|
with open(log_path, encoding="utf-8", errors="replace") as f:
|
|
for line in f:
|
|
if line.startswith("ppl-file: "):
|
|
current = os.path.normcase(line[len("ppl-file: "):].strip())
|
|
elif line.startswith("ppl-choices:") and current:
|
|
done.add(current)
|
|
return done
|
|
|
|
|
|
def run_cell(cli, model, list_path, lam, log_path, threads, cache_mb, ctx):
|
|
# Resumable: a cell interrupted half-way keeps what it scored and continues from the first
|
|
# question without a result, appending to the same log. A long cell on a slow host can then
|
|
# survive a lost terminal.
|
|
done = done_paths(log_path)
|
|
with open(list_path, encoding="utf-8") as f:
|
|
todo = [p.strip() for p in f if p.strip() and os.path.normcase(p.strip()) not in done]
|
|
if not todo:
|
|
return
|
|
if done:
|
|
print(f" resuming: {len(done)} scored, {len(todo)} to go", flush=True)
|
|
list_path = log_path + ".todo"
|
|
with open(list_path, "w", encoding="utf-8", newline="\n") as f:
|
|
f.write("\n".join(todo) + "\n")
|
|
cmd = [
|
|
cli, "-m", model, "-t", str(threads), "-c", str(ctx), "--ubatch", str(ctx), "--moe-stream",
|
|
"--cache-mb", str(cache_mb), "--io-threads", "4", "--overlap",
|
|
"--ppl-list", list_path, "--ppl-step", "--ppl-choices", " A, B, C, D",
|
|
]
|
|
if lam > 0:
|
|
cmd += ["--expert-substitute", str(lam)]
|
|
# On Windows the CLI's DLLs live in build/bin/<config>, beside the CLI or one level across; put that
|
|
# directory on PATH so the runner does not depend on the caller's shell.
|
|
env = dict(os.environ)
|
|
cli_dir = os.path.dirname(os.path.abspath(cli))
|
|
bin_dir = os.path.normpath(os.path.join(cli_dir, "..", "..", "bin", os.path.basename(cli_dir)))
|
|
env["PATH"] = bin_dir + os.pathsep + env.get("PATH", "")
|
|
with open(log_path, "a", encoding="utf-8") as log:
|
|
rc = subprocess.run(cmd, stdout=log, stderr=subprocess.STDOUT, check=False, env=env).returncode
|
|
if rc != 0:
|
|
print(f"bmoe-cli exited with {rc} (see {log_path})", file=sys.stderr)
|
|
|
|
|
|
def parse_cell(log_path, keys):
|
|
results = []
|
|
current = None
|
|
substituted = reranked = 0
|
|
with open(log_path, encoding="utf-8", errors="replace") as f:
|
|
for line in f:
|
|
if line.startswith("ppl-file: "):
|
|
current = line[len("ppl-file: "):].strip()
|
|
elif line.startswith("ppl-choices:"):
|
|
parts = line.split()[1:]
|
|
logp = [float(p.split("=")[1]) for p in parts]
|
|
results.append((current, logp))
|
|
elif line.startswith("ppl-policy:"):
|
|
# "... dropped, S/R reranked slots substituted"
|
|
tail = line.split("dropped,")[1].split()[0]
|
|
s, r = tail.split("/")
|
|
substituted += int(s)
|
|
reranked += int(r)
|
|
by_path = {os.path.normcase(k["path"]): k for k in keys}
|
|
correct = 0
|
|
rows = []
|
|
for path, logp in results:
|
|
k = by_path.get(os.path.normcase(path))
|
|
if not k:
|
|
continue
|
|
pred = max(range(len(logp)), key=lambda j: logp[j])
|
|
ok = pred == k["answer"]
|
|
correct += ok
|
|
rows.append({"path": os.path.basename(path), "subject": k["subject"], "pred": LETTERS[pred],
|
|
"answer": LETTERS[k["answer"]], "correct": ok, "logp": logp})
|
|
return correct, len(rows), substituted, reranked, rows
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--parquet", required=True)
|
|
ap.add_argument("--cli", required=True)
|
|
ap.add_argument("--model", required=True)
|
|
ap.add_argument("--out", required=True)
|
|
ap.add_argument("--lambda", dest="lambdas", type=float, action="append", required=True)
|
|
ap.add_argument("--limit", type=int, default=0)
|
|
ap.add_argument("--threads", type=int, default=8)
|
|
ap.add_argument("--cache-mb", type=int, default=2000)
|
|
ap.add_argument("--ctx", type=int, default=512)
|
|
args = ap.parse_args()
|
|
|
|
keys = build_prompts(args.parquet, os.path.join(args.out, "prompts"), args.limit)
|
|
list_path = os.path.join(args.out, "prompts", "list.txt")
|
|
print(f"{len(keys)} questions", flush=True)
|
|
summary = []
|
|
for lam in args.lambdas:
|
|
log_path = os.path.join(args.out, f"cell_L{lam:g}.log")
|
|
print(f"cell L={lam:g} ...", flush=True)
|
|
run_cell(args.cli, args.model, list_path, lam, log_path, args.threads, args.cache_mb, args.ctx)
|
|
correct, n, sub, rer, rows = parse_cell(log_path, keys)
|
|
pct = 100.0 * correct / n if n else 0.0
|
|
frac = 100.0 * sub / rer if rer else 0.0
|
|
summary.append((lam, correct, n, pct, frac))
|
|
with open(os.path.join(args.out, f"cell_L{lam:g}.json"), "w", encoding="utf-8") as f:
|
|
json.dump(rows, f, indent=1)
|
|
print(f"L={lam:g}: {correct}/{n} ({pct:.1f}%), {frac:.1f}% of slots substituted", flush=True)
|
|
print()
|
|
print("| `--expert-substitute` | tinyMMLU | slots substituted |")
|
|
print("|---|---:|---:|")
|
|
for lam, correct, n, pct, frac in summary:
|
|
print(f"| {lam:g} | {correct}/{n} ({pct:.1f} %) | {frac:.1f} % |")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|