gabfon's picture
Upload folder using huggingface_hub
3e363e2 verified
Raw History Blame Contribute Delete
3.91 kB
"""Programmatic parameter accounting and budget validation (PLAN section 6).
Section 6 asks for two things that the previous implementation did not do: a
*concrete* allocation across buckets, and programmatic validation of it. Only
the model total was checked before, so an allocation that put 46% of the budget
in PAT (against a 15-25% target) and 1.4% in the heads (against 10-15%) passed.
:func:`validate_budget` therefore checks every bucket fraction as well as the
total, and reports all violations at once rather than failing on the first.
"""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Protocol, runtime_checkable
from torch import nn
#: Section 6 budget ranges, as fractions of total trainable parameters.
BUDGET: Mapping[str, tuple[float, float]] = {
"graph_encoder": (0.10, 0.20),
"pat": (0.15, 0.25),
"reasoning_transformer": (0.45, 0.60),
"heads": (0.10, 0.15),
"embeddings": (0.05, 0.10),
}
@runtime_checkable
class BucketedModule(Protocol):
"""A module that can partition its own parameters into budget buckets."""
def parameter_buckets(self) -> Mapping[str, int]: ...
@dataclass(frozen=True)
class ParameterReport:
total: int
trainable: int
buckets: Mapping[str, int]
@property
def fractions(self) -> Mapping[str, float]:
return {
name: (count / self.total if self.total else 0.0)
for name, count in self.buckets.items()
}
def to_dict(self) -> dict[str, object]:
return {
"total_parameters": self.total,
"trainable_parameters": self.trainable,
**dict(self.buckets),
"fractions": {
name: round(value, 5) for name, value in self.fractions.items()
},
}
def count_parameters(model: nn.Module) -> ParameterReport:
total = sum(parameter.numel() for parameter in model.parameters())
trainable = sum(
parameter.numel() for parameter in model.parameters() if parameter.requires_grad
)
buckets = (
dict(model.parameter_buckets()) if isinstance(model, BucketedModule) else {}
)
return ParameterReport(total, trainable, buckets)
def validate_target(
report: ParameterReport, target: int, tolerance: float = 0.20
) -> None:
"""Check the trainable total lies within ``tolerance`` of ``target``."""
if target <= 0:
raise ValueError("target must be positive")
lower, upper = int(target * (1 - tolerance)), int(target * (1 + tolerance))
if not lower <= report.trainable <= upper:
raise ValueError(
f"trainable parameter count {report.trainable:,} is outside "
f"[{lower:,}, {upper:,}] for target {target:,}"
)
def budget_violations(
report: ParameterReport, budget: Mapping[str, tuple[float, float]] | None = None
) -> tuple[str, ...]:
"""Return a human-readable violation per bucket outside its budget range."""
ranges = budget or BUDGET
fractions = report.fractions
violations: list[str] = []
for name, (low, high) in ranges.items():
value = fractions.get(name)
if value is None:
violations.append(f"{name}: bucket missing from parameter report")
elif not low <= value <= high:
violations.append(
f"{name}: {value * 100:.1f}% outside target {low * 100:.0f}-{high * 100:.0f}%"
)
return tuple(violations)
def validate_budget(
report: ParameterReport, budget: Mapping[str, tuple[float, float]] | None = None
) -> None:
"""Fail with every budget violation listed, not just the first."""
violations = budget_violations(report, budget)
if violations:
raise ValueError(
"parameter allocation violates the PLAN section 6 budget:\n "
+ "\n ".join(violations)
)