Skip to content

Commit 959e7cd

Browse files
authored
Dynamic network carbon data energy with tests (#1257)
* Fix using correct SCI values for network energy calculation * Add tests for dynamic network carbon data intensity * Implement PR suggestions
1 parent 1252f15 commit 959e7cd

File tree

3 files changed

+148
-19
lines changed

3 files changed

+148
-19
lines changed

lib/phase_stats.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from decimal import Decimal
99
from io import StringIO
1010

11-
from lib.global_config import GlobalConfig
1211
from lib.db import DB
1312
from lib import error_helpers
1413

@@ -68,8 +67,6 @@ def generate_csv_line(run_id, metric, detail_name, phase_name, value, value_type
6867
return f"{run_id},{metric},{detail_name},{phase_name},{round(value)},{value_type},{round(max_value) if max_value is not None else ''},{round(min_value) if min_value is not None else ''},{round(sampling_rate_avg) if sampling_rate_avg is not None else ''},{round(sampling_rate_max) if sampling_rate_max is not None else ''},{round(sampling_rate_95p) if sampling_rate_95p is not None else ''},{unit},NOW()\n"
6968

7069
def build_and_store_phase_stats(run_id, sci=None):
71-
config = GlobalConfig().config
72-
7370
if not sci:
7471
sci = {}
7572

@@ -263,17 +260,20 @@ def build_and_store_phase_stats(run_id, sci=None):
263260

264261
# after going through detail metrics, create cumulated ones
265262
if network_bytes_total:
266-
# build the network energy
267-
# network via formula: https://www.green-coding.io/co2-formulas/
268-
# pylint: disable=invalid-name
269-
network_io_in_kWh = Decimal(sum(network_bytes_total)) / 1_000_000_000 * Decimal(config['sci']['N'])
270-
network_io_in_uJ = network_io_in_kWh * 3_600_000_000_000
271-
csv_buffer.write(generate_csv_line(run_id, 'network_energy_formula_global', '[FORMULA]', f"{idx:03}_{phase['name']}", network_io_in_uJ, 'TOTAL', None, None, None, None, None, 'uJ'))
272-
# co2 calculations
273-
network_io_carbon_in_ug = network_io_in_kWh * Decimal(config['sci']['I']) * 1_000_000
274-
if '[' not in phase['name']: # only for runtime sub phases
275-
software_carbon_intensity_global['network_io_carbon_in_ug'] = software_carbon_intensity_global.get('network_io_carbon_in_ug', 0) + network_io_carbon_in_ug
276-
csv_buffer.write(generate_csv_line(run_id, 'network_carbon_formula_global', '[FORMULA]', f"{idx:03}_{phase['name']}", network_io_carbon_in_ug, 'TOTAL', None, None, None, None, None, 'ug'))
263+
if sci.get('N', None) is not None and sci.get('I', None) is not None:
264+
# build the network energy by using a formula: https://www.green-coding.io/co2-formulas/
265+
# pylint: disable=invalid-name
266+
network_io_in_kWh = Decimal(sum(network_bytes_total)) / 1_000_000_000 * Decimal(sci['N'])
267+
network_io_in_uJ = network_io_in_kWh * 3_600_000_000_000
268+
csv_buffer.write(generate_csv_line(run_id, 'network_energy_formula_global', '[FORMULA]', f"{idx:03}_{phase['name']}", network_io_in_uJ, 'TOTAL', None, None, None, None, None, 'uJ'))
269+
# co2 calculations
270+
network_io_carbon_in_ug = network_io_in_kWh * Decimal(sci['I']) * 1_000_000
271+
if '[' not in phase['name']: # only for runtime sub phases
272+
software_carbon_intensity_global['network_io_carbon_in_ug'] = software_carbon_intensity_global.get('network_io_carbon_in_ug', 0) + network_io_carbon_in_ug
273+
csv_buffer.write(generate_csv_line(run_id, 'network_carbon_formula_global', '[FORMULA]', f"{idx:03}_{phase['name']}", network_io_carbon_in_ug, 'TOTAL', None, None, None, None, None, 'ug'))
274+
else:
275+
error_helpers.log_error('Cannot calculate the total network energy consumption. SCI values I and N are missing in the config.', run_id=run_id)
276+
network_io_carbon_in_ug = 0
277277
else:
278278
network_io_carbon_in_ug = 0
279279

@@ -303,14 +303,14 @@ def build_and_store_phase_stats(run_id, sci=None):
303303
csv_buffer.write(generate_csv_line(run_id, 'psu_energy_cgroup_container', detail_name, f"{idx:03}_{phase['name']}", surplus_energy_runtime * splitting_ratio, 'TOTAL', None, None, None, None, None, 'uJ'))
304304
csv_buffer.write(generate_csv_line(run_id, 'psu_power_cgroup_container', detail_name, f"{idx:03}_{phase['name']}", surplus_power_runtime * splitting_ratio, 'TOTAL', None, None, None, None, None, 'mW'))
305305

306-
# TODO: refactor to be a metric provider. Than it can also be per phase
306+
# TODO: refactor to be a metric provider. Than it can also be per phase # pylint: disable=fixme
307307
if software_carbon_intensity_global.get('machine_carbon_ug', None) is not None \
308308
and software_carbon_intensity_global.get('embodied_carbon_share_ug', None) is not None \
309309
and sci.get('R', 0) != 0 \
310310
and sci.get('R_d', None) is not None:
311311

312312
csv_buffer.write(generate_csv_line(run_id, 'software_carbon_intensity_global', '[SYSTEM]', f"{runtime_phase_idx:03}_[RUNTIME]", (software_carbon_intensity_global['machine_carbon_ug'] + software_carbon_intensity_global['embodied_carbon_share_ug'] + software_carbon_intensity_global.get('network_io_carbon_in_ug', 0)) / Decimal(sci['R']), 'TOTAL', None, None, None, None, None, f"ugCO2e/{sci['R_d']}"))
313-
# TODO End
313+
# TODO End # pylint: disable=fixme
314314

315315
csv_buffer.seek(0) # Reset buffer position to the beginning
316316
DB().copy_from(

tests/lib/test_phase_stats.py

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import os
22
import io
3+
import math
4+
from decimal import Decimal
35

46
GMT_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))+'/../../'
57

@@ -278,8 +280,124 @@ def test_phase_stats_single_network_procfs():
278280
assert data[14]['sampling_rate_95p'] == 100477, '95p sampling rate not in expected range'
279281
assert isinstance(data[14]['sampling_rate_95p'], int)
280282

281-
282-
def test_sci():
283+
def test_phase_stats_network_data():
284+
run_id = Tests.insert_run()
285+
Tests.import_network_io_cgroup_container(run_id)
286+
287+
test_sci_config = {
288+
'N': 0.001, # Network energy intensity (kWh/GB)
289+
'I': 500, # Carbon intensity (gCO2e/kWh)
290+
}
291+
292+
build_and_store_phase_stats(run_id, sci=test_sci_config)
293+
294+
# Network energy data
295+
network_energy_data = DB().fetch_all(
296+
'SELECT metric, detail_name, unit, value, type, phase FROM phase_stats WHERE phase = %s AND metric = %s',
297+
params=('004_[RUNTIME]', 'network_energy_formula_global'), fetch_mode='dict'
298+
)
299+
300+
assert len(network_energy_data) == 1, f"Expected 1 network energy formula entry, got {len(network_energy_data)}"
301+
302+
network_energy_entry = network_energy_data[0]
303+
assert network_energy_entry['metric'] == 'network_energy_formula_global'
304+
assert network_energy_entry['detail_name'] == '[FORMULA]'
305+
assert network_energy_entry['unit'] == 'uJ'
306+
assert network_energy_entry['type'] == 'TOTAL'
307+
assert network_energy_entry['phase'] == '004_[RUNTIME]'
308+
309+
network_totals = DB().fetch_all(
310+
'SELECT detail_name, value FROM phase_stats WHERE phase = %s AND metric = %s',
311+
params=('004_[RUNTIME]', 'network_total_cgroup_container'), fetch_mode='dict'
312+
)
313+
total_network_bytes = sum(row['value'] for row in network_totals)
314+
expected_network_energy_kwh = Decimal(total_network_bytes) / 1_000_000_000 * Decimal(test_sci_config['N'])
315+
expected_network_energy_uj = expected_network_energy_kwh * 3_600_000_000_000
316+
assert math.isclose(network_energy_entry['value'], expected_network_energy_uj, rel_tol=1e-5), f"Expected network energy: {expected_network_energy_uj}, got: {network_energy_entry['value']}"
317+
318+
# Network carbon data
319+
network_carbon_data = DB().fetch_all(
320+
'SELECT metric, detail_name, unit, value, type FROM phase_stats WHERE phase = %s AND metric = %s',
321+
params=('004_[RUNTIME]', 'network_carbon_formula_global'), fetch_mode='dict'
322+
)
323+
324+
assert len(network_carbon_data) == 1, "Expected 1 network carbon formula entry"
325+
326+
network_carbon_entry = network_carbon_data[0]
327+
expected_network_carbon_ug = expected_network_energy_kwh * Decimal(test_sci_config['I']) * 1_000_000
328+
329+
assert network_carbon_entry['metric'] == 'network_carbon_formula_global'
330+
assert network_carbon_entry['detail_name'] == '[FORMULA]'
331+
assert network_carbon_entry['unit'] == 'ug'
332+
assert network_carbon_entry['type'] == 'TOTAL'
333+
assert math.isclose(network_carbon_entry['value'], expected_network_carbon_ug, rel_tol=1e-5), f"Expected network carbon: {expected_network_carbon_ug}, got: {network_carbon_entry['value']}"
334+
335+
def test_sci_calculation():
336+
run_id = Tests.insert_run()
337+
Tests.import_machine_energy(run_id) # Machine energy component
338+
Tests.import_network_io_cgroup_container(run_id) # Network component (custom N parameter)
339+
340+
# Define comprehensive SCI configuration with all required parameters
341+
test_sci_config = {
342+
'N': 0.001, # Network energy intensity (kWh/GB)
343+
'I': 500, # Carbon intensity (gCO2e/kWh)
344+
'EL': 4, # Expected lifespan (years)
345+
'TE': 300000, # Total embodied emissions (gCO2e)
346+
'RS': 1, # Resource share (100%)
347+
'R': 10, # Functional unit count (10 runs)
348+
'R_d': 'test runs' # Functional unit description
349+
}
350+
351+
build_and_store_phase_stats(run_id, sci=test_sci_config)
352+
353+
# Verify all SCI components are calculated and stored correctly
354+
355+
# 1. Machine carbon from energy consumption
356+
machine_carbon_data = DB().fetch_all(
357+
'SELECT metric, value, unit FROM phase_stats WHERE phase = %s AND metric = %s',
358+
params=('004_[RUNTIME]', 'psu_carbon_ac_mcp_machine'), fetch_mode='dict'
359+
)
360+
assert len(machine_carbon_data) == 1, "Machine carbon should be calculated"
361+
machine_carbon_ug = machine_carbon_data[0]['value']
362+
363+
# 2. Embodied carbon calculation
364+
embodied_carbon_data = DB().fetch_all(
365+
'SELECT metric, value, unit FROM phase_stats WHERE phase = %s AND metric = %s',
366+
params=('004_[RUNTIME]', 'embodied_carbon_share_machine'), fetch_mode='dict'
367+
)
368+
assert len(embodied_carbon_data) == 1, "Embodied carbon should be calculated"
369+
embodied_carbon_ug = embodied_carbon_data[0]['value']
370+
371+
# 3. Network carbon calculation
372+
network_carbon_data = DB().fetch_all(
373+
'SELECT metric, value, unit FROM phase_stats WHERE phase = %s AND metric = %s',
374+
params=('004_[RUNTIME]', 'network_carbon_formula_global'), fetch_mode='dict'
375+
)
376+
assert len(network_carbon_data) == 1, "Network carbon should be calculated"
377+
network_carbon_ug = network_carbon_data[0]['value']
378+
379+
# 4. Final SCI calculation verification
380+
sci_data = DB().fetch_all(
381+
'SELECT value, unit FROM phase_stats WHERE phase = %s AND metric = %s',
382+
params=('004_[RUNTIME]', 'software_carbon_intensity_global'), fetch_mode='dict'
383+
)
384+
assert len(sci_data) == 1, "SCI should be calculated for the whole run"
385+
sci_entry = sci_data[0]
386+
387+
# Verify SCI unit format includes functional unit description - fail if other unit occurs
388+
expected_unit = f"ugCO2e/{test_sci_config['R_d']}"
389+
assert sci_entry['unit'] == expected_unit, \
390+
f"Test fails: Unexpected unit detected. Expected: {expected_unit}, got: {sci_entry['unit']}. This test is designed to fail when incorrect units are present."
391+
392+
# Verify SCI value matches expected value: (machine_carbon + embodied_carbon + network_carbon) / R
393+
expected_sci_value = (machine_carbon_ug + embodied_carbon_ug + network_carbon_ug) / Decimal(test_sci_config['R'])
394+
assert math.isclose(abs(sci_entry['value']), expected_sci_value, rel_tol=1e-5), f"SCI calculation should be correct. Expected: {expected_sci_value}, got: {sci_entry['value']}"
395+
396+
# Verify SCI value is reasonable (positive and within expected range)
397+
assert sci_entry['value'] > 0, "SCI should be positive"
398+
assert sci_entry['value'] < 1000000, "SCI should be reasonable (less than 1M ugCO2e per functional unit)"
399+
400+
def test_sci_run():
283401
runner = ScenarioRunner(uri=GMT_ROOT_DIR, uri_type='folder', filename='tests/data/usage_scenarios/stress_sci.yml', skip_system_checks=True, dev_cache_build=True, dev_no_sleeps=True, dev_no_metrics=False, dev_no_phase_stats=False)
284402

285403
out = io.StringIO()
@@ -289,7 +407,6 @@ def test_sci():
289407

290408
data = DB().fetch_all("SELECT value, unit FROM phase_stats WHERE phase = %s AND run_id = %s AND metric = 'software_carbon_intensity_global' ", params=('004_[RUNTIME]', run_id), fetch_mode='dict')
291409

292-
293410
assert len(data) == 1
294411
assert 50 < data[0]['value'] < 150
295412
assert data[0]['unit'] == 'ugCO2e/Cool run'

tests/test_functions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from metric_providers.psu.energy.ac.mcp.machine.provider import PsuEnergyAcMcpMachineProvider
1212
from metric_providers.cpu.energy.rapl.msr.component.provider import CpuEnergyRaplMsrComponentProvider
1313
from metric_providers.network.io.procfs.system.provider import NetworkIoProcfsSystemProvider
14+
from metric_providers.network.io.cgroup.container.provider import NetworkIoCgroupContainerProvider
1415

1516
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
1617

@@ -127,6 +128,17 @@ def import_network_io_procfs(run_id):
127128

128129
return df
129130

131+
def import_network_io_cgroup_container(run_id):
132+
133+
obj = NetworkIoCgroupContainerProvider(99, skip_check=True)
134+
135+
obj._filename = os.path.join(CURRENT_DIR, 'data/metrics/network_io_cgroup_container.log')
136+
df = obj.read_metrics()
137+
138+
metric_importer.import_measurements(df, 'network_io_cgroup_container', run_id)
139+
140+
return df
141+
130142
def import_cpu_energy(run_id):
131143

132144
obj = CpuEnergyRaplMsrComponentProvider(99, skip_check=True)

0 commit comments

Comments
 (0)