Source code for chia.analysis.sustainability.focal

"""FOCAL: a first-order carbon model for comparing processor architectures.

Implements the Normalized Carbon Footprint (NCF) metric from Lieven Eeckhout,
"FOCAL: A First-Order Carbon Model to Assess Processor Sustainability"
(ASPLOS '24, https://doi.org/10.1145/3620665.3640415).

FOCAL weighs a design's *embodied* footprint (proxied by chip area) against its
*operational* footprint (proxied by power under a fixed-time use case, or by
energy under a fixed-work use case) using a single embodied-to-operational
weight ``alpha`` in [0, 1]. Comparing a ``test`` design against a ``ref``
baseline, the fixed-time NCF is::

    NCF(test, ref) = alpha * (A_test / A_ref) + (1 - alpha) * (P_test / P_ref)

where ``A`` is chip area and ``P`` is power consumption. ``NCF < 1`` means the
test design has a lower total carbon footprint than the reference (i.e. it is
more sustainable); ``NCF > 1`` means it is worse; ``NCF == 1`` means the two are
carbon-equal. The same expression holds for the fixed-work scenario with energy
substituted for power.

``alpha`` is the embodied-to-operational weight in [0, 1]. Paper scenarios:
``alpha = 0.8`` (embodied-dominated, e.g. mobile and datacenter hardware) and
``alpha = 0.2`` (operational-dominated, e.g. always-connected devices).
"""

from __future__ import annotations

import sys
import textwrap
import warnings
from dataclasses import dataclass
from pathlib import Path
from typing import Sequence

# Embodied-to-operational weight scenarios from the FOCAL paper (Section 5).
ALPHA_EMBODIED_DOMINATED = 0.8     # e.g. mobile / datacenter hardware
ALPHA_OPERATIONAL_DOMINATED = 0.2  # e.g. always-connected devices

# The five FOCAL parameters that focal_sweep can vary.
SWEEPABLE_PARAMETERS = ("test_pwr", "ref_pwr", "test_area", "ref_area", "alpha_ref")

_PARAM_LABELS = {
    "test_pwr": "test power",
    "ref_pwr": "ref power",
    "test_area": "test area",
    "ref_area": "ref area",
    "alpha_ref": r"embodied-to-operational weight $\alpha$",
}


[docs] @dataclass class FocalComparison: """FOCAL comparison of a ``test`` architecture against a ``ref`` baseline. Computed under the fixed-time scenario, i.e. power is the operational proxy. (Pass energy in place of power to evaluate the fixed-work scenario; the NCF expression is identical.) """ ncf: float # normalized carbon footprint NCF(test, ref) embodied_ratio: float # A_test / A_ref (normalized embodied footprint) operational_ratio: float # P_test / P_ref (normalized operational footprint) alpha_ref: float # embodied-to-operational weight used # Operating point echoed back for traceability. test_pwr: float ref_pwr: float test_area: float ref_area: float @property def more_sustainable(self) -> bool: """True if the test design has a strictly lower carbon footprint.""" return self.ncf < 1.0 @property def verdict(self) -> str: """Human-readable verdict for the test design relative to ref.""" if self.ncf < 1.0: return "more sustainable" if self.ncf > 1.0: return "less sustainable" return "equally sustainable" @property def pct_change(self) -> float: """Percent change in carbon footprint of test vs ref. Negative means a reduction (more sustainable); e.g. -30.0 means the test design emits 30% less carbon than the reference. """ return (self.ncf - 1.0) * 100.0 @property def breakeven_alpha(self) -> float | None: """Embodied weight ``alpha`` at which test and ref are carbon-equal. Solves ``NCF(alpha) = 1`` for alpha at the current area and power ratios:: alpha* = (1 - P_test/P_ref) / (A_test/A_ref - P_test/P_ref) Returns None when the embodied and operational ratios are equal (NCF does not depend on alpha, so there is no crossing). A value outside [0, 1] means the verdict never flips within the valid weight range, i.e. the test design is uniformly more or less sustainable. """ if self.embodied_ratio == self.operational_ratio: return None return (1.0 - self.operational_ratio) / (self.embodied_ratio - self.operational_ratio)
[docs] def focal_compare( test_pwr: float, ref_pwr: float, test_area: float, ref_area: float, alpha_ref: float, ) -> FocalComparison: """Compare a ``test`` architecture against a ``ref`` baseline with FOCAL. Computes the Normalized Carbon Footprint (NCF) of the test design relative to the reference under the fixed-time scenario:: NCF = alpha_ref * (test_area / ref_area) + (1 - alpha_ref) * (test_pwr / ref_pwr) ``NCF < 1`` means the test design has a lower total carbon footprint than the reference (more sustainable); ``NCF > 1`` means it is worse. Args: test_pwr: Power of the test design (operational proxy, numerator). Substitute energy to evaluate the fixed-work scenario instead. ref_pwr: Power of the reference design (operational proxy, denominator). test_area: Chip area of the test design (embodied proxy, numerator). ref_area: Chip area of the reference design (embodied proxy, denominator). alpha_ref: Embodied-to-operational weight in [0, 1] (the FOCAL ``alpha_E2O``). 0.8 = embodied-dominated, 0.2 = operational-dominated. Returns: A FocalComparison with the NCF, its embodied/operational components, and convenience verdicts. Raises: ValueError: if a denominator is non-positive, an input is negative, or ``alpha_ref`` is outside [0, 1]. """ if ref_pwr <= 0.0 or ref_area <= 0.0: raise ValueError("ref_pwr and ref_area must be positive (they are denominators)") if test_pwr < 0.0 or test_area < 0.0: raise ValueError("test_pwr and test_area must be non-negative") if not 0.0 <= alpha_ref <= 1.0: raise ValueError(f"alpha_ref must be in [0, 1], got {alpha_ref}") embodied_ratio = test_area / ref_area operational_ratio = test_pwr / ref_pwr ncf = alpha_ref * embodied_ratio + (1.0 - alpha_ref) * operational_ratio return FocalComparison( ncf=ncf, embodied_ratio=embodied_ratio, operational_ratio=operational_ratio, alpha_ref=alpha_ref, test_pwr=test_pwr, ref_pwr=ref_pwr, test_area=test_area, ref_area=ref_area, )
[docs] @dataclass class FocalSweep: """NCF of a test/ref pair as one FOCAL parameter is swept over a range.""" parameter: str # which of the five params was swept values: list[float] # swept parameter values ncf: list[float] # NCF at each swept value comparisons: list[FocalComparison] # full comparison at each swept value breakevens: list[float] # swept values where NCF crosses 1.0 # Nominal operating point (the swept field holds its nominal value here). test_pwr: float ref_pwr: float test_area: float ref_area: float alpha_ref: float @property def crosses_unity(self) -> bool: """True if the verdict flips (NCF crosses 1) within the swept range.""" return bool(self.breakevens)
[docs] @dataclass class FocalBothRefs: """Two alpha sweeps comparing arch A and arch B, each taken as the reference. Holds the ``arch_a_ref`` sweep (A is the reference, so the test design is B) and the ``arch_b_ref`` sweep (B is the reference, so the test design is A). See ``sweep_both_arches_as_ref`` for why both directions matter. """ arch_a_ref: FocalSweep # arch A as reference, test design is arch B arch_b_ref: FocalSweep # arch B as reference, test design is arch A
[docs] def focal_sweep( test_pwr: float, ref_pwr: float, test_area: float, ref_area: float, alpha_ref: float, *, sweep: str = "alpha_ref", values: Sequence[float] | None = None, num: int = 51, span: float = 2.0, plot_path: str | Path | None = None, ) -> FocalSweep: """Sweep one FOCAL parameter and report NCF across the range. Holds four of the five FOCAL parameters fixed at the values passed in and varies the fifth (``sweep``), recomputing the test-vs-ref NCF at each point. This mirrors the paper's sensitivity analyses: sweeping ``alpha_ref`` shows how the verdict shifts between operational- and embodied-dominated regimes, while sweeping an area or power term traces NCF against that dimension. Args: test_pwr: Nominal test power. ref_pwr: Nominal reference power. test_area: Nominal test area. ref_area: Nominal reference area. alpha_ref: Nominal embodied-to-operational weight in [0, 1]. sweep: Name of the parameter to vary; one of ``SWEEPABLE_PARAMETERS``. values: Explicit values to sweep. If None, a default range is built: ``alpha_ref`` sweeps [0, 1]; any other parameter sweeps from ``nominal / span`` to ``nominal * span``. num: Number of points in the default range (ignored if ``values`` given). span: Multiplicative half-range for non-alpha default sweeps. plot_path: If given, save a PNG of NCF vs the swept parameter there (requires matplotlib, imported lazily). Returns: A FocalSweep with the swept values, the NCF at each, the per-point FocalComparison objects, and any NCF==1 break-even crossings. Raises: ValueError: if ``sweep`` is not a FOCAL parameter, or a default range cannot be built because the nominal swept value is non-positive. """ if sweep not in SWEEPABLE_PARAMETERS: raise ValueError(f"sweep must be one of {SWEEPABLE_PARAMETERS}, got {sweep!r}") if sweep in ("ref_pwr", "ref_area"): _warn_ref_sweep(sweep) base = { "test_pwr": test_pwr, "ref_pwr": ref_pwr, "test_area": test_area, "ref_area": ref_area, "alpha_ref": alpha_ref, } if values is not None: swept_values = [float(v) for v in values] elif sweep == "alpha_ref": swept_values = _linspace(0.0, 1.0, num) else: nominal = base[sweep] if nominal <= 0.0: raise ValueError( f"cannot auto-range {sweep} from a non-positive nominal " f"({nominal}); pass explicit values=..." ) swept_values = _linspace(nominal / span, nominal * span, num) comparisons: list[FocalComparison] = [] for v in swept_values: params = dict(base) params[sweep] = v comparisons.append(focal_compare(**params)) ncf = [c.ncf for c in comparisons] breakevens = _level_crossings(swept_values, ncf, 1.0) result = FocalSweep( parameter=sweep, values=swept_values, ncf=ncf, comparisons=comparisons, breakevens=breakevens, test_pwr=test_pwr, ref_pwr=ref_pwr, test_area=test_area, ref_area=ref_area, alpha_ref=alpha_ref, ) if plot_path is not None: _plot_sweep(result, plot_path) return result
[docs] def sweep_both_arches_as_ref( arch_a_pwr: float, arch_a_area: float, arch_b_pwr: float, arch_b_area: float, *, values: Sequence[float] | None = None, num: int = 51, plot_path: str | Path | None = None, ) -> FocalBothRefs: """Sweep alpha comparing two architectures with *each* used as the reference. The NCF metric is not symmetric: ``NCF(X, Y) != 1 / NCF(Y, X)`` in general, because NCF is a weighted *sum* of the area and power ratios rather than a single ratio (in fact ``NCF(X, Y) * NCF(Y, X) >= 1``). The reference design is the denominator, so for the *same* alpha you can get a different NCF — and even a different verdict — depending on which architecture you pick as the reference. There is no privileged "baseline" here, so it is worth looking at both directions before concluding that one design is more sustainable. This runs two alpha sweeps over the same grid: one with arch A as the reference (test design is B) and one with arch B as the reference (test design is A). Compare the two curves at a given alpha to see the asymmetry. Args: arch_a_pwr: Power of architecture A. arch_a_area: Chip area of architecture A. arch_b_pwr: Power of architecture B. arch_b_area: Chip area of architecture B. values: Explicit alpha values to sweep. If None, sweeps [0, 1]. num: Number of alpha points in the default range (ignored if ``values``). plot_path: If given, save a PNG overlaying both NCF curves vs alpha (requires matplotlib, imported lazily). Returns: A FocalBothRefs holding the ``arch_a_ref`` and ``arch_b_ref`` sweeps. """ # Placeholder alpha_ref; it is overridden at every point of the alpha sweep. arch_a_ref = focal_sweep( test_pwr=arch_b_pwr, ref_pwr=arch_a_pwr, test_area=arch_b_area, ref_area=arch_a_area, alpha_ref=0.5, sweep="alpha_ref", values=values, num=num, ) arch_b_ref = focal_sweep( test_pwr=arch_a_pwr, ref_pwr=arch_b_pwr, test_area=arch_a_area, ref_area=arch_b_area, alpha_ref=0.5, sweep="alpha_ref", values=values, num=num, ) result = FocalBothRefs(arch_a_ref=arch_a_ref, arch_b_ref=arch_b_ref) if plot_path is not None: _plot_both_refs(result, plot_path) return result
def _warn_ref_sweep(sweep: str) -> None: """Loudly warn that sweeping a reference value at constant alpha_ref is dicey. Fires when ``focal_sweep`` is asked to vary ``ref_pwr`` or ``ref_area``: holding ``alpha_ref`` fixed across such a sweep is internally inconsistent, because the embodied-to-operational weight that actually applies to the reference design depends on its own power and area. """ message = ( "sweeping reference values with a constant alpha_ref yields difficult " "to understand results, since the true value of alpha_ref is a function " "of the ref_pwr and ref_area" ) width = 76 bar = "!" * width lines = ["", bar, "!!" + " FOCAL WARNING ".center(width - 4) + "!!", bar] for line in textwrap.wrap(f"You are sweeping {sweep!r}: {message}.", width - 6): lines.append("!! " + line.ljust(width - 6) + " !!") lines += [bar, ""] print("\n".join(lines), file=sys.stderr) warnings.warn(message, stacklevel=3) def _linspace(lo: float, hi: float, num: int) -> list[float]: """Evenly spaced values from lo to hi inclusive (pure-Python linspace).""" if num < 1: return [] if num == 1: return [lo] step = (hi - lo) / (num - 1) return [lo + step * i for i in range(num)] def _level_crossings(xs: list[float], ys: list[float], level: float = 1.0) -> list[float]: """Find x where the piecewise-linear (xs, ys) curve crosses ``level``. Returns the x of every exact hit and every sign-change crossing (located by linear interpolation between the bracketing samples). """ crossings: list[float] = [] for i in range(len(xs) - 1): a = ys[i] - level b = ys[i + 1] - level if a == 0.0: crossings.append(xs[i]) elif (a < 0.0 < b) or (b < 0.0 < a): t = a / (a - b) crossings.append(xs[i] + t * (xs[i + 1] - xs[i])) if ys and ys[-1] - level == 0.0: crossings.append(xs[-1]) return crossings def _plot_sweep(result: FocalSweep, plot_path: str | Path) -> None: """Save a PNG of NCF vs the swept parameter (matplotlib imported lazily).""" import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(7, 5)) ax.plot(result.values, result.ncf, color="#2b6cb0", marker="o", markersize=3, linewidth=1.5, label="NCF(test, ref)") ax.axhline(1.0, color="grey", linestyle="--", linewidth=1.0, label="carbon-equal (NCF = 1)") ymin, ymax = ax.get_ylim() ax.axhspan(ymin, 1.0, color="#c6f6d5", alpha=0.3, zorder=0, label="test more sustainable") ax.set_ylim(ymin, ymax) for i, be in enumerate(result.breakevens): ax.axvline(be, color="#e53e3e", linestyle=":", linewidth=1.0, label="break-even" if i == 0 else None) ax.set_xlabel(_PARAM_LABELS.get(result.parameter, result.parameter)) ax.set_ylabel("normalized carbon footprint (NCF)") ax.set_title(f"FOCAL sweep over {result.parameter}") ax.grid(alpha=0.3) ax.legend(loc="best", fontsize=8) fig.tight_layout() out = Path(plot_path) out.parent.mkdir(parents=True, exist_ok=True) fig.savefig(out, dpi=150) plt.close(fig) print(f" FOCAL sweep plot -> {out}") def _plot_both_refs(result: FocalBothRefs, plot_path: str | Path) -> None: """Overlay the two reference-choice NCF curves vs alpha (lazy matplotlib).""" import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(7, 5)) ax.plot(result.arch_a_ref.values, result.arch_a_ref.ncf, color="#2b6cb0", marker="o", markersize=3, linewidth=1.5, label="A as ref (NCF of B vs A)") ax.plot(result.arch_b_ref.values, result.arch_b_ref.ncf, color="#dd6b20", marker="s", markersize=3, linewidth=1.5, label="B as ref (NCF of A vs B)") ax.axhline(1.0, color="grey", linestyle="--", linewidth=1.0, label="carbon-equal (NCF = 1)") ax.set_xlabel(_PARAM_LABELS["alpha_ref"]) ax.set_ylabel("normalized carbon footprint (NCF)") ax.set_title("FOCAL: same alpha, each architecture as reference") ax.grid(alpha=0.3) ax.legend(loc="best", fontsize=8) fig.tight_layout() out = Path(plot_path) out.parent.mkdir(parents=True, exist_ok=True) fig.savefig(out, dpi=150) plt.close(fig) print(f" FOCAL both-refs plot -> {out}")