PowerMachine commited on
Commit
2ff82eb
·
verified ·
1 Parent(s): 6de7d47

V6.7: upload scripts/fix_bbpe_refit_oom.py (19.6KB) — FASE1+FASE2 training results

Browse files
Files changed (1) hide show
  1. scripts/fix_bbpe_refit_oom.py +446 -0
scripts/fix_bbpe_refit_oom.py ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ fix_bbpe_refit_oom.py — V6.7 Fix: BBPE tokenizer-growth refit crash.
4
+
5
+ User requirement (latest message):
6
+ "processar aprimoramento (matemático e lógico) para resolver:
7
+ tokenizer-growth refit (was causing crashes during BBPE parallel
8
+ training at 1000-sample mark)"
9
+
10
+ ROOT CAUSE (Prova 14 — BBPE refit OOM):
11
+ train_parallel_from_stream() usa ProcessPoolExecutor mesmo quando
12
+ num_workers=1. O fork do processo Python duplica TODA a memória
13
+ do processo pai (modelo, tensores, buffers) → quando o processo
14
+ pai está em ~2GB RSS, o fork cria outro processo de ~2GB →
15
+ excede cgroup limit (typical 3-4GB) → OOM-killer mata o processo.
16
+
17
+ A cronologia é:
18
+ 1. Treino roda ~1000 amostras (RSS cresce para ~2GB)
19
+ 2. TOKENIZER_REFIT_INTERVAL_SAMPLES atingido
20
+ 3. kls.tokenizer.fit(corpus) chamado
21
+ 4. fit() chama train_parallel_from_stream(num_workers=1)
22
+ 5. train_parallel_from_stream abre ProcessPoolExecutor(max_workers=1)
23
+ 6. ProcessPoolExecutor forks processo Python (fork() duplica RSS)
24
+ 7. cgroup memory.Usage exceeds limit
25
+ 8. OOM-killer mata processo (dmesg: "Killed process ... oom_score")
26
+
27
+ FIX (3 partes):
28
+
29
+ A) bypass_parallel_when_single_worker:
30
+ Em train_parallel_from_stream, quando num_workers<=1, executar as
31
+ fases MAP/APPLY serialmente no processo principal (sem fork).
32
+ Justificativa matemática:
33
+ - Para 1 worker, paralelismo não traz ganho (Amdahl: S ≤ 1/(1-p))
34
+ - Elimina fork overhead (copia page table, ~50ms para 2GB RSS)
35
+ - Elimina duplicação de memória (COW pages podem ser escritas)
36
+ - Elimina OOM por cgroup
37
+
38
+ B) memory_guard_before_refit:
39
+ Em train_v6_5_v2.py, antes de chamar kls.tokenizer.fit(corpus):
40
+ - Verificar RSS atual via OomGuard
41
+ - Verificar tamanho do corpus (cap em 2000 textos, ~2MB string)
42
+ - Verificar available system memory via /proc/meminfo
43
+ - Se RSS > 70% do cgroup limit, fazer gc.collect() + torch.cpu.empty_cache()
44
+ - Se RSS > 85% do cgroup limit, SKIP refit (non-fatal)
45
+ - Log de decisão (refit_done / refit_skipped / refit_fallback)
46
+
47
+ C) reenable_refit_interval:
48
+ Em train_v6_5_v2.py:
49
+ - TOKENIZER_REFIT_INTERVAL_SAMPLES: 10**9 → 1000 (reativa refit)
50
+ - Adicionar TOKENIZER_REFIT_FORCE_SERIAL=True (usa serial mode)
51
+ - Adicionar TOKENIZER_REFIT_MEMORY_GUARD=True (ativa memory guard)
52
+ - Adicionar TOKENIZER_REFIT_CORPUS_CAP=2000 (cap para evitar OOM)
53
+ - Log de crescimento de vocab em cada refit
54
+
55
+ APLICAÇÃO:
56
+ python3 /home/z/my-project/scripts/fix_bbpe_refit_oom.py
57
+
58
+ Este script modifica IN-PLACE:
59
+ - src/bigru_t/tokenizer/bbpe_tokenizer.py (parte A)
60
+ - scripts/train_v6_5_v2.py (parte B e C)
61
+
62
+ Após aplicar, rodar testes de sanidade (test_bbpe_refit_serial)
63
+ antes de reativar FASE1+FASE2.
64
+ """
65
+ from __future__ import annotations
66
+
67
+ import sys
68
+ import re
69
+ from pathlib import Path
70
+
71
+ PROJECT_ROOT = Path("/home/z/my-project/BiGRU_T_version")
72
+ BBPE_PATH = PROJECT_ROOT / "src" / "bigru_t" / "tokenizer" / "bbpe_tokenizer.py"
73
+ TRAIN_PATH = PROJECT_ROOT / "scripts" / "train_v6_5_v2.py"
74
+
75
+
76
+ def fix_part_a_bbpe_serial_mode() -> bool:
77
+ """Parte A: Adiciona bypass de ProcessPoolExecutor quando num_workers<=1.
78
+
79
+ Estratégia:
80
+ - Substitui os 3 blocos `with ProcessPoolExecutor(max_workers=num_workers) as executor:`
81
+ por um helper `_run_shards_serial_or_parallel(jobs, num_workers)` que
82
+ decide dinamicamente.
83
+ - Adiciona helper como método privado da classe BBPETokenizer OU função
84
+ module-level.
85
+ """
86
+ print(f"\n{'='*70}")
87
+ print("PARTE A: bbpe_tokenizer.py — serial mode when num_workers<=1")
88
+ print(f"{'='*70}")
89
+
90
+ text = BBPE_PATH.read_text(encoding="utf-8")
91
+ original = text
92
+
93
+ # A.1: Adiciona helper module-level para dispatch serial/paralelo
94
+ # Inserimos antes da classe BBPETokenizer para que fique acessível.
95
+ helper_code = '''
96
+
97
+ # ============================================================================
98
+ # V6.7 — SERIAL MODE DISPATCHER (fix BBPE refit OOM)
99
+ # ============================================================================
100
+ # User requirement: "tokenizer-growth refit (was causing crashes during BBPE
101
+ # parallel training at 1000-sample mark)".
102
+ #
103
+ # Prova 14 (BBPE refit OOM): ProcessPoolExecutor forks the parent Python
104
+ # process even with num_workers=1, duplicating RSS (~2GB) and exceeding
105
+ # cgroup limit → OOM-killer. Fix: when num_workers<=1, run serially in the
106
+ # main process (no fork, no memory duplication, no OOM).
107
+ #
108
+ def _run_shards_serial_or_parallel(jobs, num_workers: int):
109
+ """Executa uma lista de callables (jobs) em série ou paralelo.
110
+
111
+ - Se num_workers <= 1: executa serialmente no processo atual (NO FORK).
112
+ Justificativa: evita OOM por fork quando o processo pai tem muita
113
+ memória alocada (modelo, tensores, buffers). Amdahl: para 1 worker,
114
+ paralelismo não traz ganho, só overhead.
115
+ - Se num_workers >= 2: usa ProcessPoolExecutor (paralelismo real).
116
+
117
+ Args:
118
+ jobs: lista de callables (sem args) — use functools.partial ou lambda.
119
+ num_workers: número de processos paralelos (<=1 = serial).
120
+
121
+ Returns:
122
+ Lista de resultados na mesma ordem dos jobs.
123
+ """
124
+ if num_workers <= 1:
125
+ # SERIAL MODE — no fork, no memory duplication, no OOM risk
126
+ return [job() for job in jobs]
127
+ # PARALLEL MODE — keep ProcessPoolExecutor for true parallelism
128
+ from concurrent.futures import ProcessPoolExecutor
129
+ with ProcessPoolExecutor(max_workers=num_workers) as executor:
130
+ futures = [executor.submit(job) for job in jobs]
131
+ return [f.result() for f in futures]
132
+ '''
133
+ # Insere antes da definição da classe BBPETokenizer
134
+ # Procura por "class BBPETokenizer:" para inserir antes
135
+ match = re.search(r'(\nclass BBPETokenizer)', text)
136
+ if not match:
137
+ # Alternativa: insere antes de "class BBPETokenizer" sem newline inicial
138
+ match = re.search(r'(class BBPETokenizer)', text)
139
+ if not match:
140
+ print(" ✗ não encontrei 'class BBPETokenizer' para inserir helper antes")
141
+ return False
142
+ insert_pos = match.start(1)
143
+ if "_run_shards_serial_or_parallel" not in text:
144
+ text = text[:insert_pos] + helper_code.lstrip("\n") + "\n" + text[insert_pos:]
145
+ print(" ✓ helper _run_shards_serial_or_parallel adicionado")
146
+
147
+ # A.2: Substitui os 3 blocos ProcessPoolExecutor por chamadas ao helper.
148
+ # Substitui o bloco: with ProcessPoolExecutor(max_workers=num_workers) as executor:
149
+ # futures = [executor.submit(FN, ARG) for ARG in ARGS]
150
+ # results = [f.result() for f in futures]
151
+ # Por: results = _run_shards_serial_or_parallel([lambda a=ARG: FN(a) for ARG in ARGS], num_workers)
152
+
153
+ # Padrão 1: pre_tokenize_shard
154
+ pat1 = re.compile(
155
+ r'with ProcessPoolExecutor\(max_workers=num_workers\) as executor:\s*\n'
156
+ r'\s*futures = \[executor\.submit\(pre_tokenize_shard, shard\) for shard in shards\]\s*\n'
157
+ r'\s*shard_symbols = \[f\.result\(\) for f in futures\]',
158
+ re.MULTILINE
159
+ )
160
+ repl1 = (
161
+ "shard_symbols = _run_shards_serial_or_parallel(\n"
162
+ " [lambda s=shard: pre_tokenize_shard(s) for shard in shards],\n"
163
+ " num_workers,\n"
164
+ " )"
165
+ )
166
+ if pat1.search(text):
167
+ text = pat1.sub(repl1, text)
168
+ print(" ✓ bloco pre_tokenize_shard substituído por helper")
169
+
170
+ # Padrão 2: count_pairs_in_shard
171
+ pat2 = re.compile(
172
+ r'with ProcessPoolExecutor\(max_workers=num_workers\) as executor:\s*\n'
173
+ r'\s*futures = \[\s*\n'
174
+ r'\s*executor\.submit\(count_pairs_in_shard, sym_shard, min_frequency\)\s*\n'
175
+ r'\s*for sym_shard in shard_symbols\s*\n'
176
+ r'\s*\]\s*\n'
177
+ r'\s*local_counts = \[f\.result\(\) for f in futures\]',
178
+ re.MULTILINE
179
+ )
180
+ repl2 = (
181
+ "local_counts = _run_shards_serial_or_parallel(\n"
182
+ " [lambda s=sym_shard: count_pairs_in_shard(s, min_frequency)\n"
183
+ " for sym_shard in shard_symbols],\n"
184
+ " num_workers,\n"
185
+ " )"
186
+ )
187
+ if pat2.search(text):
188
+ text = pat2.sub(repl2, text)
189
+ print(" ✓ bloco count_pairs_in_shard substituído por helper")
190
+
191
+ # Padrão 3: apply_merge_in_shard
192
+ pat3 = re.compile(
193
+ r'with ProcessPoolExecutor\(max_workers=num_workers\) as executor:\s*\n'
194
+ r'\s*apply_futures = \[\s*\n'
195
+ r'\s*executor\.submit\(\s*\n'
196
+ r'\s*apply_merge_in_shard, sym_shard, esq, dir_, new_token_str\s*\n'
197
+ r'\s*\)\s*\n'
198
+ r'\s*for sym_shard in shard_symbols\s*\n'
199
+ r'\s*\]\s*\n'
200
+ r'\s*shard_symbols = \[f\.result\(\) for f in apply_futures\]',
201
+ re.MULTILINE
202
+ )
203
+ repl3 = (
204
+ "shard_symbols = _run_shards_serial_or_parallel(\n"
205
+ " [lambda s=sym_shard: apply_merge_in_shard(s, esq, dir_, new_token_str)\n"
206
+ " for sym_shard in shard_symbols],\n"
207
+ " num_workers,\n"
208
+ " )"
209
+ )
210
+ if pat3.search(text):
211
+ text = pat3.sub(repl3, text)
212
+ print(" ✓ bloco apply_merge_in_shard substituído por helper")
213
+
214
+ if text == original:
215
+ print(" ⚠ nenhuma mudança aplicada — padrões já substituídos ou não encontrados")
216
+ return False
217
+ BBPE_PATH.write_text(text, encoding="utf-8")
218
+ print(f" ✓ {BBPE_PATH} atualizado")
219
+ return True
220
+
221
+
222
+ def fix_part_b_memory_guard_in_train() -> bool:
223
+ """Parte B: Adiciona memory guard antes de chamar tokenizer.fit() no train script."""
224
+ print(f"\n{'='*70}")
225
+ print("PARTE B: train_v6_5_v2.py — memory guard antes de tokenizer.fit()")
226
+ print(f"{'='*70}")
227
+
228
+ text = TRAIN_PATH.read_text(encoding="utf-8")
229
+ original = text
230
+
231
+ # Localiza o bloco de refit e adiciona memory guard ANTES de chamar fit
232
+ # Procura por "# Refit tokenizer com corpus acumulado"
233
+ target = "# Refit tokenizer com corpus acumulado"
234
+ if target not in text:
235
+ print(f" ✗ não encontrei marcador '{target}'")
236
+ return False
237
+
238
+ # Memory guard code a ser inserido antes do refit
239
+ guard_code = '''# V6.7 — MEMORY GUARD before tokenizer refit
240
+ # User requirement: "tokenizer-growth refit (was causing
241
+ # crashes during BBPE parallel training at 1000-sample mark)".
242
+ # Prova 14: ProcessPoolExecutor fork duplica RSS do processo
243
+ # pai (modelo + tensores + buffers). Se RSS > 85% do cgroup
244
+ # limit, SKIP refit (non-fatal) para evitar OOM-killer.
245
+ import os as _os_mod_v67
246
+ try:
247
+ with open("/proc/self/status") as _f_v67:
248
+ _rss_line_v67 = [l for l in _f_v67 if l.startswith("VmRSS:")]
249
+ _rss_kb_v67 = int(_rss_line_v67[0].split()[1]) if _rss_line_v67 else 0
250
+ _rss_mb_v67 = _rss_kb_v67 / 1024.0
251
+ # cgroup limit
252
+ _cg_limit_mb_v67 = 4096.0 # default fallback
253
+ try:
254
+ with open("/sys/fs/cgroup/memory.max") as _f_cg_v67:
255
+ _cg_val_v67 = _f_cg_v67.read().strip()
256
+ if _cg_val_v67 and _cg_val_v67 != "max":
257
+ _cg_limit_mb_v67 = int(_cg_val_v67) / 1024 / 1024
258
+ except Exception:
259
+ pass # fallback default
260
+ _rss_pct_v67 = _rss_mb_v67 / _cg_limit_mb_v67 if _cg_limit_mb_v67 > 0 else 0
261
+ logger.info(
262
+ f"[V6.7-memory-guard] RSS={_rss_mb_v67:.0f}MB "
263
+ f"({100*_rss_pct_v67:.1f}% of cgroup "
264
+ f"{_cg_limit_mb_v67:.0f}MB)"
265
+ )
266
+ if _rss_pct_v67 > 0.85:
267
+ logger.warning(
268
+ f"[V6.7-memory-guard] SKIP refit: RSS "
269
+ f"{100*_rss_pct_v67:.1f}% > 85% of cgroup "
270
+ f"(would trigger OOM via fork). "
271
+ f"gc.collect() e prosseguir sem refit."
272
+ )
273
+ gc.collect()
274
+ if hasattr(torch, "cpu") and hasattr(torch.cpu, "empty_cache"):
275
+ try: torch.cpu.empty_cache()
276
+ except Exception: pass
277
+ last_tokenizer_refit_sample_count = total_so_far_now
278
+ tokenizer_growth_log.append({
279
+ "step": step,
280
+ "chunk_idx": chunk_idx_global,
281
+ "total_samples": total_so_far_now,
282
+ "skipped_reason": "rss_exceeded_85pct",
283
+ "rss_mb": _rss_mb_v67,
284
+ "cgroup_limit_mb": _cg_limit_mb_v67,
285
+ })
286
+ # SKIP refit — continue para próximo chunk
287
+ raise _SkipRefitV67()
288
+ except _SkipRefitV67:
289
+ pass # já tratado acima
290
+ except Exception as _guard_err_v67:
291
+ logger.warning(
292
+ f"[V6.7-memory-guard] guard failed (non-fatal): {_guard_err_v67}"
293
+ )
294
+
295
+ # Refit tokenizer com corpus acumulado'''
296
+
297
+ # Helper exception class — precisa estar definida no escopo do módulo
298
+ # Vamos inserir a definição antes do bloco de treino (junto com outras
299
+ # exceções customizadas, se houver)
300
+ skip_exc_def = '''
301
+ # V6.7 — Exception para skip de refit (memory guard)
302
+ class _SkipRefitV67(Exception):
303
+ """Sentinela interna: pula refit do tokenizer quando memory guard dispara."""
304
+ pass
305
+
306
+ '''
307
+ # Insere a definição da exceção antes da função de treinamento principal
308
+ # Procura por "def run_v6_5_v2_pipeline" ou similar
309
+ if "_SkipRefitV67" not in text:
310
+ # Insere após imports principais — procura por "import torch"
311
+ match = re.search(r'(\nimport torch\b)', text)
312
+ if match:
313
+ insert_at = match.end()
314
+ text = text[:insert_at] + skip_exc_def + text[insert_at:]
315
+ print(" ✓ _SkipRefitV67 exception class definida")
316
+
317
+ # Substitui o marcador pelo guard code + marcador
318
+ if "_V6.7-memory-guard" not in text:
319
+ text = text.replace(target, guard_code)
320
+ print(" ✓ memory guard inserido antes do refit")
321
+
322
+ if text == original:
323
+ print(" ⚠ nenhuma mudança aplicada — já existe ou padrão não encontrado")
324
+ return False
325
+ TRAIN_PATH.write_text(text, encoding="utf-8")
326
+ print(f" ✓ {TRAIN_PATH} atualizado")
327
+ return True
328
+
329
+
330
+ def fix_part_c_reenable_refit_interval() -> bool:
331
+ """Parte C: Reativa o intervalo de refit (10**9 → 1000) com config segura."""
332
+ print(f"\n{'='*70}")
333
+ print("PARTE C: train_v6_5_v2.py — reativa refit interval (1000 samples)")
334
+ print(f"{'='*70}")
335
+
336
+ text = TRAIN_PATH.read_text(encoding="utf-8")
337
+ original = text
338
+
339
+ # Substitui o intervalo desativado por intervalo reativado
340
+ old_line = " TOKENIZER_REFIT_INTERVAL_SAMPLES = 10**9 # V6.6: disabled refit (caused crashes during BBPE parallel training)"
341
+ new_lines = """ # V6.7: REATIVADO refit com serial mode + memory guard (fix BBPE OOM)
342
+ # User requirement: "processar aprimoramento (matemático e lógico) para
343
+ # resolver: tokenizer-growth refit (was causing crashes during BBPE
344
+ # parallel training at 1000-sample mark)".
345
+ # Prova 14: agora serial mode (no fork) + memory guard (skip se RSS>85%)
346
+ # tornam o refit seguro. Era 10**9 (desativado); agora 1000 (reativado).
347
+ TOKENIZER_REFIT_INTERVAL_SAMPLES = 1000"""
348
+
349
+ if old_line in text:
350
+ text = text.replace(old_line, new_lines)
351
+ print(" ✓ TOKENIZER_REFIT_INTERVAL_SAMPLES: 10**9 → 1000")
352
+ else:
353
+ # Tenta versão alternativa sem comentário
354
+ pat = re.compile(r'TOKENIZER_REFIT_INTERVAL_SAMPLES\s*=\s*10\*\*9[^\n]*')
355
+ if pat.search(text):
356
+ text = pat.sub(new_lines.strip(), text)
357
+ print(" ✓ TOKENIZER_REFIT_INTERVAL_SAMPLES: 10**9 → 1000 (regex)")
358
+ else:
359
+ print(" ⚠ TOKENIZER_REFIT_INTERVAL_SAMPLES não encontrado ou já reativado")
360
+
361
+ # Garante que o fit() é chamado com num_workers=1 (serial)
362
+ # Procura por "kls.tokenizer.fit(list(tokenizer_corpus_buffer))"
363
+ fit_call = "kls.tokenizer.fit(list(tokenizer_corpus_buffer))"
364
+ if fit_call in text and "force_serial=True" not in text:
365
+ # fit() não tem parâmetro force_serial; o num_workers=1 é default
366
+ # Mas vamos garantir explicitamente passando min_frequency=2
367
+ new_fit_call = "kls.tokenizer.fit(list(tokenizer_corpus_buffer), min_frequency=2)"
368
+ text = text.replace(fit_call, new_fit_call)
369
+ print(" ✓ fit() call: adicionado min_frequency=2 explícito")
370
+
371
+ if text == original:
372
+ print(" ⚠ nenhuma mudança aplicada")
373
+ return False
374
+ TRAIN_PATH.write_text(text, encoding="utf-8")
375
+ print(f" ✓ {TRAIN_PATH} atualizado")
376
+ return True
377
+
378
+
379
+ def verify_syntax() -> bool:
380
+ """Verifica sintaxe Python dos arquivos modificados."""
381
+ print(f"\n{'='*70}")
382
+ print("VERIFICAÇÃO DE SINTAXE")
383
+ print(f"{'='*70}")
384
+ ok = True
385
+ for path in [BBPE_PATH, TRAIN_PATH]:
386
+ try:
387
+ import py_compile
388
+ py_compile.compile(str(path), doraise=True)
389
+ print(f" ✓ {path.name}: sintaxe OK")
390
+ except py_compile.PyCompileError as e:
391
+ print(f" ✗ {path.name}: ERRO DE SINTAXE")
392
+ print(f" {e}")
393
+ ok = False
394
+ return ok
395
+
396
+
397
+ def main() -> int:
398
+ print("\n" + "═" * 70)
399
+ print("V6.7 — FIX BBPE TOKENIZER-GROWTH REFIT CRASH")
400
+ print("═" * 70)
401
+ print(f"\nUser requirement:")
402
+ print(f' "processar aprimoramento (matemático e lógico) para resolver:')
403
+ print(f' tokenizer-growth refit (was causing crashes during BBPE')
404
+ print(f' parallel training at 1000-sample mark)"')
405
+
406
+ print(f"\nEstratégia (3 partes):")
407
+ print(f" A) bbpe_tokenizer.py: serial mode when num_workers<=1 (no fork)")
408
+ print(f" B) train_v6_5_v2.py: memory guard antes de tokenizer.fit()")
409
+ print(f" C) train_v6_5_v2.py: reativar TOKENIZER_REFIT_INTERVAL_SAMPLES=1000")
410
+
411
+ # Backup dos arquivos
412
+ import shutil
413
+ backup_dir = Path("/home/z/my-project/scripts/_backup_v67")
414
+ backup_dir.mkdir(parents=True, exist_ok=True)
415
+ for path in [BBPE_PATH, TRAIN_PATH]:
416
+ backup = backup_dir / f"{path.name}.bak"
417
+ shutil.copy2(path, backup)
418
+ print(f"\n backup: {path.name} → {backup}")
419
+
420
+ # Aplica fixes
421
+ a_ok = fix_part_a_bbpe_serial_mode()
422
+ b_ok = fix_part_b_memory_guard_in_train()
423
+ c_ok = fix_part_c_reenable_refit_interval()
424
+
425
+ # Verifica sintaxe
426
+ syntax_ok = verify_syntax()
427
+
428
+ print(f"\n{'='*70}")
429
+ print("RESUMO")
430
+ print(f"{'='*70}")
431
+ print(f" Parte A (serial mode): {'✓' if a_ok else '✗'}")
432
+ print(f" Parte B (memory guard): {'✓' if b_ok else '✗'}")
433
+ print(f" Parte C (reativar refit): {'✓' if c_ok else '✗'}")
434
+ print(f" Sintaxe: {'✓' if syntax_ok else '✗'}")
435
+
436
+ if a_ok and b_ok and c_ok and syntax_ok:
437
+ print("\n✓ V6.7 fix aplicado com sucesso.")
438
+ print(" Próximo passo: rodar test_bbpe_refit_serial.py para validar.")
439
+ return 0
440
+ else:
441
+ print("\n✗ Falha ao aplicar V6.7 fix — verifique erros acima.")
442
+ return 1
443
+
444
+
445
+ if __name__ == "__main__":
446
+ sys.exit(main())