Fedir-Ilina commited on
Commit
004ec91
·
verified ·
1 Parent(s): f990c3d

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +13 -0
  2. app.py +2007 -0
  3. config_manager.py +193 -0
  4. templates/index-6.html +553 -0
  5. trained_model_output/teste.txt +0 -0
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ COPY . .
10
+
11
+ EXPOSE 7860
12
+
13
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,2007 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #app.py
2
+ import os
3
+ import sys
4
+ import subprocess
5
+ # Forçar versões compatíveis
6
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "urllib3<2.0", "charset_normalizer<3.4"])
7
+ import warnings # IMPORT NECESSÁRIO antes de usar
8
+ from requests.packages.urllib3.exceptions import DependencyWarning
9
+ # Ignorar o warning do requests
10
+ warnings.simplefilter('ignore', DependencyWarning)
11
+ # No topo do seu ficheiro Python
12
+ BASE_PATH = os.path.dirname(os.path.abspath(__file__))
13
+ # Forçar o output_dir para um local fixo e absoluto
14
+ FINAL_OUTPUT_DIR = os.path.join(BASE_PATH, "trained_model_output")
15
+ import torch
16
+ import logging
17
+ import json
18
+ import chardet
19
+ import math
20
+ import psutil
21
+ import traceback
22
+ import time
23
+ import threading
24
+ import webbrowser
25
+ import platform
26
+ import cpuinfo
27
+ import statistics
28
+ import glob
29
+ import ctypes as ct
30
+ import importlib
31
+ import inspect
32
+ import torch.nn as nn
33
+
34
+ # keyboard é opcional (não disponível em HF Spaces / ambientes headless)
35
+ try:
36
+ import keyboard
37
+ _KEYBOARD_AVAILABLE = True
38
+ except Exception:
39
+ _KEYBOARD_AVAILABLE = False
40
+ #import intel_extension_for_pytorch as ipex
41
+
42
+ from typing import Dict, Union, Any
43
+ #webview.create_window("train-12-ok.py", "index-6.html") #iterface autonomo
44
+ #webview.start()
45
+ # Desativa os logs de informação do oneDNN (nível 1) e avisos (nível 2)
46
+ os.environ['TF_CPP_MIN_LOG_LEVEL'] = '0'
47
+
48
+ #import warnings
49
+ #import tensorflow as tf
50
+ #'0' = Mostra todas as mensagens (padrão).
51
+ #'1' = Filtra as mensagens de INFO.
52
+ #'2' = Filtra as mensagens de INFO e WARNING.
53
+ #'3' = Filtra todas as mensagens, incluindo ERROR.
54
+ # Suprimir avisos Python
55
+ warnings.filterwarnings("ignore", category=UserWarning, module='tensorflow')
56
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
57
+ # Ajusta logger do TensorFlow
58
+ #tf.get_logger().setLevel('ERROR')
59
+
60
+ from datetime import timedelta
61
+ from datetime import datetime
62
+ from contextlib import suppress
63
+ from threading import Thread
64
+ from flask import Flask, render_template, request, jsonify
65
+ from contextlib import contextmanager
66
+
67
+ # Importações do Hugging Face
68
+ from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments, Trainer
69
+ from transformers import TrainerCallback, TrainerState, TrainerControl
70
+ from datasets import Dataset, load_from_disk # type: ignore
71
+ from peft import LoraConfig, get_peft_model, PeftModel, TaskType # type: ignore
72
+ from transformers import DataCollatorForLanguageModeling
73
+ from flask import send_from_directory
74
+ #------------------------------------------------------
75
+ from warnings import warn as log_warning
76
+ from config_manager import _load_constants_from_file, map_backend_to_frontend, update_python_constants
77
+ #pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cpu
78
+ #---------------------
79
+ app = Flask(__name__)
80
+ all_data = []
81
+ #--------------------
82
+ print("-" * 80)
83
+ #-----------------------------------------------------------
84
+ def _ts():
85
+ return datetime.now().strftime("%H:%M:%S")
86
+
87
+ # -------------------------
88
+ # Deteção P/E cross-platform
89
+ # -------------------------
90
+ def detect_core_types():
91
+ """
92
+ Tenta:
93
+ 1) Windows: EfficiencyClass (Windows 11 expõe isto; em máquinas homogéneas virá tudo Efficiency=0).
94
+ 2) Linux: sysfs core_type (1=Atom/E, 2=Core/P; 0=Unknown).
95
+ 3) Fallback: assume homogéneo → todos P, E vazio.
96
+
97
+ Retorna (P_IDS, E_IDS, meta_dict).
98
+ """
99
+ logical = psutil.cpu_count(logical=True) or os.cpu_count() or 1
100
+ # 1) Windows EfficiencyClass
101
+ if platform.system() == "Windows":
102
+ try:
103
+ RELATION_PROCESSOR_CORE = 0
104
+
105
+ class GROUP_AFFINITY(ct.Structure):
106
+ _fields_ = [("Mask", ct.c_ulonglong),
107
+ ("Group", ct.c_ushort),
108
+ ("Reserved", ct.c_ushort * 3)]
109
+
110
+ class PROCESSOR_RELATIONSHIP(ct.Structure):
111
+ _fields_ = [("Flags", ct.c_ubyte),
112
+ ("EfficiencyClass", ct.c_ubyte),
113
+ ("Reserved", ct.c_ubyte * 20),
114
+ ("GroupCount", ct.c_ushort)]
115
+ # seguido inline por GROUP_AFFINITY[GroupCount]
116
+
117
+ class SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX_HEADER(ct.Structure):
118
+ _fields_ = [("Relationship", ct.c_int),
119
+ ("Size", ct.c_ulong)]
120
+
121
+ GetLPIEx = ct.windll.kernel32.GetLogicalProcessorInformationEx
122
+ GetLPIEx.restype = ct.c_bool
123
+ GetLPIEx.argtypes = [ct.c_int, ct.c_void_p, ct.POINTER(ct.c_ulong)]
124
+
125
+ buf_size = ct.c_ulong(0)
126
+ GetLPIEx(RELATION_PROCESSOR_CORE, None, ct.byref(buf_size))
127
+ if buf_size.value:
128
+ buf = (ct.c_byte * buf_size.value)()
129
+ if GetLPIEx(RELATION_PROCESSOR_CORE, ct.byref(buf), ct.byref(buf_size)):
130
+ eff_by_logical = {}
131
+ offset = 0
132
+ single_group = (logical <= 64)
133
+ while offset < buf_size.value:
134
+ header = SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX_HEADER.from_buffer(buf, offset)
135
+ size = header.Size
136
+ if header.Relationship == RELATION_PROCESSOR_CORE:
137
+ pr = PROCESSOR_RELATIONSHIP.from_buffer(buf, offset + ct.sizeof(header))
138
+ eff = pr.EfficiencyClass # 0 = mais performante (P), >0 = mais eficiente (E)
139
+ ga_offset = offset + ct.sizeof(header) + ct.sizeof(PROCESSOR_RELATIONSHIP)
140
+ for i in range(pr.GroupCount):
141
+ ga = GROUP_AFFINITY.from_buffer(buf, ga_offset + i * ct.sizeof(GROUP_AFFINITY))
142
+ mask = ga.Mask
143
+ if single_group:
144
+ # Mapear bits 0..63 para logical ids 0..63
145
+ for bit in range(64):
146
+ if (mask >> bit) & 1:
147
+ eff_by_logical[bit] = eff
148
+ else:
149
+ # Em sistemas multi-grupo (>64 lógicos), mapa global é mais complexo.
150
+ # Como alteração mínima, ignoramos (cai para fallback se não mapear nada).
151
+ pass
152
+ offset += size
153
+
154
+ if eff_by_logical:
155
+ p_ids = sorted([i for i in range(logical) if eff_by_logical.get(i, 0) == 0])
156
+ e_ids = sorted([i for i in range(logical) if eff_by_logical.get(i, 0) > 0])
157
+ return p_ids, e_ids, {"method": "windows_efficiencyclass", "notes": []}
158
+ except Exception as e:
159
+ # continua para Linux/fallback
160
+ pass
161
+
162
+ # 2) Linux sysfs core_type
163
+ if platform.system() == "Linux":
164
+ try:
165
+ p_ids, e_ids, unknown = [], [], []
166
+ for cpu in range(logical):
167
+ path = f"/sys/devices/system/cpu/cpu{cpu}/topology/core_type"
168
+ try:
169
+ with open(path, "r") as f:
170
+ val = f.read().strip()
171
+ except FileNotFoundError:
172
+ val = None
173
+ if val is None:
174
+ # sysfs não disponível → sai para fallback
175
+ p_ids = e_ids = []
176
+ break
177
+ try:
178
+ t = int(val)
179
+ except ValueError:
180
+ t = 0
181
+ # Kernel moderno: 1=Atom(E), 2=Core(P), 0=Unknown
182
+ if t == 2:
183
+ p_ids.append(cpu)
184
+ elif t == 1:
185
+ e_ids.append(cpu)
186
+ else:
187
+ unknown.append(cpu)
188
+ if p_ids or e_ids:
189
+ notes = []
190
+ if unknown:
191
+ notes.append(f"{len(unknown)} CPUs com core_type=Unknown (tratados como P).")
192
+ p_ids.extend(unknown) # escolha conservadora: desconhecidos como P
193
+ p_ids = sorted(p_ids)
194
+ return sorted(p_ids), sorted(e_ids), {"method": "linux_core_type", "notes": notes}
195
+ except Exception:
196
+ pass
197
+
198
+ # 3) Fallback: homogéneo
199
+ return list(range(logical)), [], {"method": "homogeneous_fallback", "notes": []}
200
+
201
+ # -------------------------
202
+ # Mapa de siblings (HT) por core físico
203
+ # -------------------------
204
+ def get_core_siblings():
205
+ """
206
+ Retorna lista de listas: cada sublista são os logical IDs que pertencem ao mesmo core físico.
207
+ """
208
+ logical = psutil.cpu_count(logical=True) or 1
209
+
210
+ if platform.system() == "Windows":
211
+ try:
212
+ RELATION_PROCESSOR_CORE = 0
213
+
214
+ class GROUP_AFFINITY(ct.Structure):
215
+ _fields_ = [("Mask", ct.c_ulonglong),
216
+ ("Group", ct.c_ushort),
217
+ ("Reserved", ct.c_ushort * 3)]
218
+
219
+ class PROCESSOR_RELATIONSHIP(ct.Structure):
220
+ _fields_ = [("Flags", ct.c_ubyte),
221
+ ("EfficiencyClass", ct.c_ubyte),
222
+ ("Reserved", ct.c_ubyte * 20),
223
+ ("GroupCount", ct.c_ushort)]
224
+
225
+ class SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX_HEADER(ct.Structure):
226
+ _fields_ = [("Relationship", ct.c_int),
227
+ ("Size", ct.c_ulong)]
228
+
229
+ GetLPIEx = ct.windll.kernel32.GetLogicalProcessorInformationEx
230
+ GetLPIEx.restype = ct.c_bool
231
+ GetLPIEx.argtypes = [ct.c_int, ct.c_void_p, ct.POINTER(ct.c_ulong)]
232
+
233
+ buf_size = ct.c_ulong(0)
234
+ GetLPIEx(RELATION_PROCESSOR_CORE, None, ct.byref(buf_size))
235
+ if not buf_size.value:
236
+ return []
237
+
238
+ buf = (ct.c_byte * buf_size.value)()
239
+ if not GetLPIEx(RELATION_PROCESSOR_CORE, ct.byref(buf), ct.byref(buf_size)):
240
+ return []
241
+
242
+ # Cada entrada RelationProcessorCore descreve UM core físico e a(s) máscara(s) dos seus logical processors.
243
+ siblings = []
244
+ offset = 0
245
+ single_group = (logical <= 64)
246
+ while offset < buf_size.value:
247
+ header = SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX_HEADER.from_buffer(buf, offset)
248
+ size = header.Size
249
+ if header.Relationship == RELATION_PROCESSOR_CORE:
250
+ pr = PROCESSOR_RELATIONSHIP.from_buffer(buf, offset + ct.sizeof(header))
251
+ ga_offset = offset + ct.sizeof(header) + ct.sizeof(PROCESSOR_RELATIONSHIP)
252
+ core_logicals = []
253
+ for i in range(pr.GroupCount):
254
+ ga = GROUP_AFFINITY.from_buffer(buf, ga_offset + i * ct.sizeof(GROUP_AFFINITY))
255
+ mask = ga.Mask
256
+ if single_group:
257
+ for bit in range(64):
258
+ if (mask >> bit) & 1:
259
+ core_logicals.append(bit)
260
+ else:
261
+ # Multi-grupo: mapping global não é trivial; manter simples
262
+ for bit in range(64):
263
+ if (mask >> bit) & 1:
264
+ core_logicals.append(bit) # aproximação
265
+ if core_logicals:
266
+ siblings.append(sorted(set(core_logicals)))
267
+ offset += size
268
+
269
+ # Ordena por menor logical id para estabilidade
270
+ siblings = sorted(siblings, key=lambda s: min(s) if s else 1e9)
271
+ return siblings
272
+ except Exception:
273
+ return []
274
+
275
+ elif platform.system() == "Linux":
276
+ sibs = []
277
+ try:
278
+ for cpu in range(logical):
279
+ path = f"/sys/devices/system/cpu/cpu{cpu}/topology/thread_siblings_list"
280
+ try:
281
+ with open(path, "r") as f:
282
+ txt = f.read().strip()
283
+ except FileNotFoundError:
284
+ return []
285
+ # Ex.: "0,6" ou "0-1,6-7"
286
+ items = []
287
+ for part in txt.split(","):
288
+ if "-" in part:
289
+ a, b = part.split("-")
290
+ items.extend(range(int(a), int(b) + 1))
291
+ else:
292
+ items.append(int(part))
293
+ sibs.append(sorted(set(items)))
294
+ # Deduplica sublistas iguais
295
+ uniq = []
296
+ seen = set()
297
+ for s in sibs:
298
+ t = tuple(s)
299
+ if t not in seen:
300
+ seen.add(t)
301
+ uniq.append(s)
302
+ uniq = sorted(uniq, key=lambda s: min(s))
303
+ return uniq
304
+ except Exception:
305
+ return []
306
+
307
+ return []
308
+
309
+ def order_by_physical_first(candidates, siblings_map):
310
+ """
311
+ Reordena 'candidates' para usar primeiro 1 logical por core físico (evita usar siblings logo de início).
312
+ Se 'siblings_map' estiver vazio, retorna candidatos ordenados naturalmente.
313
+ """
314
+ if not siblings_map:
315
+ return sorted(candidates)
316
+
317
+ cand_set = set(candidates)
318
+ # 1ª passagem: 1 por core
319
+ first_pass = []
320
+ for group in siblings_map:
321
+ pick = next((x for x in group if x in cand_set), None)
322
+ if pick is not None:
323
+ first_pass.append(pick)
324
+
325
+ # 2ª passagem: restantes (siblings), preservando ordem por grupo
326
+ others = []
327
+ for group in siblings_map:
328
+ for x in group:
329
+ if x in cand_set and x not in first_pass:
330
+ others.append(x)
331
+
332
+ # Mantém qualquer candidato que não esteja no mapa (ex.: detection parcial)
333
+ leftovers = [x for x in sorted(candidates) if x not in first_pass and x not in others]
334
+ return first_pass + others + leftovers
335
+
336
+ # =========================================================================
337
+ # 1️. Configuração base de hardware, Otimização e Hiperparâmetros
338
+ # =========================================================================
339
+ # --- 1.1 Hardware ---
340
+ LOGICAL_CPUS = psutil.cpu_count(logical=True) or os.cpu_count() or 16
341
+ PHYSICAL_CPUS = psutil.cpu_count(logical=False) or max(1, LOGICAL_CPUS // 2)
342
+ TOTAL_RAM_GB = psutil.virtual_memory().total / (1024 ** 3)
343
+ #--------------------------------------------------------------------------------------------------------------------------------------
344
+ # ===============================================================
345
+ # Ajuste de performance (auto/manual)
346
+ # ===============================================================
347
+ OMP_THREADS_UTILIZATION = 0.85
348
+ DETECTION_PERFORMANCE = "auto" # "auto" | "manual"
349
+ CORES_UTILIZATION = 1.0
350
+
351
+ # --------------------
352
+ def compute_effective_cores(cpu_count: int, utilization: float) -> int:
353
+ eff = int(math.floor(cpu_count * utilization))
354
+ return max(1, min(eff, cpu_count))
355
+
356
+
357
+ EFFECTIVE_LOGICAL_CPUS = compute_effective_cores(LOGICAL_CPUS, CORES_UTILIZATION)
358
+
359
+ # --------------------
360
+ # --- Detectar núcleos P/E ---
361
+ _DET_P_full, _DET_E_full, _META = detect_core_types() # Renomeado para indicar que são os full detections
362
+
363
+ # NOVO: Filtrar P/E-cores para estarem dentro do limite de EFFECTIVE_LOGICAL_CPUS
364
+ _DET_P = [i for i in _DET_P_full if i < EFFECTIVE_LOGICAL_CPUS]
365
+ _DET_E = [i for i in _DET_E_full if i < EFFECTIVE_LOGICAL_CPUS]
366
+
367
+
368
+
369
+ # ===============================================================
370
+ # Ajuste entre modo auto/manual (apenas meta-informação de arranque)
371
+ # ===============================================================
372
+ _AUTO_META = "manual"
373
+ OMP_THREADS_PLACEHOLDER = compute_effective_cores(EFFECTIVE_LOGICAL_CPUS, OMP_THREADS_UTILIZATION)
374
+ if DETECTION_PERFORMANCE.lower() == "auto":
375
+ omp_threads = OMP_THREADS_PLACEHOLDER
376
+ total_detected_filtered = (len(_DET_P or []) + len(_DET_E or []))
377
+ if total_detected_filtered == 0:
378
+ _AUTO_META = "auto (fallback homogéneo)"
379
+ else:
380
+ p_ratio = len(_DET_P) / total_detected_filtered if total_detected_filtered > 0 else 0
381
+ if p_ratio >= 0.5:
382
+ _AUTO_META = f"auto (balanceado P/E → OMP={omp_threads})"
383
+ else:
384
+ _AUTO_META = f"auto (balanceado E-heavy → OMP={omp_threads})"
385
+ else:
386
+ _AUTO_META = "manual"
387
+
388
+ # Log informativo (mantém o teu estilo)
389
+ print(f"[INFO] [{_ts()}] Modo de desempenho: {DETECTION_PERFORMANCE} ({_AUTO_META})")
390
+
391
+
392
+ # --- 1.2 Otimização de Threads/Processos ---
393
+ def compute_omp_threads(cpu_count, utilization):
394
+ threads = int(math.floor(cpu_count * utilization))
395
+ return max(1, min(threads, cpu_count))
396
+
397
+
398
+ # OMP passa a respeitar o teto de núcleos permitidos por CORES_UTILIZATION
399
+ OMP_THREADS = compute_omp_threads(EFFECTIVE_LOGICAL_CPUS, OMP_THREADS_UTILIZATION)
400
+
401
+ # --------------------
402
+ # Seleção para tensores (OMP): usar P primeiro; se faltar, completar com E (com ordenação física primeiro)
403
+ siblings = get_core_siblings()
404
+ ordered_P = order_by_physical_first(_DET_P, siblings)
405
+ ordered_E = order_by_physical_first(_DET_E, siblings)
406
+
407
+ needed = OMP_THREADS
408
+ P_CORE_IDS = []
409
+ if ordered_P:
410
+ takeP = ordered_P[:min(needed, len(ordered_P))]
411
+ P_CORE_IDS.extend(takeP)
412
+ needed -= len(takeP)
413
+
414
+ # Completar com E-cores se necessário
415
+ if needed > 0 and ordered_E:
416
+ takeE = [i for i in ordered_E if i not in P_CORE_IDS][:needed]
417
+ P_CORE_IDS.extend(takeE)
418
+ needed -= len(takeE)
419
+
420
+ # Fallback homogéneo: se nada acima preencheu (ex.: sem deteção ou P/E fora do range), usa os primeiros N
421
+ if not P_CORE_IDS:
422
+ # Agora limitado ao espaço permitido (0..EFFECTIVE_LOGICAL_CPUS-1)
423
+ P_CORE_IDS = list(range(min(OMP_THREADS, EFFECTIVE_LOGICAL_CPUS)))
424
+ # Isso garante que P_CORE_IDS nunca exceda EFFECTIVE_LOGICAL_CPUS
425
+
426
+ # Secundários (DataLoader): 1) E não usados, 2) P não usados, 3) quaisquer outros restantes
427
+ used = set(P_CORE_IDS)
428
+ secondary = []
429
+
430
+
431
+ # IDs disponíveis dentro do limite efetivo
432
+ available_ids_in_effective_range = set(range(EFFECTIVE_LOGICAL_CPUS))
433
+
434
+ # 1) E que sobraram e estão dentro do range efetivo
435
+ secondary.extend([i for i in (_DET_E or []) if i not in used and i in available_ids_in_effective_range])
436
+ # 2) P que sobraram e estão dentro do range efetivo
437
+ secondary.extend([i for i in (_DET_P or []) if i not in used and i in available_ids_in_effective_range])
438
+ # 3) Restantes (caso deteção seja homogénea/fallback)
439
+ secondary.extend([i for i in available_ids_in_effective_range
440
+ if i not in used
441
+ and (i not in (_DET_P or []))
442
+ and (i not in (_DET_E or []))])
443
+
444
+ # Remover duplicados preservando ordem
445
+ _seen = set()
446
+ REMAINING_CORE_IDS = [x for x in secondary if not (x in _seen or _seen.add(x))]
447
+
448
+
449
+ # O número de workers deve refletir exatamente os cores sobrantes e nunca exceder o que sobra dos efetivos
450
+ max_workers_possible = max(0, EFFECTIVE_LOGICAL_CPUS - len(P_CORE_IDS))
451
+ DATALOADER_WORKERS = min(len(REMAINING_CORE_IDS), max_workers_possible)
452
+
453
+ # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
454
+ # Variáveis de ambiente (mantidas)
455
+ os.environ["OMP_NUM_THREADS"] = str(OMP_THREADS)
456
+ os.environ["MKL_NUM_THREADS"] = str(OMP_THREADS)
457
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
458
+
459
+
460
+ # --- Afinidade (mantida) ---
461
+ def set_affinity(core_ids):
462
+ try:
463
+ if platform.system() == "Linux":
464
+ os.sched_setaffinity(0, set(core_ids))
465
+ elif platform.system() == "Windows":
466
+ psutil.Process().cpu_affinity(core_ids)
467
+ except Exception as e:
468
+ print(f"[WARN] [{_ts()}] Não foi possível definir afinidade: {e}")
469
+
470
+
471
+
472
+ # Mantém a ordem: P usados primeiro (prioridade), depois secundários
473
+ # Garante que os core_ids são únicos e estão dentro do limite.
474
+ # O conjunto final de IDs passados para afinidade deve ser <= EFFECTIVE_LOGICAL_CPUS
475
+ final_affinity_cores = sorted(list(set(P_CORE_IDS + REMAINING_CORE_IDS)))
476
+ # Filtra para garantir que nenhum ID exceda EFFECTIVE_LOGICAL_CPUS
477
+ final_affinity_cores = [i for i in final_affinity_cores if i < EFFECTIVE_LOGICAL_CPUS]
478
+ set_affinity(final_affinity_cores)
479
+
480
+ # --------------------
481
+ # Evita duplicação de logs quando Flask recarrega
482
+ if os.environ.get("WERKZEUG_RUN_MAIN") != "true":
483
+ _LOGS_PRINTED = True
484
+
485
+ SEC_E = []
486
+ SEC_P = []
487
+ SEC_OTHER = []
488
+ # --------------------
489
+ # Logs informativos detalhados (simplificados, evita repetições)
490
+ if not globals().get("_LOGS_PRINTED", False):
491
+ _LOGS_PRINTED = True # marca que já imprimimos uma vez
492
+ print(f"[INFO] [{_ts()}] Método de deteção: {_META.get('method')}")
493
+ if _META.get("notes"):
494
+ for n in _META["notes"]:
495
+ print(f"[INFO] [{_ts()}] Nota: {n}")
496
+
497
+ has_e = bool(_DET_E)
498
+ # Aqui, _DET_P e _DET_E já estão filtrados para serem <= EFFECTIVE_LOGICAL_CPUS
499
+ print(f"[INFO] [{_ts()}] Detetados (filtrados) → P: {len(_DET_P)} | E: {len(_DET_E)}")
500
+
501
+ # --- Breakdown do que foi realmente usado
502
+ P_USED_FROM_P = [i for i in P_CORE_IDS if i in (_DET_P or [])]
503
+ P_USED_FROM_E = [i for i in P_CORE_IDS if i in (_DET_E or [])]
504
+ SEC_E = [i for i in REMAINING_CORE_IDS if i in (_DET_E or [])]
505
+ SEC_P = [i for i in REMAINING_CORE_IDS if i in (_DET_P or [])]
506
+ SEC_OTHER = [i for i in REMAINING_CORE_IDS if (i not in (_DET_P or [])) and (i not in (_DET_E or []))]
507
+
508
+ # Tensores
509
+ if has_e:
510
+ # P_USED_FROM_P e P_USED_FROM_E já estão corretos devido ao filtro de _DET_P e _DET_E
511
+ print(
512
+ f"[INFO] [{_ts()}] Tensores (OMP={OMP_THREADS}) → "
513
+ f"P: {len(P_USED_FROM_P)} {sorted(P_USED_FROM_P)}"
514
+ + (f", +E: {len(P_USED_FROM_E)} {sorted(P_USED_FROM_E)}" if P_USED_FROM_E else "")
515
+ )
516
+ else:
517
+ print(f"[INFO] [{_ts()}] Tensores (OMP={OMP_THREADS}) [homogéneo] → {len(P_CORE_IDS)} {sorted(P_CORE_IDS)}")
518
+
519
+
520
+ # Secundários (DataLoader)
521
+ if has_e:
522
+ if SEC_E:
523
+ print(f"[INFO] [{_ts()}] Secundário/DataLoader → E: {len(SEC_E)} {sorted(SEC_E)}")
524
+ if SEC_P:
525
+ print(f"[INFO] [{_ts()}] Secundário/DataLoader → P: {len(SEC_P)} {sorted(SEC_P)}")
526
+ if SEC_OTHER:
527
+ print(f"[INFO] [{_ts()}] Secundário/DataLoader → Outros: {len(SEC_OTHER)} {sorted(SEC_OTHER)}")
528
+ else:
529
+ print(f"[INFO] [{_ts()}] Cores restantes (homogéneo) → {len(REMAINING_CORE_IDS)} {sorted(REMAINING_CORE_IDS)}")
530
+ print("-" * 80)
531
+ print(
532
+ f"[INFO] [{_ts()}] LOGICAL_CPUS: {LOGICAL_CPUS} | PHYSICAL_CPUS: {PHYSICAL_CPUS} | "
533
+ f"TOTAL_RAM_GB: {TOTAL_RAM_GB:.2f}"
534
+ )
535
+ print(f"[INFO] [{_ts()}] Cores Utilization (global): {CORES_UTILIZATION:.2%} → efetivos: {EFFECTIVE_LOGICAL_CPUS}")
536
+ print(f"[INFO] [{_ts()}] OMP Threads Utilization: {OMP_THREADS_UTILIZATION:.2%}")
537
+ print(f"[INFO] [{_ts()}] OMP Threads: {OMP_THREADS}, Dataloader Workers: {DATALOADER_WORKERS}")
538
+ if DATALOADER_WORKERS == 0:
539
+ print(f"[INFO] [{_ts()}] Sem cores sobrantes para DataLoader (todos {EFFECTIVE_LOGICAL_CPUS} alocados a OMP).")
540
+
541
+ # Verificação final da soma
542
+ total_allocated = len(set(P_CORE_IDS + REMAINING_CORE_IDS))
543
+ print(
544
+ f"[INFO] [{_ts()}] Total de núcleos alocados (OMP + DataLoader): {total_allocated} "
545
+ f"(de {EFFECTIVE_LOGICAL_CPUS} efetivos)"
546
+ )
547
+ print("-" * 80)
548
+ print(f"[INFO] [{_ts()}] Iniciando processo de treino...\n")
549
+ print("-" * 80)
550
+
551
+ #--------------------------------------------------------------------------------------------------
552
+ #--------------------------------------------------------------------------------------------------
553
+ # --- 1.3 Parâmetros base do treino ---
554
+ BASE_BATCH_SIZE = 1 # Valor base para o batch size (reduzido para CPU)
555
+ INITIAL_ACCUMULATION_MIN_STEPS = 2
556
+ INITIAL_ACCUMULATION_MAX_STEPS = 4
557
+ BASE_EVAL_SIZE = 2 # Batch size para avaliação (reduzido para CPU)
558
+ BASE_LEARNING_RATE = 5e-5 # 2e-5
559
+ LR_SCHEDULER_TYPE = "constant_with_warmup" # (HF: cosine + warmap) cosine
560
+ WARMUP_RATIO = 0.03 # ~3% dos 4012 steps ≈ 120
561
+ LOGGIN_STEPS = 25
562
+ SAVE_STRATEGY = "steps"
563
+ SAVE_STEPS = 2
564
+ EVAL_STEPS = 2
565
+ #MAX_GRAD_NORM = 1.0
566
+ WEIGHT_DECAY = 0.1
567
+ OPTIM = "adamw_torch"
568
+
569
+ # --- 1.4 Utilização de RAM ---
570
+ TARGET_RAM_UTILIZATION = 0.95 # Limite máximo de uso da RAM total
571
+
572
+ # --- 1.5 Parâmetros para Ajuste Dinâmico do Batch Size (ajustar_accumulation_steps) ---
573
+ # Estimação: custo empírico de RAM por batch (ajuste este valor)
574
+ ESTIMATED_BATCH_GB = 0.30 #0.4 llama
575
+
576
+ # Utilização de RAM alvo para o cálculo de acumulação de gradientes
577
+ TARGET_ACCUMULATION_RAM_UTILIZATION = TARGET_RAM_UTILIZATION # Reutiliza a RAM geral
578
+
579
+ # --- 1.6 Parâmetros para MAX_LEN Dinâmico (ajustar_max_len) ---
580
+ BASE_MAX_LEN = 256
581
+ TARGET_MAX_LEN_UTILIZATION = 0.65 #0.65
582
+ MAX_LEN_INCREMENT = 128
583
+ MAX_LEN_CAP = 256
584
+
585
+ # Valores padrão ajustáveis
586
+ DEFAULT_FALLBACK_CAP = 8192 # ou MAX_LEN_CTX, se preferires
587
+ TOKENIZER_SENTINEL_CAP = 10_000_000
588
+
589
+ # Custo empírico de RAM para o dataset (ajuste estes valores)
590
+ ESTIMATED_BASE_DATASET_RAM_GB = 1.6
591
+ COST_PER_INCREMENT_GB = 0.30 #0.45 llama
592
+
593
+ # --- 1.7 Parâmetros para DynamicAccumulationCallback ---
594
+ DYNAMIC_ACCUMULATION_MAX_STEPS = 256 # Limite superior para gradient_accumulation_steps (usado na função e no callback)
595
+ DYNAMIC_ACCUMULATION_TARGET_UTIL = 0.95 # Target de RAM no callback (não usado diretamente, mas mantido para clareza)
596
+ DYNAMIC_ACCUMULATION_HIGH_RAM_LIMIT = 90.5 # Porcentagem de RAM para começar a reduzir o step
597
+ DYNAMIC_ACCUMULATION_LOW_RAM_LIMIT = 40.0 # Porcentagem de RAM para começar a aumentar o step
598
+
599
+ # --- 1.8 Ganchos opcionais para PyTorch (se estiver disponível) ---
600
+ from contextlib import suppress
601
+
602
+ with suppress(ImportError):
603
+ import torch
604
+ # Alinhar threads de Torch com OMP para evitar oversubscription
605
+ if hasattr(torch, "set_num_threads"):
606
+ torch.set_num_threads(max(1, OMP_THREADS))
607
+ if hasattr(torch, "set_num_interop_threads"):
608
+ # Interop baixo ajuda a estabilidade — normalmente 1 ou 2 é suficiente.
609
+ torch.set_num_interop_threads(max(1, min(2, DATALOADER_WORKERS)))
610
+
611
+ # --- 1.9 Outros parâmetros -
612
+ OPTIMIZER_REFRESH_INTERVAL = 300 # segundos
613
+ # =========================================================================
614
+ # 2️.Configuração Inicial e Cálculos
615
+ # =========================================================================
616
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
617
+
618
+ # --- Funções de Ajuste Dinâmico ---
619
+ def ajustar_accumulation_steps(
620
+ base_batch: int = BASE_BATCH_SIZE,
621
+ target_utilization: float = TARGET_ACCUMULATION_RAM_UTILIZATION,
622
+ estimated_batch_gb: float = ESTIMATED_BATCH_GB,
623
+ min_steps: int = INITIAL_ACCUMULATION_MIN_STEPS,
624
+ max_steps: int = INITIAL_ACCUMULATION_MAX_STEPS
625
+ ) -> int:
626
+ """
627
+ Ajusta gradient_accumulation_steps com base na RAM disponível (somente no arranque).
628
+ Respeita limites próprios e separados da fase dinâmica.
629
+ """
630
+ mem = psutil.virtual_memory()
631
+ free_gb = mem.available / (1024 ** 3)
632
+
633
+ available_ram_for_batch_gb = TOTAL_RAM_GB * target_utilization
634
+ max_batches_fit = int(available_ram_for_batch_gb / estimated_batch_gb)
635
+
636
+ steps = max(min_steps, min(max_batches_fit // base_batch, max_steps))
637
+
638
+ logging.info(f"Memória usada: {mem.percent:.1f}% ({free_gb:.1f} GB livres)")
639
+ logging.info(f"Ajustando gradient_accumulation_steps (inicial) → {steps}")
640
+
641
+ return steps
642
+ #----------------------------------------------------------
643
+ def ajustar_max_len(
644
+ tokenizer_model_max_length: int,
645
+ target_utilization: float = TARGET_MAX_LEN_UTILIZATION,
646
+ base_max_len: int = BASE_MAX_LEN,
647
+ increment: int = MAX_LEN_INCREMENT,
648
+ max_cap: int = MAX_LEN_CAP,
649
+ estimated_base_dataset_ram_gb: float = ESTIMATED_BASE_DATASET_RAM_GB,
650
+ cost_per_increment_gb: float = COST_PER_INCREMENT_GB
651
+ ) -> int:
652
+ """
653
+ Ajusta automaticamente MAX_LEN conforme RAM disponível e limite do modelo.
654
+ """
655
+ # RAM que pode ser usada para o dataset tokenizado
656
+ target_dataset_ram_gb = TOTAL_RAM_GB * target_utilization
657
+ current_max_len = base_max_len
658
+
659
+ if target_dataset_ram_gb < estimated_base_dataset_ram_gb:
660
+ log_warning(f"RAM disponível ({target_dataset_ram_gb:.1f} GB) é menor que a RAM estimada para MAX_LEN base ({estimated_base_dataset_ram_gb:.1f} GB). Usando MAX_LEN={base_max_len}.")
661
+ return min(base_max_len, tokenizer_model_max_length)
662
+
663
+ # Quantidade de RAM que podemos usar para expandir o MAX_LEN além do custo base
664
+ expandable_ram_gb = target_dataset_ram_gb - estimated_base_dataset_ram_gb
665
+
666
+ if expandable_ram_gb > 0:
667
+ num_increments = int(expandable_ram_gb / cost_per_increment_gb)
668
+ current_max_len += num_increments * increment
669
+
670
+ # Limitar pelo MAX_LEN_CAP e pelo limite do modelo
671
+ final_max_len = min(current_max_len, max_cap, tokenizer_model_max_length)
672
+
673
+ # logging.info(f"RAM total: {TOTAL_RAM_GB:.1f} GB. RAM alvo para dataset: {target_dataset_ram_gb:.1f} GB.")
674
+ # logging.info(f"Ajustando MAX_LEN dinamicamente -> {final_max_len} (limite do modelo: {tokenizer_model_max_length})")
675
+
676
+ logging.info(
677
+ f"RAM total: {TOTAL_RAM_GB:.1f} GB. RAM alvo para dataset: {target_dataset_ram_gb:.1f} GB.")
678
+ logging.info(
679
+ f"Ajustando MAX_LEN dinamicamente -> {final_max_len} "
680
+ f"(limite efetivo do modelo/tokenizer: {tokenizer_model_max_length})"
681
+ )
682
+
683
+ return final_max_len
684
+
685
+ # ==============================
686
+ # 3.Optimizer Refresh Callback
687
+ # ==============================
688
+ class OptimizerRefreshCallback(TrainerCallback):
689
+ def __init__(self, refresh_interval_sec: int = OPTIMIZER_REFRESH_INTERVAL):
690
+ self.refresh_interval = refresh_interval_sec
691
+ self.last_refresh_time = time.time()
692
+
693
+ def on_step_end(self, args, state, control, **kwargs):
694
+ trainer = kwargs.get("trainer", None)
695
+ if trainer is None:
696
+ return control
697
+
698
+ now = time.time()
699
+ elapsed = now - self.last_refresh_time
700
+
701
+ if elapsed >= self.refresh_interval:
702
+ logging.info(f"🔄 Refresh do otimizador após {elapsed:.1f}s (passo {state.global_step})")
703
+
704
+ # Faz o refresh
705
+ if hasattr(trainer, "scaler") and trainer.scaler is not None:
706
+ trainer.scaler.step(trainer.optimizer)
707
+ trainer.scaler.update()
708
+ else:
709
+ trainer.optimizer.step()
710
+
711
+ trainer.optimizer.zero_grad()
712
+ self.last_refresh_time = now
713
+
714
+ return control
715
+
716
+ # ========================================================
717
+ # 4.CALLBACK DINÂMICO PARA AJUSTE DE GRADIENT ACCUMULATION
718
+ # ========================================================
719
+ class DynamicAccumulationCallback(TrainerCallback):
720
+ def __init__(
721
+ self,
722
+ base_batch: int = BASE_BATCH_SIZE,
723
+ max_steps: int = DYNAMIC_ACCUMULATION_MAX_STEPS,
724
+ high_ram_limit: int = DYNAMIC_ACCUMULATION_HIGH_RAM_LIMIT,
725
+ low_ram_limit: int = DYNAMIC_ACCUMULATION_LOW_RAM_LIMIT,
726
+ step_interval: int = 5, # nº de steps entre verificações
727
+ time_interval: int = 240 # intervalo mínimo em segundos (10 min)
728
+ ):
729
+ self.base_batch = base_batch
730
+ self.max_steps = max_steps
731
+ self.high_ram_limit = high_ram_limit
732
+ self.low_ram_limit = low_ram_limit
733
+ self.step_interval = step_interval
734
+ self.time_interval = time_interval
735
+
736
+ # controle interno
737
+ self.last_check_step = 0
738
+ self.last_check_time = time.time()
739
+
740
+ def on_step_end(self, args, state, control, **kwargs):
741
+ global CURRENT_ACCUM_STEPS
742
+
743
+ current_step = state.global_step
744
+ now = time.time()
745
+
746
+ # --- 1️⃣ Verifica se há override manual ativo ---
747
+ if CURRENT_ACCUM_STEPS is not None:
748
+ adjusted_steps = max(1, min(self.max_steps, CURRENT_ACCUM_STEPS))
749
+ args.gradient_accumulation_steps = adjusted_steps
750
+ effective_batch = adjusted_steps * self.base_batch
751
+
752
+ train_progress["current_accum_steps"] = adjusted_steps
753
+ logging.info(
754
+ f"🧭 Ajuste manual aplicado → gradient_accumulation_steps = {adjusted_steps} "
755
+ f"(Effective Batch: {effective_batch})"
756
+ )
757
+ CURRENT_ACCUM_STEPS = None
758
+
759
+ return control
760
+
761
+ # --- 2️⃣ Verifica se já passou o intervalo mínimo ---
762
+ step_diff = current_step - self.last_check_step
763
+ time_diff = now - self.last_check_time
764
+
765
+ if step_diff < self.step_interval and time_diff < self.time_interval:
766
+ # ainda não é hora de checar novamente
767
+ return control
768
+
769
+ # Atualiza controle de tempo e step
770
+ self.last_check_step = current_step
771
+ self.last_check_time = now
772
+
773
+ # --- 3️⃣ Ajuste automático baseado na RAM ---
774
+ mem = psutil.virtual_memory()
775
+ used = mem.percent
776
+
777
+ current_steps = args.gradient_accumulation_steps
778
+
779
+ # RAM alta → reduzir steps
780
+ if used > self.high_ram_limit and current_steps > 1:
781
+ new_steps = max(1, math.floor(current_steps / 1.6))
782
+ args.gradient_accumulation_steps = new_steps
783
+ new_effective_batch = new_steps * self.base_batch # ✅ novo valor após ajuste
784
+
785
+ train_progress["current_accum_steps"] = new_steps
786
+ logging.warning(
787
+ f"⚠️ RAM alta ({used:.1f}%) → diminuindo accumulation_steps "
788
+ f"de {current_steps} para {new_steps} "
789
+ f"(Effective Batch: {new_effective_batch})"
790
+ )
791
+
792
+ # RAM ociosa → aumentar steps
793
+ elif used < self.low_ram_limit and current_steps < self.max_steps:
794
+ new_steps = current_steps + 1
795
+ args.gradient_accumulation_steps = new_steps
796
+ new_effective_batch = new_steps * self.base_batch # ✅ recalculado após aumento
797
+
798
+ train_progress["current_accum_steps"] = new_steps
799
+ logging.info(
800
+ f"💡 RAM ociosa ({used:.1f}%) → aumentando accumulation_steps "
801
+ f"de {current_steps} para {new_steps} "
802
+ f"(Effective Batch: {new_effective_batch})"
803
+ )
804
+
805
+
806
+ return control
807
+ #----------------------------
808
+ # Sistema de Logs Coloridos
809
+ class Colors:
810
+ HEADER = "\033[95m"
811
+ OKBLUE = "\033[94m"
812
+ OKCYAN = "\033[96m"
813
+ OKGREEN = "\033[92m"
814
+ WARNING = "\033[93m"
815
+ FAIL = "\033[91m"
816
+ ENDC = "\033[0m"
817
+ BOLD = "\033[1m"
818
+ UNDERLINE = "\033[4m"
819
+
820
+ def log_info(msg):
821
+ time_str = datetime.now().strftime("%H:%M:%S")
822
+ print(f"{Colors.OKCYAN}[INFO]{Colors.ENDC} [{time_str}] {msg}")
823
+ training_logs.append(f"[INFO] [{time_str}] {msg}")
824
+
825
+ def log_success(msg):
826
+ time_str = datetime.now().strftime("%H:%M:%S")
827
+ print(f"{Colors.OKGREEN}[SUCESSO]{Colors.ENDC} [{time_str}] {msg}")
828
+ training_logs.append(f"[SUCESSO] [{time_str}] {msg}")
829
+
830
+ def log_warning(msg):
831
+ time_str = datetime.now().strftime("%H:%M:%S")
832
+ print(f"{Colors.WARNING}[AVISO]{Colors.ENDC} [{time_str}] {msg}")
833
+ training_logs.append(f"[AVISO] [{time_str}] {msg}")
834
+
835
+ def log_error(msg):
836
+ time_str = datetime.now().strftime("%H:%M:%S")
837
+ print(f"{Colors.FAIL}[ERRO]{Colors.ENDC} [{time_str}] {msg}")
838
+ training_logs.append(f"[ERRO] [{time_str}] {msg}")
839
+
840
+ def log_step(msg, step=None, percent=None):
841
+ time_str = datetime.now().strftime("%H:%M:%S")
842
+ prefix = f"[{time_str}]"
843
+ progress = f" | Passo {step}" if step is not None else ""
844
+ pct = f" ({percent:.1f}%)" if percent is not None else ""
845
+ print(f"{Colors.OKBLUE}{prefix}{Colors.ENDC} {msg}{progress}{pct}")
846
+ training_logs.append(f"{prefix} {msg}{progress}{pct}")
847
+
848
+ #---------------------------------------------------------------
849
+ # Pega o logger padrão do Python
850
+ # logger = logging.getLogger(__name__) # (Comentado porque você usa funções customizadas)
851
+ # Global trackers para o cálculo de tempo
852
+ g_last_step_time = [time.time()]
853
+ g_last_step_logged = [-1]
854
+
855
+ class SimpleStepTimerCallback(TrainerCallback):
856
+ """
857
+ Loga o tempo gasto em cada passo individual APENAS em segundos.
858
+ Usa o logger do Trainer para que se misture com os logs normais.
859
+ """
860
+ def on_step_begin(self, args, state, control, **kwargs):
861
+ # Reinicia o tempo no início de cada passo para cálculo preciso
862
+ if state.is_local_process_zero:
863
+ g_last_step_time[0] = time.time()
864
+ g_last_step_logged[0] = state.global_step
865
+
866
+ def on_step_end(self, args, state, control, **kwargs):
867
+ if not state.is_local_process_zero:
868
+ return
869
+
870
+ step = state.global_step
871
+
872
+ # Evita logar o passo '0' se on_step_begin não foi executado antes
873
+ if step == 0:
874
+ return
875
+
876
+ # O tempo inicial foi registrado em on_step_begin para este passo.
877
+ now = time.time()
878
+ elapsed_for_this_step = now - g_last_step_time[0]
879
+
880
+ # --- Geração da Mensagem Apenas em Segundos ---
881
+ # Formata o tempo para ter no máximo 3 casas decimais, por exemplo: 9.605s
882
+ time_str = f"{elapsed_for_this_step:.3f}s"
883
+
884
+ msg = (
885
+ f"PASSO: - - | {step}/{state.max_steps} | "
886
+ f"Tempo: | {time_str} |"
887
+ )
888
+
889
+ # O ponto crucial: Logar a mensagem usando o logger do Trainer
890
+ # (Trainer.log é o método do Trainer que faz o logging apropriado)
891
+ if hasattr(self, 'trainer') and hasattr(self.trainer, 'log'):
892
+ # Se a callback tiver acesso direto ao trainer (como acontece em implementações mais modernas)
893
+ self.trainer.log({"step_time": elapsed_for_this_step, "log_msg": msg})
894
+ else:
895
+ # Se for uma implementação mais simples que usa args, state, control, podemos usar o logger global
896
+ # Este é um fallback, mas o melhor é usar o logger do transformers
897
+ logger = kwargs.get('logger', None) # Tenta obter o logger se passado nos kwargs
898
+ if logger:
899
+ logger.info(f">> {msg}")
900
+ else:
901
+ # Se você tiver configurado um logger global no seu script:
902
+ # import logging
903
+ # logging.info(f">> {msg}")
904
+ # OU: Para garantir que apareça onde os seus outros logs de data e IP aparecem,
905
+ # use o logging padrão do Python.
906
+ import logging
907
+ logging.info(f">> {msg}")
908
+
909
+ # Opcional: Registar também na sua lista de logs geral, usando log_info.
910
+ # log_info(msg)
911
+
912
+ #----------------------------------------------------
913
+ # Callback para logs detalhados do treino
914
+ #----------------------------------------------------
915
+ last_log_time = [time.time()]
916
+ last_logged_step = {"step": -1}
917
+
918
+ class LogCallback(TrainerCallback):
919
+ def on_log(self, args, state, control, logs=None, **kwargs):
920
+ global train_progress
921
+ if not state.is_local_process_zero or not logs:
922
+ return
923
+
924
+ step = state.global_step
925
+ total_steps = state.max_steps
926
+ loss = logs.get("loss")
927
+ eval_loss = logs.get("eval_loss")
928
+ lr = logs.get("learning_rate")
929
+ accum = train_progress.get("current_accum_steps", args.gradient_accumulation_steps)
930
+
931
+ # Evita repetir o log para o mesmo step
932
+ if step == last_logged_step["step"]:
933
+ return
934
+ last_logged_step["step"] = step
935
+
936
+ now = time.time()
937
+ elapsed = now - last_log_time[0]
938
+ last_log_time[0] = now
939
+
940
+ if loss is not None and total_steps > 0:
941
+ percent = (step / total_steps * 100)
942
+ mem = psutil.virtual_memory()
943
+ free_gb = mem.available / (1024**3)
944
+ msg = (
945
+ f"Treino: step {step}/{total_steps} | Perda: {loss:.4f} | "
946
+ f"Tempo/step: {elapsed:.2f}s | Memória usada: {mem.percent:.1f}% | "
947
+ f"LR: {lr:.2e} | Accum: {accum} |"
948
+ )
949
+ log_step(msg, step=step, percent=percent)
950
+ train_progress.update({
951
+ "current": step,
952
+ "total": total_steps,
953
+ "percent": int(percent),
954
+ "status": "training",
955
+ "message": msg
956
+ })
957
+
958
+ elif eval_loss is not None:
959
+ msg = f"Avaliação: step {step}/{total_steps} | Eval Loss: {eval_loss:.4f}"
960
+ try:
961
+ perplexity = math.exp(eval_loss) if eval_loss < 20 else float("inf")
962
+ msg += f" | Perplexidade: {perplexity:.2f}"
963
+ except:
964
+ pass
965
+ log_info(msg)
966
+ train_progress["message"] = msg
967
+ #--------------------------------------------------------------
968
+ def on_epoch_end(self, args, state, control, **kwargs):
969
+ global epoch_losses
970
+ if state.is_local_process_zero:
971
+ # Encontrar o log da época mais recente que contenha a perda de treino
972
+ # Ou o log de avaliação da época, se houver
973
+ epoch_log_loss = next((log for log in reversed(state.log_history) if 'loss' in log and 'epoch' in log and int(log['epoch']) == int(state.epoch) and 'eval_loss' not in log), None)
974
+ epoch_log_eval_loss = next((log for log in reversed(state.log_history) if 'eval_loss' in log and 'epoch' in log and int(log['epoch']) == int(state.epoch)), None)
975
+
976
+ loss_msg = ""
977
+ if epoch_log_loss and 'loss' in epoch_log_loss:
978
+ loss = epoch_log_loss['loss']
979
+ loss_msg += f"Perda Média de Treino: {loss:.4f}"
980
+ # Adicionar apenas o loss de treino para a lista de perdas de época
981
+ epoch_losses.append({"epoch": int(state.epoch), "loss": loss})
982
+
983
+ if epoch_log_eval_loss and 'eval_loss' in epoch_log_eval_loss:
984
+ eval_loss = epoch_log_eval_loss['eval_loss']
985
+ if loss_msg:
986
+ loss_msg += " | "
987
+ loss_msg += f"Perda de Avaliação: {eval_loss:.4f}"
988
+
989
+ if loss_msg:
990
+ log_success(f"Fim da Época {int(state.epoch)} | {loss_msg}")
991
+ else:
992
+ log_success(f"Fim da Época {int(state.epoch)}")
993
+
994
+ #--------------------------------------------------
995
+ # Variáveis Globais
996
+ model = None # variável global
997
+ tokenizer = None
998
+
999
+ train_progress = {
1000
+ "current": 0,
1001
+ "total": 1,
1002
+ "percent": 0,
1003
+ "status": "not started",
1004
+ "message": "Aguardando início do treino."
1005
+ }
1006
+ training_logs = [] # se já usas este array, mantém; (ver #3 para acrescentar linhas nele)
1007
+ epoch_losses = []
1008
+ all_data = [] # Armazena os dados do último treino para poder continuar
1009
+
1010
+ chat_model = None
1011
+ chat_tokenizer = None
1012
+ CURRENT_ACCUM_STEPS = None
1013
+ #CURRENT_ACCUM_STEPS = 1 # Valor inicial padrão
1014
+
1015
+ # --- NOVOS CAMPOS (para interface mostrar info de accumulation e nº total de passos) ---
1016
+ LAST_ACCUM_ORIGIN = None # "manual" | "auto" | None
1017
+ TOTAL_TRAIN_STEPS = None # total de passos desta execução (após tokenização/Trainer)
1018
+ BASE_BATCH_SIZE_EFFECTIVE = 4 # (opcional) mantém aqui para calcular Effective Batch no /status
1019
+
1020
+ # -------------------------------------------------
1021
+ training_logs.append(f"[INFO] accumulation inicial = {CURRENT_ACCUM_STEPS} (auto)")
1022
+ # --- Execução dos Cálculos Iniciais ---
1023
+ ACCUMULATION_STEPS = ajustar_accumulation_steps()
1024
+ EFFECTIVE_BATCH_SIZE = BASE_BATCH_SIZE * ACCUMULATION_STEPS
1025
+
1026
+ LAST_ACCUM_ORIGIN = "auto" # indica que veio do ajuste automático
1027
+ BASE_BATCH_SIZE_EFFECTIVE = EFFECTIVE_BATCH_SIZE
1028
+
1029
+ #----------------------------
1030
+ # Função principal de treino
1031
+ #----------------------------
1032
+ def train_model_lora(
1033
+ file_data, epochs, model_path, output_dir, accumulation_steps,
1034
+ dataloader_workers, mode, lora_adapter_to_load=None,
1035
+ initial_epochs_completed=0, train_mode="new_train"
1036
+ ):
1037
+ """
1038
+ Função principal de treino LoRA (Low-Rank Adaptation)
1039
+ Corrigido para evitar IndentationError e melhorar a legibilidade.
1040
+ """
1041
+ # ---------------- VARIÁVEIS GLOBAIS ----------------
1042
+ global training_logs, train_progress, epoch_losses
1043
+ training_logs.clear()
1044
+ train_progress.update({
1045
+ "status": "loading",
1046
+ "percent": 0,
1047
+ "message": "Iniciando processo de treino..."
1048
+ })
1049
+ log_info("Iniciando processo de treino...")
1050
+ all_data = file_data
1051
+
1052
+ # ---------------- LOGGING INICIAL ----------------
1053
+ # log_info(f"CPU: {CPU_COUNT} núcleos, {TOTAL_RAM_GB:.1f} GB RAM")
1054
+ log_info(f"OMP Threads: {OMP_THREADS}, Dataloader Workers: {DATALOADER_WORKERS}")
1055
+ log_info(f"Batch base: {BASE_BATCH_SIZE}, Steps iniciais: {ACCUMULATION_STEPS}")
1056
+ log_info(f"Batch efetivo inicial: {EFFECTIVE_BATCH_SIZE}")
1057
+
1058
+ # ----------------------------------------------------
1059
+ # O status inicial é "starting" ou "resuming"
1060
+ initial_status_message = "Iniciando processo de treino..."
1061
+ if lora_adapter_to_load:
1062
+ initial_status_message = "Retomando treino a partir de adaptador LoRA salvo..."
1063
+
1064
+ train_progress = {
1065
+ "current": 0,
1066
+ "total": 1,
1067
+ "percent": 0,
1068
+ "status": "starting",
1069
+ "message": initial_status_message
1070
+ }
1071
+ log_info(initial_status_message)
1072
+
1073
+ # Guarda os dados para a função 'continuar'
1074
+ all_data = file_data # Atualiza a global all_data com os dados atuais do treino
1075
+
1076
+ # ----------------------------------------------------
1077
+ # Função auxiliar: detectar camadas LoRA automaticamente
1078
+ def guess_lora_targets(model):
1079
+ names = {name for name, _ in model.named_modules()}
1080
+ if any("q_proj" in n and "v_proj" in n for n in names):
1081
+ return ["q_proj", "v_proj"]
1082
+ if any("query_key_value" in n for n in names):
1083
+ return ["query_key_value"]
1084
+ if any("Wqkv" in n for n in names):
1085
+ return ["Wqkv"]
1086
+ if any("c_attn" in n for n in names):
1087
+ return ["c_attn"]
1088
+ log_warning("Não foi possível detetar os target_modules. Usando ['q_proj', 'v_proj'] por defeito.")
1089
+ return ["q_proj", "v_proj"]
1090
+
1091
+ # ----------------------------------------------------
1092
+ # BLOCO NOVO PARA XPU
1093
+ """
1094
+ Seleção de dispositivo (CPU / CUDA / XPU)
1095
+ Este patch substitui o bloco onde escolhes device_map/dtype durante o carregamento do modelo.
1096
+ Adiciona a opção "xpu" antes de cair para CUDA/CPU.
1097
+ """
1098
+
1099
+ # ----------------------------------------------------
1100
+ # Bloco 1: Carregar Modelo e Tokenizer
1101
+ try:
1102
+ log_step("A carregar modelo e tokenizer base...")
1103
+
1104
+ os.makedirs("offload", exist_ok=True)
1105
+
1106
+ global tokenizer
1107
+ tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=True)
1108
+
1109
+ # Garantir tokens especiais e consistência
1110
+ log_info(f"Tokens antes do ajuste: PAD={tokenizer.pad_token}, EOS={tokenizer.eos_token}, BOS={tokenizer.bos_token}")
1111
+ if tokenizer.pad_token is None:
1112
+ tokenizer.add_special_tokens({'pad_token': '<PAD>'})
1113
+ if tokenizer.eos_token is None:
1114
+ tokenizer.add_special_tokens({'eos_token': '</s>'})
1115
+ if tokenizer.bos_token is None:
1116
+ tokenizer.add_special_tokens({'bos_token': '<s>'})
1117
+ if tokenizer.unk_token is None:
1118
+ tokenizer.add_special_tokens({'unk_token': '<unk>'})
1119
+ log_info(
1120
+ f"Tokens após ajuste: PAD={tokenizer.pad_token}, "
1121
+ f"EOS={tokenizer.eos_token}, BOS={tokenizer.bos_token}, "
1122
+ f"UNK={tokenizer.unk_token}"
1123
+ )
1124
+
1125
+ # ------- Substituído para XPU ----------
1126
+ # Seleção de dispositivo (CPU / CUDA / XPU)
1127
+ use_xpu = hasattr(torch, "xpu") and torch.xpu.is_available()
1128
+ if use_xpu:
1129
+ # Em Intel, BF16 costuma ser a melhor escolha
1130
+ model_dtype = torch.bfloat16
1131
+ model_device_map = {"": "xpu"}
1132
+ print(f"[INFO] [{_ts()}] Device selecionado: XPU (Intel iGPU) | dtype={model_dtype}")
1133
+ elif torch.cuda.is_available():
1134
+ model_dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
1135
+ model_device_map = "auto"
1136
+ print(f"[INFO] [{_ts()}] Device selecionado: CUDA | dtype={model_dtype}")
1137
+ else:
1138
+ model_dtype = torch.bfloat16 # bf16 em CPU é ~2x mais rápido que float32
1139
+ model_device_map = "cpu"
1140
+ print(f"[INFO] [{_ts()}] Device selecionado: CPU | dtype={model_dtype}")
1141
+
1142
+ # Carregar modelo base
1143
+ model = AutoModelForCausalLM.from_pretrained(
1144
+ model_path,
1145
+ device_map=model_device_map,
1146
+ offload_folder="offload",
1147
+ dtype=model_dtype,
1148
+ low_cpu_mem_usage=True
1149
+ )
1150
+
1151
+ # Fallback: se o device_map não moveu o modelo (versões antigas)
1152
+ if use_xpu:
1153
+ try:
1154
+ model.to("xpu")
1155
+ except Exception as e:
1156
+ print(f"[WARN] [{_ts()}] .to('xpu') falhou (Transformers antigo?). "
1157
+ f"A continuar com device_map… Detalhe: {e}")
1158
+
1159
+ model.resize_token_embeddings(len(tokenizer))
1160
+ model.config.use_cache = False
1161
+ model.gradient_checkpointing_enable() # economiza memória e acelera CPU
1162
+ # ------- Fim do bloco novo XPU ----------
1163
+
1164
+ #-------------------------------------------------
1165
+ # --- Variáveis para controle de fluxo ---
1166
+ resume_from_trainer_checkpoint = None
1167
+ lora_adapter_path_final = os.path.join(output_dir, "lora_model")
1168
+ lora_adapter_to_load_for_training = None # Variável para controlar qual LoRA carregar para o treino
1169
+ lora_model_loaded_for_initial_eval = False # Adicione esta variável para controle
1170
+
1171
+ #---------------------------------------------------------------------
1172
+ # --- Lógica de 4 opções ---
1173
+ # Prioridade:
1174
+ #1. Checkpoint do Trainer
1175
+ #2. LoRA Final Salvo
1176
+ #3. LoRA passado como parâmetro (para continuar)
1177
+ #4. Novo LoRA
1178
+
1179
+ # 1. Tentar encontrar o checkpoint mais recente do Trainer
1180
+ checkpoint_dirs = [d for d in os.listdir(output_dir) if d.startswith("checkpoint-")]
1181
+ if checkpoint_dirs:
1182
+ checkpoint_dirs.sort(key=lambda x: int(x.split('-')[1]), reverse=True)
1183
+ latest_candidate_checkpoint_dir = os.path.join(output_dir, checkpoint_dirs[0])
1184
+
1185
+ trainer_state_exists = os.path.exists(os.path.join(latest_candidate_checkpoint_dir, "trainer_state.json"))
1186
+ lora_adapter_exists_in_checkpoint = (
1187
+ os.path.exists(os.path.join(latest_candidate_checkpoint_dir, "adapter_model.safetensors")) or
1188
+ os.path.exists(os.path.join(latest_candidate_checkpoint_dir, "adapter_model.bin"))
1189
+ )
1190
+
1191
+ if trainer_state_exists and lora_adapter_exists_in_checkpoint:
1192
+ resume_from_trainer_checkpoint = latest_candidate_checkpoint_dir
1193
+ lora_adapter_to_load_for_training = latest_candidate_checkpoint_dir # Usar este LoRA para o treino
1194
+ log_info(f"Opção 1: Encontrado checkpoint COMPLETO do Trainer em: {resume_from_trainer_checkpoint}. O treino será retomado daqui.")
1195
+
1196
+ # Extrair initial_epochs_completed do trainer_state.json
1197
+ trainer_state_path = os.path.join(resume_from_trainer_checkpoint, "trainer_state.json")
1198
+ if os.path.exists(trainer_state_path):
1199
+ with open(trainer_state_path, 'r') as f:
1200
+ trainer_state = json.load(f)
1201
+ initial_epochs_completed = int(trainer_state.get('epoch', 0))
1202
+ log_info(f"Checkpoint do Trainer encontrado. Épocas já completadas: {initial_epochs_completed}")
1203
+ else:
1204
+ log_warning(f"trainer_state.json não encontrado em {resume_from_trainer_checkpoint}. Assumindo 0 épocas completadas.")
1205
+ else:
1206
+ log_info(f"Diretório '{latest_candidate_checkpoint_dir}' encontrado, mas NÃO é um checkpoint COMPLETO do Trainer (faltam trainer_state.json ou arquivos do adaptador LoRA).")
1207
+
1208
+ #------------------------------------------------------------------------------------------------------------------------------------------------
1209
+ # 2. Se não encontrou checkpoint do Trainer, tentar carregar um modelo LoRA final salvo (para avaliação/decisão)
1210
+ # Esta opção deve ter prioridade sobre o LoRA final salvo, se o usuário explicitamente pediu para continuar com um LoRA.
1211
+ if not resume_from_trainer_checkpoint and os.path.exists(lora_adapter_path_final) and any(os.scandir(lora_adapter_path_final)):
1212
+ try:
1213
+ # Carrega o LoRA final para avaliação/decisão, mas não para retomar o treino do Trainer
1214
+ model = PeftModel.from_pretrained(model, lora_adapter_path_final, device_map=model_device_map)
1215
+ for name, param in model.named_parameters():
1216
+ if "lora" in name:
1217
+ param.requires_grad = True
1218
+
1219
+ lora_adapter_to_load_for_training = lora_adapter_to_load # Usar este LoRA para o treino
1220
+ log_info(f"Opção 2: Carregado adaptador LoRA de '{lora_adapter_to_load}' para continuar o treino.")
1221
+ # Se estamos continuando, precisamos saber as épocas já completadas.
1222
+ # Se lora_adapter_to_load é um checkpoint, podemos tentar ler o trainer_state.json
1223
+ # Caso contrário, assumimos que é um LoRA final e o treino começa "do zero" em termos de Trainer state.
1224
+ if "checkpoint-" in lora_adapter_to_load: # Se o LoRA passado é um checkpoint
1225
+ trainer_state_path = os.path.join(lora_adapter_to_load, "trainer_state.json")
1226
+ if os.path.exists(trainer_state_path):
1227
+ with open(trainer_state_path, 'r') as f:
1228
+ trainer_state = json.load(f)
1229
+ initial_epochs_completed = int(trainer_state.get('epoch', 0))
1230
+ log_info(f"LoRA de checkpoint encontrado. Épocas já completadas: {initial_epochs_completed}")
1231
+ else:
1232
+ log_warning(f"trainer_state.json não encontrado em {lora_adapter_to_load}. Assumindo 0 épocas completadas para continuação.")
1233
+ else:
1234
+ # Se é um LoRA final, não há estado de Trainer para retomar, então initial_epochs_completed permanece 0
1235
+ log_info("LoRA final carregado para continuação. Treino do Trainer começará do zero.")
1236
+
1237
+ except Exception as e:
1238
+ log_error(f"Falha ao carregar LoRA de '{lora_adapter_to_load}' para continuar o treino: {e}. Iniciando um novo LoRA.")
1239
+ lora_adapter_to_load = None
1240
+ lora_adapter_to_load_for_training = None # Resetar para criar um novo LoRA
1241
+
1242
+ # 3. Se não há checkpoint do Trainer nem LoRA final, mas um LoRA foi passado para continuar
1243
+ # Esta opção só deve ser considerada se NENHUMA das opções anteriores foi ativada.
1244
+ if not resume_from_trainer_checkpoint and not lora_adapter_to_load_for_training and os.path.exists(lora_adapter_path_final) and any(os.scandir(lora_adapter_path_final)):
1245
+ try:
1246
+ # Carrega o LoRA final para avaliação/decisão, mas não para retomar o treino do Trainer
1247
+ model = PeftModel.from_pretrained(model, lora_adapter_path_final, device_map=model_device_map)
1248
+ for name, param in model.named_parameters():
1249
+ if "lora" in name:
1250
+ param.requires_grad = True
1251
+ lora_model_loaded_for_initial_eval = True
1252
+ lora_adapter_to_load_for_training = lora_adapter_path_final # Usar este LoRA para o treino
1253
+ log_info(f"Opção 3: Carregado adaptador LoRA final de: {lora_adapter_path_final}. Será avaliado e aguardará decisão do usuário.")
1254
+ # Se carregamos um LoRA final, não há estado de Trainer para retomar, então initial_epochs_completed permanece 0
1255
+ initial_epochs_completed = 0 # Garante que é 0 se não veio de um checkpoint completo
1256
+ except Exception as e:
1257
+ log_warning(f"Falha ao carregar LoRA final de {lora_adapter_path_final}: {e}. Prosseguindo sem ele.")
1258
+ lora_adapter_path_final = None
1259
+ lora_model_loaded_for_initial_eval = False
1260
+ lora_adapter_to_load_for_training = None # Resetar para criar um novo LoRA
1261
+
1262
+ # --- Carregar o LoRA no modelo base ANTES de inicializar o Trainer ---
1263
+ # Este bloco garante que o modelo base tenha o LoRA aplicado antes do Trainer ser instanciado.
1264
+ # Ele só deve ser executado se um LoRA foi identificado para ser carregado
1265
+ # E se o modelo ainda não é um PeftModel (ou seja, não foi carregado nas opções 1, 2 ou 3 acima)
1266
+ if lora_adapter_to_load_for_training and not isinstance(model, PeftModel):
1267
+ try:
1268
+ model = PeftModel.from_pretrained(model, lora_adapter_to_load_for_training, device_map=model_device_map)
1269
+ for name, param in model.named_parameters():
1270
+ if "lora" in name:
1271
+ param.requires_grad = True
1272
+ log_info(f"Aplicado adaptador LoRA '{lora_adapter_to_load_for_training}' ao modelo base.")
1273
+ except Exception as e:
1274
+ log_error(f"Falha crítica ao aplicar LoRA '{lora_adapter_to_load_for_training}' ao modelo base: {e}. Iniciando um novo LoRA.")
1275
+ lora_adapter_to_load_for_training = None
1276
+ resume_from_trainer_checkpoint = None # Não podemos retomar o Trainer se o LoRA falhou
1277
+ initial_epochs_completed = 0 # Resetar se falhou
1278
+
1279
+ # 4. Se nenhuma das opções acima, criar um novo adaptador LoRA
1280
+ if not lora_adapter_to_load_for_training: # Se nenhum LoRA foi carregado/selecionado
1281
+ targets = guess_lora_targets(model)
1282
+ lora_config = LoraConfig(
1283
+ r=16, lora_alpha=32, target_modules=targets, lora_dropout=0.05, bias="none", task_type=TaskType.CAUSAL_LM
1284
+ )
1285
+ model = get_peft_model(model, lora_config)
1286
+ log_info(f"Opção 1: Nenhum checkpoint ou LoRA anterior encontrado. Novo adaptador LoRA criado (targets={targets}).")
1287
+ resume_from_trainer_checkpoint = None # Se é um novo LoRA, não há estado de Trainer para retomar
1288
+
1289
+ if hasattr(model, "print_trainable_parameters"):
1290
+ model.print_trainable_parameters()
1291
+ else:
1292
+ log_warning("print_trainable_parameters() não disponível neste modelo base.")
1293
+
1294
+ #-----------------------
1295
+ # Antes de calcular total_target_epochs, ajuste initial_epochs_completed
1296
+ # Determine o número de épocas que o Trainer deve executar
1297
+ # Este é o valor que será passado para num_train_epochs do TrainingArguments
1298
+ if train_mode == "new_train":
1299
+ # Para um novo treino, o Trainer deve executar 'epochs' vezes.
1300
+ # initial_epochs_completed é 0, o que é handled pelo Trainer.
1301
+ num_epochs_for_trainer = float(epochs)
1302
+ log_info(f"Modo 'new_train' detectado. O treino será executado por {num_epochs_for_trainer} épocas.")
1303
+
1304
+ elif train_mode == "continue_train":
1305
+ # Para continuar, o Trainer deve executar até o 'initial_epochs_completed + epochs' total.
1306
+ # Se você quer fazer mais 'X' épocas, o total final será initial_epochs_completed + X.
1307
+ num_epochs_for_trainer = float(initial_epochs_completed + epochs)
1308
+ log_info(f"Modo 'continue_train' detectado. Treino retomará da época {initial_epochs_completed} e visa um total de {num_epochs_for_trainer} épocas.")
1309
+
1310
+ else: # Default para new_train
1311
+ log_warning(f"Modo de treino desconhecido: {train_mode}. Assumindo 'new_train'.")
1312
+ num_epochs_for_trainer = float(epochs)
1313
+
1314
+ #-----------------------
1315
+ model.train()
1316
+ total_target_epochs = initial_epochs_completed + epochs
1317
+
1318
+ # ----------- Preparar/tokenizar dataset -----------
1319
+ tokenized_path = os.path.join(output_dir, "tokenized_dataset")
1320
+
1321
+ # --- CHAMADA PARA AJUSTAR MAX_LEN DINAMICAMENTE --- desativado ver função em baixo ativa
1322
+ # Chamar a função de ajuste de MAX_LEN aqui, antes de tokenizar
1323
+ # Passar o limite do tokenizer para a função
1324
+
1325
+ # vereficar nomes das funçoes - podem estar erradas
1326
+ # --- Após carregar modelo e tokenizer -----------------------------
1327
+ # Sanear limites de contexto do modelo e do tokenizer
1328
+
1329
+ # 1) Modelo
1330
+ max_len_model = getattr(model.config, "max_position_embeddings", None)
1331
+ if not (isinstance(max_len_model, int) and max_len_model > 0):
1332
+ max_len_model = DEFAULT_FALLBACK_CAP
1333
+
1334
+ # 2) Tokenizer
1335
+ tok_max_len_raw = getattr(tokenizer, "model_max_length", None)
1336
+ if (tok_max_len_raw is None
1337
+ or not isinstance(tok_max_len_raw, int)
1338
+ or tok_max_len_raw <= 0
1339
+ or tok_max_len_raw > TOKENIZER_SENTINEL_CAP):
1340
+ tok_max_len_raw = DEFAULT_FALLBACK_CAP
1341
+
1342
+ # 3) Limite efetivo
1343
+ effective_model_max_len = min(max_len_model, tok_max_len_raw)
1344
+
1345
+ # 4) Ajuste dinâmico com o teu orçamento de RAM
1346
+ DYNAMIC_MAX_LEN = ajustar_max_len(
1347
+ tokenizer_model_max_length=effective_model_max_len,
1348
+ target_utilization=TARGET_MAX_LEN_UTILIZATION,
1349
+ base_max_len=BASE_MAX_LEN,
1350
+ increment=MAX_LEN_INCREMENT,
1351
+ max_cap=MAX_LEN_CAP,
1352
+ estimated_base_dataset_ram_gb=ESTIMATED_BASE_DATASET_RAM_GB,
1353
+ cost_per_increment_gb=COST_PER_INCREMENT_GB
1354
+ )
1355
+ logging.info(f"MAX_LEN dinâmico definido para: {DYNAMIC_MAX_LEN}")
1356
+
1357
+ # ------------------------------------------------------------------
1358
+
1359
+ if os.path.exists(tokenized_path):
1360
+ log_info("Dataset tokenizado encontrado no disco. A carregar...")
1361
+ tokenized_datasets = load_from_disk(tokenized_path)
1362
+ # Verificar se o MAX_LEN do dataset carregado é compatível com o DYNAMIC_MAX_LEN
1363
+
1364
+ pass # Carrega o dataset existente
1365
+ else:
1366
+ training_data = []
1367
+ for item in file_data:
1368
+ text = None
1369
+ if "prompt" in item and "completion" in item:
1370
+ prompt = item.get("prompt", "")
1371
+ completion = item.get("completion", "")
1372
+ if isinstance(prompt, str) and isinstance(completion, str) and prompt and completion:
1373
+ text = f"<s>[INST] {prompt.strip()} [/INST] {completion.strip()}</s>"
1374
+ elif "text" in item and isinstance(item["text"], str) and item["text"].strip():
1375
+ text = f"<s>{item['text'].strip()}</s>"
1376
+ if text:
1377
+ training_data.append({"text": text})
1378
+
1379
+ if not training_data:
1380
+ log_error("Nenhum dado válido encontrado para treino após formatação.")
1381
+ train_progress.update({"status": "error", "message": "Nenhum dado válido para treino."})
1382
+ return
1383
+
1384
+ dataset = Dataset.from_list(training_data)
1385
+
1386
+ # --- USAR O DYNAMIC_MAX_LEN AQUI ---
1387
+ def tokenize_function(examples):
1388
+ return tokenizer(examples["text"], truncation=True, max_length=DYNAMIC_MAX_LEN, padding=False)
1389
+
1390
+ tokenized_datasets = dataset.map(
1391
+ tokenize_function,
1392
+ batched=True,
1393
+ num_proc=DATALOADER_WORKERS,
1394
+ remove_columns=dataset.column_names,
1395
+ )
1396
+ tokenized_datasets.save_to_disk(tokenized_path)
1397
+ log_success(f"Dataset tokenizado salvo em {tokenized_path} com MAX_LEN={DYNAMIC_MAX_LEN}")
1398
+
1399
+ split_dataset = tokenized_datasets.train_test_split(test_size=0.1, seed=42)
1400
+ train_dataset = split_dataset["train"]
1401
+ eval_dataset = split_dataset["test"]
1402
+
1403
+ data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
1404
+
1405
+ # ----------- TrainingArguments -----------
1406
+ training_args = TrainingArguments(
1407
+ output_dir=output_dir,
1408
+ num_train_epochs=num_epochs_for_trainer,
1409
+ #num_train_epochs=epochs,
1410
+ per_device_train_batch_size=BASE_BATCH_SIZE,
1411
+ per_device_eval_batch_size=BASE_EVAL_SIZE,
1412
+ gradient_accumulation_steps=ACCUMULATION_STEPS,
1413
+ learning_rate=BASE_LEARNING_RATE,
1414
+ warmup_ratio=WARMUP_RATIO,
1415
+ lr_scheduler_type=LR_SCHEDULER_TYPE, #"constant_with_warmup",
1416
+ weight_decay=WEIGHT_DECAY,
1417
+ logging_steps=LOGGIN_STEPS,
1418
+ save_strategy=SAVE_STRATEGY,
1419
+ save_steps=SAVE_STEPS,
1420
+ eval_steps=EVAL_STEPS,
1421
+ optim=OPTIM, #adamw_hf, "adamw_torch", "cosine"
1422
+ dataloader_num_workers=DATALOADER_WORKERS,
1423
+ bf16=False,
1424
+ #gradient_checkpointing=True,
1425
+ max_grad_norm=1.0,
1426
+ report_to="none",
1427
+ save_total_limit=3,
1428
+ seed=42,
1429
+ )
1430
+ #------------------------------------------------------------
1431
+ # Como inicializar o callback com os valores corretos
1432
+ _requested_dynacc_args = {
1433
+ "base_batch": BASE_BATCH_SIZE,
1434
+ "max_steps": DYNAMIC_ACCUMULATION_MAX_STEPS,
1435
+ "target_util": DYNAMIC_ACCUMULATION_TARGET_UTIL,
1436
+ }
1437
+ try:
1438
+ sig = inspect.signature(DynamicAccumulationCallback.__init__)
1439
+ supported_params = [p for p in sig.parameters.keys() if p != "self"]
1440
+ dynacc_kwargs = {k: v for k, v in _requested_dynacc_args.items() if k in supported_params}
1441
+ except Exception:
1442
+ dynacc_kwargs = {
1443
+ "base_batch": BASE_BATCH_SIZE,
1444
+ "max_steps": DYNAMIC_ACCUMULATION_MAX_STEPS,
1445
+ }
1446
+
1447
+ #-----------------------------
1448
+ trainer = Trainer(
1449
+ model=model,
1450
+ args=training_args,
1451
+ # processing_class=tokenizer, # Removido conforme sugestão
1452
+ train_dataset=train_dataset,
1453
+ eval_dataset=eval_dataset,
1454
+ data_collator=data_collator,
1455
+ #peft_config=lora_config,
1456
+ callbacks=[LogCallback(), DynamicAccumulationCallback, SimpleStepTimerCallback,
1457
+ OptimizerRefreshCallback(refresh_interval_sec=OPTIMIZER_REFRESH_INTERVAL)]
1458
+ )
1459
+ # === Iniciar a thread de monitorização ANTES do treino ===
1460
+ _hotkey_thread = threading.Thread(
1461
+ target=_iniciar_monitor_hotkeys,
1462
+ args=(model, tokenizer, trainer, output_dir, True),
1463
+ daemon=True
1464
+ )
1465
+ _hotkey_thread.start()
1466
+
1467
+ # --- ADD: registar accumulation atual (se já calculado antes do treino)
1468
+ if CURRENT_ACCUM_STEPS is not None:
1469
+ origem = LAST_ACCUM_ORIGIN or "desconhecido"
1470
+ training_logs.append(f"[INFO] gradient_accumulation_steps atual (pré-train): {CURRENT_ACCUM_STEPS} ({origem})")
1471
+ try:
1472
+ # se tiveres a base aqui; caso contrário usa BASE_BATCH_SIZE
1473
+ base_bsz = BASE_BATCH_SIZE_EFFECTIVE if 'BASE_BATCH_SIZE_EFFECTIVE' in globals() and BASE_BATCH_SIZE_EFFECTIVE else BASE_BATCH_SIZE
1474
+ eb = base_bsz * CURRENT_ACCUM_STEPS
1475
+ training_logs.append(f"[INFO] Effective Batch inicial (base={base_bsz}) = {eb}")
1476
+ except Exception:
1477
+ pass
1478
+ # --- END ADD
1479
+
1480
+ try:
1481
+ log_success("Configuração do Trainer concluída. A iniciar o treino...")
1482
+ train_progress.update({"status": "training", "message": "Iniciando o treinamento do modelo..."})
1483
+ trainer_output = trainer.train(resume_from_checkpoint=resume_from_trainer_checkpoint)
1484
+
1485
+ lora_model_path = os.path.join(output_dir, "lora_model")
1486
+ os.makedirs(lora_model_path, exist_ok=True)
1487
+ model.save_pretrained(lora_model_path)
1488
+ tokenizer.save_pretrained(lora_model_path)
1489
+ training_logs.append("Adapter LoRA salvo com sucesso!")
1490
+
1491
+ log_info("A avaliar o modelo final no dataset de validação...")
1492
+ final_metrics = trainer.evaluate()
1493
+ eval_loss = final_metrics.get("eval_loss")
1494
+ if eval_loss is not None:
1495
+ perplexity = math.exp(eval_loss)
1496
+ log_success("Avaliação Final Concluída:")
1497
+ log_info(f" -> Eval Loss: {eval_loss:.4f}")
1498
+ log_info(f" -> Perplexity: {perplexity:.4f}")
1499
+ with open(os.path.join(output_dir, "final_metrics.txt"), "w") as f:
1500
+ f.write(f"Eval Loss: {eval_loss}\nPerplexity: {perplexity}\n")
1501
+ else:
1502
+ log_warning("Não foi possível obter 'eval_loss' das métricas finais.")
1503
+
1504
+ # --- ADD: resumo final com steps e accumulation/EB
1505
+ try:
1506
+ # tentar ler steps efetivos do Trainer ao final
1507
+ steps_final = None
1508
+ if hasattr(trainer, "state") and getattr(trainer.state, "global_step", None) is not None:
1509
+ steps_final = trainer.state.global_step
1510
+ elif hasattr(trainer, "state") and getattr(trainer.state, "max_steps", None):
1511
+ steps_final = trainer.state.max_steps
1512
+ if steps_final is not None and isinstance(steps_final, int) and steps_final > 0:
1513
+ TOTAL_TRAIN_STEPS = steps_final # ajusta se necessário
1514
+ origem = LAST_ACCUM_ORIGIN or "desconhecido"
1515
+ base_bsz = BASE_BATCH_SIZE_EFFECTIVE if 'BASE_BATCH_SIZE_EFFECTIVE' in globals() and BASE_BATCH_SIZE_EFFECTIVE else BASE_BATCH_SIZE
1516
+ eb = (base_bsz * CURRENT_ACCUM_STEPS) if CURRENT_ACCUM_STEPS else None
1517
+
1518
+ training_logs.append("[SUCESSO] Treino concluído (resumo):")
1519
+ training_logs.append(f" - total_train_steps = {TOTAL_TRAIN_STEPS}")
1520
+ if CURRENT_ACCUM_STEPS is not None:
1521
+ training_logs.append(f" - gradient_accumulation_steps final = {CURRENT_ACCUM_STEPS} ({origem})")
1522
+ if eb is not None:
1523
+ training_logs.append(f" - Effective Batch final (base={base_bsz}) = {eb}")
1524
+ except Exception as _e:
1525
+ training_logs.append(f"[AVISO] não foi possível gerar resumo final detalhado ({_e})")
1526
+ # --- END ADD
1527
+
1528
+ train_progress.update({"status": "awaiting_merge", "percent": 100, "message": "Treino concluído. LoRA salvo. Decida a próxima ação."})
1529
+ training_logs.append("Treino concluído. Adapter LoRA salvo em " + lora_model_path)
1530
+ log_success("Treino concluído. Adapter LoRA salvo. Aguardando decisão (merge ou continuar).")
1531
+ except RuntimeError as e:
1532
+ msg = str(e)
1533
+ training_logs.append(f"Erro durante treino: {msg}")
1534
+ train_progress["status"] = "error"
1535
+ log_error(f"Erro de Runtime durante o treino: {e}")
1536
+ traceback.print_exc()
1537
+
1538
+ except Exception as e:
1539
+ log_error(f"Erro crítico durante o treino: {e}")
1540
+ train_progress.update({"status": "error", "message": f"Erro crítico no treino: {e}"})
1541
+ traceback.print_exc()
1542
+ return
1543
+
1544
+ #----------------------------------------------------
1545
+ TENSORBOARD_LOGDIR = r"C:\Users\ilina\startup\Treinamento\feramenta-treino\trained_model_output\logs"
1546
+ TENSORBOARD_PORT = 6006
1547
+
1548
+ def run_tensorboard():
1549
+ # Lança o TensorBoard em thread separada para não bloquear
1550
+ subprocess.Popen([
1551
+ "tensorboard",
1552
+ f"--logdir={TENSORBOARD_LOGDIR}",
1553
+ f"--port={TENSORBOARD_PORT}"
1554
+ ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
1555
+ #-----------------------------------------------------------
1556
+ #----------------------
1557
+ # Rotas FLASK
1558
+ #----------------------
1559
+ @app.route('/')
1560
+ def home():
1561
+ """ Rota principal que serve o ficheiro index.html. """
1562
+ return render_template('index-6.html')
1563
+ #---------------------------------------
1564
+ def guess_lora_targets(model):
1565
+ """
1566
+ Função auxiliar para identificar automaticamente os módulos alvo para LoRA.
1567
+ Analisa camadas lineares no modelo e retorna uma lista de nomes de camadas candidatas.
1568
+ """
1569
+ targets = []
1570
+ for name, module in model.named_modules():
1571
+ if isinstance(module, torch.nn.Linear) and module.weight.requires_grad:
1572
+ targets.append(name)
1573
+ if not targets:
1574
+ targets = ["q_proj", "v_proj"] # fallback padrão usado por PEFT
1575
+ return targets
1576
+
1577
+ # ROTA DE TREINAMENTO
1578
+ #-------------------------------------
1579
+ @app.route('/api/train', methods=['POST'])
1580
+ def handle_train_request():
1581
+ global all_data
1582
+ try:
1583
+ model_path = request.form.get('model_path')
1584
+ epochs = int(request.form.get('epochs'))
1585
+ uploaded_files = request.files.getlist('files')
1586
+
1587
+ logging.info(f"🚀 Novo treino iniciado")
1588
+ logging.info(f"Modelo: {model_path}")
1589
+ logging.info(f"Épocas: {epochs}")
1590
+
1591
+ if not all([model_path, epochs, uploaded_files]):
1592
+ return jsonify({"status": "error", "message": "Faltam parâmetros: modelo, épocas ou ficheiros."}), 400
1593
+
1594
+ # Limpa os dados de treinos anteriores
1595
+ all_data = []
1596
+ for file in uploaded_files:
1597
+ try:
1598
+ raw_content = file.read()
1599
+ encoding = chardet.detect(raw_content)['encoding'] or 'utf-8'
1600
+ content = raw_content.decode(encoding)
1601
+ for line in content.splitlines():
1602
+ if not line.strip(): continue
1603
+ if file.filename.endswith('.jsonl'):
1604
+ all_data.append(json.loads(line))
1605
+ else: # Assume .txt ou outros formatos de texto
1606
+ all_data.append({"text": line.strip()})
1607
+ except Exception as e:
1608
+ return jsonify({"status": "error", "message": f"Erro ao ler ficheiro {file.filename}: {e}"}), 400
1609
+
1610
+ if not all_data:
1611
+ return jsonify({"status": "error", "message": "Nenhum dado válido encontrado nos ficheiros."}), 400
1612
+
1613
+ output_dir = os.path.join(os.getcwd(), "trained_model_output")
1614
+ os.makedirs(output_dir, exist_ok=True)
1615
+
1616
+ training_logs.append(f"[DEBUG] CWD: {os.getcwd()}")
1617
+ training_logs.append(f"[DEBUG] OUTPUT_DIR: {output_dir}")
1618
+ training_logs.append(f"[DEBUG] FILES: {os.listdir(os.getcwd())}")
1619
+
1620
+ # Passar um flag indicando que é um "novo" treino (ou re-treino)
1621
+ thread = Thread(target=train_model_lora, args=(all_data, epochs, model_path, output_dir, ACCUMULATION_STEPS, DATALOADER_WORKERS, "new_train"))
1622
+ thread.start()
1623
+
1624
+ return jsonify({"status": "started", "message": "Requisição de treino recebida. O processo foi iniciado."})
1625
+
1626
+ except Exception as e:
1627
+ return jsonify({"status": "error", "message": f"Erro no servidor: {e}"}), 500
1628
+ #-------------------------------------------
1629
+ @app.route("/api/train_status")
1630
+ def train_status():
1631
+ progress = train_progress.get("percent", 0)
1632
+ status = train_progress.get("status", "")
1633
+ epoch_list = epoch_losses
1634
+ logs_tail = training_logs[-200:] if len(training_logs) > 200 else training_logs[:]
1635
+
1636
+ # Acrescentar as linhas pedidas (apenas texto; simples)
1637
+ if CURRENT_ACCUM_STEPS is not None:
1638
+ origem = LAST_ACCUM_ORIGIN or "desconhecido"
1639
+ logs_tail.append(f"[INFO] gradient_accumulation_steps atual: {CURRENT_ACCUM_STEPS} ({origem})")
1640
+
1641
+ # opcional: mostrar Effective Batch
1642
+ if BASE_BATCH_SIZE_EFFECTIVE:
1643
+ eb = BASE_BATCH_SIZE_EFFECTIVE * CURRENT_ACCUM_STEPS
1644
+ logs_tail.append(f"[INFO] Effective Batch (base={BASE_BATCH_SIZE_EFFECTIVE}) = {eb}")
1645
+
1646
+ if TOTAL_TRAIN_STEPS is not None:
1647
+ logs_tail.append(f"[INFO] total_train_steps desta execução: {TOTAL_TRAIN_STEPS}")
1648
+
1649
+ return jsonify({
1650
+ "progress": progress,
1651
+ "status": status,
1652
+ "epoch_losses": epoch_list,
1653
+ "logs": logs_tail
1654
+ })
1655
+
1656
+ #---------------------------------------
1657
+ @app.route('/api/load_lora_chat', methods=['POST'])
1658
+ def load_lora_chat():
1659
+ try:
1660
+ data = request.get_json()
1661
+ model_path = data.get("model_path")
1662
+ if not model_path or "C:/" in model_path:
1663
+ return jsonify({"status": "error", "message": "Caminho do modelo inválido para Linux (HF Spaces). Use o ID do Hugging Face."}), 400
1664
+
1665
+ output_dir = os.path.join(os.getcwd(), "trained_model_output")
1666
+ lora_model_path = os.path.join(output_dir, "lora_model")
1667
+
1668
+ if not os.path.exists(lora_model_path):
1669
+ return jsonify({"status": "error", "message": f"LoRA não encontrado em: {lora_model_path}"}), 404
1670
+
1671
+ # Determinar dtype e device_map
1672
+ if torch.cuda.is_available():
1673
+ model_dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
1674
+ model_device_map = "auto"
1675
+ else:
1676
+ model_dtype = torch.bfloat16
1677
+ model_device_map = "cpu"
1678
+
1679
+ # Carregar o tokenizer do LoRA primeiro para obter o tamanho de vocabulário correto
1680
+ tokenizer = AutoTokenizer.from_pretrained(lora_model_path, use_fast=True)
1681
+
1682
+ base_model = AutoModelForCausalLM.from_pretrained(
1683
+ model_path,
1684
+ dtype=model_dtype,
1685
+ device_map=model_device_map,
1686
+ low_cpu_mem_usage=True
1687
+ )
1688
+
1689
+ # CORREÇÃO: Redimensionar o modelo base para o tamanho do vocabulário do tokenizer do LoRA
1690
+ base_model.resize_token_embeddings(len(tokenizer))
1691
+
1692
+ lora_model = PeftModel.from_pretrained(base_model, lora_model_path, device_map=model_device_map)
1693
+
1694
+ chat_model = lora_model
1695
+ chat_tokenizer = tokenizer
1696
+
1697
+ log_success("Modelo LoRA carregado com sucesso para chat (sem merge).")
1698
+ train_progress.update({"status": "awaiting_merge", "message": "Modelo LoRA carregado para chat. Decida a próxima ação."})
1699
+
1700
+ return jsonify({"status": "ok", "message": "Modelo LoRA carregado para chat (sem merge)."})
1701
+ except Exception as e:
1702
+ log_error(f"Erro ao carregar LoRA para chat: {e}")
1703
+ train_progress.update({"status": "error", "message": f"Erro ao carregar LoRA para chat: {e}"})
1704
+ return jsonify({"status": "error", "message": str(e)}), 500
1705
+
1706
+ #--------------------------------------------
1707
+ @app.route('/api/decide_merge', methods=['POST'])
1708
+ def decide_merge():
1709
+ global chat_model, chat_tokenizer, train_progress
1710
+ try:
1711
+ decision = request.form.get('decision')
1712
+ model_path = request.form.get('model_path')
1713
+ output_dir = os.path.join(os.getcwd(), "trained_model_output")
1714
+ lora_model_path = os.path.join(output_dir, "lora_model")
1715
+
1716
+ if torch.cuda.is_available():
1717
+ model_dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
1718
+ model_device_map = "auto"
1719
+ else:
1720
+ model_dtype = torch.bfloat16
1721
+ model_device_map = "cpu"
1722
+
1723
+ if decision == "merge":
1724
+ log_info("Iniciando o merge do adapter LoRA com o modelo base...")
1725
+ train_progress.update({"status": "merging", "message": "Iniciando a fusão do adapter LoRA..."})
1726
+
1727
+ # Carregar o tokenizer do LoRA primeiro para obter o tamanho de vocabulário correto
1728
+ tokenizer = AutoTokenizer.from_pretrained(lora_model_path, use_fast=True)
1729
+
1730
+ base_model = AutoModelForCausalLM.from_pretrained(
1731
+ model_path,
1732
+ dtype=model_dtype,
1733
+ device_map=model_device_map,
1734
+ low_cpu_mem_usage=True
1735
+ )
1736
+ # CORREÇÃO: Redimensionar o modelo base para o tamanho do vocabulário do tokenizer do LoRA
1737
+ base_model.resize_token_embeddings(len(tokenizer))
1738
+
1739
+ lora_model = PeftModel.from_pretrained(base_model, lora_model_path)
1740
+ merged_model = lora_model.merge_and_unload()
1741
+
1742
+ final_model_path = os.path.join(output_dir, "merged_model")
1743
+ os.makedirs(final_model_path, exist_ok=True)
1744
+ merged_model.save_pretrained(final_model_path, safe_serialization=True)
1745
+
1746
+ # O tokenizer já foi carregado acima
1747
+ tokenizer.save_pretrained(final_model_path)
1748
+
1749
+ chat_model = merged_model
1750
+ chat_tokenizer = tokenizer
1751
+
1752
+ log_success("Modelo fundido com sucesso e pronto para o chat!")
1753
+ train_progress.update({"status": "finished", "percent": 100, "message": "Modelo LoRA fundido e salvo."})
1754
+ return jsonify({"status": "merged", "message": "Modelo fundido com sucesso."})
1755
+
1756
+ #-----------------------------------------------------------------------------------------
1757
+ elif decision == "continue":
1758
+ log_info("A continuar o treino por mais épocas a partir do adapter salvo...")
1759
+ epochs_to_add = int(request.form.get('epochs', 1)) # Renomeado para clareza
1760
+
1761
+ if not os.path.exists(lora_model_path):
1762
+ log_error("Nenhum adapter LoRA encontrado para retomar o treino.")
1763
+ train_progress.update({"status": "continuing_training", "message": f"Continuando treino por mais {epochs_to_add} épocas..."})
1764
+ return jsonify({"status": "error", "message": "Nenhum adapter LoRA encontrado para retomar o treino."}), 400
1765
+
1766
+ # --- Determinar épocas já completadas ---
1767
+ initial_epochs_completed = 0
1768
+ output_dir = os.path.join(os.getcwd(), "trained_model_output") # Certifique-se que output_dir está definido
1769
+ checkpoint_dirs = [d for d in os.listdir(output_dir) if d.startswith("checkpoint-")]
1770
+ if checkpoint_dirs:
1771
+ checkpoint_dirs.sort(key=lambda x: int(x.split('-')[1]), reverse=True)
1772
+ latest_candidate_checkpoint_dir = os.path.join(output_dir, checkpoint_dirs[0])
1773
+ trainer_state_path = os.path.join(latest_candidate_checkpoint_dir, "trainer_state.json")
1774
+ if os.path.exists(trainer_state_path):
1775
+ try:
1776
+ with open(trainer_state_path, 'r') as f:
1777
+ trainer_state = json.load(f)
1778
+ initial_epochs_completed = int(trainer_state.get('epoch', 0))
1779
+ log_info(f"Detectado que {initial_epochs_completed} épocas foram completadas no último checkpoint.")
1780
+ except Exception as e:
1781
+ log_warning(f"Erro ao ler trainer_state.json do checkpoint: {e}. Assumindo 0 épocas completadas.")
1782
+
1783
+ # Atualiza o status de progresso para indicar que o treino vai continuar
1784
+ train_progress.update({"status": "continuing_training", "message": f"Continuando treino por mais {epochs_to_add} épocas..."})
1785
+
1786
+ thread = Thread(
1787
+ target=train_model_lora,
1788
+ args=(
1789
+ all_data,
1790
+ epochs_to_add,
1791
+ model_path,
1792
+ output_dir,
1793
+ ACCUMULATION_STEPS,
1794
+ DATALOADER_WORKERS,
1795
+ "continue_train",
1796
+ lora_model_path,
1797
+ initial_epochs_completed,
1798
+ "continue_train"
1799
+ )
1800
+ )
1801
+ thread.start()
1802
+
1803
+ return jsonify({
1804
+ "status": "continued",
1805
+ "message": f"Treino adicional de {epochs_to_add} épocas iniciado."
1806
+ })
1807
+
1808
+ else:
1809
+ log_warning(f"Decisão inválida para /decide_merge: {decision}")
1810
+ train_progress.update({"status": "error", "message": f"Decisão inválida: {decision}"})
1811
+ return jsonify({"status": "error", "message": "Decisão inválida."}), 400
1812
+
1813
+ except Exception as e:
1814
+ log_error(f"Erro durante o merge/continuação: {e}")
1815
+ train_progress.update({"status": "error", "message": f"Erro durante o merge/continuação: {e}"})
1816
+ return jsonify({"status": "error", "message": str(e)}), 500
1817
+
1818
+ #--------------------------------------------
1819
+ @app.route('/api/chat', methods=['POST'])
1820
+ def handle_chat():
1821
+ global chat_model, chat_tokenizer
1822
+ if not chat_model or not chat_tokenizer:
1823
+ return jsonify({
1824
+ "response": "ERRO: O modelo ainda não está carregado. Faça o 'merge' ou 'load_lora_chat' primeiro."
1825
+ }), 400
1826
+ try:
1827
+ data = request.get_json()
1828
+ prompt = data.get('prompt')
1829
+ if not prompt:
1830
+ return jsonify({"response": "Erro: 'prompt' não foi enviado."}), 400
1831
+
1832
+ formatted_prompt = f"<s>[INST] {prompt} [/INST]"
1833
+ inputs = chat_tokenizer(formatted_prompt, return_tensors="pt").to(chat_model.device)
1834
+
1835
+ outputs = chat_model.generate(
1836
+ **inputs,
1837
+ max_new_tokens=256,
1838
+ num_return_sequences=1,
1839
+ eos_token_id=chat_tokenizer.eos_token_id,
1840
+ pad_token_id=chat_tokenizer.eos_token_id,
1841
+ do_sample=True,
1842
+ temperature=0.01, # <--- Sugestão: Reduzir temperatura para respostas mais determinísticas
1843
+ top_p=0.9,
1844
+ )
1845
+ response_full = chat_tokenizer.decode(outputs[0], skip_special_tokens=True)
1846
+ response_only = response_full.split("[/INST]")[-1].strip()
1847
+
1848
+ return jsonify({"response": response_only})
1849
+ except Exception as e:
1850
+ return jsonify({"response": f"Erro interno no chat: {str(e)}"}), 500
1851
+
1852
+ #----------------------------------------------------
1853
+ @app.post("/api/tensorboard/start")
1854
+ def start_tensorboard():
1855
+ threading.Thread(target=run_tensorboard, daemon=True).start()
1856
+ return jsonify({"status": "ok", "url": f"http://localhost:{TENSORBOARD_PORT}"}), 200
1857
+
1858
+ # =========================================================================
1859
+ # ROTAS DE DIRETORIO DO OUTPUT
1860
+ # =========================================================================
1861
+ @app.route('/api/model_dir', methods=['GET'])
1862
+ def list_model_dir():
1863
+ try:
1864
+ # Tenta vários caminhos possíveis no Hugging Face
1865
+ paths_to_check = [
1866
+ os.path.join(os.getcwd(), "trained_model_output"),
1867
+ "/app/trained_model_output",
1868
+ "./trained_model_output"
1869
+ ]
1870
+
1871
+ target = paths_to_check[0]
1872
+ for p in paths_to_check:
1873
+ if os.path.exists(p):
1874
+ target = p
1875
+ break
1876
+
1877
+ # Se não existir, cria agora para o botão não falhar
1878
+ if not os.path.exists(target):
1879
+ os.makedirs(target, exist_ok=True)
1880
+ return jsonify({"files": ["Pasta criada agora. Está vazia."]}), 200
1881
+
1882
+ files_list = []
1883
+ for root, dirs, filenames in os.walk(target):
1884
+ for f in filenames:
1885
+ rel_path = os.path.relpath(os.path.join(root, f), target)
1886
+ files_list.append(rel_path)
1887
+
1888
+ if not files_list:
1889
+ return jsonify({"files": ["Pasta encontrada, mas não tem ficheiros lá dentro."]}), 200
1890
+
1891
+ return jsonify({"files": sorted(files_list)}), 200
1892
+
1893
+ except Exception as e:
1894
+ return jsonify({"files": [f"Erro ao ler pasta: {str(e)}"]}), 200 # Retornamos 200 para o JS não dar erro
1895
+ #-------------------------------------------------------------------------
1896
+
1897
+
1898
+ @app.route('/api/download/<path:filename>')
1899
+ def download_file(filename):
1900
+ # Caminho absoluto para a pasta de output
1901
+ directory = os.path.join(os.getcwd(), "trained_model_output")
1902
+
1903
+ # send_from_directory é seguro e evita que utilizadores acedam a pastas do sistema
1904
+ try:
1905
+ return send_from_directory(directory, filename, as_attachment=True)
1906
+ except Exception as e:
1907
+ return str(e), 404
1908
+
1909
+ # =========================================================================
1910
+ # ROTAS DE CONFIGURAÇÃO (GET para Ler, POST para Guardar)
1911
+ # =========================================================================
1912
+
1913
+ # Rota GET: Lê os valores mais recentes do código Python (Recarregar Config. do Servidor)
1914
+ @app.route('/api/config', methods=['GET'])
1915
+ def get_config():
1916
+ # 1. Lê as constantes do ficheiro .py
1917
+ constants = _load_constants_from_file()
1918
+
1919
+ # 2. Mapeia para o formato JSON do Frontend
1920
+ config_data = map_backend_to_frontend(constants)
1921
+
1922
+ # 3. Envia os dados
1923
+ return jsonify(config_data)
1924
+
1925
+ # Rota POST: Recebe os novos valores da UI e atualiza o código Python (Guardar Configuração)
1926
+
1927
+ @app.route('/api/config', methods=['POST'])
1928
+ def save_config():
1929
+ try:
1930
+ # Recebe os dados JSON do Front-end
1931
+ new_data = request.json
1932
+
1933
+ # 1. Lógica de proteção / confirmação (Implementada no Front-end via prompt/confirmação)
1934
+ # O Front-end deve tratar a confirmação antes de enviar o POST.
1935
+ # Aqui, apenas processamos a escrita.
1936
+
1937
+ if not new_data:
1938
+ return jsonify({"error": "Dados inválidos."}), 400
1939
+
1940
+ # 2. Atualiza as constantes no ficheiro Python
1941
+ success = update_python_constants(new_data)
1942
+
1943
+ if success:
1944
+ # O ideal é recarregar a config para garantir que o que foi salvo é retornado
1945
+ constants = _load_constants_from_file()
1946
+ config_data = map_backend_to_frontend(constants)
1947
+
1948
+ return jsonify(config_data), 200 # Retorna a config atualizada
1949
+ else:
1950
+ return jsonify({"error": "Falha ao escrever no ficheiro de configuração."}), 500
1951
+
1952
+ except Exception as e:
1953
+ print(f"Erro ao guardar config: {e}")
1954
+ return jsonify({"error": str(e)}), 500
1955
+
1956
+ #------------------------------------
1957
+ @app.post("/api/adjust_accumulation")
1958
+ def adjust_accumulation():
1959
+ """
1960
+ Ajuste manual relativo do gradient_accumulation_steps.
1961
+ Usa o valor atual real do treino se CURRENT_ACCUM_STEPS não estiver definido.
1962
+ Envia { "action": "auto" } para voltar ao modo automático.
1963
+ """
1964
+ global CURRENT_ACCUM_STEPS
1965
+
1966
+ data = request.get_json(force=True)
1967
+ delta = int(data.get("delta", 0))
1968
+ action = data.get("action")
1969
+
1970
+ # Modo automático
1971
+ if action == "auto":
1972
+ CURRENT_ACCUM_STEPS = None
1973
+ logging.info("🔄 Modo automático de accumulation reativado.")
1974
+ return jsonify({"status": "ok", "mode": "auto", "message": "Modo automático reativado."})
1975
+
1976
+ # Se CURRENT_ACCUM_STEPS ainda não definido, usa valor atual do treino
1977
+ if CURRENT_ACCUM_STEPS is None:
1978
+ CURRENT_ACCUM_STEPS = train_progress.get("current_accum_steps", 16) # valor padrão caso não exista
1979
+
1980
+ # Aplica delta e limita
1981
+ CURRENT_ACCUM_STEPS = max(1, min(DYNAMIC_ACCUMULATION_MAX_STEPS, CURRENT_ACCUM_STEPS + delta))
1982
+
1983
+ logging.info(f"🎚 Ajuste manual recebido → CURRENT_ACCUM_STEPS = {CURRENT_ACCUM_STEPS} (delta={delta})")
1984
+
1985
+ return jsonify({
1986
+ "status": "ok",
1987
+ "current_steps": CURRENT_ACCUM_STEPS,
1988
+ "mode": "manual"
1989
+ })
1990
+
1991
+ #----------------------------------------------------
1992
+ model = None # variável global
1993
+
1994
+ @app.route('/api/create_model', methods=['POST'])
1995
+ def create_model():
1996
+ global model
1997
+ data = request.json
1998
+ model_name = data.get("model_name")
1999
+ if not model_name:
2000
+ return jsonify({"status": "error", "message": "model_name não fornecido."}), 400
2001
+ # O modelo real é carregado no início do treino via train_model_lora
2002
+ return jsonify({"status": "ok", "message": f"Modelo '{model_name}' será carregado ao iniciar o treino."})
2003
+ #---------------------------------------------------------------
2004
+ #if __name__ == "__main__" and os.environ.get("WERKZEUG_RUN_MAIN") == "true":
2005
+ # código de inicialização
2006
+ if __name__ == '__main__':
2007
+ app.run(host='0.0.0.0', port=7860, debug=False)
config_manager.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config_manager.py
2
+
3
+ import json
4
+ import os
5
+ import re
6
+
7
+ # Caminho para o ficheiro principal de onde vamos ler as CONSTANTES
8
+ # Altere para o caminho correto do seu arquivo com as constantes (ex: train-6-ok.py)
9
+ CONFIG_FILE_PATH = 'train-6-ok.py'
10
+
11
+ def _load_constants_from_file():
12
+ """Lê todas as CONSTANTES (ALL_CAPS) do ficheiro Python e retorna um dicionário."""
13
+ constants = {}
14
+
15
+ # Lista de constantes que têm de ser tratadas como booleanos
16
+ BOOL_CONSTANTS = ["ALLOW_SLEEP_ON_CHANGE"]
17
+
18
+ try:
19
+ with open(CONFIG_FILE_PATH, 'r', encoding='utf-8') as f:
20
+ content = f.read()
21
+ except FileNotFoundError:
22
+ print(f"ERRO: Ficheiro de configuração não encontrado em {CONFIG_FILE_PATH}")
23
+ return constants
24
+
25
+ # Expressão regular para encontrar constantes (NOME_CONSTANTE = valor)
26
+ # Ignora linhas comentadas
27
+ matches = re.findall(r'^\s*([A-Z_]+)\s*=\s*(.*)', content, re.MULTILINE)
28
+
29
+ for name, value_str in matches:
30
+ # Tenta converter para o tipo correto (Int, Float, Bool)
31
+ try:
32
+ if name in BOOL_CONSTANTS:
33
+ constants[name] = value_str.strip().lower() == "true"
34
+
35
+ elif re.match(r'^"?-?\d+"?$', value_str.strip()):
36
+ constants[name] = int(value_str.strip().replace('"','').replace("'", ""))
37
+ elif re.match(r'^"?-?\d+\.\d+"?$', value_str.strip()):
38
+ constants[name] = float(value_str.strip().replace('"','').replace("'", ""))
39
+
40
+ # Adicione mais lógica de conversão se necessário (ex: strings)
41
+ else:
42
+ constants[name] = value_str.strip().replace("'", "").replace('"', '') # Simplificação para strings/outros
43
+ except Exception:
44
+ # Em caso de erro, armazena como string original
45
+ constants[name] = value_str.strip().replace("'", "").replace('"', '')
46
+
47
+ return constants
48
+ #-------------------
49
+ def map_backend_to_frontend(constants):
50
+ """Mapeia as constantes do Backend para a estrutura JSON do Frontend de forma segura."""
51
+
52
+ def safe_int(value, default=0):
53
+ try:
54
+ return int(value)
55
+ except (ValueError, TypeError):
56
+ return default
57
+
58
+ def safe_float(value, default=0.0):
59
+ try:
60
+ return float(value)
61
+ except (ValueError, TypeError):
62
+ return default
63
+
64
+ return {
65
+ "preset": "custom",
66
+ "ramcfg": {
67
+ "base_batch": safe_int(constants.get('BASE_BATCH_SIZE', 4)),
68
+ "max_steps": safe_int(constants.get('DYNAMIC_ACCUMULATION_MAX_STEPS', 64)),
69
+ "target_utilization": safe_float(constants.get('TARGET_RAM_UTILIZATION', 0.9)),
70
+ "estimated_batch_gb": safe_float(constants.get('ESTIMATED_BATCH_GB', 0.40)),
71
+ "min_headroom_gb": 1.0,
72
+ "hysteresis": safe_int(constants.get('DYNAMIC_ACCUMULATION_HIGH_RAM_LIMIT', 85)) / 100.0,
73
+ "ema_alpha": safe_float(constants.get('DYNAMIC_ACCUMULATION_TARGET_UTIL', 0.95)),
74
+ "patience": 4,
75
+ "cooldown_s": 20.0,
76
+ "min_decision_interval_s": 2.0,
77
+ "emergency_threshold_pct": safe_int(constants.get('DYNAMIC_ACCUMULATION_LOW_RAM_LIMIT', 71)),
78
+ "warmup_steps": 100,
79
+ "allow_sleep_on_change": False,
80
+ "sleep_s": 0.5,
81
+ "total_ram_gb": safe_float(constants.get('TOTAL_RAM_GB', None))
82
+ },
83
+ "mlcfg": {
84
+ "base_max_len": safe_int(constants.get('BASE_MAX_LEN', 256)),
85
+ "target_utilization": safe_float(constants.get('TARGET_MAX_LEN_UTILIZATION', 0.65)),
86
+ "increment": safe_int(constants.get('MAX_LEN_INCREMENT', 128)),
87
+ "cap": safe_int(constants.get('MAX_LEN_CAP', 512)),
88
+ "estimated_base_dataset_ram_gb": safe_float(constants.get('ESTIMATED_BASE_DATASET_RAM_GB', 2)),
89
+ "cost_per_increment_gb": safe_float(constants.get('COST_PER_INCREMENT_GB', 0.45)),
90
+ "total_ram_gb": safe_float(constants.get('TOTAL_RAM_GB', None))
91
+ },
92
+ "training": {
93
+ "base_lr": safe_float(constants.get('BASE_LEARNING_RATE', 2e-5)),
94
+ "base_eval_size": safe_int(constants.get('BASE_EVAL_SIZE', 32)),
95
+ "optimizer": constants.get('OPTIM', "adamw_torch"),
96
+ "scheduler": constants.get('IR_SCHEDULER_TYPE', "constant_with_warmup"),
97
+ "log_steps": safe_int(constants.get('LOGGIN_STEPS', 25)),
98
+ "save_strategy": constants.get('SAVE_STRATEGY', "steps"),
99
+ "save_steps": safe_int(constants.get('SAVE_STEPS', 50)),
100
+ "eval_steps": safe_int(constants.get('EVAL_STEPS', 50))
101
+ }
102
+ }
103
+
104
+ def update_python_constants(new_data):
105
+ """Atualiza as constantes no ficheiro Python original com os novos valores."""
106
+
107
+ # 1. Mapear de volta a estrutura aninhada para CONSTANTES
108
+ # Use o nome da constante no código, não o nome do campo na UI
109
+ # Mapeamento do JSON (UI keys) para Python (CONST_NAMES)
110
+ mapping = {
111
+ # ramcfg
112
+ "base_batch": "BASE_BATCH_SIZE",
113
+ "max_steps": "DYNAMIC_ACCUMULATION_MAX_STEPS",
114
+ "target_utilization": "TARGET_RAM_UTILIZATION",
115
+ "estimated_batch_gb": "ESTIMATED_BATCH_GB",
116
+ "hysteresis": "DYNAMIC_ACCUMULATION_HIGH_RAM_LIMIT",
117
+ "ema_alpha": "DYNAMIC_ACCUMULATION_TARGET_UTIL",
118
+ "emergency_threshold_pct": "DYNAMIC_ACCUMULATION_LOW_RAM_LIMIT",
119
+ # mlcfg
120
+ "base_max_len": "BASE_MAX_LEN",
121
+ "target_utilization_ml": "TARGET_MAX_LEN_UTILIZATION",
122
+ "increment": "MAX_LEN_INCREMENT",
123
+ "cap": "MAX_LEN_CAP",
124
+ "estimated_base_dataset_ram_gb": "ESTIMATED_BASE_DATASET_RAM_GB",
125
+ "cost_per_increment_gb": "COST_PER_INCREMENT_GB",
126
+ # training
127
+ "base_lr": "BASE_LEARNING_RATE",
128
+ "base_eval_size": "BASE_EVAL_SIZE",
129
+ "optimizer": "OPTIM",
130
+ "scheduler": "IR_SCHEDULER_TYPE",
131
+ "log_steps": "LOGGIN_STEPS",
132
+ "save_strategy": "SAVE_STRATEGY",
133
+ "save_steps": "SAVE_STEPS",
134
+ "eval_steps": "EVAL_STEPS"
135
+ }
136
+
137
+ updates = {}
138
+ for ui_key, const_name in mapping.items():
139
+ if ui_key in new_data.get('ramcfg', {}):
140
+ val = new_data['ramcfg'][ui_key]
141
+ # Converte 'hysteresis' de volta para porcentagem
142
+ if ui_key == "hysteresis":
143
+ updates[const_name] = int(val * 100)
144
+ # Outras conversões...
145
+ else:
146
+ updates[const_name] = val
147
+ elif ui_key in new_data.get('mlcfg', {}):
148
+ updates[const_name] = new_data['mlcfg'][ui_key]
149
+
150
+
151
+ # 2. Ler o conteúdo atual do ficheiro
152
+ try:
153
+ with open(CONFIG_FILE_PATH, 'r', encoding='utf-8') as f:
154
+ lines = f.readlines()
155
+ except FileNotFoundError:
156
+ return False
157
+
158
+ # 3. Reescrever o ficheiro com os novos valores
159
+ new_lines = []
160
+ updated_names = set()
161
+
162
+ for line in lines:
163
+ match = re.match(r'^\s*([A-Z_]+)\s*=\s*(.*)', line)
164
+ if match:
165
+ name = match.group(1)
166
+ # Se for uma das constantes que queremos atualizar, substitui a linha
167
+ if name in updates:
168
+ new_value = updates[name]
169
+ # Formata o valor de volta para string no formato Python
170
+ if isinstance(new_value, str):
171
+ formatted_value = f'"{new_value}"'
172
+ elif isinstance(new_value, bool):
173
+ formatted_value = str(new_value).capitalize()
174
+ else:
175
+ formatted_value = str(new_value)
176
+
177
+ new_lines.append(f"{name} = {formatted_value}\n")
178
+ updated_names.add(name)
179
+ continue # Pula a linha original
180
+
181
+ # Mantém a linha inalterada (código, comentários, imports, etc.)
182
+ new_lines.append(line)
183
+
184
+ # 4. Escrever de volta
185
+ try:
186
+ with open(CONFIG_FILE_PATH, 'w', encoding='utf-8') as f:
187
+ f.writelines(new_lines)
188
+ return True
189
+ except Exception as e:
190
+ print(f"ERRO ao escrever no ficheiro: {e}")
191
+ return False
192
+
193
+ # Fim de config_manager.py
templates/index-6.html ADDED
@@ -0,0 +1,553 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="pt-PT">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Treino de IA Local e Chat</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ :root{
10
+ --bg:#0f1218; --fg:#e8eef6; --muted:#9fb3ca; --panel:#161b22; --accent:#2ea043; --accent2:#1f6feb; --warn:#d29922; --err:#f85149;
11
+ --border:#30363d; --chip:#21262d;
12
+ --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
13
+ --sans: Inter, Segoe UI, system-ui, -apple-system, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Segoe UI Emoji", "Apple Color Emoji", sans-serif;
14
+ }
15
+ *{box-sizing:border-box}
16
+ html,body{height:100%}
17
+ body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.45}
18
+ header{padding:16px 20px;border-bottom:1px solid var(--border);background:linear-gradient(180deg, #111723, #0f1218);}
19
+ h1{margin:0;font-size:20px}
20
+ h2{margin:0 0 10px;font-size:18px}
21
+ h3{margin:16px 0 8px;font-size:15px;color:var(--muted)}
22
+ main{display:grid;grid-template-columns:0.65fr 1fr;gap:16px;padding:16px}
23
+ @media (max-width:1100px){main{grid-template-columns:1fr}}
24
+ .card{background:var(--panel);border:1px solid var(--border);border-radius:10px;overflow:hidden}
25
+ .card header{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid var(--border);background:#121720}
26
+ .card .content{padding:14px}
27
+ .row{display:grid;grid-template-columns:repeat(12,1fr);gap:12px}
28
+ .col-12{grid-column:span 12}.col-8{grid-column:span 8}.col-6{grid-column:span 6}.col-4{grid-column:span 4}.col-3{grid-column:span 3}.col-2{grid-column:span 2}
29
+ label{display:block;font-size:12px;color:var(--muted);margin-bottom:6px}
30
+ input[type="text"],input[type="number"],select{width:100%;padding:10px 10px;border:1px solid var(--border);border-radius:8px;background:#0c1117;color:var(--fg);font-family:var(--mono)}
31
+ input[type="checkbox"]{transform:scale(1.1)}
32
+ .hint{font-size:11px;color:#7aa2d6;margin-top:4px}
33
+ .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:var(--chip);color:var(--fg);cursor:pointer}
34
+ .btn:hover{filter:brightness(1.08)}
35
+ .btn-primary{background:var(--accent2);border-color:#2855a3}
36
+ .btn-success{background:var(--accent);border-color:#25834c}
37
+ .btn-warn{background:#3a2d12;border-color:#5c4620;color:#ffd479}
38
+ .btn-danger{background:#3b1416;border-color:#6a1c21;color:#ffb4b7}
39
+ .btn-ghost{background:transparent}
40
+ .toolbar{display:flex;flex-wrap:wrap;gap:8px}
41
+ .chips{display:flex;flex-wrap:wrap;gap:8px}
42
+ .chip{padding:6px 10px;border-radius:999px;background:var(--chip);border:1px solid var(--border);font-size:12px}
43
+ .grid2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
44
+ .kbd{font-family:var(--mono);padding:1px 6px;border-radius:6px;background:#0c1117;border:1px solid var(--border)}
45
+ .log{font-family:var(--mono);white-space:pre-wrap;background:#0a0e14;border:1px solid var(--border);border-radius:8px;padding:10px;height:200px;overflow:auto}
46
+ .muted{color:var(--muted)}
47
+ .pill{padding:3px 8px;border-radius:999px;border:1px solid var(--border);background:#0a0e14;color:var(--muted);font-size:12px}
48
+ .ok{color:#7ee787}.warn{color:#ffd479}.err{color:#ff7b72}
49
+ .footer{padding:12px 16px;color:var(--muted);border-top:1px solid var(--border);font-size:12px}
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <header>
54
+ <h1>Treino de IA Local e Chat</h1>
55
+ </header>
56
+
57
+ <main>
58
+ <section class="card">
59
+ <header>
60
+ <h2>Configuração Dinâmica</h2>
61
+ <div class="chips">
62
+ <span id="ramSummary" class="chip">RAM target: —</span>
63
+ <span id="mlSummary" class="chip">MAX_LEN: —</span>
64
+ </div>
65
+ </header>
66
+ <div class="content">
67
+ <div class="row">
68
+ <div class="col-12" style="display:flex;align-items:flex-end;gap:8px">
69
+ <button class="btn btn-success" id="saveCfgBtn">💾 Guardar Configuração</button>
70
+ <button class="btn btn-ghost" id="refreshBtn">↻ Recarregar Config. do Servidor</button>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- NOVO: HARDWARE -->
75
+ <h3>1. Hardware (Automático)</h3>
76
+ <div class="row">
77
+ <div class="col-3">
78
+ <label for="cpu_count">CPU_COUNT</label>
79
+ <input type="number" id="cpu_count" disabled />
80
+ </div>
81
+ <div class="col-3">
82
+ <label for="omp_threads">OMP_THREADS</label>
83
+ <input type="number" id="omp_threads" disabled />
84
+ <div class="hint">Nº de threads usadas para tensores.</div>
85
+ </div>
86
+ <div class="col-3">
87
+ <label for="dataloader_workers">DATALOADER_WORKERS</label>
88
+ <input type="number" id="dataloader_workers" disabled />
89
+ <div class="hint">Nº de processos para carregar dados.</div>
90
+ </div>
91
+ <div class="col-3">
92
+ <label for="total_ram_gb">TOTAL_RAM_GB</label>
93
+ <input type="text" id="total_ram_gb" disabled />
94
+ </div>
95
+ </div>
96
+
97
+ <!-- NOVO: PARÂMETROS BASE DE TREINO -->
98
+ <h3>2. Parâmetros Base de Treino</h3>
99
+ <div class="row">
100
+ <div class="col-3">
101
+ <label for="base_batch">BASE_BATCH_SIZE (por dispositivo)</label>
102
+ <input type="number" id="base_batch" min="1" step="1" />
103
+ </div>
104
+ <div class="col-3">
105
+ <label for="base_eval_size">BASE_EVAL_SIZE</label>
106
+ <input type="number" id="base_eval_size" min="1" step="1" />
107
+ </div>
108
+ <div class="col-3">
109
+ <label for="base_learning_rate">BASE_LEARNING_RATE</label>
110
+ <input type="number" id="base_learning_rate" min="1e-6" max="1e-3" step="1e-6" />
111
+ </div>
112
+ <div class="col-3">
113
+ <label for="lr_scheduler_type">LR_SCHEDULER_TYPE</label>
114
+ <select id="lr_scheduler_type">
115
+ <option value="constant_with_warmup">constant_with_warmup</option>
116
+ <option value="cosine">cosine</option>
117
+ <option value="linear">linear</option>
118
+ <option value="constant">constant</option>
119
+ </select>
120
+ </div>
121
+ </div>
122
+
123
+ <!-- SECÇÃO ATUALIZADA: RAM / Accumulation Steps -->
124
+ <h3>3. Ajuste Dinâmico (RAM / Gradient Accumulation)</h3>
125
+ <div class="row">
126
+ <div class="col-3">
127
+ <label for="max_steps">DYNAMIC_ACCUMULATION_MAX_STEPS</label>
128
+ <input type="number" id="max_steps" min="1" step="1" />
129
+ <div class="hint">Limite superior para acumulação de gradientes.</div>
130
+ </div>
131
+ <div class="col-3">
132
+ <label for="estimated_batch_gb">ESTIMATED_BATCH_GB</label>
133
+ <input type="number" id="estimated_batch_gb" min="0.01" step="0.01" />
134
+ <div class="hint">Custo de RAM por batch (ajuste fino necessário).</div>
135
+ </div>
136
+
137
+ <div class="col-3">
138
+ <label for="high_ram_limit_pct">HIGH_RAM_LIMIT (%)</label>
139
+ <input type="number" id="high_ram_limit_pct" min="70" max="95" step="1" />
140
+ <div class="hint">RAM % para **reduzir** `accumulation_steps`.</div>
141
+ </div>
142
+ <div class="col-3">
143
+ <label for="low_ram_limit_pct">LOW_RAM_LIMIT (%)</label>
144
+ <input type="number" id="low_ram_limit_pct" min="50" max="80" step="1" />
145
+ <div class="hint">RAM % para **aumentar** `accumulation_steps`.</div>
146
+ </div>
147
+ </div>
148
+
149
+ <!-- MAX_LEN Dinâmico -->
150
+ <h3>4. MAX_LEN Dinâmico</h3>
151
+ <div class="row">
152
+ <div class="col-3">
153
+ <label for="base_max_len">BASE_MAX_LEN</label>
154
+ <input type="number" id="base_max_len" min="32" step="32" />
155
+ </div>
156
+ <div class="col-3">
157
+ <label for="ml_target_utilization">TARGET_MAX_LEN_UTILIZATION</label>
158
+ <input type="number" id="ml_target_utilization" min="0.4" max="0.9" step="0.01" />
159
+ <div class="hint">Fração da RAM para dataset tokenizado.</div>
160
+ </div>
161
+ <div class="col-3">
162
+ <label for="increment">MAX_LEN_INCREMENT</label>
163
+ <input type="number" id="increment" min="16" step="16" />
164
+ </div>
165
+ <div class="col-3">
166
+ <label for="cap">MAX_LEN_CAP</label>
167
+ <input type="number" id="cap" min="64" step="32" />
168
+ </div>
169
+
170
+ <div class="col-3">
171
+ <label for="estimated_base_dataset_ram_gb">ESTIMATED_BASE_DATASET_RAM_GB</label>
172
+ <input type="number" id="estimated_base_dataset_ram_gb" min="0.1" step="0.1" />
173
+ </div>
174
+ <div class="col-3">
175
+ <label for="cost_per_increment_gb">COST_PER_INCREMENT_GB</label>
176
+ <input type="number" id="cost_per_increment_gb" min="0.05" step="0.05" />
177
+ </div>
178
+ </div>
179
+ </div>
180
+ </section>
181
+
182
+ <section class="card">
183
+ <header>
184
+ <h2>Treinar IA com Ficheiros</h2>
185
+ <div class="chips">
186
+ <span class="pill" id="trainerState">estado: inativo</span>
187
+ </div>
188
+ </header>
189
+ <div class="content">
190
+ <div class="row">
191
+ <div class="col-12">
192
+ <label for="model_path">Caminho do Modelo Base</label>
193
+ <input type="text" id="model_path" placeholder="/caminho/para/modelo" />
194
+ </div>
195
+ <div class="col-12">
196
+ <label for="train_files">Ficheiros de Treino (.jsonl)</label>
197
+ <input type="file" id="train_files" multiple accept=".jsonl" />
198
+ </div>
199
+ <div class="col-4">
200
+ <label for="epochs">Número de Épocas</label>
201
+ <input type="number" id="epochs" min="1" step="1" value="1" />
202
+ </div>
203
+
204
+ <div class="col-8" style="display:flex;align-items:flex-end;gap:8px;flex-wrap:wrap">
205
+ <button class="btn btn-primary" id="startBtn">🚀 Iniciar Treino</button>
206
+ <button class="btn btn-warn" id="reduceBtn">➖ Reduzir (-1)</button>
207
+ <button class="btn btn-success" id="increaseBtn">➕ Aumentar (+1)</button>
208
+ </div>
209
+
210
+ <div class="col-8" style="display:flex;align-items:flex-end;gap:8px;flex-wrap:wrap;margin-top:8px">
211
+ <button class="btn" id="mergeBtn">🔗 Fazer Merge</button>
212
+ <button class="btn" id="continueBtn">🔁 Continuar (+1 Época)</button>
213
+ <button class="btn" id="testBtn">🧠 Testar Modelo (sem Merge)</button>
214
+ <button class="btn" id="tensorboardBtn">📈 Ver Gráfico (TensorBoard)</button>
215
+ <button class="btn" id="modelDirBtn">📂 Ver output_dir</button>
216
+ </div>
217
+
218
+ </div>
219
+
220
+ <div class="mt-2 w-full bg-gray-200 rounded-full h-3">
221
+ <div id="progressBar" class="bg-indigo-500 h-3 rounded-full transition-all duration-300" style="width: 0%;"></div>
222
+ </div>
223
+
224
+ <h3>Status do Treino</h3>
225
+ <div id="trainingBox" class="bg-gray-800" style="height: 500px; overflow-y: auto; white-space: pre-wrap; border: 1px solid #ccc; padding: 10px;">
226
+ À espera do início do treino…
227
+ </div>
228
+
229
+ <h3>Conteúdo do Diretório do Modelo</h3>
230
+ <div id="modelDirBox"
231
+ class="bg-gray-900"
232
+ style="height:200px;overflow-y:auto;border:1px solid #444;padding:10px;font-family:monospace;">
233
+ A carregar conteúdo do diretório...
234
+ </div>
235
+ </div>
236
+ </section>
237
+ </main>
238
+
239
+ <section class="footer">
240
+ Dica: passa o rato por cima dos rótulos para ver dicas. Endpoints esperados:
241
+ <span class="kbd">GET/POST /api/config</span>,
242
+ <span class="kbd">POST /api/train</span>,
243
+ <span class="kbd">POST /api/merge</span>,
244
+ <span class="kbd">POST /api/continue</span>,
245
+ <span class="kbd">POST /api/test</span>,
246
+ <span class="kbd">GET /api/progress</span>,
247
+ <span class="kbd">GET /api/logs</span>.
248
+
249
+ <script>
250
+ // =========================
251
+ // CONFIGURAÇÃO DE ENDPOINTS
252
+ // =========================
253
+ const API = {
254
+ config: '/api/config',
255
+ train: '/api/train',
256
+ merge: '/api/decide_merge',
257
+ cont: '/api/decide_merge',
258
+ test: '/api/load_lora_chat',
259
+ train_status: '/api/train_status',
260
+ tensorboard: '/api/tensorboard/start',
261
+ adjust_accumulation: '/api/adjust_accumulation',
262
+ model_dir: '/api/model_dir'
263
+ };
264
+
265
+ // Estado local da configuração
266
+ let cfg = {};
267
+
268
+ // =========================
269
+ // HELPERS
270
+ // =========================
271
+ const $ = id => document.getElementById(id);
272
+ const toNumber = (id, def=0) => { const el = $(id); if(!el) return def; const v = parseFloat(el.value); return Number.isFinite(v)?v:def; };
273
+ const toBool = (id, def=false) => { const el = $(id); return el?el.checked:def; };
274
+ const setElementValue = (id, value) => { const el = $(id); if(!el) return false; if(el.type==='checkbox'){ el.checked=!!value; } else { el.value=value??''; } return true; };
275
+ const getElementValue = id => { const el = $(id); if(!el) return null; return el.type==='checkbox'?el.checked:el.value; };
276
+
277
+ // =========================
278
+ // UI <-> CFG
279
+ // =========================
280
+ const FIELD_MAP = {
281
+ cpu_count:'cpu_count', omp_threads:'omp_threads', dataloader_workers:'dataloader_workers',
282
+ total_ram_gb:'total_ram_gb', base_batch:'base_batch_size', base_eval_size:'base_eval_size',
283
+ base_learning_rate:'base_learning_rate', lr_scheduler_type:'lr_scheduler_type',
284
+ max_steps:'dynamic_accumulation_max_steps', estimated_batch_gb:'estimated_batch_gb',
285
+ high_ram_limit_pct:'dynamic_accumulation_high_ram_limit', low_ram_limit_pct:'dynamic_accumulation_low_ram_limit',
286
+ base_max_len:'base_max_len', ml_target_utilization:'target_max_len_utilization',
287
+ increment:'max_len_increment', cap:'max_len_cap',
288
+ estimated_base_dataset_ram_gb:'estimated_base_dataset_ram_gb', cost_per_increment_gb:'cost_per_increment_gb'
289
+ };
290
+
291
+ function applyCfgToForm(){
292
+ for(const [elId, cfgKey] of Object.entries(FIELD_MAP)){
293
+ if(cfg[cfgKey] !== undefined) setElementValue(elId, cfg[cfgKey]);
294
+ }
295
+ // chips resumo
296
+ const ramEl = $('ramSummary'); if(ramEl && cfg.target_ram_utilization) ramEl.textContent = `RAM target: ${(cfg.target_ram_utilization*100).toFixed(0)}%`;
297
+ const mlEl = $('mlSummary'); if(mlEl && cfg.max_len_cap) mlEl.textContent = `MAX_LEN: ${cfg.max_len_cap}`;
298
+ }
299
+
300
+ function readFormToCfg(){
301
+ for(const [elId, cfgKey] of Object.entries(FIELD_MAP)){
302
+ const v = getElementValue(elId);
303
+ if(v !== null) cfg[cfgKey] = v;
304
+ }
305
+ }
306
+
307
+ // =========================
308
+ // FETCH / SAVE CONFIG
309
+ // =========================
310
+ async function fetchConfigFromServer(){
311
+ try{
312
+ const r = await fetch(API.config, {cache:'no-store'});
313
+ if(!r.ok) throw new Error('HTTP '+r.status);
314
+ cfg = await r.json();
315
+ applyCfgToForm();
316
+ } catch(e){
317
+ console.warn('Não foi possível carregar config do servidor:', e);
318
+ }
319
+ }
320
+
321
+ async function saveConfigToServer(){
322
+ readFormToCfg();
323
+ try{
324
+ const r = await fetch(API.config, {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(cfg)});
325
+ if(!r.ok) throw new Error('HTTP '+r.status);
326
+ const trainingBox = $('trainingBox');
327
+ if(trainingBox) { trainingBox.textContent += '\n✅ Configuração guardada.'; trainingBox.scrollTop = trainingBox.scrollHeight; }
328
+ } catch(e){
329
+ console.error('Erro ao guardar config:', e);
330
+ const trainingBox = $('trainingBox');
331
+ if(trainingBox) { trainingBox.textContent += '\n❌ Falha ao guardar config.'; }
332
+ }
333
+ }
334
+
335
+ // =========================
336
+ // TREINO / OPERAÇÕES
337
+ // =========================
338
+ async function startTraining(){
339
+ const fd = new FormData();
340
+ fd.append('model_path', $('model_path')?.value||'');
341
+ fd.append('epochs', $('epochs')?.value||'1');
342
+ for(const f of $('train_files')?.files||[]) fd.append('files',f,f.name);
343
+
344
+ try {
345
+ const r = await fetch(API.train,{method:'POST',body:fd});
346
+ if(!r.ok) throw new Error('HTTP '+r.status);
347
+ showMessage('🚀 Treino iniciado.', 'success');
348
+ startPolling();
349
+ } catch(e){
350
+ console.error(e);
351
+ showMessage('❌ Falha ao iniciar treino.', 'error');
352
+ }
353
+ }
354
+
355
+ async function doMerge(){
356
+ const fd = new FormData();
357
+ fd.append('decision','merge');
358
+ fd.append('model_path', $('model_path')?.value||'');
359
+ try{
360
+ const r=await fetch(API.merge,{method:'POST',body:fd});
361
+ if(!r.ok) throw new Error('HTTP '+r.status);
362
+ showMessage('Merge solicitado.','success');
363
+ } catch(e){console.error(e); showMessage('Falha ao pedir merge.','error');}
364
+ }
365
+
366
+ async function doContinue(){
367
+ const fd = new FormData();
368
+ fd.append('decision','continue');
369
+ fd.append('model_path', $('model_path')?.value||'');
370
+ fd.append('epochs','1');
371
+ try{
372
+ const r=await fetch(API.cont,{method:'POST',body:fd});
373
+ if(!r.ok) throw new Error('HTTP '+r.status);
374
+ showMessage('Continuação iniciada.','success');
375
+ startPolling();
376
+ } catch(e){console.error(e); showMessage('Falha ao continuar treino.','error');}
377
+ }
378
+
379
+ async function doTest(){
380
+ const body = JSON.stringify({
381
+ model_path: $('model_path')?.value||'',
382
+ output_dir: 'trained_model_output'
383
+ });
384
+ try{
385
+ const r=await fetch(API.test,{method:'POST',headers:{'Content-Type':'application/json'},body});
386
+ if(!r.ok) throw new Error('HTTP '+r.status);
387
+ showMessage('Modelo carregado para teste.','success');
388
+ } catch(e){console.error(e); showMessage('Falha ao testar.','error');}
389
+ }
390
+ async function openTensorBoard(){ try{ const r=await fetch('/api/tensorboard/start',{method:'POST'}); if(!r.ok) throw new Error('HTTP '+r.status); const data=await r.json(); const url=data.url||'http://localhost:6006'; showMessage('TensorBoard iniciado.','success'); setTimeout(()=>window.open(url,'_blank'),1000);} catch(e){console.error(e);showMessage('Falha ao iniciar TensorBoard.','error');} }
391
+
392
+ // =========================
393
+ // POLLING TRAIN STATUS
394
+ // =========================
395
+ let pollTimer = null;
396
+
397
+ function showMessage(msg, type = 'info') {
398
+ const trainingBox = $('trainingBox');
399
+ if(!trainingBox) return;
400
+ const prefix = type === 'success' ? '✅' :
401
+ type === 'error' ? '❌' :
402
+ type === 'warning' ? '⚠️' : 'ℹ️';
403
+ trainingBox.textContent += `\n${prefix} ${msg}`;
404
+ trainingBox.scrollTop = trainingBox.scrollHeight;
405
+ }
406
+
407
+ function startPolling() {
408
+ if (pollTimer) return;
409
+
410
+ pollTimer = setInterval(async () => {
411
+ try {
412
+ const r = await fetch(API.train_status, { cache: 'no-store' });
413
+ if (!r.ok) throw new Error('HTTP ' + r.status);
414
+ const data = await r.json();
415
+
416
+ let content = '🚀 Treino iniciado\n';
417
+
418
+ // Progresso e estado
419
+ if (data.progress !== undefined) {
420
+ const pct = typeof data.progress === 'object' ? data.progress.percent : data.progress;
421
+ content += `Progresso: ${pct}%\n`;
422
+ const progressBar = $('progressBar');
423
+ if (progressBar) progressBar.style.width = `${pct}%`;
424
+ }
425
+
426
+
427
+ if (data.status) content += `Estado: ${data.status}\n`;
428
+
429
+ // Epoch losses
430
+ if (data.epoch_losses && data.epoch_losses.length > 0) {
431
+ content += `Loss por época: ${data.epoch_losses.join(', ')}\n`;
432
+ }
433
+
434
+ // Logs
435
+ if (data.logs && data.logs.length > 0) {
436
+ content += 'Logs recentes:\n' + data.logs.join('\n');
437
+ } else {
438
+ content += 'Logs ainda não recebidos...\n';
439
+ }
440
+
441
+ trainingBox.textContent = content;
442
+ trainingBox.scrollTop = trainingBox.scrollHeight;
443
+
444
+ } catch (e) {
445
+ console.error('Erro ao atualizar treinamento:', e);
446
+ showMessage('Erro ao obter estado do treino.', 'error');
447
+ }
448
+ }, 1500);
449
+ }
450
+
451
+ // =========================
452
+ // MANUAL ADJUSTMENT ACCUMULATION
453
+ // =========================
454
+ async function manualAdjustAccumulation(delta){
455
+ try{
456
+ const r = await fetch(API.adjust_accumulation,{
457
+ method:'POST',
458
+ headers:{'Content-Type':'application/json'},
459
+ body: JSON.stringify({delta})
460
+ });
461
+ if(!r.ok) throw new Error('HTTP '+r.status);
462
+ const res = await r.json();
463
+ showMessage(`Gradient Accumulation ajustado: ${res.current_steps}`,'success');
464
+ }catch(e){ console.error(e); showMessage('Falha ao ajustar accumulation_steps.','error'); }
465
+ }
466
+
467
+ // =========================
468
+ // DIRETORIO DO MODELO
469
+ // =========================
470
+
471
+ async function loadModelDir(){
472
+ const box = $('modelDirBox');
473
+ if(!box) return;
474
+
475
+ box.innerHTML = '<div class="muted">A carregar estrutura de pastas...</div>';
476
+
477
+ try {
478
+ const r = await fetch(API.model_dir, {cache:'no-store'});
479
+ const data = await r.json();
480
+
481
+ if(!data.files || data.files.length === 0){
482
+ box.innerHTML = '<div class="warn">Diretório vazio.</div>';
483
+ return;
484
+ }
485
+
486
+ box.innerHTML = ''; // Limpa a caixa
487
+ let lastFolder = "";
488
+
489
+ data.files.forEach(file => {
490
+ // Verifica se o ficheiro está dentro de uma pasta (tem uma "/")
491
+ const parts = file.split('/');
492
+ const isInsideFolder = parts.length > 1;
493
+ const currentFolder = isInsideFolder ? parts.slice(0, -1).join('/') : "Raiz do Modelo";
494
+ const fileName = parts[parts.length - 1];
495
+
496
+ // Se mudámos de pasta, cria um cabeçalho visual para a nova pasta
497
+ if (currentFolder !== lastFolder) {
498
+ const folderHeader = document.createElement('div');
499
+ folderHeader.style = "background: #21262d; color: #7aa2d6; padding: 4px 8px; margin-top: 10px; font-weight: bold; border-radius: 4px; font-size: 11px; border-left: 3px solid #1f6feb;";
500
+ folderHeader.innerHTML = `📁 PASTA: ${currentFolder}`;
501
+ box.appendChild(folderHeader);
502
+ lastFolder = currentFolder;
503
+ }
504
+
505
+ // Cria a linha do ficheiro
506
+ const row = document.createElement('div');
507
+ row.style = "display:flex; justify-content:space-between; align-items:center; padding:4px 8px; border-bottom:1px solid #30363d; margin-left: 10px;";
508
+
509
+ row.innerHTML = `
510
+ <span style="color: #e8eef6; font-size: 12px;">${fileName}</span>
511
+ <a href="/api/download/${file}" download style="color:#58a6ff; text-decoration:none; font-size: 11px; background: #161b22; padding: 2px 6px; border-radius: 4px; border: 1px solid #30363d;">📥 Download</a>
512
+ `;
513
+ box.appendChild(row);
514
+ });
515
+
516
+ } catch(e) {
517
+ box.innerHTML = `<div class="err">Erro ao organizar pastas: ${e.message}</div>`;
518
+ }
519
+ }
520
+
521
+
522
+
523
+
524
+
525
+
526
+ // =========================
527
+ // EVENT LISTENERS
528
+ // =========================
529
+ window.addEventListener('DOMContentLoaded',()=>{
530
+ ['saveCfgBtn','refreshBtn','startBtn','mergeBtn','continueBtn','testBtn','tensorboardBtn','reduceBtn','increaseBtn', 'modelDirBtn'].forEach(id=>{
531
+
532
+ const el=$(id); if(!el) return;
533
+ switch(id){
534
+ case 'saveCfgBtn': el.addEventListener('click',saveConfigToServer); break;
535
+ case 'refreshBtn': el.addEventListener('click',fetchConfigFromServer); break;
536
+ case 'startBtn': el.addEventListener('click',startTraining); break;
537
+ case 'mergeBtn': el.addEventListener('click',doMerge); break;
538
+ case 'continueBtn': el.addEventListener('click',doContinue); break;
539
+ case 'testBtn': el.addEventListener('click',doTest); break;
540
+ case 'tensorboardBtn': el.addEventListener('click',openTensorBoard); break;
541
+ case 'reduceBtn': el.addEventListener('click',()=>manualAdjustAccumulation(-1)); break;
542
+ case 'increaseBtn': el.addEventListener('click',()=>manualAdjustAccumulation(1)); break;
543
+ case 'modelDirBtn': el.addEventListener('click',loadModelDir); break;
544
+ }
545
+ });
546
+ fetchConfigFromServer();
547
+ });
548
+
549
+ </script>
550
+ </body>
551
+ </html>
552
+
553
+
trained_model_output/teste.txt ADDED
File without changes