FireRedPunc.AXERA / python /punc_ax.py
HY-2012's picture
Add FireRedPunc AX650 model and C++ runtime
3807da3 verified
Raw History Blame Contribute Delete
10.5 kB
#!/usr/bin/env python3
"""FireRedPunc AX650 runtime.
Standalone (no torch): transformers slow BertTokenizer + AXEngine.
p = FireRedPuncAX("models")
p.punctuate("今天天气很好我们去公园散步吧")
"""
from __future__ import annotations
import argparse
import re
import sys
import time
import traceback
from pathlib import Path
import numpy as np
try:
from transformers import BertTokenizer
except Exception as exc: # pragma: no cover
raise SystemExit("transformers is required (BertTokenizer): %s" % exc)
PUNC_ALL = set(",。?!、,.?!.。、")
DIGIT_JOINERS = set(",..")
SENT_PUNCT = re.compile(r"[。!?!?]")
ALNUM = re.compile(r"[a-zA-Z0-9#]+")
class TokenDict:
def __init__(self, path: str, unk: str = ""):
self.id2word, self.word2id = self.read_dict(path)
self.unk = unk
if unk:
assert unk in self.word2id, unk
self.unkid = self.word2id[unk] if unk else -1
def get(self, key, default):
if isinstance(default, str):
default = self.word2id[default]
return self.word2id.get(key, default)
def __getitem__(self, key):
if isinstance(key, str):
return self.word2id.get(key, self.word2id[self.unk]) if self.unk else self.word2id[key]
if isinstance(key, int):
return self.id2word[key]
raise TypeError(key)
def read_dict(self, path: str):
id2word, word2id = [], {}
with open(path, encoding="utf8") as f:
for i, line in enumerate(f):
toks = line.strip().split()
if len(toks) >= 2:
word, index = toks[0], int(toks[1])
elif len(toks) == 1:
word, index = toks[0], i
else:
word, index = " ", i
if word == "<space>":
word = " "
assert len(id2word) == index
word2id[word] = index
id2word.append(word)
return id2word, word2id
class HfBertTokenizer:
"""Upstream FireRedPunc tokenizer, including the _recover_unk pass."""
def __init__(self, tokenizer_dir: str):
self.tokenizer = BertTokenizer.from_pretrained(tokenizer_dir)
def tokenize(self, text: str, recover_unk: bool = False):
tokens = self.tokenizer.tokenize(text)
tokens_id = self.tokenizer.convert_tokens_to_ids(tokens)
if recover_unk:
try:
tokens = self._recover_unk(text.lower(), tokens)
except Exception:
traceback.print_exc()
return tokens, tokens_id
def _recover_unk(self, text: str, tokens: list[str]):
if "[UNK]" not in tokens:
return tokens
new_tokens = []
text_no_space = text.replace(" ", "")
if re.match(r"^[^a-zA-Z0-9']+$", text):
tmp_text = text_no_space
if len(tmp_text) == len(tokens):
success = True
for t, tok in zip(tmp_text, tokens):
if tok != "[UNK]" and t != tok:
success = False
break
new_tokens.append(t)
if success:
return new_tokens
new_tokens = []
text_pos = 0
i = 0
while i < len(tokens):
token = tokens[i]
if token == "[UNK]":
unk_count = 0
j = i
while j < len(tokens) and tokens[j] == "[UNK]":
unk_count += 1
j += 1
post_token = ""
if j < len(tokens):
post_token = tokens[j].replace("##", "")
if post_token:
remaining = text_no_space[text_pos:]
anchor_pos = remaining.find(post_token)
unk_chars = remaining[:anchor_pos] if anchor_pos != -1 else remaining[:unk_count]
else:
unk_chars = text_no_space[text_pos:text_pos + unk_count]
for k in range(unk_count):
new_tokens.append(unk_chars[k] if k < len(unk_chars) else "")
text_pos += len(unk_chars)
i = j
else:
new_tokens.append(token)
text_pos += len(token.replace("##", ""))
i += 1
return [t for t in new_tokens if t and t != "[UNK]"]
class RuleBaedTxtFix:
@classmethod
def fix(cls, txt_ori: str, capitalize_first: bool = True):
txt = txt_ori.lower()
txt = re.sub(r"([a-z]),([a-z])", r"\1, \2", txt)
txt = re.sub(r"([a-z])。([a-z])", r"\1. \2", txt)
txt = re.sub(r"([a-z])?([a-z])", r"\1? \2", txt)
txt = re.sub(r"([a-z])!([a-z])", r"\1! \2", txt)
txt = re.sub(r"^([a-z]+),", r"\1,", txt)
txt = re.sub(r"^([a-z]+)。", r"\1.", txt)
txt = re.sub(r"^([a-z]+)?", r"\1?", txt)
txt = re.sub(r"^([a-z]+)!", r"\1!", txt)
txt = re.sub(r"( [a-zA-Z']+),$", r"\1,", txt)
txt = re.sub(r"( [a-zA-Z']+)。$", r"\1.", txt)
txt = re.sub(r"( [a-zA-Z']+)?$", r"\1?", txt)
txt = re.sub(r"( [a-zA-Z']+)!$", r"\1!", txt)
txt = re.sub("^i ", "I ", txt)
txt = re.sub("^i'm ", "I'm ", txt)
txt = re.sub("^i'd ", "I'd ", txt)
txt = re.sub("^i've ", "I've ", txt)
txt = re.sub("^i'll ", "I'll ", txt)
txt = re.sub(" i ", " I ", txt)
txt = re.sub(" i'm ", " I'm ", txt)
txt = re.sub(" i'd ", " I'd ", txt)
txt = re.sub(" i've ", " I've ", txt)
txt = re.sub(" i'll ", " I'll ", txt)
if capitalize_first and txt and re.match("[a-z]", txt[0]):
txt = txt[0].upper() + txt[1:]
txt = re.sub(r"([.!?。?!])\s+([a-z])",
lambda m: f"{m.group(1)} {m.group(2).upper()}", txt)
return txt
def strip_marks(text: str) -> str:
out = []
for i, ch in enumerate(text):
prev = text[i - 1] if i > 0 else ""
nxt = text[i + 1] if i + 1 < len(text) else ""
joins = ch in DIGIT_JOINERS and prev.isdigit() and nxt.isdigit()
out.append(" " if ch in PUNC_ALL and not joins else ch)
return "".join(out)
class FireRedPuncAX:
def __init__(self, model_dir: str | Path, seq_len: int = 128):
self.dir = Path(model_dir)
self.seq_len = int(seq_len)
self.max_tokens = self.seq_len - 1
self.cls_id = 101
self.tokenizer = HfBertTokenizer(str(self.dir / "tokenizer"))
self.in_dict = TokenDict(str(self.dir / "vocab.txt"), unk="[UNK]")
self.out_dict = TokenDict(str(self.dir / "out_dict"))
from axengine import InferenceSession
# Force the on-board NPU provider. When AXCL libs are on
# LD_LIBRARY_PATH, axengine lists AXCLRT first and tries to open a
# host PCIe device that does not exist on the board.
self.sess = InferenceSession(
str(self.dir / "punc.axmodel"),
providers=["AxEngineExecutionProvider"])
self._warmup()
def _warmup(self):
try:
self.classify("你好")
except Exception:
traceback.print_exc()
def _encode(self, text: str):
tokens, _ = self.tokenizer.tokenize(text, recover_unk=True)
ids = [self.in_dict.get(t, self.in_dict.unk) for t in tokens]
return tokens, ids
def classify(self, text: str):
text = strip_marks(text or "")
tokens, ids = self._encode(text)
ids = ids[: self.max_tokens]
n = len(ids)
if n == 0:
return [], [], []
x = np.zeros((1, self.seq_len), dtype=np.int32)
x[0, 0] = self.cls_id
x[0, 1:1 + n] = np.asarray(ids, dtype=np.int32)
mask = np.zeros((1, self.seq_len), dtype=np.float32)
mask[0, :1 + n] = 1.0
logits = self.sess.run(None, {"input_ids": x, "attention_mask": mask})[0]
preds = logits[0, :n].argmax(-1).tolist()
return tokens[:n], ids, preds
def _add_punc(self, tokens: list[str], preds: list[int]) -> str:
txt = ""
for i, token in enumerate(tokens):
tag = self.out_dict[preds[i]]
if token.startswith("##"):
token = token.replace("##", "")
elif ALNUM.search(token) and i > 0 and ALNUM.search(tokens[i - 1]):
if self.out_dict[preds[i - 1]] == " ":
token = " " + token
txt += token if tag == " " else token + tag
return txt.replace(" ", " ")
def punctuate(self, text: str, force: bool = False) -> str:
text = (text or "").strip()
if not text:
return ""
if not force and SENT_PUNCT.search(text):
return text
tokens, _, preds = self.classify(text)
if not tokens:
return text
return RuleBaedTxtFix.fix(self._add_punc(tokens, preds))
def latency(self, text: str, n: int = 30) -> dict:
self.punctuate(text, force=True)
vals = []
for _ in range(n):
t0 = time.perf_counter()
self.punctuate(text, force=True)
vals.append((time.perf_counter() - t0) * 1000)
vals.sort()
return {"median_ms": round(vals[len(vals) // 2], 2),
"p95_ms": round(vals[int(len(vals) * 0.95) - 1], 2),
"min_ms": round(vals[0], 2), "max_ms": round(vals[-1], 2)}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--model-dir", default=str(Path(__file__).resolve().parents[1] / "models"))
ap.add_argument("--text", action="append", default=[])
ap.add_argument("--text-file", default="")
ap.add_argument("--force", action="store_true")
ap.add_argument("--stats", action="store_true")
args = ap.parse_args()
p = FireRedPuncAX(args.model_dir)
texts = list(args.text or [])
if args.text_file:
texts += [ln.strip() for ln in Path(args.text_file).read_text(encoding="utf-8").splitlines() if ln.strip()]
if not texts:
texts = ["今天天气很好我们去公园散步吧",
"会议将在下午三点开始请大家准时参加",
"hello how are you i am fine thanks"]
for t in texts:
out = p.punctuate(t, force=args.force)
print(f"IN : {t}")
print(f"OUT: {out}")
if args.stats:
print(" ", p.latency(t))
if __name__ == "__main__":
main()