From 28ba2d45f645edf7e2225d544630f731ff5c30ae Mon Sep 17 00:00:00 2001 From: Wonhyuk Yang Date: Wed, 5 Aug 2026 16:49:57 +0900 Subject: [PATCH] [TOGSim] Report off-chip DRAM energy in the end-of-run stats Add an energy model that turns DRAM activity into energy using constants from a swappable cost table: E = n_ACT x row_activation_pj + bits x sum(transfer_pj_per_bit) n_ACT comes from the ramulator2 controllers (row_misses + row_conflicts, summed over channels). bits comes from served transactions times the DRAM model's own get_tx_bytes(), so the burst granularity follows the ramulator config instead of a hardcoded size; a command moves one full burst, never a partial one. The report is a separate "=== Energy statistics ===" section printed once at the end of a run, aggregated over all channels. The periodic interval logs are unchanged. Select the constants with energy_cost_table_path in the simulation config. Omit the key and no energy section is printed, so existing configs behave as before. configs/energy_tables/hbm2.yml holds the HBM2 values. Activation energy needs row state, so the report requires dram_type: ramulator2. Verified on a 2048 cube matmul: the reported 610657 activations match the sum of row_misses and row_conflicts across the 16 per-channel ramulator dumps, and 3.358 mJ over 302.879 us gives 11.085 W. --- README.md | 5 + TOGSim/extern/ramulator2 | 2 +- TOGSim/include/Dram.h | 3 + TOGSim/include/EnergyModel.h | 53 +++++++++++ TOGSim/include/SimulationConfig.h | 6 ++ TOGSim/include/Simulator.h | 1 + TOGSim/src/Common.cc | 8 ++ TOGSim/src/Dram.cc | 12 +++ TOGSim/src/EnergyModel.cc | 92 +++++++++++++++++++ TOGSim/src/Simulator.cc | 40 ++++++++ configs/energy_tables/hbm2.yml | 15 +++ ...ystolic_ws_128x128_c1_simple_noc_tpuv3.yml | 2 + 12 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 TOGSim/include/EnergyModel.h create mode 100644 TOGSim/src/EnergyModel.cc create mode 100644 configs/energy_tables/hbm2.yml diff --git a/README.md b/README.md index 8b90b87bc..742003a8c 100644 --- a/README.md +++ b/README.md @@ -420,6 +420,10 @@ ramulator_config_path: ../configs/ramulator2_configs/HBM2_TPUv3.yaml # resolved # Optional: NUMA-style DRAM partitions (channels must divide evenly) # dram_num_partitions: 2 +# --- Energy model (TOGSim, optional) --- +energy_cost_table_path: ../configs/energy_tables/hbm2.yml # resolved relative to this YAML’s directory +# Omit this key to skip the energy report. + # --- Interconnect (TOGSim) --- icnt_type: simple # simple | booksim2 icnt_latency_cycles: 10 # used when icnt_type is simple @@ -460,6 +464,7 @@ One-line meaning for each group (details in the YAML block above). - **Core (`num_cores`, `core_freq_mhz`, `core_stats_print_period_cycles`, `num_systolic_array_per_core`, `sa_weight_buffer_depth`, optional `core_type`, STONNE keys)**: how many cores, their clock, stats cadence, systolic count per core, the per-SA resident weight-slot count (must be > 0; bounds preload run-ahead—raise it to loosen the throttle), and optional non-default mesh vs STONNE mix. - **VPU (`vpu_*`)**: vector lane count, per-lane scratchpad (KB), and vector register width—**compiler** uses these for tiling/codegen. - **DRAM (`dram_type`, `dram_channels`, …)**: `ramulator2` uses `ramulator_config_path`; `simple` uses fixed latency and optional bandwidth caps (`dram_bandwidth_gbps_*`, `dram_freq_mhz` when capped). `dram_num_partitions` splits channels for NUMA-style addressing. +- **Energy (`energy_cost_table_path`)**: selects a pJ cost table (see `configs/energy_tables/`) and enables an end-of-run `=== Energy statistics ===` section reporting off-chip DRAM energy, summed over all channels. Activation energy needs row state, so it requires `dram_type: ramulator2`. Interval stats are unaffected. - **Interconnect (`icnt_*`, `booksim_config_path`)**: `simple` adds fixed hop latency (`icnt_latency_cycles`); `booksim2` points at a BookSim2 topology file. - **Codegen (`codegen_*`)**: mapping strategy (heuristic / autotune / external-hybrid), external JSON path, autotune search limits, and fusion/optimization set for the PyTorch compiler path. - **L2 (`l2d_type`, `l2d_config`, optional `l2d_hit_latency`)**: optional data cache between cores and DRAM; `l2d_config` uses AccelSim-style cache geometry strings. diff --git a/TOGSim/extern/ramulator2 b/TOGSim/extern/ramulator2 index 272ea843d..579074664 160000 --- a/TOGSim/extern/ramulator2 +++ b/TOGSim/extern/ramulator2 @@ -1 +1 @@ -Subproject commit 272ea843dffdef0719efe69c68d67de0ed9194db +Subproject commit 57907466454495fb891629c5f5dd4dfd502703af diff --git a/TOGSim/include/Dram.h b/TOGSim/include/Dram.h index 4a8975599..4a1bf5b80 100644 --- a/TOGSim/include/Dram.h +++ b/TOGSim/include/Dram.h @@ -27,6 +27,8 @@ class Dram { virtual mem_fetch* top(uint32_t cid) = 0; virtual void pop(uint32_t cid) = 0; uint32_t get_channel_id(mem_fetch* request); + /** Activity for the energy report, summed over every channel. */ + virtual DramEnergyCounters get_energy_counters() { return DramEnergyCounters{}; } virtual void print_stat() {} virtual void print_cache_stats() {}; uint32_t get_channels_per_partition() { return _n_ch_per_partition; } @@ -65,6 +67,7 @@ class DramRamulator2 : public Dram { virtual bool is_empty(uint32_t cid) override; virtual mem_fetch* top(uint32_t cid) override; virtual void pop(uint32_t cid) override; + virtual DramEnergyCounters get_energy_counters() override; virtual void print_stat() override; void print_cache_stats() override; diff --git a/TOGSim/include/EnergyModel.h b/TOGSim/include/EnergyModel.h new file mode 100644 index 000000000..7e5778283 --- /dev/null +++ b/TOGSim/include/EnergyModel.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include + +/** + * Off-chip DRAM energy constants, read from the `offchip_dram` section of the + * energy cost table selected by the simulation config's + * `energy_cost_table_path`. All values are in pJ. + */ +struct DramEnergyCosts { + std::string name; + std::string path; + double row_activation_pj = 0.0; + /* Per-bit terms kept separate so the breakdown stays visible in the report. */ + std::vector> transfer_pj_per_bit; + + double transfer_pj_per_bit_total() const; + /** "dram 1.51 + io 1.17 + phy 0.80" */ + std::string transfer_breakdown() const; +}; + +/** Raw activity a DRAM model reports for energy accounting, aggregated over all channels. */ +struct DramEnergyCounters { + uint64_t row_activations = 0; + /* Column commands served. Each moves one burst, never a partial one. */ + uint64_t transactions = 0; + /* Burst size, i.e. the DRAM model's transaction granularity. */ + uint32_t bytes_per_transaction = 0; + /* False when the DRAM model tracks no row state, so activation energy is unknown. */ + bool available = false; + + uint64_t transferred_bits() const { + return transactions * static_cast(bytes_per_transaction) * 8ull; + } +}; + +struct DramEnergy { + double activation_pj = 0.0; + double transfer_pj = 0.0; + double total_pj = 0.0; +}; + +DramEnergyCosts load_dram_energy_costs(const std::string& cost_table_path); + +DramEnergy compute_dram_energy(const DramEnergyCosts& costs, const DramEnergyCounters& counters); + +/** Scale to the largest unit that keeps the value >= 1, e.g. "1.131 mJ". */ +std::string format_energy(double pj); +std::string format_power(double watts); +std::string format_time(double seconds); diff --git a/TOGSim/include/SimulationConfig.h b/TOGSim/include/SimulationConfig.h index c099d057c..5730734b8 100644 --- a/TOGSim/include/SimulationConfig.h +++ b/TOGSim/include/SimulationConfig.h @@ -6,6 +6,8 @@ #include #include +#include "EnergyModel.h" + enum class CoreType { WS_MESH, STONNE }; enum class DramType { SIMPLE, RAMULATOR2 }; @@ -61,6 +63,10 @@ struct SimulationConfig { uint32_t icnt_latency; uint32_t icnt_stats_print_period_cycles=0; + /* Energy config. Disabled unless the config sets energy_cost_table_path. */ + bool energy_model_enabled = false; + DramEnergyCosts dram_energy_costs; + /* Sheduler config */ uint32_t num_partition=1; std::string scheduler_type; diff --git a/TOGSim/include/Simulator.h b/TOGSim/include/Simulator.h index 91baf5b5b..1ec1dcab4 100644 --- a/TOGSim/include/Simulator.h +++ b/TOGSim/include/Simulator.h @@ -40,6 +40,7 @@ class Simulator { int get_partition_id(int core_id) { return _config.partiton_map[core_id]; } std::unique_ptr& get_partition_scheduler(int core_id) { return _partition_scheduler.at(get_partition_id(core_id)); } void print_core_stat(); + void print_energy_stat(); void cycle(); const SimulationConfig& get_config() const { return _config; } const YAML::Node& get_hardware_config_yaml() const { return _hardware_config_yaml; } diff --git a/TOGSim/src/Common.cc b/TOGSim/src/Common.cc index 6f9a74d78..47ae8f9c4 100644 --- a/TOGSim/src/Common.cc +++ b/TOGSim/src/Common.cc @@ -156,6 +156,14 @@ SimulationConfig initialize_config(const YAML::Node& config, if (config["icnt_injection_ports_per_core"]) parsed_config.icnt_injection_ports_per_core = config["icnt_injection_ports_per_core"].as(); + /* Energy config */ + if (config["energy_cost_table_path"]) { + const std::string cost_table_rel = config["energy_cost_table_path"].as(); + parsed_config.dram_energy_costs = + load_dram_energy_costs(parsed_config.resolve_against_simulation_config(cost_table_rel)); + parsed_config.energy_model_enabled = true; + } + if (config["scheduler"]) parsed_config.scheduler_type = config["scheduler"].as(); if (config["num_partition"]) diff --git a/TOGSim/src/Dram.cc b/TOGSim/src/Dram.cc index 5211ef470..01be96072 100644 --- a/TOGSim/src/Dram.cc +++ b/TOGSim/src/Dram.cc @@ -333,6 +333,18 @@ void DramRamulator2::pop(uint32_t cid) { m_to_crossbar_queue[cid].pop(); } +DramEnergyCounters DramRamulator2::get_energy_counters() { + DramEnergyCounters counters; + counters.available = true; + /* _req_size is the model's own get_tx_bytes(), so one served request is exactly one burst. */ + counters.bytes_per_transaction = _req_size; + for (int ch = 0; ch < _n_ch; ch++) { + counters.row_activations += _mem[ch]->row_activations(); + counters.transactions += _mem[ch]->total_reads() + _mem[ch]->total_writes(); + } + return counters; +} + void DramRamulator2::print_stat() { spdlog::info("=== DRAM statistics ==="); if (_n_ch == 0) diff --git a/TOGSim/src/EnergyModel.cc b/TOGSim/src/EnergyModel.cc new file mode 100644 index 000000000..9f67cad04 --- /dev/null +++ b/TOGSim/src/EnergyModel.cc @@ -0,0 +1,92 @@ +#include "EnergyModel.h" + +#include +#include + +#include +#include +#include + +#include "fmt/core.h" + +double DramEnergyCosts::transfer_pj_per_bit_total() const { + double total = 0.0; + for (const auto& [name, pj] : transfer_pj_per_bit) + total += pj; + return total; +} + +std::string DramEnergyCosts::transfer_breakdown() const { + std::string out; + for (const auto& [name, pj] : transfer_pj_per_bit) { + if (!out.empty()) + out += " + "; + out += fmt::format("{} {:.2f}", name, pj); + } + return out; +} + +DramEnergyCosts load_dram_energy_costs(const std::string& cost_table_path) { + YAML::Node table; + try { + table = YAML::LoadFile(cost_table_path); + } catch (const std::exception& e) { + throw std::runtime_error( + fmt::format("[Config/Energy] Failed to load energy cost table \"{}\": {}", cost_table_path, e.what())); + } + + DramEnergyCosts parsed; + parsed.path = cost_table_path; + parsed.name = table["name"] ? table["name"].as() : "unnamed"; + + const YAML::Node dram = table["offchip_dram"]; + if (!dram) + throw std::runtime_error( + fmt::format("[Config/Energy] Energy cost table \"{}\" has no offchip_dram section", cost_table_path)); + + if (!dram["row_activation_pj"]) + throw std::runtime_error( + fmt::format("[Config/Energy] Energy cost table \"{}\": offchip_dram.row_activation_pj is required", cost_table_path)); + parsed.row_activation_pj = dram["row_activation_pj"].as(); + + const YAML::Node per_bit = dram["transfer_pj_per_bit"]; + if (!per_bit || !per_bit.IsMap() || per_bit.size() == 0) + throw std::runtime_error(fmt::format( + "[Config/Energy] Energy cost table \"{}\": offchip_dram.transfer_pj_per_bit must be a non-empty map", cost_table_path)); + for (const auto& term : per_bit) + parsed.transfer_pj_per_bit.emplace_back(term.first.as(), term.second.as()); + + spdlog::info("[Config/Energy] Loaded energy cost table \"{}\" from {}", parsed.name, cost_table_path); + return parsed; +} + +DramEnergy compute_dram_energy(const DramEnergyCosts& costs, const DramEnergyCounters& counters) { + DramEnergy energy; + energy.activation_pj = static_cast(counters.row_activations) * costs.row_activation_pj; + energy.transfer_pj = static_cast(counters.transferred_bits()) * costs.transfer_pj_per_bit_total(); + energy.total_pj = energy.activation_pj + energy.transfer_pj; + return energy; +} + +namespace { +/** Pick the unit whose scale keeps `value / scale` in [1, 1000), or the smallest unit. */ +std::string scale_units(double value, const std::vector>& units) { + for (const auto& [scale, suffix] : units) { + if (std::fabs(value) >= scale) + return fmt::format("{:.3f} {}", value / scale, suffix); + } + return fmt::format("{:.3f} {}", value / units.back().first, units.back().second); +} +} // namespace + +std::string format_energy(double pj) { + return scale_units(pj, {{1e12, "J"}, {1e9, "mJ"}, {1e6, "uJ"}, {1e3, "nJ"}, {1.0, "pJ"}}); +} + +std::string format_power(double watts) { + return scale_units(watts, {{1.0, "W"}, {1e-3, "mW"}, {1e-6, "uW"}}); +} + +std::string format_time(double seconds) { + return scale_units(seconds, {{1.0, "s"}, {1e-3, "ms"}, {1e-6, "us"}, {1e-9, "ns"}}); +} diff --git a/TOGSim/src/Simulator.cc b/TOGSim/src/Simulator.cc index 669924810..0796ed4f2 100644 --- a/TOGSim/src/Simulator.cc +++ b/TOGSim/src/Simulator.cc @@ -292,4 +292,44 @@ void Simulator::print_core_stat() _cores[core_id]->print_stats(); } spdlog::info("Total execution cycles: {}", _core_cycles); + print_energy_stat(); +} + +/* Whole-run totals only; the periodic interval logs carry no energy. */ +void Simulator::print_energy_stat() { + if (!_config.energy_model_enabled) + return; + + const DramEnergyCosts& costs = _config.dram_energy_costs; + spdlog::info("=== Energy statistics ==="); + spdlog::info("[Energy] Energy cost table \"{}\" loaded from \"{}\"", costs.name, costs.path); + + const std::string scope = + _config.dram_channels > 0 + ? fmt::format("OffChip DRAM channels 0..{} combined", _config.dram_channels - 1) + : std::string("OffChip DRAM"); + + const DramEnergyCounters counters = _dram->get_energy_counters(); + if (!counters.available) { + spdlog::info("[Energy] {} | not modeled, the configured dram_type tracks no row state", scope); + return; + } + + const DramEnergy energy = compute_dram_energy(costs, counters); + const double seconds = + (_core_cycles == 0 || _config.core_freq_mhz == 0) + ? 0.0 + : static_cast(_core_cycles) / (static_cast(_config.core_freq_mhz) * 1e6); + + if (seconds > 0.0) { + spdlog::info("[Energy] {} | {} avg power, {} over {} | {} activation, {} transfer", scope, + format_power(energy.total_pj * 1e-12 / seconds), format_energy(energy.total_pj), + format_time(seconds), format_energy(energy.activation_pj), format_energy(energy.transfer_pj)); + } else { + spdlog::info("[Energy] {} | {} total | {} activation, {} transfer", scope, format_energy(energy.total_pj), + format_energy(energy.activation_pj), format_energy(energy.transfer_pj)); + } + spdlog::info("[Energy] {} | {} activations x {:.2f} pJ | {} transactions x {} B x {:.2f} pJ/bit ({})", scope, + counters.row_activations, costs.row_activation_pj, counters.transactions, + counters.bytes_per_transaction, costs.transfer_pj_per_bit_total(), costs.transfer_breakdown()); } diff --git a/configs/energy_tables/hbm2.yml b/configs/energy_tables/hbm2.yml new file mode 100644 index 000000000..db188b594 --- /dev/null +++ b/configs/energy_tables/hbm2.yml @@ -0,0 +1,15 @@ +# Off-chip DRAM energy constants. All values in pJ. +# +# Swap this file out (via the `energy_cost_table_path` key of the simulation +# config) to model a different memory technology. +name: HBM2 + +offchip_dram: + # Energy of one row activation, counted as a PRE+ACT command pair. + row_activation_pj: 909.0 + # Per-bit terms, summed for every bit that crosses the DRAM interface. + # Kept separate so the report can show the breakdown. + transfer_pj_per_bit: + dram: 1.51 + io: 1.17 + phy: 0.80 diff --git a/configs/systolic_ws_128x128_c1_simple_noc_tpuv3.yml b/configs/systolic_ws_128x128_c1_simple_noc_tpuv3.yml index 397f0fb73..dcf529eec 100644 --- a/configs/systolic_ws_128x128_c1_simple_noc_tpuv3.yml +++ b/configs/systolic_ws_128x128_c1_simple_noc_tpuv3.yml @@ -13,6 +13,8 @@ dram_channels: 16 dram_stats_print_period_cycles: 10000 ramulator_config_path: ../configs/ramulator2_configs/HBM2_TPUv3.yaml +energy_cost_table_path: ../configs/energy_tables/hbm2.yml + icnt_type: simple icnt_latency_cycles: 10 icnt_freq_mhz: 940