Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions windows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,27 @@ Context to keep when reading these numbers:
- Same-condition A/B against unpatched vanilla llama.cpp (vendor tree with the patch bands skipped) shows the patch set costs nothing (8B: 41.5 vs 39.5 decode; 35B: 27.2 vs 27.4 — within machine drift).
- Expect day-to-day drift of a few percent on this class of machine; compare like-for-like, same day, same process if you benchmark.

### 2.1 Community benchmark submission (Hardware Benchmarks #142)

To add a Windows row to the Hardware Benchmarks discussion (#142), build the
engine and download a tier as in §1, then run:

python tools/r3_bench.py --tier 8b --submission --reporter @you

The script discards the short warmup, runs 2 timed reps (`--reps N` to
override) on the ~3k-token long-context prompt (same construction as the
Python bench's BENCH_LONG), and prints a ready-to-paste submission block —
the mean of the timed reps, with `device` / `chip` / `gpu` (the Vulkan device
from the engine log) / `ram_gb` / `os` / `engine_commit` (vendor.llama.pin)
filled in automatically. `--json` additionally prints the block as a single
JSON line for aggregation, and `benchmarks/r3/bench_<tier>_submission.json`
stores the per-rep detail.

On the memory field: `peak_active_gib` on Windows is the engine process's
peak working set during the timed reps (OS-level high-water mark). Windows
has no allocator-level counter, so this is not directly comparable with the
python-mlx column's `mlx.core.get_peak_memory()`.

---

## 3. Technical Details
Expand Down
227 changes: 216 additions & 11 deletions windows/tools/r3_bench.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@
window when compared against the reference — cross-window comparison is always
suspect (measure on the same day / same process, or treat numbers as shape-only).

Community submission mode (Hardware Benchmarks discussion #142, issue #139):
`--submission` discards the same short warmup, then runs `reps` timed reps
(default 2) on the ~3k-token long-context prompt, averages the numbers, and
prints the ready-to-paste submission block; `--json` prints the same data as
JSON for aggregation.

Usage: python tools/r3_bench.py --tier 35b [--reps 3]
python tools/r3_bench.py --tier 8b --submission --reporter @you
"""
import argparse, json, os, re, subprocess, sys, time
import argparse, ctypes, json, os, re, subprocess, sys, time

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

Expand Down Expand Up @@ -44,6 +51,21 @@ def models_root(tier):


PROMPT = ("混合专家模型(MoE)通过稀疏激活把多个专家网络与门控路由组合起来:" * 12)

# Long-context prompt for the submission block (~3k tokens after tokenization):
# identical construction and text to the Python bench's BENCH_LONG (see
# python/examples/bench.py), so prefill numbers on both runtimes refer to the
# same measured work. Short prompts only measure fixed overhead.
_PARA_ZH = '\u6d41\u5f0f\u63a8\u7406\u6846\u67b6\u7684\u8bbe\u8ba1\u8981\u70b9\u5728\u4e8e\u628a\u78c1\u76d8\u5e26\u5bbd\u3001\u7f13\u5b58\u5c42\u7ea7\u4e0e\u8ba1\u7b97\u6838\u5fc3\u4e09\u8005\u91cd\u53e0\u8d77\u6765\u3002\u6df7\u5408\u4e13\u5bb6\u6a21\u578b\u6bcf\u4e00\u5c42\u53ea\u5728\u5c11\u6570\u4e13\u5bb6\u4e0a\u6fc0\u6d3b\uff0c\u8def\u7531\u5668\u7ed9\u51fa\u7684\u7a00\u758f\u9009\u62e9\u6070\u597d\u4e3a\u6309\u9700\u52a0\u8f7d\u63d0\u4f9b\u4e86\u5929\u7136\u7684\u6279\u7c92\u5ea6\u3002\u82e5\u80fd\u5728\u524d\u4e00\u6b65\u7684\u9690\u72b6\u6001\u57fa\u7840\u4e0a\u9884\u5224\u4e0b\u4e00\u6b65\u7684\u4e13\u5bb6\u96c6\u5408\uff0c\u56fa\u6001\u786c\u76d8\u7684\u8bfb\u53d6\u5ef6\u8fdf\u5c31\u80fd\u88ab\u8ba1\u7b97\u5b8c\u5168\u63a9\u76d6\uff0c\u8fd9\u5bf9\u8fb9\u7f18\u8bbe\u5907\u4e0a\u7684\u5927\u6a21\u578b\u90e8\u7f72\u5177\u6709\u76f4\u63a5\u610f\u4e49\u3002'
_PARA_EN = 'The design of a streaming inference framework hinges on overlapping disk bandwidth, cache hierarchy, and compute. A mixture-of-experts layer activates only a handful of experts per token, and the router sparsity provides a natural granularity for on-demand loading. If the expert set of the next step can be predicted from the previous hidden state, SSD read latency hides behind compute entirely, which matters for large-model deployment on edge hardware.'
_LONG_HEADER = '\u8bf7\u4ed4\u7ec6\u9605\u8bfb\u4ee5\u4e0b\u6750\u6599\u5e76\u7528\u4e00\u53e5\u8bdd\u603b\u7ed3\u5176\u6838\u5fc3\u601d\u60f3\uff1a'


def long_prompt():
paras = [_PARA_ZH if i % 2 == 0 else _PARA_EN for i in range(40)]
return _LONG_HEADER + " ".join(paras)


PAT = {
"prompt": re.compile(r"prompt eval time =.*?,\s*([\d.]+) tokens per second"),
"eval": re.compile(r"(?<!prompt )eval time =.*?,\s*([\d.]+) tokens per second"),
Expand All @@ -53,35 +75,198 @@ def models_root(tier):
}


def one(gguf, lora, n, tier):
# ---------------------------------------------------------------- metadata --
def cpu_name():
"""CPU marketing name from the registry (no wmic/sysinfo dependency)."""
try:
import winreg
k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
r"HARDWARE\DESCRIPTION\System\CentralProcessor\0")
name, _ = winreg.QueryValueEx(k, "ProcessorNameString")
winreg.CloseKey(k)
return " ".join(str(name).split())
except OSError:
return None


def total_ram_gb():
try:
class MEMORYSTATUSEX(ctypes.Structure):
_fields_ = [("dwLength", ctypes.c_uint),
("dwMemoryLoad", ctypes.c_uint),
("ullTotalPhys", ctypes.c_uint64),
("ullAvailPhys", ctypes.c_uint64),
("ullTotalPageFile", ctypes.c_uint64),
("ullAvailPageFile", ctypes.c_uint64),
("ullTotalVirtual", ctypes.c_uint64),
("ullAvailVirtual", ctypes.c_uint64),
("ullAvailExtendedVirtual", ctypes.c_uint64)]
st = MEMORYSTATUSEX()
st.dwLength = ctypes.sizeof(st)
if not ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(st)):
return None
return st.ullTotalPhys / (1024 ** 3)
except (OSError, AttributeError):
return None


def windows_build():
v = sys.getwindowsversion()
label = "11" if v.build >= 22000 else "10"
return f"Windows {label} (build {v.build})"


def machine_name():
"""Computer name (device) from the registry; a human brand is not exposed by winreg."""
try:
import winreg
k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
r"SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName")
name, _ = winreg.QueryValueEx(k, "ComputerName")
winreg.CloseKey(k)
return str(name)
except OSError:
return None


def engine_commit():
"""Head of vendor.llama.pin at the repo root (the pinned llama.cpp fork)."""
pin = os.path.join(ROOT, "..", "vendor.llama.pin")
try:
with open(pin, encoding="utf-8") as f:
return f.read().splitlines()[0].strip()
except OSError:
return None


def vulkan_device(err):
"""Vulkan device name from the engine's own startup log, e.g:
'ggml_vulkan: 0 = AMD Radeon RX 9070 GRE (vendor) | uma: ...'."""
for line in err.splitlines():
if not re.search(r"vulkan", line, re.I):
continue
m = re.search(r"\d+\s*=\s*([^\n|()]+)", line)
if m:
return m.group(1).strip()
m = re.search(r"Device\s*\d*\s*[:=]\s*([^\n|(),]+)", line, re.I)
if m:
return m.group(1).strip()
return None
Comment on lines +142 to +154

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not match the engine log. llama.cpp prints ggml_vulkan: 0 = <device name> (vendor) | .... On that text vulkan_device() returns nothing, so the block prints (not detected). Please match that line.



def open_handle(pid):
"""Engine-process handle for the memory probe (Windows only; None otherwise)."""
if sys.platform != "win32":
return None
for _ in range(10): # the child may still be spawning
h = ctypes.windll.kernel32.OpenProcess(0x0410, False, pid) # QUERY_INFORMATION|VM_READ
if h:
return h
time.sleep(0.01)
return None


def peak_working_set_gib(handle):
"""Peak process working set (GiB) — the OS keeps the high-water mark while
any handle to the process is open, so probe after communicate(), then close."""
if not handle:
return None
try:
class PMC(ctypes.Structure):
_fields_ = [("cb", ctypes.c_uint),
("PageFaultCount", ctypes.c_uint),
("PeakWorkingSetSize", ctypes.c_size_t),
("WorkingSetSize", ctypes.c_size_t),
("QuotaPeakPagedPoolUsage", ctypes.c_size_t),
("QuotaPagedPoolUsage", ctypes.c_size_t),
("QuotaPeakNonPagedPoolUsage", ctypes.c_size_t),
("QuotaNonPagedPoolUsage", ctypes.c_size_t),
("PagefileUsage", ctypes.c_size_t),
("PeakPagefileUsage", ctypes.c_size_t)]
pmc = PMC()
pmc.cb = ctypes.sizeof(pmc)
if not ctypes.WinDLL("psapi").GetProcessMemoryInfo(handle, ctypes.byref(pmc), pmc.cb):
return None
return pmc.PeakWorkingSetSize / (1024 ** 3)
except OSError:
return None


def one(gguf, lora, n, tier, prompt=None):
cmd = [os.path.join(SERVER_BIN, "llama-completion.exe"), "-m", gguf,
"-ngl", "99", "-cmoe", "--temp", "0", "-n", str(n), "--seed", "7",
"-no-cnv", "--no-display-prompt", "-p", PROMPT]
"-no-cnv", "--no-display-prompt", "-p", prompt or PROMPT]
if lora:
cmd[3:3] = ["--lora", lora]
t0 = time.time()
r = subprocess.run(cmd, capture_output=True, text=True, encoding="utf8", errors="replace")
err = r.stderr or ""
out = {"wall_s": round(time.time() - t0, 1)}
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, encoding="utf8", errors="replace")
h = open_handle(p.pid)
_, err = p.communicate()
peak = peak_working_set_gib(h) # probed after wait while the handle is live
if h:
ctypes.windll.kernel32.CloseHandle(h)
out = {"wall_s": round(time.time() - t0, 1),
"peak_working_set_gib": round(peak, 2) if peak is not None else None,
"gpu": vulkan_device(err or "")}
for k, pat in PAT.items():
m = pat.search(err)
m = pat.search(err or "")
if m:
out[k] = float(m.group(1))
ev = re.search(r"n_ctx = (\d+)", err)
ev = re.search(r"n_ctx = (\d+)", err or "")
out["n_ctx"] = int(ev.group(1)) if ev else None
if "error" in err.lower() and "eval time" not in err:
if "error" in (err or "").lower() and "eval time" not in err:
out["error"] = [l for l in err.splitlines() if "error" in l.lower()][:2]
if "total_ms" in out and "prompt_ms" in out:
dec_ms = out["total_ms"] - out["prompt_ms"] # total = generation end-to-end, excludes load (verified)
out["decode_tps_derived"] = round(n * 1000 / dec_ms, 2) if dec_ms > 0 else None
return out


def submission_block(tier, reporter, notes, rows):
"""Ready-to-paste Hardware Benchmarks block; numbers are the mean of the timed reps."""
evs = [r["eval"] for r in rows if "eval" in r]
prs = [r["prompt"] for r in rows if "prompt" in r]
peaks = [r["peak_working_set_gib"] for r in rows if r.get("peak_working_set_gib") is not None]
gpus = sorted({r["gpu"] for r in rows if r.get("gpu")})
dev, chip = machine_name(), cpu_name()
ram = total_ram_gb()
data = {
"tier": f"edge0-{tier}",
"runtime": "windows",
"device": dev or "(unknown)",
"chip": chip or "(unknown)",
"gpu": gpus[0] if gpus else "(not detected)",
"ram_gb": round(ram) if ram else None,
"os": windows_build(),
"decode_tok_s": round(sum(evs) / len(evs), 2) if evs else None,
"prefill_tok_s": round(sum(prs) / len(prs), 2) if prs else None,
"peak_active_gib": round(max(peaks), 2) if peaks else None,
"engine_commit": engine_commit(),
"reporter": reporter or "",
"notes": notes or "",
}
return data


def print_block(d):
for k, v in d.items():
print(f"{k:<16} {'' if v is None else v}".rstrip())


def main():
ap = argparse.ArgumentParser()
ap.add_argument("--tier", choices=["35b", "8b"], required=True)
ap.add_argument("--reps", type=int, default=3)
ap.add_argument("--reps", type=int, default=None,
help="timed reps (default: 2 with --submission, 3 otherwise)")
ap.add_argument("--submission", action="store_true",
help="print the Hardware Benchmarks submission block (long-context prefill)")
ap.add_argument("--json", action="store_true",
help="with --submission, also print the block as JSON")
ap.add_argument("--reporter", default="", help="@handle for the submission block")
ap.add_argument("--notes", default="", help="free-form notes field")
a = ap.parse_args()
reps = a.reps or (2 if a.submission else 3)
exe = os.path.join(SERVER_BIN, "llama-completion.exe")
if not os.path.isfile(exe):
raise SystemExit(f"llama-completion.exe not found in {SERVER_BIN} "
Expand All @@ -90,10 +275,30 @@ def main():
gguf = os.path.join(md, f"edge0-{a.tier}-gguf", f"edge0-{a.tier}.gguf")
lora = os.path.join(md, f"edge0-{a.tier}",
f"lora_edge0_{a.tier}-gguf.gguf")
if a.submission:
warm = one(gguf, lora, 64, a.tier) # warmup (page cache + mmap fault-in), discarded
print(f"warm(discard): {warm}", flush=True)
longp = long_prompt()
rows = []
for i in range(reps):
time.sleep(5)
m = one(gguf, lora, 256, a.tier, prompt=longp)
m["rep"] = i
rows.append(m)
print(f"rep{i}: {m}", flush=True)
data = submission_block(a.tier, a.reporter, a.notes, rows)
outp = os.path.join(ROOT, "benchmarks", "r3", f"bench_{a.tier}_submission.json")
os.makedirs(os.path.dirname(outp), exist_ok=True)
with open(outp, "w", encoding="utf8") as f:
json.dump({**data, "timed_reps": rows}, f, ensure_ascii=False, indent=1)
print_block(data)
if a.json:
print("SUBMIT_JSON " + json.dumps(data, ensure_ascii=False))
return
rows = []
warm = one(gguf, lora, 64, a.tier) # warmup (page cache + mmap fault-in), discarded
print(f"warm(discard): {warm}", flush=True)
for i in range(a.reps):
for i in range(reps):
time.sleep(5)
m = one(gguf, lora, 256, a.tier)
m["rep"] = i
Expand Down
Loading