88window when compared against the reference — cross-window comparison is always
99suspect (measure on the same day / same process, or treat numbers as shape-only).
1010
11+ Community submission mode (Hardware Benchmarks discussion #142, issue #139):
12+ `--submission` discards the same short warmup, then runs `reps` timed reps
13+ (default 2) on the ~3k-token long-context prompt, averages the numbers, and
14+ prints the ready-to-paste submission block; `--json` prints the same data as
15+ JSON for aggregation.
16+
1117Usage: python tools/r3_bench.py --tier 35b [--reps 3]
18+ python tools/r3_bench.py --tier 8b --submission --reporter @you
1219"""
13- import argparse , json , os , re , subprocess , sys , time
20+ import argparse , ctypes , json , os , re , subprocess , sys , time
1421
1522ROOT = os .path .dirname (os .path .dirname (os .path .abspath (__file__ )))
1623
@@ -44,6 +51,21 @@ def models_root(tier):
4451
4552
4653PROMPT = ("混合专家模型(MoE)通过稀疏激活把多个专家网络与门控路由组合起来:" * 12 )
54+
55+ # Long-context prompt for the submission block (~3k tokens after tokenization):
56+ # identical construction and text to the Python bench's BENCH_LONG (see
57+ # python/examples/bench.py), so prefill numbers on both runtimes refer to the
58+ # same measured work. Short prompts only measure fixed overhead.
59+ _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 '
60+ _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.'
61+ _LONG_HEADER = '\u8bf7 \u4ed4 \u7ec6 \u9605 \u8bfb \u4ee5 \u4e0b \u6750 \u6599 \u5e76 \u7528 \u4e00 \u53e5 \u8bdd \u603b \u7ed3 \u5176 \u6838 \u5fc3 \u601d \u60f3 \uff1a '
62+
63+
64+ def long_prompt ():
65+ paras = [_PARA_ZH if i % 2 == 0 else _PARA_EN for i in range (40 )]
66+ return _LONG_HEADER + " " .join (paras )
67+
68+
4769PAT = {
4870 "prompt" : re .compile (r"prompt eval time =.*?,\s*([\d.]+) tokens per second" ),
4971 "eval" : re .compile (r"(?<!prompt )eval time =.*?,\s*([\d.]+) tokens per second" ),
@@ -53,35 +75,198 @@ def models_root(tier):
5375}
5476
5577
56- def one (gguf , lora , n , tier ):
78+ # ---------------------------------------------------------------- metadata --
79+ def cpu_name ():
80+ """CPU marketing name from the registry (no wmic/sysinfo dependency)."""
81+ try :
82+ import winreg
83+ k = winreg .OpenKey (winreg .HKEY_LOCAL_MACHINE ,
84+ r"HARDWARE\DESCRIPTION\System\CentralProcessor\0" )
85+ name , _ = winreg .QueryValueEx (k , "ProcessorNameString" )
86+ winreg .CloseKey (k )
87+ return " " .join (str (name ).split ())
88+ except OSError :
89+ return None
90+
91+
92+ def total_ram_gb ():
93+ try :
94+ class MEMORYSTATUSEX (ctypes .Structure ):
95+ _fields_ = [("dwLength" , ctypes .c_uint ),
96+ ("dwMemoryLoad" , ctypes .c_uint ),
97+ ("ullTotalPhys" , ctypes .c_uint64 ),
98+ ("ullAvailPhys" , ctypes .c_uint64 ),
99+ ("ullTotalPageFile" , ctypes .c_uint64 ),
100+ ("ullAvailPageFile" , ctypes .c_uint64 ),
101+ ("ullTotalVirtual" , ctypes .c_uint64 ),
102+ ("ullAvailVirtual" , ctypes .c_uint64 ),
103+ ("ullAvailExtendedVirtual" , ctypes .c_uint64 )]
104+ st = MEMORYSTATUSEX ()
105+ st .dwLength = ctypes .sizeof (st )
106+ if not ctypes .windll .kernel32 .GlobalMemoryStatusEx (ctypes .byref (st )):
107+ return None
108+ return st .ullTotalPhys / (1024 ** 3 )
109+ except (OSError , AttributeError ):
110+ return None
111+
112+
113+ def windows_build ():
114+ v = sys .getwindowsversion ()
115+ label = "11" if v .build >= 22000 else "10"
116+ return f"Windows { label } (build { v .build } )"
117+
118+
119+ def machine_name ():
120+ """Computer name (device) from the registry; a human brand is not exposed by winreg."""
121+ try :
122+ import winreg
123+ k = winreg .OpenKey (winreg .HKEY_LOCAL_MACHINE ,
124+ r"SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName" )
125+ name , _ = winreg .QueryValueEx (k , "ComputerName" )
126+ winreg .CloseKey (k )
127+ return str (name )
128+ except OSError :
129+ return None
130+
131+
132+ def engine_commit ():
133+ """Head of vendor.llama.pin at the repo root (the pinned llama.cpp fork)."""
134+ pin = os .path .join (ROOT , ".." , "vendor.llama.pin" )
135+ try :
136+ with open (pin , encoding = "utf-8" ) as f :
137+ return f .read ().splitlines ()[0 ].strip ()
138+ except OSError :
139+ return None
140+
141+
142+ def vulkan_device (err ):
143+ """Vulkan device name from the engine's own startup log, e.g:
144+ 'ggml_vulkan: 0 = AMD Radeon RX 9070 GRE (vendor) | uma: ...'."""
145+ for line in err .splitlines ():
146+ if not re .search (r"vulkan" , line , re .I ):
147+ continue
148+ m = re .search (r"\d+\s*=\s*([^\n|()]+)" , line )
149+ if m :
150+ return m .group (1 ).strip ()
151+ m = re .search (r"Device\s*\d*\s*[:=]\s*([^\n|(),]+)" , line , re .I )
152+ if m :
153+ return m .group (1 ).strip ()
154+ return None
155+
156+
157+ def open_handle (pid ):
158+ """Engine-process handle for the memory probe (Windows only; None otherwise)."""
159+ if sys .platform != "win32" :
160+ return None
161+ for _ in range (10 ): # the child may still be spawning
162+ h = ctypes .windll .kernel32 .OpenProcess (0x0410 , False , pid ) # QUERY_INFORMATION|VM_READ
163+ if h :
164+ return h
165+ time .sleep (0.01 )
166+ return None
167+
168+
169+ def peak_working_set_gib (handle ):
170+ """Peak process working set (GiB) — the OS keeps the high-water mark while
171+ any handle to the process is open, so probe after communicate(), then close."""
172+ if not handle :
173+ return None
174+ try :
175+ class PMC (ctypes .Structure ):
176+ _fields_ = [("cb" , ctypes .c_uint ),
177+ ("PageFaultCount" , ctypes .c_uint ),
178+ ("PeakWorkingSetSize" , ctypes .c_size_t ),
179+ ("WorkingSetSize" , ctypes .c_size_t ),
180+ ("QuotaPeakPagedPoolUsage" , ctypes .c_size_t ),
181+ ("QuotaPagedPoolUsage" , ctypes .c_size_t ),
182+ ("QuotaPeakNonPagedPoolUsage" , ctypes .c_size_t ),
183+ ("QuotaNonPagedPoolUsage" , ctypes .c_size_t ),
184+ ("PagefileUsage" , ctypes .c_size_t ),
185+ ("PeakPagefileUsage" , ctypes .c_size_t )]
186+ pmc = PMC ()
187+ pmc .cb = ctypes .sizeof (pmc )
188+ if not ctypes .WinDLL ("psapi" ).GetProcessMemoryInfo (handle , ctypes .byref (pmc ), pmc .cb ):
189+ return None
190+ return pmc .PeakWorkingSetSize / (1024 ** 3 )
191+ except OSError :
192+ return None
193+
194+
195+ def one (gguf , lora , n , tier , prompt = None ):
57196 cmd = [os .path .join (SERVER_BIN , "llama-completion.exe" ), "-m" , gguf ,
58197 "-ngl" , "99" , "-cmoe" , "--temp" , "0" , "-n" , str (n ), "--seed" , "7" ,
59- "-no-cnv" , "--no-display-prompt" , "-p" , PROMPT ]
198+ "-no-cnv" , "--no-display-prompt" , "-p" , prompt or PROMPT ]
60199 if lora :
61200 cmd [3 :3 ] = ["--lora" , lora ]
62201 t0 = time .time ()
63- r = subprocess .run (cmd , capture_output = True , text = True , encoding = "utf8" , errors = "replace" )
64- err = r .stderr or ""
65- out = {"wall_s" : round (time .time () - t0 , 1 )}
202+ p = subprocess .Popen (cmd , stdout = subprocess .PIPE , stderr = subprocess .PIPE ,
203+ text = True , encoding = "utf8" , errors = "replace" )
204+ h = open_handle (p .pid )
205+ _ , err = p .communicate ()
206+ peak = peak_working_set_gib (h ) # probed after wait while the handle is live
207+ if h :
208+ ctypes .windll .kernel32 .CloseHandle (h )
209+ out = {"wall_s" : round (time .time () - t0 , 1 ),
210+ "peak_working_set_gib" : round (peak , 2 ) if peak is not None else None ,
211+ "gpu" : vulkan_device (err or "" )}
66212 for k , pat in PAT .items ():
67- m = pat .search (err )
213+ m = pat .search (err or "" )
68214 if m :
69215 out [k ] = float (m .group (1 ))
70- ev = re .search (r"n_ctx = (\d+)" , err )
216+ ev = re .search (r"n_ctx = (\d+)" , err or "" )
71217 out ["n_ctx" ] = int (ev .group (1 )) if ev else None
72- if "error" in err .lower () and "eval time" not in err :
218+ if "error" in ( err or "" ) .lower () and "eval time" not in err :
73219 out ["error" ] = [l for l in err .splitlines () if "error" in l .lower ()][:2 ]
74220 if "total_ms" in out and "prompt_ms" in out :
75221 dec_ms = out ["total_ms" ] - out ["prompt_ms" ] # total = generation end-to-end, excludes load (verified)
76222 out ["decode_tps_derived" ] = round (n * 1000 / dec_ms , 2 ) if dec_ms > 0 else None
77223 return out
78224
79225
226+ def submission_block (tier , reporter , notes , rows ):
227+ """Ready-to-paste Hardware Benchmarks block; numbers are the mean of the timed reps."""
228+ evs = [r ["eval" ] for r in rows if "eval" in r ]
229+ prs = [r ["prompt" ] for r in rows if "prompt" in r ]
230+ peaks = [r ["peak_working_set_gib" ] for r in rows if r .get ("peak_working_set_gib" ) is not None ]
231+ gpus = sorted ({r ["gpu" ] for r in rows if r .get ("gpu" )})
232+ dev , chip = machine_name (), cpu_name ()
233+ ram = total_ram_gb ()
234+ data = {
235+ "tier" : f"edge0-{ tier } " ,
236+ "runtime" : "windows" ,
237+ "device" : dev or "(unknown)" ,
238+ "chip" : chip or "(unknown)" ,
239+ "gpu" : gpus [0 ] if gpus else "(not detected)" ,
240+ "ram_gb" : round (ram ) if ram else None ,
241+ "os" : windows_build (),
242+ "decode_tok_s" : round (sum (evs ) / len (evs ), 2 ) if evs else None ,
243+ "prefill_tok_s" : round (sum (prs ) / len (prs ), 2 ) if prs else None ,
244+ "peak_active_gib" : round (max (peaks ), 2 ) if peaks else None ,
245+ "engine_commit" : engine_commit (),
246+ "reporter" : reporter or "" ,
247+ "notes" : notes or "" ,
248+ }
249+ return data
250+
251+
252+ def print_block (d ):
253+ for k , v in d .items ():
254+ print (f"{ k :<16} { '' if v is None else v } " .rstrip ())
255+
256+
80257def main ():
81258 ap = argparse .ArgumentParser ()
82259 ap .add_argument ("--tier" , choices = ["35b" , "8b" ], required = True )
83- ap .add_argument ("--reps" , type = int , default = 3 )
260+ ap .add_argument ("--reps" , type = int , default = None ,
261+ help = "timed reps (default: 2 with --submission, 3 otherwise)" )
262+ ap .add_argument ("--submission" , action = "store_true" ,
263+ help = "print the Hardware Benchmarks submission block (long-context prefill)" )
264+ ap .add_argument ("--json" , action = "store_true" ,
265+ help = "with --submission, also print the block as JSON" )
266+ ap .add_argument ("--reporter" , default = "" , help = "@handle for the submission block" )
267+ ap .add_argument ("--notes" , default = "" , help = "free-form notes field" )
84268 a = ap .parse_args ()
269+ reps = a .reps or (2 if a .submission else 3 )
85270 exe = os .path .join (SERVER_BIN , "llama-completion.exe" )
86271 if not os .path .isfile (exe ):
87272 raise SystemExit (f"llama-completion.exe not found in { SERVER_BIN } "
@@ -90,10 +275,30 @@ def main():
90275 gguf = os .path .join (md , f"edge0-{ a .tier } -gguf" , f"edge0-{ a .tier } .gguf" )
91276 lora = os .path .join (md , f"edge0-{ a .tier } " ,
92277 f"lora_edge0_{ a .tier } -gguf.gguf" )
278+ if a .submission :
279+ warm = one (gguf , lora , 64 , a .tier ) # warmup (page cache + mmap fault-in), discarded
280+ print (f"warm(discard): { warm } " , flush = True )
281+ longp = long_prompt ()
282+ rows = []
283+ for i in range (reps ):
284+ time .sleep (5 )
285+ m = one (gguf , lora , 256 , a .tier , prompt = longp )
286+ m ["rep" ] = i
287+ rows .append (m )
288+ print (f"rep{ i } : { m } " , flush = True )
289+ data = submission_block (a .tier , a .reporter , a .notes , rows )
290+ outp = os .path .join (ROOT , "benchmarks" , "r3" , f"bench_{ a .tier } _submission.json" )
291+ os .makedirs (os .path .dirname (outp ), exist_ok = True )
292+ with open (outp , "w" , encoding = "utf8" ) as f :
293+ json .dump ({** data , "timed_reps" : rows }, f , ensure_ascii = False , indent = 1 )
294+ print_block (data )
295+ if a .json :
296+ print ("SUBMIT_JSON " + json .dumps (data , ensure_ascii = False ))
297+ return
93298 rows = []
94299 warm = one (gguf , lora , 64 , a .tier ) # warmup (page cache + mmap fault-in), discarded
95300 print (f"warm(discard): { warm } " , flush = True )
96- for i in range (a . reps ):
301+ for i in range (reps ):
97302 time .sleep (5 )
98303 m = one (gguf , lora , 256 , a .tier )
99304 m ["rep" ] = i
0 commit comments