Delete utils/monitoring.py with huggingface_hub
Browse files- utils/monitoring.py +0 -729
utils/monitoring.py
DELETED
|
@@ -1,729 +0,0 @@
|
|
| 1 |
-
"""monitoring.py — Monitoramento completo e evolução do CNN-BiGRU.
|
| 2 |
-
|
| 3 |
-
Rastreia métricas de treinamento, inferência, uso de recursos, evolução
|
| 4 |
-
dos componentes (EWC, Medusa, CyclicReasoning, etc.) e exporta relatórios.
|
| 5 |
-
|
| 6 |
-
==============================================================================
|
| 7 |
-
FUNCIONALIDADES
|
| 8 |
-
==============================================================================
|
| 9 |
-
|
| 10 |
-
1. Métricas de treinamento:
|
| 11 |
-
- Loss total, loss_main, loss_medusa, loss_ewc, loss_hallucination
|
| 12 |
-
- Perplexidade (PPL)
|
| 13 |
-
- Learning rate (G, V)
|
| 14 |
-
- Norma do gradiente
|
| 15 |
-
- Hypothesis activations
|
| 16 |
-
- Synergy history
|
| 17 |
-
|
| 18 |
-
2. Métricas de inferência:
|
| 19 |
-
- Tokens gerados
|
| 20 |
-
- Medusa accept rate
|
| 21 |
-
- Tempo por token (ms)
|
| 22 |
-
- Throughput (tokens/s)
|
| 23 |
-
- Memória usada (MB)
|
| 24 |
-
|
| 25 |
-
3. Métricas de evolução:
|
| 26 |
-
- Loss/PPL ao longo do tempo (por época)
|
| 27 |
-
- Convergência do CyclicReasoning (n_cycles, deltas)
|
| 28 |
-
- Codebook usage do VQ-VAE-2
|
| 29 |
-
- Quantização stats (memória economizada, etc.)
|
| 30 |
-
- EWC penalty evolution
|
| 31 |
-
|
| 32 |
-
4. Métricas de sistema:
|
| 33 |
-
- CPU/Memory usage
|
| 34 |
-
- Device info
|
| 35 |
-
- Runtime info (Xeon AVX512/AMX)
|
| 36 |
-
|
| 37 |
-
5. Exportação:
|
| 38 |
-
- JSON report
|
| 39 |
-
- CSV time series
|
| 40 |
-
- Markdown summary
|
| 41 |
-
|
| 42 |
-
==============================================================================
|
| 43 |
-
USO
|
| 44 |
-
==============================================================================
|
| 45 |
-
|
| 46 |
-
from cnn_bigru.utils.monitoring import Monitor
|
| 47 |
-
|
| 48 |
-
monitor = Monitor(output_dir="/path/to/logs")
|
| 49 |
-
monitor.start_training()
|
| 50 |
-
for epoch in range(N):
|
| 51 |
-
for batch in dataloader:
|
| 52 |
-
...
|
| 53 |
-
monitor.log_batch({
|
| 54 |
-
"loss": loss.item(),
|
| 55 |
-
"ppl": ppl,
|
| 56 |
-
"lr": lr,
|
| 57 |
-
"grad_norm": grad_norm,
|
| 58 |
-
})
|
| 59 |
-
monitor.log_epoch({"epoch": epoch, "avg_loss": ...})
|
| 60 |
-
monitor.end_training()
|
| 61 |
-
monitor.export_report()
|
| 62 |
-
|
| 63 |
-
Autor: CNN-BiGRU Project
|
| 64 |
-
"""
|
| 65 |
-
from __future__ import annotations
|
| 66 |
-
|
| 67 |
-
import csv
|
| 68 |
-
import json
|
| 69 |
-
import logging
|
| 70 |
-
import os
|
| 71 |
-
import time
|
| 72 |
-
from collections import defaultdict, deque
|
| 73 |
-
from dataclasses import dataclass, field, asdict
|
| 74 |
-
from pathlib import Path
|
| 75 |
-
from typing import Any, Dict, List, Optional, Union
|
| 76 |
-
|
| 77 |
-
import torch
|
| 78 |
-
|
| 79 |
-
logger = logging.getLogger(__name__)
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
# ============================================================================
|
| 83 |
-
# Data Classes
|
| 84 |
-
# ============================================================================
|
| 85 |
-
|
| 86 |
-
@dataclass
|
| 87 |
-
class BatchMetrics:
|
| 88 |
-
"""Métricas de um batch."""
|
| 89 |
-
step: int
|
| 90 |
-
epoch: int
|
| 91 |
-
batch_in_epoch: int
|
| 92 |
-
timestamp: float
|
| 93 |
-
loss: float = 0.0
|
| 94 |
-
loss_main: float = 0.0
|
| 95 |
-
loss_medusa: float = 0.0
|
| 96 |
-
loss_ewc: float = 0.0
|
| 97 |
-
loss_hallucination: float = 0.0
|
| 98 |
-
loss_total: float = 0.0
|
| 99 |
-
ppl: float = 0.0
|
| 100 |
-
lr_g: float = 0.0
|
| 101 |
-
lr_v: float = 0.0
|
| 102 |
-
grad_norm_g: float = 0.0
|
| 103 |
-
grad_norm_v: float = 0.0
|
| 104 |
-
v_mean: float = 0.0
|
| 105 |
-
hypothesis_activations: int = 0
|
| 106 |
-
elapsed_ms: float = 0.0
|
| 107 |
-
extra: Dict[str, Any] = field(default_factory=dict)
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
@dataclass
|
| 111 |
-
class EpochMetrics:
|
| 112 |
-
"""Métricas agregadas de uma época."""
|
| 113 |
-
epoch: int
|
| 114 |
-
avg_loss: float = 0.0
|
| 115 |
-
avg_ppl: float = 0.0
|
| 116 |
-
n_batches: int = 0
|
| 117 |
-
n_hypothesis_activations: int = 0
|
| 118 |
-
elapsed_s: float = 0.0
|
| 119 |
-
best_loss: float = float("inf")
|
| 120 |
-
worst_loss: float = 0.0
|
| 121 |
-
synergy_history: List[Dict] = field(default_factory=list)
|
| 122 |
-
extra: Dict[str, Any] = field(default_factory=dict)
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
@dataclass
|
| 126 |
-
class EvolutionMetrics:
|
| 127 |
-
"""Métricas de evolução dos componentes ao longo do tempo."""
|
| 128 |
-
cyclic_reasoning_stats: List[Dict] = field(default_factory=list)
|
| 129 |
-
vqvae2_codebook_usage: List[Dict] = field(default_factory=list)
|
| 130 |
-
quantization_stats: List[Dict] = field(default_factory=list)
|
| 131 |
-
ewc_penalty_evolution: List[Dict] = field(default_factory=list)
|
| 132 |
-
medusa_accept_rate: List[Dict] = field(default_factory=list)
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
@dataclass
|
| 136 |
-
class SystemMetrics:
|
| 137 |
-
"""Métricas de sistema."""
|
| 138 |
-
timestamp: float
|
| 139 |
-
cpu_percent: float = 0.0
|
| 140 |
-
memory_mb: float = 0.0
|
| 141 |
-
gpu_memory_mb: float = 0.0
|
| 142 |
-
n_active_threads: int = 0
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
# ============================================================================
|
| 146 |
-
# Monitor
|
| 147 |
-
# ============================================================================
|
| 148 |
-
|
| 149 |
-
class Monitor:
|
| 150 |
-
"""Monitor completo de treinamento, inferência e evolução.
|
| 151 |
-
|
| 152 |
-
Args:
|
| 153 |
-
output_dir: diretório para salvar logs e relatórios
|
| 154 |
-
max_history: número máximo de métricas em memória (FIFO)
|
| 155 |
-
log_every: intervalo de log (em batches)
|
| 156 |
-
track_system: se True, rastreia CPU/memória (requer psutil)
|
| 157 |
-
"""
|
| 158 |
-
|
| 159 |
-
def __init__(
|
| 160 |
-
self,
|
| 161 |
-
output_dir: Optional[Union[str, Path]] = None,
|
| 162 |
-
max_history: int = 10000,
|
| 163 |
-
log_every: int = 1,
|
| 164 |
-
track_system: bool = True,
|
| 165 |
-
):
|
| 166 |
-
self.output_dir = Path(output_dir) if output_dir else None
|
| 167 |
-
if self.output_dir:
|
| 168 |
-
self.output_dir.mkdir(parents=True, exist_ok=True)
|
| 169 |
-
self.max_history = max_history
|
| 170 |
-
self.log_every = log_every
|
| 171 |
-
self.track_system = track_system and self._psutil_available()
|
| 172 |
-
|
| 173 |
-
# Histórico
|
| 174 |
-
self.batch_history: deque = deque(maxlen=max_history)
|
| 175 |
-
self.epoch_history: List[EpochMetrics] = []
|
| 176 |
-
self.evolution = EvolutionMetrics()
|
| 177 |
-
self.system_history: deque = deque(maxlen=max_history)
|
| 178 |
-
|
| 179 |
-
# Estado
|
| 180 |
-
self._training_active = False
|
| 181 |
-
self._inference_active = False
|
| 182 |
-
self._start_time: Optional[float] = None
|
| 183 |
-
self._epoch_start_time: Optional[float] = None
|
| 184 |
-
self._current_epoch = 0
|
| 185 |
-
self._current_batch = 0
|
| 186 |
-
self._global_step = 0
|
| 187 |
-
|
| 188 |
-
# Agregados para época atual
|
| 189 |
-
self._epoch_losses: List[float] = []
|
| 190 |
-
self._epoch_ppls: List[float] = []
|
| 191 |
-
self._epoch_hypothesis_activations = 0
|
| 192 |
-
self._epoch_synergy_history: List[Dict] = []
|
| 193 |
-
|
| 194 |
-
# Inferência
|
| 195 |
-
self.inference_stats: Dict[str, Any] = {
|
| 196 |
-
"total_tokens": 0,
|
| 197 |
-
"total_time_ms": 0.0,
|
| 198 |
-
"medusa_accepted": 0,
|
| 199 |
-
"n_calls": 0,
|
| 200 |
-
"fallback_to_greedy": 0,
|
| 201 |
-
}
|
| 202 |
-
|
| 203 |
-
# Component tracking
|
| 204 |
-
self.component_status: Dict[str, bool] = {
|
| 205 |
-
"ewc": False,
|
| 206 |
-
"medusa": False,
|
| 207 |
-
"cyclic_reasoning": False,
|
| 208 |
-
"vqvae2": False,
|
| 209 |
-
"quantization": False,
|
| 210 |
-
"multimodal_attention": False,
|
| 211 |
-
"context_window": False,
|
| 212 |
-
}
|
| 213 |
-
|
| 214 |
-
def _psutil_available(self) -> bool:
|
| 215 |
-
try:
|
| 216 |
-
import psutil # noqa: F401
|
| 217 |
-
return True
|
| 218 |
-
except ImportError:
|
| 219 |
-
return False
|
| 220 |
-
|
| 221 |
-
# ----------------------------------------------------------------------
|
| 222 |
-
# Training lifecycle
|
| 223 |
-
# ----------------------------------------------------------------------
|
| 224 |
-
|
| 225 |
-
def start_training(self) -> None:
|
| 226 |
-
"""Inicia uma sessão de monitoramento de treino."""
|
| 227 |
-
self._training_active = True
|
| 228 |
-
self._start_time = time.time()
|
| 229 |
-
self._current_epoch = 0
|
| 230 |
-
self._current_batch = 0
|
| 231 |
-
self._global_step = 0
|
| 232 |
-
logger.info("Monitor: sessão de treino iniciada")
|
| 233 |
-
|
| 234 |
-
def start_epoch(self, epoch: int) -> None:
|
| 235 |
-
"""Inicia uma nova época."""
|
| 236 |
-
self._current_epoch = epoch
|
| 237 |
-
self._epoch_start_time = time.time()
|
| 238 |
-
self._epoch_losses = []
|
| 239 |
-
self._epoch_ppls = []
|
| 240 |
-
self._epoch_hypothesis_activations = 0
|
| 241 |
-
self._epoch_synergy_history = []
|
| 242 |
-
|
| 243 |
-
def log_batch(self, metrics: Dict[str, Any]) -> None:
|
| 244 |
-
"""Loga métricas de um batch.
|
| 245 |
-
|
| 246 |
-
Args:
|
| 247 |
-
metrics: dict com chaves como loss, ppl, lr, grad_norm, etc.
|
| 248 |
-
"""
|
| 249 |
-
if not self._training_active:
|
| 250 |
-
return
|
| 251 |
-
|
| 252 |
-
self._global_step += 1
|
| 253 |
-
self._current_batch += 1
|
| 254 |
-
|
| 255 |
-
ts = time.time()
|
| 256 |
-
batch_metrics = BatchMetrics(
|
| 257 |
-
step=self._global_step,
|
| 258 |
-
epoch=self._current_epoch,
|
| 259 |
-
batch_in_epoch=self._current_batch,
|
| 260 |
-
timestamp=ts,
|
| 261 |
-
loss=metrics.get("loss", 0.0),
|
| 262 |
-
loss_main=metrics.get("loss_main", 0.0),
|
| 263 |
-
loss_medusa=metrics.get("loss_medusa", 0.0),
|
| 264 |
-
loss_ewc=metrics.get("loss_ewc", 0.0),
|
| 265 |
-
loss_hallucination=metrics.get("loss_hallucination", 0.0),
|
| 266 |
-
loss_total=metrics.get("loss_total", metrics.get("loss", 0.0)),
|
| 267 |
-
ppl=metrics.get("ppl", 0.0),
|
| 268 |
-
lr_g=metrics.get("lr_g", metrics.get("lr", 0.0)),
|
| 269 |
-
lr_v=metrics.get("lr_v", 0.0),
|
| 270 |
-
grad_norm_g=metrics.get("grad_norm_g", metrics.get("grad_norm", 0.0)),
|
| 271 |
-
grad_norm_v=metrics.get("grad_norm_v", 0.0),
|
| 272 |
-
v_mean=metrics.get("v_mean", 0.0),
|
| 273 |
-
hypothesis_activations=metrics.get("hypothesis_activations", 0),
|
| 274 |
-
elapsed_ms=metrics.get("elapsed_ms", 0.0),
|
| 275 |
-
extra={k: v for k, v in metrics.items()
|
| 276 |
-
if k not in {"loss", "loss_main", "loss_medusa", "loss_ewc",
|
| 277 |
-
"loss_hallucination", "loss_total", "ppl",
|
| 278 |
-
"lr_g", "lr_v", "grad_norm_g", "grad_norm_v",
|
| 279 |
-
"v_mean", "hypothesis_activations", "elapsed_ms",
|
| 280 |
-
"lr", "grad_norm"}},
|
| 281 |
-
)
|
| 282 |
-
self.batch_history.append(batch_metrics)
|
| 283 |
-
|
| 284 |
-
# Acumular para época
|
| 285 |
-
if batch_metrics.loss > 0:
|
| 286 |
-
self._epoch_losses.append(batch_metrics.loss)
|
| 287 |
-
if batch_metrics.ppl > 0:
|
| 288 |
-
self._epoch_ppls.append(batch_metrics.ppl)
|
| 289 |
-
self._epoch_hypothesis_activations += batch_metrics.hypothesis_activations
|
| 290 |
-
if "synergy_history" in metrics:
|
| 291 |
-
self._epoch_synergy_history.extend(metrics["synergy_history"])
|
| 292 |
-
|
| 293 |
-
# Log
|
| 294 |
-
if self._global_step % self.log_every == 0:
|
| 295 |
-
logger.info(
|
| 296 |
-
"Batch %d (epoch %d) | loss=%.4f | ppl=%.2f | lr_g=%.2e | hyp=%d",
|
| 297 |
-
self._global_step, self._current_epoch,
|
| 298 |
-
batch_metrics.loss, batch_metrics.ppl, batch_metrics.lr_g,
|
| 299 |
-
batch_metrics.hypothesis_activations,
|
| 300 |
-
)
|
| 301 |
-
|
| 302 |
-
# Track system metrics occasionally
|
| 303 |
-
if self.track_system and self._global_step % 50 == 0:
|
| 304 |
-
self._track_system()
|
| 305 |
-
|
| 306 |
-
def end_epoch(self, extra: Optional[Dict[str, Any]] = None) -> EpochMetrics:
|
| 307 |
-
"""Finaliza a época atual e retorna métricas agregadas."""
|
| 308 |
-
if self._epoch_start_time is None:
|
| 309 |
-
logger.warning("end_epoch chamado sem start_epoch")
|
| 310 |
-
return EpochMetrics(epoch=self._current_epoch)
|
| 311 |
-
|
| 312 |
-
elapsed = time.time() - self._epoch_start_time
|
| 313 |
-
|
| 314 |
-
# Agregar
|
| 315 |
-
if self._epoch_losses:
|
| 316 |
-
avg_loss = sum(self._epoch_losses) / len(self._epoch_losses)
|
| 317 |
-
best_loss = min(self._epoch_losses)
|
| 318 |
-
worst_loss = max(self._epoch_losses)
|
| 319 |
-
else:
|
| 320 |
-
avg_loss = best_loss = worst_loss = 0.0
|
| 321 |
-
|
| 322 |
-
if self._epoch_ppls:
|
| 323 |
-
avg_ppl = sum(self._epoch_ppls) / len(self._epoch_ppls)
|
| 324 |
-
else:
|
| 325 |
-
avg_ppl = 0.0
|
| 326 |
-
|
| 327 |
-
epoch_metrics = EpochMetrics(
|
| 328 |
-
epoch=self._current_epoch,
|
| 329 |
-
avg_loss=avg_loss,
|
| 330 |
-
avg_ppl=avg_ppl,
|
| 331 |
-
n_batches=len(self._epoch_losses),
|
| 332 |
-
n_hypothesis_activations=self._epoch_hypothesis_activations,
|
| 333 |
-
elapsed_s=elapsed,
|
| 334 |
-
best_loss=best_loss,
|
| 335 |
-
worst_loss=worst_loss,
|
| 336 |
-
synergy_history=list(self._epoch_synergy_history),
|
| 337 |
-
extra=extra or {},
|
| 338 |
-
)
|
| 339 |
-
self.epoch_history.append(epoch_metrics)
|
| 340 |
-
|
| 341 |
-
logger.info(
|
| 342 |
-
"Época %d concluída | avg_loss=%.4f | avg_ppl=%.2f | hyp_act=%d | batches=%d | %.1fs",
|
| 343 |
-
self._current_epoch, avg_loss, avg_ppl,
|
| 344 |
-
self._epoch_hypothesis_activations, len(self._epoch_losses), elapsed,
|
| 345 |
-
)
|
| 346 |
-
return epoch_metrics
|
| 347 |
-
|
| 348 |
-
def end_training(self) -> Dict[str, Any]:
|
| 349 |
-
"""Finaliza a sessão de treino e retorna resumo."""
|
| 350 |
-
if self._start_time is None:
|
| 351 |
-
return {}
|
| 352 |
-
total_time = time.time() - self._start_time
|
| 353 |
-
self._training_active = False
|
| 354 |
-
|
| 355 |
-
summary = {
|
| 356 |
-
"total_time_s": total_time,
|
| 357 |
-
"total_epochs": len(self.epoch_history),
|
| 358 |
-
"total_batches": self._global_step,
|
| 359 |
-
"final_loss": self.epoch_history[-1].avg_loss if self.epoch_history else 0.0,
|
| 360 |
-
"final_ppl": self.epoch_history[-1].avg_ppl if self.epoch_history else 0.0,
|
| 361 |
-
"best_loss": min((e.avg_loss for e in self.epoch_history), default=0.0),
|
| 362 |
-
"best_ppl": min((e.avg_ppl for e in self.epoch_history), default=0.0),
|
| 363 |
-
"total_hypothesis_activations": sum(e.n_hypothesis_activations for e in self.epoch_history),
|
| 364 |
-
}
|
| 365 |
-
logger.info("Monitor: sessão de treino finalizada — %s", summary)
|
| 366 |
-
return summary
|
| 367 |
-
|
| 368 |
-
# ----------------------------------------------------------------------
|
| 369 |
-
# Inference tracking
|
| 370 |
-
# ----------------------------------------------------------------------
|
| 371 |
-
|
| 372 |
-
def log_inference(
|
| 373 |
-
self,
|
| 374 |
-
n_tokens: int,
|
| 375 |
-
elapsed_ms: float,
|
| 376 |
-
medusa_accepted: int = 0,
|
| 377 |
-
fallback_to_greedy: int = 0,
|
| 378 |
-
) -> None:
|
| 379 |
-
"""Loga uma chamada de inferência."""
|
| 380 |
-
self.inference_stats["total_tokens"] += n_tokens
|
| 381 |
-
self.inference_stats["total_time_ms"] += elapsed_ms
|
| 382 |
-
self.inference_stats["medusa_accepted"] += medusa_accepted
|
| 383 |
-
self.inference_stats["fallback_to_greedy"] += fallback_to_greedy
|
| 384 |
-
self.inference_stats["n_calls"] += 1
|
| 385 |
-
|
| 386 |
-
def get_inference_throughput(self) -> Dict[str, float]:
|
| 387 |
-
"""Retorna throughput de inferência."""
|
| 388 |
-
s = self.inference_stats
|
| 389 |
-
if s["total_time_ms"] == 0:
|
| 390 |
-
return {"tokens_per_s": 0.0, "ms_per_token": 0.0, "medusa_accept_rate": 0.0}
|
| 391 |
-
total_s = s["total_time_ms"] / 1000.0
|
| 392 |
-
tps = s["total_tokens"] / total_s if total_s > 0 else 0.0
|
| 393 |
-
mspt = s["total_time_ms"] / s["total_tokens"] if s["total_tokens"] > 0 else 0.0
|
| 394 |
-
accept_rate = s["medusa_accepted"] / max(1, s["total_tokens"])
|
| 395 |
-
return {
|
| 396 |
-
"tokens_per_s": tps,
|
| 397 |
-
"ms_per_token": mspt,
|
| 398 |
-
"medusa_accept_rate": accept_rate,
|
| 399 |
-
"n_calls": s["n_calls"],
|
| 400 |
-
"total_tokens": s["total_tokens"],
|
| 401 |
-
}
|
| 402 |
-
|
| 403 |
-
# ----------------------------------------------------------------------
|
| 404 |
-
# Evolution tracking (componentes específicos)
|
| 405 |
-
# ----------------------------------------------------------------------
|
| 406 |
-
|
| 407 |
-
def log_cyclic_reasoning(self, stats: Dict[str, Any]) -> None:
|
| 408 |
-
"""Loga estatísticas do CyclicReasoning."""
|
| 409 |
-
stats["step"] = self._global_step
|
| 410 |
-
stats["timestamp"] = time.time()
|
| 411 |
-
self.evolution.cyclic_reasoning_stats.append(stats)
|
| 412 |
-
|
| 413 |
-
def log_vqvae2_usage(self, stats: Dict[str, Any]) -> None:
|
| 414 |
-
"""Loga uso do codebook VQ-VAE-2."""
|
| 415 |
-
stats["step"] = self._global_step
|
| 416 |
-
stats["timestamp"] = time.time()
|
| 417 |
-
self.evolution.vqvae2_codebook_usage.append(stats)
|
| 418 |
-
|
| 419 |
-
def log_quantization(self, stats: Dict[str, Any]) -> None:
|
| 420 |
-
"""Loga estatísticas de quantização W8A8."""
|
| 421 |
-
stats["step"] = self._global_step
|
| 422 |
-
stats["timestamp"] = time.time()
|
| 423 |
-
self.evolution.quantization_stats.append(stats)
|
| 424 |
-
|
| 425 |
-
def log_ewc_penalty(self, penalty: float, num_tasks: int) -> None:
|
| 426 |
-
"""Loga evolução do penalty EWC."""
|
| 427 |
-
self.evolution.ewc_penalty_evolution.append({
|
| 428 |
-
"step": self._global_step,
|
| 429 |
-
"timestamp": time.time(),
|
| 430 |
-
"penalty": penalty,
|
| 431 |
-
"num_tasks": num_tasks,
|
| 432 |
-
})
|
| 433 |
-
|
| 434 |
-
def log_medusa_accept(self, accepted: int, total: int) -> None:
|
| 435 |
-
"""Loga taxa de aceitação Medusa."""
|
| 436 |
-
rate = accepted / max(1, total)
|
| 437 |
-
self.evolution.medusa_accept_rate.append({
|
| 438 |
-
"step": self._global_step,
|
| 439 |
-
"timestamp": time.time(),
|
| 440 |
-
"accepted": accepted,
|
| 441 |
-
"total": total,
|
| 442 |
-
"rate": rate,
|
| 443 |
-
})
|
| 444 |
-
|
| 445 |
-
def register_component(self, name: str, active: bool = True) -> None:
|
| 446 |
-
"""Registra que um componente está ativo."""
|
| 447 |
-
if name in self.component_status:
|
| 448 |
-
self.component_status[name] = active
|
| 449 |
-
|
| 450 |
-
# ----------------------------------------------------------------------
|
| 451 |
-
# System tracking
|
| 452 |
-
# ----------------------------------------------------------------------
|
| 453 |
-
|
| 454 |
-
def _track_system(self) -> None:
|
| 455 |
-
"""Rastreia métricas de sistema."""
|
| 456 |
-
if not self.track_system:
|
| 457 |
-
return
|
| 458 |
-
try:
|
| 459 |
-
import psutil
|
| 460 |
-
cpu = psutil.cpu_percent(interval=None)
|
| 461 |
-
mem = psutil.virtual_memory()
|
| 462 |
-
sm = SystemMetrics(
|
| 463 |
-
timestamp=time.time(),
|
| 464 |
-
cpu_percent=cpu,
|
| 465 |
-
memory_mb=mem.used / (1024 * 1024),
|
| 466 |
-
n_active_threads=psutil.cpu_count() or 0,
|
| 467 |
-
)
|
| 468 |
-
# GPU memory (se disponível)
|
| 469 |
-
if torch.cuda.is_available():
|
| 470 |
-
try:
|
| 471 |
-
gpu_mem = torch.cuda.memory_allocated() / (1024 * 1024)
|
| 472 |
-
sm.gpu_memory_mb = gpu_mem
|
| 473 |
-
except Exception:
|
| 474 |
-
pass
|
| 475 |
-
self.system_history.append(sm)
|
| 476 |
-
except Exception as e:
|
| 477 |
-
logger.debug("System tracking falhou: %s", e)
|
| 478 |
-
|
| 479 |
-
# ----------------------------------------------------------------------
|
| 480 |
-
# Exportação
|
| 481 |
-
# ----------------------------------------------------------------------
|
| 482 |
-
|
| 483 |
-
def export_report(self, output_path: Optional[Union[str, Path]] = None) -> Path:
|
| 484 |
-
"""Exporta relatório completo em JSON.
|
| 485 |
-
|
| 486 |
-
Args:
|
| 487 |
-
output_path: caminho do arquivo (default: output_dir/monitor_report.json)
|
| 488 |
-
|
| 489 |
-
Returns:
|
| 490 |
-
Path do arquivo salvo
|
| 491 |
-
"""
|
| 492 |
-
if output_path is None:
|
| 493 |
-
if self.output_dir is None:
|
| 494 |
-
raise ValueError("output_dir ou output_path deve ser fornecido")
|
| 495 |
-
output_path = self.output_dir / "monitor_report.json"
|
| 496 |
-
output_path = Path(output_path)
|
| 497 |
-
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 498 |
-
|
| 499 |
-
# Construir relatório
|
| 500 |
-
report = {
|
| 501 |
-
"metadata": {
|
| 502 |
-
"generated_at": time.time(),
|
| 503 |
-
"training_active": self._training_active,
|
| 504 |
-
"global_step": self._global_step,
|
| 505 |
-
"current_epoch": self._current_epoch,
|
| 506 |
-
},
|
| 507 |
-
"components": dict(self.component_status),
|
| 508 |
-
"training_summary": self._get_training_summary(),
|
| 509 |
-
"epoch_history": [asdict(e) for e in self.epoch_history],
|
| 510 |
-
"inference_stats": {
|
| 511 |
-
**self.inference_stats,
|
| 512 |
-
"throughput": self.get_inference_throughput(),
|
| 513 |
-
},
|
| 514 |
-
"evolution": {
|
| 515 |
-
"cyclic_reasoning": self.evolution.cyclic_reasoning_stats[-50:],
|
| 516 |
-
"vqvae2_codebook_usage": self.evolution.vqvae2_codebook_usage[-50:],
|
| 517 |
-
"quantization_stats": self.evolution.quantization_stats[-10:],
|
| 518 |
-
"ewc_penalty_evolution": self.evolution.ewc_penalty_evolution[-50:],
|
| 519 |
-
"medusa_accept_rate": self.evolution.medusa_accept_rate[-50:],
|
| 520 |
-
},
|
| 521 |
-
"system_metrics": [asdict(s) for s in list(self.system_history)[-50:]],
|
| 522 |
-
}
|
| 523 |
-
|
| 524 |
-
with open(output_path, "w", encoding="utf-8") as f:
|
| 525 |
-
json.dump(report, f, indent=2, default=str, ensure_ascii=False)
|
| 526 |
-
logger.info("Monitor report salvo em: %s", output_path)
|
| 527 |
-
return output_path
|
| 528 |
-
|
| 529 |
-
def export_csv(self, output_path: Optional[Union[str, Path]] = None) -> Path:
|
| 530 |
-
"""Exporta métricas de batch em CSV."""
|
| 531 |
-
if output_path is None:
|
| 532 |
-
if self.output_dir is None:
|
| 533 |
-
raise ValueError("output_dir ou output_path deve ser fornecido")
|
| 534 |
-
output_path = self.output_dir / "batch_metrics.csv"
|
| 535 |
-
output_path = Path(output_path)
|
| 536 |
-
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 537 |
-
|
| 538 |
-
if not self.batch_history:
|
| 539 |
-
logger.warning("Sem batch history para exportar")
|
| 540 |
-
return output_path
|
| 541 |
-
|
| 542 |
-
# Pegar chaves do primeiro item + extras
|
| 543 |
-
first = self.batch_history[0]
|
| 544 |
-
fieldnames = ["step", "epoch", "batch_in_epoch", "timestamp",
|
| 545 |
-
"loss", "loss_main", "loss_medusa", "loss_ewc",
|
| 546 |
-
"loss_hallucination", "loss_total", "ppl",
|
| 547 |
-
"lr_g", "lr_v", "grad_norm_g", "grad_norm_v",
|
| 548 |
-
"v_mean", "hypothesis_activations", "elapsed_ms"]
|
| 549 |
-
|
| 550 |
-
with open(output_path, "w", newline="", encoding="utf-8") as f:
|
| 551 |
-
writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore")
|
| 552 |
-
writer.writeheader()
|
| 553 |
-
for m in self.batch_history:
|
| 554 |
-
writer.writerow(asdict(m))
|
| 555 |
-
|
| 556 |
-
logger.info("CSV metrics salvo em: %s (%d rows)", output_path, len(self.batch_history))
|
| 557 |
-
return output_path
|
| 558 |
-
|
| 559 |
-
def export_markdown_summary(self, output_path: Optional[Union[str, Path]] = None) -> Path:
|
| 560 |
-
"""Exporta resumo em Markdown."""
|
| 561 |
-
if output_path is None:
|
| 562 |
-
if self.output_dir is None:
|
| 563 |
-
raise ValueError("output_dir ou output_path deve ser fornecido")
|
| 564 |
-
output_path = self.output_dir / "monitor_summary.md"
|
| 565 |
-
output_path = Path(output_path)
|
| 566 |
-
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 567 |
-
|
| 568 |
-
summary = self._get_training_summary()
|
| 569 |
-
throughput = self.get_inference_throughput()
|
| 570 |
-
|
| 571 |
-
lines = [
|
| 572 |
-
"# Monitor Report — CNN-BiGRU",
|
| 573 |
-
"",
|
| 574 |
-
f"**Generated at:** {time.strftime('%Y-%m-%d %H:%M:%S')}",
|
| 575 |
-
"",
|
| 576 |
-
"## Components Status",
|
| 577 |
-
"",
|
| 578 |
-
]
|
| 579 |
-
for comp, active in self.component_status.items():
|
| 580 |
-
icon = "[x]" if active else "[ ]"
|
| 581 |
-
lines.append(f"- {icon} {comp}")
|
| 582 |
-
|
| 583 |
-
lines.extend([
|
| 584 |
-
"",
|
| 585 |
-
"## Training Summary",
|
| 586 |
-
"",
|
| 587 |
-
f"- Total time: {summary.get('total_time_s', 0):.1f}s",
|
| 588 |
-
f"- Total epochs: {summary.get('total_epochs', 0)}",
|
| 589 |
-
f"- Total batches: {summary.get('total_batches', 0)}",
|
| 590 |
-
f"- Final loss: {summary.get('final_loss', 0):.4f}",
|
| 591 |
-
f"- Final PPL: {summary.get('final_ppl', 0):.2f}",
|
| 592 |
-
f"- Best loss: {summary.get('best_loss', 0):.4f}",
|
| 593 |
-
f"- Best PPL: {summary.get('best_ppl', 0):.2f}",
|
| 594 |
-
f"- Total hypothesis activations: {summary.get('total_hypothesis_activations', 0)}",
|
| 595 |
-
"",
|
| 596 |
-
"## Inference Throughput",
|
| 597 |
-
"",
|
| 598 |
-
f"- Tokens/s: {throughput.get('tokens_per_s', 0):.1f}",
|
| 599 |
-
f"- ms/token: {throughput.get('ms_per_token', 0):.2f}",
|
| 600 |
-
f"- Medusa accept rate: {throughput.get('medusa_accept_rate', 0):.2%}",
|
| 601 |
-
f"- Total tokens generated: {throughput.get('total_tokens', 0)}",
|
| 602 |
-
f"- Total inference calls: {throughput.get('n_calls', 0)}",
|
| 603 |
-
"",
|
| 604 |
-
"## Evolution",
|
| 605 |
-
"",
|
| 606 |
-
f"- CyclicReasoning events: {len(self.evolution.cyclic_reasoning_stats)}",
|
| 607 |
-
f"- VQ-VAE-2 usage events: {len(self.evolution.vqvae2_codebook_usage)}",
|
| 608 |
-
f"- Quantization events: {len(self.evolution.quantization_stats)}",
|
| 609 |
-
f"- EWC penalty events: {len(self.evolution.ewc_penalty_evolution)}",
|
| 610 |
-
f"- Medusa accept events: {len(self.evolution.medusa_accept_rate)}",
|
| 611 |
-
"",
|
| 612 |
-
"## Epoch History",
|
| 613 |
-
"",
|
| 614 |
-
"| Epoch | Avg Loss | Avg PPL | Batches | Hyp Act | Time(s) |",
|
| 615 |
-
"|-------|----------|---------|---------|---------|---------|",
|
| 616 |
-
])
|
| 617 |
-
for e in self.epoch_history:
|
| 618 |
-
lines.append(
|
| 619 |
-
f"| {e.epoch} | {e.avg_loss:.4f} | {e.avg_ppl:.2f} | "
|
| 620 |
-
f"{e.n_batches} | {e.n_hypothesis_activations} | {e.elapsed_s:.1f} |"
|
| 621 |
-
)
|
| 622 |
-
|
| 623 |
-
with open(output_path, "w", encoding="utf-8") as f:
|
| 624 |
-
f.write("\n".join(lines))
|
| 625 |
-
logger.info("Markdown summary salvo em: %s", output_path)
|
| 626 |
-
return output_path
|
| 627 |
-
|
| 628 |
-
def _get_training_summary(self) -> Dict[str, Any]:
|
| 629 |
-
"""Constrói sumário de treino."""
|
| 630 |
-
if not self.epoch_history:
|
| 631 |
-
return {"total_time_s": 0, "total_epochs": 0, "total_batches": self._global_step}
|
| 632 |
-
return {
|
| 633 |
-
"total_time_s": time.time() - (self._start_time or time.time()),
|
| 634 |
-
"total_epochs": len(self.epoch_history),
|
| 635 |
-
"total_batches": self._global_step,
|
| 636 |
-
"final_loss": self.epoch_history[-1].avg_loss,
|
| 637 |
-
"final_ppl": self.epoch_history[-1].avg_ppl,
|
| 638 |
-
"best_loss": min((e.avg_loss for e in self.epoch_history), default=0.0),
|
| 639 |
-
"best_ppl": min((e.avg_ppl for e in self.epoch_history), default=0.0),
|
| 640 |
-
"total_hypothesis_activations": sum(
|
| 641 |
-
e.n_hypothesis_activations for e in self.epoch_history
|
| 642 |
-
),
|
| 643 |
-
}
|
| 644 |
-
|
| 645 |
-
# ----------------------------------------------------------------------
|
| 646 |
-
# Reset
|
| 647 |
-
# ----------------------------------------------------------------------
|
| 648 |
-
|
| 649 |
-
def reset(self) -> None:
|
| 650 |
-
"""Limpa todo o histórico."""
|
| 651 |
-
self.batch_history.clear()
|
| 652 |
-
self.epoch_history.clear()
|
| 653 |
-
self.evolution = EvolutionMetrics()
|
| 654 |
-
self.system_history.clear()
|
| 655 |
-
self._global_step = 0
|
| 656 |
-
self._current_epoch = 0
|
| 657 |
-
self._current_batch = 0
|
| 658 |
-
self._start_time = None
|
| 659 |
-
self._epoch_start_time = None
|
| 660 |
-
self.inference_stats = {k: 0 if isinstance(v, int) else 0.0
|
| 661 |
-
for k, v in self.inference_stats.items()}
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
# ============================================================================
|
| 665 |
-
# Singleton instance (opcional)
|
| 666 |
-
# ============================================================================
|
| 667 |
-
|
| 668 |
-
_global_monitor: Optional[Monitor] = None
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
def get_monitor(output_dir: Optional[Union[str, Path]] = None) -> Monitor:
|
| 672 |
-
"""Retorna a instância global do monitor (singleton)."""
|
| 673 |
-
global _global_monitor
|
| 674 |
-
if _global_monitor is None:
|
| 675 |
-
_global_monitor = Monitor(output_dir=output_dir)
|
| 676 |
-
return _global_monitor
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
# ============================================================================
|
| 680 |
-
# Self-test
|
| 681 |
-
# ============================================================================
|
| 682 |
-
|
| 683 |
-
def _self_test():
|
| 684 |
-
"""Teste rápido do monitor."""
|
| 685 |
-
import tempfile
|
| 686 |
-
with tempfile.TemporaryDirectory() as tmpdir:
|
| 687 |
-
mon = Monitor(output_dir=tmpdir, log_every=1, track_system=True)
|
| 688 |
-
mon.start_training()
|
| 689 |
-
mon.start_epoch(0)
|
| 690 |
-
for i in range(5):
|
| 691 |
-
mon.log_batch({
|
| 692 |
-
"loss": 5.0 - i * 0.5,
|
| 693 |
-
"ppl": 100.0 - i * 10,
|
| 694 |
-
"lr_g": 1e-3,
|
| 695 |
-
"grad_norm": 0.5 + i * 0.1,
|
| 696 |
-
"hypothesis_activations": i,
|
| 697 |
-
"elapsed_ms": 10 + i,
|
| 698 |
-
})
|
| 699 |
-
mon.end_epoch()
|
| 700 |
-
mon.end_training()
|
| 701 |
-
|
| 702 |
-
mon.log_inference(n_tokens=50, elapsed_ms=500, medusa_accepted=10)
|
| 703 |
-
mon.log_cyclic_reasoning({"n_cycles": 3, "converged": True})
|
| 704 |
-
mon.log_vqvae2_usage({"top_usage": 0.5, "bottom_usage": 0.7})
|
| 705 |
-
mon.log_quantization({"reduction_pct": 60.0})
|
| 706 |
-
mon.log_ewc_penalty(penalty=0.001, num_tasks=1)
|
| 707 |
-
mon.log_medusa_accept(accepted=10, total=50)
|
| 708 |
-
|
| 709 |
-
report = mon.export_report()
|
| 710 |
-
csv_path = mon.export_csv()
|
| 711 |
-
md_path = mon.export_markdown_summary()
|
| 712 |
-
print(f"Report: {report}")
|
| 713 |
-
print(f"CSV: {csv_path}")
|
| 714 |
-
print(f"MD: {md_path}")
|
| 715 |
-
print(f"Throughput: {mon.get_inference_throughput()}")
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
if __name__ == "__main__":
|
| 719 |
-
_self_test()
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
__all__ = [
|
| 723 |
-
"BatchMetrics",
|
| 724 |
-
"EpochMetrics",
|
| 725 |
-
"EvolutionMetrics",
|
| 726 |
-
"SystemMetrics",
|
| 727 |
-
"Monitor",
|
| 728 |
-
"get_monitor",
|
| 729 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|