Spaces:
Running
Running
Add benchmark phase profile and paragraph chunk fix
Browse files- hachimimt-local.zip +2 -2
- src/benchmark_file.py +114 -0
- src/translator.py +130 -4
hachimimt-local.zip
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9d639cba50e5ca3824f08f5026276012522bdc6cb1319ee7f73d049d0f997c51
|
| 3 |
+
size 95965
|
src/benchmark_file.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Benchmark local file translation throughput."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import time
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
from app import read_text_file
|
| 10 |
+
from hardware import detect_hardware_profile
|
| 11 |
+
from text_preprocess import (
|
| 12 |
+
NORMALIZE_AUTO,
|
| 13 |
+
NORMALIZE_MODES,
|
| 14 |
+
normalization_message,
|
| 15 |
+
normalize_chinese_text,
|
| 16 |
+
)
|
| 17 |
+
from translator import Backend, HachimiTranslator
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def parse_args() -> argparse.Namespace:
|
| 21 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 22 |
+
parser.add_argument("path", type=Path, help="Input .txt file")
|
| 23 |
+
parser.add_argument("--model", default="HachimiMT-60")
|
| 24 |
+
parser.add_argument("--backend", choices=[Backend.CT2.value, Backend.TRANSFORMERS.value], default=Backend.CT2.value)
|
| 25 |
+
parser.add_argument("--beam", type=int, default=2)
|
| 26 |
+
parser.add_argument("--chunk-mode", choices=["sentence", "paragraph"], default="sentence")
|
| 27 |
+
parser.add_argument("--normalize", choices=sorted(NORMALIZE_MODES), default=NORMALIZE_AUTO)
|
| 28 |
+
parser.add_argument("--progress-seconds", type=float, default=15.0)
|
| 29 |
+
return parser.parse_args()
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def main() -> None:
|
| 33 |
+
args = parse_args()
|
| 34 |
+
total_start = time.perf_counter()
|
| 35 |
+
|
| 36 |
+
profile = detect_hardware_profile()
|
| 37 |
+
print(f"BENCH_START file={args.path}", flush=True)
|
| 38 |
+
print(f"PROFILE {profile.summary}", flush=True)
|
| 39 |
+
|
| 40 |
+
read_start = time.perf_counter()
|
| 41 |
+
text = read_text_file(args.path)
|
| 42 |
+
read_s = time.perf_counter() - read_start
|
| 43 |
+
original_chars = len(text)
|
| 44 |
+
normalize_start = time.perf_counter()
|
| 45 |
+
normalized_text = normalize_chinese_text(text, args.normalize)
|
| 46 |
+
normalize_s = time.perf_counter() - normalize_start
|
| 47 |
+
normalize_msg = normalization_message(text, normalized_text, args.normalize)
|
| 48 |
+
text = normalized_text
|
| 49 |
+
print(
|
| 50 |
+
f"INPUT chars={len(text)} original_chars={original_chars} "
|
| 51 |
+
f"lines={text.count(chr(10)) + 1 if text else 0} read_s={read_s:.3f} "
|
| 52 |
+
f"normalize_s={normalize_s:.3f} normalize={args.normalize} "
|
| 53 |
+
f"normalize_msg={normalize_msg}",
|
| 54 |
+
flush=True,
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
translator = HachimiTranslator(profile)
|
| 58 |
+
load_start = time.perf_counter()
|
| 59 |
+
status = translator.load(args.model, backend=args.backend)
|
| 60 |
+
load_s = time.perf_counter() - load_start
|
| 61 |
+
print(f"LOAD seconds={load_s:.3f} status={status}", flush=True)
|
| 62 |
+
|
| 63 |
+
translate_start = time.perf_counter()
|
| 64 |
+
last_print = 0.0
|
| 65 |
+
rows = []
|
| 66 |
+
full_text = ""
|
| 67 |
+
|
| 68 |
+
for done, total, message, result_rows, result_text in translator.translate_text_iter(
|
| 69 |
+
text,
|
| 70 |
+
chunk_mode=args.chunk_mode,
|
| 71 |
+
beam_size=args.beam,
|
| 72 |
+
):
|
| 73 |
+
now = time.perf_counter()
|
| 74 |
+
if result_rows is not None and result_text is not None:
|
| 75 |
+
rows = result_rows
|
| 76 |
+
full_text = result_text
|
| 77 |
+
|
| 78 |
+
if done == 0 or done == total or now - last_print >= args.progress_seconds:
|
| 79 |
+
elapsed = now - translate_start
|
| 80 |
+
rate = done / elapsed if elapsed > 0 else 0.0
|
| 81 |
+
eta = (total - done) / rate if rate > 0 else 0.0
|
| 82 |
+
pct = done / total * 100 if total else 100.0
|
| 83 |
+
print(
|
| 84 |
+
"PROGRESS "
|
| 85 |
+
f"done={done} total={total} pct={pct:.2f} elapsed_s={elapsed:.1f} "
|
| 86 |
+
f"rate_chunks_s={rate:.2f} eta_s={eta:.1f} message={message}",
|
| 87 |
+
flush=True,
|
| 88 |
+
)
|
| 89 |
+
last_print = now
|
| 90 |
+
|
| 91 |
+
translate_s = time.perf_counter() - translate_start
|
| 92 |
+
total_s = time.perf_counter() - total_start
|
| 93 |
+
profile = translator.last_profile
|
| 94 |
+
if profile:
|
| 95 |
+
profile_parts = []
|
| 96 |
+
for key in sorted(profile):
|
| 97 |
+
value = profile[key]
|
| 98 |
+
if isinstance(value, float):
|
| 99 |
+
profile_parts.append(f"{key}={value:.6f}")
|
| 100 |
+
else:
|
| 101 |
+
profile_parts.append(f"{key}={value}")
|
| 102 |
+
print("BENCH_PROFILE " + " ".join(profile_parts), flush=True)
|
| 103 |
+
print(
|
| 104 |
+
"BENCH_DONE "
|
| 105 |
+
f"total_s={total_s:.3f} translate_s={translate_s:.3f} minutes={total_s / 60:.3f} "
|
| 106 |
+
f"chunks={len(rows)} chars_in={len(text)} chars_out={len(full_text)} "
|
| 107 |
+
f"chunks_s={(len(rows) / translate_s) if translate_s > 0 else 0:.3f} "
|
| 108 |
+
f"chars_s={(len(text) / translate_s) if translate_s > 0 else 0:.1f}",
|
| 109 |
+
flush=True,
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
if __name__ == "__main__":
|
| 114 |
+
main()
|
src/translator.py
CHANGED
|
@@ -3,6 +3,8 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import os
|
|
|
|
|
|
|
| 6 |
from concurrent.futures import Future, ThreadPoolExecutor
|
| 7 |
from dataclasses import dataclass
|
| 8 |
from enum import Enum
|
|
@@ -309,6 +311,40 @@ class CT2SentencePieceTokenizer:
|
|
| 309 |
def __init__(self, model_path: Path) -> None:
|
| 310 |
self._source_sp = spm.SentencePieceProcessor(model_file=str(model_path / "source.spm"))
|
| 311 |
self._target_sp = spm.SentencePieceProcessor(model_file=str(model_path / "target.spm"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
|
| 313 |
def _encode_one(
|
| 314 |
self,
|
|
@@ -317,8 +353,7 @@ class CT2SentencePieceTokenizer:
|
|
| 317 |
truncation: bool = False,
|
| 318 |
max_length: int | None = None,
|
| 319 |
) -> list[int]:
|
| 320 |
-
token_ids =
|
| 321 |
-
token_ids.append(EOS_TOKEN_ID)
|
| 322 |
if truncation and max_length is not None and len(token_ids) > max_length:
|
| 323 |
token_ids = token_ids[:max_length]
|
| 324 |
if token_ids:
|
|
@@ -388,6 +423,13 @@ class CT2SentencePieceTokenizer:
|
|
| 388 |
decoded.append(self._target_sp.decode(pieces))
|
| 389 |
return decoded
|
| 390 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
|
| 392 |
class CT2FastTokenizer:
|
| 393 |
"""Minimal tokenizer.json wrapper for CTranslate2 inference."""
|
|
@@ -403,6 +445,39 @@ class CT2FastTokenizer:
|
|
| 403 |
self._tokenizer = Tokenizer.from_file(str(model_path / "tokenizer.json"))
|
| 404 |
self.pad_token_id = self._tokenizer.token_to_id("<pad>")
|
| 405 |
self._eos_token_id = self._tokenizer.token_to_id("</s>")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
|
| 407 |
def _encode_one(
|
| 408 |
self,
|
|
@@ -411,7 +486,7 @@ class CT2FastTokenizer:
|
|
| 411 |
truncation: bool = False,
|
| 412 |
max_length: int | None = None,
|
| 413 |
) -> list[int]:
|
| 414 |
-
token_ids =
|
| 415 |
if truncation and max_length is not None and len(token_ids) > max_length:
|
| 416 |
token_ids = token_ids[:max_length]
|
| 417 |
if token_ids and self._eos_token_id is not None:
|
|
@@ -474,6 +549,13 @@ class CT2FastTokenizer:
|
|
| 474 |
for token_ids in token_ids_batch
|
| 475 |
]
|
| 476 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
|
| 478 |
def _load_ct2_tokenizer(model_path: Path):
|
| 479 |
if (model_path / "source.spm").exists() and (model_path / "target.spm").exists():
|
|
@@ -584,6 +666,7 @@ class HachimiTranslator:
|
|
| 584 |
self._batch_size = self._profile.batch_size
|
| 585 |
self._tokenize_workers = self._profile.tokenize_workers
|
| 586 |
self._tokenize_pool: ThreadPoolExecutor | None = None
|
|
|
|
| 587 |
|
| 588 |
@property
|
| 589 |
def hardware_profile(self) -> HardwareProfile:
|
|
@@ -593,6 +676,19 @@ class HachimiTranslator:
|
|
| 593 |
def batch_size(self) -> int:
|
| 594 |
return self._batch_size
|
| 595 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 596 |
def set_batch_size(self, batch_size: int) -> None:
|
| 597 |
self._batch_size = max(4, min(128, int(batch_size)))
|
| 598 |
|
|
@@ -749,12 +845,20 @@ class HachimiTranslator:
|
|
| 749 |
return [tokens for job in jobs for tokens in job.result()]
|
| 750 |
|
| 751 |
def _decode_ct2_results(self, results) -> list[str]:
|
|
|
|
| 752 |
hypotheses = [result.hypotheses[0] for result in results]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 753 |
token_ids = [self._tokenizer.convert_tokens_to_ids(tokens) for tokens in hypotheses]
|
| 754 |
-
|
| 755 |
text.strip()
|
| 756 |
for text in self._tokenizer.batch_decode(token_ids, skip_special_tokens=True)
|
| 757 |
]
|
|
|
|
|
|
|
| 758 |
|
| 759 |
def _load_ct2(self, config: ModelConfig) -> None:
|
| 760 |
model_path = ensure_model_files(config, Backend.CT2)
|
|
@@ -978,7 +1082,10 @@ class HachimiTranslator:
|
|
| 978 |
) -> list[str]:
|
| 979 |
config = MODELS[self._model_key]
|
| 980 |
if source_batches is None:
|
|
|
|
| 981 |
source_batches = self._tokenize_chunks_parallel(chunks)
|
|
|
|
|
|
|
| 982 |
results = self._ct2_model.translate_batch(
|
| 983 |
source_batches,
|
| 984 |
max_batch_size=self._ct2_max_batch_size(config),
|
|
@@ -987,6 +1094,7 @@ class HachimiTranslator:
|
|
| 987 |
max_decoding_length=config.ct2_max_output_tokens,
|
| 988 |
**self._ct2_repetition_kwargs(config),
|
| 989 |
)
|
|
|
|
| 990 |
return self._decode_ct2_results(results)
|
| 991 |
|
| 992 |
def _translate_ct2_batch_pipelined(
|
|
@@ -997,7 +1105,9 @@ class HachimiTranslator:
|
|
| 997 |
prefetched_tokens: SourceTokenJobs | None,
|
| 998 |
) -> list[str]:
|
| 999 |
if prefetched_tokens is not None:
|
|
|
|
| 1000 |
source_batches = self._collect_tokenize_jobs(prefetched_tokens)
|
|
|
|
| 1001 |
else:
|
| 1002 |
source_batches = None
|
| 1003 |
return self._translate_ct2_batch(
|
|
@@ -1024,8 +1134,14 @@ class HachimiTranslator:
|
|
| 1024 |
) -> Iterator[tuple[int, int, str, list[tuple[int, str, str]] | None, str | None]]:
|
| 1025 |
"""Yield (done, total, message, rows_or_none, full_text_or_none) sau mỗi batch."""
|
| 1026 |
beam_size = self.clamp_beam(beam_size)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1027 |
chunks = self._chunk_text(text, chunk_mode)
|
|
|
|
| 1028 |
total = len(chunks)
|
|
|
|
| 1029 |
|
| 1030 |
yield 0, total, f"Đã chia {total} chunk, chuẩn bị dịch...", None, None
|
| 1031 |
|
|
@@ -1035,7 +1151,9 @@ class HachimiTranslator:
|
|
| 1035 |
next_tokens: SourceTokenJobs | None = None
|
| 1036 |
if self._backend == Backend.CT2 and total:
|
| 1037 |
first_end = min(window_size, total)
|
|
|
|
| 1038 |
next_tokens = self._submit_tokenize_jobs(chunks[:first_end])
|
|
|
|
| 1039 |
|
| 1040 |
for start in range(0, total, window_size):
|
| 1041 |
end = min(start + window_size, total)
|
|
@@ -1065,7 +1183,9 @@ class HachimiTranslator:
|
|
| 1065 |
next_end = min(next_start + window_size, total)
|
| 1066 |
if next_start < total:
|
| 1067 |
next_batch = chunks[next_start:next_end]
|
|
|
|
| 1068 |
next_tokens = self._submit_tokenize_jobs(next_batch)
|
|
|
|
| 1069 |
|
| 1070 |
translations.extend(
|
| 1071 |
self._translate_ct2_batch_pipelined(
|
|
@@ -1079,11 +1199,17 @@ class HachimiTranslator:
|
|
| 1079 |
|
| 1080 |
yield end, total, f"Đã xong {end}/{total} chunk", None, None
|
| 1081 |
|
|
|
|
| 1082 |
rows = [
|
| 1083 |
(index, chunk, translated)
|
| 1084 |
for index, (chunk, translated) in enumerate(zip(chunks, translations), start=1)
|
| 1085 |
]
|
| 1086 |
full_text = "\n".join(translations)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1087 |
yield total, total, "Hoàn tất dịch.", rows, full_text
|
| 1088 |
|
| 1089 |
def translate_text(
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import os
|
| 6 |
+
import time
|
| 7 |
+
from collections import OrderedDict
|
| 8 |
from concurrent.futures import Future, ThreadPoolExecutor
|
| 9 |
from dataclasses import dataclass
|
| 10 |
from enum import Enum
|
|
|
|
| 311 |
def __init__(self, model_path: Path) -> None:
|
| 312 |
self._source_sp = spm.SentencePieceProcessor(model_file=str(model_path / "source.spm"))
|
| 313 |
self._target_sp = spm.SentencePieceProcessor(model_file=str(model_path / "target.spm"))
|
| 314 |
+
self._encode_cache_max = _env_int(
|
| 315 |
+
"HACHIMIMT_TOKEN_CACHE_SIZE",
|
| 316 |
+
0,
|
| 317 |
+
min_value=0,
|
| 318 |
+
max_value=500_000,
|
| 319 |
+
)
|
| 320 |
+
self._encode_cache: OrderedDict[str, list[int]] = OrderedDict()
|
| 321 |
+
self._cache_hits = 0
|
| 322 |
+
self._cache_misses = 0
|
| 323 |
+
|
| 324 |
+
def _full_encode_one(self, text: str) -> list[int]:
|
| 325 |
+
if self._encode_cache_max > 0:
|
| 326 |
+
cached = self._encode_cache.get(text)
|
| 327 |
+
if cached is not None:
|
| 328 |
+
self._encode_cache.move_to_end(text)
|
| 329 |
+
self._cache_hits += 1
|
| 330 |
+
return list(cached)
|
| 331 |
+
|
| 332 |
+
self._cache_misses += 1
|
| 333 |
+
token_ids = list(self._source_sp.encode(text, out_type=int))
|
| 334 |
+
token_ids.append(EOS_TOKEN_ID)
|
| 335 |
+
if self._encode_cache_max > 0:
|
| 336 |
+
self._encode_cache[text] = list(token_ids)
|
| 337 |
+
self._encode_cache.move_to_end(text)
|
| 338 |
+
while len(self._encode_cache) > self._encode_cache_max:
|
| 339 |
+
self._encode_cache.popitem(last=False)
|
| 340 |
+
return token_ids
|
| 341 |
+
|
| 342 |
+
def cache_stats(self) -> dict[str, int]:
|
| 343 |
+
return {
|
| 344 |
+
"token_cache_entries": len(self._encode_cache),
|
| 345 |
+
"token_cache_hits": self._cache_hits,
|
| 346 |
+
"token_cache_misses": self._cache_misses,
|
| 347 |
+
}
|
| 348 |
|
| 349 |
def _encode_one(
|
| 350 |
self,
|
|
|
|
| 353 |
truncation: bool = False,
|
| 354 |
max_length: int | None = None,
|
| 355 |
) -> list[int]:
|
| 356 |
+
token_ids = self._full_encode_one(text)
|
|
|
|
| 357 |
if truncation and max_length is not None and len(token_ids) > max_length:
|
| 358 |
token_ids = token_ids[:max_length]
|
| 359 |
if token_ids:
|
|
|
|
| 423 |
decoded.append(self._target_sp.decode(pieces))
|
| 424 |
return decoded
|
| 425 |
|
| 426 |
+
def decode_tokens_batch(self, tokens_batch: list[list[str]]) -> list[str]:
|
| 427 |
+
decoded: list[str] = []
|
| 428 |
+
for tokens in tokens_batch:
|
| 429 |
+
pieces = [token for token in tokens if token not in SPECIAL_TOKEN_TO_ID]
|
| 430 |
+
decoded.append(self._target_sp.decode(pieces).strip())
|
| 431 |
+
return decoded
|
| 432 |
+
|
| 433 |
|
| 434 |
class CT2FastTokenizer:
|
| 435 |
"""Minimal tokenizer.json wrapper for CTranslate2 inference."""
|
|
|
|
| 445 |
self._tokenizer = Tokenizer.from_file(str(model_path / "tokenizer.json"))
|
| 446 |
self.pad_token_id = self._tokenizer.token_to_id("<pad>")
|
| 447 |
self._eos_token_id = self._tokenizer.token_to_id("</s>")
|
| 448 |
+
self._encode_cache_max = _env_int(
|
| 449 |
+
"HACHIMIMT_TOKEN_CACHE_SIZE",
|
| 450 |
+
0,
|
| 451 |
+
min_value=0,
|
| 452 |
+
max_value=500_000,
|
| 453 |
+
)
|
| 454 |
+
self._encode_cache: OrderedDict[str, list[int]] = OrderedDict()
|
| 455 |
+
self._cache_hits = 0
|
| 456 |
+
self._cache_misses = 0
|
| 457 |
+
|
| 458 |
+
def _full_encode_one(self, text: str) -> list[int]:
|
| 459 |
+
if self._encode_cache_max > 0:
|
| 460 |
+
cached = self._encode_cache.get(text)
|
| 461 |
+
if cached is not None:
|
| 462 |
+
self._encode_cache.move_to_end(text)
|
| 463 |
+
self._cache_hits += 1
|
| 464 |
+
return list(cached)
|
| 465 |
+
|
| 466 |
+
self._cache_misses += 1
|
| 467 |
+
token_ids = list(self._tokenizer.encode(text).ids)
|
| 468 |
+
if self._encode_cache_max > 0:
|
| 469 |
+
self._encode_cache[text] = list(token_ids)
|
| 470 |
+
self._encode_cache.move_to_end(text)
|
| 471 |
+
while len(self._encode_cache) > self._encode_cache_max:
|
| 472 |
+
self._encode_cache.popitem(last=False)
|
| 473 |
+
return token_ids
|
| 474 |
+
|
| 475 |
+
def cache_stats(self) -> dict[str, int]:
|
| 476 |
+
return {
|
| 477 |
+
"token_cache_entries": len(self._encode_cache),
|
| 478 |
+
"token_cache_hits": self._cache_hits,
|
| 479 |
+
"token_cache_misses": self._cache_misses,
|
| 480 |
+
}
|
| 481 |
|
| 482 |
def _encode_one(
|
| 483 |
self,
|
|
|
|
| 486 |
truncation: bool = False,
|
| 487 |
max_length: int | None = None,
|
| 488 |
) -> list[int]:
|
| 489 |
+
token_ids = self._full_encode_one(text)
|
| 490 |
if truncation and max_length is not None and len(token_ids) > max_length:
|
| 491 |
token_ids = token_ids[:max_length]
|
| 492 |
if token_ids and self._eos_token_id is not None:
|
|
|
|
| 549 |
for token_ids in token_ids_batch
|
| 550 |
]
|
| 551 |
|
| 552 |
+
def decode_tokens_batch(self, tokens_batch: list[list[str]]) -> list[str]:
|
| 553 |
+
token_ids_batch = [self.convert_tokens_to_ids(tokens) for tokens in tokens_batch]
|
| 554 |
+
return [
|
| 555 |
+
text.strip()
|
| 556 |
+
for text in self.batch_decode(token_ids_batch, skip_special_tokens=True)
|
| 557 |
+
]
|
| 558 |
+
|
| 559 |
|
| 560 |
def _load_ct2_tokenizer(model_path: Path):
|
| 561 |
if (model_path / "source.spm").exists() and (model_path / "target.spm").exists():
|
|
|
|
| 666 |
self._batch_size = self._profile.batch_size
|
| 667 |
self._tokenize_workers = self._profile.tokenize_workers
|
| 668 |
self._tokenize_pool: ThreadPoolExecutor | None = None
|
| 669 |
+
self._last_profile: dict[str, float | int | str] = {}
|
| 670 |
|
| 671 |
@property
|
| 672 |
def hardware_profile(self) -> HardwareProfile:
|
|
|
|
| 676 |
def batch_size(self) -> int:
|
| 677 |
return self._batch_size
|
| 678 |
|
| 679 |
+
@property
|
| 680 |
+
def last_profile(self) -> dict[str, float | int | str]:
|
| 681 |
+
return dict(self._last_profile)
|
| 682 |
+
|
| 683 |
+
def _reset_profile(self) -> None:
|
| 684 |
+
self._last_profile = {}
|
| 685 |
+
|
| 686 |
+
def _profile_add(self, key: str, seconds: float) -> None:
|
| 687 |
+
self._last_profile[key] = float(self._last_profile.get(key, 0.0)) + seconds
|
| 688 |
+
|
| 689 |
+
def _profile_set(self, key: str, value: float | int | str) -> None:
|
| 690 |
+
self._last_profile[key] = value
|
| 691 |
+
|
| 692 |
def set_batch_size(self, batch_size: int) -> None:
|
| 693 |
self._batch_size = max(4, min(128, int(batch_size)))
|
| 694 |
|
|
|
|
| 845 |
return [tokens for job in jobs for tokens in job.result()]
|
| 846 |
|
| 847 |
def _decode_ct2_results(self, results) -> list[str]:
|
| 848 |
+
start = time.perf_counter()
|
| 849 |
hypotheses = [result.hypotheses[0] for result in results]
|
| 850 |
+
decode_tokens_batch = getattr(self._tokenizer, "decode_tokens_batch", None)
|
| 851 |
+
if callable(decode_tokens_batch):
|
| 852 |
+
decoded = decode_tokens_batch(hypotheses)
|
| 853 |
+
self._profile_add("decode_s", time.perf_counter() - start)
|
| 854 |
+
return decoded
|
| 855 |
token_ids = [self._tokenizer.convert_tokens_to_ids(tokens) for tokens in hypotheses]
|
| 856 |
+
decoded = [
|
| 857 |
text.strip()
|
| 858 |
for text in self._tokenizer.batch_decode(token_ids, skip_special_tokens=True)
|
| 859 |
]
|
| 860 |
+
self._profile_add("decode_s", time.perf_counter() - start)
|
| 861 |
+
return decoded
|
| 862 |
|
| 863 |
def _load_ct2(self, config: ModelConfig) -> None:
|
| 864 |
model_path = ensure_model_files(config, Backend.CT2)
|
|
|
|
| 1082 |
) -> list[str]:
|
| 1083 |
config = MODELS[self._model_key]
|
| 1084 |
if source_batches is None:
|
| 1085 |
+
tokenize_start = time.perf_counter()
|
| 1086 |
source_batches = self._tokenize_chunks_parallel(chunks)
|
| 1087 |
+
self._profile_add("tokenize_s", time.perf_counter() - tokenize_start)
|
| 1088 |
+
infer_start = time.perf_counter()
|
| 1089 |
results = self._ct2_model.translate_batch(
|
| 1090 |
source_batches,
|
| 1091 |
max_batch_size=self._ct2_max_batch_size(config),
|
|
|
|
| 1094 |
max_decoding_length=config.ct2_max_output_tokens,
|
| 1095 |
**self._ct2_repetition_kwargs(config),
|
| 1096 |
)
|
| 1097 |
+
self._profile_add("ct2_infer_s", time.perf_counter() - infer_start)
|
| 1098 |
return self._decode_ct2_results(results)
|
| 1099 |
|
| 1100 |
def _translate_ct2_batch_pipelined(
|
|
|
|
| 1105 |
prefetched_tokens: SourceTokenJobs | None,
|
| 1106 |
) -> list[str]:
|
| 1107 |
if prefetched_tokens is not None:
|
| 1108 |
+
wait_start = time.perf_counter()
|
| 1109 |
source_batches = self._collect_tokenize_jobs(prefetched_tokens)
|
| 1110 |
+
self._profile_add("tokenize_wait_s", time.perf_counter() - wait_start)
|
| 1111 |
else:
|
| 1112 |
source_batches = None
|
| 1113 |
return self._translate_ct2_batch(
|
|
|
|
| 1134 |
) -> Iterator[tuple[int, int, str, list[tuple[int, str, str]] | None, str | None]]:
|
| 1135 |
"""Yield (done, total, message, rows_or_none, full_text_or_none) sau mỗi batch."""
|
| 1136 |
beam_size = self.clamp_beam(beam_size)
|
| 1137 |
+
self._reset_profile()
|
| 1138 |
+
self._profile_set("backend", self._backend.value if self._backend else "")
|
| 1139 |
+
self._profile_set("beam", beam_size)
|
| 1140 |
+
chunk_start = time.perf_counter()
|
| 1141 |
chunks = self._chunk_text(text, chunk_mode)
|
| 1142 |
+
self._profile_set("chunk_s", time.perf_counter() - chunk_start)
|
| 1143 |
total = len(chunks)
|
| 1144 |
+
self._profile_set("chunks", total)
|
| 1145 |
|
| 1146 |
yield 0, total, f"Đã chia {total} chunk, chuẩn bị dịch...", None, None
|
| 1147 |
|
|
|
|
| 1151 |
next_tokens: SourceTokenJobs | None = None
|
| 1152 |
if self._backend == Backend.CT2 and total:
|
| 1153 |
first_end = min(window_size, total)
|
| 1154 |
+
submit_start = time.perf_counter()
|
| 1155 |
next_tokens = self._submit_tokenize_jobs(chunks[:first_end])
|
| 1156 |
+
self._profile_add("tokenize_submit_s", time.perf_counter() - submit_start)
|
| 1157 |
|
| 1158 |
for start in range(0, total, window_size):
|
| 1159 |
end = min(start + window_size, total)
|
|
|
|
| 1183 |
next_end = min(next_start + window_size, total)
|
| 1184 |
if next_start < total:
|
| 1185 |
next_batch = chunks[next_start:next_end]
|
| 1186 |
+
submit_start = time.perf_counter()
|
| 1187 |
next_tokens = self._submit_tokenize_jobs(next_batch)
|
| 1188 |
+
self._profile_add("tokenize_submit_s", time.perf_counter() - submit_start)
|
| 1189 |
|
| 1190 |
translations.extend(
|
| 1191 |
self._translate_ct2_batch_pipelined(
|
|
|
|
| 1199 |
|
| 1200 |
yield end, total, f"Đã xong {end}/{total} chunk", None, None
|
| 1201 |
|
| 1202 |
+
assemble_start = time.perf_counter()
|
| 1203 |
rows = [
|
| 1204 |
(index, chunk, translated)
|
| 1205 |
for index, (chunk, translated) in enumerate(zip(chunks, translations), start=1)
|
| 1206 |
]
|
| 1207 |
full_text = "\n".join(translations)
|
| 1208 |
+
self._profile_add("assemble_s", time.perf_counter() - assemble_start)
|
| 1209 |
+
cache_stats = getattr(self._tokenizer, "cache_stats", None)
|
| 1210 |
+
if callable(cache_stats):
|
| 1211 |
+
for key, value in cache_stats().items():
|
| 1212 |
+
self._profile_set(key, value)
|
| 1213 |
yield total, total, "Hoàn tất dịch.", rows, full_text
|
| 1214 |
|
| 1215 |
def translate_text(
|