From baed92ecec74b6fa6c1afcd5ce5dce0471e5d452 Mon Sep 17 00:00:00 2001 From: Yuqi Xue Date: Sun, 22 Mar 2026 19:55:38 -0500 Subject: [PATCH] refactor backend and move power modeling to backend. --- CLAUDE.md | 99 ++ CONTRIBUTING.md | 5 +- README.md | 4 +- .../configs/power_gating/PowerGatingConfig.py | 309 ++++ .../tests/test_power_gating_config.py | 118 ++ neusim/npusim/backend/dvfs_policy_lib.py | 205 +++ neusim/npusim/backend/dvfs_power_getter.py | 771 +++++++++ neusim/npusim/backend/power_model.py | 1079 ++++++++++++ .../backend/tests/test_dvfs_policy_lib.py | 86 + .../backend/tests/test_dvfs_power_getter.py | 73 + .../npusim/backend/tests/test_power_model.py | 105 ++ neusim/npusim/frontend/dvfs_policy_lib.py | 220 +-- neusim/npusim/frontend/dvfs_power_getter.py | 790 +-------- neusim/npusim/frontend/power_analysis_lib.py | 1454 +---------------- 14 files changed, 2942 insertions(+), 2376 deletions(-) create mode 100644 CLAUDE.md create mode 100644 neusim/configs/power_gating/PowerGatingConfig.py create mode 100644 neusim/configs/power_gating/tests/test_power_gating_config.py create mode 100644 neusim/npusim/backend/dvfs_policy_lib.py create mode 100644 neusim/npusim/backend/dvfs_power_getter.py create mode 100644 neusim/npusim/backend/power_model.py create mode 100644 neusim/npusim/backend/tests/test_dvfs_policy_lib.py create mode 100644 neusim/npusim/backend/tests/test_dvfs_power_getter.py create mode 100644 neusim/npusim/backend/tests/test_power_model.py diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..eea388e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,99 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +NeuSim is a simulator framework for modeling performance, power, and carbon behaviors of Neural Processing Units (NPUs) running ML workloads. It supports LLMs (LLaMA, DeepSeek), diffusion models (DiT, GLIGEN), and recommendation models (DLRM). + +## Build & Development Commands + +```bash +# Install (use conda with Python 3.12.2) +pip install -e ".[dev]" + +# Run all tests with coverage +pytest + +# Run a single test file +pytest neusim/npusim/frontend/tests/test_operator.py + +# Run regression test (compares current code against previous commit) +pytest neusim/tests/regression/test_regression.py --runslow -v +# Or via env var +NEUSIM_RUN_SLOW_TESTS=1 pytest neusim/tests/regression/test_regression.py -v +# Compare against a specific commit/tag +NEUSIM_REGRESSION_BASELINE= pytest neusim/tests/regression/test_regression.py --runslow -v + +# Lint (critical ruff errors are blocking; style, format, mypy are warnings) +./lint.sh + +# Individual lint tools +ruff check --select E9,F821,F822,F823 . # critical errors only +ruff format --check . # format check +mypy . # type check +``` + +## Architecture + +The simulator has three main layers: + +**Frontend** (`neusim/npusim/frontend/`) — Model graph generation, orchestration, and analysis: +- Ops generators create operator graphs for each model type (`llm_ops_generator.py`, `dlrm_ops_generator.py`, `dit_ops_generator.py`, `gligen_ops_generator.py`) +- Orchestration wrappers for power/energy analysis (`power_analysis_lib.py`, `energy_carbon_analysis_lib.py`) +- Re-export facades for backward compatibility (`dvfs_policy_lib.py`, `dvfs_power_getter.py`) +- Memory footprint analysis (`memory_footprint_analysis_lib.py`) +- Simulation orchestration (`run_sim_lib.py`) + +**Backend** (`neusim/npusim/backend/`) — Core performance and power simulation: +- `npusim_lib.py` — Operator execution time calculation for einsum, conv2d, elementwise, attention ops; tensor shape parsing, component utilization tracking +- `power_model.py` — Energy modeling: dynamic/static energy per component, DVFS time scaling, regulator efficiency, peak FLOPS computation +- `dvfs_policy_lib.py` — DVFS policy selection: V/f band tables, frequency slowdown, voltage selection +- `dvfs_power_getter.py` — V/f lookup tables and power computation for SA, VU, SRAM, HBM, ICI components + +**XLA HLO Parser** (`neusim/xla_hlo_parser/`) — Parses XLA-style programs for operator semantics analysis. May be refactored into backend in the future. + +**Config System** (`neusim/configs/` + `configs/`): +- Pydantic models in `neusim/configs/` validate JSON configs from `configs/` +- Chip configs (`configs/chips/`): TPU hardware specs (v2 through v6p) +- Model configs (`configs/models/`): DNN architecture parameters +- System configs (`configs/systems/`): Datacenter-level settings (PUE, carbon intensity) +- Power gating configs (`neusim/configs/power_gating/`): `PowerGatingConfig` class and named presets (NoPG, Ideal, Base, HW, Full, etc.) + +## Key Abstractions + +- `Operator` class — Represents tensor operations with execution stats, DVFS config, and power gating. `OpcodeType` enum: Conv2D, Einsum, FlashAttention, Elementwise, Collective, Embedding, Other. +- `ChipConfig` — Hardware specs (systolic arrays, vector units, HBM/SRAM, bandwidth, power). +- `ModelConfig` — Combines model + parallelism + hardware + system config. +- `PowerGatingConfig` — Power gating settings per component (temporal/spatial granularity, delay cycles, power level factors). + +## Run Scripts + +Entry points in `neusim/run_scripts/`: +- `run_sim.py` — Performance simulation (Ray-distributed) +- `energy_operator_analysis_main.py` — Power/energy per-operator analysis +- `carbon_analysis_main.py` — Carbon footprint computation +- `slo_analysis_main.py` — SLO-aware NPU config search + +## Parallelism + +Supports data parallelism (DP), tensor parallelism (TP), pipeline parallelism (PP), and expert parallelism (EP) for MoE models. Multi-chip simulation includes ICI and DCN communication modeling. + +## Testing + +- Unit tests live alongside source in `tests/` subdirectories +- Regression tests live in `neusim/tests/regression/` + - `regression_runner.py` — Standalone simulation runner (no absl flags) + - `test_regression.py` — Compares outputs between current code and a baseline commit + - Marked `@pytest.mark.slow` and `@pytest.mark.regression`, skipped by default + - Always passes — generates a diff report to `results/regression/regression_report.txt` for human review + - Uses git worktree + lightweight venv to run baseline code in isolation + - Covers 7 experiments: LLM inference/training, DLRM, DiT, DeepSeek with varied parallelism (TP, PP, DP, EP, DP_DCN, PP_DCN, EP_DCN) +- `neusim/conftest.py` adds `--runslow` flag and `NEUSIM_RUN_SLOW_TESTS` env var to gate slow tests + +## Code Standards + +- Python 3.12, ruff for linting (ignores E501), mypy for type checking +- Type hints encouraged +- New files should target 90% test coverage +- Tests live alongside source in `tests/` subdirectories diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 58d9f0d..0cc4f94 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,9 +16,10 @@ The NeuSim codebase is organized as follows: - `chips/`: Chip configuration classes. - `models/`: Model configuration classes. - `systems/`: System configuration classes. + - `power_gating/`: Power gating configuration class and named presets. - **`npusim/`**: The core simulator logic. - - `backend/`: Backend performance simulation logic. - - `frontend/`: Ops generators (defining DNN model graphs) and power modeling. + - `backend/`: Core performance and power simulation logic (execution time modeling, energy modeling, DVFS policy and power lookup). + - `frontend/`: Ops generators (defining DNN model graphs), orchestration wrappers, and backward-compatible re-export facades. - **`run_scripts/`**: Scripts for running simulations, analysis, and examples. - **`xla_hlo_parser/`**: XLA-style program parser and analyzer framework. Used in NeuSim backend for analyzing operator semantics. This module may be refactored into the backend in future releases. diff --git a/README.md b/README.md index 568c459..5573798 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ The script `neusim/run_scripts/run_sim.py` automatically supports new configurat - `--versions`: specify the NPU chip versions. For example, if the user adds a new chip configuration file `configs/chips/tpuv7.json`, the user can specify `--versions="7"` to run simulations for this NPU version. #### Power Simulation Parameters -The power gating parameters are defined in `neusim/npusim/frontend/power_analysis_lib.py`. The user can modify the `get_power_gating_config()` function to add new power gating configurations, including power gating wake-up cycles and power gating policies for each component. +The power gating parameters are defined in `neusim/configs/power_gating/PowerGatingConfig.py`. The user can modify the `get_power_gating_config()` function to add new power gating configurations, including power gating wake-up cycles and power gating policies for each component. The scripts `neusim/run_scripts/energy_operator_analysis_main.py` and `neusim/run_scripts/carbon_analysis_main.py` can be invoked with command line arguments. The `--help` option shows all available options. To perform sensitivity study for power gating parameters, these two scripts support overriding the default power gating configurations via the `--power_gating_strategy` flag as follows: @@ -104,7 +104,7 @@ The `--help` option shows all available options. To perform sensitivity study fo - `_vary_PG_delay_`: vary power gating wake-up delay for sensitivity analysis. The value is specified as the ratio over the base config. See `neusim/run_scripts/run_power_gating_sensitivity_analysis.sh` for examples of how to specify different power gating strategies via the `--power_gating_strategy` flag. -See `neusim/npusim/frontend/power_analysis_lib.py:get_power_gating_config()` for how these parameters are being handled by NeuSim internally. +See `neusim/configs/power_gating/PowerGatingConfig.py:get_power_gating_config()` for how these parameters are being handled by NeuSim internally. ### Running a Single Tensor Operator Please see `neusim/run_scripts/run_single_op_main.py` for an example of how to run a single tensor operator simulation. This script is helpful for analyzing a specific operator of interest rather than simulating the entire DNN model. diff --git a/neusim/configs/power_gating/PowerGatingConfig.py b/neusim/configs/power_gating/PowerGatingConfig.py new file mode 100644 index 0000000..b24afd4 --- /dev/null +++ b/neusim/configs/power_gating/PowerGatingConfig.py @@ -0,0 +1,309 @@ +from enum import Enum +from math import ceil + +from pydantic import BaseModel + + +class PowerGatingConfig(BaseModel): + """ + Power-gating configuration. + """ + + class TemporalGranularity(Enum): + INSTRUCTION = 1 + OPERATOR = 2 + APPLICATION = 3 + + class SASpatialGranularity(Enum): + PE = 1 + PARTITION = 2 + COMPONENT = 3 + + class VUSpatialGranularity(Enum): + ALU = 1 + PARTITION = 2 + COMPONENT = 3 + + class VmemSpatialGranularity(Enum): + REGISTER_SIZE = 1 + PARTITION = 2 + + class ICISpatialGranularity(Enum): + LINK = 1 + COMPONENT = 2 + + class VoltageGranularity(Enum): + TWO_LEVEL = 1 # only on/off + MULTI_LEVEL = 2 # on/off + sleep modes + + class PowerGatingPolicy(Enum): + HW = 1 # HW-managed (auto mode) + SW = 2 # SW-managed + + name: str = "PowerGatingConfig" + SA_PG_enabled: bool = False + SA_PG_policy: PowerGatingPolicy = PowerGatingPolicy.HW + SA_temporal_granularity: TemporalGranularity = TemporalGranularity.INSTRUCTION + SA_spatial_granularity: SASpatialGranularity = SASpatialGranularity.COMPONENT + sa_partition_shapes: list[int] = [128, 128] + """partition shapes in number of PEs (128*128 by default)""" + sa_power_level_factors: list[float] = [1.0, 0.0] + """power consumption (0~1) at each voltage level (from highest power to lowest power)""" + sa_pe_pg_delay_cycles: int = 1 + """Delay in cycles of power gating and waking up a single PE.""" + sa_pg_delay_cycles: int = 10 + """Delay in cycles of power gating and waking up the entire SA.""" + + VU_PG_enabled: bool = False + VU_PG_policy: PowerGatingPolicy = PowerGatingPolicy.HW + VU_temporal_granularity: TemporalGranularity = TemporalGranularity.INSTRUCTION + VU_spatial_granularity: VUSpatialGranularity = VUSpatialGranularity.COMPONENT + vu_partition_shapes: list[int] = [8, 128] + """partition shapes in number of ALUs (8*128 by default)""" + vu_power_level_factors: list[float] = [1.0, 0.0] + """power consumption (0~1) at each voltage level (from highest power to lowest power)""" + vu_pg_delay_cycles: int = 2 + """Delay in cycles of power gating and waking up a VU.""" + + vmem_PG_enabled: bool = False + vmem_PG_policy: PowerGatingPolicy = PowerGatingPolicy.HW + vmem_temporal_granularity: TemporalGranularity = TemporalGranularity.INSTRUCTION + vmem_spatial_granularity: VmemSpatialGranularity = VmemSpatialGranularity.PARTITION + vmem_voltage_granularity: VoltageGranularity = VoltageGranularity.TWO_LEVEL + vmem_power_level_factors: list[float] = [1.0, 0.0] + """power consumption (0~1) at each voltage level (from highest power to lowest power)""" + vmem_partition_size_bytes: int = 2 * 1024 * 1024 + """partition size in bytes (2MB by default if spatial granularity is PARTITION)""" + vmem_partition_pg_delay_cycles: int = 10 + """Delay in cycles of power gating and waking up a vmem partition.""" + vmem_HW_drowsy_period_cycles: int = 2000 + """Period at which all vmem partitions are put into sleep.""" + + ici_PG_enabled: bool = False + ici_PG_policy: PowerGatingPolicy = PowerGatingPolicy.HW + ici_temporal_granularity: TemporalGranularity = TemporalGranularity.INSTRUCTION + ici_spatial_granularity: ICISpatialGranularity = ICISpatialGranularity.COMPONENT + ici_power_level_factors: list[float] = [1.0, 0.0] + """power consumption (0~1) at each voltage level (from highest power to lowest power)""" + ici_pg_delay_cycles: int = 10 + """Delay in cycles of power gating and waking up an ICI.""" + + hbm_PG_enabled: bool = False + hbm_PG_policy: PowerGatingPolicy = PowerGatingPolicy.HW + hbm_power_level_factors: list[float] = [1.0, 0.1] # 0.1 takes into account the auto refresh cost + """power consumption (0~1) at each voltage level (from highest power to lowest power)""" + hbm_refresh_interval_ns: int = 3900 + hbm_refresh_delay_ns: int = 400 # for 12H device + hbm_pg_delay_cycles: int = 60 + + other_PG_enabled: bool = False + other_PG_policy: PowerGatingPolicy = PowerGatingPolicy.HW + other_power_level_factors: list[float] = [1.0, 0.0] + """power consumption (0~1) at each voltage level (from highest power to lowest power)""" + + +def get_power_gating_config(pg_config_name: str) -> PowerGatingConfig: + """ + 'disabled', 'NoPG': no power gating. \n + 'ideal_inst_component': ideal power gating with instruction-level temporal granularity and component-level spatial granularity. \n + 'ideal_op_component': ideal power gating with operator-level temporal granularity and component-level spatial granularity. \n + 'ideal_inst_PE_ALU', 'Ideal': ideal power gating with instruction-level temporal granularity and PE/ALU-level spatial granularity. This should result in the most power savings. \n + 'Full': Same as 'Ideal' but with non-zero power-gating factor (power_level_factors) and delay cycles. \n + '\\\\_vary_Vth_\\_\\': vary Vth_low and Vth_sram for sensitivity analysis. The values are the percentage over Vdd. \n + '\\\\_vary_PG_delay_\\': vary PG delay for sensitivity analysis. The value is specified as the ratio over base config. \n + """ + pg_config = PowerGatingConfig + if pg_config_name in ["disabled", "NoPG"]: + pg_config = PowerGatingConfig(name="NoPG") + elif pg_config_name == "ideal_inst_component": + pg_config = PowerGatingConfig( + name="ideal_inst_component", + SA_PG_enabled=True, + SA_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + SA_spatial_granularity=PowerGatingConfig.SASpatialGranularity.COMPONENT, + sa_partition_shapes=[128, 128], + VU_PG_enabled=True, + VU_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + VU_spatial_granularity=PowerGatingConfig.VUSpatialGranularity.COMPONENT, + vmem_PG_enabled=True, + vmem_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + vmem_spatial_granularity=PowerGatingConfig.VmemSpatialGranularity.PARTITION, + vmem_voltage_granularity=PowerGatingConfig.VoltageGranularity.TWO_LEVEL, + vmem_power_level_factors=[1.0, 0.0], + vmem_partition_size_bytes=2 * 1024 * 1024, + ici_PG_enabled=True, + ici_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + ici_spatial_granularity=PowerGatingConfig.ICISpatialGranularity.COMPONENT, + ) + elif pg_config_name == "ideal_op_component": + pg_config = PowerGatingConfig( + name="ideal_op_component", + SA_PG_enabled=True, + SA_temporal_granularity=PowerGatingConfig.TemporalGranularity.OPERATOR, + SA_spatial_granularity=PowerGatingConfig.SASpatialGranularity.COMPONENT, + sa_partition_shapes=[128, 128], + VU_PG_enabled=True, + VU_temporal_granularity=PowerGatingConfig.TemporalGranularity.OPERATOR, + VU_spatial_granularity=PowerGatingConfig.VUSpatialGranularity.COMPONENT, + vmem_PG_enabled=True, + vmem_temporal_granularity=PowerGatingConfig.TemporalGranularity.OPERATOR, + vmem_spatial_granularity=PowerGatingConfig.VmemSpatialGranularity.PARTITION, + vmem_voltage_granularity=PowerGatingConfig.VoltageGranularity.TWO_LEVEL, + vmem_power_level_factors=[1.0, 0.0], + vmem_partition_size_bytes=2 * 1024 * 1024, + ici_PG_enabled=True, + ici_temporal_granularity=PowerGatingConfig.TemporalGranularity.OPERATOR, + ici_spatial_granularity=PowerGatingConfig.ICISpatialGranularity.COMPONENT, + ) + elif pg_config_name in ["ideal_inst_PE_ALU", "Ideal"]: + pg_config = PowerGatingConfig( + name="Ideal", + SA_PG_enabled=True, + SA_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + SA_spatial_granularity=PowerGatingConfig.SASpatialGranularity.PE, + sa_partition_shapes=[128, 128], + sa_power_level_factors=[1.0, 0.0], + sa_pe_pg_delay_cycles=0, + sa_pg_delay_cycles=0, + VU_PG_enabled=True, + VU_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + VU_spatial_granularity=PowerGatingConfig.VUSpatialGranularity.ALU, + vu_power_level_factors=[1.0, 0.0], + vu_pg_delay_cycles=0, + vmem_PG_enabled=True, + vmem_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + vmem_spatial_granularity=PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE, + vmem_voltage_granularity=PowerGatingConfig.VoltageGranularity.TWO_LEVEL, + vmem_power_level_factors=[1.0, 0.0], + vmem_partition_size_bytes=2 * 1024 * 1024, + vmem_partition_pg_delay_cycles=0, + ici_PG_enabled=True, + ici_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + ici_spatial_granularity=PowerGatingConfig.ICISpatialGranularity.COMPONENT, + ici_power_level_factors=[1.0, 0.0], + ici_pg_delay_cycles=0, + hbm_PG_enabled=True, + hbm_PG_policy=PowerGatingConfig.PowerGatingPolicy.SW, + hbm_pg_delay_cycles=0, + ) + elif pg_config_name.startswith("Base"): + pg_config = PowerGatingConfig( + name="Base", + SA_PG_enabled=True, + SA_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + SA_spatial_granularity=PowerGatingConfig.SASpatialGranularity.COMPONENT, + sa_partition_shapes=[128, 128], + # 0.03 -> 0.05 accounts for the fact that weight registers cannot be power gated + sa_power_level_factors=[1.0, 0.05], + sa_pe_pg_delay_cycles=1, + VU_PG_enabled=True, + VU_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + VU_spatial_granularity=PowerGatingConfig.VUSpatialGranularity.COMPONENT, + vu_power_level_factors=[1.0, 0.03], + vu_pg_delay_cycles=2, + vmem_PG_enabled=True, + vmem_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + vmem_spatial_granularity=PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE, + vmem_voltage_granularity=PowerGatingConfig.VoltageGranularity.TWO_LEVEL, + vmem_power_level_factors=[1.0, 0.25], + vmem_partition_size_bytes=2 * 1024 * 1024, + vmem_partition_pg_delay_cycles=4, + vmem_HW_drowsy_period_cycles=2000, + ici_PG_enabled=True, + ici_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + ici_spatial_granularity=PowerGatingConfig.ICISpatialGranularity.COMPONENT, + ici_power_level_factors=[1.0, 0.03], + ici_pg_delay_cycles=60, + hbm_PG_enabled=True, + hbm_PG_policy=PowerGatingConfig.PowerGatingPolicy.HW, + ) + elif pg_config_name.startswith("HW"): + pg_config = PowerGatingConfig( + name="HW", + SA_PG_enabled=True, + SA_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + SA_spatial_granularity=PowerGatingConfig.SASpatialGranularity.PE, + sa_partition_shapes=[128, 128], + sa_power_level_factors=[1.0, 0.03], + sa_pe_pg_delay_cycles=1, + VU_PG_enabled=True, + VU_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + VU_spatial_granularity=PowerGatingConfig.VUSpatialGranularity.COMPONENT, + vu_power_level_factors=[1.0, 0.03], + vu_pg_delay_cycles=2, + vmem_PG_enabled=True, + vmem_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + vmem_spatial_granularity=PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE, + vmem_voltage_granularity=PowerGatingConfig.VoltageGranularity.TWO_LEVEL, + vmem_power_level_factors=[1.0, 0.25], + vmem_partition_size_bytes=2 * 1024 * 1024, + vmem_partition_pg_delay_cycles=4, + vmem_HW_drowsy_period_cycles=2000, + ici_PG_enabled=True, + ici_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + ici_spatial_granularity=PowerGatingConfig.ICISpatialGranularity.COMPONENT, + ici_power_level_factors=[1.0, 0.03], + ici_pg_delay_cycles=60, + hbm_PG_enabled=True, + hbm_PG_policy=PowerGatingConfig.PowerGatingPolicy.HW, + ) + elif pg_config_name.startswith("Full"): + pg_config = PowerGatingConfig( + name="Full", + SA_PG_enabled=True, + SA_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + SA_spatial_granularity=PowerGatingConfig.SASpatialGranularity.PE, + sa_partition_shapes=[128, 128], + sa_power_level_factors=[1.0, 0.03], + sa_pe_pg_delay_cycles=1, + VU_PG_enabled=True, + VU_PG_policy=PowerGatingConfig.PowerGatingPolicy.SW, + VU_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + VU_spatial_granularity=PowerGatingConfig.VUSpatialGranularity.COMPONENT, + vu_power_level_factors=[1.0, 0.03], + vu_pg_delay_cycles=2, + vmem_PG_enabled=True, + vmem_PG_policy=PowerGatingConfig.PowerGatingPolicy.SW, + vmem_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + vmem_spatial_granularity=PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE, + vmem_voltage_granularity=PowerGatingConfig.VoltageGranularity.TWO_LEVEL, + vmem_power_level_factors=[1.0, 0.0002], + vmem_partition_size_bytes=2 * 1024 * 1024, + vmem_partition_pg_delay_cycles=10, + ici_PG_enabled=True, + ici_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, + ici_spatial_granularity=PowerGatingConfig.ICISpatialGranularity.COMPONENT, + ici_power_level_factors=[1.0, 0.03], + ici_pg_delay_cycles=60, + hbm_PG_enabled=True, + hbm_PG_policy=PowerGatingConfig.PowerGatingPolicy.SW, + ) + else: + raise ValueError(f"Unsupported power gating configuration: {pg_config_name}") + + # vary Vth_low and PG delay for sensitivity analysis + if "vary_Vth" in pg_config_name: + # name scheme: "_vary_Vth__" + pg_config.name = pg_config_name + Vth = float(pg_config_name.split("_")[-2]) + Vth_sram = float(pg_config_name.split("_")[-1]) + pg_config.sa_power_level_factors[-1] = Vth + pg_config.vu_power_level_factors[-1] = Vth + pg_config.vmem_power_level_factors[-1] = Vth_sram + pg_config.ici_power_level_factors[-1] = Vth + pg_config.hbm_power_level_factors[-1] = Vth + pg_config.other_power_level_factors[-1] = Vth + if "vary_PG_delay" in pg_config_name: + # name scheme: "_vary_PG_delay_" + # is the extra delay ratio: new delay = old delay * + # This do not apply to PEs in the SA. + pg_config.name = pg_config_name + pg_delay = float(pg_config_name.split("_")[-1]) + pg_config.sa_pg_delay_cycles = ceil(pg_config.sa_pg_delay_cycles * pg_delay) + pg_config.vu_pg_delay_cycles = ceil(pg_config.vu_pg_delay_cycles * pg_delay) + pg_config.vmem_partition_pg_delay_cycles = ceil( + pg_config.vmem_partition_pg_delay_cycles * pg_delay + ) + pg_config.ici_pg_delay_cycles = ceil(pg_config.ici_pg_delay_cycles * pg_delay) + + return pg_config diff --git a/neusim/configs/power_gating/tests/test_power_gating_config.py b/neusim/configs/power_gating/tests/test_power_gating_config.py new file mode 100644 index 0000000..4f45378 --- /dev/null +++ b/neusim/configs/power_gating/tests/test_power_gating_config.py @@ -0,0 +1,118 @@ +import unittest +from math import ceil + +from neusim.configs.power_gating.PowerGatingConfig import ( + PowerGatingConfig, + get_power_gating_config, +) + + +class TestPowerGatingConfig(unittest.TestCase): + def test_default_construction(self): + pg = PowerGatingConfig() + self.assertEqual(pg.name, "PowerGatingConfig") + self.assertFalse(pg.SA_PG_enabled) + self.assertFalse(pg.VU_PG_enabled) + self.assertFalse(pg.vmem_PG_enabled) + self.assertFalse(pg.ici_PG_enabled) + self.assertFalse(pg.hbm_PG_enabled) + self.assertFalse(pg.other_PG_enabled) + + def test_NoPG(self): + pg = get_power_gating_config("NoPG") + self.assertEqual(pg.name, "NoPG") + self.assertFalse(pg.SA_PG_enabled) + + def test_disabled_alias(self): + pg = get_power_gating_config("disabled") + self.assertEqual(pg.name, "NoPG") + + def test_ideal_inst_component(self): + pg = get_power_gating_config("ideal_inst_component") + self.assertTrue(pg.SA_PG_enabled) + self.assertEqual( + pg.SA_temporal_granularity, + PowerGatingConfig.TemporalGranularity.INSTRUCTION, + ) + self.assertEqual( + pg.SA_spatial_granularity, + PowerGatingConfig.SASpatialGranularity.COMPONENT, + ) + self.assertTrue(pg.VU_PG_enabled) + self.assertTrue(pg.vmem_PG_enabled) + self.assertTrue(pg.ici_PG_enabled) + + def test_ideal_op_component(self): + pg = get_power_gating_config("ideal_op_component") + self.assertEqual( + pg.SA_temporal_granularity, + PowerGatingConfig.TemporalGranularity.OPERATOR, + ) + + def test_Ideal(self): + pg = get_power_gating_config("Ideal") + self.assertEqual(pg.name, "Ideal") + self.assertTrue(pg.SA_PG_enabled) + self.assertEqual( + pg.SA_spatial_granularity, + PowerGatingConfig.SASpatialGranularity.PE, + ) + self.assertEqual(pg.sa_pe_pg_delay_cycles, 0) + self.assertTrue(pg.hbm_PG_enabled) + self.assertEqual(pg.sa_power_level_factors, [1.0, 0.0]) + + def test_ideal_inst_PE_ALU_alias(self): + pg = get_power_gating_config("ideal_inst_PE_ALU") + self.assertEqual(pg.name, "Ideal") + + def test_Base(self): + pg = get_power_gating_config("Base") + self.assertEqual(pg.name, "Base") + self.assertEqual(pg.sa_power_level_factors, [1.0, 0.05]) + self.assertEqual(pg.sa_pe_pg_delay_cycles, 1) + self.assertEqual( + pg.SA_spatial_granularity, + PowerGatingConfig.SASpatialGranularity.COMPONENT, + ) + + def test_HW(self): + pg = get_power_gating_config("HW") + self.assertEqual(pg.name, "HW") + self.assertEqual( + pg.SA_spatial_granularity, + PowerGatingConfig.SASpatialGranularity.PE, + ) + + def test_Full(self): + pg = get_power_gating_config("Full") + self.assertEqual(pg.name, "Full") + self.assertEqual( + pg.VU_PG_policy, + PowerGatingConfig.PowerGatingPolicy.SW, + ) + self.assertEqual( + pg.vmem_PG_policy, + PowerGatingConfig.PowerGatingPolicy.SW, + ) + + def test_vary_Vth(self): + pg = get_power_gating_config("Full_vary_Vth_0.1_0.2") + self.assertEqual(pg.name, "Full_vary_Vth_0.1_0.2") + self.assertAlmostEqual(pg.sa_power_level_factors[-1], 0.1) + self.assertAlmostEqual(pg.vmem_power_level_factors[-1], 0.2) + self.assertAlmostEqual(pg.vu_power_level_factors[-1], 0.1) + + def test_vary_PG_delay(self): + base = get_power_gating_config("Base") + pg = get_power_gating_config("Base_vary_PG_delay_2.0") + self.assertEqual(pg.name, "Base_vary_PG_delay_2.0") + self.assertEqual(pg.sa_pg_delay_cycles, ceil(base.sa_pg_delay_cycles * 2.0)) + self.assertEqual(pg.vu_pg_delay_cycles, ceil(base.vu_pg_delay_cycles * 2.0)) + + def test_invalid_name_raises(self): + with self.assertRaises(ValueError): + get_power_gating_config("nonexistent_config") + + +if __name__ == "__main__": + unittest.main() diff --git a/neusim/npusim/backend/dvfs_policy_lib.py b/neusim/npusim/backend/dvfs_policy_lib.py new file mode 100644 index 0000000..66f407c --- /dev/null +++ b/neusim/npusim/backend/dvfs_policy_lib.py @@ -0,0 +1,205 @@ +### Pre-defined DVFS policies, and helper functions to apply DVFS policies to Operators. + +from neusim.configs.chips.ChipConfig import ChipConfig +import neusim.npusim.frontend.Operator as Operator +from neusim.npusim.frontend.Operator import ComponentDVFSConfig, DVFSPolicy, DVFSConfig +from neusim.npusim.frontend.util import compute_component_slack_for_op + + +# ----- SA voltage-frequency bands ----- +SA_VF_TABLE = [ + (0.45, 0.600240096), + (0.50, 0.850340136), + (0.55, 1.149425287), + (0.60, 1.351351351), + (0.65, 1.602564103), + (0.70, 1.700680272), +] + +# ----- VU voltage-frequency bands ----- +VU_VF_TABLE = [ + (0.45, 0.600240096), + (0.50, 0.850340136), + (0.55, 1.149425287), + (0.60, 1.351351351), + (0.65, 1.602564103), + (0.70, 1.700680272), +] + +# ----- SRAM voltage-frequency bands ----- +SRAM_VF_TABLE = [ + (0.45, 0.500000000), + (0.50, 0.750750751), + (0.55, 1.050420168), + (0.60, 1.250000000), + (0.65, 1.501501502), + (0.70, 1.700680272), +] + +# ----- HBM voltage-bandwidth-frequency proxy table ----- +# (v, f_proxy_ghz) +HBM_VF_TABLE = [ + # (1.00, 1.459), + # (1.05, 1.544), + # (1.10, 1.629), + # (1.15, 1.712), + # (1.20, 1.793), + (0.45, 1.311252269), + (0.50, 1.388384755), + (0.55, 1.463666062), + (0.60, 1.543883848), + (0.65, 1.622250454), + (0.70, 1.7), +] + +ICI_VF_TABLE = [ + (0.45, 1.308720), + (0.50, 1.388110), + (0.55, 1.461829), + (0.60, 1.541220), + (0.65, 1.620610), + (0.70, 1.7), +] + + +def slowdown_freq(ratio: float, base_freq_GHz: float, min_freq_GHz: float = 0.05) -> float: + """ + Given ratio = extra / active_time, use all slack: + new_time = active_time * (1 + ratio) + new_freq = base_freq / (1 + ratio) + Clamp into [min_freq_GHz, base_freq_GHz]. + """ + if ratio <= 0: + return base_freq_GHz + f = base_freq_GHz / (1.0 + ratio) + if f < min_freq_GHz: + f = min_freq_GHz + if f > base_freq_GHz: + f = base_freq_GHz + return f + + +def pick_v_from_freq(f_ghz: float, table: list[tuple[float, float]]) -> float: + ''' + @return: voltage for a given frequency @f_ghz in the (V, f) @table. + Assume table is sorted ascending. + ''' + if f_ghz <= 0: + return 0.0 + + v0, f0 = table[0] + if f_ghz <= f0: + return v0 + + for i in range(len(table) - 1): + v_curr, f_curr = table[i] + v_next, f_next = table[i + 1] + if f_ghz > f_curr and f_ghz <= f_next: + return v_next + + return table[-1][0] + + +def comp(policy: DVFSPolicy, v: float, f_ghz: float, scaling_time_ns: int = 20) -> ComponentDVFSConfig: + '''Helper to build component entries''' + return ComponentDVFSConfig( + policy=policy, + voltage_V=v, + frequency_GHz=f_ghz, + voltage_regulator_scaling_time_ns=scaling_time_ns, + ) + + +def get_dvfs_policy_None( + op: Operator.Operator | None = None, config: ChipConfig | None = None, dvfs_cfg: DVFSConfig | None = None, # unused +) -> dict[str, ComponentDVFSConfig]: + plan = { + "sa": comp(DVFSPolicy.NONE, 0.7, 1.7, 0), + "vu": comp(DVFSPolicy.NONE, 0.7, 1.7, 0), + "sram": comp(DVFSPolicy.NONE, 0.7, 1.7, 0), + "hbm": comp(DVFSPolicy.NONE, 0.7, 1.7, 0), + "ici": comp(DVFSPolicy.NONE, 0.7, 1.7, 0), + } + return plan + + +def get_dvfs_policy_Ideal( + op: Operator.Operator, config: ChipConfig, dvfs_cfg: DVFSConfig, +) -> dict[str, ComponentDVFSConfig]: + + # Fixed base frequency for DVFS planning (GHz) + base_freq_ghz = config.freq_GHz + + # Per-operator slack ratios + extras, ratios = compute_component_slack_for_op(op) + + # SA + if op.stats.sa_time_ns > 0: + f_sa_ghz = slowdown_freq(ratios["sa"], base_freq_ghz) + v_sa = pick_v_from_freq(f_sa_ghz, SA_VF_TABLE) + else: + f_sa_ghz, v_sa = 0.05, 0.45 # min freq/v + + # VU + if op.stats.vu_time_ns > 0: + f_vu_ghz = slowdown_freq(ratios["vu"], base_freq_ghz) + v_vu = pick_v_from_freq(f_vu_ghz, VU_VF_TABLE) + else: + f_vu_ghz, v_vu = 0.05, 0.45 # min freq/v + + # SRAM / Vmem (use vmem slack) + if op.stats.vmem_time_ns > 0: + f_sram_ghz = slowdown_freq(ratios["vmem"], base_freq_ghz) + v_sram = pick_v_from_freq(f_sram_ghz, SRAM_VF_TABLE) + else: + f_sram_ghz, v_sram = 0.05, 0.45 # min freq/v + + # HBM + if op.stats.memory_time_ns > 0: + f_hbm_ghz = slowdown_freq(ratios["hbm"], base_freq_ghz) + v_hbm = pick_v_from_freq(f_hbm_ghz, HBM_VF_TABLE) + else: + f_hbm_ghz, v_hbm = 0.0, 0.45 # min freq/v + + # ICI + if op.stats.ici_time_ns > 0: + f_ici_ghz = slowdown_freq(ratios["ici"], base_freq_ghz) + v_ici = pick_v_from_freq(f_ici_ghz, ICI_VF_TABLE) + else: + f_ici_ghz, v_ici = 0.0, 0.45 # min freq/v + + plan = { + # use 200ns scaling time for IDEAL DVFS since it is most power efficient + # when calculating time overhead, we ignore this scaling time for ideal DVFS + "sa": comp(DVFSPolicy.IDEAL, v_sa, f_sa_ghz, 200), + "vu": comp(DVFSPolicy.IDEAL, v_vu, f_vu_ghz, 200), + "sram": comp(DVFSPolicy.IDEAL, v_sram, f_sram_ghz, 200), + "hbm": comp(DVFSPolicy.IDEAL, v_hbm, f_hbm_ghz, 200), + "ici": comp(DVFSPolicy.IDEAL, v_ici, f_ici_ghz, 200), + } + return plan + + +def get_dvfs_config( + op: Operator.Operator, + config: ChipConfig, + dvfs_cfg: DVFSConfig, +) -> dict[str, ComponentDVFSConfig]: + """ + Build the DVFSConfigs for each component for this operator based on dvfs_mode. + Only SA, VU, SRAM/VMEM, HBM, ICI are controlled. + """ + + plan: dict[str, ComponentDVFSConfig] = {} + # For NONE, configure as the default numbers. + if dvfs_cfg.policy == DVFSPolicy.NONE: + plan = get_dvfs_policy_None(op, config) + + # For IDEAL, use the computed per-op settings. + elif dvfs_cfg.policy == DVFSPolicy.IDEAL: + plan = get_dvfs_policy_Ideal(op, config, dvfs_cfg) + + else: + raise ValueError(f"Unsupported DVFSPolicy: {dvfs_cfg.policy}") + + return plan diff --git a/neusim/npusim/backend/dvfs_power_getter.py b/neusim/npusim/backend/dvfs_power_getter.py new file mode 100644 index 0000000..e2b5cdb --- /dev/null +++ b/neusim/npusim/backend/dvfs_power_getter.py @@ -0,0 +1,771 @@ +from absl import logging +from functools import lru_cache +import itertools +from typing import NamedTuple + +import neusim.npusim.frontend.Operator as Operator +from neusim.npusim.frontend.Operator import DVFSPolicy, DVFSConfig, ComponentDVFSConfig + +""" +Map DVFS config (voltage_V, frequency_GHz) to dynamic/static power +using discrete SA/VU/SRAM/HBM/ICI tables. +""" + +# Type aliases for table structures +class VfPoint(NamedTuple): + '''Voltage/frequency point for SA/VU/SRAM components.''' + voltage_V: float = 0.7 + frequency_GHz: float = 1.7 + static_power_W: float = 0.0 + dynamic_power_W: float = 0.0 + +class VBWPoint(NamedTuple): + '''Voltage/bandwidth point for HBM/ICI components.''' + voltage_V: float = 0.7 + bandwidth_GBs: float = 0.0 + static_power_W: float = 0.0 + dynamic_power_W: float = 0.0 + +class PowerEfficiencyPoint(NamedTuple): + '''DVFS voltage regulator power conversion efficiency point for each scaling time, activity factor, and voltage.''' + scaling_time_ns: int = 0 + activity_factor: float = 1.0 + voltage_V: float = 0.7 + power_efficiency_percent: float = 100.0 + +# Row = tuple[float, float, float] +Row = NamedTuple('Row', [('x', float), ('s', float), ('d', float)]) # x can be frequency_GHz or bandwidth_GBs +Groups = dict[float, list[Row]] # keyed by voltage_V, value is list of Rows + +# ========================= +# Lookup tables for each component at different V/f points. +# The tables are only for TPUv5p's HW spec assuming 7nm FinFET node for now. +# SA and VU points are for a single SA/VU. +# ========================= + +_SA_POINTS = [ + # voltage_V, frequency_GHz, static_power_W, dynamic_power_W + (0.45, 0, 0.5883612773214286, 0.0), + (0.45, 0.05, 0.5883612773214286, 0.244), + (0.45, 0.1, 0.5883612773214286, 0.489), + (0.45, 0.150015002, 0.5883612773214286, 0.731), + (0.45, 0.2, 0.5883612773214286, 0.975), + (0.45, 0.25, 0.5883612773214286, 1.22), + (0.45, 0.300120048, 0.5883612773214286, 1.46), + (0.45, 0.350140056, 0.5883612773214286, 1.71), + (0.45, 0.4, 0.5883612773214286, 1.95), + (0.45, 0.450045005, 0.5883612773214286, 2.2), + (0.45, 0.5, 0.5883612773214286, 2.44), + (0.45, 0.550055006, 0.5883612773214286, 2.68), + (0.45, 0.600240096, 0.5883612773214286, 2.93), + (0.5, 0.650195059, 0.7066400908035716, 3.97), + (0.5, 0.700280112, 0.7066400908035716, 4.28), + (0.5, 0.750750751, 0.7066400908035716, 4.58), + (0.5, 0.8, 0.7066400908035716, 4.89), + (0.5, 0.850340136, 0.7066400908035716, 5.2), + (0.55, 0.900900901, 0.8370500646428571, 6.76), + (0.55, 0.950570342, 0.8370500646428571, 7.13), + (0.55, 1.0, 0.8370500646428571, 7.49), + (0.55, 1.050420168, 0.8370500646428571, 7.86), + (0.55, 1.101321586, 0.8370500646428571, 8.24), + (0.6, 1.149425287, 0.9947551492857144, 10.368), + (0.6, 1.201923077, 0.9947551492857144, 10.8544), + (0.6, 1.25, 0.9947551492857144, 11.264), + (0.6, 1.302083333, 0.9947551492857144, 11.7504), + (0.6, 1.351351351, 0.9947551492857144, 12.2112), + (0.65, 1.400560224, 1.1645913942857145, 15.0016), + (0.65, 1.449275362, 1.1645913942857145, 15.5136), + (0.65, 1.501501502, 1.1645913942857145, 16.0768), + (0.65, 1.552795031, 1.1645913942857145, 16.6144), + (0.65, 1.602564103, 1.1645913942857145, 17.152), + (0.7, 1.650165017, 1.35868996 , 20.6848), + (0.7, 1.7, 1.35868996 , 21.3248), +] +SA_POINTS: list[VfPoint] = [VfPoint(*point) for point in _SA_POINTS] + +_VU_POINTS = [ + # voltage_V, frequency_GHz, static_power_W, dynamic_power_W + (0.45, 0, 0.2054646559675127, 0.0), + (0.45, 0.05, 0.2054646559675127, 0.0571), + (0.45, 0.1, 0.2054646559675127, 0.114), + (0.45, 0.150015002, 0.2054646559675127, 0.171), + (0.45, 0.2, 0.2054646559675127, 0.228), + (0.45, 0.25, 0.2054646559675127, 0.285), + (0.45, 0.300120048, 0.2054646559675127, 0.343), + (0.45, 0.350140056, 0.2054646559675127, 0.4), + (0.45, 0.4, 0.2054646559675127, 0.457), + (0.45, 0.450045005, 0.2054646559675127, 0.514), + (0.45, 0.5, 0.2054646559675127, 0.571), + (0.45, 0.550055006, 0.2054646559675127, 0.629), + (0.45, 0.600240096, 0.2054646559675127, 0.686), + (0.5, 0.650195059, 0.2459788134822335, 0.936), + (0.5, 0.700280112, 0.2459788134822335, 1.01), + (0.5, 0.750750751, 0.2459788134822335, 1.08), + (0.5, 0.8, 0.2459788134822335, 1.15), + (0.5, 0.850340136, 0.2459788134822335, 1.22), + (0.55, 0.900900901, 0.29324533058274116, 1.6), + (0.55, 0.950570342, 0.29324533058274116, 1.68), + (0.55, 1.0, 0.29324533058274116, 1.77), + (0.55, 1.050420168, 0.29324533058274116, 1.86), + (0.55, 1.101321586, 0.29324533058274116, 1.95), + (0.55, 1.149425287, 0.29324533058274116, 2.04), + (0.6, 1.201923077, 0.3458172730720812, 2.57), + (0.6, 1.25, 0.3458172730720812, 2.68), + (0.6, 1.302083333, 0.3458172730720812, 2.79), + (0.6, 1.351351351, 0.3458172730720812, 2.89), + (0.6, 1.400560224, 0.3458172730720812, 3.0), + (0.65, 1.449275362, 0.40707082074314727, 3.69), + (0.65, 1.501501502, 0.40707082074314727, 3.83), + (0.65, 1.552795031, 0.40707082074314727, 3.96), + (0.65, 1.602564103, 0.40707082074314727, 4.08), + (0.7, 1.650165017, 0.475076728, 4.94), + (0.7, 1.7, 0.475076728, 5.10), +] +VU_POINTS: list[VfPoint] = [VfPoint(*point) for point in _VU_POINTS] + +_SRAM_POINTS = [ + # voltage_V, frequency_GHz, static_power_W, dynamic_power_W + (0.45, 0, 6.43650960949367, 0.0), + (0.45, 0.05, 6.43650960949367, 0.791), + (0.45, 0.1, 6.43650960949367, 1.58), + (0.45, 0.150015002, 6.43650960949367, 2.37), + (0.45, 0.2, 6.43650960949367, 3.16), + (0.45, 0.25, 6.43650960949367, 3.95), + (0.45, 0.300120048, 6.43650960949367, 4.74), + (0.45, 0.350140056, 6.43650960949367, 5.53), + (0.45, 0.4, 6.43650960949367, 6.32), + (0.45, 0.450045005, 6.43650960949367, 7.11), + (0.45, 0.5, 6.43650960949367, 7.90), + (0.5, 0.550055006, 8.837429860654009, 10.7), + (0.5, 0.600240096, 8.837429860654009, 11.7), + (0.5, 0.650195059, 8.837429860654009, 12.7), + (0.5, 0.700280112, 8.837429860654009, 13.7), + (0.5, 0.750750751, 8.837429860654009, 14.6), + (0.55, 0.8, 11.749184207805905, 18.9), + (0.55, 0.850340136, 11.749184207805905, 20.1), + (0.55, 0.900900901, 11.749184207805905, 21.3), + (0.55, 0.950570342, 11.749184207805905, 22.4), + (0.55, 1.0, 11.749184207805905, 23.6), + (0.55, 1.050420168, 11.749184207805905, 24.8), + (0.6, 1.101321586, 15.248397765348098, 30.9), + (0.6, 1.149425287, 15.248397765348098, 32.3), + (0.6, 1.201923077, 15.248397765348098, 33.8), + (0.6, 1.25, 15.248397765348098, 35.1), + (0.65, 1.302083333, 19.386153942879744, 42.9), + (0.65, 1.351351351, 19.386153942879744, 44.6), + (0.65, 1.400560224, 19.386153942879744, 46.17327223), + (0.65, 1.449275362, 19.386153942879744, 47.77934338), + (0.65, 1.501501502, 19.386153942879744, 49.50132232), + (0.7, 1.552795031, 24.21353615, 59.4), + (0.7, 1.602564103, 24.21353615, 61.3), + (0.7, 1.650165017, 24.21353615, 63.1), + (0.7, 1.7, 24.21353615, 65.0), +] +SRAM_POINTS: list[VfPoint] = [VfPoint(*point) for point in _SRAM_POINTS] + +_HBM_POINTS = [ + ### This table has the following assumptions: + ### - Only DVFS the memory controller, not the PHY, I/O bus, and DRAM arrays. + ### - The power split between MC and PHY is 40%/60% at max BW/freq point (an empirical estimate). + # voltage_V, bandwidth_GBs, static_power_W, dynamic_power_W + (0.45, 0.00, 21.861016, 0.000000), # 0.000000 (scaled ref frequency) + (0.45, 81.029412, 21.861016, 0.796834), # 0.050000000 + (0.45, 162.058824, 21.861016, 1.593668), # 0.100000000 + (0.45, 243.088235, 21.861016, 2.390502), # 0.150000000 + (0.45, 324.117647, 21.861016, 3.187337), # 0.200000000 + (0.45, 405.147059, 21.861016, 3.984171), # 0.250000000 + (0.45, 486.176471, 21.861016, 4.781005), # 0.300000000 + (0.45, 567.205882, 21.861016, 5.577839), # 0.350000000 + (0.45, 648.235294, 21.861016, 6.374673), # 0.400000000 + (0.45, 729.264706, 21.861016, 7.171507), # 0.450000000 + (0.45, 810.294118, 21.861016, 7.968341), # 0.500000000 + (0.50, 891.323529, 22.478464, 9.124189), # 0.550000000 + (0.50, 972.352941, 22.478464, 9.953661), # 0.600000000 + (0.50, 1053.382353, 22.478464, 10.783133), # 0.650000000 + (0.50, 1134.411765, 22.478464, 11.612604), # 0.700000000 + (0.50, 1215.441176, 22.478464, 12.442076), # 0.750000000 + (0.55, 1296.470588, 23.191154, 13.848718), # 0.800000000 + (0.55, 1377.500000, 23.191154, 14.714263), # 0.850000000 + (0.55, 1458.529412, 23.191154, 15.579808), # 0.900000000 + (0.55, 1539.558824, 23.191154, 16.445353), # 0.950000000 + (0.55, 1620.588235, 23.191154, 17.310897), # 1.000000000 + (0.55, 1701.617647, 23.191154, 18.176442), # 1.050000000 + (0.60, 1782.647059, 23.956602, 19.911178), # 1.100000000 + (0.60, 1863.676471, 23.956602, 20.816232), # 1.150000000 + (0.60, 1900.000000, 23.956602, 21.221945), # 1.172413793 + (0.60, 1925.000000, 23.956602, 21.501181), # 1.187840290 + (0.60, 1950.000000, 23.956602, 21.780417), # 1.203266788 + (0.60, 1975.000000, 23.956602, 22.059654), # 1.218693285 + (0.60, 2000.000000, 23.956602, 22.338890), # 1.234119782 + (0.60, 2025.000000, 23.956602, 22.618126), # 1.249546279 + (0.65, 2050.000000, 24.778335, 23.983827), # 1.264972777 + (0.65, 2075.000000, 24.778335, 24.276313), # 1.280399274 + (0.65, 2100.000000, 24.778335, 24.568798), # 1.295825771 + (0.65, 2125.000000, 24.778335, 24.861284), # 1.311252269 + (0.65, 2150.000000, 24.778335, 25.153770), # 1.326678766 + (0.65, 2175.000000, 24.778335, 25.446255), # 1.342105263 + (0.65, 2200.000000, 24.778335, 25.738741), # 1.357531760 + (0.65, 2225.000000, 24.778335, 26.031227), # 1.372958258 + (0.65, 2240.000000, 24.778335, 26.206718), # 1.382214156 + (0.65, 2250.000000, 24.778335, 26.323712), # 1.388384755 + (0.65, 2265.000000, 24.778335, 26.499204), # 1.397640653 + (0.65, 2290.000000, 24.778335, 26.791690), # 1.413067151 + (0.65, 2315.000000, 24.778335, 27.084175), # 1.428493648 + (0.65, 2340.000000, 24.778335, 27.376661), # 1.443920145 + (0.65, 2365.000000, 24.778335, 27.669147), # 1.459346642 + (0.65, 2372.000000, 24.778335, 27.751043), # 1.463666062 + (0.65, 2397.000000, 24.778335, 28.043528), # 1.479092559 + (0.65, 2422.000000, 24.778335, 28.336014), # 1.494519056 + (0.70, 2447.000000, 25.660103, 30.029117), # 1.509945554 + (0.70, 2472.000000, 25.660103, 30.335913), # 1.525372051 + (0.70, 2497.000000, 25.660103, 30.642708), # 1.540798548 + (0.70, 2502.000000, 25.660103, 30.704067), # 1.543883848 + (0.70, 2527.000000, 25.660103, 31.010862), # 1.559310345 + (0.70, 2552.000000, 25.660103, 31.317657), # 1.574736842 + (0.70, 2577.000000, 25.660103, 31.624453), # 1.590163339 + (0.70, 2602.000000, 25.660103, 31.931248), # 1.605589837 + (0.70, 2627.000000, 25.660103, 32.238043), # 1.621016334 + (0.70, 2629.000000, 25.660103, 32.262587), # 1.622250454 + (0.70, 2654.000000, 25.660103, 32.569382), # 1.637676951 + (0.70, 2679.000000, 25.660103, 32.876177), # 1.653103448 + (0.70, 2704.000000, 25.660103, 33.182972), # 1.668529946 + (0.70, 2729.000000, 25.660103, 33.489768), # 1.683956443 + (0.70, 2754.000000, 25.660103, 33.796563), # 1.699382940 + (0.70, 2755.000000, 25.660103, 33.808835), # 1.700000000 +] +HBM_POINTS: list[VBWPoint] = [VBWPoint(*point) for point in _HBM_POINTS] + +_ICI_POINTS = [ + # voltage_V, bandwidth_GBs, static_power_W, dynamic_power_W + (0.45, 0.00, 5.208886, 0.000000), # 0.000000 (scaled ref frequency) + (0.45, 17.63, 5.208886, 0.249735), # 0.049987 + (0.45, 35.27, 5.208886, 0.499612), # 0.100003 + (0.45, 52.90, 5.208886, 0.749347), # 0.149991 + (0.45, 70.54, 5.208886, 0.999224), # 0.200007 + (0.45, 88.17, 5.208886, 1.248960), # 0.249994 + (0.45, 105.81, 5.208886, 1.498836), # 0.300010 + (0.45, 123.44, 5.208886, 1.748572), # 0.349997 + (0.45, 141.08, 5.208886, 1.998449), # 0.400013 + (0.45, 158.71, 5.208886, 2.248184), # 0.450001 + (0.45, 176.34, 5.208886, 2.497919), # 0.499988 + (0.50, 193.98, 5.356007, 2.859937), # 0.550004 + (0.50, 211.61, 5.356007, 3.119864), # 0.599992 + (0.50, 229.25, 5.356007, 3.379939), # 0.650008 + (0.50, 246.88, 5.356007, 3.639866), # 0.699995 + (0.50, 264.52, 5.356007, 3.899941), # 0.750011 + (0.55, 282.15, 5.525821, 4.340150), # 0.799998 + (0.55, 299.78, 5.525821, 4.611342), # 0.849986 + (0.55, 306.38, 5.525821, 4.712866), # 0.868699 + (0.55, 319.15, 5.525821, 4.909299), # 0.904907 + (0.55, 331.91, 5.525821, 5.105579), # 0.941086 + (0.55, 344.68, 5.525821, 5.302013), # 0.977294 + (0.55, 357.45, 5.525821, 5.498446), # 1.013501 + (0.55, 370.21, 5.525821, 5.694726), # 1.049681 + (0.60, 382.98, 5.708207, 6.159173), # 1.085888 + (0.60, 395.74, 5.708207, 6.364382), # 1.122067 + (0.60, 408.51, 5.708207, 6.569752), # 1.158275 + (0.60, 421.28, 5.708207, 6.775123), # 1.194483 + (0.60, 434.04, 5.708207, 6.980332), # 1.230662 + (0.65, 446.81, 5.904003, 7.525575), # 1.266870 + (0.65, 459.57, 5.904003, 7.740490), # 1.303049 + (0.65, 461.57, 5.904003, 7.774176), # 1.308720 + (0.65, 463.57, 5.904003, 7.807861), # 1.314390 + (0.65, 465.57, 5.904003, 7.841547), # 1.320061 + (0.65, 467.57, 5.904003, 7.875233), # 1.325732 + (0.65, 469.57, 5.904003, 7.908919), # 1.331403 + (0.65, 471.57, 5.904003, 7.942605), # 1.337073 + (0.65, 473.57, 5.904003, 7.976290), # 1.342744 + (0.65, 475.57, 5.904003, 8.009976), # 1.348415 + (0.65, 477.57, 5.904003, 8.043662), # 1.354085 + (0.65, 479.57, 5.904003, 8.077348), # 1.359756 + (0.65, 481.57, 5.904003, 8.111034), # 1.365427 + (0.65, 483.57, 5.904003, 8.144719), # 1.371098 + (0.65, 485.57, 5.904003, 8.178405), # 1.376768 + (0.65, 487.57, 5.904003, 8.212091), # 1.382439 + (0.65, 489.57, 5.904003, 8.245777), # 1.388110 + (0.65, 491.57, 5.904003, 8.279463), # 1.393781 + (0.65, 493.57, 5.904003, 8.313148), # 1.399451 + (0.65, 495.57, 5.904003, 8.346834), # 1.405122 + (0.65, 497.57, 5.904003, 8.380520), # 1.410793 + (0.65, 499.57, 5.904003, 8.414206), # 1.416463 + (0.65, 501.57, 5.904003, 8.447892), # 1.422134 + (0.65, 503.57, 5.904003, 8.481577), # 1.427805 + (0.65, 505.57, 5.904003, 8.515263), # 1.433476 + (0.65, 507.57, 5.904003, 8.548949), # 1.439146 + (0.65, 509.57, 5.904003, 8.582635), # 1.444817 + (0.65, 511.57, 5.904003, 8.616320), # 1.450488 + (0.65, 513.57, 5.904003, 8.650006), # 1.456159 + (0.65, 515.57, 5.904003, 8.683692), # 1.461829 + (0.65, 517.57, 5.904003, 8.717378), # 1.467500 + (0.65, 519.57, 5.904003, 8.751064), # 1.473171 + (0.65, 521.57, 5.904003, 8.784749), # 1.478842 + (0.65, 523.57, 5.904003, 8.818435), # 1.484512 + (0.65, 525.57, 5.904003, 8.852121), # 1.490183 + (0.65, 527.57, 5.904003, 8.885807), # 1.495854 + (0.70, 529.57, 6.114105, 9.354544), # 1.501524 + (0.70, 531.57, 6.114105, 9.389873), # 1.507195 + (0.70, 533.57, 6.114105, 9.425201), # 1.512866 + (0.70, 535.57, 6.114105, 9.460530), # 1.518537 + (0.70, 537.57, 6.114105, 9.495859), # 1.524207 + (0.70, 539.57, 6.114105, 9.531188), # 1.529878 + (0.70, 541.57, 6.114105, 9.566517), # 1.535549 + (0.70, 543.57, 6.114105, 9.601846), # 1.541220 + (0.70, 545.57, 6.114105, 9.637174), # 1.546890 + (0.70, 547.57, 6.114105, 9.672503), # 1.552561 + (0.70, 549.57, 6.114105, 9.707832), # 1.558232 + (0.70, 551.57, 6.114105, 9.743161), # 1.563902 + (0.70, 553.57, 6.114105, 9.778490), # 1.569573 + (0.70, 555.57, 6.114105, 9.813819), # 1.575244 + (0.70, 557.57, 6.114105, 9.849147), # 1.580915 + (0.70, 559.57, 6.114105, 9.884476), # 1.586585 + (0.70, 561.57, 6.114105, 9.919805), # 1.592256 + (0.70, 563.57, 6.114105, 9.955134), # 1.597927 + (0.70, 565.57, 6.114105, 9.990463), # 1.603598 + (0.70, 567.57, 6.114105, 10.025792), # 1.609268 + (0.70, 569.57, 6.114105, 10.061120), # 1.614939 + (0.70, 571.57, 6.114105, 10.096449), # 1.620610 + (0.70, 573.57, 6.114105, 10.131778), # 1.626281 + (0.70, 575.57, 6.114105, 10.167107), # 1.631951 + (0.70, 577.57, 6.114105, 10.202436), # 1.637622 + (0.70, 579.57, 6.114105, 10.237764), # 1.643293 + (0.70, 581.57, 6.114105, 10.273093), # 1.648963 + (0.70, 583.57, 6.114105, 10.308422), # 1.654634 + (0.70, 585.57, 6.114105, 10.343751), # 1.660305 + (0.70, 587.57, 6.114105, 10.379080), # 1.665976 + (0.70, 589.57, 6.114105, 10.414409), # 1.671646 + (0.70, 591.57, 6.114105, 10.449737), # 1.677317 + (0.70, 593.57, 6.114105, 10.485066), # 1.682988 + (0.70, 595.57, 6.114105, 10.520395), # 1.688659 + (0.70, 597.57, 6.114105, 10.555724), # 1.694329 + (0.70, 599.57, 6.114105, 10.591053), # 1.700000 +] +ICI_POINTS: list[VBWPoint] = [VBWPoint(*point) for point in _ICI_POINTS] + +_DVFS_VOLTAGE_REGULATOR_OVERHEAD_TABLE = [ + ### (scaling time in ns, activity factor, voltage in V, power efficiency (percentage)) + + # scaling_time_ns = 2 + (2, 0.0, 0.45, 63.95031056), (2, 0.0, 0.5, 66.08660107), (2, 0.0, 0.55, 68.22289157), + (2, 0.0, 0.6, 69.22986684), (2, 0.0, 0.65, 70.23684211), (2, 0.0, 0.7, 72.12129462), + (2, 0.1, 0.45, 64.83850932), (2, 0.1, 0.5, 67.20841129), (2, 0.1, 0.55, 69.57831325), + (2, 0.1, 0.6, 70.57863031), (2, 0.1, 0.65, 71.57894737), (2, 0.1, 0.7, 73.01935874), + (2, 0.2, 0.45, 66.17080745), (2, 0.2, 0.5, 68.5522712), (2, 0.2, 0.55, 70.93373494), + (2, 0.2, 0.6, 72.151078), (2, 0.2, 0.65, 73.36842105), (2, 0.2, 0.7, 74.82214156), + (2, 0.3, 0.45, 67.50310559), (2, 0.3, 0.5, 69.89613111), (2, 0.3, 0.55, 72.28915663), + (2, 0.3, 0.6, 73.05247305), (2, 0.3, 0.65, 73.81578947), (2, 0.3, 0.7, 75.27283726), + (2, 0.4, 0.45, 68.39130435), (2, 0.4, 0.5, 70.79203771), (2, 0.4, 0.55, 73.19277108), + (2, 0.4, 0.6, 73.9516487), (2, 0.4, 0.65, 74.71052632), (2, 0.4, 0.7, 76.06072293), + (2, 0.5, 0.45, 69.27950311), (2, 0.5, 0.5, 71.68794432), (2, 0.5, 0.55, 74.09638554), + (2, 0.5, 0.6, 74.85082435), (2, 0.5, 0.65, 75.60526316), (2, 0.5, 0.7, 76.84860859), + (2, 0.6, 0.45, 70.16770186), (2, 0.6, 0.5, 72.19981479), (2, 0.6, 0.55, 74.23192771), + (2, 0.6, 0.6, 74.98570069), (2, 0.6, 0.65, 75.73947368), (2, 0.6, 0.7, 77.0746219), + (2, 0.7, 0.45, 71.05590062), (2, 0.7, 0.5, 72.71168525), (2, 0.7, 0.55, 74.36746988), + (2, 0.7, 0.6, 75.12057705), (2, 0.7, 0.65, 75.87368421), (2, 0.7, 0.7, 77.30063521), + (2, 0.8, 0.45, 71.20393375), (2, 0.8, 0.5, 72.8911235), (2, 0.8, 0.55, 74.57831325), + (2, 0.8, 0.6, 75.33038469), (2, 0.8, 0.65, 76.08245614), (2, 0.8, 0.7, 77.45042347), + (2, 0.9, 0.45, 71.35196687), (2, 0.9, 0.5, 73.07056175), (2, 0.9, 0.55, 74.78915663), + (2, 0.9, 0.6, 75.54019235), (2, 0.9, 0.65, 76.29122807), (2, 0.9, 0.7, 77.60021174), + (2, 1.0, 0.45, 71.5), (2, 1.0, 0.5, 73.25), (2, 1.0, 0.55, 75.0), + (2, 1.0, 0.6, 75.75), (2, 1.0, 0.65, 76.5), (2, 1.0, 0.7, 77.75), + + # scaling_time_ns = 20 + (20, 0.0, 0.45, 72.0), (20, 0.0, 0.5, 73.75), (20, 0.0, 0.55, 75.5), + (20, 0.0, 0.6, 77.0), (20, 0.0, 0.65, 78.5), (20, 0.0, 0.7, 80.0), + (20, 0.1, 0.45, 73.0), (20, 0.1, 0.5, 75.0), (20, 0.1, 0.55, 77.0), + (20, 0.1, 0.6, 78.5), (20, 0.1, 0.65, 80.0), (20, 0.1, 0.7, 81.0), + (20, 0.2, 0.45, 74.5), (20, 0.2, 0.5, 76.5), (20, 0.2, 0.55, 78.5), + (20, 0.2, 0.6, 80.25), (20, 0.2, 0.65, 82.0), (20, 0.2, 0.7, 83.0), + (20, 0.3, 0.45, 76.0), (20, 0.3, 0.5, 78.0), (20, 0.3, 0.55, 80.0), + (20, 0.3, 0.6, 81.25), (20, 0.3, 0.65, 82.5), (20, 0.3, 0.7, 83.5), + (20, 0.4, 0.45, 77.0), (20, 0.4, 0.5, 79.0), (20, 0.4, 0.55, 81.0), + (20, 0.4, 0.6, 82.25), (20, 0.4, 0.65, 83.5), (20, 0.4, 0.7, 84.375), + (20, 0.5, 0.45, 78.0), (20, 0.5, 0.5, 80.0), (20, 0.5, 0.55, 82.0), + (20, 0.5, 0.6, 83.25), (20, 0.5, 0.65, 84.5), (20, 0.5, 0.7, 85.25), + (20, 0.6, 0.45, 79.0), (20, 0.6, 0.5, 80.575), (20, 0.6, 0.55, 82.15), + (20, 0.6, 0.6, 83.4), (20, 0.6, 0.65, 84.65), (20, 0.6, 0.7, 85.5), + (20, 0.7, 0.45, 80.0), (20, 0.7, 0.5, 81.15), (20, 0.7, 0.55, 82.3), + (20, 0.7, 0.6, 83.55), (20, 0.7, 0.65, 84.8), (20, 0.7, 0.7, 85.75), + (20, 0.8, 0.45, 80.16666667), (20, 0.8, 0.5, 81.35), (20, 0.8, 0.55, 82.53333333), + (20, 0.8, 0.6, 83.78333333), (20, 0.8, 0.65, 85.03333333), (20, 0.8, 0.7, 85.91666667), + (20, 0.9, 0.45, 80.33333333), (20, 0.9, 0.5, 81.55), (20, 0.9, 0.55, 82.76666667), + (20, 0.9, 0.6, 84.01666667), (20, 0.9, 0.65, 85.26666667), (20, 0.9, 0.7, 86.08333334), + (20, 1.0, 0.45, 80.5), (20, 1.0, 0.5, 81.75), (20, 1.0, 0.55, 83.0), + (20, 1.0, 0.6, 84.25), (20, 1.0, 0.65, 85.5), (20, 1.0, 0.7, 86.25), + + # scaling_time_ns = 200 + (200, 0.0, 0.45, 76.11428571), (200, 0.0, 0.5, 77.62641997), (200, 0.0, 0.55, 79.13855422), + (200, 0.0, 0.6, 79.96693793), (200, 0.0, 0.65, 80.79532164), (200, 0.0, 0.7, 82.31863783), + (200, 0.1, 0.45, 77.17142857), (200, 0.1, 0.5, 78.94113597), (200, 0.1, 0.55, 80.71084337), + (200, 0.1, 0.6, 81.52501233), (200, 0.1, 0.65, 82.33918129), (200, 0.1, 0.7, 83.34775157), + (200, 0.2, 0.45, 78.75714286), (200, 0.2, 0.5, 80.5201377), (200, 0.2, 0.55, 82.28313253), + (200, 0.2, 0.6, 83.34039668), (200, 0.2, 0.65, 84.39766082), (200, 0.2, 0.7, 85.40572696), + (200, 0.3, 0.45, 80.34285714), (200, 0.3, 0.5, 82.09913942), (200, 0.3, 0.55, 83.85542169), + (200, 0.3, 0.6, 84.3838512), (200, 0.3, 0.65, 84.9122807), (200, 0.3, 0.7, 85.92022081), + (200, 0.4, 0.45, 81.4), (200, 0.4, 0.5, 83.15180723), (200, 0.4, 0.55, 84.90361446), + (200, 0.4, 0.6, 85.42256747), (200, 0.4, 0.65, 85.94152047), (200, 0.4, 0.7, 86.82061656), + (200, 0.5, 0.45, 82.45714286), (200, 0.5, 0.5, 84.20447505), (200, 0.5, 0.55, 85.95180723), + (200, 0.5, 0.6, 86.46128373), (200, 0.5, 0.65, 86.97076023), (200, 0.5, 0.7, 87.7210123), + (200, 0.6, 0.45, 83.51428571), (200, 0.6, 0.5, 84.81166093), (200, 0.6, 0.55, 86.10903614), + (200, 0.6, 0.6, 86.61709117), (200, 0.6, 0.65, 87.1251462), (200, 0.6, 0.7, 87.97823402), + (200, 0.7, 0.45, 84.57142857), (200, 0.7, 0.5, 85.41884682), (200, 0.7, 0.55, 86.26626506), + (200, 0.7, 0.6, 86.77289861), (200, 0.7, 0.65, 87.27953216), (200, 0.7, 0.7, 88.23545574), + (200, 0.8, 0.45, 84.74761905), (200, 0.8, 0.5, 85.62923121), (200, 0.8, 0.55, 86.51084337), + (200, 0.8, 0.6, 87.01526574), (200, 0.8, 0.65, 87.51968811), (200, 0.8, 0.7, 88.40697049), + (200, 0.9, 0.45, 84.92380952), (200, 0.9, 0.5, 85.83961561), (200, 0.9, 0.55, 86.75542169), + (200, 0.9, 0.6, 87.25763287), (200, 0.9, 0.65, 87.75984405), (200, 0.9, 0.7, 88.57848525), + (200, 1.0, 0.45, 85.1), (200, 1.0, 0.5, 86.05), (200, 1.0, 0.55, 87.0), + (200, 1.0, 0.6, 87.5), (200, 1.0, 0.65, 88.0), (200, 1.0, 0.7, 88.75), +] +DVFS_VOLTAGE_REGULATOR_OVERHEAD_TABLE: list[PowerEfficiencyPoint] = [ + PowerEfficiencyPoint(*point) for point in _DVFS_VOLTAGE_REGULATOR_OVERHEAD_TABLE +] + +_FIXED_VOLTAGE_REGULATOR_OVERHEAD_TABLE = [ + ### (scaling time in ns (unused), activity factor, voltage in V (always 0.7), power efficiency (percentage)) + (0, 0.0, 0.7, 67.0), + (0, 0.1, 0.7, 85.0), + (0, 0.2, 0.7, 86.0), + (0, 0.3, 0.7, 86.5), + (0, 0.4, 0.7, 87.0), + (0, 0.5, 0.7, 87.5), + (0, 0.6, 0.7, 88.0), + (0, 0.7, 0.7, 88.5), + (0, 0.8, 0.7, 89.0), + (0, 0.9, 0.7, 89.5), + (0, 1.0, 0.7, 90.0), +] +FIXED_VOLTAGE_REGULATOR_OVERHEAD_TABLE: list[PowerEfficiencyPoint] = [ + PowerEfficiencyPoint(*point) for point in _FIXED_VOLTAGE_REGULATOR_OVERHEAD_TABLE +] + + +# ========================= +# Helpers +# ========================= + +def _group_by_voltage(points: list[VfPoint]) -> Groups: + """Group (v, x, s, d) points by v (voltage), and sort each group by x (can be frequency or bandwidth).""" + groups: Groups = {} + for v, x, s, d in points: + groups.setdefault(v, []).append(Row(x, s, d)) + for v in groups: + groups[v].sort(key=lambda t: t.x) + return groups + + +def _choose_voltage_by_request_or_range( + groups: Groups, + target_x: float, + requested_v: float | None = None, +) -> tuple[float | None, list[Row] | None]: + """ + Choose voltage rows using: + - If requested_v is not None: pick voltage closest to requested_v. + - Else: pick segment whose [min_x, max_x] best matches target_x. + Returns (v, rows). + """ + if requested_v is not None and len(groups) > 0: + best_v, best_rows = min(groups.items(), key=lambda item: abs(item[0] - requested_v)) + return best_v, best_rows + + # No requested voltage: infer from x-range + best_v = None + best_rows = None + best_dist: float | None = None + for v, rows in groups.items(): + x_min = rows[0].x + x_max = rows[-1].x + if x_min <= target_x <= x_max: + dist = 0.0 + elif target_x < x_min: + dist = x_min - target_x + else: + dist = target_x - x_max + if ( + best_dist is None + or dist < best_dist + or (dist == best_dist and (best_v is None or v < best_v)) + ): + best_dist = dist + best_v = v + best_rows = rows + return best_v, best_rows + + +def _nearest_point(rows: list[Row], target_x: float) -> Row: + """Return (x_ref, s_ref, d_ref) where x_ref is closest to target_x.""" + best = min(rows, key=lambda row: abs(row.x - target_x)) + return best + + +def _scale_dynamic(base_dyn_W: float, base_x: float, new_x: float) -> float: + """Scale dynamic power linearly with x at fixed voltage.""" + assert base_dyn_W >= 0.0 + assert base_x >= 0.0 + assert new_x >= 0.0 + if base_x == 0.0: + return base_dyn_W + else: + return base_dyn_W * (new_x / base_x) + + +def _baseline_freq_ghz(points: list[VfPoint]) -> float: + """Max frequency_GHz from (v, f, s, d) table.""" + return max(p.frequency_GHz for p in points) + + +@lru_cache(maxsize=None) +def _baseline_bw_hbm() -> float: + """Max bandwidth_GBs from HBM table.""" + return max(p.bandwidth_GBs for p in HBM_POINTS) + + +@lru_cache(maxsize=None) +def _baseline_bw_ici() -> float: + """Max bandwidth_GBs from ICI table.""" + return max(p.bandwidth_GBs for p in ICI_POINTS) + + +@lru_cache(maxsize=None) +def _max_perf_point(component: str) -> VfPoint | VBWPoint: + """Return the (v, x, s, d) row with max x (frequency or bandwidth) for component.""" + comp = str(component).strip().lower() + if comp == "sa": + return max(SA_POINTS, key=lambda p: p.frequency_GHz) + elif comp == "vu": + return max(VU_POINTS, key=lambda p: p.frequency_GHz) + elif comp == "sram": + return max(SRAM_POINTS, key=lambda p: p.frequency_GHz) + elif comp == "hbm": + return max(HBM_POINTS, key=lambda p: p.bandwidth_GBs) + elif comp == "ici": + return max(ICI_POINTS, key=lambda p: p.bandwidth_GBs) + else: + raise ValueError(f"Unsupported component: {component!r}") + + +@lru_cache(maxsize=None) +def _min_power_point(component: str) -> VfPoint | VBWPoint: + """Return the (v, x, s, d) row with min power (min voltage and frequency) for component.""" + comp = str(component).strip().lower() + if comp == "sa": + return min(SA_POINTS, key=lambda p: (p.voltage_V, p.frequency_GHz)) + elif comp == "vu": + return min(VU_POINTS, key=lambda p: (p.voltage_V, p.frequency_GHz)) + elif comp == "sram": + return min(SRAM_POINTS, key=lambda p: (p.voltage_V, p.frequency_GHz)) + elif comp == "hbm": + return min(HBM_POINTS, key=lambda p: (p.voltage_V, p.bandwidth_GBs)) + elif comp == "ici": + return min(ICI_POINTS, key=lambda p: (p.voltage_V, p.bandwidth_GBs)) + else: + raise ValueError(f"Unsupported component: {component!r}") + + +# ========================= +# Main API +# ========================= + +@lru_cache(maxsize=None) +def get_power_from_dvfs(component: str, dvfs: ComponentDVFSConfig) -> tuple[float, float]: + """ + Compute (dynamic_power_W, static_power_W) for a component from DVFSConfig. + """ + comp = str(component).strip().lower() + v_req = dvfs.voltage_V + f_req_GHz = dvfs.frequency_GHz + + # default No DVFS policy: use max performance point (peak voltage and freq/bandwidth) + if dvfs.policy == DVFSPolicy.NONE or ( + (v_req is None or v_req <= 0.0) + and (f_req_GHz is None or f_req_GHz <= 0.0) + ): + max_point = _max_perf_point(comp) + return max_point.dynamic_power_W, max_point.static_power_W + + # SA / VU / SRAM: use frequency in GHz + if comp in ("sa", "vu", "sram"): + points = SA_POINTS if comp == "sa" else VU_POINTS if comp == "vu" else SRAM_POINTS + base_freq_ghz = _baseline_freq_ghz(points) + + if f_req_GHz is None or f_req_GHz <= 0.0: + f_req_GHz = base_freq_ghz + + # first find the nearest voltage corner + groups = _group_by_voltage(points) + _, rows = _choose_voltage_by_request_or_range(groups, f_req_GHz, v_req) + assert rows is not None, f"No voltage rows available for {comp} selection." + x_ref, s_ref, d_ref = _nearest_point(rows, f_req_GHz) + + # then extrapolate the dynamic power from the existing corners using the given target frequency (Dynamic Power ~ freq) + dyn = _scale_dynamic(d_ref, x_ref, f_req_GHz) + static = s_ref + return dyn, static + + # HBM: map DVFS freq proxy to bandwidth + if comp in ("hbm", "ici"): + base_bw = _baseline_bw_hbm() if comp == "hbm" else _baseline_bw_ici() + base_freq_ghz = 1.7 + points = HBM_POINTS if comp == "hbm" else ICI_POINTS + + if f_req_GHz is not None and f_req_GHz > 0.0: + bw_target = base_bw * (f_req_GHz / base_freq_ghz) + else: + bw_target = base_bw + + # group by voltage: {v: [(bw, s, d), ...]} + groups: Groups = {} + for v, bw, st_p, dyn_p in points: + groups.setdefault(v, []).append(Row(bw, st_p, dyn_p)) + for v in groups: + groups[v].sort(key=lambda t: t.x) + + _, rows = _choose_voltage_by_request_or_range(groups, bw_target, v_req) + assert rows is not None, f"No voltage rows available for {comp} selection." + bw_ref, s_ref, d_ref = _nearest_point(rows, bw_target) + + dyn = _scale_dynamic(d_ref, bw_ref, bw_target) + static = s_ref + return dyn, static + + raise ValueError(f"Unsupported component: {component!r}") + + +@lru_cache(maxsize=None) +def get_all_dvfs_configs_for_component( + component: str, + policy: DVFSPolicy = DVFSPolicy.IDEAL, +) -> list[ComponentDVFSConfig]: + """ + Return all possible DVFS configurations for a given component. + """ + comp = str(component).strip().lower() + configs: list[ComponentDVFSConfig] = [] + + if comp in ("sa", "vu", "sram"): + points = SA_POINTS if comp == "sa" else VU_POINTS if comp == "vu" else SRAM_POINTS + for v, f, s, d in points: + configs.append( + ComponentDVFSConfig( + policy=policy, + voltage_V=v, + frequency_GHz=f, + ) + ) + return configs + + if comp in ("hbm", "ici"): + points = HBM_POINTS if comp == "hbm" else ICI_POINTS + for v, bw, s, d in points: + # Map bandwidth back to frequency proxy + base_bw = _baseline_bw_hbm() if comp == "hbm" else _baseline_bw_ici() + base_freq_ghz = 1.7 + f = base_freq_ghz * (bw / base_bw) + configs.append( + ComponentDVFSConfig( + policy=policy, + voltage_V=v, + frequency_GHz=f, + ) + ) + return configs + + raise ValueError(f"Unsupported component: {component!r}") + + +def get_all_dvfs_configs_for_op( + op: Operator.Operator, + policy: DVFSPolicy = DVFSPolicy.IDEAL, + perf_degrade_threshold: float = 0, + total_exe_time_ns: float | None = None, +) -> list[dict[str, ComponentDVFSConfig]]: + """ + Return all possible DVFS configurations for each component used by the given operator. + Sweeps through all frequencies for each component and pick the lowest voltage accordingly. + If the component is unused, just set it to the lowest power state. + - policy: DVFSPolicy to use for filling out ComponentDVFSConfig. + - perf_degrade_threshold: float, maximum allowed performance degradation for the entire workload. + """ + logging.set_verbosity(logging.INFO) + + configs: list[dict[str, ComponentDVFSConfig]] = [] + comp_names = ("sa", "vu", "sram", "hbm", "ici") + + def _get_exe_time_ns(comp_name: str) -> float: + if comp_name == "sa": + return op.stats.sa_time_ns + elif comp_name == "vu": + return op.stats.vu_time_ns + elif comp_name == "sram": + return op.stats.vmem_time_ns + elif comp_name == "hbm": + return op.stats.memory_time_ns + elif comp_name == "ici": + return op.stats.ici_time_ns + else: + raise ValueError(f"Unsupported component name: {comp_name!r}") + + # 1. Compute max allowed execution time + # max_allowed_exe_time_ns is derived from the perf_degrade_threshold and the program's original execution time. + # If total_exe_time_ns is not provided, use op.stats.execution_time_ns as the baseline. + if not total_exe_time_ns: + total_exe_time_ns = op.stats.execution_time_ns + max_slack_time_ns = total_exe_time_ns * perf_degrade_threshold + max_allowed_exe_time_ns = op.stats.execution_time_ns + max_slack_time_ns / op.stats.count + + logging.info(f"op_name: {op.name}, original_exe_time_ns: {op.stats.execution_time_ns}, max_allowed_exe_time_ns: {max_allowed_exe_time_ns}, max_op_perf_degrade: {(max_allowed_exe_time_ns / op.stats.execution_time_ns - 1) * 100:.4f}%") + + # 2. For each component, generate a list of ComponentDVFSConfigs that + # satisfy op.stats.execution_time_ns <= component exe time <= max_allowed_exe_time_ns. + # If the component is unused, just include the min power point config. + comp_configs_list: list[list[ComponentDVFSConfig]] = [] # list of ComponentDVFSConfigs for each component + for comp_name in comp_names: + exe_time_ns = _get_exe_time_ns(comp_name) + # we can always slow down the component without perf degradation if it is not the bottleneck + slowdown_factor_max = exe_time_ns / op.stats.execution_time_ns + # must have component exe time <= max_allowed_exe_time_ns + slowdown_factor_min = exe_time_ns / max_allowed_exe_time_ns + max_freq_GHz_required = min( + ( + _baseline_freq_ghz(SA_POINTS) * slowdown_factor_max + if comp_name in ("sa", "vu", "sram") + else 1.7 * slowdown_factor_max # for HBM and ICI + ) + 0.05, # step size is 0.05 GHz + 1.7, + ) + min_freq_GHz_allowed = max( + ( + _baseline_freq_ghz(SA_POINTS) * slowdown_factor_min + if comp_name in ("sa", "vu", "sram") + else 1.7 * slowdown_factor_min # for HBM and ICI + ) - 0.05, # step size is 0.05 GHz + 0, + ) + + # print(f"Component: {comp_name}, Max freq required: {max_freq_GHz_required}, Min freq allowed: {min_freq_GHz_allowed}") + + if exe_time_ns > 0: + all_cfgs = get_all_dvfs_configs_for_component(comp_name, policy) + all_cfgs = [ + cfg for cfg in all_cfgs + if cfg.frequency_GHz and max_freq_GHz_required >= cfg.frequency_GHz >= min_freq_GHz_allowed + ] + # print(f"Component: {comp_name}, Possible DVFS configs count: {len(all_cfgs)}") + comp_configs_list.append(all_cfgs) + else: + min_power_point = _min_power_point(comp_name) + comp_configs_list.append([ + ComponentDVFSConfig( + policy=policy, + voltage_V=min_power_point.voltage_V, + frequency_GHz=0.05, # assume lowest freq is 0.05 GHz for now + ) + ]) + + for combo in itertools.product(*comp_configs_list): # combo: (sa, vu, sram, ici, hbm) ComponentDVFSConfig + config_dict = {comp_name: config for comp_name, config in zip(comp_names, combo)} + configs.append(config_dict) + + logging.info(f"Total DVFS configurations generated for op {op.name}: {len(configs)}") + + return configs diff --git a/neusim/npusim/backend/power_model.py b/neusim/npusim/backend/power_model.py new file mode 100644 index 0000000..8960f5e --- /dev/null +++ b/neusim/npusim/backend/power_model.py @@ -0,0 +1,1079 @@ +"""Core power/energy modeling functions for NPU components.""" + +from math import ceil + +import numpy as np + +import neusim.npusim.frontend.Operator as Operator +from neusim.npusim.frontend.Operator import DVFSPolicy, DVFSConfig, ComponentDVFSConfig +from neusim.configs.chips.ChipConfig import ChipConfig +from neusim.configs.power_gating.PowerGatingConfig import PowerGatingConfig +from neusim.npusim.backend.dvfs_power_getter import ( + get_power_from_dvfs, + DVFS_VOLTAGE_REGULATOR_OVERHEAD_TABLE, + FIXED_VOLTAGE_REGULATOR_OVERHEAD_TABLE, +) + + +def compute_peak_sa_flops_per_sec_from_chip_config(config: ChipConfig) -> float: + freq = config.freq_GHz * 1e9 + num_sa = config.num_sa + sa_dim_size = config.sa_dim + return 2 * (sa_dim_size**2) * freq * num_sa + + +def compute_peak_vu_flops_per_sec_from_chip_config(config: ChipConfig) -> float: + freq = config.freq_GHz * 1e9 + num_vu = config.num_vu + vu_num_alus = 128 * 8 # TODO: make this a parameter in chip config + return vu_num_alus * freq * num_vu + +def compute_peak_sa_flops_per_sec_from_dvfs_config(config: ChipConfig, dvfs: ComponentDVFSConfig) -> float: + if not dvfs.frequency_GHz or dvfs.frequency_GHz <= 0: + return compute_peak_sa_flops_per_sec_from_chip_config(config) + freq = dvfs.frequency_GHz * 1e9 + num_sa = config.num_sa + sa_dim_size = config.sa_dim + return 2 * (sa_dim_size**2) * freq * num_sa + + +def compute_peak_vu_flops_per_sec_from_dvfs_config(config: ChipConfig, dvfs: ComponentDVFSConfig) -> float: + if not dvfs.frequency_GHz or dvfs.frequency_GHz <= 0: + return compute_peak_vu_flops_per_sec_from_chip_config(config) + freq = dvfs.frequency_GHz * 1e9 + num_vu = config.num_vu + vu_num_alus = 128 * 8 # TODO: make this a parameter in chip config + return vu_num_alus * freq * num_vu + +def compute_sa_flops_util(op: Operator.Operator, config: ChipConfig, dvfs: ComponentDVFSConfig) -> float: + """ + Compute SA flops utilization for an operator. + """ + peak_sa_flops_per_sec = compute_peak_sa_flops_per_sec_from_dvfs_config(config, dvfs) + sa_time_ns = op.stats.sa_time_ns + if sa_time_ns > 0: # op.op_type == Operator.OpType.MXU: + # assert sa_time_ns > 0, f"SA time is 0 for op: {op.to_csv_dict()}" + sa_flops_util = min( + (op.stats.flop_count / sa_time_ns * 1e9) / peak_sa_flops_per_sec, + 1.0, + ) + else: + sa_flops_util = 0 + return sa_flops_util + + +def compute_vu_flops_util(op: Operator.Operator, config: ChipConfig, dvfs: ComponentDVFSConfig) -> float: + """ + Compute VU flops utilization for an operator. + """ + peak_vu_flops_per_sec = compute_peak_vu_flops_per_sec_from_dvfs_config(config, dvfs) + vu_time_ns = op.stats.vu_time_ns + if op.op_type == Operator.OpType.MXU: + # assert peak_vu_flops_per_sec > 0, f"Peak VU FLOPS is {peak_vu_flops_per_sec} for op: {op.to_csv_dict()}" + # assert vu_time_ns > 0, f"VU time is {vu_time_ns} for op: {op.to_csv_dict()}" + # assumes vu flops is at least 1/8 of sa flops for accmulation + vu_flops_util = min( + (op.stats.flop_count / 8 / vu_time_ns * 1e9) / peak_vu_flops_per_sec, + 1.0, + ) + else: + if peak_vu_flops_per_sec > 0 and vu_time_ns > 0: + vu_flops_util = min( + (op.stats.flop_count / vu_time_ns * 1e9) / peak_vu_flops_per_sec, + 1.0, + ) + else: + vu_flops_util = 0 + return vu_flops_util + + +def cycle_to_ns(cycles: int, freq_GHz: float) -> float: + """ + Convert cycles to nanoseconds. + """ + return cycles / freq_GHz + + +def ns_to_cycle(ns: float, freq_GHz: float) -> float: + """ + Convert nanoseconds to cycles. + """ + return ns * freq_GHz + + +def scale_dvfs_component_time(op: Operator.Operator, config: ChipConfig) -> Operator.Operator: + """ + Scale per-component active times based on the DVFS frequency set in op.dvfs_*. + Original times are assumed to be measured at base 1.7 GHz. + """ + base_freq_GHz = config.freq_GHz + + def _scale_time_with_dvfs(time_ns: int | float, dvfs: ComponentDVFSConfig) -> int: + """ + Scale the active time when a DVFS frequency is specified. + If no DVFS freq, keep original time. + """ + if time_ns <= 0 or dvfs.frequency_GHz is None: + return int(float(time_ns)) + if dvfs.frequency_GHz <= 0: + return int(float(time_ns)) + # Original times assumed at base_freq_Hz. + return ceil(time_ns * base_freq_GHz / dvfs.frequency_GHz) + + # Apply DVFS freq to component times (performance effect only) + op.stats.sa_time_ns = _scale_time_with_dvfs(op.stats.sa_time_ns, op.dvfs_sa) + op.stats.vu_time_ns = _scale_time_with_dvfs(op.stats.vu_time_ns, op.dvfs_vu) + op.stats.vmem_time_ns = _scale_time_with_dvfs(op.stats.vmem_time_ns, op.dvfs_sram) + op.stats.ici_time_ns = _scale_time_with_dvfs(op.stats.ici_time_ns, op.dvfs_ici) + op.stats.memory_time_ns = _scale_time_with_dvfs(op.stats.memory_time_ns, op.dvfs_hbm) + + return op + + +def analyze_dynamic_energy( + op: Operator.Operator, config: ChipConfig +) -> Operator.Operator: + """ + Analyze dynamic power and energy for an operator. + + Assumes: + - Static energy & execution/component times (possibly DVFS/PG-adjusted) + have already been computed. + - `configure_dvfs_for_op` has been called to populate op.dvfs_*. + + Behavior: + - Uses get_power_from_dvfs(...) to obtain dynamic power. + - Computes dynamic energy for each component as P_dyn * active_time. + """ + + # Recompute FLOPS utils with updated times + sa_flops_util = compute_sa_flops_util(op, config, op.dvfs_sa) + vu_flops_util = compute_vu_flops_util(op, config, op.dvfs_vu) + + # Dynamic powers + if config.enable_dvfs: + sa_dyn_W, _ = get_power_from_dvfs("SA", op.dvfs_sa) + vu_dyn_W, _ = get_power_from_dvfs("VU", op.dvfs_vu) + sram_dyn_W, _ = get_power_from_dvfs("SRAM", op.dvfs_sram) + hbm_dyn_W, _ = get_power_from_dvfs("HBM", op.dvfs_hbm) + ici_dyn_W, _ = get_power_from_dvfs("ICI", op.dvfs_ici) + else: + sa_dyn_W = config.dynamic_power_sa_W + vu_dyn_W = config.dynamic_power_vu_W + sram_dyn_W = config.dynamic_power_vmem_W + hbm_dyn_W = config.dynamic_power_hbm_W + ici_dyn_W = config.dynamic_power_ici_W + + # 'other' still uses config (no DVFS enabled) + other_dyn_W = config.dynamic_power_other_W + + # Dynamic energy per component + exe_time_ns = op.stats.execution_time_ns + sa_time_ns = op.stats.sa_time_ns + vu_time_ns = op.stats.vu_time_ns + vmem_time_ns = op.stats.vmem_time_ns + ici_time_ns = op.stats.ici_time_ns + hbm_time_ns = op.stats.memory_time_ns + + op.stats.dynamic_energy_sa_J = sa_dyn_W * sa_time_ns * config.num_sa / 1e9 * sa_flops_util + op.stats.dynamic_energy_vu_J = vu_dyn_W * vu_time_ns * config.num_vu / 1e9 * vu_flops_util + op.stats.dynamic_energy_sram_J = sram_dyn_W * vmem_time_ns / 1e9 + op.stats.dynamic_energy_ici_J = ici_dyn_W * ici_time_ns / 1e9 + op.stats.dynamic_energy_hbm_J = hbm_dyn_W * hbm_time_ns / 1e9 + op.stats.dynamic_energy_other_J = other_dyn_W * exe_time_ns / 1e9 + + return op + + +def analyze_sa_static_energy( + op: Operator.Operator, config: ChipConfig, pg_config: PowerGatingConfig +) -> Operator.Operator: + """ + Static power/energy analysis for SA. + """ + if config.enable_dvfs: + _, static_sa_W = get_power_from_dvfs("SA", op.dvfs_sa) + else: + static_sa_W = config.static_power_sa_W + static_sa_W *= config.num_sa + pg_power_W = static_sa_W * pg_config.sa_power_level_factors[-1] + + # No power-gating + if not pg_config.SA_PG_enabled: + op.stats.static_energy_sa_J = static_sa_W * op.stats.execution_time_ns / 1e9 + return op + + if op.stats.sa_time_ns > 0: + # assumes in the worst case, idle intervals are evenly distributed + # over the entire execution time + worst_case_sa_idle_interval_ns = ceil( + (op.stats.execution_time_ns - 1) / (op.stats.sa_time_ns / config.sa_dim) + ) + if worst_case_sa_idle_interval_ns == 0: + # if SA is not idle (op is SA-bound), then no power gating + op.stats.static_energy_sa_J = static_sa_W * op.stats.execution_time_ns / 1e9 + return op + else: + worst_case_sa_idle_interval_ns = 0 + + sa_flops_util = compute_sa_flops_util(op, config, op.dvfs_sa) + + # calculate PG delay overhead and update op stats + if ( + op.stats.sa_time_ns > 0 + and pg_config.SA_temporal_granularity + == PowerGatingConfig.TemporalGranularity.INSTRUCTION + and pg_config.SA_spatial_granularity + == PowerGatingConfig.SASpatialGranularity.PE + ): # used by HW and Full + assert isinstance(op.stats, (Operator.EinsumStatistics, Operator.FlashAttentionStatistics)) + overhead_ns_1 = ceil( + op.stats.sa_time_ns / config.sa_dim * cycle_to_ns( + pg_config.sa_pe_pg_delay_cycles, config.freq_GHz + ) + ) + overhead_ns_2 = ceil( + op.stats.num_sa_ops * cycle_to_ns(pg_config.sa_pe_pg_delay_cycles, config.freq_GHz) + ) + overhead_ns = min(overhead_ns_1, overhead_ns_2) + op.stats.sa_time_ns += overhead_ns + elif ( + op.stats.sa_time_ns > 0 + and pg_config.SA_temporal_granularity + == PowerGatingConfig.TemporalGranularity.INSTRUCTION + and pg_config.SA_spatial_granularity + == PowerGatingConfig.SASpatialGranularity.COMPONENT + ): # used by Base (idle-detect policy) + if worst_case_sa_idle_interval_ns > 4 * cycle_to_ns( + pg_config.sa_pg_delay_cycles, config.freq_GHz + ): + pg_delay_ns = ceil( + cycle_to_ns(pg_config.sa_pg_delay_cycles, config.freq_GHz) + ) + # sa_time_ns/sa_dim is the worst case number of idle intervals + op.stats.sa_time_ns += ceil(pg_delay_ns * (op.stats.sa_time_ns / config.sa_dim)) + # if op.stats.sa_time_ns > op.stats.execution_time_ns: + # op.stats.execution_time_ns = op.stats.sa_time_ns + # op.stats.bounded_by = "Compute" + + sa_time_ns = op.stats.sa_time_ns + exe_time_ns = max(op.stats.execution_time_ns, sa_time_ns) + + if ( + pg_config.SA_temporal_granularity + == PowerGatingConfig.TemporalGranularity.INSTRUCTION + and pg_config.SA_spatial_granularity + == PowerGatingConfig.SASpatialGranularity.COMPONENT + ): + pg_energy = pg_power_W * (exe_time_ns - sa_time_ns) / 1e9 + static_energy = static_sa_W * sa_time_ns / 1e9 + + # Base policy: do not power gate if idle interval is smaller than 2x pg delay time + if sa_time_ns > 0 and worst_case_sa_idle_interval_ns < 2 * cycle_to_ns( + pg_config.sa_pg_delay_cycles, config.freq_GHz + ): + pg_energy = 0 + static_energy = static_sa_W * exe_time_ns / 1e9 + + op.stats.static_energy_sa_J = pg_energy + static_energy + return op + + if ( + pg_config.SA_temporal_granularity + == PowerGatingConfig.TemporalGranularity.INSTRUCTION + and pg_config.SA_spatial_granularity + == PowerGatingConfig.SASpatialGranularity.PARTITION + ): + raise NotImplementedError() # TODO + + if ( + pg_config.SA_temporal_granularity + == PowerGatingConfig.TemporalGranularity.INSTRUCTION + and pg_config.SA_spatial_granularity + == PowerGatingConfig.SASpatialGranularity.PE + ): + pg_energy = ( + pg_power_W * sa_time_ns / 1e9 * (1 - sa_flops_util) + + pg_power_W * (exe_time_ns - sa_time_ns) / 1e9 + ) + static_energy = static_sa_W * sa_time_ns / 1e9 * sa_flops_util + op.stats.static_energy_sa_J = pg_energy + static_energy + return op + + if ( + pg_config.SA_temporal_granularity + == PowerGatingConfig.TemporalGranularity.OPERATOR + and pg_config.SA_spatial_granularity + == PowerGatingConfig.SASpatialGranularity.COMPONENT + ): + if op.op_type == Operator.OpType.MXU: + op.stats.static_energy_sa_J = static_sa_W * exe_time_ns / 1e9 + else: + op.stats.static_energy_sa_J = 0 + return op + + if ( + pg_config.SA_temporal_granularity + == PowerGatingConfig.TemporalGranularity.OPERATOR + and pg_config.SA_spatial_granularity + == PowerGatingConfig.SASpatialGranularity.PARTITION + ): + raise NotImplementedError() # TODO + + if ( + pg_config.SA_temporal_granularity + == PowerGatingConfig.TemporalGranularity.OPERATOR + and pg_config.SA_spatial_granularity + == PowerGatingConfig.SASpatialGranularity.PE + ): + op.stats.static_energy_sa_J = static_sa_W * exe_time_ns / 1e9 * sa_flops_util + return op + + if ( + pg_config.SA_temporal_granularity + == PowerGatingConfig.TemporalGranularity.APPLICATION + and pg_config.SA_spatial_granularity + == PowerGatingConfig.SASpatialGranularity.COMPONENT + ): + raise NotImplementedError() # TODO + + if ( + pg_config.SA_temporal_granularity + == PowerGatingConfig.TemporalGranularity.APPLICATION + and pg_config.SA_spatial_granularity + == PowerGatingConfig.SASpatialGranularity.PARTITION + ): + raise NotImplementedError() # TODO + + if ( + pg_config.SA_temporal_granularity + == PowerGatingConfig.TemporalGranularity.APPLICATION + and pg_config.SA_spatial_granularity + == PowerGatingConfig.SASpatialGranularity.PE + ): + raise NotImplementedError() # TODO + + # should not reach here + raise ValueError("Unsupported/Unknown/Invalid SA power gating configuration") + + +def analyze_vu_static_energy( + op: Operator.Operator, config: ChipConfig, pg_config: PowerGatingConfig +) -> Operator.Operator: + """ + Static power/energy analysis for VU. + """ + if config.enable_dvfs: + _, static_vu_W = get_power_from_dvfs("VU", op.dvfs_vu) + else: + static_vu_W = config.static_power_vu_W + static_vu_W *= config.num_vu + pg_power_W = static_vu_W * pg_config.vu_power_level_factors[-1] + + # No power-gating + if not pg_config.VU_PG_enabled: + op.stats.static_energy_vu_J = static_vu_W * op.stats.execution_time_ns / 1e9 + return op + + if op.stats.vu_time_ns > 0: + # assumes in the worst case, idle intervals are evenly distributed + # over the entire execution time + worst_case_vu_idle_interval_ns = ceil( + (op.stats.execution_time_ns - 1) / op.stats.vu_time_ns + ) + if worst_case_vu_idle_interval_ns == 0: + # if VU is not idle (op is VU-bound), then no power gating + op.stats.static_energy_vu_J = static_vu_W * op.stats.execution_time_ns / 1e9 + return op + else: + worst_case_vu_idle_interval_ns = 0 + + vu_flops_util = compute_vu_flops_util(op, config, op.dvfs_vu) + + # calculate PG delay overhead and update op stats + if ( + pg_config.VU_temporal_granularity + == PowerGatingConfig.TemporalGranularity.INSTRUCTION + and pg_config.VU_PG_policy == PowerGatingConfig.PowerGatingPolicy.HW + ): # used by Base and HW (idle-detect policy) + if worst_case_vu_idle_interval_ns > 4 * cycle_to_ns( + pg_config.vu_pg_delay_cycles, config.freq_GHz + ): + pg_delay_ns = ceil( + cycle_to_ns(pg_config.vu_pg_delay_cycles, config.freq_GHz) + ) + # vu_time_ns is the worst case number of idle intervals + op.stats.vu_time_ns += pg_delay_ns * op.stats.vu_time_ns + # if op.stats.vu_time_ns > op.stats.execution_time_ns: + # op.stats.execution_time_ns = op.stats.vu_time_ns + # op.stats.bounded_by = "Compute" + + vu_time_ns = op.stats.vu_time_ns + exe_time_ns = max(op.stats.execution_time_ns, vu_time_ns) + + if ( + pg_config.VU_temporal_granularity + == PowerGatingConfig.TemporalGranularity.INSTRUCTION + and pg_config.VU_spatial_granularity + == PowerGatingConfig.VUSpatialGranularity.COMPONENT + ): # used by Base, HW, and Full pg_config + pg_energy = pg_power_W * (exe_time_ns - vu_time_ns) / 1e9 + static_energy = static_vu_W * vu_time_ns / 1e9 + + # HW policy: do not power gate if idle interval is smaller than 2x pg delay time + if pg_config.VU_PG_policy == PowerGatingConfig.PowerGatingPolicy.HW: + if worst_case_vu_idle_interval_ns < 2 * cycle_to_ns( + pg_config.vu_pg_delay_cycles, config.freq_GHz + ): + pg_energy = 0 + static_energy = static_vu_W * exe_time_ns / 1e9 + + op.stats.static_energy_vu_J = pg_energy + static_energy + + # Full policy: calculate number of setpm instructions + if pg_config.VU_PG_policy == PowerGatingConfig.PowerGatingPolicy.SW: + if exe_time_ns == vu_time_ns: # VU bound; no VU idle intervals + op.stats.num_setpm_vu = 0 + elif vu_time_ns == 0: # VU idle; set VUs to be PG'ed only once + op.stats.num_setpm_vu = 1 + else: # use the number of idle intervals as an estimate + op.stats.num_setpm_vu = min( + round(exe_time_ns / worst_case_vu_idle_interval_ns), + (exe_time_ns - vu_time_ns) // 32, # 32 cycles BET for VU with wake-up delay of 2 cycles on TPUv5p + # This division estimates the max number of setpm instructions + # TODO: make this a parameter in PG config + ) + return op + + if ( + pg_config.VU_temporal_granularity + == PowerGatingConfig.TemporalGranularity.INSTRUCTION + and pg_config.VU_spatial_granularity + == PowerGatingConfig.VUSpatialGranularity.PARTITION + ): + raise NotImplementedError() # TODO + + if ( + pg_config.VU_temporal_granularity + == PowerGatingConfig.TemporalGranularity.INSTRUCTION + and pg_config.VU_spatial_granularity + == PowerGatingConfig.VUSpatialGranularity.ALU + ): # only used by Ideal pg_config for now + pg_energy = ( + pg_power_W * vu_time_ns / 1e9 * (1 - vu_flops_util) + + pg_power_W * (exe_time_ns - vu_time_ns) / 1e9 + ) + static_energy = static_vu_W * vu_time_ns / 1e9 * vu_flops_util + op.stats.static_energy_vu_J = pg_energy + static_energy + return op + + if ( + pg_config.VU_temporal_granularity + == PowerGatingConfig.TemporalGranularity.OPERATOR + and pg_config.VU_spatial_granularity + == PowerGatingConfig.VUSpatialGranularity.COMPONENT + ): + if vu_time_ns > 0: + op.stats.static_energy_vu_J = static_vu_W * exe_time_ns / 1e9 + else: + op.stats.static_energy_vu_J = 0 + return op + + if ( + pg_config.VU_temporal_granularity + == PowerGatingConfig.TemporalGranularity.OPERATOR + and pg_config.VU_spatial_granularity + == PowerGatingConfig.VUSpatialGranularity.PARTITION + ): + raise NotImplementedError() # TODO + + if ( + pg_config.VU_temporal_granularity + == PowerGatingConfig.TemporalGranularity.OPERATOR + and pg_config.VU_spatial_granularity + == PowerGatingConfig.VUSpatialGranularity.ALU + ): + op.stats.static_energy_vu_J = static_vu_W * exe_time_ns / 1e9 * vu_flops_util + return op + + if ( + pg_config.VU_temporal_granularity + == PowerGatingConfig.TemporalGranularity.APPLICATION + and pg_config.VU_spatial_granularity + == PowerGatingConfig.VUSpatialGranularity.COMPONENT + ): + raise NotImplementedError() # TODO + + if ( + pg_config.VU_temporal_granularity + == PowerGatingConfig.TemporalGranularity.APPLICATION + and pg_config.VU_spatial_granularity + == PowerGatingConfig.VUSpatialGranularity.PARTITION + ): + raise NotImplementedError() # TODO + + if ( + pg_config.VU_temporal_granularity + == PowerGatingConfig.TemporalGranularity.APPLICATION + and pg_config.VU_spatial_granularity + == PowerGatingConfig.VUSpatialGranularity.ALU + ): + raise NotImplementedError() # TODO + + # should not reach here + raise ValueError("Unsupported/Unknown/Invalid VU power gating configuration") + + +def analyze_vmem_static_energy( + op: Operator.Operator, config: ChipConfig, pg_config: PowerGatingConfig +) -> Operator.Operator: + """ + Static power/energy analysis for vmem. + """ + if config.enable_dvfs: + _, static_vmem_W = get_power_from_dvfs("SRAM", op.dvfs_sram) + else: + static_vmem_W = config.static_power_vmem_W + pg_power_W = static_vmem_W * pg_config.vmem_power_level_factors[-1] + + # No power-gating + if not pg_config.vmem_PG_enabled: + op.stats.static_energy_sram_J = static_vmem_W * op.stats.execution_time_ns / 1e9 + return op + + partition_granularity = pg_config.vmem_partition_size_bytes + if ( + pg_config.vmem_spatial_granularity + == PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE + ): + partition_granularity = 4 * 1024 # 4KB + + vmem_size = config.vmem_size_MB * 1024 * 1024 + + # compute vmem capacity utilization + if op.op_type == Operator.OpType.MXU: + assert isinstance( + op.stats, (Operator.EinsumStatistics, Operator.FlashAttentionStatistics) + ), f"op_name: {op.name} :: op_type: {op.op_type}, opcode_type: {op.opcode_type}, opcode: {op.opcode} not supported for op.stats type {type(op.stats)}\nconfig: {op.config_str}" + max_vmem_demand = op.stats.max_vmem_demand_bytes + vmem_capacity_util = min(max_vmem_demand / vmem_size, 1.0) + else: + # only use 2MB per core (4MB in total) for operators w/o data reuse + vmem_capacity_util = 4 / config.vmem_size_MB + vmem_demand_ceiled = ( + int(np.ceil(vmem_capacity_util * vmem_size / partition_granularity)) + * partition_granularity + ) + vmem_capacity_util = vmem_demand_ceiled / vmem_size + + exe_time_ns = op.stats.execution_time_ns + + # calculate PG delay overhead and update op stats + if pg_config.vmem_PG_policy == PowerGatingConfig.PowerGatingPolicy.HW: + pg_delay_overhead = ceil( + op.stats.execution_time_ns + / cycle_to_ns(pg_config.vmem_HW_drowsy_period_cycles, config.freq_GHz) + * cycle_to_ns(pg_config.vmem_partition_pg_delay_cycles, config.freq_GHz) + ) + exe_time_ns += pg_delay_overhead + + + vmem_time_ns = op.stats.vmem_time_ns + + + if ( + pg_config.vmem_temporal_granularity + == PowerGatingConfig.TemporalGranularity.INSTRUCTION + and pg_config.vmem_spatial_granularity + == PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE + and pg_config.vmem_voltage_granularity + == PowerGatingConfig.VoltageGranularity.TWO_LEVEL + ): + pg_energy = ( + pg_power_W * vmem_time_ns / 1e9 * (1 - vmem_capacity_util) + + pg_power_W * (exe_time_ns - vmem_time_ns) / 1e9 + ) + static_energy = static_vmem_W * vmem_time_ns / 1e9 * vmem_capacity_util + op.stats.static_energy_sram_J = pg_energy + static_energy + + # Full policy: calculate number of setpm instructions + if pg_config.vmem_PG_policy == PowerGatingConfig.PowerGatingPolicy.SW: + # only set once per operator as we assume fixed tile size per operator for now + op.stats.num_setpm_sram = 1 + + return op + + if ( + pg_config.vmem_temporal_granularity + == PowerGatingConfig.TemporalGranularity.INSTRUCTION + and pg_config.vmem_spatial_granularity + == PowerGatingConfig.VmemSpatialGranularity.PARTITION + and pg_config.vmem_voltage_granularity + == PowerGatingConfig.VoltageGranularity.TWO_LEVEL + ): + pg_energy = ( + pg_power_W * vmem_time_ns / 1e9 * (1 - vmem_capacity_util) + + pg_power_W * (exe_time_ns - vmem_time_ns) / 1e9 + ) + static_energy = static_vmem_W * vmem_time_ns / 1e9 * vmem_capacity_util + op.stats.static_energy_sram_J = pg_energy + static_energy + return op + + if ( + pg_config.vmem_temporal_granularity + == PowerGatingConfig.TemporalGranularity.INSTRUCTION + and pg_config.vmem_spatial_granularity + == PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE + and pg_config.vmem_voltage_granularity + == PowerGatingConfig.VoltageGranularity.MULTI_LEVEL + ): + raise NotImplementedError() # TODO + + if ( + pg_config.vmem_temporal_granularity + == PowerGatingConfig.TemporalGranularity.INSTRUCTION + and pg_config.vmem_spatial_granularity + == PowerGatingConfig.VmemSpatialGranularity.PARTITION + and pg_config.vmem_voltage_granularity + == PowerGatingConfig.VoltageGranularity.MULTI_LEVEL + ): + raise NotImplementedError() # TODO + + if ( + pg_config.vmem_temporal_granularity + == PowerGatingConfig.TemporalGranularity.OPERATOR + and pg_config.vmem_spatial_granularity + == PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE + and pg_config.vmem_voltage_granularity + == PowerGatingConfig.VoltageGranularity.TWO_LEVEL + ): + pg_energy = ( + pg_power_W * vmem_time_ns / 1e9 * (1 - vmem_capacity_util) + + pg_power_W * (exe_time_ns - vmem_time_ns) / 1e9 + ) + static_energy = static_vmem_W * vmem_time_ns / 1e9 * vmem_capacity_util + op.stats.static_energy_sram_J = pg_energy + static_energy + return op + + if ( + pg_config.vmem_temporal_granularity + == PowerGatingConfig.TemporalGranularity.OPERATOR + and pg_config.vmem_spatial_granularity + == PowerGatingConfig.VmemSpatialGranularity.PARTITION + and pg_config.vmem_voltage_granularity + == PowerGatingConfig.VoltageGranularity.TWO_LEVEL + ): + pg_energy = ( + pg_power_W * vmem_time_ns / 1e9 * (1 - vmem_capacity_util) + + pg_power_W * (exe_time_ns - vmem_time_ns) / 1e9 + ) + static_energy = static_vmem_W * vmem_time_ns / 1e9 * vmem_capacity_util + op.stats.static_energy_sram_J = pg_energy + static_energy + return op + + if ( + pg_config.vmem_temporal_granularity + == PowerGatingConfig.TemporalGranularity.OPERATOR + and pg_config.vmem_spatial_granularity + == PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE + and pg_config.vmem_voltage_granularity + == PowerGatingConfig.VoltageGranularity.MULTI_LEVEL + ): + raise NotImplementedError() # TODO + + if ( + pg_config.vmem_temporal_granularity + == PowerGatingConfig.TemporalGranularity.OPERATOR + and pg_config.vmem_spatial_granularity + == PowerGatingConfig.VmemSpatialGranularity.PARTITION + and pg_config.vmem_voltage_granularity + == PowerGatingConfig.VoltageGranularity.MULTI_LEVEL + ): + raise NotImplementedError() # TODO + + if ( + pg_config.vmem_temporal_granularity + == PowerGatingConfig.TemporalGranularity.APPLICATION + and pg_config.vmem_spatial_granularity + == PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE + and pg_config.vmem_voltage_granularity + == PowerGatingConfig.VoltageGranularity.TWO_LEVEL + ): + raise NotImplementedError() # TODO + + if ( + pg_config.vmem_temporal_granularity + == PowerGatingConfig.TemporalGranularity.APPLICATION + and pg_config.vmem_spatial_granularity + == PowerGatingConfig.VmemSpatialGranularity.PARTITION + and pg_config.vmem_voltage_granularity + == PowerGatingConfig.VoltageGranularity.TWO_LEVEL + ): + raise NotImplementedError() # TODO + + if ( + pg_config.vmem_temporal_granularity + == PowerGatingConfig.TemporalGranularity.APPLICATION + and pg_config.vmem_spatial_granularity + == PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE + and pg_config.vmem_voltage_granularity + == PowerGatingConfig.VoltageGranularity.MULTI_LEVEL + ): + raise NotImplementedError() # TODO + + if ( + pg_config.vmem_temporal_granularity + == PowerGatingConfig.TemporalGranularity.APPLICATION + and pg_config.vmem_spatial_granularity + == PowerGatingConfig.VmemSpatialGranularity.PARTITION + and pg_config.vmem_voltage_granularity + == PowerGatingConfig.VoltageGranularity.MULTI_LEVEL + ): + raise NotImplementedError() # TODO + + # should not reach here + raise ValueError("Unsupported/Unknown/Invalid Vmem power gating configuration") + + +def analyze_ici_static_energy( + op: Operator.Operator, config: ChipConfig, pg_config: PowerGatingConfig +) -> Operator.Operator: + """ + Static power/energy analysis for ICI. + """ + if config.enable_dvfs: + _, static_ici_W = get_power_from_dvfs("ICI", op.dvfs_ici) + else: + static_ici_W = config.static_power_ici_W + pg_power_W = static_ici_W * pg_config.ici_power_level_factors[-1] + + # No power-gating + if not pg_config.ici_PG_enabled: + op.stats.static_energy_ici_J = static_ici_W * op.stats.execution_time_ns / 1e9 + return op + + # assert ( + # pg_config.ici_PG_policy == PowerGatingConfig.PowerGatingPolicy.HW + # ), "Only HW-managed power gating is supported for ICI" + + # calculate PG delay overhead and update op stats + if op.stats.ici_time_ns > 0: + pg_delay_ns = ceil( + 2 * cycle_to_ns(pg_config.ici_pg_delay_cycles, config.freq_GHz) + ) + op.stats.ici_time_ns += pg_delay_ns + # if op.stats.ici_time_ns > op.stats.execution_time_ns: + # op.stats.execution_time_ns = op.stats.ici_time_ns + # op.stats.bounded_by = "ICI/NVLink" + + ici_time_ns = op.stats.ici_time_ns + exe_time_ns = max(op.stats.execution_time_ns, ici_time_ns) + + if ( + pg_config.ici_temporal_granularity + == PowerGatingConfig.TemporalGranularity.INSTRUCTION + and pg_config.ici_spatial_granularity + == PowerGatingConfig.ICISpatialGranularity.LINK + ): + raise NotImplementedError() # TODO + + if ( + pg_config.ici_temporal_granularity + == PowerGatingConfig.TemporalGranularity.INSTRUCTION + and pg_config.ici_spatial_granularity + == PowerGatingConfig.ICISpatialGranularity.COMPONENT + ): + pg_energy = pg_power_W * (exe_time_ns - ici_time_ns) / 1e9 + static_energy = static_ici_W * ici_time_ns / 1e9 + op.stats.static_energy_ici_J = pg_energy + static_energy + return op + + if ( + pg_config.ici_temporal_granularity + == PowerGatingConfig.TemporalGranularity.OPERATOR + and pg_config.ici_spatial_granularity + == PowerGatingConfig.ICISpatialGranularity.LINK + ): + raise NotImplementedError() # TODO + + if ( + pg_config.ici_temporal_granularity + == PowerGatingConfig.TemporalGranularity.OPERATOR + and pg_config.ici_spatial_granularity + == PowerGatingConfig.ICISpatialGranularity.COMPONENT + ): + if ici_time_ns > 0: + op.stats.static_energy_ici_J = static_ici_W * exe_time_ns / 1e9 + else: + op.stats.static_energy_ici_J = pg_power_W * exe_time_ns / 1e9 + return op + + if ( + pg_config.ici_temporal_granularity + == PowerGatingConfig.TemporalGranularity.APPLICATION + and pg_config.ici_spatial_granularity + == PowerGatingConfig.ICISpatialGranularity.LINK + ): + raise NotImplementedError() # TODO + + if ( + pg_config.ici_temporal_granularity + == PowerGatingConfig.TemporalGranularity.APPLICATION + and pg_config.ici_spatial_granularity + == PowerGatingConfig.ICISpatialGranularity.COMPONENT + ): + raise NotImplementedError() # TODO + + # should not reach here + raise ValueError("Unsupported/Unknown/Invalid ICI power gating configuration") + + +def analyze_hbm_static_energy( + op: Operator.Operator, config: ChipConfig, pg_config: PowerGatingConfig +) -> Operator.Operator: + """ + Static power/energy analysis for HBM. + """ + if config.enable_dvfs: + _, static_hbm_W = get_power_from_dvfs("HBM", op.dvfs_hbm) + else: + static_hbm_W = config.static_power_hbm_W + pg_power_W = static_hbm_W * pg_config.hbm_power_level_factors[-1] + + # No power-gating + if not pg_config.hbm_PG_enabled: + op.stats.static_energy_hbm_J = ( + static_hbm_W * op.stats.execution_time_ns / 1e9 + ) + return op + + # assume 4MB DMA size if memory traffic is larger than this + if op.stats.memory_traffic_bytes < 4 * 1024 * 1024: + active_length_ns = op.stats.memory_time_ns + else: + active_length_ns = ( + 4 * 1024 * 1024 / (config.hbm_bw_GBps * 1024 * 1024 * 1024) * 1e9 + ) + hbm_util = op.stats.memory_time_ns / op.stats.execution_time_ns + num_periods = ceil( + op.stats.memory_time_ns / active_length_ns + ) + idle_length_ns = ceil( + op.stats.execution_time_ns / num_periods - active_length_ns + ) + + # break-even time + BET_ns = config.hbm_latency_ns * 2 + ceil(cycle_to_ns(pg_config.hbm_pg_delay_cycles, config.freq_GHz)) * 4 + idle_detect_timeout_ns = BET_ns * 4 + + if pg_config.hbm_PG_policy == PowerGatingConfig.PowerGatingPolicy.SW: + if idle_length_ns >= BET_ns: + # power gate HBM + pg_energy = pg_power_W * (op.stats.execution_time_ns - op.stats.memory_time_ns) / 1e9 + static_energy = op.stats.memory_time_ns / 1e9 * static_hbm_W + op.stats.static_energy_hbm_J = pg_energy + static_energy + else: + # do not power gate HBM + op.stats.static_energy_hbm_J = static_hbm_W * op.stats.execution_time_ns / 1e9 + elif pg_config.hbm_PG_policy == PowerGatingConfig.PowerGatingPolicy.HW: + if idle_length_ns >= idle_detect_timeout_ns: + # power gate HBM + pg_energy = pg_power_W * (op.stats.execution_time_ns - op.stats.memory_time_ns) / 1e9 + static_energy = op.stats.memory_time_ns / 1e9 * static_hbm_W + op.stats.static_energy_hbm_J = pg_energy + static_energy + + # calculate PG delay overhead and update op stats + pg_delay_ns = ceil( + cycle_to_ns(pg_config.hbm_pg_delay_cycles, config.freq_GHz) * num_periods + ) + op.stats.memory_time_ns += pg_delay_ns + else: + # do not power gate HBM + op.stats.static_energy_hbm_J = static_hbm_W * op.stats.execution_time_ns / 1e9 + else: + raise NotImplementedError("Unknown HBM power gating policy") + + return op + + +def analyze_other_static_energy( + op: Operator.Operator, config: ChipConfig, pg_config: PowerGatingConfig +) -> Operator.Operator: + """ + Static power/energy analysis for other. + """ + assert pg_config.other_PG_enabled is False, "Other power gating is not supported" + + op.stats.static_energy_other_J = ( + config.static_power_other_W * op.stats.execution_time_ns / 1e9 + ) + return op + + +def add_op_dvfs_exe_time_overhead(op: Operator.Operator, config: ChipConfig) -> Operator.Operator: + """ + Add DVFS latency overhead to op execution times and + set voltage conversion efficiency in the DVFSConfig for each component. + """ + # helpers + def _get_dvfs_config_for_component(comp: str) -> ComponentDVFSConfig: + if comp == "sa": + return op.dvfs_sa + elif comp == "vu": + return op.dvfs_vu + elif comp == "hbm": + return op.dvfs_hbm + elif comp == "ici": + return op.dvfs_ici + elif comp == "vmem": + return op.dvfs_sram + else: + raise ValueError(f"Unknown component '{comp}'") + + def _apply_exe_time_overhead_for_component(comp: str, st_ns: int): + if comp == "sa": + op.stats.sa_time_ns += st_ns + elif comp == "vu": + op.stats.vu_time_ns += st_ns + elif comp == "hbm": + op.stats.memory_time_ns += st_ns + elif comp == "ici": + op.stats.ici_time_ns += st_ns + elif comp == "vmem": + op.stats.vmem_time_ns += st_ns + else: + raise ValueError(f"Unknown component '{comp}'") + + def _get_activity_factor_for_component(comp: str): + if comp in ["sa", "vu"]: + # for SA and VU, activity factor is spatial utilization (e.g., FLOPS) + return min(1, op.stats.flops_util) + elif comp in ["hbm", "ici", "vmem"]: + # for other components, activity factor is time utilization + time_ns = { + "hbm": op.stats.memory_time_ns, + "ici": op.stats.ici_time_ns, + "vmem": op.stats.vmem_time_ns, + }[comp] + util = min(1, time_ns / op.stats.execution_time_ns) + return util + else: + raise ValueError(f"Unknown component '{comp}'") + + def _lookup_efficiency_from_table(scaling_time_ns: int, activity: float, voltage: float, policy: DVFSPolicy) -> float: + if policy == DVFSPolicy.NONE: + table = FIXED_VOLTAGE_REGULATOR_OVERHEAD_TABLE + else: + table = DVFS_VOLTAGE_REGULATOR_OVERHEAD_TABLE + + # filter out scaling time + rows = [r for r in table if r.scaling_time_ns == scaling_time_ns] + assert len(rows) > 0, f"No DVFS overhead table entry for scaling_time_ns={scaling_time_ns}" + + # pick the nearest voltage that is greater than or equal to requested voltage + voltages = sorted(set(r.voltage_V for r in rows)) + v_snap = None + for v in voltages: + if v >= voltage: + v_snap = v + break + assert v_snap is not None, f"No DVFS overhead table entry for voltage_V >= {voltage}, scaling_time_ns={scaling_time_ns}, activity_factor={activity}" + rows = [r for r in rows if r.voltage_V == v_snap] + + # pick the row with smallest activity factor >= requested activity + rows = sorted(rows, key=lambda r: r.activity_factor) + chosen_row = None + for r in rows: + if r.activity_factor >= activity: + chosen_row = r + break + assert chosen_row is not None, f"No DVFS overhead table entry for activity_factor >= {activity}, V={v_snap}, scaling_time_ns={scaling_time_ns}" + + return chosen_row.power_efficiency_percent + + # component execution times + comp_times: dict[str, int] = { + "sa": op.stats.sa_time_ns, + "vu": op.stats.vu_time_ns, + "hbm": op.stats.memory_time_ns, + "ici": op.stats.ici_time_ns, + "vmem": op.stats.vmem_time_ns, + } + + for comp, t_ns in comp_times.items(): + dvfs_config = _get_dvfs_config_for_component(comp) + dvfs_policy = dvfs_config.policy + + activity_factor = _get_activity_factor_for_component(comp) + voltage_V = dvfs_config.voltage_V or 0.7 # default to 0.7V for now + + chosen_eff= _lookup_efficiency_from_table( + scaling_time_ns=dvfs_config.voltage_regulator_scaling_time_ns, + activity=activity_factor, + voltage=voltage_V, + policy=dvfs_policy, + ) + + if t_ns > 0 and dvfs_policy not in [DVFSPolicy.NONE, DVFSPolicy.IDEAL]: + # if component is unused, do not change execution time + _apply_exe_time_overhead_for_component(comp, dvfs_config.voltage_regulator_scaling_time_ns) + dvfs_config.voltage_conversion_power_efficiency_percent = chosen_eff + + # update e2e execution time + exe_time_ns = max( + op.stats.sa_time_ns, + op.stats.vu_time_ns, + op.stats.vmem_time_ns, + op.stats.ici_time_ns, + op.stats.memory_time_ns, + ) + op.stats.execution_time_ns = exe_time_ns + bounded_by = None + for t, label in [ + (op.stats.sa_time_ns, "Compute"), + (op.stats.vu_time_ns, "Compute"), + (op.stats.vmem_time_ns, "Compute"), + (op.stats.memory_time_ns, "Memory"), + (op.stats.ici_time_ns, "ICI/NVLink"), + ]: + if t == exe_time_ns: + bounded_by = label + break + assert bounded_by, "Failed to determine bounded_by after DVFS overhead addition" + op.stats.bounded_by = bounded_by + + return op + + +def apply_regulator_efficiency(op: Operator.Operator) -> Operator.Operator: + """ + Apply regulator efficiency losses to per-component energies. + Multiply both dynamic and static energies by (100/efficiency_percent). + + Notes: + - We scale energies (J) post computation, which is equivalent to + scaling power for the already-integrated durations. + - Components covered: SA, VU, SRAM (vmem), HBM, ICI. 'other' is unchanged. + """ + + # SA + op.stats.dynamic_energy_sa_J *= 100.0 / op.dvfs_sa.voltage_conversion_power_efficiency_percent + op.stats.static_energy_sa_J *= 100.0 / op.dvfs_sa.voltage_conversion_power_efficiency_percent + + # VU + op.stats.dynamic_energy_vu_J *= 100.0 / op.dvfs_vu.voltage_conversion_power_efficiency_percent + op.stats.static_energy_vu_J *= 100.0 / op.dvfs_vu.voltage_conversion_power_efficiency_percent + + # SRAM (vmem) + op.stats.dynamic_energy_sram_J *= 100.0 / op.dvfs_sram.voltage_conversion_power_efficiency_percent + op.stats.static_energy_sram_J *= 100.0 / op.dvfs_sram.voltage_conversion_power_efficiency_percent + + # HBM + op.stats.dynamic_energy_hbm_J *= 100.0 / op.dvfs_hbm.voltage_conversion_power_efficiency_percent + op.stats.static_energy_hbm_J *= 100.0 / op.dvfs_hbm.voltage_conversion_power_efficiency_percent + + # ICI + op.stats.dynamic_energy_ici_J *= 100.0 / op.dvfs_ici.voltage_conversion_power_efficiency_percent + op.stats.static_energy_ici_J *= 100.0 / op.dvfs_ici.voltage_conversion_power_efficiency_percent + + return op diff --git a/neusim/npusim/backend/tests/test_dvfs_policy_lib.py b/neusim/npusim/backend/tests/test_dvfs_policy_lib.py new file mode 100644 index 0000000..99fe5fc --- /dev/null +++ b/neusim/npusim/backend/tests/test_dvfs_policy_lib.py @@ -0,0 +1,86 @@ +import unittest + +from neusim.npusim.frontend.Operator import DVFSPolicy, DVFSConfig, ComponentDVFSConfig +from neusim.npusim.backend.dvfs_policy_lib import ( + slowdown_freq, + pick_v_from_freq, + get_dvfs_policy_None, + get_dvfs_config, + SA_VF_TABLE, +) + + +class TestSlowdownFreq(unittest.TestCase): + def test_zero_ratio_returns_base(self): + self.assertEqual(slowdown_freq(0.0, 1.7), 1.7) + + def test_negative_ratio_returns_base(self): + self.assertEqual(slowdown_freq(-1.0, 1.7), 1.7) + + def test_positive_ratio_slows_down(self): + f = slowdown_freq(1.0, 1.7) + self.assertAlmostEqual(f, 0.85) + + def test_large_ratio_clamps_to_min(self): + f = slowdown_freq(1000.0, 1.7, min_freq_GHz=0.05) + self.assertAlmostEqual(f, 0.05) + + +class TestPickVFromFreq(unittest.TestCase): + def test_zero_freq_returns_zero(self): + self.assertEqual(pick_v_from_freq(0.0, SA_VF_TABLE), 0.0) + + def test_below_min_returns_first_voltage(self): + v = pick_v_from_freq(0.1, SA_VF_TABLE) + self.assertEqual(v, 0.45) + + def test_above_max_returns_last_voltage(self): + v = pick_v_from_freq(2.0, SA_VF_TABLE) + self.assertEqual(v, 0.70) + + def test_mid_range_returns_correct_band(self): + v = pick_v_from_freq(1.0, SA_VF_TABLE) + self.assertEqual(v, 0.55) + + +class TestGetDvfsPolicyNone(unittest.TestCase): + def test_returns_all_components(self): + plan = get_dvfs_policy_None() + self.assertIn("sa", plan) + self.assertIn("vu", plan) + self.assertIn("sram", plan) + self.assertIn("hbm", plan) + self.assertIn("ici", plan) + + def test_all_none_policy(self): + plan = get_dvfs_policy_None() + for comp_cfg in plan.values(): + self.assertEqual(comp_cfg.policy, DVFSPolicy.NONE) + self.assertAlmostEqual(comp_cfg.voltage_V, 0.7) + self.assertAlmostEqual(comp_cfg.frequency_GHz, 1.7) + + +class TestGetDvfsConfig(unittest.TestCase): + def test_none_policy_dispatches(self): + from unittest.mock import MagicMock + op = MagicMock() + from neusim.configs.chips.ChipConfig import ChipConfig + config = ChipConfig() + dvfs_cfg = DVFSConfig(policy=DVFSPolicy.NONE) + plan = get_dvfs_config(op, config, dvfs_cfg) + self.assertIn("sa", plan) + self.assertEqual(plan["sa"].policy, DVFSPolicy.NONE) + + def test_unsupported_policy_raises(self): + from unittest.mock import MagicMock + op = MagicMock() + from neusim.configs.chips.ChipConfig import ChipConfig + config = ChipConfig() + dvfs_cfg = DVFSConfig() + dvfs_cfg.policy = "UNSUPPORTED" + with self.assertRaises((ValueError, AttributeError)): + get_dvfs_config(op, config, dvfs_cfg) + + +if __name__ == "__main__": + unittest.main() diff --git a/neusim/npusim/backend/tests/test_dvfs_power_getter.py b/neusim/npusim/backend/tests/test_dvfs_power_getter.py new file mode 100644 index 0000000..83ca0e9 --- /dev/null +++ b/neusim/npusim/backend/tests/test_dvfs_power_getter.py @@ -0,0 +1,73 @@ +import unittest + +from neusim.npusim.frontend.Operator import DVFSPolicy, ComponentDVFSConfig +from neusim.npusim.backend.dvfs_power_getter import ( + get_power_from_dvfs, + get_all_dvfs_configs_for_component, + SA_POINTS, + VU_POINTS, + HBM_POINTS, +) + + +class TestGetPowerFromDvfs(unittest.TestCase): + def test_sa_none_policy_returns_max_perf(self): + dvfs = ComponentDVFSConfig(policy=DVFSPolicy.NONE, voltage_V=0.7, frequency_GHz=1.7) + dyn, static = get_power_from_dvfs("SA", dvfs) + max_sa = max(SA_POINTS, key=lambda p: p.frequency_GHz) + self.assertAlmostEqual(dyn, max_sa.dynamic_power_W) + self.assertAlmostEqual(static, max_sa.static_power_W) + + def test_vu_none_policy_returns_max_perf(self): + dvfs = ComponentDVFSConfig(policy=DVFSPolicy.NONE, voltage_V=0.7, frequency_GHz=1.7) + dyn, static = get_power_from_dvfs("VU", dvfs) + max_vu = max(VU_POINTS, key=lambda p: p.frequency_GHz) + self.assertAlmostEqual(dyn, max_vu.dynamic_power_W) + self.assertAlmostEqual(static, max_vu.static_power_W) + + def test_sa_ideal_at_known_point(self): + dvfs = ComponentDVFSConfig(policy=DVFSPolicy.IDEAL, voltage_V=0.7, frequency_GHz=1.7) + dyn, static = get_power_from_dvfs("SA", dvfs) + self.assertGreater(dyn, 0) + self.assertGreater(static, 0) + + def test_hbm_returns_positive(self): + dvfs = ComponentDVFSConfig(policy=DVFSPolicy.IDEAL, voltage_V=0.7, frequency_GHz=1.7) + dyn, static = get_power_from_dvfs("HBM", dvfs) + self.assertGreater(dyn, 0) + self.assertGreater(static, 0) + + def test_ici_returns_positive(self): + dvfs = ComponentDVFSConfig(policy=DVFSPolicy.IDEAL, voltage_V=0.7, frequency_GHz=1.7) + dyn, static = get_power_from_dvfs("ICI", dvfs) + self.assertGreater(dyn, 0) + self.assertGreater(static, 0) + + def test_unsupported_component_raises(self): + dvfs = ComponentDVFSConfig(policy=DVFSPolicy.IDEAL, voltage_V=0.7, frequency_GHz=1.7) + with self.assertRaises(ValueError): + get_power_from_dvfs("UNSUPPORTED", dvfs) + + def test_zero_freq_returns_max_perf(self): + dvfs = ComponentDVFSConfig(policy=DVFSPolicy.IDEAL, voltage_V=0.7, frequency_GHz=0.0) + dyn, static = get_power_from_dvfs("SA", dvfs) + max_sa = max(SA_POINTS, key=lambda p: p.frequency_GHz) + self.assertAlmostEqual(dyn, max_sa.dynamic_power_W) + + +class TestGetAllDvfsConfigsForComponent(unittest.TestCase): + def test_sa_configs_count(self): + configs = get_all_dvfs_configs_for_component("sa", DVFSPolicy.IDEAL) + self.assertEqual(len(configs), len(SA_POINTS)) + + def test_hbm_configs_count(self): + configs = get_all_dvfs_configs_for_component("hbm", DVFSPolicy.IDEAL) + self.assertEqual(len(configs), len(HBM_POINTS)) + + def test_unsupported_component_raises(self): + with self.assertRaises(ValueError): + get_all_dvfs_configs_for_component("UNKNOWN", DVFSPolicy.IDEAL) + + +if __name__ == "__main__": + unittest.main() diff --git a/neusim/npusim/backend/tests/test_power_model.py b/neusim/npusim/backend/tests/test_power_model.py new file mode 100644 index 0000000..099c09d --- /dev/null +++ b/neusim/npusim/backend/tests/test_power_model.py @@ -0,0 +1,105 @@ +import unittest +from unittest.mock import MagicMock + +from neusim.configs.chips.ChipConfig import ChipConfig +from neusim.npusim.frontend.Operator import DVFSPolicy, ComponentDVFSConfig +from neusim.npusim.backend.power_model import ( + compute_peak_sa_flops_per_sec_from_chip_config, + compute_peak_vu_flops_per_sec_from_chip_config, + compute_peak_sa_flops_per_sec_from_dvfs_config, + cycle_to_ns, + ns_to_cycle, + scale_dvfs_component_time, + apply_regulator_efficiency, +) + + +class TestComputePeakFlops(unittest.TestCase): + def test_sa_flops_positive(self): + config = ChipConfig() + flops = compute_peak_sa_flops_per_sec_from_chip_config(config) + self.assertGreater(flops, 0) + + def test_vu_flops_positive(self): + config = ChipConfig() + flops = compute_peak_vu_flops_per_sec_from_chip_config(config) + self.assertGreater(flops, 0) + + def test_sa_flops_from_dvfs_with_zero_freq_falls_back(self): + config = ChipConfig() + dvfs = ComponentDVFSConfig(policy=DVFSPolicy.NONE, voltage_V=0.7, frequency_GHz=0.0) + flops = compute_peak_sa_flops_per_sec_from_dvfs_config(config, dvfs) + expected = compute_peak_sa_flops_per_sec_from_chip_config(config) + self.assertAlmostEqual(flops, expected) + + +class TestCycleNsConversion(unittest.TestCase): + def test_roundtrip(self): + ns = cycle_to_ns(10, 1.7) + cycles = ns_to_cycle(ns, 1.7) + self.assertAlmostEqual(cycles, 10.0) + + def test_cycle_to_ns_known_value(self): + ns = cycle_to_ns(17, 1.7) + self.assertAlmostEqual(ns, 10.0) + + +class TestScaleDvfsComponentTime(unittest.TestCase): + def test_half_freq_doubles_time(self): + op = MagicMock() + op.stats.sa_time_ns = 100 + op.stats.vu_time_ns = 100 + op.stats.vmem_time_ns = 100 + op.stats.ici_time_ns = 100 + op.stats.memory_time_ns = 100 + + config = ChipConfig() + # Use exactly half the base frequency + half_freq = ComponentDVFSConfig( + policy=DVFSPolicy.IDEAL, voltage_V=0.5, frequency_GHz=config.freq_GHz / 2 + ) + op.dvfs_sa = half_freq + op.dvfs_vu = half_freq + op.dvfs_sram = half_freq + op.dvfs_ici = half_freq + op.dvfs_hbm = half_freq + + scale_dvfs_component_time(op, config) + self.assertEqual(op.stats.sa_time_ns, 200) + + +class TestApplyRegulatorEfficiency(unittest.TestCase): + def test_scales_energies(self): + op = MagicMock() + op.stats.dynamic_energy_sa_J = 1.0 + op.stats.static_energy_sa_J = 1.0 + op.stats.dynamic_energy_vu_J = 1.0 + op.stats.static_energy_vu_J = 1.0 + op.stats.dynamic_energy_sram_J = 1.0 + op.stats.static_energy_sram_J = 1.0 + op.stats.dynamic_energy_hbm_J = 1.0 + op.stats.static_energy_hbm_J = 1.0 + op.stats.dynamic_energy_ici_J = 1.0 + op.stats.static_energy_ici_J = 1.0 + + eff_cfg = ComponentDVFSConfig( + policy=DVFSPolicy.NONE, + voltage_V=0.7, + frequency_GHz=1.7, + voltage_conversion_power_efficiency_percent=50.0, + ) + op.dvfs_sa = eff_cfg + op.dvfs_vu = eff_cfg + op.dvfs_sram = eff_cfg + op.dvfs_hbm = eff_cfg + op.dvfs_ici = eff_cfg + + apply_regulator_efficiency(op) + # 100/50 = 2x scaling + self.assertAlmostEqual(op.stats.dynamic_energy_sa_J, 2.0) + self.assertAlmostEqual(op.stats.static_energy_sa_J, 2.0) + self.assertAlmostEqual(op.stats.dynamic_energy_hbm_J, 2.0) + + +if __name__ == "__main__": + unittest.main() diff --git a/neusim/npusim/frontend/dvfs_policy_lib.py b/neusim/npusim/frontend/dvfs_policy_lib.py index 66f407c..6c1922c 100644 --- a/neusim/npusim/frontend/dvfs_policy_lib.py +++ b/neusim/npusim/frontend/dvfs_policy_lib.py @@ -1,205 +1,15 @@ -### Pre-defined DVFS policies, and helper functions to apply DVFS policies to Operators. - -from neusim.configs.chips.ChipConfig import ChipConfig -import neusim.npusim.frontend.Operator as Operator -from neusim.npusim.frontend.Operator import ComponentDVFSConfig, DVFSPolicy, DVFSConfig -from neusim.npusim.frontend.util import compute_component_slack_for_op - - -# ----- SA voltage-frequency bands ----- -SA_VF_TABLE = [ - (0.45, 0.600240096), - (0.50, 0.850340136), - (0.55, 1.149425287), - (0.60, 1.351351351), - (0.65, 1.602564103), - (0.70, 1.700680272), -] - -# ----- VU voltage-frequency bands ----- -VU_VF_TABLE = [ - (0.45, 0.600240096), - (0.50, 0.850340136), - (0.55, 1.149425287), - (0.60, 1.351351351), - (0.65, 1.602564103), - (0.70, 1.700680272), -] - -# ----- SRAM voltage-frequency bands ----- -SRAM_VF_TABLE = [ - (0.45, 0.500000000), - (0.50, 0.750750751), - (0.55, 1.050420168), - (0.60, 1.250000000), - (0.65, 1.501501502), - (0.70, 1.700680272), -] - -# ----- HBM voltage-bandwidth-frequency proxy table ----- -# (v, f_proxy_ghz) -HBM_VF_TABLE = [ - # (1.00, 1.459), - # (1.05, 1.544), - # (1.10, 1.629), - # (1.15, 1.712), - # (1.20, 1.793), - (0.45, 1.311252269), - (0.50, 1.388384755), - (0.55, 1.463666062), - (0.60, 1.543883848), - (0.65, 1.622250454), - (0.70, 1.7), -] - -ICI_VF_TABLE = [ - (0.45, 1.308720), - (0.50, 1.388110), - (0.55, 1.461829), - (0.60, 1.541220), - (0.65, 1.620610), - (0.70, 1.7), -] - - -def slowdown_freq(ratio: float, base_freq_GHz: float, min_freq_GHz: float = 0.05) -> float: - """ - Given ratio = extra / active_time, use all slack: - new_time = active_time * (1 + ratio) - new_freq = base_freq / (1 + ratio) - Clamp into [min_freq_GHz, base_freq_GHz]. - """ - if ratio <= 0: - return base_freq_GHz - f = base_freq_GHz / (1.0 + ratio) - if f < min_freq_GHz: - f = min_freq_GHz - if f > base_freq_GHz: - f = base_freq_GHz - return f - - -def pick_v_from_freq(f_ghz: float, table: list[tuple[float, float]]) -> float: - ''' - @return: voltage for a given frequency @f_ghz in the (V, f) @table. - Assume table is sorted ascending. - ''' - if f_ghz <= 0: - return 0.0 - - v0, f0 = table[0] - if f_ghz <= f0: - return v0 - - for i in range(len(table) - 1): - v_curr, f_curr = table[i] - v_next, f_next = table[i + 1] - if f_ghz > f_curr and f_ghz <= f_next: - return v_next - - return table[-1][0] - - -def comp(policy: DVFSPolicy, v: float, f_ghz: float, scaling_time_ns: int = 20) -> ComponentDVFSConfig: - '''Helper to build component entries''' - return ComponentDVFSConfig( - policy=policy, - voltage_V=v, - frequency_GHz=f_ghz, - voltage_regulator_scaling_time_ns=scaling_time_ns, - ) - - -def get_dvfs_policy_None( - op: Operator.Operator | None = None, config: ChipConfig | None = None, dvfs_cfg: DVFSConfig | None = None, # unused -) -> dict[str, ComponentDVFSConfig]: - plan = { - "sa": comp(DVFSPolicy.NONE, 0.7, 1.7, 0), - "vu": comp(DVFSPolicy.NONE, 0.7, 1.7, 0), - "sram": comp(DVFSPolicy.NONE, 0.7, 1.7, 0), - "hbm": comp(DVFSPolicy.NONE, 0.7, 1.7, 0), - "ici": comp(DVFSPolicy.NONE, 0.7, 1.7, 0), - } - return plan - - -def get_dvfs_policy_Ideal( - op: Operator.Operator, config: ChipConfig, dvfs_cfg: DVFSConfig, -) -> dict[str, ComponentDVFSConfig]: - - # Fixed base frequency for DVFS planning (GHz) - base_freq_ghz = config.freq_GHz - - # Per-operator slack ratios - extras, ratios = compute_component_slack_for_op(op) - - # SA - if op.stats.sa_time_ns > 0: - f_sa_ghz = slowdown_freq(ratios["sa"], base_freq_ghz) - v_sa = pick_v_from_freq(f_sa_ghz, SA_VF_TABLE) - else: - f_sa_ghz, v_sa = 0.05, 0.45 # min freq/v - - # VU - if op.stats.vu_time_ns > 0: - f_vu_ghz = slowdown_freq(ratios["vu"], base_freq_ghz) - v_vu = pick_v_from_freq(f_vu_ghz, VU_VF_TABLE) - else: - f_vu_ghz, v_vu = 0.05, 0.45 # min freq/v - - # SRAM / Vmem (use vmem slack) - if op.stats.vmem_time_ns > 0: - f_sram_ghz = slowdown_freq(ratios["vmem"], base_freq_ghz) - v_sram = pick_v_from_freq(f_sram_ghz, SRAM_VF_TABLE) - else: - f_sram_ghz, v_sram = 0.05, 0.45 # min freq/v - - # HBM - if op.stats.memory_time_ns > 0: - f_hbm_ghz = slowdown_freq(ratios["hbm"], base_freq_ghz) - v_hbm = pick_v_from_freq(f_hbm_ghz, HBM_VF_TABLE) - else: - f_hbm_ghz, v_hbm = 0.0, 0.45 # min freq/v - - # ICI - if op.stats.ici_time_ns > 0: - f_ici_ghz = slowdown_freq(ratios["ici"], base_freq_ghz) - v_ici = pick_v_from_freq(f_ici_ghz, ICI_VF_TABLE) - else: - f_ici_ghz, v_ici = 0.0, 0.45 # min freq/v - - plan = { - # use 200ns scaling time for IDEAL DVFS since it is most power efficient - # when calculating time overhead, we ignore this scaling time for ideal DVFS - "sa": comp(DVFSPolicy.IDEAL, v_sa, f_sa_ghz, 200), - "vu": comp(DVFSPolicy.IDEAL, v_vu, f_vu_ghz, 200), - "sram": comp(DVFSPolicy.IDEAL, v_sram, f_sram_ghz, 200), - "hbm": comp(DVFSPolicy.IDEAL, v_hbm, f_hbm_ghz, 200), - "ici": comp(DVFSPolicy.IDEAL, v_ici, f_ici_ghz, 200), - } - return plan - - -def get_dvfs_config( - op: Operator.Operator, - config: ChipConfig, - dvfs_cfg: DVFSConfig, -) -> dict[str, ComponentDVFSConfig]: - """ - Build the DVFSConfigs for each component for this operator based on dvfs_mode. - Only SA, VU, SRAM/VMEM, HBM, ICI are controlled. - """ - - plan: dict[str, ComponentDVFSConfig] = {} - # For NONE, configure as the default numbers. - if dvfs_cfg.policy == DVFSPolicy.NONE: - plan = get_dvfs_policy_None(op, config) - - # For IDEAL, use the computed per-op settings. - elif dvfs_cfg.policy == DVFSPolicy.IDEAL: - plan = get_dvfs_policy_Ideal(op, config, dvfs_cfg) - - else: - raise ValueError(f"Unsupported DVFSPolicy: {dvfs_cfg.policy}") - - return plan +"""Backward-compatible facade — re-exports from backend.""" + +from neusim.npusim.backend.dvfs_policy_lib import ( # noqa: F401 + SA_VF_TABLE, + VU_VF_TABLE, + SRAM_VF_TABLE, + HBM_VF_TABLE, + ICI_VF_TABLE, + slowdown_freq, + pick_v_from_freq, + comp, + get_dvfs_policy_None, + get_dvfs_policy_Ideal, + get_dvfs_config, +) diff --git a/neusim/npusim/frontend/dvfs_power_getter.py b/neusim/npusim/frontend/dvfs_power_getter.py index e2b5cdb..b36cd6d 100644 --- a/neusim/npusim/frontend/dvfs_power_getter.py +++ b/neusim/npusim/frontend/dvfs_power_getter.py @@ -1,771 +1,19 @@ -from absl import logging -from functools import lru_cache -import itertools -from typing import NamedTuple - -import neusim.npusim.frontend.Operator as Operator -from neusim.npusim.frontend.Operator import DVFSPolicy, DVFSConfig, ComponentDVFSConfig - -""" -Map DVFS config (voltage_V, frequency_GHz) to dynamic/static power -using discrete SA/VU/SRAM/HBM/ICI tables. -""" - -# Type aliases for table structures -class VfPoint(NamedTuple): - '''Voltage/frequency point for SA/VU/SRAM components.''' - voltage_V: float = 0.7 - frequency_GHz: float = 1.7 - static_power_W: float = 0.0 - dynamic_power_W: float = 0.0 - -class VBWPoint(NamedTuple): - '''Voltage/bandwidth point for HBM/ICI components.''' - voltage_V: float = 0.7 - bandwidth_GBs: float = 0.0 - static_power_W: float = 0.0 - dynamic_power_W: float = 0.0 - -class PowerEfficiencyPoint(NamedTuple): - '''DVFS voltage regulator power conversion efficiency point for each scaling time, activity factor, and voltage.''' - scaling_time_ns: int = 0 - activity_factor: float = 1.0 - voltage_V: float = 0.7 - power_efficiency_percent: float = 100.0 - -# Row = tuple[float, float, float] -Row = NamedTuple('Row', [('x', float), ('s', float), ('d', float)]) # x can be frequency_GHz or bandwidth_GBs -Groups = dict[float, list[Row]] # keyed by voltage_V, value is list of Rows - -# ========================= -# Lookup tables for each component at different V/f points. -# The tables are only for TPUv5p's HW spec assuming 7nm FinFET node for now. -# SA and VU points are for a single SA/VU. -# ========================= - -_SA_POINTS = [ - # voltage_V, frequency_GHz, static_power_W, dynamic_power_W - (0.45, 0, 0.5883612773214286, 0.0), - (0.45, 0.05, 0.5883612773214286, 0.244), - (0.45, 0.1, 0.5883612773214286, 0.489), - (0.45, 0.150015002, 0.5883612773214286, 0.731), - (0.45, 0.2, 0.5883612773214286, 0.975), - (0.45, 0.25, 0.5883612773214286, 1.22), - (0.45, 0.300120048, 0.5883612773214286, 1.46), - (0.45, 0.350140056, 0.5883612773214286, 1.71), - (0.45, 0.4, 0.5883612773214286, 1.95), - (0.45, 0.450045005, 0.5883612773214286, 2.2), - (0.45, 0.5, 0.5883612773214286, 2.44), - (0.45, 0.550055006, 0.5883612773214286, 2.68), - (0.45, 0.600240096, 0.5883612773214286, 2.93), - (0.5, 0.650195059, 0.7066400908035716, 3.97), - (0.5, 0.700280112, 0.7066400908035716, 4.28), - (0.5, 0.750750751, 0.7066400908035716, 4.58), - (0.5, 0.8, 0.7066400908035716, 4.89), - (0.5, 0.850340136, 0.7066400908035716, 5.2), - (0.55, 0.900900901, 0.8370500646428571, 6.76), - (0.55, 0.950570342, 0.8370500646428571, 7.13), - (0.55, 1.0, 0.8370500646428571, 7.49), - (0.55, 1.050420168, 0.8370500646428571, 7.86), - (0.55, 1.101321586, 0.8370500646428571, 8.24), - (0.6, 1.149425287, 0.9947551492857144, 10.368), - (0.6, 1.201923077, 0.9947551492857144, 10.8544), - (0.6, 1.25, 0.9947551492857144, 11.264), - (0.6, 1.302083333, 0.9947551492857144, 11.7504), - (0.6, 1.351351351, 0.9947551492857144, 12.2112), - (0.65, 1.400560224, 1.1645913942857145, 15.0016), - (0.65, 1.449275362, 1.1645913942857145, 15.5136), - (0.65, 1.501501502, 1.1645913942857145, 16.0768), - (0.65, 1.552795031, 1.1645913942857145, 16.6144), - (0.65, 1.602564103, 1.1645913942857145, 17.152), - (0.7, 1.650165017, 1.35868996 , 20.6848), - (0.7, 1.7, 1.35868996 , 21.3248), -] -SA_POINTS: list[VfPoint] = [VfPoint(*point) for point in _SA_POINTS] - -_VU_POINTS = [ - # voltage_V, frequency_GHz, static_power_W, dynamic_power_W - (0.45, 0, 0.2054646559675127, 0.0), - (0.45, 0.05, 0.2054646559675127, 0.0571), - (0.45, 0.1, 0.2054646559675127, 0.114), - (0.45, 0.150015002, 0.2054646559675127, 0.171), - (0.45, 0.2, 0.2054646559675127, 0.228), - (0.45, 0.25, 0.2054646559675127, 0.285), - (0.45, 0.300120048, 0.2054646559675127, 0.343), - (0.45, 0.350140056, 0.2054646559675127, 0.4), - (0.45, 0.4, 0.2054646559675127, 0.457), - (0.45, 0.450045005, 0.2054646559675127, 0.514), - (0.45, 0.5, 0.2054646559675127, 0.571), - (0.45, 0.550055006, 0.2054646559675127, 0.629), - (0.45, 0.600240096, 0.2054646559675127, 0.686), - (0.5, 0.650195059, 0.2459788134822335, 0.936), - (0.5, 0.700280112, 0.2459788134822335, 1.01), - (0.5, 0.750750751, 0.2459788134822335, 1.08), - (0.5, 0.8, 0.2459788134822335, 1.15), - (0.5, 0.850340136, 0.2459788134822335, 1.22), - (0.55, 0.900900901, 0.29324533058274116, 1.6), - (0.55, 0.950570342, 0.29324533058274116, 1.68), - (0.55, 1.0, 0.29324533058274116, 1.77), - (0.55, 1.050420168, 0.29324533058274116, 1.86), - (0.55, 1.101321586, 0.29324533058274116, 1.95), - (0.55, 1.149425287, 0.29324533058274116, 2.04), - (0.6, 1.201923077, 0.3458172730720812, 2.57), - (0.6, 1.25, 0.3458172730720812, 2.68), - (0.6, 1.302083333, 0.3458172730720812, 2.79), - (0.6, 1.351351351, 0.3458172730720812, 2.89), - (0.6, 1.400560224, 0.3458172730720812, 3.0), - (0.65, 1.449275362, 0.40707082074314727, 3.69), - (0.65, 1.501501502, 0.40707082074314727, 3.83), - (0.65, 1.552795031, 0.40707082074314727, 3.96), - (0.65, 1.602564103, 0.40707082074314727, 4.08), - (0.7, 1.650165017, 0.475076728, 4.94), - (0.7, 1.7, 0.475076728, 5.10), -] -VU_POINTS: list[VfPoint] = [VfPoint(*point) for point in _VU_POINTS] - -_SRAM_POINTS = [ - # voltage_V, frequency_GHz, static_power_W, dynamic_power_W - (0.45, 0, 6.43650960949367, 0.0), - (0.45, 0.05, 6.43650960949367, 0.791), - (0.45, 0.1, 6.43650960949367, 1.58), - (0.45, 0.150015002, 6.43650960949367, 2.37), - (0.45, 0.2, 6.43650960949367, 3.16), - (0.45, 0.25, 6.43650960949367, 3.95), - (0.45, 0.300120048, 6.43650960949367, 4.74), - (0.45, 0.350140056, 6.43650960949367, 5.53), - (0.45, 0.4, 6.43650960949367, 6.32), - (0.45, 0.450045005, 6.43650960949367, 7.11), - (0.45, 0.5, 6.43650960949367, 7.90), - (0.5, 0.550055006, 8.837429860654009, 10.7), - (0.5, 0.600240096, 8.837429860654009, 11.7), - (0.5, 0.650195059, 8.837429860654009, 12.7), - (0.5, 0.700280112, 8.837429860654009, 13.7), - (0.5, 0.750750751, 8.837429860654009, 14.6), - (0.55, 0.8, 11.749184207805905, 18.9), - (0.55, 0.850340136, 11.749184207805905, 20.1), - (0.55, 0.900900901, 11.749184207805905, 21.3), - (0.55, 0.950570342, 11.749184207805905, 22.4), - (0.55, 1.0, 11.749184207805905, 23.6), - (0.55, 1.050420168, 11.749184207805905, 24.8), - (0.6, 1.101321586, 15.248397765348098, 30.9), - (0.6, 1.149425287, 15.248397765348098, 32.3), - (0.6, 1.201923077, 15.248397765348098, 33.8), - (0.6, 1.25, 15.248397765348098, 35.1), - (0.65, 1.302083333, 19.386153942879744, 42.9), - (0.65, 1.351351351, 19.386153942879744, 44.6), - (0.65, 1.400560224, 19.386153942879744, 46.17327223), - (0.65, 1.449275362, 19.386153942879744, 47.77934338), - (0.65, 1.501501502, 19.386153942879744, 49.50132232), - (0.7, 1.552795031, 24.21353615, 59.4), - (0.7, 1.602564103, 24.21353615, 61.3), - (0.7, 1.650165017, 24.21353615, 63.1), - (0.7, 1.7, 24.21353615, 65.0), -] -SRAM_POINTS: list[VfPoint] = [VfPoint(*point) for point in _SRAM_POINTS] - -_HBM_POINTS = [ - ### This table has the following assumptions: - ### - Only DVFS the memory controller, not the PHY, I/O bus, and DRAM arrays. - ### - The power split between MC and PHY is 40%/60% at max BW/freq point (an empirical estimate). - # voltage_V, bandwidth_GBs, static_power_W, dynamic_power_W - (0.45, 0.00, 21.861016, 0.000000), # 0.000000 (scaled ref frequency) - (0.45, 81.029412, 21.861016, 0.796834), # 0.050000000 - (0.45, 162.058824, 21.861016, 1.593668), # 0.100000000 - (0.45, 243.088235, 21.861016, 2.390502), # 0.150000000 - (0.45, 324.117647, 21.861016, 3.187337), # 0.200000000 - (0.45, 405.147059, 21.861016, 3.984171), # 0.250000000 - (0.45, 486.176471, 21.861016, 4.781005), # 0.300000000 - (0.45, 567.205882, 21.861016, 5.577839), # 0.350000000 - (0.45, 648.235294, 21.861016, 6.374673), # 0.400000000 - (0.45, 729.264706, 21.861016, 7.171507), # 0.450000000 - (0.45, 810.294118, 21.861016, 7.968341), # 0.500000000 - (0.50, 891.323529, 22.478464, 9.124189), # 0.550000000 - (0.50, 972.352941, 22.478464, 9.953661), # 0.600000000 - (0.50, 1053.382353, 22.478464, 10.783133), # 0.650000000 - (0.50, 1134.411765, 22.478464, 11.612604), # 0.700000000 - (0.50, 1215.441176, 22.478464, 12.442076), # 0.750000000 - (0.55, 1296.470588, 23.191154, 13.848718), # 0.800000000 - (0.55, 1377.500000, 23.191154, 14.714263), # 0.850000000 - (0.55, 1458.529412, 23.191154, 15.579808), # 0.900000000 - (0.55, 1539.558824, 23.191154, 16.445353), # 0.950000000 - (0.55, 1620.588235, 23.191154, 17.310897), # 1.000000000 - (0.55, 1701.617647, 23.191154, 18.176442), # 1.050000000 - (0.60, 1782.647059, 23.956602, 19.911178), # 1.100000000 - (0.60, 1863.676471, 23.956602, 20.816232), # 1.150000000 - (0.60, 1900.000000, 23.956602, 21.221945), # 1.172413793 - (0.60, 1925.000000, 23.956602, 21.501181), # 1.187840290 - (0.60, 1950.000000, 23.956602, 21.780417), # 1.203266788 - (0.60, 1975.000000, 23.956602, 22.059654), # 1.218693285 - (0.60, 2000.000000, 23.956602, 22.338890), # 1.234119782 - (0.60, 2025.000000, 23.956602, 22.618126), # 1.249546279 - (0.65, 2050.000000, 24.778335, 23.983827), # 1.264972777 - (0.65, 2075.000000, 24.778335, 24.276313), # 1.280399274 - (0.65, 2100.000000, 24.778335, 24.568798), # 1.295825771 - (0.65, 2125.000000, 24.778335, 24.861284), # 1.311252269 - (0.65, 2150.000000, 24.778335, 25.153770), # 1.326678766 - (0.65, 2175.000000, 24.778335, 25.446255), # 1.342105263 - (0.65, 2200.000000, 24.778335, 25.738741), # 1.357531760 - (0.65, 2225.000000, 24.778335, 26.031227), # 1.372958258 - (0.65, 2240.000000, 24.778335, 26.206718), # 1.382214156 - (0.65, 2250.000000, 24.778335, 26.323712), # 1.388384755 - (0.65, 2265.000000, 24.778335, 26.499204), # 1.397640653 - (0.65, 2290.000000, 24.778335, 26.791690), # 1.413067151 - (0.65, 2315.000000, 24.778335, 27.084175), # 1.428493648 - (0.65, 2340.000000, 24.778335, 27.376661), # 1.443920145 - (0.65, 2365.000000, 24.778335, 27.669147), # 1.459346642 - (0.65, 2372.000000, 24.778335, 27.751043), # 1.463666062 - (0.65, 2397.000000, 24.778335, 28.043528), # 1.479092559 - (0.65, 2422.000000, 24.778335, 28.336014), # 1.494519056 - (0.70, 2447.000000, 25.660103, 30.029117), # 1.509945554 - (0.70, 2472.000000, 25.660103, 30.335913), # 1.525372051 - (0.70, 2497.000000, 25.660103, 30.642708), # 1.540798548 - (0.70, 2502.000000, 25.660103, 30.704067), # 1.543883848 - (0.70, 2527.000000, 25.660103, 31.010862), # 1.559310345 - (0.70, 2552.000000, 25.660103, 31.317657), # 1.574736842 - (0.70, 2577.000000, 25.660103, 31.624453), # 1.590163339 - (0.70, 2602.000000, 25.660103, 31.931248), # 1.605589837 - (0.70, 2627.000000, 25.660103, 32.238043), # 1.621016334 - (0.70, 2629.000000, 25.660103, 32.262587), # 1.622250454 - (0.70, 2654.000000, 25.660103, 32.569382), # 1.637676951 - (0.70, 2679.000000, 25.660103, 32.876177), # 1.653103448 - (0.70, 2704.000000, 25.660103, 33.182972), # 1.668529946 - (0.70, 2729.000000, 25.660103, 33.489768), # 1.683956443 - (0.70, 2754.000000, 25.660103, 33.796563), # 1.699382940 - (0.70, 2755.000000, 25.660103, 33.808835), # 1.700000000 -] -HBM_POINTS: list[VBWPoint] = [VBWPoint(*point) for point in _HBM_POINTS] - -_ICI_POINTS = [ - # voltage_V, bandwidth_GBs, static_power_W, dynamic_power_W - (0.45, 0.00, 5.208886, 0.000000), # 0.000000 (scaled ref frequency) - (0.45, 17.63, 5.208886, 0.249735), # 0.049987 - (0.45, 35.27, 5.208886, 0.499612), # 0.100003 - (0.45, 52.90, 5.208886, 0.749347), # 0.149991 - (0.45, 70.54, 5.208886, 0.999224), # 0.200007 - (0.45, 88.17, 5.208886, 1.248960), # 0.249994 - (0.45, 105.81, 5.208886, 1.498836), # 0.300010 - (0.45, 123.44, 5.208886, 1.748572), # 0.349997 - (0.45, 141.08, 5.208886, 1.998449), # 0.400013 - (0.45, 158.71, 5.208886, 2.248184), # 0.450001 - (0.45, 176.34, 5.208886, 2.497919), # 0.499988 - (0.50, 193.98, 5.356007, 2.859937), # 0.550004 - (0.50, 211.61, 5.356007, 3.119864), # 0.599992 - (0.50, 229.25, 5.356007, 3.379939), # 0.650008 - (0.50, 246.88, 5.356007, 3.639866), # 0.699995 - (0.50, 264.52, 5.356007, 3.899941), # 0.750011 - (0.55, 282.15, 5.525821, 4.340150), # 0.799998 - (0.55, 299.78, 5.525821, 4.611342), # 0.849986 - (0.55, 306.38, 5.525821, 4.712866), # 0.868699 - (0.55, 319.15, 5.525821, 4.909299), # 0.904907 - (0.55, 331.91, 5.525821, 5.105579), # 0.941086 - (0.55, 344.68, 5.525821, 5.302013), # 0.977294 - (0.55, 357.45, 5.525821, 5.498446), # 1.013501 - (0.55, 370.21, 5.525821, 5.694726), # 1.049681 - (0.60, 382.98, 5.708207, 6.159173), # 1.085888 - (0.60, 395.74, 5.708207, 6.364382), # 1.122067 - (0.60, 408.51, 5.708207, 6.569752), # 1.158275 - (0.60, 421.28, 5.708207, 6.775123), # 1.194483 - (0.60, 434.04, 5.708207, 6.980332), # 1.230662 - (0.65, 446.81, 5.904003, 7.525575), # 1.266870 - (0.65, 459.57, 5.904003, 7.740490), # 1.303049 - (0.65, 461.57, 5.904003, 7.774176), # 1.308720 - (0.65, 463.57, 5.904003, 7.807861), # 1.314390 - (0.65, 465.57, 5.904003, 7.841547), # 1.320061 - (0.65, 467.57, 5.904003, 7.875233), # 1.325732 - (0.65, 469.57, 5.904003, 7.908919), # 1.331403 - (0.65, 471.57, 5.904003, 7.942605), # 1.337073 - (0.65, 473.57, 5.904003, 7.976290), # 1.342744 - (0.65, 475.57, 5.904003, 8.009976), # 1.348415 - (0.65, 477.57, 5.904003, 8.043662), # 1.354085 - (0.65, 479.57, 5.904003, 8.077348), # 1.359756 - (0.65, 481.57, 5.904003, 8.111034), # 1.365427 - (0.65, 483.57, 5.904003, 8.144719), # 1.371098 - (0.65, 485.57, 5.904003, 8.178405), # 1.376768 - (0.65, 487.57, 5.904003, 8.212091), # 1.382439 - (0.65, 489.57, 5.904003, 8.245777), # 1.388110 - (0.65, 491.57, 5.904003, 8.279463), # 1.393781 - (0.65, 493.57, 5.904003, 8.313148), # 1.399451 - (0.65, 495.57, 5.904003, 8.346834), # 1.405122 - (0.65, 497.57, 5.904003, 8.380520), # 1.410793 - (0.65, 499.57, 5.904003, 8.414206), # 1.416463 - (0.65, 501.57, 5.904003, 8.447892), # 1.422134 - (0.65, 503.57, 5.904003, 8.481577), # 1.427805 - (0.65, 505.57, 5.904003, 8.515263), # 1.433476 - (0.65, 507.57, 5.904003, 8.548949), # 1.439146 - (0.65, 509.57, 5.904003, 8.582635), # 1.444817 - (0.65, 511.57, 5.904003, 8.616320), # 1.450488 - (0.65, 513.57, 5.904003, 8.650006), # 1.456159 - (0.65, 515.57, 5.904003, 8.683692), # 1.461829 - (0.65, 517.57, 5.904003, 8.717378), # 1.467500 - (0.65, 519.57, 5.904003, 8.751064), # 1.473171 - (0.65, 521.57, 5.904003, 8.784749), # 1.478842 - (0.65, 523.57, 5.904003, 8.818435), # 1.484512 - (0.65, 525.57, 5.904003, 8.852121), # 1.490183 - (0.65, 527.57, 5.904003, 8.885807), # 1.495854 - (0.70, 529.57, 6.114105, 9.354544), # 1.501524 - (0.70, 531.57, 6.114105, 9.389873), # 1.507195 - (0.70, 533.57, 6.114105, 9.425201), # 1.512866 - (0.70, 535.57, 6.114105, 9.460530), # 1.518537 - (0.70, 537.57, 6.114105, 9.495859), # 1.524207 - (0.70, 539.57, 6.114105, 9.531188), # 1.529878 - (0.70, 541.57, 6.114105, 9.566517), # 1.535549 - (0.70, 543.57, 6.114105, 9.601846), # 1.541220 - (0.70, 545.57, 6.114105, 9.637174), # 1.546890 - (0.70, 547.57, 6.114105, 9.672503), # 1.552561 - (0.70, 549.57, 6.114105, 9.707832), # 1.558232 - (0.70, 551.57, 6.114105, 9.743161), # 1.563902 - (0.70, 553.57, 6.114105, 9.778490), # 1.569573 - (0.70, 555.57, 6.114105, 9.813819), # 1.575244 - (0.70, 557.57, 6.114105, 9.849147), # 1.580915 - (0.70, 559.57, 6.114105, 9.884476), # 1.586585 - (0.70, 561.57, 6.114105, 9.919805), # 1.592256 - (0.70, 563.57, 6.114105, 9.955134), # 1.597927 - (0.70, 565.57, 6.114105, 9.990463), # 1.603598 - (0.70, 567.57, 6.114105, 10.025792), # 1.609268 - (0.70, 569.57, 6.114105, 10.061120), # 1.614939 - (0.70, 571.57, 6.114105, 10.096449), # 1.620610 - (0.70, 573.57, 6.114105, 10.131778), # 1.626281 - (0.70, 575.57, 6.114105, 10.167107), # 1.631951 - (0.70, 577.57, 6.114105, 10.202436), # 1.637622 - (0.70, 579.57, 6.114105, 10.237764), # 1.643293 - (0.70, 581.57, 6.114105, 10.273093), # 1.648963 - (0.70, 583.57, 6.114105, 10.308422), # 1.654634 - (0.70, 585.57, 6.114105, 10.343751), # 1.660305 - (0.70, 587.57, 6.114105, 10.379080), # 1.665976 - (0.70, 589.57, 6.114105, 10.414409), # 1.671646 - (0.70, 591.57, 6.114105, 10.449737), # 1.677317 - (0.70, 593.57, 6.114105, 10.485066), # 1.682988 - (0.70, 595.57, 6.114105, 10.520395), # 1.688659 - (0.70, 597.57, 6.114105, 10.555724), # 1.694329 - (0.70, 599.57, 6.114105, 10.591053), # 1.700000 -] -ICI_POINTS: list[VBWPoint] = [VBWPoint(*point) for point in _ICI_POINTS] - -_DVFS_VOLTAGE_REGULATOR_OVERHEAD_TABLE = [ - ### (scaling time in ns, activity factor, voltage in V, power efficiency (percentage)) - - # scaling_time_ns = 2 - (2, 0.0, 0.45, 63.95031056), (2, 0.0, 0.5, 66.08660107), (2, 0.0, 0.55, 68.22289157), - (2, 0.0, 0.6, 69.22986684), (2, 0.0, 0.65, 70.23684211), (2, 0.0, 0.7, 72.12129462), - (2, 0.1, 0.45, 64.83850932), (2, 0.1, 0.5, 67.20841129), (2, 0.1, 0.55, 69.57831325), - (2, 0.1, 0.6, 70.57863031), (2, 0.1, 0.65, 71.57894737), (2, 0.1, 0.7, 73.01935874), - (2, 0.2, 0.45, 66.17080745), (2, 0.2, 0.5, 68.5522712), (2, 0.2, 0.55, 70.93373494), - (2, 0.2, 0.6, 72.151078), (2, 0.2, 0.65, 73.36842105), (2, 0.2, 0.7, 74.82214156), - (2, 0.3, 0.45, 67.50310559), (2, 0.3, 0.5, 69.89613111), (2, 0.3, 0.55, 72.28915663), - (2, 0.3, 0.6, 73.05247305), (2, 0.3, 0.65, 73.81578947), (2, 0.3, 0.7, 75.27283726), - (2, 0.4, 0.45, 68.39130435), (2, 0.4, 0.5, 70.79203771), (2, 0.4, 0.55, 73.19277108), - (2, 0.4, 0.6, 73.9516487), (2, 0.4, 0.65, 74.71052632), (2, 0.4, 0.7, 76.06072293), - (2, 0.5, 0.45, 69.27950311), (2, 0.5, 0.5, 71.68794432), (2, 0.5, 0.55, 74.09638554), - (2, 0.5, 0.6, 74.85082435), (2, 0.5, 0.65, 75.60526316), (2, 0.5, 0.7, 76.84860859), - (2, 0.6, 0.45, 70.16770186), (2, 0.6, 0.5, 72.19981479), (2, 0.6, 0.55, 74.23192771), - (2, 0.6, 0.6, 74.98570069), (2, 0.6, 0.65, 75.73947368), (2, 0.6, 0.7, 77.0746219), - (2, 0.7, 0.45, 71.05590062), (2, 0.7, 0.5, 72.71168525), (2, 0.7, 0.55, 74.36746988), - (2, 0.7, 0.6, 75.12057705), (2, 0.7, 0.65, 75.87368421), (2, 0.7, 0.7, 77.30063521), - (2, 0.8, 0.45, 71.20393375), (2, 0.8, 0.5, 72.8911235), (2, 0.8, 0.55, 74.57831325), - (2, 0.8, 0.6, 75.33038469), (2, 0.8, 0.65, 76.08245614), (2, 0.8, 0.7, 77.45042347), - (2, 0.9, 0.45, 71.35196687), (2, 0.9, 0.5, 73.07056175), (2, 0.9, 0.55, 74.78915663), - (2, 0.9, 0.6, 75.54019235), (2, 0.9, 0.65, 76.29122807), (2, 0.9, 0.7, 77.60021174), - (2, 1.0, 0.45, 71.5), (2, 1.0, 0.5, 73.25), (2, 1.0, 0.55, 75.0), - (2, 1.0, 0.6, 75.75), (2, 1.0, 0.65, 76.5), (2, 1.0, 0.7, 77.75), - - # scaling_time_ns = 20 - (20, 0.0, 0.45, 72.0), (20, 0.0, 0.5, 73.75), (20, 0.0, 0.55, 75.5), - (20, 0.0, 0.6, 77.0), (20, 0.0, 0.65, 78.5), (20, 0.0, 0.7, 80.0), - (20, 0.1, 0.45, 73.0), (20, 0.1, 0.5, 75.0), (20, 0.1, 0.55, 77.0), - (20, 0.1, 0.6, 78.5), (20, 0.1, 0.65, 80.0), (20, 0.1, 0.7, 81.0), - (20, 0.2, 0.45, 74.5), (20, 0.2, 0.5, 76.5), (20, 0.2, 0.55, 78.5), - (20, 0.2, 0.6, 80.25), (20, 0.2, 0.65, 82.0), (20, 0.2, 0.7, 83.0), - (20, 0.3, 0.45, 76.0), (20, 0.3, 0.5, 78.0), (20, 0.3, 0.55, 80.0), - (20, 0.3, 0.6, 81.25), (20, 0.3, 0.65, 82.5), (20, 0.3, 0.7, 83.5), - (20, 0.4, 0.45, 77.0), (20, 0.4, 0.5, 79.0), (20, 0.4, 0.55, 81.0), - (20, 0.4, 0.6, 82.25), (20, 0.4, 0.65, 83.5), (20, 0.4, 0.7, 84.375), - (20, 0.5, 0.45, 78.0), (20, 0.5, 0.5, 80.0), (20, 0.5, 0.55, 82.0), - (20, 0.5, 0.6, 83.25), (20, 0.5, 0.65, 84.5), (20, 0.5, 0.7, 85.25), - (20, 0.6, 0.45, 79.0), (20, 0.6, 0.5, 80.575), (20, 0.6, 0.55, 82.15), - (20, 0.6, 0.6, 83.4), (20, 0.6, 0.65, 84.65), (20, 0.6, 0.7, 85.5), - (20, 0.7, 0.45, 80.0), (20, 0.7, 0.5, 81.15), (20, 0.7, 0.55, 82.3), - (20, 0.7, 0.6, 83.55), (20, 0.7, 0.65, 84.8), (20, 0.7, 0.7, 85.75), - (20, 0.8, 0.45, 80.16666667), (20, 0.8, 0.5, 81.35), (20, 0.8, 0.55, 82.53333333), - (20, 0.8, 0.6, 83.78333333), (20, 0.8, 0.65, 85.03333333), (20, 0.8, 0.7, 85.91666667), - (20, 0.9, 0.45, 80.33333333), (20, 0.9, 0.5, 81.55), (20, 0.9, 0.55, 82.76666667), - (20, 0.9, 0.6, 84.01666667), (20, 0.9, 0.65, 85.26666667), (20, 0.9, 0.7, 86.08333334), - (20, 1.0, 0.45, 80.5), (20, 1.0, 0.5, 81.75), (20, 1.0, 0.55, 83.0), - (20, 1.0, 0.6, 84.25), (20, 1.0, 0.65, 85.5), (20, 1.0, 0.7, 86.25), - - # scaling_time_ns = 200 - (200, 0.0, 0.45, 76.11428571), (200, 0.0, 0.5, 77.62641997), (200, 0.0, 0.55, 79.13855422), - (200, 0.0, 0.6, 79.96693793), (200, 0.0, 0.65, 80.79532164), (200, 0.0, 0.7, 82.31863783), - (200, 0.1, 0.45, 77.17142857), (200, 0.1, 0.5, 78.94113597), (200, 0.1, 0.55, 80.71084337), - (200, 0.1, 0.6, 81.52501233), (200, 0.1, 0.65, 82.33918129), (200, 0.1, 0.7, 83.34775157), - (200, 0.2, 0.45, 78.75714286), (200, 0.2, 0.5, 80.5201377), (200, 0.2, 0.55, 82.28313253), - (200, 0.2, 0.6, 83.34039668), (200, 0.2, 0.65, 84.39766082), (200, 0.2, 0.7, 85.40572696), - (200, 0.3, 0.45, 80.34285714), (200, 0.3, 0.5, 82.09913942), (200, 0.3, 0.55, 83.85542169), - (200, 0.3, 0.6, 84.3838512), (200, 0.3, 0.65, 84.9122807), (200, 0.3, 0.7, 85.92022081), - (200, 0.4, 0.45, 81.4), (200, 0.4, 0.5, 83.15180723), (200, 0.4, 0.55, 84.90361446), - (200, 0.4, 0.6, 85.42256747), (200, 0.4, 0.65, 85.94152047), (200, 0.4, 0.7, 86.82061656), - (200, 0.5, 0.45, 82.45714286), (200, 0.5, 0.5, 84.20447505), (200, 0.5, 0.55, 85.95180723), - (200, 0.5, 0.6, 86.46128373), (200, 0.5, 0.65, 86.97076023), (200, 0.5, 0.7, 87.7210123), - (200, 0.6, 0.45, 83.51428571), (200, 0.6, 0.5, 84.81166093), (200, 0.6, 0.55, 86.10903614), - (200, 0.6, 0.6, 86.61709117), (200, 0.6, 0.65, 87.1251462), (200, 0.6, 0.7, 87.97823402), - (200, 0.7, 0.45, 84.57142857), (200, 0.7, 0.5, 85.41884682), (200, 0.7, 0.55, 86.26626506), - (200, 0.7, 0.6, 86.77289861), (200, 0.7, 0.65, 87.27953216), (200, 0.7, 0.7, 88.23545574), - (200, 0.8, 0.45, 84.74761905), (200, 0.8, 0.5, 85.62923121), (200, 0.8, 0.55, 86.51084337), - (200, 0.8, 0.6, 87.01526574), (200, 0.8, 0.65, 87.51968811), (200, 0.8, 0.7, 88.40697049), - (200, 0.9, 0.45, 84.92380952), (200, 0.9, 0.5, 85.83961561), (200, 0.9, 0.55, 86.75542169), - (200, 0.9, 0.6, 87.25763287), (200, 0.9, 0.65, 87.75984405), (200, 0.9, 0.7, 88.57848525), - (200, 1.0, 0.45, 85.1), (200, 1.0, 0.5, 86.05), (200, 1.0, 0.55, 87.0), - (200, 1.0, 0.6, 87.5), (200, 1.0, 0.65, 88.0), (200, 1.0, 0.7, 88.75), -] -DVFS_VOLTAGE_REGULATOR_OVERHEAD_TABLE: list[PowerEfficiencyPoint] = [ - PowerEfficiencyPoint(*point) for point in _DVFS_VOLTAGE_REGULATOR_OVERHEAD_TABLE -] - -_FIXED_VOLTAGE_REGULATOR_OVERHEAD_TABLE = [ - ### (scaling time in ns (unused), activity factor, voltage in V (always 0.7), power efficiency (percentage)) - (0, 0.0, 0.7, 67.0), - (0, 0.1, 0.7, 85.0), - (0, 0.2, 0.7, 86.0), - (0, 0.3, 0.7, 86.5), - (0, 0.4, 0.7, 87.0), - (0, 0.5, 0.7, 87.5), - (0, 0.6, 0.7, 88.0), - (0, 0.7, 0.7, 88.5), - (0, 0.8, 0.7, 89.0), - (0, 0.9, 0.7, 89.5), - (0, 1.0, 0.7, 90.0), -] -FIXED_VOLTAGE_REGULATOR_OVERHEAD_TABLE: list[PowerEfficiencyPoint] = [ - PowerEfficiencyPoint(*point) for point in _FIXED_VOLTAGE_REGULATOR_OVERHEAD_TABLE -] - - -# ========================= -# Helpers -# ========================= - -def _group_by_voltage(points: list[VfPoint]) -> Groups: - """Group (v, x, s, d) points by v (voltage), and sort each group by x (can be frequency or bandwidth).""" - groups: Groups = {} - for v, x, s, d in points: - groups.setdefault(v, []).append(Row(x, s, d)) - for v in groups: - groups[v].sort(key=lambda t: t.x) - return groups - - -def _choose_voltage_by_request_or_range( - groups: Groups, - target_x: float, - requested_v: float | None = None, -) -> tuple[float | None, list[Row] | None]: - """ - Choose voltage rows using: - - If requested_v is not None: pick voltage closest to requested_v. - - Else: pick segment whose [min_x, max_x] best matches target_x. - Returns (v, rows). - """ - if requested_v is not None and len(groups) > 0: - best_v, best_rows = min(groups.items(), key=lambda item: abs(item[0] - requested_v)) - return best_v, best_rows - - # No requested voltage: infer from x-range - best_v = None - best_rows = None - best_dist: float | None = None - for v, rows in groups.items(): - x_min = rows[0].x - x_max = rows[-1].x - if x_min <= target_x <= x_max: - dist = 0.0 - elif target_x < x_min: - dist = x_min - target_x - else: - dist = target_x - x_max - if ( - best_dist is None - or dist < best_dist - or (dist == best_dist and (best_v is None or v < best_v)) - ): - best_dist = dist - best_v = v - best_rows = rows - return best_v, best_rows - - -def _nearest_point(rows: list[Row], target_x: float) -> Row: - """Return (x_ref, s_ref, d_ref) where x_ref is closest to target_x.""" - best = min(rows, key=lambda row: abs(row.x - target_x)) - return best - - -def _scale_dynamic(base_dyn_W: float, base_x: float, new_x: float) -> float: - """Scale dynamic power linearly with x at fixed voltage.""" - assert base_dyn_W >= 0.0 - assert base_x >= 0.0 - assert new_x >= 0.0 - if base_x == 0.0: - return base_dyn_W - else: - return base_dyn_W * (new_x / base_x) - - -def _baseline_freq_ghz(points: list[VfPoint]) -> float: - """Max frequency_GHz from (v, f, s, d) table.""" - return max(p.frequency_GHz for p in points) - - -@lru_cache(maxsize=None) -def _baseline_bw_hbm() -> float: - """Max bandwidth_GBs from HBM table.""" - return max(p.bandwidth_GBs for p in HBM_POINTS) - - -@lru_cache(maxsize=None) -def _baseline_bw_ici() -> float: - """Max bandwidth_GBs from ICI table.""" - return max(p.bandwidth_GBs for p in ICI_POINTS) - - -@lru_cache(maxsize=None) -def _max_perf_point(component: str) -> VfPoint | VBWPoint: - """Return the (v, x, s, d) row with max x (frequency or bandwidth) for component.""" - comp = str(component).strip().lower() - if comp == "sa": - return max(SA_POINTS, key=lambda p: p.frequency_GHz) - elif comp == "vu": - return max(VU_POINTS, key=lambda p: p.frequency_GHz) - elif comp == "sram": - return max(SRAM_POINTS, key=lambda p: p.frequency_GHz) - elif comp == "hbm": - return max(HBM_POINTS, key=lambda p: p.bandwidth_GBs) - elif comp == "ici": - return max(ICI_POINTS, key=lambda p: p.bandwidth_GBs) - else: - raise ValueError(f"Unsupported component: {component!r}") - - -@lru_cache(maxsize=None) -def _min_power_point(component: str) -> VfPoint | VBWPoint: - """Return the (v, x, s, d) row with min power (min voltage and frequency) for component.""" - comp = str(component).strip().lower() - if comp == "sa": - return min(SA_POINTS, key=lambda p: (p.voltage_V, p.frequency_GHz)) - elif comp == "vu": - return min(VU_POINTS, key=lambda p: (p.voltage_V, p.frequency_GHz)) - elif comp == "sram": - return min(SRAM_POINTS, key=lambda p: (p.voltage_V, p.frequency_GHz)) - elif comp == "hbm": - return min(HBM_POINTS, key=lambda p: (p.voltage_V, p.bandwidth_GBs)) - elif comp == "ici": - return min(ICI_POINTS, key=lambda p: (p.voltage_V, p.bandwidth_GBs)) - else: - raise ValueError(f"Unsupported component: {component!r}") - - -# ========================= -# Main API -# ========================= - -@lru_cache(maxsize=None) -def get_power_from_dvfs(component: str, dvfs: ComponentDVFSConfig) -> tuple[float, float]: - """ - Compute (dynamic_power_W, static_power_W) for a component from DVFSConfig. - """ - comp = str(component).strip().lower() - v_req = dvfs.voltage_V - f_req_GHz = dvfs.frequency_GHz - - # default No DVFS policy: use max performance point (peak voltage and freq/bandwidth) - if dvfs.policy == DVFSPolicy.NONE or ( - (v_req is None or v_req <= 0.0) - and (f_req_GHz is None or f_req_GHz <= 0.0) - ): - max_point = _max_perf_point(comp) - return max_point.dynamic_power_W, max_point.static_power_W - - # SA / VU / SRAM: use frequency in GHz - if comp in ("sa", "vu", "sram"): - points = SA_POINTS if comp == "sa" else VU_POINTS if comp == "vu" else SRAM_POINTS - base_freq_ghz = _baseline_freq_ghz(points) - - if f_req_GHz is None or f_req_GHz <= 0.0: - f_req_GHz = base_freq_ghz - - # first find the nearest voltage corner - groups = _group_by_voltage(points) - _, rows = _choose_voltage_by_request_or_range(groups, f_req_GHz, v_req) - assert rows is not None, f"No voltage rows available for {comp} selection." - x_ref, s_ref, d_ref = _nearest_point(rows, f_req_GHz) - - # then extrapolate the dynamic power from the existing corners using the given target frequency (Dynamic Power ~ freq) - dyn = _scale_dynamic(d_ref, x_ref, f_req_GHz) - static = s_ref - return dyn, static - - # HBM: map DVFS freq proxy to bandwidth - if comp in ("hbm", "ici"): - base_bw = _baseline_bw_hbm() if comp == "hbm" else _baseline_bw_ici() - base_freq_ghz = 1.7 - points = HBM_POINTS if comp == "hbm" else ICI_POINTS - - if f_req_GHz is not None and f_req_GHz > 0.0: - bw_target = base_bw * (f_req_GHz / base_freq_ghz) - else: - bw_target = base_bw - - # group by voltage: {v: [(bw, s, d), ...]} - groups: Groups = {} - for v, bw, st_p, dyn_p in points: - groups.setdefault(v, []).append(Row(bw, st_p, dyn_p)) - for v in groups: - groups[v].sort(key=lambda t: t.x) - - _, rows = _choose_voltage_by_request_or_range(groups, bw_target, v_req) - assert rows is not None, f"No voltage rows available for {comp} selection." - bw_ref, s_ref, d_ref = _nearest_point(rows, bw_target) - - dyn = _scale_dynamic(d_ref, bw_ref, bw_target) - static = s_ref - return dyn, static - - raise ValueError(f"Unsupported component: {component!r}") - - -@lru_cache(maxsize=None) -def get_all_dvfs_configs_for_component( - component: str, - policy: DVFSPolicy = DVFSPolicy.IDEAL, -) -> list[ComponentDVFSConfig]: - """ - Return all possible DVFS configurations for a given component. - """ - comp = str(component).strip().lower() - configs: list[ComponentDVFSConfig] = [] - - if comp in ("sa", "vu", "sram"): - points = SA_POINTS if comp == "sa" else VU_POINTS if comp == "vu" else SRAM_POINTS - for v, f, s, d in points: - configs.append( - ComponentDVFSConfig( - policy=policy, - voltage_V=v, - frequency_GHz=f, - ) - ) - return configs - - if comp in ("hbm", "ici"): - points = HBM_POINTS if comp == "hbm" else ICI_POINTS - for v, bw, s, d in points: - # Map bandwidth back to frequency proxy - base_bw = _baseline_bw_hbm() if comp == "hbm" else _baseline_bw_ici() - base_freq_ghz = 1.7 - f = base_freq_ghz * (bw / base_bw) - configs.append( - ComponentDVFSConfig( - policy=policy, - voltage_V=v, - frequency_GHz=f, - ) - ) - return configs - - raise ValueError(f"Unsupported component: {component!r}") - - -def get_all_dvfs_configs_for_op( - op: Operator.Operator, - policy: DVFSPolicy = DVFSPolicy.IDEAL, - perf_degrade_threshold: float = 0, - total_exe_time_ns: float | None = None, -) -> list[dict[str, ComponentDVFSConfig]]: - """ - Return all possible DVFS configurations for each component used by the given operator. - Sweeps through all frequencies for each component and pick the lowest voltage accordingly. - If the component is unused, just set it to the lowest power state. - - policy: DVFSPolicy to use for filling out ComponentDVFSConfig. - - perf_degrade_threshold: float, maximum allowed performance degradation for the entire workload. - """ - logging.set_verbosity(logging.INFO) - - configs: list[dict[str, ComponentDVFSConfig]] = [] - comp_names = ("sa", "vu", "sram", "hbm", "ici") - - def _get_exe_time_ns(comp_name: str) -> float: - if comp_name == "sa": - return op.stats.sa_time_ns - elif comp_name == "vu": - return op.stats.vu_time_ns - elif comp_name == "sram": - return op.stats.vmem_time_ns - elif comp_name == "hbm": - return op.stats.memory_time_ns - elif comp_name == "ici": - return op.stats.ici_time_ns - else: - raise ValueError(f"Unsupported component name: {comp_name!r}") - - # 1. Compute max allowed execution time - # max_allowed_exe_time_ns is derived from the perf_degrade_threshold and the program's original execution time. - # If total_exe_time_ns is not provided, use op.stats.execution_time_ns as the baseline. - if not total_exe_time_ns: - total_exe_time_ns = op.stats.execution_time_ns - max_slack_time_ns = total_exe_time_ns * perf_degrade_threshold - max_allowed_exe_time_ns = op.stats.execution_time_ns + max_slack_time_ns / op.stats.count - - logging.info(f"op_name: {op.name}, original_exe_time_ns: {op.stats.execution_time_ns}, max_allowed_exe_time_ns: {max_allowed_exe_time_ns}, max_op_perf_degrade: {(max_allowed_exe_time_ns / op.stats.execution_time_ns - 1) * 100:.4f}%") - - # 2. For each component, generate a list of ComponentDVFSConfigs that - # satisfy op.stats.execution_time_ns <= component exe time <= max_allowed_exe_time_ns. - # If the component is unused, just include the min power point config. - comp_configs_list: list[list[ComponentDVFSConfig]] = [] # list of ComponentDVFSConfigs for each component - for comp_name in comp_names: - exe_time_ns = _get_exe_time_ns(comp_name) - # we can always slow down the component without perf degradation if it is not the bottleneck - slowdown_factor_max = exe_time_ns / op.stats.execution_time_ns - # must have component exe time <= max_allowed_exe_time_ns - slowdown_factor_min = exe_time_ns / max_allowed_exe_time_ns - max_freq_GHz_required = min( - ( - _baseline_freq_ghz(SA_POINTS) * slowdown_factor_max - if comp_name in ("sa", "vu", "sram") - else 1.7 * slowdown_factor_max # for HBM and ICI - ) + 0.05, # step size is 0.05 GHz - 1.7, - ) - min_freq_GHz_allowed = max( - ( - _baseline_freq_ghz(SA_POINTS) * slowdown_factor_min - if comp_name in ("sa", "vu", "sram") - else 1.7 * slowdown_factor_min # for HBM and ICI - ) - 0.05, # step size is 0.05 GHz - 0, - ) - - # print(f"Component: {comp_name}, Max freq required: {max_freq_GHz_required}, Min freq allowed: {min_freq_GHz_allowed}") - - if exe_time_ns > 0: - all_cfgs = get_all_dvfs_configs_for_component(comp_name, policy) - all_cfgs = [ - cfg for cfg in all_cfgs - if cfg.frequency_GHz and max_freq_GHz_required >= cfg.frequency_GHz >= min_freq_GHz_allowed - ] - # print(f"Component: {comp_name}, Possible DVFS configs count: {len(all_cfgs)}") - comp_configs_list.append(all_cfgs) - else: - min_power_point = _min_power_point(comp_name) - comp_configs_list.append([ - ComponentDVFSConfig( - policy=policy, - voltage_V=min_power_point.voltage_V, - frequency_GHz=0.05, # assume lowest freq is 0.05 GHz for now - ) - ]) - - for combo in itertools.product(*comp_configs_list): # combo: (sa, vu, sram, ici, hbm) ComponentDVFSConfig - config_dict = {comp_name: config for comp_name, config in zip(comp_names, combo)} - configs.append(config_dict) - - logging.info(f"Total DVFS configurations generated for op {op.name}: {len(configs)}") - - return configs +"""Backward-compatible facade — re-exports from backend.""" + +from neusim.npusim.backend.dvfs_power_getter import ( # noqa: F401 + VfPoint, + VBWPoint, + PowerEfficiencyPoint, + Row, + Groups, + SA_POINTS, + VU_POINTS, + SRAM_POINTS, + HBM_POINTS, + ICI_POINTS, + DVFS_VOLTAGE_REGULATOR_OVERHEAD_TABLE, + FIXED_VOLTAGE_REGULATOR_OVERHEAD_TABLE, + get_power_from_dvfs, + get_all_dvfs_configs_for_component, + get_all_dvfs_configs_for_op, +) diff --git a/neusim/npusim/frontend/power_analysis_lib.py b/neusim/npusim/frontend/power_analysis_lib.py index 356bf8a..882790e 100644 --- a/neusim/npusim/frontend/power_analysis_lib.py +++ b/neusim/npusim/frontend/power_analysis_lib.py @@ -1,1227 +1,78 @@ -from copy import deepcopy -from enum import Enum -from math import ceil -import select -from typing import Any +"""Power analysis orchestration facade. -from absl import flags, logging -import numpy as np -import json -import csv -import os -from collections import defaultdict -from pydantic import BaseModel +Re-exports modeling functions from backend and configs, and keeps +orchestration helpers that tie everything together. +""" + +from absl import logging -from neusim.npusim.frontend.dvfs_policy_lib import get_dvfs_config import neusim.npusim.frontend.Operator as Operator from neusim.npusim.frontend.Operator import DVFSPolicy, DVFSConfig, ComponentDVFSConfig from neusim.configs.chips.ChipConfig import ChipConfig from neusim.configs.models.ModelConfig import ModelConfig -from neusim.npusim.frontend.dvfs_power_getter import ( + +# Re-export PowerGatingConfig from configs +from neusim.configs.power_gating.PowerGatingConfig import ( # noqa: F401 + PowerGatingConfig, + get_power_gating_config, +) + +# Re-export DVFS policy from backend +from neusim.npusim.backend.dvfs_policy_lib import get_dvfs_config # noqa: F401 + +# Re-export DVFS power getter names from backend +from neusim.npusim.backend.dvfs_power_getter import ( # noqa: F401 get_all_dvfs_configs_for_op, get_power_from_dvfs, DVFS_VOLTAGE_REGULATOR_OVERHEAD_TABLE, FIXED_VOLTAGE_REGULATOR_OVERHEAD_TABLE, ) -class PowerGatingConfig(BaseModel): - """ - Power-gating configuration. - """ - - class TemporalGranularity(Enum): - INSTRUCTION = 1 - OPERATOR = 2 - APPLICATION = 3 - - class SASpatialGranularity(Enum): - PE = 1 - PARTITION = 2 - COMPONENT = 3 - - class VUSpatialGranularity(Enum): - ALU = 1 - PARTITION = 2 - COMPONENT = 3 - - class VmemSpatialGranularity(Enum): - REGISTER_SIZE = 1 - PARTITION = 2 - - class ICISpatialGranularity(Enum): - LINK = 1 - COMPONENT = 2 - - class VoltageGranularity(Enum): - TWO_LEVEL = 1 # only on/off - MULTI_LEVEL = 2 # on/off + sleep modes - - class PowerGatingPolicy(Enum): - HW = 1 # HW-managed (auto mode) - SW = 2 # SW-managed - - name: str = "PowerGatingConfig" - SA_PG_enabled: bool = False - SA_PG_policy: PowerGatingPolicy = PowerGatingPolicy.HW - SA_temporal_granularity: TemporalGranularity = TemporalGranularity.INSTRUCTION - SA_spatial_granularity: SASpatialGranularity = SASpatialGranularity.COMPONENT - sa_partition_shapes: list[int] = [128, 128] - """partition shapes in number of PEs (128*128 by default)""" - sa_power_level_factors: list[float] = [1.0, 0.0] - """power consumption (0~1) at each voltage level (from highest power to lowest power)""" - sa_pe_pg_delay_cycles: int = 1 - """Delay in cycles of power gating and waking up a single PE.""" - sa_pg_delay_cycles: int = 10 - """Delay in cycles of power gating and waking up the entire SA.""" - - VU_PG_enabled: bool = False - VU_PG_policy: PowerGatingPolicy = PowerGatingPolicy.HW - VU_temporal_granularity: TemporalGranularity = TemporalGranularity.INSTRUCTION - VU_spatial_granularity: VUSpatialGranularity = VUSpatialGranularity.COMPONENT - vu_partition_shapes: list[int] = [8, 128] - """partition shapes in number of ALUs (8*128 by default)""" - vu_power_level_factors: list[float] = [1.0, 0.0] - """power consumption (0~1) at each voltage level (from highest power to lowest power)""" - vu_pg_delay_cycles: int = 2 - """Delay in cycles of power gating and waking up a VU.""" - - vmem_PG_enabled: bool = False - vmem_PG_policy: PowerGatingPolicy = PowerGatingPolicy.HW - vmem_temporal_granularity: TemporalGranularity = TemporalGranularity.INSTRUCTION - vmem_spatial_granularity: VmemSpatialGranularity = VmemSpatialGranularity.PARTITION - vmem_voltage_granularity: VoltageGranularity = VoltageGranularity.TWO_LEVEL - vmem_power_level_factors: list[float] = [1.0, 0.0] - """power consumption (0~1) at each voltage level (from highest power to lowest power)""" - vmem_partition_size_bytes: int = 2 * 1024 * 1024 - """partition size in bytes (2MB by default if spatial granularity is PARTITION)""" - vmem_partition_pg_delay_cycles: int = 10 - """Delay in cycles of power gating and waking up a vmem partition.""" - vmem_HW_drowsy_period_cycles: int = 2000 - """Period at which all vmem partitions are put into sleep.""" - - ici_PG_enabled: bool = False - ici_PG_policy: PowerGatingPolicy = PowerGatingPolicy.HW - ici_temporal_granularity: TemporalGranularity = TemporalGranularity.INSTRUCTION - ici_spatial_granularity: ICISpatialGranularity = ICISpatialGranularity.COMPONENT - ici_power_level_factors: list[float] = [1.0, 0.0] - """power consumption (0~1) at each voltage level (from highest power to lowest power)""" - ici_pg_delay_cycles: int = 10 - """Delay in cycles of power gating and waking up an ICI.""" - - hbm_PG_enabled: bool = False - hbm_PG_policy: PowerGatingPolicy = PowerGatingPolicy.HW - hbm_power_level_factors: list[float] = [1.0, 0.1] # 0.1 takes into account the auto refresh cost - """power consumption (0~1) at each voltage level (from highest power to lowest power)""" - hbm_refresh_interval_ns: int = 3900 - hbm_refresh_delay_ns: int = 400 # for 12H device - hbm_pg_delay_cycles: int = 60 - - other_PG_enabled: bool = False - other_PG_policy: PowerGatingPolicy = PowerGatingPolicy.HW - other_power_level_factors: list[float] = [1.0, 0.0] - """power consumption (0~1) at each voltage level (from highest power to lowest power)""" - - -def get_power_gating_config(pg_config_name: str) -> PowerGatingConfig: - """ - 'disabled', 'NoPG': no power gating. \n - 'ideal_inst_component': ideal power gating with instruction-level temporal granularity and component-level spatial granularity. \n - 'ideal_op_component': ideal power gating with operator-level temporal granularity and component-level spatial granularity. \n - 'ideal_inst_PE_ALU', 'Ideal': ideal power gating with instruction-level temporal granularity and PE/ALU-level spatial granularity. This should result in the most power savings. \n - 'Full': Same as 'Ideal' but with non-zero power-gating factor (power_level_factors) and delay cycles. \n - '\\\\_vary_Vth_\\_\\': vary Vth_low and Vth_sram for sensitivity analysis. The values are the percentage over Vdd. \n - '\\\\_vary_PG_delay_\\': vary PG delay for sensitivity analysis. The value is specified as the ratio over base config. \n - """ - pg_config = PowerGatingConfig - if pg_config_name in ["disabled", "NoPG"]: - pg_config = PowerGatingConfig(name="NoPG") - elif pg_config_name == "ideal_inst_component": - pg_config = PowerGatingConfig( - name="ideal_inst_component", - SA_PG_enabled=True, - SA_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - SA_spatial_granularity=PowerGatingConfig.SASpatialGranularity.COMPONENT, - sa_partition_shapes=[128, 128], - VU_PG_enabled=True, - VU_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - VU_spatial_granularity=PowerGatingConfig.VUSpatialGranularity.COMPONENT, - vmem_PG_enabled=True, - vmem_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - vmem_spatial_granularity=PowerGatingConfig.VmemSpatialGranularity.PARTITION, - vmem_voltage_granularity=PowerGatingConfig.VoltageGranularity.TWO_LEVEL, - vmem_power_level_factors=[1.0, 0.0], - vmem_partition_size_bytes=2 * 1024 * 1024, - ici_PG_enabled=True, - ici_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - ici_spatial_granularity=PowerGatingConfig.ICISpatialGranularity.COMPONENT, - ) - elif pg_config_name == "ideal_op_component": - pg_config = PowerGatingConfig( - name="ideal_op_component", - SA_PG_enabled=True, - SA_temporal_granularity=PowerGatingConfig.TemporalGranularity.OPERATOR, - SA_spatial_granularity=PowerGatingConfig.SASpatialGranularity.COMPONENT, - sa_partition_shapes=[128, 128], - VU_PG_enabled=True, - VU_temporal_granularity=PowerGatingConfig.TemporalGranularity.OPERATOR, - VU_spatial_granularity=PowerGatingConfig.VUSpatialGranularity.COMPONENT, - vmem_PG_enabled=True, - vmem_temporal_granularity=PowerGatingConfig.TemporalGranularity.OPERATOR, - vmem_spatial_granularity=PowerGatingConfig.VmemSpatialGranularity.PARTITION, - vmem_voltage_granularity=PowerGatingConfig.VoltageGranularity.TWO_LEVEL, - vmem_power_level_factors=[1.0, 0.0], - vmem_partition_size_bytes=2 * 1024 * 1024, - ici_PG_enabled=True, - ici_temporal_granularity=PowerGatingConfig.TemporalGranularity.OPERATOR, - ici_spatial_granularity=PowerGatingConfig.ICISpatialGranularity.COMPONENT, - ) - elif pg_config_name in ["ideal_inst_PE_ALU", "Ideal"]: - pg_config = PowerGatingConfig( - name="Ideal", - SA_PG_enabled=True, - SA_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - SA_spatial_granularity=PowerGatingConfig.SASpatialGranularity.PE, - sa_partition_shapes=[128, 128], - sa_power_level_factors=[1.0, 0.0], - sa_pe_pg_delay_cycles=0, - sa_pg_delay_cycles=0, - VU_PG_enabled=True, - VU_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - VU_spatial_granularity=PowerGatingConfig.VUSpatialGranularity.ALU, - vu_power_level_factors=[1.0, 0.0], - vu_pg_delay_cycles=0, - vmem_PG_enabled=True, - vmem_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - vmem_spatial_granularity=PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE, - vmem_voltage_granularity=PowerGatingConfig.VoltageGranularity.TWO_LEVEL, - vmem_power_level_factors=[1.0, 0.0], - vmem_partition_size_bytes=2 * 1024 * 1024, - vmem_partition_pg_delay_cycles=0, - ici_PG_enabled=True, - ici_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - ici_spatial_granularity=PowerGatingConfig.ICISpatialGranularity.COMPONENT, - ici_power_level_factors=[1.0, 0.0], - ici_pg_delay_cycles=0, - hbm_PG_enabled=True, - hbm_PG_policy=PowerGatingConfig.PowerGatingPolicy.SW, - hbm_pg_delay_cycles=0, - ) - elif pg_config_name.startswith("Base"): - pg_config = PowerGatingConfig( - name="Base", - SA_PG_enabled=True, - SA_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - SA_spatial_granularity=PowerGatingConfig.SASpatialGranularity.COMPONENT, - sa_partition_shapes=[128, 128], - # 0.03 -> 0.05 accounts for the fact that weight registers cannot be power gated - sa_power_level_factors=[1.0, 0.05], - sa_pe_pg_delay_cycles=1, - VU_PG_enabled=True, - VU_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - VU_spatial_granularity=PowerGatingConfig.VUSpatialGranularity.COMPONENT, - vu_power_level_factors=[1.0, 0.03], - vu_pg_delay_cycles=2, - vmem_PG_enabled=True, - vmem_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - vmem_spatial_granularity=PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE, - vmem_voltage_granularity=PowerGatingConfig.VoltageGranularity.TWO_LEVEL, - vmem_power_level_factors=[1.0, 0.25], - vmem_partition_size_bytes=2 * 1024 * 1024, - vmem_partition_pg_delay_cycles=4, - vmem_HW_drowsy_period_cycles=2000, - ici_PG_enabled=True, - ici_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - ici_spatial_granularity=PowerGatingConfig.ICISpatialGranularity.COMPONENT, - ici_power_level_factors=[1.0, 0.03], - ici_pg_delay_cycles=60, - hbm_PG_enabled=True, - hbm_PG_policy=PowerGatingConfig.PowerGatingPolicy.HW, - ) - elif pg_config_name.startswith("HW"): - pg_config = PowerGatingConfig( - name="HW", - SA_PG_enabled=True, - SA_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - SA_spatial_granularity=PowerGatingConfig.SASpatialGranularity.PE, - sa_partition_shapes=[128, 128], - sa_power_level_factors=[1.0, 0.03], - sa_pe_pg_delay_cycles=1, - VU_PG_enabled=True, - VU_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - VU_spatial_granularity=PowerGatingConfig.VUSpatialGranularity.COMPONENT, - vu_power_level_factors=[1.0, 0.03], - vu_pg_delay_cycles=2, - vmem_PG_enabled=True, - vmem_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - vmem_spatial_granularity=PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE, - vmem_voltage_granularity=PowerGatingConfig.VoltageGranularity.TWO_LEVEL, - vmem_power_level_factors=[1.0, 0.25], - vmem_partition_size_bytes=2 * 1024 * 1024, - vmem_partition_pg_delay_cycles=4, - vmem_HW_drowsy_period_cycles=2000, - ici_PG_enabled=True, - ici_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - ici_spatial_granularity=PowerGatingConfig.ICISpatialGranularity.COMPONENT, - ici_power_level_factors=[1.0, 0.03], - ici_pg_delay_cycles=60, - hbm_PG_enabled=True, - hbm_PG_policy=PowerGatingConfig.PowerGatingPolicy.HW, - ) - elif pg_config_name.startswith("Full"): - pg_config = PowerGatingConfig( - name="Full", - SA_PG_enabled=True, - SA_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - SA_spatial_granularity=PowerGatingConfig.SASpatialGranularity.PE, - sa_partition_shapes=[128, 128], - sa_power_level_factors=[1.0, 0.03], - sa_pe_pg_delay_cycles=1, - VU_PG_enabled=True, - VU_PG_policy=PowerGatingConfig.PowerGatingPolicy.SW, - VU_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - VU_spatial_granularity=PowerGatingConfig.VUSpatialGranularity.COMPONENT, - vu_power_level_factors=[1.0, 0.03], - vu_pg_delay_cycles=2, - vmem_PG_enabled=True, - vmem_PG_policy=PowerGatingConfig.PowerGatingPolicy.SW, - vmem_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - vmem_spatial_granularity=PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE, - vmem_voltage_granularity=PowerGatingConfig.VoltageGranularity.TWO_LEVEL, - vmem_power_level_factors=[1.0, 0.0002], - vmem_partition_size_bytes=2 * 1024 * 1024, - vmem_partition_pg_delay_cycles=10, - ici_PG_enabled=True, - ici_temporal_granularity=PowerGatingConfig.TemporalGranularity.INSTRUCTION, - ici_spatial_granularity=PowerGatingConfig.ICISpatialGranularity.COMPONENT, - ici_power_level_factors=[1.0, 0.03], - ici_pg_delay_cycles=60, - hbm_PG_enabled=True, - hbm_PG_policy=PowerGatingConfig.PowerGatingPolicy.SW, - ) - else: - raise ValueError(f"Unsupported power gating configuration: {pg_config_name}") - - # vary Vth_low and PG delay for sensitivity analysis - if "vary_Vth" in pg_config_name: - # name scheme: "_vary_Vth__" - pg_config.name = pg_config_name - Vth = float(pg_config_name.split("_")[-2]) - Vth_sram = float(pg_config_name.split("_")[-1]) - pg_config.sa_power_level_factors[-1] = Vth - pg_config.vu_power_level_factors[-1] = Vth - pg_config.vmem_power_level_factors[-1] = Vth_sram - pg_config.ici_power_level_factors[-1] = Vth - pg_config.hbm_power_level_factors[-1] = Vth - pg_config.other_power_level_factors[-1] = Vth - if "vary_PG_delay" in pg_config_name: - # name scheme: "_vary_PG_delay_" - # is the extra delay ratio: new delay = old delay * - # This do not apply to PEs in the SA. - pg_config.name = pg_config_name - pg_delay = float(pg_config_name.split("_")[-1]) - pg_config.sa_pg_delay_cycles = ceil(pg_config.sa_pg_delay_cycles * pg_delay) - pg_config.vu_pg_delay_cycles = ceil(pg_config.vu_pg_delay_cycles * pg_delay) - pg_config.vmem_partition_pg_delay_cycles = ceil( - pg_config.vmem_partition_pg_delay_cycles * pg_delay - ) - pg_config.ici_pg_delay_cycles = ceil(pg_config.ici_pg_delay_cycles * pg_delay) - - return pg_config - - -def compute_peak_sa_flops_per_sec_from_chip_config(config: ChipConfig) -> float: - freq = config.freq_GHz * 1e9 - num_sa = config.num_sa - sa_dim_size = config.sa_dim - return 2 * (sa_dim_size**2) * freq * num_sa - - -def compute_peak_vu_flops_per_sec_from_chip_config(config: ChipConfig) -> float: - freq = config.freq_GHz * 1e9 - num_vu = config.num_vu - vu_num_alus = 128 * 8 # TODO: make this a parameter in chip config - return vu_num_alus * freq * num_vu - -def compute_peak_sa_flops_per_sec_from_dvfs_config(config: ChipConfig, dvfs: ComponentDVFSConfig) -> float: - if not dvfs.frequency_GHz or dvfs.frequency_GHz <= 0: - return compute_peak_sa_flops_per_sec_from_chip_config(config) - freq = dvfs.frequency_GHz * 1e9 - num_sa = config.num_sa - sa_dim_size = config.sa_dim - return 2 * (sa_dim_size**2) * freq * num_sa - - -def compute_peak_vu_flops_per_sec_from_dvfs_config(config: ChipConfig, dvfs: ComponentDVFSConfig) -> float: - if not dvfs.frequency_GHz or dvfs.frequency_GHz <= 0: - return compute_peak_vu_flops_per_sec_from_chip_config(config) - freq = dvfs.frequency_GHz * 1e9 - num_vu = config.num_vu - vu_num_alus = 128 * 8 # TODO: make this a parameter in chip config - return vu_num_alus * freq * num_vu - -def compute_sa_flops_util(op: Operator.Operator, config: ChipConfig, dvfs: ComponentDVFSConfig) -> float: - """ - Compute SA flops utilization for an operator. - """ - peak_sa_flops_per_sec = compute_peak_sa_flops_per_sec_from_dvfs_config(config, dvfs) - sa_time_ns = op.stats.sa_time_ns - if sa_time_ns > 0: # op.op_type == Operator.OpType.MXU: - # assert sa_time_ns > 0, f"SA time is 0 for op: {op.to_csv_dict()}" - sa_flops_util = min( - (op.stats.flop_count / sa_time_ns * 1e9) / peak_sa_flops_per_sec, - 1.0, - ) - else: - sa_flops_util = 0 - return sa_flops_util - - -def compute_vu_flops_util(op: Operator.Operator, config: ChipConfig, dvfs: ComponentDVFSConfig) -> float: - """ - Compute VU flops utilization for an operator. - """ - peak_vu_flops_per_sec = compute_peak_vu_flops_per_sec_from_dvfs_config(config, dvfs) - vu_time_ns = op.stats.vu_time_ns - if op.op_type == Operator.OpType.MXU: - # assert peak_vu_flops_per_sec > 0, f"Peak VU FLOPS is {peak_vu_flops_per_sec} for op: {op.to_csv_dict()}" - # assert vu_time_ns > 0, f"VU time is {vu_time_ns} for op: {op.to_csv_dict()}" - # assumes vu flops is at least 1/8 of sa flops for accmulation - vu_flops_util = min( - (op.stats.flop_count / 8 / vu_time_ns * 1e9) / peak_vu_flops_per_sec, - 1.0, - ) - else: - if peak_vu_flops_per_sec > 0 and vu_time_ns > 0: - vu_flops_util = min( - (op.stats.flop_count / vu_time_ns * 1e9) / peak_vu_flops_per_sec, - 1.0, - ) - else: - vu_flops_util = 0 - return vu_flops_util - - -def cycle_to_ns(cycles: int, freq_GHz: float) -> float: - """ - Convert cycles to nanoseconds. - """ - return cycles / freq_GHz - - -def ns_to_cycle(ns: float, freq_GHz: float) -> float: - """ - Convert nanoseconds to cycles. - """ - return ns * freq_GHz - - -def scale_dvfs_component_time(op: Operator.Operator, config: ChipConfig) -> Operator.Operator: - """ - Scale per-component active times based on the DVFS frequency set in op.dvfs_*. - Original times are assumed to be measured at base 1.7 GHz. - """ - base_freq_GHz = config.freq_GHz - - def _scale_time_with_dvfs(time_ns: int | float, dvfs: ComponentDVFSConfig) -> int: - """ - Scale the active time when a DVFS frequency is specified. - If no DVFS freq, keep original time. - """ - if time_ns <= 0 or dvfs.frequency_GHz is None: - return int(float(time_ns)) - if dvfs.frequency_GHz <= 0: - return int(float(time_ns)) - # Original times assumed at base_freq_Hz. - return ceil(time_ns * base_freq_GHz / dvfs.frequency_GHz) - - # Apply DVFS freq to component times (performance effect only) - op.stats.sa_time_ns = _scale_time_with_dvfs(op.stats.sa_time_ns, op.dvfs_sa) - op.stats.vu_time_ns = _scale_time_with_dvfs(op.stats.vu_time_ns, op.dvfs_vu) - op.stats.vmem_time_ns = _scale_time_with_dvfs(op.stats.vmem_time_ns, op.dvfs_sram) - op.stats.ici_time_ns = _scale_time_with_dvfs(op.stats.ici_time_ns, op.dvfs_ici) - op.stats.memory_time_ns = _scale_time_with_dvfs(op.stats.memory_time_ns, op.dvfs_hbm) - - return op - - -def analyze_dynamic_energy( - op: Operator.Operator, config: ChipConfig -) -> Operator.Operator: - """ - Analyze dynamic power and energy for an operator. - - Assumes: - - Static energy & execution/component times (possibly DVFS/PG-adjusted) - have already been computed. - - `configure_dvfs_for_op` has been called to populate op.dvfs_*. - - Behavior: - - Uses get_power_from_dvfs(...) to obtain dynamic power. - - Computes dynamic energy for each component as P_dyn * active_time. - """ - - # Recompute FLOPS utils with updated times - sa_flops_util = compute_sa_flops_util(op, config, op.dvfs_sa) - vu_flops_util = compute_vu_flops_util(op, config, op.dvfs_vu) - - # Dynamic powers - if config.enable_dvfs: - sa_dyn_W, _ = get_power_from_dvfs("SA", op.dvfs_sa) - vu_dyn_W, _ = get_power_from_dvfs("VU", op.dvfs_vu) - sram_dyn_W, _ = get_power_from_dvfs("SRAM", op.dvfs_sram) - hbm_dyn_W, _ = get_power_from_dvfs("HBM", op.dvfs_hbm) - ici_dyn_W, _ = get_power_from_dvfs("ICI", op.dvfs_ici) - else: - sa_dyn_W = config.dynamic_power_sa_W - vu_dyn_W = config.dynamic_power_vu_W - sram_dyn_W = config.dynamic_power_vmem_W - hbm_dyn_W = config.dynamic_power_hbm_W - ici_dyn_W = config.dynamic_power_ici_W - - # 'other' still uses config (no DVFS enabled) - other_dyn_W = config.dynamic_power_other_W - - # Dynamic energy per component - exe_time_ns = op.stats.execution_time_ns - sa_time_ns = op.stats.sa_time_ns - vu_time_ns = op.stats.vu_time_ns - vmem_time_ns = op.stats.vmem_time_ns - ici_time_ns = op.stats.ici_time_ns - hbm_time_ns = op.stats.memory_time_ns - - op.stats.dynamic_energy_sa_J = sa_dyn_W * sa_time_ns * config.num_sa / 1e9 * sa_flops_util - op.stats.dynamic_energy_vu_J = vu_dyn_W * vu_time_ns * config.num_vu / 1e9 * vu_flops_util - op.stats.dynamic_energy_sram_J = sram_dyn_W * vmem_time_ns / 1e9 - op.stats.dynamic_energy_ici_J = ici_dyn_W * ici_time_ns / 1e9 - op.stats.dynamic_energy_hbm_J = hbm_dyn_W * hbm_time_ns / 1e9 - op.stats.dynamic_energy_other_J = other_dyn_W * exe_time_ns / 1e9 - - return op - - -def analyze_sa_static_energy( - op: Operator.Operator, config: ChipConfig, pg_config: PowerGatingConfig -) -> Operator.Operator: - """ - Static power/energy analysis for SA. - """ - if config.enable_dvfs: - _, static_sa_W = get_power_from_dvfs("SA", op.dvfs_sa) - else: - static_sa_W = config.static_power_sa_W - static_sa_W *= config.num_sa - pg_power_W = static_sa_W * pg_config.sa_power_level_factors[-1] - - # No power-gating - if not pg_config.SA_PG_enabled: - op.stats.static_energy_sa_J = static_sa_W * op.stats.execution_time_ns / 1e9 - return op - - if op.stats.sa_time_ns > 0: - # assumes in the worst case, idle intervals are evenly distributed - # over the entire execution time - worst_case_sa_idle_interval_ns = ceil( - (op.stats.execution_time_ns - 1) / (op.stats.sa_time_ns / config.sa_dim) - ) - if worst_case_sa_idle_interval_ns == 0: - # if SA is not idle (op is SA-bound), then no power gating - op.stats.static_energy_sa_J = static_sa_W * op.stats.execution_time_ns / 1e9 - return op - else: - worst_case_sa_idle_interval_ns = 0 - - sa_flops_util = compute_sa_flops_util(op, config, op.dvfs_sa) - - # calculate PG delay overhead and update op stats - if ( - op.stats.sa_time_ns > 0 - and pg_config.SA_temporal_granularity - == PowerGatingConfig.TemporalGranularity.INSTRUCTION - and pg_config.SA_spatial_granularity - == PowerGatingConfig.SASpatialGranularity.PE - ): # used by HW and Full - assert isinstance(op.stats, (Operator.EinsumStatistics, Operator.FlashAttentionStatistics)) - overhead_ns_1 = ceil( - op.stats.sa_time_ns / config.sa_dim * cycle_to_ns( - pg_config.sa_pe_pg_delay_cycles, config.freq_GHz - ) - ) - overhead_ns_2 = ceil( - op.stats.num_sa_ops * cycle_to_ns(pg_config.sa_pe_pg_delay_cycles, config.freq_GHz) - ) - overhead_ns = min(overhead_ns_1, overhead_ns_2) - op.stats.sa_time_ns += overhead_ns - elif ( - op.stats.sa_time_ns > 0 - and pg_config.SA_temporal_granularity - == PowerGatingConfig.TemporalGranularity.INSTRUCTION - and pg_config.SA_spatial_granularity - == PowerGatingConfig.SASpatialGranularity.COMPONENT - ): # used by Base (idle-detect policy) - if worst_case_sa_idle_interval_ns > 4 * cycle_to_ns( - pg_config.sa_pg_delay_cycles, config.freq_GHz - ): - pg_delay_ns = ceil( - cycle_to_ns(pg_config.sa_pg_delay_cycles, config.freq_GHz) - ) - # sa_time_ns/sa_dim is the worst case number of idle intervals - op.stats.sa_time_ns += ceil(pg_delay_ns * (op.stats.sa_time_ns / config.sa_dim)) - # if op.stats.sa_time_ns > op.stats.execution_time_ns: - # op.stats.execution_time_ns = op.stats.sa_time_ns - # op.stats.bounded_by = "Compute" - - sa_time_ns = op.stats.sa_time_ns - exe_time_ns = max(op.stats.execution_time_ns, sa_time_ns) - - if ( - pg_config.SA_temporal_granularity - == PowerGatingConfig.TemporalGranularity.INSTRUCTION - and pg_config.SA_spatial_granularity - == PowerGatingConfig.SASpatialGranularity.COMPONENT - ): - pg_energy = pg_power_W * (exe_time_ns - sa_time_ns) / 1e9 - static_energy = static_sa_W * sa_time_ns / 1e9 - - # Base policy: do not power gate if idle interval is smaller than 2x pg delay time - if sa_time_ns > 0 and worst_case_sa_idle_interval_ns < 2 * cycle_to_ns( - pg_config.sa_pg_delay_cycles, config.freq_GHz - ): - pg_energy = 0 - static_energy = static_sa_W * exe_time_ns / 1e9 - - op.stats.static_energy_sa_J = pg_energy + static_energy - return op - - if ( - pg_config.SA_temporal_granularity - == PowerGatingConfig.TemporalGranularity.INSTRUCTION - and pg_config.SA_spatial_granularity - == PowerGatingConfig.SASpatialGranularity.PARTITION - ): - raise NotImplementedError() # TODO - - if ( - pg_config.SA_temporal_granularity - == PowerGatingConfig.TemporalGranularity.INSTRUCTION - and pg_config.SA_spatial_granularity - == PowerGatingConfig.SASpatialGranularity.PE - ): - pg_energy = ( - pg_power_W * sa_time_ns / 1e9 * (1 - sa_flops_util) - + pg_power_W * (exe_time_ns - sa_time_ns) / 1e9 - ) - static_energy = static_sa_W * sa_time_ns / 1e9 * sa_flops_util - op.stats.static_energy_sa_J = pg_energy + static_energy - return op - - if ( - pg_config.SA_temporal_granularity - == PowerGatingConfig.TemporalGranularity.OPERATOR - and pg_config.SA_spatial_granularity - == PowerGatingConfig.SASpatialGranularity.COMPONENT - ): - if op.op_type == Operator.OpType.MXU: - op.stats.static_energy_sa_J = static_sa_W * exe_time_ns / 1e9 - else: - op.stats.static_energy_sa_J = 0 - return op - - if ( - pg_config.SA_temporal_granularity - == PowerGatingConfig.TemporalGranularity.OPERATOR - and pg_config.SA_spatial_granularity - == PowerGatingConfig.SASpatialGranularity.PARTITION - ): - raise NotImplementedError() # TODO - - if ( - pg_config.SA_temporal_granularity - == PowerGatingConfig.TemporalGranularity.OPERATOR - and pg_config.SA_spatial_granularity - == PowerGatingConfig.SASpatialGranularity.PE - ): - op.stats.static_energy_sa_J = static_sa_W * exe_time_ns / 1e9 * sa_flops_util - return op - - if ( - pg_config.SA_temporal_granularity - == PowerGatingConfig.TemporalGranularity.APPLICATION - and pg_config.SA_spatial_granularity - == PowerGatingConfig.SASpatialGranularity.COMPONENT - ): - raise NotImplementedError() # TODO - - if ( - pg_config.SA_temporal_granularity - == PowerGatingConfig.TemporalGranularity.APPLICATION - and pg_config.SA_spatial_granularity - == PowerGatingConfig.SASpatialGranularity.PARTITION - ): - raise NotImplementedError() # TODO - - if ( - pg_config.SA_temporal_granularity - == PowerGatingConfig.TemporalGranularity.APPLICATION - and pg_config.SA_spatial_granularity - == PowerGatingConfig.SASpatialGranularity.PE - ): - raise NotImplementedError() # TODO - - # should not reach here - raise ValueError("Unsupported/Unknown/Invalid SA power gating configuration") - - -def analyze_vu_static_energy( - op: Operator.Operator, config: ChipConfig, pg_config: PowerGatingConfig -) -> Operator.Operator: - """ - Static power/energy analysis for VU. - """ - if config.enable_dvfs: - _, static_vu_W = get_power_from_dvfs("VU", op.dvfs_vu) - else: - static_vu_W = config.static_power_vu_W - static_vu_W *= config.num_vu - pg_power_W = static_vu_W * pg_config.vu_power_level_factors[-1] - - # No power-gating - if not pg_config.VU_PG_enabled: - op.stats.static_energy_vu_J = static_vu_W * op.stats.execution_time_ns / 1e9 - return op - - if op.stats.vu_time_ns > 0: - # assumes in the worst case, idle intervals are evenly distributed - # over the entire execution time - worst_case_vu_idle_interval_ns = ceil( - (op.stats.execution_time_ns - 1) / op.stats.vu_time_ns - ) - if worst_case_vu_idle_interval_ns == 0: - # if VU is not idle (op is VU-bound), then no power gating - op.stats.static_energy_vu_J = static_vu_W * op.stats.execution_time_ns / 1e9 - return op - else: - worst_case_vu_idle_interval_ns = 0 - - vu_flops_util = compute_vu_flops_util(op, config, op.dvfs_vu) - - # calculate PG delay overhead and update op stats - if ( - pg_config.VU_temporal_granularity - == PowerGatingConfig.TemporalGranularity.INSTRUCTION - and pg_config.VU_PG_policy == PowerGatingConfig.PowerGatingPolicy.HW - ): # used by Base and HW (idle-detect policy) - if worst_case_vu_idle_interval_ns > 4 * cycle_to_ns( - pg_config.vu_pg_delay_cycles, config.freq_GHz - ): - pg_delay_ns = ceil( - cycle_to_ns(pg_config.vu_pg_delay_cycles, config.freq_GHz) - ) - # vu_time_ns is the worst case number of idle intervals - op.stats.vu_time_ns += pg_delay_ns * op.stats.vu_time_ns - # if op.stats.vu_time_ns > op.stats.execution_time_ns: - # op.stats.execution_time_ns = op.stats.vu_time_ns - # op.stats.bounded_by = "Compute" - - vu_time_ns = op.stats.vu_time_ns - exe_time_ns = max(op.stats.execution_time_ns, vu_time_ns) - - if ( - pg_config.VU_temporal_granularity - == PowerGatingConfig.TemporalGranularity.INSTRUCTION - and pg_config.VU_spatial_granularity - == PowerGatingConfig.VUSpatialGranularity.COMPONENT - ): # used by Base, HW, and Full pg_config - pg_energy = pg_power_W * (exe_time_ns - vu_time_ns) / 1e9 - static_energy = static_vu_W * vu_time_ns / 1e9 - - # HW policy: do not power gate if idle interval is smaller than 2x pg delay time - if pg_config.VU_PG_policy == PowerGatingConfig.PowerGatingPolicy.HW: - if worst_case_vu_idle_interval_ns < 2 * cycle_to_ns( - pg_config.vu_pg_delay_cycles, config.freq_GHz - ): - pg_energy = 0 - static_energy = static_vu_W * exe_time_ns / 1e9 - - op.stats.static_energy_vu_J = pg_energy + static_energy - - # Full policy: calculate number of setpm instructions - if pg_config.VU_PG_policy == PowerGatingConfig.PowerGatingPolicy.SW: - if exe_time_ns == vu_time_ns: # VU bound; no VU idle intervals - op.stats.num_setpm_vu = 0 - elif vu_time_ns == 0: # VU idle; set VUs to be PG'ed only once - op.stats.num_setpm_vu = 1 - else: # use the number of idle intervals as an estimate - op.stats.num_setpm_vu = min( - round(exe_time_ns / worst_case_vu_idle_interval_ns), - (exe_time_ns - vu_time_ns) // 32, # 32 cycles BET for VU with wake-up delay of 2 cycles on TPUv5p - # This division estimates the max number of setpm instructions - # TODO: make this a parameter in PG config - ) - return op - - if ( - pg_config.VU_temporal_granularity - == PowerGatingConfig.TemporalGranularity.INSTRUCTION - and pg_config.VU_spatial_granularity - == PowerGatingConfig.VUSpatialGranularity.PARTITION - ): - raise NotImplementedError() # TODO - - if ( - pg_config.VU_temporal_granularity - == PowerGatingConfig.TemporalGranularity.INSTRUCTION - and pg_config.VU_spatial_granularity - == PowerGatingConfig.VUSpatialGranularity.ALU - ): # only used by Ideal pg_config for now - pg_energy = ( - pg_power_W * vu_time_ns / 1e9 * (1 - vu_flops_util) - + pg_power_W * (exe_time_ns - vu_time_ns) / 1e9 - ) - static_energy = static_vu_W * vu_time_ns / 1e9 * vu_flops_util - op.stats.static_energy_vu_J = pg_energy + static_energy - return op - - if ( - pg_config.VU_temporal_granularity - == PowerGatingConfig.TemporalGranularity.OPERATOR - and pg_config.VU_spatial_granularity - == PowerGatingConfig.VUSpatialGranularity.COMPONENT - ): - if vu_time_ns > 0: - op.stats.static_energy_vu_J = static_vu_W * exe_time_ns / 1e9 - else: - op.stats.static_energy_vu_J = 0 - return op - - if ( - pg_config.VU_temporal_granularity - == PowerGatingConfig.TemporalGranularity.OPERATOR - and pg_config.VU_spatial_granularity - == PowerGatingConfig.VUSpatialGranularity.PARTITION - ): - raise NotImplementedError() # TODO - - if ( - pg_config.VU_temporal_granularity - == PowerGatingConfig.TemporalGranularity.OPERATOR - and pg_config.VU_spatial_granularity - == PowerGatingConfig.VUSpatialGranularity.ALU - ): - op.stats.static_energy_vu_J = static_vu_W * exe_time_ns / 1e9 * vu_flops_util - return op - - if ( - pg_config.VU_temporal_granularity - == PowerGatingConfig.TemporalGranularity.APPLICATION - and pg_config.VU_spatial_granularity - == PowerGatingConfig.VUSpatialGranularity.COMPONENT - ): - raise NotImplementedError() # TODO - - if ( - pg_config.VU_temporal_granularity - == PowerGatingConfig.TemporalGranularity.APPLICATION - and pg_config.VU_spatial_granularity - == PowerGatingConfig.VUSpatialGranularity.PARTITION - ): - raise NotImplementedError() # TODO - - if ( - pg_config.VU_temporal_granularity - == PowerGatingConfig.TemporalGranularity.APPLICATION - and pg_config.VU_spatial_granularity - == PowerGatingConfig.VUSpatialGranularity.ALU - ): - raise NotImplementedError() # TODO - - # should not reach here - raise ValueError("Unsupported/Unknown/Invalid VU power gating configuration") - - -def analyze_vmem_static_energy( - op: Operator.Operator, config: ChipConfig, pg_config: PowerGatingConfig -) -> Operator.Operator: - """ - Static power/energy analysis for vmem. - """ - if config.enable_dvfs: - _, static_vmem_W = get_power_from_dvfs("SRAM", op.dvfs_sram) - else: - static_vmem_W = config.static_power_vmem_W - pg_power_W = static_vmem_W * pg_config.vmem_power_level_factors[-1] - - # No power-gating - if not pg_config.vmem_PG_enabled: - op.stats.static_energy_sram_J = static_vmem_W * op.stats.execution_time_ns / 1e9 - return op - - partition_granularity = pg_config.vmem_partition_size_bytes - if ( - pg_config.vmem_spatial_granularity - == PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE - ): - partition_granularity = 4 * 1024 # 4KB - - vmem_size = config.vmem_size_MB * 1024 * 1024 - - # compute vmem capacity utilization - if op.op_type == Operator.OpType.MXU: - assert isinstance( - op.stats, (Operator.EinsumStatistics, Operator.FlashAttentionStatistics) - ), f"op_name: {op.name} :: op_type: {op.op_type}, opcode_type: {op.opcode_type}, opcode: {op.opcode} not supported for op.stats type {type(op.stats)}\nconfig: {op.config_str}" - max_vmem_demand = op.stats.max_vmem_demand_bytes - vmem_capacity_util = min(max_vmem_demand / vmem_size, 1.0) - else: - # only use 2MB per core (4MB in total) for operators w/o data reuse - vmem_capacity_util = 4 / config.vmem_size_MB - vmem_demand_ceiled = ( - int(np.ceil(vmem_capacity_util * vmem_size / partition_granularity)) - * partition_granularity - ) - vmem_capacity_util = vmem_demand_ceiled / vmem_size - - exe_time_ns = op.stats.execution_time_ns - - # calculate PG delay overhead and update op stats - if pg_config.vmem_PG_policy == PowerGatingConfig.PowerGatingPolicy.HW: - pg_delay_overhead = ceil( - op.stats.execution_time_ns - / cycle_to_ns(pg_config.vmem_HW_drowsy_period_cycles, config.freq_GHz) - * cycle_to_ns(pg_config.vmem_partition_pg_delay_cycles, config.freq_GHz) - ) - exe_time_ns += pg_delay_overhead - - - vmem_time_ns = op.stats.vmem_time_ns - - - if ( - pg_config.vmem_temporal_granularity - == PowerGatingConfig.TemporalGranularity.INSTRUCTION - and pg_config.vmem_spatial_granularity - == PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE - and pg_config.vmem_voltage_granularity - == PowerGatingConfig.VoltageGranularity.TWO_LEVEL - ): - pg_energy = ( - pg_power_W * vmem_time_ns / 1e9 * (1 - vmem_capacity_util) - + pg_power_W * (exe_time_ns - vmem_time_ns) / 1e9 - ) - static_energy = static_vmem_W * vmem_time_ns / 1e9 * vmem_capacity_util - op.stats.static_energy_sram_J = pg_energy + static_energy - - # Full policy: calculate number of setpm instructions - if pg_config.vmem_PG_policy == PowerGatingConfig.PowerGatingPolicy.SW: - # only set once per operator as we assume fixed tile size per operator for now - op.stats.num_setpm_sram = 1 - - return op - - if ( - pg_config.vmem_temporal_granularity - == PowerGatingConfig.TemporalGranularity.INSTRUCTION - and pg_config.vmem_spatial_granularity - == PowerGatingConfig.VmemSpatialGranularity.PARTITION - and pg_config.vmem_voltage_granularity - == PowerGatingConfig.VoltageGranularity.TWO_LEVEL - ): - pg_energy = ( - pg_power_W * vmem_time_ns / 1e9 * (1 - vmem_capacity_util) - + pg_power_W * (exe_time_ns - vmem_time_ns) / 1e9 - ) - static_energy = static_vmem_W * vmem_time_ns / 1e9 * vmem_capacity_util - op.stats.static_energy_sram_J = pg_energy + static_energy - return op - - if ( - pg_config.vmem_temporal_granularity - == PowerGatingConfig.TemporalGranularity.INSTRUCTION - and pg_config.vmem_spatial_granularity - == PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE - and pg_config.vmem_voltage_granularity - == PowerGatingConfig.VoltageGranularity.MULTI_LEVEL - ): - raise NotImplementedError() # TODO - - if ( - pg_config.vmem_temporal_granularity - == PowerGatingConfig.TemporalGranularity.INSTRUCTION - and pg_config.vmem_spatial_granularity - == PowerGatingConfig.VmemSpatialGranularity.PARTITION - and pg_config.vmem_voltage_granularity - == PowerGatingConfig.VoltageGranularity.MULTI_LEVEL - ): - raise NotImplementedError() # TODO - - if ( - pg_config.vmem_temporal_granularity - == PowerGatingConfig.TemporalGranularity.OPERATOR - and pg_config.vmem_spatial_granularity - == PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE - and pg_config.vmem_voltage_granularity - == PowerGatingConfig.VoltageGranularity.TWO_LEVEL - ): - pg_energy = ( - pg_power_W * vmem_time_ns / 1e9 * (1 - vmem_capacity_util) - + pg_power_W * (exe_time_ns - vmem_time_ns) / 1e9 - ) - static_energy = static_vmem_W * vmem_time_ns / 1e9 * vmem_capacity_util - op.stats.static_energy_sram_J = pg_energy + static_energy - return op - - if ( - pg_config.vmem_temporal_granularity - == PowerGatingConfig.TemporalGranularity.OPERATOR - and pg_config.vmem_spatial_granularity - == PowerGatingConfig.VmemSpatialGranularity.PARTITION - and pg_config.vmem_voltage_granularity - == PowerGatingConfig.VoltageGranularity.TWO_LEVEL - ): - pg_energy = ( - pg_power_W * vmem_time_ns / 1e9 * (1 - vmem_capacity_util) - + pg_power_W * (exe_time_ns - vmem_time_ns) / 1e9 - ) - static_energy = static_vmem_W * vmem_time_ns / 1e9 * vmem_capacity_util - op.stats.static_energy_sram_J = pg_energy + static_energy - return op - - if ( - pg_config.vmem_temporal_granularity - == PowerGatingConfig.TemporalGranularity.OPERATOR - and pg_config.vmem_spatial_granularity - == PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE - and pg_config.vmem_voltage_granularity - == PowerGatingConfig.VoltageGranularity.MULTI_LEVEL - ): - raise NotImplementedError() # TODO - - if ( - pg_config.vmem_temporal_granularity - == PowerGatingConfig.TemporalGranularity.OPERATOR - and pg_config.vmem_spatial_granularity - == PowerGatingConfig.VmemSpatialGranularity.PARTITION - and pg_config.vmem_voltage_granularity - == PowerGatingConfig.VoltageGranularity.MULTI_LEVEL - ): - raise NotImplementedError() # TODO - - if ( - pg_config.vmem_temporal_granularity - == PowerGatingConfig.TemporalGranularity.APPLICATION - and pg_config.vmem_spatial_granularity - == PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE - and pg_config.vmem_voltage_granularity - == PowerGatingConfig.VoltageGranularity.TWO_LEVEL - ): - raise NotImplementedError() # TODO - - if ( - pg_config.vmem_temporal_granularity - == PowerGatingConfig.TemporalGranularity.APPLICATION - and pg_config.vmem_spatial_granularity - == PowerGatingConfig.VmemSpatialGranularity.PARTITION - and pg_config.vmem_voltage_granularity - == PowerGatingConfig.VoltageGranularity.TWO_LEVEL - ): - raise NotImplementedError() # TODO - - if ( - pg_config.vmem_temporal_granularity - == PowerGatingConfig.TemporalGranularity.APPLICATION - and pg_config.vmem_spatial_granularity - == PowerGatingConfig.VmemSpatialGranularity.REGISTER_SIZE - and pg_config.vmem_voltage_granularity - == PowerGatingConfig.VoltageGranularity.MULTI_LEVEL - ): - raise NotImplementedError() # TODO - - if ( - pg_config.vmem_temporal_granularity - == PowerGatingConfig.TemporalGranularity.APPLICATION - and pg_config.vmem_spatial_granularity - == PowerGatingConfig.VmemSpatialGranularity.PARTITION - and pg_config.vmem_voltage_granularity - == PowerGatingConfig.VoltageGranularity.MULTI_LEVEL - ): - raise NotImplementedError() # TODO - - # should not reach here - raise ValueError("Unsupported/Unknown/Invalid Vmem power gating configuration") - - -def analyze_ici_static_energy( - op: Operator.Operator, config: ChipConfig, pg_config: PowerGatingConfig -) -> Operator.Operator: - """ - Static power/energy analysis for ICI. - """ - if config.enable_dvfs: - _, static_ici_W = get_power_from_dvfs("ICI", op.dvfs_ici) - else: - static_ici_W = config.static_power_ici_W - pg_power_W = static_ici_W * pg_config.ici_power_level_factors[-1] - - # No power-gating - if not pg_config.ici_PG_enabled: - op.stats.static_energy_ici_J = static_ici_W * op.stats.execution_time_ns / 1e9 - return op - - # assert ( - # pg_config.ici_PG_policy == PowerGatingConfig.PowerGatingPolicy.HW - # ), "Only HW-managed power gating is supported for ICI" - - # calculate PG delay overhead and update op stats - if op.stats.ici_time_ns > 0: - pg_delay_ns = ceil( - 2 * cycle_to_ns(pg_config.ici_pg_delay_cycles, config.freq_GHz) - ) - op.stats.ici_time_ns += pg_delay_ns - # if op.stats.ici_time_ns > op.stats.execution_time_ns: - # op.stats.execution_time_ns = op.stats.ici_time_ns - # op.stats.bounded_by = "ICI/NVLink" - - ici_time_ns = op.stats.ici_time_ns - exe_time_ns = max(op.stats.execution_time_ns, ici_time_ns) - - if ( - pg_config.ici_temporal_granularity - == PowerGatingConfig.TemporalGranularity.INSTRUCTION - and pg_config.ici_spatial_granularity - == PowerGatingConfig.ICISpatialGranularity.LINK - ): - raise NotImplementedError() # TODO - - if ( - pg_config.ici_temporal_granularity - == PowerGatingConfig.TemporalGranularity.INSTRUCTION - and pg_config.ici_spatial_granularity - == PowerGatingConfig.ICISpatialGranularity.COMPONENT - ): - pg_energy = pg_power_W * (exe_time_ns - ici_time_ns) / 1e9 - static_energy = static_ici_W * ici_time_ns / 1e9 - op.stats.static_energy_ici_J = pg_energy + static_energy - return op - - if ( - pg_config.ici_temporal_granularity - == PowerGatingConfig.TemporalGranularity.OPERATOR - and pg_config.ici_spatial_granularity - == PowerGatingConfig.ICISpatialGranularity.LINK - ): - raise NotImplementedError() # TODO - - if ( - pg_config.ici_temporal_granularity - == PowerGatingConfig.TemporalGranularity.OPERATOR - and pg_config.ici_spatial_granularity - == PowerGatingConfig.ICISpatialGranularity.COMPONENT - ): - if ici_time_ns > 0: - op.stats.static_energy_ici_J = static_ici_W * exe_time_ns / 1e9 - else: - op.stats.static_energy_ici_J = pg_power_W * exe_time_ns / 1e9 - return op - - if ( - pg_config.ici_temporal_granularity - == PowerGatingConfig.TemporalGranularity.APPLICATION - and pg_config.ici_spatial_granularity - == PowerGatingConfig.ICISpatialGranularity.LINK - ): - raise NotImplementedError() # TODO - - if ( - pg_config.ici_temporal_granularity - == PowerGatingConfig.TemporalGranularity.APPLICATION - and pg_config.ici_spatial_granularity - == PowerGatingConfig.ICISpatialGranularity.COMPONENT - ): - raise NotImplementedError() # TODO - - # should not reach here - raise ValueError("Unsupported/Unknown/Invalid ICI power gating configuration") - - -def analyze_hbm_static_energy( - op: Operator.Operator, config: ChipConfig, pg_config: PowerGatingConfig -) -> Operator.Operator: - """ - Static power/energy analysis for HBM. - """ - if config.enable_dvfs: - _, static_hbm_W = get_power_from_dvfs("HBM", op.dvfs_hbm) - else: - static_hbm_W = config.static_power_hbm_W - pg_power_W = static_hbm_W * pg_config.hbm_power_level_factors[-1] - - # No power-gating - if not pg_config.hbm_PG_enabled: - op.stats.static_energy_hbm_J = ( - static_hbm_W * op.stats.execution_time_ns / 1e9 - ) - return op - - # assume 4MB DMA size if memory traffic is larger than this - if op.stats.memory_traffic_bytes < 4 * 1024 * 1024: - active_length_ns = op.stats.memory_time_ns - else: - active_length_ns = ( - 4 * 1024 * 1024 / (config.hbm_bw_GBps * 1024 * 1024 * 1024) * 1e9 - ) - hbm_util = op.stats.memory_time_ns / op.stats.execution_time_ns - num_periods = ceil( - op.stats.memory_time_ns / active_length_ns - ) - idle_length_ns = ceil( - op.stats.execution_time_ns / num_periods - active_length_ns - ) +# Re-export all modeling functions from backend power model +from neusim.npusim.backend.power_model import ( # noqa: F401 + compute_peak_sa_flops_per_sec_from_chip_config, + compute_peak_vu_flops_per_sec_from_chip_config, + compute_peak_sa_flops_per_sec_from_dvfs_config, + compute_peak_vu_flops_per_sec_from_dvfs_config, + compute_sa_flops_util, + compute_vu_flops_util, + cycle_to_ns, + ns_to_cycle, + scale_dvfs_component_time, + analyze_dynamic_energy, + analyze_sa_static_energy, + analyze_vu_static_energy, + analyze_vmem_static_energy, + analyze_ici_static_energy, + analyze_hbm_static_energy, + analyze_other_static_energy, + add_op_dvfs_exe_time_overhead, + apply_regulator_efficiency, +) - # break-even time - BET_ns = config.hbm_latency_ns * 2 + ceil(cycle_to_ns(pg_config.hbm_pg_delay_cycles, config.freq_GHz)) * 4 - idle_detect_timeout_ns = BET_ns * 4 - if pg_config.hbm_PG_policy == PowerGatingConfig.PowerGatingPolicy.SW: - if idle_length_ns >= BET_ns: - # power gate HBM - pg_energy = pg_power_W * (op.stats.execution_time_ns - op.stats.memory_time_ns) / 1e9 - static_energy = op.stats.memory_time_ns / 1e9 * static_hbm_W - op.stats.static_energy_hbm_J = pg_energy + static_energy - else: - # do not power gate HBM - op.stats.static_energy_hbm_J = static_hbm_W * op.stats.execution_time_ns / 1e9 - elif pg_config.hbm_PG_policy == PowerGatingConfig.PowerGatingPolicy.HW: - if idle_length_ns >= idle_detect_timeout_ns: - # power gate HBM - pg_energy = pg_power_W * (op.stats.execution_time_ns - op.stats.memory_time_ns) / 1e9 - static_energy = op.stats.memory_time_ns / 1e9 * static_hbm_W - op.stats.static_energy_hbm_J = pg_energy + static_energy +# ===================================================================== +# Orchestration helpers (stay in frontend) +# ===================================================================== - # calculate PG delay overhead and update op stats - pg_delay_ns = ceil( - cycle_to_ns(pg_config.hbm_pg_delay_cycles, config.freq_GHz) * num_periods - ) - op.stats.memory_time_ns += pg_delay_ns +def get_global_dvfs_config_helper(dvfs_config: str | DVFSConfig | DVFSPolicy | None = None) -> DVFSConfig: + if not dvfs_config: + dvfs_config = DVFSConfig() + elif isinstance(dvfs_config, DVFSPolicy): + dvfs_config = DVFSConfig(policy=dvfs_config) + elif isinstance(dvfs_config, str): + if dvfs_config != "None": + dvfs_str_split = dvfs_config.split("_") + policy = DVFSPolicy.from_str(dvfs_str_split[0]) + if len(dvfs_str_split) == 1: + dvfs_config = DVFSConfig(policy=policy) + else: + perf_degrad_factor = float(dvfs_str_split[1]) + dvfs_config = DVFSConfig(policy=policy, performance_degradation_percentage=perf_degrad_factor) else: - # do not power gate HBM - op.stats.static_energy_hbm_J = static_hbm_W * op.stats.execution_time_ns / 1e9 - else: - raise NotImplementedError("Unknown HBM power gating policy") - - return op - - -def analyze_other_static_energy( - op: Operator.Operator, config: ChipConfig, pg_config: PowerGatingConfig -) -> Operator.Operator: - """ - Static power/energy analysis for other. - """ - assert pg_config.other_PG_enabled is False, "Other power gating is not supported" + dvfs_config = DVFSConfig(policy=DVFSPolicy.NONE) - op.stats.static_energy_other_J = ( - config.static_power_other_W * op.stats.execution_time_ns / 1e9 - ) - return op + return dvfs_config def configure_dvfs_for_op( @@ -1243,195 +94,6 @@ def configure_dvfs_for_op( return op -def add_op_dvfs_exe_time_overhead(op: Operator.Operator, config: ChipConfig) -> Operator.Operator: - """ - Add DVFS latency overhead to op execution times and - set voltage conversion efficiency in the DVFSConfig for each component. - """ - # helpers - def _get_dvfs_config_for_component(comp: str) -> ComponentDVFSConfig: - if comp == "sa": - return op.dvfs_sa - elif comp == "vu": - return op.dvfs_vu - elif comp == "hbm": - return op.dvfs_hbm - elif comp == "ici": - return op.dvfs_ici - elif comp == "vmem": - return op.dvfs_sram - else: - raise ValueError(f"Unknown component '{comp}'") - - def _apply_exe_time_overhead_for_component(comp: str, st_ns: int): - if comp == "sa": - op.stats.sa_time_ns += st_ns - elif comp == "vu": - op.stats.vu_time_ns += st_ns - elif comp == "hbm": - op.stats.memory_time_ns += st_ns - elif comp == "ici": - op.stats.ici_time_ns += st_ns - elif comp == "vmem": - op.stats.vmem_time_ns += st_ns - else: - raise ValueError(f"Unknown component '{comp}'") - - def _get_activity_factor_for_component(comp: str): - if comp in ["sa", "vu"]: - # for SA and VU, activity factor is spatial utilization (e.g., FLOPS) - return min(1, op.stats.flops_util) - elif comp in ["hbm", "ici", "vmem"]: - # for other components, activity factor is time utilization - time_ns = { - "hbm": op.stats.memory_time_ns, - "ici": op.stats.ici_time_ns, - "vmem": op.stats.vmem_time_ns, - }[comp] - util = min(1, time_ns / op.stats.execution_time_ns) - return util - else: - raise ValueError(f"Unknown component '{comp}'") - - def _lookup_efficiency_from_table(scaling_time_ns: int, activity: float, voltage: float, policy: DVFSPolicy) -> float: - if policy == DVFSPolicy.NONE: - table = FIXED_VOLTAGE_REGULATOR_OVERHEAD_TABLE - else: - table = DVFS_VOLTAGE_REGULATOR_OVERHEAD_TABLE - - # filter out scaling time - rows = [r for r in table if r.scaling_time_ns == scaling_time_ns] - assert len(rows) > 0, f"No DVFS overhead table entry for scaling_time_ns={scaling_time_ns}" - - # pick the nearest voltage that is greater than or equal to requested voltage - voltages = sorted(set(r.voltage_V for r in rows)) - v_snap = None - for v in voltages: - if v >= voltage: - v_snap = v - break - assert v_snap is not None, f"No DVFS overhead table entry for voltage_V >= {voltage}, scaling_time_ns={scaling_time_ns}, activity_factor={activity}" - rows = [r for r in rows if r.voltage_V == v_snap] - - # pick the row with smallest activity factor >= requested activity - rows = sorted(rows, key=lambda r: r.activity_factor) - chosen_row = None - for r in rows: - if r.activity_factor >= activity: - chosen_row = r - break - assert chosen_row is not None, f"No DVFS overhead table entry for activity_factor >= {activity}, V={v_snap}, scaling_time_ns={scaling_time_ns}" - - return chosen_row.power_efficiency_percent - - # component execution times - comp_times: dict[str, int] = { - "sa": op.stats.sa_time_ns, - "vu": op.stats.vu_time_ns, - "hbm": op.stats.memory_time_ns, - "ici": op.stats.ici_time_ns, - "vmem": op.stats.vmem_time_ns, - } - - for comp, t_ns in comp_times.items(): - dvfs_config = _get_dvfs_config_for_component(comp) - dvfs_policy = dvfs_config.policy - - activity_factor = _get_activity_factor_for_component(comp) - voltage_V = dvfs_config.voltage_V or 0.7 # default to 0.7V for now - - chosen_eff= _lookup_efficiency_from_table( - scaling_time_ns=dvfs_config.voltage_regulator_scaling_time_ns, - activity=activity_factor, - voltage=voltage_V, - policy=dvfs_policy, - ) - - if t_ns > 0 and dvfs_policy not in [DVFSPolicy.NONE, DVFSPolicy.IDEAL]: - # if component is unused, do not change execution time - _apply_exe_time_overhead_for_component(comp, dvfs_config.voltage_regulator_scaling_time_ns) - dvfs_config.voltage_conversion_power_efficiency_percent = chosen_eff - - # update e2e execution time - exe_time_ns = max( - op.stats.sa_time_ns, - op.stats.vu_time_ns, - op.stats.vmem_time_ns, - op.stats.ici_time_ns, - op.stats.memory_time_ns, - ) - op.stats.execution_time_ns = exe_time_ns - bounded_by = None - for t, label in [ - (op.stats.sa_time_ns, "Compute"), - (op.stats.vu_time_ns, "Compute"), - (op.stats.vmem_time_ns, "Compute"), - (op.stats.memory_time_ns, "Memory"), - (op.stats.ici_time_ns, "ICI/NVLink"), - ]: - if t == exe_time_ns: - bounded_by = label - break - assert bounded_by, "Failed to determine bounded_by after DVFS overhead addition" - op.stats.bounded_by = bounded_by - - return op - - -def apply_regulator_efficiency(op: Operator.Operator) -> Operator.Operator: - """ - Apply regulator efficiency losses to per-component energies. - Multiply both dynamic and static energies by (100/efficiency_percent). - - Notes: - - We scale energies (J) post computation, which is equivalent to - scaling power for the already-integrated durations. - - Components covered: SA, VU, SRAM (vmem), HBM, ICI. 'other' is unchanged. - """ - - # SA - op.stats.dynamic_energy_sa_J *= 100.0 / op.dvfs_sa.voltage_conversion_power_efficiency_percent - op.stats.static_energy_sa_J *= 100.0 / op.dvfs_sa.voltage_conversion_power_efficiency_percent - - # VU - op.stats.dynamic_energy_vu_J *= 100.0 / op.dvfs_vu.voltage_conversion_power_efficiency_percent - op.stats.static_energy_vu_J *= 100.0 / op.dvfs_vu.voltage_conversion_power_efficiency_percent - - # SRAM (vmem) - op.stats.dynamic_energy_sram_J *= 100.0 / op.dvfs_sram.voltage_conversion_power_efficiency_percent - op.stats.static_energy_sram_J *= 100.0 / op.dvfs_sram.voltage_conversion_power_efficiency_percent - - # HBM - op.stats.dynamic_energy_hbm_J *= 100.0 / op.dvfs_hbm.voltage_conversion_power_efficiency_percent - op.stats.static_energy_hbm_J *= 100.0 / op.dvfs_hbm.voltage_conversion_power_efficiency_percent - - # ICI - op.stats.dynamic_energy_ici_J *= 100.0 / op.dvfs_ici.voltage_conversion_power_efficiency_percent - op.stats.static_energy_ici_J *= 100.0 / op.dvfs_ici.voltage_conversion_power_efficiency_percent - - return op - - -def get_global_dvfs_config_helper(dvfs_config: str | DVFSConfig | DVFSPolicy | None = None) -> DVFSConfig: - if not dvfs_config: - dvfs_config = DVFSConfig() - elif isinstance(dvfs_config, DVFSPolicy): - dvfs_config = DVFSConfig(policy=dvfs_config) - elif isinstance(dvfs_config, str): - if dvfs_config != "None": - dvfs_str_split = dvfs_config.split("_") - policy = DVFSPolicy.from_str(dvfs_str_split[0]) - if len(dvfs_str_split) == 1: - dvfs_config = DVFSConfig(policy=policy) - else: - perf_degrad_factor = float(dvfs_str_split[1]) - dvfs_config = DVFSConfig(policy=policy, performance_degradation_percentage=perf_degrad_factor) - else: - dvfs_config = DVFSConfig(policy=DVFSPolicy.NONE) - - return dvfs_config - - def analyze_operator_energy( op: Operator.Operator, config: ChipConfig,