Cortex.0 / scripts /experiments /bench_experts.py
Frankenstein-Labs's picture
initial commit
c429240
Raw History Blame Contribute Delete
3.94 kB
"""Measure what changing the number of active MoE experts actually does.
Run against any MoE GGUF. It loads the model once per expert count, times a
fixed generation, and reports throughput. The point is to replace a guess with
a number before touching a serving configuration.
"""
from __future__ import annotations
import argparse
import json
import time
from pathlib import Path
from llama_cpp import Llama
PROMPT = "Write a Python function that reverses a string."
def measure(
model_path: str,
expert_count: int | None,
tokens: int,
threads: int,
) -> dict:
"""Load the model with an optional expert override and time a generation."""
key = "expert_used_count"
overrides = None
if expert_count is not None:
overrides = {key: expert_count}
kwargs = {"kv_overrides": overrides} if overrides else {}
started = time.monotonic()
try:
model = Llama(
model_path=model_path,
n_ctx=512,
n_threads=threads,
verbose=False,
**kwargs,
)
except Exception as exc:
return {"experts": expert_count, "error": f"{type(exc).__name__}: {exc}"}
load_s = time.monotonic() - started
arch = model.metadata.get("general.architecture", "")
native = model.metadata.get(f"{arch}.{key}")
started = time.monotonic()
out = model.create_chat_completion(
messages=[{"role": "user", "content": PROMPT}],
max_tokens=tokens,
temperature=0.0,
)
gen_s = time.monotonic() - started
produced = out.get("usage", {}).get("completion_tokens", 0)
text = out["choices"][0]["message"]["content"]
del model
return {
"experts": expert_count,
"native_experts": native,
"load_s": round(load_s, 2),
"gen_s": round(gen_s, 2),
"tokens": produced,
"tok_per_s": round(produced / gen_s, 1) if gen_s else None,
"preview": text[:120],
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("model", help="path to a MoE GGUF file")
parser.add_argument(
"--experts",
default="2,8,15",
help="comma-separated expert counts to try (omit for native only)",
)
parser.add_argument("--tokens", type=int, default=128)
parser.add_argument("--threads", type=int, default=4)
parser.add_argument("--json", help="write the results to this JSON file")
args = parser.parse_args()
if not Path(args.model).is_file():
parser.error(f"model not found: {args.model}")
counts: list[int | None] = [None]
counts += [int(c) for c in args.experts.split(",") if c.strip()]
results = []
for count in counts:
label = "native" if count is None else f"{count} experts"
print(f"--- {label}", flush=True)
row = measure(args.model, count, args.tokens, args.threads)
results.append(row)
if "error" in row:
print(f" FAILED: {row['error']}", flush=True)
else:
print(
f" {row['tok_per_s']} tok/s "
f"(load {row['load_s']}s, gen {row['gen_s']}s)",
flush=True,
)
baseline = next((r for r in results if r.get("experts") is None), None)
if baseline and baseline.get("tok_per_s"):
print("\nrelative to native:", flush=True)
for row in results:
if row.get("tok_per_s"):
ratio = row["tok_per_s"] / baseline["tok_per_s"]
print(
f" {str(row['experts']):>4} experts -> x{ratio:.2f} speed",
flush=True,
)
if args.json:
Path(args.json).write_text(
json.dumps(results, indent=2, ensure_ascii=False), encoding="utf-8"
)
print(f"\nwritten to {args.json}", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())