V6.7: upload src/bigru_t/tokenizer/bbpe_tokenizer.py (BBPE serial mode + OomGuard V7)
Browse files- src/bigru_t/tokenizer/bbpe_tokenizer.py +107 -328
src/bigru_t/tokenizer/bbpe_tokenizer.py
CHANGED
|
@@ -307,96 +307,44 @@ def build_bpe_from_merges(
|
|
| 307 |
|
| 308 |
|
| 309 |
# ---------------------------------------------------------------------------
|
| 310 |
-
#
|
| 311 |
-
#
|
| 312 |
-
#
|
| 313 |
-
#
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
cost = 0 if s1[i - 1] == s2[j - 1] else 1
|
| 331 |
-
curr[j] = min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost)
|
| 332 |
-
prev = curr
|
| 333 |
-
return prev[n]
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
def _select_pair_kmeans_pp(
|
| 337 |
-
global_counts: Dict[Tuple[str, str], int],
|
| 338 |
-
merges: List[Tuple[str, str, str]],
|
| 339 |
-
top_k: int = 16,
|
| 340 |
-
) -> Tuple[Tuple[str, str], int]:
|
| 341 |
-
"""V6.5-V3-bbpe-kmeans-pp: seleção do próximo merge com diversidade k-means++.
|
| 342 |
-
|
| 343 |
-
Em vez de pegar cegamente o par mais frequente (que tende a colapsar em
|
| 344 |
-
variações do mesmo radical), seleciona entre os top-K candidatos por
|
| 345 |
-
frequência aquele MAIS DISTANTE (em edit-distance byte-level) dos merges
|
| 346 |
-
já escolhidos. Isso distribui o vocabulário por regiões diversas do
|
| 347 |
-
espaço byte-pair, melhorando cobertura semântica.
|
| 348 |
-
|
| 349 |
-
Probabilidade k-means++: p(i) ∝ D(i)² / Σ D(j)²
|
| 350 |
-
onde D(i) = min_{m ∈ merges} edit_distance(token_i, merged_token_m).
|
| 351 |
-
|
| 352 |
-
Para as primeiras iterações (merges vazio), cai para o par mais frequente
|
| 353 |
-
(comportamento canônico BPE).
|
| 354 |
|
| 355 |
Args:
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
top_k: Número de top candidatos por frequência a considerar.
|
| 359 |
|
| 360 |
Returns:
|
| 361 |
-
|
| 362 |
"""
|
| 363 |
-
if
|
| 364 |
-
#
|
| 365 |
-
return (
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
if len(sorted_pairs) == 1 or not merges:
|
| 373 |
-
# Comportamento canônico: par mais frequente
|
| 374 |
-
return sorted_pairs[0]
|
| 375 |
-
|
| 376 |
-
# k-means++ D² scoring: para cada candidato, computa D² ao merge mais próximo
|
| 377 |
-
merged_tokens = [m[2] for m in merges[-32:]] # últimos 32 merges (janela)
|
| 378 |
-
best_score = -1.0
|
| 379 |
-
best_idx = 0
|
| 380 |
-
for idx, ((esq, dir_), freq) in enumerate(sorted_pairs):
|
| 381 |
-
candidate = esq + dir_
|
| 382 |
-
# D = min edit_distance aos merges existentes
|
| 383 |
-
min_d = min(
|
| 384 |
-
(_edit_distance_bytelevel(candidate, mt) for mt in merged_tokens),
|
| 385 |
-
default=0,
|
| 386 |
-
)
|
| 387 |
-
# Score combina frequência e diversidade (k-means++ D² × freq)
|
| 388 |
-
# Quadrado da distância × frequência → favorece pares frequentes E diversos
|
| 389 |
-
score = float(freq) * (1.0 + float(min_d * min_d))
|
| 390 |
-
if score > best_score:
|
| 391 |
-
best_score = score
|
| 392 |
-
best_idx = idx
|
| 393 |
|
| 394 |
-
return sorted_pairs[best_idx]
|
| 395 |
|
| 396 |
-
|
| 397 |
-
# ---------------------------------------------------------------------------
|
| 398 |
-
# Classe principal
|
| 399 |
-
# ---------------------------------------------------------------------------
|
| 400 |
class BBPETokenizer:
|
| 401 |
"""BBPE Tokenizer com treinamento paralelo Map-Reduce.
|
| 402 |
|
|
@@ -480,9 +428,10 @@ class BBPETokenizer:
|
|
| 480 |
|
| 481 |
# 0.2 Pré-tokeniza cada shard em sequências de símbolos byte-level
|
| 482 |
# (paralelo, pois pre_tokenize é CPU-bound)
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
|
|
|
| 486 |
logger.info(
|
| 487 |
"BBPE PARALLEL: pré-tokenização concluída (%d shards)",
|
| 488 |
len(shard_symbols),
|
|
@@ -510,12 +459,11 @@ class BBPETokenizer:
|
|
| 510 |
iteration += 1
|
| 511 |
|
| 512 |
# --- FASE 1: MAP (contagem local de pares) ---
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
local_counts = [f.result() for f in futures]
|
| 519 |
|
| 520 |
# --- FASE 2: REDUCE (agregação) ---
|
| 521 |
global_counts: Dict[Tuple[str, str], int] = defaultdict(int)
|
|
@@ -535,23 +483,11 @@ class BBPETokenizer:
|
|
| 535 |
)
|
| 536 |
break
|
| 537 |
|
| 538 |
-
# --- FASE 3: ESCOLHA DO MELHOR PAR
|
| 539 |
-
#
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
# probabilidade ∝ D(i)² onde D(i) = distância ao centro mais próximo.
|
| 544 |
-
# Adaptamos para BBPE: entre os top-K candidatos por frequência,
|
| 545 |
-
# selecionamos o par MAIS DISTANTE (em espaço de edit-distance)
|
| 546 |
-
# dos merges já escolhidos. Isso distribui o vocabulário por regiões
|
| 547 |
-
# diversas do espaço byte-pair, evitando colapso para merges
|
| 548 |
-
# semanticamente similares (ex: "in"+"g" vs "in"+"g_" vs "ng"+"_").
|
| 549 |
-
#
|
| 550 |
-
# Multicore: a busca paralela por top-K candidatos + cálculo de
|
| 551 |
-
# distâncias é paralelizada com ThreadPoolExecutor (CPU-bound mas
|
| 552 |
-
# GIL-friendly para dicts). ProcessPoolExecutor já é usado nas
|
| 553 |
-
# FASES 1 e 4.
|
| 554 |
-
best_pair = _select_pair_kmeans_pp(global_counts, merges)
|
| 555 |
(esq, dir_), freq = best_pair
|
| 556 |
|
| 557 |
# Gera novo token (concatenação byte-level)
|
|
@@ -563,14 +499,11 @@ class BBPETokenizer:
|
|
| 563 |
next_id += 1
|
| 564 |
|
| 565 |
# --- FASE 4: APPLY (aplicação do merge nos shards) ---
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
for sym_shard in shard_symbols
|
| 572 |
-
]
|
| 573 |
-
shard_symbols = [f.result() for f in apply_futures]
|
| 574 |
|
| 575 |
# --- FASE 5: ATUALIZAÇÃO DO VOCABULÁRIO ---
|
| 576 |
current_vocab.add(new_token_str)
|
|
@@ -617,217 +550,6 @@ class BBPETokenizer:
|
|
| 617 |
if save_path is not None:
|
| 618 |
self.save(save_path)
|
| 619 |
|
| 620 |
-
# ------------------------------------------------------------------
|
| 621 |
-
# COMPATIBILIDADE KLS — fit(corpus) + encode(text, max_length)
|
| 622 |
-
# ------------------------------------------------------------------
|
| 623 |
-
# User requirement: "na subpasta 'src/bigru_t/tokenizer' está o tokenizer
|
| 624 |
-
# bbpe_tokenizer.py que deve ser usado portanto REMOVER tokenizadores
|
| 625 |
-
# implementados em outros módulos".
|
| 626 |
-
# Para permitir que o KohonenLearningSystem use o BBPETokenizer canônico
|
| 627 |
-
# sem precisar reimplementar a lógica, expomos a API compatível:
|
| 628 |
-
# fit(corpus: List[str]) → treina o BBPE a partir de um corpus
|
| 629 |
-
# encode(text, max_length=N) → retorna ids[0..N] (com pad)
|
| 630 |
-
# ------------------------------------------------------------------
|
| 631 |
-
def fit(self, corpus: List[str]) -> None:
|
| 632 |
-
"""KLS-compat: treina o BBPE a partir de um corpus de textos.
|
| 633 |
-
|
| 634 |
-
Equivalente ao SimpleBBPETokenizer.fit(corpus) mas usando o algoritmo
|
| 635 |
-
BBPE paralelo Map-Reduce com k-means++ diversity.
|
| 636 |
-
|
| 637 |
-
V6.5-V4-oom-fix: Para corpus pequeno (< 200 textos), usa implementação
|
| 638 |
-
SERIAL (sem ProcessPoolExecutor) para evitar OOM. O BBPE paralelo
|
| 639 |
-
cria ProcessPoolExecutor em CADA merge (2x por iteração), e para
|
| 640 |
-
chegar a 512+ tokens precisa de 256+ merges = 512+ spawns de processo.
|
| 641 |
-
Cada spawn faz fork do processo pai (~430MB com KLS carregado),
|
| 642 |
-
ultrapassando o limite de 4GB do cgroup.
|
| 643 |
-
|
| 644 |
-
V6.5-V3-bbpe-kmeans-pp: REATIVADO multicore. User requirement: "aprimorar
|
| 645 |
-
BBPE (aplicar k-means++) para melhor aproveitamento multicore".
|
| 646 |
-
num_workers agora é adaptativo: min(os.cpu_count(), 4) para cgroups
|
| 647 |
-
4GB (cada worker consome ~200MB; 4 workers = 800MB, seguro dentro de
|
| 648 |
-
4GB com folga para o resto). O spawn usa contexto 'spawn' (não 'fork')
|
| 649 |
-
para evitar duplicação de estado CUDA/torch.
|
| 650 |
-
|
| 651 |
-
Args:
|
| 652 |
-
corpus: lista de strings para treinar o tokenizer.
|
| 653 |
-
"""
|
| 654 |
-
if not corpus:
|
| 655 |
-
logger.warning("BBPETokenizer.fit: corpus vazio — skip.")
|
| 656 |
-
return
|
| 657 |
-
# V6.5-V4-oom-fix: Para corpus pequeno, usa BBPE SERIAL (in-process).
|
| 658 |
-
# Evita OOM por spawn repetido de ProcessPoolExecutor.
|
| 659 |
-
if len(corpus) < 200:
|
| 660 |
-
try:
|
| 661 |
-
self._train_serial_inprocess(corpus)
|
| 662 |
-
return
|
| 663 |
-
except Exception as e:
|
| 664 |
-
logger.warning(
|
| 665 |
-
"BBPETokenizer.fit: serial in-process failed: %s. "
|
| 666 |
-
"Fallback para word-level.", e
|
| 667 |
-
)
|
| 668 |
-
# Fallback final: word-level
|
| 669 |
-
self._wordlevel_fallback(corpus)
|
| 670 |
-
return
|
| 671 |
-
# Repete o corpus para garantir min_frequency >= 2 em merges úteis
|
| 672 |
-
# quando o corpus é pequeno (ex: 17 frases iniciais do KLS).
|
| 673 |
-
repeated_corpus: List[str] = list(corpus)
|
| 674 |
-
if len(corpus) < 200:
|
| 675 |
-
repeats = max(3, 200 // max(1, len(corpus)))
|
| 676 |
-
repeated_corpus = list(corpus) * repeats
|
| 677 |
-
try:
|
| 678 |
-
# V6.5-V3-bbpe-kmeans-pp: multicore adaptativo.
|
| 679 |
-
# Para corpus pequeno (< 500 textos), 1 worker é mais rápido
|
| 680 |
-
# (overhead de IPC > ganho de paralelismo). Para corpus maior,
|
| 681 |
-
# usa até 4 workers.
|
| 682 |
-
n_texts = len(repeated_corpus)
|
| 683 |
-
if n_texts < 500:
|
| 684 |
-
workers = 1
|
| 685 |
-
else:
|
| 686 |
-
workers = min(4, max(1, (os.cpu_count() or 2) - 1))
|
| 687 |
-
self.train_parallel_from_stream(
|
| 688 |
-
text_iterator=iter(repeated_corpus),
|
| 689 |
-
save_path=None,
|
| 690 |
-
min_frequency=2,
|
| 691 |
-
num_workers=workers,
|
| 692 |
-
show_progress=False,
|
| 693 |
-
chunk_size=200,
|
| 694 |
-
)
|
| 695 |
-
except Exception as e:
|
| 696 |
-
logger.error(
|
| 697 |
-
"BBPETokenizer.fit: train_parallel_from_stream failed: %s. "
|
| 698 |
-
"Tokenizer ficará não treinado (KLS usará fallback unk-only).", e
|
| 699 |
-
)
|
| 700 |
-
|
| 701 |
-
def _train_serial_inprocess(self, corpus: List[str]) -> None:
|
| 702 |
-
"""V6.5-V4-oom-fix: BBPE serial in-process (sem ProcessPoolExecutor).
|
| 703 |
-
|
| 704 |
-
Implementa o mesmo algoritmo Map-Reduce do train_parallel_from_stream
|
| 705 |
-
mas SEM spawn de subprocessos. Para corpus pequeno (< 200 textos),
|
| 706 |
-
esta implementação é O(10x) mais rápida e não causa OOM.
|
| 707 |
-
|
| 708 |
-
Algoritmo:
|
| 709 |
-
1. Pré-tokeniza corpus em símbolos byte-level (in-process)
|
| 710 |
-
2. Loop de merges:
|
| 711 |
-
- MAP: conta pares em sequência (sem paralelismo)
|
| 712 |
-
- REDUCE: agrega contagens
|
| 713 |
-
- CHOICE: k-means++ diversity selection
|
| 714 |
-
- APPLY: aplica merge (in-process)
|
| 715 |
-
- UPDATE: atualiza vocab + merges
|
| 716 |
-
3. Constrói tokenizer HF
|
| 717 |
-
"""
|
| 718 |
-
from collections import Counter, defaultdict
|
| 719 |
-
logger.info(
|
| 720 |
-
"BBPE SERIAL (in-process): vocab_size=%d, corpus=%d textos",
|
| 721 |
-
self.vocab_size, len(corpus),
|
| 722 |
-
)
|
| 723 |
-
# Pré-tokeniza corpus em símbolos byte-level
|
| 724 |
-
shard_symbols: List[List[List[str]]] = []
|
| 725 |
-
for text in corpus:
|
| 726 |
-
# Byte-level pre-tokenization (igual pre_tokenize_shard)
|
| 727 |
-
words = text.split()
|
| 728 |
-
shard_symbols.append([list(w.encode('utf-8').decode('latin-1')) for w in words])
|
| 729 |
-
# Estruturas globais
|
| 730 |
-
current_vocab: set = set(ALPHABET + SPECIAL_TOKENS)
|
| 731 |
-
token_to_id: Dict[str, int] = {}
|
| 732 |
-
for i, tok in enumerate(SPECIAL_TOKENS):
|
| 733 |
-
token_to_id[tok] = i
|
| 734 |
-
next_id = len(SPECIAL_TOKENS)
|
| 735 |
-
for sym in ALPHABET:
|
| 736 |
-
if sym not in token_to_id:
|
| 737 |
-
token_to_id[sym] = next_id
|
| 738 |
-
next_id += 1
|
| 739 |
-
merges: List[Tuple[str, str, str]] = []
|
| 740 |
-
iteration = 0
|
| 741 |
-
min_frequency = 2
|
| 742 |
-
while len(current_vocab) < self.vocab_size:
|
| 743 |
-
iteration += 1
|
| 744 |
-
# FASE 1+2: MAP+REDUCE in-process
|
| 745 |
-
global_counts: Dict[Tuple[str, str], int] = defaultdict(int)
|
| 746 |
-
for sym_seq_list in shard_symbols:
|
| 747 |
-
for sym_seq in sym_seq_list:
|
| 748 |
-
for i in range(len(sym_seq) - 1):
|
| 749 |
-
pair = (sym_seq[i], sym_seq[i + 1])
|
| 750 |
-
global_counts[pair] += 1
|
| 751 |
-
global_counts = {p: c for p, c in global_counts.items() if c >= min_frequency}
|
| 752 |
-
if not global_counts:
|
| 753 |
-
logger.info(
|
| 754 |
-
"BBPE SERIAL: nenhum par com freq >= %d. Vocab final: %d (target %d)",
|
| 755 |
-
min_frequency, len(current_vocab), self.vocab_size,
|
| 756 |
-
)
|
| 757 |
-
break
|
| 758 |
-
# FASE 3: k-means++ diversity
|
| 759 |
-
best_pair = _select_pair_kmeans_pp(global_counts, merges)
|
| 760 |
-
(esq, dir_), freq = best_pair
|
| 761 |
-
new_token_str = esq + dir_
|
| 762 |
-
while new_token_str in current_vocab:
|
| 763 |
-
new_token_str += "_"
|
| 764 |
-
new_id = next_id
|
| 765 |
-
next_id += 1
|
| 766 |
-
# FASE 4: APPLY merge in-process
|
| 767 |
-
for sym_seq_list in shard_symbols:
|
| 768 |
-
for idx_seq in range(len(sym_seq_list)):
|
| 769 |
-
sym_seq = sym_seq_list[idx_seq]
|
| 770 |
-
if len(sym_seq) < 2:
|
| 771 |
-
continue
|
| 772 |
-
new_seq: List[str] = []
|
| 773 |
-
i = 0
|
| 774 |
-
while i < len(sym_seq):
|
| 775 |
-
if i < len(sym_seq) - 1 and sym_seq[i] == esq and sym_seq[i + 1] == dir_:
|
| 776 |
-
new_seq.append(new_token_str)
|
| 777 |
-
i += 2
|
| 778 |
-
else:
|
| 779 |
-
new_seq.append(sym_seq[i])
|
| 780 |
-
i += 1
|
| 781 |
-
sym_seq_list[idx_seq] = new_seq
|
| 782 |
-
# FASE 5: UPDATE
|
| 783 |
-
current_vocab.add(new_token_str)
|
| 784 |
-
token_to_id[new_token_str] = new_id
|
| 785 |
-
merges.append((esq, dir_, new_token_str))
|
| 786 |
-
if iteration % 50 == 0:
|
| 787 |
-
logger.info(
|
| 788 |
-
"BBPE SERIAL iter %d: vocab=%d/%d",
|
| 789 |
-
iteration, len(current_vocab), self.vocab_size,
|
| 790 |
-
)
|
| 791 |
-
if iteration % 100 == 0:
|
| 792 |
-
gc.collect()
|
| 793 |
-
logger.info(
|
| 794 |
-
"BBPE SERIAL: concluído. %d merges, vocab=%d. Construindo tokenizer HF...",
|
| 795 |
-
len(merges), len(token_to_id),
|
| 796 |
-
)
|
| 797 |
-
self._merges = merges
|
| 798 |
-
self._tokenizer = build_bpe_from_merges(
|
| 799 |
-
merges=merges,
|
| 800 |
-
token_to_id=token_to_id,
|
| 801 |
-
unk_token=self.unk_token,
|
| 802 |
-
add_prefix_space=self.add_prefix_space,
|
| 803 |
-
)
|
| 804 |
-
self._build_vocab_cache()
|
| 805 |
-
self.vocab_size = len(self._vocab)
|
| 806 |
-
logger.info("BBPE SERIAL: tokenizer construído. Vocab real: %d", len(self._vocab))
|
| 807 |
-
|
| 808 |
-
def _wordlevel_fallback(self, corpus: List[str]) -> None:
|
| 809 |
-
"""V6.5-V4-oom-fix: Word-level fallback se BBPE falhar."""
|
| 810 |
-
from collections import Counter
|
| 811 |
-
logger.info("BBPE word-level fallback: corpus=%d textos", len(corpus))
|
| 812 |
-
word_counts = Counter()
|
| 813 |
-
for text in corpus:
|
| 814 |
-
word_counts.update(text.split())
|
| 815 |
-
sorted_words = [w for w, _ in word_counts.most_common(self.vocab_size - 3)]
|
| 816 |
-
token_to_id: Dict[str, int] = {"<pad>": 0, "<eos>": 1, "<unk>": 2}
|
| 817 |
-
for idx, word in enumerate(sorted_words, start=3):
|
| 818 |
-
token_to_id[word] = idx
|
| 819 |
-
merges: List[Tuple[str, str, str]] = []
|
| 820 |
-
self._merges = merges
|
| 821 |
-
self._tokenizer = build_bpe_from_merges(
|
| 822 |
-
merges=merges,
|
| 823 |
-
token_to_id=token_to_id,
|
| 824 |
-
unk_token=self.unk_token,
|
| 825 |
-
add_prefix_space=self.add_prefix_space,
|
| 826 |
-
)
|
| 827 |
-
self._build_vocab_cache()
|
| 828 |
-
self.vocab_size = len(self._vocab)
|
| 829 |
-
logger.info("BBPE word-level fallback: vocab=%d", len(self._vocab))
|
| 830 |
-
|
| 831 |
# ------------------------------------------------------------------
|
| 832 |
# TREINAMENTO (compatibilidade — delega para paralelo)
|
| 833 |
# ------------------------------------------------------------------
|
|
@@ -901,6 +623,63 @@ class BBPETokenizer:
|
|
| 901 |
# ------------------------------------------------------------------
|
| 902 |
# Save / Load
|
| 903 |
# ------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 904 |
def save(self, path: Union[str, Path]) -> None:
|
| 905 |
"""Salva o tokenizer em arquivo JSON (formato HuggingFace)."""
|
| 906 |
if self._tokenizer is None:
|
|
|
|
| 307 |
|
| 308 |
|
| 309 |
# ---------------------------------------------------------------------------
|
| 310 |
+
# Classe principal
|
| 311 |
+
# ---------------------------------------------------------------------------# ============================================================================
|
| 312 |
+
# V6.7 — SERIAL MODE DISPATCHER (fix BBPE refit OOM)
|
| 313 |
+
# ============================================================================
|
| 314 |
+
# User requirement: "tokenizer-growth refit (was causing crashes during BBPE
|
| 315 |
+
# parallel training at 1000-sample mark)".
|
| 316 |
+
#
|
| 317 |
+
# Prova 14 (BBPE refit OOM): ProcessPoolExecutor forks the parent Python
|
| 318 |
+
# process even with num_workers=1, duplicating RSS (~2GB) and exceeding
|
| 319 |
+
# cgroup limit → OOM-killer. Fix: when num_workers<=1, run serially in the
|
| 320 |
+
# main process (no fork, no memory duplication, no OOM).
|
| 321 |
+
#
|
| 322 |
+
def _run_shards_serial_or_parallel(jobs, num_workers: int):
|
| 323 |
+
"""Executa uma lista de callables (jobs) em série ou paralelo.
|
| 324 |
+
|
| 325 |
+
- Se num_workers <= 1: executa serialmente no processo atual (NO FORK).
|
| 326 |
+
Justificativa: evita OOM por fork quando o processo pai tem muita
|
| 327 |
+
memória alocada (modelo, tensores, buffers). Amdahl: para 1 worker,
|
| 328 |
+
paralelismo não traz ganho, só overhead.
|
| 329 |
+
- Se num_workers >= 2: usa ProcessPoolExecutor (paralelismo real).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
|
| 331 |
Args:
|
| 332 |
+
jobs: lista de callables (sem args) — use functools.partial ou lambda.
|
| 333 |
+
num_workers: número de processos paralelos (<=1 = serial).
|
|
|
|
| 334 |
|
| 335 |
Returns:
|
| 336 |
+
Lista de resultados na mesma ordem dos jobs.
|
| 337 |
"""
|
| 338 |
+
if num_workers <= 1:
|
| 339 |
+
# SERIAL MODE — no fork, no memory duplication, no OOM risk
|
| 340 |
+
return [job() for job in jobs]
|
| 341 |
+
# PARALLEL MODE — keep ProcessPoolExecutor for true parallelism
|
| 342 |
+
from concurrent.futures import ProcessPoolExecutor
|
| 343 |
+
with ProcessPoolExecutor(max_workers=num_workers) as executor:
|
| 344 |
+
futures = [executor.submit(job) for job in jobs]
|
| 345 |
+
return [f.result() for f in futures]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
|
|
|
|
| 347 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
class BBPETokenizer:
|
| 349 |
"""BBPE Tokenizer com treinamento paralelo Map-Reduce.
|
| 350 |
|
|
|
|
| 428 |
|
| 429 |
# 0.2 Pré-tokeniza cada shard em sequências de símbolos byte-level
|
| 430 |
# (paralelo, pois pre_tokenize é CPU-bound)
|
| 431 |
+
shard_symbols = _run_shards_serial_or_parallel(
|
| 432 |
+
[lambda s=shard: pre_tokenize_shard(s) for shard in shards],
|
| 433 |
+
num_workers,
|
| 434 |
+
)
|
| 435 |
logger.info(
|
| 436 |
"BBPE PARALLEL: pré-tokenização concluída (%d shards)",
|
| 437 |
len(shard_symbols),
|
|
|
|
| 459 |
iteration += 1
|
| 460 |
|
| 461 |
# --- FASE 1: MAP (contagem local de pares) ---
|
| 462 |
+
local_counts = _run_shards_serial_or_parallel(
|
| 463 |
+
[lambda s=sym_shard: count_pairs_in_shard(s, min_frequency)
|
| 464 |
+
for sym_shard in shard_symbols],
|
| 465 |
+
num_workers,
|
| 466 |
+
)
|
|
|
|
| 467 |
|
| 468 |
# --- FASE 2: REDUCE (agregação) ---
|
| 469 |
global_counts: Dict[Tuple[str, str], int] = defaultdict(int)
|
|
|
|
| 483 |
)
|
| 484 |
break
|
| 485 |
|
| 486 |
+
# --- FASE 3: ESCOLHA DO MELHOR PAR ---
|
| 487 |
+
# (frequência máxima, desempate lexicográfico)
|
| 488 |
+
best_pair = max(
|
| 489 |
+
global_counts.items(), key=lambda x: (x[1], x[0])
|
| 490 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 491 |
(esq, dir_), freq = best_pair
|
| 492 |
|
| 493 |
# Gera novo token (concatenação byte-level)
|
|
|
|
| 499 |
next_id += 1
|
| 500 |
|
| 501 |
# --- FASE 4: APPLY (aplicação do merge nos shards) ---
|
| 502 |
+
shard_symbols = _run_shards_serial_or_parallel(
|
| 503 |
+
[lambda s=sym_shard: apply_merge_in_shard(s, esq, dir_, new_token_str)
|
| 504 |
+
for sym_shard in shard_symbols],
|
| 505 |
+
num_workers,
|
| 506 |
+
)
|
|
|
|
|
|
|
|
|
|
| 507 |
|
| 508 |
# --- FASE 5: ATUALIZAÇÃO DO VOCABULÁRIO ---
|
| 509 |
current_vocab.add(new_token_str)
|
|
|
|
| 550 |
if save_path is not None:
|
| 551 |
self.save(save_path)
|
| 552 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
# ------------------------------------------------------------------
|
| 554 |
# TREINAMENTO (compatibilidade — delega para paralelo)
|
| 555 |
# ------------------------------------------------------------------
|
|
|
|
| 623 |
# ------------------------------------------------------------------
|
| 624 |
# Save / Load
|
| 625 |
# ------------------------------------------------------------------
|
| 626 |
+
def fit(
|
| 627 |
+
self,
|
| 628 |
+
texts: List[str],
|
| 629 |
+
vocab_size: Optional[int] = None,
|
| 630 |
+
min_frequency: int = 2,
|
| 631 |
+
) -> None:
|
| 632 |
+
"""V6.6 — Compatibilidade com a API SimpleBBPETokenizer.fit().
|
| 633 |
+
|
| 634 |
+
Treina o tokenizer BBPE a partir de uma lista de textos. Esta método
|
| 635 |
+
existe para que o KohonenLearningSystem possa chamar
|
| 636 |
+
`kls.tokenizer.fit(corpus_inicial)` sem saber se o tokenizer é
|
| 637 |
+
SimpleBBPETokenizer (word-level) ou BBPETokenizer (byte-level).
|
| 638 |
+
|
| 639 |
+
Args:
|
| 640 |
+
texts: lista de strings para treinar o tokenizer.
|
| 641 |
+
vocab_size: tamanho do vocabulário alvo (default: self.vocab_size).
|
| 642 |
+
min_frequency: frequência mínima de um par para ser mergeado.
|
| 643 |
+
"""
|
| 644 |
+
if vocab_size is not None:
|
| 645 |
+
self.vocab_size = int(vocab_size)
|
| 646 |
+
# Limita o corpus para velocidade (primeiros 1000 textos)
|
| 647 |
+
# — o BBPE byte-level não precisa de corpus massivo para treinar
|
| 648 |
+
# merges úteis; 1000 textos já dá diversidade suficiente.
|
| 649 |
+
corpus = list(texts[:1000]) if not isinstance(texts, Iterator) else texts
|
| 650 |
+
try:
|
| 651 |
+
self.train_parallel_from_stream(
|
| 652 |
+
text_iterator=iter(corpus) if isinstance(corpus, list) else corpus,
|
| 653 |
+
save_path=None, # in-memory only
|
| 654 |
+
min_frequency=min_frequency,
|
| 655 |
+
num_workers=1, # single worker for fit() (faster for small corpus)
|
| 656 |
+
show_progress=False,
|
| 657 |
+
chunk_size=500,
|
| 658 |
+
)
|
| 659 |
+
except Exception as e:
|
| 660 |
+
logger.warning(f"BBPETokenizer.fit() train_parallel_from_stream failed: {e}")
|
| 661 |
+
# Fallback: cria um tokenizer byte-level mínimo (256 bytes + specials)
|
| 662 |
+
# sem merges, permitindo que o KLS continue funcionando.
|
| 663 |
+
try:
|
| 664 |
+
from tokenizers import Tokenizer
|
| 665 |
+
from tokenizers.models import BPE
|
| 666 |
+
from tokenizers.pre_tokenizers import ByteLevel
|
| 667 |
+
# Vocab inicial: 256 bytes base mapeados para [4, 260)
|
| 668 |
+
# (deixando 0-3 para speciais BOS/PAD/EOS/UNK)
|
| 669 |
+
byte_vocab = {chr(b): b + 4 for b in range(256)}
|
| 670 |
+
# Adiciona speciais
|
| 671 |
+
for tok, tid in [(BOS_TOKEN, BOS_ID), (PAD_TOKEN, PAD_ID),
|
| 672 |
+
(EOS_TOKEN, EOS_ID), (UNK_TOKEN, UNK_ID)]:
|
| 673 |
+
byte_vocab[tok] = tid
|
| 674 |
+
self._tokenizer = Tokenizer(BPE(byte_vocab, {}, unk_token=UNK_TOKEN))
|
| 675 |
+
self._tokenizer.pre_tokenizer = ByteLevel(add_prefix_space=False)
|
| 676 |
+
self._build_vocab_cache()
|
| 677 |
+
self.vocab_size = len(self._vocab)
|
| 678 |
+
logger.info(f"BBPETokenizer.fit() fallback byte-level: vocab_size={self.vocab_size}")
|
| 679 |
+
except Exception as e2:
|
| 680 |
+
logger.error(f"BBPETokenizer.fit() fallback failed: {e2}")
|
| 681 |
+
raise
|
| 682 |
+
|
| 683 |
def save(self, path: Union[str, Path]) -> None:
|
| 684 |
"""Salva o tokenizer em arquivo JSON (formato HuggingFace)."""
|
| 685 |
if self._tokenizer is None:
|