Source code for slicktune.metrics
"""Training and evaluation metrics tracking."""
from __future__ import annotations
import json
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any
[docs]
@dataclass(kw_only=True)
class TrainingMetrics:
"""Snapshot of metrics for one training run.
Parameters
----------
strategy : str
Parameter strategy name (e.g. ``"lora"``).
objective : str
Objective name (e.g. ``"sft"``).
model_id : str
Base model id or path.
train_loss : float | None, optional
Final reported training loss, by default None.
eval_loss : float | None, optional
Final evaluation loss if computed, by default None.
train_runtime_sec : float | None, optional
Wall-clock training time in seconds, by default None.
train_samples_per_second : float | None, optional
Throughput reported by the trainer, by default None.
trainable_params : int | None, optional
Number of trainable parameters, by default None.
total_params : int | None, optional
Total parameter count, by default None.
probe_pass_rate : float | None, optional
Fraction of probe checks that passed after training, by default None.
eval_perplexity : float | None, optional
Holdout perplexity from Phase-2 eval, by default None.
judge_score : float | None, optional
Mean judge score in ``[0, 1]``, by default None.
extras : dict[str, Any], optional
Additional key/value metrics, by default empty.
"""
strategy: str
objective: str
model_id: str
train_loss: float | None = None
eval_loss: float | None = None
train_runtime_sec: float | None = None
train_samples_per_second: float | None = None
trainable_params: int | None = None
total_params: int | None = None
probe_pass_rate: float | None = None
eval_perplexity: float | None = None
judge_score: float | None = None
extras: dict[str, Any] = field(default_factory=dict)
@property
def trainable_percent(self) -> float | None:
"""Return trainable parameters as a percent of total.
Returns
-------
float or None
Percent trainable, or None if counts are missing.
"""
if self.trainable_params is None or self.total_params in (None, 0):
return None
return 100.0 * self.trainable_params / self.total_params
[docs]
@dataclass(kw_only=True)
class MetricsTracker:
"""Collect and persist metrics across a run.
Parameters
----------
output_dir : str or Path
Directory where ``metrics.json`` is written.
"""
output_dir: Path
[docs]
def __post_init__(self) -> None:
"""Ensure ``output_dir`` is a ``Path`` and exists."""
self.output_dir = Path(self.output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
[docs]
def save(self, metrics: TrainingMetrics) -> Path:
"""Write metrics to ``metrics.json``.
Parameters
----------
metrics : TrainingMetrics
Metrics snapshot to persist.
Returns
-------
Path
Path to the written JSON file.
"""
path = self.output_dir / "metrics.json"
payload = asdict(metrics)
payload["trainable_percent"] = metrics.trainable_percent
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
return path
[docs]
def load(self) -> TrainingMetrics:
"""Load metrics previously written by :meth:`save`.
Returns
-------
TrainingMetrics
Restored metrics object.
Raises
------
FileNotFoundError
If ``metrics.json`` is missing.
"""
path = self.output_dir / "metrics.json"
if not path.is_file():
raise FileNotFoundError(f"No metrics at {path}")
raw = json.loads(path.read_text(encoding="utf-8"))
raw.pop("trainable_percent", None)
return TrainingMetrics(**raw)
[docs]
def count_parameters(model: Any) -> tuple[int, int]:
"""Count trainable and total parameters.
Parameters
----------
model : Any
PyTorch module.
Returns
-------
tuple[int, int]
``(trainable_params, total_params)``.
"""
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
return trainable, total
__all__ = ["MetricsTracker", "TrainingMetrics", "count_parameters"]