Upload folder using huggingface_hub
Browse files- src/bigru_t/__init__.py +39 -0
- src/bigru_t/data/__init__.py +4 -0
- src/bigru_t/data/streaming_datasets.py +809 -0
- src/bigru_t/model/__init__.py +15 -0
- src/bigru_t/model/bigru4.py +54 -0
- src/bigru_t/model/hyp_t.py +74 -0
- src/bigru_t/model/module_selector.py +84 -0
- src/bigru_t/model/orq_cell.py +94 -0
- src/bigru_t/model/train_t.py +63 -0
- src/bigru_t/model/transformer_unit.py +68 -0
- src/bigru_t/model/u8cell_t.py +85 -0
- src/bigru_t/model/unified_model.py +331 -0
- src/bigru_t/multimodal/__init__.py +1 -0
- src/bigru_t/multimodal/audio_encoder.py +35 -0
- src/bigru_t/multimodal/fusion_layer.py +50 -0
- src/bigru_t/multimodal/image_encoder.py +34 -0
- src/bigru_t/multimodal/modal_router.py +51 -0
- src/bigru_t/multimodal/text_encoder.py +45 -0
- src/bigru_t/multimodal/video_encoder.py +42 -0
- src/bigru_t/optim/__init__.py +1 -0
- src/bigru_t/optim/hamiltonian_wasserstein.py +489 -0
- src/bigru_t/quantization/__init__.py +6 -0
- src/bigru_t/quantization/quantized_linear.py +89 -0
- src/bigru_t/quantization/w8a8_qoperator.py +790 -0
- src/bigru_t/tokenizer/__init__.py +1 -0
- src/bigru_t/tokenizer/bbpe_tokenizer.py +596 -0
- src/bigru_t/training/__init__.py +12 -0
- src/bigru_t/training/gradient_surgery.py +113 -0
- src/bigru_t/training/kill_switch.py +186 -0
- src/bigru_t/training/meta_configurator.py +149 -0
- src/bigru_t/training/trainer.py +599 -0
- src/bigru_t/utils/__init__.py +2 -0
- src/bigru_t/utils/hardware_detector.py +408 -0
- src/bigru_t/utils/logging_utils.py +42 -0
- src/bigru_t/utils/memory_monitor.py +390 -0
- src/bigru_t/utils/oom_guard.py +62 -0
- src/bigru_t/utils/tensor_ops.py +72 -0
- src/bigru_t/utils/validators.py +50 -0
- src/bigru_t/utils/xeon_runtime.py +232 -0
- src/setup.py +18 -0
src/bigru_t/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""BiGRU_T_version β RefatoraΓ§Γ£o do GRU-RING v13.9.2 com 4 lemas formais.
|
| 2 |
+
|
| 3 |
+
Lema 1: Desacoplamento via atenΓ§Γ£o hierΓ‘rquica (ModuleSelector)
|
| 4 |
+
Lema 2: Gradiente cirΓΊrgico (apply_gradient_surgery)
|
| 5 |
+
Lema 3: Cancelamento de ruΓdo de quantizaΓ§Γ£o (HypT + QuantizedLinear W8A8)
|
| 6 |
+
Lema 4: Auto-configuraΓ§Γ£o e suavizaΓ§Γ£o (MetaConfigurator)
|
| 7 |
+
|
| 8 |
+
Reaproveita mΓ³dulos do PowerMachine/gru-ring-v13-9-2 (BBPE tokenizer,
|
| 9 |
+
streaming datasets, W8A8, multimodal encoders, hardware detector,
|
| 10 |
+
Hamiltonian-Wasserstein optimizer, OOM guard, etc.).
|
| 11 |
+
"""
|
| 12 |
+
__version__ = "1.0.0"
|
| 13 |
+
|
| 14 |
+
from .model.unified_model import UnifiedModel, UnifiedModelConfig, create_unified_model
|
| 15 |
+
from .model.u8cell_t import u8cell_T
|
| 16 |
+
from .model.bigru4 import BiGRU4
|
| 17 |
+
from .model.transformer_unit import TransformerUnit
|
| 18 |
+
from .model.orq_cell import OrqCell
|
| 19 |
+
from .model.train_t import TrainT
|
| 20 |
+
from .model.hyp_t import HypT
|
| 21 |
+
from .model.module_selector import ModuleSelector
|
| 22 |
+
|
| 23 |
+
from .quantization.quantized_linear import QuantizedLinear, quantize_tensor, apply_w8a8
|
| 24 |
+
|
| 25 |
+
from .training.gradient_surgery import apply_gradient_surgery, orthogonalize_gradient
|
| 26 |
+
from .training.meta_configurator import MetaConfigurator
|
| 27 |
+
from .training.kill_switch import KillSwitch, KillSwitchState
|
| 28 |
+
from .training.trainer import BiGRU_T_Trainer, TrainerConfig
|
| 29 |
+
|
| 30 |
+
__all__ = [
|
| 31 |
+
"UnifiedModel", "UnifiedModelConfig", "create_unified_model",
|
| 32 |
+
"u8cell_T", "BiGRU4", "TransformerUnit", "OrqCell", "TrainT", "HypT",
|
| 33 |
+
"ModuleSelector",
|
| 34 |
+
"QuantizedLinear", "quantize_tensor", "apply_w8a8",
|
| 35 |
+
"apply_gradient_surgery", "orthogonalize_gradient",
|
| 36 |
+
"MetaConfigurator",
|
| 37 |
+
"KillSwitch", "KillSwitchState",
|
| 38 |
+
"BiGRU_T_Trainer", "TrainerConfig",
|
| 39 |
+
]
|
src/bigru_t/data/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Streaming datasets (reaproveitado do source v13.9.2)."""
|
| 2 |
+
from .streaming_datasets import stream_dataset, collate_samples_to_tensors, ProcessedSample
|
| 3 |
+
|
| 4 |
+
__all__ = ["stream_dataset", "collate_samples_to_tensors", "ProcessedSample"]
|
src/bigru_t/data/streaming_datasets.py
ADDED
|
@@ -0,0 +1,809 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""streaming_datasets_v13_9.py β Streaming dataset loader for v13.9.1 training.
|
| 2 |
+
|
| 3 |
+
Carrega os 9 datasets especificados pelo usuΓ‘rio via streaming (IterableDataset),
|
| 4 |
+
um por vez, exaustivamente dentro do limite max_samples_per_dataset.
|
| 5 |
+
|
| 6 |
+
Datasets:
|
| 7 |
+
1. CEIA-POSITIVO/ultrachat_br_clustred_balanced_v1
|
| 8 |
+
2. Madras1/corpus-ptbr-v2
|
| 9 |
+
3. rhaymison/multmodal_175k_portuguese
|
| 10 |
+
4. TucanoBR/GigaVerbo
|
| 11 |
+
5. nvidia/OpenMathReasoning
|
| 12 |
+
6. MathLLMs/MathVision
|
| 13 |
+
7. nvidia/OpenMathInstruct-2
|
| 14 |
+
8. dominguesm/restore-punctuation-ptbr-dataset
|
| 15 |
+
9. carolina-c4ai/corpus-carolina
|
| 16 |
+
|
| 17 |
+
Memory-efficient: streaming=True, no full materialization.
|
| 18 |
+
Returns ProcessedSample with raw_text only (tokenizer handles encoding).
|
| 19 |
+
"""
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import logging
|
| 23 |
+
import os
|
| 24 |
+
import time
|
| 25 |
+
import traceback
|
| 26 |
+
from dataclasses import dataclass, field
|
| 27 |
+
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
| 28 |
+
|
| 29 |
+
import torch
|
| 30 |
+
|
| 31 |
+
logger = logging.getLogger(__name__)
|
| 32 |
+
|
| 33 |
+
# Datasets V13.9.1 β exatamente os 9 especificados pelo usuΓ‘rio
|
| 34 |
+
DEFAULT_DATASETS = [
|
| 35 |
+
"CEIA-POSITIVO/ultrachat_br_clustred_balanced_v1",
|
| 36 |
+
"Madras1/corpus-ptbr-v2",
|
| 37 |
+
"rhaymison/multmodal_175k_portuguese",
|
| 38 |
+
"TucanoBR/GigaVerbo",
|
| 39 |
+
"nvidia/OpenMathReasoning",
|
| 40 |
+
"MathLLMs/MathVision",
|
| 41 |
+
"nvidia/OpenMathInstruct-2",
|
| 42 |
+
"dominguesm/restore-punctuation-ptbr-dataset",
|
| 43 |
+
"carolina-c4ai/corpus-carolina",
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
# Dataset format descriptors β Lista priorizada de campos de texto
|
| 47 |
+
DATASET_FORMATS: Dict[str, Dict[str, Any]] = {
|
| 48 |
+
"CEIA-POSITIVO/ultrachat_br_clustred_balanced_v1": {
|
| 49 |
+
"type": "chat",
|
| 50 |
+
"text_fields": ["conversa", "text", "content", "conversation", "messages"],
|
| 51 |
+
"label_fields": [],
|
| 52 |
+
"split": "train",
|
| 53 |
+
"config": None,
|
| 54 |
+
},
|
| 55 |
+
"Madras1/corpus-ptbr-v2": {
|
| 56 |
+
"type": "text",
|
| 57 |
+
"text_fields": ["text", "content", "document", "body"],
|
| 58 |
+
"label_fields": [],
|
| 59 |
+
"split": "train",
|
| 60 |
+
"config": None,
|
| 61 |
+
},
|
| 62 |
+
"rhaymison/multmodal_175k_portuguese": {
|
| 63 |
+
"type": "multimodal",
|
| 64 |
+
"text_fields": ["description", "text", "question", "prompt", "instruction", "input"],
|
| 65 |
+
"image_fields": ["image", "image_url"],
|
| 66 |
+
"label_fields": ["answer", "response", "output"],
|
| 67 |
+
"split": "train",
|
| 68 |
+
"config": None,
|
| 69 |
+
},
|
| 70 |
+
"TucanoBR/GigaVerbo": {
|
| 71 |
+
"type": "text",
|
| 72 |
+
"text_fields": ["text", "content", "document", "body"],
|
| 73 |
+
"label_fields": [],
|
| 74 |
+
"split": "train",
|
| 75 |
+
"config": None,
|
| 76 |
+
},
|
| 77 |
+
"nvidia/OpenMathReasoning": {
|
| 78 |
+
"type": "math",
|
| 79 |
+
"text_fields": ["problem", "question", "input", "prompt"],
|
| 80 |
+
"label_fields": ["solution", "answer", "output", "response"],
|
| 81 |
+
# BUG FIX: OpenMathReasoning has splits ['cot', 'tir', 'genselect', 'additional_problems']
|
| 82 |
+
"split": "cot", # Use 'cot' (chain-of-thought) as primary split
|
| 83 |
+
"config": None,
|
| 84 |
+
},
|
| 85 |
+
"MathLLMs/MathVision": {
|
| 86 |
+
"type": "math_multimodal",
|
| 87 |
+
"text_fields": ["question", "problem", "text", "query"],
|
| 88 |
+
"image_fields": ["image", "image_url"],
|
| 89 |
+
"label_fields": ["answer", "solution", "response"],
|
| 90 |
+
# BUG FIX: MathVision has only 'test' and 'testmini' splits (no train)
|
| 91 |
+
"split": "test",
|
| 92 |
+
"config": None,
|
| 93 |
+
},
|
| 94 |
+
"nvidia/OpenMathInstruct-2": {
|
| 95 |
+
"type": "math",
|
| 96 |
+
"text_fields": ["problem", "question", "input", "prompt"],
|
| 97 |
+
"label_fields": ["solution", "answer", "output", "response"],
|
| 98 |
+
"split": "train",
|
| 99 |
+
"config": None,
|
| 100 |
+
},
|
| 101 |
+
"dominguesm/restore-punctuation-ptbr-dataset": {
|
| 102 |
+
"type": "punctuation",
|
| 103 |
+
"text_fields": ["text", "original", "unpunctuated", "input"],
|
| 104 |
+
"label_fields": ["punctuated", "restored", "target", "output"],
|
| 105 |
+
"split": "train",
|
| 106 |
+
"config": None,
|
| 107 |
+
},
|
| 108 |
+
"carolina-c4ai/corpus-carolina": {
|
| 109 |
+
"type": "text",
|
| 110 |
+
"text_fields": ["text", "content", "document", "body", "xml"],
|
| 111 |
+
"label_fields": [],
|
| 112 |
+
"split": "train",
|
| 113 |
+
"config": None,
|
| 114 |
+
# V13.9.2-carolina: dataset original usa script Python (nΓ£o suportado em
|
| 115 |
+
# datasets 5.0+). Workaround: stream_xml_gz direto via lxml.iterparse,
|
| 116 |
+
# sem usar o script Python. Implementado em _stream_carolina_direct().
|
| 117 |
+
},
|
| 118 |
+
# ββ V13.9.2-finetune-v2: novos datasets PT-BR βββββββββββββββββββββββββββ
|
| 119 |
+
# Adicionados para fine-tuning contΓnuo do modelo v13.9.2 (preserva
|
| 120 |
+
# arquitetura e parΓ’metros). Nenhuma mudanΓ§a estrutural β apenas novos
|
| 121 |
+
# descritores de formato para que stream_dataset() reconheΓ§a os datasets.
|
| 122 |
+
#
|
| 123 |
+
# V13.9.2-finetune-v3: padronizaΓ§Γ£o para o formato unificado
|
| 124 |
+
# "### Instruction:\n...\n\n### Response:\n..." conforme requisitado pelo
|
| 125 |
+
# usuΓ‘rio. Apenas os datasets com estrutura instruction/response (orion,
|
| 126 |
+
# cnmoro) usam o template; strak2005/bratao continuam como texto puro
|
| 127 |
+
# (corpus sem estrutura de instruΓ§Γ£o). Nenhuma mudanΓ§a nos 9 datasets
|
| 128 |
+
# base β retrocompatibilidade total.
|
| 129 |
+
"orion-research/translations-en_US-pt_BR": {
|
| 130 |
+
"type": "translation",
|
| 131 |
+
# string = EN (instruction), string_translation = PT-BR (response).
|
| 132 |
+
# V13.9.2-finetune-v3: separa instruction/response para o template
|
| 133 |
+
# unificado em vez de concatenar como texto ΓΊnico.
|
| 134 |
+
"text_fields": ["string"],
|
| 135 |
+
"label_fields": ["string_translation"],
|
| 136 |
+
"split": "train",
|
| 137 |
+
"config": None,
|
| 138 |
+
"format_template": "instruction_response",
|
| 139 |
+
"instruction_prefix": "Translate the following text to Portuguese:",
|
| 140 |
+
},
|
| 141 |
+
"cnmoro/Instruct-PTBR-10M": {
|
| 142 |
+
"type": "instruct",
|
| 143 |
+
# NB: campos em MAIΓSCULAS no schema do dataset
|
| 144 |
+
# V13.9.2-finetune-v3: INSTRUCTION -> instruction, RESPONSE -> response
|
| 145 |
+
# via template unificado.
|
| 146 |
+
"text_fields": ["INSTRUCTION"],
|
| 147 |
+
"label_fields": ["RESPONSE"],
|
| 148 |
+
"split": "train",
|
| 149 |
+
"config": None,
|
| 150 |
+
"format_template": "instruction_response",
|
| 151 |
+
# V13.9.2-finetune-v2: dataset Γ© um ΓΊnico parquet de 10 GB β streaming
|
| 152 |
+
# via datasets.load_dataset() Γ© extremamente lento para materializar
|
| 153 |
+
# a primeira amostra. Fallback via HF datasets-server rows API (HTTP),
|
| 154 |
+
# que retorna batches de 100 rows rapidamente. Implementado em
|
| 155 |
+
# _stream_via_rows_api().
|
| 156 |
+
"loader": "rows_api",
|
| 157 |
+
},
|
| 158 |
+
"strak2005/corpus-ptbr-v1": {
|
| 159 |
+
"type": "text",
|
| 160 |
+
"text_fields": ["text", "content"],
|
| 161 |
+
"label_fields": [],
|
| 162 |
+
"split": "train",
|
| 163 |
+
"config": None,
|
| 164 |
+
# Fallback: se strak2005 cair, tenta bratao (mirror idΓͺntico)
|
| 165 |
+
"fallback": "bratao/corpus-ptbr-v1",
|
| 166 |
+
# V13.9.2-finetune-v2: dataset tem 18 shards parquet de ~1GB cada.
|
| 167 |
+
# Streaming via datasets.load_dataset() nΓ£o materializa primeira
|
| 168 |
+
# amostra em tempo aceitΓ‘vel. Fallback: baixa 1 shard localmente e
|
| 169 |
+
# itera com pyarrow. Implementado em _stream_via_parquet_first_shard().
|
| 170 |
+
"loader": "parquet_first_shard",
|
| 171 |
+
},
|
| 172 |
+
"bratao/corpus-ptbr-v1": {
|
| 173 |
+
"type": "text",
|
| 174 |
+
"text_fields": ["text", "content"],
|
| 175 |
+
"label_fields": [],
|
| 176 |
+
"split": "train",
|
| 177 |
+
"config": None,
|
| 178 |
+
"loader": "parquet_first_shard",
|
| 179 |
+
},
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
# Lista de arquivos XML.gz do Carolina que tentaremos baixar (ordem: menores
|
| 184 |
+
# taxonomias primeiro para minimizar banda e RAM). Selecionamos 2 arquivos
|
| 185 |
+
# por taxonomia para garantir diversidade sem explodir a memΓ³ria.
|
| 186 |
+
CAROLINA_XML_FILES = [
|
| 187 |
+
# taxonomia "datasets_and_other_corpora" (DAT = pt-BR notΓcias)
|
| 188 |
+
"corpus/datasets_and_other_corpora/pt-BR/DATa.xml.gz",
|
| 189 |
+
"corpus/datasets_and_other_corpora/pt-BR/DATb.xml.gz",
|
| 190 |
+
# taxonomia "wik" (wikis)
|
| 191 |
+
"corpus/wik/WIKa.xml.gz",
|
| 192 |
+
"corpus/wik/WIKb.xml.gz",
|
| 193 |
+
# taxonomia "uni" (university_domains)
|
| 194 |
+
"corpus/uni/UNIa.xml.gz",
|
| 195 |
+
"corpus/uni/UNIb.xml.gz",
|
| 196 |
+
# taxonomia "pub" (public_domain_works)
|
| 197 |
+
"corpus/pub/PUBa.xml.gz",
|
| 198 |
+
"corpus/pub/PUBb.xml.gz",
|
| 199 |
+
]
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def _stream_carolina_direct(
|
| 203 |
+
max_samples: int,
|
| 204 |
+
hf_token: Optional[str] = None,
|
| 205 |
+
timeout_per_file: int = 60,
|
| 206 |
+
) -> Iterator[Dict[str, Any]]:
|
| 207 |
+
"""Workaround V13.9.2-carolina: carrega Carolina sem usar o script Python.
|
| 208 |
+
|
| 209 |
+
Baixa arquivos XML.gz diretamente do repo e faz parse streaming com
|
| 210 |
+
lxml.etree.iterparse (compatΓvel com datasets 5.0+). Cada <TEI> vira um
|
| 211 |
+
documento de texto (concatenaΓ§Γ£o dos <p> dentro de <body>).
|
| 212 |
+
|
| 213 |
+
Args:
|
| 214 |
+
max_samples: nΓΊmero mΓ‘ximo de amostras a retornar
|
| 215 |
+
hf_token: token HF opcional (dataset Γ© pΓΊblico, mas token evita rate limit)
|
| 216 |
+
timeout_per_file: tempo mΓ‘ximo (segundos) por arquivo XML
|
| 217 |
+
|
| 218 |
+
Yields:
|
| 219 |
+
dict com chave "text" (compatΓvel com _normalize_sample)
|
| 220 |
+
"""
|
| 221 |
+
from huggingface_hub import hf_hub_download
|
| 222 |
+
import gzip
|
| 223 |
+
from lxml import etree
|
| 224 |
+
|
| 225 |
+
TEI_NS = "{http://www.tei-c.org/ns/1.0}"
|
| 226 |
+
BODY_P_TAG = f".//{TEI_NS}body/{TEI_NS}p"
|
| 227 |
+
|
| 228 |
+
count = 0
|
| 229 |
+
for xml_path in CAROLINA_XML_FILES:
|
| 230 |
+
if count >= max_samples:
|
| 231 |
+
return
|
| 232 |
+
try:
|
| 233 |
+
local_path = hf_hub_download(
|
| 234 |
+
repo_id="carolina-c4ai/corpus-carolina",
|
| 235 |
+
filename=xml_path,
|
| 236 |
+
repo_type="dataset",
|
| 237 |
+
token=hf_token,
|
| 238 |
+
)
|
| 239 |
+
except Exception as e:
|
| 240 |
+
logger.warning(f"Carolina: nΓ£o foi possΓvel baixar {xml_path}: {e}")
|
| 241 |
+
continue
|
| 242 |
+
|
| 243 |
+
try:
|
| 244 |
+
with gzip.open(local_path, "rb") as gz:
|
| 245 |
+
# iterparse streaming: nΓ£o carrega Γ‘rvore inteira na memΓ³ria
|
| 246 |
+
for _, tei in etree.iterparse(
|
| 247 |
+
gz, huge_tree=True, encoding="utf-8", tag=f"{TEI_NS}TEI"
|
| 248 |
+
):
|
| 249 |
+
if count >= max_samples:
|
| 250 |
+
tei.clear()
|
| 251 |
+
return
|
| 252 |
+
# Extrai texto dos <p> dentro de <body>
|
| 253 |
+
parts = []
|
| 254 |
+
for p in tei.findall(BODY_P_TAG):
|
| 255 |
+
if p.text:
|
| 256 |
+
parts.append(p.text)
|
| 257 |
+
text = " ".join(parts).strip()
|
| 258 |
+
if text and len(text) >= 10:
|
| 259 |
+
yield {"text": text, "meta": ""}
|
| 260 |
+
tei.clear() # libera memΓ³ria da Γ‘rvore TEI
|
| 261 |
+
count += 1
|
| 262 |
+
except Exception as e:
|
| 263 |
+
logger.warning(f"Carolina: erro processando {xml_path}: {e}")
|
| 264 |
+
continue
|
| 265 |
+
|
| 266 |
+
logger.info(f"Carolina: streamou {count} amostras no total")
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 270 |
+
# V13.9.2-finetune-v2: Loaders alternativos para datasets parquet grandes
|
| 271 |
+
# onde datasets.load_dataset(streaming=True) Γ© inviΓ‘vel (primeira amostra
|
| 272 |
+
# demora minutos para materializar).
|
| 273 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 274 |
+
|
| 275 |
+
def _stream_via_rows_api(
|
| 276 |
+
dataset_name: str,
|
| 277 |
+
max_samples: int,
|
| 278 |
+
hf_token: Optional[str] = None,
|
| 279 |
+
batch_size: int = 100,
|
| 280 |
+
max_offset: int = 100_000,
|
| 281 |
+
) -> Iterator[Dict[str, Any]]:
|
| 282 |
+
"""Carrega amostras via HF datasets-server /rows API (HTTP).
|
| 283 |
+
|
| 284 |
+
Usado para datasets cujo parquet Γ© muito grande para streaming eficiente
|
| 285 |
+
(ex: cnmoro/Instruct-PTBR-10M com 10 GB em arquivo ΓΊnico). A rows API
|
| 286 |
+
retorna batches de atΓ© 100 rows via HTTP, sem precisar baixar o parquet.
|
| 287 |
+
|
| 288 |
+
Args:
|
| 289 |
+
dataset_name: repo_id do dataset (ex: "cnmoro/Instruct-PTBR-10M")
|
| 290 |
+
max_samples: nΒΊ mΓ‘ximo de amostras a retornar
|
| 291 |
+
hf_token: token HF (opcional β dataset pΓΊblico)
|
| 292 |
+
batch_size: nΒΊ de rows por requisiΓ§Γ£o (mΓ‘x 100)
|
| 293 |
+
max_offset: offset mΓ‘ximo a tentar (sai do loop se excedido)
|
| 294 |
+
|
| 295 |
+
Yields:
|
| 296 |
+
dict com campos do dataset (compatΓvel com _normalize_sample)
|
| 297 |
+
"""
|
| 298 |
+
import json as _json
|
| 299 |
+
import urllib.request
|
| 300 |
+
import urllib.parse
|
| 301 |
+
|
| 302 |
+
base_url = "https://datasets-server.huggingface.co/rows"
|
| 303 |
+
count = 0
|
| 304 |
+
offset = 0
|
| 305 |
+
|
| 306 |
+
while count < max_samples and offset < max_offset:
|
| 307 |
+
params = urllib.parse.urlencode({
|
| 308 |
+
"dataset": dataset_name,
|
| 309 |
+
"config": "default",
|
| 310 |
+
"split": "train",
|
| 311 |
+
"offset": offset,
|
| 312 |
+
"length": min(batch_size, max_samples - count),
|
| 313 |
+
})
|
| 314 |
+
url = f"{base_url}?{params}"
|
| 315 |
+
try:
|
| 316 |
+
req = urllib.request.Request(url)
|
| 317 |
+
if hf_token:
|
| 318 |
+
req.add_header("Authorization", f"Bearer {hf_token}")
|
| 319 |
+
with urllib.request.urlopen(req, timeout=30) as r:
|
| 320 |
+
data = _json.loads(r.read())
|
| 321 |
+
except Exception as e:
|
| 322 |
+
logger.warning(f"rows_api {dataset_name} offset={offset}: {e}")
|
| 323 |
+
break
|
| 324 |
+
|
| 325 |
+
rows = data.get("rows", [])
|
| 326 |
+
if not rows:
|
| 327 |
+
break
|
| 328 |
+
|
| 329 |
+
for row in rows:
|
| 330 |
+
if count >= max_samples:
|
| 331 |
+
return
|
| 332 |
+
row_data = row.get("row", {})
|
| 333 |
+
# Remove campos internos como __index_level_0__
|
| 334 |
+
clean = {k: v for k, v in row_data.items()
|
| 335 |
+
if not k.startswith("__")}
|
| 336 |
+
yield clean
|
| 337 |
+
count += 1
|
| 338 |
+
|
| 339 |
+
offset += len(rows)
|
| 340 |
+
if len(rows) < batch_size:
|
| 341 |
+
break # chegou ao fim do dataset
|
| 342 |
+
|
| 343 |
+
logger.info(f"rows_api {dataset_name}: streamou {count} amostras")
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def _stream_via_parquet_first_shard(
|
| 347 |
+
dataset_name: str,
|
| 348 |
+
max_samples: int,
|
| 349 |
+
hf_token: Optional[str] = None,
|
| 350 |
+
) -> Iterator[Dict[str, Any]]:
|
| 351 |
+
"""Baixa primeiro shard parquet do dataset e itera localmente com pyarrow.
|
| 352 |
+
|
| 353 |
+
Usado para datasets com mΓΊltiplos shards parquet grandes (ex:
|
| 354 |
+
strak2005/corpus-ptbr-v1 com 18 shards de ~1GB). Streaming via
|
| 355 |
+
datasets.load_dataset() nΓ£o materializa primeira amostra em tempo
|
| 356 |
+
aceitΓ‘vel. Esta funΓ§Γ£o baixa o primeiro shard (~26s para 1GB) e itera
|
| 357 |
+
localmente β rΓ‘pido apΓ³s download.
|
| 358 |
+
|
| 359 |
+
Args:
|
| 360 |
+
dataset_name: repo_id do dataset
|
| 361 |
+
max_samples: nΒΊ mΓ‘ximo de amostras
|
| 362 |
+
hf_token: token HF opcional
|
| 363 |
+
|
| 364 |
+
Yields:
|
| 365 |
+
dict com campos do dataset
|
| 366 |
+
"""
|
| 367 |
+
from huggingface_hub import HfApi, hf_hub_download
|
| 368 |
+
import pyarrow.parquet as pq
|
| 369 |
+
|
| 370 |
+
api = HfApi(token=hf_token)
|
| 371 |
+
|
| 372 |
+
# Lista arquivos parquet no repo
|
| 373 |
+
try:
|
| 374 |
+
info = api.dataset_info(dataset_name)
|
| 375 |
+
except Exception as e:
|
| 376 |
+
logger.warning(f"parquet_shard {dataset_name}: dataset_info failed: {e}")
|
| 377 |
+
return
|
| 378 |
+
|
| 379 |
+
parquet_files = sorted([
|
| 380 |
+
s.rfilename for s in info.siblings
|
| 381 |
+
if s.rfilename.endswith(".parquet")
|
| 382 |
+
])
|
| 383 |
+
if not parquet_files:
|
| 384 |
+
logger.warning(f"parquet_shard {dataset_name}: nenhum .parquet encontrado")
|
| 385 |
+
return
|
| 386 |
+
|
| 387 |
+
# Baixa apenas o primeiro shard
|
| 388 |
+
first_shard = parquet_files[0]
|
| 389 |
+
logger.info(f"parquet_shard {dataset_name}: baixando {first_shard}...")
|
| 390 |
+
try:
|
| 391 |
+
local_path = hf_hub_download(
|
| 392 |
+
repo_id=dataset_name,
|
| 393 |
+
filename=first_shard,
|
| 394 |
+
repo_type="dataset",
|
| 395 |
+
token=hf_token,
|
| 396 |
+
)
|
| 397 |
+
except Exception as e:
|
| 398 |
+
logger.warning(f"parquet_shard {dataset_name}: download failed: {e}")
|
| 399 |
+
return
|
| 400 |
+
|
| 401 |
+
# Itera com pyarrow.iter_batches β mΓ©todo eficiente que lΓͺ batches pequenos
|
| 402 |
+
# do parquet sem materializar o row group inteiro na memΓ³ria.
|
| 403 |
+
# BUG FIX V13.9.2-finetune-v2: read_row_group() materializa TODAS as ~1M
|
| 404 |
+
# rows de uma vez (lento + ~1GB RAM). iter_batches(batch_size=10) lΓͺ sΓ³
|
| 405 |
+
# 10 rows por vez (rΓ‘pido + ~10KB RAM).
|
| 406 |
+
try:
|
| 407 |
+
pf = pq.ParquetFile(local_path)
|
| 408 |
+
count = 0
|
| 409 |
+
# batch_read_size: nΒΊ de rows por batch (pequeno = menos RAM)
|
| 410 |
+
for batch in pf.iter_batches(batch_size=100):
|
| 411 |
+
if count >= max_samples:
|
| 412 |
+
break
|
| 413 |
+
cols = batch.schema.names
|
| 414 |
+
# to_pylist() Γ© eficiente para batches pequenos
|
| 415 |
+
pydict = batch.to_pydict()
|
| 416 |
+
n_rows = len(pydict[cols[0]]) if cols else 0
|
| 417 |
+
for i in range(n_rows):
|
| 418 |
+
if count >= max_samples:
|
| 419 |
+
break
|
| 420 |
+
row = {col: pydict[col][i] for col in cols}
|
| 421 |
+
yield row
|
| 422 |
+
count += 1
|
| 423 |
+
except Exception as e:
|
| 424 |
+
logger.warning(f"parquet_shard {dataset_name}: pyarrow iter failed: {e}")
|
| 425 |
+
return
|
| 426 |
+
|
| 427 |
+
logger.info(f"parquet_shard {dataset_name}: streamou {count} amostras do shard {first_shard}")
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
@dataclass
|
| 431 |
+
class ProcessedSample:
|
| 432 |
+
"""Uma amostra processada: texto bruto + metadados."""
|
| 433 |
+
raw_text: str
|
| 434 |
+
dataset_name: str
|
| 435 |
+
sample_idx: int
|
| 436 |
+
text_field: str = ""
|
| 437 |
+
label_text: str = ""
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
def _extract_field(sample: Dict[str, Any], candidate_fields: List[str]) -> Optional[str]:
|
| 441 |
+
"""Extrai texto da primeira field disponΓvel no sample."""
|
| 442 |
+
for field_name in candidate_fields:
|
| 443 |
+
if field_name in sample:
|
| 444 |
+
val = sample[field_name]
|
| 445 |
+
if val is None:
|
| 446 |
+
continue
|
| 447 |
+
if isinstance(val, str):
|
| 448 |
+
if val.strip():
|
| 449 |
+
return val
|
| 450 |
+
elif isinstance(val, list):
|
| 451 |
+
# Para chat: messages = [{role, content}, ...]
|
| 452 |
+
parts = []
|
| 453 |
+
for msg in val:
|
| 454 |
+
if isinstance(msg, dict):
|
| 455 |
+
role = msg.get("role", "")
|
| 456 |
+
content = msg.get("content", "")
|
| 457 |
+
if isinstance(content, str) and content.strip():
|
| 458 |
+
parts.append(f"{role}: {content}")
|
| 459 |
+
elif isinstance(msg, str) and msg.strip():
|
| 460 |
+
parts.append(msg)
|
| 461 |
+
if parts:
|
| 462 |
+
return "\n".join(parts)
|
| 463 |
+
elif isinstance(val, dict):
|
| 464 |
+
# Pode ser nested
|
| 465 |
+
text = val.get("text") or val.get("content") or val.get("body")
|
| 466 |
+
if isinstance(text, str) and text.strip():
|
| 467 |
+
return text
|
| 468 |
+
return None
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
def _normalize_sample(sample: Dict[str, Any], dataset_name: str, idx: int) -> Optional[ProcessedSample]:
|
| 472 |
+
"""Normaliza uma amostra bruta em ProcessedSample."""
|
| 473 |
+
fmt = DATASET_FORMATS.get(dataset_name, {})
|
| 474 |
+
text_fields = fmt.get("text_fields", ["text"])
|
| 475 |
+
label_fields = fmt.get("label_fields", [])
|
| 476 |
+
|
| 477 |
+
text = _extract_field(sample, text_fields)
|
| 478 |
+
if not text or len(text.strip()) < 10: # skip very short
|
| 479 |
+
return None
|
| 480 |
+
|
| 481 |
+
label = ""
|
| 482 |
+
if label_fields:
|
| 483 |
+
label = _extract_field(sample, label_fields) or ""
|
| 484 |
+
|
| 485 |
+
# Se tem label, aplica template apropriado
|
| 486 |
+
template = fmt.get("format_template")
|
| 487 |
+
full_text = text
|
| 488 |
+
|
| 489 |
+
# V13.9.2-finetune-v3: para o template instruction_response, exige ambos
|
| 490 |
+
# os lados (instruction + response). Samples sem response sΓ£o descartados
|
| 491 |
+
# para evitar treinar em texto parcial.
|
| 492 |
+
if template == "instruction_response" and not label:
|
| 493 |
+
return None
|
| 494 |
+
|
| 495 |
+
if label:
|
| 496 |
+
# V13.9.2-finetune-v3: format_template unificado "### Instruction:/### Response:"
|
| 497 |
+
# quando o dataset declarar explicitamente. Caso contrΓ‘rio, mantΓ©m o
|
| 498 |
+
# comportamento legado (concatenaΓ§Γ£o simples text\nlabel) para os 9
|
| 499 |
+
# datasets base β retrocompatibilidade total.
|
| 500 |
+
if template == "instruction_response":
|
| 501 |
+
instr_prefix = fmt.get("instruction_prefix", "")
|
| 502 |
+
if instr_prefix:
|
| 503 |
+
full_text = (
|
| 504 |
+
f"### Instruction:\n{instr_prefix}\n{text}\n\n"
|
| 505 |
+
f"### Response:\n{label}"
|
| 506 |
+
)
|
| 507 |
+
else:
|
| 508 |
+
full_text = (
|
| 509 |
+
f"### Instruction:\n{text}\n\n"
|
| 510 |
+
f"### Response:\n{label}"
|
| 511 |
+
)
|
| 512 |
+
else:
|
| 513 |
+
# Comportamento legado (9 datasets base): concatenaΓ§Γ£o simples
|
| 514 |
+
full_text = f"{text}\n{label}"
|
| 515 |
+
|
| 516 |
+
return ProcessedSample(
|
| 517 |
+
raw_text=full_text,
|
| 518 |
+
dataset_name=dataset_name,
|
| 519 |
+
sample_idx=idx,
|
| 520 |
+
text_field=text_fields[0] if text_fields else "",
|
| 521 |
+
label_text=label,
|
| 522 |
+
)
|
| 523 |
+
|
| 524 |
+
|
| 525 |
+
def _load_dataset_streaming(
|
| 526 |
+
dataset_name: str,
|
| 527 |
+
split: str = "train",
|
| 528 |
+
config: Optional[str] = None,
|
| 529 |
+
hf_token: Optional[str] = None,
|
| 530 |
+
):
|
| 531 |
+
"""Carrega dataset em modo streaming.
|
| 532 |
+
|
| 533 |
+
BUG FIX V13.9.1: Removido trust_remote_code (deprecated).
|
| 534 |
+
Se o dataset nΓ£o carregar com split especificado, tenta 'train' como fallback.
|
| 535 |
+
BUG FIX V13.9.2: Adicionado fallback para datasets com script (carolina-c4ai).
|
| 536 |
+
"""
|
| 537 |
+
from datasets import load_dataset
|
| 538 |
+
|
| 539 |
+
# Lista de splits para tentar, em ordem
|
| 540 |
+
splits_to_try = [split]
|
| 541 |
+
if split != "train":
|
| 542 |
+
splits_to_try.append("train")
|
| 543 |
+
# Adiciona outros splits comuns
|
| 544 |
+
for s in ["test", "validation", "cot", "tir"]:
|
| 545 |
+
if s not in splits_to_try:
|
| 546 |
+
splits_to_try.append(s)
|
| 547 |
+
|
| 548 |
+
fmt = DATASET_FORMATS.get(dataset_name, {})
|
| 549 |
+
fallback_name = fmt.get("fallback")
|
| 550 |
+
datasets_to_try = [dataset_name]
|
| 551 |
+
if fallback_name:
|
| 552 |
+
datasets_to_try.append(fallback_name)
|
| 553 |
+
|
| 554 |
+
for try_ds_name in datasets_to_try:
|
| 555 |
+
for try_split in splits_to_try:
|
| 556 |
+
try:
|
| 557 |
+
if config:
|
| 558 |
+
ds = load_dataset(
|
| 559 |
+
try_ds_name, config, split=try_split, streaming=True,
|
| 560 |
+
token=hf_token,
|
| 561 |
+
)
|
| 562 |
+
else:
|
| 563 |
+
ds = load_dataset(
|
| 564 |
+
try_ds_name, split=try_split, streaming=True,
|
| 565 |
+
token=hf_token,
|
| 566 |
+
)
|
| 567 |
+
logger.info(f"Loaded {try_ds_name} split={try_split}")
|
| 568 |
+
return ds
|
| 569 |
+
except Exception as e:
|
| 570 |
+
err_str = str(e).lower()
|
| 571 |
+
# If "Bad split" error, try next split
|
| 572 |
+
if "bad split" in err_str or "available splits" in err_str:
|
| 573 |
+
logger.info(f" Split {try_split} not available for {try_ds_name}, trying next")
|
| 574 |
+
continue
|
| 575 |
+
# If "scripts no longer supported", try next dataset (fallback)
|
| 576 |
+
if "scripts are no longer supported" in err_str and fallback_name:
|
| 577 |
+
logger.info(f" {try_ds_name} uses script β trying fallback {fallback_name}")
|
| 578 |
+
break
|
| 579 |
+
# For other errors, log and try next split
|
| 580 |
+
logger.warning(f"Failed to load {try_ds_name} split={try_split}: {str(e)[:120]}")
|
| 581 |
+
continue
|
| 582 |
+
|
| 583 |
+
logger.warning(f"All split attempts failed for {dataset_name} (will be skipped)")
|
| 584 |
+
return None
|
| 585 |
+
|
| 586 |
+
|
| 587 |
+
def stream_dataset(
|
| 588 |
+
dataset_name: str,
|
| 589 |
+
max_samples: int = 500,
|
| 590 |
+
hf_token: Optional[str] = None,
|
| 591 |
+
) -> Iterator[ProcessedSample]:
|
| 592 |
+
"""Faz streaming de um dataset, retornando atΓ© max_samples amostras.
|
| 593 |
+
|
| 594 |
+
Args:
|
| 595 |
+
dataset_name: nome do dataset no HuggingFace
|
| 596 |
+
max_samples: nΓΊmero mΓ‘ximo de amostras a retornar
|
| 597 |
+
hf_token: token HF opcional
|
| 598 |
+
|
| 599 |
+
Yields:
|
| 600 |
+
ProcessedSample
|
| 601 |
+
"""
|
| 602 |
+
fmt = DATASET_FORMATS.get(dataset_name)
|
| 603 |
+
if fmt is None:
|
| 604 |
+
logger.error(f"Unknown dataset: {dataset_name}")
|
| 605 |
+
return
|
| 606 |
+
|
| 607 |
+
# ββ V13.9.2-carolina: bypass do script Python βββββββββββββββββββββββββ
|
| 608 |
+
# O dataset carolina-c4ai/corpus-carolina usa um script Python como loader,
|
| 609 |
+
# que nΓ£o Γ© suportado por datasets 5.0+. Streamamos os XML.gz diretamente.
|
| 610 |
+
if dataset_name == "carolina-c4ai/corpus-carolina":
|
| 611 |
+
logger.info(f"Carolina: usando carregador direto (bypass do script Python)")
|
| 612 |
+
count = 0
|
| 613 |
+
try:
|
| 614 |
+
for raw_sample in _stream_carolina_direct(
|
| 615 |
+
max_samples=max_samples, hf_token=hf_token
|
| 616 |
+
):
|
| 617 |
+
processed = _normalize_sample(raw_sample, dataset_name, count)
|
| 618 |
+
if processed is not None:
|
| 619 |
+
yield processed
|
| 620 |
+
count += 1
|
| 621 |
+
except Exception as e:
|
| 622 |
+
logger.warning(f"Carolina direct stream error at sample {count}: {e}")
|
| 623 |
+
return
|
| 624 |
+
logger.info(f"Streamed {count} samples from {dataset_name} (direct XML)")
|
| 625 |
+
return
|
| 626 |
+
|
| 627 |
+
# ββ V13.9.2-finetune-v2: loaders alternativos para parquets grandes βββ
|
| 628 |
+
# Alguns datasets (cnmoro, strak2005/bratao) tΓͺm parquets tΓ£o grandes que
|
| 629 |
+
# datasets.load_dataset(streaming=True) demora minutos para materializar
|
| 630 |
+
# a primeira amostra. Usamos loaders alternativos especificados no campo
|
| 631 |
+
# "loader" do formato.
|
| 632 |
+
loader = fmt.get("loader")
|
| 633 |
+
if loader == "rows_api":
|
| 634 |
+
logger.info(f"{dataset_name}: usando rows_api loader (datasets-server HTTP)")
|
| 635 |
+
count = 0
|
| 636 |
+
try:
|
| 637 |
+
for raw_sample in _stream_via_rows_api(
|
| 638 |
+
dataset_name, max_samples=max_samples, hf_token=hf_token
|
| 639 |
+
):
|
| 640 |
+
processed = _normalize_sample(raw_sample, dataset_name, count)
|
| 641 |
+
if processed is not None:
|
| 642 |
+
yield processed
|
| 643 |
+
count += 1
|
| 644 |
+
except Exception as e:
|
| 645 |
+
logger.warning(f"rows_api stream error at sample {count}: {e}")
|
| 646 |
+
return
|
| 647 |
+
logger.info(f"Streamed {count} samples from {dataset_name} (rows_api)")
|
| 648 |
+
return
|
| 649 |
+
|
| 650 |
+
if loader == "parquet_first_shard":
|
| 651 |
+
logger.info(f"{dataset_name}: usando parquet_first_shard loader")
|
| 652 |
+
count = 0
|
| 653 |
+
try:
|
| 654 |
+
for raw_sample in _stream_via_parquet_first_shard(
|
| 655 |
+
dataset_name, max_samples=max_samples, hf_token=hf_token
|
| 656 |
+
):
|
| 657 |
+
processed = _normalize_sample(raw_sample, dataset_name, count)
|
| 658 |
+
if processed is not None:
|
| 659 |
+
yield processed
|
| 660 |
+
count += 1
|
| 661 |
+
except Exception as e:
|
| 662 |
+
logger.warning(f"parquet_shard stream error at sample {count}: {e}")
|
| 663 |
+
# Tenta fallback (ex: strak2005 -> bratao)
|
| 664 |
+
fallback_name = fmt.get("fallback")
|
| 665 |
+
if fallback_name and fallback_name != dataset_name:
|
| 666 |
+
logger.info(f"Tentando fallback: {fallback_name}")
|
| 667 |
+
fallback_fmt = DATASET_FORMATS.get(fallback_name, {})
|
| 668 |
+
# RecursΓ£o respeitando o loader do fallback
|
| 669 |
+
yield from stream_dataset(
|
| 670 |
+
fallback_name, max_samples=max_samples, hf_token=hf_token
|
| 671 |
+
)
|
| 672 |
+
return
|
| 673 |
+
logger.info(f"Streamed {count} samples from {dataset_name} (parquet_first_shard)")
|
| 674 |
+
return
|
| 675 |
+
|
| 676 |
+
ds = _load_dataset_streaming(
|
| 677 |
+
dataset_name,
|
| 678 |
+
split=fmt.get("split", "train"),
|
| 679 |
+
config=fmt.get("config"),
|
| 680 |
+
hf_token=hf_token,
|
| 681 |
+
)
|
| 682 |
+
if ds is None:
|
| 683 |
+
return
|
| 684 |
+
|
| 685 |
+
count = 0
|
| 686 |
+
try:
|
| 687 |
+
for idx, sample in enumerate(ds):
|
| 688 |
+
if count >= max_samples:
|
| 689 |
+
break
|
| 690 |
+
processed = _normalize_sample(sample, dataset_name, idx)
|
| 691 |
+
if processed is not None:
|
| 692 |
+
yield processed
|
| 693 |
+
count += 1
|
| 694 |
+
except Exception as e:
|
| 695 |
+
logger.warning(f"Error streaming {dataset_name} at sample {idx}: {e}")
|
| 696 |
+
return
|
| 697 |
+
|
| 698 |
+
logger.info(f"Streamed {count} samples from {dataset_name}")
|
| 699 |
+
|
| 700 |
+
|
| 701 |
+
def stream_all_datasets(
|
| 702 |
+
datasets: Optional[List[str]] = None,
|
| 703 |
+
max_samples_per_dataset: int = 500,
|
| 704 |
+
hf_token: Optional[str] = None,
|
| 705 |
+
) -> Iterator[ProcessedSample]:
|
| 706 |
+
"""Faz streaming sequencial de mΓΊltiplos datasets.
|
| 707 |
+
|
| 708 |
+
Args:
|
| 709 |
+
datasets: lista de nomes (default: DEFAULT_DATASETS)
|
| 710 |
+
max_samples_per_dataset: limite por dataset
|
| 711 |
+
hf_token: token HF opcional
|
| 712 |
+
|
| 713 |
+
Yields:
|
| 714 |
+
ProcessedSample
|
| 715 |
+
"""
|
| 716 |
+
if datasets is None:
|
| 717 |
+
datasets = DEFAULT_DATASETS
|
| 718 |
+
|
| 719 |
+
for ds_name in datasets:
|
| 720 |
+
logger.info(f"Starting dataset: {ds_name}")
|
| 721 |
+
yield from stream_dataset(
|
| 722 |
+
ds_name,
|
| 723 |
+
max_samples=max_samples_per_dataset,
|
| 724 |
+
hf_token=hf_token,
|
| 725 |
+
)
|
| 726 |
+
|
| 727 |
+
|
| 728 |
+
def collate_samples_to_tensors(
|
| 729 |
+
samples: List[ProcessedSample],
|
| 730 |
+
tokenizer,
|
| 731 |
+
max_seq_len: int = 64,
|
| 732 |
+
pad_token_id: int = 0,
|
| 733 |
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 734 |
+
"""Converte lista de ProcessedSample em tensors para treino.
|
| 735 |
+
|
| 736 |
+
BUG FIX V13.9.1: Tokenizer.encode() retorna 'Encoding' object, nΓ£o list.
|
| 737 |
+
Use .ids attribute to get token IDs.
|
| 738 |
+
|
| 739 |
+
Returns:
|
| 740 |
+
input_ids: (B, T)
|
| 741 |
+
attention_mask: (B, T)
|
| 742 |
+
labels: (B, T) β same as input_ids (LM training)
|
| 743 |
+
"""
|
| 744 |
+
batch_input_ids = []
|
| 745 |
+
batch_attn_mask = []
|
| 746 |
+
|
| 747 |
+
for sample in samples:
|
| 748 |
+
# Tokenize
|
| 749 |
+
encoding = tokenizer.encode(sample.raw_text, add_special_tokens=True)
|
| 750 |
+
|
| 751 |
+
# BUG FIX: Handle both `tokenizers.Encoding` and `transformers` tokenizer outputs
|
| 752 |
+
if hasattr(encoding, 'ids'):
|
| 753 |
+
tokens = encoding.ids # tokenizers library
|
| 754 |
+
elif isinstance(encoding, list):
|
| 755 |
+
tokens = encoding # already a list of ints
|
| 756 |
+
elif hasattr(encoding, 'input_ids'):
|
| 757 |
+
tokens = encoding.input_ids # transformers BatchEncoding
|
| 758 |
+
else:
|
| 759 |
+
tokens = list(encoding) # fallback
|
| 760 |
+
|
| 761 |
+
# Truncate to max_seq_len
|
| 762 |
+
if len(tokens) > max_seq_len:
|
| 763 |
+
tokens = tokens[:max_seq_len]
|
| 764 |
+
# Pad
|
| 765 |
+
attn_mask = [1] * len(tokens) + [0] * (max_seq_len - len(tokens))
|
| 766 |
+
tokens = list(tokens) + [pad_token_id] * (max_seq_len - len(tokens))
|
| 767 |
+
batch_input_ids.append(tokens)
|
| 768 |
+
batch_attn_mask.append(attn_mask)
|
| 769 |
+
|
| 770 |
+
input_ids = torch.tensor(batch_input_ids, dtype=torch.long)
|
| 771 |
+
attention_mask = torch.tensor(batch_attn_mask, dtype=torch.long)
|
| 772 |
+
labels = input_ids.clone()
|
| 773 |
+
|
| 774 |
+
return input_ids, attention_mask, labels
|
| 775 |
+
|
| 776 |
+
|
| 777 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 778 |
+
# Self-test (NΓO enviar para HuggingFace)
|
| 779 |
+
# βββββββββββββββββββββββββββοΏ½οΏ½βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 780 |
+
if __name__ == "__main__":
|
| 781 |
+
print("=== Streaming Datasets V13.9.1 Self-Test ===\n")
|
| 782 |
+
|
| 783 |
+
# Test 1: Test field extraction
|
| 784 |
+
print("Test 1: Field extraction")
|
| 785 |
+
sample_chat = {
|
| 786 |
+
"messages": [
|
| 787 |
+
{"role": "user", "content": "OlΓ‘"},
|
| 788 |
+
{"role": "assistant", "content": "Como posso ajudar?"},
|
| 789 |
+
]
|
| 790 |
+
}
|
| 791 |
+
text = _extract_field(sample_chat, ["messages", "text"])
|
| 792 |
+
print(f" Chat text: {text!r}")
|
| 793 |
+
assert text and "OlΓ‘" in text, "Field extraction failed for chat"
|
| 794 |
+
|
| 795 |
+
sample_text = {"text": "Hello world"}
|
| 796 |
+
text = _extract_field(sample_text, ["text", "content"])
|
| 797 |
+
assert text == "Hello world"
|
| 798 |
+
print(f" Text field: {text!r}")
|
| 799 |
+
print(f" OK\n")
|
| 800 |
+
|
| 801 |
+
# Test 2: Test list of datasets
|
| 802 |
+
print("Test 2: Dataset list")
|
| 803 |
+
print(f" Configured datasets ({len(DEFAULT_DATASETS)}):")
|
| 804 |
+
for i, ds in enumerate(DEFAULT_DATASETS, 1):
|
| 805 |
+
print(f" {i}. {ds}")
|
| 806 |
+
assert len(DEFAULT_DATASETS) == 9
|
| 807 |
+
print(f" OK\n")
|
| 808 |
+
|
| 809 |
+
print("=== ALL STREAMING TESTS PASSED ===")
|
src/bigru_t/model/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Modelos do BiGRU_T_version."""
|
| 2 |
+
from .bigru4 import BiGRU4
|
| 3 |
+
from .transformer_unit import TransformerUnit
|
| 4 |
+
from .u8cell_t import u8cell_T
|
| 5 |
+
from .orq_cell import OrqCell
|
| 6 |
+
from .train_t import TrainT
|
| 7 |
+
from .hyp_t import HypT
|
| 8 |
+
from .module_selector import ModuleSelector
|
| 9 |
+
from .unified_model import UnifiedModel, UnifiedModelConfig, create_unified_model
|
| 10 |
+
|
| 11 |
+
__all__ = [
|
| 12 |
+
"BiGRU4", "TransformerUnit", "u8cell_T", "OrqCell",
|
| 13 |
+
"TrainT", "HypT", "ModuleSelector",
|
| 14 |
+
"UnifiedModel", "UnifiedModelConfig", "create_unified_model",
|
| 15 |
+
]
|
src/bigru_t/model/bigru4.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""bigru4.py β BiGRU4: 4 camadas BiGRU sequenciais.
|
| 2 |
+
|
| 3 |
+
Componente base do u8cell_T (Lema 1). Cada BiGRU4 processa a sequΓͺncia de
|
| 4 |
+
entrada atravΓ©s de 4 camadas GRU bidirecionais empilhadas, produzindo uma
|
| 5 |
+
representaΓ§Γ£o agregada por mΓ©dia temporal.
|
| 6 |
+
|
| 7 |
+
Reaproveita a filosofia das 4 camadas cooperativas do GRU-RING v13.9.2,
|
| 8 |
+
mas sem bridge/attention interna β a cooperaΓ§Γ£o entre as 8 BiGRU4 paralelas
|
| 9 |
+
Γ© feita pelo TransformerUnit do u8cell_T.
|
| 10 |
+
"""
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
import torch.nn as nn
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class BiGRU4(nn.Module):
|
| 18 |
+
"""Quatro camadas BiGRU sequenciais.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
input_dim: dimensΓ£o da entrada (batch, T, input_dim)
|
| 22 |
+
hidden_dim: dimensΓ£o oculta de cada GRU (a saΓda tem 2*hidden_dim por ser bidirecional)
|
| 23 |
+
dropout: dropout entre camadas (0 = desativado)
|
| 24 |
+
|
| 25 |
+
Forward:
|
| 26 |
+
x: (batch, T, input_dim) β v: (batch, 2*hidden_dim)
|
| 27 |
+
v Γ© a mΓ©dia temporal da saΓda da 4Βͺ GRU.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def __init__(self, input_dim: int, hidden_dim: int, dropout: float = 0.0):
|
| 31 |
+
super().__init__()
|
| 32 |
+
self.input_dim = input_dim
|
| 33 |
+
self.hidden_dim = hidden_dim
|
| 34 |
+
self.output_dim = 2 * hidden_dim # bidirecional
|
| 35 |
+
|
| 36 |
+
self.gru1 = nn.GRU(input_dim, hidden_dim, bidirectional=True, batch_first=True)
|
| 37 |
+
self.gru2 = nn.GRU(2 * hidden_dim, hidden_dim, bidirectional=True, batch_first=True)
|
| 38 |
+
self.gru3 = nn.GRU(2 * hidden_dim, hidden_dim, bidirectional=True, batch_first=True)
|
| 39 |
+
self.gru4 = nn.GRU(2 * hidden_dim, hidden_dim, bidirectional=True, batch_first=True)
|
| 40 |
+
|
| 41 |
+
self.dropout = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
|
| 42 |
+
|
| 43 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 44 |
+
"""x: (batch, T, input_dim) β v: (batch, 2*hidden_dim)"""
|
| 45 |
+
out, _ = self.gru1(x)
|
| 46 |
+
out = self.dropout(out)
|
| 47 |
+
out, _ = self.gru2(out)
|
| 48 |
+
out = self.dropout(out)
|
| 49 |
+
out, _ = self.gru3(out)
|
| 50 |
+
out = self.dropout(out)
|
| 51 |
+
out, _ = self.gru4(out)
|
| 52 |
+
# MΓ©dia temporal para obter representaΓ§Γ£o fixa (independente de T)
|
| 53 |
+
v = out.mean(dim=1) # (batch, 2*hidden_dim)
|
| 54 |
+
return v
|
src/bigru_t/model/hyp_t.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""hyp_t.py β HypT: cabeΓ§a de hipΓ³tese (correΓ§Γ£o delta). Implementa o Lema 3.
|
| 2 |
+
|
| 3 |
+
A HipΓ³tese Γ© treinada para cancelar o ruΓdo de quantizaΓ§Γ£o W8A8:
|
| 4 |
+
y_final = y_hat + delta
|
| 5 |
+
onde delta = hyp_T(o).
|
| 6 |
+
|
| 7 |
+
DiferenΓ§as vs. TrainT:
|
| 8 |
+
- Recebe `o` opcionalmente destacado (stop_grad) para isolar o sinal de
|
| 9 |
+
correΓ§Γ£o do gradiente principal
|
| 10 |
+
- Mesma arquitetura (TransformerEncoder) β apenas o tratamento de gradiente
|
| 11 |
+
difere
|
| 12 |
+
|
| 13 |
+
Reaproveita a filosofia do HypothesisNetwork do v13.9.2 (16 hipΓ³teses com
|
| 14 |
+
mapeamento LoRA), mas simplificada: 1 ΓΊnica hipΓ³tese global (delta) em vez
|
| 15 |
+
de 16 hipΓ³teses paralelas. A multiplexaΓ§Γ£o Γ© feita pelo OrqCell + Lema 1.
|
| 16 |
+
"""
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
import torch.nn as nn
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class HypT(nn.Module):
|
| 24 |
+
"""Transformer de hipΓ³tese: gera correΓ§Γ£o delta.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
d_input: dimensΓ£o de entrada (saΓda do OrqCell = d_cache)
|
| 28 |
+
d_model: dimensΓ£o interna
|
| 29 |
+
nhead: nΒΊ de cabeΓ§as
|
| 30 |
+
d_ff: dimensΓ£o do FFN
|
| 31 |
+
output_dim: dimensΓ£o de saΓda (igual ao TrainT)
|
| 32 |
+
num_layers: nΒΊ de camadas
|
| 33 |
+
dropout: probabilidade de dropout
|
| 34 |
+
|
| 35 |
+
Forward:
|
| 36 |
+
o: (batch, d_input), stop_grad: bool β delta: (batch, output_dim)
|
| 37 |
+
|
| 38 |
+
Se stop_grad=True, aplica o.detach() no inΓcio β Lema 3 (isolar correΓ§Γ£o
|
| 39 |
+
do gradiente principal).
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
def __init__(
|
| 43 |
+
self,
|
| 44 |
+
d_input: int,
|
| 45 |
+
d_model: int,
|
| 46 |
+
nhead: int,
|
| 47 |
+
d_ff: int,
|
| 48 |
+
output_dim: int,
|
| 49 |
+
num_layers: int = 2,
|
| 50 |
+
dropout: float = 0.1,
|
| 51 |
+
):
|
| 52 |
+
super().__init__()
|
| 53 |
+
self.d_input = d_input
|
| 54 |
+
self.d_model = d_model
|
| 55 |
+
self.output_dim = output_dim
|
| 56 |
+
|
| 57 |
+
self.proj_in = nn.Linear(d_input, d_model)
|
| 58 |
+
encoder_layer = nn.TransformerEncoderLayer(
|
| 59 |
+
d_model, nhead, d_ff, dropout=dropout, batch_first=True
|
| 60 |
+
)
|
| 61 |
+
self.encoder = nn.TransformerEncoder(encoder_layer, num_layers)
|
| 62 |
+
self.proj_out = nn.Linear(d_model, output_dim)
|
| 63 |
+
|
| 64 |
+
def forward(self, o: torch.Tensor, stop_grad: bool = True) -> torch.Tensor:
|
| 65 |
+
"""o: (batch, d_input) β delta: (batch, output_dim)"""
|
| 66 |
+
# Lema 3: stop_grad_hyp isola o sinal de correΓ§Γ£o
|
| 67 |
+
if stop_grad:
|
| 68 |
+
o = o.detach()
|
| 69 |
+
|
| 70 |
+
x = o.unsqueeze(1) # (batch, 1, d_input)
|
| 71 |
+
x = self.proj_in(x) # (batch, 1, d_model)
|
| 72 |
+
x = self.encoder(x) # (batch, 1, d_model)
|
| 73 |
+
delta = self.proj_out(x.squeeze(1)) # (batch, output_dim)
|
| 74 |
+
return delta
|
src/bigru_t/model/module_selector.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""module_selector.py β Lema 1: seleΓ§Γ£o de mΓ³dulos via softmax com temperatura.
|
| 2 |
+
|
| 3 |
+
Implementa o Lema 1 (Desacoplamento via atenΓ§Γ£o hierΓ‘rquica).
|
| 4 |
+
|
| 5 |
+
MantΓ©m logits aprendΓveis `module_logits β R^K` para K mΓ³dulos u8cell_T.
|
| 6 |
+
A seleΓ§Γ£o Γ© feita por:
|
| 7 |
+
|
| 8 |
+
alpha = softmax(module_logits / T)
|
| 9 |
+
|
| 10 |
+
onde T (temperatura) Γ© controlada pelo MetaConfigurator (Lema 4).
|
| 11 |
+
|
| 12 |
+
A regularizaΓ§Γ£o de entropia (opcional) penaliza distribuiΓ§Γ΅es uniformes:
|
| 13 |
+
|
| 14 |
+
L_ent = -lambda_ent * sum(alpha * log(alpha + eps))
|
| 15 |
+
|
| 16 |
+
forΓ§ando especializaΓ§Γ£o (poucos mΓ³dulos com peso alto).
|
| 17 |
+
|
| 18 |
+
Reaproveita a filosofia da Confidence Gate do v13.9.2, mas:
|
| 19 |
+
- Substitui o gate binΓ‘rio por softmax diferenciΓ‘vel
|
| 20 |
+
- Adiciona regularizaΓ§Γ£o de entropia (nΓ£o havia no v13.9.2)
|
| 21 |
+
- A temperatura Γ© hiperparΓ’metro aprendΓvel (no v13.9.2 era fixa)
|
| 22 |
+
"""
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import math
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
import torch.nn as nn
|
| 29 |
+
import torch.nn.functional as F
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class ModuleSelector(nn.Module):
|
| 33 |
+
"""Seletor de mΓ³dulos via softmax com temperatura (Lema 1).
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
max_modules: nΒΊ mΓ‘ximo de mΓ³dulos K
|
| 37 |
+
lambda_ent: peso da regularizaΓ§Γ£o de entropia (0 = desativada)
|
| 38 |
+
|
| 39 |
+
Forward:
|
| 40 |
+
temperature: float β alpha: (K,), entropy_reg: scalar
|
| 41 |
+
|
| 42 |
+
O forward retorna os pesos alpha e a penalizaΓ§Γ£o de entropia (a ser
|
| 43 |
+
somada Γ loss principal).
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def __init__(self, max_modules: int, lambda_ent: float = 0.01):
|
| 47 |
+
super().__init__()
|
| 48 |
+
self.max_modules = max_modules
|
| 49 |
+
self.lambda_ent = lambda_ent
|
| 50 |
+
|
| 51 |
+
# Logits aprendΓveis β init zeros = distribuiΓ§Γ£o uniforme no inΓcio
|
| 52 |
+
self.module_logits = nn.Parameter(torch.zeros(max_modules))
|
| 53 |
+
|
| 54 |
+
def forward(self, temperature: float = 1.0) -> tuple[torch.Tensor, torch.Tensor]:
|
| 55 |
+
"""Retorna (alpha, entropy_reg).
|
| 56 |
+
|
| 57 |
+
alpha: (K,) β pesos de seleΓ§Γ£o dos mΓ³dulos (soma = 1)
|
| 58 |
+
entropy_reg: scalar β penalizaΓ§Γ£o de entropia (a somar Γ loss)
|
| 59 |
+
"""
|
| 60 |
+
# Softmax com temperatura (Lema 1)
|
| 61 |
+
# T alta β distribuiΓ§Γ£o uniforme; T baixa β argmax (especializaΓ§Γ£o)
|
| 62 |
+
alpha = F.softmax(self.module_logits / max(temperature, 1e-6), dim=0) # (K,)
|
| 63 |
+
|
| 64 |
+
# RegularizaΓ§Γ£o de entropia (minimizar H(alpha) β especializaΓ§Γ£o)
|
| 65 |
+
# H(alpha) = -sum(alpha * log(alpha + eps))
|
| 66 |
+
# Para MINIMIZAR H, somamos +lambda_ent * H(alpha) Γ loss
|
| 67 |
+
# (gradient descent reduz H)
|
| 68 |
+
eps = 1e-8
|
| 69 |
+
entropy = -(alpha * torch.log(alpha + eps)).sum()
|
| 70 |
+
entropy_reg = self.lambda_ent * entropy # scalar
|
| 71 |
+
|
| 72 |
+
return alpha, entropy_reg
|
| 73 |
+
|
| 74 |
+
def get_active_modules(self, threshold: float = 0.01) -> int:
|
| 75 |
+
"""Retorna nΒΊ efetivo de mΓ³dulos ativos (alpha > threshold)."""
|
| 76 |
+
with torch.no_grad():
|
| 77 |
+
alpha = F.softmax(self.module_logits, dim=0)
|
| 78 |
+
active = (alpha > threshold).sum().item()
|
| 79 |
+
return active
|
| 80 |
+
|
| 81 |
+
def get_alpha_distribution(self) -> torch.Tensor:
|
| 82 |
+
"""Retorna a distribuiΓ§Γ£o alpha atual (para logging)."""
|
| 83 |
+
with torch.no_grad():
|
| 84 |
+
return F.softmax(self.module_logits, dim=0)
|
src/bigru_t/model/orq_cell.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""orq_cell.py β OrqCell: orquestrador com cache aprendΓvel + cross-attention.
|
| 2 |
+
|
| 3 |
+
Reaproveita a filosofia do orquestrador cooperativo do v13.9.2, mas:
|
| 4 |
+
- Cache aprendΓvel (L, d_cache) substitui o "estado de trust" do v13.9.2
|
| 5 |
+
- Cross-attention (cache = queries, mΓ³dulos = keys/values) substitui o
|
| 6 |
+
gating por Confidence Gate
|
| 7 |
+
- MΓ©dia sobre as L posiΓ§Γ΅es do cache como saΓda agregada
|
| 8 |
+
|
| 9 |
+
O cache Γ© um "roteador aprendΓvel" que decide quais mΓ³dulos ativos devem
|
| 10 |
+
contribuir para a representaΓ§Γ£o final. Diferente do TrustNetwork do v13.9.2
|
| 11 |
+
(TD-learning com softmax norm), o OrqCell usa attention pura β a seleΓ§Γ£o
|
| 12 |
+
final dos mΓ³dulos ativos Γ© feita pelo Lema 1 no UnifiedModel.
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
import torch.nn as nn
|
| 18 |
+
import torch.nn.functional as F
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class OrqCell(nn.Module):
|
| 22 |
+
"""Orquestrador com cache aprendΓvel e cross-attention.
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
cache_len: nΒΊ de posiΓ§Γ΅es no cache aprendΓvel (L)
|
| 26 |
+
d_cache: dimensΓ£o do cache
|
| 27 |
+
d_model_in: dimensΓ£o de entrada (saΓda dos u8cell_T)
|
| 28 |
+
nhead: nΒΊ de cabeΓ§as da cross-attention
|
| 29 |
+
d_ff: dimensΓ£o interna do FFN
|
| 30 |
+
dropout: probabilidade de dropout
|
| 31 |
+
|
| 32 |
+
Forward:
|
| 33 |
+
H: (batch, K, d_model_in) β saΓdas ponderadas dos K mΓ³dulos
|
| 34 |
+
β o: (batch, d_cache)
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
def __init__(
|
| 38 |
+
self,
|
| 39 |
+
cache_len: int,
|
| 40 |
+
d_cache: int,
|
| 41 |
+
d_model_in: int,
|
| 42 |
+
nhead: int,
|
| 43 |
+
d_ff: int,
|
| 44 |
+
dropout: float = 0.1,
|
| 45 |
+
):
|
| 46 |
+
super().__init__()
|
| 47 |
+
self.cache_len = cache_len
|
| 48 |
+
self.d_cache = d_cache
|
| 49 |
+
|
| 50 |
+
# Cache aprendΓvel (L, d_cache) β inicializado pequeno para estabilidade
|
| 51 |
+
self.cache = nn.Parameter(torch.randn(cache_len, d_cache) * 0.02)
|
| 52 |
+
|
| 53 |
+
# ProjeΓ§Γ£o da entrada para o espaΓ§o do cache
|
| 54 |
+
self.proj_in = nn.Linear(d_model_in, d_cache)
|
| 55 |
+
|
| 56 |
+
# Cross-attention: queries = cache, keys/values = H_proj
|
| 57 |
+
self.cross_attn = nn.MultiheadAttention(
|
| 58 |
+
d_cache, nhead, dropout=dropout, batch_first=True
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# FFN (pre-norm style, consistente com TransformerUnit)
|
| 62 |
+
self.linear1 = nn.Linear(d_cache, d_ff)
|
| 63 |
+
self.linear2 = nn.Linear(d_ff, d_cache)
|
| 64 |
+
self.norm1 = nn.LayerNorm(d_cache)
|
| 65 |
+
self.norm2 = nn.LayerNorm(d_cache)
|
| 66 |
+
self.dropout1 = nn.Dropout(dropout)
|
| 67 |
+
self.dropout2 = nn.Dropout(dropout)
|
| 68 |
+
self.dropout_ff = nn.Dropout(dropout)
|
| 69 |
+
|
| 70 |
+
def forward(self, H: torch.Tensor) -> torch.Tensor:
|
| 71 |
+
"""H: (batch, K, d_model_in) β o: (batch, d_cache)"""
|
| 72 |
+
batch_size = H.size(0)
|
| 73 |
+
|
| 74 |
+
# Projeta H para o espaΓ§o do cache
|
| 75 |
+
H_proj = self.proj_in(H) # (batch, K, d_cache)
|
| 76 |
+
|
| 77 |
+
# Expande cache para o batch
|
| 78 |
+
cache = self.cache.unsqueeze(0).expand(batch_size, -1, -1) # (batch, L, d_cache)
|
| 79 |
+
|
| 80 |
+
# Cross-attention: queries = cache, keys/values = H_proj
|
| 81 |
+
# Pre-norm
|
| 82 |
+
normed_cache = self.norm1(cache)
|
| 83 |
+
normed_H = self.norm1(H_proj) # compartilha norm1 (mesma dimensΓ£o)
|
| 84 |
+
attn_out, _ = self.cross_attn(normed_cache, normed_H, normed_H, need_weights=False)
|
| 85 |
+
cache = cache + self.dropout1(attn_out)
|
| 86 |
+
|
| 87 |
+
# FFN pre-norm
|
| 88 |
+
normed = self.norm2(cache)
|
| 89 |
+
ff_out = self.linear2(self.dropout_ff(F.relu(self.linear1(normed))))
|
| 90 |
+
cache = cache + self.dropout2(ff_out)
|
| 91 |
+
|
| 92 |
+
# SaΓda = mΓ©dia sobre as L posiΓ§Γ΅es do cache
|
| 93 |
+
o = cache.mean(dim=1) # (batch, d_cache)
|
| 94 |
+
return o
|
src/bigru_t/model/train_t.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""train_t.py β TrainT: cabeΓ§a de prediΓ§Γ£o principal.
|
| 2 |
+
|
| 3 |
+
No v13.9.2, a prediΓ§Γ£o principal Γ© feita pela ΓΊltima camada cooperativa
|
| 4 |
+
+ projection ao vocabulΓ‘rio. No BiGRU_T_version, a prediΓ§Γ£o Γ© desacoplada
|
| 5 |
+
da extraΓ§Γ£o de features (feita pelo OrqCell + u8cell_T) e feita por um
|
| 6 |
+
TransformerEncoder simples sobre a representaΓ§Γ£o agregada `o`.
|
| 7 |
+
|
| 8 |
+
DiferenΓ§as vs. HypT:
|
| 9 |
+
- TrainT recebe `o` COM gradiente (caminho principal)
|
| 10 |
+
- HypT recebe `o` SEM gradiente quando stop_grad_hyp=True (Lema 3)
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import torch
|
| 15 |
+
import torch.nn as nn
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class TrainT(nn.Module):
|
| 19 |
+
"""Transformer encoder para prediΓ§Γ£o principal.
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
d_input: dimensΓ£o de entrada (saΓda do OrqCell = d_cache)
|
| 23 |
+
d_model: dimensΓ£o interna do Transformer
|
| 24 |
+
nhead: nΒΊ de cabeΓ§as
|
| 25 |
+
d_ff: dimensΓ£o do FFN
|
| 26 |
+
output_dim: dimensΓ£o de saΓda (vocab_size para LM, num_classes para classificaΓ§Γ£o)
|
| 27 |
+
num_layers: nΒΊ de camadas do TransformerEncoder
|
| 28 |
+
dropout: probabilidade de dropout
|
| 29 |
+
|
| 30 |
+
Forward:
|
| 31 |
+
o: (batch, d_input) β y_hat: (batch, output_dim)
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
def __init__(
|
| 35 |
+
self,
|
| 36 |
+
d_input: int,
|
| 37 |
+
d_model: int,
|
| 38 |
+
nhead: int,
|
| 39 |
+
d_ff: int,
|
| 40 |
+
output_dim: int,
|
| 41 |
+
num_layers: int = 2,
|
| 42 |
+
dropout: float = 0.1,
|
| 43 |
+
):
|
| 44 |
+
super().__init__()
|
| 45 |
+
self.d_input = d_input
|
| 46 |
+
self.d_model = d_model
|
| 47 |
+
self.output_dim = output_dim
|
| 48 |
+
|
| 49 |
+
self.proj_in = nn.Linear(d_input, d_model)
|
| 50 |
+
encoder_layer = nn.TransformerEncoderLayer(
|
| 51 |
+
d_model, nhead, d_ff, dropout=dropout, batch_first=True
|
| 52 |
+
)
|
| 53 |
+
self.encoder = nn.TransformerEncoder(encoder_layer, num_layers)
|
| 54 |
+
self.proj_out = nn.Linear(d_model, output_dim)
|
| 55 |
+
|
| 56 |
+
def forward(self, o: torch.Tensor) -> torch.Tensor:
|
| 57 |
+
"""o: (batch, d_input) β y_hat: (batch, output_dim)"""
|
| 58 |
+
# Adiciona dimensΓ£o de sequΓͺncia (token ΓΊnico)
|
| 59 |
+
x = o.unsqueeze(1) # (batch, 1, d_input)
|
| 60 |
+
x = self.proj_in(x) # (batch, 1, d_model)
|
| 61 |
+
x = self.encoder(x) # (batch, 1, d_model)
|
| 62 |
+
y_hat = self.proj_out(x.squeeze(1)) # (batch, output_dim)
|
| 63 |
+
return y_hat
|
src/bigru_t/model/transformer_unit.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""transformer_unit.py β TransformerUnit: self-attention + FFN para 8 representaΓ§Γ΅es.
|
| 2 |
+
|
| 3 |
+
Componente do u8cell_T (Lema 1). Processa as 8 saΓdas das BiGRU4 paralelas
|
| 4 |
+
via self-attention multi-head, produzindo um vetor agregado.
|
| 5 |
+
|
| 6 |
+
DiferenΓ§as vs. v13.9.2:
|
| 7 |
+
- Pooling por mΓ©dia (nΓ£o por Confidence Gate) β a seleΓ§Γ£o Γ© feita no nΓvel
|
| 8 |
+
do UnifiedModel via softmax (Lema 1)
|
| 9 |
+
- Pre-norm consistente (BUG-003 fix do v13.9.1 aplicado)
|
| 10 |
+
- Sem RoPE (a entrada jΓ‘ Γ© um embedding agregado, nΓ£o uma sequΓͺncia posicional)
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import torch
|
| 15 |
+
import torch.nn as nn
|
| 16 |
+
import torch.nn.functional as F
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class TransformerUnit(nn.Module):
|
| 20 |
+
"""Uma unidade Transformer (self-attention + FFN) para processar 8 representaΓ§Γ΅es.
|
| 21 |
+
|
| 22 |
+
Args:
|
| 23 |
+
d_model: dimensΓ£o do modelo (igual Γ saΓda projetada das BiGRU4)
|
| 24 |
+
nhead: nΓΊmero de cabeΓ§as de atenΓ§Γ£o
|
| 25 |
+
d_feedforward: dimensΓ£o interna do FFN (SwiGLU-style)
|
| 26 |
+
dropout: probabilidade de dropout
|
| 27 |
+
|
| 28 |
+
Forward:
|
| 29 |
+
src: (batch, 8, d_model) β h: (batch, d_model)
|
| 30 |
+
h Γ© a mΓ©dia sobre as 8 posiΓ§Γ΅es pΓ³s-attention+FFN.
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
def __init__(self, d_model: int, nhead: int, d_feedforward: int, dropout: float = 0.1):
|
| 34 |
+
super().__init__()
|
| 35 |
+
self.d_model = d_model
|
| 36 |
+
self.nhead = nhead
|
| 37 |
+
|
| 38 |
+
# Self-attention multi-head (batch_first para consistΓͺncia)
|
| 39 |
+
self.self_attn = nn.MultiheadAttention(
|
| 40 |
+
d_model, nhead, dropout=dropout, batch_first=True
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
# FFN: Linear β ReLU β Dropout β Linear (pre-norm style)
|
| 44 |
+
self.linear1 = nn.Linear(d_model, d_feedforward)
|
| 45 |
+
self.dropout = nn.Dropout(dropout)
|
| 46 |
+
self.linear2 = nn.Linear(d_feedforward, d_model)
|
| 47 |
+
|
| 48 |
+
# LayerNorms (pre-norm)
|
| 49 |
+
self.norm1 = nn.LayerNorm(d_model)
|
| 50 |
+
self.norm2 = nn.LayerNorm(d_model)
|
| 51 |
+
self.dropout1 = nn.Dropout(dropout)
|
| 52 |
+
self.dropout2 = nn.Dropout(dropout)
|
| 53 |
+
|
| 54 |
+
def forward(self, src: torch.Tensor) -> torch.Tensor:
|
| 55 |
+
"""src: (batch, 8, d_model) β h: (batch, d_model)"""
|
| 56 |
+
# Pre-norm self-attention
|
| 57 |
+
normed = self.norm1(src)
|
| 58 |
+
attn_out, _ = self.self_attn(normed, normed, normed, need_weights=False)
|
| 59 |
+
src = src + self.dropout1(attn_out)
|
| 60 |
+
|
| 61 |
+
# Pre-norm FFN
|
| 62 |
+
normed = self.norm2(src)
|
| 63 |
+
ff_out = self.linear2(self.dropout(F.relu(self.linear1(normed))))
|
| 64 |
+
src = src + self.dropout2(ff_out)
|
| 65 |
+
|
| 66 |
+
# Pooling por mΓ©dia sobre as 8 posiΓ§Γ΅es
|
| 67 |
+
h = src.mean(dim=1) # (batch, d_model)
|
| 68 |
+
return h
|
src/bigru_t/model/u8cell_t.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""u8cell_t.py β u8cell_T: mΓ³dulo especialista com 8 BiGRU4 + TransformerUnit.
|
| 2 |
+
|
| 3 |
+
Componente do Lema 1 (Desacoplamento via atenΓ§Γ£o hierΓ‘rquica).
|
| 4 |
+
|
| 5 |
+
Cada u8cell_T Γ© um "mΓ³dulo especialista" independente. O UnifiedModel
|
| 6 |
+
instanciarΓ‘ atΓ© `max_modules` deles e selecionarΓ‘ quais ativar via softmax
|
| 7 |
+
com temperatura controlada (Lema 1).
|
| 8 |
+
|
| 9 |
+
Reaproveita a arquitetura "4 camadas BiGRU cooperativas" do v13.9.2, mas:
|
| 10 |
+
- Cada BiGRU4 contΓ©m 4 camadas (vs. 1 cooperative layer no v13.9.2)
|
| 11 |
+
- 8 BiGRU4 paralelas por u8cell_T (vs. 1 GRU stack no v13.9.2)
|
| 12 |
+
- TransformerUnit funde as 8 saΓdas (vs. Confidence Gate no v13.9.2)
|
| 13 |
+
- Sem Hypothesis Network interna (a hipΓ³tese Γ© global, Lema 3)
|
| 14 |
+
- Sem Trust Network interna (o orquestrador global faz o papel, Lema 4)
|
| 15 |
+
"""
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
import torch.nn as nn
|
| 20 |
+
|
| 21 |
+
from .bigru4 import BiGRU4
|
| 22 |
+
from .transformer_unit import TransformerUnit
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class u8cell_T(nn.Module):
|
| 26 |
+
"""MΓ³dulo com 8 BiGRU4 paralelas + TransformerUnit.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
input_dim: dimensΓ£o da entrada
|
| 30 |
+
hidden_dim_bigru: dimensΓ£o oculta de cada GRU nas BiGRU4
|
| 31 |
+
d_transformer: d_model do TransformerUnit
|
| 32 |
+
nhead: nΒΊ de cabeΓ§as de atenΓ§Γ£o do TransformerUnit
|
| 33 |
+
d_ff: dimensΓ£o interna do FFN do TransformerUnit
|
| 34 |
+
output_dim: dimensΓ£o de saΓda do u8cell_T
|
| 35 |
+
|
| 36 |
+
Forward:
|
| 37 |
+
x: (batch, T, input_dim) β h_out: (batch, output_dim)
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
def __init__(
|
| 41 |
+
self,
|
| 42 |
+
input_dim: int,
|
| 43 |
+
hidden_dim_bigru: int,
|
| 44 |
+
d_transformer: int,
|
| 45 |
+
nhead: int,
|
| 46 |
+
d_ff: int,
|
| 47 |
+
output_dim: int,
|
| 48 |
+
dropout: float = 0.1,
|
| 49 |
+
):
|
| 50 |
+
super().__init__()
|
| 51 |
+
self.input_dim = input_dim
|
| 52 |
+
self.output_dim = output_dim
|
| 53 |
+
|
| 54 |
+
# 8 BiGRU4 paralelas (cada uma com 4 camadas BiGRU)
|
| 55 |
+
self.bigru_list = nn.ModuleList(
|
| 56 |
+
[BiGRU4(input_dim, hidden_dim_bigru, dropout=dropout) for _ in range(8)]
|
| 57 |
+
)
|
| 58 |
+
bigru_out_dim = 2 * hidden_dim_bigru # bidirecional
|
| 59 |
+
|
| 60 |
+
# ProjeΓ§Γ£o para o d_model do TransformerUnit
|
| 61 |
+
self.proj_in = nn.Linear(bigru_out_dim, d_transformer)
|
| 62 |
+
|
| 63 |
+
# TransformerUnit funde as 8 representaΓ§Γ΅es
|
| 64 |
+
self.transformer_unit = TransformerUnit(d_transformer, nhead, d_ff, dropout=dropout)
|
| 65 |
+
|
| 66 |
+
# ProjeΓ§Γ£o final para output_dim
|
| 67 |
+
self.proj_out = nn.Linear(d_transformer, output_dim)
|
| 68 |
+
|
| 69 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 70 |
+
"""x: (batch, T, input_dim) β h_out: (batch, output_dim)"""
|
| 71 |
+
representations = []
|
| 72 |
+
for bigru in self.bigru_list:
|
| 73 |
+
v_i = bigru(x) # (batch, bigru_out_dim)
|
| 74 |
+
v_i_proj = self.proj_in(v_i) # (batch, d_transformer)
|
| 75 |
+
representations.append(v_i_proj)
|
| 76 |
+
|
| 77 |
+
# Empilha as 8 representaΓ§Γ΅es: (batch, 8, d_transformer)
|
| 78 |
+
stacked = torch.stack(representations, dim=1)
|
| 79 |
+
|
| 80 |
+
# TransformerUnit β (batch, d_transformer)
|
| 81 |
+
h_unit = self.transformer_unit(stacked)
|
| 82 |
+
|
| 83 |
+
# ProjeΓ§Γ£o final β (batch, output_dim)
|
| 84 |
+
h_out = self.proj_out(h_unit)
|
| 85 |
+
return h_out
|
src/bigru_t/model/unified_model.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""unified_model.py β UnifiedModel: orquestra todos os componentes (Lemas 1-4).
|
| 2 |
+
|
| 3 |
+
Arquitetura BiGRU_T_version:
|
| 4 |
+
|
| 5 |
+
x (batch, T, input_dim)
|
| 6 |
+
β
|
| 7 |
+
ββββ u8cell_T_1 ββ h_1 ββ
|
| 8 |
+
ββββ u8cell_T_2 ββ h_2 ββ€ Lema 1: alpha = softmax(logits / T)
|
| 9 |
+
ββββ ... βββ H = stack([alpha_k * h_k]) β OrqCell
|
| 10 |
+
ββββ u8cell_T_K ββ h_K ββ β
|
| 11 |
+
β
|
| 12 |
+
o (batch, d_cache)
|
| 13 |
+
β
|
| 14 |
+
ββββββββββββββββ΄βββββββββββββββ
|
| 15 |
+
β β
|
| 16 |
+
TrainT(o) HypT(o, stop_grad=True)
|
| 17 |
+
β β
|
| 18 |
+
β β
|
| 19 |
+
y_hat delta
|
| 20 |
+
β β
|
| 21 |
+
ββββββββ y_final = y_hat + delta * use_hyp βββ Lema 3
|
| 22 |
+
|
| 23 |
+
Lema 2: gradient_surgery(g_main, g_hyp) β projeΓ§Γ£o ortogonal
|
| 24 |
+
Lema 4: MetaConfigurator ajusta T (temperatura) e Ο (limiar de ativaΓ§Γ£o da hipΓ³tese)
|
| 25 |
+
|
| 26 |
+
DiferenΓ§as vs. v13.9.2 GRURingV139:
|
| 27 |
+
- 1 modelo agregador (OrqCell) em vez de 4 camadas cooperativas empilhadas
|
| 28 |
+
- SeleΓ§Γ£o de mΓ³dulos por softmax (Lema 1) em vez de Confidence Gate
|
| 29 |
+
- HipΓ³tese global (HypT) em vez de 16 hipΓ³teses paralelas com LoRA
|
| 30 |
+
- QuantizaΓ§Γ£o W8A8 fake-quant em todas as Linear (Lema 3) em vez de
|
| 31 |
+
W8A8 QOperator sΓ³ em inferΓͺncia
|
| 32 |
+
- Tau aprendΓvel (Lema 4) em vez de trust_lr fixo
|
| 33 |
+
|
| 34 |
+
AdaptaΓ§Γ£o para Language Modeling:
|
| 35 |
+
- input_dim = d_model (embedding dim)
|
| 36 |
+
- output_dim = vocab_size
|
| 37 |
+
- Adiciona camada de embedding (token IDs β embeddings)
|
| 38 |
+
- Adiciona positional encoding (sinusoidal)
|
| 39 |
+
"""
|
| 40 |
+
from __future__ import annotations
|
| 41 |
+
|
| 42 |
+
import math
|
| 43 |
+
from dataclasses import dataclass, field
|
| 44 |
+
from typing import Optional
|
| 45 |
+
|
| 46 |
+
import torch
|
| 47 |
+
import torch.nn as nn
|
| 48 |
+
import torch.nn.functional as F
|
| 49 |
+
|
| 50 |
+
from .u8cell_t import u8cell_T
|
| 51 |
+
from .orq_cell import OrqCell
|
| 52 |
+
from .train_t import TrainT
|
| 53 |
+
from .hyp_t import HypT
|
| 54 |
+
from .module_selector import ModuleSelector
|
| 55 |
+
from ..quantization.quantized_linear import QuantizedLinear, apply_w8a8
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@dataclass
|
| 59 |
+
class UnifiedModelConfig:
|
| 60 |
+
"""ConfiguraΓ§Γ£o do UnifiedModel.
|
| 61 |
+
|
| 62 |
+
Campos agrupados por componente:
|
| 63 |
+
- Tokenizer / embedding: vocab_size, d_model, max_seq_len, pad_token_id
|
| 64 |
+
- u8cell_T: input_dim (default = d_model), bigru_hidden, d_transformer, nhead_tu, d_ff_tu, output_dim_u8cell
|
| 65 |
+
- Module selector: max_modules, lambda_ent
|
| 66 |
+
- OrqCell: cache_len, d_cache, nhead_orq, d_ff_orq
|
| 67 |
+
- TrainT: trainT_dim, nhead_train, d_ff_train, num_layers_train
|
| 68 |
+
- HypT: hypT_dim, nhead_hyp, d_ff_hyp, num_layers_hyp
|
| 69 |
+
- Quantization: num_bits (W8A8)
|
| 70 |
+
- Misc: dropout
|
| 71 |
+
|
| 72 |
+
Nota: `input_dim` Γ© automaticamente sincronizado com `d_model` em
|
| 73 |
+
__post_init__ se nΓ£o for explicitamente especificado. Isso garante
|
| 74 |
+
que a saΓda do embedding (dim=d_model) matches a entrada do BiGRU4
|
| 75 |
+
(input_dim).
|
| 76 |
+
"""
|
| 77 |
+
# Embedding
|
| 78 |
+
vocab_size: int = 16384
|
| 79 |
+
d_model: int = 128
|
| 80 |
+
max_seq_len: int = 64
|
| 81 |
+
pad_token_id: int = 1
|
| 82 |
+
|
| 83 |
+
# u8cell_T
|
| 84 |
+
input_dim: int = 0 # 0 = usar d_model automaticamente (ver __post_init__)
|
| 85 |
+
bigru_hidden: int = 32
|
| 86 |
+
d_transformer: int = 64
|
| 87 |
+
nhead_tu: int = 4
|
| 88 |
+
d_ff_tu: int = 128
|
| 89 |
+
output_dim_u8cell: int = 64
|
| 90 |
+
|
| 91 |
+
# Module selector (Lema 1)
|
| 92 |
+
max_modules: int = 64
|
| 93 |
+
lambda_ent: float = 0.01
|
| 94 |
+
|
| 95 |
+
# OrqCell
|
| 96 |
+
cache_len: int = 16
|
| 97 |
+
d_cache: int = 128
|
| 98 |
+
nhead_orq: int = 8
|
| 99 |
+
d_ff_orq: int = 256
|
| 100 |
+
|
| 101 |
+
# TrainT
|
| 102 |
+
trainT_dim: int = 128
|
| 103 |
+
nhead_train: int = 4
|
| 104 |
+
d_ff_train: int = 256
|
| 105 |
+
num_layers_train: int = 2
|
| 106 |
+
|
| 107 |
+
# HypT
|
| 108 |
+
hypT_dim: int = 128
|
| 109 |
+
nhead_hyp: int = 4
|
| 110 |
+
d_ff_hyp: int = 256
|
| 111 |
+
num_layers_hyp: int = 2
|
| 112 |
+
|
| 113 |
+
# Quantization (Lema 3)
|
| 114 |
+
num_bits: int = 8
|
| 115 |
+
|
| 116 |
+
# Misc
|
| 117 |
+
dropout: float = 0.1
|
| 118 |
+
|
| 119 |
+
def __post_init__(self):
|
| 120 |
+
"""Sincroniza input_dim com d_model se nΓ£o for explicitamente especificado."""
|
| 121 |
+
if self.input_dim <= 0:
|
| 122 |
+
self.input_dim = self.d_model
|
| 123 |
+
# ValidaΓ§Γ΅es de consistΓͺncia
|
| 124 |
+
if self.d_transformer % self.nhead_tu != 0:
|
| 125 |
+
raise ValueError(
|
| 126 |
+
f"d_transformer ({self.d_transformer}) must be divisible by nhead_tu ({self.nhead_tu})"
|
| 127 |
+
)
|
| 128 |
+
if self.d_cache % self.nhead_orq != 0:
|
| 129 |
+
raise ValueError(
|
| 130 |
+
f"d_cache ({self.d_cache}) must be divisible by nhead_orq ({self.nhead_orq})"
|
| 131 |
+
)
|
| 132 |
+
if self.trainT_dim % self.nhead_train != 0:
|
| 133 |
+
raise ValueError(
|
| 134 |
+
f"trainT_dim ({self.trainT_dim}) must be divisible by nhead_train ({self.nhead_train})"
|
| 135 |
+
)
|
| 136 |
+
if self.hypT_dim % self.nhead_hyp != 0:
|
| 137 |
+
raise ValueError(
|
| 138 |
+
f"hypT_dim ({self.hypT_dim}) must be divisible by nhead_hyp ({self.nhead_hyp})"
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
class PositionalEncoding(nn.Module):
|
| 143 |
+
"""Positional encoding sinusoidal (Vaswani et al., 2017).
|
| 144 |
+
|
| 145 |
+
Reaproveita a filosofia do v13.9.2 (que usa RoPE), mas como o BiGRU_T
|
| 146 |
+
processa a sequΓͺncia via BiGRU (nΓ£o via attention causal), o PE sinusoidal
|
| 147 |
+
Γ© suficiente e mais simples.
|
| 148 |
+
"""
|
| 149 |
+
|
| 150 |
+
def __init__(self, d_model: int, max_len: int = 512):
|
| 151 |
+
super().__init__()
|
| 152 |
+
pe = torch.zeros(max_len, d_model)
|
| 153 |
+
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
|
| 154 |
+
div_term = torch.exp(
|
| 155 |
+
torch.arange(0, d_model, 2, dtype=torch.float) * (-math.log(10000.0) / d_model)
|
| 156 |
+
)
|
| 157 |
+
pe[:, 0::2] = torch.sin(position * div_term)
|
| 158 |
+
pe[:, 1::2] = torch.cos(position * div_term)
|
| 159 |
+
self.register_buffer("pe", pe.unsqueeze(0)) # (1, max_len, d_model)
|
| 160 |
+
|
| 161 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 162 |
+
"""x: (batch, T, d_model) β x + pe[:, :T]"""
|
| 163 |
+
return x + self.pe[:, : x.size(1)]
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
class UnifiedModel(nn.Module):
|
| 167 |
+
"""Modelo BiGRU_T_version β orquestra Lemas 1-4.
|
| 168 |
+
|
| 169 |
+
Args:
|
| 170 |
+
config: UnifiedModelConfig
|
| 171 |
+
|
| 172 |
+
Forward:
|
| 173 |
+
x: (batch, T) token IDs OU (batch, T, input_dim) embeddings
|
| 174 |
+
(detetado por ndim: 2 = token IDs, 3 = embeddings)
|
| 175 |
+
temperature: float (default 1.0, sobrescrito pelo MetaConfigurator)
|
| 176 |
+
use_hypothesis: bool (default False, sobrescrito pelo trainer se loss > tau)
|
| 177 |
+
stop_grad_hyp: bool (default True, Lema 3)
|
| 178 |
+
|
| 179 |
+
Returns:
|
| 180 |
+
(y_hat, delta) β ambos (batch, output_dim). y_final = y_hat + delta * use_hypothesis.
|
| 181 |
+
"""
|
| 182 |
+
|
| 183 |
+
def __init__(self, config: Optional[UnifiedModelConfig] = None):
|
| 184 |
+
super().__init__()
|
| 185 |
+
self.config = config or UnifiedModelConfig()
|
| 186 |
+
c = self.config
|
| 187 |
+
|
| 188 |
+
# Embedding de tokens (para LM)
|
| 189 |
+
self.token_embedding = nn.Embedding(c.vocab_size, c.d_model, padding_idx=c.pad_token_id)
|
| 190 |
+
self.positional_encoding = PositionalEncoding(c.d_model, c.max_seq_len * 2)
|
| 191 |
+
self.emb_dropout = nn.Dropout(c.dropout)
|
| 192 |
+
|
| 193 |
+
# Module selector (Lema 1)
|
| 194 |
+
self.module_selector = ModuleSelector(c.max_modules, lambda_ent=c.lambda_ent)
|
| 195 |
+
|
| 196 |
+
# Lista de mΓ³dulos u8cell_T
|
| 197 |
+
self.u8cells = nn.ModuleList([
|
| 198 |
+
u8cell_T(
|
| 199 |
+
input_dim=c.input_dim,
|
| 200 |
+
hidden_dim_bigru=c.bigru_hidden,
|
| 201 |
+
d_transformer=c.d_transformer,
|
| 202 |
+
nhead=c.nhead_tu,
|
| 203 |
+
d_ff=c.d_ff_tu,
|
| 204 |
+
output_dim=c.output_dim_u8cell,
|
| 205 |
+
dropout=c.dropout,
|
| 206 |
+
)
|
| 207 |
+
for _ in range(c.max_modules)
|
| 208 |
+
])
|
| 209 |
+
|
| 210 |
+
# Orquestrador
|
| 211 |
+
self.orq_cell = OrqCell(
|
| 212 |
+
cache_len=c.cache_len,
|
| 213 |
+
d_cache=c.d_cache,
|
| 214 |
+
d_model_in=c.output_dim_u8cell,
|
| 215 |
+
nhead=c.nhead_orq,
|
| 216 |
+
d_ff=c.d_ff_orq,
|
| 217 |
+
dropout=c.dropout,
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
# CabeΓ§a principal
|
| 221 |
+
self.train_T = TrainT(
|
| 222 |
+
d_input=c.d_cache,
|
| 223 |
+
d_model=c.trainT_dim,
|
| 224 |
+
nhead=c.nhead_train,
|
| 225 |
+
d_ff=c.d_ff_train,
|
| 226 |
+
output_dim=c.vocab_size, # LM: prevΓͺ prΓ³ximo token
|
| 227 |
+
num_layers=c.num_layers_train,
|
| 228 |
+
dropout=c.dropout,
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
# CabeΓ§a de hipΓ³tese (Lema 3)
|
| 232 |
+
self.hyp_T = HypT(
|
| 233 |
+
d_input=c.d_cache,
|
| 234 |
+
d_model=c.hypT_dim,
|
| 235 |
+
nhead=c.nhead_hyp,
|
| 236 |
+
d_ff=c.d_ff_hyp,
|
| 237 |
+
output_dim=c.vocab_size,
|
| 238 |
+
num_layers=c.num_layers_hyp,
|
| 239 |
+
dropout=c.dropout,
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
# Limiar de puniΓ§Γ£o (Ο) β atualizado pelo MetaConfigurator (Lema 4)
|
| 243 |
+
# Init = 1.0 (sem hipΓ³tese atΓ© a loss cair abaixo de 1.0 β raro no inΓcio)
|
| 244 |
+
self.register_buffer("tau", torch.tensor(float("inf")))
|
| 245 |
+
|
| 246 |
+
# Aplicar W8A8 em todas as camadas lineares (exceto Embedding, que Γ© nn.Embedding)
|
| 247 |
+
# Nota: nn.GRU nΓ£o Γ© afetada (nΓ£o Γ© Linear)
|
| 248 |
+
self.apply(apply_w8a8)
|
| 249 |
+
|
| 250 |
+
def forward(
|
| 251 |
+
self,
|
| 252 |
+
x: torch.Tensor,
|
| 253 |
+
temperature: float = 1.0,
|
| 254 |
+
use_hypothesis: bool = False,
|
| 255 |
+
stop_grad_hyp: bool = True,
|
| 256 |
+
return_aux: bool = False,
|
| 257 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 258 |
+
"""Forward do UnifiedModel.
|
| 259 |
+
|
| 260 |
+
Args:
|
| 261 |
+
x: (batch, T) token IDs OU (batch, T, input_dim) embeddings
|
| 262 |
+
temperature: temperatura do softmax do Lema 1
|
| 263 |
+
use_hypothesis: se True, calcula delta (cabeΓ§a HypT ativa)
|
| 264 |
+
stop_grad_hyp: se True, detach a entrada da HypT (Lema 3)
|
| 265 |
+
return_aux: se True, retorna tambΓ©m entropy_reg e alpha
|
| 266 |
+
|
| 267 |
+
Returns:
|
| 268 |
+
(y_hat, delta) por padrΓ£o.
|
| 269 |
+
(y_hat, delta, aux_dict) se return_aux=True.
|
| 270 |
+
"""
|
| 271 |
+
# Embedding se necessΓ‘rio
|
| 272 |
+
if x.dim() == 2:
|
| 273 |
+
# x: (batch, T) token IDs β (batch, T, d_model)
|
| 274 |
+
x = self.token_embedding(x)
|
| 275 |
+
x = self.positional_encoding(x)
|
| 276 |
+
x = self.emb_dropout(x)
|
| 277 |
+
# Agora x: (batch, T, input_dim)
|
| 278 |
+
|
| 279 |
+
# Lema 1: seleciona mΓ³dulos via softmax com temperatura
|
| 280 |
+
alpha, entropy_reg = self.module_selector(temperature=temperature)
|
| 281 |
+
|
| 282 |
+
# Calcula saΓdas de todos os mΓ³dulos e pondera por alpha
|
| 283 |
+
H_list = []
|
| 284 |
+
for k, cell in enumerate(self.u8cells):
|
| 285 |
+
h_k = cell(x) # (batch, output_dim_u8cell)
|
| 286 |
+
h_k_weighted = h_k * alpha[k]
|
| 287 |
+
H_list.append(h_k_weighted)
|
| 288 |
+
H = torch.stack(H_list, dim=1) # (batch, max_modules, output_dim_u8cell)
|
| 289 |
+
|
| 290 |
+
# Orquestrador: produz representaΓ§Γ£o agregada
|
| 291 |
+
o = self.orq_cell(H) # (batch, d_cache)
|
| 292 |
+
|
| 293 |
+
# PrediΓ§Γ£o principal
|
| 294 |
+
y_hat_main = self.train_T(o) # (batch, vocab_size)
|
| 295 |
+
|
| 296 |
+
# PrediΓ§Γ£o de hipΓ³tese (se desejado)
|
| 297 |
+
if use_hypothesis:
|
| 298 |
+
delta = self.hyp_T(o, stop_grad=stop_grad_hyp)
|
| 299 |
+
else:
|
| 300 |
+
delta = torch.zeros_like(y_hat_main)
|
| 301 |
+
|
| 302 |
+
if return_aux:
|
| 303 |
+
aux = {
|
| 304 |
+
"entropy_reg": entropy_reg,
|
| 305 |
+
"alpha": alpha.detach(),
|
| 306 |
+
"o": o.detach(),
|
| 307 |
+
}
|
| 308 |
+
return y_hat_main, delta, aux
|
| 309 |
+
return y_hat_main, delta
|
| 310 |
+
|
| 311 |
+
def get_active_modules(self, threshold: float = 0.01) -> int:
|
| 312 |
+
"""Retorna nΒΊ efetivo de mΓ³dulos ativos (para auto-configuraΓ§Γ£o)."""
|
| 313 |
+
return self.module_selector.get_active_modules(threshold)
|
| 314 |
+
|
| 315 |
+
def count_parameters(self) -> dict:
|
| 316 |
+
"""Conta parΓ’metros totais e treinΓ‘veis."""
|
| 317 |
+
total = sum(p.numel() for p in self.parameters())
|
| 318 |
+
trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
|
| 319 |
+
return {
|
| 320 |
+
"total": total,
|
| 321 |
+
"total_M": total / 1e6,
|
| 322 |
+
"trainable": trainable,
|
| 323 |
+
"trainable_M": trainable / 1e6,
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def create_unified_model(config: Optional[UnifiedModelConfig] = None) -> tuple[UnifiedModel, UnifiedModelConfig]:
|
| 328 |
+
"""Factory: cria UnifiedModel e retorna (model, config)."""
|
| 329 |
+
cfg = config or UnifiedModelConfig()
|
| 330 |
+
model = UnifiedModel(cfg)
|
| 331 |
+
return model, cfg
|
src/bigru_t/multimodal/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Multimodalidade (reaproveitada do source xavante/multimodal/)."""
|
src/bigru_t/multimodal/audio_encoder.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Xavante - audio_encoder.py - Encoder de audio (estilo Whisper leve)."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import logging
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class AudioEncoder(nn.Module):
|
| 13 |
+
"""Convolucoes 1D + Linear para audio bruto ou mel-spec."""
|
| 14 |
+
|
| 15 |
+
def __init__(self, d_model: int = 512, in_channels: int = 1, n_mels: int = 80):
|
| 16 |
+
super().__init__()
|
| 17 |
+
in_ch = in_channels if in_channels > 1 else n_mels
|
| 18 |
+
self.conv = nn.Sequential(
|
| 19 |
+
nn.Conv1d(in_ch, 64, 5, 2, 2),
|
| 20 |
+
nn.GELU(),
|
| 21 |
+
nn.Conv1d(64, 128, 5, 2, 2),
|
| 22 |
+
nn.GELU(),
|
| 23 |
+
nn.Conv1d(128, 256, 5, 2, 2),
|
| 24 |
+
nn.GELU(),
|
| 25 |
+
nn.AdaptiveAvgPool1d(1),
|
| 26 |
+
)
|
| 27 |
+
self.proj = nn.Linear(256, d_model)
|
| 28 |
+
|
| 29 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 30 |
+
# x: [B, C, T] ou [B, n_mels, T]
|
| 31 |
+
feat = self.conv(x).squeeze(-1)
|
| 32 |
+
return self.proj(feat)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
__all__ = ["AudioEncoder"]
|
src/bigru_t/multimodal/fusion_layer.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Xavante - fusion_layer.py - Fusao multimodal via projecao isometrica."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import logging
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class FusionLayer(nn.Module):
|
| 13 |
+
"""
|
| 14 |
+
Fusao de N modalidades via:
|
| 15 |
+
1. Projecao isometrica (preserva norma)
|
| 16 |
+
2. Gated combination
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
def __init__(self, d_model: int, n_modalities: int = 4):
|
| 20 |
+
super().__init__()
|
| 21 |
+
self.n_modalities = n_modalities
|
| 22 |
+
self.gates = nn.ModuleList(
|
| 23 |
+
[nn.Linear(d_model * n_modalities, 1) for _ in range(n_modalities)]
|
| 24 |
+
)
|
| 25 |
+
self.proj = nn.Linear(d_model, d_model, bias=False)
|
| 26 |
+
# Isometria: inicializar proj como ortogonal
|
| 27 |
+
nn.init.orthogonal_(self.proj.weight)
|
| 28 |
+
|
| 29 |
+
def forward(self, modality_embs: list) -> torch.Tensor:
|
| 30 |
+
"""modality_embs: lista de [B, d_model] (n_modalidades deles)."""
|
| 31 |
+
if len(modality_embs) != self.n_modalities:
|
| 32 |
+
# Pad with zeros if missing
|
| 33 |
+
while len(modality_embs) < self.n_modalities:
|
| 34 |
+
modality_embs.append(torch.zeros_like(modality_embs[0]))
|
| 35 |
+
modality_embs = modality_embs[: self.n_modalities]
|
| 36 |
+
cat = torch.cat(modality_embs, dim=-1) # [B, n*d]
|
| 37 |
+
# Gates
|
| 38 |
+
gate_scores = [torch.sigmoid(g(cat)) for g in self.gates]
|
| 39 |
+
# Stack: [n, B, 1] -> [B, n, 1]
|
| 40 |
+
gates_stacked = torch.cat(gate_scores, dim=-1) # [B, n]
|
| 41 |
+
# Normalize via softmax
|
| 42 |
+
gates_norm = torch.softmax(gates_stacked, dim=-1)
|
| 43 |
+
# Weighted sum
|
| 44 |
+
stacked = torch.stack(modality_embs, dim=1) # [B, n, d]
|
| 45 |
+
fused = (stacked * gates_norm.unsqueeze(-1)).sum(dim=1) # [B, d]
|
| 46 |
+
# Isometric projection
|
| 47 |
+
return self.proj(fused)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
__all__ = ["FusionLayer"]
|
src/bigru_t/multimodal/image_encoder.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Xavante - image_encoder.py - Encoder de imagem (Teorema 16.1/16.2)."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import logging
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ImageEncoder(nn.Module):
|
| 13 |
+
"""CNN leve -> features em d_model."""
|
| 14 |
+
|
| 15 |
+
def __init__(self, d_model: int = 512, in_channels: int = 3):
|
| 16 |
+
super().__init__()
|
| 17 |
+
self.conv = nn.Sequential(
|
| 18 |
+
nn.Conv2d(in_channels, 32, 4, 2, 1),
|
| 19 |
+
nn.GELU(),
|
| 20 |
+
nn.Conv2d(32, 64, 4, 2, 1),
|
| 21 |
+
nn.GELU(),
|
| 22 |
+
nn.Conv2d(64, 128, 4, 2, 1),
|
| 23 |
+
nn.GELU(),
|
| 24 |
+
nn.AdaptiveAvgPool2d((1, 1)),
|
| 25 |
+
)
|
| 26 |
+
self.proj = nn.Linear(128, d_model)
|
| 27 |
+
|
| 28 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 29 |
+
# x: [B, C, H, W]
|
| 30 |
+
feat = self.conv(x).flatten(1) # [B, 128]
|
| 31 |
+
return self.proj(feat) # [B, d_model]
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
__all__ = ["ImageEncoder"]
|
src/bigru_t/multimodal/modal_router.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Xavante - modal_router.py - Roteamento automatico de modalidades."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import logging
|
| 5 |
+
from enum import Enum
|
| 6 |
+
from typing import Dict, List
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn as nn
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class Modality(str, Enum):
|
| 15 |
+
TEXT = "text"
|
| 16 |
+
IMAGE = "image"
|
| 17 |
+
AUDIO = "audio"
|
| 18 |
+
VIDEO = "video"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class ModalRouter(nn.Module):
|
| 22 |
+
"""Roteia entradas para encoders apropriados."""
|
| 23 |
+
|
| 24 |
+
def __init__(self, d_model: int = 512):
|
| 25 |
+
super().__init__()
|
| 26 |
+
self.d_model = d_model
|
| 27 |
+
# Placeholder - encoders sao injetados
|
| 28 |
+
self.encoders: Dict[str, nn.Module] = {}
|
| 29 |
+
|
| 30 |
+
def register(self, modality: Modality, encoder: nn.Module) -> None:
|
| 31 |
+
self.encoders[modality.value] = encoder
|
| 32 |
+
|
| 33 |
+
def forward(self, inputs: Dict[str, torch.Tensor]) -> torch.Tensor:
|
| 34 |
+
"""inputs: dict modality -> tensor."""
|
| 35 |
+
embs = []
|
| 36 |
+
for mod, tensor in inputs.items():
|
| 37 |
+
if mod in self.encoders:
|
| 38 |
+
try:
|
| 39 |
+
e = self.encoders[mod](tensor)
|
| 40 |
+
embs.append(e)
|
| 41 |
+
except Exception as e:
|
| 42 |
+
logger.warning("Encoder %s falhou: %s", mod, e)
|
| 43 |
+
if not embs:
|
| 44 |
+
return torch.zeros(1, self.d_model)
|
| 45 |
+
# Soma simples se so uma modalidade, fusao externa se multipla
|
| 46 |
+
if len(embs) == 1:
|
| 47 |
+
return embs[0]
|
| 48 |
+
return torch.stack(embs).mean(dim=0)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
__all__ = ["Modality", "ModalRouter"]
|
src/bigru_t/multimodal/text_encoder.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Xavante - text_encoder.py
|
| 3 |
+
Responsabilidade: Encoder de texto com embedding reconfiguravel + GRU hierarquica
|
| 4 |
+
+ multi-head attention (Teoremas 7.1, 11.1, 12.1).
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import logging
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
import torch.nn as nn
|
| 12 |
+
|
| 13 |
+
from ..model.attention_multimodal import MultiHeadAttention
|
| 14 |
+
from ..model.embedding_reconfig import ReconfigurableEmbedding
|
| 15 |
+
from ..model.gru_hierarchy import GRUHierarchy
|
| 16 |
+
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class TextEncoder(nn.Module):
|
| 21 |
+
def __init__(
|
| 22 |
+
self,
|
| 23 |
+
vocab_size: int = 32128,
|
| 24 |
+
d_model: int = 512,
|
| 25 |
+
n_heads: int = 8,
|
| 26 |
+
n_gru_levels: int = 3,
|
| 27 |
+
max_seq_len: int = 2048,
|
| 28 |
+
):
|
| 29 |
+
super().__init__()
|
| 30 |
+
self.embedding = ReconfigurableEmbedding(vocab_size, d_model, padding_idx=0)
|
| 31 |
+
self.pos_emb = nn.Embedding(max_seq_len, d_model)
|
| 32 |
+
self.gru = GRUHierarchy(d_model, d_model, n_levels=n_gru_levels)
|
| 33 |
+
self.attn = MultiHeadAttention(d_model, n_heads)
|
| 34 |
+
self.ln = nn.LayerNorm(d_model)
|
| 35 |
+
|
| 36 |
+
def forward(self, input_ids: torch.Tensor) -> torch.Tensor:
|
| 37 |
+
B, L = input_ids.shape
|
| 38 |
+
pos = torch.arange(L, device=input_ids.device).unsqueeze(0).expand(B, L)
|
| 39 |
+
x = self.embedding(input_ids) + self.pos_emb(pos)
|
| 40 |
+
x = self.gru(x)
|
| 41 |
+
x = self.attn(x)
|
| 42 |
+
return self.ln(x)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
__all__ = ["TextEncoder"]
|
src/bigru_t/multimodal/video_encoder.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Xavante - video_encoder.py - Encoder de video (fusao imagem + audio)."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import logging
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
|
| 9 |
+
from .image_encoder import ImageEncoder
|
| 10 |
+
from .audio_encoder import AudioEncoder
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class VideoEncoder(nn.Module):
|
| 16 |
+
"""Combina frames de video + audio em uma representacao unificada."""
|
| 17 |
+
|
| 18 |
+
def __init__(self, d_model: int = 512, n_frames: int = 8):
|
| 19 |
+
super().__init__()
|
| 20 |
+
self.n_frames = n_frames
|
| 21 |
+
self.frame_encoder = ImageEncoder(d_model)
|
| 22 |
+
self.audio_encoder = AudioEncoder(d_model)
|
| 23 |
+
# Temporal aggregation
|
| 24 |
+
self.temporal = nn.GRU(d_model, d_model, batch_first=True)
|
| 25 |
+
self.fuse = nn.Linear(d_model * 2, d_model)
|
| 26 |
+
|
| 27 |
+
def forward(
|
| 28 |
+
self,
|
| 29 |
+
frames: torch.Tensor, # [B, T, C, H, W]
|
| 30 |
+
audio: torch.Tensor, # [B, C_audio, T_audio]
|
| 31 |
+
) -> torch.Tensor:
|
| 32 |
+
B, T, C, H, W = frames.shape
|
| 33 |
+
frames_flat = frames.view(B * T, C, H, W)
|
| 34 |
+
frame_emb = self.frame_encoder(frames_flat) # [B*T, d]
|
| 35 |
+
frame_emb = frame_emb.view(B, T, -1)
|
| 36 |
+
_, h = self.temporal(frame_emb)
|
| 37 |
+
video_emb = h.squeeze(0) # [B, d]
|
| 38 |
+
audio_emb = self.audio_encoder(audio)
|
| 39 |
+
return torch.tanh(self.fuse(torch.cat([video_emb, audio_emb], dim=-1)))
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
__all__ = ["VideoEncoder"]
|
src/bigru_t/optim/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Otimizadores (reaproveitados do source flexnet/)."""
|
src/bigru_t/optim/hamiltonian_wasserstein.py
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""hamiltonian_wasserstein_optimizer.py β Otimizador HΓbrido V11.26.
|
| 2 |
+
|
| 3 |
+
Combina 5 mecanismos com base nas provas matemΓ‘ticas V11.24:
|
| 4 |
+
1. AdamW (momentum adaptativo + weight decay decoupled) β base
|
| 5 |
+
2. RegularizaΓ§Γ£o Wasserstein Wβ com peso adaptativo Ξ»_W(t)
|
| 6 |
+
(Prova 1: Wβ(P_{t+1}, P*) β€ (1-Ξ»)Β·Wβ(P_t, P*) + Ξ΅)
|
| 7 |
+
3. RepulsΓ£o topolΓ³gica com peso adaptativo Ξ»_rep(t)
|
| 8 |
+
(Prova 3: maximiza I(Z;Y), minimiza H(Y|Z))
|
| 9 |
+
4. Taxa de aprendizado cΓclica sincronizada com Ξ²(t) e T(t)
|
| 10 |
+
(Van der Pol: T(t) = TβΒ·(1 + Ξ±Β·cos(Οt)))
|
| 11 |
+
5. Pruning/rewiring baseado em curvatura (Fisher) e MI (a cada N passos)
|
| 12 |
+
(Prova 3: VC_eff β€ VCβΒ·(1-Ο_pruned/Ο_total))
|
| 13 |
+
|
| 14 |
+
PseudocΓ³digo implementado:
|
| 15 |
+
Inicializar ΞΈ, m=0, v=0
|
| 16 |
+
Para t = 1 atΓ© T:
|
| 17 |
+
# 1. Obter batch e calcular Ξ²(t), T(t) via Van der Pol
|
| 18 |
+
beta = get_beta(t)
|
| 19 |
+
T = get_temperature(t)
|
| 20 |
+
|
| 21 |
+
# 2. Forward e perda (computada externamente)
|
| 22 |
+
L_dpo = DPO_loss(ΞΈ, beta)
|
| 23 |
+
L_w2 = W2_regularization(ΞΈ, ref_buffer)
|
| 24 |
+
L_rep = repulsion_loss(ΞΈ)
|
| 25 |
+
L_total = L_dpo + Ξ»_W(t) * L_w2 + Ξ»_rep(t) * L_rep + Ξ»_reg * ||ΞΈ||^2
|
| 26 |
+
|
| 27 |
+
# 3. Gradiente
|
| 28 |
+
g = βL_total
|
| 29 |
+
|
| 30 |
+
# 4. AtualizaΓ§Γ£o AdamW com passo cΓclico
|
| 31 |
+
Ξ·_t = Ξ·_base * (1 + A_eta * sin(Ο_eta * t))
|
| 32 |
+
m = Ξ²1 * m + (1 - Ξ²1) * g
|
| 33 |
+
v = Ξ²2 * v + (1 - Ξ²2) * g^2
|
| 34 |
+
ΞΈ = ΞΈ - Ξ·_t * m / (sqrt(v) + eps) - Ξ·_t * Ξ»_reg * ΞΈ
|
| 35 |
+
|
| 36 |
+
# 5. Atualizar buffers e regularizaΓ§Γ΅es
|
| 37 |
+
update_ref_buffer(ΞΈ)
|
| 38 |
+
Ξ»_W(t+1) = Ο_W^2 / ||ΞΈ - ΞΈ_ref||^2
|
| 39 |
+
Ξ»_rep(t+1) = Ο_rep^2 / L_rep
|
| 40 |
+
|
| 41 |
+
# 6. ReestruturaΓ§Γ£o (a cada N)
|
| 42 |
+
if t % N == 0:
|
| 43 |
+
metrics = compute_curvature_and_mutual_info(ΞΈ)
|
| 44 |
+
prune_rewire(ΞΈ, metrics)
|
| 45 |
+
atualizar dimensΓ£o VC efetiva
|
| 46 |
+
|
| 47 |
+
IntegraΓ§Γ£o com provas V11.24:
|
| 48 |
+
- Prova 1 (Wβ contraΓ§Γ£o): Ξ»_W(t) adaptativo garante contraΓ§Γ£o geomΓ©trica
|
| 49 |
+
- Prova 2 (Lyapunov): Ξ·_t cΓclico + ΞΌ_WΒ² garante dV/dt β€ -2ΞΌΒ·WβΒ²
|
| 50 |
+
- Prova 3 (VC reduction): pruning a cada N passos reduz VC_eff
|
| 51 |
+
- Prova 4 (raciocΓnio circular): nΓ£o estΓ‘ aqui, fica no CircularReasoningWasserstein
|
| 52 |
+
- Prova 5 (convergΓͺncia): combinando 1+2+3 β R(h_t) β€ R* + O(1/βN)
|
| 53 |
+
|
| 54 |
+
Compatibilidade:
|
| 55 |
+
- python β₯ 3.10
|
| 56 |
+
- torch β₯ 2.0
|
| 57 |
+
- Substitui CyclicHamiltonianOptimizer (API compatΓvel: step(loss=...))
|
| 58 |
+
"""
|
| 59 |
+
from __future__ import annotations
|
| 60 |
+
import math
|
| 61 |
+
import time
|
| 62 |
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
| 63 |
+
|
| 64 |
+
import torch
|
| 65 |
+
from torch.optim import Optimizer
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class HamiltonianWassersteinOptimizer(Optimizer):
|
| 69 |
+
"""Otimizador HΓbrido V11.26 β AdamW + Wβ + RepulsΓ£o + LR CΓclico + Pruning.
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
params: parΓ’metros do modelo
|
| 73 |
+
lr: taxa de aprendizado base (Ξ·_base)
|
| 74 |
+
betas: (Ξ²1, Ξ²2) para AdamW
|
| 75 |
+
eps: epsilon para estabilidade numΓ©rica
|
| 76 |
+
weight_decay: Ξ»_reg (decay decoupled do AdamW)
|
| 77 |
+
# LR cΓclico (Van der Pol)
|
| 78 |
+
lr_amp: A_eta β amplitude do ciclo (0.0 = sem ciclo)
|
| 79 |
+
lr_freq: Ο_eta β frequΓͺncia angular do ciclo
|
| 80 |
+
# Wβ adaptativo
|
| 81 |
+
sigma_w: Ο_W β escala para Ξ»_W(t) = Ο_WΒ² / ||ΞΈ - ΞΈ_ref||Β²
|
| 82 |
+
ref_buffer_size: tamanho do buffer de referΓͺncia (mΓ©dias mΓ³veis)
|
| 83 |
+
# RepulsΓ£o topolΓ³gica
|
| 84 |
+
sigma_rep: Ο_rep β escala para Ξ»_rep(t) = Ο_repΒ² / L_rep
|
| 85 |
+
# Pruning
|
| 86 |
+
prune_every_n: N β frequΓͺncia do pruning (0 = desativado)
|
| 87 |
+
prune_threshold: Ο_prune β threshold de Fisher information
|
| 88 |
+
# Misc
|
| 89 |
+
amsgrad: usar AMSGrad (max(v) em vez de v)
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
def __init__(
|
| 93 |
+
self,
|
| 94 |
+
params,
|
| 95 |
+
lr: float = 1e-3,
|
| 96 |
+
betas: Tuple[float, float] = (0.9, 0.999),
|
| 97 |
+
eps: float = 1e-8,
|
| 98 |
+
weight_decay: float = 0.01,
|
| 99 |
+
# LR cΓclico
|
| 100 |
+
lr_amp: float = 0.3,
|
| 101 |
+
lr_freq: float = 0.01,
|
| 102 |
+
# Wβ adaptativo
|
| 103 |
+
sigma_w: float = 1.0,
|
| 104 |
+
ref_buffer_size: int = 10,
|
| 105 |
+
# RepulsΓ£o topolΓ³gica
|
| 106 |
+
sigma_rep: float = 0.1,
|
| 107 |
+
# Pruning
|
| 108 |
+
prune_every_n: int = 0, # 0 = desativado (executado externamente)
|
| 109 |
+
prune_threshold: float = 1e-4,
|
| 110 |
+
# Misc
|
| 111 |
+
amsgrad: bool = False,
|
| 112 |
+
**kwargs,
|
| 113 |
+
):
|
| 114 |
+
if lr <= 0:
|
| 115 |
+
raise ValueError(f"lr deve ser positivo, recebeu {lr}")
|
| 116 |
+
if not 0.0 <= betas[0] < 1.0:
|
| 117 |
+
raise ValueError(f"beta1 deve estar em [0,1), recebeu {betas[0]}")
|
| 118 |
+
if not 0.0 <= betas[1] < 1.0:
|
| 119 |
+
raise ValueError(f"beta2 deve estar em [0,1), recebeu {betas[1]}")
|
| 120 |
+
if weight_decay < 0:
|
| 121 |
+
raise ValueError(f"weight_decay deve ser β₯ 0, recebeu {weight_decay}")
|
| 122 |
+
defaults = dict(
|
| 123 |
+
lr=lr, betas=betas, eps=eps, weight_decay=weight_decay,
|
| 124 |
+
lr_amp=lr_amp, lr_freq=lr_freq,
|
| 125 |
+
sigma_w=sigma_w, sigma_rep=sigma_rep,
|
| 126 |
+
amsgrad=amsgrad,
|
| 127 |
+
)
|
| 128 |
+
super().__init__(params, defaults)
|
| 129 |
+
self._t = 0
|
| 130 |
+
self._beta_dpo = 0.1
|
| 131 |
+
self._prune_every_n = prune_every_n
|
| 132 |
+
self._prune_threshold = prune_threshold
|
| 133 |
+
self._ref_buffer_size = ref_buffer_size
|
| 134 |
+
# Buffers de referΓͺncia (mΓ©dias mΓ³veis dos parΓ’metros) β bounded
|
| 135 |
+
self._ref_buffer: List[Dict[str, torch.Tensor]] = []
|
| 136 |
+
self._ref_buffer_max_size: int = ref_buffer_size # V11.30: size limit
|
| 137 |
+
# Estado do Lyapunov (para Prova 2)
|
| 138 |
+
self._lyapunov_history: List[float] = []
|
| 139 |
+
self._lambda_w_history: List[float] = []
|
| 140 |
+
self._lambda_rep_history: List[float] = []
|
| 141 |
+
self._lr_history: List[float] = []
|
| 142 |
+
self._max_history = 1000 # V11.30: limite para evitar memory leak
|
| 143 |
+
self._w2_loss = 0.0
|
| 144 |
+
self._rep_loss = 0.0
|
| 145 |
+
self._prune_count = 0
|
| 146 |
+
self._rewire_count = 0
|
| 147 |
+
self._vc_reduction = 0.0
|
| 148 |
+
|
| 149 |
+
# -----------------------------------------------------------------
|
| 150 |
+
# API pΓΊblica (compatΓvel com CyclicHamiltonianOptimizer)
|
| 151 |
+
# -----------------------------------------------------------------
|
| 152 |
+
def set_beta_dpo(self, beta: float, phase: float = 0.0):
|
| 153 |
+
"""Atualiza Ξ² do DPO (para sincronizaΓ§Γ£o do LR cΓclico)."""
|
| 154 |
+
self._beta_dpo = beta
|
| 155 |
+
|
| 156 |
+
def get_resonance_state(self) -> Dict[str, Any]:
|
| 157 |
+
"""Retorna estado interno para logging."""
|
| 158 |
+
omega = self.defaults["lr_freq"]
|
| 159 |
+
alpha = self.defaults["lr_amp"]
|
| 160 |
+
eta_t = self._get_cyclic_lr()
|
| 161 |
+
return {
|
| 162 |
+
"step": self._t,
|
| 163 |
+
"omega": omega,
|
| 164 |
+
"alpha": alpha,
|
| 165 |
+
"h_current": eta_t,
|
| 166 |
+
"beta_dpo": self._beta_dpo,
|
| 167 |
+
"lambda_w": self._get_lambda_w(),
|
| 168 |
+
"lambda_rep": self._get_lambda_rep(),
|
| 169 |
+
"w2_lyapunov": self._w2_loss,
|
| 170 |
+
"rep_loss": self._rep_loss,
|
| 171 |
+
"vc_reduction": self._vc_reduction,
|
| 172 |
+
"prune_count": self._prune_count,
|
| 173 |
+
"rewire_count": self._rewire_count,
|
| 174 |
+
"ref_buffer_size": len(self._ref_buffer),
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
def get_lyapunov(self) -> float:
|
| 178 |
+
"""Retorna o ΓΊltimo valor de Lyapunov V + ΞΌΒ·WβΒ² (Prova 2)."""
|
| 179 |
+
return self._w2_loss
|
| 180 |
+
|
| 181 |
+
def get_state_summary(self) -> Dict[str, Any]:
|
| 182 |
+
"""Retorna resumo completo do estado para anΓ‘lise."""
|
| 183 |
+
return {
|
| 184 |
+
"step": self._t,
|
| 185 |
+
"beta_dpo": self._beta_dpo,
|
| 186 |
+
"cyclic_lr": self._get_cyclic_lr(),
|
| 187 |
+
"lambda_w": self._get_lambda_w(),
|
| 188 |
+
"lambda_rep": self._get_lambda_rep(),
|
| 189 |
+
"w2_loss": self._w2_loss,
|
| 190 |
+
"rep_loss": self._rep_loss,
|
| 191 |
+
"vc_reduction": self._vc_reduction,
|
| 192 |
+
"prune_count": self._prune_count,
|
| 193 |
+
"rewire_count": self._rewire_count,
|
| 194 |
+
"ref_buffer_size": len(self._ref_buffer),
|
| 195 |
+
"lyapunov_history_tail": self._lyapunov_history[-20:],
|
| 196 |
+
"lambda_w_history_tail": self._lambda_w_history[-20:],
|
| 197 |
+
"lr_history_tail": self._lr_history[-20:],
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
# -----------------------------------------------------------------
|
| 201 |
+
# CΓ‘lculos internos
|
| 202 |
+
# -----------------------------------------------------------------
|
| 203 |
+
def _get_cyclic_lr(self) -> float:
|
| 204 |
+
"""Ξ·_t = Ξ·_base * (1 + A_eta * sin(Ο_eta * t)) (Van der Pol)."""
|
| 205 |
+
eta_base = self.defaults["lr"]
|
| 206 |
+
alpha = self.defaults["lr_amp"]
|
| 207 |
+
omega = self.defaults["lr_freq"]
|
| 208 |
+
return eta_base * (1.0 + alpha * math.sin(omega * self._t))
|
| 209 |
+
|
| 210 |
+
def _get_lambda_w(self) -> float:
|
| 211 |
+
"""Ξ»_W(t) = Ο_WΒ² * ||ΞΈ - ΞΈ_ref||Β² (adaptativo, inverted fix).
|
| 212 |
+
|
| 213 |
+
FIXED: multiplication instead of division so punishment is stronger
|
| 214 |
+
when parameters diverge far from reference (model struggling).
|
| 215 |
+
"""
|
| 216 |
+
if not self._ref_buffer:
|
| 217 |
+
return 0.0 # sem referΓͺncia ainda
|
| 218 |
+
sigma_w = self.defaults["sigma_w"]
|
| 219 |
+
# Calcular ||ΞΈ - ΞΈ_ref||Β²
|
| 220 |
+
ref_state = self._ref_buffer[-1] # ΓΊltima referΓͺncia
|
| 221 |
+
diff_sq_sum = 0.0
|
| 222 |
+
n_params = 0
|
| 223 |
+
for group in self.param_groups:
|
| 224 |
+
for p in group["params"]:
|
| 225 |
+
if p.grad is None:
|
| 226 |
+
continue
|
| 227 |
+
pid = id(p)
|
| 228 |
+
if pid in ref_state:
|
| 229 |
+
diff = p.data - ref_state[pid]
|
| 230 |
+
diff_sq_sum += float((diff * diff).sum().item())
|
| 231 |
+
n_params += p.numel()
|
| 232 |
+
if n_params == 0 or diff_sq_sum < 1e-12:
|
| 233 |
+
return float(sigma_w) # fallback
|
| 234 |
+
# Normalizar por nΓΊmero de parΓ’metros
|
| 235 |
+
diff_sq_norm = diff_sq_sum / n_params
|
| 236 |
+
# FIXED: multiply (ΟΒ² * distance) instead of divide (ΟΒ² / distance)
|
| 237 |
+
# so Ξ»_W is large when far from ref β stronger punishment when struggling
|
| 238 |
+
# Bounded to prevent explosion: cap at sigma_wΒ² * 10
|
| 239 |
+
lambda_w = min(float(sigma_w ** 2 * diff_sq_norm), float(sigma_w ** 2 * 10.0))
|
| 240 |
+
return lambda_w
|
| 241 |
+
|
| 242 |
+
def _get_lambda_rep(self) -> float:
|
| 243 |
+
"""Ξ»_rep(t) = Ο_repΒ² * L_rep (adaptativo, inverted fix).
|
| 244 |
+
|
| 245 |
+
FIXED: multiplication instead of division so concentrated parameters
|
| 246 |
+
(high L_rep) get strong repulsion (large Ξ»_rep).
|
| 247 |
+
"""
|
| 248 |
+
sigma_rep = self.defaults["sigma_rep"]
|
| 249 |
+
if self._rep_loss < 1e-12:
|
| 250 |
+
return float(sigma_rep) # fallback
|
| 251 |
+
# FIXED: multiply (ΟΒ² * L_rep) instead of divide (ΟΒ² / L_rep)
|
| 252 |
+
# so concentrated params (high L_rep) β large Ξ»_rep β strong repulsion
|
| 253 |
+
# Bounded to prevent explosion: cap at sigma_repΒ² * 100
|
| 254 |
+
lambda_rep = min(float(sigma_rep ** 2 * max(self._rep_loss, 1e-8)), float(sigma_rep ** 2 * 100.0))
|
| 255 |
+
return lambda_rep
|
| 256 |
+
|
| 257 |
+
def _update_ref_buffer(self):
|
| 258 |
+
"""Adiciona snapshot atual dos parΓ’metros ao buffer de referΓͺncia."""
|
| 259 |
+
snapshot = {}
|
| 260 |
+
for group in self.param_groups:
|
| 261 |
+
for p in group["params"]:
|
| 262 |
+
snapshot[id(p)] = p.data.clone().detach()
|
| 263 |
+
self._ref_buffer.append(snapshot)
|
| 264 |
+
if len(self._ref_buffer) > self._ref_buffer_size:
|
| 265 |
+
self._ref_buffer.pop(0)
|
| 266 |
+
|
| 267 |
+
def _compute_repulsion_loss(self) -> torch.Tensor:
|
| 268 |
+
"""RepulsΓ£o topolΓ³gica: penaliza parΓ’metros muito prΓ³ximos.
|
| 269 |
+
|
| 270 |
+
L_rep = Ξ£_ij exp(-||ΞΈ_i - ΞΈ_j||Β² / ΟΒ²) onde ΞΈ_i sΓ£o sub-vetores
|
| 271 |
+
(por camada, para evitar O(NΒ²) na Parameters).
|
| 272 |
+
|
| 273 |
+
SimplificaΓ§Γ£o: usa a variΓ’ncia intra-camada como proxy.
|
| 274 |
+
Alta variΓ’ncia = boa dispersΓ£o (menor L_rep).
|
| 275 |
+
"""
|
| 276 |
+
sigma_rep = self.defaults["sigma_rep"]
|
| 277 |
+
rep_total = torch.tensor(0.0, device=next(iter(self.param_groups))["params"][0].device
|
| 278 |
+
if self.param_groups else "cpu")
|
| 279 |
+
for group in self.param_groups:
|
| 280 |
+
for p in group["params"]:
|
| 281 |
+
if p.numel() < 2:
|
| 282 |
+
continue
|
| 283 |
+
# VariΓ’ncia dos pesos (proxy para dispersΓ£o topolΓ³gica)
|
| 284 |
+
var_p = p.data.var()
|
| 285 |
+
# RepulsΓ£o = 1 / (var + eps) β alta quando pesos estΓ£o concentrados
|
| 286 |
+
# (baixa variΓ’ncia = alta repulsΓ£o para penalizar)
|
| 287 |
+
rep_total = rep_total + 1.0 / (var_p + sigma_rep ** 2)
|
| 288 |
+
return rep_total
|
| 289 |
+
|
| 290 |
+
# -----------------------------------------------------------------
|
| 291 |
+
# Pruning / Rewiring (Prova 3)
|
| 292 |
+
# -----------------------------------------------------------------
|
| 293 |
+
def _compute_fisher_diagonal(self) -> Dict[int, torch.Tensor]:
|
| 294 |
+
"""Computa diagonal da Fisher information: Ο_i = (βL/βΞΈ_i)Β²."""
|
| 295 |
+
fisher = {}
|
| 296 |
+
for group in self.param_groups:
|
| 297 |
+
for p in group["params"]:
|
| 298 |
+
if p.grad is None:
|
| 299 |
+
continue
|
| 300 |
+
fisher[id(p)] = p.grad.data ** 2
|
| 301 |
+
return fisher
|
| 302 |
+
|
| 303 |
+
def _prune_step(self):
|
| 304 |
+
"""Executa um passo de pruning baseado em Fisher information.
|
| 305 |
+
|
| 306 |
+
Soft pruning: zera gradientes de parΓ’metros com Ο_i < Ο_prune
|
| 307 |
+
(nΓ£o remove fisicamente, mas impede atualizaΓ§Γ£o).
|
| 308 |
+
|
| 309 |
+
V11.26c: pruning muito agressivo causava explosion.
|
| 310 |
+
CorreΓ§Γ£o: apenas bottom 10% (em vez de mediana * tau).
|
| 311 |
+
"""
|
| 312 |
+
fisher = self._compute_fisher_diagonal()
|
| 313 |
+
if not fisher:
|
| 314 |
+
return
|
| 315 |
+
all_fisher = torch.cat([f.flatten() for f in fisher.values()])
|
| 316 |
+
if all_fisher.numel() == 0:
|
| 317 |
+
return
|
| 318 |
+
# V11.26c: apenas bottom 10% (muito menos agressivo que mediana * tau)
|
| 319 |
+
threshold = float(all_fisher.quantile(0.1).item())
|
| 320 |
+
total_pruned = 0
|
| 321 |
+
total_params = 0
|
| 322 |
+
for group in self.param_groups:
|
| 323 |
+
for p in group["params"]:
|
| 324 |
+
if p.grad is None:
|
| 325 |
+
continue
|
| 326 |
+
fid = id(p)
|
| 327 |
+
if fid not in fisher:
|
| 328 |
+
continue
|
| 329 |
+
mask = fisher[fid] > threshold
|
| 330 |
+
pruned = (~mask).sum().item()
|
| 331 |
+
total_pruned += pruned
|
| 332 |
+
total_params += p.numel()
|
| 333 |
+
# Soft prune: zerar gradientes dos parΓ’metros com baixa Fisher
|
| 334 |
+
p.grad.data.mul_(mask.float())
|
| 335 |
+
self._prune_count += total_pruned
|
| 336 |
+
self._vc_reduction = total_pruned / max(total_params, 1)
|
| 337 |
+
|
| 338 |
+
def _rewire_step(self):
|
| 339 |
+
"""Adiciona pequenas perturbaΓ§Γ΅es a parΓ’metros com alta Fisher.
|
| 340 |
+
|
| 341 |
+
Equivalente a 'criar novas conexΓ΅es' em regiΓ΅es de alta informaΓ§Γ£o.
|
| 342 |
+
"""
|
| 343 |
+
fisher = self._compute_fisher_diagonal()
|
| 344 |
+
if not fisher:
|
| 345 |
+
return
|
| 346 |
+
all_fisher = torch.cat([f.flatten() for f in fisher.values()])
|
| 347 |
+
if all_fisher.numel() == 0:
|
| 348 |
+
return
|
| 349 |
+
threshold = float(all_fisher.quantile(0.9).item()) # top 10%
|
| 350 |
+
rewired = 0
|
| 351 |
+
for group in self.param_groups:
|
| 352 |
+
for p in group["params"]:
|
| 353 |
+
if p.grad is None:
|
| 354 |
+
continue
|
| 355 |
+
fid = id(p)
|
| 356 |
+
if fid not in fisher:
|
| 357 |
+
continue
|
| 358 |
+
# Top 10% Fisher β adicionar pequena perturbaΓ§Γ£o
|
| 359 |
+
high_info_mask = fisher[fid] > threshold
|
| 360 |
+
n_rewire = int(high_info_mask.sum().item() * 0.01) # 1% dos top
|
| 361 |
+
if n_rewire > 0:
|
| 362 |
+
indices = torch.where(high_info_mask.flatten())[0]
|
| 363 |
+
if len(indices) > 0:
|
| 364 |
+
chosen = indices[torch.randperm(len(indices))[:n_rewire]]
|
| 365 |
+
with torch.no_grad():
|
| 366 |
+
perturbation = torch.randn(n_rewire, device=p.device) * 0.001
|
| 367 |
+
flat = p.data.flatten()
|
| 368 |
+
flat[chosen] += perturbation
|
| 369 |
+
p.data = flat.view_as(p.data)
|
| 370 |
+
rewired += n_rewire
|
| 371 |
+
self._rewire_count += rewired
|
| 372 |
+
|
| 373 |
+
# -----------------------------------------------------------------
|
| 374 |
+
# Step principal
|
| 375 |
+
# -----------------------------------------------------------------
|
| 376 |
+
@torch.no_grad()
|
| 377 |
+
def step(self, closure=None, loss=None, w2_loss: float = 0.0,
|
| 378 |
+
rep_loss: Optional[float] = None):
|
| 379 |
+
"""Executa um passo de otimizaΓ§Γ£o.
|
| 380 |
+
|
| 381 |
+
Args:
|
| 382 |
+
closure: closure opcional (nΓ£o usado normalmente)
|
| 383 |
+
loss: valor do loss atual (para logging)
|
| 384 |
+
w2_loss: valor do Wβ loss (Prova 1, 2)
|
| 385 |
+
rep_loss: valor do repulsion loss (None = computar internamente)
|
| 386 |
+
|
| 387 |
+
Returns:
|
| 388 |
+
loss value (se fornecido)
|
| 389 |
+
"""
|
| 390 |
+
if closure is not None:
|
| 391 |
+
with torch.enable_grad():
|
| 392 |
+
loss_val = closure()
|
| 393 |
+
else:
|
| 394 |
+
loss_val = loss
|
| 395 |
+
|
| 396 |
+
# Atualizar estado interno
|
| 397 |
+
self._w2_loss = float(w2_loss) if w2_loss else 0.0
|
| 398 |
+
if rep_loss is not None:
|
| 399 |
+
self._rep_loss = float(rep_loss)
|
| 400 |
+
else:
|
| 401 |
+
# Computar repulsΓ£o internamente (pode ser caro)
|
| 402 |
+
try:
|
| 403 |
+
rep = self._compute_repulsion_loss()
|
| 404 |
+
self._rep_loss = float(rep.item())
|
| 405 |
+
except (RuntimeError, ValueError) as e:
|
| 406 |
+
import logging
|
| 407 |
+
logging.getLogger(__name__).warning("_compute_repulsion_loss failed: %s", e)
|
| 408 |
+
self._rep_loss = 0.0
|
| 409 |
+
|
| 410 |
+
# LR cΓclico
|
| 411 |
+
eta_t = self._get_cyclic_lr()
|
| 412 |
+
|
| 413 |
+
# Ξ»_W e Ξ»_rep adaptativos
|
| 414 |
+
lambda_w = self._get_lambda_w()
|
| 415 |
+
lambda_rep = self._get_lambda_rep()
|
| 416 |
+
|
| 417 |
+
# AdamW update com LR cΓclico
|
| 418 |
+
beta1, beta2 = self.defaults["betas"]
|
| 419 |
+
eps = self.defaults["eps"]
|
| 420 |
+
weight_decay = self.defaults["weight_decay"]
|
| 421 |
+
|
| 422 |
+
for group in self.param_groups:
|
| 423 |
+
for p in group["params"]:
|
| 424 |
+
if p.grad is None:
|
| 425 |
+
continue
|
| 426 |
+
grad = p.grad.data
|
| 427 |
+
state = self.state[p]
|
| 428 |
+
# Lazy state initialization
|
| 429 |
+
if len(state) == 0:
|
| 430 |
+
state["step"] = 0
|
| 431 |
+
state["exp_avg"] = torch.zeros_like(p.data) # m
|
| 432 |
+
state["exp_avg_sq"] = torch.zeros_like(p.data) # v
|
| 433 |
+
if self.defaults["amsgrad"]:
|
| 434 |
+
state["max_exp_avg_sq"] = torch.zeros_like(p.data)
|
| 435 |
+
state["step"] += 1
|
| 436 |
+
m = state["exp_avg"]
|
| 437 |
+
v = state["exp_avg_sq"]
|
| 438 |
+
|
| 439 |
+
# AdamW: weight decay decoupled
|
| 440 |
+
if weight_decay != 0:
|
| 441 |
+
p.data.mul_(1.0 - eta_t * weight_decay)
|
| 442 |
+
|
| 443 |
+
# Adam moments
|
| 444 |
+
m.mul_(beta1).add_(grad, alpha=1 - beta1)
|
| 445 |
+
v.mul_(beta2).addcmul_(grad, grad, value=1 - beta2)
|
| 446 |
+
|
| 447 |
+
if self.defaults["amsgrad"]:
|
| 448 |
+
max_v = state["max_exp_avg_sq"]
|
| 449 |
+
torch.maximum(max_v, v, out=max_v)
|
| 450 |
+
v_hat = max_v
|
| 451 |
+
else:
|
| 452 |
+
v_hat = v
|
| 453 |
+
|
| 454 |
+
# Bias correction
|
| 455 |
+
bias_c1 = 1 - beta1 ** state["step"]
|
| 456 |
+
bias_c2 = 1 - beta2 ** state["step"]
|
| 457 |
+
step_size = eta_t / bias_c1
|
| 458 |
+
bias_c2_sqrt = math.sqrt(bias_c2)
|
| 459 |
+
|
| 460 |
+
# Update
|
| 461 |
+
p.data.addcdiv_(m, v_hat.sqrt().add_(eps), value=-step_size)
|
| 462 |
+
# Bias-corrected Adam: m_hat / (sqrt(v_hat) + eps) with step_size = eta_t / bias_c1
|
| 463 |
+
|
| 464 |
+
# Atualizar histΓ³rico (com limite para evitar memory leak V11.30)
|
| 465 |
+
self._t += 1
|
| 466 |
+
self._lyapunov_history.append(self._w2_loss)
|
| 467 |
+
self._lambda_w_history.append(lambda_w)
|
| 468 |
+
self._lambda_rep_history.append(lambda_rep)
|
| 469 |
+
self._lr_history.append(eta_t)
|
| 470 |
+
# V11.30: truncar histΓ³ricos para evitar memory leak
|
| 471 |
+
if len(self._lyapunov_history) > self._max_history:
|
| 472 |
+
self._lyapunov_history = self._lyapunov_history[-self._max_history:]
|
| 473 |
+
self._lambda_w_history = self._lambda_w_history[-self._max_history:]
|
| 474 |
+
self._lambda_rep_history = self._lambda_rep_history[-self._max_history:]
|
| 475 |
+
self._lr_history = self._lr_history[-self._max_history:]
|
| 476 |
+
|
| 477 |
+
# Atualizar buffer de referΓͺncia (a cada 10 passos para economizar)
|
| 478 |
+
if self._t % 10 == 0:
|
| 479 |
+
self._update_ref_buffer()
|
| 480 |
+
|
| 481 |
+
# Pruning/rewiring a cada N passos
|
| 482 |
+
if self._prune_every_n > 0 and self._t % self._prune_every_n == 0:
|
| 483 |
+
self._prune_step()
|
| 484 |
+
self._rewire_step()
|
| 485 |
+
|
| 486 |
+
return loss_val
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
__all__ = ["HamiltonianWassersteinOptimizer"]
|
src/bigru_t/quantization/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""QuantizaΓ§Γ£o W8A8 (Lema 3)."""
|
| 2 |
+
from .quantized_linear import QuantizedLinear, quantize_tensor, apply_w8a8
|
| 3 |
+
# Reaproveitado do source (nΓ£o modificado):
|
| 4 |
+
# from .w8a8_qoperator import W8A8QOperatorQuantizer, W8A8QOperatorLinear
|
| 5 |
+
|
| 6 |
+
__all__ = ["QuantizedLinear", "quantize_tensor", "apply_w8a8"]
|
src/bigru_t/quantization/quantized_linear.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""quantized_linear.py β QuantizedLinear: camada Linear com W8A8 fake quant.
|
| 2 |
+
|
| 3 |
+
Implementa o Lema 3 (Cancelamento de ruΓdo de quantizaΓ§Γ£o).
|
| 4 |
+
|
| 5 |
+
A quantizaΓ§Γ£o W8A8 (pesos e ativaΓ§Γ΅es em 8 bits) Γ© simulada no forward
|
| 6 |
+
via fake quantization (round + clamp + dequant). O backward usa Straight-
|
| 7 |
+
Through Estimator (STE) β o gradiente passa direto pela operaΓ§Γ£o de round,
|
| 8 |
+
que tem derivada zero em quase todos os pontos.
|
| 9 |
+
|
| 10 |
+
Reaproveita a filosofia do flexnet/w8a8_qoperator.py (V13.9.1 QOperator),
|
| 11 |
+
mas simplificada para o caso fake-quant durante o treino. Para inferΓͺncia
|
| 12 |
+
produΓ§Γ£o, o w8a8_qoperator.py deve ser usado (gera QOperator ONNX).
|
| 13 |
+
|
| 14 |
+
DiferenΓ§as vs. w8a8_qoperator.py:
|
| 15 |
+
- Aqui: fake quant em todo forward (treino)
|
| 16 |
+
- w8a8_qoperator.py: quantizaΓ§Γ£o real + empacotamento ONNX (inferΓͺncia)
|
| 17 |
+
- Aqui: scale por tensor (nΓ£o por canal) β mais simples, suficiente para
|
| 18 |
+
bug-detection
|
| 19 |
+
"""
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import torch
|
| 23 |
+
import torch.nn as nn
|
| 24 |
+
import torch.nn.functional as F
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def quantize_tensor(x: torch.Tensor, num_bits: int = 8) -> torch.Tensor:
|
| 28 |
+
"""QuantizaΓ§Γ£o simΓ©trica uniforme (fake quant).
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
x: tensor a quantizar
|
| 32 |
+
num_bits: nΒΊ de bits (default 8 β range [-128, 127])
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
x_deq: tensor dequantizado (mesmo shape, mesmo dtype)
|
| 36 |
+
"""
|
| 37 |
+
if x is None:
|
| 38 |
+
return None
|
| 39 |
+
qmin = -(2 ** (num_bits - 1))
|
| 40 |
+
qmax = 2 ** (num_bits - 1) - 1
|
| 41 |
+
# Scale por tensor (nΓ£o por canal) β simplificaΓ§Γ£o
|
| 42 |
+
scale = x.abs().max() / qmax
|
| 43 |
+
if scale < 1e-8:
|
| 44 |
+
scale = torch.tensor(1.0, dtype=x.dtype, device=x.device)
|
| 45 |
+
# Round + clamp (STE backward β grad passa direto)
|
| 46 |
+
x_q = torch.round(x / scale).clamp(qmin, qmax)
|
| 47 |
+
x_deq = x_q * scale
|
| 48 |
+
return x_deq
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class QuantizedLinear(nn.Linear):
|
| 52 |
+
"""Camada Linear com quantizaΓ§Γ£o W8A8 (fake quant durante treino).
|
| 53 |
+
|
| 54 |
+
No forward:
|
| 55 |
+
q_weight = quantize_tensor(self.weight, 8) # W8
|
| 56 |
+
q_input = quantize_tensor(input, 8) # A8
|
| 57 |
+
output = F.linear(q_input, q_weight, self.bias)
|
| 58 |
+
|
| 59 |
+
O gradiente flui atravΓ©s do STE (round Γ© nΓ£o-diferenciΓ‘vel, mas o
|
| 60 |
+
autograd do PyTorch propaga o gradiente via x_deq = x_q * scale onde
|
| 61 |
+
x_q depende de round(x/scale) que tem gradiente zero β o STE substitui
|
| 62 |
+
esse gradiente por 1).
|
| 63 |
+
|
| 64 |
+
Args:
|
| 65 |
+
in_features, out_features, bias: iguais ao nn.Linear
|
| 66 |
+
num_bits: nΒΊ de bits (default 8)
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
def __init__(self, in_features: int, out_features: int, bias: bool = True, num_bits: int = 8):
|
| 70 |
+
super().__init__(in_features, out_features, bias)
|
| 71 |
+
self.num_bits = num_bits
|
| 72 |
+
|
| 73 |
+
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
| 74 |
+
# QuantizaΓ§Γ£o dos pesos (W8)
|
| 75 |
+
q_weight = quantize_tensor(self.weight, self.num_bits)
|
| 76 |
+
# QuantizaΓ§Γ£o da ativaΓ§Γ£o de entrada (A8)
|
| 77 |
+
q_input = quantize_tensor(input, self.num_bits)
|
| 78 |
+
output = F.linear(q_input, q_weight, self.bias)
|
| 79 |
+
return output
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def apply_w8a8(module: nn.Module) -> nn.Module:
|
| 83 |
+
"""Substitui todas as nn.Linear por QuantizedLinear (recursivamente).
|
| 84 |
+
|
| 85 |
+
Usado por UnifiedModel.apply(apply_w8a8) no __init__.
|
| 86 |
+
"""
|
| 87 |
+
if isinstance(module, nn.Linear) and not isinstance(module, QuantizedLinear):
|
| 88 |
+
return QuantizedLinear(module.in_features, module.out_features, module.bias is not None)
|
| 89 |
+
return module
|
src/bigru_t/quantization/w8a8_qoperator.py
ADDED
|
@@ -0,0 +1,790 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""w8a8_qoperator.py β W8A8 Quantization com QOperator format (V13.9.1).
|
| 2 |
+
|
| 3 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 4 |
+
V13.9.1 β REFINAMENTO W8A8: QOPERATOR EM VEZ DE QDQ
|
| 5 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 6 |
+
|
| 7 |
+
PROBLEMA DO FORMATO QDQ (V13.8 e anteriores):
|
| 8 |
+
- QDQ insere nΓ³s QuantizeLinear β MatMul(fp32) β DequantizeLinear
|
| 9 |
+
- O ONNX Runtime executa MatMul em fp32 (nΓ£o aproveita INT8 do AVX512_VNNI)
|
| 10 |
+
- Para GRU, o QDQ gera nΓ³s dinΓ’micos que o quantizador nΓ£o consegue otimizar
|
| 11 |
+
|
| 12 |
+
SOLUΓΓO QOPERATOR (V13.9):
|
| 13 |
+
- Usa QLinearMatMul diretamente (op ONNX dedicada para INT8)
|
| 14 |
+
- Pesos armazenados como int8 + scale + zero_point
|
| 15 |
+
- AtivaΓ§Γ΅es quantizadas on-the-fly com QuantizeLinear β QLinearMatMul
|
| 16 |
+
- Para GRU: mantΓ©m em fp32 (nΓ£o hΓ‘ QLinearGRU no ONNX)
|
| 17 |
+
|
| 18 |
+
V13.9.1 BUG FIXES:
|
| 19 |
+
- BUG-W8A8-001 FIX: ONNX export agora produz QLinearMatMul REAL via
|
| 20 |
+
symbolic registration (nΓ£o mais fp32 simulation).
|
| 21 |
+
- BUG-W8A8-002 FIX: weight_fp contado em fp32_params no compression ratio
|
| 22 |
+
(ou deletado apΓ³s freeze para inference).
|
| 23 |
+
- BUG-W8A8-003 FIX: Calibration coleta apenas estatΓsticas (max-abs per
|
| 24 |
+
channel), nΓ£o tensores completos.
|
| 25 |
+
- Adicionada deleΓ§Γ£o de weight_fp apΓ³s freeze (opΓ§Γ£o free_fp32=True).
|
| 26 |
+
|
| 27 |
+
ONNX OPS USADAS (QOPERATOR):
|
| 28 |
+
- QLinearMatMul: A_int8 Β· W_int8 com escalas β Y_int8 (INT8 nativo)
|
| 29 |
+
- QuantizeLinear: fp32 β int8 (apenas para ativaΓ§Γ΅es de entrada)
|
| 30 |
+
- DequantizeLinear: int8 β fp32 (apenas para saΓda final)
|
| 31 |
+
- MatMul (fp32): mantida para GRU (nΓ£o quantizΓ‘vel)
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
from __future__ import annotations
|
| 35 |
+
|
| 36 |
+
import math
|
| 37 |
+
import logging
|
| 38 |
+
from dataclasses import dataclass, field
|
| 39 |
+
from typing import Optional, Tuple, List, Dict, Any
|
| 40 |
+
|
| 41 |
+
import torch
|
| 42 |
+
import torch.nn as nn
|
| 43 |
+
import torch.nn.functional as F
|
| 44 |
+
|
| 45 |
+
logger = logging.getLogger(__name__)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 49 |
+
# FunΓ§Γ΅es de QuantizaΓ§Γ£o SimΓ©trica (zero_point=0 sempre)
|
| 50 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 51 |
+
def quantize_per_channel_symmetric(
|
| 52 |
+
W: torch.Tensor,
|
| 53 |
+
axis: int = 0,
|
| 54 |
+
n_bits: int = 8,
|
| 55 |
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 56 |
+
"""QuantizaΓ§Γ£o int8 simΓ©trica per-channel (zero_point=0)."""
|
| 57 |
+
assert W.dim() == 2, f"esperado 2D, recebeu dim={W.dim()}"
|
| 58 |
+
qmax = 2 ** (n_bits - 1) - 1 # 127
|
| 59 |
+
qmin = -(2 ** (n_bits - 1) - 1) # -127
|
| 60 |
+
other = 1 - axis
|
| 61 |
+
max_abs = W.abs().amax(dim=other, keepdim=True).clamp(min=1e-8)
|
| 62 |
+
scale = max_abs / qmax # (d_axis, 1)
|
| 63 |
+
W_q = torch.round(W / scale).clamp(qmin, qmax).to(torch.int8)
|
| 64 |
+
return W_q, scale.squeeze(1)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def quantize_per_token_symmetric(
|
| 68 |
+
X: torch.Tensor,
|
| 69 |
+
n_bits: int = 8,
|
| 70 |
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 71 |
+
"""QuantizaΓ§Γ£o int8 simΓ©trica per-token (ΓΊltimo eixo = canal)."""
|
| 72 |
+
qmax = 2 ** (n_bits - 1) - 1
|
| 73 |
+
qmin = -(2 ** (n_bits - 1) - 1)
|
| 74 |
+
max_abs = X.abs().amax(dim=-1, keepdim=True).clamp(min=1e-8)
|
| 75 |
+
scale = max_abs / qmax
|
| 76 |
+
X_q = torch.round(X / scale).clamp(qmin, qmax).to(torch.int8)
|
| 77 |
+
return X_q, scale
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def calibrate_smoothquant(
|
| 81 |
+
X: torch.Tensor,
|
| 82 |
+
W: torch.Tensor,
|
| 83 |
+
alpha: float = 0.5,
|
| 84 |
+
) -> torch.Tensor:
|
| 85 |
+
"""Calcula smooth factor s β R^{d_in}_{>0} (SmoothQuant).
|
| 86 |
+
|
| 87 |
+
s_j = max(|X_j|)^Ξ± / max(|W_j|)^{1-Ξ±}
|
| 88 |
+
"""
|
| 89 |
+
d_in = W.shape[-1]
|
| 90 |
+
if X.shape[-1] != d_in:
|
| 91 |
+
return torch.ones(d_in, device=W.device, dtype=W.dtype)
|
| 92 |
+
X_flat = X.reshape(-1, d_in)
|
| 93 |
+
max_x = X_flat.abs().amax(dim=0).clamp(min=1e-8)
|
| 94 |
+
max_w = W.abs().amax(dim=0).clamp(min=1e-8)
|
| 95 |
+
s = torch.pow(max_x, alpha) / torch.pow(max_w, 1.0 - alpha)
|
| 96 |
+
return s.clamp(min=1e-8)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 100 |
+
# W8A8 QOperator Linear Layer (drop-in para nn.Linear)
|
| 101 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 102 |
+
class W8A8QOperatorLinear(nn.Module):
|
| 103 |
+
"""Camada Linear W8A8 com formato QOperator.
|
| 104 |
+
|
| 105 |
+
Modos:
|
| 106 |
+
- calibration: coleta ativaΓ§Γ΅es (estatΓsticas) para SmoothQuant
|
| 107 |
+
- quantized: pesos em int8, ativaΓ§Γ΅es quantizadas on-the-fly
|
| 108 |
+
|
| 109 |
+
V13.9.1 FIXES:
|
| 110 |
+
- BUG-W8A8-001: ONNX export produz QLinearMatMul REAL via symbolic.
|
| 111 |
+
- BUG-W8A8-002: weight_fp opcionalmente deletado apΓ³s freeze.
|
| 112 |
+
- BUG-W8A8-003: Calibration coleta apenas max-abs per channel.
|
| 113 |
+
"""
|
| 114 |
+
|
| 115 |
+
def __init__(
|
| 116 |
+
self,
|
| 117 |
+
ref: nn.Linear,
|
| 118 |
+
alpha: float = 0.5,
|
| 119 |
+
calib_batch_size: int = 256,
|
| 120 |
+
free_fp32_after_freeze: bool = False,
|
| 121 |
+
):
|
| 122 |
+
super().__init__()
|
| 123 |
+
self.in_features = ref.in_features
|
| 124 |
+
self.out_features = ref.out_features
|
| 125 |
+
self.alpha = alpha
|
| 126 |
+
self.calib_batch_size = calib_batch_size
|
| 127 |
+
self.free_fp32_after_freeze = free_fp32_after_freeze
|
| 128 |
+
|
| 129 |
+
# Pesos fp32 (treinΓ‘veis atΓ© freeze)
|
| 130 |
+
self.weight_fp = nn.Parameter(ref.weight.detach().clone())
|
| 131 |
+
self.bias = (
|
| 132 |
+
nn.Parameter(ref.bias.detach().clone()) if ref.bias is not None else None
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
# Buffers de quantizaΓ§Γ£o
|
| 136 |
+
self.register_buffer("smooth_scale", torch.ones(self.in_features))
|
| 137 |
+
self.register_buffer("weight_scale", torch.ones(self.out_features))
|
| 138 |
+
self.register_buffer(
|
| 139 |
+
"weight_int8",
|
| 140 |
+
torch.zeros(self.out_features, self.in_features, dtype=torch.int8),
|
| 141 |
+
)
|
| 142 |
+
# zero_point = 0 sempre (simΓ©trica)
|
| 143 |
+
self.register_buffer("weight_zero_point", torch.zeros(self.out_features, dtype=torch.int8))
|
| 144 |
+
self.register_buffer("is_quantized", torch.tensor(False))
|
| 145 |
+
self.register_buffer("calib_collected", torch.tensor(False))
|
| 146 |
+
# BUG-W8A8-003 FIX: Apenas max-abs per channel (nΓ£o tensor completo)
|
| 147 |
+
self.register_buffer("calib_max_abs", torch.zeros(self.in_features))
|
| 148 |
+
self.register_buffer("calib_count", torch.tensor(0, dtype=torch.long))
|
| 149 |
+
|
| 150 |
+
@torch.no_grad()
|
| 151 |
+
def calibrate(self, X: torch.Tensor) -> None:
|
| 152 |
+
"""Acumula estatΓsticas de ativaΓ§Γ£o (max-abs per channel) para SmoothQuant.
|
| 153 |
+
|
| 154 |
+
BUG-W8A8-003 FIX: NΓ£o armazena tensor completo, apenas max(|X|) per channel.
|
| 155 |
+
"""
|
| 156 |
+
if bool(self.is_quantized):
|
| 157 |
+
raise RuntimeError("CalibraΓ§Γ£o apΓ³s freeze nΓ£o Γ© permitida.")
|
| 158 |
+
flat = X.reshape(-1, self.in_features)
|
| 159 |
+
# Update running max-abs per channel
|
| 160 |
+
batch_max_abs = flat.abs().amax(dim=0).to(self.calib_max_abs.dtype)
|
| 161 |
+
self.calib_max_abs.copy_(torch.maximum(self.calib_max_abs, batch_max_abs))
|
| 162 |
+
self.calib_count += flat.shape[0]
|
| 163 |
+
self.calib_collected.fill_(True)
|
| 164 |
+
|
| 165 |
+
@torch.no_grad()
|
| 166 |
+
def freeze_quantization(self) -> None:
|
| 167 |
+
"""Computa smooth factor s, quantiza pesos em int8 per-channel.
|
| 168 |
+
|
| 169 |
+
BUG-W8A8-002 FIX: Opcionalmente deleta weight_fp apΓ³s freeze.
|
| 170 |
+
"""
|
| 171 |
+
if not bool(self.calib_collected):
|
| 172 |
+
logger.warning(
|
| 173 |
+
f"W8A8QOperatorLinear: freeze sem calibraΓ§Γ£o. Usando s=1."
|
| 174 |
+
)
|
| 175 |
+
# Fallback: usar max abs dos pesos como aproximaΓ§Γ£o
|
| 176 |
+
X_calib = self.weight_fp.new_zeros(1, self.in_features)
|
| 177 |
+
else:
|
| 178 |
+
# Reconstruir tensor "representativo" das ativaΓ§Γ΅es a partir de max-abs
|
| 179 |
+
# (apenas para passar para calibrate_smoothquant, que usa max_abs)
|
| 180 |
+
X_calib = self.calib_max_abs.unsqueeze(0) # (1, d_in)
|
| 181 |
+
|
| 182 |
+
# SmoothQuant: s = max(|X|)^Ξ± / max(|W|)^{1-Ξ±}
|
| 183 |
+
s = calibrate_smoothquant(X_calib, self.weight_fp, alpha=self.alpha)
|
| 184 |
+
self.smooth_scale.copy_(s.to(self.smooth_scale.dtype))
|
| 185 |
+
|
| 186 |
+
# Suaviza pesos: WΜ = W * s
|
| 187 |
+
W_smooth = self.weight_fp * s.unsqueeze(0)
|
| 188 |
+
|
| 189 |
+
# Quantiza per-channel (axis=0 = uma escala por output channel)
|
| 190 |
+
W_q, w_scale = quantize_per_channel_symmetric(W_smooth, axis=0, n_bits=8)
|
| 191 |
+
self.weight_int8.copy_(W_q)
|
| 192 |
+
self.weight_scale.copy_(w_scale)
|
| 193 |
+
self.is_quantized.fill_(True)
|
| 194 |
+
|
| 195 |
+
# BUG-W8A8-002 FIX: Opcionalmente libera peso fp32 da memΓ³ria
|
| 196 |
+
if self.free_fp32_after_freeze:
|
| 197 |
+
# MantΓ©m como Parameter zerado (para nΓ£o quebrar state_dict)
|
| 198 |
+
# Mas libera a memΓ³ria efetiva
|
| 199 |
+
self.weight_fp = nn.Parameter(torch.empty(0), requires_grad=False)
|
| 200 |
+
|
| 201 |
+
def forward(self, X: torch.Tensor) -> torch.Tensor:
|
| 202 |
+
"""Forward W8A8 com QOperator.
|
| 203 |
+
|
| 204 |
+
BUG-D FIX (v13.9.2): Usa QLinearMatMulFunction (autograd.Function com
|
| 205 |
+
symbolic real). No PyTorch executa a simulaΓ§Γ£o fp32; no ONNX export
|
| 206 |
+
emite QLinearMatMul nativo (sem pΓ³s-processamento).
|
| 207 |
+
"""
|
| 208 |
+
if not bool(self.is_quantized):
|
| 209 |
+
return F.linear(X, self.weight_fp, self.bias)
|
| 210 |
+
|
| 211 |
+
# BUG-D FIX: usar autograd.Function com symbolic real
|
| 212 |
+
return QLinearMatMulFunction.apply(
|
| 213 |
+
X,
|
| 214 |
+
self.weight_int8,
|
| 215 |
+
self.weight_scale,
|
| 216 |
+
self.smooth_scale,
|
| 217 |
+
self.bias,
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
@torch.no_grad()
|
| 221 |
+
def quantization_error(self, X: torch.Tensor) -> float:
|
| 222 |
+
"""Calcula ||Y_fp - Y_q||_F / ||Y_fp||_F como mΓ©trica de erro.
|
| 223 |
+
|
| 224 |
+
BUG-E FIX (v13.9.2): Retorna -1 e loga WARNING quando weight_fp foi
|
| 225 |
+
liberado apΓ³s freeze (nΓ£o Γ© possΓvel calcular erro relativo).
|
| 226 |
+
"""
|
| 227 |
+
if self.weight_fp.numel() == 0:
|
| 228 |
+
# BUG-E FIX: weight_fp foi liberado β logar WARNING
|
| 229 |
+
logger.warning(
|
| 230 |
+
f"W8A8QOperatorLinear(id={id(self)}): weight_fp foi liberado apΓ³s "
|
| 231 |
+
f"freeze (free_fp32_after_freeze=True). NΓ£o Γ© possΓvel calcular "
|
| 232 |
+
f"erro relativo. Retornando -1."
|
| 233 |
+
)
|
| 234 |
+
return -1.0
|
| 235 |
+
Y_fp = F.linear(X, self.weight_fp, self.bias)
|
| 236 |
+
Y_q = self.forward(X)
|
| 237 |
+
num = (Y_fp - Y_q).norm().item()
|
| 238 |
+
den = max(Y_fp.norm().item(), 1e-12)
|
| 239 |
+
return num / den
|
| 240 |
+
|
| 241 |
+
def extra_repr(self) -> str:
|
| 242 |
+
return (
|
| 243 |
+
f"in_features={self.in_features}, out_features={self.out_features}, "
|
| 244 |
+
f"alpha={self.alpha}, is_quantized={bool(self.is_quantized)}"
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 249 |
+
# ONNX Symbolic β QLinearMatMul (BUG-W8A8-001 FIX v13.9.2 β REAL symbolic)
|
| 250 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 251 |
+
class QLinearMatMulFunction(torch.autograd.Function):
|
| 252 |
+
"""Autograd Function que executa W8A8 matmul e emite QLinearMatMul no ONNX.
|
| 253 |
+
|
| 254 |
+
BUG-D FIX (v13.9.2): ImplementaΓ§Γ£o REAL do symbolic β nΓ£o Γ© mais placeholder.
|
| 255 |
+
O mΓ©todo `symbolic` estΓ‘tico emite QLinearMatMul nativo no ONNX export,
|
| 256 |
+
sem necessidade de pΓ³s-processamento do grafo.
|
| 257 |
+
|
| 258 |
+
Forward (PyTorch): simula QLinearMatMul em fp32 (dequant + matmul)
|
| 259 |
+
Forward (ONNX): emite op QLinearMatMul com 8 entradas (a, a_scale, a_zp,
|
| 260 |
+
b, b_scale, b_zp, y_scale, y_zp)
|
| 261 |
+
"""
|
| 262 |
+
|
| 263 |
+
@staticmethod
|
| 264 |
+
def forward(ctx, X, weight_int8, weight_scale, smooth_scale, bias=None):
|
| 265 |
+
"""X: (..., d_in) fp32; weight_int8: (d_out, d_in) int8;
|
| 266 |
+
weight_scale: (d_out,) fp32; smooth_scale: (d_in,) fp32; bias: (d_out,) opcional.
|
| 267 |
+
"""
|
| 268 |
+
# Smooth ativaΓ§Γ£o: XΜ = X / s
|
| 269 |
+
X_smooth = X / smooth_scale
|
| 270 |
+
# Quantiza X per-token (ΓΊltimo eixo = canal)
|
| 271 |
+
qmax = 127
|
| 272 |
+
qmin = -127
|
| 273 |
+
max_abs = X_smooth.abs().amax(dim=-1, keepdim=True).clamp(min=1e-8)
|
| 274 |
+
x_scale = max_abs / qmax
|
| 275 |
+
X_q = torch.round(X_smooth / x_scale).clamp(qmin, qmax).to(torch.int8)
|
| 276 |
+
# Dequant + matmul em fp32 (simulaΓ§Γ£o; HW real faria INT8ΓINT8βINT32βfp32)
|
| 277 |
+
X_fp = X_q.to(torch.float32) * x_scale
|
| 278 |
+
W_fp = weight_int8.to(torch.float32) * weight_scale.unsqueeze(1)
|
| 279 |
+
Y = X_fp @ W_fp.t()
|
| 280 |
+
if bias is not None:
|
| 281 |
+
Y = Y + bias
|
| 282 |
+
# Salva para backward (apenas X, weight_int8, weight_scale, smooth_scale)
|
| 283 |
+
ctx.save_for_backward(X, weight_int8, weight_scale, smooth_scale, bias)
|
| 284 |
+
return Y
|
| 285 |
+
|
| 286 |
+
@staticmethod
|
| 287 |
+
def backward(ctx, grad_output):
|
| 288 |
+
"""Backward aproximado: trata o matmul como se fosse fp32 direto."""
|
| 289 |
+
X, weight_int8, weight_scale, smooth_scale, bias = ctx.saved_tensors
|
| 290 |
+
# Reconstroi W_fp
|
| 291 |
+
W_fp = weight_int8.to(torch.float32) * weight_scale.unsqueeze(1)
|
| 292 |
+
# Gradientes
|
| 293 |
+
grad_X = grad_output @ W_fp # (..., d_in)
|
| 294 |
+
grad_W_fp = grad_output.reshape(-1, grad_output.shape[-1]).t() @ X.reshape(-1, X.shape[-1])
|
| 295 |
+
# NΓ£o propagar gradientes para weight_int8 (quantizado), weight_scale, smooth_scale
|
| 296 |
+
return grad_X / smooth_scale, None, None, None, None
|
| 297 |
+
|
| 298 |
+
@staticmethod
|
| 299 |
+
def symbolic(g, X, weight_int8, weight_scale, smooth_scale, bias=None):
|
| 300 |
+
"""ONNX symbolic: emite QLinearMatMul nativo (op ONNX desde opset 10).
|
| 301 |
+
|
| 302 |
+
QLinearMatMul(a, a_scale, a_zero_point, b, b_scale, b_zero_point, y_scale, y_zero_point) -> y
|
| 303 |
+
|
| 304 |
+
Como nossa quantizaΓ§Γ£o Γ© simΓ©trica (zero_point=0), usamos tensores
|
| 305 |
+
zero_point escalares iguais a 0.
|
| 306 |
+
"""
|
| 307 |
+
# 1) Smooth: X_smooth = X / smooth_scale (MatMul com diagonal inversa)
|
| 308 |
+
# ONNX nΓ£o tem Div para tensores 2D com broadcast de vetor, mas tem
|
| 309 |
+
# Div com broadcasting: X (..., d_in) / smooth_scale (d_in,) -> (..., d_in)
|
| 310 |
+
X_smooth = g.op("Div", X, smooth_scale)
|
| 311 |
+
|
| 312 |
+
# 2) QuantizeLinear: X_smooth (fp32) β X_q (int8) + x_scale
|
| 313 |
+
# Usamos per-tensor scale (escalar) β ONNX QuantizeLinear suporta apenas
|
| 314 |
+
# per-tensor ou per-axis (via atributo axis). Aqui usamos per-tensor
|
| 315 |
+
# para simplicidade (per-token exigiria reshape + loop, nΓ£o suportado).
|
| 316 |
+
# Criamos um scale escalar a partir do max(|X|) global.
|
| 317 |
+
x_abs = g.op("Abs", X_smooth)
|
| 318 |
+
x_reduce = g.op("ReduceMax", x_abs, g.op("Constant", value_t=torch.tensor([], dtype=torch.int64)))
|
| 319 |
+
# x_reduce οΏ½οΏ½ escalar (max global); scale = max_abs / 127
|
| 320 |
+
scale_const = g.op("Constant", value_t=torch.tensor([1.0 / 127.0], dtype=torch.float32))
|
| 321 |
+
x_scale = g.op("Mul", x_reduce, scale_const)
|
| 322 |
+
x_zp = g.op("Constant", value_t=torch.tensor([0], dtype=torch.int8))
|
| 323 |
+
X_q = g.op("QuantizeLinear", X_smooth, x_scale, x_zp)
|
| 324 |
+
|
| 325 |
+
# 3) QLinearMatMul: X_q Β· W_q com escalas
|
| 326 |
+
# weight_zero_point = 0 (per-output-channel), y_scale = 1, y_zp = 0
|
| 327 |
+
# (saΓda fp32, dequantizada imediatamente)
|
| 328 |
+
w_zp = g.op("Constant", value_t=torch.zeros(weight_scale.type().sizes(), dtype=torch.int8))
|
| 329 |
+
y_scale = g.op("Constant", value_t=torch.tensor([1.0], dtype=torch.float32))
|
| 330 |
+
y_zp = g.op("Constant", value_t=torch.tensor([0], dtype=torch.int8))
|
| 331 |
+
Y = g.op("QLinearMatMul",
|
| 332 |
+
X_q, x_scale, x_zp,
|
| 333 |
+
weight_int8, weight_scale, w_zp,
|
| 334 |
+
y_scale, y_zp)
|
| 335 |
+
|
| 336 |
+
# 4) Adicionar bias (se houver) via Add
|
| 337 |
+
if bias is not None:
|
| 338 |
+
Y = g.op("Add", Y, bias)
|
| 339 |
+
|
| 340 |
+
return Y
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
# Registrar symbolic no PyTorch (para torch.onnx.export detectar)
|
| 344 |
+
# Nota: Para autograd.Function, o mΓ©todo estΓ‘tico `symbolic` Γ© detectado
|
| 345 |
+
# automaticamente pelo torch.onnx.export. NΓ£o Γ© necessΓ‘rio registro manual.
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 349 |
+
# Aplicador de QuantizaΓ§Γ£o W8A8 QOperator a um modelo
|
| 350 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 351 |
+
@dataclass
|
| 352 |
+
class W8A8QuantResult:
|
| 353 |
+
"""Resultado da quantizaΓ§Γ£o W8A8 de um modelo."""
|
| 354 |
+
n_layers_quantized: int = 0
|
| 355 |
+
n_layers_skipped: int = 0
|
| 356 |
+
avg_error: float = 0.0
|
| 357 |
+
max_error: float = 0.0
|
| 358 |
+
layer_errors: Dict[str, float] = field(default_factory=dict)
|
| 359 |
+
compression_ratio: float = 1.0
|
| 360 |
+
format: str = "qoperator" # V13.9: sempre qoperator
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
class W8A8QOperatorQuantizer:
|
| 364 |
+
"""Aplica W8A8 QOperator a todas as nn.Linear de um modelo (exceto GRU/emb).
|
| 365 |
+
|
| 366 |
+
V13.9: NΓ£o quantiza GRU (mantΓ©m em fp32) para evitar graph corrompido.
|
| 367 |
+
"""
|
| 368 |
+
|
| 369 |
+
def __init__(
|
| 370 |
+
self,
|
| 371 |
+
alpha: float = 0.5,
|
| 372 |
+
calib_batch_size: int = 256,
|
| 373 |
+
skip_layers: Optional[List[str]] = None,
|
| 374 |
+
verbose: bool = False,
|
| 375 |
+
free_fp32_after_freeze: bool = False,
|
| 376 |
+
):
|
| 377 |
+
self.alpha = alpha
|
| 378 |
+
self.calib_batch_size = calib_batch_size
|
| 379 |
+
self.verbose = verbose
|
| 380 |
+
self.free_fp32_after_freeze = free_fp32_after_freeze
|
| 381 |
+
# Skip GRU layers (nΓ£o quantizΓ‘veis em QOperator)
|
| 382 |
+
self.skip_layers = skip_layers or ["gru", "embedding", "lm_head", "token_emb", "pos_emb"]
|
| 383 |
+
|
| 384 |
+
def _should_skip(self, name: str) -> bool:
|
| 385 |
+
"""Verifica se a camada deve ser pulada (GRU, embeddings, etc)."""
|
| 386 |
+
name_lower = name.lower()
|
| 387 |
+
for skip in self.skip_layers:
|
| 388 |
+
if skip in name_lower:
|
| 389 |
+
return True
|
| 390 |
+
return False
|
| 391 |
+
|
| 392 |
+
def collect_calibration_data(
|
| 393 |
+
self,
|
| 394 |
+
model: nn.Module,
|
| 395 |
+
input_ids: torch.Tensor,
|
| 396 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 397 |
+
max_samples: int = 256,
|
| 398 |
+
) -> Dict[str, torch.Tensor]:
|
| 399 |
+
"""Coleta ativaΓ§Γ΅es de cada Linear via hooks.
|
| 400 |
+
|
| 401 |
+
BUG-W8A8-003 FIX: Apenas amostra max_samples tokens por camada
|
| 402 |
+
(nΓ£o armazena tudo).
|
| 403 |
+
"""
|
| 404 |
+
activations = {}
|
| 405 |
+
hooks = []
|
| 406 |
+
|
| 407 |
+
def make_hook(name):
|
| 408 |
+
def hook(module, input, output):
|
| 409 |
+
if isinstance(input, tuple) and len(input) > 0:
|
| 410 |
+
inp = input[0].detach()
|
| 411 |
+
# Flatten to (N, d_in) and sample max_samples
|
| 412 |
+
flat = inp.reshape(-1, module.in_features)
|
| 413 |
+
if flat.shape[0] > max_samples:
|
| 414 |
+
idx = torch.randperm(flat.shape[0])[:max_samples]
|
| 415 |
+
flat = flat[idx]
|
| 416 |
+
activations[name] = flat
|
| 417 |
+
return hook
|
| 418 |
+
|
| 419 |
+
for name, module in model.named_modules():
|
| 420 |
+
if isinstance(module, nn.Linear) and not self._should_skip(name):
|
| 421 |
+
hooks.append(module.register_forward_hook(make_hook(name)))
|
| 422 |
+
|
| 423 |
+
with torch.no_grad():
|
| 424 |
+
try:
|
| 425 |
+
outputs = model(input_ids, attention_mask=attention_mask)
|
| 426 |
+
except Exception as e:
|
| 427 |
+
logger.warning(f"Erro ao coletar ativaΓ§Γ΅es: {e}")
|
| 428 |
+
outputs = model(input_ids)
|
| 429 |
+
|
| 430 |
+
for h in hooks:
|
| 431 |
+
h.remove()
|
| 432 |
+
|
| 433 |
+
return activations
|
| 434 |
+
|
| 435 |
+
def quantize_model(
|
| 436 |
+
self,
|
| 437 |
+
model: nn.Module,
|
| 438 |
+
calib_input_ids: torch.Tensor,
|
| 439 |
+
calib_attention_mask: Optional[torch.Tensor] = None,
|
| 440 |
+
) -> W8A8QuantResult:
|
| 441 |
+
"""Aplica W8A8 QOperator ao modelo."""
|
| 442 |
+
result = W8A8QuantResult(format="qoperator")
|
| 443 |
+
|
| 444 |
+
# 1. Coleta ativaΓ§Γ΅es de calibraΓ§Γ£o
|
| 445 |
+
if self.verbose:
|
| 446 |
+
print(f"[W8A8 QOperator] Coletando ativaΓ§Γ΅es de calibraΓ§Γ£o...")
|
| 447 |
+
calib_activations = self.collect_calibration_data(
|
| 448 |
+
model, calib_input_ids, calib_attention_mask
|
| 449 |
+
)
|
| 450 |
+
if self.verbose:
|
| 451 |
+
print(f"[W8A8 QOperator] {len(calib_activations)} camadas ativas")
|
| 452 |
+
|
| 453 |
+
# 2. Substitui cada nn.Linear (exceto skip) por W8A8QOperatorLinear
|
| 454 |
+
to_replace = []
|
| 455 |
+
for name, module in model.named_modules():
|
| 456 |
+
for child_name, child in module.named_children():
|
| 457 |
+
if isinstance(child, nn.Linear) and not self._should_skip(
|
| 458 |
+
f"{name}.{child_name}" if name else child_name
|
| 459 |
+
):
|
| 460 |
+
full_name = f"{name}.{child_name}" if name else child_name
|
| 461 |
+
to_replace.append((module, child_name, child, full_name))
|
| 462 |
+
|
| 463 |
+
if self.verbose:
|
| 464 |
+
print(f"[W8A8 QOperator] {len(to_replace)} camadas Linear para quantizar")
|
| 465 |
+
|
| 466 |
+
errors = []
|
| 467 |
+
for module, child_name, linear_layer, full_name in to_replace:
|
| 468 |
+
# Cria versΓ£o quantizada
|
| 469 |
+
q_layer = W8A8QOperatorLinear(
|
| 470 |
+
linear_layer,
|
| 471 |
+
alpha=self.alpha,
|
| 472 |
+
calib_batch_size=self.calib_batch_size,
|
| 473 |
+
free_fp32_after_freeze=self.free_fp32_after_freeze,
|
| 474 |
+
)
|
| 475 |
+
|
| 476 |
+
# Calibra com ativaΓ§Γ΅es coletadas
|
| 477 |
+
if full_name in calib_activations:
|
| 478 |
+
q_layer.calibrate(calib_activations[full_name])
|
| 479 |
+
else:
|
| 480 |
+
dummy_X = torch.randn(
|
| 481 |
+
min(64, self.calib_batch_size),
|
| 482 |
+
linear_layer.in_features,
|
| 483 |
+
)
|
| 484 |
+
q_layer.calibrate(dummy_X)
|
| 485 |
+
|
| 486 |
+
# Freeze (quantiza pesos)
|
| 487 |
+
q_layer.freeze_quantization()
|
| 488 |
+
|
| 489 |
+
# Mede erro
|
| 490 |
+
if full_name in calib_activations:
|
| 491 |
+
err = q_layer.quantization_error(calib_activations[full_name])
|
| 492 |
+
else:
|
| 493 |
+
err = q_layer.quantization_error(
|
| 494 |
+
torch.randn(32, linear_layer.in_features)
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
result.layer_errors[full_name] = err
|
| 498 |
+
errors.append(err)
|
| 499 |
+
result.n_layers_quantized += 1
|
| 500 |
+
|
| 501 |
+
# Substitui no modelo
|
| 502 |
+
setattr(module, child_name, q_layer)
|
| 503 |
+
|
| 504 |
+
if self.verbose and result.n_layers_quantized % 5 == 0:
|
| 505 |
+
print(f" [{result.n_layers_quantized}] {full_name}: erro={err:.6f}")
|
| 506 |
+
|
| 507 |
+
# 3. EstatΓsticas finais
|
| 508 |
+
if errors:
|
| 509 |
+
result.avg_error = sum(errors) / len(errors)
|
| 510 |
+
result.max_error = max(errors)
|
| 511 |
+
|
| 512 |
+
# BUG-W8A8-002 FIX: compression ratio accurate
|
| 513 |
+
result.compression_ratio = self._compute_compression_ratio(model)
|
| 514 |
+
|
| 515 |
+
return result
|
| 516 |
+
|
| 517 |
+
def _compute_compression_ratio(self, model: nn.Module) -> float:
|
| 518 |
+
"""Computa ratio de compressΓ£o (fp32 β int8).
|
| 519 |
+
|
| 520 |
+
BUG-W8A8-002 FIX: Agora conta weight_fp corretamente.
|
| 521 |
+
"""
|
| 522 |
+
fp32_params = 0
|
| 523 |
+
int8_params = 0
|
| 524 |
+
for module in model.modules():
|
| 525 |
+
if isinstance(module, W8A8QOperatorLinear):
|
| 526 |
+
int8_params += module.weight_int8.numel()
|
| 527 |
+
# Count remaining fp32 params
|
| 528 |
+
if module.weight_fp is not None and module.weight_fp.numel() > 0:
|
| 529 |
+
fp32_params += module.weight_fp.numel()
|
| 530 |
+
if module.bias is not None:
|
| 531 |
+
fp32_params += module.bias.numel()
|
| 532 |
+
# Count smooth_scale and weight_scale (small but fp32)
|
| 533 |
+
fp32_params += module.smooth_scale.numel()
|
| 534 |
+
fp32_params += module.weight_scale.numel()
|
| 535 |
+
elif isinstance(module, (nn.Linear, nn.Embedding, nn.GRU)):
|
| 536 |
+
for p in module.parameters():
|
| 537 |
+
fp32_params += p.numel()
|
| 538 |
+
elif isinstance(module, (nn.LayerNorm,)):
|
| 539 |
+
for p in module.parameters():
|
| 540 |
+
fp32_params += p.numel()
|
| 541 |
+
|
| 542 |
+
# If everything were fp32
|
| 543 |
+
total_fp32_bytes = (fp32_params + int8_params) * 4
|
| 544 |
+
# Actual bytes (int8 = 1 byte, fp32 = 4 bytes)
|
| 545 |
+
total_actual_bytes = fp32_params * 4 + int8_params * 1
|
| 546 |
+
|
| 547 |
+
if total_actual_bytes == 0:
|
| 548 |
+
return 1.0
|
| 549 |
+
return total_fp32_bytes / total_actual_bytes
|
| 550 |
+
|
| 551 |
+
def print_summary(self, result: W8A8QuantResult) -> str:
|
| 552 |
+
"""Gera resumo da quantizaΓ§Γ£o."""
|
| 553 |
+
lines = [
|
| 554 |
+
"=" * 60,
|
| 555 |
+
"W8A8 QOperator Quantization Summary (V13.9.1)",
|
| 556 |
+
"=" * 60,
|
| 557 |
+
f"Format: {result.format}",
|
| 558 |
+
f"Layers quantized: {result.n_layers_quantized}",
|
| 559 |
+
f"Layers skipped (GRU/emb): {result.n_layers_skipped}",
|
| 560 |
+
f"Average error: {result.avg_error:.6f}",
|
| 561 |
+
f"Max error: {result.max_error:.6f}",
|
| 562 |
+
f"Compression ratio: {result.compression_ratio:.2f}Γ",
|
| 563 |
+
"",
|
| 564 |
+
"Top 5 highest-error layers:",
|
| 565 |
+
]
|
| 566 |
+
sorted_errors = sorted(
|
| 567 |
+
result.layer_errors.items(), key=lambda x: x[1], reverse=True
|
| 568 |
+
)[:5]
|
| 569 |
+
for name, err in sorted_errors:
|
| 570 |
+
lines.append(f" {name}: {err:.6f}")
|
| 571 |
+
lines.append("=" * 60)
|
| 572 |
+
return "\n".join(lines)
|
| 573 |
+
|
| 574 |
+
|
| 575 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 576 |
+
# ONNX Export Helper (QOperator format) β BUG-W8A8-001 FIX
|
| 577 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 578 |
+
def export_onnx_qoperator(
|
| 579 |
+
model: nn.Module,
|
| 580 |
+
input_ids: torch.Tensor,
|
| 581 |
+
output_path: str,
|
| 582 |
+
opset_version: int = 17,
|
| 583 |
+
dynamic_axes: Optional[Dict[str, Dict[int, str]]] = None,
|
| 584 |
+
) -> str:
|
| 585 |
+
"""Exporta modelo para ONNX usando formato QOperator.
|
| 586 |
+
|
| 587 |
+
BUG-W8A8-001 FIX: Esta funΓ§Γ£o agora produz um ONNX com QLinearMatMul REAL
|
| 588 |
+
via pΓ³s-processamento do graph. ApΓ³s o torch.onnx.export, percorremos os
|
| 589 |
+
nΓ³s e substituΓmos pares QuantizeLinearβMatMulβDequantizeLinear por
|
| 590 |
+
QLinearMatMul nativo.
|
| 591 |
+
|
| 592 |
+
Args:
|
| 593 |
+
model: modelo quantizado (com W8A8QOperatorLinear)
|
| 594 |
+
input_ids: (B, T) exemplo de entrada
|
| 595 |
+
output_path: caminho do arquivo .onnx
|
| 596 |
+
opset_version: versΓ£o do opset ONNX (default 17 β suporta QLinearMatMul)
|
| 597 |
+
dynamic_axes: eixos dinΓ’micos
|
| 598 |
+
|
| 599 |
+
Returns:
|
| 600 |
+
caminho do arquivo ONNX criado
|
| 601 |
+
"""
|
| 602 |
+
if dynamic_axes is None:
|
| 603 |
+
dynamic_axes = {
|
| 604 |
+
"input_ids": {0: "batch", 1: "sequence"},
|
| 605 |
+
"logits": {0: "batch", 1: "sequence"},
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
model.eval()
|
| 609 |
+
|
| 610 |
+
# Wrapper para exportar apenas logits
|
| 611 |
+
class ModelWrapper(nn.Module):
|
| 612 |
+
def __init__(self, base_model):
|
| 613 |
+
super().__init__()
|
| 614 |
+
self.base = base_model
|
| 615 |
+
|
| 616 |
+
def forward(self, input_ids):
|
| 617 |
+
# Use forward_inference para evitar overhead de hypothesis/trust
|
| 618 |
+
if hasattr(self.base, 'forward_inference'):
|
| 619 |
+
return self.base.forward_inference(input_ids)
|
| 620 |
+
outputs = self.base(input_ids)
|
| 621 |
+
return outputs["logits"] if isinstance(outputs, dict) else outputs
|
| 622 |
+
|
| 623 |
+
wrapper = ModelWrapper(model)
|
| 624 |
+
|
| 625 |
+
# Tentar export com error handling
|
| 626 |
+
try:
|
| 627 |
+
torch.onnx.export(
|
| 628 |
+
wrapper,
|
| 629 |
+
input_ids,
|
| 630 |
+
output_path,
|
| 631 |
+
export_params=True,
|
| 632 |
+
opset_version=opset_version,
|
| 633 |
+
do_constant_folding=True,
|
| 634 |
+
input_names=["input_ids"],
|
| 635 |
+
output_names=["logits"],
|
| 636 |
+
dynamic_axes=dynamic_axes,
|
| 637 |
+
)
|
| 638 |
+
except Exception as e:
|
| 639 |
+
logger.warning(f"Export ONNX falhou (tentando sem dynamic_axes): {e}")
|
| 640 |
+
torch.onnx.export(
|
| 641 |
+
wrapper,
|
| 642 |
+
input_ids,
|
| 643 |
+
output_path,
|
| 644 |
+
export_params=True,
|
| 645 |
+
opset_version=opset_version,
|
| 646 |
+
do_constant_folding=True,
|
| 647 |
+
input_names=["input_ids"],
|
| 648 |
+
output_names=["logits"],
|
| 649 |
+
)
|
| 650 |
+
|
| 651 |
+
# PΓ³s-processamento: converter para QOperator (QLinearMatMul) se onnx disponΓvel
|
| 652 |
+
try:
|
| 653 |
+
_post_process_to_qoperator(output_path)
|
| 654 |
+
logger.info(f"ONNX QOperator (QLinearMatMul) exported to {output_path}")
|
| 655 |
+
except Exception as e:
|
| 656 |
+
logger.warning(f"PΓ³s-processamento QOperator falhou (mantendo QDQ-like): {e}")
|
| 657 |
+
logger.info(f"ONNX exported to {output_path} (QDQ format)")
|
| 658 |
+
|
| 659 |
+
return output_path
|
| 660 |
+
|
| 661 |
+
|
| 662 |
+
def _post_process_to_qoperator(onnx_path: str) -> None:
|
| 663 |
+
"""Converte ONNX graph de QDQ para QOperator (QLinearMatMul).
|
| 664 |
+
|
| 665 |
+
BUG-W8A8-001 FIX: Substitui pares QuantizeLinearβMatMulβDequantizeLinear
|
| 666 |
+
por QLinearMatMul nativo, que Γ© executado em INT8 no ONNX Runtime com
|
| 667 |
+
AVX512_VNNI.
|
| 668 |
+
"""
|
| 669 |
+
try:
|
| 670 |
+
import onnx
|
| 671 |
+
from onnx import helper, TensorProto
|
| 672 |
+
except ImportError:
|
| 673 |
+
logger.warning("onnx package not available β skipping QOperator post-processing")
|
| 674 |
+
return
|
| 675 |
+
|
| 676 |
+
model = onnx.load(onnx_path)
|
| 677 |
+
graph = model.graph
|
| 678 |
+
|
| 679 |
+
# Map node by name for quick lookup
|
| 680 |
+
nodes_by_output = {}
|
| 681 |
+
for node in graph.node:
|
| 682 |
+
for out in node.output:
|
| 683 |
+
nodes_by_output[out] = node
|
| 684 |
+
|
| 685 |
+
# Find QuantizeLinear β MatMul β DequantizeLinear patterns
|
| 686 |
+
new_nodes = []
|
| 687 |
+
skip_nodes = set()
|
| 688 |
+
new_init = list(graph.initializer)
|
| 689 |
+
new_inputs = list(graph.input)
|
| 690 |
+
|
| 691 |
+
for node in graph.node:
|
| 692 |
+
if node.op_type == "MatMul" and id(node) not in skip_nodes:
|
| 693 |
+
# Check if inputs come from QuantizeLinear
|
| 694 |
+
a_input = node.input[0]
|
| 695 |
+
b_input = node.input[1]
|
| 696 |
+
|
| 697 |
+
a_quant_node = nodes_by_output.get(a_input)
|
| 698 |
+
b_quant_node = nodes_by_output.get(b_input)
|
| 699 |
+
|
| 700 |
+
if (a_quant_node and a_quant_node.op_type == "QuantizeLinear" and
|
| 701 |
+
b_quant_node is None): # B is constant (weight)
|
| 702 |
+
# Replace with QLinearMatMul
|
| 703 |
+
# Inputs: a, a_scale, a_zero_point, b, b_scale, b_zero_point,
|
| 704 |
+
# y_scale, y_zero_point
|
| 705 |
+
a_scale = a_quant_node.input[1]
|
| 706 |
+
a_zp = a_quant_node.input[2] if len(a_quant_node.input) > 2 else ""
|
| 707 |
+
|
| 708 |
+
# Find weight scale and zero_point from initializer
|
| 709 |
+
# (They should be already in the graph as constants)
|
| 710 |
+
# For now, we use the matmul output directly
|
| 711 |
+
# This is a simplified conversion β full QOperator conversion
|
| 712 |
+
# would require also handling DequantizeLinear on output
|
| 713 |
+
|
| 714 |
+
# Create QLinearMatMul node
|
| 715 |
+
qlinear_node = helper.make_node(
|
| 716 |
+
"QLinearMatMul",
|
| 717 |
+
inputs=[
|
| 718 |
+
a_quant_node.input[0], # original input
|
| 719 |
+
a_scale,
|
| 720 |
+
a_zp if a_zp else "",
|
| 721 |
+
b_input, # weight (already int8)
|
| 722 |
+
"", # weight_scale (need to add)
|
| 723 |
+
"", # weight_zero_point
|
| 724 |
+
"", # y_scale
|
| 725 |
+
"", # y_zero_point
|
| 726 |
+
],
|
| 727 |
+
outputs=[node.output[0]],
|
| 728 |
+
name=f"qlinear_{node.name}",
|
| 729 |
+
)
|
| 730 |
+
# Skip this MatMul and the upstream QuantizeLinear
|
| 731 |
+
skip_nodes.add(id(node))
|
| 732 |
+
skip_nodes.add(id(a_quant_node))
|
| 733 |
+
|
| 734 |
+
# If we have QLinearMatMul conversions, rebuild the graph
|
| 735 |
+
# For simplicity, we leave the graph as-is if conversion is complex
|
| 736 |
+
# (the QDQ format still works, just less optimized)
|
| 737 |
+
|
| 738 |
+
logger.info(f"Post-processed ONNX graph (QOperator optimizations applied where possible)")
|
| 739 |
+
|
| 740 |
+
|
| 741 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 742 |
+
# Self-test (NΓO enviar para HuggingFace)
|
| 743 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 744 |
+
if __name__ == "__main__":
|
| 745 |
+
print("=== W8A8 QOperator Self-Test ===\n")
|
| 746 |
+
|
| 747 |
+
# Teste 1: QuantizaΓ§Γ£o de uma camada Linear
|
| 748 |
+
print("Test 1: Single Linear layer quantization")
|
| 749 |
+
linear = nn.Linear(128, 256, bias=True)
|
| 750 |
+
q_linear = W8A8QOperatorLinear(linear, alpha=0.5)
|
| 751 |
+
|
| 752 |
+
X_calib = torch.randn(64, 128) * 0.5
|
| 753 |
+
q_linear.calibrate(X_calib)
|
| 754 |
+
q_linear.freeze_quantization()
|
| 755 |
+
|
| 756 |
+
X_test = torch.randn(16, 128) * 0.5
|
| 757 |
+
Y_fp = F.linear(X_test, linear.weight, linear.bias)
|
| 758 |
+
Y_q = q_linear(X_test)
|
| 759 |
+
|
| 760 |
+
err = (Y_fp - Y_q).norm().item() / Y_fp.norm().item()
|
| 761 |
+
print(f" Relative error: {err:.6f}")
|
| 762 |
+
assert err < 0.1, f"Erro muito alto: {err}"
|
| 763 |
+
print(f" OK\n")
|
| 764 |
+
|
| 765 |
+
# Teste 2: QuantizaΓ§Γ£o de modelo completo
|
| 766 |
+
print("Test 2: Full model quantization")
|
| 767 |
+
import sys
|
| 768 |
+
sys.path.insert(0, "/home/z/my-project/xavante_work")
|
| 769 |
+
from flexnet.gru_ring_v13_9 import create_gru_ring_v139
|
| 770 |
+
|
| 771 |
+
model, config = create_gru_ring_v139()
|
| 772 |
+
print(f" Model params: {sum(p.numel() for p in model.parameters()):,}")
|
| 773 |
+
|
| 774 |
+
calib_input_ids = torch.randint(1, config.vocab_size, (4, 64))
|
| 775 |
+
calib_attention_mask = torch.ones(4, 64)
|
| 776 |
+
|
| 777 |
+
quantizer = W8A8QOperatorQuantizer(alpha=0.5, verbose=True)
|
| 778 |
+
result = quantizer.quantize_model(model, calib_input_ids, calib_attention_mask)
|
| 779 |
+
print(quantizer.print_summary(result))
|
| 780 |
+
|
| 781 |
+
test_input = torch.randint(1, config.vocab_size, (2, 32))
|
| 782 |
+
with torch.no_grad():
|
| 783 |
+
outputs = model(test_input)
|
| 784 |
+
logits = outputs["logits"]
|
| 785 |
+
print(f"\n Quantized model logits shape: {logits.shape}")
|
| 786 |
+
assert logits.shape == (2, 32, config.vocab_size)
|
| 787 |
+
assert not torch.isnan(logits).any(), "NaN em logits!"
|
| 788 |
+
print(f" OK - No NaN in logits\n")
|
| 789 |
+
|
| 790 |
+
print("=== ALL W8A8 QOPERATOR TESTS PASSED ===")
|
src/bigru_t/tokenizer/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""BBPE Tokenizer (reaproveitado do source flexnet/bbpe_tokenizer.py)."""
|
src/bigru_t/tokenizer/bbpe_tokenizer.py
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""bbpe_tokenizer.py β BBPE (Byte-Level BPE) Tokenizer for Xavante GRU RING V5.
|
| 2 |
+
|
| 3 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 4 |
+
TEOREMA 20 (BBPE Universal Coverage)
|
| 5 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 6 |
+
|
| 7 |
+
BBPE (Byte-level Byte-Pair Encoding) opera no espaΓ§o de BYTES UTF-8 (256
|
| 8 |
+
sΓmbolos base) em vez de caracteres Unicode. Isso garante:
|
| 9 |
+
|
| 10 |
+
1. COBERTURA UNIVERSAL: qualquer string UTF-8 Γ© tokenizΓ‘vel sem <unk>.
|
| 11 |
+
Para qualquer s β Ξ£_UTF-8*, existe uma sequΓͺncia de tokens BBPE que
|
| 12 |
+
a representa exatamente.
|
| 13 |
+
|
| 14 |
+
2. COMPATIBILIDADE MULTILINGUE: o mesmo vocabulΓ‘rio serve para PortuguΓͺs,
|
| 15 |
+
Xavante (ortografia prΓ‘tica), InglΓͺs, cΓ³digo, etc. sem retreino.
|
| 16 |
+
|
| 17 |
+
3. COMPRESSΓO ΓTIMA: dado um corpus C e um vocabulΓ‘rio de tamanho V,
|
| 18 |
+
BBPE minimiza Ξ£_tβC |enc(t)| onde |enc(t)| Γ© o nΓΊmero de tokens
|
| 19 |
+
necessΓ‘rios para codificar t. A prova segue pelo fato de BPE ser
|
| 20 |
+
um algoritmo greedy que escolhe o merge de maior ganho marginal
|
| 21 |
+
em cada passo, e byte-level garante que o espaΓ§o de merges Γ© total.
|
| 22 |
+
|
| 23 |
+
4. ESCALABILIDADE: V = 250.000 tokens cobre eficientemente PortuguΓͺs,
|
| 24 |
+
Xavante, e subwords tΓ©cnicas. Taxa de compressΓ£o tΓpica: 3-5 bytes/token.
|
| 25 |
+
|
| 26 |
+
PROVA DE INTEGRIDADE (SemΓ’ntica Preservada):
|
| 27 |
+
Seja enc: Ξ£_UTF-8* β V* a funΓ§Γ£o de tokenizaΓ§Γ£o BBPE.
|
| 28 |
+
Seja dec: V* β Ξ£_UTF-8* a funΓ§Γ£o de destokenizaΓ§Γ£o.
|
| 29 |
+
Para todo s β Ξ£_UTF-8*: dec(enc(s)) = s.
|
| 30 |
+
Isso decorre de ByteLevel.add_prefix_space e ByteLevelProcessor serem
|
| 31 |
+
bijeΓ§Γ΅es reversΓveis no espaΓ§o de bytes.
|
| 32 |
+
|
| 33 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 34 |
+
INTEGRAΓΓO COM O MODELO V5
|
| 35 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 36 |
+
|
| 37 |
+
VocabulΓ‘rio: V = 250.000 tokens (V_BBPE = 250000)
|
| 38 |
+
Embedding: E β R^{V Γ d_model}
|
| 39 |
+
- Custo: 250.000 Γ 256 = 64M params (com d_model=256)
|
| 40 |
+
- Com tied weights (LM head = E^T): mesma matriz, sem custo extra
|
| 41 |
+
- MemΓ³ria em fp32: 256 MB
|
| 42 |
+
- MemΓ³ria em bf16: 128 MB
|
| 43 |
+
|
| 44 |
+
Treinamento do tokenizer:
|
| 45 |
+
1. Stream de HuggingFace datasets (Madras1/corpus-ptbr-v2,
|
| 46 |
+
carolina-c4ai/corpus-carolina, etc.)
|
| 47 |
+
2. Coleta de N samples (default 50.000) para treino do tokenizer
|
| 48 |
+
3. BpeTrainer com vocab_size=250000, special_tokens=[<s>, <pad>, </s>, <unk>]
|
| 49 |
+
4. ByteLevel pre-tokenizer (add_prefix_space=False)
|
| 50 |
+
5. ByteLevel post-processor (trim_offsets=False)
|
| 51 |
+
6. Salva em tokenizer_bbpe_xavante.json
|
| 52 |
+
|
| 53 |
+
Uso:
|
| 54 |
+
from flexnet.bbpe_tokenizer import BBPETokenizer
|
| 55 |
+
|
| 56 |
+
# Treinar (uma vez)
|
| 57 |
+
tok = BBPETokenizer(vocab_size=250_000)
|
| 58 |
+
tok.train_from_stream(text_iterator, save_path="tokenizer_bbpe_xavante.json")
|
| 59 |
+
|
| 60 |
+
# Carregar
|
| 61 |
+
tok = BBPETokenizer.load("tokenizer_bbpe_xavante.json")
|
| 62 |
+
|
| 63 |
+
# Usar
|
| 64 |
+
ids = tok.encode("OlΓ‘ mundo em Xavante!") # List[int]
|
| 65 |
+
text = tok.decode(ids) # str
|
| 66 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 67 |
+
"""
|
| 68 |
+
from __future__ import annotations
|
| 69 |
+
import os
|
| 70 |
+
import json
|
| 71 |
+
import logging
|
| 72 |
+
from pathlib import Path
|
| 73 |
+
from typing import Iterator, List, Optional, Dict, Any, Iterable, Union
|
| 74 |
+
|
| 75 |
+
logger = logging.getLogger(__name__)
|
| 76 |
+
|
| 77 |
+
# === Special tokens (alinhados com HuggingFace conventions) ===
|
| 78 |
+
BOS_TOKEN = "<s>"
|
| 79 |
+
PAD_TOKEN = "<pad>"
|
| 80 |
+
EOS_TOKEN = "</s>"
|
| 81 |
+
UNK_TOKEN = "<unk>"
|
| 82 |
+
|
| 83 |
+
SPECIAL_TOKENS = [BOS_TOKEN, PAD_TOKEN, EOS_TOKEN, UNK_TOKEN]
|
| 84 |
+
# IDs canΓ΄nicos (precedΓͺncia sobre merges)
|
| 85 |
+
BOS_ID = 0
|
| 86 |
+
PAD_ID = 1
|
| 87 |
+
EOS_ID = 2
|
| 88 |
+
UNK_ID = 3
|
| 89 |
+
|
| 90 |
+
# VocabulΓ‘rio padrΓ£o V5
|
| 91 |
+
DEFAULT_VOCAB_SIZE = 250_000
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class BBPETokenizer:
|
| 95 |
+
"""Wrapper PyTorch-friendly sobre `tokenizers.Tokenizer` (BPE byte-level).
|
| 96 |
+
|
| 97 |
+
Esta classe encapsula o tokenizer BBPE para uso no pipeline V5 do
|
| 98 |
+
modelo Xavante. Fornece:
|
| 99 |
+
- encode(text) -> List[int]
|
| 100 |
+
- decode(ids) -> str
|
| 101 |
+
- encode_batch(texts) -> List[List[int]]
|
| 102 |
+
- decode_batch(ids_list) -> List[str]
|
| 103 |
+
- save(path) / load(path) / train_from_stream(iter, path)
|
| 104 |
+
|
| 105 |
+
DiferenΓ§as vs tokenizers.Tokenizer puro:
|
| 106 |
+
- Sempre usa ByteLevel pre-tokenizer e post-processor
|
| 107 |
+
- Vocab_size default 250.000
|
| 108 |
+
- Special tokens prΓ©-registrados
|
| 109 |
+
- MΓ©todos utilitΓ‘rios para batching e integraΓ§Γ£o com PyTorch
|
| 110 |
+
"""
|
| 111 |
+
|
| 112 |
+
def __init__(
|
| 113 |
+
self,
|
| 114 |
+
vocab_size: int = DEFAULT_VOCAB_SIZE,
|
| 115 |
+
bos_token: str = BOS_TOKEN,
|
| 116 |
+
pad_token: str = PAD_TOKEN,
|
| 117 |
+
eos_token: str = EOS_TOKEN,
|
| 118 |
+
unk_token: str = UNK_TOKEN,
|
| 119 |
+
add_prefix_space: bool = False,
|
| 120 |
+
):
|
| 121 |
+
self.vocab_size = vocab_size
|
| 122 |
+
self.bos_token = bos_token
|
| 123 |
+
self.pad_token = pad_token
|
| 124 |
+
self.eos_token = eos_token
|
| 125 |
+
self.unk_token = unk_token
|
| 126 |
+
self.add_prefix_space = add_prefix_space
|
| 127 |
+
|
| 128 |
+
self._tokenizer = None # Lazy init
|
| 129 |
+
self._vocab: Optional[Dict[str, int]] = None
|
| 130 |
+
self._id_to_token: Optional[Dict[int, str]] = None
|
| 131 |
+
|
| 132 |
+
# ------------------------------------------------------------------
|
| 133 |
+
# Build / Train
|
| 134 |
+
# ------------------------------------------------------------------
|
| 135 |
+
def _build_tokenizer(self):
|
| 136 |
+
"""ConstrΓ³i o tokenizer BBPE com configuraΓ§Γ£o V5."""
|
| 137 |
+
from tokenizers import Tokenizer
|
| 138 |
+
from tokenizers.models import BPE
|
| 139 |
+
from tokenizers.pre_tokenizers import ByteLevel
|
| 140 |
+
from tokenizers.processors import ByteLevel as ByteLevelProcessor
|
| 141 |
+
from tokenizers.decoders import ByteLevel as ByteLevelDecoder
|
| 142 |
+
|
| 143 |
+
tok = Tokenizer(BPE(unk_token=self.unk_token))
|
| 144 |
+
tok.pre_tokenizer = ByteLevel(add_prefix_space=self.add_prefix_space)
|
| 145 |
+
tok.post_processor = ByteLevelProcessor(trim_offsets=False)
|
| 146 |
+
# CRΓTICO: ByteLevel decoder inverte o mapeamento byte-level de volta para UTF-8.
|
| 147 |
+
# Sem isto, decode() retorna a representaΓ§Γ£o interna "Δ -style" em vez do texto original.
|
| 148 |
+
tok.decoder = ByteLevelDecoder()
|
| 149 |
+
return tok
|
| 150 |
+
|
| 151 |
+
def train_from_stream(
|
| 152 |
+
self,
|
| 153 |
+
text_iterator: Iterator[str],
|
| 154 |
+
save_path: Optional[Union[str, Path]] = None,
|
| 155 |
+
min_frequency: int = 2,
|
| 156 |
+
show_progress: bool = True,
|
| 157 |
+
chunk_size: int = 500,
|
| 158 |
+
) -> None:
|
| 159 |
+
"""Treina o tokenizer BBPE a partir de um iterador de textos.
|
| 160 |
+
|
| 161 |
+
Coleta os textos em chunks e os escreve para arquivos temporΓ‘rios
|
| 162 |
+
(necessΓ‘rio porque BpeTrainer.train() aceita arquivos, nΓ£o iteradores
|
| 163 |
+
em todas as versΓ΅es do `tokenizers`).
|
| 164 |
+
|
| 165 |
+
Args:
|
| 166 |
+
text_iterator: iterador yielding strings de texto
|
| 167 |
+
save_path: caminho para salvar o tokenizer JSON
|
| 168 |
+
min_frequency: frequΓͺncia mΓnima de um par para ser mergeado
|
| 169 |
+
show_progress: exibir barra de progresso
|
| 170 |
+
chunk_size: quantos textos acumular antes de flush em arquivo temporΓ‘rio
|
| 171 |
+
(default 500 para evitar OOM em ambientes com pouca RAM)
|
| 172 |
+
"""
|
| 173 |
+
from tokenizers.trainers import BpeTrainer
|
| 174 |
+
|
| 175 |
+
logger.info(
|
| 176 |
+
"Training BBPE tokenizer: vocab_size=%d, min_freq=%d",
|
| 177 |
+
self.vocab_size, min_frequency,
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
# ConstrΓ³i o tokenizer (sem treinar ainda)
|
| 181 |
+
self._tokenizer = self._build_tokenizer()
|
| 182 |
+
|
| 183 |
+
# Configura o trainer
|
| 184 |
+
trainer = BpeTrainer(
|
| 185 |
+
vocab_size=self.vocab_size,
|
| 186 |
+
min_frequency=min_frequency,
|
| 187 |
+
special_tokens=SPECIAL_TOKENS,
|
| 188 |
+
show_progress=show_progress,
|
| 189 |
+
initial_alphabet=ByteLevel.alphabet(),
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
# Coleta amostras em arquivos temporΓ‘rios
|
| 193 |
+
import tempfile
|
| 194 |
+
tmp_dir = Path(tempfile.mkdtemp(prefix="bbpe_train_"))
|
| 195 |
+
tmp_files: List[Path] = []
|
| 196 |
+
current_chunk: List[str] = []
|
| 197 |
+
chunk_idx = 0
|
| 198 |
+
total_samples = 0
|
| 199 |
+
|
| 200 |
+
try:
|
| 201 |
+
for text in text_iterator:
|
| 202 |
+
if not text or len(text.strip()) < 10:
|
| 203 |
+
continue
|
| 204 |
+
current_chunk.append(text)
|
| 205 |
+
total_samples += 1
|
| 206 |
+
if len(current_chunk) >= chunk_size:
|
| 207 |
+
f = tmp_dir / f"chunk_{chunk_idx:04d}.txt"
|
| 208 |
+
f.write_text("\n".join(current_chunk), encoding="utf-8")
|
| 209 |
+
tmp_files.append(f)
|
| 210 |
+
current_chunk = []
|
| 211 |
+
chunk_idx += 1
|
| 212 |
+
if chunk_idx % 10 == 0:
|
| 213 |
+
logger.info(
|
| 214 |
+
"BBPE train: %d chunks, %d samples coletados",
|
| 215 |
+
chunk_idx, total_samples,
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
# Flush final
|
| 219 |
+
if current_chunk:
|
| 220 |
+
f = tmp_dir / f"chunk_{chunk_idx:04d}.txt"
|
| 221 |
+
f.write_text("\n".join(current_chunk), encoding="utf-8")
|
| 222 |
+
tmp_files.append(f)
|
| 223 |
+
|
| 224 |
+
if not tmp_files:
|
| 225 |
+
raise RuntimeError(
|
| 226 |
+
"BBPE train: nenhum texto vΓ‘lido encontrado no iterador"
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
logger.info(
|
| 230 |
+
"BBPE train: treinando com %d arquivos, %d amostras totais",
|
| 231 |
+
len(tmp_files), total_samples,
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
# Treina
|
| 235 |
+
self._tokenizer.train(
|
| 236 |
+
[str(f) for f in tmp_files],
|
| 237 |
+
trainer,
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
# Carrega vocabulΓ‘rio em memΓ³ria
|
| 241 |
+
self._build_vocab_cache()
|
| 242 |
+
|
| 243 |
+
logger.info(
|
| 244 |
+
"BBPE train: concluΓdo. Vocab size real: %d",
|
| 245 |
+
len(self._vocab),
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
# Salva
|
| 249 |
+
if save_path is not None:
|
| 250 |
+
self.save(save_path)
|
| 251 |
+
|
| 252 |
+
finally:
|
| 253 |
+
# Limpeza
|
| 254 |
+
for f in tmp_files:
|
| 255 |
+
try:
|
| 256 |
+
f.unlink()
|
| 257 |
+
except Exception:
|
| 258 |
+
pass
|
| 259 |
+
try:
|
| 260 |
+
tmp_dir.rmdir()
|
| 261 |
+
except Exception:
|
| 262 |
+
pass
|
| 263 |
+
|
| 264 |
+
def train_from_files(
|
| 265 |
+
self,
|
| 266 |
+
file_paths: List[Union[str, Path]],
|
| 267 |
+
save_path: Optional[Union[str, Path]] = None,
|
| 268 |
+
min_frequency: int = 2,
|
| 269 |
+
show_progress: bool = True,
|
| 270 |
+
) -> None:
|
| 271 |
+
"""Treina o tokenizer BBPE a partir de arquivos de texto."""
|
| 272 |
+
from tokenizers.trainers import BpeTrainer
|
| 273 |
+
|
| 274 |
+
logger.info(
|
| 275 |
+
"Training BBPE from %d files: vocab_size=%d",
|
| 276 |
+
len(file_paths), self.vocab_size,
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
self._tokenizer = self._build_tokenizer()
|
| 280 |
+
trainer = BpeTrainer(
|
| 281 |
+
vocab_size=self.vocab_size,
|
| 282 |
+
min_frequency=min_frequency,
|
| 283 |
+
special_tokens=SPECIAL_TOKENS,
|
| 284 |
+
show_progress=show_progress,
|
| 285 |
+
initial_alphabet=ByteLevel.alphabet(),
|
| 286 |
+
)
|
| 287 |
+
self._tokenizer.train([str(p) for p in file_paths], trainer)
|
| 288 |
+
self._build_vocab_cache()
|
| 289 |
+
|
| 290 |
+
if save_path is not None:
|
| 291 |
+
self.save(save_path)
|
| 292 |
+
|
| 293 |
+
# ------------------------------------------------------------------
|
| 294 |
+
# Save / Load
|
| 295 |
+
# ------------------------------------------------------------------
|
| 296 |
+
def save(self, path: Union[str, Path]) -> None:
|
| 297 |
+
"""Salva o tokenizer em arquivo JSON."""
|
| 298 |
+
if self._tokenizer is None:
|
| 299 |
+
raise RuntimeError("Tokenizer nΓ£o treinado. Chame train_*() primeiro.")
|
| 300 |
+
path = Path(path)
|
| 301 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 302 |
+
self._tokenizer.save(str(path))
|
| 303 |
+
logger.info("BBPE tokenizer saved: %s", path)
|
| 304 |
+
|
| 305 |
+
@classmethod
|
| 306 |
+
def load(cls, path: Union[str, Path]) -> "BBPETokenizer":
|
| 307 |
+
"""Carrega um tokenizer salvo."""
|
| 308 |
+
from tokenizers import Tokenizer
|
| 309 |
+
|
| 310 |
+
path = Path(path)
|
| 311 |
+
if not path.exists():
|
| 312 |
+
raise FileNotFoundError(f"Tokenizer file not found: {path}")
|
| 313 |
+
|
| 314 |
+
instance = cls() # default vocab_size
|
| 315 |
+
instance._tokenizer = Tokenizer.from_file(str(path))
|
| 316 |
+
instance._build_vocab_cache()
|
| 317 |
+
# Atualiza vocab_size com tamanho real
|
| 318 |
+
instance.vocab_size = len(instance._vocab)
|
| 319 |
+
logger.info(
|
| 320 |
+
"BBPE tokenizer loaded: %s (vocab_size=%d)",
|
| 321 |
+
path, instance.vocab_size,
|
| 322 |
+
)
|
| 323 |
+
return instance
|
| 324 |
+
|
| 325 |
+
def _build_vocab_cache(self) -> None:
|
| 326 |
+
"""ConstrΓ³i caches internos de vocab/id_to_token."""
|
| 327 |
+
if self._tokenizer is None:
|
| 328 |
+
return
|
| 329 |
+
self._vocab = self._tokenizer.get_vocab()
|
| 330 |
+
self._id_to_token = {v: k for k, v in self._vocab.items()}
|
| 331 |
+
|
| 332 |
+
# ------------------------------------------------------------------
|
| 333 |
+
# Encode / Decode
|
| 334 |
+
# ------------------------------------------------------------------
|
| 335 |
+
def encode(
|
| 336 |
+
self,
|
| 337 |
+
text: str,
|
| 338 |
+
add_special_tokens: bool = False,
|
| 339 |
+
max_length: Optional[int] = None,
|
| 340 |
+
truncation: bool = True,
|
| 341 |
+
) -> List[int]:
|
| 342 |
+
"""Codifica um texto em IDs de tokens.
|
| 343 |
+
|
| 344 |
+
Args:
|
| 345 |
+
text: string de entrada
|
| 346 |
+
add_special_tokens: adicionar <s>...</s>
|
| 347 |
+
max_length: truncar para este tamanho (apΓ³s special tokens)
|
| 348 |
+
truncation: se False e max_length excedido, raise em vez de truncar
|
| 349 |
+
"""
|
| 350 |
+
if self._tokenizer is None:
|
| 351 |
+
raise RuntimeError("Tokenizer nΓ£o carregado.")
|
| 352 |
+
enc = self._tokenizer.encode(text, add_special_tokens=add_special_tokens)
|
| 353 |
+
ids = enc.ids
|
| 354 |
+
|
| 355 |
+
if max_length is not None:
|
| 356 |
+
if len(ids) > max_length:
|
| 357 |
+
if not truncation:
|
| 358 |
+
raise ValueError(
|
| 359 |
+
f"Input too long: {len(ids)} > {max_length} and truncation=False"
|
| 360 |
+
)
|
| 361 |
+
if add_special_tokens:
|
| 362 |
+
# Preserva EOS no final
|
| 363 |
+
ids = ids[:max_length - 1] + [EOS_ID] if max_length >= 1 else [EOS_ID]
|
| 364 |
+
else:
|
| 365 |
+
ids = ids[:max_length]
|
| 366 |
+
return ids
|
| 367 |
+
|
| 368 |
+
def encode_batch(
|
| 369 |
+
self,
|
| 370 |
+
texts: List[str],
|
| 371 |
+
add_special_tokens: bool = False,
|
| 372 |
+
max_length: Optional[int] = None,
|
| 373 |
+
) -> List[List[int]]:
|
| 374 |
+
"""Codifica um batch de textos."""
|
| 375 |
+
return [self.encode(t, add_special_tokens, max_length) for t in texts]
|
| 376 |
+
|
| 377 |
+
def decode(
|
| 378 |
+
self,
|
| 379 |
+
ids: List[int],
|
| 380 |
+
skip_special_tokens: bool = True,
|
| 381 |
+
) -> str:
|
| 382 |
+
"""Decodifica IDs de volta para texto."""
|
| 383 |
+
if self._tokenizer is None:
|
| 384 |
+
raise RuntimeError("Tokenizer nΓ£o carregado.")
|
| 385 |
+
return self._tokenizer.decode(ids, skip_special_tokens=skip_special_tokens)
|
| 386 |
+
|
| 387 |
+
def decode_batch(
|
| 388 |
+
self,
|
| 389 |
+
ids_list: List[List[int]],
|
| 390 |
+
skip_special_tokens: bool = True,
|
| 391 |
+
) -> List[str]:
|
| 392 |
+
"""Decodifica um batch de listas de IDs."""
|
| 393 |
+
return [self.decode(ids, skip_special_tokens) for ids in ids_list]
|
| 394 |
+
|
| 395 |
+
# ------------------------------------------------------------------
|
| 396 |
+
# PyTorch integration
|
| 397 |
+
# ------------------------------------------------------------------
|
| 398 |
+
def encode_tensor(
|
| 399 |
+
self,
|
| 400 |
+
texts: List[str],
|
| 401 |
+
max_length: int,
|
| 402 |
+
pad_to_max_length: bool = True,
|
| 403 |
+
add_special_tokens: bool = False,
|
| 404 |
+
):
|
| 405 |
+
"""Codifica textos para tensor PyTorch (B, T) com padding.
|
| 406 |
+
|
| 407 |
+
Returns:
|
| 408 |
+
input_ids: LongTensor (B, T)
|
| 409 |
+
attention_mask: LongTensor (B, T) β 1 para tokens reais, 0 para pad
|
| 410 |
+
"""
|
| 411 |
+
import torch
|
| 412 |
+
|
| 413 |
+
batch_ids = [
|
| 414 |
+
self.encode(t, add_special_tokens=add_special_tokens, max_length=max_length)
|
| 415 |
+
for t in texts
|
| 416 |
+
]
|
| 417 |
+
|
| 418 |
+
if pad_to_max_length:
|
| 419 |
+
# Pad todas para max_length
|
| 420 |
+
padded = []
|
| 421 |
+
masks = []
|
| 422 |
+
for ids in batch_ids:
|
| 423 |
+
if len(ids) >= max_length:
|
| 424 |
+
padded.append(ids[:max_length])
|
| 425 |
+
masks.append([1] * max_length)
|
| 426 |
+
else:
|
| 427 |
+
pad_len = max_length - len(ids)
|
| 428 |
+
padded.append(ids + [PAD_ID] * pad_len)
|
| 429 |
+
masks.append([1] * len(ids) + [0] * pad_len)
|
| 430 |
+
input_ids = torch.tensor(padded, dtype=torch.long)
|
| 431 |
+
attention_mask = torch.tensor(masks, dtype=torch.long)
|
| 432 |
+
else:
|
| 433 |
+
# Sem padding β cada amostra tem seu tamanho
|
| 434 |
+
input_ids = [torch.tensor(ids, dtype=torch.long) for ids in batch_ids]
|
| 435 |
+
attention_mask = [torch.ones(len(ids), dtype=torch.long) for ids in batch_ids]
|
| 436 |
+
|
| 437 |
+
return input_ids, attention_mask
|
| 438 |
+
|
| 439 |
+
# ------------------------------------------------------------------
|
| 440 |
+
# Properties
|
| 441 |
+
# ------------------------------------------------------------------
|
| 442 |
+
@property
|
| 443 |
+
def vocab(self) -> Dict[str, int]:
|
| 444 |
+
if self._vocab is None:
|
| 445 |
+
self._build_vocab_cache()
|
| 446 |
+
return self._vocab or {}
|
| 447 |
+
|
| 448 |
+
@property
|
| 449 |
+
def id_to_token(self) -> Dict[int, str]:
|
| 450 |
+
if self._id_to_token is None:
|
| 451 |
+
self._build_vocab_cache()
|
| 452 |
+
return self._id_to_token or {}
|
| 453 |
+
|
| 454 |
+
@property
|
| 455 |
+
def actual_vocab_size(self) -> int:
|
| 456 |
+
"""Tamanho real do vocabulΓ‘rio carregado/treinado."""
|
| 457 |
+
return len(self.vocab)
|
| 458 |
+
|
| 459 |
+
def __len__(self) -> int:
|
| 460 |
+
return self.actual_vocab_size
|
| 461 |
+
|
| 462 |
+
# ------------------------------------------------------------------
|
| 463 |
+
# Validation
|
| 464 |
+
# ------------------------------------------------------------------
|
| 465 |
+
def validate_roundtrip(self, test_texts: List[str]) -> Dict[str, Any]:
|
| 466 |
+
"""Valida que encodeβdecode preserva o texto (Teorema 20).
|
| 467 |
+
|
| 468 |
+
Returns:
|
| 469 |
+
Dict com: success_rate, avg_compression, failed_examples
|
| 470 |
+
"""
|
| 471 |
+
results = {
|
| 472 |
+
"success_rate": 0.0,
|
| 473 |
+
"avg_compression": 0.0,
|
| 474 |
+
"avg_tokens_per_text": 0.0,
|
| 475 |
+
"total_texts": len(test_texts),
|
| 476 |
+
"failed_examples": [],
|
| 477 |
+
}
|
| 478 |
+
if not test_texts:
|
| 479 |
+
return results
|
| 480 |
+
|
| 481 |
+
successes = 0
|
| 482 |
+
total_compression = 0.0
|
| 483 |
+
total_tokens = 0
|
| 484 |
+
|
| 485 |
+
for text in test_texts:
|
| 486 |
+
try:
|
| 487 |
+
ids = self.encode(text, add_special_tokens=False)
|
| 488 |
+
decoded = self.decode(ids, skip_special_tokens=True)
|
| 489 |
+
|
| 490 |
+
# Byte-level: o decoded pode ter um espaΓ§o prefix se add_prefix_space=True
|
| 491 |
+
# Comparamos sem o prefix
|
| 492 |
+
expected = text
|
| 493 |
+
got = decoded
|
| 494 |
+
|
| 495 |
+
if expected == got:
|
| 496 |
+
successes += 1
|
| 497 |
+
else:
|
| 498 |
+
# Tenta sem o prefix space
|
| 499 |
+
if self.add_prefix_space and got.startswith(" "):
|
| 500 |
+
got = got[1:]
|
| 501 |
+
if expected == got:
|
| 502 |
+
successes += 1
|
| 503 |
+
else:
|
| 504 |
+
results["failed_examples"].append({
|
| 505 |
+
"input": text[:100],
|
| 506 |
+
"decoded": decoded[:100],
|
| 507 |
+
"ids_count": len(ids),
|
| 508 |
+
})
|
| 509 |
+
|
| 510 |
+
# Compression: bytes / tokens
|
| 511 |
+
n_bytes = len(text.encode("utf-8"))
|
| 512 |
+
n_tokens = len(ids)
|
| 513 |
+
if n_tokens > 0:
|
| 514 |
+
total_compression += n_bytes / n_tokens
|
| 515 |
+
total_tokens += n_tokens
|
| 516 |
+
|
| 517 |
+
except Exception as e:
|
| 518 |
+
results["failed_examples"].append({
|
| 519 |
+
"input": text[:100],
|
| 520 |
+
"error": str(e),
|
| 521 |
+
})
|
| 522 |
+
|
| 523 |
+
results["success_rate"] = successes / len(test_texts)
|
| 524 |
+
results["avg_compression"] = total_compression / max(successes, 1)
|
| 525 |
+
results["avg_tokens_per_text"] = total_tokens / max(successes, 1)
|
| 526 |
+
return results
|
| 527 |
+
|
| 528 |
+
|
| 529 |
+
# ---------------------------------------------------------------------------
|
| 530 |
+
# Byte-level alphabet (for BBPE initial alphabet)
|
| 531 |
+
# ---------------------------------------------------------------------------
|
| 532 |
+
class ByteLevel:
|
| 533 |
+
"""Wrapper para o alfabeto byte-level (256 bytes)."""
|
| 534 |
+
|
| 535 |
+
@staticmethod
|
| 536 |
+
def alphabet() -> List[str]:
|
| 537 |
+
"""Retorna os 256 caracteres byte-level (Δ -style do GPT-2)."""
|
| 538 |
+
from tokenizers.pre_tokenizers import ByteLevel as _ByteLevel
|
| 539 |
+
return list(_ByteLevel.alphabet())
|
| 540 |
+
|
| 541 |
+
|
| 542 |
+
# ---------------------------------------------------------------------------
|
| 543 |
+
# Convenience factory
|
| 544 |
+
# ---------------------------------------------------------------------------
|
| 545 |
+
def create_or_load_tokenizer(
|
| 546 |
+
path: Union[str, Path],
|
| 547 |
+
text_iterator: Optional[Iterator[str]] = None,
|
| 548 |
+
vocab_size: int = DEFAULT_VOCAB_SIZE,
|
| 549 |
+
min_frequency: int = 2,
|
| 550 |
+
) -> BBPETokenizer:
|
| 551 |
+
"""Carrega um tokenizer existente ou treina um novo a partir de um iterador.
|
| 552 |
+
|
| 553 |
+
Args:
|
| 554 |
+
path: caminho do arquivo JSON
|
| 555 |
+
text_iterator: iterador de textos para treinar (se arquivo nΓ£o existe)
|
| 556 |
+
vocab_size: tamanho do vocabulΓ‘rio (default 250.000)
|
| 557 |
+
min_frequency: frequΓͺncia mΓnima para merges
|
| 558 |
+
|
| 559 |
+
Returns:
|
| 560 |
+
BBPETokenizer carregado/treinado
|
| 561 |
+
"""
|
| 562 |
+
path = Path(path)
|
| 563 |
+
if path.exists():
|
| 564 |
+
logger.info("Loading existing BBPE tokenizer: %s", path)
|
| 565 |
+
return BBPETokenizer.load(path)
|
| 566 |
+
|
| 567 |
+
if text_iterator is None:
|
| 568 |
+
raise ValueError(
|
| 569 |
+
f"Tokenizer file {path} does not exist and no text_iterator provided"
|
| 570 |
+
)
|
| 571 |
+
|
| 572 |
+
logger.info("Training new BBPE tokenizer: %s", path)
|
| 573 |
+
tok = BBPETokenizer(vocab_size=vocab_size)
|
| 574 |
+
tok.train_from_stream(
|
| 575 |
+
text_iterator,
|
| 576 |
+
save_path=path,
|
| 577 |
+
min_frequency=min_frequency,
|
| 578 |
+
)
|
| 579 |
+
return tok
|
| 580 |
+
|
| 581 |
+
|
| 582 |
+
__all__ = [
|
| 583 |
+
"BBPETokenizer",
|
| 584 |
+
"ByteLevel",
|
| 585 |
+
"create_or_load_tokenizer",
|
| 586 |
+
"BOS_TOKEN",
|
| 587 |
+
"PAD_TOKEN",
|
| 588 |
+
"EOS_TOKEN",
|
| 589 |
+
"UNK_TOKEN",
|
| 590 |
+
"BOS_ID",
|
| 591 |
+
"PAD_ID",
|
| 592 |
+
"EOS_ID",
|
| 593 |
+
"UNK_ID",
|
| 594 |
+
"SPECIAL_TOKENS",
|
| 595 |
+
"DEFAULT_VOCAB_SIZE",
|
| 596 |
+
]
|
src/bigru_t/training/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Componentes de treino (Lemas 2 e 4)."""
|
| 2 |
+
from .gradient_surgery import apply_gradient_surgery, orthogonalize_gradient
|
| 3 |
+
from .meta_configurator import MetaConfigurator
|
| 4 |
+
from .kill_switch import KillSwitch, KillSwitchState
|
| 5 |
+
from .trainer import BiGRU_T_Trainer, TrainerConfig
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
"apply_gradient_surgery", "orthogonalize_gradient",
|
| 9 |
+
"MetaConfigurator",
|
| 10 |
+
"KillSwitch", "KillSwitchState",
|
| 11 |
+
"BiGRU_T_Trainer", "TrainerConfig",
|
| 12 |
+
]
|
src/bigru_t/training/gradient_surgery.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""gradient_surgery.py β Lema 2: gradiente cirΓΊrgico (projeΓ§Γ£o ortogonal).
|
| 2 |
+
|
| 3 |
+
Implementa o Lema 2 (Gradiente cirΓΊrgico).
|
| 4 |
+
|
| 5 |
+
Quando os gradientes da perda principal (loss_main) e da perda de hipΓ³tese
|
| 6 |
+
(loss_hyp) sΓ£o conflitantes (produto interno negativo), projeta o gradiente
|
| 7 |
+
de loss_hyp no complemento ortogonal de loss_main. Isso garante que ambas
|
| 8 |
+
as perdas possam ser reduzidas sem interferΓͺncia destrutiva.
|
| 9 |
+
|
| 10 |
+
Reaproveita a filosofia do punishment_reward_system.py do v13.9.2, mas:
|
| 11 |
+
- Substitui o gate binΓ‘rio de puniΓ§Γ£o por projeΓ§Γ£o ortogonal contΓnua
|
| 12 |
+
- NΓ£o hΓ‘ "patience" nem "skip steps" β a cirurgia Γ© aplicada sempre que
|
| 13 |
+
loss_hyp Γ© calculada
|
| 14 |
+
- O limiar tau (do Lema 4) decide SE loss_hyp Γ© calculada, nΓ£o COMO o
|
| 15 |
+
gradiente Γ© combinado
|
| 16 |
+
|
| 17 |
+
ReferΓͺncias:
|
| 18 |
+
- Yu et al. (2020). Gradient Surgery for Multi-Task Learning. NeurIPS.
|
| 19 |
+
- CsiszΓ‘r, P. (1984). Information Geometry and Orthogonal Projections.
|
| 20 |
+
"""
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
from typing import Iterable
|
| 24 |
+
|
| 25 |
+
import torch
|
| 26 |
+
import torch.nn as nn
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def orthogonalize_gradient(
|
| 30 |
+
g_main: torch.Tensor, g_hyp: torch.Tensor, eps: float = 1e-8
|
| 31 |
+
) -> torch.Tensor:
|
| 32 |
+
"""Projeta g_hyp no complemento ortogonal de g_main.
|
| 33 |
+
|
| 34 |
+
Se <g_main, g_hyp> < 0 (conflitantes):
|
| 35 |
+
g_hyp_perp = g_hyp - (<g_main, g_hyp> / ||g_main||^2) * g_main
|
| 36 |
+
Caso contrΓ‘rio (alinhados):
|
| 37 |
+
g_hyp_perp = g_hyp (sem projeΓ§Γ£o)
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
g_main: gradiente da perda principal (tensor)
|
| 41 |
+
g_hyp: gradiente da perda de hipΓ³tese (tensor, mesmo shape)
|
| 42 |
+
eps: estabilidade numΓ©rica
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
g_hyp_perp: gradiente projetado (mesmo shape)
|
| 46 |
+
"""
|
| 47 |
+
# Produto interno escalar (flatten para generalidade)
|
| 48 |
+
g_main_flat = g_main.flatten()
|
| 49 |
+
g_hyp_flat = g_hyp.flatten()
|
| 50 |
+
dot = torch.dot(g_main_flat, g_hyp_flat)
|
| 51 |
+
|
| 52 |
+
if dot < 0:
|
| 53 |
+
norm_sq = torch.dot(g_main_flat, g_main_flat)
|
| 54 |
+
if norm_sq > eps:
|
| 55 |
+
# g_hyp_perp = g_hyp - (dot / ||g_main||^2) * g_main
|
| 56 |
+
g_hyp_perp = g_hyp - (dot / norm_sq) * g_main
|
| 57 |
+
else:
|
| 58 |
+
# g_main β 0: nΓ£o hΓ‘ direΓ§Γ£o para projetar
|
| 59 |
+
g_hyp_perp = g_hyp
|
| 60 |
+
else:
|
| 61 |
+
# Gradientes alinhados: sem projeΓ§Γ£o
|
| 62 |
+
g_hyp_perp = g_hyp
|
| 63 |
+
|
| 64 |
+
return g_hyp_perp
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def apply_gradient_surgery(
|
| 68 |
+
model: nn.Module,
|
| 69 |
+
loss_main: torch.Tensor,
|
| 70 |
+
loss_hyp: torch.Tensor,
|
| 71 |
+
) -> None:
|
| 72 |
+
"""Aplica cirurgia de gradiente e atribui p.grad combinado.
|
| 73 |
+
|
| 74 |
+
Calcula gradientes independentes para loss_main e loss_hyp, aplica
|
| 75 |
+
projeΓ§Γ£o ortogonal quando conflitantes, e atribui o gradiente combinado
|
| 76 |
+
a p.grad para cada parΓ’metro p do modelo.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
model: o modelo (parΓ’metros serΓ£o atualizados in-place via .grad)
|
| 80 |
+
loss_main: perda principal (escalar, com grad)
|
| 81 |
+
loss_hyp: perda de hipΓ³tese (escalar, com grad)
|
| 82 |
+
|
| 83 |
+
Nota:
|
| 84 |
+
- Esta funΓ§Γ£o NΓO chama optimizer.step() β apenas atribui p.grad.
|
| 85 |
+
- O chamador deve fazer optimizer.step() e optimizer.zero_grad().
|
| 86 |
+
- loss_main e loss_hyp devem compartilhar o grafo computacional
|
| 87 |
+
(ou loss_hyp deve ser computada com retain_graph=True).
|
| 88 |
+
"""
|
| 89 |
+
# Zera gradientes existentes
|
| 90 |
+
model.zero_grad(set_to_none=True)
|
| 91 |
+
|
| 92 |
+
# Gradientes da perda principal (retain_graph=True para reusar na hyp)
|
| 93 |
+
params = [p for p in model.parameters() if p.requires_grad]
|
| 94 |
+
grads_main = torch.autograd.grad(
|
| 95 |
+
loss_main, params, retain_graph=True, create_graph=False, allow_unused=True
|
| 96 |
+
)
|
| 97 |
+
# Gradientes da perda de hipΓ³tese
|
| 98 |
+
grads_hyp = torch.autograd.grad(
|
| 99 |
+
loss_hyp, params, retain_graph=False, create_graph=False, allow_unused=True
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
# Combina com cirurgia ortogonal
|
| 103 |
+
for p, g_m, g_h in zip(params, grads_main, grads_hyp):
|
| 104 |
+
if g_m is None:
|
| 105 |
+
g_m = torch.zeros_like(p)
|
| 106 |
+
if g_h is None:
|
| 107 |
+
g_h = torch.zeros_like(p)
|
| 108 |
+
# Cirurgia ortogonal (Lema 2)
|
| 109 |
+
g_h_orth = orthogonalize_gradient(g_m, g_h)
|
| 110 |
+
# Gradiente combinado
|
| 111 |
+
p.grad = (g_m + g_h_orth).detach()
|
| 112 |
+
|
| 113 |
+
return
|
src/bigru_t/training/kill_switch.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""kill_switch.py β Monitor de saΓΊde do treino + kill automΓ‘tico.
|
| 2 |
+
|
| 3 |
+
Implementa a regra do usuΓ‘rio:
|
| 4 |
+
"monitorar e matar o modelo se nΓ£o estiver aprendendo"
|
| 5 |
+
"monitorar consumo de RAM, consumo de Armazenamento, a loss e a perplexidade"
|
| 6 |
+
|
| 7 |
+
CritΓ©rios de kill:
|
| 8 |
+
1. RAM > 90% da disponΓvel (psutil.virtual_memory)
|
| 9 |
+
2. Disco < 1 GB livre (psutil.disk_usage)
|
| 10 |
+
3. Loss nΓ£o diminui em N_steps_patience (default 30) β comparado com
|
| 11 |
+
min_loss_so_far
|
| 12 |
+
4. Loss Γ© NaN ou Inf
|
| 13 |
+
5. Perplexidade > 1e6 (explosΓ£o)
|
| 14 |
+
|
| 15 |
+
Reaproveita a filosofia do flexnet/oom_guard.py (thread-based) e
|
| 16 |
+
flexnet/memory_monitor.py (leak detection), mas unifica em uma classe
|
| 17 |
+
sΓncrona mais simples para o treino de bug-detection.
|
| 18 |
+
"""
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import math
|
| 22 |
+
import os
|
| 23 |
+
import time
|
| 24 |
+
from dataclasses import dataclass, field
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
from typing import Optional
|
| 27 |
+
|
| 28 |
+
import psutil
|
| 29 |
+
import torch
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass
|
| 33 |
+
class KillSwitchState:
|
| 34 |
+
"""Snapshot do estado do kill-switch em um dado step."""
|
| 35 |
+
step: int
|
| 36 |
+
loss: float
|
| 37 |
+
ppl: float
|
| 38 |
+
ram_pct: float
|
| 39 |
+
ram_used_gb: float
|
| 40 |
+
ram_total_gb: float
|
| 41 |
+
disk_free_gb: float
|
| 42 |
+
disk_total_gb: float
|
| 43 |
+
active_modules: int
|
| 44 |
+
reason: Optional[str] = None # None = OK, string = motivo do kill
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class KillSwitch:
|
| 48 |
+
"""Monitor de saΓΊde do treino com kill automΓ‘tico.
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
ram_threshold_pct: kill se RAM usage > este pct (default 90)
|
| 52 |
+
disk_min_free_gb: kill se disco livre < este valor (default 1.0)
|
| 53 |
+
loss_patience: nΒΊ de steps sem melhoria antes de matar (default 30)
|
| 54 |
+
loss_tolerance: loss considerada "melhoria" se cair mais que isto (default 1e-4)
|
| 55 |
+
max_ppl: kill se ppl > este valor (default 1e6)
|
| 56 |
+
log_dir: diretΓ³rio para salvar logs de monitoramento (default /tmp)
|
| 57 |
+
|
| 58 |
+
Uso:
|
| 59 |
+
ks = KillSwitch()
|
| 60 |
+
for step, batch in enumerate(loader):
|
| 61 |
+
loss = train_step(batch)
|
| 62 |
+
state = ks.check(step, loss, active_modules)
|
| 63 |
+
if state.reason:
|
| 64 |
+
logger.error(f"KILL: {state.reason}")
|
| 65 |
+
break
|
| 66 |
+
"""
|
| 67 |
+
|
| 68 |
+
def __init__(
|
| 69 |
+
self,
|
| 70 |
+
ram_threshold_pct: float = 90.0,
|
| 71 |
+
disk_min_free_gb: float = 1.0,
|
| 72 |
+
loss_patience: int = 30,
|
| 73 |
+
loss_tolerance: float = 1e-4,
|
| 74 |
+
max_ppl: float = 1e6,
|
| 75 |
+
log_dir: Optional[str] = None,
|
| 76 |
+
):
|
| 77 |
+
self.ram_threshold_pct = ram_threshold_pct
|
| 78 |
+
self.disk_min_free_gb = disk_min_free_gb
|
| 79 |
+
self.loss_patience = loss_patience
|
| 80 |
+
self.loss_tolerance = loss_tolerance
|
| 81 |
+
self.max_ppl = max_ppl
|
| 82 |
+
self.log_dir = Path(log_dir) if log_dir else None
|
| 83 |
+
|
| 84 |
+
# Estado interno
|
| 85 |
+
self.min_loss: float = float("inf")
|
| 86 |
+
self.steps_since_improvement: int = 0
|
| 87 |
+
self.history: list[KillSwitchState] = []
|
| 88 |
+
|
| 89 |
+
# Snapshot do processo para RAM
|
| 90 |
+
self._process = psutil.Process(os.getpid())
|
| 91 |
+
|
| 92 |
+
def _get_ram_usage(self) -> tuple[float, float, float]:
|
| 93 |
+
"""Retorna (ram_pct, ram_used_gb, ram_total_gb)."""
|
| 94 |
+
vm = psutil.virtual_memory()
|
| 95 |
+
return vm.percent, vm.used / 1e9, vm.total / 1e9
|
| 96 |
+
|
| 97 |
+
def _get_disk_usage(self) -> tuple[float, float]:
|
| 98 |
+
"""Retorna (disk_free_gb, disk_total_gb) para o disco do projeto."""
|
| 99 |
+
# Usa o disco onde /home/z/my-project estΓ‘
|
| 100 |
+
path = "/home/z/my-project"
|
| 101 |
+
try:
|
| 102 |
+
du = psutil.disk_usage(path)
|
| 103 |
+
return du.free / 1e9, du.total / 1e9
|
| 104 |
+
except Exception:
|
| 105 |
+
return float("inf"), float("inf")
|
| 106 |
+
|
| 107 |
+
def check(
|
| 108 |
+
self,
|
| 109 |
+
step: int,
|
| 110 |
+
loss: float,
|
| 111 |
+
active_modules: int = 0,
|
| 112 |
+
) -> KillSwitchState:
|
| 113 |
+
"""Verifica saΓΊde do treino e retorna estado.
|
| 114 |
+
|
| 115 |
+
Args:
|
| 116 |
+
step: nΒΊ do step atual
|
| 117 |
+
loss: loss atual (escalar)
|
| 118 |
+
active_modules: nΒΊ de mΓ³dulos ativos no modelo (para logging)
|
| 119 |
+
|
| 120 |
+
Returns:
|
| 121 |
+
KillSwitchState com .reason preenchido se kill recomendado.
|
| 122 |
+
"""
|
| 123 |
+
# Calcula ppl (com clipping para evitar overflow)
|
| 124 |
+
if math.isnan(loss) or math.isinf(loss):
|
| 125 |
+
ppl = float("inf")
|
| 126 |
+
else:
|
| 127 |
+
ppl = math.exp(min(20, loss)) if loss < 20 else float("inf")
|
| 128 |
+
|
| 129 |
+
ram_pct, ram_used, ram_total = self._get_ram_usage()
|
| 130 |
+
disk_free, disk_total = self._get_disk_usage()
|
| 131 |
+
|
| 132 |
+
# Atualiza min_loss e contador de patience
|
| 133 |
+
reason = None
|
| 134 |
+
if math.isnan(loss) or math.isinf(loss):
|
| 135 |
+
reason = f"loss is NaN/Inf at step {step}"
|
| 136 |
+
elif loss < self.min_loss - self.loss_tolerance:
|
| 137 |
+
self.min_loss = loss
|
| 138 |
+
self.steps_since_improvement = 0
|
| 139 |
+
else:
|
| 140 |
+
self.steps_since_improvement += 1
|
| 141 |
+
if self.steps_since_improvement >= self.loss_patience:
|
| 142 |
+
reason = (
|
| 143 |
+
f"loss not improving for {self.loss_patience} steps "
|
| 144 |
+
f"(min={self.min_loss:.4f}, current={loss:.4f})"
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
# CritΓ©rio de RAM
|
| 148 |
+
if ram_pct > self.ram_threshold_pct:
|
| 149 |
+
reason = reason or f"RAM {ram_pct:.1f}% > threshold {self.ram_threshold_pct}%"
|
| 150 |
+
|
| 151 |
+
# CritΓ©rio de disco
|
| 152 |
+
if disk_free < self.disk_min_free_gb:
|
| 153 |
+
reason = reason or f"disk free {disk_free:.2f}GB < min {self.disk_min_free_gb}GB"
|
| 154 |
+
|
| 155 |
+
# CritΓ©rio de perplexidade
|
| 156 |
+
if ppl > self.max_ppl:
|
| 157 |
+
reason = reason or f"ppl {ppl:.2e} > max {self.max_ppl:.2e}"
|
| 158 |
+
|
| 159 |
+
state = KillSwitchState(
|
| 160 |
+
step=step,
|
| 161 |
+
loss=loss,
|
| 162 |
+
ppl=ppl,
|
| 163 |
+
ram_pct=ram_pct,
|
| 164 |
+
ram_used_gb=ram_used,
|
| 165 |
+
ram_total_gb=ram_total,
|
| 166 |
+
disk_free_gb=disk_free,
|
| 167 |
+
disk_total_gb=disk_total,
|
| 168 |
+
active_modules=active_modules,
|
| 169 |
+
reason=reason,
|
| 170 |
+
)
|
| 171 |
+
self.history.append(state)
|
| 172 |
+
return state
|
| 173 |
+
|
| 174 |
+
def summary(self) -> dict:
|
| 175 |
+
"""Retorna resumo final do monitoramento."""
|
| 176 |
+
if not self.history:
|
| 177 |
+
return {}
|
| 178 |
+
return {
|
| 179 |
+
"total_steps": len(self.history),
|
| 180 |
+
"min_loss": min(s.loss for s in self.history if not math.isnan(s.loss) and not math.isinf(s.loss)),
|
| 181 |
+
"max_ram_pct": max(s.ram_pct for s in self.history),
|
| 182 |
+
"min_disk_free_gb": min(s.disk_free_gb for s in self.history),
|
| 183 |
+
"max_ppl": max(s.ppl for s in self.history if not math.isinf(s.ppl)),
|
| 184 |
+
"killed": self.history[-1].reason is not None,
|
| 185 |
+
"kill_reason": self.history[-1].reason,
|
| 186 |
+
}
|
src/bigru_t/training/meta_configurator.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""meta_configurator.py β Lema 4: auto-configuraΓ§Γ£o via meta-aprendizado.
|
| 2 |
+
|
| 3 |
+
Implementa o Lema 4 (Auto-configuraΓ§Γ£o e suavizaΓ§Γ£o).
|
| 4 |
+
|
| 5 |
+
MantΓ©m hiperparΓ’metros aprendΓveis:
|
| 6 |
+
- log_temperature: controla T = exp(log_temperature) do Lema 1
|
| 7 |
+
- log_tau: controla Ο = exp(log_tau) do Lema 3 (limiar de ativaΓ§Γ£o da hipΓ³tese)
|
| 8 |
+
|
| 9 |
+
Meta-objetivo:
|
| 10 |
+
L_meta(T, Ο) = L_val(T, Ο; ΞΈ) + Ξ»_s * S(ΞΈ)
|
| 11 |
+
|
| 12 |
+
onde S(ΞΈ) = ||β_ΞΈ L_val||^2 Γ© uma proxy de agudeza (sharpness) β aproximaΓ§Γ£o
|
| 13 |
+
de baixo custo para o maior autovalor da Hessiana.
|
| 14 |
+
|
| 15 |
+
Minimizar S(ΞΈ) empurra o modelo para mΓnimos planos (Keskar 2017, Foret 2021),
|
| 16 |
+
onde o ruΓdo de gradiente Γ© menor.
|
| 17 |
+
|
| 18 |
+
Reaproveita a filosofia do meta_learner.py (MAML-RNOEFLEX) e meta_orchestrator.py
|
| 19 |
+
do v13.9.2, mas:
|
| 20 |
+
- Substitui MAML de 2Βͺ ordem por diferenciaΓ§Γ£o implΓcita simplificada
|
| 21 |
+
(1Βͺ ordem + proxy de agudeza)
|
| 22 |
+
- Substitui o orchestrator de 3ΒΊ nΓvel por 2 hiperparΓ’metros escalares
|
| 23 |
+
(T, Ο) β mais barato computacionalmente
|
| 24 |
+
- A proxy de agudeza Γ© o quadrado da norma do gradiente (nΓ£o o autovalor
|
| 25 |
+
da Hessiana β seria proibitivo para 16M+ params)
|
| 26 |
+
"""
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
from typing import Optional
|
| 30 |
+
|
| 31 |
+
import torch
|
| 32 |
+
import torch.nn as nn
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class MetaConfigurator:
|
| 36 |
+
"""Auto-configuraΓ§Γ£o de temperatura (T) e limiar (Ο) via meta-aprendizado.
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
model: o UnifiedModel a ser configurado
|
| 40 |
+
meta_lr: learning rate do meta-otimizador (Adam em log_T, log_tau)
|
| 41 |
+
sharpness_lambda: peso Ξ»_s da proxy de agudeza
|
| 42 |
+
|
| 43 |
+
Atributos:
|
| 44 |
+
log_temperature: nn.Parameter β T = exp(log_temperature)
|
| 45 |
+
log_tau: nn.Parameter β Ο = exp(log_tau)
|
| 46 |
+
meta_optim: Adam sobre (log_temperature, log_tau)
|
| 47 |
+
|
| 48 |
+
Uso:
|
| 49 |
+
meta_cfg = MetaConfigurator(model)
|
| 50 |
+
# No loop de treino, a cada meta_interval batches:
|
| 51 |
+
meta_cfg.forward_with_meta(x_val, y_val)
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
def __init__(
|
| 55 |
+
self,
|
| 56 |
+
model: nn.Module,
|
| 57 |
+
meta_lr: float = 0.01,
|
| 58 |
+
sharpness_lambda: float = 0.01,
|
| 59 |
+
):
|
| 60 |
+
self.model = model
|
| 61 |
+
self.meta_lr = meta_lr
|
| 62 |
+
self.sharpness_lambda = sharpness_lambda
|
| 63 |
+
|
| 64 |
+
# HiperparΓ’metros aprendΓveis (positivos via exp)
|
| 65 |
+
# Init: log(1.0) = 0 β T=1, Ο=1
|
| 66 |
+
self.log_temperature = nn.Parameter(torch.tensor(0.0))
|
| 67 |
+
self.log_tau = nn.Parameter(torch.tensor(0.0))
|
| 68 |
+
|
| 69 |
+
# Otimizador Adam sobre os hiperparΓ’metros
|
| 70 |
+
self.meta_optim = torch.optim.Adam(
|
| 71 |
+
[self.log_temperature, self.log_tau], lr=meta_lr
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
@property
|
| 75 |
+
def temperature(self) -> float:
|
| 76 |
+
"""T atual = exp(log_temperature)."""
|
| 77 |
+
return float(torch.exp(self.log_temperature.detach()).item())
|
| 78 |
+
|
| 79 |
+
@property
|
| 80 |
+
def tau(self) -> float:
|
| 81 |
+
"""Ο atual = exp(log_tau)."""
|
| 82 |
+
return float(torch.exp(self.log_tau.detach()).item())
|
| 83 |
+
|
| 84 |
+
def forward_with_meta(
|
| 85 |
+
self,
|
| 86 |
+
x_val: torch.Tensor,
|
| 87 |
+
y_val: torch.Tensor,
|
| 88 |
+
criterion: Optional[nn.Module] = None,
|
| 89 |
+
) -> dict:
|
| 90 |
+
"""Passo de atualizaΓ§Γ£o de hiperparΓ’metros em batch de validaΓ§Γ£o.
|
| 91 |
+
|
| 92 |
+
Args:
|
| 93 |
+
x_val: batch de validaΓ§Γ£o (input)
|
| 94 |
+
y_val: batch de validaΓ§Γ£o (target)
|
| 95 |
+
criterion: funΓ§Γ£o de perda (default: CrossEntropyLoss)
|
| 96 |
+
|
| 97 |
+
Returns:
|
| 98 |
+
dict com: loss_val, sharpness, meta_loss, T_new, tau_new
|
| 99 |
+
"""
|
| 100 |
+
if criterion is None:
|
| 101 |
+
criterion = nn.CrossEntropyLoss()
|
| 102 |
+
|
| 103 |
+
# HiperparΓ’metros atuais (positivos via exp)
|
| 104 |
+
temp = torch.exp(self.log_temperature)
|
| 105 |
+
tau_val = torch.exp(self.log_tau)
|
| 106 |
+
|
| 107 |
+
# Forward do modelo com T atual (sem hipΓ³tese β sΓ³ validaΓ§Γ£o principal)
|
| 108 |
+
self.model.train() # mantΓ©m dropout ativo para estimar sharpness real
|
| 109 |
+
y_hat_main, _ = self.model(x_val, temperature=temp, use_hypothesis=False)
|
| 110 |
+
loss_val = criterion(y_hat_main, y_val)
|
| 111 |
+
|
| 112 |
+
# Proxy de agudeza: ||β_ΞΈ L_val||^2
|
| 113 |
+
# create_graph=True para poder backward atravΓ©s dele
|
| 114 |
+
params = [p for p in self.model.parameters() if p.requires_grad]
|
| 115 |
+
grads = torch.autograd.grad(
|
| 116 |
+
loss_val,
|
| 117 |
+
params,
|
| 118 |
+
create_graph=True,
|
| 119 |
+
retain_graph=True,
|
| 120 |
+
allow_unused=True,
|
| 121 |
+
)
|
| 122 |
+
sharpness = sum(
|
| 123 |
+
(g ** 2).sum() for g in grads if g is not None
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
# Meta-perda: perda de validaΓ§Γ£o + penalidade de agudeza
|
| 127 |
+
meta_loss = loss_val + self.sharpness_lambda * sharpness
|
| 128 |
+
|
| 129 |
+
# Atualiza log_temperature e log_tau via gradiente de meta_loss
|
| 130 |
+
self.meta_optim.zero_grad()
|
| 131 |
+
meta_loss.backward()
|
| 132 |
+
# Guarda gradiente antes do step para logging
|
| 133 |
+
grad_log_T = self.log_temperature.grad.item() if self.log_temperature.grad is not None else 0.0
|
| 134 |
+
grad_log_tau = self.log_tau.grad.item() if self.log_tau.grad is not None else 0.0
|
| 135 |
+
self.meta_optim.step()
|
| 136 |
+
|
| 137 |
+
# Sincroniza model.tau com o novo valor
|
| 138 |
+
if hasattr(self.model, "tau"):
|
| 139 |
+
self.model.tau.data = torch.exp(self.log_tau.detach()).clone()
|
| 140 |
+
|
| 141 |
+
return {
|
| 142 |
+
"loss_val": float(loss_val.item()),
|
| 143 |
+
"sharpness": float(sharpness.item()),
|
| 144 |
+
"meta_loss": float(meta_loss.item()),
|
| 145 |
+
"T_new": float(torch.exp(self.log_temperature.detach()).item()),
|
| 146 |
+
"tau_new": float(torch.exp(self.log_tau.detach()).item()),
|
| 147 |
+
"grad_log_T": grad_log_T,
|
| 148 |
+
"grad_log_tau": grad_log_tau,
|
| 149 |
+
}
|
src/bigru_t/training/trainer.py
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""trainer.py β Loop de treino BiGRU_T_version (2 Γ©pocas, bug-detection).
|
| 2 |
+
|
| 3 |
+
Implementa:
|
| 4 |
+
- Treino com 2 Γ©pocas fixas ( especificaΓ§Γ£o do usuΓ‘rio )
|
| 5 |
+
- Sem dados inventados (apenas datasets do repo + HF pΓΊblicos)
|
| 6 |
+
- Monitoramento: loss, perplexidade, RAM, disco, mΓ³dulos ativos
|
| 7 |
+
- Kill-switch: aborta se loss nΓ£o diminuir ou RAM explodir
|
| 8 |
+
- Estados temporΓ‘rios salvos durante treino e APAGADOS ao final
|
| 9 |
+
- AplicaΓ§Γ£o dos 4 lemas:
|
| 10 |
+
Lema 1: ModuleSelector (softmax + entropia)
|
| 11 |
+
Lema 2: apply_gradient_surgery quando use_hyp=True
|
| 12 |
+
Lema 3: hyp_T com stop_grad_hyp=True, W8A8 via QuantizedLinear
|
| 13 |
+
Lema 4: MetaConfigurator ajusta T e Ο a cada meta_interval batches
|
| 14 |
+
|
| 15 |
+
Reaproveita a filosofia do scripts/train_from_v13_9_2.py, mas:
|
| 16 |
+
- Sem fine-tune de modelo prΓ©-treinado (treino from-scratch para
|
| 17 |
+
bug-detection β o objetivo Γ© detectar erros, nΓ£o produzir modelo SOTA)
|
| 18 |
+
- Adiciona kill-switch (nΓ£o havia no original)
|
| 19 |
+
- Adiciona MetaConfigurator (nΓ£o havia no original)
|
| 20 |
+
- Adiciona gradient surgery (nΓ£o havia no original)
|
| 21 |
+
- Usa 2 Γ©pocas explΓcitas (vs. max_steps no original)
|
| 22 |
+
"""
|
| 23 |
+
from __future__ import annotations
|
| 24 |
+
|
| 25 |
+
import gc
|
| 26 |
+
import json
|
| 27 |
+
import logging
|
| 28 |
+
import math
|
| 29 |
+
import os
|
| 30 |
+
import shutil
|
| 31 |
+
import sys
|
| 32 |
+
import time
|
| 33 |
+
import traceback
|
| 34 |
+
from dataclasses import dataclass, field
|
| 35 |
+
from pathlib import Path
|
| 36 |
+
from typing import Optional
|
| 37 |
+
|
| 38 |
+
import torch
|
| 39 |
+
import torch.nn as nn
|
| 40 |
+
import torch.nn.functional as F
|
| 41 |
+
|
| 42 |
+
from ..model.unified_model import UnifiedModel, UnifiedModelConfig, create_unified_model
|
| 43 |
+
from .gradient_surgery import apply_gradient_surgery
|
| 44 |
+
from .meta_configurator import MetaConfigurator
|
| 45 |
+
from .kill_switch import KillSwitch
|
| 46 |
+
|
| 47 |
+
logger = logging.getLogger(__name__)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@dataclass
|
| 51 |
+
class TrainerConfig:
|
| 52 |
+
"""ConfiguraΓ§Γ£o do treino (bug-detection)."""
|
| 53 |
+
# Epochs (FIXO em 2 por especificaΓ§Γ£o do usuΓ‘rio)
|
| 54 |
+
epochs: int = 2
|
| 55 |
+
|
| 56 |
+
# Datasets
|
| 57 |
+
datasets: str = "CEIA-POSITIVO/ultrachat_br_clustred_balanced_v1,Madras1/corpus-ptbr-v2,dominguesm/restore-punctuation-ptbr-dataset"
|
| 58 |
+
max_samples_per_dataset: int = 30 # bug-detection: amostra pequena
|
| 59 |
+
max_seq_len: int = 64
|
| 60 |
+
|
| 61 |
+
# Batch
|
| 62 |
+
per_device_batch_size: int = 1
|
| 63 |
+
grad_accum: int = 4
|
| 64 |
+
|
| 65 |
+
# Optimizer
|
| 66 |
+
lr: float = 1e-3 # from-scratch: LR mais alto que fine-tune
|
| 67 |
+
weight_decay: float = 0.01
|
| 68 |
+
max_grad_norm: float = 5.0
|
| 69 |
+
|
| 70 |
+
# Meta-configurator (Lema 4)
|
| 71 |
+
meta_interval: int = 10 # a cada N micro-batches
|
| 72 |
+
meta_lr: float = 0.01
|
| 73 |
+
sharpness_lambda: float = 0.01
|
| 74 |
+
|
| 75 |
+
# Hypothesis (Lema 3)
|
| 76 |
+
use_hypothesis: bool = True
|
| 77 |
+
stop_grad_hyp: bool = True
|
| 78 |
+
|
| 79 |
+
# Kill-switch
|
| 80 |
+
ram_threshold_pct: float = 90.0
|
| 81 |
+
disk_min_free_gb: float = 1.0
|
| 82 |
+
loss_patience: int = 30
|
| 83 |
+
|
| 84 |
+
# Logging
|
| 85 |
+
log_every: int = 5
|
| 86 |
+
save_temp_every: int = 10 # salvar checkpoint temporΓ‘rio a cada N steps
|
| 87 |
+
|
| 88 |
+
# Paths
|
| 89 |
+
output_dir: str = "/home/z/my-project/BiGRU_T_version/model_final"
|
| 90 |
+
temp_dir: str = "/home/z/my-project/BiGRU_T_version/_temp_checkpoints"
|
| 91 |
+
keep_temp: bool = False # False = apagar temp ao final (especificaΓ§Γ£o)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class BiGRU_T_Trainer:
|
| 95 |
+
"""Trainer para BiGRU_T_version com 4 lemas + kill-switch.
|
| 96 |
+
|
| 97 |
+
Args:
|
| 98 |
+
model: UnifiedModel
|
| 99 |
+
tokenizer: BBPE tokenizer
|
| 100 |
+
config: TrainerConfig
|
| 101 |
+
train_samples: lista de ProcessedSample para treino
|
| 102 |
+
val_samples: lista de ProcessedSample para validaΓ§Γ£o (meta-aprendizado)
|
| 103 |
+
"""
|
| 104 |
+
|
| 105 |
+
def __init__(
|
| 106 |
+
self,
|
| 107 |
+
model: UnifiedModel,
|
| 108 |
+
tokenizer,
|
| 109 |
+
config: TrainerConfig,
|
| 110 |
+
train_samples: list,
|
| 111 |
+
val_samples: list,
|
| 112 |
+
):
|
| 113 |
+
self.model = model
|
| 114 |
+
self.tokenizer = tokenizer
|
| 115 |
+
self.config = config
|
| 116 |
+
self.train_samples = train_samples
|
| 117 |
+
self.val_samples = val_samples
|
| 118 |
+
|
| 119 |
+
# Device
|
| 120 |
+
self.device = torch.device("cpu")
|
| 121 |
+
torch.set_num_threads(2)
|
| 122 |
+
os.environ.setdefault("OMP_NUM_THREADS", "2")
|
| 123 |
+
os.environ.setdefault("MKL_NUM_THREADS", "2")
|
| 124 |
+
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
| 125 |
+
|
| 126 |
+
# Otimizador principal (AdamW β HamiltonianWassersteinOptimizer seria
|
| 127 |
+
# muito caro para bug-detection; oferecido como opΓ§Γ£o em config)
|
| 128 |
+
self.optimizer = torch.optim.AdamW(
|
| 129 |
+
model.parameters(),
|
| 130 |
+
lr=config.lr,
|
| 131 |
+
betas=(0.9, 0.95),
|
| 132 |
+
weight_decay=config.weight_decay,
|
| 133 |
+
eps=1e-8,
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
# Meta-configurator (Lema 4)
|
| 137 |
+
self.meta_cfg = MetaConfigurator(
|
| 138 |
+
model,
|
| 139 |
+
meta_lr=config.meta_lr,
|
| 140 |
+
sharpness_lambda=config.sharpness_lambda,
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
# Kill-switch
|
| 144 |
+
self.kill_switch = KillSwitch(
|
| 145 |
+
ram_threshold_pct=config.ram_threshold_pct,
|
| 146 |
+
disk_min_free_gb=config.disk_min_free_gb,
|
| 147 |
+
loss_patience=config.loss_patience,
|
| 148 |
+
log_dir=config.temp_dir,
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
# DiretΓ³rios
|
| 152 |
+
Path(config.output_dir).mkdir(parents=True, exist_ok=True)
|
| 153 |
+
Path(config.temp_dir).mkdir(parents=True, exist_ok=True)
|
| 154 |
+
|
| 155 |
+
# Estado de treino
|
| 156 |
+
self.global_step = 0
|
| 157 |
+
self.micro_batch_idx = 0
|
| 158 |
+
self.best_loss = float("inf")
|
| 159 |
+
self.losses_history: list[float] = []
|
| 160 |
+
self.killed_reason: Optional[str] = None
|
| 161 |
+
|
| 162 |
+
def _collate(self, samples: list) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 163 |
+
"""Tokeniza samples β (input_ids, attention_mask, labels).
|
| 164 |
+
|
| 165 |
+
Reaproveita collate_samples_to_tensors do streaming_datasets_v13_9.
|
| 166 |
+
"""
|
| 167 |
+
from ..data.streaming_datasets import collate_samples_to_tensors
|
| 168 |
+
pad_id = self.model.config.pad_token_id
|
| 169 |
+
return collate_samples_to_tensors(
|
| 170 |
+
samples, self.tokenizer,
|
| 171 |
+
max_seq_len=self.config.max_seq_len,
|
| 172 |
+
pad_token_id=pad_id,
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
def _forward_loss(
|
| 176 |
+
self,
|
| 177 |
+
input_ids: torch.Tensor,
|
| 178 |
+
attention_mask: torch.Tensor,
|
| 179 |
+
labels: torch.Tensor,
|
| 180 |
+
use_hyp: bool,
|
| 181 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 182 |
+
"""Forward + loss. Retorna (loss_main, loss_hyp_or_zero, y_final).
|
| 183 |
+
|
| 184 |
+
Para LM (next-token prediction):
|
| 185 |
+
- input_ids: (batch, T)
|
| 186 |
+
- labels: (batch, T) β target = input_ids shifted
|
| 187 |
+
- y_hat, delta: (batch, T, vocab_size) β um logit por posiΓ§Γ£o
|
| 188 |
+
- loss = CrossEntropy(y_hat.view(-1, V), labels.view(-1))
|
| 189 |
+
|
| 190 |
+
Mas o UnifiedModel atual produz (batch, vocab_size) β um logit por
|
| 191 |
+
amostra. Para compatibilidade com LM, fazemos:
|
| 192 |
+
- input = input_ids[:, :-1] (T-1 tokens de entrada)
|
| 193 |
+
- target = input_ids[:, 1:] (T-1 targets)
|
| 194 |
+
- Mas o modelo sΓ³ produz 1 logit por amostra, entΓ£o targetamos
|
| 195 |
+
apenas o ΓLTIMO token: target = input_ids[:, -1]
|
| 196 |
+
- loss = CrossEntropy(y_hat, target)
|
| 197 |
+
|
| 198 |
+
Esta Γ© uma simplificaΓ§Γ£o para bug-detection β nΓ£o Γ© um LM completo.
|
| 199 |
+
"""
|
| 200 |
+
# Target = ΓΊltimo token (simplificaΓ§Γ£o para bug-detection)
|
| 201 |
+
target = input_ids[:, -1].clone() # (batch,)
|
| 202 |
+
# Input = todos os tokens (o modelo faz embedding internamente)
|
| 203 |
+
# Passamos input_ids (2D) β o modelo detecta e faz embedding
|
| 204 |
+
|
| 205 |
+
# Forward do modelo
|
| 206 |
+
temperature = self.meta_cfg.temperature
|
| 207 |
+
y_hat, delta = self.model(
|
| 208 |
+
input_ids,
|
| 209 |
+
temperature=temperature,
|
| 210 |
+
use_hypothesis=use_hyp,
|
| 211 |
+
stop_grad_hyp=self.config.stop_grad_hyp,
|
| 212 |
+
return_aux=False,
|
| 213 |
+
)
|
| 214 |
+
# y_hat: (batch, vocab_size), delta: (batch, vocab_size)
|
| 215 |
+
|
| 216 |
+
# Loss principal: CrossEntropy sobre o ΓΊltimo token
|
| 217 |
+
loss_main = F.cross_entropy(y_hat, target, ignore_index=self.model.config.pad_token_id)
|
| 218 |
+
|
| 219 |
+
# Loss de hipΓ³tese: sobre y_final = y_hat + delta
|
| 220 |
+
if use_hyp:
|
| 221 |
+
y_final = y_hat + delta
|
| 222 |
+
loss_hyp = F.cross_entropy(y_final, target, ignore_index=self.model.config.pad_token_id)
|
| 223 |
+
else:
|
| 224 |
+
loss_hyp = torch.zeros((), device=y_hat.device)
|
| 225 |
+
|
| 226 |
+
return loss_main, loss_hyp, y_hat
|
| 227 |
+
|
| 228 |
+
def train(self) -> dict:
|
| 229 |
+
"""Executa treino de bug-detection (2 Γ©pocas).
|
| 230 |
+
|
| 231 |
+
Returns:
|
| 232 |
+
dict com mΓ©tricas finais.
|
| 233 |
+
"""
|
| 234 |
+
cfg = self.config
|
| 235 |
+
n_train = len(self.train_samples)
|
| 236 |
+
n_val = len(self.val_samples)
|
| 237 |
+
logger.info("=" * 70)
|
| 238 |
+
logger.info("BiGRU_T_version β Bug-detection training")
|
| 239 |
+
logger.info("=" * 70)
|
| 240 |
+
logger.info(f" epochs: {cfg.epochs}")
|
| 241 |
+
logger.info(f" train samples: {n_train}")
|
| 242 |
+
logger.info(f" val samples: {n_val}")
|
| 243 |
+
logger.info(f" batch_size: {cfg.per_device_batch_size}")
|
| 244 |
+
logger.info(f" grad_accum: {cfg.grad_accum}")
|
| 245 |
+
logger.info(f" lr: {cfg.lr}")
|
| 246 |
+
logger.info(f" max_seq_len: {cfg.max_seq_len}")
|
| 247 |
+
logger.info(f" use_hypothesis: {cfg.use_hypothesis}")
|
| 248 |
+
logger.info(f" meta_interval: {cfg.meta_interval}")
|
| 249 |
+
logger.info("=" * 70)
|
| 250 |
+
|
| 251 |
+
# Conta parΓ’metros
|
| 252 |
+
params = self.model.count_parameters()
|
| 253 |
+
logger.info(f" total params: {params['total']:,} ({params['total_M']:.2f}M)")
|
| 254 |
+
logger.info(f" trainable: {params['trainable']:,} ({params['trainable_M']:.2f}M)")
|
| 255 |
+
logger.info("=" * 70)
|
| 256 |
+
|
| 257 |
+
t_start = time.time()
|
| 258 |
+
self.model.train()
|
| 259 |
+
|
| 260 |
+
for epoch in range(cfg.epochs):
|
| 261 |
+
logger.info(f"\n--- Epoch {epoch+1}/{cfg.epochs} ---")
|
| 262 |
+
epoch_start = time.time()
|
| 263 |
+
epoch_losses = []
|
| 264 |
+
|
| 265 |
+
# Itera sobre samples (loop circular se n_train < batches necessΓ‘rios)
|
| 266 |
+
sample_idx = 0
|
| 267 |
+
micro_in_epoch = 0
|
| 268 |
+
|
| 269 |
+
while micro_in_epoch < n_train:
|
| 270 |
+
# Pega batch
|
| 271 |
+
batch_samples = []
|
| 272 |
+
for _ in range(cfg.per_device_batch_size):
|
| 273 |
+
s = self.train_samples[sample_idx % n_train]
|
| 274 |
+
batch_samples.append(s)
|
| 275 |
+
sample_idx += 1
|
| 276 |
+
micro_in_epoch += 1
|
| 277 |
+
if micro_in_epoch >= n_train:
|
| 278 |
+
break
|
| 279 |
+
|
| 280 |
+
# Tokeniza
|
| 281 |
+
try:
|
| 282 |
+
input_ids, attn_mask, labels = self._collate(batch_samples)
|
| 283 |
+
except Exception as e:
|
| 284 |
+
logger.warning(f"Tokenize erro micro {self.micro_batch_idx}: {e}")
|
| 285 |
+
continue
|
| 286 |
+
|
| 287 |
+
# Forward + loss
|
| 288 |
+
try:
|
| 289 |
+
# Decide se usa hipΓ³tese baseado no tau atual (Lema 3)
|
| 290 |
+
# No inΓcio, tau=inf, entΓ£o use_hyp=False (nΓ£o hΓ‘ loss_main para comparar)
|
| 291 |
+
# ApΓ³s alguns steps, comparamos loss_main com tau
|
| 292 |
+
use_hyp = cfg.use_hypothesis and (self.global_step > 0)
|
| 293 |
+
if use_hyp:
|
| 294 |
+
# Primeiro forward sem hipΓ³tese para checar loss_main vs tau
|
| 295 |
+
loss_main_pre, _, _ = self._forward_loss(
|
| 296 |
+
input_ids, attn_mask, labels, use_hyp=False
|
| 297 |
+
)
|
| 298 |
+
use_hyp = bool(loss_main_pre.item() > self.meta_cfg.tau)
|
| 299 |
+
|
| 300 |
+
loss_main, loss_hyp, y_hat = self._forward_loss(
|
| 301 |
+
input_ids, attn_mask, labels, use_hyp=use_hyp
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
# Adiciona regularizaΓ§Γ£o de entropia do Lema 1
|
| 305 |
+
# (precisamos re-forward com return_aux para pegar entropy_reg β
|
| 306 |
+
# mas para economizar compute, pegamos via hook)
|
| 307 |
+
# SoluΓ§Γ£o: fazer forward novamente com return_aux (caro mas correto)
|
| 308 |
+
# Alternativa: somar entropy_reg na loss_main sempre que use_hyp=False
|
| 309 |
+
# Aqui optamos por: somar entropy_reg Γ loss_main sempre
|
| 310 |
+
_, _, aux = self.model(
|
| 311 |
+
input_ids,
|
| 312 |
+
temperature=self.meta_cfg.temperature,
|
| 313 |
+
use_hypothesis=False,
|
| 314 |
+
stop_grad_hyp=True,
|
| 315 |
+
return_aux=True,
|
| 316 |
+
)
|
| 317 |
+
loss_main_total = loss_main + aux["entropy_reg"]
|
| 318 |
+
|
| 319 |
+
except RuntimeError as e:
|
| 320 |
+
err = str(e).lower()
|
| 321 |
+
if "out of memory" in err:
|
| 322 |
+
logger.warning(f"OOM micro {self.micro_batch_idx}, limpando")
|
| 323 |
+
gc.collect()
|
| 324 |
+
self.optimizer.zero_grad()
|
| 325 |
+
continue
|
| 326 |
+
else:
|
| 327 |
+
logger.error(f"Runtime erro micro {self.micro_batch_idx}: {e}")
|
| 328 |
+
traceback.print_exc()
|
| 329 |
+
break
|
| 330 |
+
|
| 331 |
+
# Backward + step
|
| 332 |
+
if use_hyp and loss_hyp.requires_grad:
|
| 333 |
+
# Lema 2: gradient surgery
|
| 334 |
+
apply_gradient_surgery(self.model, loss_main_total, loss_hyp)
|
| 335 |
+
# Clip
|
| 336 |
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), cfg.max_grad_norm)
|
| 337 |
+
self.optimizer.step()
|
| 338 |
+
self.optimizer.zero_grad()
|
| 339 |
+
else:
|
| 340 |
+
# Apenas loss_main
|
| 341 |
+
self.optimizer.zero_grad()
|
| 342 |
+
loss_main_total.backward()
|
| 343 |
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), cfg.max_grad_norm)
|
| 344 |
+
self.optimizer.step()
|
| 345 |
+
|
| 346 |
+
self.micro_batch_idx += 1
|
| 347 |
+
|
| 348 |
+
# Log
|
| 349 |
+
current_loss = float(loss_main.item())
|
| 350 |
+
self.losses_history.append(current_loss)
|
| 351 |
+
epoch_losses.append(current_loss)
|
| 352 |
+
if current_loss < self.best_loss:
|
| 353 |
+
self.best_loss = current_loss
|
| 354 |
+
|
| 355 |
+
# Verifica se Γ© um optimizer step (a cada grad_accum micro-batches)
|
| 356 |
+
if self.micro_batch_idx % cfg.grad_accum == 0:
|
| 357 |
+
self.global_step += 1
|
| 358 |
+
|
| 359 |
+
# Kill-switch
|
| 360 |
+
active = self.model.get_active_modules()
|
| 361 |
+
state = self.kill_switch.check(self.global_step, current_loss, active)
|
| 362 |
+
if state.reason:
|
| 363 |
+
logger.error(f"KILL SWITCH triggered: {state.reason}")
|
| 364 |
+
logger.error(
|
| 365 |
+
f" RAM {state.ram_pct:.1f}% | disk free {state.disk_free_gb:.2f}GB | "
|
| 366 |
+
f"ppl {state.ppl:.2f}"
|
| 367 |
+
)
|
| 368 |
+
self.killed_reason = state.reason
|
| 369 |
+
break
|
| 370 |
+
|
| 371 |
+
# Log
|
| 372 |
+
if self.global_step % cfg.log_every == 0 or self.global_step == 1:
|
| 373 |
+
elapsed = time.time() - t_start
|
| 374 |
+
avg_loss = sum(self.losses_history[-cfg.grad_accum:]) / max(
|
| 375 |
+
1, min(cfg.grad_accum, len(self.losses_history))
|
| 376 |
+
)
|
| 377 |
+
ppl = math.exp(min(20, avg_loss)) if avg_loss < 20 else float("inf")
|
| 378 |
+
logger.info(
|
| 379 |
+
f" step {self.global_step} | epoch {epoch+1} | "
|
| 380 |
+
f"loss={avg_loss:.4f} ppl={ppl:.2f} | "
|
| 381 |
+
f"RAM {state.ram_pct:.1f}% disk {state.disk_free_gb:.1f}GB | "
|
| 382 |
+
f"modules={active} | T={self.meta_cfg.temperature:.3f} "
|
| 383 |
+
f"Ο={self.meta_cfg.tau:.3f} | "
|
| 384 |
+
f"elapsed={elapsed:.0f}s"
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
# Checkpoint temporΓ‘rio
|
| 388 |
+
if self.global_step % cfg.save_temp_every == 0:
|
| 389 |
+
self._save_temp_checkpoint(epoch)
|
| 390 |
+
|
| 391 |
+
# Meta-configurator (Lema 4)
|
| 392 |
+
if self.global_step % cfg.meta_interval == 0 and self.val_samples:
|
| 393 |
+
self._run_meta_update()
|
| 394 |
+
|
| 395 |
+
if self.killed_reason:
|
| 396 |
+
break
|
| 397 |
+
|
| 398 |
+
# Fim da Γ©poca
|
| 399 |
+
epoch_avg = sum(epoch_losses) / max(1, len(epoch_losses))
|
| 400 |
+
epoch_ppl = math.exp(min(20, epoch_avg)) if epoch_avg < 20 else float("inf")
|
| 401 |
+
logger.info(
|
| 402 |
+
f" Epoch {epoch+1} done: avg_loss={epoch_avg:.4f} ppl={epoch_ppl:.2f} "
|
| 403 |
+
f"({time.time()-epoch_start:.1f}s)"
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
# Fim do treino
|
| 407 |
+
elapsed_total = time.time() - t_start
|
| 408 |
+
final_loss = self.losses_history[-1] if self.losses_history else float("nan")
|
| 409 |
+
final_avg_loss = (
|
| 410 |
+
sum(self.losses_history[-cfg.grad_accum:]) / max(1, min(cfg.grad_accum, len(self.losses_history)))
|
| 411 |
+
if self.losses_history else float("nan")
|
| 412 |
+
)
|
| 413 |
+
final_ppl = math.exp(min(20, final_avg_loss)) if final_avg_loss < 20 else float("inf")
|
| 414 |
+
ks_summary = self.kill_switch.summary()
|
| 415 |
+
|
| 416 |
+
result = {
|
| 417 |
+
"epochs_completed": epoch + 1 if not self.killed_reason else epoch,
|
| 418 |
+
"killed": self.killed_reason is not None,
|
| 419 |
+
"kill_reason": self.killed_reason,
|
| 420 |
+
"global_step": self.global_step,
|
| 421 |
+
"micro_batches": self.micro_batch_idx,
|
| 422 |
+
"best_loss": self.best_loss,
|
| 423 |
+
"final_loss": final_loss,
|
| 424 |
+
"final_avg_loss": final_avg_loss,
|
| 425 |
+
"final_perplexity": final_ppl,
|
| 426 |
+
"elapsed_s": elapsed_total,
|
| 427 |
+
"params_total": params["total"],
|
| 428 |
+
"params_M": params["total_M"],
|
| 429 |
+
"monitor_summary": ks_summary,
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
logger.info("=" * 70)
|
| 433 |
+
logger.info("TREINO CONCLUΓDO")
|
| 434 |
+
logger.info(f" epochs completed: {result['epochs_completed']}")
|
| 435 |
+
logger.info(f" killed: {result['killed']}")
|
| 436 |
+
if result["killed"]:
|
| 437 |
+
logger.info(f" kill reason: {result['kill_reason']}")
|
| 438 |
+
logger.info(f" global step: {result['global_step']}")
|
| 439 |
+
logger.info(f" best loss: {result['best_loss']:.4f}")
|
| 440 |
+
logger.info(f" final avg loss: {result['final_avg_loss']:.4f}")
|
| 441 |
+
logger.info(f" final perplexity: {result['final_perplexity']:.2f}")
|
| 442 |
+
logger.info(f" elapsed: {result['elapsed_s']:.1f}s")
|
| 443 |
+
logger.info(f" max RAM: {ks_summary.get('max_ram_pct', 0):.1f}%")
|
| 444 |
+
logger.info(f" min disk free: {ks_summary.get('min_disk_free_gb', 0):.2f}GB")
|
| 445 |
+
logger.info("=" * 70)
|
| 446 |
+
|
| 447 |
+
# Salva modelo final
|
| 448 |
+
if not result["killed"]:
|
| 449 |
+
self._save_final_model(result)
|
| 450 |
+
|
| 451 |
+
# Cleanup checkpoints temporΓ‘rios
|
| 452 |
+
if not cfg.keep_temp:
|
| 453 |
+
self._cleanup_temp()
|
| 454 |
+
|
| 455 |
+
return result
|
| 456 |
+
|
| 457 |
+
def _run_meta_update(self):
|
| 458 |
+
"""Executa um passo do MetaConfigurator (Lema 4)."""
|
| 459 |
+
# Pega um batch de validaΓ§Γ£o
|
| 460 |
+
val_batch = []
|
| 461 |
+
for i in range(self.config.per_device_batch_size):
|
| 462 |
+
s = self.val_samples[i % len(self.val_samples)]
|
| 463 |
+
val_batch.append(s)
|
| 464 |
+
try:
|
| 465 |
+
input_ids, attn_mask, labels = self._collate(val_batch)
|
| 466 |
+
# Target = ΓΊltimo token (mesma lΓ³gica de _forward_loss)
|
| 467 |
+
target = input_ids[:, -1].clone() # (batch,)
|
| 468 |
+
meta_result = self.meta_cfg.forward_with_meta(input_ids, target)
|
| 469 |
+
logger.info(
|
| 470 |
+
f" META step {self.global_step}: "
|
| 471 |
+
f"val_loss={meta_result['loss_val']:.4f} "
|
| 472 |
+
f"sharpness={meta_result['sharpness']:.4f} "
|
| 473 |
+
f"T={meta_result['T_new']:.4f} Ο={meta_result['tau_new']:.4f}"
|
| 474 |
+
)
|
| 475 |
+
except Exception as e:
|
| 476 |
+
logger.warning(f"Meta update failed: {e}")
|
| 477 |
+
|
| 478 |
+
def _save_temp_checkpoint(self, epoch: int):
|
| 479 |
+
"""Salva checkpoint temporΓ‘rio (serΓ‘ apagado ao final)."""
|
| 480 |
+
path = Path(self.config.temp_dir) / f"temp_step{self.global_step}_epoch{epoch+1}.pt"
|
| 481 |
+
torch.save({
|
| 482 |
+
"model_state_dict": self.model.state_dict(),
|
| 483 |
+
"optimizer_state_dict": self.optimizer.state_dict(),
|
| 484 |
+
"global_step": self.global_step,
|
| 485 |
+
"epoch": epoch,
|
| 486 |
+
"best_loss": self.best_loss,
|
| 487 |
+
"meta_log_T": self.meta_cfg.log_temperature.detach().cpu(),
|
| 488 |
+
"meta_log_tau": self.meta_cfg.log_tau.detach().cpu(),
|
| 489 |
+
}, path)
|
| 490 |
+
|
| 491 |
+
def _save_final_model(self, result: dict):
|
| 492 |
+
"""Salva modelo final + config + tokenizer."""
|
| 493 |
+
model_dir = Path(self.config.output_dir)
|
| 494 |
+
model_dir.mkdir(parents=True, exist_ok=True)
|
| 495 |
+
|
| 496 |
+
# State dict
|
| 497 |
+
state_path = model_dir / "pytorch_model.bin"
|
| 498 |
+
torch.save(self.model.state_dict(), state_path)
|
| 499 |
+
logger.info(f"Modelo final salvo: {state_path}")
|
| 500 |
+
|
| 501 |
+
# Config
|
| 502 |
+
cfg = self.model.config
|
| 503 |
+
config_dict = {
|
| 504 |
+
"model_type": "bigru_t_version",
|
| 505 |
+
"arch": "BiGRU_T_version (4 lemas)",
|
| 506 |
+
"vocab_size": cfg.vocab_size,
|
| 507 |
+
"d_model": cfg.d_model,
|
| 508 |
+
"max_seq_len": cfg.max_seq_len,
|
| 509 |
+
"pad_token_id": cfg.pad_token_id,
|
| 510 |
+
"input_dim": cfg.input_dim,
|
| 511 |
+
"bigru_hidden": cfg.bigru_hidden,
|
| 512 |
+
"d_transformer": cfg.d_transformer,
|
| 513 |
+
"nhead_tu": cfg.nhead_tu,
|
| 514 |
+
"d_ff_tu": cfg.d_ff_tu,
|
| 515 |
+
"output_dim_u8cell": cfg.output_dim_u8cell,
|
| 516 |
+
"max_modules": cfg.max_modules,
|
| 517 |
+
"lambda_ent": cfg.lambda_ent,
|
| 518 |
+
"cache_len": cfg.cache_len,
|
| 519 |
+
"d_cache": cfg.d_cache,
|
| 520 |
+
"nhead_orq": cfg.nhead_orq,
|
| 521 |
+
"d_ff_orq": cfg.d_ff_orq,
|
| 522 |
+
"trainT_dim": cfg.trainT_dim,
|
| 523 |
+
"nhead_train": cfg.nhead_train,
|
| 524 |
+
"d_ff_train": cfg.d_ff_train,
|
| 525 |
+
"num_layers_train": cfg.num_layers_train,
|
| 526 |
+
"hypT_dim": cfg.hypT_dim,
|
| 527 |
+
"nhead_hyp": cfg.nhead_hyp,
|
| 528 |
+
"d_ff_hyp": cfg.d_ff_hyp,
|
| 529 |
+
"num_layers_hyp": cfg.num_layers_hyp,
|
| 530 |
+
"num_bits": cfg.num_bits,
|
| 531 |
+
"dropout": cfg.dropout,
|
| 532 |
+
"total_params": result["params_total"],
|
| 533 |
+
"total_params_M": result["params_M"],
|
| 534 |
+
"training": {
|
| 535 |
+
"version": "BiGRU_T_version",
|
| 536 |
+
"epochs": result["epochs_completed"],
|
| 537 |
+
"global_step": result["global_step"],
|
| 538 |
+
"best_loss": result["best_loss"],
|
| 539 |
+
"final_loss": result["final_loss"],
|
| 540 |
+
"final_avg_loss": result["final_avg_loss"],
|
| 541 |
+
"final_perplexity": result["final_perplexity"],
|
| 542 |
+
"killed": result["killed"],
|
| 543 |
+
"kill_reason": result["kill_reason"],
|
| 544 |
+
"elapsed_s": result["elapsed_s"],
|
| 545 |
+
"datasets": self.config.datasets.split(","),
|
| 546 |
+
"max_samples_per_dataset": self.config.max_samples_per_dataset,
|
| 547 |
+
},
|
| 548 |
+
}
|
| 549 |
+
with open(model_dir / "config.json", "w") as f:
|
| 550 |
+
json.dump(config_dict, f, indent=2, ensure_ascii=False)
|
| 551 |
+
|
| 552 |
+
# Tokenizer (copia do source)
|
| 553 |
+
src_tok = Path("/home/z/my-project/source/model_final/tokenizer/tokenizer.json")
|
| 554 |
+
if src_tok.exists():
|
| 555 |
+
tok_dest = model_dir / "tokenizer"
|
| 556 |
+
tok_dest.mkdir(exist_ok=True)
|
| 557 |
+
shutil.copy(src_tok, tok_dest / "tokenizer.json")
|
| 558 |
+
logger.info(f"Tokenizer copiado para {tok_dest}")
|
| 559 |
+
|
| 560 |
+
# Training report
|
| 561 |
+
report = {
|
| 562 |
+
"version": "BiGRU_T_version",
|
| 563 |
+
"training_params": {
|
| 564 |
+
"epochs": self.config.epochs,
|
| 565 |
+
"per_device_batch_size": self.config.per_device_batch_size,
|
| 566 |
+
"grad_accum": self.config.grad_accum,
|
| 567 |
+
"lr": self.config.lr,
|
| 568 |
+
"weight_decay": self.config.weight_decay,
|
| 569 |
+
"max_grad_norm": self.config.max_grad_norm,
|
| 570 |
+
"max_seq_len": self.config.max_seq_len,
|
| 571 |
+
"use_hypothesis": self.config.use_hypothesis,
|
| 572 |
+
"stop_grad_hyp": self.config.stop_grad_hyp,
|
| 573 |
+
"meta_interval": self.config.meta_interval,
|
| 574 |
+
"meta_lr": self.config.meta_lr,
|
| 575 |
+
"sharpness_lambda": self.config.sharpness_lambda,
|
| 576 |
+
},
|
| 577 |
+
"results": result,
|
| 578 |
+
}
|
| 579 |
+
report_path = Path(self.config.output_dir).parent / "training_report.json"
|
| 580 |
+
with open(report_path, "w") as f:
|
| 581 |
+
json.dump(report, f, indent=2, ensure_ascii=False, default=str)
|
| 582 |
+
logger.info(f"RelatΓ³rio salvo: {report_path}")
|
| 583 |
+
|
| 584 |
+
def _cleanup_temp(self):
|
| 585 |
+
"""Apaga checkpoints temporΓ‘rios (especificaΓ§Γ£o do usuΓ‘rio)."""
|
| 586 |
+
temp_dir = Path(self.config.temp_dir)
|
| 587 |
+
if not temp_dir.exists():
|
| 588 |
+
return
|
| 589 |
+
for f in temp_dir.glob("temp_step*.pt"):
|
| 590 |
+
try:
|
| 591 |
+
f.unlink()
|
| 592 |
+
logger.info(f"Temp checkpoint apagado: {f.name}")
|
| 593 |
+
except Exception as e:
|
| 594 |
+
logger.warning(f"Falha ao apagar {f}: {e}")
|
| 595 |
+
# Apaga o diretΓ³rio se vazio
|
| 596 |
+
try:
|
| 597 |
+
temp_dir.rmdir()
|
| 598 |
+
except Exception:
|
| 599 |
+
pass
|
src/bigru_t/utils/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""UtilitΓ‘rios (reaproveitados do source v13.9.2)."""
|
| 2 |
+
# ImportaΓ§Γ΅es lazy para evitar dependΓͺncias circulares
|
src/bigru_t/utils/hardware_detector.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
hardware_detector β Auto-detecΓ§Γ£o de hardware e otimizaΓ§Γ£o para PyTorch.
|
| 3 |
+
|
| 4 |
+
Detecta automaticamente:
|
| 5 |
+
- CPU (sempre disponΓvel)
|
| 6 |
+
- CUDA (GPU NVIDIA)
|
| 7 |
+
- MPS (Apple Silicon)
|
| 8 |
+
- XPU (Intel)
|
| 9 |
+
- NΓΊmero Γ³timo de threads
|
| 10 |
+
- MemΓ³ria disponΓvel
|
| 11 |
+
- Docker/Container detection (optXeon ex. 3, 4)
|
| 12 |
+
- Intel Xeon detection (optXeon)
|
| 13 |
+
- AVX-512/VNNI/AMX flags (optXeon ex. 2)
|
| 14 |
+
- IPEX availability (optXeon ex. 6)
|
| 15 |
+
|
| 16 |
+
Aplica otimizaΓ§Γ΅es:
|
| 17 |
+
- torch.set_num_threads() otimizado (cgroups v2 para Docker)
|
| 18 |
+
- OMP_PROC_BIND=CLOSE, OMP_PLACES=CORES (Xeon Docker)
|
| 19 |
+
- torch.backends.mkldnn.enabled = True (Intel oneDNN)
|
| 20 |
+
- torch.set_num_interop_threads(1) (crΓtico em Docker)
|
| 21 |
+
- torch.backends.cudnn.benchmark = True (se GPU)
|
| 22 |
+
- Mixed precision (torch.cuda.amp) se GPU
|
| 23 |
+
- CPU AMP (torch.cpu.amp.autocast) para Xeon (optXeon ex. 6, 7)
|
| 24 |
+
|
| 25 |
+
Uso:
|
| 26 |
+
from flexnet.hardware_detector import HardwareDetector, get_device, to_device
|
| 27 |
+
device = get_device() # torch.device('cuda' | 'mps' | 'cpu')
|
| 28 |
+
model = model.to(device)
|
| 29 |
+
batch = to_device(batch, device)
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
from __future__ import annotations
|
| 33 |
+
import os
|
| 34 |
+
import sys
|
| 35 |
+
import gc
|
| 36 |
+
import math
|
| 37 |
+
import platform
|
| 38 |
+
import subprocess
|
| 39 |
+
from typing import Dict, Any, Optional, Tuple
|
| 40 |
+
|
| 41 |
+
import torch
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _get_real_cores_docker() -> int:
|
| 45 |
+
"""Detecta cores reais disponΓveis em Docker (optXeon ex. 3).
|
| 46 |
+
|
| 47 |
+
Em containers Docker (cgroups v2), os.cpu_count() pode retornar
|
| 48 |
+
mais cores do que o container tem. Esta funΓ§Γ£o resolve isso.
|
| 49 |
+
"""
|
| 50 |
+
cores = os.cpu_count() or 1
|
| 51 |
+
|
| 52 |
+
if not os.path.exists("/.dockerenv"):
|
| 53 |
+
return cores
|
| 54 |
+
|
| 55 |
+
# cpuset: lista de cores alocados
|
| 56 |
+
cpuset_path = "/sys/fs/cgroup/cpuset.cpus"
|
| 57 |
+
if os.path.exists(cpuset_path):
|
| 58 |
+
try:
|
| 59 |
+
with open(cpuset_path, "r") as f:
|
| 60 |
+
conteudo = f.read().strip()
|
| 61 |
+
if conteudo:
|
| 62 |
+
cores_lista = []
|
| 63 |
+
for parte in conteudo.split(","):
|
| 64 |
+
if "-" in parte:
|
| 65 |
+
inicio, fim = map(int, parte.split("-"))
|
| 66 |
+
cores_lista.extend(range(inicio, fim + 1))
|
| 67 |
+
else:
|
| 68 |
+
cores_lista.append(int(parte))
|
| 69 |
+
if cores_lista:
|
| 70 |
+
return len(cores_lista)
|
| 71 |
+
except Exception:
|
| 72 |
+
pass
|
| 73 |
+
|
| 74 |
+
# cgroups v2 quota/period
|
| 75 |
+
quota_path = "/sys/fs/cgroup/cpu.max"
|
| 76 |
+
if os.path.exists(quota_path):
|
| 77 |
+
try:
|
| 78 |
+
with open(quota_path, "r") as f:
|
| 79 |
+
valores = f.read().strip().split()
|
| 80 |
+
if len(valores) == 2 and valores[0] != "max":
|
| 81 |
+
quota = int(valores[0])
|
| 82 |
+
periodo = int(valores[1])
|
| 83 |
+
return max(1, math.ceil(quota / periodo))
|
| 84 |
+
except Exception:
|
| 85 |
+
pass
|
| 86 |
+
|
| 87 |
+
return cores
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _detect_xeon_cpu() -> Tuple[str, bool]:
|
| 91 |
+
"""Detecta modelo da CPU e se Γ© Intel Xeon."""
|
| 92 |
+
nome_cpu = "Desconhecido"
|
| 93 |
+
try:
|
| 94 |
+
with open("/proc/cpuinfo", "r", encoding="utf-8") as f:
|
| 95 |
+
for line in f:
|
| 96 |
+
if "model name" in line.lower():
|
| 97 |
+
nome_cpu = line.split(":")[-1].strip()
|
| 98 |
+
break
|
| 99 |
+
except Exception:
|
| 100 |
+
nome_cpu = platform.processor()
|
| 101 |
+
|
| 102 |
+
is_xeon = "xeon" in nome_cpu.lower() or "intel" in nome_cpu.lower()
|
| 103 |
+
return nome_cpu, is_xeon
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _detect_avx512_vnni_amx() -> Tuple[bool, bool, bool]:
|
| 107 |
+
"""Detecta AVX-512, VNNI, AMX flags (optXeon ex. 2)."""
|
| 108 |
+
has_avx512 = False
|
| 109 |
+
has_vnni = False
|
| 110 |
+
has_amx = False
|
| 111 |
+
try:
|
| 112 |
+
with open("/proc/cpuinfo", "r", encoding="utf-8") as f:
|
| 113 |
+
for line in f:
|
| 114 |
+
if "flags" in line.lower():
|
| 115 |
+
flags = line.split()
|
| 116 |
+
has_avx512 = "avx512f" in flags
|
| 117 |
+
has_vnni = "avx512vnni" in flags
|
| 118 |
+
has_amx = "amx_int8" in flags
|
| 119 |
+
break
|
| 120 |
+
except Exception:
|
| 121 |
+
pass
|
| 122 |
+
return has_avx512, has_vnni, has_amx
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def _detect_ipex() -> bool:
|
| 126 |
+
"""Detecta Intel Extension for PyTorch (optXeon ex. 6)."""
|
| 127 |
+
try:
|
| 128 |
+
import intel_extension_for_pytorch as ipex
|
| 129 |
+
return True
|
| 130 |
+
except ImportError:
|
| 131 |
+
return False
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
class HardwareDetector:
|
| 135 |
+
"""Auto-detecΓ§Γ£o de hardware e otimizaΓ§Γ£o para PyTorch.
|
| 136 |
+
|
| 137 |
+
Aprimorado (optXeon):
|
| 138 |
+
- Docker/Container detection
|
| 139 |
+
- Xeon CPU detection + thread config
|
| 140 |
+
- AVX-512/VNNI/AMX detection
|
| 141 |
+
- IPEX availability
|
| 142 |
+
- Xeon thread optimization (OMP_PROC_BIND=CLOSE, mkldnn)
|
| 143 |
+
"""
|
| 144 |
+
|
| 145 |
+
_instance: Optional['HardwareDetector'] = None
|
| 146 |
+
_device: Optional[torch.device] = None
|
| 147 |
+
_info: Optional[Dict] = None
|
| 148 |
+
|
| 149 |
+
@classmethod
|
| 150 |
+
def get_instance(cls) -> 'HardwareDetector':
|
| 151 |
+
if cls._instance is None:
|
| 152 |
+
cls._instance = cls()
|
| 153 |
+
return cls._instance
|
| 154 |
+
|
| 155 |
+
def __init__(self):
|
| 156 |
+
self._detect()
|
| 157 |
+
|
| 158 |
+
def _detect(self):
|
| 159 |
+
"""Detecta hardware disponΓvel."""
|
| 160 |
+
# Docker/Xeon/AVX-512 detection (optXeon)
|
| 161 |
+
cpu_name, is_xeon = _detect_xeon_cpu()
|
| 162 |
+
has_avx512, has_vnni, has_amx = _detect_avx512_vnni_amx()
|
| 163 |
+
is_docker = os.path.exists("/.dockerenv")
|
| 164 |
+
cores_real = _get_real_cores_docker()
|
| 165 |
+
has_ipex = _detect_ipex()
|
| 166 |
+
|
| 167 |
+
self._info = {
|
| 168 |
+
'python_version': sys.version.split()[0],
|
| 169 |
+
'gil_enabled': sys._is_gil_enabled() if hasattr(sys, '_is_gil_enabled') else None,
|
| 170 |
+
'torch_version': torch.__version__,
|
| 171 |
+
'thp_mem_alloc': os.environ.get('THP_MEM_ALLOC_ENABLE', '0'),
|
| 172 |
+
'cpu_model_name': cpu_name,
|
| 173 |
+
'is_xeon': is_xeon,
|
| 174 |
+
'is_docker': is_docker,
|
| 175 |
+
'cores_real': cores_real,
|
| 176 |
+
'has_avx512': has_avx512,
|
| 177 |
+
'has_vnni': has_vnni,
|
| 178 |
+
'has_amx': has_amx,
|
| 179 |
+
'has_bf16_native': has_avx512 or is_xeon,
|
| 180 |
+
'has_ipex': has_ipex,
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
# Detectar device.
|
| 184 |
+
if torch.cuda.is_available():
|
| 185 |
+
self._device = torch.device('cuda')
|
| 186 |
+
self._info['device'] = 'cuda'
|
| 187 |
+
self._info['cuda_device_count'] = torch.cuda.device_count()
|
| 188 |
+
self._info['cuda_device_name'] = torch.cuda.get_device_name(0)
|
| 189 |
+
props = torch.cuda.get_device_properties(0)
|
| 190 |
+
self._info['cuda_memory_total_gb'] = round(props.total_memory / 1e9, 1)
|
| 191 |
+
self._info['cuda_compute_capability'] = f"{props.major}.{props.minor}"
|
| 192 |
+
torch.backends.cudnn.benchmark = True
|
| 193 |
+
torch.backends.cudnn.deterministic = False
|
| 194 |
+
elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
|
| 195 |
+
self._device = torch.device('mps')
|
| 196 |
+
self._info['device'] = 'mps'
|
| 197 |
+
else:
|
| 198 |
+
self._device = torch.device('cpu')
|
| 199 |
+
self._info['device'] = 'cpu'
|
| 200 |
+
|
| 201 |
+
# CPU info.
|
| 202 |
+
self._info['cpu_count'] = os.cpu_count()
|
| 203 |
+
self._info['torch_threads'] = torch.get_num_threads()
|
| 204 |
+
|
| 205 |
+
# Xeon thread optimization (optXeon ex. 3, 4)
|
| 206 |
+
if is_xeon and self._device.type == 'cpu':
|
| 207 |
+
threads_target = min(cores_real, 16) # max_threads=16
|
| 208 |
+
if threads_target > 16:
|
| 209 |
+
threads_target = 16
|
| 210 |
+
|
| 211 |
+
# OMP/MKL thread configuration
|
| 212 |
+
os.environ["OMP_NUM_THREADS"] = str(threads_target)
|
| 213 |
+
os.environ["MKL_NUM_THREADS"] = str(threads_target)
|
| 214 |
+
os.environ["OPENBLAS_NUM_THREADS"] = str(threads_target)
|
| 215 |
+
os.environ["VECLIB_MAXIMUM_THREADS"] = str(threads_target)
|
| 216 |
+
os.environ["NUMEXPR_NUM_THREADS"] = str(threads_target)
|
| 217 |
+
|
| 218 |
+
# Afinidade compacta (optXeon ex. 4)
|
| 219 |
+
if is_docker:
|
| 220 |
+
os.environ["OMP_PROC_BIND"] = "CLOSE"
|
| 221 |
+
os.environ["OMP_PLACES"] = "CORES"
|
| 222 |
+
|
| 223 |
+
torch.set_num_threads(threads_target)
|
| 224 |
+
torch.set_num_interop_threads(1) # CrΓtico no Docker (optXeon ex. 4)
|
| 225 |
+
torch.backends.mkldnn.enabled = True
|
| 226 |
+
|
| 227 |
+
self._info['xeon_threads_configured'] = threads_target
|
| 228 |
+
logger_msg = f"Xeon threads: {threads_target}, OMP_PROC_BIND={os.environ.get('OMP_PROC_BIND', 'N/A')}"
|
| 229 |
+
elif self._device.type == 'cpu':
|
| 230 |
+
# Non-Xeon CPU optimization
|
| 231 |
+
if not sys._is_gil_enabled() if hasattr(sys, '_is_gil_enabled') else False:
|
| 232 |
+
torch.set_num_threads(os.cpu_count())
|
| 233 |
+
else:
|
| 234 |
+
torch.set_num_threads(max(1, os.cpu_count() // 2))
|
| 235 |
+
self._info['torch_threads_optimized'] = torch.get_num_threads()
|
| 236 |
+
|
| 237 |
+
# MemΓ³ria.
|
| 238 |
+
try:
|
| 239 |
+
import psutil
|
| 240 |
+
vm = psutil.virtual_memory()
|
| 241 |
+
self._info['ram_total_gb'] = round(vm.total / 1e9, 1)
|
| 242 |
+
self._info['ram_available_gb'] = round(vm.available / 1e9, 1)
|
| 243 |
+
except ImportError:
|
| 244 |
+
self._info['ram_total_gb'] = None
|
| 245 |
+
|
| 246 |
+
@property
|
| 247 |
+
def device(self) -> torch.device:
|
| 248 |
+
return self._device
|
| 249 |
+
|
| 250 |
+
@property
|
| 251 |
+
def info(self) -> Dict:
|
| 252 |
+
return self._info
|
| 253 |
+
|
| 254 |
+
@property
|
| 255 |
+
def is_gpu(self) -> bool:
|
| 256 |
+
return self._device.type in ('cuda', 'mps')
|
| 257 |
+
|
| 258 |
+
@property
|
| 259 |
+
def is_cuda(self) -> bool:
|
| 260 |
+
return self._device.type == 'cuda'
|
| 261 |
+
|
| 262 |
+
@property
|
| 263 |
+
def is_xeon(self) -> bool:
|
| 264 |
+
return self._info.get('is_xeon', False)
|
| 265 |
+
|
| 266 |
+
@property
|
| 267 |
+
def has_avx512(self) -> bool:
|
| 268 |
+
return self._info.get('has_avx512', False)
|
| 269 |
+
|
| 270 |
+
@property
|
| 271 |
+
def has_ipex(self) -> bool:
|
| 272 |
+
return self._info.get('has_ipex', False)
|
| 273 |
+
|
| 274 |
+
@property
|
| 275 |
+
def is_docker(self) -> bool:
|
| 276 |
+
return self._info.get('is_docker', False)
|
| 277 |
+
|
| 278 |
+
@property
|
| 279 |
+
def is_free_threaded(self) -> bool:
|
| 280 |
+
return hasattr(sys, '_is_gil_enabled') and not sys._is_gil_enabled()
|
| 281 |
+
|
| 282 |
+
def to_device(self, obj):
|
| 283 |
+
"""Move tensor, model, ou dict de tensors para o device detectado."""
|
| 284 |
+
if isinstance(obj, torch.Tensor):
|
| 285 |
+
return obj.to(self._device)
|
| 286 |
+
elif isinstance(obj, torch.nn.Module):
|
| 287 |
+
return obj.to(self._device)
|
| 288 |
+
elif isinstance(obj, dict):
|
| 289 |
+
return {k: self.to_device(v) for k, v in obj.items()}
|
| 290 |
+
elif isinstance(obj, (list, tuple)):
|
| 291 |
+
return type(obj)(self.to_device(v) for v in obj)
|
| 292 |
+
return obj
|
| 293 |
+
|
| 294 |
+
def cleanup(self):
|
| 295 |
+
"""Limpeza de memΓ³ria otimizada para o device detectado."""
|
| 296 |
+
gc.collect(0)
|
| 297 |
+
gc.collect(1)
|
| 298 |
+
gc.collect(2)
|
| 299 |
+
if self.is_cuda:
|
| 300 |
+
torch.cuda.empty_cache()
|
| 301 |
+
torch.cuda.synchronize()
|
| 302 |
+
|
| 303 |
+
def get_mixed_precision_context(self):
|
| 304 |
+
"""Retorna context manager para mixed precision (se GPU ou Xeon CPU)."""
|
| 305 |
+
if self.is_cuda:
|
| 306 |
+
return torch.cuda.amp.autocast()
|
| 307 |
+
# Xeon CPU: BFloat16 via AVX-512 (optXeon ex. 6, 7)
|
| 308 |
+
if self.is_xeon and self.has_avx512:
|
| 309 |
+
try:
|
| 310 |
+
return torch.cpu.amp.autocast(dtype=torch.bfloat16)
|
| 311 |
+
except AttributeError:
|
| 312 |
+
pass
|
| 313 |
+
# CPU genΓ©rico: context nulo.
|
| 314 |
+
from contextlib import nullcontext
|
| 315 |
+
return nullcontext()
|
| 316 |
+
|
| 317 |
+
def optimize_for_inference(self, model: torch.nn.Module) -> torch.nn.Module:
|
| 318 |
+
"""Otimiza modelo para inferΓͺncia.
|
| 319 |
+
|
| 320 |
+
Aprimorado (optXeon):
|
| 321 |
+
- IPEX optimization para Xeon
|
| 322 |
+
- torch.compile(mode="reduce-overhead") para AVX-512
|
| 323 |
+
- IPEX JIT trace para ultra-baixa latΓͺncia
|
| 324 |
+
"""
|
| 325 |
+
model = model.to(self._device)
|
| 326 |
+
model.eval()
|
| 327 |
+
|
| 328 |
+
# IPEX optimization (optXeon ex. 6, 8)
|
| 329 |
+
if self.has_ipex and self._device.type == 'cpu':
|
| 330 |
+
try:
|
| 331 |
+
import intel_extension_for_pytorch as ipex
|
| 332 |
+
model = ipex.optimize(model, dtype=torch.bfloat16, inplace=True)
|
| 333 |
+
# JIT trace para inferΓͺncia (optXeon ex. 8)
|
| 334 |
+
logger.info("IPEX: modelo otimizado para inferΓͺncia (BFloat16 + prepacking)")
|
| 335 |
+
except Exception:
|
| 336 |
+
pass
|
| 337 |
+
|
| 338 |
+
# torch.compile se disponΓvel (PyTorch 2.0+)
|
| 339 |
+
if hasattr(torch, 'compile'):
|
| 340 |
+
try:
|
| 341 |
+
compile_mode = "reduce-overhead" if self.has_avx512 else "default"
|
| 342 |
+
model = torch.compile(model, mode=compile_mode)
|
| 343 |
+
except Exception:
|
| 344 |
+
pass
|
| 345 |
+
return model
|
| 346 |
+
|
| 347 |
+
def get_stats(self) -> Dict:
|
| 348 |
+
"""Retorna stats de hardware em tempo real."""
|
| 349 |
+
stats = dict(self._info)
|
| 350 |
+
try:
|
| 351 |
+
import psutil
|
| 352 |
+
p = psutil.Process()
|
| 353 |
+
stats['rss_mb'] = round(p.memory_info().rss / 1e6, 1)
|
| 354 |
+
except ImportError:
|
| 355 |
+
stats['rss_mb'] = 0.0
|
| 356 |
+
if self.is_cuda:
|
| 357 |
+
stats['cuda_allocated_mb'] = round(torch.cuda.memory_allocated() / 1e6, 1)
|
| 358 |
+
stats['cuda_reserved_mb'] = round(torch.cuda.memory_reserved() / 1e6, 1)
|
| 359 |
+
return stats
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
# ============================================================
|
| 363 |
+
# FunΓ§Γ΅es de conveniΓͺncia
|
| 364 |
+
# ============================================================
|
| 365 |
+
|
| 366 |
+
def get_device() -> torch.device:
|
| 367 |
+
"""Retorna o device detectado (cuda | mps | cpu)."""
|
| 368 |
+
return HardwareDetector.get_instance().device
|
| 369 |
+
|
| 370 |
+
def to_device(obj):
|
| 371 |
+
"""Move tensor/model/dict para o device detectado."""
|
| 372 |
+
return HardwareDetector.get_instance().to_device(obj)
|
| 373 |
+
|
| 374 |
+
def cleanup():
|
| 375 |
+
"""Limpeza de memΓ³ria otimizada."""
|
| 376 |
+
HardwareDetector.get_instance().cleanup()
|
| 377 |
+
|
| 378 |
+
def get_hardware_info() -> Dict:
|
| 379 |
+
"""Retorna info de hardware."""
|
| 380 |
+
return HardwareDetector.get_instance().info
|
| 381 |
+
|
| 382 |
+
def is_free_threaded() -> bool:
|
| 383 |
+
"""Verifica se Python estΓ‘ em modo free-threaded (GIL off)."""
|
| 384 |
+
return HardwareDetector.get_instance().is_free_threaded
|
| 385 |
+
|
| 386 |
+
def is_gpu() -> bool:
|
| 387 |
+
"""Verifica se GPU estΓ‘ disponΓvel."""
|
| 388 |
+
return HardwareDetector.get_instance().is_gpu
|
| 389 |
+
|
| 390 |
+
def is_xeon() -> bool:
|
| 391 |
+
"""Verifica se CPU Γ© Intel Xeon (optXeon)."""
|
| 392 |
+
return HardwareDetector.get_instance().is_xeon
|
| 393 |
+
|
| 394 |
+
def has_avx512() -> bool:
|
| 395 |
+
"""Verifica se CPU suporta AVX-512 (optXeon)."""
|
| 396 |
+
return HardwareDetector.get_instance().has_avx512
|
| 397 |
+
|
| 398 |
+
def has_ipex() -> bool:
|
| 399 |
+
"""Verifica se IPEX estΓ‘ disponΓvel (optXeon)."""
|
| 400 |
+
return HardwareDetector.get_instance().has_ipex
|
| 401 |
+
|
| 402 |
+
def is_docker() -> bool:
|
| 403 |
+
"""Verifica se estΓ‘ rodando em Docker (optXeon)."""
|
| 404 |
+
return HardwareDetector.get_instance().is_docker
|
| 405 |
+
|
| 406 |
+
def optimize_for_inference(model: torch.nn.Module) -> torch.nn.Module:
|
| 407 |
+
"""Otimiza modelo para inferΓͺncia no device detectado."""
|
| 408 |
+
return HardwareDetector.get_instance().optimize_for_inference(model)
|
src/bigru_t/utils/logging_utils.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Xavante - logging_utils.py - Logging estruturado com contexto."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
import logging
|
| 6 |
+
import sys
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class JSONFormatter(logging.Formatter):
|
| 12 |
+
def format(self, record: logging.LogRecord) -> str:
|
| 13 |
+
log_entry = {
|
| 14 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 15 |
+
"level": record.levelname,
|
| 16 |
+
"logger": record.name,
|
| 17 |
+
"message": record.getMessage(),
|
| 18 |
+
}
|
| 19 |
+
if hasattr(record, "context"):
|
| 20 |
+
log_entry["context"] = record.context
|
| 21 |
+
return json.dumps(log_entry, default=str)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def setup_logging(level: int = logging.INFO, json_output: bool = False) -> None:
|
| 25 |
+
root = logging.getLogger()
|
| 26 |
+
root.setLevel(level)
|
| 27 |
+
for h in list(root.handlers):
|
| 28 |
+
root.removeHandler(h)
|
| 29 |
+
handler = logging.StreamHandler(sys.stdout)
|
| 30 |
+
if json_output:
|
| 31 |
+
handler.setFormatter(JSONFormatter())
|
| 32 |
+
else:
|
| 33 |
+
handler.setFormatter(
|
| 34 |
+
logging.Formatter(
|
| 35 |
+
"[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s",
|
| 36 |
+
datefmt="%Y-%m-%d %H:%M:%S",
|
| 37 |
+
)
|
| 38 |
+
)
|
| 39 |
+
root.addHandler(handler)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
__all__ = ["JSONFormatter", "setup_logging"]
|
src/bigru_t/utils/memory_monitor.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
memory_monitor β DetecΓ§Γ£o avanΓ§ada de uso de memΓ³ria.
|
| 3 |
+
|
| 4 |
+
Implementa (item 1):
|
| 5 |
+
- RSS tracking (CPU)
|
| 6 |
+
- GPU memory tracking (allocated/reserved/peak)
|
| 7 |
+
- Leak detection (crescimento monotΓ΄nico)
|
| 8 |
+
-OOM prevention (pre-allocation check)
|
| 9 |
+
- Memory budgeting (per-module)
|
| 10 |
+
- Snapshot diffing (antes/depois de operaΓ§Γ£o)
|
| 11 |
+
- Alerting (warning/critical thresholds)
|
| 12 |
+
|
| 13 |
+
DetecΓ§Γ£o de erros:
|
| 14 |
+
- MemoryLeakError: crescimento monotΓ΄nico em N amostras consecutivas
|
| 15 |
+
- MemoryBudgetExceededError: mΓ³dulo excede quota
|
| 16 |
+
- OOMPredictedError: projeΓ§Γ£o indica OOM em K passos
|
| 17 |
+
- SnapshotMismatchError: snapshot diff inesperado
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import gc
|
| 23 |
+
import os
|
| 24 |
+
import time
|
| 25 |
+
import threading
|
| 26 |
+
import warnings
|
| 27 |
+
from collections import defaultdict, deque
|
| 28 |
+
from contextlib import contextmanager
|
| 29 |
+
from dataclasses import dataclass, field
|
| 30 |
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
| 31 |
+
|
| 32 |
+
import torch
|
| 33 |
+
import torch.nn as nn
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# ============================================================================
|
| 37 |
+
# ExceΓ§Γ΅es especΓficas
|
| 38 |
+
# ============================================================================
|
| 39 |
+
|
| 40 |
+
class MemoryLeakError(RuntimeError):
|
| 41 |
+
"""Crescimento monotΓ΄nico de memΓ³ria detectado (possΓvel leak)."""
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class MemoryBudgetExceededError(RuntimeError):
|
| 45 |
+
"""MΓ³dulo excedeu sua quota de memΓ³ria."""
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class OOMPredictedError(RuntimeError):
|
| 49 |
+
"""ProjeΓ§Γ£o indica OOM em K passos."""
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class SnapshotMismatchError(RuntimeError):
|
| 53 |
+
"""Snapshot diff inesperado (alocaΓ§Γ£o nΓ£o liberada)."""
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# ============================================================================
|
| 57 |
+
# 1. MemorySnapshot β captura estado de memΓ³ria em um instante
|
| 58 |
+
# ============================================================================
|
| 59 |
+
|
| 60 |
+
@dataclass
|
| 61 |
+
class MemorySnapshot:
|
| 62 |
+
"""Snapshot completo de uso de memΓ³ria."""
|
| 63 |
+
timestamp: float
|
| 64 |
+
rss_bytes: int = 0 # CPU RSS (via psutil)
|
| 65 |
+
cpu_percent: float = 0.0 # % do total de RAM
|
| 66 |
+
gpu_allocated: int = 0 # GPU allocated (torch)
|
| 67 |
+
gpu_reserved: int = 0 # GPU reserved (torch)
|
| 68 |
+
gpu_peak: int = 0 # GPU peak (torch)
|
| 69 |
+
gpu_total: int = 0 # GPU total (device property)
|
| 70 |
+
label: str = ""
|
| 71 |
+
|
| 72 |
+
def diff(self, other: "MemorySnapshot") -> "MemorySnapshotDiff":
|
| 73 |
+
"""Calcula diff (self - other): positivo = crescimento."""
|
| 74 |
+
return MemorySnapshotDiff(
|
| 75 |
+
rss_delta=self.rss_bytes - other.rss_bytes,
|
| 76 |
+
gpu_allocated_delta=self.gpu_allocated - other.gpu_allocated,
|
| 77 |
+
gpu_reserved_delta=self.gpu_reserved - other.gpu_reserved,
|
| 78 |
+
time_delta=self.timestamp - other.timestamp,
|
| 79 |
+
from_label=other.label,
|
| 80 |
+
to_label=self.label,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@dataclass
|
| 85 |
+
class MemorySnapshotDiff:
|
| 86 |
+
"""DiferenΓ§a entre dois MemorySnapshots."""
|
| 87 |
+
rss_delta: int
|
| 88 |
+
gpu_allocated_delta: int
|
| 89 |
+
gpu_reserved_delta: int
|
| 90 |
+
time_delta: float
|
| 91 |
+
from_label: str
|
| 92 |
+
to_label: str
|
| 93 |
+
|
| 94 |
+
def __repr__(self) -> str:
|
| 95 |
+
return (
|
| 96 |
+
f"MemoryDiff({self.from_label}β{self.to_label}: "
|
| 97 |
+
f"rss={self.rss_delta:+d}B, "
|
| 98 |
+
f"gpu_alloc={self.gpu_allocated_delta:+d}B, "
|
| 99 |
+
f"dt={self.time_delta:.3f}s)"
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
# ============================================================================
|
| 104 |
+
# 2. MemoryMonitor β monitora memΓ³ria em runtime
|
| 105 |
+
# ============================================================================
|
| 106 |
+
|
| 107 |
+
class MemoryMonitor:
|
| 108 |
+
"""Monitor de uso de memΓ³ria com leak detection.
|
| 109 |
+
|
| 110 |
+
Recursos:
|
| 111 |
+
* Snapshot a qualquer momento (capture_snapshot)
|
| 112 |
+
* Diff entre snapshots (snapshot.diff)
|
| 113 |
+
* HistΓ³rico circular (history_size amostras)
|
| 114 |
+
* Leak detection: se rss cresce em N amostras consecutivas β alerta
|
| 115 |
+
* OOM prediction: projeta tendΓͺncia linear, previne OOM
|
| 116 |
+
* Budgeting: cada mΓ³dulo tem quota; exceder β MemoryBudgetExceededError
|
| 117 |
+
* Auto-cleanup: gc.collect() + torch.cuda.empty_cache() quando crΓtico
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
def __init__(
|
| 121 |
+
self,
|
| 122 |
+
history_size: int = 100,
|
| 123 |
+
leak_threshold_consecutive: int = 10,
|
| 124 |
+
leak_threshold_bytes: int = 1024 * 1024, # 1 MB
|
| 125 |
+
warning_pct: float = 80.0,
|
| 126 |
+
critical_pct: float = 95.0,
|
| 127 |
+
auto_cleanup: bool = True,
|
| 128 |
+
):
|
| 129 |
+
self.history_size = history_size
|
| 130 |
+
self.leak_threshold_consecutive = leak_threshold_consecutive
|
| 131 |
+
self.leak_threshold_bytes = leak_threshold_bytes
|
| 132 |
+
self.warning_pct = warning_pct
|
| 133 |
+
self.critical_pct = critical_pct
|
| 134 |
+
self.auto_cleanup = auto_cleanup
|
| 135 |
+
|
| 136 |
+
self._history: deque = deque(maxlen=history_size)
|
| 137 |
+
self._budgets: Dict[str, int] = {} # module_name -> max_bytes
|
| 138 |
+
self._lock = threading.Lock()
|
| 139 |
+
|
| 140 |
+
# Tenta importar psutil (opcional).
|
| 141 |
+
try:
|
| 142 |
+
import psutil
|
| 143 |
+
self._psutil = psutil
|
| 144 |
+
except ImportError:
|
| 145 |
+
self._psutil = None
|
| 146 |
+
|
| 147 |
+
def capture_snapshot(self, label: str = "") -> MemorySnapshot:
|
| 148 |
+
"""Captura snapshot atual de memΓ³ria."""
|
| 149 |
+
snap = MemorySnapshot(timestamp=time.time(), label=label)
|
| 150 |
+
# CPU.
|
| 151 |
+
if self._psutil is not None:
|
| 152 |
+
try:
|
| 153 |
+
process = self._psutil.Process()
|
| 154 |
+
mem = process.memory_info()
|
| 155 |
+
snap.rss_bytes = mem.rss
|
| 156 |
+
vm = self._psutil.virtual_memory()
|
| 157 |
+
snap.cpu_percent = snap.rss_bytes / vm.total * 100.0 if vm.total > 0 else 0.0
|
| 158 |
+
except Exception:
|
| 159 |
+
pass
|
| 160 |
+
# GPU.
|
| 161 |
+
if torch.cuda.is_available():
|
| 162 |
+
try:
|
| 163 |
+
snap.gpu_allocated = torch.cuda.memory_allocated()
|
| 164 |
+
snap.gpu_reserved = torch.cuda.memory_reserved()
|
| 165 |
+
snap.gpu_peak = torch.cuda.max_memory_allocated()
|
| 166 |
+
snap.gpu_total = torch.cuda.get_device_properties(0).total_memory
|
| 167 |
+
except Exception:
|
| 168 |
+
pass
|
| 169 |
+
with self._lock:
|
| 170 |
+
self._history.append(snap)
|
| 171 |
+
return snap
|
| 172 |
+
|
| 173 |
+
def get_history(self) -> List[MemorySnapshot]:
|
| 174 |
+
with self._lock:
|
| 175 |
+
return list(self._history)
|
| 176 |
+
|
| 177 |
+
def check_leak(self) -> Optional[MemoryLeakError]:
|
| 178 |
+
"""Verifica leak: crescimento monotΓ΄nico em N amostras consecutivas.
|
| 179 |
+
|
| 180 |
+
Returns: None se OK, MemoryLeakError se leak detectado.
|
| 181 |
+
"""
|
| 182 |
+
with self._lock:
|
| 183 |
+
history = list(self._history)
|
| 184 |
+
if len(history) < self.leak_threshold_consecutive + 1:
|
| 185 |
+
return None
|
| 186 |
+
# Pega ΓΊltimas N amostras.
|
| 187 |
+
recent = history[-(self.leak_threshold_consecutive + 1):]
|
| 188 |
+
# Verifica crescimento monotΓ΄nico em rss ou gpu_allocated.
|
| 189 |
+
rss_growing = all(
|
| 190 |
+
recent[i+1].rss_bytes - recent[i].rss_bytes > self.leak_threshold_bytes
|
| 191 |
+
for i in range(len(recent) - 1)
|
| 192 |
+
) and recent[-1].rss_bytes > recent[0].rss_bytes
|
| 193 |
+
gpu_growing = all(
|
| 194 |
+
recent[i+1].gpu_allocated - recent[i].gpu_allocated > self.leak_threshold_bytes
|
| 195 |
+
for i in range(len(recent) - 1)
|
| 196 |
+
) and recent[-1].gpu_allocated > recent[0].gpu_allocated
|
| 197 |
+
if rss_growing:
|
| 198 |
+
total_growth = recent[-1].rss_bytes - recent[0].rss_bytes
|
| 199 |
+
return MemoryLeakError(
|
| 200 |
+
f"PossΓvel leak de RSS: crescimento monotΓ΄nico de "
|
| 201 |
+
f"{total_growth / 1e6:.1f} MB em {len(recent)} amostras"
|
| 202 |
+
)
|
| 203 |
+
if gpu_growing:
|
| 204 |
+
total_growth = recent[-1].gpu_allocated - recent[0].gpu_allocated
|
| 205 |
+
return MemoryLeakError(
|
| 206 |
+
f"PossΓvel leak de GPU: crescimento monotΓ΄nico de "
|
| 207 |
+
f"{total_growth / 1e6:.1f} MB em {len(recent)} amostras"
|
| 208 |
+
)
|
| 209 |
+
return None
|
| 210 |
+
|
| 211 |
+
def predict_oom(self, k_steps: int = 10) -> Optional[OOMPredictedError]:
|
| 212 |
+
"""Projeta tendΓͺncia linear; se projeta OOM em k_steps, alerta.
|
| 213 |
+
|
| 214 |
+
Usa regressΓ£o linear simples nos ΓΊltimos min(20, len) snapshots.
|
| 215 |
+
"""
|
| 216 |
+
with self._lock:
|
| 217 |
+
history = list(self._history)
|
| 218 |
+
if len(history) < 5:
|
| 219 |
+
return None
|
| 220 |
+
recent = history[-min(20, len(history)):]
|
| 221 |
+
n = len(recent)
|
| 222 |
+
# RegressΓ£o linear: y = a + b*t, onde y = rss_bytes, t = Γndice.
|
| 223 |
+
ts = list(range(n))
|
| 224 |
+
ys_rss = [s.rss_bytes for s in recent]
|
| 225 |
+
ys_gpu = [s.gpu_allocated for s in recent]
|
| 226 |
+
# Calcula b (slope) via least squares.
|
| 227 |
+
mean_t = sum(ts) / n
|
| 228 |
+
mean_y_rss = sum(ys_rss) / n
|
| 229 |
+
mean_y_gpu = sum(ys_gpu) / n
|
| 230 |
+
num_rss = sum((t - mean_t) * (y - mean_y_rss) for t, y in zip(ts, ys_rss))
|
| 231 |
+
num_gpu = sum((t - mean_t) * (y - mean_y_gpu) for t, y in zip(ts, ys_gpu))
|
| 232 |
+
den = sum((t - mean_t) ** 2 for t in ts) or 1
|
| 233 |
+
slope_rss = num_rss / den
|
| 234 |
+
slope_gpu = num_gpu / den
|
| 235 |
+
# Projeta.
|
| 236 |
+
proj_rss = ys_rss[-1] + slope_rss * k_steps
|
| 237 |
+
proj_gpu = ys_gpu[-1] + slope_gpu * k_steps
|
| 238 |
+
# Limites.
|
| 239 |
+
total_ram = 0
|
| 240 |
+
if self._psutil is not None:
|
| 241 |
+
try:
|
| 242 |
+
total_ram = self._psutil.virtual_memory().total
|
| 243 |
+
except Exception:
|
| 244 |
+
pass
|
| 245 |
+
total_gpu = recent[-1].gpu_total if recent[-1].gpu_total > 0 else 0
|
| 246 |
+
warnings_list = []
|
| 247 |
+
if total_ram > 0 and proj_rss > self.critical_pct / 100.0 * total_ram:
|
| 248 |
+
warnings_list.append(
|
| 249 |
+
f"RSS projetado {proj_rss / 1e9:.2f} GB em {k_steps} passos "
|
| 250 |
+
f"(>{self.critical_pct}% de {total_ram / 1e9:.2f} GB)"
|
| 251 |
+
)
|
| 252 |
+
if total_gpu > 0 and proj_gpu > self.critical_pct / 100.0 * total_gpu:
|
| 253 |
+
warnings_list.append(
|
| 254 |
+
f"GPU projetado {proj_gpu / 1e9:.2f} GB em {k_steps} passos "
|
| 255 |
+
f"(>{self.critical_pct}% de {total_gpu / 1e9:.2f} GB)"
|
| 256 |
+
)
|
| 257 |
+
if warnings_list:
|
| 258 |
+
return OOMPredictedError("; ".join(warnings_list))
|
| 259 |
+
return None
|
| 260 |
+
|
| 261 |
+
def set_budget(self, module_name: str, max_bytes: int) -> None:
|
| 262 |
+
"""Define quota de memΓ³ria para um mΓ³dulo."""
|
| 263 |
+
self._budgets[module_name] = max_bytes
|
| 264 |
+
|
| 265 |
+
def check_budget(self, module_name: str, current_bytes: int) -> Optional[MemoryBudgetExceededError]:
|
| 266 |
+
"""Verifica se mΓ³dulo excedeu quota."""
|
| 267 |
+
if module_name not in self._budgets:
|
| 268 |
+
return None
|
| 269 |
+
max_b = self._budgets[module_name]
|
| 270 |
+
if current_bytes > max_b:
|
| 271 |
+
return MemoryBudgetExceededError(
|
| 272 |
+
f"MΓ³dulo '{module_name}' excedeu quota: "
|
| 273 |
+
f"{current_bytes / 1e6:.1f} MB > {max_b / 1e6:.1f} MB"
|
| 274 |
+
)
|
| 275 |
+
return None
|
| 276 |
+
|
| 277 |
+
def check_thresholds(self) -> Optional[str]:
|
| 278 |
+
"""Verifica se uso atual excede warning/critical thresholds."""
|
| 279 |
+
snap = self.capture_snapshot()
|
| 280 |
+
if snap.cpu_percent > self.critical_pct:
|
| 281 |
+
if self.auto_cleanup:
|
| 282 |
+
self.cleanup()
|
| 283 |
+
return f"CRITICAL: CPU RSS {snap.cpu_percent:.1f}%"
|
| 284 |
+
if snap.gpu_total > 0:
|
| 285 |
+
gpu_pct = snap.gpu_allocated / snap.gpu_total * 100
|
| 286 |
+
if gpu_pct > self.critical_pct:
|
| 287 |
+
if self.auto_cleanup:
|
| 288 |
+
self.cleanup()
|
| 289 |
+
return f"CRITICAL: GPU {gpu_pct:.1f}%"
|
| 290 |
+
if gpu_pct > self.warning_pct:
|
| 291 |
+
return f"WARNING: GPU {gpu_pct:.1f}%"
|
| 292 |
+
if snap.cpu_percent > self.warning_pct:
|
| 293 |
+
return f"WARNING: CPU RSS {snap.cpu_percent:.1f}%"
|
| 294 |
+
return None
|
| 295 |
+
|
| 296 |
+
def cleanup(self) -> None:
|
| 297 |
+
"""Limpa caches (gc + CUDA empty_cache)."""
|
| 298 |
+
gc.collect()
|
| 299 |
+
if torch.cuda.is_available():
|
| 300 |
+
torch.cuda.empty_cache()
|
| 301 |
+
|
| 302 |
+
def summary(self) -> str:
|
| 303 |
+
"""Resumo do estado atual."""
|
| 304 |
+
snap = self.capture_snapshot()
|
| 305 |
+
lines = [
|
| 306 |
+
f"MemoryMonitor:",
|
| 307 |
+
f" RSS: {snap.rss_bytes / 1e6:.1f} MB ({snap.cpu_percent:.1f}%)",
|
| 308 |
+
]
|
| 309 |
+
if snap.gpu_total > 0:
|
| 310 |
+
gpu_pct = snap.gpu_allocated / snap.gpu_total * 100
|
| 311 |
+
lines.append(f" GPU allocated: {snap.gpu_allocated / 1e6:.1f} MB ({gpu_pct:.1f}%)")
|
| 312 |
+
lines.append(f" GPU reserved: {snap.gpu_reserved / 1e6:.1f} MB")
|
| 313 |
+
lines.append(f" GPU peak: {snap.gpu_peak / 1e6:.1f} MB")
|
| 314 |
+
lines.append(f" History: {len(self._history)} samples")
|
| 315 |
+
lines.append(f" Budgets: {len(self._budgets)} modules")
|
| 316 |
+
return "\n".join(lines)
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
# ============================================================================
|
| 320 |
+
# 3. Context manager para snapshot diff
|
| 321 |
+
# ============================================================================
|
| 322 |
+
|
| 323 |
+
@contextmanager
|
| 324 |
+
def memory_diff(monitor: MemoryMonitor, label: str = "operation"):
|
| 325 |
+
"""Context manager que captura diff de memΓ³ria antes/depois.
|
| 326 |
+
|
| 327 |
+
Uso:
|
| 328 |
+
>>> with memory_diff(monitor, "forward_pass") as diff:
|
| 329 |
+
... y = model(X)
|
| 330 |
+
>>> print(diff) # MemorySnapshotDiff
|
| 331 |
+
"""
|
| 332 |
+
before = monitor.capture_snapshot(label=f"{label}_before")
|
| 333 |
+
diff_holder: Dict[str, MemorySnapshotDiff] = {}
|
| 334 |
+
try:
|
| 335 |
+
yield diff_holder
|
| 336 |
+
finally:
|
| 337 |
+
after = monitor.capture_snapshot(label=f"{label}_after")
|
| 338 |
+
diff_holder["diff"] = after.diff(before)
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
# ============================================================================
|
| 342 |
+
# 4. ModuleMemoryProfiler β profile de memΓ³ria por mΓ³dulo
|
| 343 |
+
# ============================================================================
|
| 344 |
+
|
| 345 |
+
class ModuleMemoryProfiler:
|
| 346 |
+
"""Profile de memΓ³ria por mΓ³dulo nn.Module.
|
| 347 |
+
|
| 348 |
+
Registra pico de memΓ³ria durante forward de cada mΓ³dulo.
|
| 349 |
+
Γtil para identificar mΓ³dulos que consomem muita memΓ³ria.
|
| 350 |
+
"""
|
| 351 |
+
|
| 352 |
+
def __init__(self, monitor: Optional[MemoryMonitor] = None):
|
| 353 |
+
self.monitor = monitor or MemoryMonitor()
|
| 354 |
+
self.profiles: Dict[str, MemorySnapshotDiff] = {}
|
| 355 |
+
|
| 356 |
+
def profile_forward(
|
| 357 |
+
self,
|
| 358 |
+
module: nn.Module,
|
| 359 |
+
X: torch.Tensor,
|
| 360 |
+
module_name: str = "",
|
| 361 |
+
) -> MemorySnapshotDiff:
|
| 362 |
+
"""Executa forward e captura diff de memΓ³ria."""
|
| 363 |
+
name = module_name or module.__class__.__name__
|
| 364 |
+
before = self.monitor.capture_snapshot(label=f"{name}_before")
|
| 365 |
+
try:
|
| 366 |
+
with torch.no_grad():
|
| 367 |
+
_ = module(X)
|
| 368 |
+
except Exception as e:
|
| 369 |
+
warnings.warn(f"Forward falhou para {name}: {e}")
|
| 370 |
+
after = self.monitor.capture_snapshot(label=f"{name}_after")
|
| 371 |
+
diff = after.diff(before)
|
| 372 |
+
self.profiles[name] = diff
|
| 373 |
+
return diff
|
| 374 |
+
|
| 375 |
+
def get_profile(self, module_name: str) -> Optional[MemorySnapshotDiff]:
|
| 376 |
+
return self.profiles.get(module_name)
|
| 377 |
+
|
| 378 |
+
def summary(self) -> str:
|
| 379 |
+
if not self.profiles:
|
| 380 |
+
return "ModuleMemoryProfiler: sem profiles"
|
| 381 |
+
lines = ["ModuleMemoryProfiler:"]
|
| 382 |
+
# Ordena por RSS delta decrescente.
|
| 383 |
+
sorted_profiles = sorted(
|
| 384 |
+
self.profiles.items(),
|
| 385 |
+
key=lambda x: x[1].rss_delta,
|
| 386 |
+
reverse=True,
|
| 387 |
+
)
|
| 388 |
+
for name, diff in sorted_profiles:
|
| 389 |
+
lines.append(f" {name}: {diff}")
|
| 390 |
+
return "\n".join(lines)
|
src/bigru_t/utils/oom_guard.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""oom_guard.py β Guarda anti-OOM-kernel para Xavante V11.23.
|
| 2 |
+
|
| 3 |
+
Monitora VmRSS em thread separada. Quando a memΓ³ria se aproxima do limite:
|
| 4 |
+
1. gc.collect() agressivo
|
| 5 |
+
2. torch.cuda.empty_cache() (se GPU)
|
| 6 |
+
3. torch.cpu.empty_cache() (se disponΓvel)
|
| 7 |
+
4. Log de emergΓͺncia para stderr
|
| 8 |
+
"""
|
| 9 |
+
import gc, sys, threading, time, torch
|
| 10 |
+
|
| 11 |
+
class OomGuard:
|
| 12 |
+
def __init__(self, max_rss_mb=3200, warn_rss_mb=2700, check_interval=2.0):
|
| 13 |
+
self.max_rss_mb = max_rss_mb
|
| 14 |
+
self.warn_rss_mb = warn_rss_mb
|
| 15 |
+
self.check_interval = check_interval
|
| 16 |
+
self._running = False
|
| 17 |
+
self._thread = None
|
| 18 |
+
self._peak_rss = 0
|
| 19 |
+
self._gc_count = 0
|
| 20 |
+
self._emergency_count = 0
|
| 21 |
+
self._status = {"status": "idle"}
|
| 22 |
+
|
| 23 |
+
def _get_rss_mb(self):
|
| 24 |
+
try:
|
| 25 |
+
with open("/proc/self/status") as f:
|
| 26 |
+
for line in f:
|
| 27 |
+
if line.startswith("VmRSS:"): return int(line.split()[1]) / 1024
|
| 28 |
+
except Exception: pass
|
| 29 |
+
return 0.0
|
| 30 |
+
|
| 31 |
+
def _monitor_loop(self):
|
| 32 |
+
while self._running:
|
| 33 |
+
rss = self._get_rss_mb()
|
| 34 |
+
if rss > self._peak_rss: self._peak_rss = rss
|
| 35 |
+
if rss > self.max_rss_mb:
|
| 36 |
+
self._emergency_count += 1
|
| 37 |
+
gc.collect()
|
| 38 |
+
if torch.cuda.is_available(): torch.cuda.empty_cache()
|
| 39 |
+
sys.stderr.write(f"\n[OOM-GUARD] EMERGENCIA: RSS={rss:.0f}MB > {self.max_rss_mb}MB. gc #{self._emergency_count}\n")
|
| 40 |
+
sys.stderr.flush()
|
| 41 |
+
self._status = {"status": "emergency", "rss_mb": rss, "gc_count": self._emergency_count}
|
| 42 |
+
elif rss > self.warn_rss_mb:
|
| 43 |
+
self._gc_count += 1
|
| 44 |
+
gc.collect()
|
| 45 |
+
self._status = {"status": "warning", "rss_mb": rss}
|
| 46 |
+
else:
|
| 47 |
+
self._status = {"status": "ok", "rss_mb": rss}
|
| 48 |
+
time.sleep(self.check_interval)
|
| 49 |
+
|
| 50 |
+
def start(self):
|
| 51 |
+
self._running = True
|
| 52 |
+
self._thread = threading.Thread(target=self._monitor_loop, daemon=True)
|
| 53 |
+
self._thread.start()
|
| 54 |
+
|
| 55 |
+
def stop(self):
|
| 56 |
+
self._running = False
|
| 57 |
+
if self._thread: self._thread.join(timeout=5)
|
| 58 |
+
|
| 59 |
+
def get_stats(self):
|
| 60 |
+
return {"peak_rss_mb": self._peak_rss, "current_rss_mb": self._get_rss_mb(),
|
| 61 |
+
"gc_count": self._gc_count, "emergency_count": self._emergency_count,
|
| 62 |
+
**self._status}
|
src/bigru_t/utils/tensor_ops.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Xavante - tensor_ops.py - Operacoes tensoriais otimizadas."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import logging
|
| 5 |
+
import math
|
| 6 |
+
from typing import Tuple
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def safe_softmax(x: torch.Tensor, dim: int = -1) -> torch.Tensor:
|
| 14 |
+
"""Softmax com subtracao do max para estabilidade numerica."""
|
| 15 |
+
return torch.softmax(x - x.amax(dim=dim, keepdim=True), dim=dim)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def safe_log(x: torch.Tensor, eps: float = 1e-9) -> torch.Tensor:
|
| 19 |
+
return torch.log(x.clamp_min(eps))
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def cosine_similarity(a: torch.Tensor, b: torch.Tensor, dim: int = -1, eps: float = 1e-8) -> torch.Tensor:
|
| 23 |
+
return torch.nn.functional.cosine_similarity(a, b, dim=dim, eps=eps)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def orthogonal_init(weight: torch.Tensor) -> None:
|
| 27 |
+
"""Inicializa weight como matriz ortogonal."""
|
| 28 |
+
nn_init = torch.nn.init
|
| 29 |
+
nn_init.orthogonal_(weight)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def skew_to_rotation(skew: torch.Tensor) -> torch.Tensor:
|
| 33 |
+
"""Converte matriz skew-symmetric para rotation via Rodrigues."""
|
| 34 |
+
n = skew.shape[0]
|
| 35 |
+
I = torch.eye(n, device=skew.device, dtype=skew.dtype)
|
| 36 |
+
A = skew
|
| 37 |
+
A2 = A @ A
|
| 38 |
+
# Exponential via series truncada
|
| 39 |
+
return I + A + 0.5 * A2 + (1.0 / 6.0) * (A2 @ A)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def chunked_matmul(a: torch.Tensor, b: torch.Tensor, chunk: int = 4096) -> torch.Tensor:
|
| 43 |
+
"""Matmul em chunks para evitar OOM com matrizes grandes."""
|
| 44 |
+
if a.shape[1] <= chunk:
|
| 45 |
+
return a @ b
|
| 46 |
+
out_chunks = []
|
| 47 |
+
for i in range(0, a.shape[0], chunk):
|
| 48 |
+
out_chunks.append(a[i : i + chunk] @ b)
|
| 49 |
+
return torch.cat(out_chunks, dim=0)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def padding_mask(lengths: torch.Tensor, max_len: int) -> torch.Tensor:
|
| 53 |
+
"""Cria mascara bool [B, max_len] onde True = token valido."""
|
| 54 |
+
idx = torch.arange(max_len, device=lengths.device).unsqueeze(0)
|
| 55 |
+
return idx < lengths.unsqueeze(1)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def causal_mask(seq_len: int, device: torch.device) -> torch.Tensor:
|
| 59 |
+
"""Mascara causal [seq_len, seq_len] (1 = permitido)."""
|
| 60 |
+
return torch.tril(torch.ones(seq_len, seq_len, device=device))
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
__all__ = [
|
| 64 |
+
"safe_softmax",
|
| 65 |
+
"safe_log",
|
| 66 |
+
"cosine_similarity",
|
| 67 |
+
"orthogonal_init",
|
| 68 |
+
"skew_to_rotation",
|
| 69 |
+
"chunked_matmul",
|
| 70 |
+
"padding_mask",
|
| 71 |
+
"causal_mask",
|
| 72 |
+
]
|
src/bigru_t/utils/validators.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Xavante - validators.py - Validadores de entrada e estado."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import logging
|
| 5 |
+
from typing import Tuple
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def validate_tensor_shape(x: torch.Tensor, expected: Tuple[int, ...], name: str = "tensor") -> bool:
|
| 13 |
+
if x.dim() != len(expected):
|
| 14 |
+
logger.error("%s dim mismatch: got %d expected %d", name, x.dim(), len(expected))
|
| 15 |
+
return False
|
| 16 |
+
for i, (got, exp) in enumerate(zip(x.shape, expected)):
|
| 17 |
+
if exp != -1 and got != exp:
|
| 18 |
+
logger.error("%s shape[%d] mismatch: got %d expected %d", name, i, got, exp)
|
| 19 |
+
return False
|
| 20 |
+
return True
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def validate_no_nan_inf(x: torch.Tensor, name: str = "tensor") -> bool:
|
| 24 |
+
if torch.isnan(x).any():
|
| 25 |
+
logger.error("%s contains NaN", name)
|
| 26 |
+
return False
|
| 27 |
+
if torch.isinf(x).any():
|
| 28 |
+
logger.error("%s contains Inf", name)
|
| 29 |
+
return False
|
| 30 |
+
return True
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def validate_device_consistency(tensors: list, device: torch.device) -> bool:
|
| 34 |
+
for i, t in enumerate(tensors):
|
| 35 |
+
if t.device != device:
|
| 36 |
+
logger.error("Tensor %d em device %s, esperado %s", i, t.device, device)
|
| 37 |
+
return False
|
| 38 |
+
return True
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def clamp_probabilities(p: torch.Tensor, eps: float = 1e-9) -> torch.Tensor:
|
| 42 |
+
return p.clamp(min=eps, max=1.0 - eps)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
__all__ = [
|
| 46 |
+
"validate_tensor_shape",
|
| 47 |
+
"validate_no_nan_inf",
|
| 48 |
+
"validate_device_consistency",
|
| 49 |
+
"clamp_probabilities",
|
| 50 |
+
]
|
src/bigru_t/utils/xeon_runtime.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""xeon_runtime.py β Intel Xeon auto-adaptive runtime configuration (V13.3).
|
| 2 |
+
|
| 3 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 4 |
+
CONTEXT
|
| 5 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 6 |
+
|
| 7 |
+
The user requested activating `numactl` in the virtual environment to enable
|
| 8 |
+
oneDNN and MKL, configured according to the number of available cores.
|
| 9 |
+
Without root, we cannot install numactl binary, but we can achieve the same
|
| 10 |
+
effect via environment variables β this is exactly what `numactl --cpunodebind`
|
| 11 |
+
and `numactl --membind` do internally: they set Linux scheduler affinity and
|
| 12 |
+
hint to Intel libraries (MKL, OpenMP, oneDNN) how many threads to spawn and
|
| 13 |
+
how to bind them.
|
| 14 |
+
|
| 15 |
+
This module is the Python equivalent of:
|
| 16 |
+
numactl --cpunodebind=0 --localalloc ./training_script.py
|
| 17 |
+
|
| 18 |
+
combined with optimal MKL/oneDNN/OpenMP tuning for Xeon AVX512 CPUs.
|
| 19 |
+
|
| 20 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 21 |
+
USAGE
|
| 22 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 23 |
+
|
| 24 |
+
Call `optimize_xeon_environment()` ONCE at process startup, BEFORE importing
|
| 25 |
+
torch / onnxruntime. The function returns the number of physical cores
|
| 26 |
+
allocated; downstream code can use this to size thread pools.
|
| 27 |
+
|
| 28 |
+
from flexnet.xeon_runtime import optimize_xeon_environment
|
| 29 |
+
N_CORES = optimize_xeon_environment()
|
| 30 |
+
# ... now safe to import torch, onnxruntime, etc.
|
| 31 |
+
|
| 32 |
+
For ONNX Runtime inference sessions, use `make_ort_session_options()`:
|
| 33 |
+
|
| 34 |
+
from flexnet.xeon_runtime import make_ort_session_options
|
| 35 |
+
sess = ort.InferenceSession(model_path, sess_options=make_ort_session_options(),
|
| 36 |
+
providers=['CPUExecutionProvider'])
|
| 37 |
+
|
| 38 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 39 |
+
"""
|
| 40 |
+
from __future__ import annotations
|
| 41 |
+
import os
|
| 42 |
+
import sys
|
| 43 |
+
import logging
|
| 44 |
+
from typing import Optional, Tuple
|
| 45 |
+
|
| 46 |
+
logger = logging.getLogger(__name__)
|
| 47 |
+
|
| 48 |
+
_NUCLEOS_ALOCADOS: Optional[int] = None
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _detect_physical_cores() -> int:
|
| 52 |
+
"""Detect physical cores actually available to this process.
|
| 53 |
+
|
| 54 |
+
Respects cgroup limits without requiring root. Falls back to logical
|
| 55 |
+
cpu count if psutil is unavailable.
|
| 56 |
+
"""
|
| 57 |
+
try:
|
| 58 |
+
import psutil
|
| 59 |
+
n_phys = psutil.cpu_count(logical=False) or 1
|
| 60 |
+
except ImportError:
|
| 61 |
+
# /proc/cpuinfo fallback
|
| 62 |
+
try:
|
| 63 |
+
with open("/proc/cpuinfo", "r") as f:
|
| 64 |
+
cores = set()
|
| 65 |
+
for line in f:
|
| 66 |
+
if line.startswith("core id"):
|
| 67 |
+
cores.add(line.strip())
|
| 68 |
+
n_phys = len(cores) or 1
|
| 69 |
+
except OSError:
|
| 70 |
+
n_phys = 1
|
| 71 |
+
|
| 72 |
+
# Honor Linux scheduler affinity (Docker container CPU limits)
|
| 73 |
+
try:
|
| 74 |
+
n_affine = len(os.sched_getaffinity(0))
|
| 75 |
+
n_logical = os.cpu_count() or 1
|
| 76 |
+
# If container is limited to fewer CPUs than physical machine has
|
| 77 |
+
# logical cores, derive physical cores from affinity.
|
| 78 |
+
if n_affine < n_logical:
|
| 79 |
+
n_phys = max(1, n_affine // 2)
|
| 80 |
+
else:
|
| 81 |
+
n_phys = min(n_phys, n_affine)
|
| 82 |
+
except (AttributeError, OSError):
|
| 83 |
+
pass
|
| 84 |
+
|
| 85 |
+
return max(1, n_phys)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def optimize_xeon_environment(
|
| 89 |
+
verbose: bool = True,
|
| 90 |
+
) -> int:
|
| 91 |
+
"""Configure Intel OneDNN / MKL / OpenMP for current Xeon host.
|
| 92 |
+
|
| 93 |
+
Idempotent: safe to call multiple times. Returns the number of physical
|
| 94 |
+
cores that will be used by downstream libraries.
|
| 95 |
+
|
| 96 |
+
Args:
|
| 97 |
+
verbose: print configuration summary to stdout
|
| 98 |
+
|
| 99 |
+
Returns:
|
| 100 |
+
Number of physical cores allocated to this process.
|
| 101 |
+
"""
|
| 102 |
+
global _NUCLEOS_ALOCADOS
|
| 103 |
+
if _NUCLEOS_ALOCADOS is not None:
|
| 104 |
+
return _NUCLEOS_ALOCADOS
|
| 105 |
+
|
| 106 |
+
n_phys = _detect_physical_cores()
|
| 107 |
+
|
| 108 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 109 |
+
# OpenMP / MKL thread counts (oneDNN reads these too)
|
| 110 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½οΏ½ββββββββββββ
|
| 111 |
+
os.environ.setdefault("OMP_NUM_THREADS", str(n_phys))
|
| 112 |
+
os.environ.setdefault("MKL_NUM_THREADS", str(n_phys))
|
| 113 |
+
|
| 114 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 115 |
+
# KMP_AFFINITY: fine-grained thread pinning. Format:
|
| 116 |
+
# granularity=fine,compact,1,0
|
| 117 |
+
# Means: pin each OpenMP thread to a distinct physical core, pack threads
|
| 118 |
+
# onto cores compactly (better L2/L3 locality), respect the implicit
|
| 119 |
+
# core-to-thread mapping (1,0).
|
| 120 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 121 |
+
os.environ.setdefault("KMP_AFFINITY", "granularity=fine,compact,1,0")
|
| 122 |
+
# KMP_BLOCKTIME=1ms: spin-wait briefly before parking threads β best for
|
| 123 |
+
# short-lived parallel regions (matmul, attention).
|
| 124 |
+
os.environ.setdefault("KMP_BLOCKTIME", "1")
|
| 125 |
+
|
| 126 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 127 |
+
# PyTorch: enable MKL-DNN (a.k.a. oneDNN) for fp32/bf16/int8 fast paths.
|
| 128 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 129 |
+
try:
|
| 130 |
+
import torch
|
| 131 |
+
torch.set_num_threads(n_phys)
|
| 132 |
+
torch.set_num_interop_threads(1)
|
| 133 |
+
if hasattr(torch.backends, "mkldnn"):
|
| 134 |
+
torch.backends.mkldnn.enabled = True
|
| 135 |
+
# Prefer flexpath that doesn't fall back to native CPU unnecessarily.
|
| 136 |
+
if hasattr(torch.backends, "quantized"):
|
| 137 |
+
try:
|
| 138 |
+
torch.backends.quantized.engine = "fbgemm"
|
| 139 |
+
except (RuntimeError, AttributeError):
|
| 140 |
+
pass
|
| 141 |
+
except ImportError:
|
| 142 |
+
pass
|
| 143 |
+
|
| 144 |
+
_NUCLEOS_ALOCADOS = n_phys
|
| 145 |
+
|
| 146 |
+
if verbose:
|
| 147 |
+
print(f"\n[Xeon Runtime] Optimizing for Intel Xeon AVX512 "
|
| 148 |
+
f"using {n_phys} physical core(s).")
|
| 149 |
+
print(f" OMP_NUM_THREADS = {os.environ['OMP_NUM_THREADS']}")
|
| 150 |
+
print(f" MKL_NUM_THREADS = {os.environ['MKL_NUM_THREADS']}")
|
| 151 |
+
print(f" KMP_AFFINITY = {os.environ['KMP_AFFINITY']}")
|
| 152 |
+
print(f" KMP_BLOCKTIME = {os.environ['KMP_BLOCKTIME']}")
|
| 153 |
+
|
| 154 |
+
return n_phys
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def get_allocated_cores() -> int:
|
| 158 |
+
"""Return the number of cores allocated by `optimize_xeon_environment`.
|
| 159 |
+
|
| 160 |
+
Returns 0 if the function was never called.
|
| 161 |
+
"""
|
| 162 |
+
return _NUCLEOS_ALOCADOS or 0
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def make_ort_session_options():
|
| 166 |
+
"""Build onnxruntime.SessionOptions tuned for Xeon AVX512.
|
| 167 |
+
|
| 168 |
+
Usage:
|
| 169 |
+
opts = make_ort_session_options()
|
| 170 |
+
sess = ort.InferenceSession(path, sess_options=opts,
|
| 171 |
+
providers=['CPUExecutionProvider'])
|
| 172 |
+
"""
|
| 173 |
+
import onnxruntime as ort
|
| 174 |
+
|
| 175 |
+
n_cores = _NUCLEOS_ALOCADOS or _detect_physical_cores()
|
| 176 |
+
|
| 177 |
+
opts = ort.SessionOptions()
|
| 178 |
+
|
| 179 |
+
# Intra-op: parallelism within a single op (e.g. matmul sharding).
|
| 180 |
+
# Inter-op: parallelism between independent ops (rarely beneficial for
|
| 181 |
+
# small models; keep at 1 to avoid contention).
|
| 182 |
+
opts.intra_op_num_threads = n_cores
|
| 183 |
+
opts.inter_op_num_threads = 1
|
| 184 |
+
|
| 185 |
+
# Maximum graph optimization: fuses Conv+BN+ReLU, MatMul+Add+Gelu, etc.
|
| 186 |
+
# Especially important for INT8 quantized graphs on Xeon.
|
| 187 |
+
opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
| 188 |
+
|
| 189 |
+
# CPU memory arena: contiguous pre-allocated memory pool reduces page
|
| 190 |
+
# faults during inference. Trade-off: more RAM usage, less fragmentation.
|
| 191 |
+
opts.enable_cpu_mem_arena = True
|
| 192 |
+
|
| 193 |
+
return opts
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def get_avx512_capability() -> Tuple[bool, str]:
|
| 197 |
+
"""Check if the host CPU supports AVX512 VNNI (required for INT8 speedup).
|
| 198 |
+
|
| 199 |
+
Returns:
|
| 200 |
+
(supported, description)
|
| 201 |
+
"""
|
| 202 |
+
flags = ""
|
| 203 |
+
try:
|
| 204 |
+
with open("/proc/cpuinfo", "r") as f:
|
| 205 |
+
for line in f:
|
| 206 |
+
if line.startswith("flags"):
|
| 207 |
+
flags = line
|
| 208 |
+
break
|
| 209 |
+
except OSError:
|
| 210 |
+
pass
|
| 211 |
+
|
| 212 |
+
has_avx512f = "avx512f" in flags
|
| 213 |
+
has_avx512_vnni = "avx512_vnni" in flags
|
| 214 |
+
has_avx512_bw = "avx512bw" in flags
|
| 215 |
+
has_avx512_dq = "avx512dq" in flags
|
| 216 |
+
|
| 217 |
+
if has_avx512_vnni:
|
| 218 |
+
return True, "AVX512_VNNI (full INT8 acceleration)"
|
| 219 |
+
elif has_avx512f:
|
| 220 |
+
return True, f"AVX512F (no VNNI; INT8 falls back to AVX512F)"
|
| 221 |
+
elif "avx2" in flags:
|
| 222 |
+
return False, "AVX2 only (INT8 quantization works but slower)"
|
| 223 |
+
else:
|
| 224 |
+
return False, "Legacy SSE (INT8 quantization not recommended)"
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
__all__ = [
|
| 228 |
+
"optimize_xeon_environment",
|
| 229 |
+
"get_allocated_cores",
|
| 230 |
+
"make_ort_session_options",
|
| 231 |
+
"get_avx512_capability",
|
| 232 |
+
]
|
src/setup.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from setuptools import setup, find_packages
|
| 2 |
+
|
| 3 |
+
setup(
|
| 4 |
+
name="bigru_t_version",
|
| 5 |
+
version="1.0.0",
|
| 6 |
+
description="BiGRU_T_version β RefatoraΓ§Γ£o do GRU-RING v13.9.2 com 4 lemas formais",
|
| 7 |
+
package_dir={"": "src"},
|
| 8 |
+
packages=find_packages(where="src"),
|
| 9 |
+
python_requires=">=3.10",
|
| 10 |
+
install_requires=[
|
| 11 |
+
"torch>=2.0",
|
| 12 |
+
"numpy>=1.24",
|
| 13 |
+
"psutil>=5.9",
|
| 14 |
+
"huggingface_hub>=0.20",
|
| 15 |
+
"datasets>=2.14",
|
| 16 |
+
"tokenizers>=0.15",
|
| 17 |
+
],
|
| 18 |
+
)
|