Download strict_bench.py from HelloSun/openPangu-2.0-Flash-base-Q3_K_M_EPQ: direct link, hf CLI and curl.
- Browser
- Download file 12.9 kB
-
https://huggingface.co/HelloSun/openPangu-2.0-Flash-base-Q3_K_M_EPQ/resolve/main/strict_bench.py
- Command line
-
hf download hf://HelloSun/openPangu-2.0-Flash-base-Q3_K_M_EPQ/strict_bench.py
-
curl -L -o strict_bench.py https://huggingface.co/HelloSun/openPangu-2.0-Flash-base-Q3_K_M_EPQ/resolve/main/strict_bench.py
12.9 kB
| #!/usr/bin/env python3 | |
| """Strict-limit RAM sweep for EPQ on LFM2.5-24B-A2B Q4_K_M. | |
| Unlike the original memory_levels_ling.py (which only sets the pager's own | |
| budget), every run here is launched under a *hard* OS limit: | |
| prlimit --data=<level> --nofile=65536 | |
| On Linux >= 4.7 RLIMIT_DATA covers brk() AND mmap'd anonymous memory, so it is | |
| a real ceiling on the arena + heap + every later anonymous allocation. If the | |
| process exceeds it, malloc fails and the run aborts -- we record that as a hard | |
| failure instead of a silent overshoot. | |
| Two caveats this harness records explicitly: | |
| * RLIMIT_DATA polices *anonymous* memory only. The ~1.3-2.1 GiB of | |
| non-expert weights are mmap'd file-backed pages and are therefore NOT | |
| policed here, so peak RSS can exceed the level. Both numbers are reported. | |
| * `-march=native` on this host defines __AMX_INT8__, which makes llama.cpp | |
| allocate a ~1.88 GiB AMX repack buffer before the arena-sized context. That | |
| alone pushes the floor past 4 GiB, so the sweep runs against the | |
| GGML_NATIVE=OFF (no-AMX) build unless --amx is passed. | |
| Swap is proven absent at run time by sampling /proc/<pid>/status VmSwap; a | |
| nonzero value fails the run. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import platform | |
| import re | |
| import statistics | |
| import subprocess | |
| import sys | |
| import threading | |
| import time | |
| from pathlib import Path | |
| MIB = 1024 * 1024 | |
| GIB = 1024 ** 3 | |
| PROMPT = ("Write a detailed paragraph about how a mixture-of-experts model routes " | |
| "tokens to expert weights, and why paging those weights from SSD matters.") | |
| def args_config(args): | |
| """JSON-safe view of the argparse namespace (drops the resolve helper).""" | |
| return {k: v for k, v in vars(args).items() if not callable(v)} | |
| def host_facts() -> dict: | |
| cpu = "" | |
| for line in Path('/proc/cpuinfo').read_text().splitlines(): | |
| if line.startswith('model name'): | |
| cpu = line.split(':', 1)[1].strip() | |
| break | |
| mem_total = 0 | |
| for line in Path('/proc/meminfo').read_text().splitlines(): | |
| if line.startswith('MemTotal'): | |
| mem_total = int(line.split()[1]) * 1024 | |
| break | |
| cg = {} | |
| for k, f in (('memory.max', 'memory.max'), | |
| ('memory.swap.max', 'memory.swap.max'), | |
| ('memory.high', 'memory.high')): | |
| try: | |
| cg[k] = Path('/sys/fs/cgroup/' + f).read_text().strip() | |
| except Exception: | |
| cg[k] = None | |
| return {'cpu_model': cpu, 'cpu_threads': os.cpu_count(), | |
| 'mem_total_bytes': mem_total, 'cgroup': cg} | |
| class ProcSampler(threading.Thread): | |
| """Sample RSS / AnonHWM / VmSwap until told to stop. | |
| AnonHWM is smaps_rollup's own anonymous high-water mark and is the quantity | |
| RLIMIT_DATA actually polices, so it is what we compare against the level. | |
| """ | |
| def __init__(self, pid: int, interval: float = 0.03): | |
| super().__init__(daemon=True) | |
| self.pid = pid | |
| self.interval = interval | |
| self.stop_flag = threading.Event() | |
| self.peak_rss = 0 | |
| self.peak_anon = 0 | |
| self.peak_swap = 0 | |
| self.n = 0 | |
| def _read(self) -> tuple[int, int, int]: | |
| rss = anon = swp = 0 | |
| try: | |
| for line in Path(f'/proc/{self.pid}/status').read_text().splitlines(): | |
| if line.startswith('VmRSS:'): | |
| rss = int(line.split()[1]) * 1024 | |
| elif line.startswith('VmSwap:'): | |
| swp = int(line.split()[1]) * 1024 | |
| except OSError: | |
| pass | |
| try: | |
| for line in Path(f'/proc/{self.pid}/smaps_rollup').read_text().splitlines(): | |
| if line.startswith('Rss:'): | |
| rss = max(rss, int(line.split()[1]) * 1024) | |
| elif line.startswith('Anonymous:'): | |
| anon = int(line.split()[1]) * 1024 | |
| elif line.startswith('AnonHWM:'): | |
| anon = max(anon, int(line.split()[1]) * 1024) | |
| except OSError: | |
| pass | |
| return rss, anon, swp | |
| def run(self): | |
| while not self.stop_flag.is_set(): | |
| r, a, s = self._read() | |
| self.n += 1 | |
| self.peak_rss = max(self.peak_rss, r) | |
| self.peak_anon = max(self.peak_anon, a) | |
| self.peak_swap = max(self.peak_swap, s) | |
| time.sleep(self.interval) | |
| r, a, s = self._read() | |
| self.peak_rss = max(self.peak_rss, r) | |
| self.peak_anon = max(self.peak_anon, a) | |
| self.peak_swap = max(self.peak_swap, s) | |
| def evict(shards: list[str], helper: str) -> None: | |
| subprocess.run(['sync'], check=False) | |
| if helper: | |
| subprocess.run([helper, '--evict'] + shards, check=False) | |
| def run_once(args, level_mb: int, repeat: int) -> dict: | |
| stats_file = Path(args.output_dir) / f'level_{level_mb}_r{repeat}.jsonl' | |
| log_file = Path(args.output_dir) / f'level_{level_mb}_r{repeat}.log' | |
| stats_file.unlink(missing_ok=True) | |
| os.makedirs(args.output_dir, exist_ok=True) | |
| inner = [ | |
| str(Path(args.bin).resolve()), | |
| '-m', str(Path(args.model).resolve()), | |
| '-p', PROMPT, '-n', str(args.n_predict), '-c', str(args.ctx), | |
| '-t', str(args.threads), '-b', str(args.ubatch), '--temp', '0', | |
| '--no-interactive', '--no-chat', '--stats-file', str(stats_file), | |
| '--ram-budget-mb', str(level_mb), | |
| ] | |
| # The strict ceiling: RLIMIT_DATA = arena + heap + all anon mmap. | |
| cmd = ['prlimit', f'--data={level_mb * MIB}', '--nofile=65536'] + inner | |
| env = dict(os.environ) | |
| env.update({ | |
| 'SDQ_RAM_BUDGET_MB': str(level_mb), | |
| 'SDQ_RESERVE_MB': str(args.reserve_mb_for(level_mb)), | |
| 'SDQ_PREFETCH': '0', | |
| 'SDQ_FADV_DONTNEED': '1', | |
| 'SDQ_BENCH_NO_EOG': '1', | |
| }) | |
| evict(args.shards, args.evict_helper) | |
| t0 = time.perf_counter() | |
| proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, | |
| text=True, encoding='utf-8', errors='replace', env=env) | |
| sampler = ProcSampler(proc.pid) | |
| sampler.start() | |
| try: | |
| out, err = proc.communicate(timeout=args.timeout) | |
| except subprocess.TimeoutExpired: | |
| proc.kill() | |
| out, err = proc.communicate() | |
| sampler.stop_flag.set() | |
| return {'level_mb': level_mb, 'repeat': repeat + 1, | |
| 'error': f'timeout after {args.timeout}s'} | |
| sampler.stop_flag.set() | |
| sampler.join(timeout=2) | |
| wall = time.perf_counter() - t0 | |
| both = out + '\n' + err | |
| log_file.write_text(both, encoding='utf-8', errors='replace') | |
| res = { | |
| 'level_mb': level_mb, | |
| 'level_gib': level_mb / 1024, | |
| 'repeat': repeat + 1, | |
| 'wall_s': round(wall, 2), | |
| 'exit_code': proc.returncode, | |
| 'peak_rss_mb': round(sampler.peak_rss / MIB, 1), | |
| 'peak_anon_mb': round(sampler.peak_anon / MIB, 1), | |
| 'peak_swap_mb': round(sampler.peak_swap / MIB, 1), | |
| 'swap_free': sampler.peak_swap == 0, | |
| 'anon_within_limit': sampler.peak_anon <= level_mb * MIB, | |
| 'limit_enforced_by': 'prlimit --data (RLIMIT_DATA)', | |
| } | |
| if proc.returncode != 0: | |
| sig = -proc.returncode if proc.returncode < 0 else None | |
| res['signal'] = sig | |
| res['status'] = 'OOM_OR_ABORT' | |
| res['failure'] = ' | '.join( | |
| [l.strip() for l in err.splitlines() | |
| if re.search(r'bad_alloc|cannot allocate|MemoryError|std::terminate|' | |
| r'abort|Killed|Expert arena|too small|error', l, re.I)][-5:]) | |
| res['wall_s'] = round(wall, 2) | |
| return res | |
| res['status'] = 'OK' | |
| def grab(pat, text=both): | |
| m = re.search(pat, text, re.M) | |
| return m | |
| d = grab(r'decode:\s*(\d+)\s+tok\s*/\s*([\d.]+)s\s*=\s*([\d.]+)\s+tok/s') | |
| p = grab(r'prefill:\s*(\d+)\s+tok\s*/\s*([\d.]+)s\s*=\s*([\d.]+)\s+tok/s') | |
| mem = grab(r'總 RSS\s+(\d+)\s+MB\s*/\s+峰值\s+(\d+)\s+MB\s*/\s+swap\s+(\d+)\s+MB') | |
| split = grab(r'匿名 RSS\s+(\d+)\s+MB\s*/\s+檔案對映 RSS\s+(\d+)\s+MB\s*/\s+總 RSS\s+(\d+)\s+MB\s*/\s+峰值\s+(\d+)\s+MB') | |
| arena = grab(r'slots=\d+\s+arena=([\d.]+)\s+GiB') | |
| slots = grab(r'slots=(\d+)') | |
| ssd_tot = grab(r'全程 SSD 實際讀取:\s*([\d.]+)\s*GB') | |
| mib = grab(r'SSD 讀\s*([\d.]+)\s*MB\s*\(([\d.]+)\s*MB/tok\)') | |
| if d: | |
| res['decode_tok_s'] = float(d.group(3)) | |
| res['decode_tok'] = int(d.group(1)) | |
| if p: | |
| res['prefill_tok_s'] = float(p.group(3)) | |
| res['prefill_tok'] = int(p.group(1)) | |
| if mem: | |
| res['final_rss_mb'] = int(mem.group(1)) | |
| res['selfpeak_rss_mb'] = int(mem.group(2)) | |
| res['self_swap_mb'] = int(mem.group(3)) | |
| if split: | |
| res['rss_anon_mb'] = int(split.group(1)) | |
| res['rss_file_mb'] = int(split.group(2)) | |
| res['rss_total_mb'] = int(split.group(3)) | |
| res['selfpeak_rss_mb'] = int(split.group(4)) | |
| if arena: | |
| res['arena_gib'] = float(arena.group(1)) | |
| if slots: | |
| res['n_slots'] = int(slots.group(1)) | |
| if ssd_tot: | |
| res['total_ssd_gb'] = float(ssd_tot.group(1)) | |
| if mib: | |
| res['decode_ssd_mb'] = float(mib.group(1)) | |
| res['ssd_mb_per_token'] = float(mib.group(2)) | |
| rec = {} | |
| if stats_file.exists(): | |
| for line in stats_file.read_text().splitlines(): | |
| r = json.loads(line) | |
| if r.get('phase') == 'decode': | |
| rec = r | |
| stats_file.unlink(missing_ok=True) | |
| pl = rec.get('per_layer') or [] | |
| rates = [x['hit_rate'] for x in pl if x.get('req', 0) > 0] | |
| hr = rec.get('hit_rate') | |
| res.update({ | |
| 'expert_requests': rec.get('expert_requests'), | |
| 'expert_hits': rec.get('expert_hits'), | |
| 'hot_rate_pct': hr, | |
| 'cold_rate_pct': (100.0 - hr) if hr is not None else None, | |
| 'evictions': rec.get('evictions'), | |
| 'per_layer_hit_min': min(rates) if rates else None, | |
| 'per_layer_hit_max': max(rates) if rates else None, | |
| }) | |
| asst = '' | |
| started = False | |
| for line in out.splitlines(): | |
| if line.startswith('assistant:'): | |
| started = True | |
| rest = line[len('assistant:'):].strip() | |
| if rest: | |
| asst = rest | |
| continue | |
| if started and line.strip() and not line.startswith('['): | |
| asst += ' ' + line.strip() | |
| res['assistant'] = asst.strip()[:200] | |
| return res | |
| def main() -> None: | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument('--model', required=True) | |
| ap.add_argument('--shards', nargs='+', required=True) | |
| ap.add_argument('--bin', required=True) | |
| ap.add_argument('--levels-mb', type=int, nargs='+', default=[4096, 8192, 16384, 32768]) | |
| ap.add_argument('--repeat', type=int, default=2) | |
| ap.add_argument('--ctx', type=int, default=2048) | |
| ap.add_argument('--ubatch', type=int, default=128) | |
| ap.add_argument('--threads', type=int, default=16) | |
| ap.add_argument('--n-predict', type=int, default=48) | |
| ap.add_argument('--reserve-mb', type=int, default=2400) | |
| ap.add_argument('--timeout', type=int, default=1800) | |
| ap.add_argument('--evict-helper', default='/tmp/pagecache_resident') | |
| ap.add_argument( | |
| '--reserve-map', | |
| default='4096:2400,8192:2400,16384:2400,32768:2400', | |
| help='per-level SDQ_RESERVE_MB, "level_mb:reserve_mb,...". Calibrated ' | |
| 'by measuring anon_hwm - arena, i.e. every anonymous allocation ' | |
| 'that happens after the arena is sized.') | |
| ap.add_argument('--output-dir', default='/root/app/ling/results_strict') | |
| ap.add_argument('--out-json', default='/root/app/ling/results_strict/strict_memory_levels.json') | |
| args = ap.parse_args() | |
| args._reserve = {} | |
| for item in args.reserve_map.split(','): | |
| k, v = item.split(':') | |
| args._reserve[int(k)] = int(v) | |
| args.reserve_mb_for = lambda lvl: args._reserve.get(lvl, 2400) | |
| Path(args.output_dir).mkdir(parents=True, exist_ok=True) | |
| print(f'host: {host_facts()}', file=sys.stderr) | |
| runs = [] | |
| for lvl in args.levels_mb: | |
| for r in range(args.repeat): | |
| print(f'--- level {lvl} MB repeat {r + 1}/{args.repeat} ---', | |
| file=sys.stderr, flush=True) | |
| try: | |
| res = run_once(args, lvl, r) | |
| except Exception as e: | |
| res = {'level_mb': lvl, 'repeat': r + 1, 'error': repr(e)} | |
| runs.append(res) | |
| print(json.dumps(res, ensure_ascii=False)[:400], file=sys.stderr, flush=True) | |
| Path(args.out_json).write_text(json.dumps( | |
| {'host': host_facts(), 'config': args_config(args), 'runs': runs}, | |
| indent=2, ensure_ascii=False)) | |
| print('DONE', file=sys.stderr) | |
| Path(args.out_json).write_text(json.dumps( | |
| {'host': host_facts(), 'config': args_config(args), 'runs': runs}, | |
| indent=2, ensure_ascii=False)) | |
| if __name__ == '__main__': | |
| main() | |