Spaces:
Sleeping
Sleeping
| """ | |
| benchmark.py | |
| ============ | |
| Generic CPU inference benchmark for any BERT-family encoder model on Hugging Face. | |
| Measures: | |
| - Parameters, MACs, FLOPs (via DeepSpeed FLOPs profiler) | |
| - Mean and p95 latency (over N forward passes on CPU) | |
| Usage: | |
| python benchmark.py --model bert-base-uncased | |
| python benchmark.py --model katrjohn/TinyGreekNewsBERT \\ | |
| --tokenizer nlpaueb/bert-base-greek-uncased-v1 \\ | |
| --trust-remote-code | |
| python benchmark.py --model bert-base-uncased --runs 1000 --seq-len 128 | |
| Requirements: | |
| pip install torch transformers deepspeed numpy | |
| """ | |
| import argparse | |
| import contextlib | |
| import logging | |
| import os | |
| import time | |
| # ββ Silence noisy libraries before anything is imported ββββββββββββββββββββββ | |
| os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") | |
| os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "3") | |
| os.environ.setdefault("TF_ENABLE_ONEDNN_OPTS", "0") | |
| logging.basicConfig(level=logging.WARNING) | |
| for _noisy in ("deepspeed", "transformers", "torch"): | |
| logging.getLogger(_noisy).setLevel(logging.ERROR) | |
| def _suppress_c_stderr(): | |
| """Redirect fd 2 to /dev/null to silence C-level stderr (TF/absl/CUDA noise).""" | |
| devnull_fd = os.open(os.devnull, os.O_WRONLY) | |
| saved_fd = os.dup(2) | |
| os.dup2(devnull_fd, 2) | |
| try: | |
| yield | |
| finally: | |
| os.dup2(saved_fd, 2) | |
| os.close(saved_fd) | |
| os.close(devnull_fd) | |
| import numpy as np | |
| import torch | |
| with _suppress_c_stderr(): | |
| from deepspeed.profiling.flops_profiler import get_model_profile | |
| from transformers import AutoModel, AutoTokenizer | |
| # ββ Defaults ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SEQ_LEN = 512 | |
| WARM_UP = 20 | |
| RUNS = 10_000 | |
| SAMPLE_TEXT = "The government announced new support measures for workers today." | |
| # ββ Model loading βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_model(model_id: str, tokenizer_id: str, trust_remote_code: bool = False): | |
| tokenizer = AutoTokenizer.from_pretrained(tokenizer_id) | |
| model = AutoModel.from_pretrained(model_id, trust_remote_code=trust_remote_code) | |
| model = model.to("cpu").eval() | |
| return model, tokenizer | |
| # ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _safe_forward(model, encoded: dict): | |
| """Forward pass using only arguments accepted by model.forward().""" | |
| accepted = set(model.forward.__code__.co_varnames) | |
| filtered = {k: v for k, v in encoded.items() if k in accepted} | |
| return model(**filtered) | |
| # ββ FLOPs profiling βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def profile_flops(model, tokenizer, seq_len: int = SEQ_LEN) -> dict: | |
| """Return FLOPs, MACs, and parameter count via DeepSpeed.""" | |
| encoded = tokenizer( | |
| " ".join(["the"] * seq_len), | |
| return_tensors="pt", | |
| padding="max_length", | |
| truncation=True, | |
| max_length=seq_len, | |
| ) | |
| encoded = {k: v.to("cpu") for k, v in encoded.items()} | |
| accepted = set(model.forward.__code__.co_varnames) | |
| kwargs = {k: v for k, v in encoded.items() if k in accepted} | |
| with torch.no_grad(): | |
| logging.disable(logging.INFO) | |
| try: | |
| flops, macs, params = get_model_profile( | |
| model=model, | |
| kwargs=kwargs, | |
| warm_up=10, | |
| detailed=False, | |
| print_profile=False, | |
| as_string=False, | |
| ) | |
| finally: | |
| logging.disable(logging.NOTSET) | |
| return {"flops": flops, "macs": macs, "params": params} | |
| # ββ Latency benchmark βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def benchmark_latency( | |
| model, | |
| tokenizer, | |
| text: str = SAMPLE_TEXT, | |
| warm: int = WARM_UP, | |
| runs: int = RUNS, | |
| ) -> dict: | |
| """Return mean and p95 CPU latency in milliseconds over `runs` forward passes.""" | |
| encoded = tokenizer( | |
| text, | |
| padding="max_length", | |
| truncation=True, | |
| max_length=SEQ_LEN, | |
| return_tensors="pt", | |
| ) | |
| encoded = {k: v.to("cpu") for k, v in encoded.items()} | |
| with torch.inference_mode(): | |
| for _ in range(warm): | |
| _safe_forward(model, encoded) | |
| latencies = [] | |
| for _ in range(runs): | |
| t0 = time.perf_counter() | |
| _safe_forward(model, encoded) | |
| latencies.append((time.perf_counter() - t0) * 1_000) | |
| return { | |
| "mean_ms": float(np.mean(latencies)), | |
| "p95_ms": float(np.percentile(latencies, 95)), | |
| "runs": runs, | |
| } | |
| # ββ Output formatting βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def print_results(model_id: str, flops_data: dict, latency_data: dict) -> None: | |
| label = model_id.split("/")[-1] | |
| sep = "=" * 50 | |
| print(f"\n{sep}") | |
| print(f" {label} | CPU Benchmark Results") | |
| print(sep) | |
| print(f" Parameters : {flops_data['params'] / 1e6:>8.1f} M") | |
| print(f" MACs : {flops_data['macs'] / 1e9:>8.2f} GMac") | |
| print(f" FLOPs : {flops_data['flops'] / 1e9:>8.2f} GFLOPs (2 Γ MACs)") | |
| print(sep) | |
| print(f" Runs : {latency_data['runs']:>8,}") | |
| print(f" Mean latency: {latency_data['mean_ms']:>8.2f} ms") | |
| print(f" p95 latency: {latency_data['p95_ms']:>8.2f} ms") | |
| print(f"{sep}\n") | |
| # ββ Entry point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def parse_args(): | |
| parser = argparse.ArgumentParser( | |
| description="Generic CPU inference benchmark for BERT-family models" | |
| ) | |
| parser.add_argument("--model", type=str, required=True, | |
| help="HuggingFace model ID (e.g. bert-base-uncased)") | |
| parser.add_argument("--tokenizer", type=str, default=None, | |
| help="HuggingFace tokenizer ID (defaults to --model)") | |
| parser.add_argument("--trust-remote-code", action="store_true", | |
| help="Pass trust_remote_code=True for custom architectures") | |
| parser.add_argument("--runs", type=int, default=RUNS, | |
| help=f"Number of latency runs (default: {RUNS:,})") | |
| parser.add_argument("--warm", type=int, default=WARM_UP, | |
| help=f"Warm-up passes before timing (default: {WARM_UP})") | |
| parser.add_argument("--seq-len", type=int, default=SEQ_LEN, | |
| help=f"Sequence length for FLOPs profiling (default: {SEQ_LEN})") | |
| parser.add_argument("--text", type=str, default=SAMPLE_TEXT, | |
| help="Sample text for latency benchmark") | |
| return parser.parse_args() | |
| def main(): | |
| args = parse_args() | |
| tokenizer_id = args.tokenizer or args.model | |
| print(f"[+] Loading tokenizer : {tokenizer_id}") | |
| print(f"[+] Loading model : {args.model}") | |
| model, tokenizer = load_model( | |
| model_id=args.model, | |
| tokenizer_id=tokenizer_id, | |
| trust_remote_code=args.trust_remote_code, | |
| ) | |
| print(f"\n[+] Profiling FLOPs (seq_len={args.seq_len}) ...") | |
| flops_data = profile_flops(model, tokenizer, seq_len=args.seq_len) | |
| print(f"[+] Benchmarking latency ({args.runs:,} runs, {args.warm} warm-up) ...") | |
| latency_data = benchmark_latency( | |
| model, tokenizer, | |
| text=args.text, | |
| warm=args.warm, | |
| runs=args.runs, | |
| ) | |
| print_results(args.model, flops_data, latency_data) | |
| if __name__ == "__main__": | |
| main() | |