diff --git a/.github/workflows/check_homepage_build.yaml b/.github/workflows/check_homepage_build.yaml
index 2adcef31..30204a70 100644
--- a/.github/workflows/check_homepage_build.yaml
+++ b/.github/workflows/check_homepage_build.yaml
@@ -38,3 +38,5 @@ jobs:
run: pip install '.[docs]'
- name: Build homepage
run: mkdocs build --verbose --strict
+ env:
+ BUILD_SOCIAL_CARD: true
diff --git a/.github/workflows/deploy_homepage.yaml b/.github/workflows/deploy_homepage.yaml
index e39ea516..c6d409ab 100644
--- a/.github/workflows/deploy_homepage.yaml
+++ b/.github/workflows/deploy_homepage.yaml
@@ -37,3 +37,5 @@ jobs:
run: pip install '.[docs]'
- name: Build homepage
run: mkdocs gh-deploy --force
+ env:
+ BUILD_SOCIAL_CARD: true
diff --git a/docs/research_overview/perseus.md b/docs/research_overview/perseus.md
index aff3429c..dbdd6d27 100644
--- a/docs/research_overview/perseus.md
+++ b/docs/research_overview/perseus.md
@@ -6,9 +6,9 @@ description: Reducing Energy Bloat in Large Model Training
Perseus:
Reducing Energy Bloat in Large Model Training
-SOSP '24 (To appear)
+SOSP '24
-[**Preprint**](https://arxiv.org/abs/2312.06902)
+[**Paper**](https://arxiv.org/abs/2312.06902)
diff --git a/mkdocs.yml b/mkdocs.yml
index ca0b25d4..72da629c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -38,6 +38,7 @@ plugins:
- search
- autorefs
- social:
+ enabled: !ENV [BUILD_SOCIAL_CARD, false]
cards_dir: assets/img/social
cards_layout_options:
background_color: "#f7e96d"
diff --git a/scripts/preview_docs.sh b/scripts/preview_docs.sh
index 74fb7f5a..24c02faf 100644
--- a/scripts/preview_docs.sh
+++ b/scripts/preview_docs.sh
@@ -1,5 +1,9 @@
#!/usr/bin/env bash
+# This script builds a local version of the documentation and makes it available at localhost:7777.
+# By default it does not build social preview cards. If you want to debug social cards,
+# set the environment variable `BUILD_SOCIAL_CARD=true` to this script.
+
pip list | grep mkdocs-material 2>&1 >/dev/null
if [[ ! $? -eq 0 ]]; then
diff --git a/tests/test_carbon.py b/tests/test_carbon.py
new file mode 100644
index 00000000..b5d0bda7
--- /dev/null
+++ b/tests/test_carbon.py
@@ -0,0 +1,104 @@
+from __future__ import annotations
+
+import json
+import pytest
+import requests
+
+from unittest.mock import patch
+
+from zeus.carbon import (
+ ElectrictyMapsClient,
+ get_ip_lat_long,
+ ZeusCarbonIntensityNotFoundError,
+)
+
+
+class MockHttpResponse:
+ def __init__(self, text):
+ self.text = text
+ self.json_obj = json.loads(text)
+
+ def json(self):
+ return self.json_obj
+
+
+@pytest.fixture
+def mock_requests():
+ IP_INFO_RESPONSE = """{
+ "ip": "35.3.237.23",
+ "hostname": "0587459863.wireless.umich.net",
+ "city": "Ann Arbor",
+ "region": "Michigan",
+ "country": "US",
+ "loc": "42.2776,-83.7409",
+ "org": "AS36375 University of Michigan",
+ "postal": "48109",
+ "timezone": "America/Detroit",
+ "readme": "https://ipinfo.io/missingauth"
+ }"""
+
+ NO_MEASUREMENT_RESPONSE = r'{"error":"No recent data for zone \"US-MIDW-MISO\""}'
+
+ ELECTRICITY_MAPS_RESPONSE_LIFECYCLE = (
+ '{"zone":"US-MIDW-MISO","carbonIntensity":466,"datetime":"2024-09-24T03:00:00.000Z",'
+ '"updatedAt":"2024-09-24T02:47:02.408Z","createdAt":"2024-09-21T03:45:20.860Z",'
+ '"emissionFactorType":"lifecycle","isEstimated":true,"estimationMethod":"TIME_SLICER_AVERAGE"}'
+ )
+
+ ELECTRICITY_MAPS_RESPONSE_DIRECT = (
+ '{"zone":"US-MIDW-MISO","carbonIntensity":506,"datetime":"2024-09-27T00:00:00.000Z",'
+ '"updatedAt":"2024-09-27T00:43:50.277Z","createdAt":"2024-09-24T00:46:38.741Z",'
+ '"emissionFactorType":"direct","isEstimated":true,"estimationMethod":"TIME_SLICER_AVERAGE"}'
+ )
+
+ real_requests_get = requests.get
+
+ def mock_requests_get(url, *args, **kwargs):
+ if url == "http://ipinfo.io/json":
+ return MockHttpResponse(IP_INFO_RESPONSE)
+ elif (
+ url
+ == "https://api.electricitymap.org/v3/carbon-intensity/latest?lat=42.2776&lon=-83.7409&disableEstimations=True&emissionFactorType=direct"
+ ):
+ return MockHttpResponse(NO_MEASUREMENT_RESPONSE)
+ elif (
+ url
+ == "https://api.electricitymap.org/v3/carbon-intensity/latest?lat=42.2776&lon=-83.7409&disableEstimations=False&emissionFactorType=direct"
+ ):
+ return MockHttpResponse(ELECTRICITY_MAPS_RESPONSE_DIRECT)
+ elif (
+ url
+ == "https://api.electricitymap.org/v3/carbon-intensity/latest?lat=42.2776&lon=-83.7409&disableEstimations=False&emissionFactorType=lifecycle"
+ ):
+ return MockHttpResponse(ELECTRICITY_MAPS_RESPONSE_LIFECYCLE)
+ else:
+ return real_requests_get(url, *args, **kwargs)
+
+ patch_request_get = patch("requests.get", side_effect=mock_requests_get)
+
+ patch_request_get.start()
+
+ yield
+
+ patch_request_get.stop()
+
+
+def test_get_current_carbon_intensity(mock_requests):
+ latlong = get_ip_lat_long()
+ assert latlong == (pytest.approx(42.2776), pytest.approx(-83.7409))
+ provider = ElectrictyMapsClient(
+ latlong, estimate=True, emission_factor_type="lifecycle"
+ )
+ assert provider.get_current_carbon_intensity() == 466
+
+ provider.emission_factor_type = "direct"
+ assert provider.get_current_carbon_intensity() == 506
+
+
+def test_get_current_carbon_intensity_no_response(mock_requests):
+ latlong = get_ip_lat_long()
+ assert latlong == (pytest.approx(42.2776), pytest.approx(-83.7409))
+ provider = ElectrictyMapsClient(latlong)
+
+ with pytest.raises(ZeusCarbonIntensityNotFoundError):
+ provider.get_current_carbon_intensity()
diff --git a/zeus/carbon.py b/zeus/carbon.py
new file mode 100644
index 00000000..6098f047
--- /dev/null
+++ b/zeus/carbon.py
@@ -0,0 +1,101 @@
+"""Carbon intensity providers used for carbon-aware optimizers."""
+
+from __future__ import annotations
+
+import abc
+import requests
+from typing import Literal
+
+from zeus.exception import ZeusBaseError
+from zeus.utils.logging import get_logger
+
+logger = get_logger(__name__)
+
+
+def get_ip_lat_long() -> tuple[float, float]:
+ """Retrieve the latitude and longitude of the current IP position."""
+ try:
+ ip_url = "http://ipinfo.io/json"
+ resp = requests.get(ip_url)
+ loc = resp.json()["loc"]
+ lat, long = map(float, loc.split(","))
+ logger.info("Retrieved latitude and longitude: %s, %s", lat, long)
+ return lat, long
+ except requests.exceptions.RequestException as e:
+ logger.exception(
+ "Failed to retrieve current latitude and longitude of IP: %s", e
+ )
+ raise
+
+
+class ZeusCarbonIntensityNotFoundError(ZeusBaseError):
+ """Exception when carbon intensity measurement could not be retrieved."""
+
+ def __init__(self, message: str) -> None:
+ """Initialize carbon not found exception."""
+ super().__init__(message)
+
+
+class CarbonIntensityProvider(abc.ABC):
+ """Abstract class for implementing ways to fetch carbon intensity."""
+
+ @abc.abstractmethod
+ def get_current_carbon_intensity(self) -> float:
+ """Abstract method for fetching the current carbon intensity of the set location of the class."""
+ pass
+
+
+class ElectrictyMapsClient(CarbonIntensityProvider):
+ """Carbon Intensity Provider with ElectricityMaps API.
+
+ Reference:
+
+ 1. [ElectricityMaps](https://www.electricitymaps.com/)
+ 2. [ElectricityMaps API](https://static.electricitymaps.com/api/docs/index.html)
+ 3. [ElectricityMaps GitHub](https://github.com/electricitymaps/electricitymaps-contrib)
+ """
+
+ def __init__(
+ self,
+ location: tuple[float, float],
+ estimate: bool = False,
+ emission_factor_type: Literal["direct", "lifecycle"] = "direct",
+ ) -> None:
+ """Iniitializes ElectricityMaps Carbon Provider.
+
+ Args:
+ location: tuple of latitude and longitude (latitude, longitude)
+ estimate: bool to toggle whether carbon intensity is estimated or not
+ emission_factor_type: emission factor to be measured (`direct` or `lifestyle`)
+ """
+ self.lat, self.long = location
+ self.estimate = estimate
+ self.emission_factor_type = emission_factor_type
+
+ def get_current_carbon_intensity(self) -> float:
+ """Fetches current carbon intensity of the location of the class.
+
+ !!! Note
+ In some locations, there is no recent carbon intensity data. `self.estimate` can be used to approximate the carbon intensity in such cases.
+ """
+ try:
+ url = (
+ f"https://api.electricitymap.org/v3/carbon-intensity/latest?lat={self.lat}&lon={self.long}"
+ + f"&disableEstimations={not self.estimate}&emissionFactorType={self.emission_factor_type}"
+ )
+ resp = requests.get(url)
+ except requests.exceptions.RequestException as e:
+ logger.exception(
+ "Failed to retrieve recent carbon intensnity measurement: %s", e
+ )
+ raise
+
+ try:
+ return resp.json()["carbonIntensity"]
+ except KeyError as e:
+ # Raise exception when carbonIntensity does not exist in response
+ raise ZeusCarbonIntensityNotFoundError(
+ f"Recent carbon intensity measurement not found at `({self.lat}, {self.long})` "
+ f"with estimate set to `{self.estimate}` and emission_factor_type set to `{self.emission_factor_type}`\n"
+ f"JSON Response: {resp.text}"
+ ) from e