Spaces:
Sleeping
Sleeping
Upload 5 files
Browse files- Dockerfile +13 -0
- app.py +2007 -0
- config_manager.py +193 -0
- templates/index-6.html +553 -0
- 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
|