Text Classification
Transformers
Safetensors
Chinese
spam-detection
advertising-detection
ad-classification
chinese
modernbert
mmbert
multi-task
four-head
Instructions to use x6nux/laya-ad with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use x6nux/laya-ad with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="x6nux/laya-ad")# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("x6nux/laya-ad", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download code/scripts/bench_cpu.py from x6nux/laya-ad: direct link, hf CLI and curl.
- Browser
- Download file 8.85 kB
-
https://huggingface.co/x6nux/laya-ad/resolve/main/code/scripts/bench_cpu.py
- Command line
-
hf download hf://x6nux/laya-ad/code/scripts/bench_cpu.py
-
curl -L -o bench_cpu.py https://huggingface.co/x6nux/laya-ad/resolve/main/code/scripts/bench_cpu.py
8.85 kB
| #!/usr/bin/env python3 | |
| """CPU fp32 vs int8 动态量化基准(目标:保正确率、最大提性能)。 | |
| 流程(同机同条件,一次一个变量): | |
| 1. CPU 加载 outputs/ad-clf/best(fp32),val.jsonl 前 N(=500) 条测正确率 + 吞吐; | |
| 2. torch.ao.quantization.quantize_dynamic(model, {torch.nn.Linear}, dtype=torch.qint8); | |
| 3. 同口径重测 int8 正确率与吞吐,量化模型落盘 outputs/ad-clf-int8/quantized.pt。 | |
| 口径: | |
| - 正确率:data/processed/val.jsonl 前 500 条,label_map=data/processed/label_map.json; | |
| - 吞吐:val 真实文本,tokenizer max_len=128、batch=8,计时含 tokenize(部署真实链路), | |
| 样本/秒 = N / elapsed; | |
| - 线程:torch.set_num_threads(物理核数一半);OMP/MKL 等 env 在 import torch 之前设置; | |
| - 稳态:先跑 1 轮预热(不计吞吐),再计时 --rounds(=3) 轮取中位数; | |
| 正确率确定性强,各轮应一致,不一致会打 warn。 | |
| 采纳标准:int8_acc >= fp32_acc - 0.01 且 speedup(int8_sps/fp32_sps) >= 1.3。 | |
| 用法: | |
| python scripts/bench_cpu.py # 完整基准 | |
| python scripts/bench_cpu.py --n 16 --rounds 1 # 快速冒烟 | |
| quantized.pt 加载方式(必须先 import transformers 让 ModernBert* 类可反序列化; | |
| weights_only=False 才允许加载完整 pickle 的 nn.Module): | |
| import transformers, torch | |
| model = torch.load("outputs/ad-clf-int8/quantized.pt", map_location="cpu", weights_only=False) | |
| model.eval() | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import statistics | |
| import sys | |
| import time | |
| def _physical_cores() -> int: | |
| try: | |
| import psutil | |
| n = psutil.cpu_count(logical=False) | |
| if n: | |
| return n | |
| except Exception: | |
| pass | |
| return max(1, (os.cpu_count() or 2) // 2) # 无 psutil 时按超线程折半兜底 | |
| # ---- 线程环境变量:必须先于 import torch / transformers(它们会拉起线程池)---- | |
| PHYS_CORES = _physical_cores() | |
| THREADS = max(1, PHYS_CORES // 2) | |
| for _var in ("OMP_NUM_THREADS", "MKL_NUM_THREADS", "OPENBLAS_NUM_THREADS", | |
| "NUMEXPR_NUM_THREADS", "VECLIB_MAXIMUM_THREADS"): | |
| os.environ[_var] = str(THREADS) | |
| os.environ["TOKENIZERS_PARALLELISM"] = "false" | |
| import torch # noqa: E402 env 已就位 | |
| from transformers import AutoModelForSequenceClassification, AutoTokenizer # noqa: E402 | |
| torch.set_num_threads(THREADS) | |
| if hasattr(sys.stdout, "reconfigure"): # Windows 管道下避免 GBK 编码报错 | |
| sys.stdout.reconfigure(encoding="utf-8", errors="replace") | |
| def run_pass(model, tok, texts, y_true, batch_size, max_len): | |
| """一遍全量前向,返回 (acc, elapsed)。""" | |
| preds = [] | |
| t0 = time.perf_counter() | |
| with torch.inference_mode(): | |
| for i in range(0, len(texts), batch_size): | |
| enc = tok(texts[i:i + batch_size], truncation=True, | |
| max_length=max_len, padding=True, return_tensors="pt") | |
| preds.extend(model(**enc).logits.argmax(-1).tolist()) | |
| elapsed = time.perf_counter() - t0 | |
| acc = sum(int(p == t) for p, t in zip(preds, y_true)) / len(y_true) | |
| return acc, elapsed | |
| def bench(tag, model, tok, texts, y_true, batch_size, max_len, rounds): | |
| """预热 1 轮 + 计时 rounds 轮取中位数;正确率取计时轮第一轮。""" | |
| acc0, _ = run_pass(model, tok, texts, y_true, batch_size, max_len) | |
| print(f" [{tag}] warmup: acc={acc0:.4f}(不计吞吐)", flush=True) | |
| sps_list, sec_list, accs = [], [], [] | |
| for r in range(rounds): | |
| acc, sec = run_pass(model, tok, texts, y_true, batch_size, max_len) | |
| accs.append(acc) | |
| sec_list.append(round(sec, 3)) | |
| sps_list.append(len(texts) / sec) | |
| print(f" [{tag}] round {r + 1}: acc={acc:.4f} {sec:.1f}s " | |
| f"{len(texts) / sec:.2f} 样本/s", flush=True) | |
| if len(set(accs + [acc0])) != 1: | |
| print(f" [warn] {tag} 各轮正确率不一致: warmup={acc0} rounds={accs}", flush=True) | |
| return { | |
| "acc": accs[0], | |
| "sps": statistics.median(sps_list), | |
| "sps_rounds": [round(s, 2) for s in sps_list], | |
| "sec_rounds": sec_list, | |
| "warmup_acc": acc0, | |
| } | |
| def main(): | |
| ap = argparse.ArgumentParser(description=__doc__) | |
| ap.add_argument("--model", default="outputs/ad-clf/best") | |
| ap.add_argument("--val", default="data/processed/val.jsonl") | |
| ap.add_argument("--label_map", default="data/processed/label_map.json") | |
| ap.add_argument("--out", default="outputs/cpu_quant_bench.json") | |
| ap.add_argument("--quant_out", default="outputs/ad-clf-int8/quantized.pt") | |
| ap.add_argument("--n", type=int, default=500, help="val 前 N 条") | |
| ap.add_argument("--batch_size", type=int, default=8) | |
| ap.add_argument("--max_len", type=int, default=128) | |
| ap.add_argument("--rounds", type=int, default=3, help="计时轮数(另有 1 轮预热)") | |
| args = ap.parse_args() | |
| print(f"物理核={PHYS_CORES} torch.set_num_threads={THREADS} " | |
| f"OMP/MKL_NUM_THREADS={os.environ['OMP_NUM_THREADS']}", flush=True) | |
| print(f"torch={torch.__version__} transformers={__import__('transformers').__version__}", | |
| flush=True) | |
| # ---- 数据:val 前 N 条真实文本 ---- | |
| with open(args.val, encoding="utf-8") as f: | |
| rows = [json.loads(l) for l in f if l.strip()][:args.n] | |
| with open(args.label_map, encoding="utf-8") as f: | |
| label2id = json.load(f) | |
| texts = [r["text"] for r in rows] | |
| try: | |
| y_true = [label2id[r["label"]] for r in rows] | |
| except KeyError as e: | |
| raise SystemExit(f"label_map({args.label_map}) 缺少标签 {e};" | |
| f"数据标签集合={sorted({r['label'] for r in rows})}") | |
| print(f"样本={len(texts)} 标签分布={ {k: sum(r['label'] == k for r in rows) for k in label2id} }", | |
| flush=True) | |
| # ---- fp32 基准 ---- | |
| print(f"[1/3] 加载 fp32 模型 {args.model} ...", flush=True) | |
| tok = AutoTokenizer.from_pretrained(args.model) | |
| model = AutoModelForSequenceClassification.from_pretrained(args.model).eval() | |
| assert next(model.parameters()).device.type == "cpu", "必须在 CPU 上" | |
| print("[1/3] fp32 基准测量", flush=True) | |
| fp32 = bench("fp32", model, tok, texts, y_true, args.batch_size, args.max_len, args.rounds) | |
| # ---- int8 动态量化(只量化 nn.Linear)---- | |
| print("[2/3] quantize_dynamic -> qint8 ...", flush=True) | |
| t0 = time.perf_counter() | |
| model_int8 = torch.ao.quantization.quantize_dynamic( | |
| model, {torch.nn.Linear}, dtype=torch.qint8) | |
| print(f"[2/3] 量化完成 {time.perf_counter() - t0:.1f}s", flush=True) | |
| os.makedirs(os.path.dirname(args.quant_out) or ".", exist_ok=True) | |
| torch.save(model_int8, args.quant_out) | |
| print(f"[2/3] 量化模型已存 {args.quant_out}", flush=True) | |
| print("[3/3] int8 同口径测量", flush=True) | |
| int8 = bench("int8", model_int8, tok, texts, y_true, args.batch_size, args.max_len, | |
| args.rounds) | |
| speedup = int8["sps"] / fp32["sps"] | |
| acc_drop = fp32["acc"] - int8["acc"] | |
| accepted = (int8["acc"] >= fp32["acc"] - 0.01) and (speedup >= 1.3) | |
| result = { | |
| "model": args.model, | |
| "device": "cpu", | |
| "torch": torch.__version__, | |
| "transformers": __import__("transformers").__version__, | |
| "physical_cores": PHYS_CORES, | |
| "torch_num_threads": THREADS, | |
| "env": {k: os.environ[k] for k in | |
| ("OMP_NUM_THREADS", "MKL_NUM_THREADS", "OPENBLAS_NUM_THREADS", | |
| "NUMEXPR_NUM_THREADS", "VECLIB_MAXIMUM_THREADS", "TOKENIZERS_PARALLELISM")}, | |
| "n_samples": len(texts), | |
| "batch_size": args.batch_size, | |
| "max_len": args.max_len, | |
| "rounds": args.rounds, | |
| "sps_agg": f"median of {args.rounds} timed rounds (1 warmup excluded)", | |
| "fp32_acc": round(fp32["acc"], 4), | |
| "int8_acc": round(int8["acc"], 4), | |
| "fp32_sps": round(fp32["sps"], 2), | |
| "int8_sps": round(int8["sps"], 2), | |
| "speedup": round(speedup, 3), | |
| "acc_drop": round(acc_drop, 4), | |
| "fp32_detail": fp32, | |
| "int8_detail": int8, | |
| "quantized_model": args.quant_out, | |
| "criteria": {"max_acc_drop": 0.01, "min_speedup": 1.3}, | |
| "accepted": accepted, | |
| } | |
| os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True) | |
| with open(args.out, "w", encoding="utf-8") as f: | |
| json.dump(result, f, ensure_ascii=False, indent=2) | |
| print("=" * 60, flush=True) | |
| print(f"fp32: acc={fp32['acc']:.4f} {fp32['sps']:.2f} 样本/s", flush=True) | |
| print(f"int8: acc={int8['acc']:.4f} {int8['sps']:.2f} 样本/s", flush=True) | |
| print(f"speedup={speedup:.3f}x acc_drop={acc_drop:+.4f} " | |
| f"accepted={accepted}", flush=True) | |
| print(f"结果已写 {args.out}", flush=True) | |
| if __name__ == "__main__": | |
| main() | |